feat!: monero.ts rewrite, integration tests (#80)

* feat: move spend/view key symbols to the monero.ts implementation
* feat: add integration tests for `0001-polyseed.patch`
* feat(monero.ts): add support for backgroundSync and closing the wallet
* feat: add integration tests for `0002-wallet-background-sync-with-just-the-view-key.patch`
* feat!: require users to provide own node url
BREAKING CHANGE: Requires users manual call to `Wallet.initWallet` after wallet creation with preferred node url
* feat: add background sync test for `0002-wallet-background-sync-with-just-the-view-key.patch`
* ci: add integration tests step
* feat(monero.ts): support creating and recovering wallet from polyseed
* feat: actually test polyseeds in the integration test
* chore: remove legacy comments
* fix: uncomment getting moneroC
* feat(monero.ts): add support for reading wallet's seed
* feat: add seed test for `0009-Add-recoverDeterministicWalletFromSpendKey.patch`
* chore: slight refactor
* feat(monero.ts): add bindings for `setOffline` and `isOffline`
* feat: add integration tests for `0012-WIP-UR-functions.patch`
* fix: use correct node depending on the coin
* fix: prevent segfaults on wownero
* feat(monero.ts): add partial bindings for `Coins` and `CoinsInfo`
* feat: add integration tests for `0004-coin-control.patch`
* fix coin control
* clean up console.logs
* chore: comment out the entire block
* dev: add devcontainer config for deno
* fix(monero.ts): invalid PendingTransactionPtr brand
* feat(monero.ts): add bindings for retrieving keys and managing transactions
* feat: improve `0012-WIP-UR-functions.patch` tests to follow the airgap doc
* fix(monero.ts): make UR methods optional so wownero can load properly
* remove flaky balance assertions
* tests: add a little bit of delay to make 0002 patch test less flake-y
* tests: run wallet transaction tests on ci
* enable logging to determine why it segfaults on ci
* add delay to every syncBlockchain call
* its console logging time
* even more console.logs
* eep
* eep more
* dont assert that its not frozen
* remove console.logs
* fix(monero.ts): type typo becoming a default value
* feat(monero.ts): add bindings for `createTransactionMultDest`
* feat(monero.ts): support returning multiple values whenever necessary
* feat(monero.ts): add missing reexports
* feat(monero.ts)!: rewrite bindings
BREAKING CHANGES!:
 - Calls to methods no longer automatically throw errors, you should take care of handling errors yourself
 	- This means the whole sanitizer ordeal is gone, no more sanitize arguments etc.
 	- Some misplaced methods have been moved to their "proper" place, e.g. creating Wallet is now possible using WalletManager instance methods, instead of passing WalletManager instance to Wallet's static method
 	- Return types probably changed in places, methods were inconsitent about returning string or empty string and `string | null`, now its always `string | null`
 - Every available symbol should now be available in `symbols`, even for the things that are not yet implemented, so you can access them in that case
* tests: adapt tests to monero.ts changes
* tests: reuse dylib in tests

---------

Co-authored-by: cyan <cyjan@mrcyjanek.net>
This commit is contained in:
Mateusz Franik
2024-12-01 15:02:20 +01:00
committed by GitHub
parent c41c4dad9a
commit 40c1a1bda4
22 changed files with 3884 additions and 925 deletions
+2 -2
View File
@@ -23,7 +23,7 @@ There are at least two ways to do so:
loadMoneroDylib();
const wm = await WalletManager.new();
const wallet = await Wallet.create(wm, "./my_wallet", "password");
const wallet = await wm.createWallet("./my_wallet", "password");
console.log(await wallet.address());
@@ -41,7 +41,7 @@ There are at least two ways to do so:
loadMoneroDylib(lib);
const wm = await WalletManager.new();
const wallet = await Wallet.create(wm, "./my_wallet", "password");
const wallet = await wm.createWallet("./my_wallet", "password");
console.log(await wallet.address());
+5 -5
View File
@@ -1,6 +1,6 @@
import { moneroChecksum } from "./checksum_monero.ts";
import { getSymbol, readCString } from "./src/utils.ts";
import { dylib, loadMoneroDylib } from "./src/bindings.ts";
import { readCString } from "./src/utils.ts";
import { fns, loadMoneroDylib } from "./src/bindings.ts";
loadMoneroDylib();
@@ -21,7 +21,7 @@ export class ChecksumError extends Error {
* @returns {ChecksumError} which contains information about why checksum failed
*/
export async function validateChecksum(): Promise<ChecksumError | null> {
const cppHeaderHash = await readCString(await getSymbol("checksum_wallet2_api_c_h")!(), false);
const cppHeaderHash = await readCString(await fns.checksum_wallet2_api_c_h!(), false);
const tsHeaderHash = moneroChecksum.wallet2_api_c_h_sha256;
const errors: string[] = [];
@@ -32,14 +32,14 @@ export async function validateChecksum(): Promise<ChecksumError | null> {
errorCode++;
}
const cppSourceHash = await readCString(await getSymbol("checksum_wallet2_api_c_cpp")!(), false);
const cppSourceHash = await readCString(await fns.checksum_wallet2_api_c_cpp!(), false);
const tsSourceHash = moneroChecksum.wallet2_api_c_cpp_sha256;
if (cppSourceHash !== tsSourceHash) {
errors.push(`ERR: CPP source file check mismatch ${cppSourceHash} == ${tsSourceHash}`);
errorCode++;
}
const cppExportHash = await readCString(await getSymbol("checksum_wallet2_api_c_exp")!(), false);
const cppExportHash = await readCString(await fns.checksum_wallet2_api_c_exp!(), false);
const tsExportHash = moneroChecksum.wallet2_api_c_exp_sha256;
if (cppExportHash !== tsExportHash) {
if (Deno.build.os !== "darwin") {
+5
View File
@@ -1,6 +1,11 @@
export * from "./src/bindings.ts";
export * from "./src/coins.ts";
export * from "./src/coins_info.ts";
export * from "./src/pending_transaction.ts";
export * from "./src/symbols.ts";
export * from "./src/transaction_history.ts";
export * from "./src/transaction_info.ts";
export * from "./src/unsigned_transaction.ts";
export * from "./src/utils.ts";
export * from "./src/wallet.ts";
export * from "./src/wallet_manager.ts";
+18 -3
View File
@@ -1,8 +1,21 @@
import { type Dylib, moneroSymbols, type MoneroTsDylib, wowneroSymbols, type WowneroTsDylib } from "./symbols.ts";
import { type MoneroSymbols, moneroSymbols, type SymbolName, type WowneroSymbols, wowneroSymbols } from "./symbols.ts";
export type MoneroDylib = Deno.DynamicLibrary<MoneroSymbols>;
export type WowneroDylib = Deno.DynamicLibrary<WowneroSymbols>;
export type Dylib = MoneroDylib | WowneroDylib;
export let dylib: Dylib;
export function loadMoneroDylib(newDylib?: MoneroTsDylib) {
let dylibPrefix = "MONERO";
export const fns = new Proxy({} as { [K in SymbolName]: MoneroDylib["symbols"][`MONERO_${K}`] }, {
get(_, symbolName: SymbolName) {
return dylib.symbols[`${dylibPrefix}_${symbolName}` as keyof Dylib["symbols"]];
},
});
export function loadMoneroDylib(newDylib?: MoneroDylib) {
dylibPrefix = "MONERO";
if (newDylib) {
dylib = newDylib;
return;
@@ -27,7 +40,9 @@ export function loadMoneroDylib(newDylib?: MoneroTsDylib) {
dylib = Deno.dlopen(libPath, moneroSymbols);
}
export function loadWowneroDylib(newDylib?: WowneroTsDylib) {
export function loadWowneroDylib(newDylib?: WowneroDylib) {
dylibPrefix = "WOWNERO";
if (newDylib) {
dylib = newDylib;
return;
+53
View File
@@ -0,0 +1,53 @@
import { CoinsInfo, type CoinsInfoPtr } from "./coins_info.ts";
import { fns } from "./bindings.ts";
export type CoinsPtr = Deno.PointerObject<"coins">;
export class Coins {
#ptr: CoinsPtr;
#coins: CoinsInfo[] = [];
constructor(ptr: CoinsPtr) {
this.#ptr = ptr;
}
async count(): Promise<number> {
return await fns.Coins_count(this.#ptr);
}
async coin(index: number): Promise<CoinsInfo | null> {
if (this.#coins[index]) {
return this.#coins[index];
}
const coinPtr = await fns.Coins_coin(this.#ptr, index);
if (!coinPtr) return null;
return CoinsInfo.new(coinPtr as CoinsInfoPtr);
}
async setFrozen(index: number) {
return await fns.Coins_setFrozen(this.#ptr, index);
}
async thaw(index: number) {
return await fns.Coins_thaw(this.#ptr, index);
}
async getAllSize(): Promise<number> {
return await fns.Coins_getAll_size(this.#ptr);
}
async getAllByIndex(index: number): Promise<unknown> {
return await fns.Coins_getAll_byIndex(this.#ptr, index);
}
async refresh(): Promise<void> {
await fns.Coins_refresh(this.#ptr);
for (const coin of this.#coins) {
coin.refresh();
}
}
}
+85
View File
@@ -0,0 +1,85 @@
import { fns } from "./bindings.ts";
import { readCString } from "./utils.ts";
export type CoinsInfoPtr = Deno.PointerObject<"coinsInfo">;
export class CoinsInfo {
#ptr: CoinsInfoPtr;
#hash!: string | null;
#keyImage!: string | null;
#blockHeight!: bigint;
#amount!: bigint;
#spent!: boolean;
#spentHeight!: bigint;
#frozen!: boolean;
#unlocked!: boolean;
constructor(ptr: CoinsInfoPtr) {
this.#ptr = ptr;
}
getPointer(): CoinsInfoPtr {
return this.#ptr;
}
static async new(ptr: CoinsInfoPtr): Promise<CoinsInfo> {
const instance = new CoinsInfo(ptr);
await instance.refresh();
return instance;
}
async refresh() {
const [hash, keyImage, blockHeight, amount, spent, spentHeight, frozen, unlocked] = await Promise.all([
fns.CoinsInfo_hash(this.#ptr).then(readCString),
fns.CoinsInfo_keyImage(this.#ptr).then(readCString),
fns.CoinsInfo_blockHeight(this.#ptr),
fns.CoinsInfo_amount(this.#ptr),
fns.CoinsInfo_spent(this.#ptr),
fns.CoinsInfo_spentHeight(this.#ptr),
fns.CoinsInfo_frozen(this.#ptr),
fns.CoinsInfo_unlocked(this.#ptr),
]);
this.#hash = hash;
this.#keyImage = keyImage;
this.#blockHeight = blockHeight;
this.#amount = amount;
this.#spent = spent;
this.#spentHeight = spentHeight;
this.#frozen = frozen;
this.#unlocked = unlocked;
}
get hash(): string | null {
return this.#hash;
}
get keyImage(): string | null {
return this.#keyImage;
}
get blockHeight(): bigint {
return this.#blockHeight;
}
get amount(): bigint {
return this.#amount;
}
get spent(): boolean {
return this.#spent;
}
get spentHeight(): bigint {
return this.#spentHeight;
}
get frozen(): boolean {
return this.#frozen;
}
get unlocked(): boolean {
return this.#unlocked;
}
}
+66 -69
View File
@@ -1,87 +1,84 @@
import { CString, getSymbol, readCString, type Sanitizer } from "./utils.ts";
import { fns } from "./bindings.ts";
import { C_SEPARATOR, CString, maybeMultipleStrings, readCString } from "./utils.ts";
export type PendingTransactionPtr = Deno.PointerObject<"transactionInfo">;
export type PendingTransactionPtr = Deno.PointerObject<"pendingTransaction">;
export class PendingTransaction {
#pendingTxPtr: PendingTransactionPtr;
sanitizer?: Sanitizer;
export class PendingTransaction<MultDest extends boolean = false> {
#ptr: PendingTransactionPtr;
constructor(pendingTxPtr: PendingTransactionPtr, sanitizer?: Sanitizer) {
this.sanitizer = sanitizer;
this.#pendingTxPtr = pendingTxPtr;
#amount!: bigint;
#dust!: bigint;
#fee!: bigint;
#txid!: string | string[] | null;
#txCount!: bigint;
constructor(ptr: PendingTransactionPtr) {
this.#ptr = ptr;
}
static async new(ptr: PendingTransactionPtr): Promise<PendingTransaction> {
const instance = new PendingTransaction(ptr);
const [amount, dust, fee, txCount, txid] = await Promise.all([
fns.PendingTransaction_amount(ptr),
fns.PendingTransaction_dust(ptr),
fns.PendingTransaction_fee(ptr),
fns.PendingTransaction_txCount(ptr),
fns.PendingTransaction_txid(ptr, C_SEPARATOR),
]);
instance.#amount = amount;
instance.#dust = dust;
instance.#fee = fee;
instance.#txCount = txCount;
instance.#txid = maybeMultipleStrings(await readCString(txid));
return instance;
}
get amount(): bigint {
return this.#amount;
}
get dust(): bigint {
return this.#dust;
}
get fee(): bigint {
return this.#fee;
}
get txCount(): bigint {
return this.#txCount;
}
async commit(fileName: string, overwrite: boolean): Promise<boolean> {
return await fns.PendingTransaction_commit(this.#ptr, CString(fileName), overwrite);
}
async commitUR(maxFragmentLength: number): Promise<string | null> {
const commitUR = fns.PendingTransaction_commitUR;
if (!commitUR) return null;
return await readCString(
await commitUR(this.#ptr, maxFragmentLength),
);
}
async status(): Promise<number> {
return await getSymbol("PendingTransaction_status")(this.#pendingTxPtr);
return await fns.PendingTransaction_status(this.#ptr);
}
async errorString(): Promise<string | null> {
if (!await this.status()) return null;
const error = await getSymbol("PendingTransaction_errorString")(this.#pendingTxPtr);
if (!error) return null;
return await readCString(error) || null;
const error = await fns.PendingTransaction_errorString(this.#ptr);
return await readCString(error);
}
async throwIfError(sanitize = true): Promise<void> {
async throwIfError(): Promise<void> {
const maybeError = await this.errorString();
if (maybeError) {
if (sanitize) this.sanitizer?.();
throw new Error(maybeError);
}
}
async commit(fileName: string, overwrite: boolean, sanitize = true): Promise<boolean> {
const bool = await getSymbol("PendingTransaction_commit")(
this.#pendingTxPtr,
CString(fileName),
overwrite,
);
await this.throwIfError(sanitize);
return bool;
}
async commitUR(maxFragmentLength: number): Promise<string | null> {
const commitUR = getSymbol("PendingTransaction_commitUR");
if (!commitUR) {
return null;
}
const result = await commitUR(
this.#pendingTxPtr,
maxFragmentLength,
);
if (!result) return null;
await this.throwIfError();
return await readCString(result) || null;
}
async amount(): Promise<bigint> {
return await getSymbol("PendingTransaction_amount")(this.#pendingTxPtr);
}
async dust(): Promise<bigint> {
return await getSymbol("PendingTransaction_dust")(this.#pendingTxPtr);
}
async fee(): Promise<bigint> {
return await getSymbol("PendingTransaction_fee")(this.#pendingTxPtr);
}
async txid(separator: string, sanitize = true): Promise<string | null> {
const result = await getSymbol("PendingTransaction_txid")(
this.#pendingTxPtr,
CString(separator),
);
if (!result) return null;
await this.throwIfError(sanitize);
return await readCString(result) || null;
}
async txCount(): Promise<bigint> {
return await getSymbol("PendingTransaction_txCount")(this.#pendingTxPtr);
}
}
File diff suppressed because it is too large Load Diff
+20 -13
View File
@@ -1,34 +1,41 @@
import { fns } from "./bindings.ts";
import { TransactionInfo, TransactionInfoPtr } from "./transaction_info.ts";
import { CString, getSymbol } from "./utils.ts";
import { CString } from "./utils.ts";
export type TransactionHistoryPtr = Deno.PointerObject<"transactionHistory">;
export class TransactionHistory {
#txHistoryPtr: TransactionHistoryPtr;
#ptr: TransactionHistoryPtr;
constructor(txHistoryPtr: TransactionHistoryPtr) {
this.#txHistoryPtr = txHistoryPtr;
#count!: number;
constructor(ptr: TransactionHistoryPtr) {
this.#ptr = ptr;
}
async count(): Promise<number> {
return await getSymbol("TransactionHistory_count")(this.#txHistoryPtr);
static async new(ptr: TransactionHistoryPtr) {
const instance = new TransactionHistory(ptr);
instance.#count = await fns.TransactionHistory_count(ptr);
return instance;
}
get count(): number {
return this.#count;
}
async transaction(index: number): Promise<TransactionInfo> {
return new TransactionInfo(
(
await getSymbol("TransactionHistory_transaction")(this.#txHistoryPtr, index)
) as TransactionInfoPtr,
return TransactionInfo.new(
await fns.TransactionHistory_transaction(this.#ptr, index) as TransactionInfoPtr,
);
}
async refresh(): Promise<void> {
await getSymbol("TransactionHistory_refresh")(this.#txHistoryPtr);
await fns.TransactionHistory_refresh(this.#ptr);
}
async setTxNote(transactionId: string, note: string): Promise<void> {
await getSymbol("TransactionHistory_setTxNote")(
this.#txHistoryPtr,
await fns.TransactionHistory_setTxNote(
this.#ptr,
CString(transactionId),
CString(note),
);
+116 -73
View File
@@ -1,104 +1,147 @@
import { dylib } from "./bindings.ts";
import { getSymbol, readCString, Sanitizer } from "./utils.ts";
import { fns } from "./bindings.ts";
import { C_SEPARATOR, CString, maybeMultipleStrings, readCString, SEPARATOR } from "./utils.ts";
export type TransactionInfoPtr = Deno.PointerObject<"transactionInfo">;
export type TransactionInfoPtr = Deno.PointerObject<"pendingTransaction">;
export class TransactionInfo {
#txInfoPtr: TransactionInfoPtr;
sanitizer?: Sanitizer;
export interface TransferData {
address: string | null;
amount: bigint;
}
constructor(txInfoPtr: TransactionInfoPtr, sanitizer?: Sanitizer) {
this.#txInfoPtr = txInfoPtr;
this.sanitizer = sanitizer;
export class TransactionInfo<MultDest extends boolean = boolean> {
#ptr: TransactionInfoPtr;
#amount!: bigint;
#fee!: bigint;
#timestamp!: bigint;
#transfersCount!: number;
#paymentId!: string | null;
#hash!: string | null;
#subaddrAccount!: number;
#subaddrIndex!: string | null;
#transfers!: readonly TransferData[];
constructor(ptr: TransactionInfoPtr) {
this.#ptr = ptr;
}
static async new(ptr: TransactionInfoPtr): Promise<TransactionInfo> {
const instance = new TransactionInfo(ptr);
const [amount, paymentId, fee, hash, subaddrIndex, subaddrAccount, timestamp, transfersCount] = await Promise.all([
fns.TransactionInfo_amount(ptr),
fns.TransactionInfo_paymentId(ptr).then(readCString),
fns.TransactionInfo_fee(ptr),
fns.TransactionInfo_hash(ptr).then(readCString),
fns.TransactionInfo_subaddrIndex(ptr, C_SEPARATOR).then(readCString),
fns.TransactionInfo_subaddrAccount(ptr),
fns.TransactionInfo_timestamp(ptr),
fns.TransactionInfo_transfers_count(ptr),
]);
instance.#amount = amount;
instance.#fee = fee;
instance.#timestamp = timestamp;
instance.#transfersCount = transfersCount;
instance.#paymentId = paymentId;
instance.#hash = hash;
instance.#subaddrAccount = subaddrAccount;
instance.#subaddrIndex = subaddrIndex;
const transfers = [];
for (let i = 0; i < transfersCount; ++i) {
const [amount, address] = await Promise.all([
fns.TransactionInfo_transfers_amount(ptr, i),
fns.TransactionInfo_transfers_address(ptr, i).then(readCString),
]);
transfers.push({ amount, address });
}
Object.freeze(transfers);
instance.#transfers = transfers;
return instance;
}
get amount(): bigint {
return this.#amount;
}
get fee(): bigint {
return this.#fee;
}
get timestamp(): bigint {
return this.#timestamp;
}
get transfersCount(): number {
return this.#transfersCount;
}
get paymentId(): string | null {
return this.#paymentId;
}
get hash(): string | null {
return this.#hash;
}
get subaddrAccount(): number {
return this.#subaddrAccount;
}
get subaddrIndex(): string | null {
return this.#subaddrIndex;
}
get transfers(): readonly TransferData[] {
return this.#transfers;
}
async direction(): Promise<"in" | "out"> {
switch (await getSymbol("TransactionInfo_direction")(this.#txInfoPtr)) {
switch (await fns.TransactionInfo_direction(this.#ptr)) {
case 0:
return "in";
case 1:
return "out";
default:
await this.sanitizer?.();
throw new Error("Invalid TransactionInfo direction");
}
}
async isPending(): Promise<boolean> {
return await getSymbol("TransactionInfo_isPending")(this.#txInfoPtr);
async description(): Promise<string | null> {
return await readCString(
await fns.TransactionInfo_description(this.#ptr),
);
}
async isFailed(): Promise<boolean> {
return await getSymbol("TransactionInfo_isFailed")(this.#txInfoPtr);
}
async isCoinbase(): Promise<boolean> {
return await getSymbol("TransactionInfo_isCoinbase")(this.#txInfoPtr);
}
async amount(): Promise<bigint> {
return await getSymbol("TransactionInfo_amount")(this.#txInfoPtr);
}
async fee(): Promise<bigint> {
return await getSymbol("TransactionInfo_fee")(this.#txInfoPtr);
}
async blockHeight(): Promise<bigint> {
return await getSymbol("TransactionInfo_blockHeight")(this.#txInfoPtr);
}
async description(): Promise<string> {
const description = await getSymbol("TransactionInfo_description")(this.#txInfoPtr);
return await readCString(description) || "";
}
async subaddrIndex(): Promise<string> {
const subaddrIndex = await getSymbol("TransactionInfo_subaddrIndex")(this.#txInfoPtr);
return await readCString(subaddrIndex) || "";
}
async subaddrAccount(): Promise<number> {
return await getSymbol("TransactionInfo_subaddrAccount")(this.#txInfoPtr);
}
async label(): Promise<string> {
const label = await getSymbol("TransactionInfo_label")(this.#txInfoPtr);
return await readCString(label) || "";
async label(): Promise<string | null> {
return await readCString(
await fns.TransactionInfo_label(this.#ptr),
);
}
async confirmations(): Promise<bigint> {
return await getSymbol("TransactionInfo_confirmations")(this.#txInfoPtr);
return await fns.TransactionInfo_confirmations(this.#ptr);
}
async unlockTime(): Promise<bigint> {
return await getSymbol("TransactionInfo_unlockTime")(this.#txInfoPtr);
return await fns.TransactionInfo_unlockTime(this.#ptr);
}
async hash(): Promise<string> {
const hash = await getSymbol("TransactionInfo_hash")(this.#txInfoPtr);
return await readCString(hash) || "";
async isPending(): Promise<boolean> {
return await fns.TransactionInfo_isPending(this.#ptr);
}
async timestamp(): Promise<bigint> {
return await getSymbol("TransactionInfo_timestamp")(this.#txInfoPtr);
async isFailed(): Promise<boolean> {
return await fns.TransactionInfo_isFailed(this.#ptr);
}
async paymentId(): Promise<string> {
const paymentId = await getSymbol("TransactionInfo_paymentId")(this.#txInfoPtr);
return await readCString(paymentId) || "";
}
async transfersCount(): Promise<number> {
return await getSymbol("TransactionInfo_transfers_count")(this.#txInfoPtr);
}
async transfersAmount(index: number): Promise<bigint> {
return await getSymbol("TransactionInfo_transfers_amount")(this.#txInfoPtr, index);
}
async transfersAddress(index: number): Promise<string> {
const transfersAddress = await getSymbol("TransactionInfo_transfers_address")(this.#txInfoPtr, index);
return await readCString(transfersAddress) || "";
async isCoinbase(): Promise<boolean> {
return await fns.TransactionInfo_isCoinbase(this.#ptr);
}
}
@@ -0,0 +1,79 @@
import { fns } from "./bindings.ts";
import { C_SEPARATOR, CString, maybeMultipleStrings, readCString } from "./utils.ts";
export type UnsignedTransactionPtr = Deno.PointerObject<"pendingTransaction">;
export class UnsignedTransaction<MultDest extends boolean = false> {
#ptr: UnsignedTransactionPtr;
#amount!: string | string[] | null;
#fee!: string | string[] | null;
#txCount!: bigint;
#paymentId!: string | null;
#recipientAddress!: string | string[] | null;
constructor(ptr: UnsignedTransactionPtr) {
this.#ptr = ptr;
}
async status(): Promise<number> {
return await fns.UnsignedTransaction_status(this.#ptr);
}
async errorString(): Promise<string | null> {
return await readCString(await fns.UnsignedTransaction_errorString(this.#ptr));
}
static async new(ptr: UnsignedTransactionPtr): Promise<UnsignedTransaction> {
const instance = new UnsignedTransaction(ptr);
const [amount, paymentId, fee, txCount, recipientAddress] = await Promise.all([
fns.UnsignedTransaction_amount(ptr, C_SEPARATOR).then(readCString),
fns.UnsignedTransaction_paymentId(ptr, C_SEPARATOR).then(readCString),
fns.UnsignedTransaction_fee(ptr, C_SEPARATOR).then(readCString),
fns.UnsignedTransaction_txCount(ptr),
fns.UnsignedTransaction_recipientAddress(ptr, C_SEPARATOR).then(readCString),
]);
instance.#amount = maybeMultipleStrings(amount);
instance.#fee = maybeMultipleStrings(fee);
instance.#recipientAddress = maybeMultipleStrings(recipientAddress);
instance.#txCount = txCount;
instance.#paymentId = paymentId;
return instance;
}
get amount(): string | string[] | null {
return this.#amount;
}
get fee(): string | string[] | null {
return this.#fee;
}
get txCount(): bigint {
return this.#txCount;
}
get paymentId(): string | null {
return this.#paymentId;
}
get recipientAddress(): string | string[] | null {
return this.#recipientAddress;
}
async sign(signedFileName: string): Promise<boolean> {
return await fns.UnsignedTransaction_sign(this.#ptr, CString(signedFileName));
}
async signUR(maxFragmentLength: number): Promise<string | null> {
const signUR = fns.UnsignedTransaction_signUR;
if (!signUR) return null;
return await readCString(
await signUR(this.#ptr, maxFragmentLength),
);
}
}
+14 -18
View File
@@ -1,22 +1,19 @@
import { dylib } from "../mod.ts";
import type { moneroSymbols, MoneroTsDylib, WowneroTsDylib } from "./symbols.ts";
export type Sanitizer = () => void | PromiseLike<void>;
import { fns } from "./bindings.ts";
const textEncoder = new TextEncoder();
export function CString(string: string): Deno.PointerValue<string> {
return Deno.UnsafePointer.of(textEncoder.encode(`${string}\x00`));
export const SEPARATOR = ",";
export const C_SEPARATOR = CString(SEPARATOR);
export function maybeMultipleStrings(input: string): string | string[];
export function maybeMultipleStrings(input: null | string): null | string | string[];
export function maybeMultipleStrings(input: null | string): null | string | string[] {
if (!input) return null;
const multiple = input.split(SEPARATOR);
return multiple.length === 1 ? multiple[0] : multiple;
}
type SymbolWithoutPrefix = keyof typeof moneroSymbols extends `MONERO_${infer DylibSymbol}` ? DylibSymbol : never;
export function getSymbol<S extends SymbolWithoutPrefix>(
symbol: S,
): MoneroTsDylib["symbols"][`MONERO_${S}`] | WowneroTsDylib["symbols"][`WOWNERO_${S}`] {
if ("MONERO_free" in dylib.symbols) {
return dylib.symbols[`MONERO_${symbol}` as const];
} else {
return dylib.symbols[`WOWNERO_${symbol}` as const];
}
export function CString(string: string): Deno.PointerValue<string> {
return Deno.UnsafePointer.of(textEncoder.encode(`${string}\x00`));
}
/**
@@ -29,9 +26,8 @@ export async function readCString(pointer: Deno.PointerObject, free?: boolean):
export async function readCString(pointer: Deno.PointerValue, free?: boolean): Promise<string | null>;
export async function readCString(pointer: Deno.PointerValue, free = true): Promise<string | null> {
if (!pointer) return null;
const string = new Deno.UnsafePointerView(pointer).getCString();
if (free) {
await getSymbol("free")(pointer);
}
if (string && free) await fns.free(pointer);
return string;
}
+241 -219
View File
@@ -1,294 +1,268 @@
import { dylib } from "./bindings.ts";
import { CString, getSymbol, readCString, Sanitizer } from "./utils.ts";
import { WalletManager } from "./wallet_manager.ts";
import { WalletManager, type WalletManagerPtr } from "./wallet_manager.ts";
import { TransactionHistory, TransactionHistoryPtr } from "./transaction_history.ts";
import { PendingTransaction } from "./pending_transaction.ts";
import { PendingTransactionPtr } from "./pending_transaction.ts";
import { C_SEPARATOR, CString, readCString, SEPARATOR } from "./utils.ts";
import { PendingTransaction, PendingTransactionPtr } from "./pending_transaction.ts";
import { UnsignedTransaction, UnsignedTransactionPtr } from "./unsigned_transaction.ts";
import { Coins, CoinsPtr } from "./coins.ts";
import { fns } from "./bindings.ts";
export type WalletPtr = Deno.PointerObject<"walletManager">;
interface DaemonInfo {
address?: string;
username?: string;
password?: string;
lightWallet?: boolean;
proxyAddress?: string;
}
export class Wallet {
#walletManagerPtr: WalletManagerPtr;
#walletPtr: WalletPtr;
sanitizer?: Sanitizer;
#walletManager: WalletManager;
#ptr: WalletPtr;
constructor(walletManagerPtr: WalletManager, walletPtr: WalletPtr, sanitizer?: Sanitizer) {
this.#walletPtr = walletPtr;
this.#walletManagerPtr = walletManagerPtr.getPointer();
this.sanitizer = sanitizer;
constructor(walletManager: WalletManager, ptr: WalletPtr) {
this.#walletManager = walletManager;
this.#ptr = ptr;
}
getPointer(): WalletPtr {
return this.#walletPtr;
getPointer() {
return this.#ptr;
}
async store(path = ""): Promise<boolean> {
const bool = await getSymbol("Wallet_store")(this.#walletPtr, CString(path));
await this.throwIfError();
return bool;
}
async init(daemonInfo: DaemonInfo, log = false): Promise<boolean> {
const success = await fns.Wallet_init(
this.#ptr,
CString(daemonInfo.address ?? ""),
0n,
CString(daemonInfo.username ?? ""),
CString(daemonInfo.password ?? ""),
false,
daemonInfo.lightWallet ?? false,
CString(daemonInfo.proxyAddress ?? ""),
);
if (log) {
await fns.Wallet_init3(
this.#ptr,
CString(""),
CString(""),
CString(""),
true,
);
}
async initWallet(daemonAddress = "http://nodex.monerujo.io:18081"): Promise<void> {
await this.init();
await this.setTrustedDaemon(true);
await this.setDaemonAddress(daemonAddress);
await this.startRefresh();
await this.refreshAsync();
await this.throwIfError();
}
async setDaemonAddress(address: string): Promise<void> {
await getSymbol("WalletManager_setDaemonAddress")(
this.#walletManagerPtr,
CString(address),
);
}
async startRefresh(): Promise<void> {
await getSymbol("Wallet_startRefresh")(this.#walletPtr);
await this.throwIfError();
}
async refreshAsync(): Promise<void> {
await getSymbol("Wallet_refreshAsync")(this.#walletPtr);
await this.throwIfError();
}
async init(): Promise<boolean> {
const bool = await getSymbol("Wallet_init")(
this.#walletPtr,
CString("http://nodex.monerujo.io:18081"),
0n,
CString(""),
CString(""),
false,
false,
CString(""),
);
await this.throwIfError();
return bool;
return success;
}
async setTrustedDaemon(value: boolean): Promise<void> {
await getSymbol("Wallet_setTrustedDaemon")(this.#walletPtr, value);
return await fns.Wallet_setTrustedDaemon(this.#ptr, value);
}
static async create(
walletManager: WalletManager,
path: string,
password: string,
sanitizeError = true,
): Promise<Wallet> {
// We assign holder of the pointer in Wallet constructor
const walletManagerPtr = walletManager.getPointer();
async startRefresh(): Promise<void> {
return await fns.Wallet_startRefresh(this.#ptr);
}
const walletPtr = await getSymbol("WalletManager_createWallet")(
walletManagerPtr,
CString(path),
CString(password),
CString("English"),
0,
async refreshAsync(): Promise<void> {
return await fns.Wallet_refreshAsync(this.#ptr);
}
async setupBackgroundSync(
backgroundSyncType: number,
walletPassword: string,
backgroundCachePassword: string,
): Promise<boolean> {
return await fns.Wallet_setupBackgroundSync(
this.#ptr,
backgroundSyncType,
CString(walletPassword),
CString(backgroundCachePassword),
);
const wallet = new Wallet(walletManager, walletPtr as WalletPtr, walletManager.sanitizer);
await wallet.throwIfError(sanitizeError);
await wallet.initWallet();
return wallet;
}
static async open(
walletManager: WalletManager,
path: string,
password: string,
sanitizeError = true,
): Promise<Wallet> {
// We assign holder of the pointer in Wallet constructor
const walletManagerPtr = walletManager.getPointer();
async startBackgroundSync(): Promise<boolean> {
return await fns.Wallet_startBackgroundSync(this.#ptr);
}
const walletPtr = await getSymbol("WalletManager_openWallet")(
walletManagerPtr,
CString(path),
CString(password),
0,
async stopBackgroundSync(walletPassword: string): Promise<boolean> {
return await fns.Wallet_stopBackgroundSync(this.#ptr, CString(walletPassword));
}
async store(path = ""): Promise<boolean> {
return await fns.Wallet_store(this.#ptr, CString(path));
}
async close(store: boolean): Promise<boolean> {
return await fns.WalletManager_closeWallet(this.#walletManager.getPointer(), this.#ptr, store);
}
async seed(offset = ""): Promise<string | null> {
return await readCString(
await fns.Wallet_seed(this.#ptr, CString(offset)),
);
const wallet = new Wallet(walletManager, walletPtr as WalletPtr, walletManager.sanitizer);
await wallet.throwIfError(sanitizeError);
await wallet.initWallet();
return wallet;
}
static async recover(
walletManager: WalletManager,
path: string,
password: string,
mnemonic: string,
restoreHeight: bigint,
seedOffset: string = "",
sanitizeError = true,
): Promise<Wallet> {
// We assign holder of the pointer in Wallet constructor
const walletManagerPtr = walletManager.getPointer();
const walletPtr = await getSymbol("WalletManager_recoveryWallet")(
walletManagerPtr,
CString(path),
CString(password),
CString(mnemonic),
0,
restoreHeight,
1n,
CString(seedOffset),
async address(accountIndex = 0n, addressIndex = 0n): Promise<string | null> {
return await readCString(
await fns.Wallet_address(this.#ptr, accountIndex, addressIndex),
);
const wallet = new Wallet(walletManager, walletPtr as WalletPtr, walletManager.sanitizer);
await wallet.throwIfError(sanitizeError);
await wallet.initWallet();
return wallet;
}
async address(accountIndex = 0n, addressIndex = 0n): Promise<string> {
const address = await getSymbol("Wallet_address")(this.#walletPtr, accountIndex, addressIndex);
if (!address) {
const error = await this.errorString();
throw new Error(`Failed getting address from a wallet: ${error ?? "<Error unknown>"}`);
}
return await readCString(address);
}
async balance(accountIndex = 0): Promise<bigint> {
return await getSymbol("Wallet_balance")(this.#walletPtr, accountIndex);
return await fns.Wallet_balance(this.#ptr, accountIndex);
}
async unlockedBalance(accountIndex = 0): Promise<bigint> {
return await getSymbol("Wallet_unlockedBalance")(this.#walletPtr, accountIndex);
}
status(): Promise<number> {
return getSymbol("Wallet_status")(this.#walletPtr);
}
async errorString(): Promise<string | null> {
if (!await this.status()) return null;
const error = await getSymbol("Wallet_errorString")(this.#walletPtr);
if (!error) return null;
return await readCString(error) || null;
}
async throwIfError(sanitize = true): Promise<void> {
const maybeError = await this.errorString();
if (maybeError) {
if (sanitize) this.sanitizer?.();
throw new Error(maybeError);
}
return await fns.Wallet_unlockedBalance(this.#ptr, accountIndex);
}
async synchronized(): Promise<boolean> {
const synchronized = await getSymbol("Wallet_synchronized")(this.#walletPtr);
await this.throwIfError();
return synchronized;
return await fns.Wallet_synchronized(this.#ptr);
}
async blockChainHeight(): Promise<bigint> {
const height = await getSymbol("Wallet_blockChainHeight")(this.#walletPtr);
await this.throwIfError();
return height;
return await fns.Wallet_blockChainHeight(this.#ptr);
}
async daemonBlockChainHeight(): Promise<bigint> {
const height = await getSymbol("Wallet_daemonBlockChainHeight")(this.#walletPtr);
await this.throwIfError();
return height;
}
async managerBlockChainHeight(): Promise<bigint> {
const height = await getSymbol("WalletManager_blockchainHeight")(this.#walletManagerPtr);
await this.throwIfError();
return height;
}
async managerTargetBlockChainHeight(): Promise<bigint> {
const height = await getSymbol("WalletManager_blockchainTargetHeight")(this.#walletManagerPtr);
await this.throwIfError();
return height;
return await fns.Wallet_daemonBlockChainHeight(this.#ptr);
}
async addSubaddressAccount(label: string): Promise<void> {
await getSymbol("Wallet_addSubaddressAccount")(
this.#walletPtr,
CString(label),
);
await this.throwIfError();
return await fns.Wallet_addSubaddressAccount(this.#ptr, CString(label));
}
async numSubaddressAccounts(): Promise<bigint> {
const accountsLen = await getSymbol("Wallet_numSubaddressAccounts")(this.#walletPtr);
await this.throwIfError();
return accountsLen;
return await fns.Wallet_numSubaddressAccounts(this.#ptr);
}
async addSubaddress(accountIndex: number, label: string): Promise<void> {
await getSymbol("Wallet_addSubaddress")(
this.#walletPtr,
return await fns.Wallet_addSubaddress(
this.#ptr,
accountIndex,
CString(label),
);
await this.throwIfError();
}
async numSubaddresses(accountIndex: number): Promise<bigint> {
const address = await getSymbol("Wallet_numSubaddresses")(
this.#walletPtr,
return await fns.Wallet_numSubaddresses(
this.#ptr,
accountIndex,
);
await this.throwIfError();
return address;
}
async getSubaddressLabel(accountIndex: number, addressIndex: number): Promise<string> {
const label = await getSymbol("Wallet_getSubaddressLabel")(this.#walletPtr, accountIndex, addressIndex);
if (!label) {
const error = await this.errorString();
throw new Error(`Failed getting subaddress label from a wallet: ${error ?? "<Error unknown>"}`);
}
return await readCString(label);
async getSubaddressLabel(accountIndex: number, addressIndex: number): Promise<string | null> {
return await readCString(
await fns.Wallet_getSubaddressLabel(this.#ptr, accountIndex, addressIndex),
);
}
async setSubaddressLabel(accountIndex: number, addressIndex: number, label: string): Promise<void> {
await getSymbol("Wallet_setSubaddressLabel")(
this.#walletPtr,
accountIndex,
addressIndex,
CString(label),
);
await this.throwIfError();
return await fns.Wallet_setSubaddressLabel(this.#ptr, accountIndex, addressIndex, CString(label));
}
async getHistory(): Promise<TransactionHistory> {
const transactionHistoryPointer = await getSymbol("Wallet_history")(this.#walletPtr);
await this.throwIfError();
return new TransactionHistory(transactionHistoryPointer as TransactionHistoryPtr);
async isOffline(): Promise<boolean> {
return await fns.Wallet_isOffline(this.#ptr);
}
async setOffline(offline: boolean): Promise<void> {
return await fns.Wallet_setOffline(this.#ptr, offline);
}
async publicViewKey(): Promise<string | null> {
return await readCString(await fns.Wallet_publicViewKey(this.#ptr));
}
async secretViewKey(): Promise<string | null> {
return await readCString(await fns.Wallet_secretViewKey(this.#ptr));
}
async publicSpendKey(): Promise<string | null> {
return await readCString(await fns.Wallet_publicSpendKey(this.#ptr));
}
async secretSpendKey(): Promise<string | null> {
return await readCString(await fns.Wallet_secretSpendKey(this.#ptr));
}
async exportOutputs(fileName: string, all: boolean): Promise<boolean> {
return await fns.Wallet_exportOutputs(this.#ptr, CString(fileName), all);
}
async exportOutputsUR(maxFragmentLength: bigint, all: boolean): Promise<string | null> {
const exportOutputsUR = fns.Wallet_exportOutputsUR;
if (!exportOutputsUR) return null;
return await readCString(
await exportOutputsUR(this.#ptr, maxFragmentLength, all),
);
}
async importOutputs(fileName: string): Promise<boolean> {
return await fns.Wallet_importOutputs(this.#ptr, CString(fileName));
}
async importOutputsUR(input: string): Promise<boolean | null> {
const importOutputsUR = fns.Wallet_importOutputsUR;
if (!importOutputsUR) return null;
return await importOutputsUR(this.#ptr, CString(input));
}
async exportKeyImages(fileName: string, all: boolean): Promise<boolean> {
return await fns.Wallet_exportKeyImages(this.#ptr, CString(fileName), all);
}
async exportKeyImagesUR(maxFragmentLength: bigint, all: boolean): Promise<string | null> {
const exportKeyImagesUR = fns.Wallet_exportKeyImagesUR;
if (!exportKeyImagesUR) return null;
return await readCString(
await exportKeyImagesUR(this.#ptr, maxFragmentLength, all),
);
}
async importKeyImages(fileName: string): Promise<boolean> {
return await fns.Wallet_importKeyImages(this.#ptr, CString(fileName));
}
async importKeyImagesUR(input: string): Promise<boolean | null> {
const importKeyImagesUR = fns.Wallet_importKeyImagesUR;
if (!importKeyImagesUR) return null;
return await importKeyImagesUR(this.#ptr, CString(input));
}
async loadUnsignedTx(fileName: string): Promise<UnsignedTransaction> {
const pendingTxPtr = await fns.Wallet_loadUnsignedTx(this.#ptr, CString(fileName));
return UnsignedTransaction.new(pendingTxPtr as UnsignedTransactionPtr);
}
async loadUnsignedTxUR(input: string): Promise<UnsignedTransaction | null> {
const loadUnsignedTxUR = fns.Wallet_loadUnsignedTxUR;
if (!loadUnsignedTxUR) return null;
const pendingTxPtr = await loadUnsignedTxUR(this.#ptr, CString(input));
if (await this.status()) {
throw this.errorString();
}
return UnsignedTransaction.new(pendingTxPtr as UnsignedTransactionPtr);
}
async createTransaction(
destinationAddress: string,
amount: bigint,
pendingTransactionPriority = 0 | 1 | 2 | 3,
pendingTransactionPriority: 0 | 1 | 2 | 3,
subaddressAccount: number,
sanitize = true,
prefferedInputs = "",
mixinCount = 0,
paymentId = "",
separator = ",",
): Promise<PendingTransaction> {
const pendingTxPtr = await getSymbol("Wallet_createTransaction")(
this.#walletPtr,
): Promise<PendingTransaction | null> {
const pendingTxPtr = await fns.Wallet_createTransaction(
this.#ptr,
CString(destinationAddress),
CString(paymentId),
amount,
@@ -296,13 +270,61 @@ export class Wallet {
pendingTransactionPriority,
subaddressAccount,
CString(prefferedInputs),
CString(separator),
C_SEPARATOR,
);
await this.throwIfError(sanitize);
return new PendingTransaction(pendingTxPtr as PendingTransactionPtr);
if (!pendingTxPtr) return null;
return PendingTransaction.new(pendingTxPtr as PendingTransactionPtr);
}
async amountFromString(amount: string): Promise<bigint> {
return await getSymbol("Wallet_amountFromString")(CString(amount));
async createTransactionMultDest(
destinationAddresses: string[],
amounts: bigint[],
amountSweepAll: boolean,
pendingTransactionPriority: 0 | 1 | 2 | 3,
subaddressAccount: number,
preferredInputs: string[] = [],
mixinCount = 0,
paymentId = "",
): Promise<PendingTransaction> {
const pendingTxPtr = await fns.Wallet_createTransactionMultDest(
this.#ptr,
CString(destinationAddresses.join(SEPARATOR)),
C_SEPARATOR,
CString(paymentId),
amountSweepAll,
CString(amounts.join(SEPARATOR)),
C_SEPARATOR,
mixinCount,
pendingTransactionPriority,
subaddressAccount,
CString(preferredInputs.join(SEPARATOR)),
C_SEPARATOR,
);
return PendingTransaction.new(pendingTxPtr as PendingTransactionPtr);
}
async coins(): Promise<Coins | null> {
const coinsPtr = await fns.Wallet_coins(this.#ptr);
if (!coinsPtr) return null;
return new Coins(coinsPtr as CoinsPtr);
}
async status(): Promise<number> {
return await fns.Wallet_status(this.#ptr);
}
async errorString(): Promise<string | null> {
if (!await this.status()) return null;
const error = await fns.Wallet_errorString(this.#ptr);
return await readCString(error);
}
async throwIfError(): Promise<void> {
const maybeError = await this.errorString();
if (maybeError) {
throw new Error(maybeError);
}
}
}
+130 -8
View File
@@ -1,26 +1,148 @@
import { getSymbol, Sanitizer } from "./utils.ts";
import { fns } from "./bindings.ts";
import { CString } from "./utils.ts";
import { Wallet, WalletPtr } from "./wallet.ts";
export type WalletManagerPtr = Deno.PointerObject<"walletManager">;
export class WalletManager {
#ptr: WalletManagerPtr;
sanitizer?: Sanitizer;
constructor(walletManagerPtr: WalletManagerPtr, sanitizer?: Sanitizer) {
constructor(walletManagerPtr: WalletManagerPtr) {
this.#ptr = walletManagerPtr;
this.sanitizer = sanitizer;
}
getPointer(): WalletManagerPtr {
return this.#ptr;
}
static async new(sanitizer?: Sanitizer) {
const ptr = await getSymbol("WalletManagerFactory_getWalletManager")();
static async new() {
const ptr = await fns.WalletManagerFactory_getWalletManager();
if (!ptr) {
sanitizer?.();
throw new Error("Failed retrieving wallet manager");
}
return new WalletManager(ptr as WalletManagerPtr, sanitizer);
return new WalletManager(ptr as WalletManagerPtr);
}
async setDaemonAddress(address: string): Promise<void> {
return await fns.WalletManager_setDaemonAddress(this.#ptr, CString(address));
}
async createWallet(path: string, password: string): Promise<Wallet> {
const walletPtr = await fns.WalletManager_createWallet(
this.#ptr,
CString(path),
CString(password),
CString("English"),
0,
);
const wallet = new Wallet(this, walletPtr as WalletPtr);
await wallet.throwIfError();
return wallet;
}
async openWallet(path: string, password: string): Promise<Wallet> {
const walletPtr = await fns.WalletManager_openWallet(
this.#ptr,
CString(path),
CString(password),
0,
);
const wallet = new Wallet(this, walletPtr as WalletPtr);
await wallet.throwIfError();
return wallet;
}
async recoverWallet(
path: string,
password: string,
mnemonic: string,
restoreHeight: bigint,
seedOffset: string = "",
): Promise<Wallet> {
const walletPtr = await fns.WalletManager_recoveryWallet(
this.#ptr,
CString(path),
CString(password),
CString(mnemonic),
0,
restoreHeight,
1n,
CString(seedOffset),
);
const wallet = new Wallet(this, walletPtr as WalletPtr);
await wallet.throwIfError();
return wallet;
}
async recoverFromPolyseed(
path: string,
password: string,
mnemonic: string,
restoreHeight: bigint,
passphrase = "",
): Promise<Wallet> {
return await this.createFromPolyseed(
path,
password,
mnemonic,
restoreHeight,
passphrase,
false,
);
}
async createFromPolyseed(
path: string,
password: string,
mnemonic: string,
restoreHeight: bigint,
passphrase = "",
newWallet = true,
): Promise<Wallet> {
const walletPtr = await fns.WalletManager_createWalletFromPolyseed(
this.#ptr,
CString(path),
CString(password),
0,
CString(mnemonic),
CString(passphrase),
newWallet,
restoreHeight,
1n,
);
const wallet = new Wallet(this, walletPtr as WalletPtr);
await wallet.throwIfError();
return wallet;
}
async recoverFromKeys(
path: string,
password: string,
restoreHeight: bigint,
address: string,
viewKey: string,
spendKey: string,
): Promise<Wallet> {
const walletPtr = await fns.WalletManager_createWalletFromKeys(
this.#ptr,
CString(path),
CString(password),
CString("English"),
0,
restoreHeight,
CString(address),
CString(viewKey),
CString(spendKey),
0n,
);
const wallet = new Wallet(this, walletPtr as WalletPtr);
await wallet.throwIfError();
return wallet;
}
}