diff --git a/packages/desktop/components/modals/AccountActionsMenu.svelte b/packages/desktop/components/modals/AccountActionsMenu.svelte index 691e11a08bf..f2b1d3e0866 100644 --- a/packages/desktop/components/modals/AccountActionsMenu.svelte +++ b/packages/desktop/components/modals/AccountActionsMenu.svelte @@ -72,7 +72,7 @@ - {#if $activeProfile?.network?.id === NetworkId.Iota} + {#if $activeProfile?.network?.id === NetworkId.Iota || $activeProfile?.network?.id === NetworkId.IotaAlphanet} - import { getSelectedAccount } from '@core/account' + import { selectedAccount } from '@core/account' + import { handleError } from '@core/error/handlers/handleError' import { localize } from '@core/i18n' - import { truncateString } from '@core/utils' + import { CHRONICLE_ADDRESS_HISTORY_ROUTE, CHRONICLE_URLS } from '@core/network/constants/chronicle-urls.constant' + import { fetchWithTimeout } from '@core/nfts' + import { checkActiveProfileAuth, getActiveProfile, updateAccountPersistedDataOnActiveProfile } from '@core/profile' + import { getProfileManager } from '@core/profile-manager/stores' + import { setClipboard, truncateString } from '@core/utils' import { AccountAddress } from '@iota/sdk/out/types' import VirtualList from '@sveltejs/svelte-virtual-list' - import { FontWeight, KeyValueBox, Spinner, Text, TextType } from 'shared/components' + import { Button, FontWeight, KeyValueBox, Spinner, Text, TextType } from 'shared/components' import { onMount } from 'svelte' - let addressList: AccountAddress[] | undefined = undefined + interface AddressHistory { + address: string + items: [ + { + milestoneIndex: number + milestoneTimestamp: number + outputId: string + isSpent: boolean + } + ] + } + + const activeProfile = getActiveProfile() + const ADDRESS_GAP_LIMIT = 20 + + let knownAddresses: AccountAddress[] = [] + + $: accountIndex = $selectedAccount?.index + $: network = activeProfile?.network?.id + + let searchURL: string + let searchAddressStartIndex = 0 + let currentSearchGap = 0 + let isBusy = false + + function onCopyClick(): void { + const addresses = knownAddresses.map((address) => address.address).join(',') + setClipboard(addresses) + } onMount(() => { - getSelectedAccount() - ?.addresses() - .then((_addressList) => { - addressList = _addressList?.sort((a, b) => a.keyIndex - b.keyIndex) ?? [] - }) - .catch((err) => { - console.error(err) - addressList = [] - }) + knownAddresses = $selectedAccount?.knownAddresses + if (!knownAddresses?.length) { + isBusy = true + $selectedAccount + .addresses() + .then((_knownAddresses) => { + knownAddresses = sortAddresses(_knownAddresses) + updateAccountPersistedDataOnActiveProfile(accountIndex, { knownAddresses }) + isBusy = false + }) + .finally(() => { + isBusy = false + }) + } + + if (CHRONICLE_URLS[network] && CHRONICLE_URLS[network].length > 0) { + const chronicleRoot = CHRONICLE_URLS[network][0] + searchURL = `${chronicleRoot}${CHRONICLE_ADDRESS_HISTORY_ROUTE}` + } else { + throw new Error(localize('popups.addressHistory.errorNoChronicle')) + } }) + + async function isAddressWithHistory(address: string): Promise { + try { + const response = await fetchWithTimeout(`${searchURL}${address}`, 3, { method: 'GET' }) + const addressHistory: AddressHistory = await response.json() + return addressHistory?.items?.length > 0 + } catch (err) { + throw new Error(localize('popups.addressHistory.errorFailedFetch')) + } + } + + async function generateNextUnknownAddress(): Promise<[string, number]> { + let nextUnknownAddress: string + try { + do { + nextUnknownAddress = await getProfileManager().generateEd25519Address( + accountIndex, + searchAddressStartIndex + ) + + searchAddressStartIndex++ + } while (knownAddresses.map((accountAddress) => accountAddress.address).includes(nextUnknownAddress)) + } catch (err) { + throw new Error(localize('popups.addressHistory.errorFailedGenerate')) + } + + return [nextUnknownAddress, searchAddressStartIndex - 1] + } + + async function search(): Promise { + currentSearchGap = 0 + const tmpKnownAddresses = [...knownAddresses] + while (currentSearchGap < ADDRESS_GAP_LIMIT) { + const [nextAddressToCheck, addressIndex] = await generateNextUnknownAddress() + if (!nextAddressToCheck) { + isBusy = false + break + } + + const hasHistory = await isAddressWithHistory(nextAddressToCheck) + if (hasHistory) { + const accountAddress: AccountAddress = { + address: nextAddressToCheck, + keyIndex: addressIndex, + internal: false, + used: true, + } + + tmpKnownAddresses.push(accountAddress) + } else { + currentSearchGap++ + } + } + knownAddresses = sortAddresses(tmpKnownAddresses) + updateAccountPersistedDataOnActiveProfile(accountIndex, { knownAddresses }) + } + + async function handleSearchClick(): Promise { + isBusy = true + try { + await checkActiveProfileAuth(search, { stronghold: true, ledger: true }) + } catch (err) { + handleError(err) + } finally { + isBusy = false + } + } + + function sortAddresses(addresses: AccountAddress[] = []): AccountAddress[] { + return addresses.sort((a, b) => a.keyIndex - b.keyIndex) + }
{localize('popups.addressHistory.title')} - {localize('popups.addressHistory.disclaimer')} - {#if addressList} - {#if addressList.length > 0} + {#if knownAddresses} + {#if knownAddresses.length > 0}
- +
{/if}
+
+
+ + +
+