|
1 | 1 | <script lang="ts">
|
2 |
| - import { getSelectedAccount } from '@core/account' |
| 2 | + import { selectedAccount } from '@core/account' |
| 3 | + import { handleError } from '@core/error/handlers/handleError' |
3 | 4 | import { localize } from '@core/i18n'
|
4 |
| - import { truncateString } from '@core/utils' |
| 5 | + import { CHRONICLE_ADDRESS_HISTORY_ROUTE, CHRONICLE_URLS } from '@core/network/constants/chronicle-urls.constant' |
| 6 | + import { fetchWithTimeout } from '@core/nfts' |
| 7 | + import { checkActiveProfileAuth, getActiveProfile, updateAccountPersistedDataOnActiveProfile } from '@core/profile' |
| 8 | + import { getProfileManager } from '@core/profile-manager/stores' |
| 9 | + import { setClipboard, truncateString } from '@core/utils' |
5 | 10 | import { AccountAddress } from '@iota/sdk/out/types'
|
6 | 11 | import VirtualList from '@sveltejs/svelte-virtual-list'
|
7 |
| - import { FontWeight, KeyValueBox, Spinner, Text, TextType } from 'shared/components' |
| 12 | + import { Button, FontWeight, KeyValueBox, Spinner, Text, TextType } from 'shared/components' |
8 | 13 | import { onMount } from 'svelte'
|
9 | 14 |
|
10 |
| - let addressList: AccountAddress[] | undefined = undefined |
| 15 | + interface AddressHistory { |
| 16 | + address: string |
| 17 | + items: [ |
| 18 | + { |
| 19 | + milestoneIndex: number |
| 20 | + milestoneTimestamp: number |
| 21 | + outputId: string |
| 22 | + isSpent: boolean |
| 23 | + } |
| 24 | + ] |
| 25 | + } |
| 26 | +
|
| 27 | + const activeProfile = getActiveProfile() |
| 28 | + const ADDRESS_GAP_LIMIT = 20 |
| 29 | +
|
| 30 | + let knownAddresses: AccountAddress[] = [] |
| 31 | +
|
| 32 | + $: accountIndex = $selectedAccount?.index |
| 33 | + $: network = activeProfile?.network?.id |
| 34 | +
|
| 35 | + let searchURL: string |
| 36 | + let searchAddressStartIndex = 0 |
| 37 | + let currentSearchGap = 0 |
| 38 | + let isBusy = false |
| 39 | +
|
| 40 | + function onCopyClick(): void { |
| 41 | + const addresses = knownAddresses.map((address) => address.address).join(',') |
| 42 | + setClipboard(addresses) |
| 43 | + } |
11 | 44 |
|
12 | 45 | onMount(() => {
|
13 |
| - getSelectedAccount() |
14 |
| - ?.addresses() |
15 |
| - .then((_addressList) => { |
16 |
| - addressList = _addressList?.sort((a, b) => a.keyIndex - b.keyIndex) ?? [] |
17 |
| - }) |
18 |
| - .catch((err) => { |
19 |
| - console.error(err) |
20 |
| - addressList = [] |
21 |
| - }) |
| 46 | + knownAddresses = $selectedAccount?.knownAddresses |
| 47 | + if (!knownAddresses?.length) { |
| 48 | + isBusy = true |
| 49 | + $selectedAccount |
| 50 | + .addresses() |
| 51 | + .then((_knownAddresses) => { |
| 52 | + knownAddresses = sortAddresses(_knownAddresses) |
| 53 | + updateAccountPersistedDataOnActiveProfile(accountIndex, { knownAddresses }) |
| 54 | + isBusy = false |
| 55 | + }) |
| 56 | + .finally(() => { |
| 57 | + isBusy = false |
| 58 | + }) |
| 59 | + } |
| 60 | +
|
| 61 | + if (CHRONICLE_URLS[network] && CHRONICLE_URLS[network].length > 0) { |
| 62 | + const chronicleRoot = CHRONICLE_URLS[network][0] |
| 63 | + searchURL = `${chronicleRoot}${CHRONICLE_ADDRESS_HISTORY_ROUTE}` |
| 64 | + } else { |
| 65 | + throw new Error(localize('popups.addressHistory.errorNoChronicle')) |
| 66 | + } |
22 | 67 | })
|
| 68 | +
|
| 69 | + async function isAddressWithHistory(address: string): Promise<boolean> { |
| 70 | + try { |
| 71 | + const response = await fetchWithTimeout(`${searchURL}${address}`, 3, { method: 'GET' }) |
| 72 | + const addressHistory: AddressHistory = await response.json() |
| 73 | + return addressHistory?.items?.length > 0 |
| 74 | + } catch (err) { |
| 75 | + throw new Error(localize('popups.addressHistory.errorFailedFetch')) |
| 76 | + } |
| 77 | + } |
| 78 | +
|
| 79 | + async function generateNextUnknownAddress(): Promise<[string, number]> { |
| 80 | + let nextUnknownAddress: string |
| 81 | + try { |
| 82 | + do { |
| 83 | + nextUnknownAddress = await getProfileManager().generateEd25519Address( |
| 84 | + accountIndex, |
| 85 | + searchAddressStartIndex |
| 86 | + ) |
| 87 | +
|
| 88 | + searchAddressStartIndex++ |
| 89 | + } while (knownAddresses.map((accountAddress) => accountAddress.address).includes(nextUnknownAddress)) |
| 90 | + } catch (err) { |
| 91 | + throw new Error(localize('popups.addressHistory.errorFailedGenerate')) |
| 92 | + } |
| 93 | +
|
| 94 | + return [nextUnknownAddress, searchAddressStartIndex - 1] |
| 95 | + } |
| 96 | +
|
| 97 | + async function search(): Promise<void> { |
| 98 | + currentSearchGap = 0 |
| 99 | + const tmpKnownAddresses = [...knownAddresses] |
| 100 | + while (currentSearchGap < ADDRESS_GAP_LIMIT) { |
| 101 | + const [nextAddressToCheck, addressIndex] = await generateNextUnknownAddress() |
| 102 | + if (!nextAddressToCheck) { |
| 103 | + isBusy = false |
| 104 | + break |
| 105 | + } |
| 106 | +
|
| 107 | + const hasHistory = await isAddressWithHistory(nextAddressToCheck) |
| 108 | + if (hasHistory) { |
| 109 | + const accountAddress: AccountAddress = { |
| 110 | + address: nextAddressToCheck, |
| 111 | + keyIndex: addressIndex, |
| 112 | + internal: false, |
| 113 | + used: true, |
| 114 | + } |
| 115 | +
|
| 116 | + tmpKnownAddresses.push(accountAddress) |
| 117 | + } else { |
| 118 | + currentSearchGap++ |
| 119 | + } |
| 120 | + } |
| 121 | + knownAddresses = sortAddresses(tmpKnownAddresses) |
| 122 | + updateAccountPersistedDataOnActiveProfile(accountIndex, { knownAddresses }) |
| 123 | + } |
| 124 | +
|
| 125 | + async function handleSearchClick(): Promise<void> { |
| 126 | + isBusy = true |
| 127 | + try { |
| 128 | + await checkActiveProfileAuth(search, { stronghold: true, ledger: true }) |
| 129 | + } catch (err) { |
| 130 | + handleError(err) |
| 131 | + } finally { |
| 132 | + isBusy = false |
| 133 | + } |
| 134 | + } |
| 135 | +
|
| 136 | + function sortAddresses(addresses: AccountAddress[] = []): AccountAddress[] { |
| 137 | + return addresses.sort((a, b) => a.keyIndex - b.keyIndex) |
| 138 | + } |
23 | 139 | </script>
|
24 | 140 |
|
25 | 141 | <div class="flex flex-col space-y-6">
|
26 | 142 | <Text type={TextType.h3} fontWeight={FontWeight.semibold} lineHeight="6">
|
27 | 143 | {localize('popups.addressHistory.title')}
|
28 | 144 | </Text>
|
29 |
| - <Text fontSize="15" color="gray-700" classes="text-left">{localize('popups.addressHistory.disclaimer')}</Text> |
30 |
| - {#if addressList} |
31 |
| - {#if addressList.length > 0} |
| 145 | + {#if knownAddresses} |
| 146 | + {#if knownAddresses.length > 0} |
32 | 147 | <div class="w-full flex-col space-y-2 virtual-list-wrapper">
|
33 |
| - <VirtualList items={addressList} let:item> |
| 148 | + <VirtualList items={knownAddresses} let:item> |
34 | 149 | <div class="mb-1">
|
35 | 150 | <KeyValueBox
|
36 | 151 | isCopyable
|
|
58 | 173 | </div>
|
59 | 174 | {/if}
|
60 | 175 | </div>
|
| 176 | +<div class="flex flex-row flex-nowrap w-full space-x-4 mt-6"> |
| 177 | + <div class="flex w-full justify-center pt-8 space-x-4"> |
| 178 | + <Button outline classes="w-1/2" onClick={onCopyClick}>{localize('actions.copy')}</Button> |
| 179 | + <Button |
| 180 | + classes="w-1/2" |
| 181 | + onClick={handleSearchClick} |
| 182 | + disabled={isBusy} |
| 183 | + {isBusy} |
| 184 | + busyMessage={localize('actions.searching')}>{localize('actions.search')}</Button |
| 185 | + > |
| 186 | + </div> |
| 187 | +</div> |
61 | 188 |
|
62 | 189 | <style lang="scss">
|
63 | 190 | .virtual-list-wrapper :global(svelte-virtual-list-viewport) {
|
|
0 commit comments