From 0bbb762cd519ef06eee0fdb1b00c8e0567046d3c Mon Sep 17 00:00:00 2001 From: Gustavo Cortez Date: Tue, 17 Mar 2026 00:14:26 -0300 Subject: [PATCH 1/5] Performance: Logs - Metrics for createKey and Import --- src/store/wallet/effects/address/address.ts | 77 +++++++ src/store/wallet/effects/create/create.ts | 234 +++++++++++++++++++- src/store/wallet/effects/import/import.ts | 127 +++++++++++ 3 files changed, 432 insertions(+), 6 deletions(-) diff --git a/src/store/wallet/effects/address/address.ts b/src/store/wallet/effects/address/address.ts index 295f3a6bd5..70a37976fd 100644 --- a/src/store/wallet/effects/address/address.ts +++ b/src/store/wallet/effects/address/address.ts @@ -6,6 +6,34 @@ import {Effect} from '../../../index'; import {successGetReceiveAddress} from '../../wallet.actions'; import {logManager} from '../../../../managers/LogManager'; +const perfNow = () => + typeof global?.performance !== 'undefined' && + typeof global.performance?.now === 'function' + ? global.performance.now() + : Date.now(); + +const perfInfo = ( + label: string, + start: number, + extra?: Record, +) => { + const ms = perfNow() - start; + const suffix = extra ? ` | ${JSON.stringify(extra)}` : ''; + logManager.info(`[ADDRESS PERF] ${label}: ${ms.toFixed(1)}ms${suffix}`); + return ms; +}; + +const perfError = ( + label: string, + start: number, + extra?: Record, +) => { + const ms = perfNow() - start; + const suffix = extra ? ` | ${JSON.stringify(extra)}` : ''; + logManager.error(`[ADDRESS PERF] ${label}: ${ms.toFixed(1)}ms${suffix}`); + return ms; +}; + const BWC = BwcProvider.getInstance(); const Bitcore = BWC.getBitcore(); @@ -95,11 +123,30 @@ export const createWalletAddress = wallet.id.replace(`-${token.address}`, ''); } + const totalStart = perfNow(); + logManager.info( + `[ADDRESS PERF] createWalletAddress.start | ${JSON.stringify({ + walletId: wallet?.credentials?.walletId, + coin: wallet?.credentials?.coin, + chain: wallet?.credentials?.chain, + newAddress, + })}`, + ); + try { let addressObj: Address | undefined; if (!newAddress) { + const getLatestMainAddressStart = perfNow(); addressObj = await getLatestMainAddress(wallet); + perfInfo( + `createWalletAddress.getLatestMainAddress.${wallet.credentials.chain}.${wallet.credentials.coin}`, + getLatestMainAddressStart, + { + walletId: wallet.credentials.walletId, + }, + ); + validateOrThrow(addressObj, network); if (addressObj?.address) { @@ -114,15 +161,38 @@ export const createWalletAddress = ); } logManager.info('returned last main address'); + perfInfo( + `createWalletAddress.TOTAL.${wallet.credentials.chain}.${wallet.credentials.coin}`, + totalStart, + { + walletId: wallet.credentials.walletId, + }, + ); return resolve(addressObj.address); } } // if no main address, generate a new one try { + const getNewAddressStart = perfNow(); addressObj = await getNewAddress(wallet); + perfInfo( + `createWalletAddress.getNewAddress.${wallet.credentials.chain}.${wallet.credentials.coin}`, + getNewAddressStart, + { + walletId: wallet.credentials.walletId, + }, + ); } catch (err: any) { if (err?.name?.includes?.('MAIN_ADDRESS_GAP_REACHED')) { + const getLatestMainAddressStart2 = perfNow(); const latest = await getLatestMainAddress(wallet); + perfInfo( + `createWalletAddress.getLatestMainAddress2.${wallet.credentials.chain}.${wallet.credentials.coin}`, + getLatestMainAddressStart2, + { + walletId: wallet.credentials.walletId, + }, + ); validateOrThrow(latest, network); const receiveAddress = latest.address; wallet.receiveAddress = receiveAddress; @@ -155,6 +225,13 @@ export const createWalletAddress = ); } logManager.info('returned new generated address'); + perfInfo( + `createWalletAddress.TOTAL.${wallet.credentials.chain}.${wallet.credentials.coin}`, + totalStart, + { + walletId: wallet.credentials.walletId, + }, + ); return resolve(addressObj.address); } return reject({type: 'GENERAL_ERROR', error: 'No address generated'}); diff --git a/src/store/wallet/effects/create/create.ts b/src/store/wallet/effects/create/create.ts index 4bead68605..849f0aea43 100644 --- a/src/store/wallet/effects/create/create.ts +++ b/src/store/wallet/effects/create/create.ts @@ -61,6 +61,34 @@ import {tokenManager} from '../../../../managers/TokenManager'; import {logManager} from '../../../../managers/LogManager'; import {Analytics} from '../../../analytics/analytics.effects'; +const perfNow = () => + typeof global?.performance !== 'undefined' && + typeof global.performance?.now === 'function' + ? global.performance.now() + : Date.now(); + +const perfInfo = ( + label: string, + start: number, + extra?: Record, +) => { + const ms = perfNow() - start; + const suffix = extra ? ` | ${JSON.stringify(extra)}` : ''; + logManager.info(`[CREATEKEY PERF] ${label}: ${ms.toFixed(1)}ms${suffix}`); + return ms; +}; + +const perfError = ( + label: string, + start: number, + extra?: Record, +) => { + const ms = perfNow() - start; + const suffix = extra ? ` | ${JSON.stringify(extra)}` : ''; + logManager.error(`[CREATEKEY PERF] ${label}: ${ms.toFixed(1)}ms${suffix}`); + return ms; +}; + export interface CreateOptions { network?: Network; account?: number; @@ -102,14 +130,31 @@ export const startCreateKey = ): Effect> => async (dispatch, getState) => { return new Promise(async (resolve, reject) => { + const totalStart = perfNow(); try { const state = getState(); const network = state.APP.network; + logManager.info( + `[CREATEKEY PERF] createKey.start | ${JSON.stringify({ + context, + network, + currenciesCount: currencies?.length ?? 0, + currencies: currencies?.map(c => ({ + chain: c.chain, + coin: c.currencyAbbreviation, + isToken: c.isToken, + })), + })}`, + ); + + const rootKeyStart = perfNow(); const _key = BWC.createKey({ seedType: 'new', }); + perfInfo('createKey.BWC.createKey', rootKeyStart); + const createMultipleWalletsStart = perfNow(); const wallets = await dispatch( createMultipleWallets({ key: _key, @@ -119,18 +164,39 @@ export const startCreateKey = }, }), ); + perfInfo( + 'createKey.createMultipleWallets', + createMultipleWalletsStart, + { + walletCount: wallets?.length ?? 0, + }, + ); + const buildKeyStart = perfNow(); const key = buildKeyObj({key: _key, wallets}); + perfInfo('createKey.buildKeyObj', buildKeyStart, { + walletCount: key?.wallets?.length ?? 0, + }); + if (context !== 'onboarding') { dispatch(Analytics.track('Created Key')); } + const dispatchStart = perfNow(); dispatch( successCreateKey({ key, }), ); + perfInfo('createKey.successCreateKey.dispatch', dispatchStart); + + perfInfo('createKey.TOTAL', totalStart, { + walletCount: wallets?.length ?? 0, + }); resolve(key); } catch (err) { + perfError('createKey.TOTAL.ERROR', totalStart, { + message: err instanceof Error ? err.message : JSON.stringify(err), + }); reject(err); } }); @@ -400,6 +466,7 @@ export const createMultipleWallets = options: CreateOptions; }): Effect> => async (dispatch, getState) => { + const totalStart = perfNow(); const { WALLET, APP: { @@ -419,8 +486,28 @@ export const createMultipleWallets = const wallets: API[] = []; const tokens = currencies.filter(({isToken}) => isToken); const coins = currencies.filter(({isToken}) => !isToken); + + logManager.info( + `[CREATEKEY PERF] createMultipleWallets.start | ${JSON.stringify({ + baseCoins: coins.length, + tokens: tokens.length, + network: options?.network, + })}`, + ); + for (const coin of coins) { + const coinStart = perfNow(); try { + logManager.info( + `[CREATEKEY PERF] createMultipleWallets.coin.start | ${JSON.stringify( + { + chain: coin.chain, + coin: coin.currencyAbbreviation, + }, + )}`, + ); + + const walletCreateStart = perfNow(); const wallet = (await dispatch( createWallet({ key, @@ -432,15 +519,29 @@ export const createMultipleWallets = }, }), )) as Wallet; + perfInfo( + `createMultipleWallets.createWallet.${coin.chain}.${coin.currencyAbbreviation}`, + walletCreateStart, + {walletId: wallet?.credentials?.walletId}, + ); + const addressStart = perfNow(); const receiveAddress = (await dispatch( createWalletAddress({wallet, newAddress: true}), )) as string; + perfInfo( + `createMultipleWallets.createWalletAddress.${coin.chain}.${coin.currencyAbbreviation}`, + addressStart, + { + walletId: wallet?.credentials?.walletId, + }, + ); logManager.info(`new address generated: ${receiveAddress}`); wallet.receiveAddress = receiveAddress; wallets.push(wallet); for (const token of tokens) { if (token.chain === coin.chain) { + const tokenStart = perfNow(); const tokenWallet = await dispatch( createTokenWallet( wallet, @@ -449,19 +550,45 @@ export const createMultipleWallets = tokenOpts, ), ); + perfInfo( + `createMultipleWallets.createTokenWallet.${token.chain}.${token.currencyAbbreviation}`, + tokenStart, + { + parentWalletId: wallet?.credentials?.walletId, + tokenWalletId: (tokenWallet as Wallet | undefined)?.credentials + ?.walletId, + }, + ); if (tokenWallet) { wallets.push(tokenWallet); } } } + const chainTokens = tokens.filter(token => token.chain === coin.chain); + perfInfo( + `createMultipleWallets.coin.TOTAL.${coin.chain}.${coin.currencyAbbreviation}`, + coinStart, + { + walletId: wallet?.credentials?.walletId, + tokenCount: chainTokens.length, + }, + ); } catch (err) { + perfError( + `createMultipleWallets.coin.ERROR.${coin.chain}.${coin.currencyAbbreviation}`, + coinStart, + { + message: err instanceof Error ? err.message : JSON.stringify(err), + }, + ); const errMsg = err instanceof Error ? err.message : JSON.stringify(err); logManager.debug(`Error creating wallet - continue anyway: ${errMsg}`); } } + const buildStart = perfNow(); // build out app specific props - return wallets.map(wallet => { + const result = wallets.map(wallet => { // subscribe new wallet to push notifications if (notificationsAccepted) { dispatch(subscribePushNotifications(wallet, brazeEid!)); @@ -494,6 +621,22 @@ export const createMultipleWallets = ), ); }); + + perfInfo( + 'createMultipleWallets.buildWalletObjectsAndSubscriptions', + buildStart, + { + walletCount: result.length, + }, + ); + + perfInfo('createMultipleWallets.TOTAL', totalStart, { + walletCount: result.length, + baseCoins: coins.length, + tokens: tokens.length, + }); + + return result; }; ///////////////////////////////////////////////////////////// @@ -513,6 +656,7 @@ const createWallet = }): Effect> => async (dispatch): Promise => { return new Promise((resolve, reject) => { + const totalStart = perfNow(); const bwcClient = BWC.getClient(); const {key, coin: _coin, chain, options, context} = params; const coin = _coin === 'pol' ? 'matic' : _coin; // for creating a polygon wallet, we use matic as symbol @@ -530,21 +674,44 @@ const createWallet = ...options, }; - bwcClient.fromString( - key.createCredentials(password, { + logManager.info( + `[CREATEKEY PERF] createWallet.start | ${JSON.stringify({ coin, chain, + account, network, + context, + })}`, + ); + + const credentialsStart = perfNow(); + const credentials = key.createCredentials(password, { + coin, + chain, + network, + account, + n: 1, + m: 1, + }); + perfInfo( + `createWallet.createCredentials.${chain}.${coin}`, + credentialsStart, + { account, - n: 1, - m: 1, - }), + }, ); + const fromStringStart = perfNow(); + bwcClient.fromString(credentials); + perfInfo(`createWallet.fromString.${chain}.${coin}`, fromStringStart, { + account, + }); + const name = isL2NoSideChainNetwork(chain) && coin === chain ? BitpaySupportedCoins[coin].name : BitpaySupportedCoins[chain.toLowerCase()].name; + const apiStart = perfNow(); bwcClient.createWallet( name, 'me', @@ -560,6 +727,15 @@ const createWallet = }, (err: any) => { if (err) { + perfError( + `createWallet.createWalletAPI.ERROR.${chain}.${coin}`, + apiStart, + { + account, + name: err?.name, + message: err?.message, + }, + ); switch (err.name) { case 'bwc.ErrorCOPAYER_REGISTERED': { if (context === 'WalletConnect') { @@ -579,6 +755,18 @@ const createWallet = ), ); } + + logManager.info( + `[CREATEKEY PERF] createWallet.retry.COPAYER_REGISTERED | ${JSON.stringify( + { + coin, + chain, + previousAccount: account, + nextAccount: account + 1, + }, + )}`, + ); + return resolve( dispatch( createWallet({ @@ -594,6 +782,17 @@ const createWallet = reject(err); } else { + perfInfo( + `createWallet.createWalletAPI.${chain}.${coin}`, + apiStart, + { + account, + }, + ); + perfInfo(`createWallet.TOTAL.${chain}.${coin}`, totalStart, { + account, + walletId: bwcClient?.credentials?.walletId, + }); logManager.info(`Added Coin: ${chain}: ${coin}`); resolve(bwcClient); } @@ -613,7 +812,16 @@ const createTokenWallet = ): Effect> => async (dispatch): Promise => { return new Promise((resolve, reject) => { + const totalStart = perfNow(); try { + logManager.info( + `[CREATEKEY PERF] createTokenWallet.start | ${JSON.stringify({ + chain: associatedWallet.credentials.chain, + parentWalletId: associatedWallet.credentials.walletId, + tokenName, + tokenAddress, + })}`, + ); const bwcClient = BWC.getClient(); const tokenAddressWithSuffix = addTokenChainSuffix( tokenAddress, @@ -698,7 +906,21 @@ const createTokenWallet = resolve(bwcClient); }, ); + perfInfo( + `createTokenWallet.TOTAL.${associatedWallet.credentials.chain}.${tokenName}`, + totalStart, + { + parentWalletId: associatedWallet.credentials.walletId, + }, + ); } catch (err) { + perfError( + `createTokenWallet.ERROR.${associatedWallet.credentials.chain}.${tokenName}`, + totalStart, + { + message: err instanceof Error ? err.message : JSON.stringify(err), + }, + ); const errstring = err instanceof Error ? err.message : JSON.stringify(err); logManager.error(`Error creating token wallet: ${errstring}`); diff --git a/src/store/wallet/effects/import/import.ts b/src/store/wallet/effects/import/import.ts index 19117d9a4b..a6526bbd74 100644 --- a/src/store/wallet/effects/import/import.ts +++ b/src/store/wallet/effects/import/import.ts @@ -107,6 +107,34 @@ import { import {tokenManager} from '../../../../managers/TokenManager'; import {logManager} from '../../../../managers/LogManager'; +const perfNow = () => + typeof global?.performance !== 'undefined' && + typeof global.performance?.now === 'function' + ? global.performance.now() + : Date.now(); + +const perfInfo = ( + label: string, + start: number, + extra?: Record, +) => { + const ms = perfNow() - start; + const suffix = extra ? ` | ${JSON.stringify(extra)}` : ''; + logManager.info(`[IMPORT PERF] ${label}: ${ms.toFixed(1)}ms${suffix}`); + return ms; +}; + +const perfError = ( + label: string, + start: number, + extra?: Record, +) => { + const ms = perfNow() - start; + const suffix = extra ? ` | ${JSON.stringify(extra)}` : ''; + logManager.error(`[IMPORT PERF] ${label}: ${ms.toFixed(1)}ms${suffix}`); + return ms; +}; + const BWC = BwcProvider.getInstance(); const BwcConstants = BWC.getConstants(); @@ -848,6 +876,7 @@ export const startImportMnemonic = ): Effect => async (dispatch, getState): Promise => { return new Promise(async (resolve, reject) => { + const totalStart = perfNow(); try { const { WALLET, @@ -866,24 +895,59 @@ export const startImportMnemonic = ...WALLET.customTokenOptionsByAddress, }; const {words, xPrivKey} = importData; + + logManager.info( + `[IMPORT PERF] import.start | ${JSON.stringify({ + hasWords: !!words, + hasXPrivKey: !!xPrivKey, + hasPassphrase: !!opts?.passphrase, + })}`, + ); + + const normalizeStart = perfNow(); opts.words = normalizeMnemonic(words); opts.xPrivKey = xPrivKey; + perfInfo('import.normalizeMnemonic', normalizeStart, { + hasWords: !!opts.words, + hasXPrivKey: !!opts.xPrivKey, + }); + const serverImportStart = perfNow(); const data = await serverAssistedImport(opts); + perfInfo('import.serverAssistedImport', serverImportStart, { + walletCount: data?.wallets?.length ?? 0, + }); + + const getEvmGasWalletsStart = perfNow(); // we need to ensure that each evm/svm account has all supported wallets attached. const vmWallets = getEvmGasWallets(data.wallets); const accountsArray = [ ...new Set(vmWallets.map(wallet => wallet.credentials.account)), ]; + perfInfo('import.getEvmGasWallets', getEvmGasWalletsStart, { + vmWallets: vmWallets.length, + }); + + const createWalletsForAccountsStart = perfNow(); const _wallets = await createWalletsForAccounts( dispatch, accountsArray, data.key as KeyMethods, getBaseVMAccountCreationCoinsAndTokens(), ); + perfInfo( + 'import.createWalletsForAccounts', + createWalletsForAccountsStart, + { + _wallets: _wallets.length, + }, + ); + if (_wallets.length > 0) { data.wallets.push(..._wallets); } + + const findMatchedKeyAndUpdateStart = perfNow(); // To Avoid Duplicate wallet import const { key: _key, @@ -895,12 +959,20 @@ export const startImportMnemonic = Object.values(WALLET.keys).filter(k => !k.id.includes('readonly')), // Avoid checking readonly keys opts, ); + perfInfo( + 'import.findMatchedKeyAndUpdate', + findMatchedKeyAndUpdateStart, + { + keyId: data.key.id, + }, + ); // To clear encrypt password if (opts.keyId && isMatch(_key, WALLET.keys[opts.keyId])) { dispatch(deleteKey({keyId: opts.keyId})); } + const buildStart = perfNow(); const key = buildKeyObj({ key: _key, keyName, @@ -939,6 +1011,11 @@ export const startImportMnemonic = }), backupComplete: true, }); + perfInfo('import.buildKeyObjAndWalletObjects', buildStart); + + perfInfo('import.TOTAL', totalStart, { + walletCount: data?.wallets?.length ?? 0, + }); dispatch( successImport({ @@ -947,6 +1024,9 @@ export const startImportMnemonic = ); resolve(key); } catch (e) { + perfError('import.TOTAL.ERROR', totalStart, { + message: e instanceof Error ? e.message : JSON.stringify(e), + }); dispatch(failedImport()); reject(e); } @@ -1557,43 +1637,82 @@ export const serverAssistedImport = async ( opts: Partial, ): Promise<{key: KeyMethods; wallets: Wallet[]}> => { return new Promise((resolve, reject) => { + const totalStart = perfNow(); try { + logManager.info( + `[IMPORT PERF] serverAssistedImport.start | ${JSON.stringify({ + hasWords: !!opts?.words, + hasXPrivKey: !!opts?.xPrivKey, + hasPassphrase: !!opts?.passphrase, + })}`, + ); + + const apiStart = perfNow(); BwcProvider.API.serverAssistedImport( opts, {baseUrl: BASE_BWS_URL}, // @ts-ignore async (err, key, wallets) => { if (err) { + perfError('serverAssistedImport.API.ERROR', apiStart, { + name: err?.name, + message: err?.message, + }); return reject(err); } + perfInfo('serverAssistedImport.API.callback', apiStart, { + walletCount: wallets?.length ?? 0, + }); if (wallets.length === 0) { + perfError('serverAssistedImport.EMPTY', totalStart); return reject(new Error('WALLET_DOES_NOT_EXIST')); } else { + const dedupeStart = perfNow(); // remove duplicate wallets wallets = uniqBy(wallets, w => { return (w as any).credentials.walletId; }); + perfInfo('serverAssistedImport.dedupeWallets', dedupeStart, { + walletCount: wallets.length, + }); + const tokensStart = perfNow(); const tokens: Wallet[] = wallets.filter( (wallet: Wallet) => !!wallet.credentials.token, ); + perfInfo('serverAssistedImport.filterTokenWallets', tokensStart, { + tokenCount: tokens.length, + }); if (tokens && !!tokens.length) { + const linkStart = perfNow(); wallets = linkTokenToWallet(tokens, wallets); + perfInfo('serverAssistedImport.linkTokenToWallet', linkStart, { + tokenCount: tokens.length, + }); } + perfInfo('serverAssistedImport.TOTAL', totalStart, { + walletCount: wallets.length, + }); + return resolve({key, wallets}); } }, ); } catch (err) { + perfError('serverAssistedImport.TOTAL.ERROR', totalStart, { + message: err instanceof Error ? err.message : JSON.stringify(err), + }); return reject(err); } }); }; const linkTokenToWallet = (tokens: Wallet[], wallets: Wallet[]) => { + const totalStart = perfNow(); tokens.forEach(token => { + const tokenStart = perfNow(); // find the associated wallet to add tokens too const associatedWalletId = token.credentials.walletId.split('-0x')[0]; wallets = wallets.map((wallet: Wallet) => { @@ -1604,6 +1723,14 @@ const linkTokenToWallet = (tokens: Wallet[], wallets: Wallet[]) => { } return wallet; }); + perfInfo('linkTokenToWallet.token', tokenStart, { + tokenWalletId: token.credentials.walletId, + associatedWalletId, + }); + }); + perfInfo('linkTokenToWallet.TOTAL', totalStart, { + tokenCount: tokens.length, + walletCount: wallets.length, }); return wallets; From 78945cbe440ac84c658cf7851bd8b4b76a926c4e Mon Sep 17 00:00:00 2001 From: Gustavo Cortez Date: Wed, 18 Mar 2026 13:42:24 -0300 Subject: [PATCH 2/5] Create: Enhancement - add controlled concurrency and defer non-primary address hydration --- src/store/wallet/effects/create/create.ts | 197 +++++++++++++++------- 1 file changed, 136 insertions(+), 61 deletions(-) diff --git a/src/store/wallet/effects/create/create.ts b/src/store/wallet/effects/create/create.ts index 849f0aea43..d529b4a039 100644 --- a/src/store/wallet/effects/create/create.ts +++ b/src/store/wallet/effects/create/create.ts @@ -89,6 +89,39 @@ const perfError = ( return ms; }; +const runWithConcurrency = async ( + items: T[], + limit: number, + worker: (item: T, index: number) => Promise, +): Promise => { + const results: R[] = new Array(items.length); + let nextIndex = 0; + + const runners = Array.from( + {length: Math.min(limit, items.length)}, + async () => { + while (true) { + const currentIndex = nextIndex++; + if (currentIndex >= items.length) { + return; + } + results[currentIndex] = await worker(items[currentIndex], currentIndex); + } + }, + ); + + await Promise.all(runners); + return results; +}; + +const shouldHydrateAddressImmediately = (chain: string) => { + return ['btc', 'eth', 'matic'].includes(chain); +}; + +const defer = (fn: () => void) => { + setTimeout(fn, 0); +}; + export interface CreateOptions { network?: Network; account?: number; @@ -495,8 +528,9 @@ export const createMultipleWallets = })}`, ); - for (const coin of coins) { + const baseWalletResults = await runWithConcurrency(coins, 3, async coin => { const coinStart = perfNow(); + try { logManager.info( `[CREATEKEY PERF] createMultipleWallets.coin.start | ${JSON.stringify( @@ -525,54 +559,18 @@ export const createMultipleWallets = {walletId: wallet?.credentials?.walletId}, ); - const addressStart = perfNow(); - const receiveAddress = (await dispatch( - createWalletAddress({wallet, newAddress: true}), - )) as string; - perfInfo( - `createMultipleWallets.createWalletAddress.${coin.chain}.${coin.currencyAbbreviation}`, - addressStart, - { - walletId: wallet?.credentials?.walletId, - }, - ); - logManager.info(`new address generated: ${receiveAddress}`); - wallet.receiveAddress = receiveAddress; - wallets.push(wallet); - for (const token of tokens) { - if (token.chain === coin.chain) { - const tokenStart = perfNow(); - const tokenWallet = await dispatch( - createTokenWallet( - wallet, - token.currencyAbbreviation.toLowerCase(), - token.tokenAddress!, - tokenOpts, - ), - ); - perfInfo( - `createMultipleWallets.createTokenWallet.${token.chain}.${token.currencyAbbreviation}`, - tokenStart, - { - parentWalletId: wallet?.credentials?.walletId, - tokenWalletId: (tokenWallet as Wallet | undefined)?.credentials - ?.walletId, - }, - ); - if (tokenWallet) { - wallets.push(tokenWallet); - } - } - } const chainTokens = tokens.filter(token => token.chain === coin.chain); + perfInfo( - `createMultipleWallets.coin.TOTAL.${coin.chain}.${coin.currencyAbbreviation}`, + `createMultipleWallets.coin.createWalletOnly.${coin.chain}.${coin.currencyAbbreviation}`, coinStart, { walletId: wallet?.credentials?.walletId, tokenCount: chainTokens.length, }, ); + + return {coin, wallet, chainTokens}; } catch (err) { perfError( `createMultipleWallets.coin.ERROR.${coin.chain}.${coin.currencyAbbreviation}`, @@ -581,31 +579,78 @@ export const createMultipleWallets = message: err instanceof Error ? err.message : JSON.stringify(err), }, ); - const errMsg = err instanceof Error ? err.message : JSON.stringify(err); - logManager.debug(`Error creating wallet - continue anyway: ${errMsg}`); + return null; + } + }); + + const validBaseWallets = baseWalletResults.filter(Boolean) as Array<{ + coin: (typeof coins)[number]; + wallet: Wallet; + chainTokens: typeof tokens; + }>; + + await runWithConcurrency(validBaseWallets, 3, async ({coin, wallet}) => { + if (!shouldHydrateAddressImmediately(wallet.credentials.chain)) { + return; } + + const addressStart = perfNow(); + const receiveAddress = (await dispatch( + createWalletAddress({wallet, newAddress: true}), + )) as string; + + perfInfo( + `createMultipleWallets.createWalletAddress.${coin.chain}.${coin.currencyAbbreviation}`, + addressStart, + { + walletId: wallet?.credentials?.walletId, + }, + ); + + wallet.receiveAddress = receiveAddress; + }); + + for (const result of validBaseWallets) { + wallets.push(result.wallet); + + for (const token of result.chainTokens) { + const tokenStart = perfNow(); + const tokenWallet = await dispatch( + createTokenWallet( + result.wallet, + token.currencyAbbreviation.toLowerCase(), + token.tokenAddress!, + tokenOpts, + ), + ); + + perfInfo( + `createMultipleWallets.createTokenWallet.${token.chain}.${token.currencyAbbreviation}`, + tokenStart, + { + parentWalletId: result.wallet?.credentials?.walletId, + tokenWalletId: (tokenWallet as Wallet | undefined)?.credentials + ?.walletId, + }, + ); + + if (tokenWallet) { + wallets.push(tokenWallet); + } + } + + perfInfo( + `createMultipleWallets.coin.TOTAL.${result.coin.chain}.${result.coin.currencyAbbreviation}`, + totalStart, + { + walletId: result.wallet?.credentials?.walletId, + tokenCount: result.chainTokens.length, + }, + ); } const buildStart = perfNow(); - // build out app specific props const result = wallets.map(wallet => { - // subscribe new wallet to push notifications - if (notificationsAccepted) { - dispatch(subscribePushNotifications(wallet, brazeEid!)); - } - // subscribe new wallet to email notifications - if ( - emailNotifications && - emailNotifications.accepted && - emailNotifications.email - ) { - const prefs = { - email: emailNotifications.email, - language: defaultLanguage, - unit: 'btc', // deprecated - }; - dispatch(subscribeEmailNotifications(wallet, prefs)); - } const {currencyAbbreviation, currencyName} = dispatch( mapAbbreviationAndName( wallet.credentials.coin, @@ -613,6 +658,7 @@ export const createMultipleWallets = wallet.credentials?.token?.address, ), ); + return merge( wallet, buildWalletObj( @@ -630,6 +676,35 @@ export const createMultipleWallets = }, ); + defer(() => { + result.forEach(wallet => { + if (notificationsAccepted) { + dispatch(subscribePushNotifications(wallet, brazeEid!)); + } + + if ( + emailNotifications && + emailNotifications.accepted && + emailNotifications.email + ) { + const prefs = { + email: emailNotifications.email, + language: defaultLanguage, + unit: 'btc', + }; + dispatch(subscribeEmailNotifications(wallet, prefs)); + } + + if ( + !wallet.receiveAddress && + shouldHydrateAddressImmediately(wallet.credentials.chain) === false && + !wallet.credentials.token + ) { + dispatch(createWalletAddress({wallet, newAddress: true})); + } + }); + }); + perfInfo('createMultipleWallets.TOTAL', totalStart, { walletCount: result.length, baseCoins: coins.length, From 42b4c597cfc85631a6cb40b03aa19eba21ccfe1a Mon Sep 17 00:00:00 2001 From: Gustavo Cortez Date: Wed, 18 Mar 2026 13:43:22 -0300 Subject: [PATCH 3/5] Create: Fix - show ongoingprocess just after tap on Create button --- .../onboarding/screens/CreateKey.tsx | 77 ++++++++++--------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/src/navigation/onboarding/screens/CreateKey.tsx b/src/navigation/onboarding/screens/CreateKey.tsx index 00d12e42a0..c467162ab8 100644 --- a/src/navigation/onboarding/screens/CreateKey.tsx +++ b/src/navigation/onboarding/screens/CreateKey.tsx @@ -1,5 +1,5 @@ import {NativeStackScreenProps} from '@react-navigation/native-stack'; -import React, {useEffect, useLayoutEffect, useRef} from 'react'; +import React, {useEffect, useLayoutEffect, useRef, useCallback} from 'react'; import {ScrollView} from 'react-native'; import {useAndroidBackHandler} from 'react-navigation-backhandler'; import styled from 'styled-components/native'; @@ -109,6 +109,45 @@ const CreateOrImportKey = ({ ); }; + const onCreateKeyPress = useCallback(async () => { + try { + const context = 'onboarding'; + + showOngoingProcess('CREATING_KEY'); + await sleep(500); + + const createdKey = await dispatch( + startCreateKey(getBaseKeyCreationCoinsAndTokens(), context), + ); + + dispatch(setHomeCarouselConfig({id: createdKey.id, show: true})); + hideOngoingProcess(); + + askForTrackingThenNavigate(() => { + dispatch(Analytics.track('Clicked Create New Key', {context})); + navigation.navigate('BackupKey', { + context, + key: createdKey, + }); + }); + } catch (err: any) { + const errstring = + err instanceof Error ? err.message : JSON.stringify(err); + + logManager.error(`Error creating key: ${errstring}`); + hideOngoingProcess(); + await sleep(500); + showErrorModal(errstring); + } + }, [ + dispatch, + navigation, + showOngoingProcess, + hideOngoingProcess, + askForTrackingThenNavigate, + showErrorModal, + ]); + useLayoutEffect(() => { navigation.setOptions({ gestureEnabled: false, @@ -147,41 +186,7 @@ const CreateOrImportKey = ({ From 993e17ff1c1b4d0886a7e8b492a377cd9e2bc6cc Mon Sep 17 00:00:00 2001 From: Gustavo Cortez Date: Wed, 18 Mar 2026 17:18:46 -0300 Subject: [PATCH 4/5] Improve import process reducing duplicate wallet creation --- src/Root.tsx | 2 + src/navigation/wallet/screens/KeyOverview.tsx | 1 + src/store/wallet/effects/import/import.ts | 123 +++++++++++++----- src/utils/helper-methods.ts | 119 +++++++++++++---- 4 files changed, 184 insertions(+), 61 deletions(-) diff --git a/src/Root.tsx b/src/Root.tsx index 9178a478bc..44a86f0550 100644 --- a/src/Root.tsx +++ b/src/Root.tsx @@ -757,6 +757,7 @@ export default () => { accountsArray, key.methods as KeyMethods, getBaseEVMAccountCreationCoinsAndTokens(), + key.wallets, ); key.wallets.push(...wallets); dispatch(successAddWallet({key})); @@ -800,6 +801,7 @@ export default () => { [0], key.methods as KeyMethods, getBaseSVMAccountCreationCoinsAndTokens(), + key.wallets, ); key.wallets.push(...wallets); logManager.info( diff --git a/src/navigation/wallet/screens/KeyOverview.tsx b/src/navigation/wallet/screens/KeyOverview.tsx index 1f1f965bbe..fe04ee520f 100644 --- a/src/navigation/wallet/screens/KeyOverview.tsx +++ b/src/navigation/wallet/screens/KeyOverview.tsx @@ -886,6 +886,7 @@ const KeyOverview = () => { accountsArray, key.methods as KeyMethods, getBaseEVMAccountCreationCoinsAndTokens(), + key.wallets, password, ); diff --git a/src/store/wallet/effects/import/import.ts b/src/store/wallet/effects/import/import.ts index a6526bbd74..3574ee6aac 100644 --- a/src/store/wallet/effects/import/import.ts +++ b/src/store/wallet/effects/import/import.ts @@ -135,6 +135,27 @@ const perfError = ( return ms; }; +const getMissingVmAccounts = ( + wallets: Wallet[], + accountsArray: number[], + expectedChainCoins: Array<{chain: string; currencyAbbreviation: string}>, +) => { + const existing = new Set( + wallets.map(wallet => { + const account = wallet.credentials.account || 0; + const chain = wallet.credentials.chain; + const coin = wallet.credentials.coin; + return `${account}:${chain}:${coin}`; + }), + ); + + return accountsArray.filter(account => { + return expectedChainCoins.some(({chain, currencyAbbreviation}) => { + return !existing.has(`${account}:${chain}:${currencyAbbreviation}`); + }); + }); +}; + const BWC = BwcProvider.getInstance(); const BwcConstants = BWC.getConstants(); @@ -928,18 +949,47 @@ export const startImportMnemonic = vmWallets: vmWallets.length, }); - const createWalletsForAccountsStart = perfNow(); - const _wallets = await createWalletsForAccounts( - dispatch, + const expectedVmWallets = getBaseVMAccountCreationCoinsAndTokens(); + + const missingAccounts = getMissingVmAccounts( + data.wallets, accountsArray, - data.key as KeyMethods, - getBaseVMAccountCreationCoinsAndTokens(), + expectedVmWallets, + ); + + logManager.info( + `[PERF IMPORT OPTIMIZATION] VM account expansion | ${JSON.stringify({ + totalAccounts: accountsArray.length, + missingAccounts: missingAccounts.length, + skippedAccounts: accountsArray.length - missingAccounts.length, + })}`, + ); + + const createWalletsForAccountsStart = perfNow(); + const _wallets = + missingAccounts.length > 0 + ? await createWalletsForAccounts( + dispatch, + accountsArray, + data.key as KeyMethods, + getBaseVMAccountCreationCoinsAndTokens(), + data.wallets, + ) + : []; + logManager.info( + `[PERF IMPORT OPTIMIZATION] createWalletsForAccounts input | ${JSON.stringify( + { + originalAccounts: accountsArray, + missingAccounts, + }, + )}`, ); perfInfo( 'import.createWalletsForAccounts', createWalletsForAccountsStart, { _wallets: _wallets.length, + requestedAccounts: missingAccounts.length, }, ); @@ -973,10 +1023,41 @@ export const startImportMnemonic = } const buildStart = perfNow(); + const resultWallets = wallets.map(wallet => { + const {currencyAbbreviation, currencyName} = dispatch( + mapAbbreviationAndName( + wallet.credentials.coin, + wallet.credentials.chain, + wallet.credentials.token?.address, + ), + ); + return merge( + wallet, + buildWalletObj( + {...wallet.credentials, currencyAbbreviation, currencyName}, + tokenOptsByAddress, + ), + ); + }); const key = buildKeyObj({ key: _key, keyName, - wallets: wallets.map(wallet => { + wallets: resultWallets, + backupComplete: true, + }); + perfInfo('import.buildKeyObjAndWalletObjects', buildStart); + + perfInfo('import.TOTAL', totalStart, { + walletCount: data?.wallets?.length ?? 0, + }); + + dispatch( + successImport({ + key, + }), + ); + setTimeout(() => { + resultWallets.forEach(wallet => { // subscribe new wallet to push notifications if (notificationsAccepted) { dispatch(subscribePushNotifications(wallet, brazeEid!)); @@ -994,34 +1075,8 @@ export const startImportMnemonic = }; dispatch(subscribeEmailNotifications(wallet, prefs)); } - const {currencyAbbreviation, currencyName} = dispatch( - mapAbbreviationAndName( - wallet.credentials.coin, - wallet.credentials.chain, - wallet.credentials.token?.address, - ), - ); - return merge( - wallet, - buildWalletObj( - {...wallet.credentials, currencyAbbreviation, currencyName}, - tokenOptsByAddress, - ), - ); - }), - backupComplete: true, - }); - perfInfo('import.buildKeyObjAndWalletObjects', buildStart); - - perfInfo('import.TOTAL', totalStart, { - walletCount: data?.wallets?.length ?? 0, - }); - - dispatch( - successImport({ - key, - }), - ); + }); + }, 0); resolve(key); } catch (e) { perfError('import.TOTAL.ERROR', totalStart, { diff --git a/src/utils/helper-methods.ts b/src/utils/helper-methods.ts index 81339036bf..4de32c6a02 100644 --- a/src/utils/helper-methods.ts +++ b/src/utils/helper-methods.ts @@ -769,6 +769,39 @@ export const fixWalletAddresses = async ({ ); }; +const normalizeWalletKeyCoin = (coin: string) => { + return coin === 'pol' ? 'matic' : coin; +}; + +const buildExistingWalletKeySet = (wallets: Wallet[]) => { + return new Set( + wallets.map(wallet => { + const account = wallet.credentials.account || 0; + const chain = wallet.credentials.chain; + const coin = normalizeWalletKeyCoin(wallet.credentials.coin); + return `${account}:${chain}:${coin}`; + }), + ); +}; + +const getMissingCurrenciesForAccount = ( + account: number, + currencies: { + chain: string; + currencyAbbreviation: string; + isToken: boolean; + tokenAddress?: string; + }[], + existingWalletKeySet: Set, +) => { + return currencies.filter(currency => { + const chain = currency.chain; + const coin = normalizeWalletKeyCoin(currency.currencyAbbreviation); + const walletKey = `${account}:${chain}:${coin}`; + return !existingWalletKeySet.has(walletKey); + }); +}; + export const createWalletsForAccounts = async ( dispatch: any, accountsArray: number[], @@ -779,36 +812,68 @@ export const createWalletsForAccounts = async ( isToken: boolean; tokenAddress?: string; }[], + existingWallets: Wallet[], password?: string, ) => { - return ( - await Promise.all( - accountsArray.flatMap(async account => { - try { - const newWallets = (await dispatch( - createMultipleWallets({ - key: key as KeyMethods, - currencies, - options: { - password, - account, - customAccount: true, - }, - }), - )) as Wallet[]; - return newWallets; - } catch (err) { - const errMsg = - err instanceof Error ? err.message : JSON.stringify(err); - logManager.debug( - `Error creating wallet - continue anyway: ${errMsg}`, + const existingWalletKeySet = buildExistingWalletKeySet(existingWallets); + + const results = await Promise.all( + accountsArray.map(async account => { + const missingCurrencies = getMissingCurrenciesForAccount( + account, + currencies, + existingWalletKeySet, + ); + + logManager.info( + `[PERF IMPORT OPTIMIZATION] createWalletsForAccounts.account | ${JSON.stringify( + { + account, + requestedCurrencies: currencies.length, + missingCurrencies: missingCurrencies.length, + skippedCurrencies: currencies.length - missingCurrencies.length, + }, + )}`, + ); + + if (missingCurrencies.length === 0) { + return []; + } + + try { + const newWallets = (await dispatch( + createMultipleWallets({ + key: key as KeyMethods, + currencies: missingCurrencies, + options: { + password, + account, + customAccount: true, + }, + }), + )) as Wallet[]; + + newWallets.forEach(wallet => { + const walletAccount = wallet.credentials.account || 0; + const walletChain = wallet.credentials.chain; + const walletCoin = normalizeWalletKeyCoin(wallet.credentials.coin); + existingWalletKeySet.add( + `${walletAccount}:${walletChain}:${walletCoin}`, ); - } - }), - ) - ) - .flat() - .filter(Boolean) as Wallet[]; + }); + + return newWallets; + } catch (err) { + const errMsg = err instanceof Error ? err.message : JSON.stringify(err); + logManager.debug( + `Error creating wallet for account ${account} - continue anyway: ${errMsg}`, + ); + return []; + } + }), + ); + + return results.flat().filter(Boolean) as Wallet[]; }; export const getVMGasWallets = (wallets: Wallet[]) => { From 3d0d59dc7a63b958f104856d8e92bd7099176369 Mon Sep 17 00:00:00 2001 From: Gustavo Cortez Date: Fri, 20 Mar 2026 17:41:03 -0300 Subject: [PATCH 5/5] Metric two cases: full parallel and concurrency 3 --- src/store/wallet/effects/create/create.ts | 219 +++++++++++++++------- 1 file changed, 152 insertions(+), 67 deletions(-) diff --git a/src/store/wallet/effects/create/create.ts b/src/store/wallet/effects/create/create.ts index d529b4a039..4e64235512 100644 --- a/src/store/wallet/effects/create/create.ts +++ b/src/store/wallet/effects/create/create.ts @@ -61,6 +61,8 @@ import {tokenManager} from '../../../../managers/TokenManager'; import {logManager} from '../../../../managers/LogManager'; import {Analytics} from '../../../analytics/analytics.effects'; +const USE_FULL_PARALLEL = true; + const perfNow = () => typeof global?.performance !== 'undefined' && typeof global.performance?.now === 'function' @@ -528,60 +530,124 @@ export const createMultipleWallets = })}`, ); - const baseWalletResults = await runWithConcurrency(coins, 3, async coin => { - const coinStart = perfNow(); + const baseWalletResults = USE_FULL_PARALLEL + ? await Promise.all( + coins.map(async coin => { + const coinStart = perfNow(); - try { - logManager.info( - `[CREATEKEY PERF] createMultipleWallets.coin.start | ${JSON.stringify( - { - chain: coin.chain, - coin: coin.currencyAbbreviation, - }, - )}`, - ); + try { + logManager.info( + `[PARALLEL CREATEKEY PERF] createMultipleWallets.coin.start | ${JSON.stringify( + { + chain: coin.chain, + coin: coin.currencyAbbreviation, + }, + )}`, + ); - const walletCreateStart = perfNow(); - const wallet = (await dispatch( - createWallet({ - key, - coin: coin.currencyAbbreviation, - chain: coin.chain as SupportedChains, - options: { - ...options, - useNativeSegwit: IsSegwitCoin(coin.currencyAbbreviation), - }, + const walletCreateStart = perfNow(); + const wallet = (await dispatch( + createWallet({ + key, + coin: coin.currencyAbbreviation, + chain: coin.chain as SupportedChains, + options: { + ...options, + useNativeSegwit: IsSegwitCoin(coin.currencyAbbreviation), + }, + }), + )) as Wallet; + + perfInfo( + `createMultipleWallets.createWallet.${coin.chain}.${coin.currencyAbbreviation}`, + walletCreateStart, + {walletId: wallet?.credentials?.walletId}, + ); + + const chainTokens = tokens.filter( + token => token.chain === coin.chain, + ); + + perfInfo( + `createMultipleWallets.coin.createWalletOnly.${coin.chain}.${coin.currencyAbbreviation}`, + coinStart, + { + walletId: wallet?.credentials?.walletId, + tokenCount: chainTokens.length, + }, + ); + + return {coin, wallet, chainTokens}; + } catch (err) { + perfError( + `createMultipleWallets.coin.ERROR.${coin.chain}.${coin.currencyAbbreviation}`, + coinStart, + { + message: + err instanceof Error ? err.message : JSON.stringify(err), + }, + ); + return null; + } }), - )) as Wallet; - perfInfo( - `createMultipleWallets.createWallet.${coin.chain}.${coin.currencyAbbreviation}`, - walletCreateStart, - {walletId: wallet?.credentials?.walletId}, - ); + ) + : await runWithConcurrency(coins, 3, async coin => { + const coinStart = perfNow(); - const chainTokens = tokens.filter(token => token.chain === coin.chain); + try { + logManager.info( + `[CREATEKEY PERF] createMultipleWallets.coin.start | ${JSON.stringify( + { + chain: coin.chain, + coin: coin.currencyAbbreviation, + }, + )}`, + ); - perfInfo( - `createMultipleWallets.coin.createWalletOnly.${coin.chain}.${coin.currencyAbbreviation}`, - coinStart, - { - walletId: wallet?.credentials?.walletId, - tokenCount: chainTokens.length, - }, - ); + const walletCreateStart = perfNow(); + const wallet = (await dispatch( + createWallet({ + key, + coin: coin.currencyAbbreviation, + chain: coin.chain as SupportedChains, + options: { + ...options, + useNativeSegwit: IsSegwitCoin(coin.currencyAbbreviation), + }, + }), + )) as Wallet; + perfInfo( + `createMultipleWallets.createWallet.${coin.chain}.${coin.currencyAbbreviation}`, + walletCreateStart, + {walletId: wallet?.credentials?.walletId}, + ); - return {coin, wallet, chainTokens}; - } catch (err) { - perfError( - `createMultipleWallets.coin.ERROR.${coin.chain}.${coin.currencyAbbreviation}`, - coinStart, - { - message: err instanceof Error ? err.message : JSON.stringify(err), - }, - ); - return null; - } - }); + const chainTokens = tokens.filter( + token => token.chain === coin.chain, + ); + + perfInfo( + `createMultipleWallets.coin.createWalletOnly.${coin.chain}.${coin.currencyAbbreviation}`, + coinStart, + { + walletId: wallet?.credentials?.walletId, + tokenCount: chainTokens.length, + }, + ); + + return {coin, wallet, chainTokens}; + } catch (err) { + perfError( + `createMultipleWallets.coin.ERROR.${coin.chain}.${coin.currencyAbbreviation}`, + coinStart, + { + message: + err instanceof Error ? err.message : JSON.stringify(err), + }, + ); + return null; + } + }); const validBaseWallets = baseWalletResults.filter(Boolean) as Array<{ coin: (typeof coins)[number]; @@ -589,26 +655,45 @@ export const createMultipleWallets = chainTokens: typeof tokens; }>; - await runWithConcurrency(validBaseWallets, 3, async ({coin, wallet}) => { - if (!shouldHydrateAddressImmediately(wallet.credentials.chain)) { - return; - } - - const addressStart = perfNow(); - const receiveAddress = (await dispatch( - createWalletAddress({wallet, newAddress: true}), - )) as string; - - perfInfo( - `createMultipleWallets.createWalletAddress.${coin.chain}.${coin.currencyAbbreviation}`, - addressStart, - { - walletId: wallet?.credentials?.walletId, - }, + if (USE_FULL_PARALLEL) { + await Promise.all( + validBaseWallets.map(async ({wallet}) => { + if (!shouldHydrateAddressImmediately(wallet.credentials.chain)) { + return; + } + try { + const receiveAddress = (await dispatch( + createWalletAddress({wallet, newAddress: true}), + )) as string; + wallet.receiveAddress = receiveAddress; + } catch (err) { + const errMsg = + err instanceof Error ? err.message : JSON.stringify(err); + logManager.debug( + `Error creating wallet address - continue anyway: ${errMsg}`, + ); + } + }), ); - - wallet.receiveAddress = receiveAddress; - }); + } else { + await runWithConcurrency(validBaseWallets, 3, async ({wallet}) => { + if (!shouldHydrateAddressImmediately(wallet.credentials.chain)) { + return; + } + try { + const receiveAddress = (await dispatch( + createWalletAddress({wallet, newAddress: true}), + )) as string; + wallet.receiveAddress = receiveAddress; + } catch (err) { + const errMsg = + err instanceof Error ? err.message : JSON.stringify(err); + logManager.debug( + `Error creating wallet address - continue anyway: ${errMsg}`, + ); + } + }); + } for (const result of validBaseWallets) { wallets.push(result.wallet);