Skip to content

Commit 5125cfd

Browse files
fix(bridge): clear stale LiFi pending transfers whose source tx never landed
1 parent 4bf26fc commit 5125cfd

1 file changed

Lines changed: 41 additions & 0 deletions

File tree

  • packages/arb-token-bridge-ui/src/components/TransactionHistory

packages/arb-token-bridge-ui/src/components/TransactionHistory/helpers.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,13 @@ 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 treat a missing transaction as dropped after a grace
573+
// window, so a tx that is merely still propagating isn't reconciled away prematurely.
574+
const LIFI_SOURCE_TX_DROPPED_THRESHOLD_MS = 1000 * 60 * 30; // 30 minutes
575+
569576
export async function getUpdatedLifiTransfer(
570577
tx: LifiMergedTransaction,
571578
): Promise<MergedTransaction> {
@@ -578,6 +585,40 @@ export async function getUpdatedLifiTransfer(
578585
return tx;
579586
}
580587

588+
// Vet the cached source transaction against the chain before trusting LiFi's status.
589+
try {
590+
const sourceChainProvider = getProviderForChainId(tx.sourceChainId);
591+
const sourceTxReceipt = await sourceChainProvider.getTransactionReceipt(tx.txId);
592+
593+
if (sourceTxReceipt) {
594+
if (sourceTxReceipt.status === 0) {
595+
// Source transaction reverted on-chain — terminal failure.
596+
return {
597+
...tx,
598+
status: WithdrawalStatus.FAILURE,
599+
destinationStatus: WithdrawalStatus.FAILURE,
600+
};
601+
}
602+
} else {
603+
const pendingSourceTx = await sourceChainProvider.getTransaction(tx.txId);
604+
const isOlderThanDropThreshold =
605+
typeof tx.createdAt === 'number' &&
606+
dayjs().valueOf() - tx.createdAt > LIFI_SOURCE_TX_DROPPED_THRESHOLD_MS;
607+
608+
// No receipt and the node no longer knows the tx: it was dropped and never mined, so the
609+
// funds never moved. Mark terminal so it stops counting as a pending transfer.
610+
if (!pendingSourceTx && isOlderThanDropThreshold) {
611+
return {
612+
...tx,
613+
status: WithdrawalStatus.FAILURE,
614+
destinationStatus: WithdrawalStatus.FAILURE,
615+
};
616+
}
617+
}
618+
} catch {
619+
// RPC hiccup — don't make a terminal decision; fall back to LiFi's status below.
620+
}
621+
581622
const statusResponse = await getStatus({
582623
txHash: tx.txId,
583624
bridge: tx.toolDetails.key,

0 commit comments

Comments
 (0)