From effce18c3a616cdee1d0d78dbdb1e9a71665150c Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Thu, 9 Dec 2021 21:57:52 +0800 Subject: [PATCH 01/12] chore: add explorer API --- packages/web3-constants/evm/explorer.json | 16 ++++++++++++++++ packages/web3-shared/evm/pipes/index.ts | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 packages/web3-constants/evm/explorer.json diff --git a/packages/web3-constants/evm/explorer.json b/packages/web3-constants/evm/explorer.json new file mode 100644 index 000000000000..65da13df75fe --- /dev/null +++ b/packages/web3-constants/evm/explorer.json @@ -0,0 +1,16 @@ +{ + "API_KEY": { + "Mainnet": ["26GHYKIAZREN4HWJ5NCICTGKC183IGXY9A"], + "Ropsten": [], + "Rinkeby": [], + "Kovan": [], + "Gorli": [], + "BSC": ["5FQ5JHS1BEK4186QCZASE3SD23YD8FIMM8"], + "BSCT": [], + "Matic": ["5HVFKYFPQXWNQ2TFHCAYJJZ3YIYPWQ5HP8"], + "Mumbai": [], + "Arbitrum": ["BE8VU1P9FUKRT15FBHKQMU84829VITWWF2"], + "Arbitrum_Rinkeby": [], + "xDai": [] + } +} diff --git a/packages/web3-shared/evm/pipes/index.ts b/packages/web3-shared/evm/pipes/index.ts index bcfb4c9ace19..cb910a60fc0a 100644 --- a/packages/web3-shared/evm/pipes/index.ts +++ b/packages/web3-shared/evm/pipes/index.ts @@ -107,6 +107,24 @@ export const resolveChainColor = createLookupTableResolver( 'rgb(214, 217, 220)', ) +export const resolveExplorerAPI = createLookupTableResolver( + { + [ChainId.Mainnet]: 'https://api.etherscan.io/api', + [ChainId.Ropsten]: '', + [ChainId.Kovan]: '', + [ChainId.Rinkeby]: '', + [ChainId.Gorli]: '', + [ChainId.BSC]: 'https://api.bscscan.com/api', + [ChainId.BSCT]: '', + [ChainId.Matic]: 'https://api.polygonscan.com/api', + [ChainId.Mumbai]: '', + [ChainId.Arbitrum]: 'https://api.arbiscan.io/api', + [ChainId.Arbitrum_Rinkeby]: '', + [ChainId.xDai]: 'https://blockscout.com/xdai/mainnet/api', + }, + '' +) + export function resolveLinkOnExplorer(chainId: ChainId) { const chainDetailed = getChainDetailed(chainId) if (!chainDetailed) return '' From bfead379724970588a17f48cd2aaee5c34c0d480 Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Thu, 9 Dec 2021 22:53:33 +0800 Subject: [PATCH 02/12] fix: parse bit str --- .../RedPacket/SNSAdaptor/hooks/useAvailabilityNftRedPacket.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/mask/src/plugins/RedPacket/SNSAdaptor/hooks/useAvailabilityNftRedPacket.ts b/packages/mask/src/plugins/RedPacket/SNSAdaptor/hooks/useAvailabilityNftRedPacket.ts index 60a21a7e5b88..95a9b460db6d 100644 --- a/packages/mask/src/plugins/RedPacket/SNSAdaptor/hooks/useAvailabilityNftRedPacket.ts +++ b/packages/mask/src/plugins/RedPacket/SNSAdaptor/hooks/useAvailabilityNftRedPacket.ts @@ -1,3 +1,4 @@ +import BigNumber from 'bignumber.js' import { useAsyncRetry } from 'react-use' import { useNftRedPacketContract } from './useNftRedPacketContract' @@ -17,7 +18,7 @@ export function useAvailabilityNftRedPacket(id: string, from: string) { const isClaimed = availability.claimed_id !== '0' const totalAmount = result.erc721_token_ids.length - const bits = Number(result.bit_status).toString(2).split('') + const bits = new BigNumber(result.bit_status).toString(2).split('') const claimedAmount = bits.reduce((acc, cur) => { if (cur === '1') return acc + 1 return acc From 25beea73bbe60a7135f17541fc14eaf8030fc4b5 Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Thu, 9 Dec 2021 22:57:15 +0800 Subject: [PATCH 03/12] chore: setup explorer constants --- packages/web3-shared/evm/constants/index.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/web3-shared/evm/constants/index.ts b/packages/web3-shared/evm/constants/index.ts index ebad208cc9ff..2867e8685efa 100644 --- a/packages/web3-shared/evm/constants/index.ts +++ b/packages/web3-shared/evm/constants/index.ts @@ -10,6 +10,7 @@ import Trader from '@masknet/web3-constants/evm/trader.json' import Trending from '@masknet/web3-constants/evm/trending.json' import MaskBox from '@masknet/web3-constants/evm/mask-box.json' import RPC from '@masknet/web3-constants/evm/rpc.json' +import EXPLORER from '@masknet/web3-constants/evm/explorer.json' import PoolTogether from '@masknet/web3-constants/evm/pooltogether.json' import TokenAssetBaseURL from '@masknet/web3-constants/evm/token-asset-base-url.json' import GoodGhosting from '@masknet/web3-constants/evm/good-ghosting.json' @@ -18,6 +19,14 @@ import OpenseaAPI from '@masknet/web3-constants/evm/opensea-api.json' import Chain from '@masknet/web3-constants/evm/chain.json' import { hookTransform, transform, transformFromJSON } from './utils' +function getEnvConstants(key: string) { + try { + return process.env[key] ?? '' + } catch { + return '' + } +} + export { ZERO_ADDRESS, FAKE_SIGN_PASSWORD, EthereumNameType } from './specific' export const getAirdropConstants = transform(Airdrop) @@ -50,14 +59,12 @@ export const useTrendingConstants = hookTransform(getTrendingConstants) export const getMaskBoxConstants = transform(MaskBox) export const useMaskBoxConstants = hookTransform(getMaskBoxConstants) -let WEB3_CONSTANTS_RPC = '' -try { - WEB3_CONSTANTS_RPC = process.env.WEB3_CONSTANTS_RPC ?? '' -} catch {} - -export const getRPCConstants = transformFromJSON(WEB3_CONSTANTS_RPC, RPC) +export const getRPCConstants = transformFromJSON(getEnvConstants('WEB3_CONSTANTS_RPC'), RPC) export const useRPCConstants = hookTransform(getRPCConstants) +export const getExplorerConstants = transformFromJSON(getEnvConstants('WEB3_CONSTANTS_EXPLORER'), EXPLORER) +export const useExplorerConstants = hookTransform(getExplorerConstants) + export const getTokenAssetBaseURLConstants = transform(TokenAssetBaseURL) export const useTokenAssetBaseURLConstants = hookTransform(getTokenAssetBaseURLConstants) From 2abb2936b4d4e65acec5b7b9e020a835ba9b6561 Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Fri, 10 Dec 2021 00:44:16 +0800 Subject: [PATCH 04/12] chore: check account txs by explorer API --- cspell.json | 3 +- packages/mask/package.json | 1 + .../EthereumServices/send.ts | 60 +----- .../Wallet/services/transaction/database.ts | 1 + .../Wallet/services/transaction/helpers.ts | 30 ++- .../Wallet/services/transaction/index.ts | 8 +- .../Wallet/services/transaction/watcher.ts | 181 ++++++++++++++---- packages/web3-constants/evm/explorer.json | 2 +- .../web3-providers/src/explorer/helpers.ts | 23 +++ packages/web3-providers/src/explorer/index.ts | 31 +++ packages/web3-providers/src/explorer/types.ts | 21 ++ packages/web3-providers/src/index.ts | 1 + packages/web3-shared/evm/pipes/index.ts | 2 +- packages/web3-shared/evm/utils/index.ts | 1 + packages/web3-shared/evm/utils/payload.ts | 58 ++++++ pnpm-lock.yaml | 2 + 16 files changed, 323 insertions(+), 102 deletions(-) create mode 100644 packages/web3-providers/src/explorer/helpers.ts create mode 100644 packages/web3-providers/src/explorer/index.ts create mode 100644 packages/web3-providers/src/explorer/types.ts create mode 100644 packages/web3-shared/evm/utils/payload.ts diff --git a/cspell.json b/cspell.json index dd0efcc1c3e6..f0dd7d1434c2 100644 --- a/cspell.json +++ b/cspell.json @@ -337,7 +337,8 @@ "isfacebook", "findtruman", "uaddr", - "consts" + "consts", + "txreceipt" ], "ignoreRegExpList": ["/@servie/"], "overrides": [ diff --git a/packages/mask/package.json b/packages/mask/package.json index c849fa249621..dc3f9afbd119 100644 --- a/packages/mask/package.json +++ b/packages/mask/package.json @@ -31,6 +31,7 @@ "@masknet/shared-base": "workspace:*", "@masknet/theme": "workspace:*", "@masknet/web3-contracts": "workspace:*", + "@masknet/web3-providers": "workspace:*", "@masknet/web3-shared-evm": "workspace:*", "@msgpack/msgpack": "^2.7.1", "@servie/events": "^3.0.0", diff --git a/packages/mask/src/extension/background-script/EthereumServices/send.ts b/packages/mask/src/extension/background-script/EthereumServices/send.ts index 82d23ba2e8d2..8cde9c82be58 100644 --- a/packages/mask/src/extension/background-script/EthereumServices/send.ts +++ b/packages/mask/src/extension/background-script/EthereumServices/send.ts @@ -10,12 +10,16 @@ import { EthereumErrorType, EthereumMethodType, EthereumRpcType, - EthereumTransactionConfig, ZERO_ADDRESS, isEIP1559Supported, isSameAddress, ProviderType, SendOverrides, + getPayloadHash, + getPayloadConfig, + getPayloadChainId, + getPayloadNonce, + getTransactionHash, } from '@masknet/web3-shared-evm' import type { IJsonRpcRequest } from '@walletconnect/types' import * as MetaMask from './providers/MetaMask' @@ -80,56 +84,6 @@ function getTo(computedPayload: UnboxPromise x.hash === oldHash) if (!transaction) throw new Error('Failed to find the old transaction.') + if (transaction.hash === newHash) return transaction.hashReplacement = newHash transaction.payloadReplacement = payload await PluginDB.add({ diff --git a/packages/mask/src/plugins/Wallet/services/transaction/helpers.ts b/packages/mask/src/plugins/Wallet/services/transaction/helpers.ts index f717bf6f449c..4354f7c29161 100644 --- a/packages/mask/src/plugins/Wallet/services/transaction/helpers.ts +++ b/packages/mask/src/plugins/Wallet/services/transaction/helpers.ts @@ -10,9 +10,31 @@ import { } from '@masknet/web3-shared-evm' import { unreachable } from '@dimensiondev/kit' -export function getPayloadId(payload: JsonRpcPayload) { - if (!payload.id || payload.method !== EthereumMethodType.ETH_SEND_TRANSACTION) return '' +export function toReceipt(status: '0' | '1', transaction: Transaction): TransactionReceipt { + return { + status: status === '1', + transactionHash: transaction.hash, + transactionIndex: transaction.transactionIndex ?? 0, + blockHash: transaction.blockHash ?? '', + blockNumber: transaction.blockNumber ?? 0, + from: transaction.from, + to: transaction.to ?? '', + cumulativeGasUsed: 0, + gasUsed: 0, + logs: [], + logsBloom: '', + } +} + +export function getPayloadConfig(payload: JsonRpcPayload) { + if (!payload.id || payload.method !== EthereumMethodType.ETH_SEND_TRANSACTION) return const [config] = payload.params as [TransactionConfig] + return config +} + +export function getPayloadId(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('_')) ?? '' @@ -27,8 +49,8 @@ export function getTransactionId(transaction: Transaction | null) { export function getReceiptStatus(receipt: TransactionReceipt | null) { if (!receipt) return TransactionStatusType.NOT_DEPEND const status = receipt.status as unknown as string - if (receipt.status === false || ['0x', '0x0'].includes(status)) return TransactionStatusType.FAILED - if (receipt.status === true || ['0x1'].includes(status)) { + if (receipt.status === false || ['0', '0x', '0x0'].includes(status)) return TransactionStatusType.FAILED + if (receipt.status === true || ['1', '0x1'].includes(status)) { if (isSameAddress(receipt.from, receipt.to)) return TransactionStatusType.CANCELLED return TransactionStatusType.SUCCEED } diff --git a/packages/mask/src/plugins/Wallet/services/transaction/index.ts b/packages/mask/src/plugins/Wallet/services/transaction/index.ts index ff3d61da06c3..4c94f8c464b5 100644 --- a/packages/mask/src/plugins/Wallet/services/transaction/index.ts +++ b/packages/mask/src/plugins/Wallet/services/transaction/index.ts @@ -1,6 +1,6 @@ import type { TransactionReceipt } from 'web3-core' import type { JsonRpcPayload } from 'web3-core-helpers' -import type { ChainId, TransactionStatusType } from '@masknet/web3-shared-evm' +import { ChainId, getPayloadNonce, TransactionStatusType } from '@masknet/web3-shared-evm' import { getSendTransactionComputedPayload } from '../../../../extension/background-script/EthereumService' import * as database from './database' import * as watcher from './watcher' @@ -51,14 +51,16 @@ export async function getRecentTransactions(chainId: ChainId, address: string): const allSettled = await Promise.allSettled( transactions.map>( async ({ at, hash, hashReplacement, payload, payloadReplacement }) => { + const nonce = getPayloadNonce(payload) const receipt = (await watcher.getReceipt(chainId, hash)) || (await (hashReplacement ? watcher.getReceipt(chainId, hashReplacement) : null)) // if it cannot found receipt, then start the watching progress + // in case the user just refreshed the background page if (!receipt) { - watcher.watchTransaction(chainId, hash) - if (hashReplacement) watcher.watchTransaction(chainId, hashReplacement) + watcher.watchTransaction(chainId, hash, payload) + if (hashReplacement) watcher.watchTransaction(chainId, hashReplacement, payloadReplacement) } return { diff --git a/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts b/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts index c4cd509f363c..af1c09c62fb5 100644 --- a/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts +++ b/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts @@ -1,30 +1,62 @@ +import { first } from 'lodash-unified' import type { TransactionReceipt } from 'web3-core' +import type { JsonRpcPayload } from 'web3-core-helpers' import { WalletMessages } from '@masknet/plugin-wallet' -import { ChainId, TransactionStateType } from '@masknet/web3-shared-evm' +import { getLatestTransactions } from '@masknet/web3-providers' +import { + ChainId, + getExplorerConstants, + isSameAddress, + resolveExplorerAPI, + TransactionStateType, +} from '@masknet/web3-shared-evm' import * as EthereumService from '../../../../extension/background-script/EthereumService' import * as progress from './progress' import * as helpers from './helpers' -import { currentChainIdSettings } from '../../settings' +import { currentAccountSettings, currentChainIdSettings } from '../../settings' +import { WalletRPC } from '../../messages' + +interface TransactionRecord { + at: number + payload?: JsonRpcPayload + receipt: Promise | null +} let timer: NodeJS.Timer | null = null const WATCHED_TRANSACTION_CHECK_DELAY = 15 * 1000 // 15s -const WATCHED_TRANSACTION_MAP = new Map< - ChainId, - Map< - string, - { - at: number - receipt: Promise | null - } - > ->() +const WATCHED_TRANSACTION_MAP = new Map>() const WATCHED_TRANSACTIONS_SIZE = 40 -function getTransactionMap(chainId: ChainId) { +function getMap(chainId: ChainId) { if (!WATCHED_TRANSACTION_MAP.has(chainId)) WATCHED_TRANSACTION_MAP.set(chainId, new Map()) return WATCHED_TRANSACTION_MAP.get(chainId)! } +function getTransaction(chainId: ChainId, hash: string) { + return getMap(chainId).get(hash) +} + +function setTransaction(chainId: ChainId, hash: string, transaction: TransactionRecord) { + getMap(chainId).set(hash, transaction) +} + +function removeTransaction(chainId: ChainId, hash: string) { + getMap(chainId).delete(hash) +} + +function getTransactions(chainId: ChainId) { + const map = getMap(chainId) + return map ? [...map.entries()].sort(([, a], [, z]) => z.at - a.at) : [] +} + +function getWatchedTransactions(chainId: ChainId) { + return getTransactions(chainId).slice(0, WATCHED_TRANSACTIONS_SIZE) +} + +function getUnwatchedTransactions(chainId: ChainId) { + return getTransactions(chainId).slice(WATCHED_TRANSACTIONS_SIZE) +} + async function getTransactionReceipt(chainId: ChainId, hash: string) { try { const transaction = await EthereumService.getTransactionByHash(hash, { @@ -51,51 +83,122 @@ async function getTransactionReceipt(chainId: ChainId, hash: string) { } } -async function checkReceipt() { +async function checkReceipt(chainId: ChainId) { + await Promise.allSettled( + getWatchedTransactions(chainId).map(async ([hash, transaction]) => { + const receipt = await getTransaction(chainId, hash)?.receipt + if (receipt) return + setTransaction(chainId, hash, { + ...transaction, + receipt: getTransactionReceipt(chainId, hash), + }) + }), + ) +} + +async function checkAccount(chainId: ChainId, account: string) { + const API_URL = resolveExplorerAPI(chainId) + const { API_KEYS = [] } = getExplorerConstants(chainId) + + const watchedTransactions = getWatchedTransactions(chainId) + const latestTransactions = await getLatestTransactions(account, API_URL, { + offset: 5, + apikey: first(API_KEYS), + }) + + for (const latestTransaction of latestTransactions) { + const [watchedHash, watchedTransaction] = + watchedTransactions.find(([hash, transaction]) => { + // the transation hash exact matched + if (latestTransaction.hash === hash) return true + + // the transaction signature id exact matched + if (!transaction.payload) return false + if (helpers.getTransactionId(latestTransaction) === helpers.getPayloadId(transaction.payload)) + return true + + // the transaction nonce exact matched + const config = helpers.getPayloadConfig(transaction.payload) + if (!config) return false + return ( + isSameAddress(latestTransaction.from, config.from as string) && + latestTransaction.nonce === config.nonce + ) + }) ?? [] + + if (!watchedHash || !watchedTransaction?.payload) continue + + // replace the original transaction in DB + await WalletRPC.replaceRecentTransaction( + chainId, + account, + watchedHash, + latestTransaction.hash, + watchedTransaction.payload, + ) + + // update receipt in cache + removeTransaction(chainId, watchedHash) + setTransaction(chainId, latestTransaction.hash, { + ...watchedTransaction, + receipt: getTransactionReceipt(chainId, latestTransaction.hash), + }) + } +} + +async function checkTransaction() { if (timer !== null) { clearTimeout(timer) timer = null } const chainId = currentChainIdSettings.value - const map = getTransactionMap(chainId) - const transactions = [...map.entries()].sort(([, a], [, z]) => z.at - a.at) - const watchedTransactions = transactions.slice(0, WATCHED_TRANSACTIONS_SIZE) - const unwatchedTransactions = transactions.slice(WATCHED_TRANSACTIONS_SIZE) - unwatchedTransactions.forEach(([hash]) => unwatchTransaction(chainId, hash)) - - const checkResult = await Promise.allSettled( - watchedTransactions.map(async ([hash, transaction]) => { - const receipt = await map.get(hash)?.receipt - if (receipt) return true - map.set(hash, { - at: transaction.at, - receipt: getTransactionReceipt(chainId, hash), - }) - return false - }), + const account = currentAccountSettings.value + + // unwatch legacy transactions in the map + getUnwatchedTransactions(chainId).forEach(([hash]) => unwatchTransaction(chainId, hash)) + + try { + await checkReceipt(chainId) + await checkAccount(chainId, account) + } catch { + // do nothing + } + + // check if all transaction receipt were loaded + const allSettled = await Promise.allSettled( + getWatchedTransactions(chainId).map(([, transaction]) => transaction.receipt), ) + if (allSettled.every((x) => x.status === 'fulfilled' && x.value)) return - if (checkResult.every((x) => x.status === 'fulfilled' && x.value)) return + // kick to next the round if (timer !== null) clearTimeout(timer) - timer = setTimeout(checkReceipt, WATCHED_TRANSACTION_CHECK_DELAY) + timer = setTimeout(() => { + const chainId = currentChainIdSettings.value + const account = currentAccountSettings.value + checkReceipt(chainId) + checkAccount(chainId, account) + }, WATCHED_TRANSACTION_CHECK_DELAY) } export async function getReceipt(chainId: ChainId, hash: string) { - return getTransactionMap(chainId).get(hash)?.receipt ?? null + return getTransaction(chainId, hash)?.receipt ?? null } -export async function watchTransaction(chainId: ChainId, hash: string) { - const map = getTransactionMap(chainId) - if (!map.has(hash)) { - map.set(hash, { +export async function watchTransaction(chainId: ChainId, hash: string, payload?: JsonRpcPayload) { + const transaction = getTransaction(chainId, hash) + if (!transaction) { + setTransaction(chainId, hash, { at: Date.now(), + payload, receipt: getTransactionReceipt(chainId, hash), }) } - if (timer === null) timer = setTimeout(checkReceipt, WATCHED_TRANSACTION_CHECK_DELAY) + if (timer === null) { + timer = setTimeout(checkTransaction, WATCHED_TRANSACTION_CHECK_DELAY) + } } export function unwatchTransaction(chainId: ChainId, hash: string) { - getTransactionMap(chainId).delete(hash) + removeTransaction(chainId, hash) } diff --git a/packages/web3-constants/evm/explorer.json b/packages/web3-constants/evm/explorer.json index 65da13df75fe..1593d0492081 100644 --- a/packages/web3-constants/evm/explorer.json +++ b/packages/web3-constants/evm/explorer.json @@ -1,5 +1,5 @@ { - "API_KEY": { + "API_KEYS": { "Mainnet": ["26GHYKIAZREN4HWJ5NCICTGKC183IGXY9A"], "Ropsten": [], "Rinkeby": [], diff --git a/packages/web3-providers/src/explorer/helpers.ts b/packages/web3-providers/src/explorer/helpers.ts new file mode 100644 index 000000000000..c244cbf522f2 --- /dev/null +++ b/packages/web3-providers/src/explorer/helpers.ts @@ -0,0 +1,23 @@ +import type { Transaction as Web3Transaction } from 'web3-core' +import type { Transaction } from './types' + +export function toTransaction(transaction: Transaction): Web3Transaction & { + status: '0' | '1' + confirmations: number +} { + return { + status: transaction.txreceipt_status, + blockHash: transaction.blockHash, + blockNumber: Number.parseInt(transaction.blockNumber, 10), + confirmations: Number.parseInt(transaction.confirmations, 10), + from: transaction.from, + to: transaction.to, + gas: Number.parseInt(transaction.gas, 10), + gasPrice: transaction.gasPrice, + hash: transaction.hash, + input: transaction.input, + nonce: Number.parseInt(transaction.nonce, 10), + transactionIndex: Number.parseInt(transaction.transactionIndex, 10), + value: transaction.value, + } +} diff --git a/packages/web3-providers/src/explorer/index.ts b/packages/web3-providers/src/explorer/index.ts new file mode 100644 index 000000000000..66ff627e91b4 --- /dev/null +++ b/packages/web3-providers/src/explorer/index.ts @@ -0,0 +1,31 @@ +import urlcat from 'urlcat' +import { toTransaction } from './helpers' +import type { Transaction } from './types' + +export async function getLatestTransactions( + account: string, + url: string, + { + offset = 10, + apikey, + }: Partial<{ + offset?: number + apikey?: string + }> = {}, +) { + const response = await fetch( + urlcat(url, { + module: 'account', + action: 'txlist', + address: account.toLowerCase(), + startBlock: 0, + endblock: 999999999999, + page: 1, + offset, + sort: 'desc', + apikey, + }), + ) + const rawTransactions = (await response.json()) as Transaction[] + return rawTransactions.map(toTransaction) +} diff --git a/packages/web3-providers/src/explorer/types.ts b/packages/web3-providers/src/explorer/types.ts new file mode 100644 index 000000000000..1e0668b354d9 --- /dev/null +++ b/packages/web3-providers/src/explorer/types.ts @@ -0,0 +1,21 @@ +export interface Transaction { + blockNumber: string + timeStamp: string + hash: string + nonce: string + blockHash: string + transactionIndex: string + from: string + to: string + value: string + gas: string + gasPrice: string + isError: string + errorCode: string + txreceipt_status: '0' | '1' + input: string + contractAddress: string + cumulativeGasUsed: string + gasUsed: string + confirmations: string +} diff --git a/packages/web3-providers/src/index.ts b/packages/web3-providers/src/index.ts index 598e380c7843..9222154cf83c 100644 --- a/packages/web3-providers/src/index.ts +++ b/packages/web3-providers/src/index.ts @@ -1,3 +1,4 @@ export * from './opensea' export * from './coingecko' export * from './rarible' +export * from './explorer' diff --git a/packages/web3-shared/evm/pipes/index.ts b/packages/web3-shared/evm/pipes/index.ts index cb910a60fc0a..fdacc21eb0cf 100644 --- a/packages/web3-shared/evm/pipes/index.ts +++ b/packages/web3-shared/evm/pipes/index.ts @@ -122,7 +122,7 @@ export const resolveExplorerAPI = createLookupTableResolver( [ChainId.Arbitrum_Rinkeby]: '', [ChainId.xDai]: 'https://blockscout.com/xdai/mainnet/api', }, - '' + '', ) export function resolveLinkOnExplorer(chainId: ChainId) { diff --git a/packages/web3-shared/evm/utils/index.ts b/packages/web3-shared/evm/utils/index.ts index adee12f8b27c..fc90c9233703 100644 --- a/packages/web3-shared/evm/utils/index.ts +++ b/packages/web3-shared/evm/utils/index.ts @@ -7,3 +7,4 @@ export * from './number' export * from './chainDetailed' export * from './transaction' export * from './domain' +export * from './payload' diff --git a/packages/web3-shared/evm/utils/payload.ts b/packages/web3-shared/evm/utils/payload.ts new file mode 100644 index 000000000000..6266f39ee82c --- /dev/null +++ b/packages/web3-shared/evm/utils/payload.ts @@ -0,0 +1,58 @@ +import { first } from 'lodash-unified' +import type { JsonRpcPayload, JsonRpcResponse } from 'web3-core-helpers' +import { EthereumMethodType, EthereumTransactionConfig } from '../types' + +export function getPayloadChainId(payload: JsonRpcPayload) { + switch (payload.method) { + // here are methods that contracts may emit + case EthereumMethodType.ETH_CALL: + case EthereumMethodType.ETH_ESTIMATE_GAS: + case EthereumMethodType.ETH_SEND_TRANSACTION: + const config = first(payload.params) as { chainId?: string } | undefined + return typeof config?.chainId === 'string' ? Number.parseInt(config.chainId, 16) || undefined : undefined + default: + return + } +} + +export function getPayloadConfig(payload: JsonRpcPayload) { + switch (payload.method) { + case EthereumMethodType.ETH_SEND_TRANSACTION: { + const [config] = payload.params as [EthereumTransactionConfig] + return config + } + case EthereumMethodType.MASK_REPLACE_TRANSACTION: { + const [, config] = payload.params as [string, EthereumTransactionConfig] + return config + } + default: + 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 +} + +export function getTransactionHash(response?: JsonRpcResponse) { + if (!response) return '' + const hash = response?.result as string | undefined + if (typeof hash !== 'string') return '' + if (!/^0x([\dA-Fa-f]{64})$/.test(hash)) return '' + return hash +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 220f073565ea..325bd50c3217 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -302,6 +302,7 @@ importers: '@masknet/shared-base': workspace:* '@masknet/theme': workspace:* '@masknet/web3-contracts': workspace:* + '@masknet/web3-providers': workspace:* '@masknet/web3-shared-evm': workspace:* '@msgpack/msgpack': ^2.7.1 '@nice-labs/emit-file-webpack-plugin': ^1.1.2 @@ -439,6 +440,7 @@ importers: '@masknet/shared-base': link:../shared-base '@masknet/theme': link:../theme '@masknet/web3-contracts': link:../web3-contracts + '@masknet/web3-providers': link:../web3-providers '@masknet/web3-shared-evm': link:../web3-shared/evm '@msgpack/msgpack': 2.7.1 '@servie/events': 3.0.0 From 0252bb02e9d6acfa587cbe6bf6a6a3713b35736e Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Sun, 12 Dec 2021 01:36:58 +0800 Subject: [PATCH 05/12] fix(ui): the pending title --- .../components/InjectedComponents/ToolboxUnstyled.tsx | 10 ++++++---- .../src/plugins/Wallet/services/transaction/helpers.ts | 4 ++-- .../src/plugins/Wallet/services/transaction/watcher.ts | 9 ++------- packages/web3-providers/src/explorer/index.ts | 8 ++++++-- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/mask/src/components/InjectedComponents/ToolboxUnstyled.tsx b/packages/mask/src/components/InjectedComponents/ToolboxUnstyled.tsx index d310247b4550..3915098e35ed 100644 --- a/packages/mask/src/components/InjectedComponents/ToolboxUnstyled.tsx +++ b/packages/mask/src/components/InjectedComponents/ToolboxUnstyled.tsx @@ -40,8 +40,10 @@ import { SetupGuideStep } from './SetupGuide' import stringify from 'json-stable-stringify' const useStyles = makeStyles()((theme) => ({ - font: { + title: { color: theme.palette.mode === 'dark' ? theme.palette.text.primary : 'rgb(15, 20, 25)', + display: 'flex', + alignItems: 'center', }, paper: { borderRadius: 4, @@ -160,7 +162,7 @@ export function ToolboxHintUnstyled(props: ToolboxHintProps) { justifyContent: 'space-between', alignItems: 'center', }}> - {title} + {title} {shouldDisplayChainIndicator ? ( - + {t('plugin_wallet_pending_transactions', { count: pendingTransactions.length, })} - + ) } diff --git a/packages/mask/src/plugins/Wallet/services/transaction/helpers.ts b/packages/mask/src/plugins/Wallet/services/transaction/helpers.ts index 4354f7c29161..33c9f9547863 100644 --- a/packages/mask/src/plugins/Wallet/services/transaction/helpers.ts +++ b/packages/mask/src/plugins/Wallet/services/transaction/helpers.ts @@ -1,4 +1,4 @@ -import { sha3 } from 'web3-utils' +import { sha3, toHex } from 'web3-utils' import type { Transaction, TransactionConfig, TransactionReceipt } from 'web3-core' import type { JsonRpcPayload } from 'web3-core-helpers' import { @@ -43,7 +43,7 @@ export function getPayloadId(payload: JsonRpcPayload) { export function getTransactionId(transaction: Transaction | null) { if (!transaction) return '' const { from, to, input, value } = transaction - return sha3([from, to, input || '0x0', value || '0x0'].join('_')) ?? '' + return sha3([from, to, input, toHex(value)].join('_')) ?? '' } export function getReceiptStatus(receipt: TransactionReceipt | null) { diff --git a/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts b/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts index af1c09c62fb5..f4a2a9c5a00f 100644 --- a/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts +++ b/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts @@ -161,7 +161,7 @@ async function checkTransaction() { try { await checkReceipt(chainId) await checkAccount(chainId, account) - } catch { + } catch (error) { // do nothing } @@ -173,12 +173,7 @@ async function checkTransaction() { // kick to next the round if (timer !== null) clearTimeout(timer) - timer = setTimeout(() => { - const chainId = currentChainIdSettings.value - const account = currentAccountSettings.value - checkReceipt(chainId) - checkAccount(chainId, account) - }, WATCHED_TRANSACTION_CHECK_DELAY) + timer = setTimeout(checkTransaction, WATCHED_TRANSACTION_CHECK_DELAY) } export async function getReceipt(chainId: ChainId, hash: string) { diff --git a/packages/web3-providers/src/explorer/index.ts b/packages/web3-providers/src/explorer/index.ts index 66ff627e91b4..dd12a881a031 100644 --- a/packages/web3-providers/src/explorer/index.ts +++ b/packages/web3-providers/src/explorer/index.ts @@ -26,6 +26,10 @@ export async function getLatestTransactions( apikey, }), ) - const rawTransactions = (await response.json()) as Transaction[] - return rawTransactions.map(toTransaction) + const rawTransactions = (await response.json()) as { + message: string + result?: Transaction[] + status: '0' | '1' + } + return rawTransactions.result?.map(toTransaction) ?? [] } From e08ff9bc14b01255a8229c6e552d31202d052b74 Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Sun, 12 Dec 2021 19:26:04 +0800 Subject: [PATCH 06/12] refactor: tx error messgae --- packages/mask/shared-ui/locales/en-US.json | 1 + .../EthereumServices/error.ts | 49 ++++++++++++++++--- .../Wallet/SNSAdaptor/TransactionDialog.tsx | 16 +----- packages/mask/src/plugins/Wallet/constants.ts | 2 +- packages/plugins/Wallet/src/constants.ts | 5 +- 5 files changed, 49 insertions(+), 24 deletions(-) diff --git a/packages/mask/shared-ui/locales/en-US.json b/packages/mask/shared-ui/locales/en-US.json index 4cbe53f66cb5..0d475a50c9b8 100644 --- a/packages/mask/shared-ui/locales/en-US.json +++ b/packages/mask/shared-ui/locales/en-US.json @@ -276,6 +276,7 @@ "plugin_wallet_transaction_confirmed": "Your transaction was confirmed!", "plugin_wallet_transaction_reverted": "Transaction was reverted!", "plugin_wallet_transaction_rejected": "Transaction was rejected!", + "plugin_wallet_transaction_underpriced": "Transaction underpriced.", "plugin_wallet_transaction_server_error": "Transaction was failed due to an internal JSON-RPC server error.", "plugin_wallet_view_on_explorer": "View on Explorer", "plugin_ito_placeholder_when_token_unselected": "Please Select a Token first", diff --git a/packages/mask/src/extension/background-script/EthereumServices/error.ts b/packages/mask/src/extension/background-script/EthereumServices/error.ts index f2d93dfd7f9b..f735f8c7cf3c 100644 --- a/packages/mask/src/extension/background-script/EthereumServices/error.ts +++ b/packages/mask/src/extension/background-script/EthereumServices/error.ts @@ -1,16 +1,51 @@ import { isNil } from 'lodash-unified' import type { JsonRpcResponse } from 'web3-core-helpers' +import { JSON_RPC_ERROR_CODE } from '@masknet/plugin-wallet' +import { i18n } from '../../../../shared-ui/locales_legacy' + +function getInternalError(error: unknown, response?: JsonRpcResponse | null, fallback?: string): Error { + { + const rpcError = error + if (rpcError instanceof Error && rpcError.message) return rpcError + if (rpcError && typeof (rpcError as Error).message === 'string') return new Error((rpcError as Error).message) + if (rpcError && typeof rpcError === 'string') return new Error(rpcError) + } + + { + const responseError = response?.error as unknown + if (responseError instanceof Error) return getError(responseError, null, fallback) + if (responseError && typeof (responseError as Error).message === 'string') + return getError(responseError, null, fallback) + if (responseError && typeof responseError === 'string') return new Error(responseError) + } + if (fallback) return new Error(fallback) + return new Error('Unknown Error.') +} export function hasError(error: unknown, response?: JsonRpcResponse | null) { return !isNil(error) || !isNil(response?.error) } export function getError(error: unknown, response?: JsonRpcResponse | null, fallback?: string): Error { - if (error instanceof Error && error.message) return error - if (typeof error === 'string' && error) return new Error(error) - const responseError = response?.error as unknown - if (responseError instanceof Error) return getError(responseError, null, fallback) - if (typeof response?.error === 'string' && response?.error) return new Error(response.error) - if (fallback) return new Error(fallback) - return new Error('Unknown Error.') + const internalError = getInternalError(error, response, fallback) + const internalErrorMessage = (() => { + const { code, message } = internalError as unknown as { code?: number; message: string } + + if (message.includes(`"code":${JSON_RPC_ERROR_CODE.INTERNAL_ERROR}`)) + return i18n.t('plugin_wallet_transaction_server_error') + if (message.includes('User denied message signature.')) return i18n.t('plugin_wallet_cancel_sign') + if (message.includes('User denied transaction signature.')) return i18n.t('plugin_wallet_transaction_rejected') + if (message.includes('transaction underpriced')) return i18n.t('plugin_wallet_transaction_underpriced') + if ( + typeof code === 'number' && + (code === JSON_RPC_ERROR_CODE.INTERNAL_ERROR || + (code <= JSON_RPC_ERROR_CODE.SERVER_ERROR_RANGE_START && + code >= JSON_RPC_ERROR_CODE.SERVER_ERROR_RANGE_END)) + ) { + return i18n.t('plugin_wallet_transaction_server_error') + } + return internalError.message + })() + + return new Error(internalErrorMessage) } diff --git a/packages/mask/src/plugins/Wallet/SNSAdaptor/TransactionDialog.tsx b/packages/mask/src/plugins/Wallet/SNSAdaptor/TransactionDialog.tsx index 7033ba25d10e..dfa367f5a01a 100644 --- a/packages/mask/src/plugins/Wallet/SNSAdaptor/TransactionDialog.tsx +++ b/packages/mask/src/plugins/Wallet/SNSAdaptor/TransactionDialog.tsx @@ -13,7 +13,6 @@ import { useI18N } from '../../../utils' import { useRemoteControlledDialog } from '@masknet/shared' import { InjectedDialog } from '../../../components/shared/InjectedDialog' import { WalletMessages } from '../messages' -import { JSON_RPC_ErrorCode } from '../constants' const useStyles = makeStyles()((theme) => ({ content: { @@ -129,20 +128,7 @@ function TransactionDialogUI(props: TransactionDialogUIProps) { <> - { - // it is trick, log(e) print {"code": 4001 ...}, log(e.code) print -1 - state.error.message === 'MetaMask Message Signature: User denied message signature.' - ? t('plugin_wallet_cancel_sign') - : state.error.message.includes('User denied transaction signature.') - ? t('plugin_wallet_transaction_rejected') - : state.error.code === JSON_RPC_ErrorCode.INTERNAL_ERROR || - state.error.message.includes(`"code":${JSON_RPC_ErrorCode.INTERNAL_ERROR}`) || - (state.error.code && - state.error.code <= JSON_RPC_ErrorCode.SERVER_ERROR_RANGE_START && - state.error.code >= JSON_RPC_ErrorCode.SERVER_ERROR_RANGE_END) - ? t('plugin_wallet_transaction_server_error') - : state.error.message - } + {state.error.message} ) : null} diff --git a/packages/mask/src/plugins/Wallet/constants.ts b/packages/mask/src/plugins/Wallet/constants.ts index e59e391cff0e..98aeccfc745d 100644 --- a/packages/mask/src/plugins/Wallet/constants.ts +++ b/packages/mask/src/plugins/Wallet/constants.ts @@ -1,7 +1,7 @@ export { PLUGIN_IDENTIFIER, HD_PATH_WITHOUT_INDEX_ETHEREUM, - JSON_RPC_ErrorCode, + JSON_RPC_ERROR_CODE as JSON_RPC_ErrorCode, UPDATE_CHAIN_STATE_DELAY, } from '@masknet/plugin-wallet' diff --git a/packages/plugins/Wallet/src/constants.ts b/packages/plugins/Wallet/src/constants.ts index f34029cb0a9e..6f13ded8c212 100644 --- a/packages/plugins/Wallet/src/constants.ts +++ b/packages/plugins/Wallet/src/constants.ts @@ -11,7 +11,10 @@ export const HD_PATH_WITHOUT_INDEX_ETHEREUM = "m/44'/60'/0'/0" export const MAX_DERIVE_COUNT = 99 // https://www.jsonrpc.org/specification#error_object -export enum JSON_RPC_ErrorCode { +export enum JSON_RPC_ERROR_CODE { + INVALID_REQUEST = -32600, + METHOD_NOT_FOUND = 32601, + INVALID_PARAMS = -32602, INTERNAL_ERROR = -32603, SERVER_ERROR_RANGE_START = -32000, SERVER_ERROR_RANGE_END = -32099, From 8aa5243ce9a6643b916379dd1df370a73c1da768 Mon Sep 17 00:00:00 2001 From: guanbinrui <52657989+guanbinrui@users.noreply.github.com> Date: Mon, 13 Dec 2021 10:14:23 +0800 Subject: [PATCH 07/12] fix: transaction timeout (#5166) * refactor: introduce tx timeout state * refactor: code style & limits --- .../SNSAdaptor/TransactionSnackbar/index.tsx | 2 +- .../Wallet/services/transaction/helpers.ts | 12 +- .../Wallet/services/transaction/index.ts | 6 +- .../Wallet/services/transaction/watcher.ts | 152 +++++++++++------- 4 files changed, 109 insertions(+), 63 deletions(-) diff --git a/packages/mask/src/plugins/Wallet/SNSAdaptor/TransactionSnackbar/index.tsx b/packages/mask/src/plugins/Wallet/SNSAdaptor/TransactionSnackbar/index.tsx index 51aba9c5ad62..5c6349ab38a1 100644 --- a/packages/mask/src/plugins/Wallet/SNSAdaptor/TransactionSnackbar/index.tsx +++ b/packages/mask/src/plugins/Wallet/SNSAdaptor/TransactionSnackbar/index.tsx @@ -127,7 +127,7 @@ export function TransactionSnackbar() { if (progress.state.type === TransactionStateType.FAILED) { showSingletonSnackbar('Swap Token', { ...config, - ...{ message: getFullMessage('Transaction rejected', hash) }, + ...{ message: getFullMessage('Transaction failed', hash) }, }) return } diff --git a/packages/mask/src/plugins/Wallet/services/transaction/helpers.ts b/packages/mask/src/plugins/Wallet/services/transaction/helpers.ts index 6afee26c9dad..c8999e89bcda 100644 --- a/packages/mask/src/plugins/Wallet/services/transaction/helpers.ts +++ b/packages/mask/src/plugins/Wallet/services/transaction/helpers.ts @@ -26,11 +26,9 @@ export function toReceipt(status: '0' | '1', transaction: Transaction): Transact } } -// the payload that derives from transaction only for generating transaction signature export function toPayload(transaction: Transaction): JsonRpcPayload { return { jsonrpc: '2.0', - // the payload id is not related to the transaction signature id: '0', method: EthereumMethodType.ETH_SEND_TRANSACTION, params: [ @@ -61,6 +59,16 @@ export function getPayloadId(payload: JsonRpcPayload) { return sha3([from, to, data, value].join('_')) ?? '' } +export function getPayloadFrom(payload: JsonRpcPayload) { + const config = getPayloadConfig(payload) + return config?.from as string | undefined +} + +export function getPayloadTo(payload: JsonRpcPayload) { + const config = getPayloadConfig(payload) + return config?.to as string | undefined +} + export function getTransactionId(transaction: Transaction | null) { if (!transaction) return '' const { from, to, input, value } = transaction diff --git a/packages/mask/src/plugins/Wallet/services/transaction/index.ts b/packages/mask/src/plugins/Wallet/services/transaction/index.ts index 4c94f8c464b5..70373d5171e9 100644 --- a/packages/mask/src/plugins/Wallet/services/transaction/index.ts +++ b/packages/mask/src/plugins/Wallet/services/transaction/index.ts @@ -1,6 +1,6 @@ import type { TransactionReceipt } from 'web3-core' import type { JsonRpcPayload } from 'web3-core-helpers' -import { ChainId, getPayloadNonce, TransactionStatusType } from '@masknet/web3-shared-evm' +import type { ChainId, TransactionStatusType } from '@masknet/web3-shared-evm' import { getSendTransactionComputedPayload } from '../../../../extension/background-script/EthereumService' import * as database from './database' import * as watcher from './watcher' @@ -51,7 +51,6 @@ export async function getRecentTransactions(chainId: ChainId, address: string): const allSettled = await Promise.allSettled( transactions.map>( async ({ at, hash, hashReplacement, payload, payloadReplacement }) => { - const nonce = getPayloadNonce(payload) const receipt = (await watcher.getReceipt(chainId, hash)) || (await (hashReplacement ? watcher.getReceipt(chainId, hashReplacement) : null)) @@ -60,7 +59,8 @@ export async function getRecentTransactions(chainId: ChainId, address: string): // in case the user just refreshed the background page if (!receipt) { watcher.watchTransaction(chainId, hash, payload) - if (hashReplacement) watcher.watchTransaction(chainId, hashReplacement, payloadReplacement) + if (hashReplacement && payloadReplacement) + watcher.watchTransaction(chainId, hashReplacement, payloadReplacement) } return { diff --git a/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts b/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts index 79edbc7dcbf7..fc3a5f450acc 100644 --- a/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts +++ b/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts @@ -13,50 +13,74 @@ import { import * as EthereumService from '../../../../extension/background-script/EthereumService' import * as progress from './progress' import * as helpers from './helpers' -import { currentAccountSettings, currentChainIdSettings } from '../../settings' +import { currentChainIdSettings } from '../../settings' import { WalletRPC } from '../../messages' -interface TransactionRecord { +interface StorageItem { at: number - payload?: JsonRpcPayload + limits: number + payload: JsonRpcPayload receipt: Promise | null } -let timer: NodeJS.Timer | null = null -const WATCHED_TRANSACTION_CHECK_DELAY = 15 * 1000 // 15s -const WATCHED_TRANSACTION_MAP = new Map>() -const WATCHED_TRANSACTIONS_SIZE = 40 +class Storage { + static SIZE = 40 -function getMap(chainId: ChainId) { - if (!WATCHED_TRANSACTION_MAP.has(chainId)) WATCHED_TRANSACTION_MAP.set(chainId, new Map()) - return WATCHED_TRANSACTION_MAP.get(chainId)! -} + private map = new Map>() -function getTransaction(chainId: ChainId, hash: string) { - return getMap(chainId).get(hash) -} + private getStorage(chainId: ChainId) { + if (!this.map.has(chainId)) this.map.set(chainId, new Map()) + return this.map.get(chainId)! + } -function setTransaction(chainId: ChainId, hash: string, transaction: TransactionRecord) { - getMap(chainId).set(hash, transaction) -} + public hasItem(chainId: ChainId, hash: string) { + return this.getStorage(chainId).has(hash) + } -function removeTransaction(chainId: ChainId, hash: string) { - getMap(chainId).delete(hash) -} + public getItem(chainId: ChainId, hash: string) { + return this.getStorage(chainId).get(hash) + } -function getTransactions(chainId: ChainId) { - const map = getMap(chainId) - return map ? [...map.entries()].sort(([, a], [, z]) => z.at - a.at) : [] -} + public setItem(chainId: ChainId, hash: string, transaction: StorageItem) { + this.getStorage(chainId).set(hash, transaction) + } -function getWatchedTransactions(chainId: ChainId) { - return getTransactions(chainId).slice(0, WATCHED_TRANSACTIONS_SIZE) -} + public removeItem(chainId: ChainId, hash: string) { + this.getStorage(chainId).delete(hash) + } + + public getItems(chainId: ChainId) { + const map = this.getStorage(chainId) + return map ? [...map.entries()].sort(([, a], [, z]) => z.at - a.at) : [] + } + + public getWatched(chainId: ChainId) { + return this.getItems(chainId).slice(0, Storage.SIZE) + } + + public getUnwatched(chainId: ChainId) { + return this.getItems(chainId).slice(Storage.SIZE) + } -function getUnwatchedTransactions(chainId: ChainId) { - return getTransactions(chainId).slice(WATCHED_TRANSACTIONS_SIZE) + public getWatchedAccounts(chainId: ChainId) { + return this.getWatched(chainId) + .map(([_, transaction]) => helpers.getPayloadFrom(transaction.payload)) + .filter(Boolean) as string[] + } + + public getUnwatchedAccounts(chainId: ChainId) { + return this.getUnwatched(chainId) + .map(([_, transaction]) => helpers.getPayloadFrom(transaction.payload)) + .filter(Boolean) as string[] + } } +let timer: NodeJS.Timer | null = null +const storage = new Storage() +const CHECK_TIMES = 30 +const CHECK_DELAY = 30 * 1000 // seconds +const CHECK_LATEST_SIZE = 5 + async function getTransactionReceipt(chainId: ChainId, hash: string) { try { const transaction = await EthereumService.getTransactionByHash(hash, { @@ -85,10 +109,10 @@ async function getTransactionReceipt(chainId: ChainId, hash: string) { async function checkReceipt(chainId: ChainId) { await Promise.allSettled( - getWatchedTransactions(chainId).map(async ([hash, transaction]) => { - const receipt = await getTransaction(chainId, hash)?.receipt + storage.getWatched(chainId).map(async ([hash, transaction]) => { + const receipt = await storage.getItem(chainId, hash)?.receipt if (receipt) return - setTransaction(chainId, hash, { + storage.setItem(chainId, hash, { ...transaction, receipt: getTransactionReceipt(chainId, hash), }) @@ -100,9 +124,9 @@ async function checkAccount(chainId: ChainId, account: string) { const API_URL = resolveExplorerAPI(chainId) const { API_KEYS = [] } = getExplorerConstants(chainId) - const watchedTransactions = getWatchedTransactions(chainId) + const watchedTransactions = storage.getWatched(chainId) const latestTransactions = await getLatestTransactions(account, API_URL, { - offset: 5, + offset: CHECK_LATEST_SIZE, apikey: first(API_KEYS), }) @@ -138,8 +162,8 @@ async function checkAccount(chainId: ChainId, account: string) { ) // update receipt in cache - removeTransaction(chainId, watchedHash) - setTransaction(chainId, latestTransaction.hash, { + storage.removeItem(chainId, watchedHash) + storage.setItem(chainId, latestTransaction.hash, { ...watchedTransaction, payload: helpers.toPayload(latestTransaction), receipt: getTransactionReceipt(chainId, latestTransaction.hash), @@ -147,54 +171,68 @@ async function checkAccount(chainId: ChainId, account: string) { } } -async function checkTransaction() { - if (timer !== null) { - clearTimeout(timer) - timer = null - } +async function check() { + // stop any pending task + stopCheck() const chainId = currentChainIdSettings.value - const account = currentAccountSettings.value - // unwatch legacy transactions in the map - getUnwatchedTransactions(chainId).forEach(([hash]) => unwatchTransaction(chainId, hash)) + // unwatch legacy transactions + storage.getUnwatched(chainId).forEach(([hash]) => unwatchTransaction(chainId, hash)) + + // update limits + storage.getWatched(chainId).forEach(([hash, transaction]) => { + storage.setItem(chainId, hash, { + ...transaction, + limits: Math.max(0, transaction.limits - 1), + }) + }) try { await checkReceipt(chainId) - await checkAccount(chainId, account) + for (const account of storage.getWatchedAccounts(chainId)) await checkAccount(chainId, account) } catch (error) { // do nothing } // check if all transaction receipts were found const allSettled = await Promise.allSettled( - getWatchedTransactions(chainId).map(([, transaction]) => transaction.receipt), + storage.getWatched(chainId).map(([, transaction]) => transaction.receipt), ) if (allSettled.every((x) => x.status === 'fulfilled' && x.value)) return // kick to the next round + startCheck(true) +} + +function startCheck(force: boolean) { + if (force) stopCheck() + if (timer === null) { + timer = setTimeout(check, CHECK_DELAY) + } +} + +function stopCheck() { if (timer !== null) clearTimeout(timer) - timer = setTimeout(checkTransaction, WATCHED_TRANSACTION_CHECK_DELAY) + timer = null } export async function getReceipt(chainId: ChainId, hash: string) { - return getTransaction(chainId, hash)?.receipt ?? null + return storage.getItem(chainId, hash)?.receipt ?? null } -export async function watchTransaction(chainId: ChainId, hash: string, payload?: JsonRpcPayload) { - const transaction = getTransaction(chainId, hash) - if (!transaction) { - setTransaction(chainId, hash, { +export async function watchTransaction(chainId: ChainId, hash: string, payload: JsonRpcPayload) { + if (!storage.hasItem(chainId, hash)) { + storage.setItem(chainId, hash, { at: Date.now(), payload, - receipt: getTransactionReceipt(chainId, hash), + limits: CHECK_TIMES, + receipt: Promise.resolve(null), }) } - if (timer === null) { - timer = setTimeout(checkTransaction, WATCHED_TRANSACTION_CHECK_DELAY) - } + startCheck(false) } export function unwatchTransaction(chainId: ChainId, hash: string) { - removeTransaction(chainId, hash) + storage.removeItem(chainId, hash) } From 6d685036a1e09edf2d1538ccd2197b8bb5066b96 Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Tue, 14 Dec 2021 23:52:57 +0800 Subject: [PATCH 08/12] fix: get receipt even transaction was replced --- .../dashboard/src/components/CreateWalletFrame/index.tsx | 3 +-- .../dashboard/src/components/LoadingPlaceholder/index.tsx | 3 +-- .../CreateMaskWallet/components/CreateMnemonic/index.tsx | 3 +-- .../src/pages/Personas/components/AddPersonaCard/index.tsx | 3 +-- .../src/pages/Personas/components/ContactsTable/index.tsx | 3 +-- .../Wallets/components/CollectiblePlaceHolder/index.tsx | 3 +-- .../extension/background-script/EthereumServices/send.ts | 6 ++++-- .../popups/pages/Wallet/components/ActivityList/index.tsx | 5 ++++- packages/mask/src/plugins/Wallet/constants.ts | 2 +- .../src/plugins/Wallet/services/transaction/database.ts | 4 +++- .../mask/src/plugins/Wallet/services/transaction/helpers.ts | 2 +- .../mask/src/plugins/Wallet/services/transaction/index.ts | 4 +++- 12 files changed, 22 insertions(+), 19 deletions(-) diff --git a/packages/dashboard/src/components/CreateWalletFrame/index.tsx b/packages/dashboard/src/components/CreateWalletFrame/index.tsx index 5aa79be11dec..7d686c863ae9 100644 --- a/packages/dashboard/src/components/CreateWalletFrame/index.tsx +++ b/packages/dashboard/src/components/CreateWalletFrame/index.tsx @@ -1,6 +1,5 @@ import { memo } from 'react' -import { makeStyles } from '@masknet/theme' -import { MaskColorVar } from '@masknet/theme' +import { makeStyles, MaskColorVar } from '@masknet/theme' const useStyles = makeStyles()({ root: { diff --git a/packages/dashboard/src/components/LoadingPlaceholder/index.tsx b/packages/dashboard/src/components/LoadingPlaceholder/index.tsx index bfddb50e3586..035ad4cac8be 100644 --- a/packages/dashboard/src/components/LoadingPlaceholder/index.tsx +++ b/packages/dashboard/src/components/LoadingPlaceholder/index.tsx @@ -1,7 +1,6 @@ import { memo } from 'react' import { Box, Typography } from '@mui/material' -import { makeStyles } from '@masknet/theme' -import { MaskColorVar } from '@masknet/theme' +import { makeStyles, MaskColorVar } from '@masknet/theme' import { LoadingAnimation } from '@masknet/shared' const useStyles = makeStyles()((theme) => ({ diff --git a/packages/dashboard/src/pages/CreateMaskWallet/components/CreateMnemonic/index.tsx b/packages/dashboard/src/pages/CreateMaskWallet/components/CreateMnemonic/index.tsx index 5e3b7d915015..806cde00905a 100644 --- a/packages/dashboard/src/pages/CreateMaskWallet/components/CreateMnemonic/index.tsx +++ b/packages/dashboard/src/pages/CreateMaskWallet/components/CreateMnemonic/index.tsx @@ -1,7 +1,6 @@ import { memo, useCallback, useEffect, useState } from 'react' import { Alert, Box, Button, Typography } from '@mui/material' -import { makeStyles } from '@masknet/theme' -import { MaskColorVar } from '@masknet/theme' +import { makeStyles, MaskColorVar } from '@masknet/theme' import { InfoIcon, RefreshIcon } from '@masknet/icons' import { useDashboardI18N } from '../../../../locales' import { ChainId, ProviderType } from '@masknet/web3-shared-evm' diff --git a/packages/dashboard/src/pages/Personas/components/AddPersonaCard/index.tsx b/packages/dashboard/src/pages/Personas/components/AddPersonaCard/index.tsx index 1281137bf048..5114be6598d4 100644 --- a/packages/dashboard/src/pages/Personas/components/AddPersonaCard/index.tsx +++ b/packages/dashboard/src/pages/Personas/components/AddPersonaCard/index.tsx @@ -1,7 +1,6 @@ import { memo, useState } from 'react' import { Button, TextField } from '@mui/material' -import { makeStyles } from '@masknet/theme' -import { MaskColorVar } from '@masknet/theme' +import { makeStyles, MaskColorVar } from '@masknet/theme' import { useDashboardI18N } from '../../../../locales' import { isPersonaNameLengthValid, PERSONA_NAME_MAX_LENGTH } from '../../../../utils/checkLengthExceed' diff --git a/packages/dashboard/src/pages/Personas/components/ContactsTable/index.tsx b/packages/dashboard/src/pages/Personas/components/ContactsTable/index.tsx index f43ba6c14e78..aec0eb4c26ec 100644 --- a/packages/dashboard/src/pages/Personas/components/ContactsTable/index.tsx +++ b/packages/dashboard/src/pages/Personas/components/ContactsTable/index.tsx @@ -2,8 +2,7 @@ import { Dispatch, memo, SetStateAction, useEffect, useMemo, useState } from 're import { useContacts } from '../../hooks/useContacts' import type { RelationProfile } from '@masknet/shared' import { TableContainer, Box, TablePagination, Stack, Table, TableBody } from '@mui/material' -import { makeStyles } from '@masknet/theme' -import { MaskColorVar } from '@masknet/theme' +import { makeStyles, MaskColorVar } from '@masknet/theme' import { EmptyContactPlaceholder } from '../EmptyContactPlaceholder' import { LoadingPlaceholder } from '../../../../components/LoadingPlaceholder' import { sortBy } from 'lodash-unified' diff --git a/packages/dashboard/src/pages/Wallets/components/CollectiblePlaceHolder/index.tsx b/packages/dashboard/src/pages/Wallets/components/CollectiblePlaceHolder/index.tsx index 087b572dda82..99430c67191f 100644 --- a/packages/dashboard/src/pages/Wallets/components/CollectiblePlaceHolder/index.tsx +++ b/packages/dashboard/src/pages/Wallets/components/CollectiblePlaceHolder/index.tsx @@ -1,7 +1,6 @@ import { memo } from 'react' import { MiniMaskIcon } from '@masknet/icons' -import { makeStyles } from '@masknet/theme' -import { MaskColorVar } from '@masknet/theme' +import { makeStyles, MaskColorVar } from '@masknet/theme' import { useDashboardI18N } from '../../../../locales' import { WalletIcon } from '@masknet/shared' import { Box } from '@mui/material' diff --git a/packages/mask/src/extension/background-script/EthereumServices/send.ts b/packages/mask/src/extension/background-script/EthereumServices/send.ts index ab5405bd043a..95a4dc44f472 100644 --- a/packages/mask/src/extension/background-script/EthereumServices/send.ts +++ b/packages/mask/src/extension/background-script/EthereumServices/send.ts @@ -396,12 +396,14 @@ export async function INTERNAL_send( async function getTransactionReceipt() { const [hash] = payload.params as [string] + // redirect receipt queries to tx watcher + const transaction = await WalletRPC.getRecentTransaction(chainIdFinally, account, hash) + try { callback(null, { id: payload.id, jsonrpc: payload.jsonrpc, - // redirect receipt queries to tx watcher - result: await WalletRPC.getReceipt(chainIdFinally, hash), + result: transaction?.receipt ?? null, } as JsonRpcResponse) } catch { callback(null, { diff --git a/packages/mask/src/extension/popups/pages/Wallet/components/ActivityList/index.tsx b/packages/mask/src/extension/popups/pages/Wallet/components/ActivityList/index.tsx index 1a51c04a66af..18a887e1567b 100644 --- a/packages/mask/src/extension/popups/pages/Wallet/components/ActivityList/index.tsx +++ b/packages/mask/src/extension/popups/pages/Wallet/components/ActivityList/index.tsx @@ -140,7 +140,10 @@ export const ActivityListUI = memo(({ dataSource, chainId } const toAddress = getToAddress(transaction.receipt, transaction.computedPayload) return ( Date: Wed, 15 Dec 2021 11:45:48 +0800 Subject: [PATCH 09/12] refactor: no need to update cache if hash exact matched --- .../mask/src/plugins/Wallet/services/transaction/watcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts b/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts index fc3a5f450acc..ce27e8f68f58 100644 --- a/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts +++ b/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts @@ -150,7 +150,7 @@ async function checkAccount(chainId: ChainId, account: string) { ) }) ?? [] - if (!watchedHash || !watchedTransaction?.payload) continue + if (!watchedHash || !watchedTransaction?.payload || watchedHash !== latestTransaction.hash) continue // replace the original transaction in DB await WalletRPC.replaceRecentTransaction( From 63aae0259799e4a4793372bad505128e70df6ba9 Mon Sep 17 00:00:00 2001 From: guanbinrui <52657989+guanbinrui@users.noreply.github.com> Date: Wed, 15 Dec 2021 13:38:04 +0800 Subject: [PATCH 10/12] fix: the wrong logic Co-authored-by: Hancheng Zhou --- .../mask/src/plugins/Wallet/services/transaction/watcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts b/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts index ce27e8f68f58..01eeaa765ce0 100644 --- a/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts +++ b/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts @@ -150,7 +150,7 @@ async function checkAccount(chainId: ChainId, account: string) { ) }) ?? [] - if (!watchedHash || !watchedTransaction?.payload || watchedHash !== latestTransaction.hash) continue + if (!watchedHash || !watchedTransaction?.payload || watchedHash === latestTransaction.hash) continue // replace the original transaction in DB await WalletRPC.replaceRecentTransaction( From 20e113d25e381c56f0c1a1b0583cbbef77680e5e Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Mon, 20 Dec 2021 10:43:38 +0800 Subject: [PATCH 11/12] refactor: more fallback --- .../mask/src/plugins/Wallet/services/transaction/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mask/src/plugins/Wallet/services/transaction/helpers.ts b/packages/mask/src/plugins/Wallet/services/transaction/helpers.ts index 54ff87656d94..be93bd0dafe9 100644 --- a/packages/mask/src/plugins/Wallet/services/transaction/helpers.ts +++ b/packages/mask/src/plugins/Wallet/services/transaction/helpers.ts @@ -72,7 +72,7 @@ export function getPayloadId(payload: JsonRpcPayload) { export function getTransactionId(transaction: Transaction | null) { if (!transaction) return '' const { from, to, input, value } = transaction - return sha3([from, to, input || '0x0', toHex(value) || '0x0'].join('_')) ?? '' + return sha3([from, to, input || '0x0', toHex(value || '0x0') || '0x0'].join('_')) ?? '' } export function getReceiptStatus(receipt: TransactionReceipt | null) { From c969af6ac125a7385580e1c029b42664ef931976 Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Wed, 29 Dec 2021 16:11:04 +0800 Subject: [PATCH 12/12] fix: lint error --- .../background-script/EthereumServices/send.ts | 1 - .../plugins/Wallet/services/transaction/watcher.ts | 13 ++++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/mask/src/extension/background-script/EthereumServices/send.ts b/packages/mask/src/extension/background-script/EthereumServices/send.ts index 844041e34666..d1488a3750e9 100644 --- a/packages/mask/src/extension/background-script/EthereumServices/send.ts +++ b/packages/mask/src/extension/background-script/EthereumServices/send.ts @@ -9,7 +9,6 @@ import { EthereumErrorType, EthereumMethodType, EthereumRpcType, - EthereumTransactionConfig, isEIP1559Supported, isSameAddress, ProviderType, diff --git a/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts b/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts index d1cce07ad7fc..9fcc17b748d4 100644 --- a/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts +++ b/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts @@ -3,12 +3,7 @@ import type { TransactionReceipt } from 'web3-core' import type { JsonRpcPayload } from 'web3-core-helpers' import { WalletMessages } from '@masknet/plugin-wallet' import { Explorer } from '@masknet/web3-providers' -import { - ChainId, - getExplorerConstants, - isSameAddress, - TransactionStateType, -} from '@masknet/web3-shared-evm' +import { ChainId, getExplorerConstants, isSameAddress, TransactionStateType } from '@masknet/web3-shared-evm' import * as EthereumService from '../../../../extension/background-script/EthereumService' import * as progress from './progress' import * as helpers from './helpers' @@ -23,7 +18,7 @@ interface StorageItem { } class Storage { - static SIZE = 40 + static MAX_ITEM_SIZE = 40 private map = new Map>() @@ -54,11 +49,11 @@ class Storage { } public getWatched(chainId: ChainId) { - return this.getItems(chainId).slice(0, Storage.SIZE) + return this.getItems(chainId).slice(0, Storage.MAX_ITEM_SIZE) } public getUnwatched(chainId: ChainId) { - return this.getItems(chainId).slice(Storage.SIZE) + return this.getItems(chainId).slice(Storage.MAX_ITEM_SIZE) } public getWatchedAccounts(chainId: ChainId) {