From a5dc3a4addcd6f8a3469cf0d4203cbbe88c9ba81 Mon Sep 17 00:00:00 2001 From: cyan Date: Thu, 8 Oct 2026 12:49:34 +0200 Subject: [PATCH] fix: dismissed transaction that finishes completing async (#3687) can overwrite new one GHSA-5p55-jf6f-c7wx --- lib/view_model/send/send_view_model.dart | 70 ++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 07ab72b9b..28c80f4e9 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -336,6 +336,12 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor @observable PendingTransaction? pendingTransaction; + // Bumped when a preparation is dismissed or replaced. + // + // Finished call sets pendingTransaction only if it matches this value to + // its own value. + int _sendPreparationId = 0; + String? get pendingTransactionAdditionalCostNotice { final additionalCost = pendingTransaction?.additionalCost; @@ -596,6 +602,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor @action Future dismissTransaction() async { + _sendPreparationId++; + _ledgerTxStateTimer?.cancel(); + _ledgerTxStateTimer = null; state = InitialExecutionState(); if (ocpRequest != null) { clearOutputs(); @@ -688,19 +697,36 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor Future createTransaction({ExchangeProvider? provider, Trade? trade}) async { _currentTrade = trade; _currentProvider = provider; + final preparationId = ++_sendPreparationId; + _ledgerTxStateTimer?.cancel(); + _ledgerTxStateTimer = null; pendingTransaction = null; + Timer? preparationTimer; + + bool replaced() { + if (preparationId == _sendPreparationId) { + return false; + } + preparationTimer?.cancel(); + return true; + } try { if (!(state is IsExecutingState)) state = IsExecutingState(); if (wallet.isHardwareWallet) { if (walletType == WalletType.monero) { - _ledgerTxStateTimer = Timer.periodic(Duration(seconds: 1), (timer) { + preparationTimer = Timer.periodic(Duration(seconds: 1), (timer) { + if (preparationId != _sendPreparationId) { + timer.cancel(); + return; + } if (monero!.getLastLedgerCommand() == "INS_CLSAG") { timer.cancel(); state = IsDeviceSigningResponseState(); } }); + _ledgerTxStateTimer = preparationTimer; } else { state = IsAwaitingDeviceResponseState(); } @@ -728,7 +754,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor // Direct Transfer (Simple routing, no approval needed) if (selector == transferSig) { - pendingTransaction = await evm!.createRawCallDataTransaction( + final prepared = await evm!.createRawCallDataTransaction( wallet, routerTo, routerData, @@ -738,6 +764,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor ? _settingsStore.useBlinkProtection : false, ); + if (replaced()) { + return null; + } + pendingTransaction = prepared; state = ExecutedSuccessfullyState(); return pendingTransaction; } @@ -784,6 +814,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor requiredAmount: BigInt.zero, // Wait until it equals 0 waitForExactMatch: true, ); + if (replaced()) { + return null; + } if (!resetConfirmed) { state = FailureState('Failed to reset USDT allowance. Please try again.'); @@ -803,6 +836,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor sourceTokenDecimals: trade.sourceTokenDecimals, priority: priority); + if (replaced()) { + return null; + } if (approvalTx == null) { state = FailureState('Failed to build approval transaction'); return null; @@ -821,6 +857,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor spender: routerTo, requiredAmount: requiredAmount, ); + if (replaced()) { + return null; + } if (!isApproved) { state = FailureState( @@ -831,6 +870,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor '[Swaps.xyz sending flow] Approval transaction confirmed on-chain. Proceeding with swap execution.'); } catch (e, s) { printV('[Swaps.xyz sending flow] Approval transaction error: $e\n$s'); + if (replaced()) { + return null; + } state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency)); return null; } @@ -838,7 +880,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor // Construct Final Swap Transaction printV('[Swaps.xyz sending flow] Building swap transaction'); - pendingTransaction = await evm!.createRawCallDataTransaction( + final prepared = await evm!.createRawCallDataTransaction( wallet, routerTo, routerData, @@ -850,6 +892,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor ? _settingsStore.useBlinkProtection : false, ); + if (replaced()) { + return null; + } + pendingTransaction = prepared; state = ExecutedSuccessfullyState(); return pendingTransaction; @@ -859,6 +905,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor return null; } catch (e, s) { printV('Swaps.xyz transaction error: $e\n$s'); + if (replaced()) { + return null; + } state = FailureState( 'Failed to create Swaps.xyz transaction - ${translateErrorMessage(e, wallet.type, wallet.currency)}'); return null; @@ -886,7 +935,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor strictParsing: false, ) ?? Money.zero(fromCurrency); - pendingTransaction = await solana!.signAndPrepareJupiterSwapTransaction( + final prepared = await solana!.signAndPrepareJupiterSwapTransaction( wallet, swapTransactionBase64!, requestId!, @@ -894,6 +943,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor amount, Money.tryParse(fee.toString(), CryptoCurrency.sol) ?? Money.zero(CryptoCurrency.sol), ); + if (replaced()) { + return null; + } + pendingTransaction = prepared; state = ExecutedSuccessfullyState(); return pendingTransaction; @@ -916,7 +969,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor } } - pendingTransaction = await wallet.createTransaction(_credentials(provider)); + final prepared = await wallet.createTransaction(_credentials(provider)); + if (replaced()) { + return null; + } + pendingTransaction = prepared; final txAmountDouble = double.tryParse(pendingTransaction?.amountFormatted ?? '0') ?? 0.0; final bool isTradeTx = trade != null && provider != null; @@ -964,6 +1021,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor state = ExecutedSuccessfullyState(); return pendingTransaction; } catch (e) { + if (replaced()) { + return null; + } _ledgerTxStateTimer?.cancel(); // if (e is LedgerException) { // final errorCode = e.errorCode.toRadixString(16);