diff --git a/docs/specifications/deep-links.md b/docs/specifications/deep-links.md index 96a7fe4d4a0..1f81e7231f3 100644 --- a/docs/specifications/deep-links.md +++ b/docs/specifications/deep-links.md @@ -22,12 +22,21 @@ confirmation on behalf of the user. ## Scheme -The Firefly deep link scheme can be broken down to the following (simple) syntax: +Our system incorporates two specific deeplink schemes—namely IOTA and SHIMMER. Breaking down the Firefly deep link scheme reveals the following simple syntax: + + +**IOTA** ``` iota[-]:///[?param=] ``` +**Shimmer** + +``` +firefly[-]:///[?param=] +``` + The parameters are as follows: - `stage` - indicates a specific stage of the app to target, options are: @@ -40,15 +49,14 @@ The parameters are as follows: - `operation` - an operation within a specific context (see below for more detail) - `param` - query parameter(s) relevant for the specified operation -If you wish to target the production version, simply omit this from the prefix: +To target the production version simply don't specify any stages, example for Shimmer: ``` firefly:// ``` -:::caution -This deep link scheme is **NOT** compatible with Firefly V1, as that version of the application is in maintenance mode. -::: +This prefix is specifically meant for the production version of Firefly. You don't need to add anything else after ``firefly://`` + ## Contexts @@ -103,7 +111,7 @@ This operation brings the user to the send confirmation popup: The deep link structure is as follows: ``` -firefly://wallet/sendConfirmation?address=
&amount=[&unit=][&assetId=][&metadata=][&tag=][&giftStorageDeposit=][&disableToggleGift=][&disableChangeExpiration=][&surplus=] +firefly://wallet/sendConfirmation?address=
&amount=[&unit=][&assetId=][&metadata=][&tag=][&giftStorageDeposit=][&disableToggleGift=][&disableChangeExpiration=][&surplus=][&expiration=] ``` The following parameters are **required**: @@ -115,7 +123,7 @@ The following parameters are **required**: The following parameters are **optional**: -- `unit` - a specified denomination of the token to use, if applicable (default for IOTA is `Mi`, SMR is `SMR`) +- `unit` - a specified denomination of the token to use, if applicable (default for IOTA is `micro`, SMR is `glow`) - `assetId` - the identifier of the asset to send, e.g. `4218` (IOTA), `4219` (SMR), or a native token ID (default is base token of the network, i.e. IOTA or SMR) - `metadata` - a string of text to embed as metadata in the transaction - `tag` - a string to tag the transaction for indexing purposes @@ -123,15 +131,16 @@ The following parameters are **optional**: - `disableToggleGift` - prevents the user from being able to toggle the option to gift the storage deposit - `disableChangeExpiration` - prevents the user from being able to change the expiration time of the transaction - `surplus` - send additional amounts of the base token when transferring native tokens +- `expiration` - the expiration time of the transaction, e.g. `1w`, `2d`, `5h` or `10m`. Also accepts a UNIX timestamp in milliseconds. Example: -[!button Click me!](firefly://wallet/sendForm?address=iota1qrhacyfwlcnzkvzteumekfkrrwks98mpdm37cj4xx3drvmjvnep6xqgyzyx&amount=10&unit=Gi&giftStorageDeposit=true&surplus=1&metadata=Take%20my%20money) +[!button Click me!](firefly://wallet/sendConfirmation?address=iota1qrhacyfwlcnzkvzteumekfkrrwks98mpdm37cj4xx3drvmjvnep6xqgyzyx&amount=10&unit=Gi&giftStorageDeposit=true&surplus=1&metadata=Take%20my%20money&expiration=1h) Source: ``` -firefly://wallet/sendConfirmation?address=iota1qrhacyfwlcnzkvzteumekfkrrwks98mpdm37cj4xx3drvmjvnep6xqgyzyx&amount=10&unit=Gi&giftStorageDeposit=true&disableToggleGift=true&surplus=1&metadata=Take%20my%20money +firefly://wallet/sendConfirmation?address=iota1qrhacyfwlcnzkvzteumekfkrrwks98mpdm37cj4xx3drvmjvnep6xqgyzyx&amount=10&unit=Gi&giftStorageDeposit=true&disableToggleGift=true&surplus=1&metadata=Take%20my%20money&expiration=1h ``` ### Collectibles diff --git a/docs/static/send-confirmation-popup.png b/docs/static/send-confirmation-popup.png index f9a06e6e169..9a1693504bf 100644 Binary files a/docs/static/send-confirmation-popup.png and b/docs/static/send-confirmation-popup.png differ diff --git a/docs/static/send-form-popup.png b/docs/static/send-form-popup.png index 474b785c0c2..913a30d6d12 100644 Binary files a/docs/static/send-form-popup.png and b/docs/static/send-form-popup.png differ diff --git a/packages/desktop/.sentrycliignore b/packages/desktop/.sentrycliignore deleted file mode 100644 index 01a935f091c..00000000000 --- a/packages/desktop/.sentrycliignore +++ /dev/null @@ -1,6 +0,0 @@ -node_modules/ -scripts/ - -postcss.config.js -webpack.config.js -electron-builder-config.js diff --git a/packages/desktop/components/modals/AccountActionsMenu.svelte b/packages/desktop/components/modals/AccountActionsMenu.svelte index 691e11a08bf..c0e6ad55268 100644 --- a/packages/desktop/components/modals/AccountActionsMenu.svelte +++ b/packages/desktop/components/modals/AccountActionsMenu.svelte @@ -34,6 +34,11 @@ modal?.close() } + function onWithdrawFromL2Click(): void { + openPopup({ id: PopupId.WithdrawFromL2 }) + modal?.close() + } + function onVerifyAddressClick(): void { const ADDRESS_INDEX = 0 checkOrConnectLedger(() => { @@ -72,13 +77,16 @@ - {#if $activeProfile?.network?.id === NetworkId.Iota} + {#if $activeProfile?.network?.id === NetworkId.Iota || $activeProfile?.network?.id === NetworkId.IotaAlphanet} {/if} + {#if $activeProfile?.network?.id === NetworkId.Shimmer || $activeProfile?.network?.id === NetworkId.Testnet} + + {/if} {#if $isActiveLedgerProfile} - 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}
+
+
+ + +
+