From 3a32798623eb149561017dc092426b1db7ab7787 Mon Sep 17 00:00:00 2001 From: Jean Ribeiro Date: Mon, 18 Dec 2023 18:36:16 -0300 Subject: [PATCH] feat: multi address algorithm in balance finder for IOTA profiles (#1610) * refactor: clean BalanceFinderView * refactor: clean code for SyncAccountsPopup * refactor: cleanup single account search in onboarding Co-authored-by: Jean Ribeiro * feat: implement multi account search in recovery flow * add algorithm in sync accounts popup * adds accountGapLimit in accountStartIndex * enhancement: adds number of accounts found in SyncAccountsPopup * fix: set previousAccountsLength to 0 on first sync --------- Co-authored-by: Nicole O'Brien --- .../popup/popups/SyncAccountsPopup.svelte | 96 ++++++-- .../views/BalanceFinderView.svelte | 205 ++++++++++-------- ...account-recovery-configuration.constant.ts | 4 +- 3 files changed, 194 insertions(+), 111 deletions(-) diff --git a/packages/desktop/components/popup/popups/SyncAccountsPopup.svelte b/packages/desktop/components/popup/popups/SyncAccountsPopup.svelte index 7eb71f4072..6ff892058f 100644 --- a/packages/desktop/components/popup/popups/SyncAccountsPopup.svelte +++ b/packages/desktop/components/popup/popups/SyncAccountsPopup.svelte @@ -15,39 +15,37 @@ import { closePopup } from '@desktop/auxiliary/popup' import { onDestroy, onMount } from 'svelte' import PopupTemplate from '../PopupTemplate.svelte' + import { StardustNetworkId } from '@core/network/enums' export let searchForBalancesOnLoad = false - const { type } = $activeProfile + const { network, type } = $activeProfile - const initialAccountRange = DEFAULT_ACCOUNT_RECOVERY_CONFIGURATION[type].initialAccountRange - const addressGapLimitIncrement = DEFAULT_ACCOUNT_RECOVERY_CONFIGURATION[type].addressGapLimit + const DEFAULT_CONFIG = DEFAULT_ACCOUNT_RECOVERY_CONFIGURATION[type] + + let accountStartIndex = 0 + let accountGapLimit = DEFAULT_CONFIG.initialAccountRange let previousAccountGapLimit = 0 - let previousAddressGapLimit = 0 - let currentAccountGapLimit = initialAccountRange - let currentAddressGapLimit = addressGapLimitIncrement + + let addressStartIndex = 0 + const addressGapLimit = DEFAULT_CONFIG.addressGapLimit + let error = '' let isBusy = false let hasUsedWalletFinder = false + let previousAccountsLength = 0 + $: totalBalance = sumBalanceForAccounts($visibleActiveAccounts) async function searchForBalance(): Promise { try { error = '' isBusy = true - const recoverAccountsPayload: RecoverAccountsPayload = { - accountStartIndex: 0, - accountGapLimit: currentAccountGapLimit, - addressGapLimit: currentAddressGapLimit, - syncOptions: DEFAULT_SYNC_OPTIONS, - } - await recoverAccounts(recoverAccountsPayload) + await (networkSearchMethod[network.id] ?? singleAddressSearch)() await loadAccounts() - previousAccountGapLimit = currentAccountGapLimit - previousAddressGapLimit = currentAddressGapLimit - currentAccountGapLimit += initialAccountRange - currentAddressGapLimit += addressGapLimitIncrement + previousAccountsLength = $visibleActiveAccounts.length + previousAccountGapLimit = accountGapLimit hasUsedWalletFinder = true } catch (err) { error = localize(err.error) @@ -60,8 +58,70 @@ } } + const networkSearchMethod: { [key in StardustNetworkId]?: () => Promise } = { + [StardustNetworkId.Iota]: multiAddressSearch, + [StardustNetworkId.Shimmer]: singleAddressSearch, + [StardustNetworkId.Testnet]: singleAddressSearch, + } + + async function singleAddressSearch(): Promise { + const recoverAccountsPayload: RecoverAccountsPayload = { + accountStartIndex, + accountGapLimit, + addressGapLimit: 1, + syncOptions: { ...DEFAULT_SYNC_OPTIONS, addressStartIndex: 0 }, + } + + await recoverAccounts(recoverAccountsPayload) + + const numberOfAccountsFound = Math.max(0, $visibleActiveAccounts.length - previousAccountsLength) + accountStartIndex = accountStartIndex + accountGapLimit + numberOfAccountsFound + } + + let searchCount = 0 + let depthSearchCount = 0 + let breadthSearchCountSinceLastDepthSearch = 0 + let depthSearch = false + // Please don't modify this algorithm without consulting with the team + async function multiAddressSearch(): Promise { + let recoverAccountsPayload: RecoverAccountsPayload + + if ( + !depthSearch && + breadthSearchCountSinceLastDepthSearch && + breadthSearchCountSinceLastDepthSearch % accountGapLimit === 0 + ) { + // Depth search + depthSearch = true + recoverAccountsPayload = { + accountStartIndex: accountGapLimit, + accountGapLimit: 1, + addressGapLimit: (searchCount - depthSearchCount) * addressGapLimit, + syncOptions: { ...DEFAULT_SYNC_OPTIONS, addressStartIndex: 0 }, + } + breadthSearchCountSinceLastDepthSearch = 0 + depthSearchCount++ + accountGapLimit++ + } else { + // Breadth search + depthSearch = false + recoverAccountsPayload = { + accountStartIndex, + accountGapLimit, + addressGapLimit: addressGapLimit, + syncOptions: { ...DEFAULT_SYNC_OPTIONS, addressStartIndex }, + } + breadthSearchCountSinceLastDepthSearch++ + addressStartIndex += addressGapLimit + } + + await recoverAccounts(recoverAccountsPayload) + + searchCount++ + } + async function onFindBalancesClick(): Promise { - await checkActiveProfileAuth(searchForBalance, { + await checkActiveProfileAuth(() => searchForBalance(), { stronghold: true, ledger: true, props: { searchForBalancesOnLoad: true }, diff --git a/packages/desktop/views/onboarding/views/restore-profile/views/BalanceFinderView.svelte b/packages/desktop/views/onboarding/views/restore-profile/views/BalanceFinderView.svelte index 251e29ea5d..dee524bb30 100644 --- a/packages/desktop/views/onboarding/views/restore-profile/views/BalanceFinderView.svelte +++ b/packages/desktop/views/onboarding/views/restore-profile/views/BalanceFinderView.svelte @@ -5,130 +5,153 @@ import { DEFAULT_SYNC_OPTIONS } from '@core/account/constants' import { localize } from '@core/i18n' import { LedgerAppName, checkOrConnectLedger } from '@core/ledger' + import { StardustNetworkId, SupportedNetworkId } from '@core/network' import { ProfileType } from '@core/profile' import { RecoverAccountsPayload, createAccount, recoverAccounts } from '@core/profile-manager' import { DEFAULT_ACCOUNT_RECOVERY_CONFIGURATION } from '@core/profile/constants' import { checkOrUnlockStronghold } from '@core/stronghold/actions' + import { formatTokenAmountBestMatch } from '@core/token' import { OnboardingLayout } from '@views/components' import { onDestroy, onMount } from 'svelte' import { restoreProfileRouter } from '../restore-profile-router' - import { SupportedNetworkId } from '@core/network' - import { formatTokenAmountBestMatch } from '@core/token' - const initialAccountRange = DEFAULT_ACCOUNT_RECOVERY_CONFIGURATION[$onboardingProfile.type].initialAccountRange - const addressGapLimitIncrement = DEFAULT_ACCOUNT_RECOVERY_CONFIGURATION[$onboardingProfile.type].addressGapLimit - let previousAccountGapLimit = 0 - let previousAddressGapLimit = 0 - let currentAccountGapLimit = initialAccountRange - let currentAddressGapLimit = addressGapLimitIncrement + interface IAccountBalance { + alias: string + total: string + } + + const { network, type } = $onboardingProfile + + const DEFAULT_CONFIG = DEFAULT_ACCOUNT_RECOVERY_CONFIGURATION[type] + + let accountStartIndex = 0 + let accountGapLimit = DEFAULT_CONFIG.initialAccountRange + + let addressStartIndex = 0 + const addressGapLimit = DEFAULT_CONFIG.addressGapLimit + let error = '' let isBusy = false - async function onFindBalancesClick(): Promise { - await checkOnboardingProfileAuth(async () => await findBalances(SearchMethod.SingleAddress)) - } + let accountsBalances: IAccountBalance[] = [] - let accountsBalances: { alias: string; total: string }[] = [] - - interface ISearch { - accountStartIndex: number - accountGapLimit: number - accountEndIndex: number - addressStartIndex: number - addressGapLimit: number - addressEndIndex: number - searchSpace: number - estimatedTime: number - actualTime?: number + const networkSearchMethod: { [key in StardustNetworkId]?: () => Promise } = { + [StardustNetworkId.Iota]: multiAddressSearch, + [StardustNetworkId.Shimmer]: singleAddressSearch, + [StardustNetworkId.Testnet]: singleAddressSearch, } - const searches: ISearch[] = [] - enum SearchMethod { - SingleAddress = 'SingleAddress', - Address = 'MultiAddress', + async function singleAddressSearch(): Promise { + const recoverAccountsPayload: RecoverAccountsPayload = { + accountStartIndex, + accountGapLimit, + addressGapLimit: 1, + syncOptions: { ...DEFAULT_SYNC_OPTIONS, addressStartIndex: 0 }, + } + + accountsBalances = await recoverAndGetBalances(recoverAccountsPayload) + const numberOfAccountsFound = Math.max(0, accountsBalances.length - accountStartIndex) + + accountStartIndex = accountStartIndex + accountGapLimit + numberOfAccountsFound } - async function findBalances(method: SearchMethod): Promise { - if (method === SearchMethod.SingleAddress) { - try { - error = '' - isBusy = true - const recoverAccountsPayload: RecoverAccountsPayload = { - accountStartIndex: 0, - accountGapLimit: currentAccountGapLimit, - addressGapLimit: currentAddressGapLimit, - syncOptions: { ...DEFAULT_SYNC_OPTIONS, addressStartIndex: 0 }, - } - const search: ISearch = { - accountStartIndex: recoverAccountsPayload.accountStartIndex, - accountGapLimit: recoverAccountsPayload.accountGapLimit, - accountEndIndex: recoverAccountsPayload.accountGapLimit, - addressStartIndex: recoverAccountsPayload.syncOptions.addressStartIndex, - addressGapLimit: recoverAccountsPayload.addressGapLimit, - addressEndIndex: recoverAccountsPayload.addressGapLimit, - searchSpace: recoverAccountsPayload.accountGapLimit * recoverAccountsPayload.addressGapLimit, - estimatedTime: 1 * recoverAccountsPayload.accountGapLimit * recoverAccountsPayload.addressGapLimit, - } - - const startTime = new Date().getTime() - - let accounts: IAccount[] = [] - accounts = [...accounts, ...(await recoverAccounts(recoverAccountsPayload))] - - if (accountsBalances.length === 0 && accounts.length === 0) { - accounts = [await createAccount({ alias: `${localize('general.account')} 1` })] - } - - accountsBalances = await Promise.all( - accounts.map(async (account: IAccount) => { - const alias = await account.getMetadata().alias - const balance = await account.getBalance() - const formattedBalance = formatTokenAmountBestMatch( - Number(balance?.baseCoin?.total ?? 0), - $onboardingProfile?.network?.baseToken - ) - return { - alias, - total: formattedBalance, - } - }) - ) - - const endTime = new Date().getTime() - search.actualTime = endTime - startTime - searches.push(search) - - previousAccountGapLimit = currentAccountGapLimit - previousAddressGapLimit = currentAddressGapLimit - currentAccountGapLimit += initialAccountRange - currentAddressGapLimit += addressGapLimitIncrement - } catch (err) { - error = localize(err.error) - console.error(error) - } finally { - isBusy = false + let searchCount = 0 + let depthSearchCount = 0 + let breadthSearchCountSinceLastDepthSearch = 0 + let depthSearch = false + // Please don't modify this algorithm without consulting with the team + async function multiAddressSearch(): Promise { + let recoverAccountsPayload: RecoverAccountsPayload + + if ( + !depthSearch && + breadthSearchCountSinceLastDepthSearch && + breadthSearchCountSinceLastDepthSearch % accountGapLimit === 0 + ) { + // Depth search + depthSearch = true + recoverAccountsPayload = { + accountStartIndex: accountGapLimit, + accountGapLimit: 1, + addressGapLimit: (searchCount - depthSearchCount) * addressGapLimit, + syncOptions: { ...DEFAULT_SYNC_OPTIONS, addressStartIndex: 0 }, } + breadthSearchCountSinceLastDepthSearch = 0 + depthSearchCount++ + accountGapLimit++ + } else { + // Breadth search + depthSearch = false + recoverAccountsPayload = { + accountStartIndex, + accountGapLimit, + addressGapLimit: addressGapLimit, + syncOptions: { ...DEFAULT_SYNC_OPTIONS, addressStartIndex }, + } + breadthSearchCountSinceLastDepthSearch++ + addressStartIndex += addressGapLimit + } + + accountsBalances = await recoverAndGetBalances(recoverAccountsPayload) + + searchCount++ + } + + async function findBalances(): Promise { + try { + error = '' + isBusy = true + await (networkSearchMethod[network.id] ?? singleAddressSearch)() + } catch (err) { + error = localize(err.error) + console.error(error) + } finally { + isBusy = false } } - function onContinueClick(): void { - $restoreProfileRouter.next() + async function recoverAndGetBalances(payload: RecoverAccountsPayload): Promise { + const accounts = await recoverAccounts(payload) + return await Promise.all(accounts.map(getAccountBalanceFromAccount)) + } + + async function getAccountBalanceFromAccount(account: IAccount): Promise { + const alias = (await account.getMetadata())?.alias + + const balance = await account.getBalance() + const baseToken = network.baseToken + const baseCoinBalance = Number(balance?.baseCoin?.total) ?? 0 + const total = formatTokenAmountBestMatch(baseCoinBalance, baseToken) + + return { alias, total } } function checkOnboardingProfileAuth(callback) { - if ($onboardingProfile.type === ProfileType.Software) { + if (type === ProfileType.Software) { return checkOrUnlockStronghold(callback) } else { return checkOrConnectLedger( callback, false, - $onboardingProfile?.network.id === SupportedNetworkId.Iota ? LedgerAppName.Iota : LedgerAppName.Shimmer + network.id === SupportedNetworkId.Iota ? LedgerAppName.Iota : LedgerAppName.Shimmer ) } } + function onContinueClick(): void { + $restoreProfileRouter.next() + } + + async function onFindBalancesClick(): Promise { + await checkOnboardingProfileAuth(async () => await findBalances()) + } + onMount(async () => { - await checkOnboardingProfileAuth(async () => await findBalances(SearchMethod.SingleAddress)) + await onFindBalancesClick() + if (accountsBalances.length === 0) { + const account = await createAccount({ alias: `${localize('general.account')} 1` }) + accountsBalances = [await getAccountBalanceFromAccount(account)] + } }) onDestroy(() => {}) diff --git a/packages/shared/src/lib/core/profile/constants/default-account-recovery-configuration.constant.ts b/packages/shared/src/lib/core/profile/constants/default-account-recovery-configuration.constant.ts index ec9dec539f..42d70b36f5 100644 --- a/packages/shared/src/lib/core/profile/constants/default-account-recovery-configuration.constant.ts +++ b/packages/shared/src/lib/core/profile/constants/default-account-recovery-configuration.constant.ts @@ -6,12 +6,12 @@ export const DEFAULT_ACCOUNT_RECOVERY_CONFIGURATION: AccountRecoveryConfiguratio initialAccountRange: 3, accountGapLimit: 1, numberOfRoundsBetweenBreadthSearch: 1, - addressGapLimit: 1, + addressGapLimit: 10, }, [ProfileType.Software]: { initialAccountRange: 10, accountGapLimit: 1, numberOfRoundsBetweenBreadthSearch: 1, - addressGapLimit: 1, + addressGapLimit: 100, }, }