Skip to content

Commit

Permalink
feat: multi address algorithm in balance finder for IOTA profiles (#1610
Browse files Browse the repository at this point in the history
)

* refactor: clean BalanceFinderView

* refactor: clean code for SyncAccountsPopup

* refactor: cleanup single account search in onboarding

Co-authored-by: Jean Ribeiro <[email protected]>

* 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 <[email protected]>
  • Loading branch information
jeeanribeiro and nicole-obrien authored Dec 18, 2023
1 parent daf059c commit 3a32798
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 111 deletions.
96 changes: 78 additions & 18 deletions packages/desktop/components/popup/popups/SyncAccountsPopup.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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)
Expand All @@ -60,8 +58,70 @@
}
}
const networkSearchMethod: { [key in StardustNetworkId]?: () => Promise<void> } = {
[StardustNetworkId.Iota]: multiAddressSearch,
[StardustNetworkId.Shimmer]: singleAddressSearch,
[StardustNetworkId.Testnet]: singleAddressSearch,
}
async function singleAddressSearch(): Promise<void> {
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<void> {
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<void> {
await checkActiveProfileAuth(searchForBalance, {
await checkActiveProfileAuth(() => searchForBalance(), {
stronghold: true,
ledger: true,
props: { searchForBalancesOnLoad: true },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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<void> } = {
[StardustNetworkId.Iota]: multiAddressSearch,
[StardustNetworkId.Shimmer]: singleAddressSearch,
[StardustNetworkId.Testnet]: singleAddressSearch,
}
const searches: ISearch[] = []
enum SearchMethod {
SingleAddress = 'SingleAddress',
Address = 'MultiAddress',
async function singleAddressSearch(): Promise<void> {
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<void> {
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<void> {
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<void> {
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<IAccountBalance[]> {
const accounts = await recoverAccounts(payload)
return await Promise.all(accounts.map(getAccountBalanceFromAccount))
}
async function getAccountBalanceFromAccount(account: IAccount): Promise<IAccountBalance> {
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<void> {
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(() => {})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
}

0 comments on commit 3a32798

Please sign in to comment.