What changed, and why it matters
This commit is a routine 'Cleanup and Bug fixes' patch for the Cake Wallet cryptocurrency app. It mainly refactors transaction-committing code, fixes a possible crash when closing a payment-result screen, adds retry logic for Payjoin network setup, and improves how the exchange screen decides whether the deposit currency matches the current wallet. There is no clear security vulnerability being fixed, but the changes do remove some fragile UI behavior and make transaction handling more consistent.
Treat as a normal bug-fix/cleanup commit. Review the new isDepositSameCurrency logic for correctness with token variants, verify the Payjoin retry does not loop indefinitely on persistent errors, and ensure the refactored commitTransaction state transitions still handle cancellation and failures correctly. No urgent security response is indicated.
Security signals we found
Refactoring of transaction commit flow with added try/catch and state management
Guard added before Navigator.pop to prevent calling pop on an unmounted or non-poppable route
Retry loop added to Payjoin OHTTP key fetch (up to 5 retries) for transient network errors
Currency matching logic broadened from equality to tag/title comparison in exchange deposit handling
Removal of a hard-coded XMR deposit currency warning dialog in exchange UI
Evidence from the diff
The diff refactors SendViewModel.commitTransaction into helper methods (_handleOcpRequest, _commitApprovalTransaction, _commitUR, _addTransactionDescription), adds a mounted/canPop guard before Navigator.pop in SendCard, introduces retry logic in PayjoinManager.initReceiver for transient OHTTP key fetch failures, adds NanoWallet.updateTransactionsHistory as a wrapper, and replaces direct currency equality checks in ExchangeViewModel with a new isDepositSameCurrency getter that also compares currency tag/title. The Podfile.lock checksums are updated but no version metadata is supplied. No explicit security issue or CVE is referenced.
Changed components
cw_bitcoin/lib/payjoin/manager.dartcw_nano/lib/nano_wallet.dartlib/src/screens/exchange/exchange_page.dartlib/src/screens/send/widgets/send_card.dartlib/view_model/exchange/exchange_view_model.dartlib/view_model/send/send_view_model.dartInspect captured patch +158 / −153
diff --git a/cw_bitcoin/lib/payjoin/manager.dart b/cw_bitcoin/lib/payjoin/manager.dart
index b1886cb4..ca7e262c 100644
--- a/cw_bitcoin/lib/payjoin/manager.dart
+++ b/cw_bitcoin/lib/payjoin/manager.dart
@@ -165,25 +165,33 @@ class PayjoinManager {
return initReceiver(address);
}
- Future<Receiver> initReceiver(String address, [bool isTestnet = false]) async {
- final ohttpKeys = await PayjoinUri.fetchOhttpKeys(
- ohttpRelay: await randomOhttpRelayUrl(),
- payjoinDirectory: payjoinDirectoryUrl,
- );
+ Future<Receiver> initReceiver(String address, [bool isTestnet = false, int retryCount = 0]) async {
+ try {
+ final ohttpKeys = await PayjoinUri.fetchOhttpKeys(
+ ohttpRelay: await randomOhttpRelayUrl(),
+ payjoinDirectory: payjoinDirectoryUrl,
+ );
- final newReceiver = await NewReceiver.create(
- address: address,
- network: isTestnet ? Network.testnet : Network.bitcoin,
- directory: payjoinDirectoryUrl,
- ohttpKeys: ohttpKeys,
- );
- final persister = PayjoinReceiverPersister.impl();
- final receiverToken = await newReceiver.persist(persister: persister);
- final receiver = await Receiver.load(persister: persister, token: receiverToken);
+ final newReceiver = await NewReceiver.create(
+ address: address,
+ network: isTestnet ? Network.testnet : Network.bitcoin,
+ directory: payjoinDirectoryUrl,
+ ohttpKeys: ohttpKeys,
+ );
+ final persister = PayjoinReceiverPersister.impl();
+ final receiverToken = await newReceiver.persist(persister: persister);
+ final receiver = await Receiver.load(persister: persister, token: receiverToken);
- await _payjoinStorage.insertReceiverSession(receiver, _wallet.id);
+ await _payjoinStorage.insertReceiverSession(receiver, _wallet.id);
- return receiver;
+ return receiver;
+ } catch (e) {
+ if (e.toString().contains("error sending request for url") && retryCount < 5) {
+ return initReceiver(address, isTestnet, ++retryCount);
+ } else {
+ rethrow;
+ }
+ }
}
Future<void> spawnReceiver({
diff --git a/cw_nano/lib/nano_wallet.dart b/cw_nano/lib/nano_wallet.dart
index a0761254..cab02921 100644
--- a/cw_nano/lib/nano_wallet.dart
+++ b/cw_nano/lib/nano_wallet.dart
@@ -261,6 +261,8 @@ abstract class NanoWalletBase
}
}
+ Future<void> updateTransactionsHistory() async => await updateTransactions();
+
Future<bool> updateTransactions() async {
try {
if (_isTransactionUpdating) {
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 7919129c..50399668 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -231,46 +231,46 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS:
- bitbox_flutter: 9505732798041c413152669751beeaecc5fe400f
- connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
+ bitbox_flutter: 506f80b961ddf646b0d80cef9f6eadaab96d91b0
+ connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
CryptoSwift: e64e11850ede528a02a0f3e768cec8e9d92ecb90
- cw_decred: a02cf30175a46971c1e2fa22c48407534541edc6
- cw_mweb: 3aea2fb35b2bd04d8b2d21b83216f3b8fb768d85
- device_display_brightness: 04374ebd653619292c1d996f00f42877ea19f17f
- device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89
- devicelocale: bd64aa714485a8afdaded0892c1e7d5b7f680cf8
+ cw_decred: 9c0e1df74745b51a1289ec5e91fb9e24b68fa14a
+ cw_mweb: 22cd01dfb8ad2d39b15332006f22046aaa8352a3
+ device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7
+ device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
+ devicelocale: 35ba84dc7f45f527c3001535d8c8d104edd5d926
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
dnssec_proof: d461cac7bd3301eb7447f87936745a0c1ae0a67e
- fast_scanner: 2cb1ad3e69e645e9980fb4961396ce5804caa3e3
- file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517
+ fast_scanner: 44c00940355a51258cd6c2085734193cd23d95bc
+ file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
- flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
- flutter_local_authentication: 989278c681612f1ee0e36019e149137f114b9d7f
- flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
- flutter_mailer: 3a8cd4f36c960fb04528d5471097270c19fec1c4
- flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418
- fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
- image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
- in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937
- integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
+ flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
+ flutter_local_authentication: 1172a4dd88f6306dadce067454e2c4caf07977bb
+ flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f
+ flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
+ flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
+ fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
+ image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
+ in_app_review: 436034b18594851a7328d7f1c2ed5ec235b79cfc
+ integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
- package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
- path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
+ package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
+ path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
payjoin_flutter: d9d4c8aa16bd5dfedb9b21d0edc8199e0187d96e
- permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
- reown_yttrium: cee334ade64725b1d83f7b34c706a6aae2696d58
+ permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
+ reown_yttrium: c0e87e5965fa60a3559564cc35cffbba22976089
SDWebImage: 9f177d83116802728e122410fb25ad88f5c7608a
- sensitive_clipboard: 161e9abc3d56b3131309d8a321eb4690a803c16b
- share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
- shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
- sp_scanner: b1bc9321690980bdb44bba7ec85d5543e716d1b5
+ sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986
+ share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
+ shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
+ sp_scanner: eaa617fa827396b967116b7f1f43549ca62e9a12
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
- torch_dart: f4620705d10f05492fab047f2fa1c3a600e7d17d
- uni_links: ed8c961e47ed9ce42b6d91e1de8049e38a4b3152
- universal_ble: ff19787898040d721109c6324472e5dd4bc86adc
- url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
- wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
+ torch_dart: d2cf778332cc6e6a3b362dcf45e4dde52bc34e35
+ uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
+ universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
+ url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
+ wakelock_plus: 76957ab028e12bfa4e66813c99e46637f367fc7e
YttriumWrapper: 31e937fe9fbe0f1314d2ca6be9ce9b379a059966
PODFILE CHECKSUM: 5296465b1c6d14d506230356756826012f65d97a
diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart
index db0114a8..823706d9 100644
--- a/lib/src/screens/exchange/exchange_page.dart
+++ b/lib/src/screens/exchange/exchange_page.dart
@@ -629,7 +629,7 @@ class ExchangePage extends BasePage {
void _onCurrencyChange(CryptoCurrency currency, ExchangeViewModel exchangeViewModel,
GlobalKey<ExchangeCardState> key) {
- final isCurrentTypeWallet = currency == exchangeViewModel.wallet.currency;
+ final isCurrentTypeWallet = exchangeViewModel.isDepositSameCurrency;
key.currentState!.changeSelectedCurrency(currency);
key.currentState!.changeWalletName(isCurrentTypeWallet ? exchangeViewModel.wallet.name : '');
@@ -708,22 +708,6 @@ class ExchangePage extends BasePage {
isMoneroWallet: exchangeViewModel.isMoneroWallet,
currencies: exchangeViewModel.depositCurrencies,
onCurrencySelected: (currency) {
- // FIXME: need to move it into view model
- if (currency == CryptoCurrency.xmr &&
- exchangeViewModel.wallet.type != WalletType.monero) {
- showPopUp<void>(
- context: context,
- builder: (dialogContext) {
- return AlertWithOneAction(
- alertTitle: S.of(context).error,
- alertContent: S.of(context).exchange_incorrect_current_wallet_for_xmr,
- buttonText: S.of(context).ok,
- buttonAction: () => Navigator.of(dialogContext).pop(),
- );
- });
- return;
- }
-
exchangeViewModel.changeDepositCurrency(currency: currency);
},
currencyButtonColor: Colors.transparent,
diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart
index dee54419..fed3ebfd 100644
--- a/lib/src/screens/send/widgets/send_card.dart
+++ b/lib/src/screens/send/widgets/send_card.dart
@@ -243,7 +243,9 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
PaymentRequest paymentRequest,
PaymentFlowResult result,
) async {
- Navigator.of(context).pop();
+ if (context.mounted && Navigator.of(context).canPop()) {
+ Navigator.of(context).pop();
+ }
if (result.wallet != null) {
walletSwitcherViewModel.selectWallet(result.wallet!);
diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart
index 0642362c..112072d1 100644
--- a/lib/view_model/exchange/exchange_view_model.dart
+++ b/lib/view_model/exchange/exchange_view_model.dart
@@ -150,12 +150,11 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
}
});
- isDepositAddressEnabled = !(depositCurrency == wallet.currency);
+ isDepositAddressEnabled = !(isDepositSameCurrency);
depositAmount = '';
receiveAmount = '';
receiveAddress = '';
- depositAddress =
- depositCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : '';
+ depositAddress = isDepositSameCurrency ? wallet.walletAddresses.addressForExchange : '';
provider = providerList.firstOrNull;
final initialProvider = provider;
@@ -190,6 +189,11 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
}
}
+ bool get isDepositSameCurrency =>
+ depositCurrency == wallet.currency ||
+ depositCurrency.tag == wallet.currency.tag ||
+ depositCurrency.tag == wallet.currency.title;
+
bool get isElectrumWallet => [
WalletType.bitcoin,
WalletType.litecoin,
@@ -412,10 +416,16 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@action
void changeDepositCurrency({required CryptoCurrency currency}) {
+ print("@@@@@@@@");
+ print(currency);
+ print(currency.tag);
+ print(currency.title);
+ print(wallet.currency.tag);
+ print(wallet.currency.title);
depositCurrency = currency;
isFixedRateMode = false;
_onPairChange();
- isDepositAddressEnabled = !(depositCurrency == wallet.currency);
+ isDepositAddressEnabled = !(isDepositSameCurrency);
}
@action
@@ -423,7 +433,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
receiveCurrency = currency;
isFixedRateMode = false;
_onPairChange();
- isDepositAddressEnabled = !(depositCurrency == wallet.currency);
+ isDepositAddressEnabled = !(isDepositSameCurrency);
}
@action
@@ -534,12 +544,11 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
_providers.map(
(element) => element
.fetchRate(
- from: depositCurrency,
- to: receiveCurrency,
- amount: amount,
- isFixedRateMode: isFixedRateMode,
- isReceiveAmount: isFixedRateMode
- )
+ from: depositCurrency,
+ to: receiveCurrency,
+ amount: amount,
+ isFixedRateMode: isFixedRateMode,
+ isReceiveAmount: isFixedRateMode)
.timeout(
Duration(seconds: 7),
onTimeout: () => 0.0,
diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart
index cc9da280..e2a0cf2b 100644
--- a/lib/view_model/send/send_view_model.dart
+++ b/lib/view_model/send/send_view_model.dart
@@ -730,79 +730,64 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
}
}
- @action
- Future<void> commitTransaction(BuildContext context) async {
- if (pendingTransaction == null) {
- throw Exception("Pending transaction doesn't exist. It should not be happened.");
+ Future<void> _handleOcpRequest() async {
+ if (OpenCryptoPayService.requiresClientCommit(selectedCryptoCurrency)) {
+ await pendingTransaction!.commit();
}
- if (ocpRequest != null) {
- state = TransactionCommitting();
- if (OpenCryptoPayService.requiresClientCommit(selectedCryptoCurrency)) {
- await pendingTransaction!.commit();
- }
-
- await _ocpService.commitOpenCryptoPayRequest(
- pendingTransaction!.hex,
- txId: pendingTransaction!.id,
- request: ocpRequest!,
- asset: selectedCryptoCurrency,
- );
-
- state = TransactionCommitted();
+ await _ocpService.commitOpenCryptoPayRequest(
+ pendingTransaction!.hex,
+ txId: pendingTransaction!.id,
+ request: ocpRequest!,
+ asset: selectedCryptoCurrency,
+ );
+ }
- return;
+ Future<void> _commitApprovalTransaction() async {
+ if (_pendingApprovalTx != null) {
+ await _pendingApprovalTx!.commit();
+ _pendingApprovalTx = null;
+ // Small pause to ensure allowance is indexed
+ await Future.delayed(const Duration(milliseconds: 300));
}
- // Swaps.xyz approval (if any), then commit the prebuilt router tx
- if (_isSwapsXYZCallDataTx) {
- if (_pendingApprovalTx != null) {
- await _pendingApprovalTx!.commit();
- _pendingApprovalTx = null;
- // Small pause to ensure allowance is indexed
- await Future.delayed(const Duration(milliseconds: 300));
- }
-
- await pendingTransaction!.commit();
- _isSwapsXYZCallDataTx = false;
+ await pendingTransaction!.commit();
+ _isSwapsXYZCallDataTx = false;
+ }
- state = TransactionCommitted();
- return; // skip the regular flow below
+ Future<void> _commitUR(BuildContext context) async {
+ final urstr = await pendingTransaction!.commitUR();
+ final result = await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, arguments: urstr);
+ if (result == null) {
+ throw "Canceled by user";
}
+ }
- String address = outputs.fold('', (acc, value) {
- return value.isParsedAddress
- ? '$acc${value.address}\n${value.extractedAddress}\n\n'
- : '$acc${value.address}\n\n';
- });
-
- address = address.trim();
-
- String note = outputs.fold('', (acc, value) => '$acc${value.note}\n');
-
- note = note.trim();
+ @action
+ Future<void> commitTransaction(BuildContext context) async {
+ if (pendingTransaction == null) {
+ throw Exception("Pending transaction doesn't exist. It should not be happened.");
+ }
try {
state = TransactionCommitting();
- if (pendingTransaction!.shouldCommitUR()) {
- final urstr = await pendingTransaction!.commitUR();
- final result =
- await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, arguments: urstr);
- if (result == null) {
- state = FailureState("Canceled by user");
- return;
- }
+ if (ocpRequest != null) {
+ await _handleOcpRequest();
+ } else if (_isSwapsXYZCallDataTx) {
+ // Swaps.xyz approval (if any), then commit the prebuilt router tx
+ await _commitApprovalTransaction();
+ } else if (pendingTransaction!.shouldCommitUR()) {
+ await _commitUR(context);
} else {
await pendingTransaction!.commit();
}
- if (walletType == WalletType.nano) {
- nano!.updateTransactions(wallet);
- }
+ state = TransactionCommitted();
- // Immediate transaction update for EVM chains, Solana, and Tron
- if (isEVMWallet || walletType == WalletType.solana || walletType == WalletType.tron) {
+ // Immediate transaction update for EVM chains, Solana, Tron, and Nano
+ if (isEVMWallet ||
+ [WalletType.solana, WalletType.tron, WalletType.nano].contains(walletType)) {
Future.delayed(Duration(seconds: 4), () async {
try {
await wallet.updateTransactionsHistory();
@@ -813,36 +798,51 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
}
if (pendingTransaction!.id.isNotEmpty) {
- TransactionInfo? tx;
- if (walletType == WalletType.monero) {
- await Future.delayed(Duration(milliseconds: 450));
- await wallet.fetchTransactions();
- final txhistory = monero!.getTransactionHistory(wallet);
- tx = txhistory.transactions.values.last;
- }
- final descriptionKey = '${pendingTransaction!.id}_${wallet.walletAddresses.primaryAddress}';
- _settingsStore.shouldSaveRecipientAddress
- ? await transactionDescriptionBox.add(TransactionDescription(
- id: descriptionKey,
- recipientAddress: address,
- transactionNote: note,
- transactionKey: tx?.additionalInfo["key"] as String?,
- ))
- : await transactionDescriptionBox.add(TransactionDescription(
- id: descriptionKey,
- transactionNote: note,
- transactionKey: tx?.additionalInfo["key"] as String?,
- ));
+ _addTransactionDescription();
}
final sharedPreferences = await SharedPreferences.getInstance();
await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name),
DateTime.now().add(Duration(minutes: 1)).toIso8601String());
- state = TransactionCommitted();
} catch (e) {
state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency));
}
}
+ Future<void> _addTransactionDescription() async {
+ String address = outputs.fold('', (acc, value) {
+ return value.isParsedAddress
+ ? '$acc${value.address}\n${value.extractedAddress}\n\n'
+ : '$acc${value.address}\n\n';
+ });
+
+ address = address.trim();
+
+ String note = outputs.fold('', (acc, value) => '$acc${value.note}\n');
+
+ note = note.trim();
+
+ TransactionInfo? tx;
+ if (walletType == WalletType.monero) {
+ await Future.delayed(Duration(milliseconds: 450));
+ await wallet.fetchTransactions();
+ final txhistory = monero!.getTransactionHistory(wallet);
+ tx = txhistory.transactions.values.last;
+ }
+ final descriptionKey = '${pendingTransaction!.id}_${wallet.walletAddresses.primaryAddress}';
+ _settingsStore.shouldSaveRecipientAddress
+ ? await transactionDescriptionBox.add(TransactionDescription(
+ id: descriptionKey,
+ recipientAddress: address,
+ transactionNote: note,
+ transactionKey: tx?.additionalInfo["key"] as String?,
+ ))
+ : await transactionDescriptionBox.add(TransactionDescription(
+ id: descriptionKey,
+ transactionNote: note,
+ transactionKey: tx?.additionalInfo["key"] as String?,
+ ));
+ }
+
Object _credentials([ExchangeProvider? provider]) {
final priority = _settingsStore.priority[wallet.type];
Why this scored 32/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.