From 40c1a1bda4b6f125c702f5a37ecc48a6ebec24b8 Mon Sep 17 00:00:00 2001 From: Mateusz Franik <47059999+Im-Beast@users.noreply.github.com> Date: Sun, 1 Dec 2024 15:02:20 +0100 Subject: 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 --- impls/monero.ts/src/wallet.ts | 446 ++++++++++++++++++++++-------------------- 1 file changed, 234 insertions(+), 212 deletions(-) (limited to 'impls/monero.ts/src/wallet.ts') diff --git a/impls/monero.ts/src/wallet.ts b/impls/monero.ts/src/wallet.ts index ea25f21..673ccab 100644 --- a/impls/monero.ts/src/wallet.ts +++ b/impls/monero.ts/src/wallet.ts @@ -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 { - const bool = await getSymbol("Wallet_store")(this.#walletPtr, CString(path)); - await this.throwIfError(); - return bool; - } + async init(daemonInfo: DaemonInfo, log = false): Promise { + 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 { - await this.init(); await this.setTrustedDaemon(true); - await this.setDaemonAddress(daemonAddress); await this.startRefresh(); await this.refreshAsync(); - await this.throwIfError(); + + return success; } - async setDaemonAddress(address: string): Promise { - await getSymbol("WalletManager_setDaemonAddress")( - this.#walletManagerPtr, - CString(address), - ); + async setTrustedDaemon(value: boolean): Promise { + return await fns.Wallet_setTrustedDaemon(this.#ptr, value); } async startRefresh(): Promise { - await getSymbol("Wallet_startRefresh")(this.#walletPtr); - await this.throwIfError(); + return await fns.Wallet_startRefresh(this.#ptr); } async refreshAsync(): Promise { - await getSymbol("Wallet_refreshAsync")(this.#walletPtr); - await this.throwIfError(); - } - - async init(): Promise { - const bool = await getSymbol("Wallet_init")( - this.#walletPtr, - CString("http://nodex.monerujo.io:18081"), - 0n, - CString(""), - CString(""), - false, - false, - CString(""), + return await fns.Wallet_refreshAsync(this.#ptr); + } + + async setupBackgroundSync( + backgroundSyncType: number, + walletPassword: string, + backgroundCachePassword: string, + ): Promise { + return await fns.Wallet_setupBackgroundSync( + this.#ptr, + backgroundSyncType, + CString(walletPassword), + CString(backgroundCachePassword), ); - await this.throwIfError(); - return bool; } - async setTrustedDaemon(value: boolean): Promise { - await getSymbol("Wallet_setTrustedDaemon")(this.#walletPtr, value); - } - - static async create( - walletManager: WalletManager, - path: string, - password: string, - sanitizeError = true, - ): Promise { - // We assign holder of the pointer in Wallet constructor - const walletManagerPtr = walletManager.getPointer(); - - const walletPtr = await getSymbol("WalletManager_createWallet")( - walletManagerPtr, - CString(path), - CString(password), - CString("English"), - 0, - ); + async startBackgroundSync(): Promise { + return await fns.Wallet_startBackgroundSync(this.#ptr); + } - const wallet = new Wallet(walletManager, walletPtr as WalletPtr, walletManager.sanitizer); - await wallet.throwIfError(sanitizeError); - await wallet.initWallet(); + async stopBackgroundSync(walletPassword: string): Promise { + return await fns.Wallet_stopBackgroundSync(this.#ptr, CString(walletPassword)); + } - return wallet; + async store(path = ""): Promise { + return await fns.Wallet_store(this.#ptr, CString(path)); } - static async open( - walletManager: WalletManager, - path: string, - password: string, - sanitizeError = true, - ): Promise { - // We assign holder of the pointer in Wallet constructor - const walletManagerPtr = walletManager.getPointer(); + async close(store: boolean): Promise { + return await fns.WalletManager_closeWallet(this.#walletManager.getPointer(), this.#ptr, store); + } - const walletPtr = await getSymbol("WalletManager_openWallet")( - walletManagerPtr, - CString(path), - CString(password), - 0, + async seed(offset = ""): Promise { + 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 { - // 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 { + 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(); + async balance(accountIndex = 0): Promise { + return await fns.Wallet_balance(this.#ptr, accountIndex); + } - return wallet; + async unlockedBalance(accountIndex = 0): Promise { + return await fns.Wallet_unlockedBalance(this.#ptr, accountIndex); } - async address(accountIndex = 0n, addressIndex = 0n): Promise { - 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 ?? ""}`); - } - return await readCString(address); + async synchronized(): Promise { + return await fns.Wallet_synchronized(this.#ptr); } - async balance(accountIndex = 0): Promise { - return await getSymbol("Wallet_balance")(this.#walletPtr, accountIndex); + async blockChainHeight(): Promise { + return await fns.Wallet_blockChainHeight(this.#ptr); } - async unlockedBalance(accountIndex = 0): Promise { - return await getSymbol("Wallet_unlockedBalance")(this.#walletPtr, accountIndex); + async daemonBlockChainHeight(): Promise { + return await fns.Wallet_daemonBlockChainHeight(this.#ptr); } - status(): Promise { - return getSymbol("Wallet_status")(this.#walletPtr); + async addSubaddressAccount(label: string): Promise { + return await fns.Wallet_addSubaddressAccount(this.#ptr, CString(label)); } - async errorString(): Promise { - if (!await this.status()) return null; + async numSubaddressAccounts(): Promise { + return await fns.Wallet_numSubaddressAccounts(this.#ptr); + } - const error = await getSymbol("Wallet_errorString")(this.#walletPtr); - if (!error) return null; + async addSubaddress(accountIndex: number, label: string): Promise { + return await fns.Wallet_addSubaddress( + this.#ptr, + accountIndex, + CString(label), + ); + } - return await readCString(error) || null; + async numSubaddresses(accountIndex: number): Promise { + return await fns.Wallet_numSubaddresses( + this.#ptr, + accountIndex, + ); } - async throwIfError(sanitize = true): Promise { - const maybeError = await this.errorString(); - if (maybeError) { - if (sanitize) this.sanitizer?.(); - throw new Error(maybeError); - } + async getSubaddressLabel(accountIndex: number, addressIndex: number): Promise { + return await readCString( + await fns.Wallet_getSubaddressLabel(this.#ptr, accountIndex, addressIndex), + ); } - async synchronized(): Promise { - const synchronized = await getSymbol("Wallet_synchronized")(this.#walletPtr); - await this.throwIfError(); - return synchronized; + async setSubaddressLabel(accountIndex: number, addressIndex: number, label: string): Promise { + return await fns.Wallet_setSubaddressLabel(this.#ptr, accountIndex, addressIndex, CString(label)); } - async blockChainHeight(): Promise { - const height = await getSymbol("Wallet_blockChainHeight")(this.#walletPtr); - await this.throwIfError(); - return height; + async isOffline(): Promise { + return await fns.Wallet_isOffline(this.#ptr); } - async daemonBlockChainHeight(): Promise { - const height = await getSymbol("Wallet_daemonBlockChainHeight")(this.#walletPtr); - await this.throwIfError(); - return height; + async setOffline(offline: boolean): Promise { + return await fns.Wallet_setOffline(this.#ptr, offline); } - async managerBlockChainHeight(): Promise { - const height = await getSymbol("WalletManager_blockchainHeight")(this.#walletManagerPtr); - await this.throwIfError(); - return height; + async publicViewKey(): Promise { + return await readCString(await fns.Wallet_publicViewKey(this.#ptr)); } - async managerTargetBlockChainHeight(): Promise { - const height = await getSymbol("WalletManager_blockchainTargetHeight")(this.#walletManagerPtr); - await this.throwIfError(); - return height; + async secretViewKey(): Promise { + return await readCString(await fns.Wallet_secretViewKey(this.#ptr)); } - async addSubaddressAccount(label: string): Promise { - await getSymbol("Wallet_addSubaddressAccount")( - this.#walletPtr, - CString(label), - ); - await this.throwIfError(); + async publicSpendKey(): Promise { + return await readCString(await fns.Wallet_publicSpendKey(this.#ptr)); } - async numSubaddressAccounts(): Promise { - const accountsLen = await getSymbol("Wallet_numSubaddressAccounts")(this.#walletPtr); - await this.throwIfError(); - return accountsLen; + async secretSpendKey(): Promise { + return await readCString(await fns.Wallet_secretSpendKey(this.#ptr)); } - async addSubaddress(accountIndex: number, label: string): Promise { - await getSymbol("Wallet_addSubaddress")( - this.#walletPtr, - accountIndex, - CString(label), - ); - await this.throwIfError(); + async exportOutputs(fileName: string, all: boolean): Promise { + return await fns.Wallet_exportOutputs(this.#ptr, CString(fileName), all); } - async numSubaddresses(accountIndex: number): Promise { - const address = await getSymbol("Wallet_numSubaddresses")( - this.#walletPtr, - accountIndex, + async exportOutputsUR(maxFragmentLength: bigint, all: boolean): Promise { + const exportOutputsUR = fns.Wallet_exportOutputsUR; + if (!exportOutputsUR) return null; + + return await readCString( + await exportOutputsUR(this.#ptr, maxFragmentLength, all), ); - await this.throwIfError(); - return address; } - async getSubaddressLabel(accountIndex: number, addressIndex: number): Promise { - 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 ?? ""}`); - } - return await readCString(label); + async importOutputs(fileName: string): Promise { + return await fns.Wallet_importOutputs(this.#ptr, CString(fileName)); } - async setSubaddressLabel(accountIndex: number, addressIndex: number, label: string): Promise { - await getSymbol("Wallet_setSubaddressLabel")( - this.#walletPtr, - accountIndex, - addressIndex, - CString(label), + async importOutputsUR(input: string): Promise { + const importOutputsUR = fns.Wallet_importOutputsUR; + if (!importOutputsUR) return null; + + return await importOutputsUR(this.#ptr, CString(input)); + } + + async exportKeyImages(fileName: string, all: boolean): Promise { + return await fns.Wallet_exportKeyImages(this.#ptr, CString(fileName), all); + } + + async exportKeyImagesUR(maxFragmentLength: bigint, all: boolean): Promise { + const exportKeyImagesUR = fns.Wallet_exportKeyImagesUR; + if (!exportKeyImagesUR) return null; + + return await readCString( + await exportKeyImagesUR(this.#ptr, maxFragmentLength, all), ); - await this.throwIfError(); } - async getHistory(): Promise { - const transactionHistoryPointer = await getSymbol("Wallet_history")(this.#walletPtr); - await this.throwIfError(); - return new TransactionHistory(transactionHistoryPointer as TransactionHistoryPtr); + async importKeyImages(fileName: string): Promise { + return await fns.Wallet_importKeyImages(this.#ptr, CString(fileName)); + } + + async importKeyImagesUR(input: string): Promise { + const importKeyImagesUR = fns.Wallet_importKeyImagesUR; + if (!importKeyImagesUR) return null; + + return await importKeyImagesUR(this.#ptr, CString(input)); + } + + async loadUnsignedTx(fileName: string): Promise { + const pendingTxPtr = await fns.Wallet_loadUnsignedTx(this.#ptr, CString(fileName)); + return UnsignedTransaction.new(pendingTxPtr as UnsignedTransactionPtr); + } + + async loadUnsignedTxUR(input: string): Promise { + 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 { - const pendingTxPtr = await getSymbol("Wallet_createTransaction")( - this.#walletPtr, + ): Promise { + 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, + ); + + if (!pendingTxPtr) return null; + return PendingTransaction.new(pendingTxPtr as PendingTransactionPtr); + } + + async createTransactionMultDest( + destinationAddresses: string[], + amounts: bigint[], + amountSweepAll: boolean, + pendingTransactionPriority: 0 | 1 | 2 | 3, + subaddressAccount: number, + preferredInputs: string[] = [], + mixinCount = 0, + paymentId = "", + ): Promise { + 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, ); - await this.throwIfError(sanitize); - return new PendingTransaction(pendingTxPtr as PendingTransactionPtr); + return PendingTransaction.new(pendingTxPtr as PendingTransactionPtr); + } + + async coins(): Promise { + const coinsPtr = await fns.Wallet_coins(this.#ptr); + if (!coinsPtr) return null; + + return new Coins(coinsPtr as CoinsPtr); + } + + async status(): Promise { + return await fns.Wallet_status(this.#ptr); + } + + async errorString(): Promise { + if (!await this.status()) return null; + const error = await fns.Wallet_errorString(this.#ptr); + return await readCString(error); } - async amountFromString(amount: string): Promise { - return await getSymbol("Wallet_amountFromString")(CString(amount)); + async throwIfError(): Promise { + const maybeError = await this.errorString(); + if (maybeError) { + throw new Error(maybeError); + } } } -- cgit v1.2.3