From 2e2915e9afde90784b301fb23eefbf6e5fc6e41f Mon Sep 17 00:00:00 2001 From: gregs Date: Tue, 30 Apr 2024 15:03:04 -0300 Subject: [PATCH] Activity details loading (#1519) --- .../transactions/consolidatedTransactions.ts | 20 +- .../resources/transactions/transaction.ts | 298 +++++++++--------- .../customNetworkTransactions/index.ts | 8 +- .../components/BottomSheet/BottomSheet.tsx | 2 +- .../popup/hooks/useInfiniteTransactionList.ts | 4 +- .../hooks/useTransactionListForPendingTxs.ts | 1 - .../hooks/useWatchPendingTransactions.ts | 4 - .../pages/home/Activity/ActivityDetails.tsx | 70 ++-- .../popup/pages/home/Approvals/Approvals.tsx | 3 - 9 files changed, 199 insertions(+), 211 deletions(-) diff --git a/src/core/resources/transactions/consolidatedTransactions.ts b/src/core/resources/transactions/consolidatedTransactions.ts index b124815a21..203489f289 100644 --- a/src/core/resources/transactions/consolidatedTransactions.ts +++ b/src/core/resources/transactions/consolidatedTransactions.ts @@ -27,7 +27,6 @@ const CONSOLIDATED_TRANSACTIONS_TIMEOUT = 20000; export type ConsolidatedTransactionsArgs = { address: Address; currency: SupportedCurrencyKey; - testnetMode: boolean; userChainIds: number[]; }; @@ -37,12 +36,11 @@ export type ConsolidatedTransactionsArgs = { export const consolidatedTransactionsQueryKey = ({ address, currency, - testnetMode, userChainIds, }: ConsolidatedTransactionsArgs) => createQueryKey( 'consolidatedTransactions', - { address, currency, testnetMode, userChainIds }, + { address, currency, userChainIds }, { persisterVersion: 1 }, ); @@ -56,12 +54,7 @@ type ConsolidatedTransactionsQueryKey = ReturnType< export async function fetchConsolidatedTransactions< TSelectData = ConsolidatedTransactionsResult, >( - { - address, - currency, - testnetMode, - userChainIds, - }: ConsolidatedTransactionsArgs, + { address, currency, userChainIds }: ConsolidatedTransactionsArgs, config: QueryConfig< ConsolidatedTransactionsResult, Error, @@ -73,7 +66,6 @@ export async function fetchConsolidatedTransactions< consolidatedTransactionsQueryKey({ address, currency, - testnetMode, userChainIds, }), consolidatedTransactionsQueryFunction, @@ -153,12 +145,7 @@ async function parseConsolidatedTransactions( export function useConsolidatedTransactions< TSelectData = ConsolidatedTransactionsResult, >( - { - address, - currency, - userChainIds, - testnetMode, - }: ConsolidatedTransactionsArgs, + { address, currency, userChainIds }: ConsolidatedTransactionsArgs, config: InfiniteQueryConfig< ConsolidatedTransactionsResult, Error, @@ -169,7 +156,6 @@ export function useConsolidatedTransactions< consolidatedTransactionsQueryKey({ address, currency, - testnetMode, userChainIds, }), consolidatedTransactionsQueryFunction, diff --git a/src/core/resources/transactions/transaction.ts b/src/core/resources/transactions/transaction.ts index d183dd0ac2..af5254e5e9 100644 --- a/src/core/resources/transactions/transaction.ts +++ b/src/core/resources/transactions/transaction.ts @@ -1,12 +1,13 @@ +import { Provider, TransactionResponse } from '@ethersproject/providers'; import { formatUnits } from '@ethersproject/units'; -import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { QueryClient, useQuery, useQueryClient } from '@tanstack/react-query'; import { Hash, getProvider } from '@wagmi/core'; import { Address } from 'wagmi'; import { i18n } from '~/core/languages'; import { addysHttp } from '~/core/network/addys'; import { QueryFunctionResult, createQueryKey } from '~/core/react-query'; -import { SupportedCurrencyKey } from '~/core/references'; +import { SUPPORTED_CHAIN_IDS, SupportedCurrencyKey } from '~/core/references'; import { consolidatedTransactionsQueryFunction, consolidatedTransactionsQueryKey, @@ -16,15 +17,13 @@ import { useCurrentAddressStore, useCurrentCurrencyStore, } from '~/core/state'; -import { useTestnetModeStore } from '~/core/state/currentSettings/testnetMode'; +import { customNetworkTransactionsStore } from '~/core/state/transactions/customNetworkTransactions'; import { ChainId } from '~/core/types/chains'; import { - MinedTransaction, - PendingTransaction, + RainbowTransaction, TransactionApiResponse, TxHash, } from '~/core/types/transactions'; -import { isCustomChain } from '~/core/utils/chains'; import { parseTransaction } from '~/core/utils/transactions'; import { useUserChains } from '~/entries/popup/hooks/useUserChains'; import { RainbowError, logger } from '~/logger'; @@ -52,6 +51,14 @@ export const fetchTransaction = async ({ currency: SupportedCurrencyKey; chainId: ChainId; }) => { + if (!SUPPORTED_CHAIN_IDS.includes(chainId)) { + return fetchTransactionDataFromProvider({ + chainId, + hash, + account: address, + }); + } + try { const response = await addysHttp.get<{ payload: { transaction: TransactionApiResponse }; @@ -64,7 +71,11 @@ export const fetchTransaction = async ({ const localPendingTx = searchInLocalPendingTransactions(address, hash); if (localPendingTx) return localPendingTx; - const providerTx = await getCustomChainTransaction({ chainId, hash }); + const providerTx = await fetchTransactionDataFromProvider({ + chainId, + hash, + account: address, + }); return providerTx; } const parsedTx = parseTransaction({ tx, currency, chainId }); @@ -83,133 +94,121 @@ export const fetchTransaction = async ({ } }; -type PaginatedTransactions = { pages: ConsolidatedTransactionsResult[] }; +async function guessTransactionType( + provider: Provider, + transaction: TransactionResponse, +) { + if (!transaction.to) return 'deployment'; -export function useBackendTransaction({ - hash, - chainId, - enabled, -}: { - hash?: TxHash; - chainId?: ChainId; - enabled: boolean; -}) { - const queryClient = useQueryClient(); - const { currentAddress: address } = useCurrentAddressStore(); - const { currentCurrency: currency } = useCurrentCurrencyStore(); - const { testnetMode } = useTestnetModeStore(); - const { chains } = useUserChains(); + const code = await provider.getCode(transaction.to); + if (code && code !== '0x') return 'contract_interaction'; - const paginatedTransactionsKey = consolidatedTransactionsQueryKey({ - address, - currency, - testnetMode, - userChainIds: chains.map((chain) => chain.id), - }); - - const params = { - hash: hash!, - address, - currency, - chainId, - }; - - return useQuery({ - queryKey: createQueryKey('transaction', params), - queryFn: () => - fetchTransaction({ - hash: params.hash, - address: params.address, - currency: params.currency, - chainId: params.chainId!, - }), - enabled: !!hash && !!address && !!chainId && enabled, - initialData: () => { - const queryData = queryClient.getQueryData( - paginatedTransactionsKey, - ); - const pages = queryData?.pages || []; - for (const page of pages) { - const tx = page.transactions.find((tx) => tx.hash === hash); - if (tx) return tx; - } - }, - initialDataUpdatedAt: () => - queryClient.getQueryState(paginatedTransactionsKey)?.dataUpdatedAt, - }); + return 'send'; } -const getCustomChainTransaction = async ({ +const fetchTransactionDataFromProvider = async ({ chainId, hash, + account, }: { chainId: number; hash: Hash; -}) => { + account: Address; +}): Promise => { const provider = getProvider({ chainId }); const transaction = await provider.getTransaction(hash); + if (!transaction) throw `getCustomChainTransaction: couldn't find transaction`; - const block = transaction?.blockHash - ? 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', - blockNumber: transaction.blockNumber || 0, - minedAt: block?.timestamp || 0, - confirmations: transaction.confirmations, - gasUsed: transaction.gasLimit.toString(), - hash: transaction.hash as Hash, - nonce: transaction.nonce, - chainId: transaction.chainId, - from: transaction.from as Address, - to: transaction.to as Address, - data: transaction.data, - value, - type: 'send', - title: i18n.t('transactions.send.confirmed'), - baseFee: block?.baseFeePerGas?.toString(), - maxFeePerGas: transaction.maxFeePerGas?.toString(), - maxPriorityFeePerGas: transaction.maxPriorityFeePerGas?.toString(), - gasPrice: transaction.gasPrice?.toString(), - } satisfies MinedTransaction) - : ({ - status: 'pending', - hash: transaction.hash as Hash, - nonce: transaction.nonce, - chainId: transaction.chainId, - from: transaction.from as Address, - to: transaction.to as Address, - data: transaction.data, - value, - type: 'send', - title: i18n.t('transactions.send.pending'), - } satisfies PendingTransaction); - return parsedTransaction; + const direction = + transaction.from === account ? ('out' as const) : ('in' as const); + + const baseTransaction = { + hash: transaction.hash as Hash, + nonce: transaction.nonce, + chainId: transaction.chainId, + from: transaction.from as Address, + to: transaction.to as Address, + data: transaction.data, + value, + direction, + + feeType: !transaction.maxFeePerGas ? 'legacy' : 'eip-1559', + gasLimit: transaction.gasLimit.toString(), + maxFeePerGas: transaction.maxFeePerGas?.toString(), + maxPriorityFeePerGas: transaction.maxPriorityFeePerGas?.toString(), + gasPrice: transaction.gasPrice?.toString(), + } as const; + + if (transaction.blockNumber !== undefined) { + const [receipt, block, type] = await Promise.all([ + provider.getTransactionReceipt(transaction.hash), + provider.getBlock(transaction.blockNumber), + guessTransactionType(provider, transaction), + ]); + const status = receipt.status === 1 ? 'confirmed' : 'failed'; + + return { + ...baseTransaction, + status, + type, + title: i18n.t(`transactions.${type}.${status}`), + + blockNumber: transaction.blockNumber, + minedAt: transaction.timestamp!, + confirmations: transaction.confirmations, + + gasUsed: receipt.gasUsed.toString(), + + feeType: !block.baseFeePerGas ? 'legacy' : 'eip-1559', + fee: receipt.cumulativeGasUsed.mul(receipt.effectiveGasPrice).toString(), + baseFee: block.baseFeePerGas?.toString(), + }; + } + + const type = await guessTransactionType(provider, transaction); + + return { + ...baseTransaction, + status: 'pending', + type, + title: i18n.t(`transactions.${type}.pending`), + }; }; -export function useCustomNetworkTransaction({ - hash, - chainId, - enabled, -}: { - hash?: TxHash; - chainId?: ChainId; - enabled: boolean; -}) { - return useQuery({ - queryKey: createQueryKey('providerTransaction', { chainId, hash }), - queryFn: () => - getCustomChainTransaction({ chainId: chainId!, hash: hash! }), - enabled: !!hash && !!chainId && enabled, - }); -} +type PaginatedTransactions = { pages: ConsolidatedTransactionsResult[] }; + +const findTransactionInConsolidatedBEQueryCache = ( + queryClient: QueryClient, + queryKey: ReturnType, + { hash, chainId }: { hash: Hash; chainId: ChainId }, +) => { + const queryData = queryClient.getQueryData(queryKey); + const pages = queryData?.pages; + if (!pages) return; + + for (const page of pages) { + const tx = page.transactions.find( + (tx) => tx.hash === hash && tx.chainId === chainId, + ); + if (tx) return tx; + } +}; + +const findTransactionInCustomNetworkTransactionsStore = ( + account: Address, + { hash, chainId }: { hash: Hash; chainId: ChainId }, +) => { + const { getCustomNetworkTransactions } = + customNetworkTransactionsStore.getState(); + return getCustomNetworkTransactions({ address: account }).find( + (tx) => tx.hash === hash && tx.chainId === chainId, + ); +}; export const useTransaction = ({ chainId, @@ -218,32 +217,49 @@ export const useTransaction = ({ chainId?: number; hash?: `0x${string}`; }) => { - const customChain = !!chainId && isCustomChain(chainId); - const { - data: backendTransaction, - isLoading: backendTransactionIsLoading, - isFetched: backendTransactionIsFetched, - } = useBackendTransaction({ - hash, - chainId, - enabled: !customChain && !!hash && !!chainId, + const queryClient = useQueryClient(); + + const { currentAddress: address } = useCurrentAddressStore(); + const { currentCurrency: currency } = useCurrentCurrencyStore(); + const { chains } = useUserChains(); + + const params = { + hash: hash!, + address, + currency, + chainId: chainId!, + }; + + const consolidatedTransactionsKey = consolidatedTransactionsQueryKey({ + address, + currency, + userChainIds: chains.map((chain) => chain.id), }); - const { - data: providerTransaction, - isLoading: providerTransactionIsLoading, - isFetched: providerTransactionIsFetched, - } = useCustomNetworkTransaction({ - hash, - chainId, - enabled: customChain, + + return useQuery({ + queryKey: createQueryKey('transaction', params), + queryFn: () => fetchTransaction(params), + enabled: !!hash && !!address && !!chainId, + initialData: () => { + if (!hash || !chainId) return; + + const tx = SUPPORTED_CHAIN_IDS.includes(chainId) + ? findTransactionInConsolidatedBEQueryCache( + queryClient, + consolidatedTransactionsKey, + { hash, chainId }, + ) + : findTransactionInCustomNetworkTransactionsStore(address, { + hash, + chainId, + }); + + if (tx) return tx; + }, + initialDataUpdatedAt: () => { + if (!chainId || !SUPPORTED_CHAIN_IDS.includes(chainId)) return undefined; + return queryClient.getQueryState(consolidatedTransactionsKey) + ?.dataUpdatedAt; + }, }); - return { - data: customChain ? providerTransaction : backendTransaction, - isLoading: customChain - ? providerTransactionIsLoading - : backendTransactionIsLoading, - isFetched: customChain - ? providerTransactionIsFetched - : backendTransactionIsFetched, - }; }; diff --git a/src/core/state/transactions/customNetworkTransactions/index.ts b/src/core/state/transactions/customNetworkTransactions/index.ts index cf29db2fab..d3f401eda7 100644 --- a/src/core/state/transactions/customNetworkTransactions/index.ts +++ b/src/core/state/transactions/customNetworkTransactions/index.ts @@ -1,20 +1,20 @@ import { Address } from 'wagmi'; import create from 'zustand'; -import { MinedTransaction } from '~/core/types/transactions'; +import { RainbowTransaction } from '~/core/types/transactions'; import { createStore } from '../../internal/createStore'; export interface CustomNetworkTransactionsState { customNetworkTransactions: Record< Address, - Record + Record >; getCustomNetworkTransactions: ({ address, }: { address: Address; - }) => MinedTransaction[]; + }) => RainbowTransaction[]; addCustomNetworkTransactions: ({ address, chainId, @@ -22,7 +22,7 @@ export interface CustomNetworkTransactionsState { }: { address: Address; chainId: number; - transaction: MinedTransaction; + transaction: RainbowTransaction; }) => void; clearCustomNetworkTransactions: ({ address, diff --git a/src/design-system/components/BottomSheet/BottomSheet.tsx b/src/design-system/components/BottomSheet/BottomSheet.tsx index 5593e4c2b4..a0ced71a3c 100644 --- a/src/design-system/components/BottomSheet/BottomSheet.tsx +++ b/src/design-system/components/BottomSheet/BottomSheet.tsx @@ -70,7 +70,7 @@ export const BottomSheet = ({ exit={{ opacity: 1, y: 800 }} key="bottom" transition={{ duration: transition_duration_s }} - layout + layout="preserve-aspect" isModal > { address, currency, userChainIds: supportedChainIds, - testnetMode, }); useEffect(() => { diff --git a/src/entries/popup/hooks/useWatchPendingTransactions.ts b/src/entries/popup/hooks/useWatchPendingTransactions.ts index e30722d3c3..9c91209244 100644 --- a/src/entries/popup/hooks/useWatchPendingTransactions.ts +++ b/src/entries/popup/hooks/useWatchPendingTransactions.ts @@ -11,7 +11,6 @@ import { useNonceStore, usePendingTransactionsStore, } from '~/core/state'; -import { useTestnetModeStore } from '~/core/state/currentSettings/testnetMode'; import { useCustomNetworkTransactionsStore } from '~/core/state/transactions/customNetworkTransactions'; import { useUserChainsStore } from '~/core/state/userChains'; import { @@ -42,7 +41,6 @@ export const useWatchPendingTransactions = ({ const { currentCurrency } = useCurrentCurrencyStore(); const { addCustomNetworkTransactions } = useCustomNetworkTransactionsStore(); const { userChains } = useUserChainsStore(); - const { testnetMode } = useTestnetModeStore(); const pendingTransactions = useMemo( () => storePendingTransactions[address] || [], @@ -249,7 +247,6 @@ export const useWatchPendingTransactions = ({ queryKey: consolidatedTransactionsQueryKey({ address, currency: currentCurrency, - testnetMode, userChainIds: Object.keys(userChains).map(Number), }), }); @@ -277,7 +274,6 @@ export const useWatchPendingTransactions = ({ processNonces, processPendingTransaction, setPendingTransactions, - testnetMode, userChains, ]); diff --git a/src/entries/popup/pages/home/Activity/ActivityDetails.tsx b/src/entries/popup/pages/home/Activity/ActivityDetails.tsx index ace91bfbf9..05ed8554c3 100644 --- a/src/entries/popup/pages/home/Activity/ActivityDetails.tsx +++ b/src/entries/popup/pages/home/Activity/ActivityDetails.tsx @@ -2,7 +2,7 @@ import { BigNumber } from '@ethersproject/bignumber'; import { formatUnits } from '@ethersproject/units'; import { motion } from 'framer-motion'; import { useMemo } from 'react'; -import { useParams, useSearchParams } from 'react-router-dom'; +import { Navigate, useParams, useSearchParams } from 'react-router-dom'; import { i18n } from '~/core/languages'; import { useApprovals } from '~/core/resources/approvals/approvals'; @@ -569,7 +569,7 @@ export function ActivityDetails() { const { hash, chainId } = useParams<{ hash: TxHash; chainId: string }>(); const { isWatchingWallet } = useWallets(); - const { data: transaction, isLoading } = useTransaction({ + const { data: transaction } = useTransaction({ hash, chainId: Number(chainId), }); @@ -611,49 +611,45 @@ export function ActivityDetails() { const backToHome = () => navigate(ROUTES.HOME, { - state: { skipTransitionOnRoute: ROUTES.HOME }, + state: { skipTransitionOnRoute: ROUTES.HOME, tab: 'activity' }, }); const onRevoke = () => { triggerRevokeApproval({ show: true, approval: approvalToRevoke }); }; + if (!transaction) { + return ; + } + return ( - - {!isLoading && !!transaction && ( - <> - - } - titleComponent={} - rightComponent={ - - } + + } + titleComponent={} + rightComponent={ + - - - } - padding="20px" - gap="20px" - > - - {additionalDetails && ( - - )} - - - {transaction.status === 'pending' && ( - - )} - - - )} + } + /> + + + } + padding="20px" + gap="20px" + > + + {additionalDetails && } + + + {transaction.status === 'pending' && ( + + )} + ); } diff --git a/src/entries/popup/pages/home/Approvals/Approvals.tsx b/src/entries/popup/pages/home/Approvals/Approvals.tsx index f106e770f5..e86aa6a64a 100644 --- a/src/entries/popup/pages/home/Approvals/Approvals.tsx +++ b/src/entries/popup/pages/home/Approvals/Approvals.tsx @@ -13,7 +13,6 @@ import { import { useConsolidatedTransactions } from '~/core/resources/transactions/consolidatedTransactions'; import { useCurrentAddressStore, useCurrentCurrencyStore } from '~/core/state'; import { useCurrentThemeStore } from '~/core/state/currentSettings/currentTheme'; -import { useTestnetModeStore } from '~/core/state/currentSettings/testnetMode'; import { useUserChainsStore } from '~/core/state/userChains'; import { ChainId } from '~/core/types/chains'; import { RainbowTransaction, TxHash } from '~/core/types/transactions'; @@ -331,7 +330,6 @@ export const Approvals = () => { approval: Approval | null; spender: ApprovalSpender | null; }>({ approval: null, spender: null }); - const { testnetMode } = useTestnetModeStore(); const supportedMainnetIds = SUPPORTED_MAINNET_CHAINS.map((c: Chain) => c.id); const [sort, setSort] = useState('recent'); const [activeTab, setActiveTab] = useState('tokens'); @@ -340,7 +338,6 @@ export const Approvals = () => { address: currentAddress, currency: currentCurrency, userChainIds: supportedMainnetIds, - testnetMode, }); const revokeTransactions = useMemo(