From c8724dda599ccd94dd711304f0950321a2104fc6 Mon Sep 17 00:00:00 2001 From: William Swanson Date: Mon, 13 Nov 2023 10:39:31 -0800 Subject: [PATCH 1/6] Convert the `uniqueStrings` function to use a Set --- src/core/currency/wallet/enabled-tokens.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/core/currency/wallet/enabled-tokens.ts b/src/core/currency/wallet/enabled-tokens.ts index 0acbe5236..9783beb19 100644 --- a/src/core/currency/wallet/enabled-tokens.ts +++ b/src/core/currency/wallet/enabled-tokens.ts @@ -47,13 +47,12 @@ export function tokenIdsToCurrencyCodes( * optionally removing the items in `omit`. */ export function uniqueStrings(array: string[], omit: string[] = []): string[] { - const table: { [key: string]: true } = {} - for (const item of omit) table[item] = true + const table = new Set(omit) const out: string[] = [] for (const item of array) { - if (table[item]) continue - table[item] = true + if (table.has(item)) continue + table.add(item) out.push(item) } return out From 54c1493c2cad8f586ce74bc08c432cde19c01336 Mon Sep 17 00:00:00 2001 From: William Swanson Date: Wed, 15 Nov 2023 08:19:26 -0800 Subject: [PATCH 2/6] Move an action to the right section --- src/core/actions.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/core/actions.ts b/src/core/actions.ts index 6a917c521..e9dbd35b3 100644 --- a/src/core/actions.ts +++ b/src/core/actions.ts @@ -193,6 +193,14 @@ export type RootAction = txidHashes: TxidHashes } } + | { + // Called when a currency engine fires the onAddressChecked callback. + type: 'CURRENCY_ENGINE_CHANGED_UNACTIVATED_TOKEN_IDS' + payload: { + unactivatedTokenIds: string[] + walletId: string + } + } | { type: 'CURRENCY_ENGINE_GOT_TXS' payload: { @@ -303,14 +311,6 @@ export type RootAction = walletInfo: EdgeWalletInfo } } - | { - // Called when a currency engine fires the onAddressChecked callback. - type: 'CURRENCY_ENGINE_CHANGED_UNACTIVATED_TOKEN_IDS' - payload: { - unactivatedTokenIds: string[] - walletId: string - } - } | { // Fired when we fetch exchange pairs from some server. type: 'EXCHANGE_PAIRS_FETCHED' From 96630262993ebf8a88e7d03773f4f82dd8848ce3 Mon Sep 17 00:00:00 2001 From: William Swanson Date: Mon, 13 Nov 2023 10:59:03 -0800 Subject: [PATCH 3/6] Save enabled tokens using tokenId's --- CHANGELOG.md | 1 + src/core/account/plugin-api.ts | 16 ++-- src/core/actions.ts | 9 ++- .../currency/wallet/currency-wallet-api.ts | 24 +++--- .../wallet/currency-wallet-callbacks.ts | 4 +- .../wallet/currency-wallet-cleaners.ts | 14 ++-- .../currency/wallet/currency-wallet-files.ts | 73 +++++++++++++------ .../currency/wallet/currency-wallet-pixie.ts | 28 +++---- .../wallet/currency-wallet-reducer.ts | 32 ++++---- 9 files changed, 113 insertions(+), 88 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23167a308..32a5185fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- changed: Save enabled tokens by their tokenId, not by their currency code. - fixed: Add missing `export` to the `EdgeCorePluginFactory` type definition. ## 1.11.0 (2023-10-18) diff --git a/src/core/account/plugin-api.ts b/src/core/account/plugin-api.ts index 66a356db6..d7f02f5ef 100644 --- a/src/core/account/plugin-api.ts +++ b/src/core/account/plugin-api.ts @@ -93,15 +93,15 @@ export class CurrencyConfig payload: { accountId, pluginId, tokenId: newTokenId, token } }) - // Do we need to tweak enabled tokens? - if (oldToken.currencyCode !== token.currencyCode) { + if (newTokenId !== tokenId) { + // Enable the new token if the tokenId changed: const { wallets } = ai.props.state.currency for (const walletId of Object.keys(wallets)) { const walletState = wallets[walletId] if ( walletState.accountId !== accountId || walletState.pluginId !== pluginId || - !walletState.enabledTokens.includes(oldToken.currencyCode) + !walletState.enabledTokenIds.includes(tokenId) ) { continue } @@ -112,17 +112,15 @@ export class CurrencyConfig type: 'CURRENCY_WALLET_ENABLED_TOKENS_CHANGED', payload: { walletId, - currencyCodes: uniqueStrings( - [...walletState.enabledTokens, token.currencyCode], - [oldToken.currencyCode] + enabledTokenIds: uniqueStrings( + [...walletState.enabledTokenIds, newTokenId], + [tokenId] ) } }) } - } - // Remove the old token if the tokenId changed: - if (newTokenId !== tokenId) { + // Remove the old token if the tokenId changed: ai.props.dispatch({ type: 'ACCOUNT_CUSTOM_TOKEN_REMOVED', payload: { accountId, pluginId, tokenId } diff --git a/src/core/actions.ts b/src/core/actions.ts index e9dbd35b3..9ca2a8224 100644 --- a/src/core/actions.ts +++ b/src/core/actions.ts @@ -255,7 +255,7 @@ export type RootAction = | { type: 'CURRENCY_WALLET_ENABLED_TOKENS_CHANGED' payload: { - currencyCodes: string[] + enabledTokenIds: string[] walletId: string } } @@ -295,6 +295,13 @@ export type RootAction = walletId: string } } + | { + type: 'CURRENCY_WALLET_LOADED_TOKEN_FILE' + payload: { + enabledTokenIds: string[] + walletId: string + } + } | { // Called when a currency wallet receives a new name. type: 'CURRENCY_WALLET_NAME_CHANGED' diff --git a/src/core/currency/wallet/currency-wallet-api.ts b/src/core/currency/wallet/currency-wallet-api.ts index 9fe764e8c..438a7816b 100644 --- a/src/core/currency/wallet/currency-wallet-api.ts +++ b/src/core/currency/wallet/currency-wallet-api.ts @@ -55,7 +55,6 @@ import { } from './currency-wallet-files' import { CurrencyWalletInput } from './currency-wallet-pixie' import { MergedTransaction } from './currency-wallet-reducer' -import { tokenIdsToCurrencyCodes, uniqueStrings } from './enabled-tokens' import { upgradeMemos } from './upgrade-memos' const fakeMetadata = { @@ -197,23 +196,18 @@ export function makeCurrencyWalletApi( // Tokens: async changeEnabledTokenIds(tokenIds: string[]): Promise { - const { dispatch, state, walletId, walletState } = input.props - const { builtinTokens, customTokens } = state.accounts[accountId] - const { currencyInfo } = walletState + const { dispatch, walletId, walletState } = input.props + const { accountId, pluginId } = walletState + const accountState = input.props.state.accounts[accountId] + const allTokens = accountState.allTokens[pluginId] ?? {} + + const enabledTokenIds = tokenIds.filter( + tokenId => allTokens[tokenId] != null + ) dispatch({ type: 'CURRENCY_WALLET_ENABLED_TOKENS_CHANGED', - payload: { - walletId, - currencyCodes: uniqueStrings( - tokenIdsToCurrencyCodes( - builtinTokens[pluginId], - customTokens[pluginId], - currencyInfo, - tokenIds - ) - ) - } + payload: { walletId, enabledTokenIds } }) }, diff --git a/src/core/currency/wallet/currency-wallet-callbacks.ts b/src/core/currency/wallet/currency-wallet-callbacks.ts index 838cf93d1..cb537a372 100644 --- a/src/core/currency/wallet/currency-wallet-callbacks.ts +++ b/src/core/currency/wallet/currency-wallet-callbacks.ts @@ -19,9 +19,9 @@ import { combineTxWithFile } from './currency-wallet-api' import { asIntegerString } from './currency-wallet-cleaners' import { loadAddressFiles, - loadEnabledTokensFile, loadFiatFile, loadNameFile, + loadTokensFile, loadTxFileNames, setupNewTxMetadata } from './currency-wallet-files' @@ -333,7 +333,7 @@ export function watchCurrencyWallet(input: CurrencyWalletInput): void { const changes = getStorageWalletLastChanges(props.state, walletId) if (changes !== lastChanges) { lastChanges = changes - await loadEnabledTokensFile(input) + await loadTokensFile(input) await loadFiatFile(input) await loadNameFile(input) await loadTxFileNames(input) diff --git a/src/core/currency/wallet/currency-wallet-cleaners.ts b/src/core/currency/wallet/currency-wallet-cleaners.ts index b7fb21670..d1fd06de7 100644 --- a/src/core/currency/wallet/currency-wallet-cleaners.ts +++ b/src/core/currency/wallet/currency-wallet-cleaners.ts @@ -194,12 +194,16 @@ export function asIntegerString(raw: unknown): string { // --------------------------------------------------------------------- /** - * This uses currency codes, since we cannot break the data on disk. - * To fix this one day, we can either migrate to a new file name, - * or we can use `asEither` to switch between this format - * and some new format based on token ID's. + * Old core versions used currency codes instead of tokenId's. */ -export const asEnabledTokensFile = asArray(asString) +export const asLegacyTokensFile = asArray(asString) + +/** + * Stores enabled tokenId's on disk. + */ +export const asTokensFile = asObject({ + enabledTokenIds: asArray(asString) +}) export const asTransactionFile = asObject({ txid: asString, diff --git a/src/core/currency/wallet/currency-wallet-files.ts b/src/core/currency/wallet/currency-wallet-files.ts index edb72490d..0bdc7e277 100644 --- a/src/core/currency/wallet/currency-wallet-files.ts +++ b/src/core/currency/wallet/currency-wallet-files.ts @@ -17,10 +17,11 @@ import { } from '../../storage/storage-selectors' import { combineTxWithFile } from './currency-wallet-api' import { - asEnabledTokensFile, asLegacyAddressFile, asLegacyMapFile, + asLegacyTokensFile, asLegacyTransactionFile, + asTokensFile, asTransactionFile, asWalletFiatFile, asWalletNameFile, @@ -31,16 +32,19 @@ import { } from './currency-wallet-cleaners' import { CurrencyWalletInput } from './currency-wallet-pixie' import { TxFileNames } from './currency-wallet-reducer' +import { currencyCodesToTokenIds } from './enabled-tokens' const CURRENCY_FILE = 'Currency.json' -const ENABLED_TOKENS_FILE = 'EnabledTokens.json' const LEGACY_MAP_FILE = 'fixedLegacyFileNames.json' +const LEGACY_TOKENS_FILE = 'EnabledTokens.json' +const TOKENS_FILE = 'Tokens.json' const WALLET_NAME_FILE = 'WalletName.json' -const enabledTokensFile = makeJsonFile(asEnabledTokensFile) const legacyAddressFile = makeJsonFile(asLegacyAddressFile) const legacyMapFile = makeJsonFile(asLegacyMapFile) +const legacyTokensFile = makeJsonFile(asLegacyTokensFile) const legacyTransactionFile = makeJsonFile(asLegacyTransactionFile) +const tokensFile = makeJsonFile(asTokensFile) const transactionFile = makeJsonFile(asTransactionFile) const walletFiatFile = makeJsonFile(asWalletFiatFile) const walletNameFile = makeJsonFile(asWalletNameFile) @@ -48,14 +52,14 @@ const walletNameFile = makeJsonFile(asWalletNameFile) /** * Updates the enabled tokens on a wallet. */ -export async function changeEnabledTokens( +export async function writeTokensFile( input: CurrencyWalletInput, - currencyCodes: string[] + enabledTokenIds: string[] ): Promise { const { state, walletId } = input.props const disklet = getStorageWalletDisklet(state, walletId) - await enabledTokensFile.save(disklet, ENABLED_TOKENS_FILE, currencyCodes) + await tokensFile.save(disklet, TOKENS_FILE, { enabledTokenIds }) } /** @@ -146,23 +150,6 @@ export async function setCurrencyWalletFiat( }) } -export async function loadEnabledTokensFile( - input: CurrencyWalletInput -): Promise { - const { dispatch, state, walletId } = input.props - const disklet = getStorageWalletDisklet(state, walletId) - - const clean = await enabledTokensFile.load(disklet, ENABLED_TOKENS_FILE) - if (clean == null) return - - // Future currencyCode to tokenId logic will live here. - - dispatch({ - type: 'CURRENCY_WALLET_ENABLED_TOKENS_CHANGED', - payload: { walletId: input.props.walletId, currencyCodes: clean } - }) -} - /** * Loads the wallet fiat currency file. */ @@ -219,6 +206,46 @@ export async function loadNameFile(input: CurrencyWalletInput): Promise { }) } +/** + * Load the enabled tokens file, with fallback to the legacy file. + */ +export async function loadTokensFile( + input: CurrencyWalletInput +): Promise { + const { dispatch, state, walletId } = input.props + const disklet = getStorageWalletDisklet(state, walletId) + + const clean = await tokensFile.load(disklet, TOKENS_FILE) + if (clean != null) { + dispatch({ + type: 'CURRENCY_WALLET_LOADED_TOKEN_FILE', + payload: { walletId: input.props.walletId, ...clean } + }) + return + } + + const legacyCurrencyCodes = await legacyTokensFile.load( + disklet, + LEGACY_TOKENS_FILE + ) + const { accountId, currencyInfo, pluginId } = input.props.walletState + const accountState = input.props.state.accounts[accountId] + const tokenIds = currencyCodesToTokenIds( + accountState.builtinTokens[pluginId], + accountState.customTokens[pluginId], + currencyInfo, + legacyCurrencyCodes ?? [] + ) + + dispatch({ + type: 'CURRENCY_WALLET_LOADED_TOKEN_FILE', + payload: { + walletId: input.props.walletId, + enabledTokenIds: tokenIds + } + }) +} + /** * Loads transaction metadata files. */ diff --git a/src/core/currency/wallet/currency-wallet-pixie.ts b/src/core/currency/wallet/currency-wallet-pixie.ts index bd72ca402..bd89b071d 100644 --- a/src/core/currency/wallet/currency-wallet-pixie.ts +++ b/src/core/currency/wallet/currency-wallet-pixie.ts @@ -39,16 +39,16 @@ import { } from './currency-wallet-callbacks' import { asIntegerString, asPublicKeyFile } from './currency-wallet-cleaners' import { - changeEnabledTokens, loadAddressFiles, - loadEnabledTokensFile, loadFiatFile, loadNameFile, - loadTxFileNames + loadTokensFile, + loadTxFileNames, + writeTokensFile } from './currency-wallet-files' import { CurrencyWalletState, - initialEnabledTokens + initialEnabledTokenIds } from './currency-wallet-reducer' import { tokenIdsToCurrencyCodes, uniqueStrings } from './enabled-tokens' @@ -94,10 +94,6 @@ export const walletPixie: TamePixie = combinePixies({ input.props.io ) - // We need to know which tokens are enabled, - // so the engine can start in the right state: - await loadEnabledTokensFile(input) - // We need to know which transactions exist, // since new transactions may come in from the network: await loadTxFileNames(input) @@ -114,6 +110,10 @@ export const walletPixie: TamePixie = combinePixies({ payload: { walletInfo: publicWalletInfo, walletId } }) + // We need to know which tokens are enabled, + // so the engine can start in the right state: + await loadTokensFile(input) + // Start the engine: const accountState = state.accounts[accountId] const engine = await plugin.makeCurrencyEngine(publicWalletInfo, { @@ -331,17 +331,17 @@ export const walletPixie: TamePixie = combinePixies({ * we will consolidate those down to a single write to disk. */ tokenSaver(input: CurrencyWalletInput) { - let lastEnabledTokens: string[] = initialEnabledTokens + let lastEnabledTokenIds: string[] = initialEnabledTokenIds return async function update() { - const { enabledTokens } = input.props.walletState - if (enabledTokens !== lastEnabledTokens && enabledTokens != null) { - await changeEnabledTokens(input, enabledTokens).catch(error => + const { enabledTokenIds } = input.props.walletState + if (enabledTokenIds !== lastEnabledTokenIds && enabledTokenIds != null) { + await writeTokensFile(input, enabledTokenIds).catch(error => input.props.onError(error) ) await snooze(100) // Rate limiting } - lastEnabledTokens = enabledTokens + lastEnabledTokenIds = enabledTokenIds } }, @@ -349,7 +349,7 @@ export const walletPixie: TamePixie = combinePixies({ let lastState: CurrencyWalletState | undefined let lastSettings: JsonObject = {} let lastTokens: EdgeTokenMap = {} - let lastEnabledTokenIds: string[] = initialEnabledTokens + let lastEnabledTokenIds: string[] = initialEnabledTokenIds return async () => { const { state, walletState, walletOutput } = input.props diff --git a/src/core/currency/wallet/currency-wallet-reducer.ts b/src/core/currency/wallet/currency-wallet-reducer.ts index 7e5ec2853..78cf3ad51 100644 --- a/src/core/currency/wallet/currency-wallet-reducer.ts +++ b/src/core/currency/wallet/currency-wallet-reducer.ts @@ -17,7 +17,7 @@ import { RootAction } from '../../actions' import { findCurrencyPluginId } from '../../plugins/plugins-selectors' import { RootState } from '../../root-reducer' import { TransactionFile } from './currency-wallet-cleaners' -import { currencyCodesToTokenIds, uniqueStrings } from './enabled-tokens' +import { uniqueStrings } from './enabled-tokens' /** Maps from txid hash to file creation date & path. */ export interface TxFileNames { @@ -67,7 +67,6 @@ export interface CurrencyWalletState { readonly balances: EdgeBalances readonly currencyInfo: EdgeCurrencyInfo readonly enabledTokenIds: string[] - readonly enabledTokens: string[] readonly engineFailure: Error | null readonly engineStarted: boolean readonly fiat: string @@ -94,7 +93,7 @@ export interface CurrencyWalletNext { readonly self: CurrencyWalletState } -export const initialEnabledTokens: string[] = [] +export const initialEnabledTokenIds: string[] = [] const currencyWalletInner = buildReducer< CurrencyWalletState, @@ -144,22 +143,13 @@ const currencyWalletInner = buildReducer< return next.root.plugins.currency[pluginId].currencyInfo }, - enabledTokenIds: memoizeReducer( - next => - next.root.accounts[next.self.accountId].builtinTokens[next.self.pluginId], - next => - next.root.accounts[next.self.accountId].customTokens[next.self.pluginId], - next => next.self.currencyInfo, - next => next.self.enabledTokens, - currencyCodesToTokenIds - ), - - enabledTokens(state = initialEnabledTokens, action): string[] { - if (action.type === 'CURRENCY_WALLET_ENABLED_TOKENS_CHANGED') { - const { currencyCodes } = action.payload - // Check for actual changes: - currencyCodes.sort((a, b) => (a === b ? 0 : a > b ? 1 : -1)) - if (!compare(currencyCodes, state)) return currencyCodes + enabledTokenIds(state = initialEnabledTokenIds, action): string[] { + if (action.type === 'CURRENCY_WALLET_LOADED_TOKEN_FILE') { + return action.payload.enabledTokenIds + } else if (action.type === 'CURRENCY_WALLET_ENABLED_TOKENS_CHANGED') { + const { enabledTokenIds } = action.payload + const sorted = sortTokenIds(enabledTokenIds) + if (!compare(sorted, state)) return sorted } return state }, @@ -451,3 +441,7 @@ export function mergeTx( return out } + +function sortTokenIds(tokenIds: string[]): string[] { + return tokenIds.sort((a, b) => (a === b ? 0 : a > b ? 1 : -1)) +} From b1aa06c59b093793df802ef7df24613cab88674d Mon Sep 17 00:00:00 2001 From: William Swanson Date: Tue, 14 Nov 2023 10:35:48 -0800 Subject: [PATCH 4/6] Save a `detectedTokenIds` array on disk --- CHANGELOG.md | 1 + src/core/actions.ts | 1 + .../currency/wallet/currency-wallet-api.ts | 4 ++++ .../wallet/currency-wallet-cleaners.ts | 8 ++++++- .../currency/wallet/currency-wallet-files.ts | 7 +++++- .../currency/wallet/currency-wallet-pixie.ts | 23 +++++++++++-------- .../wallet/currency-wallet-reducer.ts | 15 ++++++++++-- src/types/types.ts | 3 +++ 8 files changed, 48 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32a5185fa..7743f2088 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- added: Expose auto-detected tokens as `EdgeCurrencyWallet.detectedTokenIds`. - changed: Save enabled tokens by their tokenId, not by their currency code. - fixed: Add missing `export` to the `EdgeCorePluginFactory` type definition. diff --git a/src/core/actions.ts b/src/core/actions.ts index 9ca2a8224..a82cfb04d 100644 --- a/src/core/actions.ts +++ b/src/core/actions.ts @@ -298,6 +298,7 @@ export type RootAction = | { type: 'CURRENCY_WALLET_LOADED_TOKEN_FILE' payload: { + detectedTokenIds: string[] enabledTokenIds: string[] walletId: string } diff --git a/src/core/currency/wallet/currency-wallet-api.ts b/src/core/currency/wallet/currency-wallet-api.ts index 438a7816b..7f79f57e9 100644 --- a/src/core/currency/wallet/currency-wallet-api.ts +++ b/src/core/currency/wallet/currency-wallet-api.ts @@ -211,6 +211,10 @@ export function makeCurrencyWalletApi( }) }, + get detectedTokenIds(): string[] { + return input.props.walletState.detectedTokenIds + }, + get enabledTokenIds(): string[] { return input.props.walletState.enabledTokenIds }, diff --git a/src/core/currency/wallet/currency-wallet-cleaners.ts b/src/core/currency/wallet/currency-wallet-cleaners.ts index d1fd06de7..35f9ace70 100644 --- a/src/core/currency/wallet/currency-wallet-cleaners.ts +++ b/src/core/currency/wallet/currency-wallet-cleaners.ts @@ -202,7 +202,13 @@ export const asLegacyTokensFile = asArray(asString) * Stores enabled tokenId's on disk. */ export const asTokensFile = asObject({ - enabledTokenIds: asArray(asString) + // All the tokens that the engine should check. + // This includes both manually-enabled tokens and auto-enabled tokens: + enabledTokenIds: asArray(asString), + + // These tokenId's have been detected on-chain at least once. + // The user can still remove them from the enabled tokens list. + detectedTokenIds: asArray(asString) }) export const asTransactionFile = asObject({ diff --git a/src/core/currency/wallet/currency-wallet-files.ts b/src/core/currency/wallet/currency-wallet-files.ts index 0bdc7e277..6af0dde89 100644 --- a/src/core/currency/wallet/currency-wallet-files.ts +++ b/src/core/currency/wallet/currency-wallet-files.ts @@ -54,12 +54,16 @@ const walletNameFile = makeJsonFile(asWalletNameFile) */ export async function writeTokensFile( input: CurrencyWalletInput, + detectedTokenIds: string[], enabledTokenIds: string[] ): Promise { const { state, walletId } = input.props const disklet = getStorageWalletDisklet(state, walletId) - await tokensFile.save(disklet, TOKENS_FILE, { enabledTokenIds }) + await tokensFile.save(disklet, TOKENS_FILE, { + detectedTokenIds, + enabledTokenIds + }) } /** @@ -241,6 +245,7 @@ export async function loadTokensFile( type: 'CURRENCY_WALLET_LOADED_TOKEN_FILE', payload: { walletId: input.props.walletId, + detectedTokenIds: [], enabledTokenIds: tokenIds } }) diff --git a/src/core/currency/wallet/currency-wallet-pixie.ts b/src/core/currency/wallet/currency-wallet-pixie.ts index bd89b071d..0f445955b 100644 --- a/src/core/currency/wallet/currency-wallet-pixie.ts +++ b/src/core/currency/wallet/currency-wallet-pixie.ts @@ -46,10 +46,7 @@ import { loadTxFileNames, writeTokensFile } from './currency-wallet-files' -import { - CurrencyWalletState, - initialEnabledTokenIds -} from './currency-wallet-reducer' +import { CurrencyWalletState, initialTokenIds } from './currency-wallet-reducer' import { tokenIdsToCurrencyCodes, uniqueStrings } from './enabled-tokens' export interface CurrencyWalletOutput { @@ -331,16 +328,22 @@ export const walletPixie: TamePixie = combinePixies({ * we will consolidate those down to a single write to disk. */ tokenSaver(input: CurrencyWalletInput) { - let lastEnabledTokenIds: string[] = initialEnabledTokenIds + let lastDetectedTokenIds: string[] = initialTokenIds + let lastEnabledTokenIds: string[] = initialTokenIds return async function update() { - const { enabledTokenIds } = input.props.walletState - if (enabledTokenIds !== lastEnabledTokenIds && enabledTokenIds != null) { - await writeTokensFile(input, enabledTokenIds).catch(error => - input.props.onError(error) + const { detectedTokenIds, enabledTokenIds } = input.props.walletState + const isChanged = + detectedTokenIds !== lastDetectedTokenIds || + enabledTokenIds !== lastEnabledTokenIds + const isReady = detectedTokenIds != null && enabledTokenIds != null + if (isChanged && isReady) { + await writeTokensFile(input, detectedTokenIds, enabledTokenIds).catch( + error => input.props.onError(error) ) await snooze(100) // Rate limiting } + lastDetectedTokenIds = detectedTokenIds lastEnabledTokenIds = enabledTokenIds } }, @@ -349,7 +352,7 @@ export const walletPixie: TamePixie = combinePixies({ let lastState: CurrencyWalletState | undefined let lastSettings: JsonObject = {} let lastTokens: EdgeTokenMap = {} - let lastEnabledTokenIds: string[] = initialEnabledTokenIds + let lastEnabledTokenIds: string[] = initialTokenIds return async () => { const { state, walletState, walletOutput } = input.props diff --git a/src/core/currency/wallet/currency-wallet-reducer.ts b/src/core/currency/wallet/currency-wallet-reducer.ts index 78cf3ad51..09d79b5bb 100644 --- a/src/core/currency/wallet/currency-wallet-reducer.ts +++ b/src/core/currency/wallet/currency-wallet-reducer.ts @@ -66,6 +66,7 @@ export interface CurrencyWalletState { readonly allEnabledTokenIds: string[] readonly balances: EdgeBalances readonly currencyInfo: EdgeCurrencyInfo + readonly detectedTokenIds: string[] readonly enabledTokenIds: string[] readonly engineFailure: Error | null readonly engineStarted: boolean @@ -93,7 +94,8 @@ export interface CurrencyWalletNext { readonly self: CurrencyWalletState } -export const initialEnabledTokenIds: string[] = [] +// Used for detectedTokenIds & enabledTokenIds: +export const initialTokenIds: string[] = [] const currencyWalletInner = buildReducer< CurrencyWalletState, @@ -143,7 +145,16 @@ const currencyWalletInner = buildReducer< return next.root.plugins.currency[pluginId].currencyInfo }, - enabledTokenIds(state = initialEnabledTokenIds, action): string[] { + detectedTokenIds(state = initialTokenIds, action): string[] { + if (action.type === 'CURRENCY_WALLET_LOADED_TOKEN_FILE') { + return action.payload.detectedTokenIds + } else if (action.type === 'CURRENCY_ENGINE_CLEARED') { + return [] + } + return state + }, + + enabledTokenIds(state = initialTokenIds, action): string[] { if (action.type === 'CURRENCY_WALLET_LOADED_TOKEN_FILE') { return action.payload.enabledTokenIds } else if (action.type === 'CURRENCY_WALLET_ENABLED_TOKENS_CHANGED') { diff --git a/src/types/types.ts b/src/types/types.ts index a3cf301b5..5ccd8d2ac 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -1070,6 +1070,9 @@ export interface EdgeCurrencyWallet { readonly changeEnabledTokenIds: (tokenIds: string[]) => Promise readonly enabledTokenIds: string[] + /* Tokens detected on chain */ + readonly detectedTokenIds: string[] + // Transaction history: readonly getNumTransactions: ( opts?: EdgeCurrencyCodeOptions From 80d4123780ed5a969744ad5e1f0731ad7a088351 Mon Sep 17 00:00:00 2001 From: William Swanson Date: Tue, 14 Nov 2023 15:24:38 -0800 Subject: [PATCH 5/6] Add an `onNewTokens` engine event --- CHANGELOG.md | 2 ++ src/core/actions.ts | 9 +++++ .../wallet/currency-wallet-callbacks.ts | 34 +++++++++++++++++++ .../wallet/currency-wallet-reducer.ts | 9 +++++ src/types/types.ts | 4 ++- 5 files changed, 57 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7743f2088..c4fd4198a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- added: Accept an `onNewTokens` callback from `EdgeCurrencyEngine`. +- added: Emit an `enabledDetectedTokens` event when auto-enabling tokens. - added: Expose auto-detected tokens as `EdgeCurrencyWallet.detectedTokenIds`. - changed: Save enabled tokens by their tokenId, not by their currency code. - fixed: Add missing `export` to the `EdgeCorePluginFactory` type definition. diff --git a/src/core/actions.ts b/src/core/actions.ts index a82cfb04d..62787bebc 100644 --- a/src/core/actions.ts +++ b/src/core/actions.ts @@ -201,6 +201,15 @@ export type RootAction = walletId: string } } + | { + // Called when a currency engine fires the onNewTokens callback. + type: 'CURRENCY_ENGINE_DETECTED_TOKENS' + payload: { + detectedTokenIds: string[] + enablingTokenIds: string[] + walletId: string + } + } | { type: 'CURRENCY_ENGINE_GOT_TXS' payload: { diff --git a/src/core/currency/wallet/currency-wallet-callbacks.ts b/src/core/currency/wallet/currency-wallet-callbacks.ts index cb537a372..250bce0d4 100644 --- a/src/core/currency/wallet/currency-wallet-callbacks.ts +++ b/src/core/currency/wallet/currency-wallet-callbacks.ts @@ -34,6 +34,7 @@ import { mergeTx, TxidHashes } from './currency-wallet-reducer' +import { uniqueStrings } from './enabled-tokens' let throttleRateLimitMs = 5000 @@ -122,6 +123,39 @@ export function makeCurrencyWalletCallbacks( }) }, + onNewTokens(tokenIds: string[]) { + pushUpdate({ + id: walletId, + action: 'onNewTokens', + updateFunc: () => { + // Before we update redux, figure out what's truly new: + const { detectedTokenIds, enabledTokenIds } = input.props.walletState + const enablingTokenIds = uniqueStrings(tokenIds, [ + ...detectedTokenIds, + ...enabledTokenIds + ]) + + // Update redux: + input.props.dispatch({ + type: 'CURRENCY_ENGINE_DETECTED_TOKENS', + payload: { + detectedTokenIds: tokenIds, + enablingTokenIds, + walletId + } + }) + + // Fire an event to the GUI: + if (enablingTokenIds.length > 0) { + const walletApi = input.props?.walletOutput?.walletApi + if (walletApi != null) { + emit(walletApi, 'enabledDetectedTokens', enablingTokenIds) + } + } + } + }) + }, + onUnactivatedTokenIdsChanged(unactivatedTokenIds: string[]) { pushUpdate({ id: walletId, diff --git a/src/core/currency/wallet/currency-wallet-reducer.ts b/src/core/currency/wallet/currency-wallet-reducer.ts index 09d79b5bb..9d279b456 100644 --- a/src/core/currency/wallet/currency-wallet-reducer.ts +++ b/src/core/currency/wallet/currency-wallet-reducer.ts @@ -148,6 +148,12 @@ const currencyWalletInner = buildReducer< detectedTokenIds(state = initialTokenIds, action): string[] { if (action.type === 'CURRENCY_WALLET_LOADED_TOKEN_FILE') { return action.payload.detectedTokenIds + } else if (action.type === 'CURRENCY_ENGINE_DETECTED_TOKENS') { + const { detectedTokenIds } = action.payload + const mergedList = sortTokenIds( + uniqueStrings([...state, ...detectedTokenIds]) + ) + if (!compare(mergedList, state)) return mergedList } else if (action.type === 'CURRENCY_ENGINE_CLEARED') { return [] } @@ -161,6 +167,9 @@ const currencyWalletInner = buildReducer< const { enabledTokenIds } = action.payload const sorted = sortTokenIds(enabledTokenIds) if (!compare(sorted, state)) return sorted + } else if (action.type === 'CURRENCY_ENGINE_DETECTED_TOKENS') { + const { enablingTokenIds } = action.payload + return sortTokenIds(uniqueStrings([...state, ...enablingTokenIds])) } return state }, diff --git a/src/types/types.ts b/src/types/types.ts index 5ccd8d2ac..baef4ea1c 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -773,6 +773,7 @@ export interface EdgeCurrencyEngineCallbacks { currencyCode: string, nativeBalance: string ) => void + readonly onNewTokens: (tokenIds: string[]) => void readonly onStakingStatusChanged: (status: EdgeStakingStatus) => void readonly onTransactionsChanged: (transactions: EdgeTransaction[]) => void readonly onTxidsChanged: (txids: EdgeTxidMap) => void @@ -970,9 +971,10 @@ export type EdgeReceiveAddress = EdgeFreshAddress & { } export interface EdgeCurrencyWalletEvents { + addressChanged: void close: void + enabledDetectedTokens: string[] newTransactions: EdgeTransaction[] - addressChanged: void transactionsChanged: EdgeTransaction[] wcNewContractCall: JsonObject } From 1c12c67564e572a37a9df26998d9d74319e842e5 Mon Sep 17 00:00:00 2001 From: William Swanson Date: Wed, 29 Nov 2023 13:12:04 -0800 Subject: [PATCH 6/6] Avoid excessive writes to the tokens file --- .../wallet/currency-wallet-reducer.ts | 65 +++++++++++-------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/src/core/currency/wallet/currency-wallet-reducer.ts b/src/core/currency/wallet/currency-wallet-reducer.ts index 9d279b456..4b9ae02be 100644 --- a/src/core/currency/wallet/currency-wallet-reducer.ts +++ b/src/core/currency/wallet/currency-wallet-reducer.ts @@ -145,34 +145,33 @@ const currencyWalletInner = buildReducer< return next.root.plugins.currency[pluginId].currencyInfo }, - detectedTokenIds(state = initialTokenIds, action): string[] { - if (action.type === 'CURRENCY_WALLET_LOADED_TOKEN_FILE') { - return action.payload.detectedTokenIds - } else if (action.type === 'CURRENCY_ENGINE_DETECTED_TOKENS') { - const { detectedTokenIds } = action.payload - const mergedList = sortTokenIds( - uniqueStrings([...state, ...detectedTokenIds]) - ) - if (!compare(mergedList, state)) return mergedList - } else if (action.type === 'CURRENCY_ENGINE_CLEARED') { - return [] + detectedTokenIds: sortStringsReducer( + (state = initialTokenIds, action): string[] => { + if (action.type === 'CURRENCY_WALLET_LOADED_TOKEN_FILE') { + return action.payload.detectedTokenIds + } else if (action.type === 'CURRENCY_ENGINE_DETECTED_TOKENS') { + const { detectedTokenIds } = action.payload + return uniqueStrings([...state, ...detectedTokenIds]) + } else if (action.type === 'CURRENCY_ENGINE_CLEARED') { + return [] + } + return state } - return state - }, + ), - enabledTokenIds(state = initialTokenIds, action): string[] { - if (action.type === 'CURRENCY_WALLET_LOADED_TOKEN_FILE') { - return action.payload.enabledTokenIds - } else if (action.type === 'CURRENCY_WALLET_ENABLED_TOKENS_CHANGED') { - const { enabledTokenIds } = action.payload - const sorted = sortTokenIds(enabledTokenIds) - if (!compare(sorted, state)) return sorted - } else if (action.type === 'CURRENCY_ENGINE_DETECTED_TOKENS') { - const { enablingTokenIds } = action.payload - return sortTokenIds(uniqueStrings([...state, ...enablingTokenIds])) + enabledTokenIds: sortStringsReducer( + (state = initialTokenIds, action): string[] => { + if (action.type === 'CURRENCY_WALLET_LOADED_TOKEN_FILE') { + return action.payload.enabledTokenIds + } else if (action.type === 'CURRENCY_WALLET_ENABLED_TOKENS_CHANGED') { + return action.payload.enabledTokenIds + } else if (action.type === 'CURRENCY_ENGINE_DETECTED_TOKENS') { + const { enablingTokenIds } = action.payload + return uniqueStrings([...state, ...enablingTokenIds]) + } + return state } - return state - }, + ), engineFailure(state = null, action): Error | null { if (action.type === 'CURRENCY_ENGINE_FAILED') { @@ -462,6 +461,18 @@ export function mergeTx( return out } -function sortTokenIds(tokenIds: string[]): string[] { - return tokenIds.sort((a, b) => (a === b ? 0 : a > b ? 1 : -1)) +type StringsReducer = ( + state: string[] | undefined, + action: RootAction +) => string[] + +function sortStringsReducer(reducer: StringsReducer): StringsReducer { + return (state, action) => { + const out = reducer(state, action) + if (out === state) return state + + out.sort((a, b) => (a === b ? 0 : a > b ? 1 : -1)) + if (state == null || !compare(out, state)) return out + return state + } }