diff --git a/src/core/providers/proxy.ts b/src/core/providers/proxy.ts index a3f2243631..7c4de22309 100644 --- a/src/core/providers/proxy.ts +++ b/src/core/providers/proxy.ts @@ -3,6 +3,7 @@ import { ChainId } from '../types/chains'; export const proxyRpcEndpoint = (endpoint: string, chainId: ChainId) => { if ( + endpoint && endpoint !== 'http://127.0.0.1:8545' && endpoint !== 'http://localhost:8545' && !endpoint.includes('http://10.') && diff --git a/src/core/resources/transactions/consolidatedTransactions.ts b/src/core/resources/transactions/consolidatedTransactions.ts index 5bb8b8b5a6..b124815a21 100644 --- a/src/core/resources/transactions/consolidatedTransactions.ts +++ b/src/core/resources/transactions/consolidatedTransactions.ts @@ -136,7 +136,7 @@ async function parseConsolidatedTransactions( currency: SupportedCurrencyKey, ) { const data = message?.payload?.transactions || []; - const parsedTransactionPromises = data + return data .map((tx) => parseTransaction({ tx, @@ -145,11 +145,6 @@ async function parseConsolidatedTransactions( }), ) .filter(Boolean); - - const parsedConsolidatedTransactions = ( - await Promise.all(parsedTransactionPromises) - ).flat(); - return parsedConsolidatedTransactions; } // /////////////////////////////////////////////// diff --git a/src/core/resources/transactions/transaction.ts b/src/core/resources/transactions/transaction.ts index 020c02a3ff..d183dd0ac2 100644 --- a/src/core/resources/transactions/transaction.ts +++ b/src/core/resources/transactions/transaction.ts @@ -1,3 +1,4 @@ +import { formatUnits } from '@ethersproject/units'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { Hash, getProvider } from '@wagmi/core'; import { Address } from 'wagmi'; @@ -10,7 +11,11 @@ import { consolidatedTransactionsQueryFunction, consolidatedTransactionsQueryKey, } from '~/core/resources/transactions/consolidatedTransactions'; -import { useCurrentAddressStore, useCurrentCurrencyStore } from '~/core/state'; +import { + pendingTransactionsStore, + useCurrentAddressStore, + useCurrentCurrencyStore, +} from '~/core/state'; import { useTestnetModeStore } from '~/core/state/currentSettings/testnetMode'; import { ChainId } from '~/core/types/chains'; import { @@ -28,6 +33,14 @@ type ConsolidatedTransactionsResult = QueryFunctionResult< typeof consolidatedTransactionsQueryFunction >; +const searchInLocalPendingTransactions = (userAddress: Address, hash: Hash) => { + const { pendingTransactions } = pendingTransactionsStore.getState(); + const localPendingTx = pendingTransactions[userAddress]?.find( + (tx) => tx.hash === hash, + ); + return localPendingTx; +}; + export const fetchTransaction = async ({ hash, address, @@ -48,6 +61,9 @@ export const fetchTransaction = async ({ }); const tx = response.data.payload.transaction; if (response.data.meta.status === 'pending') { + const localPendingTx = searchInLocalPendingTransactions(address, hash); + if (localPendingTx) return localPendingTx; + const providerTx = await getCustomChainTransaction({ chainId, hash }); return providerTx; } @@ -55,9 +71,15 @@ export const fetchTransaction = async ({ if (!parsedTx) throw new Error('Failed to parse transaction'); return parsedTx; } catch (e) { + // if it's a pending tx BE may be in another mempool and it will return 404, + // which throws and gets caught here, so we check if we got it in localstorage + const localPendingTx = searchInLocalPendingTransactions(address, hash); + if (localPendingTx) return localPendingTx; + logger.error(new RainbowError('fetchTransaction: '), { message: (e as Error)?.message, }); + throw e; // log & rethrow } }; @@ -133,6 +155,9 @@ const getCustomChainTransaction = async ({ ? await provider.getBlock(transaction?.blockHash) : undefined; + const decimals = 18; // assuming every chain uses 18 decimals + const value = formatUnits(transaction.value, decimals); + const parsedTransaction = transaction.blockNumber ? ({ status: 'confirmed', @@ -146,7 +171,7 @@ const getCustomChainTransaction = async ({ from: transaction.from as Address, to: transaction.to as Address, data: transaction.data, - value: transaction.value.toString(), + value, type: 'send', title: i18n.t('transactions.send.confirmed'), baseFee: block?.baseFeePerGas?.toString(), @@ -162,7 +187,7 @@ const getCustomChainTransaction = async ({ from: transaction.from as Address, to: transaction.to as Address, data: transaction.data, - value: transaction.value.toString(), + value, type: 'send', title: i18n.t('transactions.send.pending'), } satisfies PendingTransaction); diff --git a/src/core/resources/transactions/transactions.ts b/src/core/resources/transactions/transactions.ts index 8aa44285d1..512a6f999b 100644 --- a/src/core/resources/transactions/transactions.ts +++ b/src/core/resources/transactions/transactions.ts @@ -103,7 +103,7 @@ async function parseTransactions( currency: SupportedCurrencyKey, ) { const data = message?.payload?.transactions || []; - const parsedTransactionPromises = data + return data .map((tx) => parseTransaction({ tx, @@ -114,11 +114,6 @@ async function parseTransactions( }), ) .filter(Boolean); - - const parsedTransactions = ( - await Promise.all(parsedTransactionPromises) - ).flat(); - return parsedTransactions; } // /////////////////////////////////////////////// diff --git a/src/core/utils/transactions.ts b/src/core/utils/transactions.ts index 80c0e81aa3..a957f20817 100644 --- a/src/core/utils/transactions.ts +++ b/src/core/utils/transactions.ts @@ -242,8 +242,10 @@ export function parseTransaction({ if ( !type || - (transactionTypeShouldHaveChanges(type) && changes.length === 0) || - !tx.address_from + !tx.address_from || + (status !== 'failed' && // failed txs won't have changes + transactionTypeShouldHaveChanges(type) && + changes.length === 0) ) return; // filters some spam or weird api responses diff --git a/src/entries/popup/pages/home/Activity/ActivityContextMenu.tsx b/src/entries/popup/pages/home/Activity/ActivityContextMenu.tsx index ee8f06c540..099e5a18cf 100644 --- a/src/entries/popup/pages/home/Activity/ActivityContextMenu.tsx +++ b/src/entries/popup/pages/home/Activity/ActivityContextMenu.tsx @@ -4,7 +4,6 @@ import { i18n } from '~/core/languages'; import { shortcuts } from '~/core/references/shortcuts'; import { useCurrentHomeSheetStore } from '~/core/state/currentHomeSheet'; import { useSelectedTransactionStore } from '~/core/state/selectedTransaction'; -import { ChainId } from '~/core/types/chains'; import { RainbowTransaction } from '~/core/types/transactions'; import { truncateAddress } from '~/core/utils/address'; import { copy } from '~/core/utils/copy'; @@ -45,10 +44,7 @@ export function ActivityContextMenu({ }); }; - const viewOnExplorer = () => { - const explorer = getTransactionBlockExplorer(transaction); - goToNewTab({ url: explorer?.url }); - }; + const explorer = getTransactionBlockExplorer(transaction); const onSpeedUp = () => { setCurrentHomeSheet('speedUp'); @@ -115,15 +111,15 @@ export function ActivityContextMenu({ )} - - {transaction?.chainId === ChainId.mainnet - ? i18n.t('speed_up_and_cancel.view_on_etherscan') - : i18n.t('speed_up_and_cancel.view_on_explorer')} - + {explorer && ( + goToNewTab({ url: explorer.url })} + shortcut={shortcuts.activity.VIEW_TRANSACTION.display} + > + {i18n.t('view_on_explorer', { explorer: explorer.name })} + + )} ; +const formatFee = (transaction: RainbowTransaction) => { + if ( + transaction.native !== undefined && + transaction.native.fee !== undefined + ) { + // if the fee is less than $0.01, the provider returns 0 so we display it as <$0.01 + const feeInNative = + +transaction.native.fee <= 0.01 ? 0.01 : transaction.native.fee; + return `${+feeInNative <= 0.01 ? '<' : ''}${formatCurrency(feeInNative)}`; + } + + const nativeCurrencySymbol = findRainbowChainForChainId(transaction.chainId) + ?.nativeCurrency.symbol; + + if (!transaction.fee || !nativeCurrencySymbol) return; + + return `${formatNumber(transaction.fee)} ${nativeCurrencySymbol}`; +}; function FeeData({ transaction: tx }: { transaction: RainbowTransaction }) { - const { native, feeType } = tx; + const { feeType } = tx; + + // if baseFee is undefined (like in pending txs or custom networks the api wont have data about it) + // so we try to calculate with the data we may have locally + const baseFee = + tx.baseFee || + (tx.maxFeePerGas && + tx.maxPriorityFeePerGas && + BigNumber.from(tx.maxFeePerGas).sub(tx.maxPriorityFeePerGas).toString()); - const maxPriorityFeePerGas = - tx.maxPriorityFeePerGas && formatUnits(tx.maxPriorityFeePerGas, 'gwei'); - const maxFeePerGas = tx.maxFeePerGas && formatUnits(tx.maxFeePerGas, 'gwei'); - const baseFee = tx.baseFee && formatUnits(tx.baseFee, 'gwei'); + const fee = formatFee(tx); - const gasPrice = tx.gasPrice && formatUnits(tx.gasPrice, 'gwei'); + if ((!baseFee || !tx.maxPriorityFeePerGas) && !tx.gasPrice) return null; return ( <> - {native?.fee && ( + {fee && ( )} {feeType === 'legacy' ? ( <> - {gasPrice && ( + {tx.gasPrice && ( )} @@ -185,15 +208,8 @@ function FeeData({ transaction: tx }: { transaction: RainbowTransaction }) { symbol="barometer" label={i18n.t('activity_details.base_fee')} value={ - baseFee ? `${formatNumber(baseFee)} Gwei` : - } - /> - ) @@ -203,8 +219,10 @@ function FeeData({ transaction: tx }: { transaction: RainbowTransaction }) { symbol="barometer" label={i18n.t('activity_details.max_priority_fee')} value={ - maxPriorityFeePerGas ? ( - `${formatNumber(maxPriorityFeePerGas)} Gwei` + tx.maxPriorityFeePerGas ? ( + `${formatNumber( + formatUnits(tx.maxPriorityFeePerGas, 'gwei'), + )} Gwei` ) : ( ) @@ -216,25 +234,37 @@ function FeeData({ transaction: tx }: { transaction: RainbowTransaction }) { ); } +const formatValue = (transaction: RainbowTransaction) => { + const formattedValueInNative = + transaction.native && + transaction.native.value && + Number(transaction.native.value) > 0 && + formatCurrency(transaction.native.value); + + if (formattedValueInNative) return formattedValueInNative; + + const nativeCurrencySymbol = findRainbowChainForChainId(transaction.chainId) + ?.nativeCurrency.symbol; + + if (!nativeCurrencySymbol) return; + + const formattedValue = + Number(transaction.value) > 0 && + `${formatNumber(transaction.value)} ${nativeCurrencySymbol}`; + + return formattedValue; +}; function NetworkData({ transaction: tx }: { transaction: RainbowTransaction }) { - const { nonce, native, value } = tx; - const { rainbowChains } = useRainbowChains(); - const chain = getChain({ chainId: tx.chainId }); + const chain = findRainbowChainForChainId(tx.chainId); + const value = formatValue(tx); return ( - {native?.value && +native?.value > 0 && ( - - )} - {!(native?.value && +native?.value) && value && ( + {value && ( )} - {ChainNameDisplay[tx.chainId] || - rainbowChains.find((chain) => chain.id === tx.chainId)?.name} + {ChainNameDisplay[tx.chainId] || chain?.name} } /> - {tx.status != 'pending' && } - {nonce >= 0 && ( + + {tx.nonce >= 0 && ( )} diff --git a/static/json/languages/en_US.json b/static/json/languages/en_US.json index b8cb9dd576..75a29fc5a9 100644 --- a/static/json/languages/en_US.json +++ b/static/json/languages/en_US.json @@ -1742,6 +1742,7 @@ "unverified": "Unverified", "skip": "Skip", "done": "Done", + "view_on_explorer": "View on %{explorer}", "watch_asset": { "chain_id": "Chain ID", "symbol": "Symbol",