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[]) => {