From da26b51b530361a3003dbfae84ef2547038b8ff1 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 24 Jun 2024 17:56:23 -0700 Subject: [PATCH] Add EdgeMemoryWallet Intended to be used briefly and without any local state saved. It returns once the underlying engine is fully synced. --- CHANGELOG.md | 2 + src/core/account/account-api.ts | 14 +++ src/core/account/memory-wallet.ts | 197 ++++++++++++++++++++++++++++++ src/types/types.ts | 19 +++ 4 files changed, 232 insertions(+) create mode 100644 src/core/account/memory-wallet.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a2494d78..8edd6eba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- added: Add `makeMemoryWallet` method to ephemeral wallet objects that can query balances and spend funds + ## 2.8.1 (2024-07-11) - fixed: Filter transactions with empty (zero) nativeAmount and networkFee 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: (