From b946ba9338d65393c964936bd4d3b58ff96292cd Mon Sep 17 00:00:00 2001 From: Matthew Date: Fri, 13 Feb 2026 12:43:02 -0800 Subject: [PATCH 01/10] Move ZanoSyncTracker to utils It's useful for other similar slow sync privacy chains --- .../WeightedSyncTracker.ts} | 13 ++++++++----- src/zano/ZanoEngine.ts | 9 ++++++--- 2 files changed, 14 insertions(+), 8 deletions(-) rename src/{zano/ZanoSyncTracker.ts => common/WeightedSyncTracker.ts} (85%) diff --git a/src/zano/ZanoSyncTracker.ts b/src/common/WeightedSyncTracker.ts similarity index 85% rename from src/zano/ZanoSyncTracker.ts rename to src/common/WeightedSyncTracker.ts index e706f29cc..de96c840d 100644 --- a/src/zano/ZanoSyncTracker.ts +++ b/src/common/WeightedSyncTracker.ts @@ -1,6 +1,6 @@ import type { EdgeSyncStatus } from 'edge-core-js/types' -import { SyncEngine, SyncTracker } from '../common/SyncTracker' +import { SyncEngine, SyncTracker } from './SyncTracker' const SYNC_PROGRESS_WEIGHT = 0.85 const BALANCE_PROGRESS_WEIGHT = 0.05 @@ -9,7 +9,7 @@ const TRANSACTION_PROGRESS_WEIGHT = 0.1 /** * A sync status tracker that works block-by-block. */ -export interface ZanoSyncTracker extends SyncTracker { +export interface WeightedSyncTracker extends SyncTracker { updateBalanceRatio: (ratio: number) => void updateBlockRatio: ( ratio: number, @@ -20,9 +20,12 @@ export interface ZanoSyncTracker extends SyncTracker { } /** - * Creates a Sync + * Creates a weighted sync tracker that blends block, balance, + * and history progress into a single ratio. */ -export function makeZanoSyncTracker(engine: SyncEngine): ZanoSyncTracker { +export function makeWeightedSyncTracker( + engine: SyncEngine +): WeightedSyncTracker { let balanceRatio = 0 let blockRatio = 0 let blockRatioDetail: [number, number] = [0, 1] @@ -61,7 +64,7 @@ export function makeZanoSyncTracker(engine: SyncEngine): ZanoSyncTracker { lastTotalRatio = status.totalRatio } - const out: ZanoSyncTracker = { + const out: WeightedSyncTracker = { resetSync() { balanceRatio = 0 blockRatio = 0 diff --git a/src/zano/ZanoEngine.ts b/src/zano/ZanoEngine.ts index 6a29a3b31..74a07ae9a 100644 --- a/src/zano/ZanoEngine.ts +++ b/src/zano/ZanoEngine.ts @@ -29,7 +29,10 @@ import { } from '../common/lifecycleManager' import { MakeTxParams } from '../common/types' import { cleanTxLogs, safeParseInt } from '../common/utils' -import { makeZanoSyncTracker, ZanoSyncTracker } from './ZanoSyncTracker' +import { + makeWeightedSyncTracker, + WeightedSyncTracker +} from '../common/WeightedSyncTracker' import { ZanoTools } from './ZanoTools' import { asGetAliasDetailsResponse, @@ -49,7 +52,7 @@ import { export class ZanoEngine extends CurrencyEngine< ZanoTools, SafeZanoWalletInfo, - ZanoSyncTracker + WeightedSyncTracker > { networkInfo: ZanoNetworkInfo otherData!: ZanoWalletOtherData @@ -65,7 +68,7 @@ export class ZanoEngine extends CurrencyEngine< walletInfo: SafeZanoWalletInfo, opts: EdgeCurrencyEngineOptions ) { - super(env, tools, walletInfo, opts, makeZanoSyncTracker) + super(env, tools, walletInfo, opts, makeWeightedSyncTracker) this.networkInfo = env.networkInfo this.unlockedBalanceMap = new Map() From afb01b014acc35d1ad5788f582d309f997bb29c6 Mon Sep 17 00:00:00 2001 From: Matthew Date: Fri, 13 Feb 2026 11:34:37 -0800 Subject: [PATCH 02/10] Upgrade edge-core-js --- cli/cliContext.ts | 1 + package-lock.json | 14 +++++++------- package.json | 2 +- test/engine/engine.test.ts | 6 ++++-- test/eos/activation.test.ts | 3 ++- test/tezos/engine.test.ts | 3 ++- 6 files changed, 17 insertions(+), 12 deletions(-) diff --git a/cli/cliContext.ts b/cli/cliContext.ts index 22a1dd39c..d7899e7ac 100644 --- a/cli/cliContext.ts +++ b/cli/cliContext.ts @@ -164,6 +164,7 @@ export async function makeCliEngine( enabledTokenIds: settings.enabledTokens[pluginId] ?? [], log, userSettings: {}, + walletSettings: {}, walletLocalDisklet: navigateDisklet(disklet, pluginId), walletLocalEncryptedDisklet: navigateDisklet( disklet, diff --git a/package-lock.json b/package-lock.json index 6de17c356..98db0d14c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "edge-currency-accountbased", - "version": "4.82.1", + "version": "4.83.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "edge-currency-accountbased", - "version": "4.82.1", + "version": "4.83.0", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@bitcoinerlab/secp256k1": "^1.2.0", @@ -87,7 +87,7 @@ "chai": "^4.2.0", "clipanion": "^4.0.0-rc.2", "crypto-browserify": "^3.12.0", - "edge-core-js": "^2.42.0", + "edge-core-js": "2.46.0", "esbuild-loader": "^2.20.0", "eslint": "^8.19.0", "eslint-config-standard-kit": "0.15.1", @@ -8489,15 +8489,15 @@ } }, "node_modules/edge-core-js": { - "version": "2.44.0", - "resolved": "https://registry.npmjs.org/edge-core-js/-/edge-core-js-2.44.0.tgz", - "integrity": "sha512-w5XKlNudoITA8XicA3FPR2L26xt5d+Um9HYbbxPkFHDtk9lD5hO6SnmRlFa5ngZMoga6UfOCooa5aDHRWrt2lw==", + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/edge-core-js/-/edge-core-js-2.46.0.tgz", + "integrity": "sha512-Fi44+fKpqA88Xg1ja/lJNlZotl6BbdqmFNsmSwGYGufHuxQv0KB5zAS2dtQskbGRLYV6x9ntx3nqzkn+vCgT3A==", "dev": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { "@nymproject/mix-fetch": "^1.4.4", "aes-js": "^3.1.0", - "base-x": "^4.0.0", + "base-x": "^4.0.1", "biggystring": "^4.2.3", "cleaners": "^0.3.17", "currency-codes": "^1.5.1", diff --git a/package.json b/package.json index 042dd5f88..ca511c537 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "chai": "^4.2.0", "clipanion": "^4.0.0-rc.2", "crypto-browserify": "^3.12.0", - "edge-core-js": "^2.42.0", + "edge-core-js": "2.46.0", "esbuild-loader": "^2.20.0", "eslint": "^8.19.0", "eslint-config-standard-kit": "0.15.1", diff --git a/test/engine/engine.test.ts b/test/engine/engine.test.ts index 72d510b54..4417aed8f 100644 --- a/test/engine/engine.test.ts +++ b/test/engine/engine.test.ts @@ -102,7 +102,8 @@ describe('Engine', function () { const currencyEngineOptions: EdgeCurrencyEngineOptions = { callbacks, log: fakeLog, - userSettings: undefined, + userSettings: {}, + walletSettings: {}, walletLocalDisklet, walletLocalEncryptedDisklet: walletLocalDisklet, customTokens: {}, @@ -292,7 +293,8 @@ describe('Engine', function () { const currencyEngineOptions: EdgeCurrencyEngineOptions = { callbacks, log: fakeLog, - userSettings: undefined, + userSettings: {}, + walletSettings: {}, walletLocalDisklet, walletLocalEncryptedDisklet: walletLocalDisklet, customTokens: {}, diff --git a/test/eos/activation.test.ts b/test/eos/activation.test.ts index d637d4d4d..22f1e5cc3 100644 --- a/test/eos/activation.test.ts +++ b/test/eos/activation.test.ts @@ -87,7 +87,8 @@ describe(`EOS activation`, function () { const currencyEngineOptions: EdgeCurrencyEngineOptions = { callbacks, log: fakeLog, - userSettings: undefined, + userSettings: {}, + walletSettings: {}, walletLocalDisklet, walletLocalEncryptedDisklet: walletLocalDisklet, customTokens: {}, diff --git a/test/tezos/engine.test.ts b/test/tezos/engine.test.ts index 352fb8e6c..bf7d67db7 100644 --- a/test/tezos/engine.test.ts +++ b/test/tezos/engine.test.ts @@ -83,7 +83,8 @@ describe(`Tezos engine`, function () { const currencyEngineOptions: EdgeCurrencyEngineOptions = { callbacks, log: fakeLog, - userSettings: undefined, + userSettings: {}, + walletSettings: {}, walletLocalDisklet, walletLocalEncryptedDisklet: walletLocalDisklet, customTokens: {}, From 810c1710a963ff06e4b13b19be35183921779749 Mon Sep 17 00:00:00 2001 From: peachbits Date: Mon, 9 Feb 2026 23:59:20 -0800 Subject: [PATCH 03/10] Stub monero plugin with skeleton files --- src/index.ts | 2 + src/monero/MoneroEngine.ts | 71 ++++++++++++++++++++++++ src/monero/MoneroTools.ts | 90 ++++++++++++++++++++++++++++++ src/monero/moneroInfo.ts | 61 ++++++++++++++++++++ src/monero/moneroTypes.ts | 111 +++++++++++++++++++++++++++++++++++++ 5 files changed, 335 insertions(+) create mode 100644 src/monero/MoneroEngine.ts create mode 100644 src/monero/MoneroTools.ts create mode 100644 src/monero/moneroInfo.ts create mode 100644 src/monero/moneroTypes.ts diff --git a/src/index.ts b/src/index.ts index 2e6a1e8fd..fd6055a85 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ import { calibration } from './filecoin/calibrationInfo' import { filecoin } from './filecoin/filecoinInfo' import { fio } from './fio/fioInfo' import { hedera } from './hedera/hederaInfo' +import { monero } from './monero/moneroInfo' import { piratechain } from './piratechain/piratechainInfo' import { liberland } from './polkadot/info/liberlandInfo' import { liberlandtestnet } from './polkadot/info/liberlandTestnetInfo' @@ -42,6 +43,7 @@ const plugins = { hedera, liberland, liberlandtestnet, + monero, piratechain, polkadot, ripple, diff --git a/src/monero/MoneroEngine.ts b/src/monero/MoneroEngine.ts new file mode 100644 index 000000000..98e6824a8 --- /dev/null +++ b/src/monero/MoneroEngine.ts @@ -0,0 +1,71 @@ +import { + EdgeCurrencyEngine, + EdgeCurrencyEngineOptions, + EdgeEnginePrivateKeyOptions, + EdgeSpendInfo, + EdgeTransaction, + EdgeWalletInfo, + JsonObject +} from 'edge-core-js/types' + +import { CurrencyEngine } from '../common/CurrencyEngine' +import { PluginEnvironment } from '../common/innerPlugin' +import { + makeWeightedSyncTracker, + WeightedSyncTracker +} from '../common/WeightedSyncTracker' +import { MoneroTools } from './MoneroTools' +import { + asSafeMoneroWalletInfo, + MoneroNetworkInfo, + SafeMoneroWalletInfo +} from './moneroTypes' + +export class MoneroEngine extends CurrencyEngine< + MoneroTools, + SafeMoneroWalletInfo, + WeightedSyncTracker +> { + setOtherData(_raw: unknown): void { + // Stub: no-op + } + + async syncNetwork(_opts: EdgeEnginePrivateKeyOptions): Promise { + return 1000 + } + + async makeSpend(_edgeSpendInfo: EdgeSpendInfo): Promise { + throw new Error('Not implemented') + } + + async signTx( + _edgeTransaction: EdgeTransaction, + _privateKeys: JsonObject + ): Promise { + throw new Error('Not implemented') + } + + async broadcastTx( + _edgeTransaction: EdgeTransaction + ): Promise { + throw new Error('Not implemented') + } +} + +export async function makeCurrencyEngine( + env: PluginEnvironment, + tools: MoneroTools, + walletInfo: EdgeWalletInfo, + opts: EdgeCurrencyEngineOptions +): Promise { + const safeWalletInfo = asSafeMoneroWalletInfo(walletInfo) + const engine = new MoneroEngine( + env, + tools, + safeWalletInfo, + opts, + makeWeightedSyncTracker + ) + await engine.loadEngine() + return engine +} diff --git a/src/monero/MoneroTools.ts b/src/monero/MoneroTools.ts new file mode 100644 index 000000000..94352e12d --- /dev/null +++ b/src/monero/MoneroTools.ts @@ -0,0 +1,90 @@ +import { + EdgeCurrencyInfo, + EdgeCurrencyTools, + EdgeEncodeUri, + EdgeIo, + EdgeParsedUri, + EdgeWalletInfo, + JsonObject +} from 'edge-core-js/types' + +import { PluginEnvironment } from '../common/innerPlugin' +import { mergeDeeply } from '../common/utils' +import { MoneroNetworkInfo } from './moneroTypes' + +export class MoneroTools implements EdgeCurrencyTools { + io: EdgeIo + currencyInfo: EdgeCurrencyInfo + + constructor(env: PluginEnvironment) { + const { currencyInfo, io } = env + this.io = io + this.currencyInfo = currencyInfo + // Stub: no CppBridge yet + } + + async createPrivateKey( + _walletType: string, + _opts?: JsonObject + ): Promise { + throw new Error('Not implemented') + } + + async derivePublicKey(_walletInfo: EdgeWalletInfo): Promise { + throw new Error('Not implemented') + } + + async getDisplayPrivateKey( + _privateWalletInfo: EdgeWalletInfo + ): Promise { + throw new Error('Not implemented') + } + + async getDisplayPublicKey( + _publicWalletInfo: EdgeWalletInfo + ): Promise { + throw new Error('Not implemented') + } + + async importPrivateKey( + _input: string, + _opts?: JsonObject + ): Promise { + throw new Error('Not implemented') + } + + async isValidAddress(_address: string): Promise { + throw new Error('Not implemented') + } + + async parseUri( + _uri: string, + _currencyCode?: string, + _customTokens?: unknown + ): Promise { + throw new Error('Not implemented') + } + + async encodeUri( + _obj: EdgeEncodeUri, + _customTokens?: unknown + ): Promise { + throw new Error('Not implemented') + } +} + +export async function makeCurrencyTools( + env: PluginEnvironment +): Promise { + return new MoneroTools(env) +} + +export async function updateInfoPayload( + env: PluginEnvironment, + infoPayload: JsonObject +): Promise { + const { ...networkInfo } = infoPayload + env.networkInfo = mergeDeeply(env.networkInfo, networkInfo) +} + +export { makeCurrencyEngine } from './MoneroEngine' diff --git a/src/monero/moneroInfo.ts b/src/monero/moneroInfo.ts new file mode 100644 index 000000000..357bc1739 --- /dev/null +++ b/src/monero/moneroInfo.ts @@ -0,0 +1,61 @@ +import { EdgeCurrencyInfo, JsonObject } from 'edge-core-js/types' + +import { makeOuterPlugin } from '../common/innerPlugin' +import type { MoneroTools } from './MoneroTools' +import { + EDGE_MONERO_LWS_SERVER, + MoneroNetworkInfo, + MoneroUserSettings +} from './moneroTypes' + +const networkInfo: MoneroNetworkInfo = { + networkType: 0 +} + +const defaultSettings: MoneroUserSettings = { + enableCustomServers: false, + moneroLightwalletServer: EDGE_MONERO_LWS_SERVER +} + +export const currencyInfo: EdgeCurrencyInfo = { + currencyCode: 'XMR', + displayName: 'Monero', + pluginId: 'monero', + requiredConfirmations: 10, + walletType: 'wallet:monero', + + addressExplorer: 'https://xmrchain.net/search?value=%s', + transactionExplorer: + 'https://blockchair.com/monero/transaction/%s?from=edgeapp', + + denominations: [ + { + name: 'XMR', + multiplier: '1000000000000', + symbol: 'ɱ' + } + ], + + defaultSettings, + + unsafeSyncNetwork: true, + chainDisplayName: 'Monero', + assetDisplayName: 'Monero' +} + +export const monero = makeOuterPlugin< + MoneroNetworkInfo, + MoneroTools, + JsonObject +>({ + currencyInfo, + asInfoPayload: payload => payload, + networkInfo, + + async getInnerPlugin() { + return await import( + /* webpackChunkName: "monero" */ + './MoneroTools' + ) + } +}) diff --git a/src/monero/moneroTypes.ts b/src/monero/moneroTypes.ts new file mode 100644 index 000000000..fbd150969 --- /dev/null +++ b/src/monero/moneroTypes.ts @@ -0,0 +1,111 @@ +import { + asBoolean, + asCodec, + asMaybe, + asObject, + asOptional, + asString, + Cleaner +} from 'cleaners' + +export const EDGE_MONERO_LWS_SERVER = 'https://monerolws1.edge.app' + +export const asMoneroInitOptions = asObject({ + edgeApiKey: asOptional(asString, '') +}) +export type MoneroInitOptions = ReturnType + +export interface MoneroNetworkInfo { + networkType: number +} + +export const asMoneroUserSettings = asObject({ + enableCustomServers: asMaybe(asBoolean, false), + moneroLightwalletServer: asMaybe(asString, EDGE_MONERO_LWS_SERVER) +}) +export type MoneroUserSettings = ReturnType + +export interface MoneroPrivateKeys { + moneroKey: string + moneroSpendKeyPrivate: string + moneroSpendKeyPublic: string +} + +export const asMoneroPrivateKeys = ( + pluginId: string +): Cleaner => { + const asKeys = asObject({ + [`${pluginId}Key`]: asString, + [`${pluginId}SpendKeyPrivate`]: asString, + [`${pluginId}SpendKeyPublic`]: asString + }) + + return asCodec( + raw => { + const clean = asKeys(raw) + return { + moneroKey: clean[`${pluginId}Key`], + moneroSpendKeyPrivate: clean[`${pluginId}SpendKeyPrivate`], + moneroSpendKeyPublic: clean[`${pluginId}SpendKeyPublic`] + } + }, + clean => ({ + [`${pluginId}Key`]: clean.moneroKey, + [`${pluginId}SpendKeyPrivate`]: clean.moneroSpendKeyPrivate, + [`${pluginId}SpendKeyPublic`]: clean.moneroSpendKeyPublic + }) + ) +} + +const asMoneroPublicKeysRaw = asObject({ + moneroAddress: asString, + moneroViewKeyPrivate: asString, + moneroViewKeyPublic: asString, + moneroSpendKeyPublic: asString +}) + +interface MoneroPublicKeys { + publicKey: string + moneroAddress: string + moneroViewKeyPrivate: string + moneroViewKeyPublic: string + moneroSpendKeyPublic: string +} + +const asMoneroPublicKeys: Cleaner = asCodec( + (raw): MoneroPublicKeys => { + const clean = asMoneroPublicKeysRaw(raw) + return { + ...clean, + publicKey: clean.moneroAddress + } + }, + (clean): ReturnType => ({ + moneroAddress: clean.moneroAddress, + moneroViewKeyPrivate: clean.moneroViewKeyPrivate, + moneroViewKeyPublic: clean.moneroViewKeyPublic, + moneroSpendKeyPublic: clean.moneroSpendKeyPublic + }) +) + +export interface SafeMoneroWalletInfo { + id: string + type: string + keys: MoneroPublicKeys +} + +export const asSafeMoneroWalletInfo: Cleaner = asCodec( + (raw): SafeMoneroWalletInfo => { + const obj = asObject({ + id: asString, + type: asString, + keys: asMoneroPublicKeys + })(raw) + return obj + }, + clean => ({ + id: clean.id, + type: clean.type, + keys: asMoneroPublicKeys(clean.keys) + }) +) From 49e8cbc8c85c6f7fb0c1ebbb012519cdc8677ddb Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 12 Feb 2026 13:52:27 -0800 Subject: [PATCH 04/10] Add react-native-monero package and IO bridge --- package-lock.json | 15 +++++++++++++++ package.json | 4 ++++ rn-monero.d.ts | 3 +++ rn-monero.js | 1 + src/declare-modules.d.ts | 9 +++++++++ src/monero/moneroInfo.ts | 2 +- src/monero/moneroIo.ts | 30 ++++++++++++++++++++++++++++++ src/monero/moneroTypes.ts | 20 +++++++++++++++++++- test/builtinTokens.test.ts | 1 + 9 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 rn-monero.d.ts create mode 100644 rn-monero.js create mode 100644 src/monero/moneroIo.ts diff --git a/package-lock.json b/package-lock.json index 98db0d14c..233dd9e7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -111,6 +111,7 @@ "prettier": "^2.2.0", "process": "^0.11.10", "querystring": "^0.2.1", + "react-native-monero": "0.1.0", "react-native-piratechain": "0.5.0", "react-native-zano": "^0.2.7", "react-native-zcash": "0.10.1", @@ -128,6 +129,7 @@ "webpack-dev-server": "^5.2.4" }, "peerDependencies": { + "react-native-monero": "^0.1.0", "react-native-piratechain": "v0.5.0", "react-native-zano": "^0.2.7", "react-native-zcash": "^0.10.1" @@ -15557,6 +15559,19 @@ "react-native": ">=0.81" } }, + "node_modules/react-native-monero": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/react-native-monero/-/react-native-monero-0.1.0.tgz", + "integrity": "sha512-lyKYOtN1tVkEXD3/gvfYj8bQ4d2ZYOQ8+hgiT6i4UroYNaPZjvs6yKXTrRbIXbJrApCYPxQgNyGgdkT/Rk46wQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "cleaners": "^0.3.17" + }, + "peerDependencies": { + "react-native": ">=0.47.0 <1.0.0" + } + }, "node_modules/react-native-piratechain": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/react-native-piratechain/-/react-native-piratechain-0.5.0.tgz", diff --git a/package.json b/package.json index ca511c537..b60e247a3 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,8 @@ "/LICENSE", "/package.json", "/README.md", + "/rn-monero.d.ts", + "/rn-monero.js", "/rn-piratechain.d.ts", "/rn-piratechain.js", "/rn-zano.d.ts", @@ -165,6 +167,7 @@ "prettier": "^2.2.0", "process": "^0.11.10", "querystring": "^0.2.1", + "react-native-monero": "0.1.0", "react-native-piratechain": "0.5.0", "react-native-zano": "^0.2.7", "react-native-zcash": "0.10.1", @@ -182,6 +185,7 @@ "webpack-dev-server": "^5.2.4" }, "peerDependencies": { + "react-native-monero": "^0.1.0", "react-native-piratechain": "v0.5.0", "react-native-zano": "^0.2.7", "react-native-zcash": "^0.10.1" diff --git a/rn-monero.d.ts b/rn-monero.d.ts new file mode 100644 index 000000000..b21a66e2c --- /dev/null +++ b/rn-monero.d.ts @@ -0,0 +1,3 @@ +import type { MoneroIo } from './src/monero/moneroTypes' + +export function makeMoneroIo(): MoneroIo diff --git a/rn-monero.js b/rn-monero.js new file mode 100644 index 000000000..7e5ef3e03 --- /dev/null +++ b/rn-monero.js @@ -0,0 +1 @@ +export { makeMoneroIo } from './lib/monero/moneroIo' diff --git a/src/declare-modules.d.ts b/src/declare-modules.d.ts index 31b1be050..f88082b86 100644 --- a/src/declare-modules.d.ts +++ b/src/declare-modules.d.ts @@ -47,8 +47,17 @@ declare module 'react-native' { sourceUri: string } } + MoneroLwsfModule: NativeMoneroModule ZanoModule: NativeZanoModule } + + export class NativeEventEmitter { + constructor(nativeModule?: any) + addListener( + eventType: string, + listener: (event: any) => void + ): { remove: () => void } + } } declare module 'tronweb' { diff --git a/src/monero/moneroInfo.ts b/src/monero/moneroInfo.ts index 357bc1739..7223028f8 100644 --- a/src/monero/moneroInfo.ts +++ b/src/monero/moneroInfo.ts @@ -9,7 +9,7 @@ import { } from './moneroTypes' const networkInfo: MoneroNetworkInfo = { - networkType: 0 + networkType: 'MAINNET' } const defaultSettings: MoneroUserSettings = { diff --git a/src/monero/moneroIo.ts b/src/monero/moneroIo.ts new file mode 100644 index 000000000..07adb0d9f --- /dev/null +++ b/src/monero/moneroIo.ts @@ -0,0 +1,30 @@ +import { NativeEventEmitter, NativeModules } from 'react-native' +import type { WalletEventData } from 'react-native-monero' +import { bridgifyObject, emit, onMethod } from 'yaob' + +import type { MoneroIo } from './moneroTypes' + +export function makeMoneroIo(): MoneroIo { + const nativeModule = NativeModules.MoneroLwsfModule + + const io: MoneroIo = bridgifyObject({ + on: onMethod, + async callMonero(name: string, jsonArguments: string[]): Promise { + return nativeModule.callMonero(name, jsonArguments) + }, + get methodNames(): string[] { + return nativeModule.methodNames + }, + get documentDirectory(): string { + return nativeModule.documentDirectory + } + }) + + // Forward native wallet events through the yaob bridge + const emitter = new NativeEventEmitter(nativeModule) + emitter.addListener('MoneroWalletEvent', (event: WalletEventData) => { + emit(io, 'walletEvent', event) + }) + + return io +} diff --git a/src/monero/moneroTypes.ts b/src/monero/moneroTypes.ts index fbd150969..aab13ba8e 100644 --- a/src/monero/moneroTypes.ts +++ b/src/monero/moneroTypes.ts @@ -7,6 +7,8 @@ import { asString, Cleaner } from 'cleaners' +import type { WalletEventData } from 'react-native-monero' +import type { Subscriber } from 'yaob' export const EDGE_MONERO_LWS_SERVER = 'https://monerolws1.edge.app' @@ -16,7 +18,7 @@ export const asMoneroInitOptions = asObject({ export type MoneroInitOptions = ReturnType export interface MoneroNetworkInfo { - networkType: number + networkType: 'MAINNET' | 'TESTNET' | 'STAGENET' } export const asMoneroUserSettings = asObject({ @@ -109,3 +111,19 @@ export const asSafeMoneroWalletInfo: Cleaner = asCodec( keys: asMoneroPublicKeys(clean.keys) }) ) + +// --- yaob-compatible IO interface for bridging events across webview --- + +export interface MoneroWalletEvents { + walletEvent: WalletEventData +} + +export interface MoneroIo { + on: Subscriber + readonly callMonero: ( + name: string, + jsonArguments: string[] + ) => Promise + readonly methodNames: string[] + readonly documentDirectory: string +} diff --git a/test/builtinTokens.test.ts b/test/builtinTokens.test.ts index cde42249f..260517ff7 100644 --- a/test/builtinTokens.test.ts +++ b/test/builtinTokens.test.ts @@ -17,6 +17,7 @@ const fakePluginOptions: EdgeCorePluginOptions = { piratechain: {}, zcash: {} }, + monero: {}, zano: {} }, pluginDisklet: fakeIo.disklet From 9e294a2850cd43fb87c68bacc6efe7cd696b6975 Mon Sep 17 00:00:00 2001 From: peachbits Date: Mon, 16 Feb 2026 16:21:16 -0800 Subject: [PATCH 05/10] Add tools and engine Parity with edge-currency-monero --- src/monero/MoneroEngine.ts | 709 ++++++++++++++++++++++- src/monero/MoneroTools.ts | 264 ++++++++- src/monero/moneroInfo.ts | 1 + src/monero/moneroTypes.ts | 43 +- test/plugin/moneroTools.parseUri.test.ts | 45 ++ 5 files changed, 1011 insertions(+), 51 deletions(-) create mode 100644 test/plugin/moneroTools.parseUri.test.ts diff --git a/src/monero/MoneroEngine.ts b/src/monero/MoneroEngine.ts index 98e6824a8..de8e218a3 100644 --- a/src/monero/MoneroEngine.ts +++ b/src/monero/MoneroEngine.ts @@ -1,54 +1,727 @@ +import { add, eq, gt, lt, mul, sub } from 'biggystring' +import { asMaybe } from 'cleaners' import { EdgeCurrencyEngine, EdgeCurrencyEngineOptions, EdgeEnginePrivateKeyOptions, + EdgeMemo, EdgeSpendInfo, EdgeTransaction, EdgeWalletInfo, - JsonObject + InsufficientFundsError, + JsonObject, + NoAmountSpecifiedError, + PendingFundsError } from 'edge-core-js/types' +import type { TransactionDirection } from 'react-native-monero' +import { base64, base64url } from 'rfc4648' import { CurrencyEngine } from '../common/CurrencyEngine' import { PluginEnvironment } from '../common/innerPlugin' +import { + LifecycleManager, + makeLifecycleManager +} from '../common/lifecycleManager' +import { cleanTxLogs, matchJson } from '../common/utils' import { makeWeightedSyncTracker, WeightedSyncTracker } from '../common/WeightedSyncTracker' import { MoneroTools } from './MoneroTools' import { + AddressInfoResponse, + asAddressInfoResponse, + asLoginResponse, + asMoneroInitOptions, + asMoneroPrivateKeys, + asMoneroUserSettings, + asMoneroWalletOtherData, asSafeMoneroWalletInfo, + LoginResponse, + MoneroInitOptions, MoneroNetworkInfo, - SafeMoneroWalletInfo + MoneroPrivateKeys, + MoneroUserSettings, + MoneroWalletOtherData, + SafeMoneroWalletInfo, + translateFee } from './moneroTypes' +// Poll intervals (ms) returned by syncNetwork: +const SYNC_POLL_MS = 1000 // actively syncing / backfilling +const SYNCED_POLL_MS = 20000 // caught up to chain tip +const ERROR_POLL_MS = 5000 // back off after a sync error + +/** + * Converts an Edge walletId (standard base64) into the form the native monero + * layer expects. The native code embeds the id in a filesystem path and rejects + * any character outside [A-Za-z0-9_-], so we re-encode as base64url and strip + * the '=' padding. + */ +const asNativeWalletId = (walletId: string): string => + base64url.stringify(base64.parse(walletId), { pad: false }) + export class MoneroEngine extends CurrencyEngine< MoneroTools, SafeMoneroWalletInfo, WeightedSyncTracker > { - setOtherData(_raw: unknown): void { - // Stub: no-op + networkInfo: MoneroNetworkInfo + currentSettings: MoneroUserSettings + otherData!: MoneroWalletOtherData + initOptions: MoneroInitOptions + unlockedBalance: string + private readonly nativeWalletId: LifecycleManager + private sendKeysToNative?: (keys: MoneroPrivateKeys) => void + private syncStartHeight: number | undefined + private txSortOrder: 'asc' | 'desc' = 'asc' + private unsubscribeWalletEvent?: () => void + private abortKeysWait?: () => void + + constructor( + env: PluginEnvironment, + tools: MoneroTools, + walletInfo: SafeMoneroWalletInfo, + initOptions: JsonObject, + opts: EdgeCurrencyEngineOptions + ) { + super(env, tools, walletInfo, opts, makeWeightedSyncTracker) + this.networkInfo = env.networkInfo + this.initOptions = asMoneroInitOptions(initOptions) + + this.unlockedBalance = '0' + + this.currentSettings = asMoneroUserSettings(opts.userSettings) + + // Singleton promise resolved once by the first syncNetwork call. + // Stays resolved across restarts so onStart gets keys immediately. + const keysPromise = new Promise(resolve => { + this.sendKeysToNative = resolve + }) + + this.nativeWalletId = makeLifecycleManager({ + onStart: async () => { + let abortKeysWait: (() => void) | undefined + const abortPromise = new Promise((resolve, reject) => { + abortKeysWait = () => reject(new Error('Engine stopped')) + }) + this.abortKeysWait = abortKeysWait + const keys = await Promise.race([keysPromise, abortPromise]) + this.abortKeysWait = undefined + const base64UrlWalletId = asNativeWalletId(this.walletId) + + const defaults = asMoneroUserSettings({}) + const daemonAddress = this.currentSettings.enableCustomServers + ? this.currentSettings.moneroLightwalletServer + : defaults.moneroLightwalletServer + + let birthdayHeight: number + try { + if (daemonAddress === this.networkInfo.edgeLwsServer) { + await this.tools.cppBridge.setLwsApiKey(this.initOptions.edgeApiKey) + + const loginResult = await this.loginToLwsServer( + daemonAddress, + this.walletInfo.keys.moneroAddress, + this.walletInfo.keys.moneroViewKeyPrivate + ) + + if (loginResult.start_height != null) { + birthdayHeight = loginResult.start_height + } else { + const addressInfo = await this.getAddressInfo( + daemonAddress, + this.walletInfo.keys.moneroAddress, + this.walletInfo.keys.moneroViewKeyPrivate + ) + birthdayHeight = addressInfo.start_height + } + } else { + await this.tools.cppBridge.setLwsApiKey('') + + const addressInfo = await this.getAddressInfo( + daemonAddress, + this.walletInfo.keys.moneroAddress, + this.walletInfo.keys.moneroViewKeyPrivate + ) + birthdayHeight = addressInfo.start_height + } + + await this.tools.cppBridge.openWallet( + base64UrlWalletId, + 'lws', + keys.moneroKey, + base64url.stringify(base64.parse(keys.dataKey)), + this.networkInfo.networkType, + birthdayHeight, + daemonAddress + ) + + // Subscribe to native wallet events for immediate tx detection + const unsubscribeWalletEvent = this.tools.moneroIo.on( + 'walletEvent', + event => { + if (event.walletId !== base64UrlWalletId) return + if (event.eventName !== 'pendingTransactionReceived') return + + this.queryTransactions(base64UrlWalletId).catch(err => + this.log.error( + `Event-triggered queryTransactions error: ${String(err)}` + ) + ) + } + ) + this.unsubscribeWalletEvent = unsubscribeWalletEvent + + return base64UrlWalletId + } catch (error: unknown) { + if (!(error instanceof Error)) throw error + this.log.error(`Failed to open wallet: ${error.message}`) + throw error + } + }, + + onStop: async (nativeWalletId: string) => { + if (this.unsubscribeWalletEvent != null) { + this.unsubscribeWalletEvent() + this.unsubscribeWalletEvent = undefined + } + try { + await this.tools.cppBridge.closeWallet(nativeWalletId) + this.log(`Wallet closed: ${nativeWalletId}`) + } catch (error: unknown) { + this.log.error(`Error closing wallet: ${String(error)}`) + } + }, + + onError: error => { + this.log.error('Monero lifecycle error:', String(error)) + } + }) } - async syncNetwork(_opts: EdgeEnginePrivateKeyOptions): Promise { - return 1000 + setOtherData(raw: unknown): void { + this.otherData = asMoneroWalletOtherData(raw) } - async makeSpend(_edgeSpendInfo: EdgeSpendInfo): Promise { - throw new Error('Not implemented') + async loginToLwsServer( + serverUrl: string, + address: string, + viewKey: string + ): Promise { + const url = `${serverUrl}/login` + const response = await this.tools.io.fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json' + }, + body: JSON.stringify({ + address, + api_key: this.initOptions.edgeApiKey, + create_account: true, + generated_locally: true, + view_key: viewKey + }) + }) + if (!response.ok) { + const text = await response.text() + throw new Error(`LWS login failed with ${response.status}: ${text}`) + } + const json = await response.json() + return asLoginResponse(json) + } + + async getAddressInfo( + serverUrl: string, + address: string, + viewKey: string + ): Promise { + const url = `${serverUrl}/get_address_info` + const response = await this.tools.io.fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json' + }, + body: JSON.stringify({ + address, + api_key: this.initOptions.edgeApiKey, + view_key: viewKey + }) + }) + if (!response.ok) { + const text = await response.text() + throw new Error( + `LWS get_address_info failed with ${response.status}: ${text}` + ) + } + const json = await response.json() + return asAddressInfoResponse(json) + } + + async syncNetwork(opts: EdgeEnginePrivateKeyOptions): Promise { + if (!this.engineOn) return SYNC_POLL_MS + + if (this.sendKeysToNative != null) { + this.sendKeysToNative( + asMoneroPrivateKeys(this.currencyInfo.pluginId)(opts.privateKeys) + ) + this.sendKeysToNative = undefined + } + + const nativeWalletId = await this.nativeWalletId.get() + if (nativeWalletId == null) { + return SYNC_POLL_MS + } + + try { + const status = await this.tools.cppBridge.getWalletStatus(nativeWalletId) + if (status.networkHeight === 0) { + return SYNC_POLL_MS + } + + this.updateBlockHeight(status.networkHeight) + + // Capture the first reported synced height as our baseline for + // progress tracking. This is reset when the wallet restarts + // (settings change, resync, daemon change). + if (this.syncStartHeight == null) { + this.syncStartHeight = status.syncedHeight + } + + const isSynced = status.syncedHeight >= status.networkHeight - 1 + + if (isSynced) { + this.syncTracker.updateBlockRatio( + 1, + status.syncedHeight, + status.networkHeight + ) + + const balance = status.balance + this.unlockedBalance = status.unlockedBalance + this.updateBalance(null, balance) + this.syncTracker.updateBalanceRatio(1) + + await this.queryTransactions(nativeWalletId) + + // Only report history as complete once the ascending backfill has + // ingested every page (it flips txSortOrder to 'desc' when done). + // While still backfilling, poll quickly to pull the next page. + if (this.txSortOrder === 'desc') { + this.syncTracker.updateHistoryRatio(1) + return SYNCED_POLL_MS + } + return SYNC_POLL_MS + } else { + const range = status.networkHeight - this.syncStartHeight + // Clamp to 0 so a reorg (syncedHeight < syncStartHeight) can't feed a + // negative ratio into the weighted sync tracker: + const ratio = + range > 0 + ? Math.max(0, (status.syncedHeight - this.syncStartHeight) / range) + : 0 + + this.syncTracker.updateBlockRatio( + ratio, + status.syncedHeight, + status.networkHeight + ) + return SYNC_POLL_MS + } + } catch (error: unknown) { + this.log.error(`syncNetwork error: ${String(error)}`) + return ERROR_POLL_MS + } + } + + private async queryTransactions(nativeWalletId: string): Promise { + const PAGE_SIZE = 50 + + try { + if (this.txSortOrder === 'asc') { + const shouldSendEvents = await this.queryTransactionsAsc( + nativeWalletId, + PAGE_SIZE + ) + if (!shouldSendEvents) { + return + } + this.sendTransactionEvents() + return + } + + await this.queryTransactionsDesc(nativeWalletId, PAGE_SIZE) + + this.sendTransactionEvents() + } catch (error: unknown) { + this.log.error(`queryTransactions error: ${String(error)}`) + } + } + + private async queryTransactionsAsc( + nativeWalletId: string, + pageSize: number + ): Promise { + const startPage = Math.floor( + this.otherData.processedTransactionCount / pageSize + ) + + const txPage = await this.tools.cppBridge.getAllTransactions( + nativeWalletId, + startPage, + pageSize, + 'asc' + ) + + if (txPage.totalCount === 0) { + // No history to backfill, so treat the ascending pass as complete: + this.txSortOrder = 'desc' + return false + } + + const onPageBoundary = + this.otherData.processedTransactionCount % pageSize === 0 + let foundKnown = this.otherData.mostRecentTxid == null || onPageBoundary + for (const tx of txPage.transactions) { + if (!foundKnown) { + if (tx.hash === this.otherData.mostRecentTxid) { + foundKnown = true + } + continue + } + this.processTransaction(tx) + this.otherData.mostRecentTxid = tx.hash + } + + this.otherData.processedTransactionCount = + startPage * pageSize + txPage.transactions.length + this.walletLocalDataDirty = true + + this.syncTracker.updateHistoryRatio( + this.otherData.processedTransactionCount / txPage.totalCount + ) + + if (this.otherData.processedTransactionCount >= txPage.totalCount) { + this.txSortOrder = 'desc' + } + + return true + } + + private async queryTransactionsDesc( + nativeWalletId: string, + pageSize: number + ): Promise { + let page = 0 + let foundKnownTx = false + let newestTxid: string | undefined + + while (!foundKnownTx) { + const txPage = await this.tools.cppBridge.getAllTransactions( + nativeWalletId, + page, + pageSize, + 'desc' + ) + + if (page === 0 && txPage.transactions.length > 0) { + newestTxid = txPage.transactions[0].hash + } + + for (const tx of txPage.transactions) { + if (tx.hash === this.otherData.mostRecentTxid) { + foundKnownTx = true + break + } + this.processTransaction(tx) + } + + if ( + foundKnownTx || + txPage.transactions.length < pageSize || + (page + 1) * pageSize >= txPage.totalCount + ) { + if ( + newestTxid != null && + newestTxid !== this.otherData.mostRecentTxid + ) { + this.otherData.mostRecentTxid = newestTxid + this.otherData.processedTransactionCount = txPage.totalCount + this.walletLocalDataDirty = true + } + break + } + + page++ + } + } + + private processTransaction(tx: { + hash: string + direction: TransactionDirection + isPending: boolean + isFailed: boolean + amount: string + fee: string + blockHeight: number + timestamp: number + paymentId: string + txKey?: string + }): void { + const memos: EdgeMemo[] = [] + + if ( + tx.paymentId != null && + tx.paymentId !== '' && + tx.paymentId !== '0000000000000000' // returned when there is no payment id + ) { + memos.push({ + memoName: 'payment id', + type: 'hex', + value: tx.paymentId + }) + } + + // TransactionDirection from react-native-monero: 0 = incoming, 1 = outgoing + const isReceive = tx.direction === 0 + const ourReceiveAddresses: string[] = isReceive + ? [this.walletInfo.keys.moneroAddress] + : [] + + let nativeAmount: string + const networkFee = tx.fee + + if (isReceive) { + nativeAmount = tx.amount + } else { + nativeAmount = `-${add(tx.amount, tx.fee)}` + } + + const blockHeight = tx.isPending ? 0 : tx.blockHeight + + const edgeTransaction: EdgeTransaction = { + blockHeight, + currencyCode: this.currencyInfo.currencyCode, + date: tx.timestamp, + isSend: !isReceive, + memos, + nativeAmount, + networkFee, + networkFees: [{ tokenId: null, nativeAmount: networkFee }], + otherParams: {}, + ourReceiveAddresses, + signedTx: '', + tokenId: null, + txid: tx.hash, + txSecret: tx.txKey, + walletId: this.walletId + } + + if (tx.isFailed) { + edgeTransaction.confirmations = 'failed' + } + + this.addTransaction(null, edgeTransaction) + } + + async killEngine(): Promise { + this.abortKeysWait?.() + await this.nativeWalletId.stop() + this.syncStartHeight = undefined + this.unlockedBalance = '0' + this.txSortOrder = 'asc' + this.syncTracker.resetSync() + await super.killEngine() + } + + async resyncBlockchain(): Promise { + await this.killEngine() + await this.clearBlockchainCache() + await this.tools.cppBridge.deleteWallet( + asNativeWalletId(this.walletId), + 'lws' + ) + await this.startEngine() + } + + async changeUserSettings(userSettings: JsonObject): Promise { + const newSettings = asMaybe(asMoneroUserSettings)(userSettings) + if (newSettings == null || matchJson(this.currentSettings, newSettings)) { + return + } + + this.currentSettings = newSettings + await this.killEngine() + await this.startEngine() + } + + async getMaxSpendable(edgeSpendInfo: EdgeSpendInfo): Promise { + const { tokenId } = edgeSpendInfo + + if (tokenId != null) { + throw new Error('Monero does not support tokens') + } + + const nativeWalletId = await this.nativeWalletId.get() + if (nativeWalletId == null) { + throw new Error('Wallet not ready') + } + + const [spendTarget] = edgeSpendInfo.spendTargets + if (spendTarget?.publicAddress == null) { + throw new Error('Missing destination address') + } + + try { + const result = await this.tools.cppBridge.createTransaction( + nativeWalletId, + [{ address: spendTarget.publicAddress, amount: '0' }], + translateFee(edgeSpendInfo.networkFeeOption) + ) + + const maxSpendable = sub(this.unlockedBalance, result.fee) + if (lt(maxSpendable, '0')) { + return '0' + } + return maxSpendable + } catch (error: unknown) { + this.log.error(`getMaxSpendable error: ${String(error)}`) + throw error + } + } + + async makeSpend(edgeSpendInfoIn: EdgeSpendInfo): Promise { + const { edgeSpendInfo, currencyCode } = this.makeSpendCheck(edgeSpendInfoIn) + const { memos = [], tokenId, networkFeeOption } = edgeSpendInfo + + if (tokenId != null) { + throw new Error('Monero does not support tokens') + } + + const nativeWalletId = await this.nativeWalletId.get() + if (nativeWalletId == null) { + throw new Error('Wallet not ready') + } + + const recipients: Array<{ address: string; amount: string }> = [] + let totalAmount = '0' + + for (const spendTarget of edgeSpendInfo.spendTargets) { + const { publicAddress, nativeAmount } = spendTarget + + if (publicAddress == null) { + throw new Error('Missing destination address') + } + if (nativeAmount == null || eq(nativeAmount, '0')) { + throw new NoAmountSpecifiedError() + } + + recipients.push({ + address: publicAddress, + amount: nativeAmount + }) + totalAmount = add(totalAmount, nativeAmount) + } + + const balance = this.getBalance({ tokenId: null }) + if (gt(totalAmount, balance)) { + throw new InsufficientFundsError({ tokenId: null }) + } + if (gt(totalAmount, this.unlockedBalance)) { + throw new PendingFundsError() + } + + const priority = translateFee(networkFeeOption) + + let txid: string + let signedTxHex: string + let networkFee: string + + try { + const result = await this.tools.cppBridge.createTransaction( + nativeWalletId, + recipients, + priority + ) + txid = result.txid + signedTxHex = result.signedTxHex + networkFee = result.fee + } catch (error: unknown) { + this.warn(`FAILURE makeSpend createTransaction: ${String(error)}`) + if (error instanceof Error) { + if (error.message.includes('not enough money')) { + throw new InsufficientFundsError({ tokenId: null }) + } + if (error.message.includes('pending')) { + throw new PendingFundsError() + } + } + throw error + } + + const totalWithFee = add(totalAmount, networkFee) + const txNativeAmount = mul(totalWithFee, '-1') + + const edgeTransaction: EdgeTransaction = { + blockHeight: 0, + currencyCode, + date: 0, + isSend: true, + memos, + nativeAmount: txNativeAmount, + networkFee, + networkFees: [{ tokenId: null, nativeAmount: networkFee }], + otherParams: { + recipients, + priority + }, + ourReceiveAddresses: [], + signedTx: signedTxHex, + tokenId: null, + txid, + walletId: this.walletId + } + + return edgeTransaction } async signTx( - _edgeTransaction: EdgeTransaction, + edgeTransaction: EdgeTransaction, _privateKeys: JsonObject ): Promise { - throw new Error('Not implemented') + if (edgeTransaction.txid.length !== 64) { + throw new Error('Invalid transaction: missing or malformed txid') + } + if (edgeTransaction.signedTx.length === 0) { + throw new Error('Invalid transaction: missing signed transaction data') + } + return edgeTransaction } async broadcastTx( - _edgeTransaction: EdgeTransaction + edgeTransaction: EdgeTransaction ): Promise { - throw new Error('Not implemented') + const nativeWalletId = await this.nativeWalletId.get() + if (nativeWalletId == null) { + throw new Error('Wallet not ready') + } + + try { + await this.tools.cppBridge.broadcastTransaction( + nativeWalletId, + edgeTransaction.signedTx + ) + + edgeTransaction.date = Date.now() / 1000 + + this.warn(`SUCCESS broadcastTx\n${cleanTxLogs(edgeTransaction)}`) + return edgeTransaction + } catch (error: unknown) { + this.warn(`FAILURE broadcastTx: ${String(error)}`) + throw error + } } } @@ -58,14 +731,12 @@ export async function makeCurrencyEngine( walletInfo: EdgeWalletInfo, opts: EdgeCurrencyEngineOptions ): Promise { + const { initOptions } = env + const safeWalletInfo = asSafeMoneroWalletInfo(walletInfo) - const engine = new MoneroEngine( - env, - tools, - safeWalletInfo, - opts, - makeWeightedSyncTracker - ) + const engine = new MoneroEngine(env, tools, safeWalletInfo, initOptions, opts) + await engine.loadEngine() + return engine } diff --git a/src/monero/MoneroTools.ts b/src/monero/MoneroTools.ts index 94352e12d..2eee5d3e4 100644 --- a/src/monero/MoneroTools.ts +++ b/src/monero/MoneroTools.ts @@ -1,75 +1,277 @@ +import { mul, toFixed } from 'biggystring' import { EdgeCurrencyInfo, EdgeCurrencyTools, EdgeEncodeUri, EdgeIo, + EdgeLog, + EdgeMetaToken, EdgeParsedUri, + EdgeTokenMap, EdgeWalletInfo, JsonObject } from 'edge-core-js/types' +import { CppBridge } from 'react-native-monero/lib/src/CppBridge' import { PluginEnvironment } from '../common/innerPlugin' -import { mergeDeeply } from '../common/utils' -import { MoneroNetworkInfo } from './moneroTypes' +import { parseUriCommon } from '../common/uriHelpers' +import { getLegacyDenomination, mergeDeeply } from '../common/utils' +import { + asMoneroPrivateKeys, + asSafeMoneroWalletInfo, + MoneroIo, + MoneroNetworkInfo +} from './moneroTypes' + +/** + * Thrown when a Monero payment is requested with a standalone payment id (a + * `tx_payment_id` URI parameter). Monero only delivers a payment id to the + * recipient when it is embedded in an integrated address: long ids are ignored + * by the receiving wallet and short ids cannot be sent on their own, so we + * reject rather than sending to the bare address without it. + */ +class UnsupportedPaymentIdError extends Error { + constructor() { + super( + 'Monero no longer supports separate payment IDs. Ask the recipient for an integrated address.' + ) + this.name = 'UnsupportedPaymentIdError' + Object.setPrototypeOf(this, UnsupportedPaymentIdError.prototype) + } +} export class MoneroTools implements EdgeCurrencyTools { + cppBridge: CppBridge + moneroIo: MoneroIo io: EdgeIo + log: EdgeLog + builtinTokens: EdgeTokenMap currencyInfo: EdgeCurrencyInfo + networkInfo: MoneroNetworkInfo constructor(env: PluginEnvironment) { - const { currencyInfo, io } = env + const { builtinTokens, currencyInfo, io, log, nativeIo, networkInfo } = env this.io = io + this.log = log this.currencyInfo = currencyInfo - // Stub: no CppBridge yet - } + this.builtinTokens = builtinTokens + this.networkInfo = networkInfo - async createPrivateKey( - _walletType: string, - _opts?: JsonObject - ): Promise { - throw new Error('Not implemented') - } - - async derivePublicKey(_walletInfo: EdgeWalletInfo): Promise { - throw new Error('Not implemented') + const moneroIo = nativeIo.monero as MoneroIo + if (moneroIo == null) throw new Error('Need monero native IO') + this.moneroIo = moneroIo + this.cppBridge = new CppBridge(moneroIo) } async getDisplayPrivateKey( - _privateWalletInfo: EdgeWalletInfo + privateWalletInfo: EdgeWalletInfo ): Promise { - throw new Error('Not implemented') + const { pluginId } = this.currencyInfo + const keys = asMoneroPrivateKeys(pluginId)(privateWalletInfo.keys) + return keys.moneroKey } - async getDisplayPublicKey( - _publicWalletInfo: EdgeWalletInfo - ): Promise { - throw new Error('Not implemented') + async getDisplayPublicKey(publicWalletInfo: EdgeWalletInfo): Promise { + const { keys } = asSafeMoneroWalletInfo(publicWalletInfo) + return keys.moneroViewKeyPrivate } async importPrivateKey( - _input: string, + input: string, _opts?: JsonObject ): Promise { - throw new Error('Not implemented') + const { pluginId } = this.currencyInfo + const { networkType } = this.networkInfo + const mnemonic = input.trim() + + const keys = await this.cppBridge.seedAndKeysFromMnemonic( + mnemonic, + networkType + ) + + return { + [`${pluginId}Key`]: mnemonic, + [`${pluginId}SpendKeyPrivate`]: keys.secretSpendKey, + [`${pluginId}SpendKeyPublic`]: keys.publicSpendKey + } + } + + async createPrivateKey(walletType: string): Promise { + if (walletType !== this.currencyInfo.walletType) { + throw new Error('InvalidWalletType') + } + const { networkType } = this.networkInfo + + const generatedWallet = await this.cppBridge.generateWallet(networkType) + + return await this.importPrivateKey(generatedWallet.mnemonic) + } + + async derivePublicKey(walletInfo: EdgeWalletInfo): Promise { + if (walletInfo.type !== this.currencyInfo.walletType) { + throw new Error('InvalidWalletType') + } + + const { pluginId } = this.currencyInfo + const { networkType } = this.networkInfo + + const moneroPrivateKeys = asMoneroPrivateKeys(pluginId)(walletInfo.keys) + const { moneroKey } = moneroPrivateKeys + + const derivedKeys = await this.cppBridge.seedAndKeysFromMnemonic( + moneroKey, + networkType + ) + + return { + moneroAddress: derivedKeys.address, + moneroViewKeyPrivate: derivedKeys.secretViewKey, + moneroViewKeyPublic: derivedKeys.publicViewKey, + moneroSpendKeyPublic: derivedKeys.publicSpendKey + } } - async isValidAddress(_address: string): Promise { - throw new Error('Not implemented') + async isValidAddress(address: string): Promise { + const { networkType } = this.networkInfo + return await this.cppBridge.isValidAddress(address, networkType) } async parseUri( - _uri: string, - _currencyCode?: string, - _customTokens?: unknown + uri: string, + currencyCode?: string, + customTokens?: EdgeMetaToken[] ): Promise { - throw new Error('Not implemented') + const { pluginId } = this.currencyInfo + const { networkType } = this.networkInfo + const networks = { [pluginId]: true, monero: true } + + if (uri.startsWith('monero:')) { + try { + const parsed = await this.cppBridge.parseUri(uri, networkType) + + const edgeParsedUri: EdgeParsedUri = { + publicAddress: parsed.address + } + + if (parsed.amount !== '0' && parsed.amount !== '') { + edgeParsedUri.nativeAmount = parsed.amount + edgeParsedUri.currencyCode = currencyCode ?? 'XMR' + } + + // A non-empty paymentId means the URI carried a standalone + // tx_payment_id; integrated addresses keep the id inside the address + // itself. Reject rather than silently sending to the bare address + // without it. + if (parsed.paymentId !== '') { + throw new UnsupportedPaymentIdError() + } + + if (parsed.recipientName !== '' || parsed.txDescription !== '') { + edgeParsedUri.metadata = {} + if (parsed.recipientName !== '') { + edgeParsedUri.metadata.name = parsed.recipientName + } + if (parsed.txDescription !== '') { + edgeParsedUri.metadata.notes = parsed.txDescription + } + } + + return edgeParsedUri + } catch (e) { + // A standalone payment id is an intentional rejection, not a parse + // failure, so surface it instead of retrying via the common parser: + if (e instanceof UnsupportedPaymentIdError) throw e + // Fall through to the common parser if native parsing fails: + this.log.warn( + `Native parseUri failed, using common parser: ${String(e)}` + ) + } + } + + const { parsedUri, edgeParsedUri } = await parseUriCommon({ + currencyInfo: this.currencyInfo, + uri, + networks, + builtinTokens: this.builtinTokens, + currencyCode: currencyCode ?? 'XMR', + customTokens + }) + + const address = edgeParsedUri.publicAddress ?? '' + + const isValid = await this.isValidAddress(address) + if (!isValid) { + throw new Error('InvalidPublicAddressError') + } + + const txAmount = parsedUri.query.tx_amount + if (txAmount != null && edgeParsedUri.nativeAmount == null) { + const denom = getLegacyDenomination( + currencyCode ?? 'XMR', + this.currencyInfo, + customTokens ?? [], + this.builtinTokens + ) + if (denom != null) { + let nativeAmount = mul(txAmount, denom.multiplier) + nativeAmount = toFixed(nativeAmount, 0, 0) + edgeParsedUri.nativeAmount = nativeAmount + edgeParsedUri.currencyCode = currencyCode ?? 'XMR' + } + } + + // Reject a standalone tx_payment_id for the same reason as the native + // branch above: Monero cannot deliver it without an integrated address. + if (parsedUri.query.tx_payment_id != null) { + throw new UnsupportedPaymentIdError() + } + + const recipientName = parsedUri.query.recipient_name + const txDescription = parsedUri.query.tx_description + if (recipientName != null || txDescription != null) { + edgeParsedUri.metadata = edgeParsedUri.metadata ?? {} + if (recipientName != null) { + edgeParsedUri.metadata.name = recipientName + } + if (txDescription != null) { + edgeParsedUri.metadata.notes = txDescription + } + } + + return edgeParsedUri } async encodeUri( - _obj: EdgeEncodeUri, - _customTokens?: unknown + obj: EdgeEncodeUri, + _customTokens: EdgeMetaToken[] = [] ): Promise { - throw new Error('Not implemented') + const { publicAddress, nativeAmount, label, message } = obj + const { networkType } = this.networkInfo + + if (publicAddress == null) { + throw new Error('InvalidPublicAddressError') + } + + const isValid = await this.isValidAddress(publicAddress) + if (!isValid) { + throw new Error('InvalidPublicAddressError') + } + + if (nativeAmount == null && label == null && message == null) { + return publicAddress + } + + const uri = await this.cppBridge.encodeUri( + { + address: publicAddress, + amount: nativeAmount ?? '0', + recipientName: label, + txDescription: message + }, + networkType + ) + + return uri } } diff --git a/src/monero/moneroInfo.ts b/src/monero/moneroInfo.ts index 7223028f8..4e3e85f92 100644 --- a/src/monero/moneroInfo.ts +++ b/src/monero/moneroInfo.ts @@ -9,6 +9,7 @@ import { } from './moneroTypes' const networkInfo: MoneroNetworkInfo = { + edgeLwsServer: EDGE_MONERO_LWS_SERVER, networkType: 'MAINNET' } diff --git a/src/monero/moneroTypes.ts b/src/monero/moneroTypes.ts index aab13ba8e..2d173971f 100644 --- a/src/monero/moneroTypes.ts +++ b/src/monero/moneroTypes.ts @@ -2,12 +2,16 @@ import { asBoolean, asCodec, asMaybe, + asNumber, asObject, asOptional, asString, Cleaner } from 'cleaners' -import type { WalletEventData } from 'react-native-monero' +import type { + TransactionPriority, + WalletEventData +} from 'react-native-monero' import type { Subscriber } from 'yaob' export const EDGE_MONERO_LWS_SERVER = 'https://monerolws1.edge.app' @@ -18,6 +22,7 @@ export const asMoneroInitOptions = asObject({ export type MoneroInitOptions = ReturnType export interface MoneroNetworkInfo { + edgeLwsServer: string networkType: 'MAINNET' | 'TESTNET' | 'STAGENET' } @@ -28,6 +33,7 @@ export const asMoneroUserSettings = asObject({ export type MoneroUserSettings = ReturnType export interface MoneroPrivateKeys { + dataKey: string moneroKey: string moneroSpendKeyPrivate: string moneroSpendKeyPublic: string @@ -37,6 +43,7 @@ export const asMoneroPrivateKeys = ( pluginId: string ): Cleaner => { const asKeys = asObject({ + dataKey: asString, [`${pluginId}Key`]: asString, [`${pluginId}SpendKeyPrivate`]: asString, [`${pluginId}SpendKeyPublic`]: asString @@ -46,12 +53,14 @@ export const asMoneroPrivateKeys = ( raw => { const clean = asKeys(raw) return { + dataKey: clean.dataKey, moneroKey: clean[`${pluginId}Key`], moneroSpendKeyPrivate: clean[`${pluginId}SpendKeyPrivate`], moneroSpendKeyPublic: clean[`${pluginId}SpendKeyPublic`] } }, clean => ({ + dataKey: clean.dataKey, [`${pluginId}Key`]: clean.moneroKey, [`${pluginId}SpendKeyPrivate`]: clean.moneroSpendKeyPrivate, [`${pluginId}SpendKeyPublic`]: clean.moneroSpendKeyPublic @@ -112,6 +121,38 @@ export const asSafeMoneroWalletInfo: Cleaner = asCodec( }) ) +export function translateFee(fee?: string): TransactionPriority { + // monerod priority levels: 1 = Low, 2 = Normal/Default, 3 = High + if (fee === 'low') return 1 + if (fee === 'high') return 3 + return 2 +} + +export const asMoneroWalletOtherData = asObject({ + processedTransactionCount: asMaybe(asNumber, 0), + mostRecentTxid: asMaybe(asString) +}) +export type MoneroWalletOtherData = ReturnType + +export const asLoginResponse = asObject({ + new_address: asBoolean, + generated_locally: asOptional(asBoolean), + start_height: asOptional(asNumber) +}) +export type LoginResponse = ReturnType + +export const asAddressInfoResponse = asObject({ + blockchain_height: asNumber, + locked_funds: asString, + scanned_block_height: asNumber, + scanned_height: asNumber, + start_height: asNumber, + total_received: asString, + total_sent: asString, + transaction_height: asNumber +}) +export type AddressInfoResponse = ReturnType + // --- yaob-compatible IO interface for bridging events across webview --- export interface MoneroWalletEvents { diff --git a/test/plugin/moneroTools.parseUri.test.ts b/test/plugin/moneroTools.parseUri.test.ts new file mode 100644 index 000000000..b21d676b5 --- /dev/null +++ b/test/plugin/moneroTools.parseUri.test.ts @@ -0,0 +1,45 @@ +import { assert } from 'chai' + +import { MoneroTools } from '../../src/monero/MoneroTools' + +// A real-looking standard mainnet address. parseUri is stubbed below, so the +// value only needs to round-trip through publicAddress. +const ADDRESS = + '44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A' + +// Build a MoneroTools instance with only the pieces parseUri touches, plus a +// cppBridge.parseUri stub that echoes back a fixed paymentId. +const makeTools = (paymentId: string): MoneroTools => + Object.assign(Object.create(MoneroTools.prototype), { + currencyInfo: { pluginId: 'monero' }, + networkInfo: { networkType: 'MAINNET' }, + builtinTokens: {}, + cppBridge: { + parseUri: async () => ({ + address: ADDRESS, + amount: '0', + paymentId, + recipientName: '', + txDescription: '' + }) + } + }) as unknown as MoneroTools + +describe('MoneroTools.parseUri payment id handling', () => { + it('accepts a monero: URI with no payment id', async () => { + const parsed = await makeTools('').parseUri(`monero:${ADDRESS}`) + assert.equal(parsed.publicAddress, ADDRESS) + }) + + it('rejects a standalone payment id instead of dropping it', async () => { + let thrownName: string | undefined + try { + await makeTools('deadbeefdeadbeef').parseUri( + `monero:${ADDRESS}?tx_payment_id=deadbeefdeadbeef` + ) + } catch (error: unknown) { + thrownName = error instanceof Error ? error.name : undefined + } + assert.equal(thrownName, 'UnsupportedPaymentIdError') + }) +}) From ae721abaa7cddb99e9ab8c12cc8f637e7a586b6d Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 16 Mar 2026 10:16:46 -0700 Subject: [PATCH 06/10] Force new wallet to save birthdayheight --- src/monero/MoneroEngine.ts | 65 +++++++++++++++++++++++--------------- src/monero/MoneroTools.ts | 49 +++++++++++++++++++++++++--- src/monero/moneroTypes.ts | 31 ++++++++++++++++-- 3 files changed, 113 insertions(+), 32 deletions(-) diff --git a/src/monero/MoneroEngine.ts b/src/monero/MoneroEngine.ts index de8e218a3..794b8a5d0 100644 --- a/src/monero/MoneroEngine.ts +++ b/src/monero/MoneroEngine.ts @@ -115,38 +115,28 @@ export class MoneroEngine extends CurrencyEngine< ? this.currentSettings.moneroLightwalletServer : defaults.moneroLightwalletServer - let birthdayHeight: number try { - if (daemonAddress === this.networkInfo.edgeLwsServer) { - await this.tools.cppBridge.setLwsApiKey(this.initOptions.edgeApiKey) - - const loginResult = await this.loginToLwsServer( - daemonAddress, - this.walletInfo.keys.moneroAddress, - this.walletInfo.keys.moneroViewKeyPrivate - ) - - if (loginResult.start_height != null) { - birthdayHeight = loginResult.start_height - } else { - const addressInfo = await this.getAddressInfo( - daemonAddress, - this.walletInfo.keys.moneroAddress, - this.walletInfo.keys.moneroViewKeyPrivate - ) - birthdayHeight = addressInfo.start_height - } - } else { - await this.tools.cppBridge.setLwsApiKey('') - - const addressInfo = await this.getAddressInfo( + // LWS setup: API key and login + const isEdgeLws = daemonAddress === this.networkInfo.edgeLwsServer + let loginResult: LoginResponse | undefined + await this.tools.cppBridge.setLwsApiKey( + isEdgeLws ? this.initOptions.edgeApiKey : '' + ) + if (isEdgeLws) { + loginResult = await this.loginToLwsServer( daemonAddress, this.walletInfo.keys.moneroAddress, this.walletInfo.keys.moneroViewKeyPrivate ) - birthdayHeight = addressInfo.start_height } + // Resolve birthday height (never open a wallet with height 0) + const birthdayHeight = await this.resolveBirthdayHeight( + keys.birthdayHeight, + daemonAddress, + loginResult + ) + await this.tools.cppBridge.openWallet( base64UrlWalletId, 'lws', @@ -204,6 +194,31 @@ export class MoneroEngine extends CurrencyEngine< this.otherData = asMoneroWalletOtherData(raw) } + /** + * Determine the wallet's creation height. For LWS wallets the login + * response or getAddressInfo endpoint is used as a fallback. + */ + private async resolveBirthdayHeight( + height: number | undefined, + daemonAddress: string, + loginResult?: LoginResponse + ): Promise { + if (height != null) return height + + // For Edge LWS, the login response may already have it + if (loginResult?.start_height != null) { + return loginResult.start_height + } + + // Fall back to getAddressInfo + const addressInfo = await this.getAddressInfo( + daemonAddress, + this.walletInfo.keys.moneroAddress, + this.walletInfo.keys.moneroViewKeyPrivate + ) + return addressInfo.start_height + } + async loginToLwsServer( serverUrl: string, address: string, diff --git a/src/monero/MoneroTools.ts b/src/monero/MoneroTools.ts index 2eee5d3e4..f36835cdf 100644 --- a/src/monero/MoneroTools.ts +++ b/src/monero/MoneroTools.ts @@ -17,8 +17,11 @@ import { PluginEnvironment } from '../common/innerPlugin' import { parseUriCommon } from '../common/uriHelpers' import { getLegacyDenomination, mergeDeeply } from '../common/utils' import { + asGetBlockCountResponse, + asMoneroKeyOptions, asMoneroPrivateKeys, asSafeMoneroWalletInfo, + EDGE_MONERO_SERVER, MoneroIo, MoneroNetworkInfo } from './moneroTypes' @@ -63,12 +66,38 @@ export class MoneroTools implements EdgeCurrencyTools { this.cppBridge = new CppBridge(moneroIo) } + async getBlockCount(monerodUrl: string): Promise { + const url = `${monerodUrl.replace(/\/$/, '')}/json_rpc` + const response = await this.io.fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: '0', + method: 'get_block_count' + }) + }) + if (!response.ok) { + const text = await response.text() + throw new Error(`get_block_count failed ${response.status}: ${text}`) + } + const json = await response.json() + const parsed = asGetBlockCountResponse(json) + return parsed.result.count + } + async getDisplayPrivateKey( privateWalletInfo: EdgeWalletInfo ): Promise { const { pluginId } = this.currencyInfo - const keys = asMoneroPrivateKeys(pluginId)(privateWalletInfo.keys) - return keys.moneroKey + const { birthdayHeight, moneroKey } = asMoneroPrivateKeys(pluginId)( + privateWalletInfo.keys + ) + const birthdayHeightString = + birthdayHeight != null + ? `\n\nBirthday Height:\n${birthdayHeight.toString()}` + : '' + return `Seed Phrase:\n${moneroKey}${birthdayHeightString}` } async getDisplayPublicKey(publicWalletInfo: EdgeWalletInfo): Promise { @@ -78,7 +107,7 @@ export class MoneroTools implements EdgeCurrencyTools { async importPrivateKey( input: string, - _opts?: JsonObject + opts?: JsonObject ): Promise { const { pluginId } = this.currencyInfo const { networkType } = this.networkInfo @@ -89,8 +118,16 @@ export class MoneroTools implements EdgeCurrencyTools { networkType ) + const { birthdayHeight } = asMoneroKeyOptions(opts) + + const currentNetworkHeight = await this.getBlockCount(EDGE_MONERO_SERVER) + if (birthdayHeight > currentNetworkHeight) { + throw new Error('InvalidBirthdayHeight') // must be less than current block height + } + return { [`${pluginId}Key`]: mnemonic, + [`${pluginId}BirthdayHeight`]: birthdayHeight, [`${pluginId}SpendKeyPrivate`]: keys.secretSpendKey, [`${pluginId}SpendKeyPublic`]: keys.publicSpendKey } @@ -104,7 +141,11 @@ export class MoneroTools implements EdgeCurrencyTools { const generatedWallet = await this.cppBridge.generateWallet(networkType) - return await this.importPrivateKey(generatedWallet.mnemonic) + const birthdayHeight = await this.getBlockCount(EDGE_MONERO_SERVER) + + return await this.importPrivateKey(generatedWallet.mnemonic, { + birthdayHeight + }) } async derivePublicKey(walletInfo: EdgeWalletInfo): Promise { diff --git a/src/monero/moneroTypes.ts b/src/monero/moneroTypes.ts index 2d173971f..a4e19b60d 100644 --- a/src/monero/moneroTypes.ts +++ b/src/monero/moneroTypes.ts @@ -1,6 +1,7 @@ import { asBoolean, asCodec, + asEither, asMaybe, asNumber, asObject, @@ -14,7 +15,10 @@ import type { } from 'react-native-monero' import type { Subscriber } from 'yaob' +import { asIntegerString } from '../common/types' + export const EDGE_MONERO_LWS_SERVER = 'https://monerolws1.edge.app' +export const EDGE_MONERO_SERVER = `https://monerod.edge.app` export const asMoneroInitOptions = asObject({ edgeApiKey: asOptional(asString, '') @@ -32,9 +36,25 @@ export const asMoneroUserSettings = asObject({ }) export type MoneroUserSettings = ReturnType +const asBirthdayHeight = (raw: unknown): number => + parseInt(asEither(asNumber, asIntegerString)(raw).toString()) + +export const asMoneroKeyOptions = asObject({ + birthdayHeight: asBirthdayHeight +}) +export type MoneroKeyOptions = ReturnType + +export const asGetBlockCountResponse = asObject({ + result: asObject({ + count: asNumber + }) +}) +export type GetBlockCountResponse = ReturnType + export interface MoneroPrivateKeys { dataKey: string moneroKey: string + birthdayHeight?: number moneroSpendKeyPrivate: string moneroSpendKeyPublic: string } @@ -45,6 +65,7 @@ export const asMoneroPrivateKeys = ( const asKeys = asObject({ dataKey: asString, [`${pluginId}Key`]: asString, + [`${pluginId}BirthdayHeight`]: asOptional(asNumber), [`${pluginId}SpendKeyPrivate`]: asString, [`${pluginId}SpendKeyPublic`]: asString }) @@ -54,14 +75,18 @@ export const asMoneroPrivateKeys = ( const clean = asKeys(raw) return { dataKey: clean.dataKey, - moneroKey: clean[`${pluginId}Key`], - moneroSpendKeyPrivate: clean[`${pluginId}SpendKeyPrivate`], - moneroSpendKeyPublic: clean[`${pluginId}SpendKeyPublic`] + moneroKey: clean[`${pluginId}Key`] as string, + birthdayHeight: clean[`${pluginId}BirthdayHeight`] as + | number + | undefined, + moneroSpendKeyPrivate: clean[`${pluginId}SpendKeyPrivate`] as string, + moneroSpendKeyPublic: clean[`${pluginId}SpendKeyPublic`] as string } }, clean => ({ dataKey: clean.dataKey, [`${pluginId}Key`]: clean.moneroKey, + [`${pluginId}BirthdayHeight`]: clean.birthdayHeight, [`${pluginId}SpendKeyPrivate`]: clean.moneroSpendKeyPrivate, [`${pluginId}SpendKeyPublic`]: clean.moneroSpendKeyPublic }) From b64e299fc9ab0f77fb0a035b09d45c39191dc710 Mon Sep 17 00:00:00 2001 From: peachbits Date: Mon, 16 Feb 2026 19:20:45 -0800 Subject: [PATCH 07/10] Include Edge monerod server in default settings --- src/monero/moneroInfo.ts | 5 ++++- src/monero/moneroTypes.ts | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/monero/moneroInfo.ts b/src/monero/moneroInfo.ts index 4e3e85f92..e307efdca 100644 --- a/src/monero/moneroInfo.ts +++ b/src/monero/moneroInfo.ts @@ -4,6 +4,7 @@ import { makeOuterPlugin } from '../common/innerPlugin' import type { MoneroTools } from './MoneroTools' import { EDGE_MONERO_LWS_SERVER, + EDGE_MONERO_SERVER, MoneroNetworkInfo, MoneroUserSettings } from './moneroTypes' @@ -15,7 +16,9 @@ const networkInfo: MoneroNetworkInfo = { const defaultSettings: MoneroUserSettings = { enableCustomServers: false, - moneroLightwalletServer: EDGE_MONERO_LWS_SERVER + enableCustomMonerod: false, + moneroLightwalletServer: EDGE_MONERO_LWS_SERVER, + monerodServer: EDGE_MONERO_SERVER } export const currencyInfo: EdgeCurrencyInfo = { diff --git a/src/monero/moneroTypes.ts b/src/monero/moneroTypes.ts index a4e19b60d..0c0c8ea4a 100644 --- a/src/monero/moneroTypes.ts +++ b/src/monero/moneroTypes.ts @@ -32,7 +32,9 @@ export interface MoneroNetworkInfo { export const asMoneroUserSettings = asObject({ enableCustomServers: asMaybe(asBoolean, false), - moneroLightwalletServer: asMaybe(asString, EDGE_MONERO_LWS_SERVER) + enableCustomMonerod: asMaybe(asBoolean, false), + moneroLightwalletServer: asMaybe(asString, EDGE_MONERO_LWS_SERVER), + monerodServer: asMaybe(asString, EDGE_MONERO_SERVER) }) export type MoneroUserSettings = ReturnType From 8a39429bbbcec4118e87975deac1d38ce1cde361 Mon Sep 17 00:00:00 2001 From: peachbits Date: Mon, 16 Feb 2026 19:21:53 -0800 Subject: [PATCH 08/10] Add backend switching with walletSettings --- src/monero/MoneroEngine.ts | 111 +++++++++++++++++++++++++++++-------- src/monero/moneroInfo.ts | 1 + src/monero/moneroTypes.ts | 7 +++ 3 files changed, 95 insertions(+), 24 deletions(-) diff --git a/src/monero/MoneroEngine.ts b/src/monero/MoneroEngine.ts index 794b8a5d0..1d8622180 100644 --- a/src/monero/MoneroEngine.ts +++ b/src/monero/MoneroEngine.ts @@ -13,7 +13,7 @@ import { NoAmountSpecifiedError, PendingFundsError } from 'edge-core-js/types' -import type { TransactionDirection } from 'react-native-monero' +import type { TransactionDirection, WalletBackend } from 'react-native-monero' import { base64, base64url } from 'rfc4648' import { CurrencyEngine } from '../common/CurrencyEngine' @@ -36,6 +36,7 @@ import { asMoneroPrivateKeys, asMoneroUserSettings, asMoneroWalletOtherData, + asMoneroWalletSettings, asSafeMoneroWalletInfo, LoginResponse, MoneroInitOptions, @@ -43,6 +44,7 @@ import { MoneroPrivateKeys, MoneroUserSettings, MoneroWalletOtherData, + MoneroWalletSettings, SafeMoneroWalletInfo, translateFee } from './moneroTypes' @@ -68,6 +70,7 @@ export class MoneroEngine extends CurrencyEngine< > { networkInfo: MoneroNetworkInfo currentSettings: MoneroUserSettings + currentWalletSettings: MoneroWalletSettings otherData!: MoneroWalletOtherData initOptions: MoneroInitOptions unlockedBalance: string @@ -77,6 +80,7 @@ export class MoneroEngine extends CurrencyEngine< private txSortOrder: 'asc' | 'desc' = 'asc' private unsubscribeWalletEvent?: () => void private abortKeysWait?: () => void + private settingsChangeQueue: Promise = Promise.resolve() constructor( env: PluginEnvironment, @@ -91,7 +95,12 @@ export class MoneroEngine extends CurrencyEngine< this.unlockedBalance = '0' + // Shared across all wallets using this engine: this.currentSettings = asMoneroUserSettings(opts.userSettings) + // Unique to this particular wallet instance: + this.currentWalletSettings = asMoneroWalletSettings( + opts.walletSettings ?? {} + ) // Singleton promise resolved once by the first syncNetwork call. // Stays resolved across restarts so onStart gets keys immediately. @@ -110,36 +119,47 @@ export class MoneroEngine extends CurrencyEngine< this.abortKeysWait = undefined const base64UrlWalletId = asNativeWalletId(this.walletId) + const { backend } = this.currentWalletSettings + this.log('Using backend:', backend) const defaults = asMoneroUserSettings({}) - const daemonAddress = this.currentSettings.enableCustomServers - ? this.currentSettings.moneroLightwalletServer - : defaults.moneroLightwalletServer + const daemonAddress = + backend === 'lws' + ? this.currentSettings.enableCustomServers + ? this.currentSettings.moneroLightwalletServer + : defaults.moneroLightwalletServer + : this.currentSettings.enableCustomMonerod + ? this.currentSettings.monerodServer + : defaults.monerodServer try { - // LWS setup: API key and login - const isEdgeLws = daemonAddress === this.networkInfo.edgeLwsServer + // LWS-specific setup: API key and login let loginResult: LoginResponse | undefined - await this.tools.cppBridge.setLwsApiKey( - isEdgeLws ? this.initOptions.edgeApiKey : '' - ) - if (isEdgeLws) { - loginResult = await this.loginToLwsServer( - daemonAddress, - this.walletInfo.keys.moneroAddress, - this.walletInfo.keys.moneroViewKeyPrivate + if (backend === 'lws') { + const isEdgeLws = daemonAddress === this.networkInfo.edgeLwsServer + await this.tools.cppBridge.setLwsApiKey( + isEdgeLws ? this.initOptions.edgeApiKey : '' ) + if (isEdgeLws) { + loginResult = await this.loginToLwsServer( + daemonAddress, + this.walletInfo.keys.moneroAddress, + this.walletInfo.keys.moneroViewKeyPrivate + ) + } } // Resolve birthday height (never open a wallet with height 0) const birthdayHeight = await this.resolveBirthdayHeight( keys.birthdayHeight, + backend, daemonAddress, + defaults.moneroLightwalletServer, loginResult ) await this.tools.cppBridge.openWallet( base64UrlWalletId, - 'lws', + backend, keys.moneroKey, base64url.stringify(base64.parse(keys.dataKey)), this.networkInfo.networkType, @@ -200,22 +220,40 @@ export class MoneroEngine extends CurrencyEngine< */ private async resolveBirthdayHeight( height: number | undefined, + backend: WalletBackend, daemonAddress: string, + edgeLwsServer: string, loginResult?: LoginResponse ): Promise { - if (height != null) return height + if (height != null && height > 0) return height - // For Edge LWS, the login response may already have it - if (loginResult?.start_height != null) { + // For Edge LWS, the login response may already have it (a zero here is + // not a valid creation height, so fall through to recovery): + if (loginResult?.start_height != null && loginResult.start_height > 0) { return loginResult.start_height } - // Fall back to getAddressInfo + // monerod cannot report a wallet's creation height, so recover it from + // whichever LWS the user has enabled (their custom LWS if configured, + // otherwise the Edge LWS) rather than always crossing to the Edge server: + const serverUrl = + backend === 'lws' + ? daemonAddress + : this.currentSettings.enableCustomServers + ? this.currentSettings.moneroLightwalletServer + : edgeLwsServer const addressInfo = await this.getAddressInfo( - daemonAddress, + serverUrl, this.walletInfo.keys.moneroAddress, this.walletInfo.keys.moneroViewKeyPrivate ) + + if (addressInfo.start_height === 0) { + throw new Error( + 'Cannot open wallet: birthdayHeight is 0. ' + + 'The wallet creation height could not be determined.' + ) + } return addressInfo.start_height } @@ -556,7 +594,7 @@ export class MoneroEngine extends CurrencyEngine< await this.clearBlockchainCache() await this.tools.cppBridge.deleteWallet( asNativeWalletId(this.walletId), - 'lws' + this.currentWalletSettings.backend ) await this.startEngine() } @@ -567,9 +605,34 @@ export class MoneroEngine extends CurrencyEngine< return } - this.currentSettings = newSettings - await this.killEngine() - await this.startEngine() + const run = this.settingsChangeQueue.then(async () => { + this.currentSettings = newSettings + await this.killEngine() + await this.startEngine() + }) + // Keep the queue usable for later changes even if this one throws, while + // still surfacing the error to this caller: + this.settingsChangeQueue = run.catch(() => {}) + await run + } + + async changeWalletSettings(walletSettings: JsonObject): Promise { + const newSettings = asMaybe(asMoneroWalletSettings)(walletSettings) + if ( + newSettings == null || + matchJson(this.currentWalletSettings, newSettings) + ) { + return + } + + const run = this.settingsChangeQueue.then(async () => { + this.currentWalletSettings = newSettings + await this.killEngine() + await this.clearBlockchainCache() + await this.startEngine() + }) + this.settingsChangeQueue = run.catch(() => {}) + await run } async getMaxSpendable(edgeSpendInfo: EdgeSpendInfo): Promise { diff --git a/src/monero/moneroInfo.ts b/src/monero/moneroInfo.ts index e307efdca..94884e8aa 100644 --- a/src/monero/moneroInfo.ts +++ b/src/monero/moneroInfo.ts @@ -41,6 +41,7 @@ export const currencyInfo: EdgeCurrencyInfo = { ], defaultSettings, + hasWalletSettings: true, unsafeSyncNetwork: true, chainDisplayName: 'Monero', diff --git a/src/monero/moneroTypes.ts b/src/monero/moneroTypes.ts index 0c0c8ea4a..bf94d69a0 100644 --- a/src/monero/moneroTypes.ts +++ b/src/monero/moneroTypes.ts @@ -7,10 +7,12 @@ import { asObject, asOptional, asString, + asValue, Cleaner } from 'cleaners' import type { TransactionPriority, + WalletBackend, WalletEventData } from 'react-native-monero' import type { Subscriber } from 'yaob' @@ -38,6 +40,11 @@ export const asMoneroUserSettings = asObject({ }) export type MoneroUserSettings = ReturnType +export const asMoneroWalletSettings = asObject({ + backend: asMaybe(asValue('lws', 'monerod'), 'lws') +}) +export type MoneroWalletSettings = ReturnType + const asBirthdayHeight = (raw: unknown): number => parseInt(asEither(asNumber, asIntegerString)(raw).toString()) From ad0367219ccec2af0161e907c605f71762c4c9ad Mon Sep 17 00:00:00 2001 From: peachbits Date: Tue, 14 Apr 2026 16:14:47 -0700 Subject: [PATCH 09/10] Pass in birthday height, if present, to the login endpoint --- src/monero/MoneroEngine.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/monero/MoneroEngine.ts b/src/monero/MoneroEngine.ts index 1d8622180..2135afb48 100644 --- a/src/monero/MoneroEngine.ts +++ b/src/monero/MoneroEngine.ts @@ -143,7 +143,8 @@ export class MoneroEngine extends CurrencyEngine< loginResult = await this.loginToLwsServer( daemonAddress, this.walletInfo.keys.moneroAddress, - this.walletInfo.keys.moneroViewKeyPrivate + this.walletInfo.keys.moneroViewKeyPrivate, + keys.birthdayHeight // pass it along in case we have it already ) } } @@ -260,7 +261,8 @@ export class MoneroEngine extends CurrencyEngine< async loginToLwsServer( serverUrl: string, address: string, - viewKey: string + viewKey: string, + birthdayHeight?: number ): Promise { const url = `${serverUrl}/login` const response = await this.tools.io.fetch(url, { @@ -274,7 +276,8 @@ export class MoneroEngine extends CurrencyEngine< api_key: this.initOptions.edgeApiKey, create_account: true, generated_locally: true, - view_key: viewKey + view_key: viewKey, + birthday_height: birthdayHeight }) }) if (!response.ok) { From 2584c3fe2a81f3ad8c7ed935a0f3623cf31d1ef3 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 27 Apr 2026 12:59:44 -0700 Subject: [PATCH 10/10] Add nym fetch support --- src/monero/MoneroEngine.ts | 65 ++++++++++++++++--- src/monero/MoneroTools.ts | 128 ++++++++++++++++++++++++++++++++++++- src/monero/moneroInfo.ts | 3 +- src/monero/moneroTypes.ts | 3 +- 4 files changed, 186 insertions(+), 13 deletions(-) diff --git a/src/monero/MoneroEngine.ts b/src/monero/MoneroEngine.ts index 2135afb48..bce64f7d9 100644 --- a/src/monero/MoneroEngine.ts +++ b/src/monero/MoneroEngine.ts @@ -4,6 +4,7 @@ import { EdgeCurrencyEngine, EdgeCurrencyEngineOptions, EdgeEnginePrivateKeyOptions, + EdgeFetchFunction, EdgeMemo, EdgeSpendInfo, EdgeTransaction, @@ -22,7 +23,7 @@ import { LifecycleManager, makeLifecycleManager } from '../common/lifecycleManager' -import { cleanTxLogs, matchJson } from '../common/utils' +import { cleanTxLogs, makeEngineFetch, matchJson } from '../common/utils' import { makeWeightedSyncTracker, WeightedSyncTracker @@ -74,11 +75,13 @@ export class MoneroEngine extends CurrencyEngine< otherData!: MoneroWalletOtherData initOptions: MoneroInitOptions unlockedBalance: string + private readonly engineFetch: EdgeFetchFunction private readonly nativeWalletId: LifecycleManager private sendKeysToNative?: (keys: MoneroPrivateKeys) => void private syncStartHeight: number | undefined private txSortOrder: 'asc' | 'desc' = 'asc' private unsubscribeWalletEvent?: () => void + private unsubscribeNymFetch?: () => Promise private abortKeysWait?: () => void private settingsChangeQueue: Promise = Promise.resolve() @@ -102,8 +105,20 @@ export class MoneroEngine extends CurrencyEngine< opts.walletSettings ?? {} ) - // Singleton promise resolved once by the first syncNetwork call. - // Stays resolved across restarts so onStart gets keys immediately. + // Fetch wrapper that re-evaluates the user's networkPrivacy choice + // on every request, so changes via Currency Settings take effect + // without restarting the engine. + this.engineFetch = makeEngineFetch(env.io, () => { + return this.currentSettings.networkPrivacy === 'nym' + ? { privacy: 'nym' } + : {} + }) + + // Singleton promise resolved once by the first syncNetwork call. The + // lifecycle closure captures this already-resolved promise, so onStart gets + // keys immediately across engine restarts. If killEngine runs before + // syncNetwork ever resolves it, the abortKeysWait race in onStart rejects + // the wait so stop() does not hang. const keysPromise = new Promise(resolve => { this.sendKeysToNative = resolve }) @@ -158,6 +173,13 @@ export class MoneroEngine extends CurrencyEngine< loginResult ) + // Hook up the Nym mixnet proxy (before openWallet so the + // very first LWSF request is already routed through it). + this.unsubscribeNymFetch = await this.tools.setupNymFetch( + this.currentSettings.networkPrivacy === 'nym', + daemonAddress + ) + await this.tools.cppBridge.openWallet( base64UrlWalletId, backend, @@ -186,6 +208,14 @@ export class MoneroEngine extends CurrencyEngine< return base64UrlWalletId } catch (error: unknown) { + if (this.unsubscribeNymFetch != null) { + try { + await this.unsubscribeNymFetch() + } catch (cleanupError: unknown) { + this.log.error(`Error disabling nym: ${String(cleanupError)}`) + } + this.unsubscribeNymFetch = undefined + } if (!(error instanceof Error)) throw error this.log.error(`Failed to open wallet: ${error.message}`) throw error @@ -197,6 +227,14 @@ export class MoneroEngine extends CurrencyEngine< this.unsubscribeWalletEvent() this.unsubscribeWalletEvent = undefined } + if (this.unsubscribeNymFetch != null) { + try { + await this.unsubscribeNymFetch() + } catch (error: unknown) { + this.log.error(`Error disabling nym: ${String(error)}`) + } + this.unsubscribeNymFetch = undefined + } try { await this.tools.cppBridge.closeWallet(nativeWalletId) this.log(`Wallet closed: ${nativeWalletId}`) @@ -258,14 +296,23 @@ export class MoneroEngine extends CurrencyEngine< return addressInfo.start_height } - async loginToLwsServer( + // The Edge API key must only be sent to the Edge LWS (never a custom or + // third-party server) and never as an empty string: + private edgeApiKeyBody(serverUrl: string): { api_key?: string } { + const { edgeApiKey } = this.initOptions + return serverUrl === this.networkInfo.edgeLwsServer && edgeApiKey !== '' + ? { api_key: edgeApiKey } + : {} + } + + private async loginToLwsServer( serverUrl: string, address: string, viewKey: string, birthdayHeight?: number ): Promise { const url = `${serverUrl}/login` - const response = await this.tools.io.fetch(url, { + const response = await this.engineFetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -273,7 +320,7 @@ export class MoneroEngine extends CurrencyEngine< }, body: JSON.stringify({ address, - api_key: this.initOptions.edgeApiKey, + ...this.edgeApiKeyBody(serverUrl), create_account: true, generated_locally: true, view_key: viewKey, @@ -288,13 +335,13 @@ export class MoneroEngine extends CurrencyEngine< return asLoginResponse(json) } - async getAddressInfo( + private async getAddressInfo( serverUrl: string, address: string, viewKey: string ): Promise { const url = `${serverUrl}/get_address_info` - const response = await this.tools.io.fetch(url, { + const response = await this.engineFetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -302,7 +349,7 @@ export class MoneroEngine extends CurrencyEngine< }, body: JSON.stringify({ address, - api_key: this.initOptions.edgeApiKey, + ...this.edgeApiKeyBody(serverUrl), view_key: viewKey }) }) diff --git a/src/monero/MoneroTools.ts b/src/monero/MoneroTools.ts index f36835cdf..517a66802 100644 --- a/src/monero/MoneroTools.ts +++ b/src/monero/MoneroTools.ts @@ -3,6 +3,7 @@ import { EdgeCurrencyInfo, EdgeCurrencyTools, EdgeEncodeUri, + EdgeFetchFunction, EdgeIo, EdgeLog, EdgeMetaToken, @@ -12,10 +13,15 @@ import { JsonObject } from 'edge-core-js/types' import { CppBridge } from 'react-native-monero/lib/src/CppBridge' +import { base64 } from 'rfc4648' import { PluginEnvironment } from '../common/innerPlugin' import { parseUriCommon } from '../common/uriHelpers' -import { getLegacyDenomination, mergeDeeply } from '../common/utils' +import { + getLegacyDenomination, + makeEngineFetch, + mergeDeeply +} from '../common/utils' import { asGetBlockCountResponse, asMoneroKeyOptions, @@ -26,6 +32,16 @@ import { MoneroNetworkInfo } from './moneroTypes' +interface NymCppBridge { + setNymEnabled: (enabled: boolean, baseUrl: string) => Promise + resolveFetch: ( + requestId: string, + status: number, + bodyBase64: string + ) => Promise + rejectFetch: (requestId: string, errorMessage: string) => Promise +} + /** * Thrown when a Monero payment is requested with a standalone payment id (a * `tx_payment_id` URI parameter). Monero only delivers a payment id to the @@ -47,14 +63,18 @@ export class MoneroTools implements EdgeCurrencyTools { cppBridge: CppBridge moneroIo: MoneroIo io: EdgeIo + engineFetch: EdgeFetchFunction log: EdgeLog builtinTokens: EdgeTokenMap currencyInfo: EdgeCurrencyInfo networkInfo: MoneroNetworkInfo + private nymFetchUsers = 0 + private unsubscribeNymFetch?: () => void constructor(env: PluginEnvironment) { const { builtinTokens, currencyInfo, io, log, nativeIo, networkInfo } = env this.io = io + this.engineFetch = makeEngineFetch(io) this.log = log this.currencyInfo = currencyInfo this.builtinTokens = builtinTokens @@ -66,9 +86,113 @@ export class MoneroTools implements EdgeCurrencyTools { this.cppBridge = new CppBridge(moneroIo) } + private get nymCppBridge(): NymCppBridge { + return this.cppBridge as unknown as NymCppBridge + } + + async setupNymFetch( + enabled: boolean, + daemonAddress: string + ): Promise<() => Promise> { + if (!enabled) { + if (this.nymFetchUsers === 0) { + await this.nymCppBridge.setNymEnabled(false, '') + } + return async () => {} + } + + if (this.unsubscribeNymFetch == null) { + this.unsubscribeNymFetch = this.moneroIo.on('walletEvent', event => { + if (event.eventName !== 'nymFetchRequest') return + + // The native layer reuses the walletId field as the request id for + // this event type. A single shared listener prevents duplicate + // fetches when multiple Monero engines are active. + this.handleNymFetchRequest(event.walletId, event.data).catch(() => { + // handleNymFetchRequest reports failures through rejectFetch. + }) + }) + } + + this.nymFetchUsers += 1 + try { + await this.nymCppBridge.setNymEnabled( + true, + daemonAddress.replace(/\/$/, '') + ) + } catch (error: unknown) { + this.nymFetchUsers -= 1 + if (this.nymFetchUsers === 0 && this.unsubscribeNymFetch != null) { + this.unsubscribeNymFetch() + this.unsubscribeNymFetch = undefined + } + throw error + } + + let released = false + return async () => { + if (released) return + released = true + + if (this.nymFetchUsers > 0) this.nymFetchUsers -= 1 + if (this.nymFetchUsers === 0) { + try { + await this.nymCppBridge.setNymEnabled(false, '') + } finally { + if (this.unsubscribeNymFetch != null) { + this.unsubscribeNymFetch() + this.unsubscribeNymFetch = undefined + } + } + } + } + } + + private async handleNymFetchRequest( + requestId: string, + payloadJson: string + ): Promise { + try { + const payload = JSON.parse(payloadJson) as { + url: string + method: string + headers: Record + bodyBase64: string + } + + const bodyBytes = base64.parse(payload.bodyBase64) + let body: ArrayBuffer | undefined + if (bodyBytes.length > 0) { + body = new ArrayBuffer(bodyBytes.length) + new Uint8Array(body).set(bodyBytes) + } + + const response = await this.io.fetch(payload.url, { + method: payload.method, + headers: payload.headers, + body, + privacy: 'nym' + }) + + const responseBytes = new Uint8Array(await response.arrayBuffer()) + await this.nymCppBridge.resolveFetch( + requestId, + response.status, + responseBytes.length > 0 ? base64.stringify(responseBytes) : '' + ) + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error) + try { + await this.nymCppBridge.rejectFetch(requestId, message) + } catch (rejectError: unknown) { + this.log.error(`rejectFetch failed: ${String(rejectError)}`) + } + } + } + async getBlockCount(monerodUrl: string): Promise { const url = `${monerodUrl.replace(/\/$/, '')}/json_rpc` - const response = await this.io.fetch(url, { + const response = await this.engineFetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ diff --git a/src/monero/moneroInfo.ts b/src/monero/moneroInfo.ts index 94884e8aa..06c8512a9 100644 --- a/src/monero/moneroInfo.ts +++ b/src/monero/moneroInfo.ts @@ -18,7 +18,8 @@ const defaultSettings: MoneroUserSettings = { enableCustomServers: false, enableCustomMonerod: false, moneroLightwalletServer: EDGE_MONERO_LWS_SERVER, - monerodServer: EDGE_MONERO_SERVER + monerodServer: EDGE_MONERO_SERVER, + networkPrivacy: 'none' } export const currencyInfo: EdgeCurrencyInfo = { diff --git a/src/monero/moneroTypes.ts b/src/monero/moneroTypes.ts index bf94d69a0..cd12f351d 100644 --- a/src/monero/moneroTypes.ts +++ b/src/monero/moneroTypes.ts @@ -36,7 +36,8 @@ export const asMoneroUserSettings = asObject({ enableCustomServers: asMaybe(asBoolean, false), enableCustomMonerod: asMaybe(asBoolean, false), moneroLightwalletServer: asMaybe(asString, EDGE_MONERO_LWS_SERVER), - monerodServer: asMaybe(asString, EDGE_MONERO_SERVER) + monerodServer: asMaybe(asString, EDGE_MONERO_SERVER), + networkPrivacy: asOptional(asValue('none', 'nym'), 'none') }) export type MoneroUserSettings = ReturnType