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/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 = ({ 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/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..4e64235512 100644 --- a/src/store/wallet/effects/create/create.ts +++ b/src/store/wallet/effects/create/create.ts @@ -61,6 +61,69 @@ 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' + ? 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; +}; + +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; @@ -102,14 +165,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 +199,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 +501,7 @@ export const createMultipleWallets = options: CreateOptions; }): Effect> => async (dispatch, getState) => { + const totalStart = perfNow(); const { WALLET, APP: { @@ -419,66 +521,221 @@ export const createMultipleWallets = const wallets: API[] = []; const tokens = currencies.filter(({isToken}) => isToken); const coins = currencies.filter(({isToken}) => !isToken); - for (const coin of coins) { - try { - const wallet = (await dispatch( - createWallet({ - key, - coin: coin.currencyAbbreviation, - chain: coin.chain as SupportedChains, - options: { - ...options, - useNativeSegwit: IsSegwitCoin(coin.currencyAbbreviation), - }, + + logManager.info( + `[CREATEKEY PERF] createMultipleWallets.start | ${JSON.stringify({ + baseCoins: coins.length, + tokens: tokens.length, + network: options?.network, + })}`, + ); + + const baseWalletResults = USE_FULL_PARALLEL + ? await Promise.all( + coins.map(async coin => { + const coinStart = perfNow(); + + 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), + }, + }), + )) 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; - - const receiveAddress = (await dispatch( - createWalletAddress({wallet, newAddress: true}), - )) as string; - logManager.info(`new address generated: ${receiveAddress}`); - wallet.receiveAddress = receiveAddress; - wallets.push(wallet); - for (const token of tokens) { - if (token.chain === coin.chain) { - const tokenWallet = await dispatch( - createTokenWallet( - wallet, - token.currencyAbbreviation.toLowerCase(), - token.tokenAddress!, - tokenOpts, - ), + ) + : await runWithConcurrency(coins, 3, async coin => { + 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, + 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; + } + }); + + const validBaseWallets = baseWalletResults.filter(Boolean) as Array<{ + coin: (typeof coins)[number]; + wallet: Wallet; + chainTokens: typeof tokens; + }>; + + 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}`, ); - if (tokenWallet) { - wallets.push(tokenWallet); - } } + }), + ); + } else { + await runWithConcurrency(validBaseWallets, 3, async ({wallet}) => { + if (!shouldHydrateAddressImmediately(wallet.credentials.chain)) { + return; } - } catch (err) { - const errMsg = err instanceof Error ? err.message : JSON.stringify(err); - logManager.debug(`Error creating wallet - continue anyway: ${errMsg}`); - } + 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}`, + ); + } + }); } - // build out app specific props - return 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)); + 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(); + const result = wallets.map(wallet => { const {currencyAbbreviation, currencyName} = dispatch( mapAbbreviationAndName( wallet.credentials.coin, @@ -486,6 +743,7 @@ export const createMultipleWallets = wallet.credentials?.token?.address, ), ); + return merge( wallet, buildWalletObj( @@ -494,6 +752,51 @@ export const createMultipleWallets = ), ); }); + + perfInfo( + 'createMultipleWallets.buildWalletObjectsAndSubscriptions', + buildStart, + { + walletCount: result.length, + }, + ); + + 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, + tokens: tokens.length, + }); + + return result; }; ///////////////////////////////////////////////////////////// @@ -513,6 +816,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 +834,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 +887,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 +915,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 +942,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 +972,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 +1066,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..3574ee6aac 100644 --- a/src/store/wallet/effects/import/import.ts +++ b/src/store/wallet/effects/import/import.ts @@ -107,6 +107,55 @@ 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 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(); @@ -848,6 +897,7 @@ export const startImportMnemonic = ): Effect => async (dispatch, getState): Promise => { return new Promise(async (resolve, reject) => { + const totalStart = perfNow(); try { const { WALLET, @@ -866,24 +916,88 @@ 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)), ]; - const _wallets = await createWalletsForAccounts( - dispatch, + perfInfo('import.getEvmGasWallets', getEvmGasWalletsStart, { + vmWallets: vmWallets.length, + }); + + 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, + }, + ); + if (_wallets.length > 0) { data.wallets.push(..._wallets); } + + const findMatchedKeyAndUpdateStart = perfNow(); // To Avoid Duplicate wallet import const { key: _key, @@ -895,16 +1009,55 @@ 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 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!)); @@ -922,31 +1075,13 @@ 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, - }); - - dispatch( - successImport({ - key, - }), - ); + }); + }, 0); resolve(key); } catch (e) { + perfError('import.TOTAL.ERROR', totalStart, { + message: e instanceof Error ? e.message : JSON.stringify(e), + }); dispatch(failedImport()); reject(e); } @@ -1557,43 +1692,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 +1778,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; 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[]) => {