diff --git a/packages/synapse-interface/components/Portfolio/Transaction/MostRecentTransaction.tsx b/packages/synapse-interface/components/Portfolio/Transaction/MostRecentTransaction.tsx index adba89a3e9..257c9c633a 100644 --- a/packages/synapse-interface/components/Portfolio/Transaction/MostRecentTransaction.tsx +++ b/packages/synapse-interface/components/Portfolio/Transaction/MostRecentTransaction.tsx @@ -98,26 +98,19 @@ export const MostRecentTransaction = () => { return transactions }, [userHistoricalTransactions, fallbackQueryHistoricalTransactions]) - const lastPendingBridgeTransaction: PendingBridgeTransaction = useMemo(() => { - return pendingBridgeTransactions?.[0] - }, [pendingBridgeTransactions]) + const lastPendingBridgeTransaction: PendingBridgeTransaction = + pendingBridgeTransactions?.[0] - const lastPendingTransaction: BridgeTransaction = useMemo(() => { - return pendingAwaitingCompletionTransactionsWithFallback?.[0] - }, [pendingAwaitingCompletionTransactionsWithFallback]) + const lastPendingTransaction: BridgeTransaction = + pendingAwaitingCompletionTransactionsWithFallback?.[0] - const lastHistoricalTransaction: BridgeTransaction = useMemo(() => { - return userHistoricalTransactionsWithFallback?.[0] - }, [userHistoricalTransactionsWithFallback]) + const lastHistoricalTransaction: BridgeTransaction = + userHistoricalTransactionsWithFallback?.[0] const recentMinutesInUnix: number = 15 * 60 - const isLastHistoricalTransactionRecent: boolean = useMemo( - () => - currentTime - lastHistoricalTransaction?.toInfo?.time < - recentMinutesInUnix, - [currentTime] - ) + const isLastHistoricalTransactionRecent: boolean = + currentTime - lastHistoricalTransaction?.toInfo?.time < recentMinutesInUnix const seenLastHistoricalTransaction: boolean = useMemo(() => { if (!seenHistoricalTransactions || !userHistoricalTransactions) { @@ -133,136 +126,114 @@ export const MostRecentTransaction = () => { let transaction - return useMemo(() => { - if ( - isUserHistoricalTransactionsLoading || - isUserPendingTransactionsLoading - ) { - return null - } - - if (!masqueradeActive && lastPendingBridgeTransaction) { - transaction = lastPendingBridgeTransaction as PendingBridgeTransaction - return ( -
- -
- ) - } - - if (!masqueradeActive && lastPendingTransaction) { - transaction = lastPendingTransaction as BridgeTransaction - return ( -
- -
- ) - } - - if ( - !masqueradeActive && - lastHistoricalTransaction && - isLastHistoricalTransactionRecent && - !seenLastHistoricalTransaction - ) { - transaction = lastHistoricalTransaction as BridgeTransaction - return ( -
- -
- ) - } - }, [ - currentTime, - lastPendingBridgeTransaction, - lastHistoricalTransaction, - lastPendingTransaction, - userHistoricalTransactions, - isUserHistoricalTransactionsLoading, - isUserPendingTransactionsLoading, - seenHistoricalTransactions, - pendingAwaitingCompletionTransactions, - fallbackQueryHistoricalTransactions, - fallbackQueryPendingTransactions, - pendingBridgeTransactions, - masqueradeActive, - seenLastHistoricalTransaction, - isUserHistoricalTransactionsLoading, - isUserPendingTransactionsLoading, - ]) + if (isUserHistoricalTransactionsLoading || isUserPendingTransactionsLoading) { + return null + } + + if (!masqueradeActive && lastPendingBridgeTransaction) { + transaction = lastPendingBridgeTransaction as PendingBridgeTransaction + return ( +
+ +
+ ) + } + + if (!masqueradeActive && lastPendingTransaction) { + transaction = lastPendingTransaction as BridgeTransaction + return ( +
+ +
+ ) + } + + if ( + !masqueradeActive && + lastHistoricalTransaction && + isLastHistoricalTransactionRecent && + !seenLastHistoricalTransaction + ) { + transaction = lastHistoricalTransaction as BridgeTransaction + return ( +
+ +
+ ) + } } diff --git a/packages/synapse-interface/components/Portfolio/Transaction/PendingTransaction.tsx b/packages/synapse-interface/components/Portfolio/Transaction/PendingTransaction.tsx index 95b406929a..66c165627c 100644 --- a/packages/synapse-interface/components/Portfolio/Transaction/PendingTransaction.tsx +++ b/packages/synapse-interface/components/Portfolio/Transaction/PendingTransaction.tsx @@ -8,8 +8,6 @@ import { } from '@/slices/transactions/actions' import { BridgeType } from '@/slices/api/generated' import { getTimeMinutesFromNow } from '@/utils/time' -import { ARBITRUM, ETH } from '@/constants/chains/master' -import { USDC } from '@/constants/tokens/bridgeable' import { Transaction, TransactionProps, @@ -17,7 +15,6 @@ import { TransactionStatus, } from './Transaction' import { ApplicationState } from '@/slices/application/reducer' -import { BRIDGE_REQUIRED_CONFIRMATIONS } from '@/constants/bridge' import { TransactionOptions } from './TransactionOptions' import { getExplorerTxUrl, getExplorerAddressUrl } from '@/constants/urls' import { getTransactionExplorerLink } from './components/TransactionExplorerLink' @@ -27,6 +24,8 @@ import { useFallbackBridgeDestinationQuery } from '@/utils/hooks/useFallbackBrid import { useSynapseContext } from '@/utils/providers/SynapseProvider' import { DISCORD_URL } from '@/constants/urls' import { useApplicationState } from '@/slices/application/hooks' +import { getEstimatedBridgeTime } from '@/utils/getEstimatedBridgeTime' +import { useBridgeTxStatus } from '@/utils/hooks/useBridgeTxStatus' interface PendingTransactionProps extends TransactionProps { eventType?: number @@ -76,42 +75,11 @@ export const PendingTransaction = ({ } }, [transactionHash, isSubmitted, isCompleted]) - const estimatedCompletionInSeconds: number = useMemo(() => { - if (bridgeModuleName) { - return synapseSDK.getEstimatedTime(originChain?.id, bridgeModuleName) - } - - if (formattedEventType) { - const fetchedBridgeModuleName: string = - synapseSDK.getBridgeModuleName(formattedEventType) - return synapseSDK.getEstimatedTime( - originChain?.id, - fetchedBridgeModuleName - ) - } - // Fallback last resort estimated duration calculation - // Remove this when fallback origin queries return eventType - // CCTP Classification - if (originChain.id === ARBITRUM.id || originChain.id === ETH.id) { - const isCCTP: boolean = - originToken.addresses[originChain.id] === USDC.addresses[originChain.id] - if ((eventType === 10 || eventType === 11) && isCCTP) { - const attestationTime: number = 13 * 60 - return ( - (BRIDGE_REQUIRED_CONFIRMATIONS[originChain.id] * - originChain.blockTime) / - 1000 + - attestationTime - ) - } - } - // All other transactions - return originChain - ? (BRIDGE_REQUIRED_CONFIRMATIONS[originChain.id] * - originChain.blockTime) / - 1000 - : 0 - }, [originChain, eventType, originToken, bridgeModuleName, transactionHash]) + const estimatedCompletionInSeconds = getEstimatedBridgeTime({ + bridgeOriginChain: originChain, + bridgeModuleName, + formattedEventType, + }) const currentTime: number = Math.floor(Date.now() / 1000) @@ -120,7 +88,7 @@ export const PendingTransaction = ({ if (!isSubmitted || currentTime < startedTimestamp) { return 0 } else if (startedTimestamp < currentTime) { - return Math.floor((currentTime - startedTimestamp) / 60) + return Math.ceil((currentTime - startedTimestamp) / 60) } else { return 0 } @@ -162,34 +130,19 @@ export const PendingTransaction = ({ estimatedCompletionInSeconds / 60 ) - const timeRemaining: number = useMemo(() => { - return estimatedCompletionInMinutes - initialElapsedMinutes - }, [ - estimatedCompletionInMinutes, - initialElapsedMinutes, - updatedElapsedTime, - startedTimestamp, - transactionHash, - ]) + const timeRemaining: number = + estimatedCompletionInMinutes - initialElapsedMinutes - const isDelayed: boolean = useMemo(() => timeRemaining < 0, [timeRemaining]) + const isDelayed: boolean = timeRemaining < -1 - const isSignificantlyDelayed: boolean = useMemo(() => { - if (isDelayed) { - return timeRemaining < -5 - } - return false - }, [timeRemaining, estimatedCompletionInMinutes, isDelayed]) + const isSignificantlyDelayed: boolean = isDelayed && timeRemaining < -5 // Set fallback period to extend 5 mins past estimated duration - const useFallback: boolean = useMemo( - () => timeRemaining >= -5 && timeRemaining <= 1 && !isCompleted, - [timeRemaining, isCompleted] - ) + const useFallback: boolean = + timeRemaining >= -5 && timeRemaining <= 1 && !isCompleted - const isReconnectedAndRetryFallback: boolean = useMemo(() => { - return updatedCurrentTime - lastConnectedTimestamp < 300 - }, [lastConnectedTimestamp, updatedCurrentTime]) + const isReconnectedAndRetryFallback: boolean = + updatedCurrentTime - lastConnectedTimestamp < 300 const bridgeType: BridgeType = useMemo(() => { if (synapseSDK && formattedEventType) { @@ -206,19 +159,15 @@ export const PendingTransaction = ({ return BridgeType.Bridge }, [synapseSDK, bridgeModuleName, formattedEventType]) - const originFallback = useFallbackBridgeOriginQuery({ - useFallback: - (isDelayed && useFallback) || - (isDelayed && isReconnectedAndRetryFallback), + useFallbackBridgeOriginQuery({ + useFallback: useFallback || (isDelayed && isReconnectedAndRetryFallback), chainId: originChain?.id, txnHash: transactionHash, bridgeType: bridgeType, }) - const destinationFallback = useFallbackBridgeDestinationQuery({ - useFallback: - (isDelayed && useFallback) || - (isDelayed && isReconnectedAndRetryFallback), + useFallbackBridgeDestinationQuery({ + useFallback: useFallback || (isDelayed && isReconnectedAndRetryFallback), chainId: destinationChain?.id, address: destinationAddress, kappa: kappa, @@ -226,6 +175,16 @@ export const PendingTransaction = ({ bridgeType: bridgeType, }) + const _isComplete = useBridgeTxStatus({ + originChainId: originChain.id, + destinationChainId: destinationChain.id, + transactionHash, + bridgeModuleName, + kappa, + checkStatus: isDelayed, + elapsedTime: updatedElapsedTime, + }) + useEffect(() => { if (!isSubmitted && transactionHash) { const maxRetries = 3 @@ -304,7 +263,7 @@ export const PendingTransaction = ({ } transactionHash={transactionHash} transactionStatus={transactionStatus} - isCompleted={isCompleted} + isCompleted={isCompleted ?? _isComplete} kappa={kappa} > { const sharedClass: string = - 'flex bg-tint border-t border-surface text-sm items-center' + 'flex bg-tint border-t border-surface text-sm items-center rounded-b-lg' if (transactionStatus === TransactionStatus.PENDING_WALLET_ACTION) { return ( @@ -360,7 +321,7 @@ const TransactionStatusDetails = ({ return (
Initiating...
{isDelayed && isSignificantlyDelayed && ( <> @@ -534,14 +495,27 @@ const TransactionStatusDetails = ({ } if (transactionStatus === TransactionStatus.COMPLETED) { - const handleExplorerClick = () => { - const explorerLink: string = getTransactionExplorerLink({ - kappa, - fromChainId: originChain.id, - toChainId: destinationChain.id, - }) - window.open(explorerLink, '_blank', 'noopener,noreferrer') + let handleExplorerClick + + if (!kappa) { + handleExplorerClick = () => { + const explorerLink: string = getExplorerAddressUrl({ + chainId: destinationChain.id, + address: connectedAddress as Address, + }) + window.open(explorerLink, '_blank', 'noopener,noreferrer') + } + } else { + handleExplorerClick = () => { + const explorerLink: string = getTransactionExplorerLink({ + kappa, + fromChainId: originChain.id, + toChainId: destinationChain.id, + }) + window.open(explorerLink, '_blank', 'noopener,noreferrer') + } } + return (
-
Confirmed on Synapse Explorer
+
Confirmed on Explorer
- {!destinationIsSender && ( + {isDestinationValid && !isDestinationSender && (
to {shortenAddress(destinationAddress)}
)} {isToday ? ( @@ -37,7 +40,9 @@ export const Completed = ({ Today
) : ( -
{formattedTime}
+
+ {formattedTime ? formattedTime : 'Completed'} +
)} ) diff --git a/packages/synapse-interface/components/StateManagedBridge/ToTokenListOverlay.tsx b/packages/synapse-interface/components/StateManagedBridge/ToTokenListOverlay.tsx index 1aca37c638..59fbb17864 100644 --- a/packages/synapse-interface/components/StateManagedBridge/ToTokenListOverlay.tsx +++ b/packages/synapse-interface/components/StateManagedBridge/ToTokenListOverlay.tsx @@ -21,7 +21,6 @@ import { CloseButton } from './components/CloseButton' import { SearchResults } from './components/SearchResults' import { formatBigIntToString } from '@/utils/bigint/format' import { FetchState } from '@/slices/portfolio/actions' -import { calculateEstimatedTransactionTime } from '@/utils/calculateEstimatedTransactionTime' interface TokenWithRates extends Token { exchangeRate: bigint diff --git a/packages/synapse-interface/slices/transactions/updater.tsx b/packages/synapse-interface/slices/transactions/updater.tsx index 180062b24b..c9d6d2b77e 100644 --- a/packages/synapse-interface/slices/transactions/updater.tsx +++ b/packages/synapse-interface/slices/transactions/updater.tsx @@ -61,7 +61,9 @@ export default function Updater(): null { }: PortfolioState = usePortfolioState() const [fetchUserHistoricalActivity, fetchedHistoricalActivity] = - useLazyGetUserHistoricalActivityQuery({ pollingInterval: POLLING_INTERVAL }) + useLazyGetUserHistoricalActivityQuery({ + pollingInterval: POLLING_INTERVAL, + }) const [fetchUserPendingActivity, fetchedPendingActivity] = useLazyGetUserPendingTransactionsQuery({ @@ -211,17 +213,24 @@ export default function Updater(): null { // Store pending transactions until completed based on Explorer query useEffect(() => { - const hasUserPendingTransactions: boolean = - Array.isArray(userPendingTransactions) && - !isUserPendingTransactionsLoading - - if (hasUserPendingTransactions) { + if (checkTransactionsExist(userPendingTransactions)) { userPendingTransactions.forEach( (pendingTransaction: BridgeTransaction) => { - const isStored: boolean = pendingAwaitingCompletionTransactions?.some( - (storedTransaction: BridgeTransaction) => - storedTransaction?.kappa === pendingTransaction?.kappa - ) + let isStored: boolean = false + + if (checkTransactionsExist(pendingAwaitingCompletionTransactions)) { + isStored = pendingAwaitingCompletionTransactions?.some( + (storedTransaction: BridgeTransaction) => + storedTransaction?.kappa === pendingTransaction?.kappa + ) + } + + if (checkTransactionsExist(fallbackQueryHistoricalTransactions)) { + isStored = fallbackQueryHistoricalTransactions?.some( + (storedTransaction: BridgeTransaction) => + storedTransaction?.kappa === pendingTransaction?.kappa + ) + } if (!isStored) { dispatch( @@ -231,16 +240,16 @@ export default function Updater(): null { } ) } - }, [userPendingTransactions]) + }, [userPendingTransactions, fallbackQueryHistoricalTransactions]) // Handle updating stored pending transactions state throughout progress useEffect(() => { const hasUserHistoricalTransactions: boolean = - Array.isArray(userHistoricalTransactions) && + checkTransactionsExist(userHistoricalTransactions) && !isUserHistoricalTransactionsLoading const hasPendingBridgeTransactions: boolean = - Array.isArray(pendingBridgeTransactions) && + checkTransactionsExist(pendingBridgeTransactions) && pendingBridgeTransactions.length > 0 if (hasUserHistoricalTransactions && activeTab === PortfolioTabs.ACTIVITY) { @@ -320,7 +329,11 @@ export default function Updater(): null { } ) } - }, [userHistoricalTransactions, activeTab]) + }, [ + activeTab, + userHistoricalTransactions, + fallbackQueryHistoricalTransactions, + ]) // Handle adding completed fallback historical transaction to seen list useEffect(() => { @@ -383,7 +396,7 @@ export default function Updater(): null { */ useEffect(() => { const hasUserHistoricalTransactions: boolean = - Array.isArray(userHistoricalTransactions) && + checkTransactionsExist(userHistoricalTransactions) && !isUserHistoricalTransactionsLoading if ( diff --git a/packages/synapse-interface/utils/calculateEstimatedTransactionTime.tsx b/packages/synapse-interface/utils/calculateEstimatedTransactionTime.tsx deleted file mode 100644 index a1a7f9ea54..0000000000 --- a/packages/synapse-interface/utils/calculateEstimatedTransactionTime.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Chain, Token } from './types' -import { Address } from 'viem' -import { BRIDGE_REQUIRED_CONFIRMATIONS } from '@/constants/bridge' -import { tokenAddressToToken } from '@/constants/tokens' -import { ARBITRUM, ETH } from '@/constants/chains/master' -import { USDC } from '@/constants/tokens/bridgeable' -import { CHAINS_BY_ID } from '@/constants/chains' - -// Utility function to fetch estimated transaction time -// Returned as a number, in seconds -export const calculateEstimatedTransactionTime = ({ - originChainId, - originTokenAddress, -}: { - originChainId: number - originTokenAddress: Address -}): number => { - const originChain: Chain = CHAINS_BY_ID[originChainId] - const originToken: Token = tokenAddressToToken( - originChainId, - originTokenAddress - ) - const baseEstimatedCompletionInSeconds: number = - (BRIDGE_REQUIRED_CONFIRMATIONS[originChainId] * originChain?.blockTime) / - 1000 - - let estimatedCompletionInSeconds: number - - // Specific to CCTP Transactions - if (originChainId === ARBITRUM.id || originChainId === ETH.id) { - const isCCTP: boolean = originTokenAddress === USDC.addresses[originChainId] - const attestationTime: number = 13 * 60 - - estimatedCompletionInSeconds = - baseEstimatedCompletionInSeconds + attestationTime - - return isCCTP - ? estimatedCompletionInSeconds - : baseEstimatedCompletionInSeconds - } - - return baseEstimatedCompletionInSeconds -} diff --git a/packages/synapse-interface/utils/getEstimatedBridgeTime.tsx b/packages/synapse-interface/utils/getEstimatedBridgeTime.tsx new file mode 100644 index 0000000000..69cf4eb41e --- /dev/null +++ b/packages/synapse-interface/utils/getEstimatedBridgeTime.tsx @@ -0,0 +1,68 @@ +import { useSynapseContext } from './providers/SynapseProvider' +import { BRIDGE_REQUIRED_CONFIRMATIONS } from '@/constants/bridge' +import { Chain } from './types' + +enum SynapseBridgeModule { + BRIDGE = 'SynapseBridge', + CCTP = 'SynapseCCTP', +} + +/** + * Fetches estimated duration of Bridge Transaction from Synapse SDK + * + * @param bridgeOriginChain - The selected origin chain. + * @param bridgeModuleName - The name of the bridge module. e.g 'Bridge' or 'CCTP'. + * @param formattedEventType - The name of the bridge event. + * @returns - The estimated time for a bridge operation, in seconds. + */ +export const getEstimatedBridgeTime = ({ + bridgeOriginChain, + bridgeModuleName, + formattedEventType, +}: { + bridgeOriginChain: Chain + bridgeModuleName?: string + formattedEventType?: string +}) => { + const { synapseSDK } = useSynapseContext() + + if (!bridgeOriginChain) return null + + if (bridgeModuleName) { + return synapseSDK.getEstimatedTime(bridgeOriginChain.id, bridgeModuleName) + } + + if (formattedEventType) { + const fetchedBridgeModuleName: string = + synapseSDK.getBridgeModuleName(formattedEventType) + + return synapseSDK.getEstimatedTime( + bridgeOriginChain?.id, + fetchedBridgeModuleName + ) + } + + // Fallback estimated time when inputs invalid + return synapseSDK.getEstimatedTime( + bridgeOriginChain.id, + SynapseBridgeModule.BRIDGE + ) +} + +export const getEstimatedBridgeTimeInMinutes = ({ + bridgeOriginChain, + bridgeModuleName, + formattedEventType, +}: { + bridgeOriginChain: Chain + bridgeModuleName?: string + formattedEventType?: string +}) => { + const estimatedBridgeTime = getEstimatedBridgeTime({ + bridgeOriginChain, + bridgeModuleName, + formattedEventType, + }) + + return estimatedBridgeTime ? Math.ceil(estimatedBridgeTime / 60) : null +} diff --git a/packages/synapse-interface/utils/hooks/useBridgeTxStatus.tsx b/packages/synapse-interface/utils/hooks/useBridgeTxStatus.tsx new file mode 100644 index 0000000000..067707e9c9 --- /dev/null +++ b/packages/synapse-interface/utils/hooks/useBridgeTxStatus.tsx @@ -0,0 +1,72 @@ +import { useState, useEffect } from 'react' +import { useSynapseContext } from '@/utils/providers/SynapseProvider' + +interface UseBridgeTxStatusProps { + originChainId: number + destinationChainId: number + transactionHash: string + bridgeModuleName?: string + kappa?: string + checkStatus: boolean + elapsedTime: number // used as trigger to refetch status +} + +export const useBridgeTxStatus = ({ + originChainId, + destinationChainId, + transactionHash, + bridgeModuleName, + kappa, + checkStatus = false, + elapsedTime, +}: UseBridgeTxStatusProps) => { + const [isComplete, setIsComplete] = useState(false) + const { synapseSDK } = useSynapseContext() + + const getKappa = async (): Promise => { + if (!bridgeModuleName || !originChainId || !transactionHash) return + return await synapseSDK.getSynapseTxId( + originChainId, + bridgeModuleName, + transactionHash + ) + } + + const getBridgeTxStatus = async ( + destinationChainId: number, + bridgeModuleName: string, + kappa: string + ) => { + if (!destinationChainId || !bridgeModuleName || !kappa) return null + return await synapseSDK.getBridgeTxStatus( + destinationChainId, + bridgeModuleName, + kappa + ) + } + + useEffect(() => { + if (!checkStatus) return + ;(async () => { + let _kappa + + if (!kappa) { + _kappa = await getKappa() + } else { + _kappa = kappa + } + + const txStatus = await getBridgeTxStatus( + destinationChainId, + bridgeModuleName, + _kappa + ) + + if (txStatus !== null) { + setIsComplete(txStatus) + } + })() + }, [elapsedTime, checkStatus]) + + return isComplete +}