diff --git a/packages/mask/src/extension/popups/pages/Wallet/index.tsx b/packages/mask/src/extension/popups/pages/Wallet/index.tsx index f24b67593077..d592da42e92b 100644 --- a/packages/mask/src/extension/popups/pages/Wallet/index.tsx +++ b/packages/mask/src/extension/popups/pages/Wallet/index.tsx @@ -1,15 +1,15 @@ +import urlcat from 'urlcat' +import { lazy, Suspense, useEffect } from 'react' +import { useAsyncRetry } from 'react-use' +import { Route, Routes, useNavigate, useLocation } from 'react-router-dom' import { WalletStartUp } from './components/StartUp' import { WalletAssets } from './components/WalletAssets' -import { Route, Routes, useNavigate, useLocation } from 'react-router-dom' -import { lazy, Suspense, useEffect } from 'react' import { PopupRoutes, relativeRouteOf } from '@masknet/shared-base' import { WalletContext } from './hooks/useWalletContext' import { LoadingPlaceholder } from '../../components/LoadingPlaceholder' -import { useAsyncRetry } from 'react-use' import { WalletMessages, WalletRPC } from '../../../../plugins/Wallet/messages' import SelectWallet from './SelectWallet' import { useWalletLockStatus } from './hooks/useWalletLockStatus' -import urlcat from 'urlcat' import { WalletHeader } from './components/WalletHeader' import { useChainId, useWallet, useWeb3State } from '@masknet/plugin-infra/web3' import { NetworkPluginID, TransactionDescriptorType } from '@masknet/web3-shared-base' diff --git a/packages/mask/src/plugins/Gitcoin/SNSAdaptor/DonateDialog.tsx b/packages/mask/src/plugins/Gitcoin/SNSAdaptor/DonateDialog.tsx index 5111b5b48bed..7ba0f86157cb 100644 --- a/packages/mask/src/plugins/Gitcoin/SNSAdaptor/DonateDialog.tsx +++ b/packages/mask/src/plugins/Gitcoin/SNSAdaptor/DonateDialog.tsx @@ -172,12 +172,13 @@ export function DonateDialog(props: DonateDialogProps) { ({ flexDirection: 'column', position: 'relative', marginTop: theme.spacing(1.5), - lineHeight: '22px', + lineHeight: '20px', justifyContent: 'center', alignItems: 'center', - padding: '13px 0', + padding: '12px 0', fontSize: 18, }, buttonLabel: { @@ -36,7 +36,7 @@ const useStyles = makeStyles()((theme) => ({ }, })) -export interface EthereumERC20TokenApprovedBoundaryProps { +export interface EthereumERC20TokenApprovedBoundaryProps extends withClasses<'button'> { amount: string spender?: string token?: FungibleToken @@ -63,7 +63,7 @@ export function EthereumERC20TokenApprovedBoundary(props: EthereumERC20TokenAppr } = props const { t } = useI18N() - const { classes } = useStyles() + const classes = useStylesExtends(useStyles(), props) const [{ type: approveStateType, allowance }, transactionState, approveCallback, resetApproveCallback] = useERC20TokenApproveCallback(token?.address ?? '', amount, spender) diff --git a/packages/plugin-infra/src/web3-state/TransactionWatcher.ts b/packages/plugin-infra/src/web3-state/TransactionWatcher.ts index e58a25ecef44..20e644f1b880 100644 --- a/packages/plugin-infra/src/web3-state/TransactionWatcher.ts +++ b/packages/plugin-infra/src/web3-state/TransactionWatcher.ts @@ -36,11 +36,10 @@ class Watcher { constructor( protected storage: StorageItem>, - protected checkers: Array>, + protected checkers: Array>, protected options: { - checkDelay: number - getTransactionCreator: (transaction: Transaction) => string - onNotify: (id: string, status: TransactionStatusType, transaction: Transaction) => void + delay: number + onNotify: (chainId: ChainId, id: string, transaction: Transaction, status: TransactionStatusType) => void }, ) {} @@ -110,14 +109,11 @@ class Watcher { for (const [id, { transaction }] of watchedTransactions) { for (const checker of this.checkers) { try { - const status = await checker.checkStatus( - id, - chainId, - this.options.getTransactionCreator(transaction), - ) + const status = await checker.getStatus(chainId, id, transaction) if (status !== TransactionStatusType.NOT_DEPEND) { - this.removeTransaction(chainId, id) - this.options.onNotify(id, status, transaction) + await this.unwatchTransaction(chainId, id) + this.options.onNotify(chainId, id, transaction, status) + break } } catch (error) { console.warn('Failed to check transaction status.') @@ -132,7 +128,7 @@ class Watcher { public startCheck(chainId: ChainId) { this.stopCheck() if (this.timer === null) { - this.timer = setTimeout(this.check.bind(this, chainId), this.options.checkDelay) + this.timer = setTimeout(this.check.bind(this, chainId), this.options.delay) } } @@ -168,7 +164,7 @@ export class TransactionWatcherState constructor( protected context: Plugin.Shared.SharedContext, protected chainIds: ChainId[], - protected checkers: Array>, + protected checkers: Array>, protected subscriptions: { chainId?: Subscription transactions?: Subscription>> @@ -224,34 +220,32 @@ export class TransactionWatcherState } } - private getWatcher(chainId: ChainId) { + protected getWatcher(chainId: ChainId) { if (!this.watchers.has(chainId)) this.watchers.set( chainId, new Watcher(this.storage, this.checkers, { - checkDelay: this.options.defaultBlockDelay * 1000, - getTransactionCreator: this.options.getTransactionCreator, + delay: this.options.defaultBlockDelay * 1000, onNotify: this.notifyTransaction.bind(this), }), ) return this.watchers.get(chainId)! } - private resumeWatcher(chainId: ChainId) { + protected resumeWatcher(chainId: ChainId) { const watcher = this.getWatcher(chainId) watcher.startCheck(chainId) } async watchTransaction(chainId: ChainId, id: string, transaction: Transaction) { await this.getWatcher(chainId).watchTransaction(chainId, id, transaction) - this.emitter.emit('progress', id, TransactionStatusType.NOT_DEPEND, transaction) } async unwatchTransaction(chainId: ChainId, id: string) { await this.getWatcher(chainId).unwatchTransaction(chainId, id) } - notifyTransaction(id: string, status: TransactionStatusType, transaction: Transaction) { - this.emitter.emit('progress', id, status, transaction) + async notifyTransaction(chainId: ChainId, id: string, transaction: Transaction, status: TransactionStatusType) { + throw new Error('Method not implemented.') } } diff --git a/packages/plugin-infra/src/web3/useNonFungibleTokenContract.ts b/packages/plugin-infra/src/web3/useNonFungibleTokenContract.ts index 5e187724df7b..afc9773a39e0 100644 --- a/packages/plugin-infra/src/web3/useNonFungibleTokenContract.ts +++ b/packages/plugin-infra/src/web3/useNonFungibleTokenContract.ts @@ -14,7 +14,7 @@ export function useNonFungibleTokenContract, Web3Helper.SchemaTypeScope> | undefined >(async () => { - if (!connection || !address || !options) return - return connection.getNonFungibleTokenContract?.(address, schemaType, options) - }, [address, schemaType, connection, JSON.stringify(options)]) + if (!connection || !address) return + return connection.getNonFungibleTokenContract?.(address, schemaType) + }, [address, schemaType, connection]) } diff --git a/packages/plugins/EVM/src/state/Connection/middleware/Transaction.ts b/packages/plugins/EVM/src/state/Connection/middleware/Transaction.ts index 2819826ff527..106db1d5fc49 100644 --- a/packages/plugins/EVM/src/state/Connection/middleware/Transaction.ts +++ b/packages/plugins/EVM/src/state/Connection/middleware/Transaction.ts @@ -31,14 +31,6 @@ export class RecentTransaction implements Middleware { const status = getReceiptStatus(receipt) if (!receipt?.transactionHash || status === TransactionStatusType.NOT_DEPEND) return - // update in house transaction state - await Transaction?.updateTransaction?.( - context.chainId, - context.account, - receipt.transactionHash, - status, - ) - // update built-in notifier BalanceNotifier?.emitter.emit('update', { chainId: context.chainId, diff --git a/packages/plugins/EVM/src/state/TransactionWatcher.ts b/packages/plugins/EVM/src/state/TransactionWatcher.ts index 5466bddad638..ca68a35421b5 100644 --- a/packages/plugins/EVM/src/state/TransactionWatcher.ts +++ b/packages/plugins/EVM/src/state/TransactionWatcher.ts @@ -3,8 +3,10 @@ import { getEnumAsArray } from '@dimensiondev/kit' import type { Plugin } from '@masknet/plugin-infra' import { TransactionWatcherState } from '@masknet/plugin-infra/web3' import { ChainId, Transaction } from '@masknet/web3-shared-evm' -import type { RecentTransaction } from '@masknet/web3-shared-base' +import { RecentTransaction, TransactionStatusType } from '@masknet/web3-shared-base' import { TransactionCheckers } from './TransactionWatcher/checker' +import { Web3StateSettings } from '../settings' +import { getSubscriptionCurrentValue } from '@masknet/shared-base' export class TransactionWatcher extends TransactionWatcherState { constructor( @@ -25,4 +27,28 @@ export class TransactionWatcher extends TransactionWatcherState Transaction?.transactions).then((transactions) => { + if (transactions?.some((x) => Object.keys(x.candidates).includes(id))) + this.emitter.emit('progress', id, status, transaction) + }) + } } diff --git a/packages/plugins/EVM/src/state/TransactionWatcher/checkers/AccountChecker.ts b/packages/plugins/EVM/src/state/TransactionWatcher/checkers/AccountChecker.ts index 2d30b8f1d59c..38c35cf39cfa 100644 --- a/packages/plugins/EVM/src/state/TransactionWatcher/checkers/AccountChecker.ts +++ b/packages/plugins/EVM/src/state/TransactionWatcher/checkers/AccountChecker.ts @@ -1,29 +1,69 @@ +import { sha3, toHex } from 'web3-utils' import { first } from 'lodash-unified' -import { Explorer } from '@masknet/web3-providers' -import type { TransactionChecker, TransactionStatusType } from '@masknet/web3-shared-base' -import { ChainId, getExplorerConstants } from '@masknet/web3-shared-evm' +import { Explorer, ExplorerAPI } from '@masknet/web3-providers' +import { TransactionChecker, TransactionStatusType } from '@masknet/web3-shared-base' +import { ChainId, getExplorerConstants, Transaction } from '@masknet/web3-shared-evm' + +class TTL { + private cache: Record = {} + + get(key: string): T | undefined { + if (!this.cache[key]) return + const { value, at, ttl } = this.cache[key] + return Date.now() - at < ttl ? value : undefined + } + set(key: string, value: T, ttl = Number.MAX_SAFE_INTEGER) { + this.cache[key] = { + at: Date.now(), + ttl, + value, + } + } +} /** * Fetch latest tx list of the the given account. It creates a signature for each transaction. * Treat two transactions the same with the identical transaction hash or signature. */ -export class AccountChecker implements TransactionChecker { +export class AccountChecker implements TransactionChecker { static CHECK_LATEST_TRANSACTION_SIZE = 5 + private ttl = new TTL() + + private getExplorerTransactionId(transaction: ExplorerAPI.Transaction | null) { + if (!transaction) return '' + const { from, to, input, value } = transaction + return sha3([from, to, input || '0x0', toHex(value || '0x0') || '0x0'].join('_')) ?? '' + } + + private getTransactionId(transaction: Transaction) { + const { from, to, data = '0x0', value = '0x0' } = transaction + if (!from || !to) return '' + return sha3([from, to, data, value].join('_')) ?? '' + } + private async fetchLatestTransactions(chainId: ChainId, account: string) { + const key = `${chainId}_${account.toLowerCase}` + const hit = this.ttl.get(key) + if (hit) return hit + const { API_KEYS = [], EXPLORER_API = '' } = getExplorerConstants(chainId) - return Explorer.getLatestTransactions(account, EXPLORER_API, { + const transactions = await Explorer.getLatestTransactions(account, EXPLORER_API, { offset: AccountChecker.CHECK_LATEST_TRANSACTION_SIZE, apikey: first(API_KEYS), }) + this.ttl.set(key, transactions, 15 * 1000) + return transactions } - async checkStatus(id: string, chainId: ChainId, account: string): Promise { - throw new Error('Method not implemented') - // const latestTransactions = await this.fetchLatestTransactions(chainId, account) - // const tx = latestTransactions.find((x) => x.hash === id) - // if (!tx) return TransactionStatusType.NOT_DEPEND - // // '1' for successful transactions and '0' for failed transactions. - // return tx.status === '1' ? TransactionStatusType.SUCCEED : TransactionStatusType.FAILED + async getStatus(chainId: ChainId, id: string, transaction: Transaction): Promise { + const account = transaction.from as string | undefined + if (!account) throw new Error('Cannot found account.') + const latestTransactions = await this.fetchLatestTransactions(chainId, account) + const txId = this.getTransactionId(transaction) + const tx = latestTransactions.find((x) => x.hash === id || this.getExplorerTransactionId(x) === txId) + if (!tx) return TransactionStatusType.NOT_DEPEND + // '1' for successful transactions and '0' for failed transactions. + return tx.status === '1' ? TransactionStatusType.SUCCEED : TransactionStatusType.FAILED } } diff --git a/packages/plugins/EVM/src/state/TransactionWatcher/checkers/ReceiptChecker.ts b/packages/plugins/EVM/src/state/TransactionWatcher/checkers/ReceiptChecker.ts index e3fe518a7195..c14bba3a8051 100644 --- a/packages/plugins/EVM/src/state/TransactionWatcher/checkers/ReceiptChecker.ts +++ b/packages/plugins/EVM/src/state/TransactionWatcher/checkers/ReceiptChecker.ts @@ -1,10 +1,10 @@ import type { TransactionChecker, TransactionStatusType } from '@masknet/web3-shared-base' -import type { ChainId } from '@masknet/web3-shared-evm' +import type { ChainId, Transaction } from '@masknet/web3-shared-evm' import { Web3StateSettings } from '../../../settings' import { getReceiptStatus } from '../../Connection/utils' -export class ReceiptChecker implements TransactionChecker { - async checkStatus(id: string, chainId: ChainId): Promise { +export class ReceiptChecker implements TransactionChecker { + async getStatus(chainId: ChainId, id: string, transaction: Transaction): Promise { const connection = await Web3StateSettings.value.Connection?.getConnection?.({ chainId, }) diff --git a/packages/web3-shared/base/src/specs/index.ts b/packages/web3-shared/base/src/specs/index.ts index bccc344cfa85..3fa01ca0582c 100644 --- a/packages/web3-shared/base/src/specs/index.ts +++ b/packages/web3-shared/base/src/specs/index.ts @@ -555,8 +555,8 @@ export interface ProviderOptions { account?: string } -export interface TransactionChecker { - checkStatus(id: string, chainId: ChainId, account: string): Promise +export interface TransactionChecker { + getStatus(chainId: ChainId, id: string, transaction: Transaction): Promise } export interface ConnectionOptions { @@ -931,7 +931,7 @@ export interface TransactionWatcherState { /** Remove a transaction from the watch list. */ unwatchTransaction: (chainId: ChainId, id: string) => Promise /** Update transaction status */ - notifyTransaction: (id: string, status: TransactionStatusType, transaction: Transaction) => void + notifyTransaction: (chainId: ChainId, id: string, transaction: Transaction, status: TransactionStatusType) => Promise } export interface ProviderState { /** The account of the currently visiting site. */ diff --git a/packages/web3-shared/evm/utils/payload.ts b/packages/web3-shared/evm/utils/payload.ts index 030cd5e9407e..58c2e282ac65 100644 --- a/packages/web3-shared/evm/utils/payload.ts +++ b/packages/web3-shared/evm/utils/payload.ts @@ -1,6 +1,5 @@ import BigNumber from 'bignumber.js' import { first } from 'lodash-unified' -import { sha3 } from 'web3-utils' import type { JsonRpcPayload } from 'web3-core-helpers' import { EthereumMethodType, Transaction } from '../types' @@ -21,14 +20,6 @@ export function getPayloadId(payload: JsonRpcPayload) { return typeof payload.id === 'string' ? Number.parseInt(payload.id, 10) : payload.id } -export function getPayloadSignature(payload: JsonRpcPayload) { - const config = getPayloadConfig(payload) - if (!config) return - const { from, to, data = '0x0', value = '0x0' } = config - if (!from || !to) return - return sha3([from, to, data, value].join('_')) ?? undefined -} - export function getPayloadFrom(payload: JsonRpcPayload): string | undefined { switch (payload.method) { case EthereumMethodType.ETH_SIGN: @@ -43,31 +34,11 @@ export function getPayloadFrom(payload: JsonRpcPayload): string | undefined { } } -export function getPayloadTo(payload: JsonRpcPayload) { - const config = getPayloadConfig(payload) - return config?.to as string | undefined -} - export function getPayloadChainId(payload: JsonRpcPayload) { const config = getPayloadConfig(payload) return typeof config?.chainId === 'string' ? Number.parseInt(config.chainId, 16) || undefined : undefined } -export function getPayloadAccount(payload: JsonRpcPayload): string | undefined { - switch (payload.method) { - case EthereumMethodType.ETH_SIGN: - return first(payload.params) - case EthereumMethodType.PERSONAL_SIGN: - return payload.params?.[1] - case EthereumMethodType.ETH_SIGN_TYPED_DATA: - return first(payload.params) - case EthereumMethodType.ETH_SEND_TRANSACTION: - return getPayloadConfig(payload)?.from as string | undefined - default: - return - } -} - export function getPayloadConfig(payload: JsonRpcPayload) { switch (payload.method) { case EthereumMethodType.ETH_CALL: @@ -85,22 +56,3 @@ export function getPayloadConfig(payload: JsonRpcPayload) { return } } - -export function getPayloadHash(payload: JsonRpcPayload) { - switch (payload.method) { - case EthereumMethodType.ETH_SEND_TRANSACTION: { - return '' - } - case EthereumMethodType.MASK_REPLACE_TRANSACTION: { - const [hash] = payload.params as [string] - return hash - } - default: - return '' - } -} - -export function getPayloadNonce(payload: JsonRpcPayload) { - const config = getPayloadConfig(payload) - return config?.nonce -}