Skip to content

Commit 6ee934c

Browse files
msarcevbegonaalvarezdbrancoder
authored
Feat: Add "discover addresses" using a permanode (#7750)
* feat: Add address generation and history checking (chronicle) with fixed step algo to AddressHistoryPopup * feat: Implement search algo using "search gap" Add some error handing * fix: Fix the chronicle URL constants for all networks * feat: Localize error strings * feat: Add missing field in checkAndMigrateChrysalisProfiles.ts * fix: typo * chore: improve error handling & add alphanet * feat: init and sort * chore: remove unncessary disclaimer --------- Co-authored-by: Begoña Álvarez de la Cruz <[email protected]> Co-authored-by: Branko Bosnic <[email protected]>
1 parent e200015 commit 6ee934c

File tree

7 files changed

+157
-20
lines changed

7 files changed

+157
-20
lines changed

packages/desktop/components/modals/AccountActionsMenu.svelte

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
<Modal bind:this={modal} {...$$restProps}>
7373
<account-actions-menu class="flex flex-col">
7474
<MenuItem icon={Icon.Doc} title={localize('actions.viewBalanceBreakdown')} onClick={onViewBalanceClick} />
75-
{#if $activeProfile?.network?.id === NetworkId.Iota}
75+
{#if $activeProfile?.network?.id === NetworkId.Iota || $activeProfile?.network?.id === NetworkId.IotaAlphanet}
7676
<MenuItem
7777
icon={Icon.Timer}
7878
title={localize('actions.viewAddressHistory')}

packages/desktop/components/popups/AddressHistoryPopup.svelte

+137-16
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,146 @@
11
<script lang="ts">
2-
import { getSelectedAccount } from '@core/account'
2+
import { selectedAccount } from '@core/account'
3+
import { handleError } from '@core/error/handlers/handleError'
34
import { localize } from '@core/i18n'
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'
49
import { truncateString } from '@core/utils'
510
import { AccountAddress } from '@iota/sdk/out/types'
611
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'
813
import { onMount } from 'svelte'
914
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
1139
1240
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-
})
41+
knownAddresses = $selectedAccount?.knownAddresses
42+
if (!knownAddresses?.length) {
43+
isBusy = true
44+
$selectedAccount
45+
.addresses()
46+
.then((_knownAddresses) => {
47+
knownAddresses = sortAddresses(_knownAddresses)
48+
updateAccountPersistedDataOnActiveProfile(accountIndex, { knownAddresses })
49+
isBusy = false
50+
})
51+
.finally(() => {
52+
isBusy = false
53+
})
54+
}
55+
56+
if (CHRONICLE_URLS[network] && CHRONICLE_URLS[network].length > 0) {
57+
const chronicleRoot = CHRONICLE_URLS[network][0]
58+
searchURL = `${chronicleRoot}${CHRONICLE_ADDRESS_HISTORY_ROUTE}`
59+
} else {
60+
throw new Error(localize('popups.addressHistory.errorNoChronicle'))
61+
}
2262
})
63+
64+
async function isAddressWithHistory(address: string): Promise<boolean> {
65+
try {
66+
const response = await fetchWithTimeout(`${searchURL}${address}`, 3, { method: 'GET' })
67+
const addressHistory: AddressHistory = await response.json()
68+
return addressHistory?.items?.length > 0
69+
} catch (err) {
70+
throw new Error(localize('popups.addressHistory.errorFailedFetch'))
71+
}
72+
}
73+
74+
async function generateNextUnknownAddress(): Promise<[string, number]> {
75+
let nextUnknownAddress: string
76+
try {
77+
do {
78+
nextUnknownAddress = await getProfileManager().generateEd25519Address(
79+
accountIndex,
80+
searchAddressStartIndex
81+
)
82+
83+
searchAddressStartIndex++
84+
} while (knownAddresses.map((accountAddress) => accountAddress.address).includes(nextUnknownAddress))
85+
} catch (err) {
86+
throw new Error(localize('popups.addressHistory.errorFailedGenerate'))
87+
}
88+
89+
return [nextUnknownAddress, searchAddressStartIndex - 1]
90+
}
91+
92+
async function search(): Promise<void> {
93+
currentSearchGap = 0
94+
const tmpKnownAddresses = [...knownAddresses]
95+
while (currentSearchGap < ADDRESS_GAP_LIMIT) {
96+
const [nextAddressToCheck, addressIndex] = await generateNextUnknownAddress()
97+
if (!nextAddressToCheck) {
98+
isBusy = false
99+
break
100+
}
101+
102+
const hasHistory = await isAddressWithHistory(nextAddressToCheck)
103+
if (hasHistory) {
104+
const accountAddress: AccountAddress = {
105+
address: nextAddressToCheck,
106+
keyIndex: addressIndex,
107+
internal: false,
108+
used: true,
109+
}
110+
111+
tmpKnownAddresses.push(accountAddress)
112+
} else {
113+
currentSearchGap++
114+
}
115+
}
116+
knownAddresses = sortAddresses(tmpKnownAddresses)
117+
updateAccountPersistedDataOnActiveProfile(accountIndex, { knownAddresses })
118+
}
119+
120+
async function handleSearchClick(): Promise<void> {
121+
isBusy = true
122+
try {
123+
await checkActiveProfileAuth(search, { stronghold: true, ledger: true })
124+
} catch (err) {
125+
handleError(err)
126+
} finally {
127+
isBusy = false
128+
}
129+
}
130+
131+
function sortAddresses(addresses: AccountAddress[] = []): AccountAddress[] {
132+
return addresses.sort((a, b) => a.keyIndex - b.keyIndex)
133+
}
23134
</script>
24135

