@@ -566,6 +566,44 @@ export async function getUpdatedCctpTransfer(tx: MergedTransaction): Promise<Mer
566566 return { ...tx , status : WithdrawalStatus . UNCONFIRMED } ;
567567}
568568
569+ // A LiFi entry is cached optimistically once the wallet returns a source tx hash. If that
570+ // transaction never actually lands on-chain (reverted, or dropped from the mempool and never
571+ // mined), the entry would otherwise sit "pending" forever — LiFi's status API keeps reporting
572+ // PENDING for a hash it never saw. We only give up after a grace window, so a tx that is merely
573+ // still propagating isn't reconciled away prematurely.
574+ const LIFI_SOURCE_TX_DROPPED_THRESHOLD_MS = 1000 * 60 * 30 ; // 30 minutes
575+
576+ /**
577+ * Corroborating on-chain check, used only when LiFi's status API has no record of the source tx
578+ * executing. Returns true only when the cached source tx definitively did not succeed: it
579+ * reverted, or (past the grace window) is neither mined nor still in the mempool. A transient RPC
580+ * error returns false, so a healthy transfer is never turned terminal on a single bad read.
581+ */
582+ async function isLifiSourceTxDropped ( tx : LifiMergedTransaction ) : Promise < boolean > {
583+ if (
584+ typeof tx . createdAt !== 'number' ||
585+ dayjs ( ) . valueOf ( ) - tx . createdAt <= LIFI_SOURCE_TX_DROPPED_THRESHOLD_MS
586+ ) {
587+ return false ;
588+ }
589+
590+ try {
591+ const sourceChainProvider = getProviderForChainId ( tx . sourceChainId ) ;
592+ const sourceTxReceipt = await sourceChainProvider . getTransactionReceipt ( tx . txId ) ;
593+
594+ if ( sourceTxReceipt ) {
595+ // Mined: only counts as failed if it reverted on-chain.
596+ return sourceTxReceipt . status === 0 ;
597+ }
598+
599+ // No receipt: dropped only if the node no longer knows the tx (not mined, not in mempool).
600+ const pendingSourceTx = await sourceChainProvider . getTransaction ( tx . txId ) ;
601+ return ! pendingSourceTx ;
602+ } catch {
603+ return false ;
604+ }
605+ }
606+
569607export async function getUpdatedLifiTransfer (
570608 tx : LifiMergedTransaction ,
571609) : Promise < MergedTransaction > {
@@ -610,7 +648,13 @@ export async function getUpdatedLifiTransfer(
610648 sourceStatus = WithdrawalStatus . CONFIRMED ;
611649 destinationStatus = WithdrawalStatus . UNCONFIRMED ;
612650 } else {
613- sourceStatus = WithdrawalStatus . UNCONFIRMED ;
651+ // LiFi has no record of the source tx executing. Past the grace window, corroborate against
652+ // the chain: if it reverted or was dropped (never mined, not in mempool), fail it instead of
653+ // leaving it pending forever. Both signals are required, so a transfer LiFi simply hasn't
654+ // indexed yet — or a transient RPC miss — won't be turned terminal.
655+ sourceStatus = ( await isLifiSourceTxDropped ( tx ) )
656+ ? WithdrawalStatus . FAILURE
657+ : WithdrawalStatus . UNCONFIRMED ;
614658 destinationStatus = WithdrawalStatus . UNCONFIRMED ;
615659 }
616660 if ( 'txHash' in statusResponse . receiving ) {
0 commit comments