diff --git a/cspell.json b/cspell.json
index d22fffdca815..979bbbada9a3 100644
--- a/cspell.json
+++ b/cspell.json
@@ -358,7 +358,8 @@
"walletlink",
"WNATIVE",
"xdescribe",
- "xtest"
+ "xtest",
+ "txreceipt"
],
"ignoreRegExpList": ["/@servie/"],
"overrides": [
diff --git a/packages/mask/shared-ui/locales/en-US.json b/packages/mask/shared-ui/locales/en-US.json
index 097bd8385303..30c5c46365ab 100644
--- a/packages/mask/shared-ui/locales/en-US.json
+++ b/packages/mask/shared-ui/locales/en-US.json
@@ -282,6 +282,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/components/InjectedComponents/ToolboxUnstyled.tsx b/packages/mask/src/components/InjectedComponents/ToolboxUnstyled.tsx
index 399897911df2..dd73b57afd5d 100644
--- a/packages/mask/src/components/InjectedComponents/ToolboxUnstyled.tsx
+++ b/packages/mask/src/components/InjectedComponents/ToolboxUnstyled.tsx
@@ -34,8 +34,10 @@ import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord'
import { usePersonaConnectStatus } from '../DataSource/usePersonaConnectStatus'
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,
@@ -135,7 +137,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/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/extension/background-script/EthereumServices/send.ts b/packages/mask/src/extension/background-script/EthereumServices/send.ts
index 4b674eb52274..d1488a3750e9 100644
--- a/packages/mask/src/extension/background-script/EthereumServices/send.ts
+++ b/packages/mask/src/extension/background-script/EthereumServices/send.ts
@@ -1,4 +1,3 @@
-import { first } from 'lodash-unified'
import { EthereumAddress } from 'wallet.ts'
import { toHex } from 'web3-utils'
import type { HttpProvider } from 'web3-core'
@@ -10,11 +9,14 @@ import {
EthereumErrorType,
EthereumMethodType,
EthereumRpcType,
- EthereumTransactionConfig,
isEIP1559Supported,
isSameAddress,
ProviderType,
SendOverrides,
+ getPayloadHash,
+ getPayloadConfig,
+ getPayloadChainId,
+ getTransactionHash,
isZeroAddress,
} from '@masknet/web3-shared-evm'
import type { IJsonRpcRequest } from '@walletconnect/types'
@@ -80,56 +82,6 @@ function getTo(computedPayload: UnboxPromise(({ dataSource, chainId }
const toAddress = getToAddress(transaction.receipt, transaction.computedPayload)
return (
({
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/SNSAdaptor/TransactionSnackbar/index.tsx b/packages/mask/src/plugins/Wallet/SNSAdaptor/TransactionSnackbar/index.tsx
index 37832698ff82..f65ad417551e 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(t('plugin_wallet_snackbar_swap_token'), {
...config,
- ...{ message: getFullMessage('Transaction rejected', hash) },
+ ...{ message: getFullMessage('Transaction failed', hash) },
})
return
}
diff --git a/packages/mask/src/plugins/Wallet/constants.ts b/packages/mask/src/plugins/Wallet/constants.ts
index 1730a5fa31e2..8d2236530d36 100644
--- a/packages/mask/src/plugins/Wallet/constants.ts
+++ b/packages/mask/src/plugins/Wallet/constants.ts
@@ -1,7 +1,7 @@
export {
PLUGIN_ID,
HD_PATH_WITHOUT_INDEX_ETHEREUM,
- JSON_RPC_ErrorCode,
+ JSON_RPC_ERROR_CODE,
UPDATE_CHAIN_STATE_DELAY,
} from '@masknet/plugin-wallet'
diff --git a/packages/mask/src/plugins/Wallet/services/transaction/database.ts b/packages/mask/src/plugins/Wallet/services/transaction/database.ts
index 55be91511440..3678ac111846 100644
--- a/packages/mask/src/plugins/Wallet/services/transaction/database.ts
+++ b/packages/mask/src/plugins/Wallet/services/transaction/database.ts
@@ -61,15 +61,18 @@ export async function replaceRecentTransaction(
address: string,
oldHash: string,
newHash: string,
- payload?: JsonRpcPayload,
+ newPayload?: JsonRpcPayload,
) {
const now = new Date()
const recordId = getRecordId(chainId, address)
const chunk = await PluginDB.get('recent-transactions', recordId)
const transaction = chunk?.transactions.find((x) => 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
+ if (newPayload) {
+ transaction.payloadReplacement = newPayload
+ }
await PluginDB.add({
type: 'recent-transactions',
id: recordId,
diff --git a/packages/mask/src/plugins/Wallet/services/transaction/helpers.ts b/packages/mask/src/plugins/Wallet/services/transaction/helpers.ts
index 331f36b491cc..be93bd0dafe9 100644
--- a/packages/mask/src/plugins/Wallet/services/transaction/helpers.ts
+++ b/packages/mask/src/plugins/Wallet/services/transaction/helpers.ts
@@ -72,14 +72,14 @@ 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) {
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..b02453de9acb 100644
--- a/packages/mask/src/plugins/Wallet/services/transaction/index.ts
+++ b/packages/mask/src/plugins/Wallet/services/transaction/index.ts
@@ -12,6 +12,7 @@ export * from './watcher'
export interface RecentTransaction {
at: Date
hash: string
+ hashReplacement?: string
status: TransactionStatusType
receipt?: TransactionReceipt | null
payload?: JsonRpcPayload
@@ -56,14 +57,17 @@ export async function getRecentTransactions(chainId: ChainId, address: string):
(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 && payloadReplacement)
+ watcher.watchTransaction(chainId, hashReplacement, payloadReplacement)
}
return {
at,
- hash: receipt?.transactionHash ?? hash,
+ hash,
+ hashReplacement,
status: helpers.getReceiptStatus(receipt),
receipt,
payload,
diff --git a/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts b/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts
index 58cde9ad15eb..9fcc17b748d4 100644
--- a/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts
+++ b/packages/mask/src/plugins/Wallet/services/transaction/watcher.ts
@@ -1,30 +1,80 @@
+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 { Explorer } from '@masknet/web3-providers'
+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'
import { currentChainIdSettings } from '../../settings'
+import { WalletRPC } from '../../messages'
-let timer: ReturnType | 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_TRANSACTIONS_SIZE = 40
-
-function getTransactionMap(chainId: ChainId) {
- if (!WATCHED_TRANSACTION_MAP.has(chainId)) WATCHED_TRANSACTION_MAP.set(chainId, new Map())
- return WATCHED_TRANSACTION_MAP.get(chainId)!
+interface StorageItem {
+ at: number
+ limits: number
+ payload: JsonRpcPayload
+ receipt: Promise | null
}
+class Storage {
+ static MAX_ITEM_SIZE = 40
+
+ private map = new Map>()
+
+ private getStorage(chainId: ChainId) {
+ if (!this.map.has(chainId)) this.map.set(chainId, new Map())
+ return this.map.get(chainId)!
+ }
+
+ public hasItem(chainId: ChainId, hash: string) {
+ return this.getStorage(chainId).has(hash)
+ }
+
+ public getItem(chainId: ChainId, hash: string) {
+ return this.getStorage(chainId).get(hash)
+ }
+
+ public setItem(chainId: ChainId, hash: string, transaction: StorageItem) {
+ this.getStorage(chainId).set(hash, transaction)
+ }
+
+ 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.MAX_ITEM_SIZE)
+ }
+
+ public getUnwatched(chainId: ChainId) {
+ return this.getItems(chainId).slice(Storage.MAX_ITEM_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, {
@@ -51,51 +101,131 @@ async function getTransactionReceipt(chainId: ChainId, hash: string) {
}
}
-async function checkReceipt() {
- 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,
+async function checkReceipt(chainId: ChainId) {
+ await Promise.allSettled(
+ storage.getWatched(chainId).map(async ([hash, transaction]) => {
+ const receipt = await storage.getItem(chainId, hash)?.receipt
+ if (receipt) return
+ storage.setItem(chainId, hash, {
+ ...transaction,
receipt: getTransactionReceipt(chainId, hash),
})
- return false
}),
)
+}
+
+async function checkAccount(chainId: ChainId, account: string) {
+ const { API_KEYS = [], EXPLORER_API = '' } = getExplorerConstants(chainId)
+
+ const watchedTransactions = storage.getWatched(chainId)
+ const latestTransactions = await Explorer.getLatestTransactions(account, EXPLORER_API, {
+ offset: CHECK_LATEST_SIZE,
+ apikey: first(API_KEYS),
+ })
+
+ for (const latestTransaction of latestTransactions) {
+ const [watchedHash, watchedTransaction] =
+ watchedTransactions.find(([hash, transaction]) => {
+ // the transaction 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 || watchedHash === latestTransaction.hash) continue
+
+ // replace the original transaction in DB
+ await WalletRPC.replaceRecentTransaction(
+ chainId,
+ account,
+ watchedHash,
+ latestTransaction.hash,
+ watchedTransaction.payload,
+ )
+
+ // update receipt in cache
+ storage.removeItem(chainId, watchedHash)
+ storage.setItem(chainId, latestTransaction.hash, {
+ ...watchedTransaction,
+ payload: helpers.toPayload(latestTransaction),
+ receipt: getTransactionReceipt(chainId, latestTransaction.hash),
+ })
+ }
+}
+
+async function check() {
+ // stop any pending task
+ stopCheck()
+
+ const chainId = currentChainIdSettings.value
+
+ // 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)
+ 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(
+ 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)
+ }
+}
- if (checkResult.every((x) => x.status === 'fulfilled' && x.value)) return
+function stopCheck() {
if (timer !== null) clearTimeout(timer)
- timer = setTimeout(checkReceipt, WATCHED_TRANSACTION_CHECK_DELAY)
+ timer = null
}
export async function getReceipt(chainId: ChainId, hash: string) {
- return getTransactionMap(chainId).get(hash)?.receipt ?? null
+ return storage.getItem(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) {
+ if (!storage.hasItem(chainId, hash)) {
+ storage.setItem(chainId, hash, {
at: Date.now(),
- receipt: getTransactionReceipt(chainId, hash),
+ payload,
+ limits: CHECK_TIMES,
+ receipt: Promise.resolve(null),
})
}
- if (timer === null) timer = setTimeout(checkReceipt, WATCHED_TRANSACTION_CHECK_DELAY)
+ startCheck(false)
}
export function unwatchTransaction(chainId: ChainId, hash: string) {
- getTransactionMap(chainId).delete(hash)
+ storage.removeItem(chainId, hash)
}
diff --git a/packages/plugins/Wallet/src/constants.ts b/packages/plugins/Wallet/src/constants.ts
index f153d83a1ba7..0062e0060e36 100644
--- a/packages/plugins/Wallet/src/constants.ts
+++ b/packages/plugins/Wallet/src/constants.ts
@@ -13,7 +13,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,
diff --git a/packages/web3-constants/evm/explorer.json b/packages/web3-constants/evm/explorer.json
index 26b1e74b38e1..187da84cfb05 100644
--- a/packages/web3-constants/evm/explorer.json
+++ b/packages/web3-constants/evm/explorer.json
@@ -1,4 +1,20 @@
{
+ "API_KEYS": {
+ "Mainnet": ["26GHYKIAZREN4HWJ5NCICTGKC183IGXY9A"],
+ "Ropsten": [],
+ "Rinkeby": [],
+ "Kovan": [],
+ "Gorli": [],
+ "BSC": ["5FQ5JHS1BEK4186QCZASE3SD23YD8FIMM8"],
+ "BSCT": [],
+ "Matic": ["5HVFKYFPQXWNQ2TFHCAYJJZ3YIYPWQ5HP8"],
+ "Mumbai": [],
+ "Arbitrum": ["BE8VU1P9FUKRT15FBHKQMU84829VITWWF2"],
+ "Arbitrum_Rinkeby": [],
+ "xDai": [],
+ "Celo": [],
+ "Fantom": ["AST4WWPNEYDURUXG2GH32JZMYWEFDP999S"]
+ },
"EXPLORER_API": {
"Mainnet": "https://api.etherscan.io/api",
"Ropsten": "",
@@ -9,26 +25,10 @@
"BSCT": "",
"Matic": "https://api.polygonscan.com/api",
"Mumbai": "",
- "Arbitrum": "",
+ "Arbitrum": "https://api.arbiscan.io/api",
"Arbitrum_Rinkeby": "",
- "xDai": "",
- "Celo": "",
+ "xDai": "https://blockscout.com/xdai/mainnet/api",
+ "Celo": "https://explorer.celo.org/api",
"Fantom": "https://api.ftmscan.com/api"
- },
- "EXPLORER_API_KEY": {
- "Mainnet": "99IX9MBZKG8FYQUECXDNDPATGVDDH6JCWW",
- "Ropsten": "",
- "Rinkeby": "",
- "Kovan": "",
- "Gorli": "",
- "BSC": "7IJXTFIFB28ARWKZVA78GS29TT5YG82M5Z",
- "BSCT": "",
- "Matic": "PEMG6SUGQ71X55MVW2A1YK5J49V8VV6RF9",
- "Mumbai": "",
- "Arbitrum": "",
- "Arbitrum_Rinkeby": "",
- "xDai": "",
- "Celo": "",
- "Fantom": ""
}
}
diff --git a/packages/web3-providers/src/explorer/helpers.ts b/packages/web3-providers/src/explorer/helpers.ts
new file mode 100644
index 000000000000..989ffcd1a7f9
--- /dev/null
+++ b/packages/web3-providers/src/explorer/helpers.ts
@@ -0,0 +1,23 @@
+import type { Transaction } from './types'
+import type { ExplorerAPI } from '..'
+
+export function toTransaction(transaction: Transaction): ExplorerAPI.Transaction & {
+ status: '0' | '1'
+ confirmations: number
+} {
+ return {
+ nonce: Number.parseInt(transaction.nonce, 10),
+ blockHash: transaction.blockHash,
+ blockNumber: Number.parseInt(transaction.blockNumber, 10),
+ from: transaction.from,
+ to: transaction.to,
+ gas: Number.parseInt(transaction.gas, 10),
+ gasPrice: transaction.gasPrice,
+ hash: transaction.hash,
+ input: transaction.input,
+ transactionIndex: Number.parseInt(transaction.transactionIndex, 10),
+ value: transaction.value,
+ status: transaction.txreceipt_status,
+ confirmations: Number.parseInt(transaction.confirmations, 10),
+ }
+}
diff --git a/packages/web3-providers/src/explorer/index.ts b/packages/web3-providers/src/explorer/index.ts
new file mode 100644
index 000000000000..834d2da5778e
--- /dev/null
+++ b/packages/web3-providers/src/explorer/index.ts
@@ -0,0 +1,32 @@
+import urlcat from 'urlcat'
+import type { ExplorerAPI } from '..'
+import type { Transaction } from './types'
+import { toTransaction } from './helpers'
+
+export class NativeExplorerAPI implements ExplorerAPI.Provider {
+ async getLatestTransactions(
+ account: string,
+ url: string,
+ { offset = 10, apikey }: ExplorerAPI.PageInfo = {},
+ ): Promise {
+ 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 {
+ message: string
+ result?: Transaction[]
+ status: '0' | '1'
+ }
+ return rawTransactions.result?.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 26f1517af6de..1751fa75f898 100644
--- a/packages/web3-providers/src/index.ts
+++ b/packages/web3-providers/src/index.ts
@@ -2,6 +2,7 @@ import { CoinGeckoAPI } from './coingecko'
import { OpenSeaAPI } from './opensea'
import { RaribleAPI } from './rarible'
import { NFTScanAPI } from './NFTScan'
+import { NativeExplorerAPI } from './explorer'
import { RSS3API } from './rss3'
export * from './types'
@@ -11,6 +12,7 @@ export const OpenSea = new OpenSeaAPI()
export const Rarible = new RaribleAPI()
export const NFTScan = new NFTScanAPI()
export const CoinGecko = new CoinGeckoAPI()
+export const Explorer = new NativeExplorerAPI()
export const RSS3 = new RSS3API()
// Method for provider proxy
diff --git a/packages/web3-providers/src/types.ts b/packages/web3-providers/src/types.ts
index 6ce0c55d3977..b4978c8a5b25 100644
--- a/packages/web3-providers/src/types.ts
+++ b/packages/web3-providers/src/types.ts
@@ -1,3 +1,4 @@
+import type { Transaction as Web3Transaction } from 'web3-core'
import type RSS3 from 'rss3-next'
import type { CurrencyType } from '@masknet/plugin-infra'
import type {
@@ -8,6 +9,21 @@ import type {
NativeTokenDetailed,
} from '@masknet/web3-shared-evm'
+export namespace ExplorerAPI {
+ export type Transaction = Web3Transaction & {
+ status: '0' | '1'
+ confirmations: number
+ }
+
+ export interface PageInfo {
+ offset?: number
+ apikey?: string
+ }
+
+ export interface Provider {
+ getLatestTransactions(account: string, url: string, pageInfo?: PageInfo): Promise
+ }
+}
export namespace RSS3BaseAPI {
export interface GeneralAsset {
platform: string
diff --git a/packages/web3-shared/evm/constants/index.ts b/packages/web3-shared/evm/constants/index.ts
index c59b2170c154..3e0aae6421eb 100644
--- a/packages/web3-shared/evm/constants/index.ts
+++ b/packages/web3-shared/evm/constants/index.ts
@@ -14,16 +14,24 @@ 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 TokenList from '@masknet/web3-constants/evm/token-list.json'
import TokenAssetBaseURL from '@masknet/web3-constants/evm/token-asset-base-url.json'
import GoodGhosting from '@masknet/web3-constants/evm/good-ghosting.json'
import SpaceStationGalaxy from '@masknet/web3-constants/evm/space-station-galaxy.json'
import OpenseaAPI from '@masknet/web3-constants/evm/opensea-api.json'
-import Explorer from '@masknet/web3-constants/evm/explorer.json'
import CryptoArtAI from '@masknet/web3-constants/evm/cryptoartai.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)
@@ -68,14 +76,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 getTokenListConstants = transform(TokenList)
export const useTokenListConstants = hookTransform(getTokenListConstants)
@@ -90,12 +96,10 @@ export const useGoodGhostingConstants = hookTransform(getGoodGhostingConstants)
export const getSpaceStationGalaxyConstants = transform(SpaceStationGalaxy)
export const useSpaceStationGalaxyConstants = hookTransform(getSpaceStationGalaxyConstants)
+
export const getOpenseaAPIConstants = transform(OpenseaAPI)
export const useOpenseaAPIConstants = hookTransform(getOpenseaAPIConstants)
-export const getExplorerConstants = transform(Explorer)
-export const useExplorerConstants = hookTransform(getExplorerConstants)
-
export const getCryptoArtAIConstants = transform(CryptoArtAI)
export const useCryptoArtAIConstants = hookTransform(getCryptoArtAIConstants)
diff --git a/packages/web3-shared/evm/utils/index.ts b/packages/web3-shared/evm/utils/index.ts
index 96971829d131..223bf5607608 100644
--- a/packages/web3-shared/evm/utils/index.ts
+++ b/packages/web3-shared/evm/utils/index.ts
@@ -6,4 +6,5 @@ export * from './formatter'
export * from './chainDetailed'
export * from './transaction'
export * from './domain'
+export * from './payload'
export * from './misc'
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
+}