25136
<div class="flex flex-col space-y-6">
26137
<Text type={TextType.h3} fontWeight={FontWeight.semibold} lineHeight="6">
27138
{localize('popups.addressHistory.title')}
28139
</Text>
29-
<Text fontSize="15" color="gray-700" classes="text-left">{localize('popups.addressHistory.disclaimer')}</Text>
30-
{#if addressList}
31-
{#if addressList.length > 0}
140+
{#if knownAddresses}
141+
{#if knownAddresses.length > 0}
32142
<div class="w-full flex-col space-y-2 virtual-list-wrapper">
33-
<VirtualList items={addressList} let:item>
143+
<VirtualList items={knownAddresses} let:item>
34144
<div class="mb-1">
35145
<KeyValueBox
36146
isCopyable
@@ -58,6 +168,17 @@
58168
</div>
59169
{/if}
60170
</div>
171+
<div class="flex flex-row flex-nowrap w-full space-x-4 mt-6">
172+
<Button
173+
classes="w-full"
174+
onClick={handleSearchClick}
175+
disabled={isBusy}
176+
{isBusy}
177+
busyMessage={localize('actions.searching')}
178+
>
179+
{localize('actions.search')}
180+
</Button>
181+
</div>
61182

62183
<style lang="scss">
63184
.virtual-list-wrapper :global(svelte-virtual-list-viewport) {

packages/shared/lib/core/account/actions/buildAccountStateAndPersistedData.ts

+2
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ export async function buildAccountStateAndPersistedData(
99
color?: string
1010
): Promise<[IAccountState, IPersistedAccountData]> {
1111
const { index } = account.getMetadata()
12+
const knownAddresses = await account.addresses()
1213
const persistedAccountData: IPersistedAccountData = {
1314
name: name || `${localize('general.account')} ${index + 1}`,
1415
color: color || getRandomAccountColor(),
1516
hidden: false,
1617
shouldRevote: false,
18+
knownAddresses,
1719
}
1820
const accountState = await buildAccountState(account, persistedAccountData)
1921
return [accountState, persistedAccountData]
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { ParticipationEventId } from '@iota/sdk/out/types'
1+
import { AccountAddress, ParticipationEventId } from '@iota/sdk/out/types'
22

33
export interface IPersistedAccountData {
44
name: string
55
color: string
66
hidden: boolean
77
shouldRevote: boolean
88
removedProposalIds?: ParticipationEventId[]
9+
knownAddresses: AccountAddress[]
910
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { NetworkId } from '../enums'
2+
3+
export const CHRONICLE_URLS: Readonly<{ [key in NetworkId]?: string[] }> = {
4+
[NetworkId.Iota]: ['https://chronicle.stardust-mainnet.iotaledger.net/'],
5+
[NetworkId.IotaAlphanet]: ['https://chronicle.alphanet.iotaledger.net/'],
6+
[NetworkId.Shimmer]: ['https://chronicle.shimmer.network/'],
7+
[NetworkId.Testnet]: ['https://chronicle.testnet.shimmer.network/'],
8+
}
9+
10+
export const CHRONICLE_ADDRESS_HISTORY_ROUTE = 'api/explorer/v2/ledger/updates/by-address/'

packages/shared/lib/core/profile/actions/profiles/checkAndMigrateChrysalisProfiles.ts

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export function checkAndMigrateChrysalisProfiles(): boolean {
4848
color: account.color,
4949
hidden: chrysalisProfile.hiddenAccounts?.includes(account.id) ?? false,
5050
shouldRevote: false,
51+
knownAddresses: [],
5152
}
5253

5354
if (chrysalisProfile.lastUsedAccountId === account.id) {

packages/shared/locales/en.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -1185,9 +1185,11 @@
11851185
},
11861186
"addressHistory": {
11871187
"title": "Address history",
1188-
"disclaimer": "List of addresses with funds or known by this profile",
11891188
"indexAndType": "{internal, select, true {Internal address} other {Deposit address}} {index}",
1190-
"internal": "Internal"
1189+
"internal": "Internal",
1190+
"errorNoChronicle": "Chronicle not configured",
1191+
"errorFailedFetch": "Couldn't fetch address history",
1192+
"errorFailedGenerate": "Couldn't generate a new address"
11911193
}
11921194
},
11931195
"charts": {

0 commit comments

Comments
 (0)