Skip to content

Commit 85e1630

Browse files
authored
Merge branch 'develop' into chore/update-electron
2 parents a5e5d1c + fff73d8 commit 85e1630

File tree

7 files changed

+164
-21
lines changed

7 files changed

+164
-21
lines changed

Diff for: 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')}

Diff for: packages/desktop/components/popups/AddressHistoryPopup.svelte

+144-17
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,151 @@
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'
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'
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
39+
40+
function onCopyClick(): void {
41+
const addresses = knownAddresses.map((address) => address.address).join(',')
42+
setClipboard(addresses)
43+
}
1144
1245
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+
}
2267
})
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+
}
23139
</script>
24140

25141
<div class="flex flex-col space-y-6">
26142
<Text type={TextType.h3} fontWeight={FontWeight.semibold} lineHeight="6">
27143
{localize('popups.addressHistory.title')}
28144
</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}
32147
<div class="w-full flex-col space-y-2 virtual-list-wrapper">
33-
<VirtualList items={addressList} let:item>
148+
<VirtualList items={knownAddresses} let:item>
34149
<div class="mb-1">
35150
<KeyValueBox
36151
isCopyable
@@ -58,6 +173,18 @@
58173
</div>
59174
{/if}
60175
</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>
61188

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

Diff for: 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/'

Diff for: 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) {

Diff for: 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)