From 114fe24212825a8ce54c8b98ba4c07b28208a5fc Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 24 Jun 2024 17:46:36 -0700 Subject: [PATCH 1/3] Make getMaxSpendable reusable --- .../currency/wallet/currency-wallet-api.ts | 66 +++------------- src/core/currency/wallet/max-spend.ts | 78 +++++++++++++++++++ 2 files changed, 87 insertions(+), 57 deletions(-) create mode 100644 src/core/currency/wallet/max-spend.ts diff --git a/src/core/currency/wallet/currency-wallet-api.ts b/src/core/currency/wallet/currency-wallet-api.ts index c652a7d2..e62df42f 100644 --- a/src/core/currency/wallet/currency-wallet-api.ts +++ b/src/core/currency/wallet/currency-wallet-api.ts @@ -1,4 +1,4 @@ -import { add, div, eq, lte, mul, sub } from 'biggystring' +import { div, eq, mul } from 'biggystring' import { Disklet } from 'disklet' import { bridgifyObject, onMethod, watchMethod } from 'yaob' @@ -55,6 +55,7 @@ import { } from './currency-wallet-files' import { CurrencyWalletInput } from './currency-wallet-pixie' import { MergedTransaction } from './currency-wallet-reducer' +import { getMaxSpendableInner } from './max-spend' import { mergeMetadata } from './metadata' import { upgradeMemos } from './upgrade-memos' @@ -388,62 +389,13 @@ export function makeCurrencyWalletApi( return await engine.broadcastTx(tx, { privateKeys }) }, async getMaxSpendable(spendInfo: EdgeSpendInfo): Promise { - spendInfo = upgradeMemos(spendInfo, plugin.currencyInfo) - // Figure out which asset this is: - const upgradedCurrency = upgradeCurrencyCode({ - allTokens: input.props.state.accounts[accountId].allTokens[pluginId], - currencyInfo: plugin.currencyInfo, - tokenId: spendInfo.tokenId - }) - - if (typeof engine.getMaxSpendable === 'function') { - // Only provide wallet info if currency requires it: - const privateKeys = unsafeMakeSpend ? walletInfo.keys : undefined - - return await engine.getMaxSpendable( - { ...spendInfo, ...upgradedCurrency }, - { privateKeys } - ) - } - - const { networkFeeOption, customNetworkFee } = spendInfo - const balance = engine.getBalance(upgradedCurrency) - - // Copy all the spend targets, setting the amounts to 0 - // but keeping all other information so we can get accurate fees: - const spendTargets = spendInfo.spendTargets.map(spendTarget => { - return { ...spendTarget, nativeAmount: '0' } - }) - - // The range of possible values includes `min`, but not `max`. - function getMax(min: string, max: string): Promise { - const diff = sub(max, min) - if (lte(diff, '1')) { - return Promise.resolve(min) - } - const mid = add(min, div(diff, '2')) - - // Try the average: - spendTargets[0].nativeAmount = mid - - // Only provide wallet info if currency requires it: - const privateKeys = unsafeMakeSpend ? walletInfo.keys : undefined - - return engine - .makeSpend( - { - ...upgradedCurrency, - spendTargets, - networkFeeOption, - customNetworkFee - }, - { privateKeys } - ) - .then(() => getMax(mid, max)) - .catch(() => getMax(min, mid)) - } - - return await getMax('0', add(balance, '1')) + return await getMaxSpendableInner( + spendInfo, + plugin, + engine, + input.props.state.accounts[accountId].allTokens[pluginId], + walletInfo + ) }, async getPaymentProtocolInfo( paymentProtocolUrl: string diff --git a/src/core/currency/wallet/max-spend.ts b/src/core/currency/wallet/max-spend.ts new file mode 100644 index 00000000..28d31db9 --- /dev/null +++ b/src/core/currency/wallet/max-spend.ts @@ -0,0 +1,78 @@ +import { add, div, lte, sub } from 'biggystring' + +import { + EdgeCurrencyEngine, + EdgeCurrencyPlugin, + EdgeSpendInfo, + EdgeTokenMap, + EdgeWalletInfo +} from '../../../browser' +import { upgradeCurrencyCode } from '../../../types/type-helpers' +import { upgradeMemos } from './upgrade-memos' + +export const getMaxSpendableInner = async ( + spendInfo: EdgeSpendInfo, + plugin: EdgeCurrencyPlugin, + engine: EdgeCurrencyEngine, + allTokens: EdgeTokenMap, + walletInfo: EdgeWalletInfo +): Promise => { + spendInfo = upgradeMemos(spendInfo, plugin.currencyInfo) + // Figure out which asset this is: + const upgradedCurrency = upgradeCurrencyCode({ + allTokens, + currencyInfo: plugin.currencyInfo, + tokenId: spendInfo.tokenId + }) + + const unsafeMakeSpend = plugin.currencyInfo.unsafeMakeSpend ?? false + + if (typeof engine.getMaxSpendable === 'function') { + // Only provide wallet info if currency requires it: + const privateKeys = unsafeMakeSpend ? walletInfo.keys : undefined + + return await engine.getMaxSpendable( + { ...spendInfo, ...upgradedCurrency }, + { privateKeys } + ) + } + + const { networkFeeOption, customNetworkFee } = spendInfo + const balance = engine.getBalance(upgradedCurrency) + + // Copy all the spend targets, setting the amounts to 0 + // but keeping all other information so we can get accurate fees: + const spendTargets = spendInfo.spendTargets.map(spendTarget => { + return { ...spendTarget, nativeAmount: '0' } + }) + + // The range of possible values includes `min`, but not `max`. + function getMax(min: string, max: string): Promise { + const diff = sub(max, min) + if (lte(diff, '1')) { + return Promise.resolve(min) + } + const mid = add(min, div(diff, '2')) + + // Try the average: + spendTargets[0].nativeAmount = mid + + // Only provide wallet info if currency requires it: + const privateKeys = unsafeMakeSpend ? walletInfo.keys : undefined + + return engine + .makeSpend( + { + ...upgradedCurrency, + spendTargets, + networkFeeOption, + customNetworkFee + }, + { privateKeys } + ) + .then(() => getMax(mid, max)) + .catch(() => getMax(min, mid)) + } + + return await getMax('0', add(balance, '1')) +} From 23e99f4892053c87540a690be058dda8ddbfc4a7 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 24 Jun 2024 17:56:23 -0700 Subject: [PATCH 2/3] Add EdgeMemoryWallet Intended to be used briefly and without any local state saved. It returns once the underlying engine is fully synced. --- CHANGELOG.md | 1 + src/core/account/account-api.ts | 14 +++ src/core/account/memory-wallet.ts | 197 ++++++++++++++++++++++++++++++ src/types/types.ts | 19 +++ 4 files changed, 231 insertions(+) create mode 100644 src/core/account/memory-wallet.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e71567ac..884fff05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- added: Add `makeMemoryWallet` method to ephemeral wallet objects that can query balances and spend funds - fixed: Correctly handle `null` fetch bodies on Android. ## 2.8.1 (2024-07-11) diff --git a/src/core/account/account-api.ts b/src/core/account/account-api.ts index 305e16bb..47c7a38a 100644 --- a/src/core/account/account-api.ts +++ b/src/core/account/account-api.ts @@ -17,6 +17,7 @@ import { EdgeGetActivationAssetsOptions, EdgeGetActivationAssetsResults, EdgeLobby, + EdgeMemoryWallet, EdgePendingVoucher, EdgePluginMap, EdgeResult, @@ -59,6 +60,7 @@ import { changeWalletStates } from './account-files' import { AccountState } from './account-reducer' import { makeDataStoreApi } from './data-store-api' import { makeLobbyApi } from './lobby-api' +import { makeMemoryWalletInner } from './memory-wallet' import { CurrencyConfig, SwapConfig } from './plugin-api' /** @@ -498,6 +500,18 @@ export function makeAccountApi(ai: ApiInput, accountId: string): EdgeAccount { return await finishWalletCreation(ai, accountId, walletInfo.id, opts) }, + async makeMemoryWallet( + walletType: string, + opts: EdgeCreateCurrencyWalletOptions = {} + ): Promise { + const config = Object.values(currencyConfigs).find( + plugin => plugin.currencyInfo.walletType === walletType + ) + if (config == null) throw new Error('Invalid walletType') + + return await makeMemoryWalletInner(ai, config, walletType, opts) + }, + async createCurrencyWallets( createWallets: EdgeCreateCurrencyWallet[] ): Promise>> { diff --git a/src/core/account/memory-wallet.ts b/src/core/account/memory-wallet.ts new file mode 100644 index 00000000..29b3f4a3 --- /dev/null +++ b/src/core/account/memory-wallet.ts @@ -0,0 +1,197 @@ +import { makeMemoryDisklet } from 'disklet' +import { bridgifyObject, close, update, watchMethod } from 'yaob' + +import { + EdgeBalanceMap, + EdgeCreateCurrencyWalletOptions, + EdgeCurrencyConfig, + EdgeMemoryWallet, + EdgeSpendInfo, + EdgeTokenId, + EdgeTransaction, + EdgeWalletInfo +} from '../../browser' +import { makePeriodicTask, PeriodicTask } from '../../util/periodic-task' +import { snooze } from '../../util/snooze' +import { getMaxSpendableInner } from '../currency/wallet/max-spend' +import { makeLog } from '../log/log' +import { getCurrencyTools } from '../plugins/plugins-selectors' +import { ApiInput } from '../root-pixie' + +let memoryWalletCount = 0 + +export const makeMemoryWalletInner = async ( + ai: ApiInput, + config: EdgeCurrencyConfig, + walletType: string, + opts: EdgeCreateCurrencyWalletOptions = {} +): Promise => { + const { keys } = opts + if (keys == null) throw new Error('No keys provided') + + const walletId = `memorywallet-${memoryWalletCount++}` + const walletInfo: EdgeWalletInfo = { + id: walletId, + type: walletType, + keys + } + + const tools = await getCurrencyTools(ai, config.currencyInfo.pluginId) + const publicKeys = await tools.derivePublicKey(walletInfo) + walletInfo.keys = { ...publicKeys, ...walletInfo.keys } + + const log = makeLog(ai.props.logBackend, `${walletId}-${walletType}`) + let balanceMap: EdgeBalanceMap = new Map() + let detectedTokenIds: string[] = [] + let syncRatio: number = 0 + + let needsUpdate = false + const updateWallet = (): void => { + if (needsUpdate) { + update(out) + needsUpdate = false + } + } + const updater = makePeriodicTask(async () => { + await snooze(1000) // one second + updateWallet() + }, 0) + + const plugin = ai.props.state.plugins.currency[config.currencyInfo.pluginId] + const engine = await plugin.makeCurrencyEngine(walletInfo, { + callbacks: { + onAddressChanged: () => {}, + onAddressesChecked: (progressRatio: number) => { + if (out.syncRatio === 1) return + + if (progressRatio === 1) { + syncRatio = progressRatio + needsUpdate = true + } + }, + onNewTokens: (tokenIds: string[]) => { + const sortedTokenIds = tokenIds.sort((a, b) => a.localeCompare(b)) + + if (detectedTokenIds.length !== sortedTokenIds.length) { + detectedTokenIds = sortedTokenIds + needsUpdate = true + return + } + for (let i = 0; i < sortedTokenIds.length; i++) { + if (detectedTokenIds[i] !== sortedTokenIds[i]) { + detectedTokenIds = sortedTokenIds + needsUpdate = true + return + } + } + }, + onStakingStatusChanged: () => {}, + onTokenBalanceChanged: (tokenId: EdgeTokenId, balance: string) => { + if (balanceMap.get(tokenId) === balance) return + + balanceMap = new Map(balanceMap) + balanceMap.set(tokenId, balance) + needsUpdate = true + }, + onTransactionsChanged: () => {}, + onTxidsChanged: () => {}, + onUnactivatedTokenIdsChanged: () => {}, + onWcNewContractCall: () => {}, + onBlockHeightChanged: () => {}, + onBalanceChanged: () => {} + }, + customTokens: { ...config.customTokens }, + enabledTokenIds: [...Object.keys(config.allTokens)], + log, + userSettings: { ...(config.userSettings ?? {}) }, + walletLocalDisklet: makeMemoryDisklet(), + walletLocalEncryptedDisklet: makeMemoryDisklet() + }) + + const { + unsafeBroadcastTx = false, + unsafeMakeSpend = false, + unsafeSyncNetwork = false + } = plugin.currencyInfo + + const privateKeys = { ...keys } + + let syncNetworkTask: PeriodicTask + // Setup syncNetwork routine if defined by the currency engine: + if (engine.syncNetwork != null) { + // Get the private keys if required by the engine: + const doNetworkSync = async (): Promise => { + if (engine.syncNetwork != null) { + const delay = await engine.syncNetwork({ + privateKeys: unsafeSyncNetwork ? { privateKeys: keys } : undefined + }) + syncNetworkTask.setDelay(delay) + } else { + syncNetworkTask.stop() + } + } + syncNetworkTask = makePeriodicTask(doNetworkSync, 10000, { + onError: error => { + ai.props.log.error(error) + } + }) + syncNetworkTask.start({ wait: false }) + } + + const out = bridgifyObject({ + watch: watchMethod, + get balanceMap() { + return balanceMap + }, + get detectedTokenIds() { + return detectedTokenIds + }, + get syncRatio() { + return syncRatio + }, + async changeEnabledTokenIds(tokenIds: string[]) { + if (engine.changeEnabledTokenIds != null) { + await engine.changeEnabledTokenIds(tokenIds) + } + }, + async startEngine() { + await engine.startEngine() + syncNetworkTask?.start({ wait: false }) + }, + async getMaxSpendable(spendInfo: EdgeSpendInfo) { + return await getMaxSpendableInner( + spendInfo, + plugin, + engine, + config.allTokens, + walletInfo + ) + }, + async makeSpend(spendInfo: EdgeSpendInfo) { + return await engine.makeSpend( + spendInfo, + unsafeMakeSpend ? privateKeys : undefined + ) + }, + async signTx(tx: EdgeTransaction) { + return await engine.signTx(tx, privateKeys) + }, + async broadcastTx(tx: EdgeTransaction) { + return await engine.broadcastTx( + tx, + unsafeBroadcastTx ? privateKeys : undefined + ) + }, + async saveTx() {}, + + async close() { + log.warn('killing memory wallet') + syncNetworkTask?.stop() + close(out) + await engine.killEngine() + } + }) + + updater.start({ wait: false }) + return out +} diff --git a/src/types/types.ts b/src/types/types.ts index 9b8887ee..4e7f4207 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -1239,6 +1239,21 @@ export interface EdgeCurrencyWallet { readonly otherMethods: EdgeOtherMethods } +export interface EdgeMemoryWallet { + readonly watch: Subscriber + readonly balanceMap: EdgeBalanceMap + readonly detectedTokenIds: string[] + readonly syncRatio: number + readonly changeEnabledTokenIds: (tokenIds: string[]) => Promise + readonly startEngine: () => Promise + readonly getMaxSpendable: (spendInfo: EdgeSpendInfo) => Promise + readonly makeSpend: (spendInfo: EdgeSpendInfo) => Promise + readonly signTx: (tx: EdgeTransaction) => Promise + readonly broadcastTx: (tx: EdgeTransaction) => Promise + readonly saveTx: (tx: EdgeTransaction) => Promise + readonly close: () => Promise +} + // --------------------------------------------------------------------- // swap plugin // --------------------------------------------------------------------- @@ -1636,6 +1651,10 @@ export interface EdgeAccount { walletId: string ) => Promise readonly waitForAllWallets: () => Promise + readonly makeMemoryWallet: ( + walletType: string, + opts?: EdgeCreateCurrencyWalletOptions + ) => Promise // Token & wallet activation: readonly getActivationAssets: ( From 276818637dd3a4562260f2a49089f058248e830e Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 24 Jun 2024 23:15:12 -0700 Subject: [PATCH 3/3] Add lightMode option to makeCurrencyEngine The memory wallet is temporary that just needs to get balances and spend. An engine recognizing lightMode can reduce unnecessary operations like transaction history --- src/core/account/memory-wallet.ts | 1 + src/types/types.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/core/account/memory-wallet.ts b/src/core/account/memory-wallet.ts index 29b3f4a3..baaf9a0c 100644 --- a/src/core/account/memory-wallet.ts +++ b/src/core/account/memory-wallet.ts @@ -102,6 +102,7 @@ export const makeMemoryWalletInner = async ( }, customTokens: { ...config.customTokens }, enabledTokenIds: [...Object.keys(config.allTokens)], + lightMode: true, log, userSettings: { ...(config.userSettings ?? {}) }, walletLocalDisklet: makeMemoryDisklet(), diff --git a/src/types/types.ts b/src/types/types.ts index 4e7f4207..afc65841 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -894,6 +894,9 @@ export interface EdgeCurrencyEngineCallbacks { export interface EdgeCurrencyEngineOptions { callbacks: EdgeCurrencyEngineCallbacks + /** True if we only need a balance and the ability to spend it. */ + lightMode?: boolean + // Wallet-scoped IO objects: log: EdgeLog walletLocalDisklet: Disklet