From 517612095ba94f55795ab1a025c44ea510d9a408 Mon Sep 17 00:00:00 2001 From: malik1004x Date: Thu, 20 Aug 2026 23:14:12 +0200 Subject: [PATCH 01/15] safeguard calls to coin integration classes (#3534) --- lib/entities/default_settings_migration.dart | 26 ++++++++++++++------ lib/store/settings_store.dart | 14 +++++++---- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index ae94b47e1..4696fab40 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -95,8 +95,12 @@ Future defaultSettingsMigration( case 1: await sharedPreferences.setString( PreferencesKey.currentFiatCurrencyKey, FiatCurrency.usd.toString()); - await sharedPreferences.setInt(PreferencesKey.currentTransactionPriorityKeyLegacy, - monero!.getDefaultTransactionPriority().raw); + + if (monero != null) { + await sharedPreferences.setInt( + PreferencesKey.currentTransactionPriorityKeyLegacy, + monero!.getDefaultTransactionPriority().raw); + } await sharedPreferences.setInt( PreferencesKey.currentBalanceDisplayModeKey, BalanceDisplayMode.availableBalance.raw); await sharedPreferences.setBool('save_recipient_address', true); @@ -791,6 +795,10 @@ Future _backupWowneroSeeds(Box havenSeedStore) async { } Future _updateMoneroPriority(SharedPreferences sharedPreferences) async { + if (monero == null) { + return; + } + final currentPriority = await sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority) ?? monero!.getDefaultTransactionPriority().serialize(); @@ -1071,11 +1079,15 @@ Future generateBackupPassword(SecureStorage secureStorage) async { Future changeTransactionPriorityAndFeeRateKeys(SharedPreferences sharedPreferences) async { final legacyTransactionPriority = - sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy)!; - await sharedPreferences.setInt( - PreferencesKey.moneroTransactionPriority, legacyTransactionPriority); - await sharedPreferences.setInt(PreferencesKey.bitcoinTransactionPriority, - bitcoin!.getMediumTransactionPriority().serialize()); + sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy); + if (legacyTransactionPriority != null) { + await sharedPreferences.setInt( + PreferencesKey.moneroTransactionPriority, legacyTransactionPriority); + } + if (bitcoin != null) { + await sharedPreferences.setInt(PreferencesKey.bitcoinTransactionPriority, + bitcoin!.getMediumTransactionPriority().serialize()); + } } Future fixBtcDerivationPaths() async { diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 95ab5d52c..f699640e7 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -1777,12 +1777,16 @@ abstract class SettingsStoreBase with Store { Future reload() async { final sharedPreferences = await getIt.getAsync(); - fiatCurrency = FiatCurrency.deserialize( - raw: sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey)!); + final savedFiatCurrency = sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey); + if (savedFiatCurrency != null) { + fiatCurrency = FiatCurrency.deserialize(raw: savedFiatCurrency); + } - priority[WalletType.monero] = monero?.deserializeMoneroTransactionPriority( - raw: sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority)!) ?? - priority[WalletType.monero]!; + if (monero != null && + sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority) != null) { + priority[WalletType.monero] = monero!.deserializeMoneroTransactionPriority( + raw: sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority)!); + } if (wownero != null && sharedPreferences.getInt(PreferencesKey.wowneroTransactionPriority) != null) { From c248a53a3557fecb11defd37895ccdf62e68ab98 Mon Sep 17 00:00:00 2001 From: malik1004x Date: Thu, 20 Aug 2026 23:14:47 +0200 Subject: [PATCH 02/15] hide keyboard after restoring backup on ios (#3533) --- lib/src/screens/restore/restore_from_backup_page.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/screens/restore/restore_from_backup_page.dart b/lib/src/screens/restore/restore_from_backup_page.dart index a8edd8e99..acf7e167f 100644 --- a/lib/src/screens/restore/restore_from_backup_page.dart +++ b/lib/src/screens/restore/restore_from_backup_page.dart @@ -11,6 +11,7 @@ import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/view_model/restore_from_backup_view_model.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:file_picker/file_picker.dart'; +import "package:flutter/services.dart"; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; @@ -180,8 +181,11 @@ class RestoreFromBackupPage extends BasePage { return; } + + FocusManager.instance.primaryFocus?.unfocus(); try { await restoreFromBackupViewModel.import(textEditingController.text); + await SystemChannels.textInput.invokeMethod("TextInput.hide"); } on IncompatibleBackupAppException catch (e) { final isConfirmed = await showPopUp( context: context, From 8eddd4d481ceea80568fdad98f137d4d0092754c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?BSN=20=E2=88=9E/21M?= Date: Fri, 21 Aug 2026 12:33:26 +0200 Subject: [PATCH 03/15] =?UTF-8?q?26-08-19=5FUpdate=20Trans=C3=B6ation=5Fde?= =?UTF-8?q?=5FDE=20(#3537)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- res/values/strings_de.arb | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index a0f89e794..9b953104e 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -98,7 +98,7 @@ "available_balance_description": "Der „Verfügbare Kontostand“ bzw. „Bestätigter Kontostand“ sind Guthaben, die sofort ausgegeben werden können. Wenn Guthaben im unteren Kontostand angezeigt wird, aber nicht im oberen, müssen Sie ein paar Minuten warten, bis die eingehenden Gelder weitere Netzwerkbestätigungen erhalten. Sobald sie mehr Bestätigungen haben, können sie ausgegeben werden.", "available_balance_short": "Durchschnitt: ${amount}", "avg_savings": "Durchschn. Ersparnis", - "avl": "Avl", + "avl": "Verf.", "awaitDAppProcessing": "Bitte warten Sie, bis die dApp die Verarbeitung abgeschlossen hat.", "awaiting_payment_confirmation": "Warten auf Zahlungsbestätigung", "background_sync": "Hintergrund-Synchronisierung", @@ -265,7 +265,7 @@ "contact_support": "Support kontaktieren", "contacts": "Kontakte", "continue_text": "Weiter", - "continue_to_swap": "Weiter tauschen", + "continue_to_swap": "Weiter zum Swap", "contract_address": "Vertragsadresse", "contract_warning": "Diese Vertragsadresse wurde als potenziell betrügerisch gekennzeichnet. Bitte gehen Sie mit Vorsicht vor.", "contractName": "Vertragsname", @@ -383,7 +383,6 @@ "donation_link_details": "Details zum Spendenlink", "done": "Fertig", "duress_pin_description": "Dadurch wird eine Notfall-PIN eingerichtet, eine erweiterte Funktion, die von den meisten Nutzern nicht verwendet werden sollte. Diese PIN sollte nur verwendet werden, wenn Sie in Gefahr sind. Nachdem Sie diese PIN verwendet haben, werden alle Ihre Wallets gelöscht. Stellen Sie daher bitte sicher, dass alle Ihre Seeds gesichert sind, bevor Sie sie verwenden.", - "duress_PIN_set_up_successfully": "Die Notfall-PIN wurde erfolgreich eingerichtet", "durres_PIN": "Notfall-PIN", "durres_PIN_set_up_successfully": "Die Notfall-PIN wurde erfolgreich eingerichtet", "e_sign_consent": "Einwilligung zur E-Signatur", @@ -551,13 +550,12 @@ "how_to_connect": "So verbinden Sie sich", "how_to_use": "So verwenden Sie", "how_to_use_card": "So verwenden Sie diese Karte", - "hww_exit_desc": "Dadurch wird der laufende Kopplungsvorgang mit Ihrem Hardware-Gerät beendet", + "hww_exit_desc": "Dadurch wird der laufende Kopplungsvorgang mit Ihrem Hardware-Gerät beendet.", "i_understand": "Ich verstehe", "icon_style": "Symbolstil", "id": "ID: ", - "if_you_dont_see_your_device": "Wenn Sie Ihr Gerät oben nicht sehen, stellen Sie bitte sicher, dass Ihr Ledger eingeschaltet und entsperrt ist!", + "if_you_dont_see_your_device": "Wenn Sie Ihr Gerät oben nicht sehen, stellen Sie bitte sicher, dass es eingeschaltet und entsperrt ist!", "ignor": "Ignorieren", - "ignore": "Ignorieren", "import": "Importieren", "importNFTs": "NFTs importieren", "in_store": "Im Geschäft", @@ -688,8 +686,8 @@ "narrow": "Schmal", "network": "Netzwerk", "network_address_detected": "${network}-Adresse erkannt", - "network_prefix_on": "An", - "network_switch_failed": "Der Wechsel zum angeforderten Netzwerk ist fehlgeschlagen. Die Zahlung wurde nicht angewendet. Bitte versuchen Sie es erneut.", + "network_prefix_on": "auf", + "network_switch_failed": "Der Wechsel zum angeforderten Netzwerk ist fehlgeschlagen. Die Zahlung wurde nicht ausgeführt. Bitte versuchen Sie es erneut.", "networks": "Netzwerke", "new_first_wallet_text": "Ihre Kryptowährungen sicher aufzubewahren ist ein Kinderspiel", "new_node_testing": "Testen des neuen Nodes", @@ -817,7 +815,6 @@ "please_fill_totp": "Bitte geben Sie den 8-stelligen Code ein, der auf Ihrem anderen Gerät angezeigt wird", "please_make_selection": "Bitte treffen Sie unten eine Auswahl, um Ihre Wallet zu erstellen oder wiederherzustellen.", "please_reference_document": "Bitte beachten Sie die untenstehenden Dokumente für weitere Informationen.", - "Please_reference_document": "Weitere Informationen finden Sie in den Dokumenten unten.", "please_select": "Bitte auswählen:", "please_select_backup_file": "Bitte wählen Sie eine Sicherungsdatei aus und geben Sie das Sicherungskennwort ein.", "please_try_to_connect_to_another_node": "Bitte versuchen Sie, eine Verbindung zu einem anderen Node herzustellen", @@ -1010,7 +1007,7 @@ "select_asset": "Wählen Sie Asset aus", "select_backup_file": "Backup-Datei auswählen", "select_buy_provider_notice": "Wählen Sie oben einen Kaufanbieter aus. Sie können diesen Bildschirm überspringen, indem Sie Ihren Standard-Kaufanbieter in den App-Einstellungen festlegen.", - "select_compatible_wallet": "Wählen Sie eine Ihrer kompatiblen Geldbörsen aus", + "select_compatible_wallet": "Wählen Sie eine Ihrer kompatiblen Wallets aus.", "select_destination": "Bitte wählen Sie das Ziel für die Backup-Datei aus.", "select_fiat_currency_title": "Wählen Sie Währung aus", "select_hw_account_below": "Bitte wählen Sie unten aus, welches Konto wiederhergestellt werden soll:", @@ -1020,9 +1017,9 @@ "select_network_title": "Wählen Sie Netzwerk", "select_other_wallet": "Anderes Wallet auswählen", "select_receiver": "Empfänger auswählen", - "select_recipient_network": "Wählen Sie Empfängernetzwerk aus", - "select_recipient_network_description": "Mehrere Netzwerke sind mit der angegebenen Empfängeradresse kompatibel", - "select_recipient_network_for_tx": "Wählen Sie das Netzwerk des Empfängers dieser Transaktion aus", + "select_recipient_network": "Wählen Sie das Empfängernetzwerk aus.", + "select_recipient_network_description": "Mehrere Netzwerke sind mit der angegebenen Empfängeradresse kompatibel.", + "select_recipient_network_for_tx": "Wählen Sie das Netzwerk des Empfängers für diese Transaktion aus.", "select_sell_provider_notice": "Wählen Sie oben einen Verkaufsanbieter aus. Sie können diesen Bildschirm überspringen, indem Sie in den App-Einstellungen Ihren Standard-Verkaufsanbieter festlegen.", "select_token": "Token auswählen", "select_your_country": "Bitte wählen Sie Ihr Land aus", @@ -1055,8 +1052,8 @@ "send_templates": "Vorlagen", "send_title": "Senden", "send_to": "Senden an", - "send_to_network": "An ${network} senden", - "send_to_network_description": "Möchten Sie zu Ihrem ${network}-Wallet wechseln oder Ihr ${currentNetwork}-Guthaben für diese Zahlung eintauschen?", + "send_to_network": "Über ${network} senden", + "send_to_network_description": "Möchten Sie zu Ihrer ${network}-Wallet wechseln oder Ihr ${currentNetwork}-Guthaben für diese Zahlung tauschen?", "send_to_this_address": "Senden Sie ${currency} ${tag} an diese Adresse", "send_xmr": "XMR senden", "send_your_wallet": "Ihre Wallet", @@ -1178,8 +1175,8 @@ "support_title_other_links": "Weitere Support-Links", "supported": "Unterstützt", "swap": "Swap", - "swap_from_network": "Von ${network} tauschen", - "swap_from_network_description": "Um Gelder an das ${targetNetwork}-Netzwerk zu senden, müssen Sie Ihre ${currentNetwork}-Gelder austauschen. Möchten Sie fortfahren?", + "swap_from_network": "${network}-Guthaben tauschen", + "swap_from_network_description": "Um Guthaben an das ${targetNetwork}-Netzwerk zu senden, müssen Sie Ihr ${currentNetwork}-Guthaben tauschen. Möchten Sie fortfahren?", "swap_id": "Swap-ID", "swap_provider_initial_desc": "Wählen Sie die Art des Swap-Anbieters aus, die Sie beim Tauschen bevorzugen.", "swap_providers": "Swap-Anbieter", @@ -1192,7 +1189,7 @@ "swipe_to_send": "Zum Senden wischen", "switch_camera": "Kamera wechseln", "switch_input_currency": "Zwischen Krypto- und Fiat-Eingabe wechseln", - "switch_to_x_wallet": "Wechseln Sie zum ${network} Wallet", + "switch_to_x_wallet": "Zur ${network}-Wallet wechseln", "switch_wallet": "Wallet wechseln", "switchToETHWallet": "Bitte wechseln Sie zu einer Ethereum-Wallet und versuchen Sie es erneut", "switchToWCCompatibleWallet": "Bitte wechseln Sie zu einer WalletConnect-kompatiblen Wallet und versuchen Sie es erneut (${chains}).", @@ -1366,7 +1363,7 @@ "unspent_coins_details_title": "Details zu nicht ausgegebenen Coins", "unspent_coins_title": "Nicht ausgegebene Coins", "unsupported_asset": "Wir unterstützen diese Aktion für dieses Asset nicht. Bitte erstellen Sie eine Wallet mit einem unterstützten Asset-Typ oder wechseln Sie zu einer Wallet mit einem unterstützten Asset-Typ.", - "unsupported_network_requested": "Diese Zahlungsanforderung bezieht sich auf ein Netzwerk, das wir nicht unterstützen (Ketten-ID ${chainId}).", + "unsupported_network_requested": "Diese Zahlungsanforderung bezieht sich auf ein Netzwerk, das wir nicht unterstützen (Chain-ID ${chainId}).", "unsupported_token_requested": "Diese Zahlungsanforderung bezieht sich auf einen Token, den wir im ausgewählten Netzwerk nicht identifizieren konnten.", "uptime": "Betriebszeit", "upto": "bis zu ${value}", @@ -1520,4 +1517,4 @@ "zcash_network_upgrade_desc_1": "Das Zcash-Netzwerk wurde auf Ironwood aktualisiert. Damit Ihr Guthaben wieder ausgegeben werden kann, muss es migriert werden. Die Migration erfolgt automatisch und unter Wahrung Ihrer Privatsphäre. Sie beginnt, sobald Sie Ihre Zcash-Wallet öffnen.", "zcash_network_upgrade_desc_2": "Nach Abschluss der Migration wird Ihr verfügbares Guthaben wieder wie gewohnt angezeigt und Sie können Zcash wieder normal senden und empfangen.", "zcash_network_upgrade_desc_3": "Bitte lassen Sie Ihre Zcash-Wallet vollständig synchronisieren und anschließend die App geöffnet, bis die Migration abgeschlossen ist. Dies kann je nach Guthaben einige Minuten dauern." -} \ No newline at end of file +} From 484f42aa8be8f9178203e6453a8f8da55271aa74 Mon Sep 17 00:00:00 2001 From: malik1004x Date: Fri, 21 Aug 2026 20:10:04 +0200 Subject: [PATCH 04/15] fix tx history scroll behavior (#3549) --- .../widgets/coins_page/assets_history/history_modal.dart | 4 +++- .../widgets/coins_page/assets_history/history_section.dart | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_modal.dart b/lib/new-ui/widgets/coins_page/assets_history/history_modal.dart index 913266746..11f2310c2 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/history_modal.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/history_modal.dart @@ -39,7 +39,9 @@ class HistoryModal extends StatelessWidget { child: Stack( children: [ Material( - child: CustomScrollView(controller: ModalScrollController.of(context), slivers: [ + child: CustomScrollView(physics: ClampingScrollPhysics(), + controller: ModalScrollController.of(context), + slivers: [ HistorySection( detailsAsPage: true, dashboardViewModel: dashboardViewModel, diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_section.dart b/lib/new-ui/widgets/coins_page/assets_history/history_section.dart index cc73b556f..d27835ee7 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/history_section.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/history_section.dart @@ -22,6 +22,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:intl/intl.dart'; +import "package:modal_bottom_sheet/modal_bottom_sheet.dart"; class HistorySection extends StatelessWidget { const HistorySection( @@ -107,8 +108,8 @@ class HistorySection extends StatelessWidget { Navigator.of(context).push(CupertinoPageRoute( builder: (context) => Material(child: page))); } else { - showModalBottomSheet( - isScrollControlled: true, + showMaterialModalBottomSheet( + backgroundColor: Colors.transparent, context: context, builder: (context) => FractionallySizedBox(heightFactor: 0.9, child: page)); From 419d8cdeb478b161cab329bdfcad4cca8f65f987 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Sat, 22 Aug 2026 13:10:19 +0100 Subject: [PATCH 05/15] fix: generic fixes after anypay (#3543) * fix android CI * feat: add anypay core models, parser and routing engine * feat: add anypay resolver, anypay service and wallet switch service * refactor: route the send page payment flow through anypay service * fix: show message when there's no swap providers for a pair and reset the trade guard on dismissal * refactor: merge the swap from network and send to network decision pages together and clean up anypay widgets * feat: reapply deep links to a currently open send flow and register the solana scheme * feat: add support for zcash address detection to anypay and other minor fixes * refactor: remove trailing icon * feat: hide swap flow for sp and mweb addresses * feat: enhance zcash address detection and deeplink handling * fix: pin bitcoin_base * refactor: Move the anypay decision navigation out of the send page and address minor review comments * refactor: Move the anypay decision navigation out of the send page and address minor review comments * feat: handle exception in switch service and mvoe balance compute to call site * refactor: combine anypay entry points to one and extract selected mode widget * fix: tx priority for doge * chore: add dogecoin to isElectrumWallet util * fix: annoying mobx error --------- Co-authored-by: Czarek Nakamoto --- lib/entities/preferences_key.dart | 3 ++- lib/new-ui/pages/send_page.dart | 13 +++++++------ lib/reactions/wallet_utils.dart | 1 + lib/store/settings_store.dart | 20 ++++++++++++++++++++ lib/view_model/send/send_view_model.dart | 5 +++-- 5 files changed, 33 insertions(+), 9 deletions(-) diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 63c94bdb7..d35c010f5 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -63,7 +63,8 @@ class PreferencesKey { static const zanoTransactionPriority = 'current_fee_priority_zano'; static const wowneroTransactionPriority = 'current_fee_priority_wownero'; static const decredTransactionPriority = 'current_fee_priority_decred'; - static const zcashTransactionPriority = 'current_fee_priority_decred'; + static const zcashTransactionPriority = 'current_fee_priority_zcash'; + static const dogecoinTransactionPriority = 'current_fee_priority_dogecoin'; static const customBitcoinFeeRate = 'custom_electrum_fee_rate'; static const silentPaymentsCardDisplay = 'silentPaymentsCardDisplay'; static const mwebCardDisplay = 'mwebCardDisplay'; diff --git a/lib/new-ui/pages/send_page.dart b/lib/new-ui/pages/send_page.dart index 0b0c51d03..0a6ea20d6 100644 --- a/lib/new-ui/pages/send_page.dart +++ b/lib/new-ui/pages/send_page.dart @@ -215,12 +215,7 @@ class _NewSendPageState extends State { final _addressFocusNode = FocusNode(); final _disposers = []; StreamSubscription? _deepLinkSubscription; - late final AnyPayFlow _anyPayFlow = AnyPayFlow( - anyPayService: widget.anyPayService, - sendViewModel: widget.sendViewModel, - authService: widget.authService, - walletSwitcherViewModel: widget.walletSwitcherViewModel, - ); + late final AnyPayFlow _anyPayFlow; ContactRecord? newContactAddress; bool _justHandledPasteButton = false; @@ -228,6 +223,12 @@ class _NewSendPageState extends State { @override void initState() { super.initState(); + _anyPayFlow = AnyPayFlow( + anyPayService: widget.anyPayService, + sendViewModel: widget.sendViewModel, + authService: widget.authService, + walletSwitcherViewModel: widget.walletSwitcherViewModel, + ); _addInputControllers(); _disposers.add( diff --git a/lib/reactions/wallet_utils.dart b/lib/reactions/wallet_utils.dart index dc2ed1af4..2e94fa9b5 100644 --- a/lib/reactions/wallet_utils.dart +++ b/lib/reactions/wallet_utils.dart @@ -32,6 +32,7 @@ bool isElectrumWallet(WalletType walletType) { case WalletType.bitcoin: case WalletType.litecoin: case WalletType.bitcoinCash: + case WalletType.dogecoin: return true; default: return false; diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index f699640e7..f845cf86a 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/utilities.dart'; import 'package:cake_wallet/decred/decred.dart'; +import 'package:cake_wallet/dogecoin/dogecoin.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/di.dart'; @@ -168,6 +169,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? initialZanoTransactionPriority, TransactionPriority? initialDecredTransactionPriority, TransactionPriority? initialZcashTransactionPriority, + TransactionPriority? initialDogecoinTransactionPriority, Country? initialCakePayCountry}) : nodes = ObservableMap.of(nodes), powNodes = ObservableMap.of(powNodes), @@ -273,6 +275,9 @@ abstract class SettingsStoreBase with Store { if (initialZcashTransactionPriority != null) { priority[WalletType.zcash] = initialZcashTransactionPriority; } + if (initialDogecoinTransactionPriority != null) { + priority[WalletType.dogecoin] = initialDogecoinTransactionPriority; + } if (initialCakePayCountry != null) { selectedCakePayCountry = initialCakePayCountry; @@ -345,6 +350,9 @@ abstract class SettingsStoreBase with Store { case WalletType.zcash: key = PreferencesKey.zcashTransactionPriority; break; + case WalletType.dogecoin: + key = PreferencesKey.dogecoinTransactionPriority; + break; default: key = null; } @@ -1194,6 +1202,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? zanoTransactionPriority; TransactionPriority? decredTransactionPriority; TransactionPriority? zcashTransactionPriority; + TransactionPriority? dogecoinTransactionPriority; if (sharedPreferences.getInt(PreferencesKey.havenTransactionPriority) != null) { havenTransactionPriority = monero?.deserializeMoneroTransactionPriority( @@ -1241,6 +1250,10 @@ abstract class SettingsStoreBase with Store { zcashTransactionPriority = zcash?.deserializeZcashTransactionPriority( raw: sharedPreferences.getInt(PreferencesKey.zcashTransactionPriority)!); } + if (sharedPreferences.getInt(PreferencesKey.dogecoinTransactionPriority) != null) { + dogecoinTransactionPriority = dogecoin?.deserializeDogeCoinTransactionPriority( + sharedPreferences.getInt(PreferencesKey.dogecoinTransactionPriority)!); + } moneroTransactionPriority ??= monero?.getDefaultTransactionPriority(); bitcoinTransactionPriority ??= bitcoin?.getMediumTransactionPriority(); @@ -1256,6 +1269,7 @@ abstract class SettingsStoreBase with Store { bscTransactionPriority ??= evm?.getDefaultTransactionPriority(); zanoTransactionPriority ??= zano?.getDefaultTransactionPriority(); zcashTransactionPriority ??= zcash?.getDefaultTransactionPriority(); + dogecoinTransactionPriority ??= dogecoin?.getDefaultTransactionPriority(); final currentBalanceDisplayMode = BalanceDisplayMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey)!); @@ -1744,6 +1758,7 @@ abstract class SettingsStoreBase with Store { initialBitcoinCashTransactionPriority: bitcoinCashTransactionPriority, initialDecredTransactionPriority: decredTransactionPriority, initialZcashTransactionPriority: zcashTransactionPriority, + initialDogecoinTransactionPriority: dogecoinTransactionPriority, initialShouldRequireTOTP2FAForAccessingWallet: shouldRequireTOTP2FAForAccessingWallet, initialShouldRequireTOTP2FAForSendsToContact: shouldRequireTOTP2FAForSendsToContact, initialShouldRequireTOTP2FAForSendsToNonContact: shouldRequireTOTP2FAForSendsToNonContact, @@ -1847,6 +1862,11 @@ abstract class SettingsStoreBase with Store { priority[WalletType.zcash] = zcash!.deserializeZcashTransactionPriority( raw: sharedPreferences.getInt(PreferencesKey.zcashTransactionPriority)!); } + if (dogecoin != null && + sharedPreferences.getInt(PreferencesKey.dogecoinTransactionPriority) != null) { + priority[WalletType.dogecoin] = dogecoin!.deserializeDogeCoinTransactionPriority( + sharedPreferences.getInt(PreferencesKey.dogecoinTransactionPriority)!); + } final generateSubaddresses = sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey); diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 670bc5a17..df85d0cac 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -167,9 +167,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor bool get isEVMWallet => isEVMCompatibleChain(walletType); - @action CryptoCurrency _outputCryptoCurrencyHandler([CryptoCurrency? override]) { - if (override != null && override != selectedCryptoCurrency) selectedCryptoCurrency = override; + if (override != null && override != selectedCryptoCurrency) { + runInAction(() => selectedCryptoCurrency = override); + } return selectedCryptoCurrency; } From 9d30225211f04864b3c60546e2fde8fd19b6c0b9 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Sun, 23 Aug 2026 21:28:35 +0200 Subject: [PATCH 06/15] fix rive_common dependency --- pubspec_overrides.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec_overrides.yaml b/pubspec_overrides.yaml index 94a1cf6c9..b3b236a4a 100644 --- a/pubspec_overrides.yaml +++ b/pubspec_overrides.yaml @@ -1224,8 +1224,8 @@ dependency_overrides: ref: 2dbfe918a3d38340f378ab2734b1c97ebcb4ab26 rive_common: git: - url: https://github.com/MrCyjaneK/rive_common - ref: 9457422fce3f1524437f6a1429200f048931a2fe + url: https://github.com/malik1004x/rive_common + ref: 34ef174e87dc4a29e728babfaad8dd9464b704ae riverpod: git: url: https://github.com/rrousselGit/riverpod From c8380129791e9f59ed0106872fec849d1eeec39e Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Mon, 24 Aug 2026 14:48:32 +0200 Subject: [PATCH 07/15] Enhance Money class with precision handling and zero trimming (#3548) * feat: enhance Money class with precision handling, scale alignment, and trailing zero trimming --- cw_core/lib/amount/exchange_rate.dart | 28 ++- cw_core/lib/amount/money.dart | 157 ++++++++---- cw_core/lib/amount/money_double.dart | 15 +- cw_core/lib/amount/utils.dart | 13 + cw_core/lib/parse_fixed.dart | 4 +- cw_core/test/amount/exchange_rate_test.dart | 250 ++++++++++++++++---- cw_core/test/amount/money_test.dart | 123 +++++++++- cw_core/test/amount/utils.dart | 41 ++++ 8 files changed, 522 insertions(+), 109 deletions(-) create mode 100644 cw_core/lib/amount/utils.dart create mode 100644 cw_core/test/amount/utils.dart diff --git a/cw_core/lib/amount/exchange_rate.dart b/cw_core/lib/amount/exchange_rate.dart index efcb06abd..ce3f31325 100644 --- a/cw_core/lib/amount/exchange_rate.dart +++ b/cw_core/lib/amount/exchange_rate.dart @@ -2,6 +2,8 @@ import 'package:cw_core/amount/money.dart'; import 'package:cw_core/currency.dart'; class ExchangeRate { + const ExchangeRate({required this.base, required this.quote}); + /// The currency being priced (e.g. BTC in a BTC/USD pair). final Currency base; @@ -9,8 +11,6 @@ class ExchangeRate { /// (e.g. 45000 USD in a BTC/USD pair). final Money quote; - const ExchangeRate({required this.base, required this.quote}); - /// Converts [amount] between the base and quote currencies. /// /// Results are truncated toward zero. @@ -18,23 +18,31 @@ class ExchangeRate { /// Throws an [ArgumentError] if [amount]'s currency is not part of /// this pair. Money convert(Money amount) { - if (base == quote.currency && base == amount.currency) return amount; - - final scale = BigInt.from(10).pow(base.decimals); + if (base == quote.currency && base == amount.currency) { + return amount; + } if (amount.currency == base) { - if (quote.isZero) return Money.zero(quote.currency); + final scale = BigInt.from(10).pow(amount.decimals); - return Money(amount.amount * quote.amount ~/ scale, quote.currency); + return quote.isZero + ? Money.zero(quote.currency) + : Money(amount.amount * quote.amount ~/ scale, quote.currency, quote.decimals); } if (amount.currency == quote.currency) { - if (quote.isZero) return Money.zero(base); + if (quote.isZero) { + return Money.zero(base); + } - return Money(amount.amount * scale ~/ quote.amount, base); + final numerator = amount.amount * BigInt.from(10).pow(base.decimals + quote.decimals); + final denominator = quote.amount * BigInt.from(10).pow(amount.decimals); + + return Money(numerator ~/ denominator, base); } throw ArgumentError( - "Unable to convert ${amount.currency.symbol} in ${base.symbol}/${quote.currency.symbol} pair"); + "Unable to convert ${amount.currency.symbol} in ${base.symbol}/${quote.currency.symbol} pair", + ); } } diff --git a/cw_core/lib/amount/money.dart b/cw_core/lib/amount/money.dart index de5483e47..3997da501 100644 --- a/cw_core/lib/amount/money.dart +++ b/cw_core/lib/amount/money.dart @@ -1,4 +1,6 @@ -import "package:cw_core/crypto_amount_format.dart"; +import "dart:math"; + +import "package:cw_core/amount/utils.dart"; import "package:cw_core/crypto_currency.dart"; import "package:cw_core/currency.dart"; import "package:cw_core/format_fixed.dart"; @@ -7,39 +9,63 @@ import "package:cw_core/parse_fixed.dart"; export "money_local.dart"; class Money implements Comparable { - const Money(this.amount, this.currency); + const Money(this.amount, this.currency, [int? overrideDecimals]) + : _overrideDecimals = overrideDecimals; factory Money.zero(Currency currency) => Money(BigInt.zero, currency); factory Money.fromInt(int amount, Currency currency) => Money(BigInt.from(amount), currency); - /// Parse the [source] and turn it into [Money] + /// Parse the [source] and turn it into [Money] trimming trailing 0s /// /// Throws a [FormatException] if the [source] is not a valid decimal or /// not in canonical representation or if it is a decimal when [isBaseUnit] - factory Money.parse(source, Currency currency, {bool isBaseUnit = false}) { - final amount = isBaseUnit - ? BigInt.parse(source.toString()) - : parseFixed(source.toString(), currency.decimals); + factory Money.parse( + String source, + Currency currency, { + bool isBaseUnit = false, + bool strictParsing = true, + }) { + if (!isBaseUnit) { + source = trimTrailingFractionZeros(source); + } + final decimals = strictParsing ? currency.decimals : _getActualDecimals(source, currency); + final amount = isBaseUnit ? BigInt.parse(source) : parseFixed(source, decimals); - return Money(amount, currency); + return Money(amount, currency, decimals); } - /// Parse the [source] and turn it into [Money] if possible + /// Parse the [source] and turn it into [Money] if possible trimming trailing 0s /// /// As [parse] except that this method returns `null` if the input is not /// valid or if it is a decimal when [isBaseUnit] - static Money? tryParse(source, Currency currency, {bool isBaseUnit = false}) { - final amount = isBaseUnit - ? BigInt.tryParse(source.toString()) - : tryParseFixed(source.toString(), currency.decimals); + static Money? tryParse( + String source, + Currency currency, { + bool isBaseUnit = false, + bool strictParsing = true, + }) { + try { + if (!isBaseUnit) { + source = trimTrailingFractionZeros(source); + } + final decimals = strictParsing ? currency.decimals : _getActualDecimals(source, currency); + final amount = isBaseUnit ? BigInt.tryParse(source) : tryParseFixed(source, decimals); - return amount != null ? Money(amount, currency) : null; + return amount != null ? Money(amount, currency, decimals) : null; + } catch (_) { + return null; + } } final BigInt amount; final Currency currency; + final int? _overrideDecimals; + + /// Returns the amount of decimals of [currency] + int get decimals => _overrideDecimals ?? currency.decimals; + /// Returns the sign of this [BigInt] amount. /// Returns 0 for zero, -1 for values less than zero and +1 for values /// greater than zero. @@ -59,15 +85,25 @@ class Money implements Comparable { int compareTo(Money other) { _assertSameCurrency(other); - return amount.compareTo(other.amount); + final aligned = _align(other); + return aligned.a.compareTo(aligned.b); } /// Returns `true` if [other] is the same amount of money in /// the same currency. @override - bool operator ==(Object other) => - identical(this, other) || - (other is Money && other.currency == currency && other.amount == amount); + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + if (other is! Money || other.currency != currency) { + return false; + } + + final aligned = _align(other); + return aligned.a == aligned.b; + } /// Returns `true` when this money is less than [other]. /// @@ -76,7 +112,8 @@ class Money implements Comparable { bool operator <(Money other) { _assertSameCurrency(other, "Cannot compare money in different currencies."); - return amount < other.amount; + final aligned = _align(other); + return aligned.a < aligned.b; } /// Returns `true` when this money is less than or equal to [other]. @@ -86,7 +123,8 @@ class Money implements Comparable { bool operator <=(Money other) { _assertSameCurrency(other, "Cannot compare money in different currencies."); - return amount <= other.amount; + final aligned = _align(other); + return aligned.a <= aligned.b; } /// Returns `true` when this money is greater than [other]. @@ -96,7 +134,8 @@ class Money implements Comparable { bool operator >(Money other) { _assertSameCurrency(other, "Cannot compare money in different currencies."); - return amount > other.amount; + final aligned = _align(other); + return aligned.a > aligned.b; } /// Returns `true` when this money is greater than or equal to [other]. @@ -106,7 +145,8 @@ class Money implements Comparable { bool operator >=(Money other) { _assertSameCurrency(other, "Cannot compare money in different currencies."); - return amount >= other.amount; + final aligned = _align(other); + return aligned.a >= aligned.b; } /// Adds the amount of [other] to this amount. @@ -116,11 +156,12 @@ class Money implements Comparable { Money operator +(Money other) { _assertSameCurrency(other); - return _withAmount(amount + other.amount); + final aligned = _align(other); + return copyWith(amount: aligned.a + aligned.b, decimals: aligned.decimals); } /// unary minus operator. - Money operator -() => _withAmount(-amount); + Money operator -() => copyWith(amount: -amount); /// Subtracts the amount of [other] from this amount. /// @@ -129,13 +170,14 @@ class Money implements Comparable { Money operator -(Money other) { _assertSameCurrency(other); - return _withAmount(amount - other.amount); + final aligned = _align(other); + return copyWith(amount: aligned.a - aligned.b, decimals: aligned.decimals); } /// Returns [Money] multiplied by [other]. /// /// The result is again [Money]. - Money operator *(BigInt other) => _withAmount(amount * other); + Money operator *(BigInt other) => copyWith(amount: amount * other); /// Returns [Money] divided by [other]. /// @@ -156,7 +198,7 @@ class Money implements Comparable { final twiceR = r << 1; final mag = (twiceR >= B) ? q + BigInt.one : q; - return _withAmount(neg ? -mag : mag); + return copyWith(amount: neg ? -mag : mag); } /// Creates a copy of this [Money] object with optional new values @@ -164,46 +206,63 @@ class Money implements Comparable { /// /// If just [currency] is provided and the decimals missmatch /// the amount will be transformed to keep its canonical representation - Money copyWith({BigInt? amount, Currency? currency}) { - if (currency != null && amount == null && currency.decimals != this.currency.decimals) { + Money copyWith({BigInt? amount, Currency? currency, int? decimals}) { + if (currency != null && amount == null && decimals == null && + currency.decimals != this.decimals) { return Money( - _transformAmount(this.amount, this.currency.decimals, currency.decimals), + _transformAmount(this.amount, this.decimals, currency.decimals), currency, + currency.decimals, ); } - return Money(amount ?? this.amount, currency ?? this.currency); + return Money(amount ?? this.amount, currency ?? this.currency, decimals ?? this.decimals); } - /// Creates new instance with the same currency and given [amount]. - Money _withAmount(BigInt amount) => Money(amount, currency); - void _assertSameCurrency(Money other, [String? message]) { if (currency != other.currency) { throw ArgumentError(message ?? "Cannot operate with money values in different currencies."); } } - BigInt _transformAmount(BigInt source, int sourceDecimals, int targetDecimals) { - if (sourceDecimals == targetDecimals) { - return source; + static BigInt _transformAmount(BigInt source, int sourceDecimals, int targetDecimals) { + final diff = targetDecimals - sourceDecimals; + + return diff == 0 + ? source + : diff > 0 + ? source * BigInt.from(10).pow(diff) + : source ~/ BigInt.from(10).pow(-diff); + } + + ({BigInt a, BigInt b, int decimals}) _align(Money other) { + final decimals = max(this.decimals, other.decimals); + return ( + a: _transformAmount(amount, this.decimals, decimals), + b: _transformAmount(other.amount, other.decimals, decimals), + decimals: decimals, + ); + } + + static int _getActualDecimals(String value, Currency currency) { + final comps = value.split("."); + if (comps.length > 2) { + throw FormatException("Money._getActualDecimals: too many decimal points, value, $value"); } - if (sourceDecimals > targetDecimals) { - return parseFixed( - formatFixed(source, sourceDecimals).withMaxDecimals(targetDecimals), - targetDecimals, - ); - } else { - return parseFixed(formatFixed(source, sourceDecimals), targetDecimals); - } + return max(comps.length == 2 ? comps[1].length : 0, currency.decimals); } @override - int get hashCode => amount.hashCode ^ currency.hashCode; + int get hashCode { + final value = formatFixed(amount, this.decimals, trimZeros: true); + final decimals = _getActualDecimals(value, currency); + + return Object.hash(value, decimals, currency); + } @override - String toString() => formatFixed(amount, currency.decimals); + String toString() => formatFixed(amount, decimals); String toStringWithSymbol({ int? fractionalDigits, @@ -227,7 +286,9 @@ class Money implements Comparable { bool useBaseUnit = false, }) => formatFixed( - amount, + decimals != currency.decimals + ? _transformAmount(amount, decimals, currency.decimals) + : amount, useBaseUnit ? 0 : currency.decimals, fractionalDigits: fractionalDigits, trimZeros: trimZeros, diff --git a/cw_core/lib/amount/money_double.dart b/cw_core/lib/amount/money_double.dart index 8dc44c4e3..6c8aa766c 100644 --- a/cw_core/lib/amount/money_double.dart +++ b/cw_core/lib/amount/money_double.dart @@ -1,13 +1,16 @@ import "dart:math"; + import "package:cw_core/amount/money.dart"; +import "package:cw_core/amount/utils.dart"; import "package:cw_core/currency.dart"; +/// Turn a double representation of a currency amount to a proper Money representation +/// truncating currencies with more than 20 decimals because double can not handle more extension ToMoney on double { - /// Turn a double representation of a currency amount to a proper Money representation - /// truncating currencies with more than 20 decimals because double can not handle more - Money? tryToMoney(Currency currency) => - Money.tryParse(toStringAsFixed(min(currency.decimals, 20)), currency); + Money? tryToMoney(Currency currency) => Money.tryParse(_toSafeString(currency), currency); - Money toMoney(Currency currency) => - Money.parse(toStringAsFixed(min(currency.decimals, 20)), currency); + Money toMoney(Currency currency) => Money.parse(_toSafeString(currency), currency); + + String _toSafeString(Currency currency) => + trimTrailingFractionZeros(toStringAsFixed(min(currency.decimals, 20))); } diff --git a/cw_core/lib/amount/utils.dart b/cw_core/lib/amount/utils.dart new file mode 100644 index 000000000..28c61bb83 --- /dev/null +++ b/cw_core/lib/amount/utils.dart @@ -0,0 +1,13 @@ +String trimTrailingFractionZeros(String value) { + if (!value.contains('.')) { + return value; + } + + var end = value.length; + while (end > 0 && value[end - 1] == "0") { + end--; + } + + final trimmed = end == value.length ? value : value.substring(0, end); + return trimmed.endsWith('.') ? trimmed.substring(0, trimmed.length - 1) : trimmed; +} diff --git a/cw_core/lib/parse_fixed.dart b/cw_core/lib/parse_fixed.dart index a5a6d7b82..35d2e263c 100644 --- a/cw_core/lib/parse_fixed.dart +++ b/cw_core/lib/parse_fixed.dart @@ -55,7 +55,7 @@ BigInt parseFixed(String value, int decimals) { } final whole = comps.isNotEmpty ? comps[0] : "0"; - final fraction = (comps.length == 2 ? comps[1] : "0").padRight(decimals, "0"); + final fraction = (comps.length == 2 ? comps[1] : "").padRight(decimals, "0"); if (fraction.length > multiplier.length - 1) { throw FormatException( @@ -64,7 +64,7 @@ BigInt parseFixed(String value, int decimals) { } final wholeValue = BigInt.parse(whole); - final fractionValue = BigInt.parse(fraction); + final fractionValue = fraction.isEmpty ? BigInt.zero : BigInt.parse(fraction); final multiplierValue = BigInt.parse(multiplier); var wei = (wholeValue * multiplierValue) + fractionValue; diff --git a/cw_core/test/amount/exchange_rate_test.dart b/cw_core/test/amount/exchange_rate_test.dart index 3873e349f..e7932d8d5 100644 --- a/cw_core/test/amount/exchange_rate_test.dart +++ b/cw_core/test/amount/exchange_rate_test.dart @@ -1,9 +1,10 @@ import 'package:cw_core/amount/exchange_rate.dart'; import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/currency.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'utils.dart'; + void main() { group('ExchangeRate', () { final rate = ExchangeRate( @@ -99,49 +100,216 @@ void main() { test('handle quote being 0 correctly', () { final zeroRate = ExchangeRate( base: CryptoCurrency.btc, - quote: Money(BigInt.zero, EUR), + quote: Money(BigInt.zero, EUR, EUR.decimals), ); expect(zeroRate.convert(Money.fromInt(100, CryptoCurrency.btc)), Money(BigInt.zero, EUR)); expect(zeroRate.convert(Money.fromInt(100, EUR)), Money(BigInt.zero, CryptoCurrency.btc)); }); + + test('handle super low quote correctly', () { + final zeroRate = ExchangeRate( + base: CryptoCurrency.shib, + quote: Money.parse("0.00000444", EUR, strictParsing: false), + ); + + expect( + zeroRate.convert(Money.parse("1", CryptoCurrency.shib)), + Money.parse("0.00000444", EUR, strictParsing: false), + ); + expect( + zeroRate.convert(Money.fromInt(10000, EUR)), + Money.parse("22522522.522522522522522522", CryptoCurrency.shib), + ); + + expect( + zeroRate.convert(Money.parse("200", CryptoCurrency.shib)), + Money.parse("0.000888", EUR, strictParsing: false), + ); + }); + + group('Precision and scale', () { + final rate = ExchangeRate(base: CryptoCurrency.btc, quote: Money.parse("60000", EUR)); + + test('convert is insensitive to the input scale (forward)', () { + // Same value, three different internal scales. + final coarse = Money(BigInt.one, CryptoCurrency.btc, 0); + final normal = Money.parse("1", CryptoCurrency.btc); + final fine = Money.parse("1.000000000000000000", CryptoCurrency.btc, strictParsing: false); + + expect(rate.convert(coarse), Money.parse("60000.00", EUR)); + expect(rate.convert(normal), Money.parse("60000.00", EUR)); + expect(rate.convert(fine), Money.parse("60000.00", EUR)); + }); + + test('convert is insensitive to the input scale (reverse)', () { + expect( + rate.convert(Money(BigInt.one, EUR, 0)), // €1 at scale 0 + rate.convert(Money.parse("1", EUR)), // €1 at scale 2 + ); + }); + + test('sub-unit precision in the input does not leak into the result', () { + // ad = 18 here, well past BTC's 8. The divisor must cancel the *amount's* + // scale, not the base currency's. + final overPrecise = + Money.parse("1.000000000000000001", CryptoCurrency.btc, strictParsing: false); + + expect(rate.convert(overPrecise), Money.parse("60000.00", EUR)); + }); + + test('the result carries the quote scale, not the currency default', () { + final preciseRate = ExchangeRate( + base: CryptoCurrency.btc, + quote: Money.parse("60000.123456", EUR, strictParsing: false), + ); + + final result = preciseRate.convert(Money.parse("1", CryptoCurrency.btc)); + + expect(result.decimals, 6); + expect(result, Money.parse("60000.123456", EUR, strictParsing: false)); + }); + + test('a quote scale finer than the currency survives the round trip', () { + final preciseRate = ExchangeRate( + base: CryptoCurrency.btc, + quote: Money.parse("60000.50", EUR, strictParsing: false), + ); + + expect( + preciseRate.convert(preciseRate.convert(Money.parse("1", CryptoCurrency.btc))), + Money.parse("1", CryptoCurrency.btc), + ); + }); + }); + + group('Truncation', () { + final rate = ExchangeRate(base: CryptoCurrency.btc, quote: Money.parse("60000", EUR)); + + test('truncates rather than rounding, even past the halfway point', () { + // 9 sats = 0.54 cents. Rounding would give 1. + expect(rate.convert(Money(BigInt.from(9), CryptoCurrency.btc)), Money.zero(EUR)); + // 1 cent = 16.66 sats. Rounding would give 17. + expect(rate.convert(Money(BigInt.one, EUR)), Money.fromInt(16, CryptoCurrency.btc)); + }); + + test('truncates toward zero for negatives, not downward', () { + // -0.54 cents must be 0, not -1. + expect(rate.convert(Money(BigInt.from(-9), CryptoCurrency.btc)), Money.zero(EUR)); + expect(rate.convert(Money(BigInt.from(-1), EUR)), Money.fromInt(-16, CryptoCurrency.btc)); + }); + + test('truncation is symmetric around zero', () { + final positive = rate.convert(Money(BigInt.from(7), EUR)); + final negative = rate.convert(Money(BigInt.from(-7), EUR)); + + expect(negative, -positive); + }); + + test('round trip loses less than one quote base unit', () { + final original = Money.parse("0.12345678", CryptoCurrency.btc); + final back = rate.convert(rate.convert(original)); + final oneQuoteUnitInBaseUnits = + BigInt.from(10).pow(CryptoCurrency.btc.decimals) ~/ rate.quote.amount; + + expect( + (original - back).amount.abs(), + lessThanOrEqualTo(oneQuoteUnitInBaseUnits + BigInt.one), + ); + }); + + test('round trip never inflates the amount', () { + // Both truncations round toward zero, so the result can only shrink. + for (final source in ["0.12345678", "1", "0.5", "21000000"]) { + final original = Money.parse(source, CryptoCurrency.btc); + expect( + rate.convert(rate.convert(original)) <= original, + isTrue, + reason: "round trip grew $source", + ); + } + }); + + test('a value below one base unit of the quote collapses to zero', () { + final back = rate.convert(rate.convert(Money(BigInt.one, CryptoCurrency.btc))); + + expect(back.isZero, isTrue); + }); + }); + + group('Extreme decimal spreads', () { + test('30 decimals against 0 decimals: NANO/JPY', () { + final rate = ExchangeRate(base: CryptoCurrency.nano, quote: Money.parse("150", JPY)); + + expect(rate.convert(Money.parse("1", CryptoCurrency.nano)), Money.fromInt(150, JPY)); + expect(rate.convert(Money.fromInt(150, JPY)), Money.parse("1", CryptoCurrency.nano)); + expect(rate.convert(Money.fromInt(1, JPY)).decimals, 30); + }); + + test('0 decimals against 18 decimals: JPY/SHIB', () { + final rate = ExchangeRate(base: JPY, quote: Money.parse("2500", CryptoCurrency.shib)); + + expect(rate.convert(Money.fromInt(2, JPY)), Money.parse("5000", CryptoCurrency.shib)); + expect(rate.convert(Money.parse("5000", CryptoCurrency.shib)), Money.fromInt(2, JPY)); + }); + + test('no overflow at the extremes', () { + final rate = ExchangeRate( + base: CryptoCurrency.shib, + quote: Money.parse("0.00000444", EUR, strictParsing: false), + ); + final totalSupply = Money.parse("589000000000000", CryptoCurrency.shib); + + expect(rate.convert(totalSupply).isNegative, isFalse); + expect(rate.convert(rate.convert(totalSupply)) <= totalSupply, isTrue); + }); + }); + + group('Degenerate pairs', () { + test('identity pair returns the amount untouched', () { + final identity = ExchangeRate(base: EUR, quote: Money.parse("1", EUR)); + + expect(identity.convert(Money.parse("42.42", EUR)), Money.parse("42.42", EUR)); + expect(identity.convert(Money.zero(EUR)), Money.zero(EUR)); + }); + + test('a same-currency pair silently ignores its own rate', () { + final bogus = ExchangeRate(base: EUR, quote: Money.parse("2", EUR)); + expect(bogus.convert(Money.parse("10", EUR)), Money.parse("10", EUR)); + }); + + test('a rate of exactly one is a no-op in both directions', () { + final rate = ExchangeRate(base: JPY, quote: Money.fromInt(100, EUR)); + + expect(rate.convert(Money.fromInt(7, JPY)), Money.fromInt(700, EUR)); + expect(rate.convert(Money.fromInt(700, EUR)), Money.fromInt(7, JPY)); + }); + + test('negative quotes propagate their sign', () { + final inverted = ExchangeRate(base: CryptoCurrency.btc, quote: Money.parse("-60000", EUR)); + + expect( + inverted.convert(Money.parse("1", CryptoCurrency.btc)), + Money.parse("-60000.00", EUR), + ); + expect( + inverted.convert(Money.parse("-60000.00", EUR)), + Money.parse("1", CryptoCurrency.btc), + ); + }); + + test('zero amount with a zero quote', () { + final zeroRate = ExchangeRate(base: CryptoCurrency.btc, quote: Money.zero(EUR)); + + expect(zeroRate.convert(Money.zero(CryptoCurrency.btc)), Money.zero(EUR)); + expect(zeroRate.convert(Money.zero(EUR)), Money.zero(CryptoCurrency.btc)); + }); + + test('rejects a foreign currency in both slots', () { + final rate = ExchangeRate(base: CryptoCurrency.btc, quote: Money.parse("60000", EUR)); + + expect(() => rate.convert(Money.parse("1", CryptoCurrency.xmr)), throwsArgumentError); + expect(() => rate.convert(Money.parse("1", USD)), throwsArgumentError); + }); + }); }); } - -class FiatCurrency implements Currency { - const FiatCurrency({ - required this.symbol, - required this.countryCode, - required this.fullName, - this.decimals = 2, - }); - - final String countryCode; - - @override - final String fullName; - - @override - final int decimals; - - @override - final String symbol; - - @override - String? get iconPath => throw UnimplementedError(); - - @override - String get name => throw UnimplementedError(); - - @override - String? get tag => throw UnimplementedError(); - - @override - Money parseAmount(String value) => Money.parse(value, this); - - @override - Money? tryParseAmount(String value) => Money.tryParse(value, this); -} - -const EUR = FiatCurrency(symbol: 'EUR', countryCode: "eur", fullName: "Euro"); -const USD = FiatCurrency(symbol: 'USD', countryCode: "usd", fullName: "US Dollar"); -const JPY = FiatCurrency(symbol: 'JPY', countryCode: "jpn", fullName: "Japanese Yen", decimals: 0); diff --git a/cw_core/test/amount/money_test.dart b/cw_core/test/amount/money_test.dart index 8f045bdcb..6a91f206f 100644 --- a/cw_core/test/amount/money_test.dart +++ b/cw_core/test/amount/money_test.dart @@ -2,6 +2,8 @@ import "package:cw_core/amount/money.dart"; import "package:cw_core/crypto_currency.dart"; import "package:flutter_test/flutter_test.dart"; +import "utils.dart"; + void main() { group("Money", () { test("parse", () { @@ -27,7 +29,7 @@ void main() { expect(() => Money.parse("1,11", CryptoCurrency.btc), throwsFormatException); // To many decimals - expect(() => Money.parse("-1.000000000000000", CryptoCurrency.btc), throwsFormatException); + expect(() => Money.parse("-1.0000000000000001", CryptoCurrency.btc), throwsFormatException); money = Money.parse("1", CryptoCurrency.btc, isBaseUnit: true); expect(money.amount, BigInt.from(1)); @@ -66,7 +68,7 @@ void main() { expect(money?.amount, isNull); // To many decimals - money = Money.tryParse("-1.000000000000000", CryptoCurrency.btc); + money = Money.tryParse("-1.0000000000000001", CryptoCurrency.btc); expect(money?.amount, isNull); money = Money.tryParse("1", CryptoCurrency.btc, isBaseUnit: true); @@ -373,5 +375,122 @@ void main() { }); }); }); + + group("different scales", () { + final coarseOne = Money(BigInt.one, CryptoCurrency.btc, 0); + final fineOne = Money(BigInt.from(100000000), CryptoCurrency.btc, 8); + + test("== across scales", () { + expect(coarseOne, equals(fineOne)); + expect(coarseOne, isNot(equals(Money(BigInt.two, CryptoCurrency.btc, 0)))); + }); + + test("equal values across scales hash equally", () { + expect(coarseOne.hashCode, equals(fineOne.hashCode)); + + expect( + Money(BigInt.from(-11), CryptoCurrency.btc, 1).hashCode, + equals(Money(BigInt.from(-110000000), CryptoCurrency.btc, 8).hashCode), + ); + }); + + test("zero hashes equally at every scale", () { + final hashes = [0, 1, 8, 18, 30] + .map((s) => Money(BigInt.zero, CryptoCurrency.btc, s).hashCode) + .toSet(); + + expect(hashes, hasLength(1)); + }); + + test("whole units are not confused with their digits", () { + // (100, scale 0) is one hundred, not one: stripping must stop at 0. + final oneHundred = Money(BigInt.from(100), CryptoCurrency.btc, 0); + + expect(oneHundred, isNot(equals(coarseOne))); + expect(oneHundred.hashCode, isNot(equals(coarseOne.hashCode))); + }); + + test("Set deduplicates across scales", () { + expect( + { + Money(BigInt.from(11), CryptoCurrency.btc, 1), + Money(BigInt.from(110000000), CryptoCurrency.btc, 8), + Money(BigInt.from(1100), CryptoCurrency.btc, 3), + }, + hasLength(1), + ); + }); + + test("+ aligns operands", () { + expect(coarseOne + fineOne, equals(Money(BigInt.two, CryptoCurrency.btc, 0))); + expect(coarseOne + fineOne, equals(fineOne + coarseOne)); + }); + + test("- aligns operands", () { + // Regression: this used to subtract the raw amounts, i.e. 1 - 100000000. + expect((coarseOne - fineOne).isZero, isTrue); + expect((fineOne - coarseOne).isZero, isTrue); + + final threeCoarse = Money(BigInt.from(3), CryptoCurrency.btc, 0); + expect(threeCoarse - fineOne, equals(Money(BigInt.two, CryptoCurrency.btc, 0))); + expect(threeCoarse - fineOne, equals(-(fineOne - threeCoarse))); + }); + + test("the result keeps the finer scale", () { + expect((coarseOne + fineOne).decimals, greaterThanOrEqualTo(fineOne.decimals)); + }); + + test("sub-unit precision survives a coarse operand", () { + final oneSatoshi = Money(BigInt.one, CryptoCurrency.btc, 8); + + expect( + coarseOne + oneSatoshi, + equals(Money(BigInt.from(100000001), CryptoCurrency.btc, 8)), + ); + }); + + test("comparison operators align", () { + expect(coarseOne < fineOne, isFalse); + expect(coarseOne <= fineOne, isTrue); + expect(coarseOne >= fineOne, isTrue); + expect(coarseOne > fineOne, isFalse); + expect(Money(BigInt.two, CryptoCurrency.btc, 0) > fineOne, isTrue); + }); + + test("compareTo aligns and agrees with ==", () { + expect(coarseOne.compareTo(fineOne), isZero); + expect(coarseOne.compareTo(fineOne) == 0, equals(coarseOne == fineOne)); + expect(Money(BigInt.two, CryptoCurrency.btc, 0).compareTo(fineOne), isPositive); + expect(Money(BigInt.zero, CryptoCurrency.btc, 0).compareTo(fineOne), isNegative); + }); + + test("toStringWithPrecision", () { + expect( + Money.parse("60000.123456", USD, strictParsing: false).toStringWithPrecision(), + "60000.12", + ); + expect( + Money.parse("1.23456789", USD, strictParsing: false).toStringWithPrecision(), + "1.23", + ); + expect(Money(BigInt.one, CryptoCurrency.btc, 0).toStringWithPrecision(), "1"); + expect( + Money(BigInt.one, CryptoCurrency.btc, 0).toStringWithPrecision(useBaseUnit: true), + "100000000", + ); + }); + + test("toString", () { + expect( + Money.parse("60000.123456", CryptoCurrency.btc, strictParsing: false).toString(), + "60000.123456", + ); + expect( + Money.parse("1.23456789", CryptoCurrency.btc, strictParsing: false).toString(), + "1.23456789", + ); + expect(Money(BigInt.one, CryptoCurrency.btc, 0).toString(), "1"); + }); + }); }); } diff --git a/cw_core/test/amount/utils.dart b/cw_core/test/amount/utils.dart new file mode 100644 index 000000000..70081e62d --- /dev/null +++ b/cw_core/test/amount/utils.dart @@ -0,0 +1,41 @@ +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/currency.dart'; + +class FiatCurrency implements Currency { + const FiatCurrency({ + required this.symbol, + required this.countryCode, + required this.fullName, + this.decimals = 2, + }); + + final String countryCode; + + @override + final String fullName; + + @override + final int decimals; + + @override + final String symbol; + + @override + String? get iconPath => throw UnimplementedError(); + + @override + String get name => throw UnimplementedError(); + + @override + String? get tag => throw UnimplementedError(); + + @override + Money parseAmount(String value) => Money.parse(value, this); + + @override + Money? tryParseAmount(String value) => Money.tryParse(value, this); +} + +const EUR = FiatCurrency(symbol: 'EUR', countryCode: "eur", fullName: "Euro"); +const USD = FiatCurrency(symbol: 'USD', countryCode: "usd", fullName: "US Dollar"); +const JPY = FiatCurrency(symbol: 'JPY', countryCode: "jpn", fullName: "Japanese Yen", decimals: 0); From a8ca70b43d340e075abb88ef09374e4fb3119b6c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 16:00:46 +0000 Subject: [PATCH 08/15] fix(ui): make unchecked NewSimpleCheckbox visible in light themes The unchecked background resolved to colorScheme.surfaceContainerHighest, which is pure white in LightTheme, so the unselected checkbox was an invisible white disc on near-white list rows. In light brightness use onSurfaceVariant with alpha 80 instead; dark themes are unchanged. --- lib/src/widgets/new_list_row/new_simple_checkbox.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/widgets/new_list_row/new_simple_checkbox.dart b/lib/src/widgets/new_list_row/new_simple_checkbox.dart index b6eb7e8e6..5885b6bda 100644 --- a/lib/src/widgets/new_list_row/new_simple_checkbox.dart +++ b/lib/src/widgets/new_list_row/new_simple_checkbox.dart @@ -20,7 +20,9 @@ class NewSimpleCheckbox extends StatelessWidget { decoration: BoxDecoration( color: value ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.surfaceContainerHighest, + : Theme.of(context).brightness == Brightness.light + ? Theme.of(context).colorScheme.onSurfaceVariant.withAlpha(80) + : Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: const BorderRadius.all(Radius.circular(50)), ), height: 24, From 703cc22b33c400ab953e1a679e5f7c41148d2b08 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Mon, 24 Aug 2026 21:21:29 +0100 Subject: [PATCH 09/15] fix: solana wallet bugs and security issues (#3484) * fix android CI * fix: duplicate outgoing tx for jup swaps and stuck pending state * fix solana security risk by handling duplicate token symbols in wallet transactions * feat: implement additional cost handling for pending transactions in Solana wallet. * fix: Items on security audit list for solana wallet * refactor: streamline fee payer index handling and improve error logging * fix: verifySignature for solana and handle wrong mint on default token * fix: token decimals defaulting to zero and breaking amount parsing in solana * fix: use token mask in tx history * refactor: apply lint to modified code * fix: merge conflicts * fix: use Money and mint decimals when parsing sol swaps * test: add unit tests for SPL token amount handling and parsing * test: add more tests * fix: update decimal handling for fetched token and remove unused fields --- cw_core/lib/exceptions.dart | 6 + cw_core/lib/pending_transaction.dart | 1 + cw_core/lib/spl_token.dart | 59 +-- cw_core/test/parse_fixed_test.dart | 13 + cw_core/test/spl_token_amount_test.dart | 104 ++++ cw_solana/lib/default_spl_tokens.dart | 2 +- cw_solana/lib/pending_solana_transaction.dart | 12 +- cw_solana/lib/solana_client.dart | 459 ++++++++---------- cw_solana/lib/solana_exceptions.dart | 4 + cw_solana/lib/solana_transaction_history.dart | 11 +- cw_solana/lib/solana_wallet.dart | 113 +++-- cw_solana/lib/solana_wallet_service.dart | 6 +- .../test/solana_currency_resolution_test.dart | 108 +++++ .../src/cards/cake_pay_buy_card_page.dart | 1 + .../provider/jupiter_exchange_provider.dart | 4 + .../assets_history/history_tile.dart | 4 +- .../widgets/send_page/send_confirm_sheet.dart | 21 + lib/solana/cw_solana.dart | 12 +- .../exchange_trade/exchange_trade_page.dart | 1 + lib/view_model/send/send_view_model.dart | 24 +- res/values/strings_ar.arb | 2 + res/values/strings_bg.arb | 2 + res/values/strings_cs.arb | 2 + res/values/strings_de.arb | 2 + res/values/strings_en.arb | 2 + res/values/strings_es.arb | 2 + res/values/strings_fa.arb | 2 + res/values/strings_fr.arb | 2 + res/values/strings_gn.arb | 2 + res/values/strings_ha.arb | 2 + res/values/strings_hi.arb | 2 + res/values/strings_hr.arb | 2 + res/values/strings_hy.arb | 2 + res/values/strings_id.arb | 2 + res/values/strings_it.arb | 4 +- res/values/strings_ja.arb | 4 +- res/values/strings_ko.arb | 4 +- res/values/strings_my.arb | 4 +- res/values/strings_nl.arb | 4 +- res/values/strings_pl.arb | 4 +- res/values/strings_pt.arb | 4 +- res/values/strings_pt_BR.arb | 2 + res/values/strings_ru.arb | 4 +- res/values/strings_th.arb | 4 +- res/values/strings_tl.arb | 4 +- res/values/strings_tr.arb | 4 +- res/values/strings_uk.arb | 4 +- res/values/strings_ur.arb | 4 +- res/values/strings_vi.arb | 4 +- res/values/strings_yo.arb | 4 +- res/values/strings_zh.arb | 4 +- tool/configure.dart | 1 + 52 files changed, 687 insertions(+), 373 deletions(-) create mode 100644 cw_core/test/spl_token_amount_test.dart create mode 100644 cw_solana/test/solana_currency_resolution_test.dart diff --git a/cw_core/lib/exceptions.dart b/cw_core/lib/exceptions.dart index cc0729ee5..76d4782ad 100644 --- a/cw_core/lib/exceptions.dart +++ b/cw_core/lib/exceptions.dart @@ -55,6 +55,12 @@ class SignSPLTokenTransactionRentException implements Exception {} class NoAssociatedTokenAccountException implements Exception {} +class AmbiguousTokenSymbolException implements Exception { + AmbiguousTokenSymbolException(this.symbol); + + final String symbol; +} + class RestoreFromSeedException implements Exception { final String message; diff --git a/cw_core/lib/pending_transaction.dart b/cw_core/lib/pending_transaction.dart index 3d8912fa3..fb4031936 100644 --- a/cw_core/lib/pending_transaction.dart +++ b/cw_core/lib/pending_transaction.dart @@ -12,6 +12,7 @@ mixin PendingTransaction { Money get amount; Money get fee; + Money? get additionalCost => null; String get amountFormatted; String get feeFormatted => fee.toStringWithSymbol(fractionalDigits: 8); diff --git a/cw_core/lib/spl_token.dart b/cw_core/lib/spl_token.dart index 5ac9330fb..140b50bb4 100644 --- a/cw_core/lib/spl_token.dart +++ b/cw_core/lib/spl_token.dart @@ -3,7 +3,6 @@ import "package:cw_core/db/sqlite.dart"; import "package:sqflite/sqflite.dart"; class SPLToken extends CryptoCurrency { - SPLToken({ required this.name, required this.symbol, @@ -33,17 +32,19 @@ class SPLToken extends CryptoCurrency { required String mint, required String symbol, required String mintAddress, + required int decimal, String? iconPath, bool isPotentialScam = false, - }) => SPLToken( - name: name, - symbol: symbol, - mintAddress: mintAddress, - decimal: 0, - mint: mint, - iconPath: iconPath, - isPotentialScam: isPotentialScam, - ); + }) => + SPLToken( + name: name, + symbol: symbol, + mintAddress: mintAddress, + decimal: decimal, + mint: mint, + iconPath: iconPath, + isPotentialScam: isPotentialScam, + ); SPLToken.copyWith(SPLToken other, {String? icon, String? tag, bool? enabled, String? walletName}) : name = other.name, @@ -125,18 +126,18 @@ class SPLToken extends CryptoCurrency { } Map toMap() => { - selfIdColumn: id, - "walletName": walletName, - "name": name, - "symbol": symbol, - "mintAddress": mintAddress, - "decimal": decimal, - "mint": mint, - "enabled": _enabled ? 1 : 0, - "iconPath": iconPath, - "tag": tag, - "isPotentialScam": isPotentialScam ? 1 : 0, - }; + selfIdColumn: id, + "walletName": walletName, + "name": name, + "symbol": symbol, + "mintAddress": mintAddress, + "decimal": decimal, + "mint": mint, + "enabled": _enabled ? 1 : 0, + "iconPath": iconPath, + "tag": tag, + "isPotentialScam": isPotentialScam ? 1 : 0, + }; static String get tableName => "SPLToken"; static String get selfIdColumn => "${tableName}Id"; @@ -166,7 +167,8 @@ class SPLToken extends CryptoCurrency { return List.generate(list.length, (index) => SPLToken.fromMap(list[index])); } - static Future> getAllForWallet(String walletName) async => selectList("walletName = ?", [walletName]); + static Future> getAllForWallet(String walletName) async => + selectList("walletName = ?", [walletName]); static Future getByMint(String walletName, String mintAddress) async { final list = await selectList("walletName = ? AND mintAddress = ?", [walletName, mintAddress]); @@ -174,12 +176,13 @@ class SPLToken extends CryptoCurrency { } static Future deleteForWallet(String walletName, String mintAddress) => db!.delete( - tableName, - where: "walletName = ? AND mintAddress = ?", - whereArgs: [walletName, mintAddress], - ); + tableName, + where: "walletName = ? AND mintAddress = ?", + whereArgs: [walletName, mintAddress], + ); - static Future deleteAllForWallet(String walletName) => db!.delete(tableName, where: "walletName = ?", whereArgs: [walletName]); + static Future deleteAllForWallet(String walletName) => + db!.delete(tableName, where: "walletName = ?", whereArgs: [walletName]); static Future renameWallet(String oldName, String newName) async { await db!.delete(tableName, where: "walletName = ?", whereArgs: [newName]); diff --git a/cw_core/test/parse_fixed_test.dart b/cw_core/test/parse_fixed_test.dart index c102618e1..b9e905208 100644 --- a/cw_core/test/parse_fixed_test.dart +++ b/cw_core/test/parse_fixed_test.dart @@ -45,6 +45,19 @@ void main() { test('should fail to parse `.`, missing value', () => expect(() => parseFixed(".", 6), throwsFormatException)); }); + + group("parseFixed, zero decimal currency", () { + test("should parse 5 as 5", () => expect(parseFixed("5", 0), BigInt.from(5))); + + test("should parse 0 as 0", () => expect(parseFixed("0", 0), BigInt.from(0))); + + test("should parse 1. as 1", () => expect(parseFixed("1.", 0), BigInt.from(1))); + + test("should parse -3 as -3", () => expect(parseFixed("-3", 0), BigInt.from(-3))); + + test("should fail to parse 5.5, fractional component exceeds decimals", + () => expect(() => parseFixed("5.5", 0), throwsFormatException)); + }); }); group('tryParseFixed', () { diff --git a/cw_core/test/spl_token_amount_test.dart b/cw_core/test/spl_token_amount_test.dart new file mode 100644 index 000000000..d686b8c46 --- /dev/null +++ b/cw_core/test/spl_token_amount_test.dart @@ -0,0 +1,104 @@ +import "package:cw_core/amount/money.dart"; +import "package:cw_core/spl_token.dart"; +import "package:flutter_test/flutter_test.dart"; + +void main() { + group("SPL token raw amounts", () { + test("6 decimal token renders raw base units", () { + final jup = SPLToken( + name: "Jupiter", + symbol: "JUP", + mint: "jup", + mintAddress: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", + decimal: 6, + ); + + expect(Money(BigInt.from(100000), jup).toStringWithPrecision(), "0.1"); + expect(Money(BigInt.from(931614), jup).toStringWithPrecision(), "0.931614"); + }); + + test("8 decimal token renders raw base units", () { + final xstock = SPLToken( + name: "Amazon xStock", + symbol: "AMZNX", + mint: "amznx", + mintAddress: "mintAddressWithEightDecimals", + decimal: 8, + ); + + expect(Money(BigInt.from(341694), xstock).toStringWithPrecision(), "0.00341694"); + }); + + test("the same raw amount means different values per decimals", () { + final raw = BigInt.from(100000); + + final six = SPLToken( + name: "Six", + symbol: "SIX", + mint: "six", + mintAddress: "mintAddressWithSixDecimals", + decimal: 6, + ); + + final nine = SPLToken( + name: "Nine", + symbol: "NINE", + mint: "nine", + mintAddress: "mintAddressWithNineDecimals", + decimal: 9, + ); + + final zero = SPLToken( + name: "Zero", + symbol: "ZERO", + mint: "zero", + mintAddress: "mintAddressWithZeroDecimals", + decimal: 0, + ); + + expect(Money(raw, six).toStringWithPrecision(), "0.1"); + expect(Money(raw, nine).toStringWithPrecision(), "0.0001"); + expect(Money(raw, zero).toStringWithPrecision(), "100000"); + }); + }); + + group("SPL token amount parsing", () { + test("parses whole amounts for a zero decimal token", () { + final zero = SPLToken( + name: "Zero", + symbol: "ZERO", + mint: "zero", + mintAddress: "mintAddressWithZeroDecimals", + decimal: 0, + ); + + expect(Money.parse("5", zero).amount, BigInt.from(5)); + expect(Money.parse("0", zero).amount, BigInt.zero); + }); + + test("parses fractional amounts for a 6 decimal token", () { + final jup = SPLToken( + name: "Jupiter", + symbol: "JUP", + mint: "jup", + mintAddress: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", + decimal: 6, + ); + + expect(Money.parse("0.1", jup).amount, BigInt.from(100000)); + expect(Money.parse("10.339089", jup).amount, BigInt.from(10339089)); + }); + + test("rejects more fractional digits than the token has decimals", () { + final zero = SPLToken( + name: "Zero", + symbol: "ZERO", + mint: "zero", + mintAddress: "mintAddressWithZeroDecimals", + decimal: 0, + ); + + expect(() => Money.parse("5.5", zero), throwsFormatException); + }); + }); +} diff --git a/cw_solana/lib/default_spl_tokens.dart b/cw_solana/lib/default_spl_tokens.dart index c9dd6fac0..48ca274a3 100644 --- a/cw_solana/lib/default_spl_tokens.dart +++ b/cw_solana/lib/default_spl_tokens.dart @@ -87,7 +87,7 @@ class DefaultSPLTokens { symbol: 'GMT', mintAddress: '7i5KKsX2weiTkry7jA4ZwSuXGhs5eJBEjY8vVxR4pfRx', decimal: 9, - mint: 'ray', + mint: 'gmt', iconPath: 'assets/images/gmt_icon.png', enabled: false, ), diff --git a/cw_solana/lib/pending_solana_transaction.dart b/cw_solana/lib/pending_solana_transaction.dart index 2f4f614d5..4594633b8 100644 --- a/cw_solana/lib/pending_solana_transaction.dart +++ b/cw_solana/lib/pending_solana_transaction.dart @@ -13,6 +13,7 @@ class PendingSolanaTransaction with PendingTransaction { required this.serializedTransaction, required this.destinationAddress, required this.sendTransaction, + this.additionalCost, }); @override @@ -22,15 +23,10 @@ class PendingSolanaTransaction with PendingTransaction { final Money fee; @override - String get amountFormatted { - String stringifiedAmount = amount.toString(); + final Money? additionalCost; - if (stringifiedAmount.toString().length >= 6) { - stringifiedAmount = stringifiedAmount.substring(0, 6); - } - - return stringifiedAmount; - } + @override + String get amountFormatted => amount.toString(); @override Future commit() async { diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart index 015cc013c..7e218179c 100644 --- a/cw_solana/lib/solana_client.dart +++ b/cw_solana/lib/solana_client.dart @@ -3,9 +3,7 @@ import 'dart:convert'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_core/amount/money.dart'; -import 'package:cw_core/amount/money_double.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/currency.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/solana_rpc_http_service.dart'; @@ -215,17 +213,10 @@ class SolanaWalletClient { // For native solana transactions if (instruction.accounts.length < 2) continue; - // Get the fee payer index based on transaction type - // For legacy transfers, the first account is usually the fee payer - // For versioned, the first account in instruction is usually the fee payer - final feePayerIndex = - txResponse.version == TransactionType.legacy ? 0 : instruction.accounts[0]; - final transactionModel = await _parseNativeTransaction( message: message, meta: meta, fee: fee, - feePayerIndex: feePayerIndex, walletAddress: walletAddress, signature: signature, blockTime: blockTime, @@ -234,8 +225,8 @@ class SolanaWalletClient { if (transactionModel != null) { return [transactionModel]; } - } else if (programId == SPLTokenProgramConst.tokenProgramId) { - // For SPL Token transactions + } else if (programId == SPLTokenProgramConst.tokenProgramId || + programId == SPLTokenProgramConst.token2022ProgramId) { if (instruction.accounts.length < 2) continue; final transactionModel = await _parseSPLTokenTransaction( @@ -265,7 +256,8 @@ class SolanaWalletClient { bool hasTokenTransfer = false; for (final otherInstruction in instructions) { final otherProgramId = message.accountKeys[otherInstruction.programIdIndex]; - if (otherProgramId == SPLTokenProgramConst.tokenProgramId) { + if (otherProgramId == SPLTokenProgramConst.tokenProgramId || + otherProgramId == SPLTokenProgramConst.token2022ProgramId) { hasTokenTransfer = true; break; } @@ -389,6 +381,18 @@ class SolanaWalletClient { return walletSent && walletReceived; } + static CryptoCurrency currencyForRawAmount(SPLToken? token, int mintDecimals) { + if (token != null && token.decimals == mintDecimals) { + return token; + } + + return CryptoCurrency( + name: (token?.title ?? "TOKEN").toLowerCase(), + title: token?.title ?? "TOKEN", + decimals: mintDecimals, + ); + } + /// Parses a swap transaction and creates dual entries (outgoing and incoming) Future> _parseSwapTransaction({ required VersionedMessage message, @@ -407,6 +411,9 @@ class SolanaWalletClient { final preTokenBalances = meta.preTokenBalances; final postTokenBalances = meta.postTokenBalances; + final walletPaidFee = accountKeys.isNotEmpty && accountKeys.first.address == walletAddress; + final feeAdjustment = walletPaidFee ? BigInt.from(fee) : BigInt.zero; + String? decreasedMintForWallet; String? increasedMintForWallet; @@ -442,8 +449,7 @@ class SolanaWalletClient { decreasedMintForWallet != increasedMintForWallet; // Parse outgoing side (what was sent) - double outgoingAmount = 0.0; - Currency outgoingToken = CryptoCurrency.sol; + Money? outgoingMoney; String? outgoingMintAddress; String? outgoingFrom; String? outgoingTo; @@ -461,12 +467,12 @@ class SolanaWalletClient { if (accountAddress == walletAddress) { final preBalance = preBalances[i]; final postBalance = postBalances[i]; - final balanceChange = preBalance - postBalance; + + final balanceChange = preBalance - postBalance - feeAdjustment; if (balanceChange > BigInt.zero) { // The wallet sent SOL - outgoingAmount = balanceChange.toDouble() / SolanaUtils.lamportsPerSol; - outgoingToken = CryptoCurrency.sol; + outgoingMoney = Money(balanceChange, CryptoCurrency.sol); outgoingMintAddress = null; outgoingFrom = walletAddress; // We find the intermediate account or swap program account @@ -484,7 +490,7 @@ class SolanaWalletClient { } // If no SOL outgoing, we check if there are any SPL token balance changes for the wallet - if (outgoingAmount == 0.0 && preTokenBalances != null) { + if (outgoingMoney == null && preTokenBalances != null && postTokenBalances != null) { for (final preTokenBal in preTokenBalances) { final owner = preTokenBal.owner?.address ?? ''; @@ -494,24 +500,23 @@ class SolanaWalletClient { if (isSplToSplSwap && mint != decreasedMintForWallet) { continue; } - final preAmount = preTokenBal.uiTokenAmount.uiAmount ?? 0.0; + final preRaw = BigInt.tryParse(preTokenBal.uiTokenAmount.amount) ?? BigInt.zero; // We find the corresponding post balance - for (final postTokenBal in postTokenBalances ?? []) { + for (final postTokenBal in postTokenBalances) { final postOwner = postTokenBal.owner?.address ?? ''; final postMint = postTokenBal.mint.address; - final postAmount = postTokenBal.uiTokenAmount.uiAmount ?? 0.0; if (postOwner == walletAddress && postMint == mint) { - final diff = preAmount - postAmount; + final postRaw = BigInt.tryParse(postTokenBal.uiTokenAmount.amount) ?? BigInt.zero; + final diff = preRaw - postRaw; - if (diff > 0) { + if (diff > BigInt.zero) { // The wallet sent tokens - outgoingAmount = diff.toDouble(); - outgoingMintAddress = mint; final token = await getTokenInfo(mint); - outgoingToken = - token ?? const CryptoCurrency(name: "TOKEN", title: "TOKEN", decimals: 6); + outgoingMoney = + Money(diff, currencyForRawAmount(token, preTokenBal.uiTokenAmount.decimals)); + outgoingMintAddress = mint; outgoingFrom = walletAddress; // We find the intermediate account if (instructions.isNotEmpty && instructions[0].accounts.isNotEmpty) { @@ -526,14 +531,15 @@ class SolanaWalletClient { } } - if (outgoingAmount > 0) break; + if (outgoingMoney != null) { + break; + } } } } // Parse incoming side (what was received) - double incomingAmount = 0.0; - Currency incomingToken = CryptoCurrency.sol; + Money? incomingMoney; String? incomingMintAddress; String? incomingFrom; String? incomingTo; @@ -550,12 +556,11 @@ class SolanaWalletClient { if (accountAddress == walletAddress) { final preBalance = preBalances[i]; final postBalance = postBalances[i]; - final balanceChange = postBalance - preBalance; + final balanceChange = postBalance - preBalance + feeAdjustment; if (balanceChange > BigInt.zero) { // The wallet received SOL - incomingAmount = balanceChange.toDouble() / SolanaUtils.lamportsPerSol; - incomingToken = CryptoCurrency.sol; + incomingMoney = Money(balanceChange, CryptoCurrency.sol); incomingMintAddress = null; incomingTo = walletAddress; // We find the intermediate account @@ -573,7 +578,7 @@ class SolanaWalletClient { } // If no SOL incoming, check SPL token incoming using ATA derivation - if (incomingAmount == 0.0 && preTokenBalances != null && postTokenBalances != null) { + if (incomingMoney == null && preTokenBalances != null && postTokenBalances != null) { // Collect all unique mints from token balances (excluding wrapped SOL) final mints = {}; for (final tokenBal in preTokenBalances) { @@ -595,17 +600,22 @@ class SolanaWalletClient { final walletSolAddress = SolAddress(walletAddress); final mintSolAddress = SolAddress(mint); - final ata = AssociatedTokenAccountProgramUtils.associatedTokenAccount( + final standardAta = AssociatedTokenAccountProgramUtils.associatedTokenAccount( mint: mintSolAddress, owner: walletSolAddress, ); - final ataAddress = ata.address.address; + final token2022Ata = AssociatedTokenAccountProgramUtils.associatedTokenAccount( + mint: mintSolAddress, + owner: walletSolAddress, + tokenProgramId: SPLTokenProgramConst.token2022ProgramId, + ); + final ataAddresses = [standardAta.address.address, token2022Ata.address.address]; - // We check if this ATA address appears in the account keys + // We check if either ATA address appears in the account keys int? ataAccountIndex; for (int i = 0; i < accountKeys.length; i++) { final accountKey = accountKeys[i]; - if (accountKey.address == ataAddress) { + if (ataAddresses.contains(accountKey.address)) { ataAccountIndex = i; break; } @@ -613,15 +623,17 @@ class SolanaWalletClient { // If ATA is in the transaction, we check for balance changes if (ataAccountIndex != null) { - double preAmount = 0.0; - double postAmount = 0.0; + BigInt preRaw = BigInt.zero; + BigInt postRaw = BigInt.zero; + int? mintDecimals; // We find the pre balance for (final preTokenBal in preTokenBalances) { final accountIndex = preTokenBal.accountIndex; final tokenMint = preTokenBal.mint.address; if (accountIndex == ataAccountIndex && tokenMint == mint) { - preAmount = preTokenBal.uiTokenAmount.uiAmount?.toDouble() ?? 0.0; + preRaw = BigInt.tryParse(preTokenBal.uiTokenAmount.amount) ?? BigInt.zero; + mintDecimals = preTokenBal.uiTokenAmount.decimals; break; } } @@ -631,18 +643,18 @@ class SolanaWalletClient { final accountIndex = postTokenBal.accountIndex; final tokenMint = postTokenBal.mint.address; if (accountIndex == ataAccountIndex && tokenMint == mint) { - postAmount = postTokenBal.uiTokenAmount.uiAmount?.toDouble() ?? 0.0; + postRaw = BigInt.tryParse(postTokenBal.uiTokenAmount.amount) ?? BigInt.zero; + mintDecimals = postTokenBal.uiTokenAmount.decimals; break; } } - final diff = postAmount - preAmount; - if (diff > 0) { + final diff = postRaw - preRaw; + if (diff > BigInt.zero && mintDecimals != null) { // The wallet received tokens - incomingAmount = diff.toDouble(); - incomingMintAddress = mint; final token = await getTokenInfo(mint); - incomingToken = token ?? const CryptoCurrency(name: "TOKEN", title: "TOKEN", decimals: 6); + incomingMoney = Money(diff, currencyForRawAmount(token, mintDecimals)); + incomingMintAddress = mint; incomingTo = walletAddress; // We find the intermediate account if (instructions.isNotEmpty && instructions[0].accounts.isNotEmpty) { @@ -663,7 +675,7 @@ class SolanaWalletClient { } // Outgoing transaction model - if (outgoingAmount > 0.0 && outgoingFrom != null && outgoingTo != null) { + if (outgoingMoney != null && outgoingFrom != null && outgoingTo != null) { final outgoingId = '${signature}_outgoing'; // We create a composite ID for the outgoing transaction swapTransactions.add(SolanaTransactionModel( @@ -671,7 +683,7 @@ class SolanaWalletClient { from: outgoingFrom, to: outgoingTo, id: outgoingId, - amount: outgoingAmount.toMoney(outgoingToken), + amount: outgoingMoney, programId: outgoingMintAddress == null ? SystemProgramConst.programId.address : SPLTokenProgramConst.tokenProgramId.address, @@ -681,7 +693,7 @@ class SolanaWalletClient { } // Incoming transaction model - if (incomingAmount > 0.0 && incomingFrom != null && incomingTo != null) { + if (incomingMoney != null && incomingFrom != null && incomingTo != null) { final incomingId = '${signature}_incoming'; // We create a composite ID for the incoming transaction swapTransactions.add(SolanaTransactionModel( @@ -689,7 +701,7 @@ class SolanaWalletClient { from: incomingFrom, to: incomingTo, id: incomingId, - amount: incomingAmount.toMoney(incomingToken), + amount: incomingMoney, programId: incomingMintAddress == null ? SystemProgramConst.programId.address : SPLTokenProgramConst.tokenProgramId.address, @@ -705,7 +717,6 @@ class SolanaWalletClient { required VersionedMessage message, required ConfirmedTransactionMeta meta, required int fee, - required int feePayerIndex, required String walletAddress, required String signature, required BigInt? blockTime, @@ -736,8 +747,7 @@ class SolanaWalletClient { // Positive = wallet lost SOL, negative = wallet gained. final walletChange = walletPre - walletPost; - final bool walletPaidFee = - feePayerIndex < accountKeys.length && accountKeys[feePayerIndex].address == walletAddress; + final walletPaidFee = accountKeys.first.address == walletAddress; // Net transfer amount excluding the fee. final netChange = walletPaidFee ? walletChange - BigInt.from(fee) : walletChange; @@ -808,8 +818,9 @@ class SolanaWalletClient { mintAddress = accountKeys[accounts[1]].address; } - double userPreAmount = 0.0; - double userPostAmount = 0.0; + BigInt userPreRaw = BigInt.zero; + BigInt userPostRaw = BigInt.zero; + int? mintDecimals; if (preTokenBalances != null) { for (final preBal in preTokenBalances) { @@ -820,7 +831,8 @@ class SolanaWalletClient { continue; } mintAddress ??= preBal.mint.address; - userPreAmount = preBal.uiTokenAmount.uiAmount ?? 0.0; + mintDecimals = preBal.uiTokenAmount.decimals; + userPreRaw = BigInt.tryParse(preBal.uiTokenAmount.amount) ?? BigInt.zero; break; } } @@ -836,19 +848,21 @@ class SolanaWalletClient { continue; } mintAddress ??= postBal.mint.address; - userPostAmount = postBal.uiTokenAmount.uiAmount ?? 0.0; + mintDecimals = postBal.uiTokenAmount.decimals; + userPostRaw = BigInt.tryParse(postBal.uiTokenAmount.amount) ?? BigInt.zero; break; } } } } - final diff = userPreAmount - userPostAmount; - final rawAmount = diff.abs(); + final diff = userPreRaw - userPostRaw; - final amountInString = rawAmount.toStringAsFixed(6); - final amount = double.parse(amountInString); - final isOutgoing = diff > 0; + if (diff == BigInt.zero || mintDecimals == null) { + return null; + } + + final isOutgoing = diff > BigInt.zero; // Resolve sender/receiver from token balance owners String? senderOwner; @@ -886,7 +900,7 @@ class SolanaWalletClient { from: sender, to: receiver, id: signature, - amount: amount.toMoney(splToken ?? CryptoCurrency.sol), + amount: Money(diff.abs(), currencyForRawAmount(splToken, mintDecimals)), programId: SPLTokenProgramConst.tokenProgramId.address, blockTimeInInt: blockTime?.toInt() ?? 0, fee: Money.fromInt(fee, CryptoCurrency.sol), @@ -1013,8 +1027,7 @@ class SolanaWalletClient { final transactions = []; try { - final signatures = - await _getAllSignaturesSinceLastFetch(address, untilSignature, commitment); + final signatures = await _getAllSignaturesSinceLastFetch(address, untilSignature, commitment); if (signatures.isEmpty) return TransactionSyncResult(transactions: transactions); @@ -1096,11 +1109,9 @@ class SolanaWalletClient { if (associatedTokenAccount == null) { try { - associatedTokenAccount = await _getOrCreateAssociatedTokenAccount( - payerPrivateKey: privateKey, + associatedTokenAccount = await _findAssociatedTokenAccount( mintAddress: SolAddress(mintAddress), ownerAddress: ownerWalletAddress, - shouldCreateATA: false, ); } catch (e, s) { printV('$e \n $s'); @@ -1122,12 +1133,33 @@ class SolanaWalletClient { ); } - final Map tokenInfoCache = {}; + final Map tokenInfoCache = {}; Future getTokenInfo(String mintAddress) async { - if (tokenInfoCache.containsKey(mintAddress)) return tokenInfoCache[mintAddress]; + final cached = tokenInfoCache[mintAddress]; + if (cached != null) { + return cached; + } - return tokenInfoCache[mintAddress] = await fetchSPLTokenInfo(mintAddress); + final fetched = await fetchSPLTokenInfo(mintAddress); + if (fetched != null) { + tokenInfoCache[mintAddress] = fetched; + } + + return fetched; + } + + Future _fetchMintDecimals(String mintAddress) async { + try { + final supply = await _provider!.request( + SolanaRPCGetTokenSupply(account: SolAddress(mintAddress)), + ); + + return supply.decimals; + } catch (e) { + printV("Could not read decimals for mint $mintAddress: ${e.toString()}"); + return null; + } } Future fetchSPLTokenInfo(String mintAddress) async { @@ -1150,18 +1182,29 @@ class SolanaWalletClient { final symbol = decodedResponse['symbol'] ?? ''; final name = decodedResponse['name'] ?? ''; - final decimal = decodedResponse['decimals'] ?? '0'; + final rawDecimals = decodedResponse["decimals"]; final iconPath = decodedResponse['logo'] ?? ''; final filteredTokenSymbol = symbol.replaceFirst(RegExp('^\\\$'), '').replaceAll('\u0000', ''); + final reportedDecimals = + rawDecimals is num ? rawDecimals.toInt() : int.tryParse("${rawDecimals ?? ""}"); + + final decimals = (reportedDecimals != null && reportedDecimals > 0) + ? reportedDecimals + : await _fetchMintDecimals(mintAddress); + + if (decimals == null) { + return null; + } + return SPLToken( name: name, mint: symbol, symbol: filteredTokenSymbol, mintAddress: mintAddress, iconPath: iconPath, - decimal: int.tryParse(decimal) ?? 0, + decimal: decimals, ); } catch (e, s) { printV('Error fetching token info: $e \n $s'); @@ -1189,11 +1232,18 @@ class SolanaWalletClient { String filteredTokenSymbol = metadata.symbol.replaceFirst(RegExp('^\\\$'), '').replaceAll('\u0000', ''); + final decimals = await _fetchMintDecimals(token.mint.address); + + if (decimals == null) { + return null; + } + return SPLToken.fromMetadata( name: metadata.name, mint: metadata.symbol, symbol: filteredTokenSymbol, mintAddress: token.mint.address, + decimal: decimals, iconPath: iconPath, ); } catch (_) {} @@ -1217,7 +1267,7 @@ class SolanaWalletClient { }) async { const commitment = Commitment.confirmed; - if (inputAmount.currency == CryptoCurrency.sol) { + if (tokenMint == null) { return _signNativeTokenTransaction( inputAmount: inputAmount, destinationAddress: destinationAddress, @@ -1229,7 +1279,7 @@ class SolanaWalletClient { } else { return _signSPLTokenTransaction( tokenDecimals: inputAmount.currency.decimals, - tokenMint: tokenMint!, + tokenMint: tokenMint, inputAmount: inputAmount, ownerPrivateKey: ownerPrivateKey, destinationAddress: destinationAddress, @@ -1270,53 +1320,28 @@ class SolanaWalletClient { ); } - Future _getMessageForSPLTokenTransaction({ - required SolAddress ownerAddress, - required SolAddress destinationAddress, - required int tokenDecimals, - required SolAddress mintAddress, - required SolAddress sourceAccount, - required Money amount, - required Commitment commitment, - required SolAddress tokenProgramId, - }) async { - final instructions = [ - SPLTokenProgram.transferChecked( - layout: SPLTokenTransferCheckedLayout( - amount: amount.amount, - decimals: tokenDecimals, - ), - mint: mintAddress, - source: sourceAccount, - destination: destinationAddress, - owner: ownerAddress, - ) - ]; - - final latestBlockhash = await _getLatestBlockhash(commitment); - - return Message.compile( - transactionInstructions: instructions, - payer: ownerAddress, - recentBlockhash: latestBlockhash, - ); - } - Future _getFeeFromCompiledMessage(Message message, Commitment commitment) { final base64Message = base64Encode(message.serialize()); return getFeeForMessage(base64Message, commitment); } - Future hasSufficientFundsLeftForRent({ - required Money inputAmount, - required Money solBalance, - required Money fee, - }) async { + Future _getRentExemptionAmount(int space) async { final rent = await _provider!.request( + SolanaRPCGetMinimumBalanceForRentExemption(size: space), + ); + + return Money(rent, CryptoCurrency.sol); + } + + Future hasSufficientFundsLeftForRent({ + required Money totalOutflow, + required Money solBalance, + }) async { + final rentBuffer = await _provider!.request( SolanaRPCGetMinimumBalanceForRentExemption(size: SolanaTokenAccountUtils.accountSize), ); - return (solBalance - (inputAmount + fee)) > Money(rent, CryptoCurrency.sol); + return (solBalance - totalOutflow) > Money(rentBuffer, CryptoCurrency.sol); } Future _signNativeTokenTransaction({ @@ -1343,8 +1368,7 @@ class SolanaWalletClient { if (!isSendAll) { final hasSufficientFundsLeft = await hasSufficientFundsLeftForRent( - inputAmount: inputAmount, - fee: fee, + totalOutflow: inputAmount + fee, solBalance: solBalance, ); @@ -1357,6 +1381,10 @@ class SolanaWalletClient { if (isSendAll) { final updatedLamports = inputAmount - fee; + if (updatedLamports.isNegative || updatedLamports.isZero) { + throw SolanaTransactionWrongBalanceException(CryptoCurrency.sol); + } + final transaction = _constructNativeTransaction( ownerPrivateKey: ownerPrivateKey, destinationAddress: destinationAddress, @@ -1502,21 +1530,10 @@ class SolanaWalletClient { return SPLTokenProgramConst.tokenProgramId; } - Future _getOrCreateAssociatedTokenAccount({ - required SolanaPrivateKey payerPrivateKey, + Future _findAssociatedTokenAccount({ required SolAddress ownerAddress, required SolAddress mintAddress, - required bool shouldCreateATA, }) async { - // For transaction history loading (shouldCreateATA: false), try standard token program first - // to avoid unnecessary RPC call. Only fetch token program ID when creating accounts. - SolAddress tokenProgramId = SPLTokenProgramConst.tokenProgramId; - - if (shouldCreateATA) { - // Only fetch token program ID when we need to create an account - tokenProgramId = await _getTokenProgramId(mintAddress); - } - // Try with standard token program first (most common case) var associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount( mint: mintAddress, @@ -1539,71 +1556,29 @@ class SolanaWalletClient { // If account exists with standard program, return it if (accountInfo != null) return associatedTokenAccount; - // If not found and we're not creating, try Token-2022 as fallback - if (!shouldCreateATA) { - try { - final token2022ProgramId = await _getTokenProgramId(mintAddress); - if (token2022ProgramId.address != SPLTokenProgramConst.tokenProgramId.address) { - associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount( - mint: mintAddress, - owner: ownerAddress, - tokenProgramId: token2022ProgramId, + // if its not found under the standard program, then we try Token-2022, which derives a different address + try { + final token2022ProgramId = await _getTokenProgramId(mintAddress); + if (token2022ProgramId.address != SPLTokenProgramConst.tokenProgramId.address) { + associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount( + mint: mintAddress, + owner: ownerAddress, + tokenProgramId: token2022ProgramId, + ); + + try { + accountInfo = await _provider!.request( + SolanaRPCGetAccountInfo( + account: associatedTokenAccount.address, + commitment: Commitment.confirmed, + ), ); + if (accountInfo != null) return associatedTokenAccount; + } catch (_) {} + } + } catch (_) {} - try { - accountInfo = await _provider!.request( - SolanaRPCGetAccountInfo( - account: associatedTokenAccount.address, - commitment: Commitment.confirmed, - ), - ); - if (accountInfo != null) return associatedTokenAccount; - } catch (_) {} - } - } catch (_) {} - return null; - } - - // For account creation, use the detected token program ID - associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount( - mint: mintAddress, - owner: ownerAddress, - tokenProgramId: tokenProgramId, - ); - - final payerAddress = payerPrivateKey.publicKey().toAddress(); - - final createAssociatedTokenAccount = AssociatedTokenAccountProgram.associatedTokenAccount( - payer: payerAddress, - associatedToken: associatedTokenAccount.address, - owner: ownerAddress, - mint: mintAddress, - tokenProgramId: tokenProgramId, - ); - - final blockhash = await _getLatestBlockhash(Commitment.confirmed); - - final transaction = SolanaTransaction( - payerKey: payerAddress, - instructions: [createAssociatedTokenAccount], - recentBlockhash: blockhash, - type: TransactionType.v0, - ); - - final serializedTransaction = await _signTransactionInternal( - ownerPrivateKey: payerPrivateKey, - transaction: transaction, - ); - - await sendTransaction( - serializedTransaction: serializedTransaction, - commitment: Commitment.confirmed, - ); - - // Wait for confirmation - await Future.delayed(const Duration(seconds: 2)); - - return associatedTokenAccount; + return null; } Future _signSPLTokenTransaction({ @@ -1620,6 +1595,7 @@ class SolanaWalletClient { ProgramDerivedAddress? associatedSenderAccount; SolAddress senderTokenProgramId = tokenProgramId; + int? senderAccountSpace; try { associatedSenderAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount( @@ -1638,6 +1614,7 @@ class SolanaWalletClient { if (accountInfo != null) { senderTokenProgramId = accountInfo.owner; + senderAccountSpace = accountInfo.space; } else { associatedSenderAccount = null; } @@ -1664,6 +1641,7 @@ class SolanaWalletClient { if (accountInfo != null) { senderTokenProgramId = accountInfo.owner; + senderAccountSpace = accountInfo.space; } else { associatedSenderAccount = null; } @@ -1679,18 +1657,16 @@ class SolanaWalletClient { ); } - // Get or create recipient account using the sender's token program ID - // This ensures both accounts use the same program - ProgramDerivedAddress? associatedRecipientAccount; + final ProgramDerivedAddress associatedRecipientAccount; + bool shouldCreateRecipientAccount = false; + try { - // First, try to get/create with the sender's actual program ID final recipientPDA = AssociatedTokenAccountProgramUtils.associatedTokenAccount( mint: mintAddress, owner: SolAddress(destinationAddress), tokenProgramId: senderTokenProgramId, ); - // Check if account exists with correct program SolanaAccountInfo? recipientInfo; try { recipientInfo = await _provider!.request( @@ -1703,39 +1679,16 @@ class SolanaWalletClient { recipientInfo = null; } - if (recipientInfo != null && recipientInfo.owner.address == senderTokenProgramId.address) { - associatedRecipientAccount = recipientPDA; - } else { - // Create the account with the correct program ID - final createATA = AssociatedTokenAccountProgram.associatedTokenAccount( - payer: ownerPrivateKey.publicKey().toAddress(), - associatedToken: recipientPDA.address, - owner: SolAddress(destinationAddress), - mint: mintAddress, - tokenProgramId: senderTokenProgramId, + if (recipientInfo != null && recipientInfo.owner.address != senderTokenProgramId.address) { + throw SolanaCreateAssociatedTokenAccountException( + "Recipient token account is owned by ${recipientInfo.owner.address}", ); - - final blockhash = await _getLatestBlockhash(Commitment.confirmed); - final createTransaction = SolanaTransaction( - payerKey: ownerPrivateKey.publicKey().toAddress(), - instructions: [createATA], - recentBlockhash: blockhash, - type: TransactionType.v0, - ); - - final serializedCreateTx = await _signTransactionInternal( - ownerPrivateKey: ownerPrivateKey, - transaction: createTransaction, - ); - - await sendTransaction( - serializedTransaction: serializedCreateTx, - commitment: Commitment.confirmed, - ); - - await Future.delayed(const Duration(seconds: 2)); - associatedRecipientAccount = recipientPDA; } + + shouldCreateRecipientAccount = recipientInfo == null; + associatedRecipientAccount = recipientPDA; + } on SolanaCreateAssociatedTokenAccountException { + rethrow; } catch (e) { throw SolanaCreateAssociatedTokenAccountException(e.toString()); } @@ -1751,30 +1704,42 @@ class SolanaWalletClient { decimals: tokenDecimals, ); + final instructions = [ + if (shouldCreateRecipientAccount) + AssociatedTokenAccountProgram.associatedTokenAccountIdempotent( + payer: ownerPrivateKey.publicKey().toAddress(), + associatedToken: associatedRecipientAccount.address, + owner: SolAddress(destinationAddress), + mint: mintAddress, + tokenProgramId: senderTokenProgramId, + ), + transferInstructions, + ]; + final latestBlockHash = await _getLatestBlockhash(commitment); final transaction = SolanaTransaction( payerKey: ownerPrivateKey.publicKey().toAddress(), - instructions: [transferInstructions], + instructions: instructions, recentBlockhash: latestBlockHash, ); - final message = await _getMessageForSPLTokenTransaction( - ownerAddress: ownerPrivateKey.publicKey().toAddress(), - tokenDecimals: tokenDecimals, - mintAddress: mintAddress, - destinationAddress: associatedRecipientAccount.address, - sourceAccount: associatedSenderAccount.address, - amount: inputAmount, - commitment: commitment, - tokenProgramId: tokenProgramId, + final message = Message.compile( + transactionInstructions: instructions, + payer: ownerPrivateKey.publicKey().toAddress(), + recentBlockhash: latestBlockHash, ); final fee = await _getFeeFromCompiledMessage(message, commitment); + // The sender account exists by this point, so its space is set, and the recipient + // account is the same size because it holds the same mint under the same program. + final accountCreationCost = shouldCreateRecipientAccount + ? await _getRentExemptionAmount(senderAccountSpace!) + : Money.zero(CryptoCurrency.sol); + final hasSufficientFundsLeft = await hasSufficientFundsLeftForRent( - inputAmount: Money.zero(CryptoCurrency.sol), - fee: fee, + totalOutflow: accountCreationCost + fee, solBalance: solBalance, ); @@ -1796,6 +1761,7 @@ class SolanaWalletClient { destinationAddress: destinationAddress, sendTransaction: sendTx, fee: fee, + additionalCost: shouldCreateRecipientAccount ? accountCreationCost : null, ); } @@ -1884,19 +1850,10 @@ class SolanaWalletClient { final mint = tokenData['mint'] as String? ?? ''; if (mint.isEmpty) continue; - final amountRaw = tokenData['amountRaw'] as String? ?? '0'; - - final decimals = tokenData['decimals'] as int? ?? 0; - - final associatedTokenAddress = tokenData['associatedTokenAddress'] as String? ?? ''; - tokens.add( MoralisSolanaTokenBalance( mint: mint, amount: amount, - amountRaw: amountRaw, - decimals: decimals, - associatedTokenAddress: associatedTokenAddress, ), ); } @@ -1954,15 +1911,9 @@ class SolanaWalletClient { class MoralisSolanaTokenBalance { final String mint; final double amount; - final String amountRaw; - final int decimals; - final String associatedTokenAddress; const MoralisSolanaTokenBalance({ required this.mint, required this.amount, - required this.amountRaw, - required this.decimals, - required this.associatedTokenAddress, }); } diff --git a/cw_solana/lib/solana_exceptions.dart b/cw_solana/lib/solana_exceptions.dart index 96ba0bb6f..a3873a37a 100644 --- a/cw_solana/lib/solana_exceptions.dart +++ b/cw_solana/lib/solana_exceptions.dart @@ -36,3 +36,7 @@ class SolanaNoAssociatedTokenAccountException extends NoAssociatedTokenAccountEx final String account; final String mint; } + +class SolanaAmbiguousTokenSymbolException extends AmbiguousTokenSymbolException { + SolanaAmbiguousTokenSymbolException(super.symbol); +} diff --git a/cw_solana/lib/solana_transaction_history.dart b/cw_solana/lib/solana_transaction_history.dart index 00fcbec90..bb96ab8b6 100644 --- a/cw_solana/lib/solana_transaction_history.dart +++ b/cw_solana/lib/solana_transaction_history.dart @@ -88,9 +88,14 @@ abstract class SolanaTransactionHistoryBase extends TransactionHistoryBase) { - final tx = SolanaTransactionInfo.fromJson(val); - _update(tx); + if (val is! Map) { + return; + } + + try { + _update(SolanaTransactionInfo.fromJson(val)); + } catch (e) { + printV("Skipping unreadable solana transaction ${entry.key}: ${e.toString()}"); } }); } catch (e) { diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index b41ee421c..55ad9f9f3 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -251,12 +251,7 @@ abstract class SolanaWalletBase await updateTokenBalance(); - final transactionCurrency = balance.keys.firstWhere( - (currency) => - currency.title == credentials.currency.title && - currency.tag == credentials.currency.tag, - orElse: () => throw Exception( - 'Currency ${credentials.currency.title} ${credentials.currency.tag} is not accessible in the wallet, try to enable it first.')); + final transactionCurrency = resolveTransactionCurrency(credentials.currency, balance.keys); final walletBalanceForCurrency = balance[transactionCurrency]!.available; @@ -285,9 +280,8 @@ abstract class SolanaWalletBase } String? tokenMint; - // Token Mint is only needed for transactions that are not native tokens(non-SOL transactions) - if (transactionCurrency.title != CryptoCurrency.sol.title) { - tokenMint = (transactionCurrency as SPLToken).mintAddress; + if (transactionCurrency is SPLToken) { + tokenMint = transactionCurrency.mintAddress; } return _client.signSolanaTransaction( @@ -302,6 +296,33 @@ abstract class SolanaWalletBase ); } + static CryptoCurrency resolveTransactionCurrency( + CryptoCurrency requestedCurrency, + Iterable availableCurrencies, + ) { + final matches = requestedCurrency is SPLToken + ? availableCurrencies + .where((currency) => + currency is SPLToken && currency.mintAddress == requestedCurrency.mintAddress) + .toList(growable: false) + : availableCurrencies + .where((currency) => + currency.title == requestedCurrency.title && currency.tag == requestedCurrency.tag) + .toList(growable: false); + + if (matches.isEmpty) { + throw Exception( + "Currency ${requestedCurrency.title} ${requestedCurrency.tag} is not accessible in the wallet, try to enable it first.", + ); + } + + if (matches.length > 1) { + throw SolanaAmbiguousTokenSymbolException(requestedCurrency.title); + } + + return matches.first; + } + @override Future> fetchTransactions() async => {}; @@ -448,6 +469,8 @@ abstract class SolanaWalletBase } } + static final _swapIdSuffixPattern = RegExp(r"_(outgoing|incoming)$"); + void _addTransactions(List transactions) { final Map result = {}; @@ -464,6 +487,11 @@ abstract class SolanaWalletBase isPending: false, fee: transactionModel.fee, ); + + final baseSignature = transactionModel.id.replaceFirst(_swapIdSuffixPattern, ""); + if (baseSignature != transactionModel.id) { + transactionHistory.transactions.remove(baseSignature); + } } transactionHistory.addMany(result); @@ -588,7 +616,9 @@ abstract class SolanaWalletBase // Fetch SOL and SPL token balances in parallel for better performance await Future.wait([ _fetchSOLBalance().then((solBalance) { - balance[CryptoCurrency.sol] = solBalance; + if (solBalance != null) { + balance[CryptoCurrency.sol] = solBalance; + } }), _updateSplTokenBalancesInternal(tokenMints: tokenMints), ]); @@ -596,10 +626,15 @@ abstract class SolanaWalletBase await save(); } - Future _fetchSOLBalance() async { - final balance = await _client.getBalance(solanaAddress); - - return SolanaBalance(balance); + Future _fetchSOLBalance() async { + try { + return SolanaBalance( + await _client.getBalance(solanaAddress, throwOnError: true), + ); + } catch (e) { + printV("Error fetching SOL balance: ${e.toString()}"); + return null; + } } /// Internal helper to update SPL token balances. @@ -743,7 +778,7 @@ abstract class SolanaWalletBase name: tokenInfo.name, symbol: tokenInfo.symbol, mintAddress: mint, - decimal: moralisToken.decimals, + decimal: tokenInfo.decimal, mint: tokenInfo.mint, iconPath: tokenInfo.iconPath, tag: 'SOL', @@ -886,7 +921,7 @@ abstract class SolanaWalletBase await _clearLastSyncedSignature(source); } - updateTokenBalance(); + await updateTokenBalance(); } Future _removeTokenTransactionsInHistory(SPLToken token) async { @@ -932,47 +967,25 @@ abstract class SolanaWalletBase return Base58Encoder.encode(signature); } - List> bytesFromSigString(String signatureString) { - final regex = RegExp(r'Signature\(\[(.+)\], publicKey: (.+)\)'); - final match = regex.firstMatch(signatureString); - - if (match != null) { - final bytesString = match.group(1)!; - final base58EncodedPublicKeyString = match.group(2)!; - final sigBytes = bytesString.split(', ').map(int.parse).toList(); - - List pubKeyBytes = SolAddrDecoder().decodeAddr(base58EncodedPublicKeyString); - - return [sigBytes, pubKeyBytes]; - } else { - throw const FormatException('Invalid Signature string format'); - } - } - @override Future verifyMessage(String message, String signature, {String? address}) async { - String signatureString = utf8.decode(HEX.decode(signature)); - - List> bytes = bytesFromSigString(signatureString); - - final messageBytes = utf8.encode(message); - final sigBytes = bytes[0]; - final pubKeyBytes = bytes[1]; - - if (address == null) { + if (address == null || address.isEmpty) { return false; } - // make sure the address derived from the public key provided matches the one we expect - final pub = SolanaPublicKey.fromBytes(pubKeyBytes); - if (address != pub.toAddress().address) { + try { + final signatureBytes = Base58Decoder.decode(signature); + + final publicKey = SolanaPublicKey.fromBytes(SolAddrDecoder().decodeAddr(address)); + + return publicKey.verify( + message: utf8.encode(message), + signature: signatureBytes, + ); + } catch (e) { + printV("Error verifying solana message: ${e.toString()}"); return false; } - - return pub.verify( - message: messageBytes, - signature: sigBytes, - ); } SolanaRPC? get solanaProvider => _client.getSolanaProvider; diff --git a/cw_solana/lib/solana_wallet_service.dart b/cw_solana/lib/solana_wallet_service.dart index a557507f5..85eae6024 100644 --- a/cw_solana/lib/solana_wallet_service.dart +++ b/cw_solana/lib/solana_wallet_service.dart @@ -92,7 +92,7 @@ class SolanaWalletService extends WalletService< @override Future remove(String wallet) async { - File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true); + await File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true); final walletInfo = await WalletInfo.get(wallet, getType()); if (walletInfo == null) { throw Exception('Wallet not found'); @@ -104,8 +104,8 @@ class SolanaWalletService extends WalletService< } final prefs = await SharedPreferences.getInstance(); - for (final key in prefs.getKeys().where( - (k) => k.startsWith('solana_last_synced_signature_${wallet}_'))) { + for (final key + in prefs.getKeys().where((k) => k.startsWith('solana_last_synced_signature_${wallet}_'))) { await prefs.remove(key); } } diff --git a/cw_solana/test/solana_currency_resolution_test.dart b/cw_solana/test/solana_currency_resolution_test.dart new file mode 100644 index 000000000..ef97ec399 --- /dev/null +++ b/cw_solana/test/solana_currency_resolution_test.dart @@ -0,0 +1,108 @@ +import "package:cw_core/crypto_currency.dart"; +import "package:cw_core/spl_token.dart"; +import "package:cw_solana/solana_client.dart"; +import "package:cw_solana/solana_exceptions.dart"; +import "package:cw_solana/solana_wallet.dart"; +import "package:flutter_test/flutter_test.dart"; + +void main() { + group("currencyForRawAmount", () { + test("keeps the stored token when its decimals match the mint", () { + final jup = SPLToken( + name: "Jupiter", + symbol: "JUP", + mint: "jup", + mintAddress: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", + decimal: 6, + ); + + expect(SolanaWalletClient.currencyForRawAmount(jup, 6), same(jup)); + }); + + test("prefers the mint decimals when the stored token disagrees", () { + final staleJup = SPLToken( + name: "Jupiter", + symbol: "JUP", + mint: "jup", + mintAddress: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", + decimal: 0, + ); + + final resolved = SolanaWalletClient.currencyForRawAmount(staleJup, 6); + + expect(resolved.decimals, 6); + expect(resolved.title, "JUP"); + expect(resolved, isNot(same(staleJup))); + }); + + test("falls back to a placeholder title when the token is unknown", () { + final resolved = SolanaWalletClient.currencyForRawAmount(null, 8); + + expect(resolved.decimals, 8); + expect(resolved.title, "TOKEN"); + expect(resolved.name, "token"); + }); + + test("always reports the mint decimals", () { + for (final decimals in [0, 1, 6, 8, 9, 18]) { + expect(SolanaWalletClient.currencyForRawAmount(null, decimals).decimals, decimals); + } + }); + }); + + group("resolveTransactionCurrency", () { + final realUsdc = SPLToken( + name: "USD Coin", + symbol: "USDC", + mint: "usdc", + mintAddress: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + decimal: 6, + ); + + final scamUsdc = SPLToken( + name: "USD Coin", + symbol: "USDC", + mint: "usdc", + mintAddress: "scamMintAddressClaimingTheUsdcSymbol", + decimal: 9, + ); + + test("picks the requested mint even when another token claims the symbol", () { + final resolved = SolanaWalletBase.resolveTransactionCurrency(realUsdc, [scamUsdc, realUsdc]); + + expect(resolved, same(realUsdc)); + expect((resolved as SPLToken).mintAddress, realUsdc.mintAddress); + }); + + test("picks the scam mint only when the scam mint is the one requested", () { + final resolved = SolanaWalletBase.resolveTransactionCurrency(scamUsdc, [scamUsdc, realUsdc]); + + expect(resolved, same(scamUsdc)); + }); + + test("throws when the requested mint is not in the wallet", () { + expect( + () => SolanaWalletBase.resolveTransactionCurrency(realUsdc, [scamUsdc]), + throwsA(isA()), + ); + }); + + test("throws on ambiguity when a symbol lookup matches two tokens", () { + expect( + () => SolanaWalletBase.resolveTransactionCurrency( + CryptoCurrency(name: "usdc", title: "USDC", decimals: 6, tag: "SOL"), + [scamUsdc, realUsdc], + ), + throwsA(isA()), + ); + }); + + test("resolves a native currency by title and tag", () { + expect( + SolanaWalletBase.resolveTransactionCurrency( + CryptoCurrency.sol, [CryptoCurrency.sol, realUsdc]), + same(CryptoCurrency.sol), + ); + }); + }); +} diff --git a/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart b/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart index d10233a34..8e0a470cd 100644 --- a/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart +++ b/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart @@ -631,6 +631,7 @@ class CakePayBuyCardPage extends BasePage { amountValue: _sendViewModel.amountParsingProxy .asDisplayStringWithSymbol(_sendViewModel.pendingTransaction!.amount), quantity: 'QTY: ${cakePayBuyCardViewModel.quantity}', + explanation: _sendViewModel.pendingTransactionAdditionalCostNotice, fiatAmountValue: _sendViewModel.pendingTransactionFiatAmountFormatted, fee: S.of(bottomSheetContext).send_fee, feeValue: _sendViewModel.amountParsingProxy diff --git a/lib/exchange/provider/jupiter_exchange_provider.dart b/lib/exchange/provider/jupiter_exchange_provider.dart index a8ea814a4..2f1fc9070 100644 --- a/lib/exchange/provider/jupiter_exchange_provider.dart +++ b/lib/exchange/provider/jupiter_exchange_provider.dart @@ -138,6 +138,10 @@ class JupiterExchangeProvider extends ExchangeProvider { if (!_isSolanaCurrency(from) || !_isSolanaCurrency(to)) { return 0.0; } + + if (amount <= 0) { + return 0.0; + } final inputMint = _getTokenMint(from); final outputMint = _getTokenMint(to); diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_tile.dart b/lib/new-ui/widgets/coins_page/assets_history/history_tile.dart index f26b0790e..426957a9b 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/history_tile.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/history_tile.dart @@ -100,9 +100,9 @@ class HistoryTile extends StatelessWidget { children: [ Opacity( opacity: pending ? 0.5 : 1, - child: CakeImageWidget( + child: TokenImageWidget( imageUrl: asset?.iconPath ?? "", - width: 34, + size: 34, ), ), Align( diff --git a/lib/new-ui/widgets/send_page/send_confirm_sheet.dart b/lib/new-ui/widgets/send_page/send_confirm_sheet.dart index 5b34686d3..35f710209 100644 --- a/lib/new-ui/widgets/send_page/send_confirm_sheet.dart +++ b/lib/new-ui/widgets/send_page/send_confirm_sheet.dart @@ -201,6 +201,7 @@ class SendTransactionDetails extends StatelessWidget { Widget _buildMainContent(BuildContext context) { return Observer(builder: (context) { final transaction = sendViewModel.pendingTransaction; + final additionalCostNotice = sendViewModel.pendingTransactionAdditionalCostNotice; final currencySymbol = sendViewModel.amountParsingProxy.getCryptoSymbol(sendViewModel.selectedCryptoCurrency); @@ -398,6 +399,26 @@ class SendTransactionDetails extends StatelessWidget { ), ), ), + if (additionalCostNotice != null) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Container( + height: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + ), + Padding( + padding: const EdgeInsets.all(12), + child: Text( + additionalCostNotice, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ], if (sendViewModel.isElectrumWallet) ...[ Padding( padding: EdgeInsets.symmetric(horizontal: 12), diff --git a/lib/solana/cw_solana.dart b/lib/solana/cw_solana.dart index 77656732c..46edfca2a 100644 --- a/lib/solana/cw_solana.dart +++ b/lib/solana/cw_solana.dart @@ -130,11 +130,11 @@ class CWSolana extends Solana { final token = (wallet as SolanaWallet).splTokenBySymbol(transaction.amount.currency.symbol); - if (token == null) { - throw StateError('No SPL token for symbol ${transaction.amount.currency.symbol}'); + if (token != null) { + return token; } - return token; + return transaction.amount.currency as CryptoCurrency; } @override @@ -276,6 +276,8 @@ class CWSolana extends Solana { 'Jupiter swap returned unknown status: $status. Error: $errorMessage. Code: $errorCode', ); } + } on JupiterSwapFailedException { + rethrow; } catch (e) { throw Exception('Failed to execute Jupiter swap: $e'); } @@ -420,7 +422,9 @@ class CWSolana extends Solana { if (discoveredMints.isNotEmpty) { await wallet.updateSPLTokenTransactions(specificMints: discoveredMints); } - } catch (_) {} + } catch (e) { + printV("Error discovering wallet tokens: ${e.toString()}"); + } } @override diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index 73966ab33..87bbc39f7 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -328,6 +328,7 @@ class ExchangeTradeState extends State { amount: S.of(bottomSheetContext).send_amount, amountValue: sendVM.amountParsingProxy .getDisplayCryptoAmount(amountValue, sendVM.selectedCryptoCurrency), + explanation: sendVM.pendingTransactionAdditionalCostNotice, fiatAmountValue: fiatAmountValue, fee: isEVMCompatibleChain(sendVM.walletType) ? S.of(bottomSheetContext).send_estimated_fee diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index df85d0cac..9e3fa7f55 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -336,6 +336,18 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor @observable PendingTransaction? pendingTransaction; + String? get pendingTransactionAdditionalCostNotice { + final additionalCost = pendingTransaction?.additionalCost; + + if (additionalCost == null) { + return null; + } + + return S.current.recipient_account_creation_fee( + _appStore.amountParsingProxy.asDisplayStringWithSymbol(additionalCost), + ); + } + @computed String get balance { if (walletType == WalletType.litecoin && coinTypeToSpendFrom == UnspentCoinType.mweb) { @@ -1179,11 +1191,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name), DateTime.now().add(Duration(minutes: 1)).toIso8601String()); } catch (e) { - if (e is JupiterSwapFailedException) { - await _updateSolanaTrade(signature: e.signature, isSuccess: false); - } state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency)); - await _updateSolanaTrade(signature: '', isSuccess: false); + + final failedSignature = e is JupiterSwapFailedException ? e.signature : ""; + + await _updateSolanaTrade(signature: failedSignature, isSuccess: false); } } @@ -1509,6 +1521,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor return S.current.solana_no_associated_token_account_exception; } + if (error is AmbiguousTokenSymbolException) { + return S.current.ambiguous_token_symbol_exception(error.symbol); + } + if (errorMessage.contains('found no record of a prior credit')) { return S.current.insufficient_funds_for_tx; } diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 5d3eb9f2d..24f501597 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -61,6 +61,7 @@ "already_have_account": "هل لديك حساب بالفعل؟", "already_your_username": "هذا هو اسم المستخدم الخاص بك بالفعل!", "always": "دائمًا", + "ambiguous_token_symbol_exception": "يستخدم أكثر من رمز مميز في هذه المحفظة الرمز ${symbol}. قم بتعطيل الرمز المميز المكرر حتى يمكن التعرف على الرمز الصحيح قبل الإرسال.", "amount": "الكمية: ", "amount_is_below_minimum_limit": "سيكون رصيدك بعد الرسوم أقل من الحد الأدنى للمبلغ المطلوب للتبادل (${min})", "amount_is_estimate": "مبلغ الاستلام تقديري", @@ -845,6 +846,7 @@ "received": "تم الاستلام", "receiving": "الاستلام", "recieving": "جارٍ الاستلام", + "recipient_account_creation_fee": "هناك حاجة إلى ${amount} إضافية لإنشاء حساب رمزي للمستلم، لأنه ليس لديه حساب حتى الآن.", "recipient_address": "عنوان المستلم", "reconnect": "إعادة الاتصال", "reconnect_alert_text": "هل أنت متأكد أنك تريد إعادة الاتصال؟", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index d14612fb7..b443d1af8 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -61,6 +61,7 @@ "already_have_account": "Вече имате акаунт?", "already_your_username": "Това вече е вашето потребителско име!", "always": "Винаги", + "ambiguous_token_symbol_exception": "Повече от един токен в този портфейл използва символа ${symbol}. Деактивирайте дублиращия се токен, за да може правилният да бъде идентифициран преди изпращане.", "amount": "Количество: ", "amount_is_below_minimum_limit": "Вашият баланс след таксите ще бъде по-малък от минималната сума, необходима за обмяната (${min})", "amount_is_estimate": "Сумата за получаване е приблизителна", @@ -845,6 +846,7 @@ "received": "Получено", "receiving": "Получаване", "recieving": "Получаване", + "recipient_account_creation_fee": "Необходими са допълнителни ${amount} за създаване на токен акаунт за получателя, тъй като той все още няма такъв.", "recipient_address": "Адрес на получателя", "reconnect": "Повторно свързване", "reconnect_alert_text": "Сигурни ли сте, че искате да се свържете отново?", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index c0c0815d6..f62daa6d3 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -61,6 +61,7 @@ "already_have_account": "Už máte účet?", "already_your_username": "Toto už je vaše uživatelské jméno!", "always": "Vždy", + "ambiguous_token_symbol_exception": "Více než jeden token v této peněžence používá symbol ${symbol}. Deaktivujte duplicitní token, aby bylo možné před odesláním identifikovat ten správný.", "amount": "Částka: ", "amount_is_below_minimum_limit": "Váš zůstatek po poplatcích by byl nižší než minimální částka potřebná pro směnu (${min})", "amount_is_estimate": "Přijatá částka je pouze odhad.", @@ -845,6 +846,7 @@ "received": "Přijato", "receiving": "Přijímání", "recieving": "Přijímání", + "recipient_account_creation_fee": "K vytvoření tokenového účtu pro příjemce je potřeba další ${amount}, protože ještě žádný nemá.", "recipient_address": "Adresa příjemce", "reconnect": "Znovu připojit", "reconnect_alert_text": "Opravdu se chcete znovu připojit?", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 9b953104e..dd7abc180 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -63,6 +63,7 @@ "already_have_account": "Haben Sie bereits ein Konto?", "already_your_username": "Das ist bereits Ihr Benutzername!", "always": "Immer", + "ambiguous_token_symbol_exception": "Mehr als ein Token in dieser Wallet verwendet das Symbol ${symbol}. Deaktivieren Sie das Duplikat-Token, damit das richtige vor dem Senden identifiziert werden kann.", "amount": "Betrag: ", "amount_hidden": "Versteckter Betrag", "amount_is_below_minimum_limit": "Ihr Guthaben nach Gebühren läge unter dem für den Swap erforderlichen Mindestbetrag (${min}).", @@ -866,6 +867,7 @@ "received": "Empfangen", "receiving": "Empfangen", "recieving": "Empfangen", + "recipient_account_creation_fee": "Für die Erstellung eines Token-Kontos für den Empfänger ist ein zusätzlicher Betrag von ${amount} erforderlich, da dieser noch keins hat.", "recipient_address": "Empfängeradresse", "recipient_number": "Empfänger ${number}", "reconnect": "Erneut verbinden", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 7b65547c6..9c04a21c4 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -63,6 +63,7 @@ "already_have_account": "Already have an account?", "already_your_username": "This is already your username!", "always": "Always", + "ambiguous_token_symbol_exception": "More than one token in this wallet uses the symbol ${symbol}. Disable the duplicate token so the correct one can be identified before sending.", "amount": "Amount: ", "amount_hidden": "Amount hidden", "amount_is_below_minimum_limit": "Your balance after fees would be less than the minimum amount needed for the exchange (${min})", @@ -868,6 +869,7 @@ "received": "Received", "receiving": "Receiving", "recieving": "Recieving", + "recipient_account_creation_fee": "An extra ${amount} is needed to create a token account for the recipient, because they do not have one yet.", "recipient_address": "Recipient address", "recipient_number": "Recipient ${number}", "reconnect": "Reconnect", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 1e29a5c19..78321bd2c 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -61,6 +61,7 @@ "already_have_account": "¿Ya tienes una cuenta?", "already_your_username": "¡Este ya es tu nombre de usuario!", "always": "Siempre", + "ambiguous_token_symbol_exception": "Más de un token en esta billetera usa el símbolo ${symbol}. Deshabilite el token duplicado para que se pueda identificar el correcto antes de enviarlo.", "amount": "Cantidad: ", "amount_is_below_minimum_limit": "Tu saldo después de las comisiones sería inferior al importe mínimo necesario para el intercambio (${min})", "amount_is_estimate": "La cantidad a recibir es una estimación", @@ -847,6 +848,7 @@ "received": "Recibido", "receiving": "Recibiendo", "recieving": "Recibiendo", + "recipient_account_creation_fee": "Se necesita ${amount} adicional para crear una cuenta simbólica para el destinatario, porque aún no tiene una.", "recipient_address": "Dirección del destinatario", "reconnect": "Reconectar", "reconnect_alert_text": "¿Estás seguro de que quieres reconectar?", diff --git a/res/values/strings_fa.arb b/res/values/strings_fa.arb index f40a8e113..cf886a43b 100644 --- a/res/values/strings_fa.arb +++ b/res/values/strings_fa.arb @@ -61,6 +61,7 @@ "already_have_account": "از قبل حساب کاربری دارید؟", "already_your_username": "این از قبل نام کاربری شماست!", "always": "همیشه", + "ambiguous_token_symbol_exception": "بیش از یک توکن در این کیف پول از نماد ${symbol} استفاده می‌کند. رمز تکراری را غیرفعال کنید تا قبل از ارسال، کد صحیح شناسایی شود.", "amount": "مقدار: ", "amount_is_below_minimum_limit": "موجودی شما پس از کسر کارمزد کمتر از حداقل مقدار مورد نیاز برای مبادله (${min}) خواهد بود", "amount_is_estimate": "مقدار دریافتی تقریبی است", @@ -845,6 +846,7 @@ "received": "دریافت‌شده", "receiving": "دریافت", "recieving": "دریافت", + "recipient_account_creation_fee": "یک ${amount} اضافی برای ایجاد یک حساب رمزی برای گیرنده مورد نیاز است، زیرا آنها هنوز حسابی ندارند.", "recipient_address": "آدرس دریافت‌کننده", "reconnect": "اتصال دوباره", "reconnect_alert_text": "آیا مطمئن هستید که می‌خواهید دوباره وصل شوید؟", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 0ced5ec4d..d3f135c88 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -61,6 +61,7 @@ "already_have_account": "Vous avez déjà un compte ?", "already_your_username": "C'est déjà votre nom d'utilisateur !", "always": "Toujours", + "ambiguous_token_symbol_exception": "Plus d'un jeton dans ce portefeuille utilise le symbole ${symbol}. Désactivez le jeton en double afin que le bon puisse être identifié avant l'envoi.", "amount": "Montant : ", "amount_is_below_minimum_limit": "Votre solde après les frais serait inférieur au montant minimum requis pour l'échange (${min})", "amount_is_estimate": "Le montant à recevoir est une estimation", @@ -845,6 +846,7 @@ "received": "Reçu", "receiving": "Réception", "recieving": "Réception", + "recipient_account_creation_fee": "Un ${amount} supplémentaire est nécessaire pour créer un compte token pour le destinataire, car il n'en a pas encore.", "recipient_address": "Adresse du destinataire", "reconnect": "Reconnecter", "reconnect_alert_text": "Êtes-vous sûr de vouloir vous reconnecter ?", diff --git a/res/values/strings_gn.arb b/res/values/strings_gn.arb index 679d73244..551e16d42 100644 --- a/res/values/strings_gn.arb +++ b/res/values/strings_gn.arb @@ -61,6 +61,7 @@ "already_have_account": "¿Erekóma peteĩ cuenta?", "already_your_username": "¡Kóva ha'e nde puruhára réra voi!", "always": "Tapia", + "ambiguous_token_symbol_exception": "Hetave peteĩ token ko billetera-pe oipuru símbolo ${symbol}. Embogue pe token duplicado ikatu hag̃uáicha ojekuaa pe hekopetegua oñemondo mboyve.", "amount": "Hetakue: ", "amount_is_below_minimum_limit": "Nde saldo, pe comisión rire, saʼivéta pe mínimo oñeikotevẽva hag̃ua pe intercambio (${min})", "amount_is_estimate": "Pe monto oñemog̃uahẽtáva ha’e peteĩ jehecha hag̃a.", @@ -849,6 +850,7 @@ "received": "Ojapyhy", "receiving": "Oñemog̃uahẽ", "recieving": "Oñemog̃uahẽ", + "recipient_account_creation_fee": "Oñeikotevẽ peteĩ ${amount} extra ojejapo hag̃ua peteĩ cuenta token rehegua pe ohupytysévape g̃uarã, ndorekóigui gueteri hikuái peteĩ.", "recipient_address": "Ojapyhýva kundaharape", "reconnect": "Eñembojoaju jey", "reconnect_alert_text": "¿Reime añetehápe reñembojoaju jeyse?", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 1df9ce6fa..ea901d7c4 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -61,6 +61,7 @@ "already_have_account": "Kana da asusu tuni?", "already_your_username": "Wannan tuni sunan mai amfani naka ne!", "always": "Koyaushe", + "ambiguous_token_symbol_exception": "Alama fiye da ɗaya a cikin wannan jakar tana amfani da alamar ${symbol}. Kashe alamar kwafin don a iya gano madaidaicin kafin aikawa.", "amount": "Adadi: ", "amount_is_below_minimum_limit": "Ma'auninka bayan kuɗaɗen ma'amala zai zama ƙasa da mafi ƙarancin adadin da ake buƙata don musayar (${min})", "amount_is_estimate": "Adadin da za a karɓa kimantawa ne", @@ -847,6 +848,7 @@ "received": "An karɓa", "receiving": "Karɓa", "recieving": "Ana karɓa", + "recipient_account_creation_fee": "Ana buƙatar ƙarin ${amount} don ƙirƙirar asusun token ga mai karɓa, saboda ba su da ɗaya tukuna.", "recipient_address": "Adireshin mai karɓa", "reconnect": "Sake haɗawa", "reconnect_alert_text": "Shin kun tabbata kuna son sake haɗawa?", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 0aa460024..15edf0080 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -61,6 +61,7 @@ "already_have_account": "क्या आपके पास पहले से एक खाता है?", "already_your_username": "यह पहले से ही आपका यूज़रनेम है!", "always": "हमेशा", + "ambiguous_token_symbol_exception": "इस वॉलेट में एक से अधिक टोकन प्रतीक ${symbol} का उपयोग करते हैं। डुप्लिकेट टोकन को अक्षम करें ताकि भेजने से पहले सही टोकन की पहचान की जा सके।", "amount": "राशि: ", "amount_is_below_minimum_limit": "फीस के बाद आपका बैलेंस एक्सचेंज के लिए आवश्यक न्यूनतम राशि (${min}) से कम होगा", "amount_is_estimate": "प्राप्त होने वाली राशि अनुमानित है", @@ -847,6 +848,7 @@ "received": "प्राप्त", "receiving": "प्राप्त करना", "recieving": "प्राप्त हो रहा है", + "recipient_account_creation_fee": "प्राप्तकर्ता के लिए एक टोकन खाता बनाने के लिए अतिरिक्त ${amount} की आवश्यकता होती है, क्योंकि उनके पास अभी तक कोई खाता नहीं है।", "recipient_address": "प्राप्तकर्ता का पता", "reconnect": "पुनः कनेक्ट करें", "reconnect_alert_text": "क्या आप वाकई फिर से कनेक्ट करना चाहते हैं?", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 4f5271137..2d99234eb 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -61,6 +61,7 @@ "already_have_account": "Već imate račun?", "already_your_username": "Ovo je već vaše korisničko ime!", "always": "Uvijek", + "ambiguous_token_symbol_exception": "Više od jednog tokena u ovom novčaniku koristi simbol ${symbol}. Onemogućite dvostruki token kako bi se ispravan mogao identificirati prije slanja.", "amount": "Iznos: ", "amount_is_below_minimum_limit": "Vaš saldo nakon naknada bio bi manji od minimalnog iznosa potrebnog za razmjenu (${min})", "amount_is_estimate": "Primljeni iznos je procijenjen", @@ -845,6 +846,7 @@ "received": "Primljeno", "receiving": "Primanje", "recieving": "Primanje", + "recipient_account_creation_fee": "Dodatni iznos od ${amount} je potreban za stvaranje token računa za primatelja jer ga on još nema.", "recipient_address": "Adresa primatelja", "reconnect": "Ponovno poveži", "reconnect_alert_text": "Jeste li sigurni da se želite ponovno povezati?", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index a5930373f..f1a209c91 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -61,6 +61,7 @@ "already_have_account": "Արդեն ունե՞ք հաշիվ։", "already_your_username": "Սա արդեն ձեր օգտանունն է։", "always": "Միշտ", + "ambiguous_token_symbol_exception": "Այս դրամապանակում մեկից ավելի թոքեններ օգտագործում են ${symbol} խորհրդանիշը: Անջատեք կրկնօրինակ նշանը, որպեսզի ճիշտը հնարավոր լինի նույնացնել նախքան ուղարկելը:", "amount": "Գումար՝ ", "amount_is_below_minimum_limit": "Միջնորդավճարներից հետո Ձեր մնացորդը կլինի փոխանակման համար անհրաժեշտ նվազագույն գումարից (${min}) պակաս", "amount_is_estimate": "Ստացվող գումարը մոտավոր է", @@ -845,6 +846,7 @@ "received": "Ստացված", "receiving": "Ստացում", "recieving": "Ստացում", + "recipient_account_creation_fee": "Ստացողի համար խորհրդանշական հաշիվ ստեղծելու համար անհրաժեշտ է լրացուցիչ ${amount}, քանի որ նրանք դեռ չունեն:", "recipient_address": "Ստացողի հասցե", "reconnect": "Վերամիանալ", "reconnect_alert_text": "Վստա՞հ եք, որ ցանկանում եք կրկին միանալ:", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 16fcb4881..c1fea5b4d 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -61,6 +61,7 @@ "already_have_account": "Sudah punya akun?", "already_your_username": "Ini sudah merupakan nama pengguna Anda!", "always": "Selalu", + "ambiguous_token_symbol_exception": "Lebih dari satu token di dompet ini menggunakan simbol ${symbol}. Nonaktifkan token duplikat sehingga token yang benar dapat diidentifikasi sebelum dikirim.", "amount": "Jumlah: ", "amount_is_below_minimum_limit": "Saldo Anda setelah biaya akan lebih rendah dari jumlah minimum yang diperlukan untuk penukaran (${min})", "amount_is_estimate": "Jumlah yang diterima adalah perkiraan", @@ -847,6 +848,7 @@ "received": "Diterima", "receiving": "Menerima", "recieving": "Menerima", + "recipient_account_creation_fee": "${amount} tambahan diperlukan untuk membuat akun token bagi penerima, karena mereka belum memilikinya.", "recipient_address": "Alamat penerima", "reconnect": "Hubungkan kembali", "reconnect_alert_text": "Apakah Anda yakin ingin terhubung kembali?", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index a7f73b629..d7ae0b9c0 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -61,6 +61,7 @@ "already_have_account": "Hai già un account?", "already_your_username": "Questo è già il tuo nome utente!", "always": "Sempre", + "ambiguous_token_symbol_exception": "Più di un token in questo portafoglio utilizza il simbolo ${symbol}. Disabilitare il token duplicato in modo da poter identificare quello corretto prima dell'invio.", "amount": "Importo: ", "amount_is_below_minimum_limit": "Il saldo dopo le commissioni sarebbe inferiore all'importo minimo richiesto per lo scambio (${min})", "amount_is_estimate": "L'importo da ricevere è una stima", @@ -1476,5 +1477,6 @@ "zcash_card_enable_later": "Puoi sempre abilitare questa carta in seguito nelle impostazioni", "zcash_card_missing_funds": "Fondi mancanti?", "zcash_card_scan": "Scansiona", - "zcash_card_warning": "Non chiudere l'app finché la procedura non è completata, altrimenti sarà necessario riavviare il processo da zero." + "zcash_card_warning": "Non chiudere l'app finché la procedura non è completata, altrimenti sarà necessario riavviare il processo da zero.", + "recipient_account_creation_fee": "È necessario un extra di ${amount} per creare un account token per il destinatario, perché non ne ha ancora uno." } \ No newline at end of file diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 8acfdaf15..e9708e48d 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -61,6 +61,7 @@ "already_have_account": "すでにアカウントをお持ちですか?", "already_your_username": "これはすでにあなたのユーザー名です!", "always": "常に", + "ambiguous_token_symbol_exception": "このウォレット内の複数のトークンは、シンボル ${symbol} を使用しています。送信前に正しいトークンを識別できるように、重複したトークンを無効にします。", "amount": "金額: ", "amount_is_below_minimum_limit": "手数料差し引き後の残高が、交換に必要な最低金額(${min})を下回ります", "amount_is_estimate": "受け取り金額は概算です", @@ -1474,5 +1475,6 @@ "zcash_card_enable_later": "このカードは後から設定でいつでも有効にできます", "zcash_card_missing_funds": "資金が見当たりませんか?", "zcash_card_scan": "スキャン", - "zcash_card_warning": "手順が完了するまでアプリを閉じないでください。閉じると、このプロセスを最初からやり直す必要があります。" + "zcash_card_warning": "手順が完了するまでアプリを閉じないでください。閉じると、このプロセスを最初からやり直す必要があります。", + "recipient_account_creation_fee": "受信者はまだトークン アカウントを持っていないため、追加の ${amount} が必要です。" } \ No newline at end of file diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 2777da024..35dd2e704 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -61,6 +61,7 @@ "already_have_account": "이미 계정이 있으신가요?", "already_your_username": "이미 사용 중인 사용자 이름입니다!", "always": "항상", + "ambiguous_token_symbol_exception": "이 지갑에 있는 두 개 이상의 토큰이 ${symbol} 기호를 사용합니다. 전송하기 전에 올바른 토큰을 식별할 수 있도록 중복 토큰을 비활성화합니다.", "amount": "수량: ", "amount_is_below_minimum_limit": "수수료 차감 후 잔액이 교환에 필요한 최소 금액(${min})보다 적을 것입니다", "amount_is_estimate": "수신 금액은 예상치입니다", @@ -1473,5 +1474,6 @@ "zcash_card_enable_later": "설정에서 언제든지 나중에 이 카드를 활성화할 수 있습니다.", "zcash_card_missing_funds": "자금이 사라졌나요?", "zcash_card_scan": "스캔", - "zcash_card_warning": "절차가 완료될 때까지 앱을 닫지 마십시오. 닫으면 이 과정을 처음부터 다시 시작해야 합니다." + "zcash_card_warning": "절차가 완료될 때까지 앱을 닫지 마십시오. 닫으면 이 과정을 처음부터 다시 시작해야 합니다.", + "recipient_account_creation_fee": "아직 토큰 계정이 없기 때문에 수신자를 위한 토큰 계정을 생성하려면 추가 ${amount}가 필요합니다." } \ No newline at end of file diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index e8e9eac61..990a568e2 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -61,6 +61,7 @@ "already_have_account": "အကောင့်ရှိပြီးသားလား?", "already_your_username": "ဒါက သင့်အသုံးပြုသူအမည် ဖြစ်ပြီးသားပါ!", "always": "အမြဲတမ်း", + "ambiguous_token_symbol_exception": "ဤပိုက်ဆံအိတ်ရှိ တိုကင်တစ်ခုထက်ပိုသော သင်္ကေတ ${symbol} ကို အသုံးပြုသည်။ မပို့မီ မှန်ကန်သောတစ်ခုကို ဖော်ထုတ်နိုင်စေရန် ပွားနေသော တိုကင်ကို ပိတ်ပါ။", "amount": "ပမာဏ: ", "amount_is_below_minimum_limit": "အခကြေးငွေများနုတ်ပြီးနောက် သင့်လက်ကျန်ငွေသည် လဲလှယ်ရန်လိုအပ်သည့် အနိမ့်ဆုံးပမာဏ (${min}) ထက် နည်းသွားမည်", "amount_is_estimate": "လက်ခံရရှိမည့်ပမာဏသည် ခန့်မှန်းတန်ဖိုးဖြစ်သည်။", @@ -1473,5 +1474,6 @@ "zcash_card_enable_later": "သင်သည် ဤကတ်ကို ဆက်တင်များတွင် နောက်မှ အချိန်မရွေး ဖွင့်နိုင်ပါသည်။", "zcash_card_missing_funds": "ငွေကြေး ပျောက်ဆုံးနေပါသလား?", "zcash_card_scan": "စကင်", - "zcash_card_warning": "လုပ်ငန်းစဉ် ပြီးဆုံးသည်အထိ အက်ပ်ကို မပိတ်ပါနှင့်။ ပိတ်လိုက်ပါက ဤလုပ်ငန်းစဉ်ကို အစမှ ပြန်လည်စတင်ရမည်ဖြစ်သည်။" + "zcash_card_warning": "လုပ်ငန်းစဉ် ပြီးဆုံးသည်အထိ အက်ပ်ကို မပိတ်ပါနှင့်။ ပိတ်လိုက်ပါက ဤလုပ်ငန်းစဉ်ကို အစမှ ပြန်လည်စတင်ရမည်ဖြစ်သည်။", + "recipient_account_creation_fee": "လက်ခံသူအတွက် တိုကင်အကောင့်တစ်ခုဖန်တီးရန် ၎င်းတို့တွင် တစ်ခုမရှိသေးသောကြောင့် အပို ${amount} လိုအပ်ပါသည်။" } \ No newline at end of file diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index d9d677a95..0671d7e14 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -61,6 +61,7 @@ "already_have_account": "Heb je al een account?", "already_your_username": "Dit is al je gebruikersnaam!", "always": "Altijd", + "ambiguous_token_symbol_exception": "Meer dan één token in deze portemonnee gebruikt het symbool ${symbol}. Schakel het dubbele token uit, zodat het juiste token kan worden geïdentificeerd voordat het wordt verzonden.", "amount": "Bedrag: ", "amount_is_below_minimum_limit": "Je saldo na kosten zou lager zijn dan het minimale bedrag dat nodig is voor de exchange (${min})", "amount_is_estimate": "Het te ontvangen bedrag is een schatting", @@ -1474,5 +1475,6 @@ "zcash_card_enable_later": "Je kunt deze kaart later altijd inschakelen via de instellingen", "zcash_card_missing_funds": "Fondsen ontbreken?", "zcash_card_scan": "Scannen", - "zcash_card_warning": "Sluit de app niet voordat de procedure is voltooid. Als je dit wel doet, moet dit proces helemaal opnieuw worden gestart." + "zcash_card_warning": "Sluit de app niet voordat de procedure is voltooid. Als je dit wel doet, moet dit proces helemaal opnieuw worden gestart.", + "recipient_account_creation_fee": "Er is een extra ${amount} nodig om een ​​tokenaccount voor de ontvanger aan te maken, omdat deze er nog geen heeft." } \ No newline at end of file diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 8650bd0dc..e0de2a835 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -61,6 +61,7 @@ "already_have_account": "Masz już konto?", "already_your_username": "To już jest Twoja nazwa użytkownika!", "always": "Zawsze", + "ambiguous_token_symbol_exception": "Więcej niż jeden token w tym portfelu używa symbolu ${symbol}. Wyłącz duplikat tokena, aby przed wysłaniem można było zidentyfikować właściwy.", "amount": "Kwota: ", "amount_is_below_minimum_limit": "Twoje saldo po opłatach byłoby niższe niż minimalna kwota wymagana do wymiany (${min})", "amount_is_estimate": "Otrzymywana kwota jest szacunkowa", @@ -1473,5 +1474,6 @@ "zcash_card_enable_later": "Zawsze możesz włączyć tę kartę później w ustawieniach", "zcash_card_missing_funds": "Brakuje środków?", "zcash_card_scan": "Skanuj", - "zcash_card_warning": "Nie zamykaj aplikacji do czasu zakończenia procedury. Jeśli to zrobisz, proces będzie musiał rozpocząć się od nowa." + "zcash_card_warning": "Nie zamykaj aplikacji do czasu zakończenia procedury. Jeśli to zrobisz, proces będzie musiał rozpocząć się od nowa.", + "recipient_account_creation_fee": "Do założenia konta tokenowego dla odbiorcy potrzebna jest dodatkowa ${amount}, ponieważ jeszcze takiego nie posiada." } \ No newline at end of file diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 2a392da2b..d9204e345 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -61,6 +61,7 @@ "already_have_account": "Já tem uma conta?", "already_your_username": "Este já é o seu nome de utilizador!", "always": "Sempre", + "ambiguous_token_symbol_exception": "Mais de um token nesta carteira usa o símbolo ${symbol}. Desative o token duplicado para que o correto possa ser identificado antes do envio.", "amount": "Quantidade: ", "amount_is_below_minimum_limit": "Seu saldo após as taxas seria menor do que o valor mínimo necessário para a troca (${min})", "amount_is_estimate": "O valor a receber é uma estimativa", @@ -1476,5 +1477,6 @@ "zcash_card_enable_later": "Você sempre pode ativar este cartão mais tarde nas configurações", "zcash_card_missing_funds": "Fundos em falta?", "zcash_card_scan": "Escanear", - "zcash_card_warning": "Não feche o aplicativo até que o procedimento seja concluído; se você fizer isso, esse processo precisará ser reiniciado do zero." + "zcash_card_warning": "Não feche o aplicativo até que o procedimento seja concluído; se você fizer isso, esse processo precisará ser reiniciado do zero.", + "recipient_account_creation_fee": "É necessário um ${amount} extra para criar uma conta token para o destinatário, porque ele ainda não tem uma." } \ No newline at end of file diff --git a/res/values/strings_pt_BR.arb b/res/values/strings_pt_BR.arb index 51e816979..d703a3ec8 100644 --- a/res/values/strings_pt_BR.arb +++ b/res/values/strings_pt_BR.arb @@ -58,6 +58,7 @@ "already_have_account": "Já tem uma conta?", "already_your_username": "Este já é o seu nome de usuário!", "always": "Sempre", + "ambiguous_token_symbol_exception": "Mais de um token nesta carteira usa o símbolo ${symbol}. Desative o token duplicado para que o correto possa ser identificado antes do envio.", "amount": "Quantidade: ", "amount_is_below_minimum_limit": "Seu saldo após as taxas seria menor do que o valor mínimo necessário para a troca (${min})", "amount_is_estimate": "O valor a receber é uma estimativa", @@ -802,6 +803,7 @@ "received": "Recebido", "receiving": "Recebendo", "recieving": "Recebendo", + "recipient_account_creation_fee": "É necessário um ${amount} extra para criar uma conta token para o destinatário, porque ele ainda não tem uma.", "recipient_address": "Endereço do destinatário", "reconnect": "Reconectar", "reconnect_alert_text": "Tem certeza de que deseja reconectar?", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 1bdeee2a1..aeb56f31c 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -61,6 +61,7 @@ "already_have_account": "У вас уже есть аккаунт?", "already_your_username": "Это уже ваше имя пользователя!", "always": "Всегда", + "ambiguous_token_symbol_exception": "Более одного токена в этом кошельке используют символ ${symbol}. Отключите дубликат токена, чтобы можно было определить правильный перед отправкой.", "amount": "Сумма: ", "amount_is_below_minimum_limit": "Ваш баланс после комиссий будет меньше минимальной суммы, необходимой для обмена (${min})", "amount_is_estimate": "Сумма к получению является приблизительной", @@ -1474,5 +1475,6 @@ "zcash_card_enable_later": "Вы всегда можете включить эту карту позже в настройках", "zcash_card_missing_funds": "Не хватает средств?", "zcash_card_scan": "Сканировать", - "zcash_card_warning": "Не закрывайте приложение до завершения процедуры. Если вы это сделаете, процесс придется перезапустить с нуля." + "zcash_card_warning": "Не закрывайте приложение до завершения процедуры. Если вы это сделаете, процесс придется перезапустить с нуля.", + "recipient_account_creation_fee": "Для создания учетной записи токена для получателя требуется дополнительная сумма ${amount}, поскольку у него ее еще нет." } \ No newline at end of file diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index b8e98d11b..44096cbd1 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -61,6 +61,7 @@ "already_have_account": "มีบัญชีอยู่แล้วใช่ไหม?", "already_your_username": "นี่คือชื่อผู้ใช้ของคุณอยู่แล้ว!", "always": "เสมอ", + "ambiguous_token_symbol_exception": "โทเค็นมากกว่าหนึ่งรายการในกระเป๋าเงินนี้ใช้สัญลักษณ์ ${symbol} ปิดใช้งานโทเค็นที่ซ้ำกันเพื่อให้สามารถระบุโทเค็นที่ถูกต้องก่อนส่ง", "amount": "จำนวน: ", "amount_is_below_minimum_limit": "ยอดคงเหลือของคุณหลังหักค่าธรรมเนียมจะน้อยกว่าจำนวนขั้นต่ำที่จำเป็นสำหรับการแลกเปลี่ยน (${min})", "amount_is_estimate": "จำนวนเงินที่ได้รับเป็นการประมาณการ", @@ -1473,5 +1474,6 @@ "zcash_card_enable_later": "คุณสามารถเปิดใช้งานการ์ดนี้ได้เสมอในภายหลังในการตั้งค่า", "zcash_card_missing_funds": "ยอดเงินหายไป?", "zcash_card_scan": "สแกน", - "zcash_card_warning": "อย่าปิดแอปจนกว่าขั้นตอนจะเสร็จสิ้น หากคุณปิดแอป กระบวนการนี้จะต้องเริ่มใหม่ตั้งแต่ต้น" + "zcash_card_warning": "อย่าปิดแอปจนกว่าขั้นตอนจะเสร็จสิ้น หากคุณปิดแอป กระบวนการนี้จะต้องเริ่มใหม่ตั้งแต่ต้น", + "recipient_account_creation_fee": "จำเป็นต้องมี ${amount} เพิ่มเติมเพื่อสร้างบัญชีโทเค็นสำหรับผู้รับ เนื่องจากยังไม่มีบัญชี" } \ No newline at end of file diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 86a0d4519..bdb4d46a6 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -61,6 +61,7 @@ "already_have_account": "Mayroon ka nang account?", "already_your_username": "Ito na ang username mo!", "always": "Palagi", + "ambiguous_token_symbol_exception": "Higit sa isang token sa wallet na ito ang gumagamit ng simbolo na ${symbol}. I-disable ang duplicate na token para matukoy ang tama bago ipadala.", "amount": "Halaga: ", "amount_is_below_minimum_limit": "Ang iyong balanse pagkatapos ng mga bayarin ay magiging mas mababa kaysa sa minimum na halagang kailangan para sa palitan (${min})", "amount_is_estimate": "Ang halagang matatanggap ay isang pagtatantya", @@ -1473,5 +1474,6 @@ "zcash_card_enable_later": "Maaari mo itong paganahin anumang oras sa ibang pagkakataon sa mga setting", "zcash_card_missing_funds": "Nawawalang pondo?", "zcash_card_scan": "I-scan", - "zcash_card_warning": "Huwag isara ang app hanggang sa makumpleto ang proseso; kung gagawin mo ito, kakailanganing magsimula muli mula sa simula." + "zcash_card_warning": "Huwag isara ang app hanggang sa makumpleto ang proseso; kung gagawin mo ito, kakailanganing magsimula muli mula sa simula.", + "recipient_account_creation_fee": "Kailangan ng dagdag na ${amount} para gumawa ng token account para sa tatanggap, dahil wala pa sila nito." } \ No newline at end of file diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index a2f6a18b6..67684f6df 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -61,6 +61,7 @@ "already_have_account": "Zaten bir hesabınız var mı?", "already_your_username": "Bu zaten kullanıcı adınız!", "always": "Her zaman", + "ambiguous_token_symbol_exception": "Bu cüzdanda birden fazla token ${symbol} sembolünü kullanıyor. Göndermeden önce doğru jetonun belirlenebilmesi için yinelenen jetonu devre dışı bırakın.", "amount": "Miktar: ", "amount_is_below_minimum_limit": "Ücretlerden sonra bakiyeniz, takas için gereken minimum miktardan daha az olur (${min})", "amount_is_estimate": "Alınacak tutar tahminidir", @@ -1473,5 +1474,6 @@ "zcash_card_enable_later": "Bu kartı daha sonra ayarlardan her zaman etkinleştirebilirsiniz", "zcash_card_missing_funds": "Bakiye eksik mi?", "zcash_card_scan": "Tara", - "zcash_card_warning": "İşlem tamamlanana kadar uygulamayı kapatmayın; aksi takdirde bu işlemin baştan yeniden başlatılması gerekecektir." + "zcash_card_warning": "İşlem tamamlanana kadar uygulamayı kapatmayın; aksi takdirde bu işlemin baştan yeniden başlatılması gerekecektir.", + "recipient_account_creation_fee": "Alıcının henüz bir token hesabı olmadığı için, alıcı için bir token hesabı oluşturmak için fazladan ${amount} gerekiyor." } \ No newline at end of file diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 93ae72c1e..14fa38f59 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -61,6 +61,7 @@ "already_have_account": "Вже маєте обліковий запис?", "already_your_username": "Це вже ваше ім’я користувача!", "always": "Завжди", + "ambiguous_token_symbol_exception": "Більш ніж один токен у цьому гаманці використовує символ ${symbol}. Вимкніть повторюваний маркер, щоб можна було визначити правильний перед надсиланням.", "amount": "Сума: ", "amount_is_below_minimum_limit": "Ваш баланс після комісій буде меншим за мінімальну суму, необхідну для обміну (${min})", "amount_is_estimate": "Сума отримання є приблизною", @@ -1474,5 +1475,6 @@ "zcash_card_enable_later": "Ви завжди можете ввімкнути цю картку пізніше в налаштуваннях", "zcash_card_missing_funds": "Зникли кошти?", "zcash_card_scan": "Сканувати", - "zcash_card_warning": "Не закривайте застосунок, доки процедура не завершиться. Якщо ви це зробите, цей процес потрібно буде перезапустити з нуля." + "zcash_card_warning": "Не закривайте застосунок, доки процедура не завершиться. Якщо ви це зробите, цей процес потрібно буде перезапустити з нуля.", + "recipient_account_creation_fee": "Потрібна додаткова сума в розмірі ${amount}, щоб створити обліковий запис-токен для одержувача, оскільки він його ще не має." } \ No newline at end of file diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 80e93d207..ba77da2bf 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -61,6 +61,7 @@ "already_have_account": "کیا آپ کے پاس پہلے سے اکاؤنٹ ہے؟", "already_your_username": "یہ پہلے ہی آپ کا صارف نام ہے!", "always": "ہمیشہ", + "ambiguous_token_symbol_exception": "اس بٹوے میں ایک سے زیادہ ٹوکن ${symbol} کی علامت استعمال کرتا ہے۔ ڈپلیکیٹ ٹوکن کو غیر فعال کریں تاکہ بھیجنے سے پہلے صحیح کی شناخت ہو سکے۔", "amount": "رقم: ", "amount_is_below_minimum_limit": "فیس کے بعد آپ کا بیلنس ایکسچینج کے لیے درکار کم از کم رقم (${min}) سے کم ہوگا", "amount_is_estimate": "وصول ہونے والی رقم ایک تخمینہ ہے۔", @@ -1475,5 +1476,6 @@ "zcash_card_enable_later": "آپ اس کارڈ کو بعد میں بھی ترتیبات میں فعال کر سکتے ہیں", "zcash_card_missing_funds": "فنڈز غائب ہیں؟", "zcash_card_scan": "اسکین کریں", - "zcash_card_warning": "طریقہ کار مکمل ہونے تک ایپ بند نہ کریں۔ اگر آپ ایسا کریں گے تو یہ عمل شروع سے دوبارہ شروع کرنا پڑے گا۔" + "zcash_card_warning": "طریقہ کار مکمل ہونے تک ایپ بند نہ کریں۔ اگر آپ ایسا کریں گے تو یہ عمل شروع سے دوبارہ شروع کرنا پڑے گا۔", + "recipient_account_creation_fee": "وصول کنندہ کے لیے ٹوکن اکاؤنٹ بنانے کے لیے ایک اضافی ${amount} کی ضرورت ہے، کیونکہ ان کے پاس ابھی تک کوئی اکاؤنٹ نہیں ہے۔" } \ No newline at end of file diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 68fd9afd1..41b269d47 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -61,6 +61,7 @@ "already_have_account": "Bạn đã có tài khoản chưa?", "already_your_username": "Đây đã là tên người dùng của bạn!", "always": "Luôn", + "ambiguous_token_symbol_exception": "Nhiều mã thông báo trong ví này sử dụng ký hiệu ${symbol}. Vô hiệu hóa mã thông báo trùng lặp để có thể xác định mã thông báo chính xác trước khi gửi.", "amount": "Số lượng: ", "amount_is_below_minimum_limit": "Số dư của bạn sau khi trừ phí sẽ thấp hơn số tiền tối thiểu cần thiết để trao đổi (${min})", "amount_is_estimate": "Số tiền nhận được chỉ là ước tính", @@ -1474,5 +1475,6 @@ "zcash_card_enable_later": "Bạn luôn có thể bật thẻ này sau trong phần cài đặt", "zcash_card_missing_funds": "Thiếu tiền?", "zcash_card_scan": "Quét", - "zcash_card_warning": "Không đóng ứng dụng cho đến khi quy trình hoàn tất; nếu bạn làm vậy, quy trình này sẽ phải bắt đầu lại từ đầu." + "zcash_card_warning": "Không đóng ứng dụng cho đến khi quy trình hoàn tất; nếu bạn làm vậy, quy trình này sẽ phải bắt đầu lại từ đầu.", + "recipient_account_creation_fee": "Cần thêm ${amount} để tạo tài khoản mã thông báo cho người nhận vì họ chưa có tài khoản." } \ No newline at end of file diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 6ca70f8e1..15f858b93 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -61,6 +61,7 @@ "already_have_account": "Ṣé o ti ní àkántì tẹ́lẹ̀?", "already_your_username": "Eyi ti jẹ orúkọ olumulo rẹ tẹlẹ!", "always": "Nígbà gbogbo", + "ambiguous_token_symbol_exception": "Die e sii ju ami-ami kan ninu apamọwọ yii nlo aami ${symbol}. Pa àmi àdáwòkọ rẹ́ kí ó lè mọ èyí tí ó tọ́ kí ó tó fi ránṣẹ́.", "amount": "Iye: ", "amount_is_below_minimum_limit": "Iwontunwonsi rẹ lẹ́yìn àwọn owó ìdúná yóò kéré ju iye kéréjù tí a nílò fún paṣipaarọ (${min})", "amount_is_estimate": "Iye tí a máa gba jẹ́ àfojúsùn", @@ -1474,5 +1475,6 @@ "zcash_card_enable_later": "O le ma a mu kaadi yii ṣiṣẹ nigbamii ninu awọn eto", "zcash_card_missing_funds": "Ṣe owó rẹ sọnù?", "zcash_card_scan": "Ṣàwárí", - "zcash_card_warning": "Maṣe pa ohun elo naa titi ilana naa yoo fi pari; bí o bá ṣe bẹ́ẹ̀, ilana yìí yóò ní láti tún bẹ̀rẹ̀ láti ìbẹ̀rẹ̀." + "zcash_card_warning": "Maṣe pa ohun elo naa titi ilana naa yoo fi pari; bí o bá ṣe bẹ́ẹ̀, ilana yìí yóò ní láti tún bẹ̀rẹ̀ láti ìbẹ̀rẹ̀.", + "recipient_account_creation_fee": "A nilo afikun ${amount} lati ṣẹda akọọlẹ ami-ami kan fun olugba, nitori wọn ko ni ọkan sibẹsibẹ." } \ No newline at end of file diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 8e3256b97..711735023 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -61,6 +61,7 @@ "already_have_account": "已经有账户了?", "already_your_username": "这已经是您的用户名!", "always": "始终", + "ambiguous_token_symbol_exception": "该钱包中有多个代币使用符号 ${symbol}。禁用重复的令牌,以便在发送之前可以识别正确的令牌。", "amount": "数量:", "amount_is_below_minimum_limit": "扣除手续费后,您的余额将低于兑换所需的最低金额(${min})", "amount_is_estimate": "接收金额为预估值", @@ -1474,5 +1475,6 @@ "zcash_card_enable_later": "您随时可以在设置中启用此卡", "zcash_card_missing_funds": "资金不见了?", "zcash_card_scan": "扫描", - "zcash_card_warning": "在该过程完成之前请勿关闭应用程序,否则该过程将需要从头重新开始。" + "zcash_card_warning": "在该过程完成之前请勿关闭应用程序,否则该过程将需要从头重新开始。", + "recipient_account_creation_fee": "需要额外的 ${amount} 来为收件人创建一个令牌帐户,因为他们还没有令牌帐户。" } \ No newline at end of file diff --git a/tool/configure.dart b/tool/configure.dart index 93cb2d598..b0a884969 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -922,6 +922,7 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/spl_token.dart'; import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/utils/print_verbose.dart'; """; const solanaCWHeaders = """ From 9b50011e75f7861ccb21f818a0ba4c5f5ab34590 Mon Sep 17 00:00:00 2001 From: malik1004x Date: Mon, 24 Aug 2026 22:24:52 +0200 Subject: [PATCH 10/15] Hide broken options in Bitcoin wallets without a private key (#3532) * hide unavailable receive page options * hide broken options in wallets without a private key * hide exporting unavailable logs * hide sign/verify on airgapped wallets * hide mweb on hwws * hide mweb ad for hww --- cw_bitcoin/lib/bitcoin_wallet.dart | 21 +++++++++++++++++++ cw_bitcoin/lib/electrum_wallet.dart | 1 + cw_core/lib/wallet_base.dart | 11 ++++++++++ .../screens/settings/other_settings_page.dart | 3 +++ lib/src/screens/settings/privacy_page.dart | 2 +- .../dashboard/dashboard_view_model.dart | 2 +- .../dashboard/receive_option_view_model.dart | 3 ++- .../settings/other_settings_view_model.dart | 6 ++++++ .../settings/privacy_settings_view_model.dart | 12 +++++++---- 9 files changed, 54 insertions(+), 7 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 4f791aeb4..ac40de051 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -7,6 +7,7 @@ import 'package:cw_bitcoin/.secrets.g.dart' as secrets; import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; +import "package:cw_bitcoin/bitcoin_receive_page_option.dart"; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; @@ -33,6 +34,7 @@ import 'package:cw_core/output_info.dart'; import 'package:cw_core/payjoin_session.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/sync_status.dart'; +import "package:cw_core/receive_page_option.dart"; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -417,6 +419,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { late final PayjoinManager payjoinManager; + @override + bool get hasPayjoinSupport => keys.privateKey.isNotEmpty; + + @override + bool get hasLightningSupport => lightningWallet?.sdk != null; + bool get isPayjoinAvailable => unspentCoinsInfo.values .where((element) => element.walletId == id && element.isSending && !element.isFrozen) .isNotEmpty; @@ -673,4 +681,17 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { return super.signMessage(message, address: address); } + + @override + bool receiveOptionAvailable(ReceivePageOption option) { + if(option == BitcoinReceivePageOption.lightning) { + return hasLightningSupport; + } + + if(option == BitcoinReceivePageOption.silent_payments) { + return hasSilentPaymentsScanning; + } + + return true; + } } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index c82ac8ebd..1da016931 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -407,6 +407,7 @@ abstract class ElectrumWalletBase @override bool isTestnet; + @override bool get hasSilentPaymentsScanning => type == WalletType.bitcoin && keys.privateKey.isNotEmpty; @observable diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 89d6356da..024095b80 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -1,3 +1,4 @@ +import "package:cw_core/receive_page_option.dart"; import 'package:mobx/mobx.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/transaction_info.dart'; @@ -136,4 +137,14 @@ abstract class WalletBase checkNodeHealth(); + + bool get hasPayjoinSupport => false; + bool get hasLightningSupport => false; + bool get hasSilentPaymentsScanning => false; + + // hardware wallet - bitbox, ledger, trezor. + // we also have airgap wallets but those can't sign messages + bool get canSignMessages => walletInfo.hardwareWalletType == null || walletInfo.isHardwareWallet; + + bool receiveOptionAvailable(ReceivePageOption option) => true; } diff --git a/lib/src/screens/settings/other_settings_page.dart b/lib/src/screens/settings/other_settings_page.dart index 3f5291984..512f71984 100644 --- a/lib/src/screens/settings/other_settings_page.dart +++ b/lib/src/screens/settings/other_settings_page.dart @@ -143,6 +143,7 @@ class OtherSettingsPage extends BasePage { }, }), ), + if(_otherSettingsViewModel.hasSignVerify) ListItemRegularRow( keyValue: "security_backup_page_sign_and_verify", label: S.of(context).sign_verify_title, @@ -152,10 +153,12 @@ class OtherSettingsPage extends BasePage { ], if (_otherSettingsViewModel.walletType == WalletType.bitcoin) "btc_logging": [ + if(_otherSettingsViewModel.hasLightning) ListItemRegularRow( keyValue: "export_lightning_logs", label: S.of(context).export_lightning_logs, onTap: () => onExportLNLog(context)), + if(_otherSettingsViewModel.hasPayjoin) ListItemRegularRow( keyValue: "export_payjoin_logs", label: S.of(context).export_payjoin_logs, diff --git a/lib/src/screens/settings/privacy_page.dart b/lib/src/screens/settings/privacy_page.dart index 3e35f08c3..241202ff8 100644 --- a/lib/src/screens/settings/privacy_page.dart +++ b/lib/src/screens/settings/privacy_page.dart @@ -76,7 +76,7 @@ class PrivacyPage extends BasePage { }), ], "": [ - if (_privacySettingsViewModel.isBitcoin) + if (_privacySettingsViewModel.hasSilentPaymentsScanning) ListItemRegularRow( iconPath: "assets/new-ui/settings_row_icons/silent-payments.svg", keyValue: "silent_payments", diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index fb2d53b74..e74771b1b 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -635,7 +635,7 @@ abstract class DashboardViewModelBase with Store { if (settingsStore.mwebAdDismissed) return false; - return Platform.isAndroid || Platform.isIOS; + return (Platform.isAndroid || Platform.isIOS) && !wallet.isHardwareWallet; } @action diff --git a/lib/view_model/dashboard/receive_option_view_model.dart b/lib/view_model/dashboard/receive_option_view_model.dart index 0f3afeff9..f82fd9e4c 100644 --- a/lib/view_model/dashboard/receive_option_view_model.dart +++ b/lib/view_model/dashboard/receive_option_view_model.dart @@ -35,7 +35,8 @@ abstract class ReceiveOptionViewModelBase with Store { ReceivePageOption selectedReceiveOption; @computed - List get options => _wallet.walletAddresses.receivePageOptions; + List get options => + _wallet.walletAddresses.receivePageOptions.where(_wallet.receiveOptionAvailable).toList(); String get walletTypeString => walletTypeToString(_wallet.type); diff --git a/lib/view_model/settings/other_settings_view_model.dart b/lib/view_model/settings/other_settings_view_model.dart index 36ede0802..fa3a274ae 100644 --- a/lib/view_model/settings/other_settings_view_model.dart +++ b/lib/view_model/settings/other_settings_view_model.dart @@ -47,6 +47,8 @@ abstract class OtherSettingsViewModelBase with Store { final SettingsStore _settingsStore; final SendViewModel sendViewModel; + bool get hasSignVerify => _wallet.canSignMessages; + @computed TransactionPriority get transactionPriority { final priority = _settingsStore.getPriority(walletType, chainId: chainId); @@ -173,6 +175,10 @@ abstract class OtherSettingsViewModelBase with Store { return null; } + bool get hasPayjoin => _wallet.hasPayjoinSupport; + + bool get hasLightning => _wallet.hasLightningSupport; + Future getLightningLog() async { final path = await pathForWalletDir(name: _wallet.name, type: walletType); final logFile = File("$path/lightning.log"); diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index 5c1a41225..6eeabc0a6 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -25,7 +25,12 @@ abstract class PrivacySettingsViewModelBase with Store { bool get isBitcoin => _wallet.type == WalletType.bitcoin; @computed - bool get hasMWEB => _wallet.type == WalletType.litecoin && (Platform.isIOS || Platform.isAndroid); + bool get hasSilentPaymentsScanning => _wallet.hasSilentPaymentsScanning; + + @computed + bool get hasMWEB => + _wallet.type == WalletType.litecoin && (Platform.isIOS || Platform.isAndroid) && + !_wallet.isHardwareWallet; @computed bool get isAutoGenerateSubaddressesEnabled => @@ -108,11 +113,10 @@ abstract class PrivacySettingsViewModelBase with Store { bool get usePayjoin => _settingsStore.usePayjoin; @computed - bool get canUsePayjoin => _wallet.type == WalletType.bitcoin && DeviceInfo.instance.isMobile; + bool get canUsePayjoin => _wallet.hasPayjoinSupport && DeviceInfo.instance.isMobile; @computed - bool get canUseLightning => - _wallet.type == WalletType.bitcoin && !Platform.isWindows && !Platform.isLinux; + bool get canUseLightning => _wallet.hasLightningSupport; @computed bool get useLightning => _wallet.type == WalletType.bitcoin && bitcoin!.useLightning(_wallet); From b72c5c32327141807ecfe0c3007678d3cf1bf8e4 Mon Sep 17 00:00:00 2001 From: malik1004x Date: Mon, 24 Aug 2026 22:35:48 +0200 Subject: [PATCH 11/15] fix icons in card customizer in light theme (#3552) --- lib/new-ui/pages/card_customizer.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/new-ui/pages/card_customizer.dart b/lib/new-ui/pages/card_customizer.dart index 9973ec265..df5026fb4 100644 --- a/lib/new-ui/pages/card_customizer.dart +++ b/lib/new-ui/pages/card_customizer.dart @@ -238,8 +238,14 @@ class _CardCustomizerState extends State { height: 48, decoration: BoxDecoration( borderRadius: BorderRadius.circular(18), - color: Theme.of(context) - .colorScheme + color: Theme.of(context).brightness == + Brightness.light + ? Theme.of(context) + .colorScheme + .onSurfaceVariant + .withAlpha(64) + : Theme.of(context) + .colorScheme .surfaceContainerHigh, border: Border.all( color: isSelected From 3d9b79f536ad88c8bfe1b840c93a5dc130011388 Mon Sep 17 00:00:00 2001 From: cyan Date: Tue, 25 Aug 2026 20:01:52 +0300 Subject: [PATCH 12/15] feat: bip39 decred (#3348) --- cw_decred/lib/api/libdcrwallet.dart | 2 +- cw_decred/lib/mnemonic_validation.dart | 24 +++++ cw_decred/lib/wallet.dart | 37 ++++++- .../lib/wallet_creation_credentials.dart | 14 ++- cw_decred/lib/wallet_service.dart | 100 ++++++++++++++++-- cw_decred/pubspec.yaml | 1 + lib/core/wallet_creation_service.dart | 2 +- lib/decred/cw_decred.dart | 25 +++-- lib/di.dart | 3 +- lib/reactions/wallet_utils.dart | 2 +- .../screens/restore/wallet_restore_page.dart | 2 +- .../advanced_privacy_settings_view_model.dart | 5 +- lib/view_model/wallet_creation_vm.dart | 2 + .../wallet_groups_display_view_model.dart | 7 +- lib/view_model/wallet_new_vm.dart | 6 +- lib/view_model/wallet_restore_view_model.dart | 1 + pubspec.lock | 8 +- scripts/android/build_decred.sh | 2 +- scripts/ios/build_decred.sh | 2 +- tool/configure.dart | 6 +- 20 files changed, 208 insertions(+), 43 deletions(-) create mode 100644 cw_decred/lib/mnemonic_validation.dart diff --git a/cw_decred/lib/api/libdcrwallet.dart b/cw_decred/lib/api/libdcrwallet.dart index a6d1e1017..f1de723e6 100644 --- a/cw_decred/lib/api/libdcrwallet.dart +++ b/cw_decred/lib/api/libdcrwallet.dart @@ -182,7 +182,7 @@ class Libwallet { final cName = name.toCString(); final cSignReq = signReq.toCString(); res = executePayloadFn( - fn: () => dcrwalletApi.createSignedTransaction(cName, cSignReq), + fn: () => dcrwalletApi.createTransaction(cName, cSignReq), ptrsToFree: [cName, cSignReq], ); break; diff --git a/cw_decred/lib/mnemonic_validation.dart b/cw_decred/lib/mnemonic_validation.dart new file mode 100644 index 000000000..96cb26f36 --- /dev/null +++ b/cw_decred/lib/mnemonic_validation.dart @@ -0,0 +1,24 @@ +import "package:bip39/bip39.dart" as bip39; + +class DecredMnemonicIsIncorrectException implements Exception { + @override + String toString() => + "Decred mnemonic has incorrect format. Mnemonic should be a valid BIP39 seed (12 or 24 words) or a native Decred seed (15 words)."; +} + +bool isValidDecredMnemonic(String mnemonic, {bool allowNativeSeed = true}) { + if (bip39.validateMnemonic(mnemonic)) { + return true; + } + if (!allowNativeSeed) { + return false; + } + final wordCount = mnemonic.trim().split(RegExp(r"\s+")).where((word) => word.isNotEmpty).length; + return wordCount == 15; +} + +void validateDecredMnemonic(String mnemonic, {bool allowNativeSeed = true}) { + if (!isValidDecredMnemonic(mnemonic, allowNativeSeed: allowNativeSeed)) { + throw DecredMnemonicIsIncorrectException(); + } +} diff --git a/cw_decred/lib/wallet.dart b/cw_decred/lib/wallet.dart index 831390ba3..9bb76643e 100644 --- a/cw_decred/lib/wallet.dart +++ b/cw_decred/lib/wallet.dart @@ -22,8 +22,10 @@ import 'package:cw_decred/wallet_service.dart'; import 'package:cw_decred/balance.dart'; import 'package:cw_decred/transaction_info.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/sync_status.dart'; @@ -36,9 +38,11 @@ part 'wallet.g.dart'; class DecredWallet = DecredWalletBase with _$DecredWallet; abstract class DecredWalletBase - extends WalletBase with Store { + extends WalletBase + with Store, WalletKeysFile { DecredWalletBase(WalletInfo walletInfo, DerivationInfo derivationInfo, String password, - Box unspentCoinsInfo, Libwallet libwallet, Function() closeLibwallet) + Box unspentCoinsInfo, Libwallet libwallet, Function() closeLibwallet, + {this.passphrase, required this.encryptionFileUtils}) : _password = password, _libwallet = libwallet, _closeLibwallet = closeLibwallet, @@ -113,6 +117,17 @@ abstract class DecredWalletBase return _seed; } + @override + final String? passphrase; + + final EncryptionFileUtils encryptionFileUtils; + + @override + WalletKeysData get walletKeysData => WalletKeysData( + mnemonic: seed, + passphrase: passphrase, + ); + @override Object get keys => {}; @@ -402,11 +417,11 @@ abstract class DecredWalletBase "feerate": creds.feeRate ?? defaultFeeRate, "password": _password, "sendall": sendAll, + "sign": true, }; final res = await _libwallet.createSignedTransaction(walletInfo.name, jsonEncode(signReq)); final decoded = json.decode(res); - final signedHex = decoded["signedhex"]; - + final signedHex = decoded["hex"]; final send = () async { await _libwallet.sendRawTransaction(walletInfo.name, signedHex); await updateBalance(); @@ -518,7 +533,12 @@ abstract class DecredWalletBase String uniqueTxID(String id, int vout) => "$id:$vout"; @override - Future save() async {} + Future save() async { + if (watchingOnly) { + return; + } + await saveKeysFile(_password, encryptionFileUtils); + } @override bool get hasRescan => walletBirthdayBlockHeight() != -1; @@ -620,6 +640,13 @@ abstract class DecredWalletBase } await sourceDir.delete(recursive: true); + + for (final suffix in const ['.keys', '.keys.backup']) { + final file = File(p.join(newDirPath, '${walletInfo.name}$suffix')); + if (file.existsSync()) { + await file.rename(p.join(newDirPath, '$newWalletName$suffix')); + } + } } @override diff --git a/cw_decred/lib/wallet_creation_credentials.dart b/cw_decred/lib/wallet_creation_credentials.dart index ca0451447..80557bc25 100644 --- a/cw_decred/lib/wallet_creation_credentials.dart +++ b/cw_decred/lib/wallet_creation_credentials.dart @@ -3,8 +3,15 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/hardware/hardware_account_data.dart'; class DecredNewWalletCredentials extends WalletCredentials { - DecredNewWalletCredentials({required String name, WalletInfo? walletInfo}) - : super(name: name, walletInfo: walletInfo); + DecredNewWalletCredentials( + {required String name, + String? password, + String? passphrase, + this.mnemonic, + WalletInfo? walletInfo}) + : super(name: name, password: password, passphrase: passphrase, walletInfo: walletInfo); + + final String? mnemonic; } class DecredRestoreWalletFromSeedCredentials extends WalletCredentials { @@ -12,8 +19,9 @@ class DecredRestoreWalletFromSeedCredentials extends WalletCredentials { {required String name, required String password, required this.mnemonic, + String? passphrase, WalletInfo? walletInfo}) - : super(name: name, password: password, walletInfo: walletInfo); + : super(name: name, password: password, passphrase: passphrase, walletInfo: walletInfo); final String mnemonic; } diff --git a/cw_decred/lib/wallet_service.dart b/cw_decred/lib/wallet_service.dart index 46f980708..a0385b00e 100644 --- a/cw_decred/lib/wallet_service.dart +++ b/cw_decred/lib/wallet_service.dart @@ -1,6 +1,10 @@ import 'dart:convert'; import 'dart:io'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:cw_core/encryption_file_utils.dart'; +import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_decred/api/libdcrwallet.dart'; +import 'package:cw_decred/mnemonic_validation.dart'; import 'package:cw_decred/wallet_creation_credentials.dart'; import 'package:cw_decred/wallet.dart'; import 'package:cw_core/wallet_base.dart'; @@ -17,9 +21,10 @@ class DecredWalletService extends WalletService< DecredRestoreWalletFromSeedCredentials, DecredRestoreWalletFromPubkeyCredentials, DecredRestoreWalletFromHardwareCredentials> { - DecredWalletService(this.unspentCoinsInfoSource); + DecredWalletService(this.unspentCoinsInfoSource, this.isDirect); final Box unspentCoinsInfoSource; + final bool isDirect; final seedRestorePath = "m/44'/42'"; static final seedRestorePathTestnet = "m/44'/1'"; static final pubkeyRestorePath = "m/44'/42'/0'"; @@ -55,6 +60,11 @@ class DecredWalletService extends WalletService< @override Future create(DecredNewWalletCredentials credentials, {bool? isTestnet}) async { + final strength = credentials.seedPhraseLength == 24 ? 256 : 128; + final mnemonic = (credentials.mnemonic?.isNotEmpty == true) + ? credentials.mnemonic! + : bip39.generateMnemonic(strength: strength); + validateDecredMnemonic(mnemonic, allowNativeSeed: false); await this.init(); final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType()); final network = isTestnet == true ? testnet : mainnet; @@ -62,12 +72,16 @@ class DecredWalletService extends WalletService< "name": credentials.walletInfo!.name, "datadir": dirPath, "pass": credentials.password!, + "mnemonic": mnemonic, + "seedpass": credentials.passphrase ?? "", + "birthday": DateTime.now().millisecondsSinceEpoch ~/ 1000, "net": network, "unsyncedaddrs": true, }; await libwallet!.createWallet(jsonEncode(config)); final di = await credentials.walletInfo!.getDerivationInfo(); di.derivationPath = isTestnet == true ? seedRestorePathTestnet : seedRestorePath; + di.derivationType = DerivationType.bip39; await di.save(); credentials.walletInfo!.save(); credentials.walletInfo!.network = network; @@ -78,9 +92,14 @@ class DecredWalletService extends WalletService< // going forward. credentials.walletInfo!.dirPath = ""; credentials.walletInfo!.path = ""; - final wallet = DecredWallet(credentials.walletInfo!, di, credentials.password!, - this.unspentCoinsInfoSource, libwallet!, closeLibwallet); + final wallet = _createWalletInstance( + credentials.walletInfo!, + di, + credentials.password!, + passphrase: credentials.passphrase, + ); await wallet.init(); + await wallet.save(); return wallet; } @@ -151,9 +170,10 @@ class DecredWalletService extends WalletService< "unsyncedaddrs": true, }; await libwallet!.loadWallet(jsonEncode(config)); - final wallet = DecredWallet( - walletInfo, di, password, this.unspentCoinsInfoSource, libwallet!, closeLibwallet); + final passphrase = await _loadPassphrase(name, password); + final wallet = _createWalletInstance(walletInfo, di, password, passphrase: passphrase); await wallet.init(); + await _persistSeedDerivationType(wallet); return wallet; } @@ -184,8 +204,7 @@ class DecredWalletService extends WalletService< libwallet = await Libwallet.spawn(); libwallet!.initLibdcrwallet("", "err"); } - final currentWallet = DecredWallet( - currentWalletInfo, di, password, this.unspentCoinsInfoSource, libwallet!, closeLibwallet); + final currentWallet = _createWalletInstance(currentWalletInfo, di, password); await currentWallet.renameWalletFiles(newName); @@ -201,6 +220,7 @@ class DecredWalletService extends WalletService< @override Future restoreFromSeed(DecredRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { + validateDecredMnemonic(credentials.mnemonic); await this.init(); final network = isTestnet == true ? testnet : mainnet; final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType()); @@ -209,22 +229,81 @@ class DecredWalletService extends WalletService< "datadir": dirPath, "pass": credentials.password!, "mnemonic": credentials.mnemonic, + "seedpass": credentials.passphrase ?? "", "net": network, "unsyncedaddrs": true, }; await libwallet!.createWallet(jsonEncode(config)); final di = await credentials.walletInfo!.getDerivationInfo(); di.derivationPath = isTestnet == true ? seedRestorePathTestnet : seedRestorePath; + di.derivationType = + bip39.validateMnemonic(credentials.mnemonic) ? DerivationType.bip39 : DerivationType.def; await di.save(); credentials.walletInfo!.network = network; credentials.walletInfo!.dirPath = ""; credentials.walletInfo!.path = ""; - final wallet = DecredWallet(credentials.walletInfo!, di, credentials.password!, - this.unspentCoinsInfoSource, libwallet!, closeLibwallet); + final wallet = _createWalletInstance( + credentials.walletInfo!, + di, + credentials.password!, + passphrase: credentials.passphrase, + ); await wallet.init(); + await wallet.save(); return wallet; } + EncryptionFileUtils get _encryptionFileUtils => encryptionFileUtilsFor(isDirect); + + DecredWallet _createWalletInstance( + WalletInfo walletInfo, + DerivationInfo di, + String password, { + String? passphrase, + }) { + return DecredWallet( + walletInfo, + di, + password, + unspentCoinsInfoSource, + libwallet!, + closeLibwallet, + passphrase: passphrase, + encryptionFileUtils: _encryptionFileUtils, + ); + } + + Future _loadPassphrase(String name, String password) async { + if (!await WalletKeysFile.hasKeysFile(name, getType())) { + return null; + } + try { + final keys = await WalletKeysFile.readKeysFile( + name, + getType(), + password, + _encryptionFileUtils, + ); + return keys.passphrase; + } catch (_) { + return null; + } + } + + Future _persistSeedDerivationType(DecredWallet wallet) async { + final seed = wallet.seed; + if (seed == null || seed.isEmpty) { + return; + } + final expected = + bip39.validateMnemonic(seed) ? DerivationType.bip39 : DerivationType.def; + if (wallet.derivationInfo.derivationType == expected) { + return; + } + wallet.derivationInfo.derivationType = expected; + await wallet.derivationInfo.save(); + } + // restoreFromKeys only supports restoring a watch only wallet from an account // pubkey. @override @@ -247,8 +326,7 @@ class DecredWalletService extends WalletService< credentials.walletInfo!.network = network; credentials.walletInfo!.dirPath = ""; credentials.walletInfo!.path = ""; - final wallet = DecredWallet(credentials.walletInfo!, di, credentials.password!, - this.unspentCoinsInfoSource, libwallet!, closeLibwallet); + final wallet = _createWalletInstance(credentials.walletInfo!, di, credentials.password!); await wallet.init(); return wallet; } diff --git a/cw_decred/pubspec.yaml b/cw_decred/pubspec.yaml index 1b808e293..7a41bc097 100644 --- a/cw_decred/pubspec.yaml +++ b/cw_decred/pubspec.yaml @@ -12,6 +12,7 @@ environment: dependencies: flutter: sdk: flutter + bip39: ^1.0.6 cw_core: path: ../cw_core diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index c40b810e4..d627c1a31 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -86,6 +86,7 @@ class WalletCreationService { case WalletType.dogecoin: case WalletType.nano: case WalletType.zcash: + case WalletType.decred: return true; case WalletType.monero: case WalletType.wownero: @@ -93,7 +94,6 @@ class WalletCreationService { case WalletType.haven: case WalletType.banano: case WalletType.zano: - case WalletType.decred: return false; } } diff --git a/lib/decred/cw_decred.dart b/lib/decred/cw_decred.dart index 18b955af8..958a544f0 100644 --- a/lib/decred/cw_decred.dart +++ b/lib/decred/cw_decred.dart @@ -5,13 +5,26 @@ class CWDecred extends Decred { @override WalletCredentials createDecredNewWalletCredentials( - {required String name, WalletInfo? walletInfo}) => - DecredNewWalletCredentials(name: name, walletInfo: walletInfo); + {required String name, + String? password, + String? passphrase, + String? mnemonic, + WalletInfo? walletInfo}) => + DecredNewWalletCredentials( + name: name, + password: password, + passphrase: passphrase, + mnemonic: mnemonic, + walletInfo: walletInfo); @override WalletCredentials createDecredRestoreWalletFromSeedCredentials( - {required String name, required String mnemonic, required String password}) => - DecredRestoreWalletFromSeedCredentials(name: name, mnemonic: mnemonic, password: password); + {required String name, + required String mnemonic, + required String password, + String? passphrase}) => + DecredRestoreWalletFromSeedCredentials( + name: name, mnemonic: mnemonic, password: password, passphrase: passphrase); @override WalletCredentials createDecredRestoreWalletFromPubkeyCredentials( @@ -19,8 +32,8 @@ class CWDecred extends Decred { DecredRestoreWalletFromPubkeyCredentials(name: name, pubkey: pubkey, password: password); @override - WalletService createDecredWalletService(Box unspentCoinSource) => - DecredWalletService(unspentCoinSource); + WalletService createDecredWalletService(Box unspentCoinSource, bool isDirect) => + DecredWalletService(unspentCoinSource, isDirect); @override List getTransactionPriorities() => DecredTransactionPriority.all; diff --git a/lib/di.dart b/lib/di.dart index cd27c53c3..147f1900c 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1343,7 +1343,8 @@ Future setup({ case WalletType.zano: return zano!.createZanoWalletService(); case WalletType.decred: - return decred!.createDecredWalletService(_unspentCoinsInfoSource); + return decred!.createDecredWalletService( + _unspentCoinsInfoSource, SettingsStoreBase.walletPasswordDirectInput); case WalletType.haven: return HavenWalletService(); case WalletType.zcash: diff --git a/lib/reactions/wallet_utils.dart b/lib/reactions/wallet_utils.dart index 2e94fa9b5..56bf2080c 100644 --- a/lib/reactions/wallet_utils.dart +++ b/lib/reactions/wallet_utils.dart @@ -17,11 +17,11 @@ bool isBIP39Wallet(WalletType walletType) { case WalletType.monero: case WalletType.dogecoin: case WalletType.zcash: + case WalletType.decred: return true; case WalletType.wownero: case WalletType.haven: case WalletType.zano: - case WalletType.decred: case WalletType.none: return false; } diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 3e5ed20aa..1a0db8d1c 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -630,7 +630,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> } if ((walletRestoreViewModel.type == WalletType.decred) && - seedWords.length != WalletRestoreViewModelBase.decredSeedMnemonicLength) { + ![12, 15, 24].contains(seedWords.length)) { return false; } diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index d594f087f..39e9346b6 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -59,6 +59,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { case WalletType.solana: case WalletType.tron: case WalletType.zcash: + case WalletType.decred: return true; case WalletType.bitcoin: @@ -74,7 +75,6 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { case WalletType.none: case WalletType.haven: case WalletType.zano: - case WalletType.decred: return false; } } @@ -107,7 +107,8 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { WalletType.zano, WalletType.dogecoin, WalletType.zcash, - ].contains(type); + WalletType.decred, + ].contains(type); @computed bool get addCustomNode => _addCustomNode; diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index eeb2bd476..5a94bbd1a 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -179,6 +179,8 @@ abstract class WalletCreationVMBase with Store { ); } return bitcoin!.getElectrumDerivations()[DerivationType.electrum]!.first; + case WalletType.decred: + return DerivationInfo(derivationType: DerivationType.bip39); default: return DerivationInfo(derivationType: DerivationType.unknown); } diff --git a/lib/view_model/wallet_groups_display_view_model.dart b/lib/view_model/wallet_groups_display_view_model.dart index 5be1d29a4..fd8fdc23e 100644 --- a/lib/view_model/wallet_groups_display_view_model.dart +++ b/lib/view_model/wallet_groups_display_view_model.dart @@ -131,6 +131,10 @@ abstract class WalletGroupsDisplayViewModelBase with Store { bool isNotMoneroBip39Wallet = wallet.type == WalletType.monero && di.derivationType != DerivationType.bip39; + bool isNotDecredBip39Wallet = wallet.type == WalletType.decred && + di.derivationType != DerivationType.bip39 && + di.derivationType != DerivationType.unknown; + // Exclude if any of these conditions are true shouldExcludeGroup = shouldExcludeGroup || isNonBIP39Wallet || @@ -138,7 +142,8 @@ abstract class WalletGroupsDisplayViewModelBase with Store { isElectrumDerivationType || isSameTypeAsSelectedWallet || isNonSeedWallet || - isNotMoneroBip39Wallet; + isNotMoneroBip39Wallet || + isNotDecredBip39Wallet; } if (shouldExcludeGroup) continue; diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index f7fa7c7c7..e313ca035 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -149,7 +149,11 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { network: zcashNetwork, ); case WalletType.decred: - return decred!.createDecredNewWalletCredentials(name: name); + return decred!.createDecredNewWalletCredentials( + name: name, + password: walletPassword, + passphrase: passphrase, + mnemonic: newWalletArguments!.mnemonic); case WalletType.none: case WalletType.haven: throw Exception('Unexpected type: ${type.toString()}'); diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 467ad065a..16767a66e 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -216,6 +216,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { name: name, mnemonic: seed, password: password, + passphrase: passphrase, ); case WalletType.zcash: return zcash!.createZcashRestoreWalletFromSeedCredentials( diff --git a/pubspec.lock b/pubspec.lock index f0277a2dc..83f756386 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1871,8 +1871,8 @@ packages: dependency: "direct overridden" description: path: "." - ref: 40af7c48cf34211ba3cc9e87dd05ed1af8a429a2 - resolved-ref: 40af7c48cf34211ba3cc9e87dd05ed1af8a429a2 + ref: "40af7c48cf34211ba3cc9e87dd05ed1af8a429a2" + resolved-ref: "40af7c48cf34211ba3cc9e87dd05ed1af8a429a2" url: "https://github.com/cake-tech/ledger-usb-plus" source: git version: "1.0.8" @@ -3161,8 +3161,8 @@ packages: dependency: "direct overridden" description: path: "." - ref: 6735c1bdf7f42c501d51c20564eb957c3a12b981 - resolved-ref: 6735c1bdf7f42c501d51c20564eb957c3a12b981 + ref: "6735c1bdf7f42c501d51c20564eb957c3a12b981" + resolved-ref: "6735c1bdf7f42c501d51c20564eb957c3a12b981" url: "https://github.com/cake-tech/universal_ble.git" source: git version: "2.0.0" diff --git a/scripts/android/build_decred.sh b/scripts/android/build_decred.sh index 1ade0f8c8..cabf11fb1 100755 --- a/scripts/android/build_decred.sh +++ b/scripts/android/build_decred.sh @@ -7,7 +7,7 @@ cd "$(dirname "$0")" CW_DECRED_DIR=$(realpath ../..)/cw_decred LIBWALLET_PATH="${PWD}/decred/libwallet" LIBWALLET_URL="https://github.com/decred/libwallet.git" -LIBWALLET_VERSION="05f8d7374999400fe4d525eb365c39b77d307b14" +LIBWALLET_VERSION="ecc4a5fb9594368777848de42d7e072d62406507" if [[ -e $LIBWALLET_PATH ]]; then rm -fr $LIBWALLET_PATH || true diff --git a/scripts/ios/build_decred.sh b/scripts/ios/build_decred.sh index 37384f4e1..5e92cc6f5 100755 --- a/scripts/ios/build_decred.sh +++ b/scripts/ios/build_decred.sh @@ -3,7 +3,7 @@ set -e . ./config.sh LIBWALLET_PATH="${EXTERNAL_IOS_SOURCE_DIR}/libwallet" LIBWALLET_URL="https://github.com/decred/libwallet.git" -LIBWALLET_VERSION="05f8d7374999400fe4d525eb365c39b77d307b14" +LIBWALLET_VERSION="ecc4a5fb9594368777848de42d7e072d62406507" if [[ -e $LIBWALLET_PATH ]]; then rm -fr $LIBWALLET_PATH diff --git a/tool/configure.dart b/tool/configure.dart index b0a884969..e3f5f385c 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -1259,12 +1259,12 @@ import 'package:cw_decred/mnemonic.dart'; abstract class Decred { WalletCredentials createDecredNewWalletCredentials( - {required String name, WalletInfo? walletInfo}); + {required String name, String? password, String? passphrase, String? mnemonic, WalletInfo? walletInfo}); WalletCredentials createDecredRestoreWalletFromSeedCredentials( - {required String name, required String mnemonic, required String password}); + {required String name, required String mnemonic, required String password, String? passphrase}); WalletCredentials createDecredRestoreWalletFromPubkeyCredentials( {required String name, required String pubkey, required String password}); - WalletService createDecredWalletService(Box unspentCoinSource); + WalletService createDecredWalletService(Box unspentCoinSource, bool isDirect); List getTransactionPriorities(); TransactionPriority getDecredTransactionPriorityMedium(); From bd36227974b550ebb07b2b9e1e8a909e7a59d79b Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 25 Aug 2026 19:29:41 +0200 Subject: [PATCH 13/15] Update `breez_sdk_spark_flutter` to v0.23.0 and adjust pubspec overrides (#3558) * chore: update `breez_sdk_spark_flutter` to v0.23.0 and update pubspec overrides * feat: use latest breez sdk --- .../lib/lightning/lightning_wallet.dart | 124 ++++++++++-------- cw_bitcoin/pubspec.yaml | 2 +- pubspec_overrides.yaml | 2 +- 3 files changed, 73 insertions(+), 55 deletions(-) diff --git a/cw_bitcoin/lib/lightning/lightning_wallet.dart b/cw_bitcoin/lib/lightning/lightning_wallet.dart index ea9399c6a..49b4979e7 100644 --- a/cw_bitcoin/lib/lightning/lightning_wallet.dart +++ b/cw_bitcoin/lib/lightning/lightning_wallet.dart @@ -17,6 +17,16 @@ bool _breezSdkSparkLibUninitialized = true; Stream? _logStream; class LightningWallet { + LightningWallet({ + required this.mnemonic, + required this.apiKey, + required this.lnurlDomain, + this.network = Network.mainnet, + this.passphrase, + this.seedBytes, + this.cachedAddress, + }); + final String mnemonic; final String? passphrase; final Uint8List? seedBytes; @@ -29,16 +39,6 @@ class LightningWallet { static int MAX_RETRIES = 10; - LightningWallet({ - required this.mnemonic, - this.passphrase, - this.seedBytes, - required this.apiKey, - required this.lnurlDomain, - this.network = Network.mainnet, - this.cachedAddress, - }); - static bool get isAvailable => Platform.isIOS || Platform.isAndroid || Platform.isMacOS; Currency get currency => CryptoCurrency.btcln; @@ -87,10 +87,11 @@ class LightningWallet { ? Seed.entropy(seedBytes!) : Seed.mnemonic(mnemonic: mnemonic, passphrase: passphrase); final config = defaultConfig(network: Network.mainnet).copyWith( - lnurlDomain: lnurlDomain, - apiKey: apiKey, - privateEnabledDefault: true, - maxDepositClaimFee: MaxFee.rate(satPerVbyte: BigInt.from(5))); + lnurlDomain: lnurlDomain, + apiKey: apiKey, + privateEnabledDefault: true, + maxDepositClaimFee: MaxFee.rate(satPerVbyte: BigInt.from(5)), + ); final connectRequest = ConnectRequest( config: config, @@ -110,7 +111,7 @@ class LightningWallet { printV(e); } - await sdk.syncWallet(request: SyncWalletRequest()); + await sdk.syncWallet(request: const SyncWalletRequest()); return true; } catch (e) { @@ -139,31 +140,33 @@ class LightningWallet { } } catch (_) {} // No need to log here since it should be in the lightning log retries++; - await Future.delayed(Duration(milliseconds: 500)); + await Future.delayed(const Duration(milliseconds: 500)); } return cachedAddress; } Future getDepositAddress() async => (await sdk.receivePayment( - request: ReceivePaymentRequest(paymentMethod: ReceivePaymentMethod.bitcoinAddress()))) - .paymentRequest; + request: const ReceivePaymentRequest(paymentMethod: ReceivePaymentMethod.bitcoinAddress()), + )) + .paymentRequest; Future getBalance() async { try { - return Money((await sdk.getInfo(request: GetInfoRequest(ensureSynced: true))).balanceSats, - CryptoCurrency.btcln); + return Money( + (await sdk.getInfo(request: const GetInfoRequest(ensureSynced: true))).balanceSats, + CryptoCurrency.btcln, + ); } on SdkError_Generic catch (_) { } on SdkError_NetworkError catch (_) {} return Money.zero(CryptoCurrency.btcln); } - Future registerAddress(String username) async { - return (await sdk.registerLightningAddress( - request: RegisterLightningAddressRequest(username: username))) - .lightningAddress; - } + Future registerAddress(String username) async => (await sdk.registerLightningAddress( + request: RegisterLightningAddressRequest(username: username), + )) + .lightningAddress; Future getBolt11Invoice(BigInt? amount, String description) async { try { @@ -180,7 +183,9 @@ class LightningWallet { } on SdkError_NetworkError catch (_) { return null; } on SdkError_SparkError catch (e) { - if (!e.field0.contains("dns") && !e.field0.contains("TimedOut")) rethrow; + if (!e.field0.contains("dns") && !e.field0.contains("TimedOut")) { + rethrow; + } return null; } } @@ -196,17 +201,22 @@ class LightningWallet { } } - Future createTransaction(String address, BigInt? amountSats, - BitcoinTransactionPriority? priority, bool feesIncluded) async { + Future createTransaction( + String address, + BigInt? amountSats, + BitcoinTransactionPriority? priority, + bool feesIncluded, + ) async { final inputType = await sdk.parse(input: address); final feePolicy = feesIncluded ? FeePolicy.feesIncluded : FeePolicy.feesExcluded; if (inputType is InputType_Bolt11Invoice) { final request = PrepareSendPaymentRequest( - paymentRequest: inputType.field0.invoice.bolt11, - amount: amountSats, - feePolicy: feePolicy); + paymentRequest: PaymentRequest.input(input: inputType.field0.invoice.bolt11), + amount: amountSats, + feePolicy: feePolicy, + ); final prepareResponse = await sdk.prepareSendPayment(request: request); final paymentMethod = prepareResponse.paymentMethod; @@ -227,7 +237,8 @@ class LightningWallet { commitOverride: () async { try { final res = await sdk.sendPayment( - request: SendPaymentRequest(prepareResponse: prepareResponse)); + request: SendPaymentRequest(prepareResponse: prepareResponse), + ); printV(res.payment.status.name); return res.payment.id; } on SdkError_SparkError catch (e) { @@ -240,7 +251,7 @@ class LightningWallet { ); } } else if (inputType is InputType_LightningAddress || inputType is InputType_LnurlPay) { - final optionalValidateSuccessActionUrl = true; + const optionalValidateSuccessActionUrl = true; PrepareLnurlPayRequest request; if (inputType is InputType_LightningAddress) { @@ -274,7 +285,7 @@ class LightningWallet { ); } else if (inputType is InputType_BitcoinAddress) { final request = PrepareSendPaymentRequest( - paymentRequest: inputType.field0.address, + paymentRequest: PaymentRequest.input(input: inputType.field0.address), amount: amountSats, feePolicy: feePolicy, ); @@ -309,7 +320,8 @@ class LightningWallet { final options = SendPaymentOptions.bitcoinAddress(confirmationSpeed: onchainConfirmationSpeed); final res = await sdk.sendPayment( - request: SendPaymentRequest(prepareResponse: prepareResponse, options: options)); + request: SendPaymentRequest(prepareResponse: prepareResponse, options: options), + ); return res.payment.id; }, ); @@ -326,7 +338,7 @@ class LightningWallet { // statusFilter: [PaymentStatus.completed], fromTimestamp: fromDate != null ? BigInt.from((fromDate.millisecondsSinceEpoch / 1000).round()) : null, - assetFilter: AssetFilter.bitcoin(), + assetFilter: const AssetFilter.bitcoin(), offset: 0, limit: 50, sortAscending: false, // Sort order (true = oldest first, false = newest first) @@ -357,7 +369,7 @@ class LightningWallet { /// Future>> getUnclaimedDeposits() async { final unclaimedDeposits = >[]; - final response = await sdk.listUnclaimedDeposits(request: ListUnclaimedDepositsRequest()); + final response = await sdk.listUnclaimedDeposits(request: const ListUnclaimedDepositsRequest()); for (final deposit in response.deposits) { final unclaimedDeposit = { "txId": deposit.txid, @@ -380,7 +392,7 @@ class LightningWallet { return unclaimedDeposits; } - Future claimDeposit(String txId, int vout, BigInt newFee) async { + Future claimDeposit(String txId, int vout, BigInt newFee) async { final response = await sdk.claimDeposit( request: ClaimDepositRequest( txid: txId, @@ -389,11 +401,18 @@ class LightningWallet { ), ); - return _getElectrumTransactionInfoFromPayment(response.payment); + if (response.payment == null) { + return null; + } + return _getElectrumTransactionInfoFromPayment(response.payment!); } Future refundDeposit( - String txId, int vout, String destinationAddress, BigInt feeRate) async { + String txId, + int vout, + String destinationAddress, + BigInt feeRate, + ) async { final response = await sdk.refundDeposit( request: RefundDepositRequest( txid: txId, @@ -464,17 +483,16 @@ class LightningWallet { ); } - ElectrumTransactionInfo _getElectrumTransactionInfoFromDepositInfo(DepositInfo deposit) { - return ElectrumTransactionInfo( - WalletType.bitcoin, - id: deposit.txid, - amount: Money(deposit.amountSats, currency), - direction: TransactionDirection.incoming, - isPending: true, - fee: Money.zero(currency), - date: DateTime.now(), - confirmations: 0, - additionalInfo: {"isLightning": true, "isSparkDeposit": true}, - ); - } + ElectrumTransactionInfo _getElectrumTransactionInfoFromDepositInfo(DepositInfo deposit) => + ElectrumTransactionInfo( + WalletType.bitcoin, + id: deposit.txid, + amount: Money(deposit.amountSats, currency), + direction: TransactionDirection.incoming, + isPending: true, + fee: Money.zero(currency), + date: DateTime.now(), + confirmations: 0, + additionalInfo: {"isLightning": true, "isSparkDeposit": true}, + ); } diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 46a8992cc..b447dda68 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -72,7 +72,7 @@ dependencies: breez_sdk_spark_flutter: git: url: https://github.com/breez/breez-sdk-spark-flutter - ref: v0.14.0 + ref: v0.23.0 dev_dependencies: flutter_test: diff --git a/pubspec_overrides.yaml b/pubspec_overrides.yaml index b3b236a4a..b91e7dc84 100644 --- a/pubspec_overrides.yaml +++ b/pubspec_overrides.yaml @@ -120,7 +120,7 @@ dependency_overrides: breez_sdk_spark_flutter: git: url: https://github.com/breez/breez-sdk-spark-flutter - ref: e18b6437daaf0572de6aea2439c2ec4d22658bbb + ref: 5943a3e773203f21728f87f1c0779040db8f2c28 bs58: git: url: https://github.com/litaoh/base58 From 06e75551a77325539eabc4a0f27fb1df033e6b8a Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Wed, 26 Aug 2026 04:45:03 +0100 Subject: [PATCH 14/15] CW-1615: Harden walletconnect auth (#3547) * fix android CI * fix: harden walletconnect auth so a connected session won't transfer on dApp when wallet is switched within the app * refactor: improve session mgmt and better error handling, also added a loader to the listing page * feat: switch to newer id for solana dapps connections and fix requests not triggering --- .../chain_service/eth/evm_chain_service.dart | 213 ++++++++++++++++-- .../chain_service/solana/solana_chain_id.dart | 6 +- .../solana/solana_chain_service.dart | 89 +++++++- .../wallet_connect_key_service.dart | 54 +++-- .../services/walletkit_service.dart | 143 +++++++----- .../wallet_connect/utils/method_utils.dart | 30 ++- .../wc_connections_listing_view.dart | 21 ++ lib/store/app_store.dart | 22 +- res/values/strings_ar.arb | 1 + res/values/strings_bg.arb | 1 + res/values/strings_cs.arb | 1 + res/values/strings_de.arb | 1 + res/values/strings_en.arb | 1 + res/values/strings_es.arb | 1 + res/values/strings_fa.arb | 1 + res/values/strings_fr.arb | 1 + res/values/strings_gn.arb | 1 + res/values/strings_ha.arb | 1 + res/values/strings_hi.arb | 1 + res/values/strings_hr.arb | 1 + res/values/strings_hy.arb | 1 + res/values/strings_id.arb | 1 + res/values/strings_it.arb | 1 + res/values/strings_ja.arb | 1 + res/values/strings_ko.arb | 1 + res/values/strings_my.arb | 1 + res/values/strings_nl.arb | 1 + res/values/strings_pl.arb | 1 + res/values/strings_pt.arb | 1 + res/values/strings_pt_BR.arb | 1 + res/values/strings_ru.arb | 1 + res/values/strings_th.arb | 1 + res/values/strings_tl.arb | 1 + res/values/strings_tr.arb | 1 + res/values/strings_uk.arb | 1 + res/values/strings_ur.arb | 1 + res/values/strings_vi.arb | 1 + res/values/strings_yo.arb | 1 + res/values/strings_zh.arb | 1 + 39 files changed, 495 insertions(+), 114 deletions(-) diff --git a/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart b/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart index c8618d85f..e30fc2e54 100644 --- a/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart +++ b/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart @@ -1,3 +1,4 @@ +import "dart:async"; import 'dart:convert'; import 'package:cake_wallet/di.dart'; @@ -5,6 +6,7 @@ import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cw_core/crypto_currency.dart'; +import "package:cw_core/utils/print_verbose.dart"; import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:eth_sig_util/eth_sig_util.dart'; import 'package:eth_sig_util/util/utils.dart'; @@ -18,6 +20,7 @@ import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wall import 'package:cake_wallet/src/screens/wallet_connect/models/wc_connection_model.dart'; import 'package:cake_wallet/src/screens/wallet_connect/utils/eth_utils.dart'; import 'package:cake_wallet/src/screens/wallet_connect/utils/method_utils.dart'; +import "package:cake_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_message_display_widget.dart"; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/evm/evm.dart'; @@ -82,7 +85,9 @@ class EvmChainServiceImpl { if (appStore.wallet != null && isEVMCompatibleChain(appStore.wallet!.type)) { final walletClient = evm?.getWeb3Client(appStore.wallet!); - if (walletClient != null) return walletClient; + if (walletClient != null) { + return walletClient; + } } final node = appStore.settingsStore.getCurrentNode(appStore.wallet!.type); @@ -93,8 +98,18 @@ class EvmChainServiceImpl { Future personalSign(String topic, dynamic parameters) async { debugPrint('personalSign request: $parameters'); - final pRequest = walletKit.pendingRequests.getAll().last; + final pRequest = _pendingRequest(topic, EVMSupportedMethods.personalSign.name); + if (pRequest == null) { + return; + } + final address = EthUtils.getAddressFromSessionRequest(pRequest); + + if (!_isRequestAuthorized(topic, requestAddress: address)) { + await _rejectUnauthorizedRequest(topic, pRequest.id); + return; + } + final data = EthUtils.getDataFromSessionRequest(pRequest); final message = EthUtils.getUtf8Message(data.toString()); var response = JsonRpcResponse(id: pRequest.id, jsonrpc: '2.0'); @@ -104,6 +119,7 @@ class EvmChainServiceImpl { method: pRequest.method, chainId: pRequest.chainId, address: address, + topic: topic, transportType: pRequest.transportType.name, verifyContext: pRequest.verifyContext, ); @@ -141,8 +157,18 @@ class EvmChainServiceImpl { Future ethSign(String topic, dynamic parameters) async { debugPrint('ethSign request: $parameters'); - final pRequest = walletKit.pendingRequests.getAll().last; + final pRequest = _pendingRequest(topic, EVMSupportedMethods.ethSign.name); + if (pRequest == null) { + return; + } + final address = EthUtils.getAddressFromSessionRequest(pRequest); + + if (!_isRequestAuthorized(topic, requestAddress: address)) { + await _rejectUnauthorizedRequest(topic, pRequest.id); + return; + } + final data = EthUtils.getDataFromSessionRequest(pRequest); final message = EthUtils.getUtf8Message(data.toString()); var response = JsonRpcResponse(id: pRequest.id, jsonrpc: '2.0'); @@ -152,6 +178,7 @@ class EvmChainServiceImpl { method: pRequest.method, chainId: pRequest.chainId, address: address, + topic: topic, transportType: pRequest.transportType.name, verifyContext: pRequest.verifyContext, ); @@ -189,8 +216,18 @@ class EvmChainServiceImpl { Future ethSignTypedData(String topic, dynamic parameters) async { debugPrint('ethSignTypedData request: $parameters'); - final pRequest = walletKit.pendingRequests.getAll().last; + final pRequest = _pendingRequest(topic, EVMSupportedMethods.ethSignTypedData.name); + if (pRequest == null) { + return; + } + final address = EthUtils.getAddressFromSessionRequest(pRequest); + + if (!_isRequestAuthorized(topic, requestAddress: address)) { + await _rejectUnauthorizedRequest(topic, pRequest.id); + return; + } + final data = EthUtils.getDataFromSessionRequest(pRequest) as String; var response = JsonRpcResponse(id: pRequest.id, jsonrpc: '2.0'); @@ -199,6 +236,7 @@ class EvmChainServiceImpl { method: pRequest.method, chainId: pRequest.chainId, address: address, + topic: topic, transportType: pRequest.transportType.name, verifyContext: pRequest.verifyContext, ); @@ -236,8 +274,18 @@ class EvmChainServiceImpl { final permitRequestMessage = await extractPermitData(parameters); - final pRequest = walletKit.pendingRequests.getAll().last; + final pRequest = _pendingRequest(topic, EVMSupportedMethods.ethSignTypedDataV4.name); + if (pRequest == null) { + return; + } + final address = EthUtils.getAddressFromSessionRequest(pRequest); + + if (!_isRequestAuthorized(topic, requestAddress: address)) { + await _rejectUnauthorizedRequest(topic, pRequest.id); + return; + } + final data = EthUtils.getDataFromSessionRequest(pRequest) as String; var response = JsonRpcResponse(id: pRequest.id, jsonrpc: '2.0'); @@ -246,6 +294,7 @@ class EvmChainServiceImpl { method: pRequest.method, chainId: pRequest.chainId, address: address, + topic: topic, transportType: pRequest.transportType.name, verifyContext: pRequest.verifyContext, ); @@ -280,10 +329,22 @@ class EvmChainServiceImpl { Future ethSignTransaction(String topic, dynamic parameters) async { debugPrint('ethSignTransaction request: $parameters'); - final SessionRequest pRequest = walletKit.pendingRequests.getAll().last; + final pRequest = _pendingRequest(topic, EVMSupportedMethods.ethSignTransaction.name); + if (pRequest == null) { + return; + } + final data = EthUtils.getTransactionFromSessionRequest(pRequest); - if (data == null) return; + if (data == null) { + _respondMalformedRequest(topic, pRequest.id); + return; + } + + if (!_isRequestAuthorized(topic, requestAddress: data['from']?.toString())) { + await _rejectUnauthorizedRequest(topic, pRequest.id); + return; + } final address = EthUtils.getAddressFromSessionRequest(pRequest); var response = JsonRpcResponse(id: pRequest.id, jsonrpc: '2.0'); @@ -293,6 +354,7 @@ class EvmChainServiceImpl { method: pRequest.method, chainId: pRequest.chainId, address: address, + topic: topic, transportType: pRequest.transportType.name, verifyContext: pRequest.verifyContext, ); @@ -333,10 +395,21 @@ class EvmChainServiceImpl { Future ethSendTransaction(String topic, dynamic parameters) async { debugPrint('ethSendTransaction request: $parameters'); - final SessionRequest pRequest = walletKit.pendingRequests.getAll().last; + final pRequest = _pendingRequest(topic, EVMSupportedMethods.ethSendTransaction.name); + if (pRequest == null) { + return; + } final data = EthUtils.getTransactionFromSessionRequest(pRequest); - if (data == null) return; + if (data == null) { + _respondMalformedRequest(topic, pRequest.id); + return; + } + + if (!_isRequestAuthorized(topic, requestAddress: data['from']?.toString())) { + await _rejectUnauthorizedRequest(topic, pRequest.id); + return; + } var response = JsonRpcResponse(id: pRequest.id, jsonrpc: '2.0'); @@ -344,6 +417,7 @@ class EvmChainServiceImpl { data, method: pRequest.method, chainId: pRequest.chainId, + topic: topic, transportType: pRequest.transportType.name, verifyContext: pRequest.verifyContext, ); @@ -379,6 +453,77 @@ class EvmChainServiceImpl { _handleResponseForTopic(topic, response); } + void _respondMalformedRequest(String topic, int requestId) { + final error = Errors.getSdkError(Errors.MALFORMED_REQUEST_PARAMS); + _handleResponseForTopic( + topic, + JsonRpcResponse( + id: requestId, + jsonrpc: "2.0", + error: JsonRpcError(code: error.code, message: error.message), + ), + ); + } + + SessionRequest? _pendingRequest(String topic, String method) { + final matches = walletKit.pendingRequests + .getAll() + .where((request) => request.topic == topic && request.method == method); + + return matches.isEmpty ? null : matches.last; + } + + bool _isRequestAuthorized(String topic, {String? requestAddress}) { + final wallet = appStore.wallet; + if (wallet == null) { + return false; + } + + final keys = wcKeyService.getKeysForChain(wallet); + if (keys.isEmpty) { + return false; + } + + final walletAddress = keys.first.publicKey; + + if (!MethodsUtils.isSessionOwnedByWallet(walletKit.sessions.get(topic), walletAddress)) { + return false; + } + + if (requestAddress != null && !MethodsUtils.isSameAccount(requestAddress, walletAddress)) { + return false; + } + + return true; + } + + Future _rejectUnauthorizedRequest(String topic, int requestId) async { + unawaited( + bottomSheetService.queueBottomSheet( + isModalDismissible: true, + widget: BottomSheetMessageDisplayWidget( + message: S.current.wc_request_for_different_wallet, + ), + ), + ); + + try { + await walletKit.respondSessionRequest( + topic: topic, + response: JsonRpcResponse( + id: requestId, + jsonrpc: "2.0", + error: const JsonRpcError( + code: 4100, + message: "The requested account has not been authorized by the user.", + ), + ), + ); + } catch (e) { + printV("rejectUnauthorizedRequest: $e"); + } + } + void _handleResponseForTopic(String topic, JsonRpcResponse response) async { final session = walletKit.sessions.get(topic); @@ -388,7 +533,9 @@ class EvmChainServiceImpl { response: response, ); - if (session == null) return; + if (session == null) { + return; + } MethodsUtils.handleRedirect( topic, @@ -397,7 +544,9 @@ class EvmChainServiceImpl { response.error == null, ); } on ReownSignError catch (error) { - if (session == null) return; + if (session == null) { + return; + } MethodsUtils.handleRedirect( topic, @@ -413,6 +562,7 @@ class EvmChainServiceImpl { String? method, String? chainId, String? address, + String? topic, VerifyContext? verifyContext, required String transportType, }) async { @@ -456,6 +606,7 @@ class EvmChainServiceImpl { method: method, chainId: chainId, address: address ?? transaction.from?.hex ?? '', + topic: topic, transportType: transportType, verifyContext: verifyContext, extraModels: feeRows, @@ -472,13 +623,17 @@ class EvmChainServiceImpl { String nativeSymbol, ) { final gasLimit = transaction.maxGas; - if (gasLimit == null || gasLimit <= 0) return const []; + if (gasLimit == null || gasLimit <= 0) { + return const []; + } final gasLimitBig = BigInt.from(gasLimit); final isEip1559 = transaction.isEIP1559; final perGasWei = isEip1559 ? transaction.maxFeePerGas?.getInWei : transaction.gasPrice?.getInWei; - if (perGasWei == null || perGasWei <= BigInt.zero) return const []; + if (perGasWei == null || perGasWei <= BigInt.zero) { + return const []; + } final feeWei = gasLimitBig * perGasWei; final feeNative = feeWei.toDouble() / 1e18; @@ -500,19 +655,25 @@ class EvmChainServiceImpl { ) { final cryptoPart = '${_formatNativeAmount(feeNative)} $nativeSymbol'; - if (nativeCurrency == null) return cryptoPart; + if (nativeCurrency == null) { + return cryptoPart; + } try { final fiatStore = getIt.get(); final price = fiatStore.prices[nativeCurrency]; - if (price == null || price <= 0) return cryptoPart; + if (price == null || price <= 0) { + return cryptoPart; + } final fiatSymbol = appStore.settingsStore.fiatCurrency.title; final fiatValue = calculateFiatAmount( price: price, cryptoAmount: feeNative.toString(), ); - if (fiatValue.isEmpty || fiatValue == '0.00') return cryptoPart; + if (fiatValue.isEmpty || fiatValue == '0.00') { + return cryptoPart; + } return '$cryptoPart (~ $fiatValue $fiatSymbol)'; } catch (_) { @@ -521,14 +682,20 @@ class EvmChainServiceImpl { } String _formatNativeAmount(double value) { - if (value == 0) return '0'; - if (value >= 0.0001) return value.toStringAsFixed(6); + if (value == 0) { + return '0'; + } + if (value >= 0.0001) { + return value.toStringAsFixed(6); + } return value.toStringAsExponential(4); } Future _ensureWCTransactionHasGasLimit(Transaction transaction) async { final hasGasLimit = transaction.maxGas != null && transaction.maxGas! > 0; - if (hasGasLimit) return transaction; + if (hasGasLimit) { + return transaction; + } final hint = transaction.gasPrice ?? transaction.maxFeePerGas ?? await ethClient.getGasPrice(); @@ -689,7 +856,9 @@ $messageDetails'''; final fieldType = field['type'] as String; final value = message[field['name'] as String]; - if (value == null) continue; + if (value == null) { + continue; + } if (types.containsKey(fieldType)) { final nestedFields = types[fieldType] as List; @@ -714,7 +883,9 @@ $messageDetails'''; } String _toCamelCase(String input) { - if (input.isEmpty) return input; + if (input.isEmpty) { + return input; + } return input[0].toUpperCase() + input.substring(1).toLowerCase(); } diff --git a/lib/src/screens/wallet_connect/services/chain_service/solana/solana_chain_id.dart b/lib/src/screens/wallet_connect/services/chain_service/solana/solana_chain_id.dart index c5a3c9d25..a4012cd57 100644 --- a/lib/src/screens/wallet_connect/services/chain_service/solana/solana_chain_id.dart +++ b/lib/src/screens/wallet_connect/services/chain_service/solana/solana_chain_id.dart @@ -1,4 +1,4 @@ -enum SolanaChainId { mainnet, devnet, testnet } +enum SolanaChainId { mainnet, mainnetLegacy, devnet, testnet } extension SolanaChainIdX on SolanaChainId { String chain() { @@ -6,8 +6,10 @@ extension SolanaChainIdX on SolanaChainId { switch (this) { case SolanaChainId.mainnet: + name = "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"; + break; + case SolanaChainId.mainnetLegacy: name = '4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ'; - // '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'; break; case SolanaChainId.devnet: name = 'EtWTRABZaYq6iMfeYKouRu166VU2xqa1'; diff --git a/lib/src/screens/wallet_connect/services/chain_service/solana/solana_chain_service.dart b/lib/src/screens/wallet_connect/services/chain_service/solana/solana_chain_service.dart index b5d12f357..dfba2bf59 100644 --- a/lib/src/screens/wallet_connect/services/chain_service/solana/solana_chain_service.dart +++ b/lib/src/screens/wallet_connect/services/chain_service/solana/solana_chain_service.dart @@ -1,16 +1,20 @@ +import "dart:async"; import 'dart:convert'; import 'package:blockchain_utils/base58/base58.dart'; import 'package:blockchain_utils/blockchain_utils.dart' as blockchain_utils; import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/solana/solana_supported_methods.dart'; +import "package:cw_core/utils/print_verbose.dart"; import 'package:flutter/material.dart'; import 'package:on_chain/solana/solana.dart'; import 'package:reown_walletkit/reown_walletkit.dart'; +import "package:cake_wallet/generated/i18n.dart"; import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart'; import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/solana/solana_chain_id.dart'; import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart'; import 'package:cake_wallet/src/screens/wallet_connect/utils/method_utils.dart'; +import "package:cake_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_message_display_widget.dart"; import 'package:cake_wallet/store/app_store.dart'; class SolanaChainService { @@ -47,7 +51,16 @@ class SolanaChainService { Future solanaSignMessage(String topic, dynamic parameters) async { debugPrint('solanaSignMessage request: $parameters'); - final pRequest = walletKit.pendingRequests.getAll().last; + final pRequest = _pendingRequest(topic, SolanaSupportedMethods.solSignMessage.name); + if (pRequest == null) { + return; + } + + if (!_isRequestAuthorized(topic)) { + await _rejectUnauthorizedRequest(topic, pRequest.id); + return; + } + var response = JsonRpcResponse(id: pRequest.id, jsonrpc: '2.0'); try { @@ -65,6 +78,7 @@ class SolanaChainService { method: pRequest.method, chainId: pRequest.chainId, address: privateKey.publicKey().toAddress().address, + topic: topic, transportType: pRequest.transportType.name, ); @@ -95,7 +109,16 @@ class SolanaChainService { Future solanaSignTransaction(String topic, dynamic parameters) async { debugPrint('solanaSignTransaction: ${jsonEncode(parameters)}'); - final pRequest = walletKit.pendingRequests.getAll().last; + final pRequest = _pendingRequest(topic, SolanaSupportedMethods.solSignTransaction.name); + if (pRequest == null) { + return; + } + + if (!_isRequestAuthorized(topic)) { + await _rejectUnauthorizedRequest(topic, pRequest.id); + return; + } + var response = JsonRpcResponse(id: pRequest.id, jsonrpc: '2.0'); try { @@ -130,6 +153,7 @@ class SolanaChainService { method: pRequest.method, chainId: pRequest.chainId, address: privateKey.publicKey().toAddress().address, + topic: topic, transportType: pRequest.transportType.name, ); @@ -160,7 +184,16 @@ class SolanaChainService { Future solanaSignAllTransaction(String topic, dynamic parameters) async { debugPrint('solanaSignAllTransaction: ${jsonEncode(parameters)}'); - final pRequest = walletKit.pendingRequests.getAll().last; + final pRequest = _pendingRequest(topic, SolanaSupportedMethods.solSignAllTransaction.name); + if (pRequest == null) { + return; + } + + if (!_isRequestAuthorized(topic)) { + await _rejectUnauthorizedRequest(topic, pRequest.id); + return; + } + var response = JsonRpcResponse(id: pRequest.id, jsonrpc: '2.0'); try { @@ -174,6 +207,7 @@ class SolanaChainService { method: pRequest.method, chainId: pRequest.chainId, address: privateKey.publicKey().toAddress().address, + topic: topic, transportType: pRequest.transportType.name, ); @@ -224,6 +258,55 @@ class SolanaChainService { return SolanaPrivateKey.fromSeedHex(keys[0].privateKey); } + SessionRequest? _pendingRequest(String topic, String method) { + final matches = walletKit.pendingRequests + .getAll() + .where((request) => request.topic == topic && request.method == method); + + return matches.isEmpty ? null : matches.last; + } + + bool _isRequestAuthorized(String topic) { + final wallet = appStore.wallet; + if (wallet == null) { + return false; + } + + final keys = wcKeyService.getKeysForChain(wallet); + if (keys.isEmpty) { + return false; + } + + return MethodsUtils.isSessionOwnedByWallet(walletKit.sessions.get(topic), keys.first.publicKey); + } + + Future _rejectUnauthorizedRequest(String topic, int requestId) async { + unawaited( + bottomSheetService.queueBottomSheet( + isModalDismissible: true, + widget: BottomSheetMessageDisplayWidget( + message: S.current.wc_request_for_different_wallet, + ), + ), + ); + + try { + await walletKit.respondSessionRequest( + topic: topic, + response: JsonRpcResponse( + id: requestId, + jsonrpc: "2.0", + error: const JsonRpcError( + code: 4100, + message: "The requested account has not been authorized by the user.", + ), + ), + ); + } catch (e) { + printV("rejectUnauthorizedRequest: $e"); + } + } + void _handleResponseForTopic(String topic, JsonRpcResponse response) async { final session = walletKit.sessions.get(topic); diff --git a/lib/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart b/lib/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart index 38772da10..4d09213d7 100644 --- a/lib/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart +++ b/lib/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart @@ -43,30 +43,36 @@ class KeyServiceImpl implements WalletConnectKeyService { @override List getKeys(WalletBase wallet) { - final keys = [ - ChainKeyModel( - chains: [ - 'eip155:1', - 'eip155:5', - 'eip155:56', - 'eip155:137', - 'eip155:8453', - 'eip155:42161', - 'eip155:80001', - ], - privateKey: _getPrivateKeyForWallet(wallet), - publicKey: _getPublicKeyForWallet(wallet), - ), - ChainKeyModel( - chains: [ - 'solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ', // main-net - 'solana:8E9rvCKLFQia2Y35HXjjpWzj8weVo44K', // test-net - ], - privateKey: _getPrivateKeyForWallet(wallet), - publicKey: _getPublicKeyForWallet(wallet), - ), - ]; - return keys; + if (isEVMCompatibleChain(wallet.type)) { + return [ + ChainKeyModel( + chains: [ + 'eip155:1', + 'eip155:56', + 'eip155:137', + 'eip155:8453', + 'eip155:42161', + ], + privateKey: _getPrivateKeyForWallet(wallet), + publicKey: _getPublicKeyForWallet(wallet), + ), + ]; + } + + if (wallet.type == WalletType.solana) { + return [ + ChainKeyModel( + chains: [ + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', // main-net + 'solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ', // legacy main-net id older dapps still use + ], + privateKey: _getPrivateKeyForWallet(wallet), + publicKey: _getPublicKeyForWallet(wallet), + ), + ]; + } + + return []; } @override diff --git a/lib/src/screens/wallet_connect/services/walletkit_service.dart b/lib/src/screens/wallet_connect/services/walletkit_service.dart index aa3c26307..09c1432f5 100644 --- a/lib/src/screens/wallet_connect/services/walletkit_service.dart +++ b/lib/src/screens/wallet_connect/services/walletkit_service.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:eth_sig_util/util/utils.dart'; import 'package:flutter/material.dart'; import 'package:mobx/mobx.dart'; @@ -42,7 +41,8 @@ abstract class WalletKitServiceBase with Store { ) : pairings = ObservableList(), sessions = ObservableList(), auth = ObservableList(), - isInitialized = false; + isInitialized = false, + isLoadingConnections = true; final AppStore appStore; final SharedPreferences sharedPreferences; @@ -54,6 +54,9 @@ abstract class WalletKitServiceBase with Store { @observable bool isInitialized; + @observable + bool isLoadingConnections; + /// The list of requests from the dapp @observable ObservableList pairings; @@ -91,33 +94,23 @@ abstract class WalletKitServiceBase with Store { _walletKit.onSessionProposal.subscribe(_onSessionProposal); _walletKit.onSessionProposalError.subscribe(_onSessionProposalError); _walletKit.onSessionConnect.subscribe(_onSessionConnect); - _walletKit.onSessionAuthRequest.subscribe(_onSessionAuthRequest); + + if (isEVMCompatibleChain(appStore.wallet!.type)) { + _walletKit.onSessionAuthRequest.subscribe(_onSessionAuthRequest); + } _walletKit.pairings.onSync.subscribe(_onPairingsSync); _walletKit.core.pairing.onPairingDelete.subscribe(_onPairingDelete); _walletKit.core.pairing.onPairingExpire.subscribe(_onPairingDelete); // Setup our accounts - List chainKeys = walletKeyService.getKeys(appStore.wallet!); + final chainKeys = walletKeyService.getKeys(appStore.wallet!); for (final chainKey in chainKeys) { for (final chainId in chainKey.chains) { - if (isEVMCompatibleChain(appStore.wallet!.type)) { - // Register account for all EVM chains (chainId is already in eip155:format) - _walletKit.registerAccount( - chainId: chainId, - accountAddress: chainKey.publicKey, - ); - } else { - final chainNameSpace = getChainNameSpaceAndIdBasedOnWalletType( - appStore.wallet!.type, - ); - if (chainNameSpace == chainId) { - _walletKit.registerAccount( - chainId: chainId, - accountAddress: chainKey.publicKey, - ); - } - } + _walletKit.registerAccount( + chainId: chainId, + accountAddress: chainKey.publicKey, + ); } } } @@ -144,42 +137,43 @@ abstract class WalletKitServiceBase with Store { } } - await _emitEvent(); - _refreshPairings(); - sessions.clear(); - auth.clear(); + _reloadSessionsForCurrentWallet(); - final newSessions = _walletKit.sessions.getAll(); - sessions.addAll(newSessions); + auth.clear(); final newAuthRequests = _walletKit.sessionAuthRequests.getAll(); auth.addAll(newAuthRequests); - if (isEVMCompatibleChain(appStore.wallet!.type)) { - for (final cId in EVMChainId.values) { - EvmChainServiceImpl( - reference: cId, - appStore: appStore, - wcKeyService: walletKeyService, - bottomSheetService: _bottomSheetHandler, - walletKit: _walletKit, - ); - } + isLoadingConnections = false; + for (final cId in EVMChainId.values) { + EvmChainServiceImpl( + reference: cId, + appStore: appStore, + wcKeyService: walletKeyService, + bottomSheetService: _bottomSheetHandler, + walletKit: _walletKit, + ); } - if (appStore.wallet!.type == WalletType.solana) { - for (final cId in SolanaChainId.values) { - SolanaChainService( - reference: cId, - appStore: appStore, - wcKeyService: walletKeyService, - bottomSheetService: _bottomSheetHandler, - walletKit: _walletKit, - ); - } + for (final cId in SolanaChainId.values) { + SolanaChainService( + reference: cId, + appStore: appStore, + wcKeyService: walletKeyService, + bottomSheetService: _bottomSheetHandler, + walletKit: _walletKit, + ); } + + unawaited(() async { + try { + await _emitEvent(); + } catch (e) { + printV("emitEvent failed: $e"); + } + }()); } @action @@ -195,6 +189,10 @@ abstract class WalletKitServiceBase with Store { for (var session in engineSessions) { final chainKeys = walletKeyService.getKeysForChain(appStore.wallet!); for (var chain in chainKeys) { + if (!MethodsUtils.isSessionOwnedByWallet(session, chain.publicKey)) { + continue; + } + for (var chainID in chain.chains) { try { final events = NamespaceUtils.getNamespacesEventsForChain( @@ -228,8 +226,21 @@ abstract class WalletKitServiceBase with Store { } @action - FutureOr onDispose() { - log('walletKit dispose'); + void resetConnectionsState() { + sessions.clear(); + auth.clear(); + pairings.clear(); + + isInitialized = false; + isLoadingConnections = true; + } + + @action + Future onDispose() async { + log("walletKit dispose"); + + resetConnectionsState(); + _walletKit.core.removeLogListener(_logListener); _walletKit.core.pairing.onPairingInvalid.unsubscribe(_onPairingInvalid); @@ -246,11 +257,11 @@ abstract class WalletKitServiceBase with Store { _walletKit.core.pairing.onPairingDelete.unsubscribe(_onPairingDelete); _walletKit.core.pairing.onPairingExpire.unsubscribe(_onPairingDelete); - sessions.clear(); - auth.clear(); - pairings.clear(); - - isInitialized = false; + try { + await _walletKit.core.relayClient.disconnect().timeout(const Duration(seconds: 3)); + } catch (e) { + printV("walletKit relay disconnect: $e"); + } } ReownWalletKit get walletKit => _walletKit; @@ -534,8 +545,7 @@ abstract class WalletKitServiceBase with Store { } await _removePairingTopicFromLocalStorage(topic); - sessions.clear(); - sessions.addAll(_walletKit.sessions.getAll()); + _reloadSessionsForCurrentWallet(); _refreshPairings(); } @@ -559,8 +569,7 @@ abstract class WalletKitServiceBase with Store { printV('disconnectSession: $e'); } - sessions.clear(); - sessions.addAll(_walletKit.sessions.getAll()); + _reloadSessionsForCurrentWallet(); if (pairingTopic != null && !_walletKit.sessions.getAll().any((s) => s.pairingTopic == pairingTopic)) { @@ -619,6 +628,24 @@ abstract class WalletKitServiceBase with Store { pairings.addAll(filteredPairings); } + @action + void _reloadSessionsForCurrentWallet() { + sessions.clear(); + + final chainKeys = walletKeyService.getKeysForChain(appStore.wallet!); + if (chainKeys.isEmpty) { + return; + } + + final walletPublicKey = chainKeys.first.publicKey; + + sessions.addAll( + _walletKit.sessions + .getAll() + .where((session) => MethodsUtils.isSessionOwnedByWallet(session, walletPublicKey)), + ); + } + @action List getSessionsForPairingInfo(PairingInfo pairing) { return sessions.where((element) => element.pairingTopic == pairing.topic).toList(); diff --git a/lib/src/screens/wallet_connect/utils/method_utils.dart b/lib/src/screens/wallet_connect/utils/method_utils.dart index 78a11f871..9a0e83a59 100644 --- a/lib/src/screens/wallet_connect/utils/method_utils.dart +++ b/lib/src/screens/wallet_connect/utils/method_utils.dart @@ -11,9 +11,27 @@ import 'package:flutter/material.dart'; import 'package:reown_walletkit/reown_walletkit.dart'; class MethodsUtils { - static final walletKit = getIt.get().walletKit; + static ReownWalletKit get walletKit => getIt.get().walletKit; static final bottomSheetService = getIt.get(); + static bool isSessionOwnedByWallet(SessionData? session, String walletPublicKey) { + if (session == null || walletPublicKey.isEmpty) { + return false; + } + + final accounts = session.namespaces.values.expand((namespace) => namespace.accounts); + + return accounts.any((account) => isSameAccount(account.split(":").last, walletPublicKey)); + } + + static bool isSameAccount(String a, String b) { + if (a.startsWith("0x") && b.startsWith("0x")) { + return a.toLowerCase() == b.toLowerCase(); + } + + return a == b; + } + static const _transactionMethods = { 'eth_sendTransaction', 'eth_signTransaction', @@ -28,13 +46,19 @@ class MethodsUtils { String? method, String? chainId, String? address, + String? topic, required String transportType, List extraModels = const [], VerifyContext? verifyContext, }) async { final appStore = getIt.get(); - final pending = walletKit.pendingRequests.getAll(); - final session = pending.isNotEmpty ? walletKit.sessions.get(pending.last.topic) : null; + SessionData? session; + if (topic != null) { + session = walletKit.sessions.get(topic); + } else { + final pending = walletKit.pendingRequests.getAll(); + session = pending.isNotEmpty ? walletKit.sessions.get(pending.last.topic) : null; + } final dAppMetadata = session?.peer.metadata; final isTransaction = method != null && _transactionMethods.contains(method); diff --git a/lib/src/screens/wallet_connect/wc_connections_listing_view.dart b/lib/src/screens/wallet_connect/wc_connections_listing_view.dart index b422b801b..f688994ce 100644 --- a/lib/src/screens/wallet_connect/wc_connections_listing_view.dart +++ b/lib/src/screens/wallet_connect/wc_connections_listing_view.dart @@ -12,6 +12,7 @@ import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/permission_handler.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; +import "package:flutter/cupertino.dart"; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -145,6 +146,26 @@ class WalletConnectConnectionsView extends StatelessWidget { const SizedBox(height: 24), Observer( builder: (_) { + if (walletKitService.isLoadingConnections) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 32), + child: Center( + child: Column( + spacing: 12, + children: [ + const CupertinoActivityIndicator(), + Text( + "${S.of(context).loading}...", + style: theme.textTheme.bodyMedium?.copyWith( + color: colors.onSurfaceVariant, + ), + ), + ], + ), + ), + ); + } + if (walletKitService.pairings.isEmpty) { return Padding( padding: const EdgeInsets.symmetric(vertical: 32), diff --git a/lib/store/app_store.dart b/lib/store/app_store.dart index 85a9bbba5..706a9a0ea 100644 --- a/lib/store/app_store.dart +++ b/lib/store/app_store.dart @@ -1,5 +1,3 @@ -import "dart:async"; - import "package:cake_wallet/core/amount_parsing_proxy.dart"; import "package:cake_wallet/di.dart"; import "package:cake_wallet/entities/preferences_key.dart"; @@ -60,13 +58,17 @@ abstract class AppStoreBase with Store { Future changeCurrentWallet( WalletBase, TransactionInfo> wallet) async { final changingToSameWalletType = this.wallet?.type == wallet.type; + final previousWalletType = this.wallet?.type; await this.wallet?.close(shouldCleanup: !changingToSameWalletType); this.wallet = wallet; this.wallet!.setExceptionHandler(ExceptionHandler.onError); if (isWalletConnectCompatibleChain(wallet.type)) { - unawaited(_setupWalletConnect()); + getIt.get().resetConnectionsState(); + _queueWalletConnectAction(_setupWalletConnect); + } else if (previousWalletType != null && isWalletConnectCompatibleChain(previousWalletType)) { + _queueWalletConnectAction(_disposeWalletConnect); } await getIt.get().setString(PreferencesKey.currentWalletName, wallet.name); await getIt @@ -74,6 +76,12 @@ abstract class AppStoreBase with Store { .setInt(PreferencesKey.currentWalletType, serializeToInt(wallet.type)); } + Future _lastWalletConnectAction = Future.value(); + + void _queueWalletConnectAction(Future Function() action) { + _lastWalletConnectAction = _lastWalletConnectAction.then((_) => action()); + } + Future _setupWalletConnect() async { try { final wcService = getIt.get(); @@ -84,4 +92,12 @@ abstract class AppStoreBase with Store { printV("WalletConnect setup failed: $e\n$s"); } } + + Future _disposeWalletConnect() async { + try { + await getIt.get().onDispose(); + } catch (e) { + printV("WalletConnect teardown failed: $e"); + } + } } diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 24f501597..3b13b21dc 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -1421,6 +1421,7 @@ "wc_permission_sign_messages": "توقيع الرسائل والبيانات المكتوبة", "wc_permission_switch_chains": "قم بالتبديل وإضافة سلاسل EVM", "wc_permission_view_balance": "عرض رصيد محفظتك ونشاطك", + "wc_request_for_different_wallet": "تم تقديم هذا الطلب لمحفظة مختلفة. افتح المحفظة المتصلة بهذا التطبيق وحاول مرة أخرى.", "wc_scam_warning_message": "يبدو أن هذا الطلب مرسل من عملية احتيال معروفة. يرجى المضي قدما بحذر.", "wc_scam_warning_title": "تحذير!", "wc_scan_qr": "مسح QR", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index b443d1af8..c94868932 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -1421,6 +1421,7 @@ "wc_permission_sign_messages": "Подписвайте съобщения и въведени данни", "wc_permission_switch_chains": "Превключете и добавете EVM вериги", "wc_permission_view_balance": "Вижте баланса и активността на вашия портфейл", + "wc_request_for_different_wallet": "Тази заявка е направена за различен портфейл. Отворете портфейла, свързан с това приложение, и опитайте отново.", "wc_scam_warning_message": "Тази заявка изглежда е от известна измама. Моля, продължете с повишено внимание.", "wc_scam_warning_title": "ПРЕДУПРЕЖДЕНИЕ!", "wc_scan_qr": "Сканирайте QR", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index f62daa6d3..2e68008be 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -1421,6 +1421,7 @@ "wc_permission_sign_messages": "Podepisujte zprávy a zadaná data", "wc_permission_switch_chains": "Přepněte a přidejte řetězy EVM", "wc_permission_view_balance": "Prohlédněte si zůstatek a aktivitu v peněžence", + "wc_request_for_different_wallet": "Tento požadavek byl podán pro jinou peněženku. Otevřete peněženku připojenou k této aplikaci a zkuste to znovu.", "wc_scam_warning_message": "Zdá se, že tento požadavek pochází od známého podvodu. Postupujte prosím opatrně.", "wc_scam_warning_title": "VAROVÁNÍ!", "wc_scan_qr": "Naskenujte QR", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index dd7abc180..8044948e8 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -1460,6 +1460,7 @@ "wc_permission_sign_messages": "Nachrichten und strukturierte Daten signieren", "wc_permission_switch_chains": "EVM-Ketten wechseln und hinzufügen", "wc_permission_view_balance": "Kontostand und Aktivität Ihrer Wallet anzeigen", + "wc_request_for_different_wallet": "Diese Anfrage wurde für eine andere Wallet gestellt. Öffnen Sie die mit dieser App verbundene Wallet und versuchen Sie es erneut.", "wc_scam_warning_message": "Diese Anfrage scheint von einem bekannten Betrug zu stammen. Bitte gehen Sie vorsichtig vor.", "wc_scam_warning_title": "WARNUNG!", "wc_scan_qr": "QR scannen", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 9c04a21c4..b208f6836 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -1462,6 +1462,7 @@ "wc_permission_sign_messages": "Sign messages and typed data", "wc_permission_switch_chains": "Switch and add EVM chains", "wc_permission_view_balance": "View your wallet balance and activity", + "wc_request_for_different_wallet": "This request was made for a different wallet. Open the wallet connected to this app and try again.", "wc_scam_warning_message": "This request appears to be from a known scam. Please proceed with caution.", "wc_scam_warning_title": "WARNING!", "wc_scan_qr": "Scan QR", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 78321bd2c..ac702c0f6 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -1423,6 +1423,7 @@ "wc_permission_sign_messages": "Firmar mensajes y datos escritos", "wc_permission_switch_chains": "Cambie y agregue cadenas EVM", "wc_permission_view_balance": "Ver el saldo y la actividad de su billetera", + "wc_request_for_different_wallet": "Esta solicitud se realizó para una billetera diferente. Abra la billetera conectada a esta aplicación e inténtelo nuevamente.", "wc_scam_warning_message": "Esta solicitud parece provenir de una estafa conocida. Proceda con precaución.", "wc_scam_warning_title": "¡ADVERTENCIA!", "wc_scan_qr": "Escanear QR", diff --git a/res/values/strings_fa.arb b/res/values/strings_fa.arb index cf886a43b..17f55785a 100644 --- a/res/values/strings_fa.arb +++ b/res/values/strings_fa.arb @@ -1421,6 +1421,7 @@ "wc_permission_sign_messages": "پیام ها و داده های تایپ شده را امضا کنید", "wc_permission_switch_chains": "زنجیره های EVM را تغییر دهید و اضافه کنید", "wc_permission_view_balance": "موجودی و فعالیت کیف پول خود را مشاهده کنید", + "wc_request_for_different_wallet": "این درخواست برای یک کیف پول متفاوت انجام شده است. کیف پول متصل به این برنامه را باز کنید و دوباره امتحان کنید.", "wc_scam_warning_message": "به نظر می رسد این درخواست از یک کلاهبرداری شناخته شده باشد. لطفا با احتیاط ادامه دهید", "wc_scam_warning_title": "هشدار!", "wc_scan_qr": "اسکن QR", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index d3f135c88..a8c8df56a 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -1421,6 +1421,7 @@ "wc_permission_sign_messages": "Signer les messages et les données saisies", "wc_permission_switch_chains": "Changer et ajouter des chaînes EVM", "wc_permission_view_balance": "Consultez le solde et l'activité de votre portefeuille", + "wc_request_for_different_wallet": "Cette demande a été faite pour un autre portefeuille. Ouvrez le portefeuille connecté à cette application et réessayez.", "wc_scam_warning_message": "Cette demande semble provenir d'une arnaque connue. Veuillez procéder avec prudence.", "wc_scam_warning_title": "AVERTISSEMENT!", "wc_scan_qr": "Scanner le QR", diff --git a/res/values/strings_gn.arb b/res/values/strings_gn.arb index 551e16d42..c86512e7b 100644 --- a/res/values/strings_gn.arb +++ b/res/values/strings_gn.arb @@ -1428,6 +1428,7 @@ "wc_permission_sign_messages": "Marandu ofirma ha dato ojehaipyréva", "wc_permission_switch_chains": "Embohasa ha emoĩve umi cadena EVM rehegua", "wc_permission_view_balance": "Ehecha nde billetera saldo ha actividad", + "wc_request_for_different_wallet": "Ko mba’ejerure ojejapo peteĩ billetera iñambuéva rehe. Eipe’a pe billetera oñembojoajúva ko app-pe ha eñeha’ã jey.", "wc_scam_warning_message": "Ko pedido ojekuaa oúha peteî estafa ojekuaávagui. Por favor, peprocede ñeñangareko reheve.", "wc_scam_warning_title": "ÑEMONGYHYJE!", "wc_scan_qr": "Escanear QR", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index ea901d7c4..29cda58b1 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -1423,6 +1423,7 @@ "wc_permission_sign_messages": "Sa hannu kan saƙonni da rubutattun bayanai", "wc_permission_switch_chains": "Canja kuma ƙara sarƙoƙin EVM", "wc_permission_view_balance": "Duba ma'auni na walat ɗin ku da aiki", + "wc_request_for_different_wallet": "An yi wannan buƙatar don wani walat ɗin daban. Bude walat ɗin da aka haɗa zuwa wannan app ɗin kuma a sake gwadawa.", "wc_scam_warning_message": "Wannan buƙatar ta bayyana daga wata zamba da aka sani. Da fatan za a ci gaba da taka tsantsan.", "wc_scam_warning_title": "GARGADI!", "wc_scan_qr": "Duba QR", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 15edf0080..13d18ae19 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -1423,6 +1423,7 @@ "wc_permission_sign_messages": "संदेशों पर हस्ताक्षर करें और डेटा टाइप करें", "wc_permission_switch_chains": "स्विच करें और ईवीएम चेन जोड़ें", "wc_permission_view_balance": "अपना वॉलेट बैलेंस और गतिविधि देखें", + "wc_request_for_different_wallet": "यह अनुरोध एक अलग वॉलेट के लिए किया गया था. इस ऐप से जुड़ा वॉलेट खोलें और पुनः प्रयास करें।", "wc_scam_warning_message": "ऐसा प्रतीत होता है कि यह अनुरोध किसी ज्ञात घोटाले से आया है। कृपया सावधानी से आगे बढ़ें.", "wc_scam_warning_title": "चेतावनी!", "wc_scan_qr": "QR स्कैन करें", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 2d99234eb..0e4f55c43 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -1421,6 +1421,7 @@ "wc_permission_sign_messages": "Potpisivati ​​poruke i upisane podatke", "wc_permission_switch_chains": "Prebacite i dodajte EVM lance", "wc_permission_view_balance": "Pogledajte svoj novčanik i aktivnost", + "wc_request_for_different_wallet": "Ovaj je zahtjev upućen za drugi novčanik. Otvorite novčanik povezan s ovom aplikacijom i pokušajte ponovno.", "wc_scam_warning_message": "Čini se da ovaj zahtjev potječe iz poznate prijevare. Molimo nastavite s oprezom.", "wc_scam_warning_title": "UPOZORENJE!", "wc_scan_qr": "Skeniraj QR", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index f1a209c91..e45a24dce 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -1421,6 +1421,7 @@ "wc_permission_sign_messages": "Ստորագրեք հաղորդագրությունները և մուտքագրված տվյալները", "wc_permission_switch_chains": "Միացրեք և ավելացրեք EVM շղթաներ", "wc_permission_view_balance": "Դիտեք ձեր դրամապանակի մնացորդը և գործունեությունը", + "wc_request_for_different_wallet": "Այս հարցումն արվել է այլ դրամապանակի համար: Բացեք այս հավելվածին միացված դրամապանակը և նորից փորձեք:", "wc_scam_warning_message": "Այս հարցումը, կարծես, հայտնի խարդախությունից է: Խնդրում ենք շարունակել զգուշությամբ:", "wc_scam_warning_title": "ԶԳՈՒՇԱՑՈՒՄ.", "wc_scan_qr": "Սկանավորել QR", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index c1fea5b4d..478dc7d68 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -1424,6 +1424,7 @@ "wc_permission_sign_messages": "Menandatangani pesan dan mengetik data", "wc_permission_switch_chains": "Beralih dan tambahkan rantai EVM", "wc_permission_view_balance": "Lihat saldo dan aktivitas dompet Anda", + "wc_request_for_different_wallet": "Permintaan ini dibuat untuk dompet yang berbeda. Buka dompet yang terhubung ke aplikasi ini dan coba lagi.", "wc_scam_warning_message": "Permintaan ini tampaknya berasal dari penipuan yang diketahui. Silakan lanjutkan dengan hati-hati.", "wc_scam_warning_title": "PERINGATAN!", "wc_scan_qr": "Pindai QR", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index d7ae0b9c0..1fc6ff179 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -1422,6 +1422,7 @@ "wc_permission_sign_messages": "Firmare messaggi e dati digitati", "wc_permission_switch_chains": "Cambia e aggiungi catene EVM", "wc_permission_view_balance": "Visualizza il saldo e l'attività del tuo portafoglio", + "wc_request_for_different_wallet": "Questa richiesta è stata effettuata per un portafoglio diverso. Apri il portafoglio connesso a questa app e riprova.", "wc_scam_warning_message": "Questa richiesta sembra provenire da una truffa nota. Si prega di procedere con cautela.", "wc_scam_warning_title": "AVVERTIMENTO!", "wc_scan_qr": "Scansione QR", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index e9708e48d..b6836bf67 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -1421,6 +1421,7 @@ "wc_permission_sign_messages": "メッセージと入力されたデータに署名する", "wc_permission_switch_chains": "EVMチェーンの切り替えと追加", "wc_permission_view_balance": "ウォレットの残高とアクティビティを表示する", + "wc_request_for_different_wallet": "このリクエストは別のウォレットに対して行われました。このアプリに接続されているウォレットを開いて、もう一度お試しください。", "wc_scam_warning_message": "このリクエストは既知の詐欺によるものと思われます。慎重に進めてください。", "wc_scam_warning_title": "警告!", "wc_scan_qr": "QRをスキャン", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 35dd2e704..852be390a 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -1420,6 +1420,7 @@ "wc_permission_sign_messages": "메시지 및 입력된 데이터에 서명", "wc_permission_switch_chains": "EVM 체인 전환 및 추가", "wc_permission_view_balance": "지갑 잔액 및 활동 보기", + "wc_request_for_different_wallet": "이 요청은 다른 지갑에 대해 이루어졌습니다. 이 앱에 연결된 지갑을 열고 다시 시도해 보세요.", "wc_scam_warning_message": "이 요청은 알려진 사기에서 나온 것 같습니다. 주의해서 진행하시기 바랍니다.", "wc_scam_warning_title": "경고!", "wc_scan_qr": "QR 스캔", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 990a568e2..879e8382c 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -1420,6 +1420,7 @@ "wc_permission_sign_messages": "မက်ဆေ့ချ်များကို လက်မှတ်ထိုးပြီး ဒေတာကို ရိုက်ထည့်ပါ။", "wc_permission_switch_chains": "EVM chain များကို ပြောင်းပြီး ထည့်ပါ", "wc_permission_view_balance": "သင့်ပိုက်ဆံအိတ်လက်ကျန်ငွေနှင့် လုပ်ဆောင်ချက်ကို ကြည့်ရှုပါ။", + "wc_request_for_different_wallet": "ဤတောင်းဆိုချက်ကို မတူညီသော ပိုက်ဆံအိတ်အတွက် ပြုလုပ်ထားပါသည်။ ဤအက်ပ်နှင့်ချိတ်ဆက်ထားသော ပိုက်ဆံအိတ်ကိုဖွင့်ပြီး ထပ်စမ်းကြည့်ပါ။", "wc_scam_warning_message": "ဤတောင်းဆိုချက်သည် လူသိများသော လိမ်လည်မှုတစ်ခုမှ ဖြစ်ပုံရသည်။ ကျေးဇူးပြု၍ သတိဖြင့် ဆက်လုပ်ပါ။", "wc_scam_warning_title": "သတိပေးချက်။", "wc_scan_qr": "QR ကိုစကင်န်ဖတ်ပါ။", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 0671d7e14..9f7bc6d20 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -1421,6 +1421,7 @@ "wc_permission_sign_messages": "Onderteken berichten en getypte gegevens", "wc_permission_switch_chains": "Schakel over en voeg EVM-ketens toe", "wc_permission_view_balance": "Bekijk je walletsaldo en activiteit", + "wc_request_for_different_wallet": "Dit verzoek is gedaan voor een andere portemonnee. Open de portemonnee die met deze app is verbonden en probeer het opnieuw.", "wc_scam_warning_message": "Dit verzoek lijkt afkomstig te zijn van een bekende oplichting. Ga alstublieft voorzichtig te werk.", "wc_scam_warning_title": "WAARSCHUWING!", "wc_scan_qr": "QR scannen", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index e0de2a835..013896b11 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -1420,6 +1420,7 @@ "wc_permission_sign_messages": "Podpisuj wiadomości i wpisywane dane", "wc_permission_switch_chains": "Przełącz i dodaj łańcuchy EVM", "wc_permission_view_balance": "Sprawdź saldo i aktywność swojego portfela", + "wc_request_for_different_wallet": "To żądanie dotyczyło innego portfela. Otwórz portfel połączony z tą aplikacją i spróbuj ponownie.", "wc_scam_warning_message": "Wygląda na to, że to żądanie pochodzi ze znanego oszustwa. Proszę postępować ostrożnie.", "wc_scam_warning_title": "OSTRZEŻENIE!", "wc_scan_qr": "Zeskanuj kod QR", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index d9204e345..c3ab4bae7 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -1423,6 +1423,7 @@ "wc_permission_sign_messages": "Assinar mensagens e dados digitados", "wc_permission_switch_chains": "Alternar e adicionar cadeias EVM", "wc_permission_view_balance": "Veja o saldo e a atividade da sua carteira", + "wc_request_for_different_wallet": "Esta solicitação foi feita para uma carteira diferente. Abra a carteira conectada a este aplicativo e tente novamente.", "wc_scam_warning_message": "Esta solicitação parece ser de um golpe conhecido. Por favor, proceda com cautela.", "wc_scam_warning_title": "AVISO!", "wc_scan_qr": "Digitalize QR", diff --git a/res/values/strings_pt_BR.arb b/res/values/strings_pt_BR.arb index d703a3ec8..c552992e0 100644 --- a/res/values/strings_pt_BR.arb +++ b/res/values/strings_pt_BR.arb @@ -1361,6 +1361,7 @@ "wc_permission_sign_messages": "Assinar mensagens e dados digitados", "wc_permission_switch_chains": "Alternar e adicionar cadeias EVM", "wc_permission_view_balance": "Veja o saldo e a atividade da sua carteira", + "wc_request_for_different_wallet": "Esta solicitação foi feita para uma carteira diferente. Abra a carteira conectada a este aplicativo e tente novamente.", "wc_scam_warning_message": "Esta solicitação parece ser de um golpe conhecido. Por favor, proceda com cautela.", "wc_scam_warning_title": "AVISO!", "wc_scan_qr": "Digitalize QR", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index aeb56f31c..ca12a089d 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -1421,6 +1421,7 @@ "wc_permission_sign_messages": "Подписывать сообщения и вводимые данные", "wc_permission_switch_chains": "Переключение и добавление цепочек EVM", "wc_permission_view_balance": "Просматривайте баланс и активность своего кошелька", + "wc_request_for_different_wallet": "Этот запрос был сделан для другого кошелька. Откройте кошелек, подключенный к этому приложению, и повторите попытку.", "wc_scam_warning_message": "Судя по всему, этот запрос поступил от известного мошенника. Пожалуйста, действуйте осторожно.", "wc_scam_warning_title": "ПРЕДУПРЕЖДЕНИЕ!", "wc_scan_qr": "Сканировать QR-код", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 44096cbd1..818a8f6c1 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -1420,6 +1420,7 @@ "wc_permission_sign_messages": "เซ็นข้อความและพิมพ์ข้อมูล", "wc_permission_switch_chains": "สลับและเพิ่มเชน EVM", "wc_permission_view_balance": "ดูยอดคงเหลือในกระเป๋าสตางค์และกิจกรรมของคุณ", + "wc_request_for_different_wallet": "คำขอนี้สร้างขึ้นสำหรับกระเป๋าเงินอื่น เปิดกระเป๋าเงินที่เชื่อมต่อกับแอพนี้แล้วลองอีกครั้ง", "wc_scam_warning_message": "ดูเหมือนว่าคำขอนี้มาจากกลโกงที่ทราบ โปรดดำเนินการด้วยความระมัดระวัง", "wc_scam_warning_title": "คำเตือน!", "wc_scan_qr": "สแกนคิวอาร์", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index bdb4d46a6..374cb1616 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -1420,6 +1420,7 @@ "wc_permission_sign_messages": "Mag-sign ng mga mensahe at nai-type na data", "wc_permission_switch_chains": "Lumipat at magdagdag ng mga EVM chain", "wc_permission_view_balance": "Tingnan ang balanse at aktibidad ng iyong wallet", + "wc_request_for_different_wallet": "Ang kahilingang ito ay ginawa para sa ibang wallet. Buksan ang wallet na nakakonekta sa app na ito at subukang muli.", "wc_scam_warning_message": "Mukhang mula sa isang kilalang scam ang kahilingang ito. Mangyaring magpatuloy nang may pag-iingat.", "wc_scam_warning_title": "BABALA!", "wc_scan_qr": "I-scan ang QR", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 67684f6df..82b27c80f 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -1420,6 +1420,7 @@ "wc_permission_sign_messages": "Mesajları ve yazılan verileri imzalayın", "wc_permission_switch_chains": "EVM zincirlerini değiştirin ve ekleyin", "wc_permission_view_balance": "Cüzdan bakiyenizi ve etkinliğinizi görüntüleyin", + "wc_request_for_different_wallet": "Bu istek farklı bir cüzdan için yapıldı. Bu uygulamaya bağlı cüzdanı açın ve tekrar deneyin.", "wc_scam_warning_message": "Bu isteğin bilinen bir dolandırıcılıktan geldiği anlaşılıyor. Lütfen dikkatli bir şekilde ilerleyin.", "wc_scam_warning_title": "UYARI!", "wc_scan_qr": "QR'yi tara", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 14fa38f59..6558649ff 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -1421,6 +1421,7 @@ "wc_permission_sign_messages": "Підписуйте повідомлення та введені дані", "wc_permission_switch_chains": "Перемикайте та додавайте ланцюжки EVM", "wc_permission_view_balance": "Перегляньте свій баланс гаманця та активність", + "wc_request_for_different_wallet": "Цей запит було зроблено для іншого гаманця. Відкрийте гаманець, підключений до цієї програми, і повторіть спробу.", "wc_scam_warning_message": "Схоже, цей запит надійшов від відомого шахрая. Будь ласка, будьте обережні.", "wc_scam_warning_title": "УВАГА!", "wc_scan_qr": "Сканувати QR", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index ba77da2bf..f241478ab 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -1422,6 +1422,7 @@ "wc_permission_sign_messages": "پیغامات پر دستخط کریں اور ڈیٹا ٹائپ کریں۔", "wc_permission_switch_chains": "ای وی ایم چینز کو تبدیل کریں اور شامل کریں۔", "wc_permission_view_balance": "اپنے والیٹ کا بیلنس اور سرگرمی دیکھیں", + "wc_request_for_different_wallet": "یہ درخواست ایک مختلف بٹوے کے لیے کی گئی تھی۔ اس ایپ سے منسلک والیٹ کھولیں اور دوبارہ کوشش کریں۔", "wc_scam_warning_message": "یہ درخواست کسی معروف اسکام کی طرف سے معلوم ہوتی ہے۔ براہ کرم احتیاط کے ساتھ آگے بڑھیں۔", "wc_scam_warning_title": "وارننگ!", "wc_scan_qr": "QR اسکین کریں۔", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 41b269d47..594657af9 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -1421,6 +1421,7 @@ "wc_permission_sign_messages": "Ký tin nhắn và dữ liệu có cấu trúc", "wc_permission_switch_chains": "Chuyển đổi và thêm chuỗi EVM", "wc_permission_view_balance": "Xem số dư và hoạt động ví của bạn", + "wc_request_for_different_wallet": "Yêu cầu này được thực hiện cho một ví khác. Hãy mở ví được kết nối với ứng dụng này và thử lại.", "wc_scam_warning_message": "Yêu cầu này có vẻ là từ một trò lừa đảo đã biết. Hãy tiến hành thận trọng.", "wc_scam_warning_title": "CẢNH BÁO!", "wc_scan_qr": "Quét QR", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 15f858b93..def4de265 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -1421,6 +1421,7 @@ "wc_permission_sign_messages": "Wole awọn ifiranṣẹ ati titẹ data", "wc_permission_switch_chains": "Yipada ki o si fi EVM dè", "wc_permission_view_balance": "Wo iwọntunwọnsi apamọwọ rẹ ati iṣẹ ṣiṣe", + "wc_request_for_different_wallet": "A ṣe ibeere yii fun apamọwọ ti o yatọ. Ṣii apamọwọ ti a ti sopọ si app yii ki o tun gbiyanju lẹẹkansi.", "wc_scam_warning_message": "Ibeere yii han lati wa lati itanjẹ ti a mọ. Jọwọ tẹsiwaju pẹlu iṣọra.", "wc_scam_warning_title": "IKILO!", "wc_scan_qr": "Ṣayẹwo QR", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 711735023..91e885810 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -1421,6 +1421,7 @@ "wc_permission_sign_messages": "签署消息和键入的数据", "wc_permission_switch_chains": "切换并添加EVM链", "wc_permission_view_balance": "查看您的钱包余额和活动", + "wc_request_for_different_wallet": "此请求是针对不同的钱包提出的。打开连接到该应用程序的钱包并重试。", "wc_scam_warning_message": "此请求似乎来自一个已知的骗局。请谨慎行事。", "wc_scam_warning_title": "警告!", "wc_scan_qr": "扫描二维码", From 3ba43ede62666f8d3640b5041edc8daa6ba35e7c Mon Sep 17 00:00:00 2001 From: cyan Date: Thu, 27 Aug 2026 05:58:22 +0300 Subject: [PATCH 15/15] Cyjan zec mutex 2 (#3569) * fix: mutex on zec * refactor: simplify orchard migratable balance checks and update related methods --- cw_zcash/lib/src/zcash_wallet.dart | 36 ++++++++----------- .../dashboard/dashboard_view_model.dart | 26 ++------------ lib/zcash/cw_zcash.dart | 2 +- tool/configure.dart | 2 +- 4 files changed, 19 insertions(+), 47 deletions(-) diff --git a/cw_zcash/lib/src/zcash_wallet.dart b/cw_zcash/lib/src/zcash_wallet.dart index 14cdf8f66..0a6e192d8 100644 --- a/cw_zcash/lib/src/zcash_wallet.dart +++ b/cw_zcash/lib/src/zcash_wallet.dart @@ -81,6 +81,9 @@ abstract class ZcashWalletBase CryptoCurrency.zec: ZcashBalance.zero(), }); + @observable + bool _orchardMigratable = false; + static const int _autoShieldMinSweep = 30000; // zkool's migrate::MIN_SD. @@ -1251,22 +1254,7 @@ abstract class ZcashWalletBase return migratable; } - Future hasOrchardMigratableBalance() async { - - final (active, migratableOrchard) = await runWithCoin( - accountId: accountId, - func: (final coin) async => ( - await zkool_network.isIronwoodActive(c: coin), - await _migratableOrchardTotal(coin), - ), - ); - - if(!active) { - return false; - } - - return migratableOrchard > BigInt.zero; - } + bool hasOrchardMigratableBalance() => _orchardMigratable; Future _$autoShield() async { if (syncStatus is! SyncedSyncStatus) { @@ -1449,11 +1437,15 @@ abstract class ZcashWalletBase unavailableAmount = sweepable <= BigInt.from(minSweep) ? BigInt.zero : sweepable; } - balance[currency] = ZcashBalance( - Money(availableAmount, currency), - Money(unavailableAmount, currency), - frozen: Money.zero(currency), - ); + runInAction(() { + _orchardMigratable = + ironwoodActive == true && migratableOrchard > BigInt.zero; + balance[currency] = ZcashBalance( + Money(availableAmount, currency), + Money(unavailableAmount, currency), + frozen: Money.zero(currency), + ); + }); } catch (e, stackTrace) { printV("Balance update error: $e"); printV("Stack trace: $stackTrace"); @@ -1784,6 +1776,7 @@ abstract class ZcashWalletBase required final int accountId, required final FutureOr Function(zkool_coin.Coin c) func, }) async { + await runWithCoinMutex.acquire(); var newC = zkool_coin.Coin(); newC = await newC.openDatabase(dbFilepath: c.dbFilepath); newC = await newC.setAccount(account: accountId); @@ -1792,7 +1785,6 @@ abstract class ZcashWalletBase runWithCoinCount++; printV("run with coin: $runWithCoinCount"); - await runWithCoinMutex.acquire(); try { newC = await newC.setAccount(account: accountId); return await func(newC); diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index e74771b1b..994121a90 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -96,7 +96,6 @@ abstract class DashboardViewModelBase with Store { isShowFirstYatIntroduction = false, isShowSecondYatIntroduction = false, isShowThirdYatIntroduction = false, - isMigratingToIronwood = false, filterItems = [], exchangeFilterItems = [], subname = '', @@ -251,8 +250,6 @@ abstract class DashboardViewModelBase with Store { reaction((_) => tradesStore.trades, (_) => tradeMonitor.monitorActiveTrades(wallet.id)); - addZcashMigrationReaction(); - tradeMonitor.monitorActiveTrades(wallet.id); } @@ -377,21 +374,9 @@ abstract class DashboardViewModelBase with Store { ); } - @observable - ReactionDisposer? zcashMigrationReactionDisposer; - - @action - void addZcashMigrationReaction() { - zcashMigrationReactionDisposer?.reaction.dispose(); - zcashMigrationReactionDisposer = null; - - if (wallet.type == WalletType.zcash) { - zcashMigrationReactionDisposer = reaction((_) => wallet.balance.values.first, (_) async { - isMigratingToIronwood = - wallet.type == WalletType.zcash && await zcash!.hasOrchardMigratableBalance(wallet); - }); - } - } + @computed + bool get isMigratingToIronwood => + wallet.type == WalletType.zcash && (zcash?.hasOrchardMigratableBalance(wallet) ?? false); @computed bool get isSyncHeavy { @@ -1289,8 +1274,6 @@ abstract class DashboardViewModelBase with Store { name = wallet.name; loadFilterItems(); - addZcashMigrationReaction(); - if (wallet.type == WalletType.monero) { subname = monero!.getCurrentAccount(wallet).label; @@ -1572,9 +1555,6 @@ abstract class DashboardViewModelBase with Store { } } - @observable - bool isMigratingToIronwood; - String getTransactionType(TransactionInfo tx) { if (wallet.type == WalletType.bitcoin) { if (tx.isReplaced == true) return ' (replaced)'; diff --git a/lib/zcash/cw_zcash.dart b/lib/zcash/cw_zcash.dart index 8abef07dc..525dac193 100644 --- a/lib/zcash/cw_zcash.dart +++ b/lib/zcash/cw_zcash.dart @@ -223,7 +223,7 @@ class CWZcash extends Zcash { } @override - Future hasOrchardMigratableBalance(WalletBase wallet) { + bool hasOrchardMigratableBalance(WalletBase wallet) { return (wallet as ZcashWallet).hasOrchardMigratableBalance(); } } diff --git a/tool/configure.dart b/tool/configure.dart index e3f5f385c..a741c2024 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -1777,7 +1777,7 @@ abstract class Zcash { bool showMissingFundsCard(WalletBase wallet); Future rescanInternalChange(WalletBase wallet); bool ironwoodActive(WalletAddresses walletAddresses); - Future hasOrchardMigratableBalance(WalletBase wallet); + bool hasOrchardMigratableBalance(WalletBase wallet); } """;