From 4e53982dc6198d4097763d0ab9b679670b08d421 Mon Sep 17 00:00:00 2001 From: h0ngcha0 Date: Thu, 18 Jan 2024 00:00:43 +0100 Subject: [PATCH 01/17] Limit tx history --- packages/dapp/src/services/wallet.service.ts | 2 +- .../src/background/accounDeployAction.ts | 2 +- .../extension/src/background/notification.ts | 9 +- .../src/background/transactions/badgeText.ts | 2 +- .../transactions/sources/onchain.ts | 10 +- .../transactions/sources/voyager.ts | 32 +--- .../src/background/transactions/store.ts | 51 +----- .../src/background/transactions/tracking.ts | 11 +- .../transactions/transactionExecution.ts | 2 +- .../extension/src/shared/storage/array.ts | 17 +- packages/extension/src/shared/transactions.ts | 93 ---------- .../src/shared/transactions/index.ts | 161 ++++++++++++++++++ .../src/shared/transactions/store.ts | 27 +++ .../transactions/transformers.ts | 0 .../accountTokens/useTransactionStatus.ts | 2 +- .../accounts/accountTransactions.state.ts | 2 +- yarn.lock | 33 ++++ 17 files changed, 259 insertions(+), 197 deletions(-) delete mode 100644 packages/extension/src/shared/transactions.ts create mode 100644 packages/extension/src/shared/transactions/index.ts create mode 100644 packages/extension/src/shared/transactions/store.ts rename packages/extension/src/{background => shared}/transactions/transformers.ts (100%) diff --git a/packages/dapp/src/services/wallet.service.ts b/packages/dapp/src/services/wallet.service.ts index 4955f6abe..e295b802d 100644 --- a/packages/dapp/src/services/wallet.service.ts +++ b/packages/dapp/src/services/wallet.service.ts @@ -64,7 +64,7 @@ export const signMessage = async (message: string, messageHasher: MessageHasher) return await alephium.signMessage({ signerAddress: alephium.connectedAccount.address, - message, + message: message, messageHasher }) } diff --git a/packages/extension/src/background/accounDeployAction.ts b/packages/extension/src/background/accounDeployAction.ts index c34d17843..2cafe3ea9 100644 --- a/packages/extension/src/background/accounDeployAction.ts +++ b/packages/extension/src/background/accounDeployAction.ts @@ -1,4 +1,4 @@ import { ExtQueueItem } from "../shared/actionQueue/types" import { BaseWalletAccount } from "../shared/wallet.model" import { BackgroundService } from "./background" -import { addTransaction } from "./transactions/store" +import { addTransaction } from "../shared/transactions/store" diff --git a/packages/extension/src/background/notification.ts b/packages/extension/src/background/notification.ts index 045a915ad..18989ead3 100644 --- a/packages/extension/src/background/notification.ts +++ b/packages/extension/src/background/notification.ts @@ -41,11 +41,10 @@ export async function sentTransactionNotification( meta?: TransactionMeta, ) { const id = `TX:${hash}:${networkId}` - const title = `${meta?.title || "Transaction"} ${ - ["ACCEPTED_ON_CHAIN", "ACCEPTED_ON_MEMPOOL", "PENDING"].includes(status) - ? "succeeded" - : "rejected" - }` + const title = `${meta?.title || "Transaction"} ${["ACCEPTED_ON_CHAIN", "ACCEPTED_ON_MEMPOOL", "PENDING"].includes(status) + ? "succeeded" + : "rejected" + }` return browser.notifications.create(id, { type: "basic", title, diff --git a/packages/extension/src/background/transactions/badgeText.ts b/packages/extension/src/background/transactions/badgeText.ts index f8b89b06a..30a36ff2c 100644 --- a/packages/extension/src/background/transactions/badgeText.ts +++ b/packages/extension/src/background/transactions/badgeText.ts @@ -8,7 +8,7 @@ import { Transaction } from "../../shared/transactions" import { BaseWalletAccount } from "../../shared/wallet.model" import { accountsEqual } from "../../shared/wallet.service" import { walletStore } from "../../shared/wallet/walletStore" -import { transactionsStore } from "./store" +import { transactionsStore } from "../../shared/transactions/store" // selects transactions that are pending and match the provided account diff --git a/packages/extension/src/background/transactions/sources/onchain.ts b/packages/extension/src/background/transactions/sources/onchain.ts index 3cfef253e..44f5b10b1 100644 --- a/packages/extension/src/background/transactions/sources/onchain.ts +++ b/packages/extension/src/background/transactions/sources/onchain.ts @@ -1,13 +1,9 @@ import { ExplorerProvider } from "@alephium/web3" import { getNetwork } from "../../../shared/network" -import { - Transaction, - getInFlightTransactions, -} from "../../../shared/transactions" +import { Transaction } from "../../../shared/transactions" import { getTransactionsStatusUpdate } from "../determineUpdates" -export async function getTransactionsUpdate(transactions: Transaction[]) { - const transactionsToCheck = getInFlightTransactions(transactions) +export async function getTransactionsUpdate(transactionsToCheck: Transaction[]) { // as this function tends to run into 429 errors, we'll simply keep the old status when it fails // TODO: we should add a cooldown when user run into 429 errors @@ -43,5 +39,5 @@ export async function getTransactionsUpdate(transactions: Transaction[]) { [], ) - return getTransactionsStatusUpdate(transactions, updatedTransactions) + return getTransactionsStatusUpdate(transactionsToCheck, updatedTransactions) } diff --git a/packages/extension/src/background/transactions/sources/voyager.ts b/packages/extension/src/background/transactions/sources/voyager.ts index 58566b28b..8c7a1135b 100644 --- a/packages/extension/src/background/transactions/sources/voyager.ts +++ b/packages/extension/src/background/transactions/sources/voyager.ts @@ -1,12 +1,9 @@ -import { ExplorerProvider } from "@alephium/web3" import join from "url-join" -import { Network, getNetwork } from "../../../shared/network" -import { Transaction, compareTransactions } from "../../../shared/transactions" +import { Network } from "../../../shared/network" +import { Transaction, getTransactionsPerAccount } from "../../../shared/transactions" import { WalletAccount } from "../../../shared/wallet.model" import { fetchWithTimeout } from "../../utils/fetchWithTimeout" -import { mapAlephiumTransactionToTransaction } from "../transformers" -import { Transaction as AlephiumTransaction } from '@alephium/web3/dist/src/api/api-explorer' export interface VoyagerTransaction { blockId: string @@ -39,27 +36,6 @@ export async function getTransactionHistory( accountsToPopulate: WalletAccount[], metadataTransactions: Transaction[], ) { - const transactionsPerAccount = await Promise.all( - accountsToPopulate.map(async (account) => { - const network = await getNetwork(account.networkId) - const explorerProvider = new ExplorerProvider(network.explorerApiUrl) - - // confirmed txs - const transactions: AlephiumTransaction[] = await explorerProvider.addresses.getAddressesAddressTransactions(account.address) - - return transactions.map((transaction) => - mapAlephiumTransactionToTransaction( - transaction, - account, - metadataTransactions.find((tx) => - compareTransactions(tx, { - hash: transaction.hash, - account: { networkId: account.networkId }, - }), - )?.meta, - ), - ) - }), - ) - return transactionsPerAccount.flat() + const transactionsPerAccount = await getTransactionsPerAccount(accountsToPopulate, metadataTransactions) + return Array.from(transactionsPerAccount.values()).flat() } diff --git a/packages/extension/src/background/transactions/store.ts b/packages/extension/src/background/transactions/store.ts index 3b11337c8..92bcd0278 100644 --- a/packages/extension/src/background/transactions/store.ts +++ b/packages/extension/src/background/transactions/store.ts @@ -1,53 +1,10 @@ -import { differenceWith } from "lodash-es" - -import { ArrayStorage } from "../../shared/storage" -import { StorageChange } from "../../shared/storage/types" -import { - Transaction, - TransactionRequest, - compareTransactions, -} from "../../shared/transactions" +import { transactionsStore } from "../../shared/transactions/store" import { runAddedHandlers, runChangedStatusHandlers } from "./onupdate" -export const transactionsStore = new ArrayStorage([], { - namespace: "core:transactions", - areaName: "local", - compare: compareTransactions, -}) - -export const addTransaction = async (transaction: TransactionRequest) => { - // sanity checks - if (!transaction.hash) { - return // dont throw - } - - const newTransaction = { - status: "RECEIVED" as const, - timestamp: Date.now(), - ...transaction, - } - - return transactionsStore.push(newTransaction) -} - -export const getUpdatedTransactionsForChangeSet = ( - changeSet: StorageChange, -) => { - const updatedTransactions = differenceWith( - changeSet.oldValue ?? [], - changeSet.newValue ?? [], - equalTransactionWithStatus, - ) - return updatedTransactions -} - -const equalTransactionWithStatus = ( - a: Transaction, - b: Transaction, -): boolean => { - return compareTransactions(a, b) && a.status === b.status -} +// basically what i will do: +// if we remove txs, how does this function affect the tx history since it subscribe +// to the change? transactionsStore.subscribe((_, changeSet) => { const findOldTransaction = (hash: string) => changeSet.oldValue?.find( (oldTransaction) => oldTransaction.hash === hash, diff --git a/packages/extension/src/background/transactions/tracking.ts b/packages/extension/src/background/transactions/tracking.ts index 5adf2f53f..a4659190b 100644 --- a/packages/extension/src/background/transactions/tracking.ts +++ b/packages/extension/src/background/transactions/tracking.ts @@ -5,7 +5,7 @@ import { WalletAccount } from "../../shared/wallet.model" import { accountsEqual } from "../../shared/wallet.service" import { getTransactionsUpdate } from "./sources/onchain" import { getTransactionHistory } from "./sources/voyager" -import { transactionsStore } from "./store" +import { transactionsStore } from "../../shared/transactions/store" import { partition } from "lodash" export interface TransactionTracker { @@ -21,15 +21,14 @@ export const transactionTracker: TransactionTracker = { uniqAccounts, allTransactions, ) - return transactionsStore.push(historyTransactions) + + // We set the tx history directly here, which potentially will remove historical transactions + return transactionsStore.set(historyTransactions) }, async update() { const allTransactions = await transactionsStore.get() const pendingTransactions = getInFlightTransactions(allTransactions) - const updatedTransactions = await getTransactionsUpdate( - // is smart enough to filter for just the pending transactions, as the rest needs no update - allTransactions, - ) + const updatedTransactions = await getTransactionsUpdate(pendingTransactions) const [toBeRemoved, toBeKept] = partition(updatedTransactions, (tx) => tx.status === "REMOVED_FROM_MEMPOOL") if (toBeRemoved.length > 0) { await transactionsStore.remove((tx) => toBeRemoved.some((toBeRemovedTx) => tx.hash === toBeRemovedTx.hash)) diff --git a/packages/extension/src/background/transactions/transactionExecution.ts b/packages/extension/src/background/transactions/transactionExecution.ts index e02dff289..57130ad44 100644 --- a/packages/extension/src/background/transactions/transactionExecution.ts +++ b/packages/extension/src/background/transactions/transactionExecution.ts @@ -2,7 +2,7 @@ import { ReviewTransactionResult, } from "../../shared/actionQueue/types" import { BackgroundService } from "../background" -import { addTransaction } from "./store" +import { addTransaction } from "../../shared/transactions/store" export const executeTransactionAction = async ( transaction: ReviewTransactionResult, diff --git a/packages/extension/src/shared/storage/array.ts b/packages/extension/src/shared/storage/array.ts index 517b6e08e..33ee3f25c 100644 --- a/packages/extension/src/shared/storage/array.ts +++ b/packages/extension/src/shared/storage/array.ts @@ -24,7 +24,8 @@ export function mergeArrayStableWith( compareFn: (a: T, b: T) => boolean = isEqual, insertMode: "unshift" | "push" = "push", ): T[] { - const result = reverse(uniqWith(reverse(source), compareFn)) // 2x reverse to keep the order while keeping the last occurrence of duplicates + // 2x reverse to keep the order while keeping the last occurrence of duplicates + const result: T[] = reverse(uniqWith(reverse(source), compareFn)) for (const element of other) { const index = result.findIndex((e) => compareFn(e, element)) if (index === -1) { @@ -42,6 +43,7 @@ interface ArrayStorageOptions extends ObjectStorageOptions { export interface IArrayStorage extends BaseStorage { get(selector?: SelectorFn): Promise + set(value: AllowArray): Promise push(value: AllowArray | SetterFn): Promise unshift(value: AllowArray | SetterFn): Promise remove(value: AllowArray | SelectorFn): Promise @@ -88,8 +90,13 @@ export class ArrayStorage implements IArrayStorage { return isFunction(setterOrArray) ? setterOrArray(setterArg) : Array.isArray(setterOrArray) - ? setterOrArray - : [setterOrArray] + ? setterOrArray + : [setterOrArray] + } + + public async set(value: AllowArray): Promise { + const setValue = Array.isArray(value) ? value : [value] + await this.storageImplementation.set(setValue) } public async push(value: AllowArray | SetterFn): Promise { @@ -117,8 +124,8 @@ export class ArrayStorage implements IArrayStorage { const valuesToRemove = isFunction(value) ? await this.get(value) : Array.isArray(value) - ? value - : [value] + ? value + : [value] const newAll = differenceWith(all, valuesToRemove, this.compare) await this.storageImplementation.set(newAll) return valuesToRemove diff --git a/packages/extension/src/shared/transactions.ts b/packages/extension/src/shared/transactions.ts deleted file mode 100644 index afc824cf4..000000000 --- a/packages/extension/src/shared/transactions.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { lowerCase, upperFirst } from "lodash-es" -import { Call } from "starknet" -import { ReviewTransactionResult } from "./actionQueue/types" - -import { WalletAccount } from "./wallet.model" -import { AlephiumExplorerTransaction } from "./explorer/type" - -export type Status = 'NOT_RECEIVED' | 'RECEIVED' | 'PENDING' | 'ACCEPTED_ON_MEMPOOL' | 'ACCEPTED_ON_L2' | 'ACCEPTED_ON_CHAIN' | 'REJECTED' | 'REMOVED_FROM_MEMPOOL'; - -// Global Constants for Transactions -export const SUCCESS_STATUSES: Status[] = [ - "ACCEPTED_ON_MEMPOOL", - "ACCEPTED_ON_CHAIN", - "ACCEPTED_ON_L2", - "PENDING", -] - -export const TRANSACTION_STATUSES_TO_TRACK: Status[] = [ - "RECEIVED", - "ACCEPTED_ON_MEMPOOL", - "NOT_RECEIVED", -] - -export interface TransactionMeta { - title?: string - subTitle?: string - transactions?: Call | Call[] // TODO: remove this - type?: string // TODO: in future can be DECLARE | DEPLOY | CALL - request?: ReviewTransactionResult - explorer?: AlephiumExplorerTransaction -} - -export interface TransactionBase { - hash: string - account: { - networkId: string - } -} - -export interface TransactionRequest extends TransactionBase { - account: WalletAccount - meta?: TransactionMeta -} - -export interface Transaction extends TransactionRequest { - status: Status - failureReason?: { code: string; error_message: string } - timestamp: number -} - -export const compareTransactions = ( - a: TransactionBase, - b: TransactionBase, -): boolean => a.hash === b.hash && a.account.networkId === a.account.networkId - -export function entryPointToHumanReadable(entryPoint: string): string { - try { - return upperFirst(lowerCase(entryPoint)) - } catch { - return entryPoint - } -} - -export const getInFlightTransactions = ( - transactions: Transaction[], -): Transaction[] => - transactions.filter( - ({ status, meta }) => - TRANSACTION_STATUSES_TO_TRACK.includes(status) - ) - -export function nameTransaction(calls: Call | Call[]) { - const callsArray = Array.isArray(calls) ? calls : [calls] - const entrypointNames = callsArray.map((call) => call.entrypoint) - return transactionNamesToTitle(entrypointNames) -} - -export function transactionNamesToTitle( - names: string | string[], -): string | undefined { - if (!Array.isArray(names)) { - names = [names] - } - const entrypointNames = names.map((name) => lowerCase(name)) - const lastName = entrypointNames.pop() - const title = entrypointNames.length - ? `${entrypointNames.join(", ")} and ${lastName}` - : lastName - return upperFirst(title) -} - -// ===== ALPH ====== - diff --git a/packages/extension/src/shared/transactions/index.ts b/packages/extension/src/shared/transactions/index.ts new file mode 100644 index 000000000..318594975 --- /dev/null +++ b/packages/extension/src/shared/transactions/index.ts @@ -0,0 +1,161 @@ +import { ExplorerProvider } from "@alephium/web3" +//import { getAccounts } from "../../shared/account/store" +import { lowerCase, upperFirst } from "lodash-es" +import { Call } from "starknet" +import { ReviewTransactionResult } from "../actionQueue/types" +import { WalletAccount } from "../wallet.model" +import { AlephiumExplorerTransaction } from "../explorer/type" +import { mapAlephiumTransactionToTransaction } from "./transformers" +import { Transaction as AlephiumTransaction } from '@alephium/web3/dist/src/api/api-explorer' +import { getNetwork } from "../network" + +export type Status = 'NOT_RECEIVED' | 'RECEIVED' | 'PENDING' | 'ACCEPTED_ON_MEMPOOL' | 'ACCEPTED_ON_L2' | 'ACCEPTED_ON_CHAIN' | 'REJECTED' | 'REMOVED_FROM_MEMPOOL'; + +// Global Constants for Transactions +export const SUCCESS_STATUSES: Status[] = [ + "ACCEPTED_ON_MEMPOOL", + "ACCEPTED_ON_CHAIN", + "ACCEPTED_ON_L2", + "PENDING", +] + +export const TRANSACTION_STATUSES_TO_TRACK: Status[] = [ + "RECEIVED", + "ACCEPTED_ON_MEMPOOL", + "NOT_RECEIVED", +] + +export interface TransactionMeta { + title?: string + subTitle?: string + transactions?: Call | Call[] // TODO: remove this + type?: string // TODO: in future can be DECLARE | DEPLOY | CALL + request?: ReviewTransactionResult + explorer?: AlephiumExplorerTransaction +} + +export interface TransactionBase { + hash: string + account: { + networkId: string + } +} + +export interface TransactionRequest extends TransactionBase { + account: WalletAccount + meta?: TransactionMeta +} + +export interface Transaction extends TransactionRequest { + status: Status + failureReason?: { code: string; error_message: string } + timestamp: number +} + +export const compareTransactions = ( + a: TransactionBase, + b: TransactionBase, +): boolean => a.hash === b.hash && a.account.networkId === a.account.networkId + +export function entryPointToHumanReadable(entryPoint: string): string { + try { + return upperFirst(lowerCase(entryPoint)) + } catch { + return entryPoint + } +} + +export const getInFlightTransactions = ( + transactions: Transaction[], +): Transaction[] => + transactions.filter( + ({ status }) => + TRANSACTION_STATUSES_TO_TRACK.includes(status) + ) + +export function nameTransaction(calls: Call | Call[]) { + const callsArray = Array.isArray(calls) ? calls : [calls] + const entrypointNames = callsArray.map((call) => call.entrypoint) + return transactionNamesToTitle(entrypointNames) +} + +export function transactionNamesToTitle( + names: string | string[], +): string | undefined { + if (!Array.isArray(names)) { + names = [names] + } + const entrypointNames = names.map((name) => lowerCase(name)) + const lastName = entrypointNames.pop() + const title = entrypointNames.length + ? `${entrypointNames.join(", ")} and ${lastName}` + : lastName + return upperFirst(title) +} + +// ===== ALPH ====== +export async function getTransactionsPerAccount( + accountsToPopulate: WalletAccount[], + metadataTransactions: Transaction[], +): Promise> { + const getTransactions = buildGetTransactionsFn(metadataTransactions) + const transactionsPerAccount = new Map() + await Promise.all( + accountsToPopulate.map(async (account) => { + const transactions = await getTransactions(account) + transactionsPerAccount.set(account, transactions) + }), + ) + + return transactionsPerAccount +} + +// The number of transactions fetched has the following constraints: +// 1) At most 50 transactions (3.1kb per tx, 50 txs = 155kb per account), +// max `chrome.storage.local` quota per extension is 5mb +// 2) At least total number of transactions from the last two days +function buildGetTransactionsFn(metadataTransactions: Transaction[]) { + return async (account: WalletAccount) => { + const currentDate = new Date() + const network = await getNetwork(account.networkId) + const explorerProvider = new ExplorerProvider(network.explorerApiUrl) + const limit = 50 + + let page = 1 + let continueFetching = true + const result: Transaction[] = [] + while (continueFetching) { + const transactions: AlephiumTransaction[] = await explorerProvider.addresses.getAddressesAddressTransactions(account.address, { page, limit }) + const convertedTxs = transactions.map((transaction) => + mapAlephiumTransactionToTransaction( + transaction, + account, + metadataTransactions.find((tx) => + compareTransactions(tx, { + hash: transaction.hash, + account: { networkId: account.networkId }, + }), + )?.meta, + ), + ) + result.push(...convertedTxs) + + if (transactions.length < limit) { + continueFetching = false + } else { + const lastTxDate = new Date(transactions[transactions.length - 1].timestamp) + const txCreatedSinceInMinutes = (currentDate.valueOf() - lastTxDate.valueOf()) / 1000 / 60 + const txCreatedLessThan2Days = txCreatedSinceInMinutes < 60 * 24 * 2 + + if (!txCreatedLessThan2Days) { + continueFetching = false + } + } + + page += 1 + } + + console.log(`fetched tx for account ${account.address} in network ${account.networkId}`, result) + return result + } +} diff --git a/packages/extension/src/shared/transactions/store.ts b/packages/extension/src/shared/transactions/store.ts new file mode 100644 index 000000000..f671e0041 --- /dev/null +++ b/packages/extension/src/shared/transactions/store.ts @@ -0,0 +1,27 @@ +import { ArrayStorage } from "../../shared/storage" +import { + Transaction, + TransactionRequest, + compareTransactions, +} from "../../shared/transactions" + +export const transactionsStore = new ArrayStorage([], { + namespace: "core:transactions", + areaName: "local", + compare: compareTransactions, +}) + +export const addTransaction = async (transaction: TransactionRequest) => { + // sanity checks + if (!transaction.hash) { + return // dont throw + } + + const newTransaction = { + status: "RECEIVED" as const, + timestamp: Date.now(), + ...transaction, + } + + return transactionsStore.push(newTransaction) +} diff --git a/packages/extension/src/background/transactions/transformers.ts b/packages/extension/src/shared/transactions/transformers.ts similarity index 100% rename from packages/extension/src/background/transactions/transformers.ts rename to packages/extension/src/shared/transactions/transformers.ts diff --git a/packages/extension/src/ui/features/accountTokens/useTransactionStatus.ts b/packages/extension/src/ui/features/accountTokens/useTransactionStatus.ts index 5ab6cb3fb..aabd425ab 100644 --- a/packages/extension/src/ui/features/accountTokens/useTransactionStatus.ts +++ b/packages/extension/src/ui/features/accountTokens/useTransactionStatus.ts @@ -1,7 +1,7 @@ import { memoize } from "lodash-es" import { useMemo } from "react" -import { transactionsStore } from "../../../background/transactions/store" +import { transactionsStore } from "../../../shared/transactions/store" import { useArrayStorage } from "../../../shared/storage/hooks" import { Transaction, Status as DetailedStatus } from "../../../shared/transactions" diff --git a/packages/extension/src/ui/features/accounts/accountTransactions.state.ts b/packages/extension/src/ui/features/accounts/accountTransactions.state.ts index 0cda74881..484eae724 100644 --- a/packages/extension/src/ui/features/accounts/accountTransactions.state.ts +++ b/packages/extension/src/ui/features/accounts/accountTransactions.state.ts @@ -2,7 +2,7 @@ import { ExplorerProvider } from "@alephium/web3" import { memoize } from "lodash-es" import { useEffect, useMemo, useState } from "react" import { Transaction as AlephiumTransaction } from '@alephium/web3/dist/src/api/api-explorer' -import { transactionsStore } from "../../../background/transactions/store" +import { transactionsStore } from "../../../shared/transactions/store" import { getNetwork } from "../../../shared/network" import { useArrayStorage } from "../../../shared/storage/hooks" import { Transaction } from "../../../shared/transactions" diff --git a/yarn.lock b/yarn.lock index 33dda005c..8b9619c5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12624,6 +12624,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + function.prototype.name@^1.1.0, function.prototype.name@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" @@ -13260,6 +13265,13 @@ hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + hast-to-hyperscript@^9.0.0: version "9.0.1" resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz#9b67fd188e4c81e8ad66f803855334173920218d" @@ -14058,6 +14070,14 @@ is-absolute-url@^3.0.0: resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== +is-accessor-descriptor@^0.1.6: + version "0.1.7" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.7.tgz#118964354c1d55b616764042b70b782e777139c3" + integrity sha512-Qw68noCVXHAbixVCPiM1lrVG+hT9DfrdA/bOKe0rs/KoJYOF7CzFnymagWRbi3b6ngxO9cH5NMke94v9rAkbfQ== + dependencies: + gopd "^1.0.1" + hasown "^2.0.0" + is-accessor-descriptor@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" @@ -14165,6 +14185,14 @@ is-core-module@^2.1.0, is-core-module@^2.11.0, is-core-module@^2.5.0, is-core-mo dependencies: has "^1.0.3" +is-data-descriptor@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.5.tgz#0907e763b9989b5b01c10a5aa5d09469bbae392e" + integrity sha512-a7ZgE8u0mkmoWpRMbmuwFQRFa2IFNcbG4eR+iqMYghjMTdXYitWgttZCxGit7zCc98gvO1TvOF0sSEZuZEvbPQ== + dependencies: + gopd "^1.0.1" + hasown "^2.0.0" + is-data-descriptor@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" @@ -15885,6 +15913,11 @@ kind-of@^4.0.0: dependencies: is-buffer "^1.1.5" +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" From 814ec788e536182b15e0d6f04dd5643ef21fe586 Mon Sep 17 00:00:00 2001 From: h0ngcha0 Date: Fri, 19 Jan 2024 16:19:14 +0100 Subject: [PATCH 02/17] Stability fixes --- packages/dapp/src/services/wallet.service.ts | 2 +- .../src/background/accountMessaging.ts | 15 +- .../extension/src/background/networkStatus.ts | 21 ++- .../extension/src/background/notification.ts | 9 +- .../src/background/transactions/store.ts | 4 - .../src/background/transactions/tracking.ts | 2 +- packages/extension/src/background/wallet.ts | 155 ++++++++++++++++-- .../src/shared/messages/AccountMessage.ts | 5 + .../extension/src/shared/storage/array.ts | 8 +- .../extension/src/shared/storage/hooks.ts | 27 +-- .../src/shared/transactions/index.ts | 2 +- .../features/accountTokens/AccountTokens.tsx | 16 +- .../accountTokens/AccountTokensButtons.tsx | 19 +-- .../accountTokens/AccountTokensHeader.tsx | 17 +- .../accountTokens/SendTokenScreen.tsx | 5 +- .../ui/features/accountTokens/TokenList.tsx | 19 +-- .../ui/features/accountTokens/tokens.state.ts | 24 ++- .../ui/features/accounts/AccountContainer.tsx | 9 +- .../features/accounts/AccountListScreen.tsx | 33 ++-- .../accounts/AccountNavigationBar.tsx | 5 +- .../ui/features/accounts/AccountScreen.tsx | 9 +- .../features/accounts/AccountScreenEmpty.tsx | 26 ++- .../src/ui/features/accounts/AddAccount.tsx | 6 +- .../ui/features/accounts/accounts.state.ts | 5 +- .../ui/features/recovery/recovery.service.ts | 4 +- .../src/ui/features/send/SendScreen.tsx | 4 +- .../src/ui/services/backgroundAccounts.ts | 5 + 27 files changed, 299 insertions(+), 157 deletions(-) diff --git a/packages/dapp/src/services/wallet.service.ts b/packages/dapp/src/services/wallet.service.ts index e295b802d..4955f6abe 100644 --- a/packages/dapp/src/services/wallet.service.ts +++ b/packages/dapp/src/services/wallet.service.ts @@ -64,7 +64,7 @@ export const signMessage = async (message: string, messageHasher: MessageHasher) return await alephium.signMessage({ signerAddress: alephium.connectedAccount.address, - message: message, + message, messageHasher }) } diff --git a/packages/extension/src/background/accountMessaging.ts b/packages/extension/src/background/accountMessaging.ts index b98945be8..d99b2cbf7 100644 --- a/packages/extension/src/background/accountMessaging.ts +++ b/packages/extension/src/background/accountMessaging.ts @@ -7,7 +7,7 @@ import { encryptForUi } from "./crypto" export const handleAccountMessage: HandleMessage = async ({ msg, - background: { wallet, actionQueue }, + background: { wallet }, messagingKeys: { privateKey }, respond, }) => { @@ -29,6 +29,19 @@ export const handleAccountMessage: HandleMessage = async ({ }) } + case "DISCOVER_ACCOUNTS": { + const { networkId } = msg.data + if (networkId) { + await wallet.deriveActiveAccountsForNetworkIfNonExistence(networkId) + } else { + await wallet.deriveActiveAccountsIfNonExistence() + } + + return respond({ + type: "DISCOVER_ACCOUNTS_RES", + }) + } + case "NEW_ACCOUNT": { if (!(await wallet.isSessionOpen())) { throw Error("you need an open session") diff --git a/packages/extension/src/background/networkStatus.ts b/packages/extension/src/background/networkStatus.ts index 8c92c8cb5..dece1e856 100644 --- a/packages/extension/src/background/networkStatus.ts +++ b/packages/extension/src/background/networkStatus.ts @@ -1,10 +1,7 @@ import { ExplorerProvider, NodeProvider } from "@alephium/web3" -import urljoin from "url-join" - import { Network, NetworkStatus } from "../shared/network" import { KeyValueStorage } from "../shared/storage" import { createStaleWhileRevalidateCache } from "./swr" -import { fetchWithTimeout } from "./utils/fetchWithTimeout" type SwrCacheKey = string @@ -45,13 +42,15 @@ export const isNetworkHealthy = async (network: Network): Promise => { const nodeProvider = new NodeProvider(network.nodeUrl) const explorerProvider = new ExplorerProvider(network.explorerApiUrl) - try { - const nodeReleaseVersion = (await nodeProvider.infos.getInfosVersion()).version - const explorerReleaseVersion = (await explorerProvider.infos.getInfos()).releaseVersion + return swr(`${network.id}-network-status`, async () => { + try { + const nodeReleaseVersion = (await nodeProvider.infos.getInfosVersion()).version + const explorerReleaseVersion = (await explorerProvider.infos.getInfos()).releaseVersion - return !!nodeReleaseVersion && !!explorerReleaseVersion - } catch (exception) { - console.debug('Exception when checking network healthy', exception) - return false - } + return !!nodeReleaseVersion && !!explorerReleaseVersion + } catch (exception) { + console.debug('Exception when checking network healthy', exception) + return false + } + }) } diff --git a/packages/extension/src/background/notification.ts b/packages/extension/src/background/notification.ts index 18989ead3..045a915ad 100644 --- a/packages/extension/src/background/notification.ts +++ b/packages/extension/src/background/notification.ts @@ -41,10 +41,11 @@ export async function sentTransactionNotification( meta?: TransactionMeta, ) { const id = `TX:${hash}:${networkId}` - const title = `${meta?.title || "Transaction"} ${["ACCEPTED_ON_CHAIN", "ACCEPTED_ON_MEMPOOL", "PENDING"].includes(status) - ? "succeeded" - : "rejected" - }` + const title = `${meta?.title || "Transaction"} ${ + ["ACCEPTED_ON_CHAIN", "ACCEPTED_ON_MEMPOOL", "PENDING"].includes(status) + ? "succeeded" + : "rejected" + }` return browser.notifications.create(id, { type: "basic", title, diff --git a/packages/extension/src/background/transactions/store.ts b/packages/extension/src/background/transactions/store.ts index 92bcd0278..06785fdea 100644 --- a/packages/extension/src/background/transactions/store.ts +++ b/packages/extension/src/background/transactions/store.ts @@ -1,10 +1,6 @@ import { transactionsStore } from "../../shared/transactions/store" import { runAddedHandlers, runChangedStatusHandlers } from "./onupdate" -// basically what i will do: - -// if we remove txs, how does this function affect the tx history since it subscribe -// to the change? transactionsStore.subscribe((_, changeSet) => { const findOldTransaction = (hash: string) => changeSet.oldValue?.find( (oldTransaction) => oldTransaction.hash === hash, diff --git a/packages/extension/src/background/transactions/tracking.ts b/packages/extension/src/background/transactions/tracking.ts index a4659190b..42807e83c 100644 --- a/packages/extension/src/background/transactions/tracking.ts +++ b/packages/extension/src/background/transactions/tracking.ts @@ -22,7 +22,7 @@ export const transactionTracker: TransactionTracker = { allTransactions, ) - // We set the tx history directly here, which potentially will remove historical transactions + // Set the tx history directly here, which potentially trims historical transactions return transactionsStore.set(historyTransactions) }, async update() { diff --git a/packages/extension/src/background/wallet.ts b/packages/extension/src/background/wallet.ts index 01851bb77..0cecad09c 100644 --- a/packages/extension/src/background/wallet.ts +++ b/packages/extension/src/background/wallet.ts @@ -11,6 +11,8 @@ import { groupOfAddress, KeyType, Account, + ExplorerProvider, + TOTAL_NUMBER_OF_GROUPS } from "@alephium/web3" import { PrivateKeyWallet, @@ -24,6 +26,7 @@ import { withHiddenSelector } from "../shared/account/selectors" import { Network, defaultNetwork, + getNetworks, } from "../shared/network" import { IArrayStorage, @@ -86,6 +89,7 @@ export class Wallet { private readonly sessionStore: IObjectStorage, private readonly getNetwork: GetNetwork, ) { } + async signAndSubmitUnsignedTx( account: WalletAccount, params: SignUnsignedTxParams, @@ -97,7 +101,7 @@ export class Wallet { async submitSignedTx(account: WalletAccount, unsignedTx: string, signature: string): Promise { const network = await this.getNetwork(account.networkId) const nodeProvider = new NodeProvider(network.nodeUrl) - await nodeProvider.transactions.postTransactionsSubmit({ unsignedTx, signature}) + await nodeProvider.transactions.postTransactionsSubmit({ unsignedTx, signature }) return } @@ -229,23 +233,7 @@ export class Wallet { } public async newAccountShared(secret: string, startIndex: number, networkId: string, keyType: KeyType, forGroup?: number): Promise { - const [privateKey, index] = forGroup === undefined ? [deriveHDWalletPrivateKey(secret, keyType, startIndex), startIndex] - : deriveHDWalletPrivateKeyForGroup(secret, forGroup, keyType, startIndex) - const publicKey = publicKeyFromPrivateKey(privateKey, keyType) - const newAddress = addressFromPublicKey(publicKey, keyType) - - const account: WalletAccount = { - address: newAddress, - networkId: networkId, - signer: { - type: "local_secret" as const, - publicKey: publicKey, - keyType: keyType, - derivationIndex: index, - group: groupOfAddress(newAddress) - }, - type: "alephium", - } + const account: WalletAccount = this.deriveAccount(secret, startIndex, networkId, keyType, forGroup) await this.walletStore.push([account]) @@ -449,4 +437,135 @@ export class Wallet { await this.walletStore.push(accounts) } } + + public deriveAccount(secret: string, startIndex: number, networkId: string, keyType: KeyType, forGroup?: number): WalletAccount { + const [privateKey, index] = forGroup === undefined ? [deriveHDWalletPrivateKey(secret, keyType, startIndex), startIndex] + : deriveHDWalletPrivateKeyForGroup(secret, forGroup, keyType, startIndex) + const publicKey = publicKeyFromPrivateKey(privateKey, keyType) + const newAddress = addressFromPublicKey(publicKey, keyType) + + return { + address: newAddress, + networkId: networkId, + signer: { + type: "local_secret" as const, + publicKey: publicKey, + keyType: keyType, + derivationIndex: index, + group: groupOfAddress(newAddress) + }, + type: "alephium", + } + } + + public async deriveActiveAccountsIfNonExistence(): Promise { + const accounts = await this.walletStore.get() + + if (accounts.length === 0) { + console.info("no accounts exist, deriving active accounts") + const walletAccounts = await this.deriveActiveAccounts() + if (walletAccounts.length > 0) { + await this.walletStore.push(walletAccounts) + await this.selectAccount(walletAccounts[0]) + } + } else { + console.info("accounts exist, do not deriving active accounts") + } + } + + public async deriveActiveAccountsForNetworkIfNonExistence(networkId: string): Promise { + const accounts = await this.walletStore.get() + + if (accounts.filter(account => account.networkId == networkId).length === 0) { + console.info(`no accounts exist for ${networkId}, deriving active accounts`) + const walletAccounts = await this.deriveActiveAccountsForNetwork(networkId) + if (walletAccounts.length > 0) { + await this.walletStore.push(walletAccounts) + await this.selectAccount(walletAccounts[0]) + } + } else { + console.info(`accounts exist for ${networkId}, do not deriving active accounts`) + } + } + + public async deriveActiveAccounts(): Promise { + const networks = await getNetworks() + const walletAccounts: WalletAccount[] = [] + for (const network of networks) { + const walletAccountsForNetwork = await this.deriveActiveAccountsForNetwork(network.id) + walletAccounts.push(...walletAccountsForNetwork) + } + + return walletAccounts + } + + public async deriveActiveAccountsForNetwork(networkId: string): Promise { + console.log(`derived active accounts for ${networkId}`) + const session = await this.sessionStore.get() + if (!(await this.isSessionOpen()) || !session) { + throw Error("no open session") + } + + const network = await this.getNetwork(networkId) + + const walletAccounts: WalletAccount[] = [] + + for (let group = 0; group < TOTAL_NUMBER_OF_GROUPS; group++) { + const walletAccountsForGroup = await this.deriveActiveAccountsForGroup(session.secret, network, 'default', group, [], []) + walletAccounts.push(...walletAccountsForGroup) + } + + return walletAccounts + } + + public async deriveActiveAccountsForGroup( + secret: string, + network: Network, + keyType: KeyType, + forGroup: number, + allWalletAccounts: { wallet: WalletAccount, active: boolean }[], + activeWalletAccounts: WalletAccount[] + ): Promise { + const minGap = 5 + const derivationBatchSize = 10 + if (!network.explorerUrl) { + return [] + } + + const explorerService = new ExplorerProvider(network.explorerApiUrl) + const gapSatisfied = (allWalletAccounts.length >= minGap) && allWalletAccounts.slice(-minGap).every(item => item.active === false); + + if (gapSatisfied) { + return activeWalletAccounts + } else { + let startIndex = getNextPathIndex(allWalletAccounts.map(account => account.wallet.signer.derivationIndex)) + const newWalletAccounts = [] + for (let i = 0; i < derivationBatchSize; i++) { + const newWalletAccount = this.deriveAccount(secret, startIndex, network.id, keyType, forGroup) + newWalletAccounts.push(newWalletAccount) + startIndex = newWalletAccount.signer.derivationIndex + 1 + } + + const results = await explorerService.addresses.postAddressesUsed(newWalletAccounts.map(account => account.address)) + + const updatedActiveWalletAccounts = activeWalletAccounts + for (let i = 0; i < derivationBatchSize; i++) { + const newWalletAccount = newWalletAccounts[i] + const result = results[i] + if (result) { + updatedActiveWalletAccounts.push(newWalletAccount) + } + allWalletAccounts.push({ wallet: newWalletAccount, active: result }) + } + + return this.deriveActiveAccountsForGroup( + secret, + network, + keyType, + forGroup, + allWalletAccounts, + updatedActiveWalletAccounts + ) + } + } } diff --git a/packages/extension/src/shared/messages/AccountMessage.ts b/packages/extension/src/shared/messages/AccountMessage.ts index cd771c441..f1421bc79 100644 --- a/packages/extension/src/shared/messages/AccountMessage.ts +++ b/packages/extension/src/shared/messages/AccountMessage.ts @@ -58,3 +58,8 @@ export type AccountMessage = type: "GET_ENCRYPTED_SEED_PHRASE_RES" data: { encryptedSeedPhrase: string } } + | { + type: "DISCOVER_ACCOUNTS" + data: { networkId?: string } + } + | { type: "DISCOVER_ACCOUNTS_RES" } diff --git a/packages/extension/src/shared/storage/array.ts b/packages/extension/src/shared/storage/array.ts index 33ee3f25c..7fce33317 100644 --- a/packages/extension/src/shared/storage/array.ts +++ b/packages/extension/src/shared/storage/array.ts @@ -90,8 +90,8 @@ export class ArrayStorage implements IArrayStorage { return isFunction(setterOrArray) ? setterOrArray(setterArg) : Array.isArray(setterOrArray) - ? setterOrArray - : [setterOrArray] + ? setterOrArray + : [setterOrArray] } public async set(value: AllowArray): Promise { @@ -124,8 +124,8 @@ export class ArrayStorage implements IArrayStorage { const valuesToRemove = isFunction(value) ? await this.get(value) : Array.isArray(value) - ? value - : [value] + ? value + : [value] const newAll = differenceWith(all, valuesToRemove, this.compare) await this.storageImplementation.set(newAll) return valuesToRemove diff --git a/packages/extension/src/shared/storage/hooks.ts b/packages/extension/src/shared/storage/hooks.ts index 50c602440..af43b1f19 100644 --- a/packages/extension/src/shared/storage/hooks.ts +++ b/packages/extension/src/shared/storage/hooks.ts @@ -1,4 +1,4 @@ -import { memoize } from "lodash-es" +import { memoize, isEqual } from "lodash-es" import { useCallback, useEffect, useMemo, useState } from "react" import { swrCacheProvider } from "../../ui/services/swr" @@ -17,9 +17,13 @@ export function useKeyValueStorage< ) const set = useCallback( - (value: T[K]) => { - swrCacheProvider.set(storage.namespace + ":" + key.toString(), value) - setValue(value) + (v: T[K]) => { + const k = storage.namespace + ":" + key.toString() + + if (!isEqual(swrCacheProvider.get(k), v)) { + swrCacheProvider.set(storage.namespace + ":" + key.toString(), v) + setValue(v) + } }, [key, storage.namespace], ) @@ -40,8 +44,10 @@ export function useObjectStorage(storage: IObjectStorage): T { const set = useCallback( (value: T) => { - swrCacheProvider.set(storage.namespace, value) - setValue(value) + if (!isEqual(swrCacheProvider.get(storage.namespace), value)) { + swrCacheProvider.set(storage.namespace, value) + setValue(value) + } }, [storage.namespace], ) @@ -69,9 +75,11 @@ export function useArrayStorage( ) const set = useCallback( - (value: T[]) => { - swrCacheProvider.set(storage.namespace, value) - setValue(value) + (v: T[]) => { + if (!isEqual(swrCacheProvider.get(storage.namespace), v)) { + swrCacheProvider.set(storage.namespace, v) + setValue(v) + } }, [storage.namespace], ) @@ -83,6 +91,5 @@ export function useArrayStorage( }, [selector, storage, set]) const filteredValue = useMemo(() => value.filter(selector), [value, selector]) - return filteredValue } diff --git a/packages/extension/src/shared/transactions/index.ts b/packages/extension/src/shared/transactions/index.ts index 318594975..2119d3759 100644 --- a/packages/extension/src/shared/transactions/index.ts +++ b/packages/extension/src/shared/transactions/index.ts @@ -155,7 +155,7 @@ function buildGetTransactionsFn(metadataTransactions: Transaction[]) { page += 1 } - console.log(`fetched tx for account ${account.address} in network ${account.networkId}`, result) + console.debug(`fetched tx for account ${account.address} in network ${account.networkId}`, result) return result } } diff --git a/packages/extension/src/ui/features/accountTokens/AccountTokens.tsx b/packages/extension/src/ui/features/accountTokens/AccountTokens.tsx index cf4bc3643..cafc0a465 100644 --- a/packages/extension/src/ui/features/accountTokens/AccountTokens.tsx +++ b/packages/extension/src/ui/features/accountTokens/AccountTokens.tsx @@ -1,7 +1,6 @@ import { CellStack } from "@argent/ui" import { Flex, VStack } from "@chakra-ui/react" import { FC } from "react" - import { Account } from "../accounts/Account" import { getAccountName, @@ -13,38 +12,35 @@ import { StatusMessageBannerContainer } from "../statusMessage/StatusMessageBann import { AccountTokensButtons } from "./AccountTokensButtons" import { AccountTokensHeader } from "./AccountTokensHeader" import { TokenList } from "./TokenList" -import { useCurrencyDisplayEnabled } from "./tokenPriceHooks" -import { useAccountStatus } from "./useAccountStatus" +import { useFungibleTokensWithBalance } from "./tokens.state" interface AccountTokensProps { account: Account } export const AccountTokens: FC = ({ account }) => { - const status = useAccountStatus(account) const { accountNames } = useAccountMetadata() const { isBackupRequired } = useBackupRequired() - const currencyDisplayEnabled = useCurrencyDisplayEnabled() - + const { tokenDetails: tokensForAccount } = useFungibleTokensWithBalance(account) + //const tokensForAccount: TokenWithBalance[] = [] const accountName = getAccountName(account, accountNames) const showBackupBanner = isBackupRequired - const tokenListVariant = currencyDisplayEnabled ? "default" : "no-currency" return ( - + {showBackupBanner && } - + ) diff --git a/packages/extension/src/ui/features/accountTokens/AccountTokensButtons.tsx b/packages/extension/src/ui/features/accountTokens/AccountTokensButtons.tsx index 2d1aec12b..b52b13066 100644 --- a/packages/extension/src/ui/features/accountTokens/AccountTokensButtons.tsx +++ b/packages/extension/src/ui/features/accountTokens/AccountTokensButtons.tsx @@ -2,30 +2,29 @@ import { AlertDialog, Button, icons } from "@argent/ui" import { Flex, SimpleGrid } from "@chakra-ui/react" import { FC, useCallback, useMemo, useState } from "react" import { useNavigate } from "react-router-dom" +import { TokenWithBalance } from "../../../shared/token/type" import { useAppState } from "../../app.state" import { routes } from "../../routes" -import { Account } from "../accounts/Account" -import { useNetworkFeeToken, useFungibleTokensWithBalance } from "./tokens.state" +import { useNetworkFeeToken } from "./tokens.state" const { AddIcon, SendIcon } = icons interface AccountTokensButtonsProps { - account: Account + tokens: TokenWithBalance[] } export const AccountTokensButtons: FC = ({ - account, + tokens }) => { const navigate = useNavigate() const { switcherNetworkId } = useAppState() const sendToken = useNetworkFeeToken(switcherNetworkId) - const { tokenDetails, tokenDetailsIsInitialising } = useFungibleTokensWithBalance(account) const hasNonZeroBalance = useMemo(() => { - return tokenDetails.some(({ balance }) => balance?.gt(0)) - }, [tokenDetails]) + return tokens.some(({ balance }) => balance?.gt(0)) + }, [tokens]) const [alertDialogIsOpen, setAlertDialogIsOpen] = useState(false) @@ -34,10 +33,7 @@ export const AccountTokensButtons: FC = ({ }, []) const onSend = useCallback(() => { - /** tokenDetailsIsInitialising - balance is unknown, let the Send screen deal with it */ - if ( - (tokenDetailsIsInitialising || hasNonZeroBalance) - ) { + if (hasNonZeroBalance) { navigate(routes.sendScreen()) } else { setAlertDialogIsOpen(true) @@ -45,7 +41,6 @@ export const AccountTokensButtons: FC = ({ }, [ hasNonZeroBalance, navigate, - tokenDetailsIsInitialising, ]) const onAddFunds = useCallback(() => { diff --git a/packages/extension/src/ui/features/accountTokens/AccountTokensHeader.tsx b/packages/extension/src/ui/features/accountTokens/AccountTokensHeader.tsx index 69ac9ee77..3cb57b89f 100644 --- a/packages/extension/src/ui/features/accountTokens/AccountTokensHeader.tsx +++ b/packages/extension/src/ui/features/accountTokens/AccountTokensHeader.tsx @@ -1,27 +1,25 @@ -import { FieldError, H2 } from "@argent/ui" +import { H2 } from "@argent/ui" import { VStack } from "@chakra-ui/react" import { FC } from "react" import { prettifyCurrencyValue } from "../../../shared/token/price" +import { TokenWithBalance } from "../../../shared/token/type" import { BaseWalletAccount } from "../../../shared/wallet.model" import { AddressCopyButtonMain } from "../../components/AddressCopyButton" -import { AccountStatus } from "../accounts/accounts.service" import { useSumTokenBalancesToCurrencyValue } from "./tokenPriceHooks" -import { useFungibleTokensWithBalance } from "./tokens.state" interface AccountSubheaderProps { - status: AccountStatus account: BaseWalletAccount + tokens: TokenWithBalance[] accountName?: string } export const AccountTokensHeader: FC = ({ - status, account, + tokens, accountName }) => { - const { tokenDetails } = useFungibleTokensWithBalance(account) - const sumCurrencyValue = useSumTokenBalancesToCurrencyValue(tokenDetails) + const sumCurrencyValue = useSumTokenBalancesToCurrencyValue(tokens) const accountAddress = account.address return ( @@ -32,11 +30,6 @@ export const AccountTokensHeader: FC = ({

{accountName}

)} - {status.code === "ERROR" && ( - - {status.text} - - )} ) } diff --git a/packages/extension/src/ui/features/accountTokens/SendTokenScreen.tsx b/packages/extension/src/ui/features/accountTokens/SendTokenScreen.tsx index 679ad1be2..6a19d6769 100644 --- a/packages/extension/src/ui/features/accountTokens/SendTokenScreen.tsx +++ b/packages/extension/src/ui/features/accountTokens/SendTokenScreen.tsx @@ -34,9 +34,7 @@ import { isValidAddress, normalizeAddress, } from "../../services/addresses" -import { - sendTransferTransaction, sendUnsignedTxTransaction, -} from "../../services/transactions" +import { sendTransferTransaction } from "../../services/transactions" import { useOnClickOutside } from "../../services/useOnClickOutside" import { H3, H5 } from "../../theme/Typography" import { Account } from "../accounts/Account" @@ -322,6 +320,7 @@ export const SendTokenScreen: FC = () => { [addressBook.contacts, addressBook.userAccounts, inputRecipient], ) + // If ALPH only, can still sweep const sweepTransaction: Promise = useMemo( async () => { let result = undefined diff --git a/packages/extension/src/ui/features/accountTokens/TokenList.tsx b/packages/extension/src/ui/features/accountTokens/TokenList.tsx index dad218c31..c9c7fd02a 100644 --- a/packages/extension/src/ui/features/accountTokens/TokenList.tsx +++ b/packages/extension/src/ui/features/accountTokens/TokenList.tsx @@ -1,18 +1,18 @@ import { FC, Suspense } from "react" import { useNavigate } from "react-router-dom" -import { Token, TokenWithBalance } from "../../../shared/token/type" +import { TokenWithBalance } from "../../../shared/token/type" import { ErrorBoundary } from "../../components/ErrorBoundary" import ErrorBoundaryFallbackWithCopyError from "../../components/ErrorBoundaryFallbackWithCopyError" import { routes } from "../../routes" -import { useSelectedAccount } from "../accounts/accounts.state" import { NewTokenButton } from "./NewTokenButton" import { TokenListItemVariant } from "./TokenListItem" import { TokenListItemContainer } from "./TokenListItemContainer" -import { useFungibleTokensWithBalance } from "./tokens.state" +import { Account } from "../accounts/Account" interface TokenListProps { - tokenList?: Token[] + account: Account + tokens: TokenWithBalance[] showNewTokenButton?: boolean showTokenSymbol?: boolean variant?: TokenListItemVariant @@ -20,22 +20,19 @@ interface TokenListProps { } export const TokenList: FC = ({ - tokenList, + account, + tokens, showNewTokenButton = true, showTokenSymbol = false, - variant, - navigateToSend = true, + variant }) => { const navigate = useNavigate() - const account = useSelectedAccount() - const tokensForAccount = useFungibleTokensWithBalance(account) if (!account) { return null } - const tokens: TokenWithBalance[] | undefined = tokenList || tokensForAccount.tokenDetails tokens.forEach(token => { - token.balance = tokensForAccount.tokenDetails.find(td => td.id === token.id)?.balance + token.balance = tokens.find(td => td.id === token.id)?.balance }) return ( diff --git a/packages/extension/src/ui/features/accountTokens/tokens.state.ts b/packages/extension/src/ui/features/accountTokens/tokens.state.ts index ff85e1cf2..6c7975e53 100644 --- a/packages/extension/src/ui/features/accountTokens/tokens.state.ts +++ b/packages/extension/src/ui/features/accountTokens/tokens.state.ts @@ -161,11 +161,9 @@ export const useFungibleTokensWithBalance = ( isValidating: allUserTokensIsValidating, error: allUserTokensError } = useAllTokensWithBalance(account) - const selectedAccount = useAccount(account) const networkId = useMemo(() => { - return selectedAccount?.networkId ?? "" - }, [selectedAccount?.networkId]) - + return account?.networkId ?? "" + }, [account?.networkId]) const sortedTokenIds = allUserTokens.map((t) => t.id).sort() const cachedTokens = useTokensInNetwork(networkId) const { @@ -173,8 +171,8 @@ export const useFungibleTokensWithBalance = ( isValidating, error } = useSWR( - selectedAccount && [ - getAccountIdentifier(selectedAccount), + account && [ + getAccountIdentifier(account), sortedTokenIds, "accountFungibleTokens", ], @@ -212,7 +210,6 @@ export const useFungibleTokensWithBalance = ( }, } ) - const tokenDetailsIsInitialising = !error && !fungibleTokens return { @@ -226,10 +223,9 @@ export const useFungibleTokensWithBalance = ( export const useAllTokensWithBalance = ( account?: BaseWalletAccount ): UseBaseTokensWithBalance => { - const selectedAccount = useAccount(account) const networkId = useMemo(() => { - return selectedAccount?.networkId ?? "" - }, [selectedAccount?.networkId]) + return account?.networkId ?? "" + }, [account?.networkId]) const { pendingTransactions } = useAccountTransactions(account) const pendingTransactionsRef = useRef(pendingTransactions) @@ -241,19 +237,19 @@ export const useAllTokensWithBalance = ( mutate, } = useSWR( // skip if no account selected - selectedAccount && [ - getAccountIdentifier(selectedAccount), + account && [ + getAccountIdentifier(account), "accountTokens", ], async () => { - if (!selectedAccount) { + if (!account) { return } const allTokens: BaseTokenWithBalance[] = [] const network = await getNetwork(networkId) const nodeProvider = new NodeProvider(network.nodeUrl) - const tokenBalances = await getBalances(nodeProvider, selectedAccount.address) + const tokenBalances = await getBalances(nodeProvider, account.address) for (const tokenId of tokenBalances.keys()) { if (allTokens.findIndex((t) => t.id == tokenId) === -1) { diff --git a/packages/extension/src/ui/features/accounts/AccountContainer.tsx b/packages/extension/src/ui/features/accounts/AccountContainer.tsx index 84a0a6146..2d04f52e2 100644 --- a/packages/extension/src/ui/features/accounts/AccountContainer.tsx +++ b/packages/extension/src/ui/features/accounts/AccountContainer.tsx @@ -10,20 +10,21 @@ import { NavLink } from "react-router-dom" import { routes } from "../../routes" import { AccountNavigationBar } from "./AccountNavigationBar" -import { useSelectedAccount } from "./accounts.state" import { useAccountTransactions } from "./accountTransactions.state" +import { Account } from "../accounts/Account" -const { WalletIcon, NftIcon, ActivityIcon, SwapIcon } = icons +const { WalletIcon, NftIcon, ActivityIcon } = icons export interface AccountContainerProps extends PropsWithChildren { + account: Account scrollKey: string } export const AccountContainer: FC = ({ + account, scrollKey, children, }) => { - const account = useSelectedAccount() const { pendingTransactions } = useAccountTransactions(account) const { scrollRef, scroll } = useScrollRestoration(scrollKey) @@ -33,7 +34,7 @@ export const AccountContainer: FC = ({ return ( <> - + {children} theme.bg1}; - opacity: 0.5; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; -` - export const AccountListScreen: FC = () => { const navigate = useNavigate() const returnTo = useReturnTo() + const [accountsDiscovered, setAcccountsDiscovered] = useState(false) const selectedAccount = useSelectedAccount() const allAccounts = useAccounts({ showHidden: true }) const [hiddenAccounts, visibleAccounts] = partition( @@ -66,6 +56,21 @@ export const AccountListScreen: FC = () => { } }, [navigate, returnTo]) + useEffect(() => { + if (allAccounts.length === 0) { + discoverAccounts(currentNetwork.id) + .then(() => setAcccountsDiscovered(true)) + .catch((e) => { + console.error(e) + setAcccountsDiscovered(true) + }) + } + }, [currentNetwork]) + + if (allAccounts.length === 0 && !accountsDiscovered) { + return + } + return ( <> { + account?: Account showAccountButton?: boolean } export const AccountNavigationBar: FC = ({ + account, scroll, showAccountButton = true, }) => { const { accountNames } = useAccountMetadata() - const account = useSelectedAccount() const navigate = useNavigate() const location = useLocation() const returnTo = useCurrentPathnameWithQuery() diff --git a/packages/extension/src/ui/features/accounts/AccountScreen.tsx b/packages/extension/src/ui/features/accounts/AccountScreen.tsx index 784881db9..203c3103a 100644 --- a/packages/extension/src/ui/features/accounts/AccountScreen.tsx +++ b/packages/extension/src/ui/features/accounts/AccountScreen.tsx @@ -17,12 +17,13 @@ interface AccountScreenProps { export const AccountScreen: FC = ({ tab }) => { const account = useSelectedAccount() + const shouldShowFullScreenStatusMessage = useShouldShowFullScreenStatusMessage() const { addAccount } = useAddAccount() - const hasAcccount = !!account - const showEmpty = !hasAcccount + const hasAccount = !!account + const showEmpty = !hasAccount let body: ReactNode let scrollKey = "accounts/AccountScreen" @@ -45,5 +46,7 @@ export const AccountScreen: FC = ({ tab }) => { assertNever(tab) } - return {body} + return ( + {body} + ) } diff --git a/packages/extension/src/ui/features/accounts/AccountScreenEmpty.tsx b/packages/extension/src/ui/features/accounts/AccountScreenEmpty.tsx index 46a4f3308..e012e65e5 100644 --- a/packages/extension/src/ui/features/accounts/AccountScreenEmpty.tsx +++ b/packages/extension/src/ui/features/accounts/AccountScreenEmpty.tsx @@ -1,6 +1,8 @@ import { Empty, EmptyButton, icons } from "@argent/ui" import { partition } from "lodash-es" -import { FC, useEffect } from "react" +import { FC, useEffect, useState } from "react" +import { discoverAccounts } from "../../services/backgroundAccounts" +import { LoadingScreen } from "../actions/LoadingScreen" import { useCurrentNetwork } from "../networks/useNetworks" import { AccountNavigationBar } from "./AccountNavigationBar" @@ -20,6 +22,7 @@ export const AccountScreenEmpty: FC = ({ isDeploying, }) => { const currentNetwork = useCurrentNetwork() + const [accountsDiscovered, setAcccountsDiscovered] = useState(false) const allAccounts = useAccounts({ showHidden: true }) const [hiddenAccounts, visibleAccounts] = partition( allAccounts, @@ -32,15 +35,28 @@ export const AccountScreenEmpty: FC = ({ if (hasVisibleAccounts) { autoSelectAccountOnNetwork(currentNetwork.id) } + + if (allAccounts.length === 0) { + discoverAccounts(currentNetwork.id) + .then(() => setAcccountsDiscovered(true)) + .catch((e) => { + console.error(e) + setAcccountsDiscovered(true) + }) + } }, [currentNetwork.id, hasVisibleAccounts]) + + if (allAccounts.length === 0 && !accountsDiscovered) { + return + } + return ( <> - + } - title={`You have no ${hasHiddenAccounts ? "visible " : ""}accounts on ${ - currentNetwork.name - }`} + title={`You have no ${hasHiddenAccounts ? "visible " : ""}accounts on ${currentNetwork.name + }`} > } diff --git a/packages/extension/src/ui/features/accounts/AddAccount.tsx b/packages/extension/src/ui/features/accounts/AddAccount.tsx index 008dbd109..3bfae3664 100644 --- a/packages/extension/src/ui/features/accounts/AddAccount.tsx +++ b/packages/extension/src/ui/features/accounts/AddAccount.tsx @@ -1,5 +1,4 @@ -import { FC, useCallback, useState } from "react" -import { useNavigate } from "react-router-dom" +import { FC, useState } from "react" import { useAppState } from "../../app.state" import A from "tracking-link" @@ -72,14 +71,13 @@ const groupOptions = ["any", ...Array.from(Array(TOTAL_NUMBER_OF_GROUPS).keys()) const signOptions = ["default", "schnorr"] as const export const AddAccount: FC = () => { - const navigate = useNavigate() const [hasError, setHasError] = useState(false) const [group, setGroup] = useState(groupOptions[0]) const [signMethod, setSignMethod] = useState(signOptions[0]) const { switcherNetworkId } = useAppState() const { addAccount } = useAddAccount() - + const parsedGroup = group === "any" ? undefined : parseInt(group) const parsedKeyType = signMethod === "default" ? "default" : "bip340-schnorr" diff --git a/packages/extension/src/ui/features/accounts/accounts.state.ts b/packages/extension/src/ui/features/accounts/accounts.state.ts index 0677c0c4d..bf0f9c013 100644 --- a/packages/extension/src/ui/features/accounts/accounts.state.ts +++ b/packages/extension/src/ui/features/accounts/accounts.state.ts @@ -5,7 +5,7 @@ import { withHiddenSelector, withoutHiddenSelector, } from "../../../shared/account/selectors" -import { accountStore, addAccounts } from "../../../shared/account/store" +import { accountStore } from "../../../shared/account/store" import { defaultNetwork } from "../../../shared/network" import { useArrayStorage, @@ -16,7 +16,6 @@ import { accountsEqual } from "../../../shared/wallet.service" import { walletStore } from "../../../shared/wallet/walletStore" import { useCurrentNetwork } from "../networks/useNetworks" import { Account } from "./Account" -import { useAddAccount } from "./useAddAccount" export const mapWalletAccountsToAccounts = ( walletAccounts: WalletAccount[], @@ -83,7 +82,7 @@ export const useAccountsOnNetwork = ({ export const useAccount = ( account?: BaseWalletAccount, ): Account | undefined => { - const accounts = useAccounts({ allNetworks: true, showHidden: true }) + const accounts = useAccounts({ allNetworks: true, showHidden: true }) // Does it need to be all networks here? return useMemo(() => { if (!account) { return undefined diff --git a/packages/extension/src/ui/features/recovery/recovery.service.ts b/packages/extension/src/ui/features/recovery/recovery.service.ts index 75f572a13..21108bd7f 100644 --- a/packages/extension/src/ui/features/recovery/recovery.service.ts +++ b/packages/extension/src/ui/features/recovery/recovery.service.ts @@ -1,7 +1,5 @@ -import { some } from "lodash-es" - import { defaultNetwork } from "../../../shared/network" -import { accountsEqual, isEqualWalletAddress } from "../../../shared/wallet.service" +import { accountsEqual } from "../../../shared/wallet.service" import { useAppState } from "../../app.state" import { routes } from "../../routes" import { diff --git a/packages/extension/src/ui/features/send/SendScreen.tsx b/packages/extension/src/ui/features/send/SendScreen.tsx index 601f11e44..4b543fec0 100644 --- a/packages/extension/src/ui/features/send/SendScreen.tsx +++ b/packages/extension/src/ui/features/send/SendScreen.tsx @@ -55,7 +55,6 @@ export const SendScreen: FC = () => { const currentQueryValue = watch().query const { tokenDetails: fungibleTokens } = useFungibleTokensWithBalance(account) const tokenList = useCustomTokenList(fungibleTokens, account?.networkId, currentQueryValue) - if (!account) { return <> } @@ -86,7 +85,8 @@ export const SendScreen: FC = () => { }> { + sendMessage({ type: "DISCOVER_ACCOUNTS", data: { networkId } }) + return waitForMessage("DISCOVER_ACCOUNTS_RES") +} + export const importNewLedgerAccount = async (account: Account, hdIndex: number, networkId: string) => { sendMessage({ type: "NEW_LEDGER_ACCOUNT", data: { account, hdIndex, networkId } }) try { From 2ba73db30ed9f8a52bfe9ec459bf0c23d418591b Mon Sep 17 00:00:00 2001 From: h0ngcha0 Date: Mon, 22 Jan 2024 09:53:39 +0100 Subject: [PATCH 03/17] Enable sweep if ALPH only --- .../accountTokens/SendTokenScreen.tsx | 66 +++++++++++++------ 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/packages/extension/src/ui/features/accountTokens/SendTokenScreen.tsx b/packages/extension/src/ui/features/accountTokens/SendTokenScreen.tsx index 6a19d6769..bc91d8249 100644 --- a/packages/extension/src/ui/features/accountTokens/SendTokenScreen.tsx +++ b/packages/extension/src/ui/features/accountTokens/SendTokenScreen.tsx @@ -34,7 +34,7 @@ import { isValidAddress, normalizeAddress, } from "../../services/addresses" -import { sendTransferTransaction } from "../../services/transactions" +import { sendTransferTransaction, sendUnsignedTxTransaction } from "../../services/transactions" import { useOnClickOutside } from "../../services/useOnClickOutside" import { H3, H5 } from "../../theme/Typography" import { Account } from "../accounts/Account" @@ -53,6 +53,7 @@ import { TokenMenuDeprecated } from "./TokenMenuDeprecated" import { useTokenUnitAmountToCurrencyValue } from "./tokenPriceHooks" import { formatTokenBalance, toTokenView } from "./tokens.service" import { + useAllTokensWithBalance, useNetworkFeeToken, useToken } from "./tokens.state" @@ -218,7 +219,9 @@ export const SendTokenScreen: FC = () => { id: tokenId || "0x0", networkId: account?.networkId || "Unknown", }) + const { tokenWithBalance } = useTokenBalanceForAccount({ token, account }) + const { tokenDetails: allTokensWithBalance } = useAllTokensWithBalance(account) const resolver = useYupValidationResolver(SendSchema) const feeToken = useNetworkFeeToken(account?.networkId) @@ -311,6 +314,10 @@ export const SendTokenScreen: FC = () => { return tokenId === ALPH_TOKEN_ID || tokenId === undefined } + const isSweepingAllAsset = (tokenId: string | undefined): boolean => { + return maxClicked && allTokensWithBalance.length === 1 && isAlphToken(tokenId) + } + const recipientInAddressBook = useMemo( () => // Check if inputRecipient is in Contacts or userAccounts @@ -320,7 +327,6 @@ export const SendTokenScreen: FC = () => { [addressBook.contacts, addressBook.userAccounts, inputRecipient], ) - // If ALPH only, can still sweep const sweepTransaction: Promise = useMemo( async () => { let result = undefined @@ -450,27 +456,41 @@ export const SendTokenScreen: FC = () => { { if (account) { - let destination: Destination - if (isAlphToken(tokenId)) { - destination = { - address: recipient, - attoAlphAmount: convertAlphAmountWithDecimals(amount) ?? '?', - tokens: [] + // If ALPH only, can still sweep + if (isSweepingAllAsset(tokenId)) { + const sweepResult = await sweepTransaction + if (sweepResult) { + sweepResult.unsignedTxs.map((sweepUnsignedTx) => { + sendUnsignedTxTransaction({ + signerAddress: account.address, + networkId: account.networkId, + unsignedTx: sweepUnsignedTx.unsignedTx + }) + }) } } else { - destination = { - address: recipient, - attoAlphAmount: DUST_AMOUNT, - tokens: [{ id: tokenId as string, amount: convertAmountWithDecimals(amount, decimals) ?? '?' }] + let destination: Destination + if (isAlphToken(tokenId)) { + destination = { + address: recipient, + attoAlphAmount: convertAlphAmountWithDecimals(amount) ?? '?', + tokens: [] + } + } else { + destination = { + address: recipient, + attoAlphAmount: DUST_AMOUNT, + tokens: [{ id: tokenId as string, amount: convertAmountWithDecimals(amount, decimals) ?? '?' }] + } } - } - sendTransferTransaction({ - signerAddress: account.address, - signerKeyType: account.signer.keyType, - networkId: account.networkId, - destinations: [destination], - }) + sendTransferTransaction({ + signerAddress: account.address, + signerKeyType: account.signer.keyType, + networkId: account.networkId, + destinations: [destination], + }) + } navigate(routes.accountTokens(), { replace: true }) } @@ -609,6 +629,14 @@ export const SendTokenScreen: FC = () => { )} + { + (isSweepingAllAsset(tokenId) && txsNumber > 1) ? ( + + Warning: This will sweep all ALPHs to the recipient. + `Due to the number of UTXOs, you need to sign ${txsNumber} transactions` + + ) : <> + }