From 993dea82a929d14a141287aeb41ec44042ee10a6 Mon Sep 17 00:00:00 2001 From: Serhii Date: Thu, 20 Feb 2025 18:53:18 +0200 Subject: [PATCH 001/243] replace connectMapToListWithTransform with reaction --- .../dashboard/dashboard_view_model.dart | 55 ++++++++++--------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 99283a409..7cc26b33e 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -578,6 +578,8 @@ abstract class DashboardViewModelBase with Store { ReactionDisposer? _onMoneroBalanceChangeReaction; + ReactionDisposer? _transactionDisposer; + @computed bool get hasPowNodes => [WalletType.nano, WalletType.banano].contains(wallet.type); @@ -682,32 +684,35 @@ abstract class DashboardViewModelBase with Store { ); } - connectMapToListWithTransform( - appStore.wallet!.transactionHistory.transactions, - transactions, - (TransactionInfo? transaction) => TransactionListItem( - transaction: transaction!, - balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore, - key: ValueKey( - '${wallet.type.name}_transaction_history_item_${transaction.id}_key', + _transactionDisposer?.reaction.dispose(); + + _transactionDisposer = reaction( + (_) => appStore.wallet!.transactionHistory.transactions.values.toList(), + (List txs) { + // Clear existing transactions + transactions.clear(); + + // Apply filtering before adding to the list + transactions.addAll( + txs.where((tx) { + if (wallet.type == WalletType.monero) { + return monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id; + } + if (wallet.type == WalletType.wownero) { + return wow.wownero!.getTransactionInfoAccountId(tx) == wow.wownero!.getCurrentAccount(wallet).id; + } + return true; // Include other wallet types without filtering + }).map( + (tx) => TransactionListItem( + transaction: tx, + balanceViewModel: balanceViewModel, + settingsStore: appStore.settingsStore, + key: ValueKey('${wallet.type.name}_transaction_history_item_${tx.id}_key'), ), - ), filter: (TransactionInfo? tx) { - if (tx == null) { - return false; - } - - if (wallet.type == WalletType.monero) { - return monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id; - } - - if (wallet.type == WalletType.wownero) { - return wow.wownero!.getTransactionInfoAccountId(tx) == - wow.wownero!.getCurrentAccount(wallet).id; - } - - return true; - }); + ), + ); + } + ); } @action From 068d6916b977b0500a06fc4577113764a38f8377 Mon Sep 17 00:00:00 2001 From: Serhii Date: Thu, 20 Feb 2025 19:19:01 +0200 Subject: [PATCH 002/243] minor fixes --- lib/view_model/dashboard/dashboard_view_model.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 7cc26b33e..2834be0e1 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -689,10 +689,9 @@ abstract class DashboardViewModelBase with Store { _transactionDisposer = reaction( (_) => appStore.wallet!.transactionHistory.transactions.values.toList(), (List txs) { - // Clear existing transactions - transactions.clear(); - // Apply filtering before adding to the list + transactions.clear(); + transactions.addAll( txs.where((tx) { if (wallet.type == WalletType.monero) { @@ -701,7 +700,7 @@ abstract class DashboardViewModelBase with Store { if (wallet.type == WalletType.wownero) { return wow.wownero!.getTransactionInfoAccountId(tx) == wow.wownero!.getCurrentAccount(wallet).id; } - return true; // Include other wallet types without filtering + return true; }).map( (tx) => TransactionListItem( transaction: tx, From 37555a832055a78c5937ebbe3110057f730caebb Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Sat, 22 Feb 2025 03:06:30 +0200 Subject: [PATCH 003/243] minor fix for tablet Android --- .../desktop_settings_page.dart | 4 ++-- lib/src/widgets/setting_actions.dart | 18 ++++-------------- scripts/ios/app_env.sh | 2 +- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart b/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart index 83139ec97..bbcb80791 100644 --- a/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart +++ b/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart @@ -20,7 +20,7 @@ class DesktopSettingsPage extends StatefulWidget { } class _DesktopSettingsPageState extends State { - final int itemCount = SettingActions.desktopSettings.length; + final int itemCount = SettingActions.all.length; int? currentPage; @@ -54,7 +54,7 @@ class _DesktopSettingsPageState extends State { child: ListView.separated( padding: EdgeInsets.only(top: 0), itemBuilder: (_, index) { - final item = SettingActions.desktopSettings[index]; + final item = SettingActions.all[index]; if (!widget.dashboardViewModel.hasSilentPayments && item.name(context) == S.of(context).silent_payments_settings) { diff --git a/lib/src/widgets/setting_actions.dart b/lib/src/widgets/setting_actions.dart index c5938b746..d383129cc 100644 --- a/lib/src/widgets/setting_actions.dart +++ b/lib/src/widgets/setting_actions.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:flutter/material.dart'; @@ -20,20 +22,8 @@ class SettingActions { walletSettingAction, addressBookSettingAction, silentPaymentsSettingAction, - litecoinMwebSettingAction, - exportOutputsAction, - securityBackupSettingAction, - privacySettingAction, - displaySettingAction, - otherSettingAction, - supportSettingAction, - ]; - - static List desktopSettings = [ - connectionSettingAction, - walletSettingAction, - addressBookSettingAction, - silentPaymentsSettingAction, + if (Platform.isIOS || Platform.isAndroid) litecoinMwebSettingAction, + if (Platform.isIOS || Platform.isAndroid) exportOutputsAction, securityBackupSettingAction, privacySettingAction, displaySettingAction, diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 29599ede3..c6d3778f3 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -19,7 +19,7 @@ MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.23.2" -CAKEWALLET_BUILD_NUMBER=296 +CAKEWALLET_BUILD_NUMBER=298 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" From d1de481558350d5807a4e4e60c9b6b8d36cbc79a Mon Sep 17 00:00:00 2001 From: tuxsudo Date: Mon, 24 Feb 2025 16:46:12 -0500 Subject: [PATCH 004/243] Update PIN page (#2051) * Update PIN page * Update spacers --- lib/src/screens/pin_code/pin_code_widget.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/src/screens/pin_code/pin_code_widget.dart b/lib/src/screens/pin_code/pin_code_widget.dart index d39c88cc9..dcd875d7a 100644 --- a/lib/src/screens/pin_code/pin_code_widget.dart +++ b/lib/src/screens/pin_code/pin_code_widget.dart @@ -136,7 +136,7 @@ class PinCodeState extends State { }, child: Container( color: Theme.of(context).colorScheme.background, - padding: EdgeInsets.only(left: 40.0, right: 40.0, bottom: 40.0), + padding: EdgeInsets.only(left: 40.0, right: 40.0, bottom: 60.0), child: Column( children: [ Spacer(flex: 2), @@ -146,7 +146,7 @@ class PinCodeState extends State { fontWeight: FontWeight.w500, color: Theme.of(context).extension()!.titleColor)), - Spacer(flex: 3), + Spacer(flex: 8), Container( width: 180, child: Row( @@ -168,7 +168,7 @@ class PinCodeState extends State { }), ), ), - Spacer(flex: 2), + Spacer(flex: 3), if (widget.hasLengthSwitcher) ...[ TextButton( onPressed: () { @@ -204,8 +204,8 @@ class PinCodeState extends State { childAspectRatio: _aspectRatio, physics: const NeverScrollableScrollPhysics(), children: List.generate(12, (index) { - const double marginRight = 15; - const double marginLeft = 15; + const double marginRight = 8; + const double marginLeft = 8; if (index == 9) { // Empty container @@ -243,12 +243,12 @@ class PinCodeState extends State { key: ValueKey('pin_code_button_${index}_key'), onPressed: () => _push(index), style: TextButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: Theme.of(context).cardColor, shape: CircleBorder(), ), child: Text('$index', style: TextStyle( - fontSize: 30.0, + fontSize: 25.0, fontWeight: FontWeight.w600, color: Theme.of(context).extension()!.titleColor)), ), From 465ff7e78ca493c6775ad51ee66f10f9aef80e61 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Mon, 24 Feb 2025 23:01:58 +0100 Subject: [PATCH 005/243] feat: Add app-version to Chatwoot Contact Attributes (#2049) --- lib/di.dart | 4 +- .../support_chat/support_chat_page.dart | 11 ++-- .../support_chat/widgets/chatwoot_widget.dart | 51 +++++++++++++------ lib/view_model/support_view_model.dart | 10 +++- 4 files changed, 50 insertions(+), 26 deletions(-) diff --git a/lib/di.dart b/lib/di.dart index 9f2256304..3e10bd7a1 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1256,7 +1256,7 @@ Future setup({ getIt.registerFactoryParam( (Order order, _) => OrderDetailsPage(getIt.get(param1: order))); - getIt.registerFactory(() => SupportViewModel()); + getIt.registerFactory(() => SupportViewModel(getIt.get())); getIt.registerFactory(() => SupportPage(getIt.get())); @@ -1418,4 +1418,4 @@ Future setup({ getIt.registerFactory(() => SeedVerificationPage(getIt.get())); _isSetupFinished = true; -} \ No newline at end of file +} diff --git a/lib/src/screens/support_chat/support_chat_page.dart b/lib/src/screens/support_chat/support_chat_page.dart index 203b75775..b66af5a59 100644 --- a/lib/src/screens/support_chat/support_chat_page.dart +++ b/lib/src/screens/support_chat/support_chat_page.dart @@ -3,10 +3,8 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/support_chat/widgets/chatwoot_widget.dart'; import 'package:cake_wallet/view_model/support_view_model.dart'; -import 'package:cw_core/utils/print_verbose.dart'; import 'package:flutter/material.dart'; - class SupportChatPage extends BasePage { SupportChatPage(this.supportViewModel, {required this.secureStorage}); @@ -23,17 +21,16 @@ class SupportChatPage extends BasePage { Widget body(BuildContext context) => FutureBuilder( future: getCookie(), builder: (BuildContext context, AsyncSnapshot snapshot) { - printV(snapshot.data); if (snapshot.hasData) return ChatwootWidget( secureStorage, - supportUrl: supportViewModel.fetchUrl(authToken: snapshot.data!) + supportUrl: supportViewModel.fetchUrl(authToken: snapshot.data!), + appVersion: supportViewModel.appVersion, ); return Container(); }, ); - Future getCookie() async { - return await secureStorage.read(key: COOKIE_KEY) ?? ""; - } + Future getCookie() async => + await secureStorage.read(key: COOKIE_KEY) ?? ""; } diff --git a/lib/src/screens/support_chat/widgets/chatwoot_widget.dart b/lib/src/screens/support_chat/widgets/chatwoot_widget.dart index 4f5e58c97..3b9c3301a 100644 --- a/lib/src/screens/support_chat/widgets/chatwoot_widget.dart +++ b/lib/src/screens/support_chat/widgets/chatwoot_widget.dart @@ -1,46 +1,50 @@ import 'dart:convert'; import 'package:cake_wallet/core/secure_storage.dart'; -import 'package:cw_core/utils/print_verbose.dart'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; const COOKIE_KEY = 'chatwootCookie'; class ChatwootWidget extends StatefulWidget { - ChatwootWidget(this.secureStorage, {required this.supportUrl}); + const ChatwootWidget( + this.secureStorage, { + required this.supportUrl, + required this.appVersion, + }); final SecureStorage secureStorage; final String supportUrl; + final String appVersion; @override ChatwootWidgetState createState() => ChatwootWidgetState(); } class ChatwootWidgetState extends State { - final GlobalKey _webViewkey = GlobalKey(); + final GlobalKey _webViewKey = GlobalKey(); @override Widget build(BuildContext context) => InAppWebView( - key: _webViewkey, - initialSettings: InAppWebViewSettings( - transparentBackground: true, - ), + key: _webViewKey, + initialSettings: InAppWebViewSettings(transparentBackground: true), initialUrlRequest: URLRequest(url: WebUri(widget.supportUrl)), onWebViewCreated: (InAppWebViewController controller) { controller.addWebMessageListener( WebMessageListener( jsObjectName: 'ReactNativeWebView', - onPostMessage: (WebMessage? message, WebUri? sourceOrigin, bool isMainFrame, - PlatformJavaScriptReplyProxy replyProxy) { + onPostMessage: (WebMessage? message, WebUri? sourceOrigin, + bool isMainFrame, PlatformJavaScriptReplyProxy replyProxy) { final shortenedMessage = message?.data.toString().substring(16); - if (shortenedMessage != null && isJsonString(shortenedMessage)) { + if (shortenedMessage != null && + _isJsonString(shortenedMessage)) { final parsedMessage = jsonDecode(shortenedMessage); final eventType = parsedMessage["event"]; if (eventType == 'loaded') { final authToken = parsedMessage["config"]["authToken"]; - printV(authToken); - storeCookie(authToken as String); + _storeCookie(authToken as String); + _setCustomAttributes( + controller, {"app_version": widget.appVersion}); } } }, @@ -49,7 +53,7 @@ class ChatwootWidgetState extends State { }, ); - bool isJsonString(String str) { + bool _isJsonString(String str) { try { jsonDecode(str); } catch (e) { @@ -58,7 +62,24 @@ class ChatwootWidgetState extends State { return true; } - Future storeCookie(String value) async { - await widget.secureStorage.write(key: COOKIE_KEY, value: value); + /// Add additional contact attributes to the chatwoot chat. + /// IMPORTANT: You have to add the attribute key in the chatwoot settings + /// under: settings/custom-attributes + Future _setCustomAttributes( + InAppWebViewController controller, + Map customAttributes, + ) { + final attributeObject = { + "event": "set-custom-attributes", + "customAttributes": customAttributes, + }; + return controller.postWebMessage( + message: WebMessage( + data: "chatwoot-widget:${jsonEncode(attributeObject)}", + ), + ); } + + Future _storeCookie(String value) => + widget.secureStorage.write(key: COOKIE_KEY, value: value); } diff --git a/lib/view_model/support_view_model.dart b/lib/view_model/support_view_model.dart index d48b182b1..766bcc9a2 100644 --- a/lib/view_model/support_view_model.dart +++ b/lib/view_model/support_view_model.dart @@ -1,16 +1,19 @@ +import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/view_model/settings/link_list_item.dart'; import 'package:cake_wallet/view_model/settings/settings_list_item.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/.secrets.g.dart' as secrets; part 'support_view_model.g.dart'; class SupportViewModel = SupportViewModelBase with _$SupportViewModel; abstract class SupportViewModelBase with Store { - SupportViewModelBase() + final SettingsStore settingsStore; + + SupportViewModelBase(this.settingsStore) : items = [ LinkListItem( title: 'Email', @@ -116,5 +119,8 @@ abstract class SupportViewModelBase with Store { return supportUrl; } + String get appVersion => + "${isMoneroOnly ? "Monero.com" : "Cake Wallet"} - ${settingsStore.appVersion}"; + List items; } From 67f0a463d1daef6ee92e1d3b05cbc78ab0b94853 Mon Sep 17 00:00:00 2001 From: Matteo Salonia Date: Tue, 25 Feb 2025 00:28:53 +0100 Subject: [PATCH 006/243] Update strings_it.arb (#2046) Fix many translation errors --- res/values/strings_it.arb | 529 +++++++++++++++++++------------------- 1 file changed, 264 insertions(+), 265 deletions(-) diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 4e9972f3f..d894f8ac4 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -25,31 +25,31 @@ "address_book": "Rubrica indirizzi", "address_book_menu": "Rubrica indirizzi", "address_detected": "Indirizzo rilevato", - "address_from_domain": "Questo indirizzo è da ${domain} in poi Unstoppable Domains", - "address_from_yat": "Questo indirizzo è da ${emoji} in poi Yat", - "address_label": "Address label", + "address_from_domain": "Questo indirizzo è da ${domain} su Unstoppable Domains", + "address_from_yat": "Questo indirizzo è da ${emoji} su Yat", + "address_label": "Etichetta indirizzo", "address_remove_contact": "Rimuovi contatto", "address_remove_content": "Sei sicuro di voler eliminare il contatto selezionato?", "addresses": "Indirizzi", "advanced_settings": "Impostazioni avanzate", - "aggressive": "Fervente", - "agree": "d'accordo", + "aggressive": "Aggressivo", + "agree": "Conferma", "agree_and_continue": "Accetta e continua", "agree_to": "Creando un account accetti il ​​", "alert_notice": "Avviso", "all": "TUTTO", "all_coins": "Tutte le monete", - "all_trades": "Svi obrti", - "all_transactions": "Sve transakcije", + "all_trades": "Tutti gli scambi", + "all_transactions": "Tutte le transazioni", "alphabetical": "Alfabetico", "already_have_account": "Hai già un account?", - "always": "sempre", - "amount": "Ammontare: ", + "always": "Sempre", + "amount": "Importo: ", "amount_is_below_minimum_limit": "Il saldo dopo le commissioni sarebbe inferiore all'importo minimo necessario per lo scambio (${min})", - "amount_is_estimate": "L'ammontare da ricevere è una stima", - "amount_is_guaranteed": "L'ammontare da ricevere è fisso", + "amount_is_estimate": "L'importo da ricevere è una stima", + "amount_is_guaranteed": "L'importo da ricevere è fisso", "and": "e", - "anonpay_description": "Genera ${type}. Il destinatario può ${method} con qualsiasi criptovaluta supportata e riceverai fondi in questo portafoglio.", + "anonpay_description": "Genera ${type}. Il destinatario può ${method} con qualsiasi criptovaluta supportata, e riceverai fondi in questo portafoglio.", "apk_update": "Aggiornamento APK", "approve": "Approvare", "arrive_in_this_address": "${currency} ${tag}arriverà a questo indirizzo", @@ -61,11 +61,11 @@ "auth_store_incorrect_password": "PIN non corretto", "authenticated": "Autenticato", "authentication": "Autenticazione", - "auto_generate_addresses": "Auto Genera indirizzi", + "auto_generate_addresses": "Genera automaticamente indirizzi", "auto_generate_subaddresses": "Genera automaticamente sottindirizzi", "automatic": "Automatico", "available_balance": "Saldo Disponibile", - "available_balance_description": "Il saldo disponibile è il saldo totale meno i fondi congelati. I fondi congelati sono fondi che sono stati inviati ma non sono ancora stati confermati.", + "available_balance_description": "I “Saldo Disponibile” o “Saldo Confermato” sono fondi che possono essere spesi immediatamente. Se i fondi appaiono nel saldo sottostante ma non nel saldo soprastante, allora aspetta un paio di minuti per i fondi in arrivo per ottenere maggiori conferme di rete. Dopo aver ottenuto più conferme, saranno spendibili.", "avg_savings": "Risparmio medio", "awaitDAppProcessing": "Attendi gentilmente che la dApp termini l'elaborazione.", "awaiting_payment_confirmation": "In attesa di conferma del pagamento", @@ -73,38 +73,38 @@ "backup": "Backup", "backup_file": "Backup file", "backup_password": "Backup password", - "balance": "Bilancia", - "balance_page": "Pagina di equilibrio", - "bill_amount": "Importo della fattura", + "balance": "Saldo", + "balance_page": "Pagina di saldo", + "bill_amount": "Importo della ricevuta", "billing_address_info": "Se ti viene richiesto un indirizzo di fatturazione, fornisci il tuo indirizzo di spedizione", "biometric_auth_reason": "Scansiona la tua impronta per autenticarti", - "bitcoin_dark_theme": "Tema oscuro di Bitcoin", - "bitcoin_light_theme": "Tema luce Bitcoin", + "bitcoin_dark_theme": "Tema scuro Bitcoin", + "bitcoin_light_theme": "Tema chiaro Bitcoin", "bitcoin_payments_require_1_confirmation": "I pagamenti in bitcoin richiedono 1 conferma, che può richiedere 20 minuti o più. Grazie per la vostra pazienza! Riceverai un'e-mail quando il pagamento sarà confermato.", "block_height": "Altezza del blocco", "block_remaining": "1 blocco rimanente", "Blocks_remaining": "${status} Blocchi Rimanenti", "bluetooth": "Bluetooth", - "bright_theme": "Colorato", + "bright_theme": "Chiaro", "bump_fee": "Commissione per bump", - "buy": "Comprare", + "buy": "Compra", "buy_alert_content": "Attualmente supportiamo solo l'acquisto di Bitcoin, Ethereum, Litecoin e Monero. Crea o passa al tuo portafoglio Bitcoin, Ethereum, Litecoin o Monero.", "buy_bitcoin": "Acquista Bitcoin", "buy_now": "Acquista ora", "buy_provider_unavailable": "Provider attualmente non disponibile.", "buy_with": "Acquista con", "by_cake_pay": "da Cake Pay", - "cake_2fa_preset": "Torta 2FA Preset", - "cake_dark_theme": "Tema oscuro della torta", - "cake_pay_account_note": "Iscriviti con solo un indirizzo email per vedere e acquistare le carte. Alcune sono anche disponibili con uno sconto!", + "cake_2fa_preset": "Preset Cake 2FA", + "cake_dark_theme": "Tema scuro Cake", + "cake_pay_account_note": "Iscriviti solamente con un indirizzo email per vedere e acquistare le carte. Alcune sono anche disponibili con uno sconto!", "cake_pay_learn_more": "Acquista e riscatta istantaneamente carte regalo nell'app!\nScorri da sinistra a destra per saperne di più.", - "cake_pay_save_order": "La carta deve essere inviata alla tua e-mail entro 1 giorno lavorativo \n Salva il tuo ID ordine:", - "cake_pay_subtitle": "Acquista carte prepagate in tutto il mondo e carte regalo", - "cake_pay_web_cards_subtitle": "Acquista carte prepagate e carte regalo in tutto il mondo", + "cake_pay_save_order": "La carta deve essere inviata alla tua e-mail entro 1 giorno lavorativo \n Salva l'ID del tuo ordine:", + "cake_pay_subtitle": "Acquista carte prepagate e carte regalo internazionali", + "cake_pay_web_cards_subtitle": "Acquista carte prepagate e carte regalo internazionali", "cake_pay_web_cards_title": "Carte Web Cake Pay", "cake_seeds_save_disclaimer": "Si prega di salvare queste parole in un posto sicuro! Avrai bisogno di queste parole per ripristinare il portafoglio su un nuovo dispositivo.", "cake_wallet": "Cake Wallet", - "cakepay_confirm_no_vpn": "Conferma che non sto usando un proxy o una VPN", + "cakepay_confirm_no_vpn": "Confermo che non sto usando una proxy o una VPN", "cakepay_confirm_purchase": "Conferma l'acquisto", "cakepay_confirm_terms_agreed": "Accetto i termini e le condizioni presentati qui:", "cakepay_confirm_voided_refund": "Capisco che i tentativi di rimborso da un paese limitato annullano qualsiasi rimborso", @@ -118,18 +118,18 @@ "cards": "Carte", "chains": "Catene", "change": "Cambia", - "change_backup_password_alert": "I precedenti file di backup non potranno essere importati con la nuova password di backup. La nuova password di backup verrà usata soltanto per i nuovi file di backup. Sei sicuro di voler cambiare la tua password di backup?", + "change_backup_password_alert": "I tuoi file di backup precedenti non potranno essere importati con la nuova password di backup. La nuova password di backup verrà usata soltanto per i nuovi file di backup. Sei sicuro di voler cambiare la tua password di backup?", "change_currency": "Cambia Valuta", "change_current_node": "Sei sicuro di voler cambiare il nodo corrente con ${node}?", "change_current_node_title": "Cambia nodo corrente", - "change_exchange_provider": "Provider di swap di cambiamento", + "change_exchange_provider": "Cambia Swap Provider", "change_language": "Cambia lingua", "change_language_to": "Cambiare lingua in ${language}?", "change_password": "Cambia password", - "change_rep": "Cambia rappresentante", - "change_rep_message": "Sei sicuro di voler cambiare rappresentante?", + "change_rep": "Cambia Rappresentante", + "change_rep_message": "Sei sicuro di voler cambiare rappresentanti?", "change_rep_successful": "Rappresentante modificato con successo", - "change_selected_exchanges": "Modificare gli scambi selezionati", + "change_selected_exchanges": "Modificare gli Exchange selezionati", "change_wallet_alert_content": "Sei sicuro di voler cambiare il portafoglio attuale con ${wallet_name}?", "change_wallet_alert_title": "Cambia portafoglio attuale", "choose_a_payment_method": "Scegli un metodo di pagamento", @@ -140,33 +140,32 @@ "choose_derivation": "Scegli la derivazione del portafoglio", "choose_from_available_options": "Scegli tra le opzioni disponibili:", "choose_one": "Scegline uno", - "choose_relay": "Scegli un relè da utilizzare", - "choose_wallet_currency": "Gentilmente scegli la moneta del portafoglio:", + "choose_relay": "Scegli un relay da utilizzare", + "choose_wallet_currency": "Scegli la valuta del portafoglio:", "choose_wallet_group": "Scegli il gruppo del portafoglio", "clear": "Pulisci", - "clearnet_link": "Collegamento Clearnet", - "close": "Chiudere", - "coin_control": "Controllo monete (opzionale)", - "cold_or_recover_wallet": "Aggiungi un portafoglio di sola lettura da Cupcake o un portafoglio freddo o recupera un portafoglio di carta", + "clearnet_link": "Link Clearnet", + "close": "Chiudi", + "coin_control": "Controllo valute (opzionale)", + "cold_or_recover_wallet": "Aggiungi un portafoglio di sola lettura da Cupcake o un cold wallet o recupera un portafoglio di carta", "color_theme": "Colore tema", "commit_transaction_amount_fee": "Invia transazione\nAmmontare: ${amount}\nCommissione: ${fee}", "confirm": "Conferma", "confirm_delete_template": "Questa azione cancellerà questo modello. Desideri continuare?", "confirm_delete_wallet": "Questa azione cancellerà questo portafoglio. Desideri continuare?", - "confirm_fee_deduction": "Conferma la detrazione delle commissioni", - "confirm_fee_deduction_content": "Accetti di detrarre la commissione dall'output?", + "confirm_fee_deduction": "Conferma detrazione delle commissioni", + "confirm_fee_deduction_content": "Accetti la detrazione della commissione dall'output?", "confirm_passphrase": "Conferma passphrase", "confirm_sending": "Conferma l'invio", - "confirm_silent_payments_switch_node": "Il tuo nodo corrente non supporta i pagamenti silenziosi \\ ncake Wallet passerà a un nodo compatibile, solo per la scansione", + "confirm_silent_payments_switch_node": "Il tuo nodo corrente non supporta Silent Payments\\n\\nCake Wallet passerà a un nodo compatibile durante la scansione.", "confirmations": "Conferme", - "confirmed": "Saldo confermato", + "confirmed": "Saldo Confermato", "confirmed_tx": "Confermato", - "congratulation": "Congratulazioni!", "congratulations": "Congratulazioni!", "connect_an_existing_yat": "Collegare un Yat esistente", "connect_yats": "Connetti Yats", - "connect_your_hardware_wallet": "Collega il tuo portafoglio hardware con Bluetooth o USB", - "connect_your_hardware_wallet_ios": "Collega il tuo portafoglio hardware con Bluetooth", + "connect_your_hardware_wallet": "Collega il tuo portafoglio hardware tramite Bluetooth o USB", + "connect_your_hardware_wallet_ios": "Collega il tuo portafoglio hardware tramite Bluetooth", "connection_sync": "Connessione e sincronizzazione", "connectWalletPrompt": "Collega il tuo portafoglio con WalletConnect per effettuare transazioni", "contact": "Contatta", @@ -176,38 +175,38 @@ "contact_name_exists": "Esiste già un contatto con quel nome. Scegli un nome diverso.", "contact_support": "Contatta l'assistenza", "continue_text": "Continua", - "contract_warning": "Questo indirizzo del contratto è stato contrassegnato come potenzialmente fraudolento. Si prega di elaborare con cautela.", - "contractName": "Nome del contratto", - "contractSymbol": "Simbolo del contratto", + "contract_warning": "L'indirizzo di questo contratto è stato contrassegnato come potenzialmente fraudolento. Si prega di procedere con cautela.", + "contractName": "Nome Contratto", + "contractSymbol": "Simbolo del Contratto", "copied_key_to_clipboard": " ${key} copiata negli Appunti", "copied_to_clipboard": "Copiato negli Appunti", "copy": "Copia", "copy_address": "Copia Indirizzo", "copy_id": "Copia ID", "copyWalletConnectLink": "Copia il collegamento WalletConnect dalla dApp e incollalo qui", - "corrupted_seed_notice": "I file per questo portafoglio sono corrotti e non sono in grado di essere aperti. Visualizza la frase del seme, salvala e ripristina il portafoglio.\n\nSe il valore è vuoto, il seme non è stato in grado di essere recuperato correttamente.", + "corrupted_seed_notice": "I file per questo portafoglio sono corrotti e non è possibile aprirli. Visualizza la frase del seme, salvala e ripristina il portafoglio.\n\nSe il valore è vuoto, non è stato possibile recuperare correttamente il seme.", "countries": "Paesi", "create_account": "Crea account", "create_backup": "Crea backup", "create_donation_link": "Crea un link per la donazione", - "create_invoice": "Crea fattura", - "create_new": "Genera nuovo Portafoglio", + "create_invoice": "Crea ricevuta", + "create_new": "Crea nuovo Portafoglio", "create_new_account": "Crea nuovo account", "create_new_seed": "Crea nuovo seme", "creating_new_wallet": "Creazione nuovo portafoglio", "creating_new_wallet_error": "Errore: ${description}", "creation_date": "Data di creazione", - "custom": "personalizzato", - "custom_drag": "Custom (Hold and Drag)", + "custom": "Personalizza", + "custom_drag": "Personalizza (Tieni e Trascina)", "custom_redeem_amount": "Importo di riscatto personalizzato", "custom_value": "Valore personalizzato", "dark_theme": "Scuro", "debit_card": "Carta di debito", - "debit_card_terms": "L'archiviazione e l'utilizzo del numero della carta di pagamento (e delle credenziali corrispondenti al numero della carta di pagamento) in questo portafoglio digitale sono soggetti ai Termini e condizioni del contratto applicabile con il titolare della carta con l'emittente della carta di pagamento, come in vigore da tempo al tempo.", + "debit_card_terms": "L'archiviazione e l'utilizzo del numero della carta di pagamento (e delle credenziali corrispondenti al numero della carta di pagamento) in questo portafoglio digitale sono soggetti ai Termini e condizioni del contratto applicabile con il titolare della carta con l'emittente della carta di pagamento, come in vigore di tanto in tanto.", "decimal_places_error": "Troppe cifre decimali", "decimals_cannot_be_zero": "Il decimale token non può essere zero.", "default_buy_provider": "Provider di acquisto predefinito", - "default_sell_provider": "Fornitore di vendita predefinito", + "default_sell_provider": "Provider di vendita predefinito", "delete": "Elimina", "delete_account": "Elimina account", "delete_wallet": "Elimina portafoglio", @@ -219,46 +218,46 @@ "description": "Descrizione", "destination_tag": "Tag destinazione:", "dfx_option_description": "Acquista Crypto con EUR & CHF. Per i clienti al dettaglio e aziendali in Europa", - "didnt_get_code": "Non ricevi il codice?", + "didnt_get_code": "Non hai ricevuto il codice?", "digit_pin": "-cifre PIN", "digital_and_physical_card": "carta di debito prepagata digitale e fisica", - "disable": "disattivare", - "disable_bulletin": "Disabilita Bollettino dello stato del servizio", + "disable": "Disabilita", + "disable_bulletin": "Disabilita bollettino dello stato del servizio", "disable_buy": "Disabilita l'azione di acquisto", "disable_cake_2fa": "Disabilita Cake 2FA", "disable_exchange": "Disabilita scambio", - "disable_fee_api_warning": "Disattivando questo, i tassi delle commissioni potrebbero essere inaccurati in alcuni casi, quindi potresti finire in eccesso o sostenere le commissioni per le transazioni", + "disable_fee_api_warning": "Disattivando quest'opzione, i tassi delle commissioni potrebbero essere inaccurati in alcuni casi, quindi potresti finire per pagare troppo o troppo poco le commissioni per le tue transazioni", "disable_fiat": "Disabilita fiat", "disable_sell": "Disabilita l'azione di vendita", - "disable_trade_option": "Disabilita l'opzione commerciale", + "disable_trade_option": "Disabilita l'opzione di scambio", "disableBatteryOptimization": "Disabilita l'ottimizzazione della batteria", - "disableBatteryOptimizationDescription": "Vuoi disabilitare l'ottimizzazione della batteria per far funzionare la sincronizzazione in background più libera e senza intoppi?", + "disableBatteryOptimizationDescription": "Vuoi disabilitare l'ottimizzazione della batteria per migliorare la sincronizzazione in background?", "disabled": "Disabilitato", "discount": "Risparmia ${value}%", "display_settings": "Impostazioni di visualizzazione", "displayable": "Visualizzabile", "do_not_have_enough_gas_asset": "Non hai abbastanza ${currency} per effettuare una transazione con le attuali condizioni della rete blockchain. Hai bisogno di più ${currency} per pagare le commissioni della rete blockchain, anche se stai inviando una risorsa diversa.", "do_not_send": "Non inviare", - "do_not_share_warning_text": "Non condividerli con nessun altro, incluso il supporto.\n\nI tuoi fondi possono e saranno rubati!", + "do_not_share_warning_text": "Non condividerli con nessun altro, incluso il supporto.\n\nI tuoi fondi possono essere e saranno rubati!", "do_not_show_me": "Non mostrarmelo di nuovo", - "domain_looks_up": "Ricerche di domini", - "donation_link_details": "Dettagli del collegamento alla donazione", + "domain_looks_up": "Cerca domini", + "donation_link_details": "Dettagli del link di donazione", "e_sign_consent": "Consenso alla firma elettronica", "edit": "Modifica", "edit_backup_password": "Modifica Password Backup", - "edit_node": "Modifica nodo", - "edit_token": "Modifica token", + "edit_node": "Modifica Nodo", + "edit_token": "Modifica Token", "electrum_address_disclaimer": "Generiamo nuovi indirizzi ogni volta che ne utilizzi uno, ma gli indirizzi precedenti continuano a funzionare", "email_address": "Indirizzo e-mail", - "enable": "Abilitare", - "enable_mempool_api": "API di Mempool per commissioni e date accurate", - "enable_replace_by_fee": "Abilita sostituzione per fee", - "enable_silent_payments_scanning": "Inizia a scansionare i pagamenti silenziosi, fino a raggiungere la punta", + "enable": "Abilita", + "enable_mempool_api": "API Mempool per commissioni e date accurate", + "enable_replace_by_fee": "Abilita Replace-By-Fee", + "enable_silent_payments_scanning": "Inizia a scansionare le transazioni inviate al tuo indirizzo Silent Payments", "enabled": "Abilitato", "enter_amount": "Inserisci importo", "enter_backup_password": "Inserisci la password di backup qui", "enter_code": "Inserisci codice", - "enter_seed_phrase": "Inserisci la tua frase di semi", + "enter_seed_phrase": "Inserisci la tua frase del seme", "enter_totp_code": "Inserisci il codice TOTP.", "enter_wallet_password": "Immettere la password del portafoglio", "enter_your_note": "Inserisci la tua nota…", @@ -268,22 +267,22 @@ "enterWalletConnectURI": "Inserisci l'URI di WalletConnect", "error": "Errore", "error_dialog_content": "Oops, abbiamo ricevuto qualche errore.\n\nSi prega di inviare il rapporto di errore al nostro team di supporto per migliorare l'applicazione.", - "error_text_account_name": "Il nome del conto può contenere solo lettere, numeri\ne deve avere una lunghezza compresa tra 1 e 15 caratteri", - "error_text_address": "L'indirizzo del Portafoglio deve corrispondere alla tipologia\ndi criptovaluta", - "error_text_amount": "L'ammontare può contenere solo numeri", - "error_text_contact_name": "Il nome del Contatto non può contenere i simboli ` , ' \" \ne deve avere una lunghezza compresa tra 1 e 32 caratteri", + "error_text_account_name": "Il nome dell'account può contenere solo lettere, numeri\ne deve avere una lunghezza compresa tra 1 e 15 caratteri", + "error_text_address": "L'indirizzo del portafoglio deve corrispondere alla tipologia\ndi criptovaluta", + "error_text_amount": "L'importo può contenere solo numeri", + "error_text_contact_name": "Il nome del contatto non può contenere i simboli ` , ' \" \ne deve avere una lunghezza compresa tra 1 e 32 caratteri", "error_text_crypto_currency": "Il numero delle cifre decimali\ndeve essere inferiore o uguale a 12", - "error_text_fiat": "L'ammontare non può eccedere il saldo dispoinibile.\nIl numero di cifre decimali deve essere inferiore o uguale a 2", - "error_text_input_above_maximum_limit": "L'ammontare è superiore al massimo", - "error_text_input_below_minimum_limit": "L'ammontare è inferiore al minimo", - "error_text_keys": "Le chiavi del portafoglio possono contenere solo 64 caratteri in hex", + "error_text_fiat": "L'importo non può eccedere il saldo disponibile.\nIl numero di cifre decimali deve essere inferiore o uguale a 2", + "error_text_input_above_maximum_limit": "L'importo è superiore al massimo", + "error_text_input_below_minimum_limit": "L'importo è inferiore al minimo", + "error_text_keys": "Le chiavi del portafoglio possono contenere solo 64 caratteri esadecimali (hex)", "error_text_limits_loading_failed": "Lo scambio per ${provider} non è stato creato. Caricamento dei limiti fallito", - "error_text_maximum_limit": "Lo scambio per ${provider} non è stato creato. L'ammontare è superiore al massimo: ${max} ${currency}", - "error_text_minimal_limit": "Lo scambio per ${provider} non è stato creato. L'ammontare è inferiore al minimo: ${min} ${currency}", - "error_text_node_address": "Gentilmente inserisci un indirizzo iPv4", + "error_text_maximum_limit": "Lo scambio per ${provider} non è stato creato. L'importo è superiore al massimo: ${max} ${currency}", + "error_text_minimal_limit": "Lo scambio per ${provider} non è stato creato. L'importo è inferiore al minimo: ${min} ${currency}", + "error_text_node_address": "Gentilmente inserisci un indirizzo IPv4", "error_text_node_port": "La porta del nodo può contenere solo numeri compresi tra 0 e 65535", "error_text_node_proxy_address": "Inserisci :, ad esempio 127.0.0.1:9050", - "error_text_payment_id": "L'ID del pagamento può contenere solo da 16 a 64 caratteri in esadecimale", + "error_text_payment_id": "L'ID del pagamento può contenere solo da 16 a 64 caratteri esadecimali (hex)", "error_text_subaddress_name": "Il nome del sottoindirizzo non può contenere i simboli ` , ' \" \ne deve avere una lunghezza compresa tra 1 e 20 caratteri", "error_text_template": "Il nome del modello e l'indirizzo non possono contenere i simboli ` , ' \" \ne devono avere una lunghezza compresa tra 1 e 106 caratteri", "error_text_wallet_name": "Il nome del portafoglio può contenere solo lettere, numeri, _ - simboli\ne deve avere una lunghezza compresa tra 1 e 33 caratteri", @@ -293,25 +292,25 @@ "estimated": "Stimato", "estimated_new_fee": "Nuova commissione stimata", "estimated_receive_amount": "Importo di ricezione stimato", - "etherscan_history": "Storia Etherscan", + "etherscan_history": "Cronologia Etherscan", "event": "Evento", "events": "Eventi", "exchange": "Scambio", - "exchange_incorrect_current_wallet_for_xmr": "Se vuoi scambiare XMR dal tuo portafoglio di torta Monero Balance, si prega di passare prima al portafoglio Monero.", + "exchange_incorrect_current_wallet_for_xmr": "Se vuoi scambiare XMR dal tuo portafoglio Cake Wallet, devi prima passare al portafoglio Monero.", "exchange_new_template": "Nuovo modello", "exchange_provider_unsupported": "${providerName} non è più supportato!", - "exchange_result_confirm": "Cliccando su Conferma, invierai ${fetchingLabel} ${from} dal tuo portafoglio chiamato ${walletName} all'indirizzo mostrato qui in basso. O puoi inviare dal tuo portafoglio esterno all'indirizzo/codice QR mostrato in basso.\n\nGentilmente clicca su Conferma per continuare o torna indietro per cambiare l'ammontare.", - "exchange_result_description": "Devi inviare un minimo di ${fetchingLabel} ${from} all'indirizzo mostrato nella pagina seguente. Se invii un ammontare inferiore a ${fetchingLabel} ${from} la conversione potrebbe non andare a buon fine e l'indirizzo potrebbe non essere rimborsato.", - "exchange_result_write_down_ID": "*Gentilmente fai una copia o trascrivi il tuo ID mostrato in alto.", - "exchange_result_write_down_trade_id": "Per favore fai una copia o trascrivi l'ID dello scambio per continuare.", - "exchange_sync_alert_content": "Gentilmente aspetta che il tuo portafoglio sia sincronizzato", - "expired": "Scaduta", + "exchange_result_confirm": "Cliccando su Conferma, invierai ${fetchingLabel} ${from} dal tuo portafoglio chiamato ${walletName} all'indirizzo mostrato qui sotto. Oppure puoi inviare dal tuo portafoglio esterno all'indirizzo/codice QR mostrato in basso.\n\nClicca su Conferma per continuare, o torna indietro per cambiare l'importo.", + "exchange_result_description": "Devi inviare un minimo di ${fetchingLabel} ${from} all'indirizzo mostrato nella pagina seguente. Se invii un importo inferiore a ${fetchingLabel} ${from} la conversione potrebbe non andare a buon fine, e l'indirizzo potrebbe non essere rimborsato.", + "exchange_result_write_down_ID": "*Copia o trascrivi il tuo ID mostrato in alto.", + "exchange_result_write_down_trade_id": "Copia o trascrivi l'ID dello scambio per continuare.", + "exchange_sync_alert_content": "Gentilmente aspetta che il tuo portafoglio venga sincronizzato", + "expired": "Scaduto", "expires": "Scade", "expiresOn": "Scade il", "expiry_and_validity": "Scadenza e validità", "export_backup": "Esporta backup", - "export_logs": "Registri di esportazione", - "export_outputs": "Output di esportazione", + "export_logs": "Esporta log", + "export_outputs": "Esporta output", "extra_id": "Extra ID:", "extracted_address_content": "Invierai i tuoi fondi a\n${recipient_name}", "failed_authentication": "Autenticazione fallita. ${state_error}", @@ -321,94 +320,94 @@ "fee_rate": "Tasso di commissione", "fetching": "Recupero", "fiat_api": "Fiat API", - "fiat_balance": "Equilibrio fiat", + "fiat_balance": "Saldo fiat", "field_required": "Questo campo è obbligatorio", "fill_code": "Compila il codice di verifica fornito alla tua email", - "filter_by": "Filtrirati po", + "filter_by": "Filtra per", "first_wallet_text": "Portafoglio fantastico per Monero, Bitcoin, Ethereum, Litecoin, e Haven", "fixed_pair_not_supported": "Questa coppia fissa non è supportata con i servizi di swap selezionati", "fixed_rate": "Tasso fisso", - "fixed_rate_alert": "Potrai inserire l'ammontare da ricevere quando il tasso è fisso. Vuoi cambiare alla modalità tasso fisso?", + "fixed_rate_alert": "Potrai inserire l'importo da ricevere quando il tasso è fisso. Vuoi cambiare alla modalità tasso fisso?", "forgot_password": "Password dimenticata", - "freeze": "Congelare", + "freeze": "Congela", "frequently_asked_questions": "Domande frequenti", "frozen": "Congelato", - "frozen_balance": "Equilibrio congelato", + "frozen_balance": "Saldo Congelato", "full_balance": "Saldo Completo", - "gas_exceeds_allowance": "Il gas richiesto dalla transazione supera l'indennità.", + "gas_exceeds_allowance": "Il Gas richiesto dalla transazione supera il limite consentito.", "generate_name": "Genera nome", "generating_gift_card": "Generazione carta regalo", "get_a": "Prendi un ", "get_card_note": "che puoi ricaricare con le valute digitali. Non sono necessarie informazioni aggiuntive!", "get_your_yat": "Ottieni il tuo Yat", "gift_card_amount": "Importo del buono regalo", - "gift_card_balance_note": "Le carte regalo con un saldo residuo appariranno qui", + "gift_card_balance_note": "Le carte regalo con saldo residuo appariranno qui", "gift_card_is_generated": "Il buono regalo è stato generato", "gift_card_number": "Numero del buono regalo", "gift_card_redeemed_note": "Le carte regalo che hai riscattato appariranno qui", "gift_cards": "Carte regalo", - "gift_cards_unavailable": "Le carte regalo sono disponibili per l'acquisto solo tramite Monero, Bitcoin e Litecoin in questo momento", + "gift_cards_unavailable": "Le carte regalo sono attualmente disponibili per l'acquisto solo tramite Monero, Bitcoin e Litecoin", "got_it": "Ho capito", "gross_balance": "Saldo lordo", - "group_by_type": "Gruppo per tipo", - "haven_app": "Haven by Cake Wallet", + "group_by_type": "Raggruppa per tipo", + "haven_app": "Haven di Cake Wallet", "haven_app_wallet_text": "Portafoglio fantastico per Haven", "help": "aiuto", "hidden_addresses": "Indirizzi nascosti", "hidden_balance": "Saldo Nascosto", - "hide": "Nascondere", + "hide": "Nascondi", "hide_details": "Nascondi dettagli", "high_contrast_theme": "Tema ad alto contrasto", "home_screen_settings": "Impostazioni della schermata iniziale", "how_to_use": "Come usare", "how_to_use_card": "Come usare questa carta", "id": "ID: ", - "if_you_dont_see_your_device": "Se non vedi il tuo dispositivo sopra, assicurati che il tuo libro mastro sia sveglio e sbloccato!", - "ignor": "Ignorare", - "import": "Importare", + "if_you_dont_see_your_device": "Se non vedi il tuo dispositivo sopra, assicurati che il tuo Ledger sia attivo e sbloccato!", + "ignor": "Ignora", + "import": "Importa", "importNFTs": "Importa NFT", "in_store": "In negozio", "incoming": "In arrivo", "incorrect_seed": "Il testo inserito non è valido.", - "incorrect_seed_option": "Errato. Per favore riprova", + "incorrect_seed_option": "Errato. Si prega di riprovare", "incorrect_seed_option_back": "Errato. Assicurati che il tuo seme venga salvato correttamente e riprova.", "inputs": "Input", "insufficient_funds_for_tx": "Fondi insufficienti per eseguire correttamente la transazione.", - "insufficient_lamport_for_tx": "Non hai abbastanza SOL per coprire la transazione e la sua quota di transazione. Si prega di aggiungere più SOL al tuo portafoglio o ridurre l'importo SOL che stai inviando.", - "insufficient_lamports": "Non hai abbastanza SOL per coprire la transazione e la sua quota di transazione. Hai bisogno di almeno ${solValueNeeded} sol. Si prega di aggiungere più SOL al tuo portafoglio o ridurre l'importo SOL che stai inviando", - "insufficientFundsForRentError": "Non hai abbastanza SOL per coprire la tassa di transazione e l'affitto per il conto. Si prega di aggiungere più SOL al tuo portafoglio o ridurre l'importo SOL che stai inviando", - "introducing_cake_pay": "Presentazione di Cake Pay!", + "insufficient_lamport_for_tx": "Non hai abbastanza SOL per coprire la transazione e la sua quota di transazione. Aggiungi più SOL al tuo portafoglio, o riduci l'importo di SOL che stai inviando.", + "insufficient_lamports": "Non hai abbastanza SOL per coprire la transazione e la sua quota di transazione. Hai bisogno di almeno ${solValueNeeded} SOL. Aggiungi più SOL al tuo portafoglio, o riduci l'importo di SOL che stai inviando", + "insufficientFundsForRentError": "Non hai abbastanza SOL per coprire la tassa di transazione e l'affitto per il conto. Aggiungi più SOL al tuo portafoglio, o riduci l'importo di SOL che stai inviando", + "introducing_cake_pay": "Vi presentiamo Cake Pay!", "invalid_input": "Inserimento non valido", "invalid_password": "Password non valida", - "invoice_details": "Dettagli della fattura", + "invoice_details": "Dettagli della ricevuta", "is_percentage": "è", - "keys": "Tasti", + "keys": "Chiavi", "last_30_days": "Ultimi 30 giorni", - "learn_more": "Impara di più", - "ledger_connection_error": "Impossibile connettersi al libro mastro. Per favore riprova.", - "ledger_error_device_locked": "Il libro mastro è bloccato", + "learn_more": "Scopri di più", + "ledger_connection_error": "Impossibile connettersi al Ledger. Si prega di riprovare.", + "ledger_error_device_locked": "Il Ledger è bloccato", "ledger_error_tx_rejected_by_user": "Transazione rifiutata sul dispositivo", - "ledger_error_wrong_app": "Assicurati di aprire l'app giusta sul libro mastro", - "ledger_please_enable_bluetooth": "Si prega di consentire al Bluetooth di rilevare il libro mastro", - "legacy": "Eredità", - "light_theme": "Bianco", + "ledger_error_wrong_app": "Assicurati di aver aperto l'app giusta sul Ledger", + "ledger_please_enable_bluetooth": "Abilita il Bluetooth per rilevare il tuo Ledger", + "legacy": "Legacy", + "light_theme": "Chiaro", "litecoin_enable_mweb_sync": "Abilita la scansione MWeb", "litecoin_mweb": "MWeb", "litecoin_mweb_always_scan": "Imposta MWeb per scansionare sempre", "litecoin_mweb_description": "MWeb è un nuovo protocollo che porta transazioni più veloci, più economiche e più private a Litecoin", - "litecoin_mweb_dismiss": "Congedare", + "litecoin_mweb_dismiss": "Chiudi", "litecoin_mweb_display_card": "Mostra la scheda MWeb", - "litecoin_mweb_enable": "Abilita mWeb", + "litecoin_mweb_enable": "Abilita MWeb", "litecoin_mweb_enable_later": "È possibile scegliere di abilitare nuovamente MWeb nelle impostazioni di visualizzazione.", - "litecoin_mweb_logs": "Registri mWeb", + "litecoin_mweb_logs": "Registri MWeb", "litecoin_mweb_node": "Nodo MWeb", - "litecoin_mweb_pegin": "Piolo in", - "litecoin_mweb_pegout": "PEG OUT", + "litecoin_mweb_pegin": "Peg In", + "litecoin_mweb_pegout": "Peg Out", "litecoin_mweb_scanning": "Scansione MWeb", "litecoin_mweb_settings": "Impostazioni MWeb", - "litecoin_mweb_warning": "L'uso di MWeb inizialmente scaricherà ~ 600 MB di dati e potrebbe richiedere fino a 30 minuti a seconda della velocità di rete. Questi dati iniziali scaricheranno solo una volta e saranno disponibili per tutti i portafogli Litecoin", + "litecoin_mweb_warning": "L'uso di MWeb inizialmente scaricherà ~600 MB di dati, e potrebbe richiedere fino a 30 minuti a seconda della velocità di rete. Questi dati iniziali verranno scaricati solo una volta, e saranno disponibili per tutti i portafogli Litecoin", "litecoin_what_is_mweb": "Cos'è MWeb?", - "live_fee_rates": "Tariffe delle commissioni dal vivo tramite API", + "live_fee_rates": "Tariffe delle commissioni in tempo reale tramite API", "load_more": "Carica di più", "loading_your_wallet": "Caricamento portafoglio", "login": "Accedi", @@ -431,35 +430,35 @@ "min_value": "Min: ${value} ${currency}", "minutes_to_pin_code": "${minute} minuti", "mm": "mm", - "modify_2fa": "Modifica Torta 2FA", + "modify_2fa": "Modifica Cake 2FA", "monero_com": "Monero.com by Cake Wallet", "monero_com_wallet_text": "Portafoglio fantastico per Monero", - "monero_dark_theme": "Tema scuro di Monero", - "monero_light_theme": "Tema leggero Monero", + "monero_dark_theme": "Tema scuro Monero", + "monero_light_theme": "Tema chiaro Monero", "moonpay_alert_text": "Il valore dell'importo deve essere maggiore o uguale a ${minAmount} ${fiatCurrency}", "more_options": "Altre opzioni", "mweb_confirmed": "MWeb confermato", "mweb_unconfirmed": "MWeb non confermato", "name": "Nome", "nano_current_rep": "Rappresentante attuale", - "nano_gpt_thanks_message": "Grazie per aver usato il nanogpt! Ricorda di tornare al browser dopo il completamento della transazione!", + "nano_gpt_thanks_message": "Grazie per aver usato nanogpt! Ricorda di tornare al browser dopo il completamento della transazione!", "nano_pick_new_rep": "Scegli un nuovo rappresentante", - "nanogpt_subtitle": "Tutti i modelli più recenti (GPT-4, Claude). Abbonamento nno, paga con cripto.", + "nanogpt_subtitle": "Tutti i modelli più recenti (GPT-4, Claude). Nessun abbonamento, paga con cripto.", "narrow": "Stretto", - "new_first_wallet_text": "Mantenere la tua criptovaluta è un pezzo di torta", + "new_first_wallet_text": "Mantenere la tua criptovaluta è un gioco da ragazzi", "new_node_testing": "Test novo nodo", "new_subaddress_create": "Crea", "new_subaddress_label_name": "Nome etichetta", "new_subaddress_title": "Nuovo indirizzo", "new_template": "Nuovo modello", - "new_wallet": "Nuovo Portafoglio", + "new_wallet": "Nuovo portafoglio", "newConnection": "Nuova connessione", "no_cards_found": "Nessuna carta trovata", "no_id_needed": "Nessun ID necessario!", "no_id_required": "Nessun ID richiesto. Ricarica e spendi ovunque", - "no_relay_on_domain": "Non esiste un inoltro per il dominio dell'utente oppure l'inoltro non è disponibile. Scegli un relè da utilizzare.", - "no_relays": "Nessun relè", - "no_relays_message": "Abbiamo trovato un record Nostr NIP-05 per questo utente, ma non contiene alcun relè. Si prega di indicare al destinatario di aggiungere inoltri al proprio record Nostr.", + "no_relay_on_domain": "Non esiste un inoltro per il dominio dell'utente oppure l'inoltro non è disponibile. Scegli un relay da utilizzare.", + "no_relays": "Nessun relay", + "no_relays_message": "Abbiamo trovato un record Nostr NIP-05 per questo utente, ma non contiene alcun relay. Indica al destinatario di aggiungere relay al proprio record Nostr.", "node_address": "Indirizzo Nodo", "node_connection_failed": "Connessione fallita", "node_connection_successful": "Connessione avvenuta con successo", @@ -470,16 +469,16 @@ "nodes": "Nodi", "nodes_list_reset_to_default_message": "Sei sicuro di voler ripristinare le impostazioni predefinite?", "none_of_selected_providers_can_exchange": "Nessuno dei provider selezionati può fare questo scambio", - "noNFTYet": "Nessun NFT ancora", + "noNFTYet": "Ancora nessun NFT", "normal": "Normale", "note_optional": "Nota (opzionale)", "note_tap_to_change": "Nota (clicca per cambiare)", "nullURIError": "L'URI è nullo", - "offer_expires_in": "Offerta termina tra: ", + "offer_expires_in": "L'offerta termina tra: ", "offline": "Offline", "ok": "OK", - "old_fee": "Vecchia tassa", - "onion_link": "Collegamento a cipolla", + "old_fee": "Vecchia commissione", + "onion_link": "Link Onion", "online": "in linea", "onramper_option_description": "Acquista rapidamente la criptovaluta con molti metodi di pagamento. Disponibile nella maggior parte dei paesi. Gli spread e le commissioni variano.", "open_gift_card": "Apri carta regalo", @@ -494,18 +493,18 @@ "order_id": "ID ordine", "order_physical_card": "Ordine carta fisica", "other_settings": "Altre impostazioni", - "outdated_electrum_wallet_description": "I nuovi portafogli Bitcoin creati in Cake ora hanno un seme di 24 parole. È obbligatorio creare un nuovo portafoglio Bitcoin e trasferire tutti i fondi nel nuovo portafoglio di 24 parole e smettere di usare portafogli con un seme di 12 parole. Ti preghiamo di farlo immediatamente per proteggere i tuoi fondi.", + "outdated_electrum_wallet_description": "I nuovi portafogli Bitcoin creati su Cake Wallet ora hanno un seme di 24 parole. È obbligatorio creare un nuovo portafoglio Bitcoin e trasferire tutti i fondi nel nuovo portafoglio di 24 parole e smettere di usare portafogli con un seme di 12 parole. Ti preghiamo di farlo immediatamente per proteggere i tuoi fondi.", "outdated_electrum_wallet_receive_warning": "Se questo portafoglio ha un seme di 12 parole ed è stato creato in Cake, NON depositare Bitcoin in questo portafoglio. Qualsiasi BTC trasferito su questo portafoglio potrebbe andare perso. Crea un nuovo portafoglio di 24 parole (tocca il menu in alto a destra, seleziona Portafogli, scegli Crea nuovo portafoglio, quindi seleziona Bitcoin) e sposta IMMEDIATAMENTE lì il tuo BTC. I nuovi portafogli BTC (24 parole) di Cake sono sicuri", "outgoing": "In uscita", "outputs": "Output", - "overshot": "Overs -shot", + "overshot": "Overshot", "overwrite_amount": "Sovrascrivi quantità", "pairingInvalidEvent": "Associazione evento non valido", "passphrase": "Passphrase (opzionale)", - "passphrases_doesnt_match": "Le passphrasi non corrispondono, riprova", + "passphrases_doesnt_match": "Le passphrase non corrispondono, riprova", "password": "Password", "paste": "Incolla", - "pause_wallet_creation": "La possibilità di creare Haven Wallet è attualmente sospesa.", + "pause_wallet_creation": "La possibilità di creare Wallet Haven è attualmente sospesa.", "payment_id": "ID Pagamento: ", "payment_was_received": "Il tuo pagamento è stato ricevuto.", "pending": " (non confermati)", @@ -515,9 +514,9 @@ "pin_number": "Numero PIN", "placeholder_contacts": "I tuoi contatti saranno mostrati qui", "placeholder_transactions": "Le tue transazioni saranno mostrate qui", - "please_fill_totp": "Inserisci il codice di 8 cifre presente sull'altro tuo dispositivo", + "please_fill_totp": "Inserisci il codice di 8 cifre presente sul tuo altro dispositivo", "please_make_selection": "Gentilmente seleziona se vuoi generare o recuperare il tuo portafoglio.", - "please_reference_document": "Si prega di fare riferimento ai documenti di seguito per ulteriori informazioni.", + "please_reference_document": "Si prega di fare riferimento ai documenti seguenti per ulteriori informazioni.", "please_select": "Gentilmente seleziona:", "please_select_backup_file": "Gentilmente seleziona il file di backup e inserisci la password di backup.", "please_try_to_connect_to_another_node": "Gentilmente prova a connetterti ad un altro nodo", @@ -534,34 +533,34 @@ "privacy_policy": "Informativa sulla privacy", "privacy_settings": "Impostazioni privacy", "private_key": "Chiave privata", - "proceed_after_one_minute": "Se lo schermo non procede dopo 1 minuto, controlla la tua email.", + "proceed_after_one_minute": "Se la schermata non procede dopo 1 minuto, controlla la tua email.", "proceed_on_device": "Procedi sul tuo dispositivo", "proceed_on_device_description": "Segui le istruzioni richieste sul portafoglio hardware", "profile": "Profilo", "provider_error": "${provider} errore", "public_key": "Chiave pubblica", "purchase_gift_card": "Acquista carta regalo", - "purple_dark_theme": "Tema oscuro viola", + "purple_dark_theme": "Tema scuro viola", "qr_fullscreen": "Tocca per aprire il codice QR a schermo intero", - "qr_payment_amount": "Questo codice QR contiene l'ammontare del pagamento. Vuoi sovrascrivere il varlore attuale?", + "qr_payment_amount": "Questo codice QR contiene l'importo del pagamento. Vuoi sovrascrivere il valore attuale?", "quantity": "Quantità", "question_to_disable_2fa": "Sei sicuro di voler disabilitare Cake 2FA? Non sarà più necessario un codice 2FA per accedere al portafoglio e ad alcune funzioni.", - "receivable_balance": "Bilanciamento creditizio", + "receivable_balance": "Importo ricevibile", "receive": "Ricevi", - "receive_amount": "Ammontare", + "receive_amount": "Importo", "received": "Ricevuto", "recipient_address": "Indirizzo di destinazione", "reconnect": "Riconnetti", "reconnect_alert_text": "Sei sicuro di volerti riconnettere?", "reconnect_your_hardware_wallet": "Ricollega il tuo portafoglio hardware", "reconnection": "Riconnessione", - "red_dark_theme": "Red Dark Theme", - "red_light_theme": "Tema della luce rossa", - "redeemed": "Redento", + "red_dark_theme": "Tema rosso scuro", + "red_light_theme": "Tema rosso chiaro", + "redeemed": "Riscattato", "refund_address": "Indirizzo di rimborso", - "reject": "Rifiutare", + "reject": "Rifiuta", "remaining": "rimanente", - "remove": "Remuovi", + "remove": "Rimuovi", "remove_node": "Rimuovi nodo", "remove_node_message": "Sei sicuro di voler rimuovere il nodo selezionato?", "rename": "Rinomina", @@ -569,15 +568,15 @@ "rep_warning_sub": "Il tuo rappresentante non sembra essere in regola. Tocca qui per selezionarne uno nuovo", "repeat_wallet_password": "Ripeti la password del portafoglio", "repeated_password_is_incorrect": "La password ripetuta non è corretta. Si prega di ripetere di nuovo la password del portafoglio.", - "require_for_adding_contacts": "Richiesto per l'aggiunta di contatti", + "require_for_adding_contacts": "Richiedi per l'aggiunta di contatti", "require_for_all_security_and_backup_settings": "Richiedi per tutte le impostazioni di sicurezza e backup", - "require_for_assessing_wallet": "Richiesto per l'accesso al portafoglio", - "require_for_creating_new_wallets": "Richiesto per la creazione di nuovi portafogli", - "require_for_exchanges_to_external_wallets": "Richiede scambi con portafogli esterni", + "require_for_assessing_wallet": "Richiedi per l'accesso al portafoglio", + "require_for_creating_new_wallets": "Richiedi per la creazione di nuovi portafogli", + "require_for_exchanges_to_external_wallets": "Richiedi per scambi con portafogli esterni", "require_for_exchanges_to_internal_wallets": "Richiedi per gli scambi ai portafogli interni", "require_for_sends_to_contacts": "Richiedi per gli invii ai contatti", - "require_for_sends_to_internal_wallets": "Richiesto per invii a portafogli interni", - "require_for_sends_to_non_contacts": "Richiesto per invii a non contatti", + "require_for_sends_to_internal_wallets": "Richiedi per invii a portafogli interni", + "require_for_sends_to_non_contacts": "Richiedi per invii a non contatti", "require_pin_after": "Richiedi PIN dopo", "rescan": "Scansiona di nuovo", "resend_code": "Per favore, invialo nuovamente", @@ -589,13 +588,13 @@ "restore_bitcoin_description_from_seed": "Recupera il tuo portafoglio da una combinazione di 12 parole", "restore_bitcoin_title_from_keys": "Recupera da WIF", "restore_description_from_backup": "Puoi recuperare l'app Cake Wallet per intero dal tuo file di backup", - "restore_description_from_hardware_wallet": "Ripristina da un portafoglio hardware di libro mastro", + "restore_description_from_hardware_wallet": "Ripristina da un portafoglio hardware Ledger", "restore_description_from_keys": "Recupera il tuo portafoglio da una sequenza di caratteri generati dalle tue chiavi private", "restore_description_from_seed": "Recupera il tuo portafoglio da una combinazione di 25 o 13 parole", "restore_description_from_seed_keys": "Recupera il tuo portafoglio dal seme/chiavi che hai salvato in un posto sicuro", - "restore_existing_wallet": "Ripristina il portafoglio esistente", - "restore_from_date_or_blockheight": "Gentilmente inserisci la data di un paio di giorni prima che hai creato questo portafoglio. Oppure inserisci l'altezza del blocco se la conosci", - "restore_from_seed_placeholder": "Gentilmente inserisci o incolla il tuo seme qui", + "restore_existing_wallet": "Ripristina un portafoglio esistente", + "restore_from_date_or_blockheight": "Inserisci la data un paio di giorni prima della creazione di questo portafoglio. Oppure inserisci l'altezza del blocco, se la conosci", + "restore_from_seed_placeholder": "Inserisci o incolla il tuo seme qui", "restore_new_seed": "Nuovo seme", "restore_next": "Prossimo", "restore_recover": "Recupera", @@ -610,11 +609,11 @@ "restore_view_key_private": "Chiave di Visualizzazione (privata)", "restore_wallet": "Recupera Portafoglio", "restore_wallet_name": "Nome Portafoglio", - "restore_wallet_restore_description": "Descrizione recupero Portafoglio", + "restore_wallet_restore_description": "Descrizione recupero portafoglio", "robinhood_option_description": "Acquista e trasferisci istantaneamente utilizzando la carta di debito, il conto bancario o il saldo di Robinhood. Solo USA.", "router_no_route": "Nessun percorso definito per ${name}", "save": "Salva", - "save_backup_password": "Gentilmente assicurati di aver salvato la password del tuo backup. Senza questa non sarai in grado di importare i tuoi file di backup.", + "save_backup_password": "Assicurati di aver salvato la password del tuo backup. Senza questa non sarai in grado di importare i tuoi file di backup.", "save_backup_password_alert": "Salva password Backup", "save_to_downloads": "Salva in Download", "saved_the_trade_id": "Ho salvato l'ID dello scambio", @@ -636,30 +635,30 @@ "seed_alert_yes": "Sì, l'ho fatto", "seed_choose": "Scegli la lingua del seme", "seed_display_path": "Menu -> Sicurezza e backup -> Mostra chiave/Semi", - "seed_hex_form": "Seme di portafoglio (forma esadecimale)", - "seed_key": "Chiave di semi", - "seed_language": "Linguaggio di semi", + "seed_hex_form": "Seme del portafoglio (formato esadecimale (hex))", + "seed_key": "Chiave del seme", + "seed_language": "Lingua del seme", "seed_language_chinese": "Cinese", "seed_language_chinese_traditional": "Cinese tradizionale)", - "seed_language_czech": "ceco", + "seed_language_czech": "Ceco", "seed_language_dutch": "Olandese", "seed_language_english": "Inglese", "seed_language_french": "Francese", "seed_language_german": "Tedesco", "seed_language_italian": "Italiano", "seed_language_japanese": "Giapponese", - "seed_language_korean": "coreano", + "seed_language_korean": "Coreano", "seed_language_next": "Prossimo", "seed_language_portuguese": "Portoghese", "seed_language_russian": "Russo", "seed_language_spanish": "Spagnolo", - "seed_phrase_length": "Lunghezza della frase seed", + "seed_phrase_length": "Lunghezza della frase del seme", "seed_position_question_one": "Qual è il", - "seed_position_question_two": "Parola della tua frase di semi?", + "seed_position_question_two": "Parola della tua frase del seme?", "seed_reminder": "Gentilmente trascrivi le parole. Ti tornerà utile in caso perdessi o ripristinassi il tuo telefono", "seed_share": "Condividi seme", "seed_title": "Seme", - "seed_verified": "Semi verificato", + "seed_verified": "Seme verificato", "seed_verified_subtext": "Puoi usare il tuo seme salvato in seguito per ripristinare questo portafoglio in caso di corruzione o perdere il dispositivo. \n\n Puoi visualizzare di nuovo questo seme dal", "seedtype": "Seedtype", "seedtype_alert_content": "La condivisione di semi con altri portafogli è possibile solo con Bip39 SeedType.", @@ -668,21 +667,21 @@ "seedtype_polyseed": "Polyseed (16 parole)", "seedtype_wownero": "Wownero (14 parole)", "select_backup_file": "Seleziona file di backup", - "select_buy_provider_notice": "Seleziona un fornitore di acquisto sopra. È possibile saltare questa schermata impostando il provider di acquisto predefinito nelle impostazioni dell'app.", + "select_buy_provider_notice": "Seleziona un provider di acquisto sopra. È possibile saltare questa schermata impostando il provider di acquisto predefinito nelle impostazioni dell'app.", "select_destination": "Seleziona la destinazione per il file di backup.", - "select_hw_account_below": "Seleziona quale account ripristina di seguito:", - "select_sell_provider_notice": "Seleziona un fornitore di vendita sopra. Puoi saltare questa schermata impostando il tuo fornitore di vendita predefinito nelle impostazioni dell'app.", + "select_hw_account_below": "Seleziona quale account ripristinare:", + "select_sell_provider_notice": "Seleziona un provider di vendita sopra. Puoi saltare questa schermata impostando il tuo provider di vendita predefinito nelle impostazioni dell'app.", "select_your_country": "Seleziona il tuo paese", - "sell": "Vendere", + "sell": "Vendi", "sell_alert_content": "Al momento supportiamo solo la vendita di Bitcoin, Ethereum e Litecoin. Crea o passa al tuo portafoglio Bitcoin, Ethereum o Litecoin.", "sell_monero_com_alert_content": "La vendita di Monero non è ancora supportata", "send": "Invia", "send_address": "${cryptoCurrency} indirizzo", - "send_amount": "Ammontare:", - "send_change_to_you": "Cambiamento, a te:", + "send_amount": "Importo:", + "send_change_to_you": "Modifica, a te:", "send_creating_transaction": "Creazione della transazione", - "send_error_currency": "L'ammontare può contenere solo numeri", - "send_error_minimum_value": "L'ammontare minimo è 0.01", + "send_error_currency": "L'importo può contenere solo numeri", + "send_error_minimum_value": "L'importo minimo è 0.01", "send_estimated_fee": "Commissione stimata:", "send_fee": "Commissione:", "send_name": "Nome", @@ -698,8 +697,8 @@ "send_your_wallet": "Il tuo portafoglio", "sending": "Invio", "sent": "Inviato", - "service_health_disabled": "Il Bollettino sanitario di servizio è disabilitato", - "service_health_disabled_message": "Questa è la pagina del Bollettino sanitario del servizio, è possibile abilitare questa pagina in Impostazioni -> Privacy", + "service_health_disabled": "Il Bollettino dello stato del servizio è disabilitato", + "service_health_disabled_message": "Questa è la pagina del Bollettino dello stato del servizio, è possibile abilitare questa pagina in Impostazioni -> Privacy", "set_a_pin": "Imposta un pin", "set_up_a_wallet": "Imposta un portafoglio", "settings": "Impostazioni", @@ -708,7 +707,7 @@ "settings_can_be_changed_later": "Queste impostazioni possono essere modificate in seguito nelle impostazioni dell'app", "settings_change_language": "Cambia lingua", "settings_change_pin": "Cambia PIN", - "settings_currency": "Moneta", + "settings_currency": "Valuta", "settings_current_node": "Nodo attuale", "settings_dark_mode": "Tema scuro", "settings_display_balance": "Mostra saldo", @@ -726,25 +725,25 @@ "settings_trades": "Scambi", "settings_transactions": "Transazioni", "settings_wallets": "Portafogli", - "setup_2fa": "Imposta la torta 2FA", + "setup_2fa": "Imposta Cake 2FA", "setup_2fa_text": "Cake 2FA funziona utilizzando TOTP come secondo fattore di autenticazione.\n\nIl TOTP di Cake 2FA richiede il supporto SHA-512 e 8 cifre; ciò fornisce una maggiore sicurezza. Maggiori informazioni e app supportate sono disponibili nella guida.", "setup_pin": "Imposta PIN", "setup_successful": "Il tuo PIN è stato impostato con successo!", "setup_totp_recommended": "Imposta TOTP", - "setup_warning_2fa_text": "Dovrai ripristinare il tuo portafoglio dal seme mnemonico.\n\nIl supporto di Cake non sarà in grado di assisterti se perdi l'accesso ai tuoi seed 2FA o mnemonici.\nCake 2FA è una seconda autenticazione per determinate azioni nel portafoglio. Prima di utilizzare Cake 2FA, ti consigliamo di leggere la guida.NON è sicuro come la conservazione a freddo.\n\nSe perdi l'accesso alla tua app 2FA o alle chiavi TOTP, perderai l'accesso a questo portafoglio. ", + "setup_warning_2fa_text": "Cake 2FA è un secondo fattore di autenticazione per determinate azioni nel portafoglio. NON è sicuro quanto il cold storage.\n\nSe perdi l'accesso alla tua app 2FA o chiavi TOTP, NON POTRAI ACCEDERE a questo portafoglio. Dovrai recuperare il portafoglio dal seme mnemonico.\n\nIl supporto di Cakenon potrà assisterti se perdi l'accesso alla tua 2FA, o semi mnemonici.\nPrima di usare Cake 2FA ti consigliamo di leggere la guida.", "setup_your_debit_card": "Configura la tua carta di debito", - "share": "Condividere", + "share": "Condividi", "share_address": "Condividi indirizzo", - "shared_seed_wallet_groups": "Gruppi di portafoglio di semi condivisi", - "show": "Spettacolo", + "shared_seed_wallet_groups": "Gruppi di portafogli di semi condivisi", + "show": "Mostra", "show_address_book_popup": "Mostra popup della rubrica", - "show_balance": "Lunga stampa per mostrare l'equilibrio", - "show_balance_toast": "A lungo pressa per nascondere o mostrare l'equilibrio", + "show_balance": "Tieni premuto per mostrare l'importo", + "show_balance_toast": "Tieni premuto per nascondere o mostrare l'importo", "show_details": "Mostra dettagli", "show_keys": "Mostra seme/chiavi", "show_market_place": "Mostra mercato", "show_seed": "Mostra seme", - "sign_message": "Messaggio di firma", + "sign_message": "Firma messaggio", "sign_up": "Registrati", "sign_verify_message": "Firmare / verificare", "sign_verify_message_sub": "Firma o verifica un messaggio utilizzando la chiave privata", @@ -753,23 +752,23 @@ "signature_invalid_error": "La firma non è valida per il messaggio dato", "signTransaction": "Firma la transazione", "signup_for_card_accept_terms": "Registrati per la carta e accetta i termini.", - "silent_payment": "Pagamento silenzioso", - "silent_payments": "Pagamenti silenziosi", - "silent_payments_always_scan": "Impostare i pagamenti silenziosi che scansionano sempre", + "silent_payment": "Silent Payment", + "silent_payments": "Silent Payments", + "silent_payments_always_scan": "Impostare la scansione perpetua di Silent Payments", "silent_payments_disclaimer": "I nuovi indirizzi non sono nuove identità. È un riutilizzo di un'identità esistente con un'etichetta diversa.", - "silent_payments_display_card": "Mostra la carta di pagamenti silenziosi", + "silent_payments_display_card": "Mostra la carta di Silent Payments", "silent_payments_scan_from_date": "Scansionare dalla data", - "silent_payments_scan_from_date_or_blockheight": "Inserisci l'altezza del blocco che si desidera iniziare la scansione per i pagamenti silenziosi in arrivo o, utilizza invece la data. Puoi scegliere se il portafoglio continua a scansionare ogni blocco o controlla solo l'altezza specificata.", + "silent_payments_scan_from_date_or_blockheight": "Inserisci l'altezza del blocco da cui si desidera iniziare la scansione per i pagamenti Silent Payments in arrivo, o utilizza la data. Puoi scegliere se il portafoglio continua a scansionare ogni blocco o controlla solo l'altezza specificata.", "silent_payments_scan_from_height": "Scansione dall'altezza del blocco", - "silent_payments_scanned_tip": "Scansionato per dare la mancia! (${tip})", - "silent_payments_scanning": "Scansione di pagamenti silenziosi", - "silent_payments_settings": "Impostazioni di pagamenti silenziosi", + "silent_payments_scanned_tip": "Scansionato alla punta! (${tip})", + "silent_payments_scanning": "Scansione Silent Payments", + "silent_payments_settings": "Impostazioni Silent Payments", "single_seed_wallets_group": "Portafogli singoli", "slidable": "Scorrevole", - "solana_create_associated_token_account_exception": "Errore Creazione di account token associato per l'indirizzo dei ricevimenti.", + "solana_create_associated_token_account_exception": "Errore nella creazione di un account token associato per l'indirizzo destinatario.", "solana_no_associated_token_account_exception": "Non esiste un account token associato per questo indirizzo.", - "solana_sign_native_transaction_rent_exception": "La transazione non può essere completata. Sol insufficiente lasciato in affitto dopo la transazione. Si prega di ricaricare il saldo SOL o ridurre la quantità di sol che stai inviando.", - "solana_sign_spl_token_transaction_rent_exception": "La transazione non può essere completata. Sol insufficiente lasciato in affitto dopo la transazione. Si prega di ricaricare il tuo equilibrio sol.", + "solana_sign_native_transaction_rent_exception": "La transazione non può essere completata. SOL insufficienti per l'affitto dopo la transazione. Si prega di ricaricare il saldo SOL, o ridurre la quantità di SOL che stai inviando.", + "solana_sign_spl_token_transaction_rent_exception": "La transazione non può essere completata. SOL insufficienti per l'affitto dopo la transazione. Si prega di ricaricare il tuo saldo SOL.", "sort_by": "Ordina per", "spend_key_private": "Chiave di spesa (privata)", "spend_key_public": "Chiave di spesa (pubblica)", @@ -780,31 +779,31 @@ "submit_request": "invia una richiesta", "successful": "Riuscito", "support_description_guides": "Documentazione e supporto per problemi comuni", - "support_description_live_chat": "Gratuito e veloce! I rappresentanti di supporto qualificati sono disponibili per assistere", - "support_description_other_links": "Unisciti alle nostre comunità o raggiungici i nostri partner attraverso altri metodi", - "support_title_guides": "Documenti del portafoglio per torta", - "support_title_live_chat": "Supporto dal vivo", + "support_description_live_chat": "Gratuito e veloce! I rappresentanti di supporto qualificati sono disponibili per assistervi", + "support_description_other_links": "Unisciti alle nostre comunità, o contatta i nostri partner attraverso altri metodi", + "support_title_guides": "Documentazione Cake Wallet", + "support_title_live_chat": "Supporto in tempo reale", "support_title_other_links": "Altri collegamenti di supporto", "swap": "Scambio", - "sweeping_wallet": "Portafoglio ampio", - "sweeping_wallet_alert": "Questo non dovrebbe richiedere molto tempo. NON LASCIARE QUESTA SCHERMATA O I FONDI SPAZZATI POTREBBERO ANDARE PERSI", + "sweeping_wallet": "Svuota portafoglio", + "sweeping_wallet_alert": "Questo non dovrebbe richiedere molto tempo. NON LASCIARE QUESTA SCHERMATA O I FONDI DA TRASFERIRE POTREBBERO ANDARE PERSI", "switchToETHWallet": "Passa a un portafoglio Ethereum e riprova", "switchToEVMCompatibleWallet": "Passa a un portafoglio compatibile con EVM e riprova (Ethereum, Polygon)", "symbol": "Simbolo", "sync_all_wallets": "Sincronizza tutti i portafogli", - "sync_status_attempting_scan": "Tentando la scansione", + "sync_status_attempting_scan": "TENTATIVO DI SCANSIONE", "sync_status_attempting_sync": "TENTATIVO DI SINCRONIZZAZIONE", "sync_status_connected": "CONNESSO", "sync_status_connecting": "CONNESSIONE", "sync_status_failed_connect": "DISCONNESSO", "sync_status_not_connected": "NON CONNESSO", - "sync_status_starting_scan": "Avvia scansione (da ${height})", - "sync_status_starting_sync": "INIZIO SINC", + "sync_status_starting_scan": "AVVIO SCANSIONE (da ${height})", + "sync_status_starting_sync": "INIZIO SINCRONIZZAZIONE", "sync_status_syncronized": "SINCRONIZZATO", "sync_status_syncronizing": "SINCRONIZZAZIONE", - "sync_status_timed_out": "FUORI TEMPO", - "sync_status_unsupported": "Nodo non supportato", - "syncing_wallet_alert_content": "Il saldo e l'elenco delle transazioni potrebbero non essere completi fino a quando non viene visualizzato \"SYNCHRONIZED\" in alto. Clicca/tocca per saperne di più.", + "sync_status_timed_out": "TIMED OUT", + "sync_status_unsupported": "NODO NON SUPPORTATO", + "syncing_wallet_alert_content": "Il saldo e l'elenco delle transazioni potrebbero non essere completi fin quando non viene visualizzato \"SINCRONIZZATO\" in alto. Clicca/tocca per saperne di più.", "syncing_wallet_alert_title": "Il tuo portafoglio si sta sincronizzando", "template": "Modello", "template_name": "Nome modello", @@ -827,7 +826,7 @@ "total": "Totale", "total_saving": "Risparmio totale", "totp_2fa_failure": "Codice non corretto. Prova un codice diverso o genera una nuova chiave segreta. Utilizza un'app 2FA compatibile che supporti codici a 8 cifre e SHA512.", - "totp_2fa_success": "Successo! Cake 2FA abilitato per questo portafoglio. Ricordati di salvare il tuo seme mnemonico nel caso in cui perdi l'accesso al portafoglio.", + "totp_2fa_success": "Successo! Cake 2FA abilitato per questo portafoglio. Ricordati di salvare il tuo seme mnemonico nel caso tu perda l'accesso al portafoglio.", "totp_auth_url": "URL DI AUT. TOTP", "totp_code": "Codice TOTP", "totp_secret_code": "TOTP codice segreto", @@ -848,7 +847,7 @@ "trade_is_powered_by": "Questo scambio è fornito da ${provider}", "trade_not_created": "Scambio non creato", "trade_not_found": "Scambio non trovato.", - "trade_state_btc_sent": "Btc inviati", + "trade_state_btc_sent": "BTC inviati", "trade_state_complete": "Completato", "trade_state_confirming": "Conferma", "trade_state_created": "Creato", @@ -864,48 +863,48 @@ "trade_state_unpaid": "Non pagato", "trades": "Scambi", "transaction_cost": "Costo delle transazioni", - "transaction_details_amount": "Ammontare", + "transaction_details_amount": "Importo", "transaction_details_copied": "${title} copiati negli Appunti", "transaction_details_date": "Data", "transaction_details_fee": "Commissione", "transaction_details_height": "Altezza", "transaction_details_recipient_address": "Indirizzi dei destinatari", - "transaction_details_source_address": "Indirizzo di partenza", + "transaction_details_source_address": "Indirizzo mittente", "transaction_details_title": "Dettagli Transazione", "transaction_details_transaction_id": "ID Transazione", "transaction_key": "Chiave Transazione", - "transaction_priority_fast": "Alta", - "transaction_priority_fastest": "Massima", - "transaction_priority_medium": "Media", + "transaction_priority_fast": "Veloce", + "transaction_priority_fastest": "Più veloce", + "transaction_priority_medium": "Medio", "transaction_priority_regular": "Regolare", - "transaction_priority_slow": "Bassa", + "transaction_priority_slow": "Lento", "transaction_sent": "Transazione inviata!", - "transaction_sent_notice": "Se lo schermo non procede dopo 1 minuto, controlla un block explorer e la tua email.", + "transaction_sent_notice": "Se la schermata non procede dopo 1 minuto, controlla un block explorer e la tua email.", "transactions": "Transazioni", "transactions_by_date": "Transazioni per data", - "trongrid_history": "Storia del trongride", - "trusted": "di fiducia", + "trongrid_history": "Cronologia TronGrid", + "trusted": "Fidato", "tx_commit_exception_no_dust_on_change": "La transazione viene respinta con questo importo. Con queste monete è possibile inviare ${min} senza modifiche o ${max} che restituisce il cambiamento.", "tx_commit_failed": "Commit di transazione non riuscita. Si prega di contattare il supporto.", - "tx_commit_failed_no_peers": "La transazione non è riuscita a trasmettere, riprovare in un secondo o giù di lì", + "tx_commit_failed_no_peers": "Errore nella trasmissione della transazione, si prega di provare nuovamente", "tx_invalid_input": "Stai usando il tipo di input sbagliato per questo tipo di pagamento", - "tx_no_dust_exception": "La transazione viene respinta inviando un importo troppo piccolo. Per favore, prova ad aumentare l'importo.", - "tx_not_enough_inputs_exception": "Input non sufficienti disponibili. Seleziona di più sotto il controllo delle monete", - "tx_rejected_bip68_final": "La transazione ha input non confermati e non è stata sostituita per tassa.", - "tx_rejected_dust_change": "Transazione respinta dalle regole di rete, quantità bassa variazione (polvere). Prova a inviare tutto o ridurre l'importo.", - "tx_rejected_dust_output": "Transazione respinta dalle regole di rete, bassa quantità di output (polvere). Si prega di aumentare l'importo.", - "tx_rejected_dust_output_send_all": "Transazione respinta dalle regole di rete, bassa quantità di output (polvere). Si prega di controllare il saldo delle monete selezionate sotto controllo delle monete.", - "tx_rejected_vout_negative": "Non abbastanza saldo per pagare le commissioni di questa transazione. Si prega di controllare il saldo delle monete sotto controllo delle monete.", + "tx_no_dust_exception": "La transazione viene respinta inviando un importo troppo piccolo. Prova ad aumentare l'importo.", + "tx_not_enough_inputs_exception": "Non sono disponibili abbastanza input. Selezionane di più in Controllo monete", + "tx_rejected_bip68_final": "La transazione ha input non confermati e non è stato effettuato il replace-by-fee.", + "tx_rejected_dust_change": "Transazione respinta dalle regole di rete, bassa quantità di cambio (dust). Prova a inviare tutto o ridurre l'importo.", + "tx_rejected_dust_output": "Transazione respinta dalle regole di rete, bassa quantità di output (dust). Si prega di aumentare l'importo.", + "tx_rejected_dust_output_send_all": "Transazione respinta dalle regole di rete, bassa quantità di output (dust). Si prega di controllare il saldo delle monete selezionate sotto controllo delle monete.", + "tx_rejected_vout_negative": "Il saldo disponibile non è sufficiente per pagare le commissioni di questa transazione. Si prega di controllare il saldo delle monete sotto Controllo monete.", "tx_wrong_balance_exception": "Non hai abbastanza ${currency} per inviare questo importo.", "tx_wrong_balance_with_amount_exception": "Non hai abbastanza ${currency} per inviare la quantità totale di ${amount}", - "tx_zero_fee_exception": "Impossibile inviare transazioni con 0 tassa. Prova ad aumentare la tariffa o controlla la connessione per le ultime stime.", + "tx_zero_fee_exception": "Impossibile inviare transazioni con 0 commissioni. Prova ad aumentare le commissioni, o controlla la connessione per le ultime stime.", "unavailable_balance": "Saldo non disponibile", - "unavailable_balance_description": "Saldo non disponibile: questo totale include i fondi bloccati nelle transazioni in sospeso e quelli che hai congelato attivamente nelle impostazioni di controllo delle monete. I saldi bloccati diventeranno disponibili una volta completate le rispettive transazioni, mentre i saldi congelati rimarranno inaccessibili per le transazioni finché non deciderai di sbloccarli.", + "unavailable_balance_description": "Saldo non disponibile: questo totale include i fondi bloccati nelle transazioni in sospeso e quelli che hai congelato attivamente nelle impostazioni di Controllo monete. I saldi bloccati diventeranno disponibili una volta completate le rispettive transazioni, mentre i saldi congelati rimarranno inaccessibili per le transazioni finché non deciderai di sbloccarli.", "unconfirmed": "Saldo non confermato", "understand": "Capisco", - "unlock": "Sbloccare", + "unlock": "Sblocca", "unmatched_currencies": "La valuta del tuo portafoglio attuale non corrisponde a quella del QR scansionato", - "unspent_change": "Modifica", + "unspent_change": "Resto", "unspent_coins_details_title": "Dettagli sulle monete non spese", "unspent_coins_title": "Monete non spese", "unsupported_asset": "Non supportiamo questa azione per questa risorsa. Crea o passa a un portafoglio di un tipo di asset supportato.", @@ -927,23 +926,23 @@ "verify_with_2fa": "Verifica con Cake 2FA", "version": "Versione ${currentVersion}", "view_all": "Visualizza tutto", - "view_in_block_explorer": "View in Block Explorer", + "view_in_block_explorer": "Visualizza su Block Explorer", "view_key_private": "Chiave di visualizzazione (privata)", "view_key_public": "Chiave di visualizzazione (pubblica)", - "view_transaction_on": "View Transaction on ", + "view_transaction_on": "Visualizza transazione su ", "voting_weight": "Peso di voto", "waitFewSecondForTxUpdate": "Attendi qualche secondo affinché la transazione venga riflessa nella cronologia delle transazioni", "waiting_payment_confirmation": "In attesa di conferma del pagamento", "wallet": "Portafoglio", "wallet_group": "Gruppo di portafoglio", "wallet_group_description_existing_seed": "Hai scelto di utilizzare un seme esistente per questo portafoglio. Puoi verificare di nuovo il seme se devi confermarlo o scriverlo.", - "wallet_group_description_four": "Per creare un portafoglio con un seme completamente nuovo.", - "wallet_group_description_one": "Nel portafoglio di torte, puoi creare un", + "wallet_group_description_four": "per creare un portafoglio con un seme completamente nuovo.", + "wallet_group_description_one": "Su Cake Wallet, puoi creare un", "wallet_group_description_open_wallet": "Altrimenti, puoi continuare ad aprire il portafoglio", "wallet_group_description_three": "Per vedere la schermata di portafogli e/o gruppi di portafogli disponibili. O scegli", "wallet_group_description_two": "Selezionando un portafoglio esistente con cui condividere un seme. Ogni gruppo di portafoglio può contenere un singolo portafoglio di ciascun tipo di valuta. \n\n È possibile selezionare", "wallet_group_description_view_seed": "Puoi sempre visualizzare di nuovo questo seme sotto", - "wallet_group_empty_state_text_one": "Sembra che tu non abbia alcun gruppo di portafoglio compatibile !\n\n TAP", + "wallet_group_empty_state_text_one": "Sembra che tu non abbia alcun gruppo di portafoglio compatibile!\n\nPremi", "wallet_group_empty_state_text_two": "Di seguito per crearne uno nuovo.", "wallet_keys": "Seme Portafoglio /chiavi", "wallet_list_create_new_wallet": "Crea Nuovo Portafoglio", @@ -959,47 +958,47 @@ "wallet_list_wallet_name": "Nome del portafoglio", "wallet_menu": "Menù", "wallet_name": "Nome del Portafoglio", - "wallet_name_exists": "Il portafoglio con quel nome è già esistito", + "wallet_name_exists": "Un portafoglio con quel nome già esiste", "wallet_password_is_empty": "La password del portafoglio è vuota. La password del portafoglio non dovrebbe essere vuota", "wallet_recovery_height": "Altezza di recupero", "wallet_restoration_store_incorrect_seed_length": "Lunghezza seme non corretta", "wallet_seed": "Seme Portafoglio", - "wallet_seed_legacy": "Seme di portafoglio legacy", + "wallet_seed_legacy": "Seme portafoglio legacy", "wallet_store_monero_wallet": "Portafoglio Monero", - "walletConnect": "PortafoglioConnetti", + "walletConnect": "WalletConnect", "wallets": "Portafogli", - "warning": "Avvertimento", + "warning": "Attenzione", "welcome": "Benvenuto", - "welcome_subtitle_new_wallet": "Se vuoi ricominciare da capo, tocca Crea un nuovo portafoglio di seguito e sarai alle gare.", - "welcome_subtitle_restore_wallet": "Se hai un portafoglio esistente che vuoi portare nella torta, scegli semplicemente il ripristino del portafoglio esistente e ti guideremo attraverso il processo.", + "welcome_subtitle_new_wallet": "Se vuoi ricominciare da capo, premi Crea un nuovo portafoglio qui sotto.", + "welcome_subtitle_restore_wallet": "Se hai un portafoglio esistente che vuoi importare in Cake Wallet, scegli semplicemente il ripristino del portafoglio esistente e ti guideremo attraverso il processo.", "welcome_to_cakepay": "Benvenuto in Cake Pay!", - "what_is_silent_payments": "Che cos'è i pagamenti silenziosi?", + "what_is_silent_payments": "Che cos'è Silent Payments?", "widgets_address": "Indirizzo", "widgets_or": "o", "widgets_restore_from_blockheight": "Recupera da altezza blocco", "widgets_restore_from_date": "Recupera da data", "widgets_seed": "Seme", "wouoldLikeToConnect": "vorrei connettermi", - "write_down_backup_password": "Gentilmente trascrivi la password del backup, che è usata per importare i tuoi file di backup.", - "xlm_extra_info": "Gentilmente ricorda di indicare il Memo ID quando invii la transazione XLM per lo scambio", + "write_down_backup_password": "Trascrivi la password del backup, che è usata per importare i tuoi file di backup.", + "xlm_extra_info": "Ricorda di indicare il Memo ID quando invii la transazione XLM per lo scambio", "xmr_available_balance": "Saldo Disponibile", "xmr_full_balance": "Saldo Totale", "xmr_hidden": "Nascosto", - "xmr_to_error": "XMR.TO errore", - "xmr_to_error_description": "Ammontare invalido. Il limite massimo è 8 cifre dopo il punto decimale", - "xrp_extra_info": "Gentilmente ricorda di indicare il Tag di Destinazione quando invii una transazione XRP per lo scambio", + "xmr_to_error": "Errore XMR.TO", + "xmr_to_error_description": "Importo invalido. Il limite massimo è 8 cifre dopo il punto decimale", + "xrp_extra_info": "Ricorda di indicare il Tag di Destinazione quando invii una transazione XRP per lo scambio", "yat": "Yat", "yat_address": "Indirizzo Yat", "yat_alert_content": "Gli utenti di Cake Wallet possono ora inviare e ricevere tutte le loro valute preferite con un nome utente unico basato su emoji.", "yat_alert_title": "Invia e ricevi criptovalute più facilmente con Yat", - "yat_error": "Yat errore", + "yat_error": "Errore Yat", "yat_error_content": "Nessun indirizzo collegato a questo Yat. Prova un altro Yat", - "yat_popup_content": "Ora puoi inviare e ricevere criptovalute in Cake Wallet con il tuo Yat, un breve nome utente basato su emoji. Gestisci Yats in qualsiasi momento nella schermata delle impostazioni", - "yat_popup_title": "L'indirizzo del tuo portafoglio può essere emoji.", + "yat_popup_content": "Ora puoi inviare e ricevere criptovalute in Cake Wallet con il tuo Yat, un breve nome utente basato su emoji. Gestisci gli Yat in qualsiasi momento nella schermata delle impostazioni", + "yat_popup_title": "L'indirizzo del tuo portafoglio può essere composto da emoji.", "yesterday": "Ieri", "you_now_have_debit_card": "Ora hai una carta di debito", "you_pay": "Tu paghi", "you_will_get": "Converti a", "you_will_send": "Conveti da", "yy": "YY" -} \ No newline at end of file +} From 81ca38857fcf581ad197ad6135303418c827fe55 Mon Sep 17 00:00:00 2001 From: Serhii Date: Tue, 25 Feb 2025 03:10:10 +0200 Subject: [PATCH 007/243] fix fiat amount on token change (#2042) --- lib/src/screens/send/widgets/send_card.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index eed0ce233..24cbd2061 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -459,6 +459,14 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin sendViewModel.selectedCryptoCurrency, (Currency currency) { + if (output.sendAll) { + output.setSendAll(sendViewModel.balance); + } + + output.setCryptoAmount(cryptoAmountController.text); + }); + reaction((_) => output.fiatAmount, (String amount) { if (amount != fiatAmountController.text) { fiatAmountController.text = amount; From 1f08596a753fa08d016f9f3016e754acf4e0efa0 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Wed, 26 Feb 2025 16:42:10 +0100 Subject: [PATCH 008/243] Update dfx subdomain (#2053) --- lib/buy/dfx/dfx_buy_provider.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/buy/dfx/dfx_buy_provider.dart b/lib/buy/dfx/dfx_buy_provider.dart index c11a0d771..eba48632b 100644 --- a/lib/buy/dfx/dfx_buy_provider.dart +++ b/lib/buy/dfx/dfx_buy_provider.dart @@ -310,7 +310,7 @@ class DFXBuyProvider extends BuyProvider { final accessToken = await auth(cryptoCurrencyAddress); - final uri = Uri.https('services.dfx.swiss', actionType, { + final uri = Uri.https('app.dfx.swiss', actionType, { 'session': accessToken, 'lang': 'en', 'asset-out': isBuyAction ? quote.cryptoCurrency.toString() : quote.fiatCurrency.toString(), From 3a56277c27ebe2172d6c643e3cc2b6986a95c029 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Thu, 27 Feb 2025 05:49:14 +0200 Subject: [PATCH 009/243] Minor locale change [skip ci] --- 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_fr.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 | 2 +- res/values/strings_ko.arb | 2 +- res/values/strings_my.arb | 2 +- res/values/strings_nl.arb | 2 +- res/values/strings_pt.arb | 2 +- res/values/strings_ru.arb | 2 +- res/values/strings_tl.arb | 2 +- res/values/strings_tr.arb | 2 +- res/values/strings_uk.arb | 2 +- res/values/strings_ur.arb | 2 +- res/values/strings_vi.arb | 2 +- res/values/strings_yo.arb | 2 +- res/values/strings_zh.arb | 2 +- 26 files changed, 27 insertions(+), 27 deletions(-) diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 2fcba584c..afa3676b3 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -175,7 +175,7 @@ "contact_name_exists": " .ﻒﻠﺘﺨﻣ ﻢﺳﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ .ﻞﻌﻔﻟﺎﺑ ﺓﺩﻮﺟﻮﻣ ﻢﺳﻻﺍ ﺍﺬﻬﺑ ﻝﺎﺼﺗﺍ ﺔﻬﺟ", "contact_support": "اتصل بالدعم", "continue_text": "التالي", - "contract_warning": "تم وضع علامة على عنوان العقد هذا على أنه احتيالي محتمل. يرجى المعالجة بحذر.", + "contract_warning": "تم وضع علامة على عنوان العقد هذا على أنه احتيالي محتمل. يرجى المتابعة بحذر.", "contractName": "ﺪﻘﻌﻟﺍ ﻢﺳﺍ", "contractSymbol": "ﺪﻘﻌﻟﺍ ﺰﻣﺭ", "copied_key_to_clipboard": "تم نسخ ${key} إلى الحافظة", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index b79f8c8f7..9d8e8b8f8 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -175,7 +175,7 @@ "contact_name_exists": "Вече съществува контакт с това име. Моля, изберете друго име.", "contact_support": "Свържи се с отдел поддръжка", "continue_text": "Напред", - "contract_warning": "Този адрес на договора е маркиран като потенциално измамник. Моля, обработете с повишено внимание.", + "contract_warning": "Този адрес на договора е маркиран като потенциално измамник. Моля, продължете с повишено внимание.", "contractName": "Име на договора", "contractSymbol": "Договор Символ", "copied_key_to_clipboard": "Копиран ключ: ${key}", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 391513fda..4ba213d65 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -175,7 +175,7 @@ "contact_name_exists": "Kontakt s tímto jménem již existuje. Vyberte prosím jiný název.", "contact_support": "Kontaktovat podporu", "continue_text": "Pokračovat", - "contract_warning": "Tato adresa smlouvy byla označena jako potenciálně podvodná. Zpracovejte prosím opatrně.", + "contract_warning": "Tato adresa smlouvy byla označena jako potenciálně podvodná. Pokračujte opatrně.", "contractName": "Název smlouvy", "contractSymbol": "Symbol smlouvy", "copied_key_to_clipboard": "Zkopírován ${key} do schránky", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 641e99125..c807b3ecc 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -175,7 +175,7 @@ "contact_name_exists": "Ein Kontakt mit diesem Namen besteht bereits. Bitte wählen Sie einen anderen Namen.", "contact_support": "Support kontaktieren", "continue_text": "Weiter", - "contract_warning": "Diese Contract Adresse wurde als potenziell betrügerisch gekennzeichnet. Bitte fahren Sie mit Vorsicht fort.", + "contract_warning": "Diese Vertragsadresse wurde als potenziell betrügerisch gekennzeichnet. Bitte gehen Sie mit Vorsicht vor.", "contractName": "Contract-Name", "contractSymbol": "Contract-Symbol", "copied_key_to_clipboard": "${key} in Zwischenablage kopiert", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 08ecad2a5..a02cabe55 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -175,7 +175,7 @@ "contact_name_exists": "A contact with that name already exists. Please choose a different name.", "contact_support": "Contact Support", "continue_text": "Continue", - "contract_warning": "This contract address has been flagged as potentially fraudulent. Please process with caution.", + "contract_warning": "This contract address has been flagged as potentially fraudulent. Please proceed with caution.", "contractName": "Contract Name", "contractSymbol": "Contract Symbol", "copied_key_to_clipboard": "Copied ${key} to Clipboard", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index c5220d580..cdf1bac98 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -175,7 +175,7 @@ "contact_name_exists": "Ya existe un contacto con ese nombre. Elija un nombre diferente.", "contact_support": "Contactar con Soporte", "continue_text": "Continuar", - "contract_warning": "Esta dirección de contrato ha sido marcada como potencialmente fraudulenta. Por favor, procese con precaución.", + "contract_warning": "Esta dirección de contrato ha sido marcada como potencialmente fraudulenta. Proceda con precaución.", "contractName": "Nombre del contrato", "contractSymbol": "Símbolo de contrato", "copied_key_to_clipboard": "Copiado ${key} al portapapeles", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 8e9cb197a..69fa3c31c 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -175,7 +175,7 @@ "contact_name_exists": "Un contact portant ce nom existe déjà. Veuillez choisir un autre nom.", "contact_support": "Contacter l'assistance", "continue_text": "Continuer", - "contract_warning": "Cette adresse contractuelle a été signalée comme potentiellement frauduleuse. Veuillez traiter avec prudence.", + "contract_warning": "Cette adresse contractuelle a été signalée comme potentiellement frauduleuse. Veuillez procéder avec prudence.", "contractName": "Nom du contrat", "contractSymbol": "Symbole du contrat", "copied_key_to_clipboard": "${key} copiée vers le presse-papier", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 3c870d6f1..3667da5f6 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -175,7 +175,7 @@ "contact_name_exists": "An riga an sami lamba tare da wannan sunan. Da fatan za a zaɓi suna daban.", "contact_support": "Tuntuɓi Support", "continue_text": "Ci gaba", - "contract_warning": "An kafa wannan adireshin kwantaragin kwangilar yayin da yuwuwar zamba. Da fatan za a aiwatar da taka tsantsan.", + "contract_warning": "An kafa wannan adireshin kwantaragin kwangilar yayin da yuwuwar zamba. Da fatan za a ci gaba da taka tsantsan.", "contractName": "Sunan Kwangila", "contractSymbol": "Alamar Kwangila", "copied_key_to_clipboard": "An kwafa ${key} a cikin kwafin", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 5ee761ed4..53ddaea29 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -175,7 +175,7 @@ "contact_name_exists": "उस नाम का एक संपर्क पहले से मौजूद है. कृपया कोई भिन्न नाम चुनें.", "contact_support": "सहायता से संपर्क करें", "continue_text": "जारी रहना", - "contract_warning": "इस अनुबंध के पते को संभावित रूप से धोखाधड़ी के रूप में चिह्नित किया गया है। कृपया सावधानी के साथ प्रक्रिया करें।", + "contract_warning": "इस अनुबंध के पते को संभावित रूप से धोखाधड़ी के रूप में चिह्नित किया गया है। कृपया सावधानी के साथ आगे बढ़ें।", "contractName": "अनुबंध का नाम", "contractSymbol": "अनुबंध चिह्न", "copied_key_to_clipboard": "की नकल की ${key} क्लिपबोर्ड पर", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 02bb9960c..88a481430 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -175,7 +175,7 @@ "contact_name_exists": "Kontakt s tim imenom već postoji. Odaberite drugo ime.", "contact_support": "Kontaktirajte podršku", "continue_text": "Nastavak", - "contract_warning": "Ova adresa ugovora označena je kao potencijalno lažna. Molimo obradite s oprezom.", + "contract_warning": "Ova adresa ugovora označena je kao potencijalno lažna. Molimo nastavite s oprezom.", "contractName": "Naziv ugovora", "contractSymbol": "Simbol ugovora", "copied_key_to_clipboard": "${key} kopiran u međuspremnik", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 64b3dc2d8..087354ef2 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -175,7 +175,7 @@ "contact_name_exists": "Այդ անվանման հետ կապ կա արդեն: Խնդրում ենք ընտրել այլ անուն:", "contact_support": "Հետադարձ կապ", "continue_text": "Շարունակել", - "contract_warning": "Պայմանագրի այս հասցեն դրոշմել է որպես հնարավոր կեղծ: Խնդրում ենք զգուշությամբ մշակել:", + "contract_warning": "Պայմանագրի այս հասցեն դրոշմել է որպես հնարավոր կեղծ: Խնդրում ենք զգուշությամբ շարունակեք:", "contractName": "Գործարքի անուն", "contractSymbol": "Գործարքի Նշան", "copied_key_to_clipboard": "${key} պատճենված է տեքստի բուֆերում", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 6f3aaf898..cb9f648bb 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -175,7 +175,7 @@ "contact_name_exists": "Kontak dengan nama tersebut sudah ada. Silakan pilih nama lain.", "contact_support": "Hubungi Dukungan", "continue_text": "Lanjutkan", - "contract_warning": "Alamat kontrak ini telah ditandai sebagai berpotensi curang. Silakan memproses dengan hati -hati.", + "contract_warning": "Alamat kontrak ini telah ditandai sebagai berpotensi curang. Silakan lanjutkan dengan hati -hati.", "contractName": "Nama Kontrak", "contractSymbol": "Simbol Kontrak", "copied_key_to_clipboard": "Kunci ${key} disalin ke Clipboard", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index d894f8ac4..a48ee65e8 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -175,7 +175,7 @@ "contact_name_exists": "Esiste già un contatto con quel nome. Scegli un nome diverso.", "contact_support": "Contatta l'assistenza", "continue_text": "Continua", - "contract_warning": "L'indirizzo di questo contratto è stato contrassegnato come potenzialmente fraudolento. Si prega di procedere con cautela.", + "contract_warning": "Questo indirizzo del contratto è stato contrassegnato come potenzialmente fraudolento. Si prega di procedere con cautela.", "contractName": "Nome Contratto", "contractSymbol": "Simbolo del Contratto", "copied_key_to_clipboard": " ${key} copiata negli Appunti", @@ -1001,4 +1001,4 @@ "you_will_get": "Converti a", "you_will_send": "Conveti da", "yy": "YY" -} +} \ No newline at end of file diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 759319dcd..a23b5f705 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -175,7 +175,7 @@ "contact_name_exists": "その名前の連絡先はすでに存在します。別の名前を選択してください。", "contact_support": "サポートに連絡する", "continue_text": "持続する", - "contract_warning": "この契約住所は、潜在的に不正としてフラグが立てられています。注意して処理してください。", + "contract_warning": "この契約住所は、潜在的に不正としてフラグが立てられています。注意して進めてください。", "contractName": "契約名", "contractSymbol": "契約記号", "copied_key_to_clipboard": "コピー済み ${key} クリップボードへ", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 3bf10d786..9de4769cf 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -175,7 +175,7 @@ "contact_name_exists": "해당 이름을 가진 연락처가 이미 존재합니다. 다른 이름을 선택하세요.", "contact_support": "지원팀에 문의", "continue_text": "잇다", - "contract_warning": "이 계약 주소는 잠재적으로 사기성으로 표시되었습니다. 주의해서 처리하십시오.", + "contract_warning": "이 계약 주소는 잠재적으로 사기성으로 표시되었습니다. 주의해서 계속 진행하십시오.", "contractName": "계약명", "contractSymbol": "계약 기호", "copied_key_to_clipboard": "복사 ${key} 클립 보드로", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 6dad99f36..66000dde5 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -175,7 +175,7 @@ "contact_name_exists": "ထိုအမည်နှင့် အဆက်အသွယ်တစ်ခု ရှိနှင့်ပြီးဖြစ်သည်။ အခြားအမည်တစ်ခုကို ရွေးပါ။", "contact_support": "ပံ့ပိုးကူညီမှုထံ ဆက်သွယ်ပါ။", "continue_text": "ဆက်လက်", - "contract_warning": "ဒီစာချုပ်လိပ်စာအလားအလာအလားအလာအလားအလာအလံများကိုအလံလွှင့်တင်ခဲ့သည်။ ကျေးဇူးပြုပြီးသတိဖြင့်လုပ်ငန်းစဉ်။", + "contract_warning": "ဒီစာချုပ်လိပ်စာအလားအလာအလားအလာအလားအလာအလံများကိုအလံလွှင့်တင်ခဲ့သည်။ ကျေးဇူးပြု. သတိဖြင့်ဆက်လက်လုပ်ဆောင်ပါ။", "contractName": "စာချုပ်အမည်", "contractSymbol": "စာချုပ်သင်္ကေတ", "copied_key_to_clipboard": "${key} ကို Clipboard သို့ ကူးယူထားသည်။", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 7ef0dd6ca..faec44dfc 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -175,7 +175,7 @@ "contact_name_exists": "Er bestaat al een contact met die naam. Kies een andere naam.", "contact_support": "Contact opnemen met ondersteuning", "continue_text": "Doorgaan met", - "contract_warning": "Dit contractadres is gemarkeerd als mogelijk frauduleus. Verwerk met voorzichtigheid.", + "contract_warning": "Dit contractadres is gemarkeerd als mogelijk frauduleus. Ga voorzichtig te werk.", "contractName": "Contractnaam", "contractSymbol": "Contractsymbool", "copied_key_to_clipboard": "Gekopieerd ${key} naar het klembord", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 4356b64bd..dadb38064 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -175,7 +175,7 @@ "contact_name_exists": "Um contato com esse nome já existe. Escolha um nome diferente.", "contact_support": "Contatar Suporte", "continue_text": "Continuar", - "contract_warning": "Este endereço do contrato foi sinalizado como potencialmente fraudulento. Por favor, processe com cautela.", + "contract_warning": "Este endereço do contrato foi sinalizado como potencialmente fraudulento. Por favor, continue com cautela.", "contractName": "Nome do contrato", "contractSymbol": "Símbolo do Contrato", "copied_key_to_clipboard": "${key} copiada para a área de transferência", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index d354a758a..3c4ea4f98 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -175,7 +175,7 @@ "contact_name_exists": "Контакт с таким именем уже существует. Пожалуйста, выберите другое имя.", "contact_support": "Связаться со службой поддержки", "continue_text": "Продолжить", - "contract_warning": "Этот адрес контракта был отмечен как потенциально мошеннический. Пожалуйста, обработайтесь с осторожностью.", + "contract_warning": "Этот адрес контракта был отмечен как потенциально мошеннический. Пожалуйста, продолжайте с осторожностью.", "contractName": "Название контракта", "contractSymbol": "Символ контракта", "copied_key_to_clipboard": "Скопировано ${key} в буфер обмена", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index e4b8e87b5..7e3e97cb7 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -175,7 +175,7 @@ "contact_name_exists": "Ang isang pakikipag -ugnay sa pangalang iyon ay mayroon na. Mangyaring pumili ng ibang pangalan.", "contact_support": "Makipag-ugnay sa Suporta", "continue_text": "Magpatuloy", - "contract_warning": "Ang address ng kontrata na ito ay na -flag bilang potensyal na mapanlinlang. Mangyaring iproseso nang may pag -iingat.", + "contract_warning": "Ang address ng kontrata na ito ay na -flag bilang potensyal na mapanlinlang. Mangyaring magpatuloy sa pag -iingat.", "contractName": "Pangalan ng Kontrata", "contractSymbol": "Simbolo ng Kontrata", "copied_key_to_clipboard": "Kinopya ang ${key} sa Clipboard", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 38e16b0e9..d02960b51 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -175,7 +175,7 @@ "contact_name_exists": "Bu isimde bir kişi zaten mevcut. Lütfen farklı bir ad seçin.", "contact_support": "Destek ile İletişime Geç", "continue_text": "Devam et", - "contract_warning": "Bu sözleşme adresi potansiyel olarak hileli olarak işaretlenmiştir. Lütfen dikkatle işleyin.", + "contract_warning": "Bu sözleşme adresi potansiyel olarak hileli olarak işaretlenmiştir. Lütfen dikkatle devam edin.", "contractName": "Sözleşme Adı", "contractSymbol": "Sözleşme Sembolü", "copied_key_to_clipboard": "${key} panoya kopyalandı", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 4493634a8..13e6267c9 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -175,7 +175,7 @@ "contact_name_exists": "Контакт із такою назвою вже існує. Виберіть інше ім'я.", "contact_support": "Звернутися до служби підтримки", "continue_text": "Продовжити", - "contract_warning": "Ця адреса контракту була позначена як потенційно шахрайська. Будь ласка, обробляйте обережно.", + "contract_warning": "Ця адреса контракту була позначена як потенційно шахрайська. Будь ласка, продовжуйте обережно.", "contractName": "Назва контракту", "contractSymbol": "Контракт символ", "copied_key_to_clipboard": "Скопійовано ${key} в буфер обміну", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index d03dd2785..1c9f6bd6e 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -175,7 +175,7 @@ "contact_name_exists": " ۔ﮟﯾﺮﮐ ﺐﺨﺘﻨﻣ ﻡﺎﻧ ﻒﻠﺘﺨﻣ ﮏﯾﺍ ﻡﺮﮐ ﮦﺍﺮﺑ ۔ﮯﮨ ﺩﻮﺟﻮﻣ ﮯﺳ ﮯﻠﮩﭘ ﮧﻄﺑﺍﺭ ﮏﯾﺍ ﮫﺗﺎﺳ ﮯﮐ ﻡﺎﻧ ﺱﺍ", "contact_support": "سپورٹ سے رابطہ کریں۔", "continue_text": "جاری رہے", - "contract_warning": "اس معاہدے کے پتے کو ممکنہ طور پر جعلی قرار دیا گیا ہے۔ براہ کرم احتیاط کے ساتھ کارروائی کریں۔", + "contract_warning": "اس معاہدے کے پتے کو ممکنہ طور پر جعلی قرار دیا گیا ہے۔ براہ کرم احتیاط کے ساتھ آگے بڑھیں۔", "contractName": "ﻡﺎﻧ ﺎﮐ ﮦﺪﮨﺎﻌﻣ", "contractSymbol": "ﺖﻣﻼﻋ ﯽﮐ ﮦﺪﮨﺎﻌﻣ", "copied_key_to_clipboard": "${key} کو کلپ بورڈ پر کاپی کیا گیا۔", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 2d5ce9770..6e821a5ab 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -174,7 +174,7 @@ "contact_name_exists": "Một liên hệ với cái tên đó đã tồn tại. Vui lòng chọn một tên khác.", "contact_support": "Liên hệ Hỗ trợ", "continue_text": "Tiếp tục", - "contract_warning": "Địa chỉ hợp đồng này đã được gắn cờ là có khả năng lừa đảo. Vui lòng xử lý một cách thận trọng.", + "contract_warning": "Địa chỉ hợp đồng này đã được gắn cờ là có khả năng lừa đảo. Hãy tiến hành thận trọng.", "contractName": "Tên Hợp đồng", "contractSymbol": "Ký hiệu Hợp đồng", "copied_key_to_clipboard": "Đã sao chép ${key} vào khay nhớ tạm", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index b837e6a89..e3851543c 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -175,7 +175,7 @@ "contact_name_exists": "Olubasọrọ pẹlu orukọ yẹn ti wa tẹlẹ. Jọwọ yan orukọ ti o yatọ.", "contact_support": "Bá ìranlọ́wọ́ sọ̀rọ̀", "continue_text": "Tẹ̀síwájú", - "contract_warning": "Adirẹsi adehun adehun yii ti samisi bi arekereke. Jọwọ ṣe ilana pẹlu iṣọra.", + "contract_warning": "Adirẹsi adehun adehun yii ti samisi bi arekereke. Jọwọ tẹsiwaju pẹlu iṣọra.", "contractName": "Orukọ adehun", "contractSymbol": "Aami adehun", "copied_key_to_clipboard": "Ti ṣeda ${key} sí àtẹ àkọsílẹ̀", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 466acf985..a1e14a5c3 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -175,7 +175,7 @@ "contact_name_exists": "已存在具有该名称的联系人。请选择不同的名称。", "contact_support": "联系支持", "continue_text": "继续", - "contract_warning": "该合同地址已被标记为潜在的欺诈性。请谨慎处理。", + "contract_warning": "该合同地址已被标记为潜在的欺诈性。请谨慎行事。", "contractName": "合约名称", "contractSymbol": "合约符号", "copied_key_to_clipboard": "复制 ${key} 到剪贴板", From 57fe3287faffe34d65bd7119f52c9dac9aca0c49 Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Wed, 26 Feb 2025 19:49:57 -0800 Subject: [PATCH 010/243] Cw 939 whitelist known tokens (#2038) * [skip-ci] init * don't get price data for potential scam tokens * updates * dont fetch fiat price for scam currencies --- cw_core/lib/crypto_currency.dart | 4 +- lib/ethereum/cw_ethereum.dart | 5 ++ lib/polygon/cw_polygon.dart | 5 ++ lib/reactions/fiat_rate_update.dart | 5 +- lib/solana/cw_solana.dart | 5 ++ .../screens/dashboard/edit_token_page.dart | 24 ++++++++-- lib/tron/cw_tron.dart | 5 ++ .../dashboard/home_settings_view_model.dart | 47 ++++++++++++++++--- tool/configure.dart | 8 ++++ 9 files changed, 97 insertions(+), 11 deletions(-) diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 29dc03f9e..00d49c288 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -11,7 +11,8 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen this.iconPath, this.tag, this.enabled = false, - }) + this.isPotentialScam = false, + }) : super(title: title, raw: raw); final String name; @@ -20,6 +21,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen final String? iconPath; final int decimals; final bool enabled; + final bool isPotentialScam; set enabled(bool value) => this.enabled = value; diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart index 413cafc30..dc91e4fc2 100644 --- a/lib/ethereum/cw_ethereum.dart +++ b/lib/ethereum/cw_ethereum.dart @@ -209,4 +209,9 @@ class CWEthereum extends Ethereum { throw err; } } + + @override + List getDefaultTokenContractAddresses() { + return DefaultEthereumErc20Tokens().initialErc20Tokens.map((e) => e.contractAddress).toList(); + } } diff --git a/lib/polygon/cw_polygon.dart b/lib/polygon/cw_polygon.dart index 7c447406d..b8f78e9e2 100644 --- a/lib/polygon/cw_polygon.dart +++ b/lib/polygon/cw_polygon.dart @@ -208,4 +208,9 @@ class CWPolygon extends Polygon { throw err; } } + + @override + List getDefaultTokenContractAddresses() { + return DefaultPolygonErc20Tokens().initialPolygonErc20Tokens.map((e) => e.contractAddress).toList(); + } } diff --git a/lib/reactions/fiat_rate_update.dart b/lib/reactions/fiat_rate_update.dart index 62710c515..de3dea4a2 100644 --- a/lib/reactions/fiat_rate_update.dart +++ b/lib/reactions/fiat_rate_update.dart @@ -60,9 +60,12 @@ Future startFiatRateUpdate( tron!.getTronTokenCurrencies(appStore.wallet!).where((element) => element.enabled); } - if (currencies != null) { for (final currency in currencies) { + // skip potential scams: + if (currency.isPotentialScam) { + continue; + } () async { fiatConversionStore.prices[currency] = await FiatConversionService.fetchPrice( crypto: currency, diff --git a/lib/solana/cw_solana.dart b/lib/solana/cw_solana.dart index 7894f77ed..d8257396f 100644 --- a/lib/solana/cw_solana.dart +++ b/lib/solana/cw_solana.dart @@ -154,4 +154,9 @@ class CWSolana extends Solana { double? getEstimateFees(WalletBase wallet) { return (wallet as SolanaWallet).estimatedFee; } + + @override + List getDefaultTokenContractAddresses() { + return DefaultSPLTokens().initialSPLTokens.map((e) => e.mintAddress).toList(); + } } diff --git a/lib/src/screens/dashboard/edit_token_page.dart b/lib/src/screens/dashboard/edit_token_page.dart index bb0d4a3e2..11a584b39 100644 --- a/lib/src/screens/dashboard/edit_token_page.dart +++ b/lib/src/screens/dashboard/edit_token_page.dart @@ -211,6 +211,21 @@ class _EditTokenPageBodyState extends State { .checkIfERC20TokenContractAddressIsAPotentialScamAddress( _contractAddressController.text, ); + + final isWhitelisted = await widget.homeSettingsViewModel + .checkIfTokenIsWhitelisted(_contractAddressController.text); + + bool isPotentialScam = hasPotentialError; + final tokenSymbol = _tokenSymbolController.text.toUpperCase(); + + // check if the token symbol is the same as any of the base currencies symbols (ETH, SOL, POL, TRX, etc): + // if it is, then it's probably a scam unless it's in the whitelist + final baseCurrencySymbols = + CryptoCurrency.all.map((e) => e.title.toUpperCase()).toList(); + if (baseCurrencySymbols.contains(tokenSymbol) && !isWhitelisted) { + isPotentialScam = true; + } + final actionCall = () async { try { await widget.homeSettingsViewModel.addToken( @@ -219,6 +234,7 @@ class _EditTokenPageBodyState extends State { title: _tokenSymbolController.text.toUpperCase(), decimals: int.parse(_tokenDecimalController.text), iconPath: _tokenIconPathController.text, + isPotentialScam: isPotentialScam, ), contractAddress: _contractAddressController.text, ); @@ -226,7 +242,6 @@ class _EditTokenPageBodyState extends State { if (mounted) { Navigator.pop(context); } - } catch (e) { showPopUp( context: context, @@ -303,7 +318,8 @@ class _EditTokenPageBodyState extends State { if (token != null) { final isZano = widget.homeSettingsViewModel.walletType == WalletType.zano; if (_tokenNameController.text.isEmpty || isZano) _tokenNameController.text = token.name; - if (_tokenSymbolController.text.isEmpty || isZano) _tokenSymbolController.text = token.title; + if (_tokenSymbolController.text.isEmpty || isZano) + _tokenSymbolController.text = token.title; if (_tokenIconPathController.text.isEmpty) _tokenIconPathController.text = token.iconPath ?? ''; if (_tokenDecimalController.text.isEmpty || isZano) @@ -338,7 +354,9 @@ class _EditTokenPageBodyState extends State { placeholder: S.of(context).token_contract_address, options: [AddressTextFieldOption.paste], buttonColor: Theme.of(context).hintColor, - validator: widget.homeSettingsViewModel.walletType == WalletType.zano ? null : AddressValidator(type: widget.homeSettingsViewModel.nativeToken).call, + validator: widget.homeSettingsViewModel.walletType == WalletType.zano + ? null + : AddressValidator(type: widget.homeSettingsViewModel.nativeToken).call, onPushPasteButton: (_) { _pasteText(); }, diff --git a/lib/tron/cw_tron.dart b/lib/tron/cw_tron.dart index 8bceafe01..bf2fac590 100644 --- a/lib/tron/cw_tron.dart +++ b/lib/tron/cw_tron.dart @@ -133,4 +133,9 @@ class CWTron extends Tron { void updateTronGridUsageState(WalletBase wallet, bool isEnabled) { (wallet as TronWallet).updateScanProviderUsageState(isEnabled); } + + @override + List getDefaultTokenContractAddresses() { + return DefaultTronTokens().initialTronTokens.map((e) => e.contractAddress).toList(); + } } diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index 197d550c3..e3e02a045 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -114,13 +114,12 @@ abstract class HomeSettingsViewModelBase with Store { if (_balanceViewModel.wallet.type == WalletType.zano) { await zano!.addZanoAssetById(_balanceViewModel.wallet, contractAddress); } - + _updateTokensList(); _updateFiatPrices(token); } catch (e) { throw e; - } - finally { + } finally { isAddingToken = false; } } @@ -189,6 +188,40 @@ abstract class HomeSettingsViewModelBase with Store { } } + bool checkIfTokenIsWhitelisted(String contractAddress) { + // get the default tokens for each currency type: + List defaultTokenAddresses = []; + switch (_balanceViewModel.wallet.type) { + case WalletType.ethereum: + defaultTokenAddresses = ethereum!.getDefaultTokenContractAddresses(); + break; + case WalletType.polygon: + defaultTokenAddresses = polygon!.getDefaultTokenContractAddresses(); + break; + case WalletType.solana: + defaultTokenAddresses = solana!.getDefaultTokenContractAddresses(); + break; + case WalletType.tron: + defaultTokenAddresses = tron!.getDefaultTokenContractAddresses(); + break; + case WalletType.zano: + case WalletType.banano: + case WalletType.monero: + case WalletType.none: + case WalletType.bitcoin: + case WalletType.litecoin: + case WalletType.haven: + case WalletType.nano: + case WalletType.wownero: + case WalletType.bitcoinCash: + return false; + } + + // check if the contractAddress is in the defaultTokenAddresses + bool isInWhitelist = defaultTokenAddresses.any((element) => element == contractAddress); + return isInWhitelist; + } + Future _isPotentialScamTokenViaMoralis( String contractAddress, String chainName, @@ -363,6 +396,7 @@ abstract class HomeSettingsViewModelBase with Store { CryptoCurrency get nativeToken => _balanceViewModel.wallet.currency; void _updateFiatPrices(CryptoCurrency token) async { + if (token.isPotentialScam) return; // don't fetch price data for potential scam tokens try { _balanceViewModel.fiatConvertationStore.prices[token] = await FiatConversionService.fetchPrice( @@ -455,9 +489,10 @@ abstract class HomeSettingsViewModelBase with Store { } if (_balanceViewModel.wallet.type == WalletType.zano) { - tokens.addAll(zano!.getZanoAssets(_balanceViewModel.wallet) - .where((element) => _matchesSearchText(element)) - .toList() + tokens.addAll(zano! + .getZanoAssets(_balanceViewModel.wallet) + .where((element) => _matchesSearchText(element)) + .toList() ..sort(_sortFunc)); } } diff --git a/tool/configure.dart b/tool/configure.dart index 7fb72d5e4..214288078 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -872,6 +872,7 @@ import 'package:cw_evm/evm_chain_wallet.dart'; import 'package:cw_ethereum/ethereum_client.dart'; import 'package:cw_ethereum/ethereum_wallet.dart'; import 'package:cw_ethereum/ethereum_wallet_service.dart'; +import 'package:cw_ethereum/default_ethereum_erc20_tokens.dart'; import 'package:eth_sig_util/util/utils.dart'; @@ -922,6 +923,7 @@ abstract class Ethereum { void setLedgerConnection(WalletBase wallet, ledger.LedgerConnection connection); Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); + List getDefaultTokenContractAddresses(); } """; @@ -977,6 +979,7 @@ import 'package:cw_evm/evm_chain_wallet.dart'; import 'package:cw_polygon/polygon_client.dart'; import 'package:cw_polygon/polygon_wallet.dart'; import 'package:cw_polygon/polygon_wallet_service.dart'; +import 'package:cw_polygon/default_polygon_erc20_tokens.dart'; import 'package:eth_sig_util/util/utils.dart'; @@ -1027,6 +1030,7 @@ abstract class Polygon { void setLedgerConnection(WalletBase wallet, ledger.LedgerConnection connection); Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); + List getDefaultTokenContractAddresses(); } """; @@ -1269,6 +1273,7 @@ import 'package:cw_solana/solana_wallet_service.dart'; import 'package:cw_solana/solana_transaction_info.dart'; import 'package:cw_solana/solana_transaction_credentials.dart'; import 'package:cw_solana/solana_wallet_creation_credentials.dart'; +import 'package:cw_solana/default_spl_tokens.dart'; """; const solanaCwPart = "part 'cw_solana.dart';"; const solanaContent = """ @@ -1310,6 +1315,7 @@ abstract class Solana { String getTokenAddress(CryptoCurrency asset); List? getValidationLength(CryptoCurrency type); double? getEstimateFees(WalletBase wallet); + List getDefaultTokenContractAddresses(); } """; @@ -1355,6 +1361,7 @@ import 'package:cw_tron/tron_client.dart'; import 'package:cw_tron/tron_token.dart'; import 'package:cw_tron/tron_wallet.dart'; import 'package:cw_tron/tron_wallet_service.dart'; +import 'package:cw_tron/default_tron_tokens.dart'; """; const tronCwPart = "part 'cw_tron.dart';"; @@ -1386,6 +1393,7 @@ abstract class Tron { String? getTronTRC20EstimatedFee(WalletBase wallet); void updateTronGridUsageState(WalletBase wallet, bool isEnabled); + List getDefaultTokenContractAddresses(); } """; From fd4254d65b4feca76bfe12a4dee1adf028ea6cb0 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Fri, 28 Feb 2025 19:55:22 +0200 Subject: [PATCH 011/243] Add unstoppable domains Better error message --- lib/buy/robinhood/robinhood_buy_provider.dart | 2 +- lib/entities/parse_address_from_domain.dart | 58 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/lib/buy/robinhood/robinhood_buy_provider.dart b/lib/buy/robinhood/robinhood_buy_provider.dart index 937af0315..b44fab311 100644 --- a/lib/buy/robinhood/robinhood_buy_provider.dart +++ b/lib/buy/robinhood/robinhood_buy_provider.dart @@ -59,7 +59,7 @@ class RobinhoodBuyProvider extends BuyProvider { case WalletType.bitcoinCash: return await wallet.signMessage(message, address: wallet.walletAddresses.address); default: - throw Exception("WalletType is not available for Robinhood ${wallet.type}"); + throw Exception("Wallet Type ${wallet.type.name} is not available for Robinhood"); } } diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index f5f96a8cd..3fbbe0709 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -32,33 +32,56 @@ class AddressResolver { static const unstoppableDomains = [ "888", + "academy", + "agency", "altimist", "anime", "austin", "bald", + "bay", "benji", "bet", "binanceus", "bitcoin", "bitget", + "bitscrunch", "blockchain", + "boomer", + "boston", "ca", + "caw", + "cc", + "chat", "chomp", "clay", + "club", "co", "com", + "company", "crypto", "dao", + "design", "dfz", "digital", + "doga", + "donut", "dream", + "email", + "emir", "eth", "ethermail", + "family", "farms", + "finance", "fun", + "fyi", + "games", + "global", "go", "group", + "guru", "hi", + "hockey", "host", "info", "io", @@ -68,43 +91,78 @@ class AddressResolver { "lfg", "life", "live", + "llc", + "ltc", "ltd", "manga", + "me", + "media", "metropolis", + "miami", + "miku", + "money", "moon", "mumu", "net", + "network", + "news", "nft", + "npc", + "onchain", "online", "org", + "podcast", "pog", "polygon", "press", + "privacy", "pro", "propykeys", "pudgy", "pw", + "quantum", + "rad", "raiin", + "retardio", + "rip", + "rocks", "secret", + "services", "site", "smobler", + "social", + "solutions", "space", "stepn", "store", + "studio", + "systems", "tball", + "tea", + "team", "tech", + "technology", + "today", + "tribe", + "u", "ubu", "uno", "unstoppable", + "vip", "wallet", "website", + "wif", "wifi", "witg", + "work", + "world", "wrkx", + "wtf", "x", "xmr", "xyz", "zil", + "zone" ]; static String? extractAddressByType({required String raw, required CryptoCurrency type}) { From 130f877234517d435e68c50b2459f6de5fc9b95e Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Sun, 2 Mar 2025 17:29:53 +0200 Subject: [PATCH 012/243] minor: improve eth/poly fees error message --- cw_evm/lib/evm_chain_exceptions.dart | 4 ++-- cw_evm/lib/evm_chain_wallet.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cw_evm/lib/evm_chain_exceptions.dart b/cw_evm/lib/evm_chain_exceptions.dart index d0f300270..c7509a17f 100644 --- a/cw_evm/lib/evm_chain_exceptions.dart +++ b/cw_evm/lib/evm_chain_exceptions.dart @@ -16,8 +16,8 @@ class EVMChainTransactionCreationException implements Exception { class EVMChainTransactionFeesException implements Exception { final String exceptionMessage; - EVMChainTransactionFeesException() - : exceptionMessage = 'Current balance is less than the estimated fees for this transaction.'; + EVMChainTransactionFeesException(String currency) + : exceptionMessage = 'Transaction failed due to insufficient $currency balance to cover the fees.'; @override String toString() => exceptionMessage; diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index 646abd7ae..9691993ad 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -417,7 +417,7 @@ abstract class EVMChainWalletBase // check the fees on the base currency (Eth/Polygon) if (estimatedFeesForTransaction > balance[currency]!.balance) { - throw EVMChainTransactionFeesException(); + throw EVMChainTransactionFeesException(currency.title); } if (currencyBalance.balance < totalAmount) { From de40b2f9aa9f92d88305516d6f2acf3ae613c66b Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Mon, 3 Mar 2025 17:36:20 -0800 Subject: [PATCH 013/243] Address list fixes CW-883 (#1995) * fix show/hide buttons not updating address list * fix label not updating * cleanup * minor fix --- .../screens/receive/widgets/address_list.dart | 40 +++---- .../wallet_address_list_view_model.dart | 104 ++++++++---------- 2 files changed, 68 insertions(+), 76 deletions(-) diff --git a/lib/src/screens/receive/widgets/address_list.dart b/lib/src/screens/receive/widgets/address_list.dart index 45891d03a..8d22d81ef 100644 --- a/lib/src/screens/receive/widgets/address_list.dart +++ b/lib/src/screens/receive/widgets/address_list.dart @@ -1,4 +1,3 @@ - import 'dart:math'; import 'package:cake_wallet/di.dart'; @@ -21,6 +20,7 @@ import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; class AddressList extends StatefulWidget { const AddressList({ @@ -37,7 +37,6 @@ class AddressList extends StatefulWidget { } class _AddressListState extends State { - bool showHiddenAddresses = false; void _toggleHiddenAddresses() { @@ -62,7 +61,7 @@ class _AddressListState extends State { void updateItems() { setState(() { - items = getItems(widget.addressListViewModel.items, showHiddenAddresses); + items = getItems(widget.addressListViewModel.forceRecomputeItems, showHiddenAddresses); }); } @@ -132,9 +131,10 @@ class _AddressListState extends State { showTrailingButton: widget.addressListViewModel.showAddManualAddresses, showSearchButton: true, onSearchCallback: updateItems, - trailingButtonTap: () => Navigator.of(context).pushNamed(Routes.newSubaddress).then((value) { - updateItems(); // refresh the new address - }), + trailingButtonTap: () => + Navigator.of(context).pushNamed(Routes.newSubaddress).then((value) { + updateItems(); // refresh the new address + }), trailingIcon: Icon( Icons.add, size: 20, @@ -149,7 +149,8 @@ class _AddressListState extends State { cell = Container(); } else { cell = Observer(builder: (_) { - final isCurrent = item.address == widget.addressListViewModel.address.address && editable; + final isCurrent = + item.address == widget.addressListViewModel.address.address && editable; final backgroundColor = isCurrent ? Theme.of(context).extension()!.currentTileBackgroundColor : Theme.of(context).extension()!.tilesBackgroundColor; @@ -157,17 +158,17 @@ class _AddressListState extends State { ? Theme.of(context).extension()!.currentTileTextColor : Theme.of(context).extension()!.tilesTextColor; - return AddressCell.fromItem( item, isCurrent: isCurrent, hasBalance: widget.addressListViewModel.isBalanceAvailable, hasReceived: widget.addressListViewModel.isReceivedAvailable, - // hasReceived: - backgroundColor: (kDebugMode && item.isHidden) ? - Theme.of(context).colorScheme.error : - (kDebugMode && item.isManual) ? Theme.of(context).colorScheme.error.withBlue(255) : - backgroundColor, + // hasReceived: + backgroundColor: (kDebugMode && item.isHidden) + ? Theme.of(context).colorScheme.error + : (kDebugMode && item.isManual) + ? Theme.of(context).colorScheme.error.withBlue(255) + : backgroundColor, textColor: textColor, onTap: (_) { if (widget.onSelect != null) { @@ -177,9 +178,11 @@ class _AddressListState extends State { widget.addressListViewModel.setAddress(item); }, onEdit: editable - ? () => Navigator.of(context).pushNamed(Routes.newSubaddress, arguments: item).then((value) { - updateItems(); // refresh the new address - }) + ? () => Navigator.of(context) + .pushNamed(Routes.newSubaddress, arguments: item) + .then((value) { + updateItems(); // refresh the new address + }) : null, isHidden: item.isHidden, onHide: () => _hideAddress(item), @@ -191,8 +194,8 @@ class _AddressListState extends State { return index != 0 ? cell : ClipRRect( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(30), topRight: Radius.circular(30)), + borderRadius: + BorderRadius.only(topLeft: Radius.circular(30), topRight: Radius.circular(30)), child: cell, ); }, @@ -203,5 +206,4 @@ class _AddressListState extends State { await widget.addressListViewModel.toggleHideAddress(item); updateItems(); } - } diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 773d8335c..ef1c52f31 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -26,14 +26,14 @@ import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_i import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/amount_converter.dart'; import 'package:cw_core/currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:intl/intl.dart'; import 'package:mobx/mobx.dart'; part 'wallet_address_list_view_model.g.dart'; -class WalletAddressListViewModel = WalletAddressListViewModelBase - with _$WalletAddressListViewModel; +class WalletAddressListViewModel = WalletAddressListViewModelBase with _$WalletAddressListViewModel; abstract class PaymentURI { PaymentURI({required this.amount, required this.address}); @@ -222,9 +222,7 @@ class ZanoURI extends PaymentURI { } } - -abstract class WalletAddressListViewModelBase - extends WalletChangeListenerViewModel with Store { +abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store { WalletAddressListViewModelBase({ required AppStore appStore, required this.yatStore, @@ -245,8 +243,7 @@ abstract class WalletAddressListViewModelBase _init(); selectedCurrency = walletTypeToCryptoCurrency(wallet.type); - hasAccounts = [WalletType.monero, WalletType.wownero, WalletType.haven] - .contains(wallet.type); + hasAccounts = [WalletType.monero, WalletType.wownero, WalletType.haven].contains(wallet.type); } static const String _cryptoNumberPattern = '0.00000000'; @@ -259,8 +256,7 @@ abstract class WalletAddressListViewModelBase double? _fiatRate; String _rawAmount = ''; - List get currencies => - [walletTypeToCryptoCurrency(wallet.type), ...FiatCurrency.all]; + List get currencies => [walletTypeToCryptoCurrency(wallet.type), ...FiatCurrency.all]; String get buttonTitle { if (isElectrumWallet) { @@ -286,8 +282,8 @@ abstract class WalletAddressListViewModelBase WalletType get type => wallet.type; @computed - WalletAddressListItem get address => WalletAddressListItem( - address: wallet.walletAddresses.address, isPrimary: false); + WalletAddressListItem get address => + WalletAddressListItem(address: wallet.walletAddresses.address, isPrimary: false); @computed PaymentURI get uri { @@ -317,25 +313,23 @@ abstract class WalletAddressListViewModelBase case WalletType.wownero: return WowneroURI(amount: amount, address: address.address); case WalletType.zano: - return ZanoURI(amount: amount, address: address.address); + return ZanoURI(amount: amount, address: address.address); case WalletType.none: throw Exception('Unexpected type: ${type.toString()}'); } } + @computed ObservableList get items => ObservableList() ..addAll(_baseItems) ..addAll(addressList); - @computed - ObservableList get addressList { + ObservableList _computeAddressList() { final addressList = ObservableList(); if (wallet.type == WalletType.monero) { - final primaryAddress = - monero!.getSubaddressList(wallet).subaddresses.first; - final addressItems = - monero!.getSubaddressList(wallet).subaddresses.map((subaddress) { + final primaryAddress = monero!.getSubaddressList(wallet).subaddresses.first; + final addressItems = monero!.getSubaddressList(wallet).subaddresses.map((subaddress) { final isPrimary = subaddress == primaryAddress; return WalletAddressListItem( @@ -351,10 +345,8 @@ abstract class WalletAddressListViewModelBase } if (wallet.type == WalletType.wownero) { - final primaryAddress = - wownero!.getSubaddressList(wallet).subaddresses.first; - final addressItems = - wownero!.getSubaddressList(wallet).subaddresses.map((subaddress) { + final primaryAddress = wownero!.getSubaddressList(wallet).subaddresses.first; + final addressItems = wownero!.getSubaddressList(wallet).subaddresses.map((subaddress) { final isPrimary = subaddress == primaryAddress; return WalletAddressListItem( @@ -367,10 +359,8 @@ abstract class WalletAddressListViewModelBase } if (wallet.type == WalletType.haven) { - final primaryAddress = - haven!.getSubaddressList(wallet).subaddresses.first; - final addressItems = - haven!.getSubaddressList(wallet).subaddresses.map((subaddress) { + final primaryAddress = haven!.getSubaddressList(wallet).subaddresses.first; + final addressItems = haven!.getSubaddressList(wallet).subaddresses.map((subaddress) { final isPrimary = subaddress == primaryAddress; return WalletAddressListItem( @@ -384,8 +374,7 @@ abstract class WalletAddressListViewModelBase if (isElectrumWallet) { if (bitcoin!.hasSelectedSilentPayments(wallet)) { - final addressItems = - bitcoin!.getSilentPaymentAddresses(wallet).map((address) { + final addressItems = bitcoin!.getSilentPaymentAddresses(wallet).map((address) { final isPrimary = address.id == 0; return WalletAddressListItem( @@ -436,8 +425,7 @@ abstract class WalletAddressListViewModelBase if (wallet.type == WalletType.litecoin && addressItems.length >= 1000) { // find the index of the last item with a txCount > 0 final addressItemsList = addressItems.toList(); - int index = addressItemsList - .lastIndexWhere((item) => (item.txCount ?? 0) > 0); + int index = addressItemsList.lastIndexWhere((item) => (item.txCount ?? 0) > 0); if (index == -1) { index = 0; } @@ -451,22 +439,19 @@ abstract class WalletAddressListViewModelBase if (wallet.type == WalletType.ethereum) { final primaryAddress = ethereum!.getAddress(wallet); - addressList.add(WalletAddressListItem( - isPrimary: true, name: null, address: primaryAddress)); + addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } if (wallet.type == WalletType.polygon) { final primaryAddress = polygon!.getAddress(wallet); - addressList.add(WalletAddressListItem( - isPrimary: true, name: null, address: primaryAddress)); + addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } if (wallet.type == WalletType.solana) { final primaryAddress = solana!.getAddress(wallet); - addressList.add(WalletAddressListItem( - isPrimary: true, name: null, address: primaryAddress)); + addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } if (wallet.type == WalletType.nano) { @@ -480,21 +465,18 @@ abstract class WalletAddressListViewModelBase if (wallet.type == WalletType.tron) { final primaryAddress = tron!.getAddress(wallet); - addressList.add(WalletAddressListItem( - isPrimary: true, name: null, address: primaryAddress)); + addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } for (var i = 0; i < addressList.length; i++) { if (!(addressList[i] is WalletAddressListItem)) continue; - (addressList[i] as WalletAddressListItem).isHidden = wallet - .walletAddresses.hiddenAddresses + (addressList[i] as WalletAddressListItem).isHidden = wallet.walletAddresses.hiddenAddresses .contains((addressList[i] as WalletAddressListItem).address); } for (var i = 0; i < addressList.length; i++) { if (!(addressList[i] is WalletAddressListItem)) continue; - (addressList[i] as WalletAddressListItem).isManual = wallet - .walletAddresses.manualAddresses + (addressList[i] as WalletAddressListItem).isManual = wallet.walletAddresses.manualAddresses .contains((addressList[i] as WalletAddressListItem).address); } @@ -516,13 +498,28 @@ abstract class WalletAddressListViewModelBase return addressList; } + @computed + ObservableList get addressList { + return _computeAddressList(); + } + + List get forceRecomputeItems { + // necessary because the addressList contains non-observable items + List recomputed = []; + recomputed.addAll(_baseItems); + recomputed.addAll(_computeAddressList()); + return recomputed; + } + Future toggleHideAddress(WalletAddressListItem item) async { if (item.isHidden) { - wallet.walletAddresses.hiddenAddresses - .removeWhere((element) => element == item.address); + item.isHidden = false; + wallet.walletAddresses.hiddenAddresses.removeWhere((element) => element == item.address); } else { + item.isHidden = true; wallet.walletAddresses.hiddenAddresses.add(item.address); } + // update the address list: await wallet.walletAddresses.saveAddressesInBox(); if (wallet.type == WalletType.monero) { monero! @@ -568,28 +565,22 @@ abstract class WalletAddressListViewModelBase ].contains(wallet.type); @computed - bool get isElectrumWallet => [ - WalletType.bitcoin, - WalletType.litecoin, - WalletType.bitcoinCash - ].contains(wallet.type); + bool get isElectrumWallet => + [WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type); @computed bool get isBalanceAvailable => isElectrumWallet; @computed - bool get isReceivedAvailable => - [WalletType.monero, WalletType.wownero].contains(wallet.type); + bool get isReceivedAvailable => [WalletType.monero, WalletType.wownero].contains(wallet.type); @computed bool get isSilentPayments => - wallet.type == WalletType.bitcoin && - bitcoin!.hasSelectedSilentPayments(wallet); + wallet.type == WalletType.bitcoin && bitcoin!.hasSelectedSilentPayments(wallet); @computed bool get isAutoGenerateSubaddressEnabled => - _settingsStore.autoGenerateSubaddressStatus != - AutoGenerateSubaddressStatus.disabled && + _settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled && !isSilentPayments; @computed @@ -672,8 +663,7 @@ abstract class WalletAddressListViewModelBase @action void _convertAmountToCrypto() { final cryptoCurrency = walletTypeToCryptoCurrency(wallet.type); - final fiatRate = - _fiatRate ?? (fiatConversionStore.prices[cryptoCurrency] ?? 0.0); + final fiatRate = _fiatRate ?? (fiatConversionStore.prices[cryptoCurrency] ?? 0.0); if (fiatRate <= 0.0) { dev.log("Invalid Fiat Rate $fiatRate"); From 0c14c0b1f493536d31eb6d967b6d3e1fc11174e9 Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Mon, 3 Mar 2025 17:38:00 -0800 Subject: [PATCH 014/243] CW-687 Coin control fixes (#2058) * fix coin control * don't reselect frozen coins --- lib/view_model/send/send_view_model.dart | 15 ++++++++------- .../unspent_coins_list_view_model.dart | 13 +++++++++++-- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 53c52aa1e..998521ede 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -102,7 +102,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor outputs .add(Output(wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency)); - unspentCoinsListViewModel.initialSetup(); + unspentCoinsListViewModel.initialSetup().then((_) { + unspentCoinsListViewModel.resetUnspentCoinsInfoSelections(); + }); } @observable @@ -728,11 +730,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor return S.current.insufficient_funds_for_tx; } - return - '''${S.current.insufficient_funds_for_tx} \n\n''' - '''${S.current.balance}: ${parsedErrorMessageResult.balanceEth} ${walletType == WalletType.polygon ? "POL" : "ETH"} (${parsedErrorMessageResult.balanceUsd} ${fiatFromSettings.name})\n\n''' - '''${S.current.transaction_cost}: ${parsedErrorMessageResult.txCostEth} ${walletType == WalletType.polygon ? "POL" : "ETH"} (${parsedErrorMessageResult.txCostUsd} ${fiatFromSettings.name})\n\n''' - '''${S.current.overshot}: ${parsedErrorMessageResult.overshotEth} ${walletType == WalletType.polygon ? "POL" : "ETH"} (${parsedErrorMessageResult.overshotUsd} ${fiatFromSettings.name})'''; + return '''${S.current.insufficient_funds_for_tx} \n\n''' + '''${S.current.balance}: ${parsedErrorMessageResult.balanceEth} ${walletType == WalletType.polygon ? "POL" : "ETH"} (${parsedErrorMessageResult.balanceUsd} ${fiatFromSettings.name})\n\n''' + '''${S.current.transaction_cost}: ${parsedErrorMessageResult.txCostEth} ${walletType == WalletType.polygon ? "POL" : "ETH"} (${parsedErrorMessageResult.txCostUsd} ${fiatFromSettings.name})\n\n''' + '''${S.current.overshot}: ${parsedErrorMessageResult.overshotEth} ${walletType == WalletType.polygon ? "POL" : "ETH"} (${parsedErrorMessageResult.overshotUsd} ${fiatFromSettings.name})'''; } return errorMessage; @@ -809,4 +810,4 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor return false; } -} \ No newline at end of file +} diff --git a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart index 52820adcb..9a8a4a8f2 100644 --- a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart +++ b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart @@ -68,7 +68,6 @@ abstract class UnspentCoinsListViewModelBase with Store { bool get hasAdjustableFieldChanged => items.any(_hasAdjustableFieldChanged); - Future saveUnspentCoinInfo(UnspentCoinsItem item) async { try { final existingInfo = _unspentCoinsInfo.values @@ -79,7 +78,6 @@ abstract class UnspentCoinsListViewModelBase with Store { existingInfo.isSending = item.isSending; existingInfo.note = item.note; - await existingInfo.save(); _updateUnspentCoinsInfo(); } catch (e) { @@ -167,6 +165,17 @@ abstract class UnspentCoinsListViewModelBase with Store { items.addAll(unspents); } + @action + void resetUnspentCoinsInfoSelections() { + // reset all unspent coins selections to true except frozen ones + for (final item in items) { + if (!item.isFrozen) { + item.isSending = true; + saveUnspentCoinInfo(item); + } + } + } + @action void toggleSelectAll(bool value) { for (final item in items) { From 074a38704ed2ab7e7a76e145e03c6090bb56c9de Mon Sep 17 00:00:00 2001 From: Serhii Date: Tue, 4 Mar 2025 03:56:40 +0200 Subject: [PATCH 015/243] Improve bip 353 payment method selection modal (#2052) * update selection modal UI * localisation --- lib/entities/bip_353_record.dart | 50 ++++---- lib/src/widgets/alert_with_picker_option.dart | 120 ++++++++++++++++++ lib/src/widgets/base_alert_dialog.dart | 4 +- 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_fr.arb | 2 + res/values/strings_ha.arb | 2 + res/values/strings_hi.arb | 4 +- res/values/strings_hr.arb | 2 + res/values/strings_hy.arb | 2 + res/values/strings_id.arb | 2 + res/values/strings_it.arb | 2 + res/values/strings_ja.arb | 2 + res/values/strings_ko.arb | 2 + res/values/strings_my.arb | 2 + res/values/strings_nl.arb | 2 + res/values/strings_pl.arb | 2 + res/values/strings_pt.arb | 2 + res/values/strings_ru.arb | 2 + res/values/strings_th.arb | 2 + res/values/strings_tl.arb | 2 + res/values/strings_tr.arb | 2 + res/values/strings_uk.arb | 2 + res/values/strings_ur.arb | 2 + res/values/strings_vi.arb | 2 + res/values/strings_yo.arb | 2 + res/values/strings_zh.arb | 2 + 31 files changed, 204 insertions(+), 28 deletions(-) create mode 100644 lib/src/widgets/alert_with_picker_option.dart diff --git a/lib/entities/bip_353_record.dart b/lib/entities/bip_353_record.dart index 80c0099a8..8685185da 100644 --- a/lib/entities/bip_353_record.dart +++ b/lib/entities/bip_353_record.dart @@ -1,5 +1,6 @@ import 'package:basic_utils/basic_utils.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/alert_with_picker_option.dart'; import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -100,44 +101,41 @@ class Bip353Record { Map addressMap, ) async { final entriesList = addressMap.entries.toList(); - final displayItems = entriesList.map((e) { - final extendedKeyName = keyDisplayMap[e.key] ?? e.key; - final truncatedValue = _truncate(e.value, front: 6, back: 6); - return '$extendedKeyName : $truncatedValue'; + final List> displayItems = entriesList.map((entry) { + final originalKey = entry.key; + final originalValue = entry.value; + + final extendedKeyName = keyDisplayMap[originalKey] ?? originalKey; + final truncatedValue = _truncate(originalValue, front: 6, back: 6); + + return { + 'displayKey': extendedKeyName, + 'displayValue': truncatedValue, + 'originalKey': originalKey, + 'originalValue': originalValue, + }; }).toList(); - String? selectedDisplayItem; + String? selectedOriginalValue; if (context.mounted) { await showPopUp( context: context, builder: (dialogContext) { - return Picker( - selectedAtIndex: 0, - title: - '$bip353Name \n was successfully resolved to the following addresses, please choose one:', - items: displayItems, - onItemSelected: (String displayItem) { - selectedDisplayItem = displayItem; + return AlertWithPickerOption( + alertTitle: S.of(context).multiple_addresses_detected + '\n$bip353Name', + alertTitleTextSize: 14, + alertSubtitle: S.of(context).please_choose_one + ':', + options: displayItems, + onOptionSelected: (Map chosenItem) { + selectedOriginalValue = chosenItem['originalValue']; }, + alertBarrierDismissible: true, ); }, ); } - - if (selectedDisplayItem == null) { - if (displayItems.isEmpty) { - return null; - } - selectedDisplayItem = displayItems[0]; - } - - final index = displayItems.indexOf(selectedDisplayItem!); - if (index < 0) { - return null; - } - - return entriesList[index].value; + return selectedOriginalValue; } static String _truncate(String value, {int front = 6, int back = 6}) { diff --git a/lib/src/widgets/alert_with_picker_option.dart b/lib/src/widgets/alert_with_picker_option.dart new file mode 100644 index 000000000..bedcdefd4 --- /dev/null +++ b/lib/src/widgets/alert_with_picker_option.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:cake_wallet/src/widgets/base_alert_dialog.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; + +class AlertWithPickerOption extends BaseAlertDialog { + AlertWithPickerOption( + {required this.alertTitle, + required this.alertTitleTextSize, + required this.alertSubtitle, + required this.options, + this.onOptionSelected, + this.alertBarrierDismissible = true, + Key? key}); + + final String alertTitle; + final double alertTitleTextSize; + final String alertSubtitle; + final List> options; + final ValueChanged>? onOptionSelected; + final bool alertBarrierDismissible; + + @override + String get titleText => alertTitle; + + @override + double? get titleTextSize => alertTitleTextSize; + + @override + String get contentText => alertSubtitle; + + @override + bool get barrierDismissible => alertBarrierDismissible; + + @override + Widget actionButtons(BuildContext context) => Container(); + + @override + bool get isBottomDividerExists => false; + + @override + Widget content(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 16), + Text( + contentText, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w500, + fontFamily: 'Lato', + color: Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none), + ), + const SizedBox(height: 4), + Container( + constraints: const BoxConstraints(maxHeight: 200), + child: ListView.separated( + shrinkWrap: true, + padding: EdgeInsets.zero, + separatorBuilder: (context, index) => const SizedBox(height: 8), + itemCount: options.length, + itemBuilder: (context, index) { + final item = options[index]; + final displayKey = item['displayKey'] ?? ''; + final displayValue = item['displayValue'] ?? ''; + return GestureDetector( + onTap: () { + onOptionSelected?.call(item); + Navigator.of(context).pop(); + }, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: Theme.of(context).colorScheme.background), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + displayKey, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + color: Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + ), + ), + Row( + children: [ + Text( + displayValue, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + color: Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + ), + ), + const SizedBox(width: 4), + Icon( + Icons.arrow_forward_ios, + size: 18, + color: Theme.of(context).extension()!.titleColor, + ), + ], + ), + ], + ), + ), + ); + }, + ), + ), + ], + ); + } +} diff --git a/lib/src/widgets/base_alert_dialog.dart b/lib/src/widgets/base_alert_dialog.dart index bede33ebf..02b8f85ab 100644 --- a/lib/src/widgets/base_alert_dialog.dart +++ b/lib/src/widgets/base_alert_dialog.dart @@ -9,6 +9,8 @@ class BaseAlertDialog extends StatelessWidget { String? get titleText => ''; + double? get titleTextSize => 20; + String get contentText => ''; String get leftActionButtonText => ''; @@ -46,7 +48,7 @@ class BaseAlertDialog extends StatelessWidget { titleText!, textAlign: TextAlign.center, style: TextStyle( - fontSize: 20, + fontSize: titleTextSize, fontFamily: 'Lato', fontWeight: FontWeight.w600, color: Theme.of(context).extension()!.titleColor, diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index afa3676b3..0bffae6c4 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -437,6 +437,7 @@ "monero_light_theme": " ضوء مونيرو", "moonpay_alert_text": "يجب أن تكون قيمة المبلغ أكبر من أو تساوي ${minAmount} ${fiatCurrency}", "more_options": "المزيد من الخيارات", + "multiple_addresses_detected": "عناوين متعددة تم اكتشافها", "mweb_confirmed": "أكد MWEB", "mweb_unconfirmed": "غير مؤكد MWEB", "name": "ﻢﺳﺍ", @@ -513,6 +514,7 @@ "pin_number": "الرقم السري", "placeholder_contacts": "سيتم عرض جهات الاتصال الخاصة بك هنا", "placeholder_transactions": "سيتم عرض معاملاتك هنا", + "please_choose_one": "الرجاء اختيار واحد", "please_fill_totp": "يرجى ملء الرمز المكون من 8 أرقام الموجود على جهازك الآخر", "please_make_selection": "يرجى الأختيار لإنشاء أو استعادة محفظتك.", "please_reference_document": "يرجى الرجوع إلى الوثائق أدناه لمزيد من المعلومات.", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 9d8e8b8f8..5195ab39c 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -437,6 +437,7 @@ "monero_light_theme": "Лека тема Monero", "moonpay_alert_text": "Сумата трябва да бъде най-малко ${minAmount} ${fiatCurrency}", "more_options": "Още настройки", + "multiple_addresses_detected": "Открити множество адреси", "mweb_confirmed": "Потвърден MWeb", "mweb_unconfirmed": "Непотвърден mweb", "name": "Име", @@ -513,6 +514,7 @@ "pin_number": "PIN код", "placeholder_contacts": "Вашите контакти ще се покажат тук", "placeholder_transactions": "Вашите транзакции ще се покажат тук", + "please_choose_one": "Моля, изберете един", "please_fill_totp": "Моля, попълнете 8-цифрения код на другото ви устройство", "please_make_selection": "Моля, изберете отдолу за създаване или възстановяване на портфейл.", "please_reference_document": "Моля, вижте документите по-долу за повече информация.", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 4ba213d65..4babb1070 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -437,6 +437,7 @@ "monero_light_theme": "Světlé téma Monero", "moonpay_alert_text": "Částka musí být větší nebo rovna ${minAmount} ${fiatCurrency}", "more_options": "Více možností", + "multiple_addresses_detected": "Detekované více adres", "mweb_confirmed": "Potvrzený mweb", "mweb_unconfirmed": "Nepotvrzené mWeb", "name": "název", @@ -513,6 +514,7 @@ "pin_number": "Číslo PIN", "placeholder_contacts": "Vaše kontakty budou zobrazeny zde", "placeholder_transactions": "Vaše transakce budou zobrazeny zde", + "please_choose_one": "Vyberte si prosím jeden", "please_fill_totp": "Vyplňte prosím 8místný kód na vašem druhém zařízení", "please_make_selection": "Prosím vyberte si níže, jestli chcete vytvořit, nebo obnovit peněženku.", "please_reference_document": "Více informací naleznete v dokumentu níže.", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index c807b3ecc..69c9e5783 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -437,6 +437,7 @@ "monero_light_theme": "Monero Light-Thema", "moonpay_alert_text": "Der Wert des Betrags muss größer oder gleich ${minAmount} ${fiatCurrency} sein", "more_options": "Weitere Optionen", + "multiple_addresses_detected": "Mehrere Adressen erkannt", "mweb_confirmed": "Bestätigt MWeb", "mweb_unconfirmed": "Unbestätigter MWeb", "name": "Name", @@ -513,6 +514,7 @@ "pin_number": "PIN-Nummer", "placeholder_contacts": "Ihre Kontakte werden hier angezeigt", "placeholder_transactions": "Ihre Transaktionen werden hier angezeigt", + "please_choose_one": "Bitte wählen Sie einen", "please_fill_totp": "Bitte geben Sie den 8-stelligen Code ein, der auf Ihrem anderen Gerät vorhanden ist", "please_make_selection": "Bitte treffen Sie unten eine Auswahl zum Erstellen oder Wiederherstellen Ihrer Wallet.", "please_reference_document": "Bitte verweisen Sie auf die folgenden Dokumente, um weitere Informationen zu erhalten.", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index a02cabe55..8fa1f92b5 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -437,6 +437,7 @@ "monero_light_theme": "Monero Light Theme", "moonpay_alert_text": "Value of the amount must be more or equal to ${minAmount} ${fiatCurrency}", "more_options": "More Options", + "multiple_addresses_detected": "Multiple addresses detected", "mweb_confirmed": "Confirmed MWEB", "mweb_unconfirmed": "Unconfirmed MWEB", "name": "Name", @@ -514,6 +515,7 @@ "pin_number": "PIN number", "placeholder_contacts": "Your contacts will be displayed here", "placeholder_transactions": "Your transactions will be displayed here", + "please_choose_one": "Please choose one", "please_fill_totp": "Please fill in the 8-digit code present on your other device", "please_make_selection": "Please make a selection below to create or recover your wallet.", "please_reference_document": "Please reference the documents below for more information.", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index cdf1bac98..c6363a15f 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -437,6 +437,7 @@ "monero_light_theme": "Tema ligero de Monero", "moonpay_alert_text": "El valor de la cantidad debe ser mayor o igual a ${minAmount} ${fiatCurrency}", "more_options": "Más Opciones", + "multiple_addresses_detected": "Múltiples direcciones detectadas", "mweb_confirmed": "Confirmado mweb", "mweb_unconfirmed": "Mweb no confirmado", "name": "Nombre", @@ -514,6 +515,7 @@ "pin_number": "Número PIN", "placeholder_contacts": "Tus contactos se mostrarán aquí", "placeholder_transactions": "Tus transacciones se mostrarán aquí", + "please_choose_one": "Elija uno", "please_fill_totp": "Completa el código de 8 dígitos presente en su otro dispositivo", "please_make_selection": "Selecciona a continuación para crear o recuperar su billetera.", "please_reference_document": "Consulta los documentos a continuación para obtener más información.", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 69fa3c31c..41409968a 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -437,6 +437,7 @@ "monero_light_theme": "Thème de lumière Monero", "moonpay_alert_text": "Le montant doit être au moins égal à ${minAmount} ${fiatCurrency}", "more_options": "Plus d'options", + "multiple_addresses_detected": "Plusieurs adresses détectées", "mweb_confirmed": "Confirmé MWEB", "mweb_unconfirmed": "Mweb non confirmé", "name": "Nom", @@ -513,6 +514,7 @@ "pin_number": "Numéro PIN", "placeholder_contacts": "Vos contacts apparaîtront ici", "placeholder_transactions": "Vos transactions apparaîtront ici", + "please_choose_one": "Veuillez en choisir un", "please_fill_totp": "Veuillez renseigner le code à 8 chiffres affiché sur votre autre appareil", "please_make_selection": "Merci de faire un choix ci-dessous pour créer ou restaurer votre portefeuille (wallet).", "please_reference_document": "Veuillez vous référer aux documents ci-dessous pour plus d'informations.", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 3667da5f6..c528c58d2 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -437,6 +437,7 @@ "monero_light_theme": "Jigon Hasken Monero", "moonpay_alert_text": "Darajar adadin dole ne ya zama fiye ko daidai da ${minAmount} ${fiatCurrency}", "more_options": "Ƙarin Zaɓuɓɓuka", + "multiple_addresses_detected": "An gano adiresoshin da aka gano", "mweb_confirmed": "Tabbatar da Mweb", "mweb_unconfirmed": "Myconfired", "name": "Suna", @@ -515,6 +516,7 @@ "pin_number": "Lambar PIN", "placeholder_contacts": "Za a nuna lambobin sadarwar ku anan", "placeholder_transactions": "Za a nuna ma'amalolin ku anan", + "please_choose_one": "Da fatan za a zabi ɗaya", "please_fill_totp": "Da fatan za a cika lambar lambobi 8 da ke yanzu a kan sauran na'urarku", "please_make_selection": "Don Allah zaɓi ƙasa don ƙirƙira ko dawo da kwalinku.", "please_reference_document": "Da fatan za a nemi takaddun da ke ƙasa don ƙarin bayani.", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 53ddaea29..cc1a7c6e1 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -437,6 +437,7 @@ "monero_light_theme": "मोनेरो लाइट थीम", "moonpay_alert_text": "राशि का मूल्य अधिक है या करने के लिए बराबर होना चाहिए ${minAmount} ${fiatCurrency}", "more_options": "और विकल्प", + "multiple_addresses_detected": "कई पते का पता चला", "mweb_confirmed": "MWEB की पुष्टि की", "mweb_unconfirmed": "अपुष्ट MWEB", "name": "नाम", @@ -505,8 +506,8 @@ "paste": "पेस्ट करें", "pause_wallet_creation": "हेवन वॉलेट बनाने की क्षमता फिलहाल रुकी हुई है।", "payment_id": "भुगतान ID: ", - "Payment_was_received": "आपका भुगतान प्राप्त हो गया था।", "payment_was_received": "आपका भुगतान प्राप्त हुआ था।", + "Payment_was_received": "आपका भुगतान प्राप्त हो गया था।", "pending": " (अपूर्ण)", "percentageOf": "${amount} का", "pin_at_top": "शीर्ष पर ${token} पिन करें", @@ -514,6 +515,7 @@ "pin_number": "पिन नंबर", "placeholder_contacts": "आपके संपर्क यहां प्रदर्शित होंगे", "placeholder_transactions": "आपके लेनदेन यहां प्रदर्शित होंगे", + "please_choose_one": "एक का चयन करें", "please_fill_totp": "कृपया अपने दूसरे डिवाइस पर मौजूद 8 अंकों का कोड भरें", "please_make_selection": "कृपया नीचे चयन करें अपना बटुआ बनाएं या पुनर्प्राप्त करें.", "please_reference_document": "कृपया अधिक जानकारी के लिए नीचे दिए गए दस्तावेज़ देखें।", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 88a481430..9aae5501e 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -437,6 +437,7 @@ "monero_light_theme": "Monero lagana tema", "moonpay_alert_text": "Vrijednost iznosa mora biti veća ili jednaka ${minAmount} ${fiatCurrency}", "more_options": "Više opcija", + "multiple_addresses_detected": "Otkrivene više adresa", "mweb_confirmed": "Potvrđen MWeb", "mweb_unconfirmed": "Nepotvrđeni mWeb", "name": "Ime", @@ -513,6 +514,7 @@ "pin_number": "PIN broj", "placeholder_contacts": "Vaši će kontakti biti prikazani ovdje", "placeholder_transactions": "Vaše će transakcije biti prikazane ovdje", + "please_choose_one": "Molimo odaberite jedan", "please_fill_totp": "Unesite 8-znamenkasti kod koji se nalazi na vašem drugom uređaju", "please_make_selection": "Molimo odaberite opcije niže za izradu novog novčanika ili za oporavak postojećeg.", "please_reference_document": "Molimo pogledajte dokumente ispod za više informacija.", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 087354ef2..bc99fda74 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -437,6 +437,7 @@ "monero_light_theme": "Monero պայծառ տեսք", "moonpay_alert_text": "Գումարի արժեքը պետք է լինի հավասար կամ ավելի քան ${minAmount} ${fiatCurrency}", "more_options": "Այլ տարբերակներ", + "multiple_addresses_detected": "Հայտնաբերվել են բազմաթիվ հասցեներ", "mweb_confirmed": "Հաստատված MWEB", "mweb_unconfirmed": "Չկարգավորված Mweb", "name": "Անուն", @@ -513,6 +514,7 @@ "pin_number": "Պին-կոդ", "placeholder_contacts": "Ձեր կոնտակտները այստեղ կցուցադրվեն", "placeholder_transactions": "Ձեր գործարքները այստեղ կցուցադրվեն", + "please_choose_one": "Խնդրում ենք ընտրել մեկը", "please_fill_totp": "Խնդրում ենք լրացնել 8-նիշյա կոդը, որը կա ձեր այլ սարքի վրա", "please_make_selection": "Խնդրում ենք ընտրություն կատարել ներքևում ձեր դրամապանակը ստեղծելու կամ վերականգնելու համար", "please_reference_document": "Խնդրում ենք դիտել ներքևի փաստաթղթերը ավելի շատ տեղեկատվության համար", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index cb9f648bb..1458d69ef 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -437,6 +437,7 @@ "monero_light_theme": "Tema Cahaya Monero", "moonpay_alert_text": "Nilai jumlah harus lebih atau sama dengan ${minAmount} ${fiatCurrency}", "more_options": "Opsi Lainnya", + "multiple_addresses_detected": "Banyak alamat terdeteksi", "mweb_confirmed": "Mengkonfirmasi mWeb", "mweb_unconfirmed": "MWEB yang belum dikonfirmasi", "name": "Nama", @@ -515,6 +516,7 @@ "pin_number": "Nomor PIN", "placeholder_contacts": "Kontak Anda akan ditampilkan di sini", "placeholder_transactions": "Transaksi Anda akan ditampilkan di sini", + "please_choose_one": "Pilih satu", "please_fill_totp": "Harap isi kode 8 digit yang ada di perangkat Anda yang lain", "please_make_selection": "Silahkan membuat pilihan di bawah ini untuk membuat atau memulihkan dompet Anda.", "please_reference_document": "Silakan referensikan dokumen di bawah ini untuk informasi lebih lanjut.", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index a48ee65e8..0ec8de33a 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -437,6 +437,7 @@ "monero_light_theme": "Tema chiaro Monero", "moonpay_alert_text": "Il valore dell'importo deve essere maggiore o uguale a ${minAmount} ${fiatCurrency}", "more_options": "Altre opzioni", + "multiple_addresses_detected": "Più indirizzi rilevati", "mweb_confirmed": "MWeb confermato", "mweb_unconfirmed": "MWeb non confermato", "name": "Nome", @@ -514,6 +515,7 @@ "pin_number": "Numero PIN", "placeholder_contacts": "I tuoi contatti saranno mostrati qui", "placeholder_transactions": "Le tue transazioni saranno mostrate qui", + "please_choose_one": "Scegli uno", "please_fill_totp": "Inserisci il codice di 8 cifre presente sul tuo altro dispositivo", "please_make_selection": "Gentilmente seleziona se vuoi generare o recuperare il tuo portafoglio.", "please_reference_document": "Si prega di fare riferimento ai documenti seguenti per ulteriori informazioni.", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index a23b5f705..e4b16ec85 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -438,6 +438,7 @@ "monero_light_theme": "モネロ ライト テーマ", "moonpay_alert_text": "金額の値は以上でなければなりません ${minAmount} ${fiatCurrency}", "more_options": "その他のオプション", + "multiple_addresses_detected": "複数のアドレスが検出されました", "mweb_confirmed": "確認されたMWEB", "mweb_unconfirmed": "未確認のMWEB", "name": "名前", @@ -514,6 +515,7 @@ "pin_number": "PIN番号", "placeholder_contacts": "連絡先はここに表示されます", "placeholder_transactions": "あなたの取引はここに表示されます", + "please_choose_one": "選択してください", "please_fill_totp": "他のデバイスにある 8 桁のコードを入力してください", "please_make_selection": "以下を選択してください ウォレットを作成または回復する.", "please_reference_document": "詳細については、以下のドキュメントを参照してください。", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 9de4769cf..056a091c2 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -437,6 +437,7 @@ "monero_light_theme": "모네로 라이트 테마", "moonpay_alert_text": "금액은 다음보다 크거나 같아야합니다 ${minAmount} ${fiatCurrency}", "more_options": "추가 옵션", + "multiple_addresses_detected": "여러 주소가 감지되었습니다", "mweb_confirmed": "확인 mweb", "mweb_unconfirmed": "확인되지 않은 mweb", "name": "이름", @@ -513,6 +514,7 @@ "pin_number": "PIN 번호", "placeholder_contacts": "연락처가 여기에 표시됩니다", "placeholder_transactions": "거래가 여기에 표시됩니다", + "please_choose_one": "하나를 선택하십시오", "please_fill_totp": "다른 기기에 있는 8자리 코드를 입력하세요.", "please_make_selection": "아래에서 선택하십시오 지갑 만들기 또는 복구.", "please_reference_document": "자세한 내용은 아래 문서를 참조하십시오.", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 66000dde5..a4a17760c 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -437,6 +437,7 @@ "monero_light_theme": "Monero Light အပြင်အဆင်", "moonpay_alert_text": "ပမာဏ၏တန်ဖိုးသည် ${minAmount} ${fiatCurrency} နှင့် ပိုနေရမည်", "more_options": "နောက်ထပ် ရွေးချယ်စရာများ", + "multiple_addresses_detected": "အများအပြားလိပ်စာများရှာဖွေတွေ့ရှိ", "mweb_confirmed": "အတည်ပြုလိုက် mweb", "mweb_unconfirmed": "အတည်မပြုနိုင်သော mweb", "name": "နာမည်", @@ -513,6 +514,7 @@ "pin_number": "လျှို့ဝှက်နံပါတ်", "placeholder_contacts": "သင့်အဆက်အသွယ်များကို ဤနေရာတွင် ပြသပါမည်။", "placeholder_transactions": "သင်၏ ငွေပေးငွေယူများကို ဤနေရာတွင် ပြသပါမည်။", + "please_choose_one": "ကျေးဇူးပြု. ရွေးပါ", "please_fill_totp": "သင့်အခြားစက်တွင်ရှိသော ဂဏန်း ၈ လုံးကုဒ်ကို ကျေးဇူးပြု၍ ဖြည့်ပါ။", "please_make_selection": "သင့်ပိုက်ဆံအိတ်ကို ဖန်တီးရန် သို့မဟုတ် ပြန်လည်ရယူရန် အောက်တွင် ရွေးချယ်မှုတစ်ခု ပြုလုပ်ပါ။", "please_reference_document": "နောက်ထပ်အချက်အလက်များအတွက် အောက်ပါစာရွက်စာတမ်းများကို ကိုးကားပါ။", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index faec44dfc..e950a75bd 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -437,6 +437,7 @@ "monero_light_theme": "Monero Light-thema", "moonpay_alert_text": "Waarde van het bedrag moet meer of gelijk zijn aan ${minAmount} ${fiatCurrency}", "more_options": "Meer opties", + "multiple_addresses_detected": "Meerdere adressen gedetecteerd", "mweb_confirmed": "Bevestigde MWEB", "mweb_unconfirmed": "Onbevestigde MWEB", "name": "Naam", @@ -513,6 +514,7 @@ "pin_number": "PIN-nummer", "placeholder_contacts": "Je contacten worden hier weergegeven", "placeholder_transactions": "Uw transacties worden hier weergegeven", + "please_choose_one": "Kies er een", "please_fill_totp": "Vul de 8-cijferige code in die op uw andere apparaat aanwezig is", "please_make_selection": "Maak hieronder uw keuze tot maak of herstel je portemonnee.", "please_reference_document": "Raadpleeg de onderstaande documenten voor meer informatie.", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 3ee50dc1a..e4ac7286d 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -437,6 +437,7 @@ "monero_light_theme": "Lekki motyw Monero", "moonpay_alert_text": "Wartość kwoty musi być większa lub równa ${minAmount} ${fiatCurrency}", "more_options": "Więcej opcji", + "multiple_addresses_detected": "Wykryto wiele adresów", "mweb_confirmed": "Potwierdził MWEB", "mweb_unconfirmed": "Niepotwierdzone MWEB", "name": "Nazwa", @@ -513,6 +514,7 @@ "pin_number": "Numer PIN", "placeholder_contacts": "Twoje kontakty zostaną wyświetlone tutaj", "placeholder_transactions": "Twoje transakcje zostaną wyświetlone tutaj", + "please_choose_one": "Wybierz jeden", "please_fill_totp": "Wpisz 8-cyfrowy kod znajdujący się na drugim urządzeniu", "please_make_selection": "Wybierz poniżej, aby utworzyć lub przywrócić swój portfel.", "please_reference_document": "Proszę odwołać się do poniższych dokumentów, aby uzyskać więcej informacji.", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index dadb38064..5e0c246bd 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -438,6 +438,7 @@ "monero_light_theme": "Monero Light Theme", "moonpay_alert_text": "O valor do montante deve ser maior ou igual a ${minAmount} ${fiatCurrency}", "more_options": "Mais opções", + "multiple_addresses_detected": "Vários endereços detectados", "mweb_confirmed": "MWEB confirmado", "mweb_unconfirmed": "MWEB não confirmado", "name": "Nome", @@ -515,6 +516,7 @@ "pin_number": "Número PIN", "placeholder_contacts": "Seus contatos serão exibidos aqui", "placeholder_transactions": "Suas transações serão exibidas aqui", + "please_choose_one": "Escolha um", "please_fill_totp": "Por favor, preencha o código de 8 dígitos presente em seu outro dispositivo", "please_make_selection": "Escolha se quer criar uma carteira nova ou restaurar uma antiga.", "please_reference_document": "Por favor, consulte os documentos abaixo para mais informações.", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 3c4ea4f98..f09ca70e2 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -437,6 +437,7 @@ "monero_light_theme": "Светлая тема Monero", "moonpay_alert_text": "Сумма должна быть больше или равна ${minAmount} ${fiatCurrency}", "more_options": "Дополнительные параметры", + "multiple_addresses_detected": "Обнаружено несколько адресов", "mweb_confirmed": "Подтверждено MWEB", "mweb_unconfirmed": "Неподтвержденная MWEB", "name": "Имя", @@ -514,6 +515,7 @@ "pin_number": "ПИН-код", "placeholder_contacts": "Ваши контакты будут отображаться здесь", "placeholder_transactions": "Ваши транзакции будут отображаться здесь", + "please_choose_one": "Пожалуйста, выберите один", "please_fill_totp": "Пожалуйста, введите 8-значный код на другом устройстве", "please_make_selection": "Выберите способ создания кошелька: создать новый или восстановить ваш существующий.", "please_reference_document": "Пожалуйста, обратитесь к документам ниже для получения дополнительной информации.", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 8bf0efeb8..5206db0bf 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -437,6 +437,7 @@ "monero_light_theme": "ธีมแสง Monero", "moonpay_alert_text": "มูลค่าของจำนวนต้องมากกว่าหรือเท่ากับ ${minAmount} ${fiatCurrency}", "more_options": "ตัวเลือกเพิ่มเติม", + "multiple_addresses_detected": "ตรวจพบหลายที่อยู่", "mweb_confirmed": "MWEB ยืนยันแล้ว", "mweb_unconfirmed": "mweb ที่ไม่ได้รับการยืนยัน", "name": "ชื่อ", @@ -513,6 +514,7 @@ "pin_number": "หมายเลข PIN", "placeholder_contacts": "รายชื่อผู้ติดต่อของคุณจะปรากฏที่นี่", "placeholder_transactions": "ธุรกรรมของคุณจะปรากฏที่นี่", + "please_choose_one": "โปรดเลือกหนึ่ง", "please_fill_totp": "กรุณากรอกรหัส 8 หลักที่อยู่ในอุปกรณ์อื่นของคุณ", "please_make_selection": "โปรดเลือกตามด้านล่างเพื่อสร้างหรือกู้กระเป๋าของคุณ", "please_reference_document": "โปรดอ้างอิงเอกสารด้านล่างสำหรับข้อมูลเพิ่มเติม", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 7e3e97cb7..4befe732a 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -437,6 +437,7 @@ "monero_light_theme": "Monero Light Theme", "moonpay_alert_text": "Ang halaga ay dapat na higit pa o katumbas ng ${minAmount} ${fiatCurrency}", "more_options": "Higit pang mga Pagpipilian", + "multiple_addresses_detected": "Maramihang mga address na napansin", "mweb_confirmed": "Nakumpirma na MWeb", "mweb_unconfirmed": "Hindi nakumpirma si Mweb", "name": "Pangalan", @@ -513,6 +514,7 @@ "pin_number": "Numero ng PIN", "placeholder_contacts": "Ang iyong mga contact ay ipapakita dito", "placeholder_transactions": "Ang iyong mga transaksyon ay ipapakita dito", + "please_choose_one": "Mangyaring pumili ng isa", "please_fill_totp": "Mangyaring punan ang 8-digit na code na naroroon sa iyong iba pang device", "please_make_selection": "Mangyaring gumawa ng isang pagpipilian sa ibaba upang lumikha o mabawi ang iyong wallet.", "please_reference_document": "Mangyaring sumangguni sa mga dokumento sa ibaba para sa karagdagang impormasyon.", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index d02960b51..efb00c98f 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -437,6 +437,7 @@ "monero_light_theme": "Monero Hafif Tema", "moonpay_alert_text": "Tutar ${minAmount} ${fiatCurrency} miktarına eşit veya daha fazla olmalıdır", "more_options": "Daha Fazla Seçenek", + "multiple_addresses_detected": "Birden çok adres tespit edildi", "mweb_confirmed": "Onaylanmış mweb", "mweb_unconfirmed": "Doğrulanmamış mweb", "name": "İsim", @@ -513,6 +514,7 @@ "pin_number": "PIN kodu", "placeholder_contacts": "Kişilerin burada görüntülenecek", "placeholder_transactions": "İşlemlerin burada görüntülenecek", + "please_choose_one": "Lütfen bir tane seçin", "please_fill_totp": "Lütfen diğer cihazınızda bulunan 8 haneli kodu girin", "please_make_selection": "Cüzdan oluşturmak veya geri döndürmek için aşağıdan seçim yap.", "please_reference_document": "Daha fazla bilgi için lütfen aşağıdaki belgelere bakınız.", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 13e6267c9..c670b174a 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -437,6 +437,7 @@ "monero_light_theme": "Легка тема Monero", "moonpay_alert_text": "Значення суми має бути більшим або дорівнювати ${minAmount} ${fiatCurrency}", "more_options": "Більше параметрів", + "multiple_addresses_detected": "Виявлено кілька адрес", "mweb_confirmed": "Підтвердив Mweb", "mweb_unconfirmed": "Неперевірений MWEB", "name": "Ім'я", @@ -513,6 +514,7 @@ "pin_number": "PIN-код", "placeholder_contacts": "Тут будуть показані ваші контакти", "placeholder_transactions": "Тут відображатимуться ваші транзакції", + "please_choose_one": "Будь ласка, виберіть один", "please_fill_totp": "Будь ласка, введіть 8-значний код, наявний на вашому іншому пристрої", "please_make_selection": "Оберіть спосіб створення гаманця: створити новий чи відновити ваш існуючий.", "please_reference_document": "Для отримання додаткової інформації зверніться до документів нижче.", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 1c9f6bd6e..8a12b50dc 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -437,6 +437,7 @@ "monero_light_theme": "مونیرو لائٹ تھیم", "moonpay_alert_text": "رقم کی قدر ${minAmount} ${fiatCurrency} کے برابر یا زیادہ ہونی چاہیے۔", "more_options": "مزید زرائے", + "multiple_addresses_detected": "متعدد پتے کا پتہ چلا", "mweb_confirmed": "تصدیق شدہ MWEB", "mweb_unconfirmed": "غیر مصدقہ MWEB", "name": "ﻡﺎﻧ", @@ -515,6 +516,7 @@ "pin_number": "پن نمبر", "placeholder_contacts": "آپ کے رابطے یہاں دکھائے جائیں گے۔", "placeholder_transactions": "آپ کے لین دین یہاں دکھائے جائیں گے۔", + "please_choose_one": "براہ کرم ایک کا انتخاب کریں", "please_fill_totp": "براہ کرم اپنے دوسرے آلے پر موجود 8 ہندسوں کا کوڈ پُر کریں۔", "please_make_selection": "اپنا بٹوہ بنانے یا بازیافت کرنے کے لیے براہ کرم ذیل میں ایک انتخاب کریں۔", "please_reference_document": "مزید معلومات کے لیے براہ کرم نیچے دی گئی دستاویزات کا حوالہ دیں۔", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 6e821a5ab..637e10a76 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -436,6 +436,7 @@ "monero_light_theme": "Chủ đề sáng Monero", "moonpay_alert_text": "Giá trị số tiền phải lớn hơn hoặc bằng ${minAmount} ${fiatCurrency}", "more_options": "Thêm tùy chọn", + "multiple_addresses_detected": "Nhiều địa chỉ được phát hiện", "mweb_confirmed": "Xác nhận MWEB", "mweb_unconfirmed": "MWEB chưa được xác nhận", "name": "Tên", @@ -512,6 +513,7 @@ "pin_number": "Số PIN", "placeholder_contacts": "Danh bạ của bạn sẽ được hiển thị ở đây", "placeholder_transactions": "Giao dịch của bạn sẽ được hiển thị ở đây", + "please_choose_one": "Vui lòng chọn một", "please_fill_totp": "Vui lòng điền mã 8 chữ số trên thiết bị khác của bạn", "please_make_selection": "Vui lòng chọn một tùy chọn dưới đây để tạo hoặc khôi phục ví của bạn.", "please_reference_document": "Vui lòng tham khảo các tài liệu dưới đây để biết thêm thông tin.", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index e3851543c..ca2f6ade0 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -438,6 +438,7 @@ "monero_light_theme": "Monero Light Akori", "moonpay_alert_text": "Iye owó kò gbọ́dọ̀ kéré ju ${minAmount} ${fiatCurrency}", "more_options": "Ìyàn àfikún", + "multiple_addresses_detected": "Awọn adirẹsi ọpọ rii", "mweb_confirmed": "Jẹrisi Mweb", "mweb_unconfirmed": "Ajopo Mweb", "name": "Oruko", @@ -514,6 +515,7 @@ "pin_number": "Òǹkà ìdánimọ̀ àdáni", "placeholder_contacts": "A máa fihàn àwọn olùbásọ̀rọ̀ yín ḿbí", "placeholder_transactions": "A máa fihàn àwọn àránṣẹ́ yín ḿbí", + "please_choose_one": "Jọwọ yan ọkan", "please_fill_totp": "Jọwọ bọ ti ẹrọ ti o wọle ni 8-digits ti o wa ni eto miiran re", "please_make_selection": "Ẹ jọ̀wọ́, yàn dá àpamọ́wọ́ yín tàbí dá àpamọ́wọ́ yín padà n’ísàlẹ̀.", "please_reference_document": "Ẹ jọ̀wọ́ fi àwọn àkọsílẹ̀ l’ábẹ́ túbọ̀ mọ ìsọfúnni.", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index a1e14a5c3..d272b08b2 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -437,6 +437,7 @@ "monero_light_theme": "门罗币浅色主题", "moonpay_alert_text": "金额的价值必须大于或等于 ${minAmount} ${fiatCurrency}", "more_options": "更多选项", + "multiple_addresses_detected": "检测到的多个地址", "mweb_confirmed": "确认的MWEB", "mweb_unconfirmed": "未经证实的MWEB", "name": "姓名", @@ -513,6 +514,7 @@ "pin_number": "PIN 码", "placeholder_contacts": "您的联系人将显示在这里", "placeholder_transactions": "您的交易将显示在这里", + "please_choose_one": "请选择一个", "please_fill_totp": "请填写您其他设备上的 8 位代码", "please_make_selection": "请在下面进行选择 创建或恢复您的钱包.", "please_reference_document": "请参考以下文档以获取更多信息。", From dcd978eb38f370fce975d23517d73aa4728df0f3 Mon Sep 17 00:00:00 2001 From: Serhii Date: Tue, 4 Mar 2025 03:58:56 +0200 Subject: [PATCH 016/243] CW-924-Don't-prompt-to-save-to-address-book-on-standard-Bitcoin-Litecoin-Bitcoin-Cash-addresses (#2043) * don't prompt regular btc ltc bch address * minor fix --- lib/core/address_validator.dart | 3 +++ lib/src/screens/send/send_page.dart | 36 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 65c9faff9..888be1768 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -281,6 +281,9 @@ class AddressValidator extends TextValidator { } } + static String get silentPaymentAddressPattern => SilentPaymentAddress.regex.pattern; + static String get mWebAddressPattern => MwebAddress.regex.pattern; + static String? getAddressFromStringPattern(CryptoCurrency type) { String? pattern = null; diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 6e9178cda..d3c741c0c 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/core/execution_state.dart'; @@ -544,6 +545,10 @@ class SendPage extends BasePage { ); newContactAddress = newContactAddress ?? sendViewModel.newContactAddress(); + if (newContactAddress?.address != null && isRegularElectrumAddress(newContactAddress!.address)) { + newContactAddress = null; + } + if (sendViewModel.coinTypeToSpendFrom != UnspentCoinType.any) newContactAddress = null; if (newContactAddress != null && sendViewModel.showAddressBookPopup) { @@ -664,4 +669,35 @@ class SendPage extends BasePage { ), context: context); } + + bool isRegularElectrumAddress(String address) { + final supportedTypes = [CryptoCurrency.btc, CryptoCurrency.ltc, CryptoCurrency.bch]; + final excludedPatterns = [ + RegExp(AddressValidator.silentPaymentAddressPattern), + RegExp(AddressValidator.mWebAddressPattern) + ]; + + final trimmed = address.trim(); + + bool isValid = false; + for (var type in supportedTypes) { + final addressPattern = AddressValidator.getAddressFromStringPattern(type); + if (addressPattern != null) { + final regex = RegExp('^$addressPattern\$'); + if (regex.hasMatch(trimmed)) { + isValid = true; + break; + } + } + } + + for (var pattern in excludedPatterns) { + if (pattern.hasMatch(trimmed)) { + return false; + } + } + + return isValid; + } + } From f28af937ecc9d176cf4c9330338b4a64d98e7f7c Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Tue, 4 Mar 2025 19:12:19 +0200 Subject: [PATCH 017/243] Fix edge case of sending amount that will leave near dust amount as a change (#2044) --- cw_bitcoin/lib/electrum_wallet.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index e7b8be156..64595b253 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -915,6 +915,10 @@ abstract class ElectrumWalletBase } } + // if the amount left for change is less than dust, but not less than 0 + // then add it to the fees + fee += amountLeftForChange; + return EstimatedTxResult( utxos: utxoDetails.utxos, inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, @@ -1366,7 +1370,7 @@ abstract class ElectrumWalletBase List updatedUnspentCoins = []; final previousUnspentCoins = List.from(unspentCoins.where((utxo) => - utxo.bitcoinAddressRecord.type != SegwitAddresType.mweb && + utxo.bitcoinAddressRecord.type != SegwitAddresType.mweb && utxo.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)); if (hasSilentPaymentsScanning) { @@ -1424,7 +1428,6 @@ abstract class ElectrumWalletBase required List updatedUnspentCoins, required List?> results, }) { - if (failedCount == results.length) { printV("All UTXOs failed to fetch, falling back to previous UTXOs"); return previousUnspentCoins; From e596c19b402cc4f6e7d415abaea7a8626a2aae5a Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Thu, 6 Mar 2025 01:41:23 +0200 Subject: [PATCH 018/243] Fixate decimal points to 2 (#2066) --- lib/buy/moonpay/moonpay_provider.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/buy/moonpay/moonpay_provider.dart b/lib/buy/moonpay/moonpay_provider.dart index 959d38a57..a6966db33 100644 --- a/lib/buy/moonpay/moonpay_provider.dart +++ b/lib/buy/moonpay/moonpay_provider.dart @@ -168,8 +168,7 @@ class MoonPayProvider extends BuyProvider { final params = { 'baseCurrencyCode': baseCurrencyCode, - 'baseCurrencyAmount': amount.toString(), - 'amount': amount.toString(), + 'baseCurrencyAmount': amount.toStringAsFixed(2), 'paymentMethod': paymentMethod, 'areFeesIncluded': 'false', 'apiKey': _apiKey @@ -232,7 +231,7 @@ class MoonPayProvider extends BuyProvider { ? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}' : '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}', 'baseCurrencyCode': isBuyAction ? quote.fiatCurrency.name : quote.cryptoCurrency.name, - 'baseCurrencyAmount': amount.toString(), + 'baseCurrencyAmount': amount.toStringAsFixed(2), 'walletAddress': cryptoCurrencyAddress, 'lockAmount': 'false', 'showAllCurrencies': 'false', From 09f20b2a7bf3ef15bfcfcf4471e1f9dfad94708a Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Thu, 6 Mar 2025 01:25:38 +0100 Subject: [PATCH 019/243] CW-843: Enhance Wallet Groups Implementation (#2045) * feat: Enhance Wallet Groups Implementation by using hashedIdentifiers instead of parentAddresses * fix: Call updateWalletGroups even if group has an hash identifier * feat: Add secrets to workflow * feat: Enhance Wallet Groups Implementation by using hashedIdentifiers instead of parentAddresses * Handle wallet grouping edgecase where wallet is restored via non seed medium * fix: Valid wallet/wallet groups not showing up when choosing wallet/groups for creating new wallets --- .../workflows/automated_integration_test.yml | 4 + .github/workflows/pr_test_build_android.yml | 1 + .github/workflows/pr_test_build_linux.yml | 1 + .../bitcoin_wallet_creation_credentials.dart | 2 - ...coin_cash_wallet_creation_credentials.dart | 2 - cw_core/lib/wallet_credentials.dart | 2 - cw_core/lib/wallet_info.dart | 11 +- ...evm_chain_wallet_creation_credentials.dart | 1 - .../lib/nano_wallet_creation_credentials.dart | 2 - .../solana_wallet_creation_credentials.dart | 2 - .../lib/tron_wallet_creation_credentials.dart | 2 - ios/Podfile.lock | 2 +- lib/bitcoin/cw_bitcoin.dart | 2 - lib/bitcoin_cash/cw_bitcoin_cash.dart | 2 - lib/core/new_wallet_arguments.dart | 2 - lib/di.dart | 11 +- lib/entities/hash_wallet_identifier.dart | 21 +++ lib/entities/wallet_edit_page_arguments.dart | 4 +- lib/entities/wallet_group.dart | 11 +- lib/entities/wallet_manager.dart | 145 ++++++++++++------ lib/ethereum/cw_ethereum.dart | 2 - lib/nano/cw_nano.dart | 2 - lib/polygon/cw_polygon.dart | 2 - lib/reactions/on_current_wallet_change.dart | 4 + lib/solana/cw_solana.dart | 2 - .../new_wallet/wallet_group_display_page.dart | 1 - lib/src/screens/wallet/wallet_edit_page.dart | 4 +- .../screens/wallet_list/wallet_list_page.dart | 2 +- lib/tron/cw_tron.dart | 13 +- lib/view_model/wallet_creation_vm.dart | 6 +- .../wallet_groups_display_view_model.dart | 10 +- .../wallet_list/wallet_edit_view_model.dart | 4 +- lib/view_model/wallet_new_vm.dart | 7 - tool/configure.dart | 13 +- tool/utils/secret_key.dart | 2 + 35 files changed, 181 insertions(+), 123 deletions(-) create mode 100644 lib/entities/hash_wallet_identifier.dart diff --git a/.github/workflows/automated_integration_test.yml b/.github/workflows/automated_integration_test.yml index 0869db8ea..539513111 100644 --- a/.github/workflows/automated_integration_test.yml +++ b/.github/workflows/automated_integration_test.yml @@ -223,6 +223,10 @@ jobs: echo "const nanoTestWalletReceiveAddress = '${{ secrets.NANO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const wowneroTestWalletReceiveAddress = '${{ secrets.WOWNERO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const moneroTestWalletBlockHeight = '${{ secrets.MONERO_TEST_WALLET_BLOCK_HEIGHT }}';" >> lib/.secrets.g.dart + # end of test secrets + echo "const chainflipApiKey = '${{ secrets.CHAINFLIP_API_KEY }}';" >> lib/.secrets.g.dart + echo "const chainflipAffiliateFee = '${{ secrets.CHAINFLIP_AFFILIATE_FEE }}';" >> lib/.secrets.g.dart + echo "const walletGroupSalt = '${{ secrets.WALLET_GROUP_SALT }}';" >> lib/.secrets.g.dart - name: Rename app run: | diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 25fb144e3..6c7e82409 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -172,6 +172,7 @@ jobs: # end of test secrets echo "const chainflipApiKey = '${{ secrets.CHAINFLIP_API_KEY }}';" >> lib/.secrets.g.dart echo "const chainflipAffiliateFee = '${{ secrets.CHAINFLIP_AFFILIATE_FEE }}';" >> lib/.secrets.g.dart + echo "const walletGroupSalt = '${{ secrets.WALLET_GROUP_SALT }}';" >> lib/.secrets.g.dart - name: prepare monero_c and cache run: | diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index a9d8085b6..e307dc410 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -168,6 +168,7 @@ jobs: # end of test secrets echo "const chainflipApiKey = '${{ secrets.CHAINFLIP_API_KEY }}';" >> lib/.secrets.g.dart echo "const chainflipAffiliateFee = '${{ secrets.CHAINFLIP_AFFILIATE_FEE }}';" >> lib/.secrets.g.dart + echo "const walletGroupSalt = '${{ secrets.WALLET_GROUP_SALT }}';" >> lib/.secrets.g.dart - name: prepare monero_c and cache run: | diff --git a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart index a1b1418b8..177d61e87 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart @@ -11,13 +11,11 @@ class BitcoinNewWalletCredentials extends WalletCredentials { String? derivationPath, String? passphrase, this.mnemonic, - String? parentAddress, }) : super( name: name, walletInfo: walletInfo, password: password, passphrase: passphrase, - parentAddress: parentAddress, ); final String? mnemonic; diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_creation_credentials.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_creation_credentials.dart index af93cdbf8..90004a485 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_creation_credentials.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_creation_credentials.dart @@ -8,13 +8,11 @@ class BitcoinCashNewWalletCredentials extends WalletCredentials { String? password, String? passphrase, this.mnemonic, - String? parentAddress, }) : super( name: name, walletInfo: walletInfo, password: password, passphrase: passphrase, - parentAddress: parentAddress ); final String? mnemonic; } diff --git a/cw_core/lib/wallet_credentials.dart b/cw_core/lib/wallet_credentials.dart index 55c24bf37..30ae2546c 100644 --- a/cw_core/lib/wallet_credentials.dart +++ b/cw_core/lib/wallet_credentials.dart @@ -10,7 +10,6 @@ abstract class WalletCredentials { this.passphrase, this.derivationInfo, this.hardwareWalletType, - this.parentAddress, }) { if (this.walletInfo != null && derivationInfo != null) { this.walletInfo!.derivationInfo = derivationInfo; @@ -19,7 +18,6 @@ abstract class WalletCredentials { final String name; final int? height; - String? parentAddress; int? seedPhraseLength; String? password; String? passphrase; diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index bd035e30a..d62e61941 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -81,6 +81,8 @@ class WalletInfo extends HiveObject { this.derivationInfo, this.hardwareWalletType, this.parentAddress, + this.hashedWalletIdentifier, + this.isNonSeedWallet, ) : _yatLastUsedAddressController = StreamController.broadcast(); factory WalletInfo.external({ @@ -99,6 +101,8 @@ class WalletInfo extends HiveObject { DerivationInfo? derivationInfo, HardwareWalletType? hardwareWalletType, String? parentAddress, + String? hashedWalletIdentifier, + bool? isNonSeedWallet, }) { return WalletInfo( id, @@ -116,6 +120,8 @@ class WalletInfo extends HiveObject { derivationInfo, hardwareWalletType, parentAddress, + hashedWalletIdentifier, + isNonSeedWallet ?? false, ); } @@ -196,8 +202,11 @@ class WalletInfo extends HiveObject { @HiveField(24) List? manualAddresses; - + @HiveField(25) + String? hashedWalletIdentifier; + @HiveField(26, defaultValue: false) + bool isNonSeedWallet; String get yatLastUsedAddress => yatLastUsedAddressRaw ?? ''; diff --git a/cw_evm/lib/evm_chain_wallet_creation_credentials.dart b/cw_evm/lib/evm_chain_wallet_creation_credentials.dart index 5075e6289..d9595a243 100644 --- a/cw_evm/lib/evm_chain_wallet_creation_credentials.dart +++ b/cw_evm/lib/evm_chain_wallet_creation_credentials.dart @@ -7,7 +7,6 @@ class EVMChainNewWalletCredentials extends WalletCredentials { required super.name, super.walletInfo, super.password, - super.parentAddress, this.mnemonic, super.passphrase, }); diff --git a/cw_nano/lib/nano_wallet_creation_credentials.dart b/cw_nano/lib/nano_wallet_creation_credentials.dart index 59789aec7..8eb6dc2d2 100644 --- a/cw_nano/lib/nano_wallet_creation_credentials.dart +++ b/cw_nano/lib/nano_wallet_creation_credentials.dart @@ -8,13 +8,11 @@ class NanoNewWalletCredentials extends WalletCredentials { String? password, DerivationType? derivationType, this.mnemonic, - String? parentAddress, String? passphrase, }) : super( name: name, password: password, walletInfo: walletInfo, - parentAddress: parentAddress, passphrase: passphrase, ); diff --git a/cw_solana/lib/solana_wallet_creation_credentials.dart b/cw_solana/lib/solana_wallet_creation_credentials.dart index 121ef2b44..309c41cf3 100644 --- a/cw_solana/lib/solana_wallet_creation_credentials.dart +++ b/cw_solana/lib/solana_wallet_creation_credentials.dart @@ -6,14 +6,12 @@ class SolanaNewWalletCredentials extends WalletCredentials { required String name, WalletInfo? walletInfo, String? password, - String? parentAddress, this.mnemonic, String? passphrase, }) : super( name: name, walletInfo: walletInfo, password: password, - parentAddress: parentAddress, passphrase: passphrase, ); final String? mnemonic; diff --git a/cw_tron/lib/tron_wallet_creation_credentials.dart b/cw_tron/lib/tron_wallet_creation_credentials.dart index fd9066acd..402cff2ff 100644 --- a/cw_tron/lib/tron_wallet_creation_credentials.dart +++ b/cw_tron/lib/tron_wallet_creation_credentials.dart @@ -7,13 +7,11 @@ class TronNewWalletCredentials extends WalletCredentials { WalletInfo? walletInfo, String? password, this.mnemonic, - String? parentAddress, String? passphrase, }) : super( name: name, walletInfo: walletInfo, password: password, - parentAddress: parentAddress, passphrase: passphrase, ); diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 9e2a8507a..d400d3f81 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -277,4 +277,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: e448f662d4c41f0c0b1ccbb78afd57dbf895a597 -COCOAPODS: 1.15.2 +COCOAPODS: 1.15.2 \ No newline at end of file diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index bf9ec0c4d..f93ba9550 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -34,7 +34,6 @@ class CWBitcoin extends Bitcoin { String? password, String? passphrase, String? mnemonic, - String? parentAddress, }) => BitcoinNewWalletCredentials( name: name, @@ -42,7 +41,6 @@ class CWBitcoin extends Bitcoin { password: password, passphrase: passphrase, mnemonic: mnemonic, - parentAddress: parentAddress, ); @override diff --git a/lib/bitcoin_cash/cw_bitcoin_cash.dart b/lib/bitcoin_cash/cw_bitcoin_cash.dart index b74448703..be0323bfa 100644 --- a/lib/bitcoin_cash/cw_bitcoin_cash.dart +++ b/lib/bitcoin_cash/cw_bitcoin_cash.dart @@ -17,14 +17,12 @@ class CWBitcoinCash extends BitcoinCash { String? password, String? passphrase, String? mnemonic, - String? parentAddress, }) => BitcoinCashNewWalletCredentials( name: name, walletInfo: walletInfo, password: password, passphrase: passphrase, - parentAddress: parentAddress, mnemonic: mnemonic, ); diff --git a/lib/core/new_wallet_arguments.dart b/lib/core/new_wallet_arguments.dart index 2581c57bb..9e2ef9df4 100644 --- a/lib/core/new_wallet_arguments.dart +++ b/lib/core/new_wallet_arguments.dart @@ -3,12 +3,10 @@ import 'package:cw_core/wallet_type.dart'; class NewWalletArguments { final WalletType type; final String? mnemonic; - final String? parentAddress; final bool isChildWallet; NewWalletArguments({ required this.type, - this.parentAddress, this.mnemonic, this.isChildWallet = false, }); diff --git a/lib/di.dart b/lib/di.dart index 3e10bd7a1..174183f9a 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -392,11 +392,10 @@ Future setup({ getIt.registerFactory(() => NewWalletTypeViewModel(_walletInfoSource)); getIt.registerFactory( - () { - final instance = WalletManager(_walletInfoSource, getIt.get()); - instance.updateWalletGroups(); - return instance; - }, + () => WalletManager( + _walletInfoSource, + getIt.get(), + ), ); getIt.registerFactoryParam( @@ -812,7 +811,7 @@ Future setup({ editingWallet: arguments.editingWallet, isWalletGroup: arguments.isWalletGroup, groupName: arguments.groupName, - parentAddress: arguments.parentAddress, + walletGroupKey: arguments.walletGroupKey, ), ); }); diff --git a/lib/entities/hash_wallet_identifier.dart b/lib/entities/hash_wallet_identifier.dart new file mode 100644 index 000000000..8e593ec79 --- /dev/null +++ b/lib/entities/hash_wallet_identifier.dart @@ -0,0 +1,21 @@ +import 'dart:convert'; + +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cw_core/wallet_base.dart'; +import 'package:hashlib/hashlib.dart'; + +String createHashedWalletIdentifier(WalletBase wallet) { + if (wallet.seed == null) return ''; + + final salt = secrets.walletGroupSalt; + final combined = '$salt.${wallet.seed}'; + + // Convert to UTF-8 bytes. + final bytes = utf8.encode(combined); + + // Perform SHA-256 hash. + final digest = sha256.convert(bytes); + + // Return the hex string representation of the hash. + return digest.toString(); +} diff --git a/lib/entities/wallet_edit_page_arguments.dart b/lib/entities/wallet_edit_page_arguments.dart index 260471f7e..6217195dc 100644 --- a/lib/entities/wallet_edit_page_arguments.dart +++ b/lib/entities/wallet_edit_page_arguments.dart @@ -10,7 +10,7 @@ class WalletEditPageArguments { this.isWalletGroup = false, this.walletListViewModel, this.groupName = '', - this.parentAddress = '', + this.walletGroupKey = '', this.walletEditViewModel, this.walletNewVM, this.authService, @@ -19,7 +19,7 @@ class WalletEditPageArguments { final WalletListItem editingWallet; final bool isWalletGroup; final String groupName; - final String parentAddress; + final String walletGroupKey; final WalletListViewModel? walletListViewModel; final WalletEditViewModel? walletEditViewModel; diff --git a/lib/entities/wallet_group.dart b/lib/entities/wallet_group.dart index 9845aea65..ab94f3eb3 100644 --- a/lib/entities/wallet_group.dart +++ b/lib/entities/wallet_group.dart @@ -1,13 +1,14 @@ import 'package:cw_core/wallet_info.dart'; class WalletGroup { - WalletGroup(this.parentAddress) : wallets = []; + WalletGroup(this.groupKey) : wallets = []; - /// Main identifier for each group, compulsory. - final String parentAddress; + /// Primary identifier for the group. Previously was `parentAddress`. + /// Now we store either the wallet's hash OR fallback to parentAddress/address. + final String groupKey; - /// Child wallets that share the same parent address within this group - List wallets; + /// Child wallets that share the same group key + final List wallets; /// Custom name for the group, editable for multi-child wallet groups String? groupName; diff --git a/lib/entities/wallet_manager.dart b/lib/entities/wallet_manager.dart index 29c873dae..8bcabcaf2 100644 --- a/lib/entities/wallet_manager.dart +++ b/lib/entities/wallet_manager.dart @@ -1,70 +1,61 @@ +import 'package:cake_wallet/entities/hash_wallet_identifier.dart'; import 'package:cake_wallet/entities/wallet_group.dart'; +import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:hive/hive.dart'; import 'package:shared_preferences/shared_preferences.dart'; class WalletManager { - WalletManager( - this._walletInfoSource, - this._sharedPreferences, - ); + WalletManager(this._walletInfoSource, this._sharedPreferences); final Box _walletInfoSource; final SharedPreferences _sharedPreferences; final List walletGroups = []; - /// Categorize wallets into groups based on their parentAddress. - /// - /// Update the lead wallet for each group and clean up empty groups - /// i.e remove group if there's no lead wallet (i.e, no wallets left) void updateWalletGroups() { walletGroups.clear(); - for (var walletInfo in _walletInfoSource.values) { - final group = _getOrCreateGroup(_resolveParentAddress(walletInfo)); + for (final walletInfo in _walletInfoSource.values) { + final groupKey = _resolveGroupKey(walletInfo); + final group = _getOrCreateGroup(groupKey); group.wallets.add(walletInfo); } - walletGroups.removeWhere((group) => group.wallets.isEmpty); - + walletGroups.removeWhere((g) => g.wallets.isEmpty); _loadCustomGroupNames(); } - /// Function to determine the correct parentAddress for a wallet. - /// - /// If it's a parent wallet (parentAddress is null), - /// use its own address as parentAddress. - String _resolveParentAddress(WalletInfo walletInfo) { + String _resolveGroupKey(WalletInfo walletInfo) { + if (walletInfo.hashedWalletIdentifier != null && + walletInfo.hashedWalletIdentifier!.isNotEmpty) { + return walletInfo.hashedWalletIdentifier!; + } + + // Fallback to old logic return walletInfo.parentAddress ?? walletInfo.address; } - /// Check if a group with the parentAddress already exists, - /// If no group exists, create a new one. - /// - WalletGroup _getOrCreateGroup(String parentAddress) { + WalletGroup _getOrCreateGroup(String groupKey) { return walletGroups.firstWhere( - (group) => group.parentAddress == parentAddress, + (g) => g.groupKey == groupKey, orElse: () { - final newGroup = WalletGroup(parentAddress); + final newGroup = WalletGroup(groupKey); walletGroups.add(newGroup); return newGroup; }, ); } - /// Add a new wallet and update lead wallet after adding. void addWallet(WalletInfo walletInfo) { - final group = _getOrCreateGroup(_resolveParentAddress(walletInfo)); + final groupKey = _resolveGroupKey(walletInfo); + final group = _getOrCreateGroup(groupKey); group.wallets.add(walletInfo); } - /// Removes a wallet from a group i.e when it's deleted. - /// - /// Update lead wallet after removing, - /// Remove the group if it's empty (i.e., no lead wallet). void removeWallet(WalletInfo walletInfo) { - final group = _getOrCreateGroup(_resolveParentAddress(walletInfo)); + final groupKey = _resolveGroupKey(walletInfo); + final group = _getOrCreateGroup(groupKey); group.wallets.remove(walletInfo); if (group.wallets.isEmpty) { @@ -72,39 +63,99 @@ class WalletManager { } } - /// Returns all the child wallets within a group. - /// - /// If the group is not found, returns an empty group with no wallets. - List getWalletsInGroup(String parentAddress) { + List getWalletsInGroup(String groupKey) { return walletGroups .firstWhere( - (group) => group.parentAddress == parentAddress, - orElse: () => WalletGroup(parentAddress), + (g) => g.groupKey == groupKey, + orElse: () => WalletGroup(groupKey), ) .wallets; } - /// Iterate through all groups and load their custom names from storage void _loadCustomGroupNames() { for (var group in walletGroups) { - final groupName = _sharedPreferences.getString('wallet_group_name_${group.parentAddress}'); + final key = 'wallet_group_name_${group.groupKey}'; + final groupName = _sharedPreferences.getString(key); if (groupName != null && group.wallets.length > 1) { - group.groupName = groupName; // Restore custom name + group.groupName = groupName; } } } - /// Save custom name for a group - void _saveCustomGroupName(String parentAddress, String name) { - _sharedPreferences.setString('wallet_group_name_$parentAddress', name); + void _saveCustomGroupName(String groupKey, String name) { + _sharedPreferences.setString('wallet_group_name_$groupKey', name); } - // Set custom group name and persist it - void setGroupName(String parentAddress, String name) { - if (parentAddress.isEmpty || name.isEmpty) return; + void setGroupName(String groupKey, String name) { + if (groupKey.isEmpty || name.isEmpty) return; - final group = walletGroups.firstWhere((group) => group.parentAddress == parentAddress); + final group = walletGroups.firstWhere((g) => g.groupKey == groupKey); group.setCustomName(name); - _saveCustomGroupName(parentAddress, name); // Persist the custom name + _saveCustomGroupName(groupKey, name); + } + + // --------------------------------------------------------------------------- + // This performs a Group-Based Lazy Migration: + // If the user opens a wallet in an old group, + // we migrate ALL wallets that share its old group key to a new hash. + // --------------------------------------------------------------------------- + + /// When a user opens a wallet, check if it has a real hash. + /// If not, migrate the ENTIRE old group so they keep the same group name + /// and end up with the same new hash (preserving grouping). + Future ensureGroupHasHashedIdentifier(WalletBase openedWallet) async { + WalletInfo walletInfo = openedWallet.walletInfo; + + // If the openedWallet already has an hash, then there is nothing to do + if (walletInfo.hashedWalletIdentifier != null && + walletInfo.hashedWalletIdentifier!.isNotEmpty) { + updateWalletGroups(); // Still skeptical of calling this here. Looking for a better spot. + return; + } + + // Identify the old group key for this wallet + final oldGroupKey = _resolveGroupKey(walletInfo); // parentAddress fallback + + // Find all wallets that share this old group key (i.e the old group) + final oldGroupWallets = _walletInfoSource.values.where((w) { + final key = w.hashedWalletIdentifier != null && w.hashedWalletIdentifier!.isNotEmpty + ? w.hashedWalletIdentifier + : (w.parentAddress ?? w.address); + return key == oldGroupKey; + }).toList(); + + if (oldGroupWallets.isEmpty) { + // This shouldn't happen, but just in case it does, we return. + return; + } + + // Next, we determine the new group hash for these wallets + // Since they share the same seed, we can assign that group hash + // to all the wallets to preserve grouping. + final newGroupHash = createHashedWalletIdentifier(openedWallet); + + // Migrate the old group name from oldGroupKey(i.e parentAddress) to newGroupHash + await _migrateGroupName(oldGroupKey, newGroupHash); + + // Then we assign this new hash to each wallet in that old group and save them + for (final wallet in oldGroupWallets) { + wallet.hashedWalletIdentifier = newGroupHash; + await wallet.save(); + } + + // Finally, we rebuild the groups so that these wallets are now in the new group + updateWalletGroups(); + } + + /// Copy an old group name to the new group key, then remove the old key. + Future _migrateGroupName(String oldGroupKey, String newGroupKey) async { + final oldNameKey = 'wallet_group_name_$oldGroupKey'; + final newNameKey = 'wallet_group_name_$newGroupKey'; + + final oldGroupName = _sharedPreferences.getString(oldNameKey); + if (oldGroupName != null) { + await _sharedPreferences.setString(newNameKey, oldGroupName); + await _sharedPreferences.remove(oldNameKey); + } } } diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart index dc91e4fc2..40c7a0f77 100644 --- a/lib/ethereum/cw_ethereum.dart +++ b/lib/ethereum/cw_ethereum.dart @@ -11,7 +11,6 @@ class CWEthereum extends Ethereum { WalletCredentials createEthereumNewWalletCredentials({ required String name, String? mnemonic, - String? parentAddress, WalletInfo? walletInfo, String? password, String? passphrase, @@ -20,7 +19,6 @@ class CWEthereum extends Ethereum { name: name, walletInfo: walletInfo, password: password, - parentAddress: parentAddress, mnemonic: mnemonic, passphrase: passphrase, ); diff --git a/lib/nano/cw_nano.dart b/lib/nano/cw_nano.dart index 9a2243d00..463e19d65 100644 --- a/lib/nano/cw_nano.dart +++ b/lib/nano/cw_nano.dart @@ -94,14 +94,12 @@ class CWNano extends Nano { WalletInfo? walletInfo, String? password, String? mnemonic, - String? parentAddress, String? passphrase, }) => NanoNewWalletCredentials( name: name, password: password, mnemonic: mnemonic, - parentAddress: parentAddress, walletInfo: walletInfo, passphrase: passphrase, ); diff --git a/lib/polygon/cw_polygon.dart b/lib/polygon/cw_polygon.dart index b8f78e9e2..ec98137c5 100644 --- a/lib/polygon/cw_polygon.dart +++ b/lib/polygon/cw_polygon.dart @@ -11,7 +11,6 @@ class CWPolygon extends Polygon { WalletCredentials createPolygonNewWalletCredentials({ required String name, String? mnemonic, - String? parentAddress, WalletInfo? walletInfo, String? password, String? passphrase, @@ -21,7 +20,6 @@ class CWPolygon extends Polygon { walletInfo: walletInfo, password: password, mnemonic: mnemonic, - parentAddress: parentAddress, passphrase: passphrase, ); diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 3840b042e..a6475571d 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -1,6 +1,8 @@ +import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/update_haven_rate.dart'; +import 'package:cake_wallet/entities/wallet_manager.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; @@ -59,6 +61,8 @@ void startCurrentWalletChangeReaction( return; } + await getIt.get().ensureGroupHasHashedIdentifier(wallet); + final node = settingsStore.getCurrentNode(wallet.type); startWalletSyncStatusChangeReaction(wallet, fiatConversionStore); diff --git a/lib/solana/cw_solana.dart b/lib/solana/cw_solana.dart index d8257396f..f0df5fba1 100644 --- a/lib/solana/cw_solana.dart +++ b/lib/solana/cw_solana.dart @@ -11,7 +11,6 @@ class CWSolana extends Solana { WalletCredentials createSolanaNewWalletCredentials({ required String name, String? mnemonic, - String? parentAddress, WalletInfo? walletInfo, String? password, String? passphrase, @@ -21,7 +20,6 @@ class CWSolana extends Solana { walletInfo: walletInfo, password: password, mnemonic: mnemonic, - parentAddress: parentAddress, passphrase: passphrase, ); diff --git a/lib/src/screens/new_wallet/wallet_group_display_page.dart b/lib/src/screens/new_wallet/wallet_group_display_page.dart index 549449a68..cd64353e2 100644 --- a/lib/src/screens/new_wallet/wallet_group_display_page.dart +++ b/lib/src/screens/new_wallet/wallet_group_display_page.dart @@ -150,7 +150,6 @@ class WalletGroupsDisplayBody extends StatelessWidget { arguments: NewWalletArguments( type: walletGroupsDisplayViewModel.type, mnemonic: mnemonic, - parentAddress: walletGroupsDisplayViewModel.parentAddress, isChildWallet: true, ), ); diff --git a/lib/src/screens/wallet/wallet_edit_page.dart b/lib/src/screens/wallet/wallet_edit_page.dart index 340091a1e..9e62284de 100644 --- a/lib/src/screens/wallet/wallet_edit_page.dart +++ b/lib/src/screens/wallet/wallet_edit_page.dart @@ -112,7 +112,7 @@ class WalletEditPage extends BasePage { pageArguments.editingWallet, password: password, isWalletGroup: pageArguments.isWalletGroup, - groupParentAddress: pageArguments.parentAddress, + walletGroupKey: pageArguments.walletGroupKey, ); }, callback: (bool isAuthenticatedSuccessfully, @@ -128,7 +128,7 @@ class WalletEditPage extends BasePage { await walletEditViewModel.changeName( pageArguments.editingWallet, isWalletGroup: pageArguments.isWalletGroup, - groupParentAddress: pageArguments.parentAddress, + walletGroupKey: pageArguments.walletGroupKey, ); confirmed = true; } diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index 681ca4d8a..569dce958 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -227,7 +227,7 @@ class WalletListBodyState extends State { editingWallet: wallet, isWalletGroup: true, groupName: groupName, - parentAddress: group.parentAddress, + walletGroupKey: group.groupKey, ), ); }, diff --git a/lib/tron/cw_tron.dart b/lib/tron/cw_tron.dart index bf2fac590..2726e873d 100644 --- a/lib/tron/cw_tron.dart +++ b/lib/tron/cw_tron.dart @@ -14,16 +14,15 @@ class CWTron extends Tron { WalletInfo? walletInfo, String? password, String? mnemonic, - String? parentAddress, String? passphrase, }) => TronNewWalletCredentials( - name: name, - walletInfo: walletInfo, - password: password, - mnemonic: mnemonic, - passphrase: passphrase, - parentAddress: parentAddress); + name: name, + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + ); @override WalletCredentials createTronRestoreWalletFromSeedCredentials({ diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index bd6732b4a..edaff441d 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/entities/generate_name.dart'; +import 'package:cake_wallet/entities/hash_wallet_identifier.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/store/app_store.dart'; @@ -103,13 +104,16 @@ abstract class WalletCreationVMBase with Store { showIntroCakePayCard: (!walletCreationService.typeExists(type)) && type != WalletType.haven, derivationInfo: credentials.derivationInfo ?? getDefaultCreateDerivation(), hardwareWalletType: credentials.hardwareWalletType, - parentAddress: credentials.parentAddress, ); credentials.walletInfo = walletInfo; final wallet = restoreWallet != null ? await processFromRestoredWallet(credentials, restoreWallet) : await process(credentials); + + final isNonSeedWallet = isRecovery ? wallet.seed == null : false; + walletInfo.isNonSeedWallet = isNonSeedWallet; + walletInfo.hashedWalletIdentifier = createHashedWalletIdentifier(wallet); walletInfo.address = wallet.walletAddresses.address; await _walletInfoSource.add(walletInfo); await _appStore.changeCurrentWallet(wallet); diff --git a/lib/view_model/wallet_groups_display_view_model.dart b/lib/view_model/wallet_groups_display_view_model.dart index 08515febf..09d6d656c 100644 --- a/lib/view_model/wallet_groups_display_view_model.dart +++ b/lib/view_model/wallet_groups_display_view_model.dart @@ -47,8 +47,6 @@ abstract class WalletGroupsDisplayViewModelBase with Store { @observable WalletInfo? selectedSingleWallet; - @observable - String? parentAddress; @observable bool isFetchingMnemonic; @@ -77,9 +75,6 @@ abstract class WalletGroupsDisplayViewModelBase with Store { walletToUse.name, ); - parentAddress = - isGroupSelected ? selectedWalletGroup!.parentAddress : selectedSingleWallet!.address; - return wallet.seed; } catch (e) { return null; @@ -130,11 +125,14 @@ abstract class WalletGroupsDisplayViewModelBase with Store { // Check that selected wallet type is not present already in group bool isSameTypeAsSelectedWallet = wallet.type == type; + bool isNonSeedWallet = wallet.isNonSeedWallet; + // Exclude if any of these conditions are true return isNonBIP39Wallet || isNanoDerivationType || isElectrumDerivationType || - isSameTypeAsSelectedWallet; + isSameTypeAsSelectedWallet || + isNonSeedWallet; }); if (shouldExcludeGroup) continue; diff --git a/lib/view_model/wallet_list/wallet_edit_view_model.dart b/lib/view_model/wallet_list/wallet_edit_view_model.dart index 343f160db..5379f1679 100644 --- a/lib/view_model/wallet_list/wallet_edit_view_model.dart +++ b/lib/view_model/wallet_list/wallet_edit_view_model.dart @@ -40,7 +40,7 @@ abstract class WalletEditViewModelBase with Store { Future changeName( WalletListItem walletItem, { String? password, - String? groupParentAddress, + String? walletGroupKey, bool isWalletGroup = false, }) async { state = WalletEditRenamePending(); @@ -48,7 +48,7 @@ abstract class WalletEditViewModelBase with Store { if (isWalletGroup) { _walletManager.updateWalletGroups(); - _walletManager.setGroupName(groupParentAddress!, newName); + _walletManager.setGroupName(walletGroupKey!, newName); } else { await _walletLoadingService.renameWallet( walletItem.type, diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index aa933eadc..0cd730028 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -109,7 +109,6 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { password: walletPassword, passphrase: passphrase, mnemonic: newWalletArguments!.mnemonic, - parentAddress: newWalletArguments!.parentAddress, ); case WalletType.haven: return haven!.createHavenNewWalletCredentials( @@ -119,7 +118,6 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { name: name, password: walletPassword, mnemonic: newWalletArguments!.mnemonic, - parentAddress: newWalletArguments!.parentAddress, passphrase: passphrase, ); case WalletType.bitcoinCash: @@ -128,7 +126,6 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { password: walletPassword, passphrase: passphrase, mnemonic: newWalletArguments!.mnemonic, - parentAddress: newWalletArguments!.parentAddress, ); case WalletType.nano: case WalletType.banano: @@ -136,7 +133,6 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { name: name, password: walletPassword, mnemonic: newWalletArguments!.mnemonic, - parentAddress: newWalletArguments!.parentAddress, passphrase: passphrase, ); case WalletType.polygon: @@ -144,7 +140,6 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { name: name, password: walletPassword, mnemonic: newWalletArguments!.mnemonic, - parentAddress: newWalletArguments!.parentAddress, passphrase: passphrase, ); case WalletType.solana: @@ -152,7 +147,6 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { name: name, password: walletPassword, mnemonic: newWalletArguments!.mnemonic, - parentAddress: newWalletArguments!.parentAddress, passphrase: passphrase, ); case WalletType.tron: @@ -160,7 +154,6 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { name: name, password: walletPassword, mnemonic: newWalletArguments!.mnemonic, - parentAddress: newWalletArguments!.parentAddress, passphrase: passphrase, ); case WalletType.wownero: diff --git a/tool/configure.dart b/tool/configure.dart index 214288078..c26e3f44b 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -160,7 +160,7 @@ abstract class Bitcoin { String? passphrase, }); WalletCredentials createBitcoinRestoreWalletFromWIFCredentials({required String name, required String password, required String wif, WalletInfo? walletInfo}); - WalletCredentials createBitcoinNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? passphrase, String? mnemonic, String? parentAddress}); + WalletCredentials createBitcoinNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? passphrase, String? mnemonic}); WalletCredentials createBitcoinHardwareWalletCredentials({required String name, required HardwareAccountData accountData, WalletInfo? walletInfo}); List getWordList(); Map getWalletKeys(Object wallet); @@ -882,7 +882,7 @@ import 'package:eth_sig_util/util/utils.dart'; abstract class Ethereum { List getEthereumWordList(String language); WalletService createEthereumWalletService(Box walletInfoSource, bool isDirect); - WalletCredentials createEthereumNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? parentAddress, String? passphrase}); + WalletCredentials createEthereumNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? passphrase}); WalletCredentials createEthereumRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password, String? passphrase}); WalletCredentials createEthereumRestoreWalletFromPrivateKey({required String name, required String privateKey, required String password}); WalletCredentials createEthereumHardwareWalletCredentials({required String name, required HardwareAccountData hwAccountData, WalletInfo? walletInfo}); @@ -989,7 +989,7 @@ import 'package:eth_sig_util/util/utils.dart'; abstract class Polygon { List getPolygonWordList(String language); WalletService createPolygonWalletService(Box walletInfoSource, bool isDirect); - WalletCredentials createPolygonNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? parentAddress, String? passphrase}); + WalletCredentials createPolygonNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? passphrase}); WalletCredentials createPolygonRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password, String? passphrase}); WalletCredentials createPolygonRestoreWalletFromPrivateKey({required String name, required String privateKey, required String password}); WalletCredentials createPolygonHardwareWalletCredentials({required String name, required HardwareAccountData hwAccountData, WalletInfo? walletInfo}); @@ -1077,7 +1077,7 @@ abstract class BitcoinCash { Box walletInfoSource, Box unspentCoinSource, bool isDirect); WalletCredentials createBitcoinCashNewWalletCredentials( - {required String name, WalletInfo? walletInfo, String? password, String? passphrase, String? mnemonic, String? parentAddress}); + {required String name, WalletInfo? walletInfo, String? password, String? passphrase, String? mnemonic}); WalletCredentials createBitcoinCashRestoreWalletFromSeedCredentials( {required String name, required String mnemonic, required String password, String? passphrase}); @@ -1161,7 +1161,6 @@ abstract class Nano { required String name, String? password, String? mnemonic, - String? parentAddress, WalletInfo? walletInfo, String? passphrase, }); @@ -1281,7 +1280,7 @@ abstract class Solana { List getSolanaWordList(String language); WalletService createSolanaWalletService(Box walletInfoSource, bool isDirect); WalletCredentials createSolanaNewWalletCredentials( - {required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? parentAddress, String? passphrase}); + {required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? passphrase}); WalletCredentials createSolanaRestoreWalletFromSeedCredentials( {required String name, required String mnemonic, required String password, String? passphrase}); WalletCredentials createSolanaRestoreWalletFromPrivateKey( @@ -1369,7 +1368,7 @@ import 'package:cw_tron/default_tron_tokens.dart'; abstract class Tron { List getTronWordList(String language); WalletService createTronWalletService(Box walletInfoSource, bool isDirect); - WalletCredentials createTronNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? parentAddress, String? passphrase}); + WalletCredentials createTronNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? passphrase}); WalletCredentials createTronRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password, String? passphrase}); WalletCredentials createTronRestoreWalletFromPrivateKey({required String name, required String privateKey, required String password}); String getAddress(WalletBase wallet); diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index 48614900a..0ef38e939 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -77,6 +77,7 @@ class SecretKey { SecretKey('moneroTestWalletBlockHeight', () => ''), SecretKey('chainflipApiKey', () => ''), SecretKey('chainflipAffiliateFee', () => ''), + SecretKey('walletGroupSalt', () => hex.encode(encrypt.Key.fromSecureRandom(16).bytes)), ]; static final evmChainsSecrets = [ @@ -88,6 +89,7 @@ class SecretKey { static final solanaSecrets = [ SecretKey('ankrApiKey', () => ''), + SecretKey('nowNodesApiKey', () => ''), SecretKey('chainStackApiKey', () => ''), ]; From 1aad8366c4786ffddbd0afcd5b18cd2a6257eff8 Mon Sep 17 00:00:00 2001 From: Serhii Date: Thu, 6 Mar 2025 19:39:41 +0200 Subject: [PATCH 020/243] Cw 793 implement kryptonim dfx to fiat buy sell option (#2068) * init commit * add authorization data * Update lib/buy/kryptonim/kryptonim.dart --------- Co-authored-by: Omar Hatem --- .github/workflows/pr_test_build_android.yml | 1 + .github/workflows/pr_test_build_linux.yml | 1 + assets/images/kryptonim_dark.png | Bin 0 -> 20086 bytes assets/images/kryptonim_light.png | Bin 0 -> 19654 bytes lib/buy/buy_provider.dart | 2 +- lib/buy/buy_quote.dart | 26 +++ lib/buy/dfx/dfx_buy_provider.dart | 4 +- lib/buy/kryptonim/kryptonim.dart | 222 ++++++++++++++++++++ lib/buy/meld/meld_buy_provider.dart | 2 +- lib/buy/moonpay/moonpay_provider.dart | 4 +- lib/buy/onramper/onramper_buy_provider.dart | 2 +- lib/buy/payment_method.dart | 9 + lib/di.dart | 5 + lib/entities/provider_types.dart | 14 +- lib/view_model/buy/buy_sell_view_model.dart | 2 +- tool/utils/secret_key.dart | 1 + 16 files changed, 285 insertions(+), 10 deletions(-) create mode 100644 assets/images/kryptonim_dark.png create mode 100644 assets/images/kryptonim_light.png create mode 100644 lib/buy/kryptonim/kryptonim.dart diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 6c7e82409..762144ac1 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -172,6 +172,7 @@ jobs: # end of test secrets echo "const chainflipApiKey = '${{ secrets.CHAINFLIP_API_KEY }}';" >> lib/.secrets.g.dart echo "const chainflipAffiliateFee = '${{ secrets.CHAINFLIP_AFFILIATE_FEE }}';" >> lib/.secrets.g.dart + echo "const kryptonimApiKey = '${{ secrets.KRYPTONIM_API_KEY }}';" >> lib/.secrets.g.dart echo "const walletGroupSalt = '${{ secrets.WALLET_GROUP_SALT }}';" >> lib/.secrets.g.dart - name: prepare monero_c and cache diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index e307dc410..c5cb26dd9 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -168,6 +168,7 @@ jobs: # end of test secrets echo "const chainflipApiKey = '${{ secrets.CHAINFLIP_API_KEY }}';" >> lib/.secrets.g.dart echo "const chainflipAffiliateFee = '${{ secrets.CHAINFLIP_AFFILIATE_FEE }}';" >> lib/.secrets.g.dart + echo "const kryptonimApiKey = '${{ secrets.KRYPTONIM_API_KEY }}';" >> lib/.secrets.g.dart echo "const walletGroupSalt = '${{ secrets.WALLET_GROUP_SALT }}';" >> lib/.secrets.g.dart - name: prepare monero_c and cache diff --git a/assets/images/kryptonim_dark.png b/assets/images/kryptonim_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..646d550bac3af58eef0b07247f3882a5418098f6 GIT binary patch literal 20086 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 bDFpr>tmA9K84ZD)00000NkvXXu0mjfHE~1x literal 0 HcmV?d00001 diff --git a/assets/images/kryptonim_light.png b/assets/images/kryptonim_light.png new file mode 100644 index 0000000000000000000000000000000000000000..85e64a3f2296e0c9ba9f36bf8049d6b36c2a42ac GIT binary patch literal 19654 zcmXV&bySqy_x71_gh4Kq(P-?=uV{v>6Y&9`rUlr z_YW3pF*9e*eQNLhxvoRlf2wjg&&Zykp`qa@$V;oEp@GC7f1v2VUj}3Ie*wQhPU>=! zXr)6GTWDx-GzDpi_wJweGBD@NjIVnAXbDaA&3TSvUKB_?6k{@&6z%f-wyi8P=66Mp z+6~-QB72?6LSstF)bA%f7>FlDBh8TWaMHY?TgE-yba2D6F}ESG;Zb^XBJh3kCf#Xq z@~(0CCT*vs%!bIr2{I#PK%)?$`0uNg|7xllO8EB~r-H7NVyYA>%s4s6@U`Ogzb3XV{@-U= zc^LG~nuN=GLp{(pAika-&)l-+OA;G6G~N5DLDB&PoP)|lDoiaC5_N^)jXA%fVK)ytp;f7cr( zw%ksq!|cHWn0?suOF5Nq9c_}khRimHs96QTU3_Y>r&X&R<2^bdzKiBhobMwT`7fyx5j2YYhO`>hA+R0xxY7jSr3?A6F-c5~$ z5Tp2BrR%r=bH8~javWJk-9{$K<{@S0bMApH3h@*-i zZBq)hYoyoU*HOE#rW+9icaiCTDw=nc*@?L(fGS{3S zy8NVf%do_I$JjQ3wqk8}vznuw$}gA20{st!i@*A>ctnTbab1Db9kCMUDSaAeg$;bo zRb%na3dRtb4bto0aNQRa&ytjCEbKlAH9NNY6De>X#A z%JAW9FKNV(EejO>pX3mVF0t@6A+KL&w4i-jOR=)v(RT;+YDiA-Uj_j^R8W-N-ghC- zFtm-hYClQe{MzFo(VJb*i+Q)R1+Rmx)`zbr{m~|s`(pmBBv@GZ*|pxe#`3C2 z7KBMia*}IPM;%$f7K*EeW(wnaC}wZiaKJBYLm+-1$iir$s69wsO5SL^MmR$6l#Pb6 z@A)9EiuI7d#$VtG4n+4j647K`w@TQxnFe{rG>yGl^Cg$swZXCP==Jnp|8#s&kcJ5} z?rpCb$y8ep{YayiU)vTNmvKWVBO1%5on@I>5I4j-sq2sSaHSR><-UUlxu>_Zt|gOe zNI=7lceWU;*8lDJ4Hj+bQr(VD{^HoJ*PZXB&XSI8y=m~CJB{Qe+*AT_oX)5q zSi=HdW00%GTcW@`qNM$Ktff104;Qbl5Li!^!eXtC^*zm{wj7lq9<;i<*$Z)?5etpk zso(fMMx>A7;?`9ppdqOI@ z{@u$~<-4%f$soV}dk<1-RM71k4Da)4^O{qX(oRY30xCrGG~539R*5-Yo60&wq0IRS zcwQ0_!LTYvR`iFTPFyIWYB*aevW<(BXj zddFYWiRzhV{Zd8H^B`{Jjb?ZUG0UwID`y!cMU3RjE5ly=Qbxosj>aSRkhKGt<+B>q zMH@~o$8MSm_{Fb5uB=zn(^MOFzUWiqU85>ri&!u<;?M+6gbHSn&0DSLlW2`$AyeFWqc#NH9FFf3iaS5hir?NwtG!0L%tml`dZ(i` zdU`baIM(%1yd@NC{{@5NQN>b+DK`R})%6bF?FQnI7z|OO=2RyHcl~{yB{G_h=4F)9 z=o9~nrS3;U!v}vf1#5dF{;qSro0q`p6!C5GqY)&OQnYv%^Ti)kL|!#5p&CO~L-mcL zAP5gDr<=RH@Pj94Tr_J3+8qY z7JKWw=7;BSgcA8EZN7ymtvE}|3llb<(c>xR4Kf4kK;T7M@uRc5ej`#gEKq6E`@9Eb z4B`SFDIrjz8Ix}88>n!cw97nM;mx`i>l0A?%r>@yzRjoI{gyGz^2W(Yb7EMX7D_Zj zwP3~)oX$(rDsr3eP7Pud_apa&_&ed_d)K}@+2a8nv$tIc@?KR&??5ys5*+Gj;pKKY z5Sh&eholU8ab%OZqIP9PaN+phE6f_1XI3%1fP#7Hp1G&^g8}p?IqP z+$yu{SE*>6UY*Aa=n`$1BlFRhDimY-Q2D zK*Pr)JXE>RLF(tx4J`{Z(m-3&kP+B2mok6$kd<8O9#C&jK3-(LLkk%Xiw@EYu%?XTZ+|qzJMi2}|ptaI(X)9p4GXIkq z>c=dXul6Z^Mtc~aSHdu#^5MJ*dU#78@=?R%)?*Uwj?x~IDuI~G$xW?yDEB&sx3L2` zSqXBZNh|1RqmXWiEUOhWT3ib9DWN(jPl#+mpby6bmSyFqrID3s4ty>8-^Lvc2$z+= z7G`W9#tOsWVhPmE=#wfULwvrs7!EG`>kZ~la{ky@Y0e;Cdy9;?HTZnRQnmTNKz-jUw2+tL`dxN<0Q)`t`7?OS;mz6pe>+uCVJz?@{ZC&Ohz>n7A5Ic>IcB@U;ZuF*`_xc3~X7P!OfQ>XYlZJZH=HsH6;`Ft*B~ z)NE#?_t8U9>rgi8flnjZOK9gi?j9HKhkrUYlq%B5OxT#%{ZY&h^}^nz4Zph`eIM!D zY+Roo2M@W=-g<9fCYr-3q8DfX-}V7|(0Ule zZjtaoV|31Gn6(eu3DJ2wv|@yOjHiZ;5$6}Rt#>NaH&eY`2QQVAvzXo$Y@SW({0mLa zS)b?sSy}oyf6NscE11!>>=6-&ATXdez}j{FnV*O<452J2LC(C6rG`jz@7OE9ed8zn zq{e}!O~;2D@XtSf+xliPxaYy$tx4u}aJmKQa<%sy~+SzD?I@ zknon)(!$;11Bp=GNJ~#7X|A5@w9Ltnkci~Bl2?XFkdytzbm^g<_Q zs)j|2GF)dAY8QRIGT-Lew}q1)Mx_c31jkQ3=O5Q*5O?#jd^TatF90&@WIrE^#Wv}5 zB|7#x8~@np`t^i2)Z%n=luW=eFBq4aibrgJ1v|Ru!HF$7KgFp@D`Cu**TOflTg0yA z%B&o60DM=&^>)!ss9^sTp*xZxDOm#=Ya}O@R4w^b{6}>tQ0IMWaSn{O)GxrRTobuk z{mN@SRW521=`}KSO;_w{9-fF1cU(GkKC?j1^WPX4SM<^V=)0wlt$=mNqWcq>pH{P- z@q89YcXeinyq4g$lV`-^@R%T+Hbp75Wmlh;vND%c$FsHGIvG=bR~wmSQTz+;=sCtp*jv+lgzC~#{V0^V_vNL8EzfnZ$uxM; zWm0d(`(*IRRZ4L3IpzSD@6+$i+V~5vadfI6eaH(w1uUN3QM;joRb7r)D9lL zw-%}am`2>0R0i>;Vl~UZDSnxch0)a#NfGg^tmJFhh+E4^hQXN(xyjzHZnPToH?kOJ zPNicYpV%^bza7QQ-h3J_8%35+Zd*qa8Kq$nJt|UpS7U1TE7-mY;;vNkY|OLb)2jnF zSs?oK-^ot75=m;AXSg`eyG%Ykt}@H(yhyxzN(YUqk{rUW;t|^64lxJTVB_Q`FBLgz zpX$e=g~L{mFe$%>Ymm49lF;K^Lt%$MzJ?UsbrDlNHuiBu#~ElkS8C24ajG%O^NL3_ zG{}jsTF{aHe%D+K>xz^{i?@loCbbGTz{Rd%7 zZ)>;+d*5Vr1Hnv_V&X39n=0kFK02XqZ5O`M=D!H`3CDg`;@h2F-O>e2fga_7q>#EB_-gO)(Bh*@5xWJYf${p6~mraQKLLfdNVdZ!FGII`M~ zdLwWP8kfv%uoGd0Wl}q9+qi`xb}2D(8~#fG-^mv5T2vi%w1*E*d~s{pL>LMzL4Sh_ z?r(UcI|d%7Q_ogby-^B`meZz5ILjtPPW?WKUS&`mSll$&`1H+kAc5o`<{?(rWl8f< z=K0M_cHGT&u4ME&BO{3vs_wlK4qzIwzBDZQkDQMD7;p*NW6g8x!jrQAqSk&ym5Q99 z=2~s>TdKhUis`<+kaIaAvQ;G^Qyuh8v+HetuRi4n#{Prrv>gLToq5RuJHtj3;o}}@ zM_h2HlM31MYnjB!NZN`WZ!KH#4-221CZsCM8I?aBr2AVkx|`{-PHI#6`=#gh&%%J0 z783?MAB&_=KVqnEe9N?7%ubi!!;hR<8Hjf||I1aG#N;5W=5|Rh%}9GGG6wcA*vN33 zcuK6x@kN8=on*BF*{pUEZSSykNICDZxV7GJkBw&F53=q%1Rr?=72_a7rapI-)OXLxRLijf z%iXDphT}iXH|ZG;;|k_!NDp3aS=@Q<52)97`F&)eXiS`PqfT~|j|`7(Q=^JFeP_jZ z@ED7 z3$?;;2iP`n@ae!XrLG}kn^9Y>4O?DP^!?I?n(y&fnu2p?{j2a#9&3byVh^`Ib}ZYW06?8DeTozn1cCVt4h6s|hh46jN`#GYh8*Mf0Pv%#aUo zuf+kPZ{$sQa(}3)lNpbYnAC}Jk=MU4#^B9ow-1+oWB1`u7b&V9DN$&wqU{?YvEx3v zcA7LBwgdU=;#hMQieX=B;_g3ZzKwZhJh>pY56mpbz|g``6yk?IjPC}nKG3sqtEt` zZ{-g|jQh1J?tebLzS7S5``fbqW(^J|8jaXLZ{knx%i^K1 zOjBJXB(K8fZmLHc??mKcjyrGm-vf>LoPBXhHhM>O&W=dpN!d8@(=sAO^YfZ}Xlz#@ zz4+M#4Y2-Kz;;?GHsqEY=K8n)h@~`1H~h@J%Mal2jzxH3PL9Zr4T)-uSwrM<*R{3- z1J8tWRrQS-EG)vn)Jv?!wLv`RuLS1E@PlAeiz=d-X7sw`Jh{(!@Dn#qXntlYfx#_9 z$FB$%;;3f!|C6y6t?D={HFH!-5u7E_7u)^G&k2I)k& zN(PvDKPInPwd@URv7bTYMFufbf)7Ku>6ReLq#Y7M98&%^%s_E?K4lzSeeS@JTmkBkc4DK6h-=&7VQVK@QD!R$eT5GKP|Tf5A!!Co!b#KJ?h6!WkN#czWmRB=DSe{ z#iF|h<;A6h96sj)j{9WHmS9{dZ-b`u8LJ(X68m^g1lx@H+^aur{;^xJLZQ!hPJR}C zCo}SPs5SJ8MDLrXHrZrj!%0G>ke7ZtE%ArO27q_tnq6Q3HN9NIr4|)_!KLr9mhhw< z;qg{=DAJgLYxIqSjl>S!EVe1@)pOR-Kl-WOW;J|CNtdr&)}T{wemIGa5xFZU#3=pI zK(;V*GP|m)A{>rBc^`Fu$vsK6DkYViQzozCsGQ2SH3K34YO}ZAR!(RdNBuYZrRSm# zQSeL)wToT8fG5bNt~Y%%8=qLR=Um9qcuKk?8*jV)hv}pBV}r!IQ^qmlAPkC;kMHN8 zKjo>#LUFh{I~xDIE()~gk64#C&qMxlrdmkc5vr)j%N!?I@{hNFaXMiLEX)%vG;S)* zALLz?^RBa7zzF>@M}RylN~*P7Mi*Fi*(iVdqt}h83T+jn*={>Eoy$l6P%_RS*mTeq z0ezC+@0AR`kU*CK&ZJo^qeS)o5o>eHo#Lu!phjteZQ(U)O)D0Wn9qJzDhe`l`@>YY z*|>OT3!EKY8yX8!%mmjP`fYmKn6YH3-(pYx*PkkOgrNnE(dwzxiH=Rpc%|Dy76zS2 z|77nTjAB+(smZgO@Z;u)6z=%Ru-IbKpFiA!a{6TC2=&(NiTVtXFTKDE3iV{D=m-J` z&+8|S$r?2yl2}0``rf-1FqHwblyugQ)bx==&po|Z{Z@`L-{)eUzw2_MFlAU2H6W7A$&evI zCMMcu?6#dhF?4hdFDHGymrE#Ws)sv@R|I(fwA z}ao^uxZMa-tFzc8p&!Wv*`a@3U7Z%s@htQlAY2&$cRI89GlZeY+v z13Gbbf2|z;PGGo=rnJbkzNv}bon~1&EZc=F>hD_McFe(r<%fPX7#?+_IjzRYp{tCb zv{H)a`nffhUXg?Eoe7|U>M62_y4EH1l@bUrMrJyYLmh{1LYJd%VymuDJc+7*x2)&W z`r6y48fhnDuj`k+pqM&U++t3)!H_Y_>#FP}HGXk4{^T!_XnwCOl|lg0@&@$0<@u5^ ziPmXw93~_H*~{mY8^!lbn-C`oYp{RY%tUL;xsT#b8yo~2s{N>eO9DpKzmqNJHL<=} z^&*y*M5j((=Yk44q@V>x%kjB}^XSLz`{s?eYpL0+oD#`1+gOSgms#s7m8pI04p3Fw zI^Le4j_%L_$?RwMt;?Z#3VHBKc;mxidW0Q?w2IpMs~4 zgj#Y_Cd_ydp{$U_7_yq+ImrH`yTV z&9t-F(h4iGT+5g!InvAL5oa|*13pjH(>V+So(rtPR8o0%5{+;9sCeZ0{uN_e50)L%iUW)2?P2GW_B^b> zLe>h6x2$CxZ@7J@7-y)p8EiR#Lm*+lVW)IyL|KM^>{9a`v(=VtY|oC)2<43=qIY~Q zFZz{*ICTLg#4`hJY{M&kJ|7dq8ZI=nbXY>y5Rghm9<2LC?|V4EeDs`e`i;+dRVEHQ4y5nH z`)Ukht**^qRH;c+zw%HwAU~h=QO^PVwcwGGrg9|v_A!=GN=V%a69(B zVgK#V7x6Lz_(^S(yO~J7y}%6hR^HU-)VL>m!{H%Sch%6?hMvoZ zN3Nu8mSTE6bd5^UEAFxVRLb_H?ft7XwEyn~Xb!Gx{BO*7Aer-eNB`m1)p{EH3qD05 z^BXH&Ovp<)6>uC<3K`zZo;MeK(E?bnQvTYRpJG||HA``rt>fK`v`4d_vNmQx{wD zNuQ>g`Uw9BPl&Ag{JaW2#<89lwd1IZB;QW zyv{?3S)&M6NtKw#Dh8%1cumV+ezVnwM5Qy*5||8VQU5G?RsPL(E7^Dqz3LyelkLI< zHuRPp>^#6+1f*9wRw9oj4O-F zZJ!)WGK;cG&B(6{8G*Mx!4>8pF+l0)3l)^}zbctdJSlPfsSa(z0Nqd0FfoGf@ZdU2 z-9$PpztH=rn{0Ms+j+!?+K#Qau$k8P!T#ch#JcwW6Z(F4Ey{nqaY5SZ{=#dUWo0~9 z^Bx-+Gq#O;JT_!!)rFZ_+7QLNDi4dkijDQV))=5F3|kEL1e@&Kv^~MyT%mq&fB$nQ z>Qy=@6+O9^y3SG|aYA(OJRngim{?s=!hMUZbxU%E6I7lf_eNMPg-Zq~LeBoalepPA zps4o6qJGDBAfylR#!6@ZUVq9LOFqzn#ZD0kk6>?cXIsJk8%KamJ#B_DP^4b4u{v{EOc2R_~QQ>P0*gkJnwEHqRX2Z#*cSwQCiLtsvCj zR@`DXxG-%12}PGzMx~pahD9C z#&t5H%K!x0u>GqjpnH61c>o?KC(v)kxk8Or-PU|Q!`vq&rBQ`GdpoGtdtFmCSbWLD zT)3D->TdvX;7=ZSLo_R~Yv3m|rIr+1js5D3=(7+3Hx~!ifk9|;qDh$I^k?5KCH-gh zcZr%tVd97iod#QWW777@K{M|h)xJG`m+ikTo*f#-5PfkwJ%4fgFAj2>;U%UTavQQ^ zyf}lAG;{i8<9Wkiof_=4G^{Q;x@JO|Kn;DLvkrCbw{4+Nf>grS8wC2DR%Ok6)|~F{eyJYQi47K9m zm9??&mu465U8ih`@%pV!qd&}bp1eKmJ2X-9b7TxXxLNK3U<6#g?5^qL9W2{ zMS?gyu)~8ZP_cOYMzSEnzZWJ_vPU{S36Zd`+dXPP2z}T~4)iuXgd%#e$a|TngD>5xzy%iTAc=lGW3xAt6ri|ANW&j=S!A7vdvGfleyF3Ptg=!7^^I8DvYgGX%C1Pc5{2FgJgY>ss&rv3A9f%Ur&!CDLD&Z!6t(H^qZaEzdrJaJ%`14D z!mgW3nhfd=C(RBTb=!TjSgmW_7*L)VBVU_K1X1N!KHACEL{*k1HM=sYVZG)D4E7M@@kZ@CWsPS;cGjOicnFtHq`(om+_5?1l>v@{K_*-X*_-2@ZA43K75}I zsMz9}y#W~ILzS(-|IhTW*lN4MSa&h~zmMTmW?Hmx0=+L#ms#jXd$mS^3RTDYkfoWZyf-HhMjzmzDv@XsD{Kl@=R7Q;S1GU`w4hXd>df@36g4;l z+fR|(K02z)zbj?eWuLukiAeG=4?DDj>N312NHTpX|2Jx?9nKO!TJNX#h_I?}kNYtm z+LH8r1*H9Bp$R3NgYh}n5hLSJU3$wTPjD9wx*LgIv^e$SM|rmL{mj&@gBSrR%IADq zj=kwnvX97^$k>;U_Vi;`5#Aeld|M7~3xqA^{x+|jog*-u%*($c$F^?Appm59vgVmS zu7@}#(Hf{R9QwUxMnbRGnus0DSgH12hQUu2zw7Go@P51o1t)nWZZQhOXd!Yd%CS`v zES&%t%mfscV}lnJC1sO$QI}i^xjVHCtZ5`Z<|Al$!&Z5e4DaW z@a-)?$t8sL>vE~wm;CL;vKLe5x9x9RYfEL9VzrxZDu7R*D`cOR%C<313O}wgX20ph z3I01F)W5GU64Z{LQn^kUw$tvC8Jrx*Jw2HF4>gQI+twX_dU^8E-bjzmafZ0WcQ#S9 zH=Nh72k!N(hRdE-wJ_#i`YsUoaas$%qsu;J4v!Wdq$e-GrqHN{=0c>U#bB_LLM>KY zCzH}elvzq^{s65U+<1lHjpwO_p5t$ZY&Q8B%9l1IG)+TAJ*E;Zxx+2AmRG5*Cr&`_ z*ZL5Y=OXyJ0GA53gqw+_UVd1TQ~WJbsw&*l8iz)>&;Qo*+zUE`{DvH~{$`E1netVS z^bR7_7y-CPCxr)_%KZSPWIib-IjNbCjIyi=C%!j8Xah`>A-{sfR#J(YmIAQ0^OHXP z5v&$l6oA;Og}0q4{AUMQj(FGibzFed$K-wqPA<=`;x5ZqffwMs8{?a`2VYD(PikRo zHl>nRZ44RCPt;btIcRM~F8bbN9c3G$l7$wzAN;+qX1d!Y*NQ#U>-#t+=G_)4hn`?2 zRdJ#0`TK<4N`gYV+7+49TW6VlnlH#>7SvsOW|ZE06Cb?;+@R%4~4Z#f<=4>JZw ztA3lvw9*Z?e&Rm#{e)VTh4fwcKA(!?O%?rNDQ>y)#P1Sdzt*9;<$}lx>1qnklL2nE z%iP{ge_0-znd-S}YCv%7@L#%^v0|t6yFK!^w}{QlctT6?0MFuGAGIb|*0-2B#p<~o z;7cc?HSUcbH%-!;xDaZ&=uq^?T8tAl6MT>gzel#+-gnL1xLL?`ok~0byWX2__`=Xa z4%1a|Nb$vN=uC{s$YvHF>s%gg9?9zLYP@XU-()s;tlJ&P%X8r_w-E`9pyLGVdK#u` z)}FlSiJAqjg>7czdT@zBLmWJnUArJ?C{C672@W2eMSwTPTR}`@xI5Fn*iQ|n<#JC2 zblSEurE3SKuKl+(aO$ekW4T^xk<{PDF(L&rL|XK(Un9?uCw0X7-kdPFxrywMCCGYq zHuQ3rlukp)e3gK!Qn2}>u0zm-p4+@AexQ3DN)D`r#_{io{=Fe*KbP37?!aZhc_W}p zhtx}XpV>-itsa_V$t;oc5+`T!C?RfxM8ubuBxO$9uc7E%oSA|*tG9N5wa$mkKttsR z-kez|T-h36X8w>=hI&~a-19qtpZ+>p>9wjE*T9-p<9@bgh-N9vMkg=Cm<|MH(Tn=w zr-PG2VxvTZ@PwCuzIEaCOwQm}-Us^TDHf39;&^;{syj0YXiU;G0WC#LHtxXpqEQ>5 zb6;Gcm_*BvA$2Lo^}N9;>ux}uRi|_s;B4oz{!LVuWFw^Wc|YVueQl^8{WD*NB2kJ@ zth49*VgAUrxt)A2q3T@#ZotHdd3}ZjHqymeZd=!O0l&fS8mCRnnK+i{Q5S*BUd`lr zhW~_+t&{X}MOVeg{E~Yq%mEEddl?0{Ns8P;uL4hIZ2wP;;zD$f)KW^I-(jWaX~`e? zk(~ENtWYv4ZOXtN?c`3LWk=?+rpoVtSIQ(#CA~23hA@02+muqQAEtC zDUUSR;OyYmL;=Vm3xu(}{8Gw$|B-oM!z@f?ED*@~b!>d*Q-JbPM{?bMuln6f?I+yV zuAQQsj@b~Gw>K#|@ugNl+b(MeV-|cefLCKbvSOsLes>O3fmwm)=Go?%*MAG0ueS=V zN@^yyu@WM#_U-r0XAiGaX(J>2*AF{!C#fGUdr600?nn_E|Jn56k7|qKeR_B4ig_3j z|B7a#L>ypog+gC)-;TB!1B3It8<4|1(yvzds`#9~_wtcE$YJ?&XLfN93v-iL+JCLs ze4&<0=%zQbo1xy?YMX@Sv7e+NRiy&Dtsue!*bdL}6AWgpu$ZMDayyn#^W`97TfET; zJAPsk`6zC2C6u^YEX``u#&<9D9eY99sUdb9>e0+l1X|6|BvtXEtd7BVkzeOAhn#qU zCu|tvS(88lI*M9$ggvK0$Wgky4}IbP&0fO)2V9Oj@-L%Q)Kz>lpeE8}h;CI4;Peru2ZovsX1;7uIIDpO^z8ahuWSYpjiOHA_ z+rD;`?bD*cF@74T73kk}i%%oeu_nKE1_1b{58@``pY}QZBv-%uWj)SIuKlqg=TW%c zeZ1E7)l{4#brj9xGO?Y7Nd(tr2n}dHHSybmYUR_Q|Llc0bDr>oE0ZLIkj83}zDO z^?sp@Tm`=9m?nI1e#F-)riqwc7CKA!#YhkKlsM`;$)-N3TP|LiDWXF1StlrLZ@8wd z>7&uey}Y*~_^$+DypX3e{_;SEwgR&C|2X zeDLANZTIZ$H=jw~z1%E%@ zVJFzj=Z$iUkZG_);)JJu~CrJe1?^0WejLwno+e-TE1 zIYV5HAng`3$!vY=qB>H%H6;^cJO2Z?i_tzHFQ$eiQEY_g(S?((Xlz18yS4MEQpWu^ zu0AUz>?(`#HdW1V1+Q`*G)m>rg(rBkTlEA~^^~tu9{K%lO9JMet1)8JDcj<=wV}fx z{+*#7p3mJZBl(ER%MDDnDwKYwI0Jf&=f0+sQIO&Q1@i6ObCHExn8Cmrx-*U(BYvQ7V}IzTAg91VG(`{gmqMXrdq@w3t`0@H_bi6o35WX zd0$mg7cCdfq8JC~UKflzFn3A(eD8;;*Ia-HWu;C@xyXXwqm^K3Nu0D@D=?HnRYizu zR)9wJH$9sv^JkZk!IXyIYZg~X=Vg}(RAuL(7gz6`eJ2j<#TG7hxVTa&`_}hL82qOP z(|6Io#Ekkt=c<-1H4Ja)zw*#H)paI6z*FhO$R`R2*AvuWN_K*_DVeNQr-#d(Qe>>; zY17&^%hgqGd@nrLw>B8wp5F5B4FFC`u7#BNR{|TXa*xe@`K>{mMDUE%5Y$5*!^){m zvJo*rw z5q#He~6`jDG8@2;!S5md@GDl_Ji~l4+we#?whSg;S<1* zBv3L(X(?q!|GsF+$qVuQia{#|A$gxg4Zw1at$3d~KSMT8y-f_P7i;$5jE8B)Jn)uH z-EwdPkj?X$^u>xtE)DRJR#viB;JxR=yq-iG(`H=xZ)+crk#Q>j_F|h;w#t2P%_2Gx zdfl&k0vAWAKFFg^=T;ZE9%X(UNjlMphnoL*JShQv=LehlTT*XFzwfj9Uqva8-r9Na zr68w_u^op#&+_qmxVx~L)N>nO{r&Z3JH$jPYbVo}>9?G{`9#XP45DwDXt}s3PWS+L*gj_g z70-^EZrx8TRy?wCK+a%mWFNrhUy)rO( zC-e6)xiOY|5%)61_)zcKmBAX)1Qbvh!sKiQAe=XXS|)Hf>;k{)!t7J&#Od>??Mq3QHHH z3bKT2X06(CEP&_|KqldvpS;Cmu16Fj^OiM-H^RxKK* zAq-Y;HXql?(r;T%(&ShIyOFrG6k@(j=ksp*&4(RWKInwxv)d8XLcTJYje4lsR94r& zyA;MSxG|l@JJYRH!s?`bCvarrj7HgH{@#s;-`jA1{z=Ktk%RZJ8+87!qz}2sV!u>3L8AcM|=9CF6rH_+y2Fkfy#!qq`U-oLjIUC+6 z&!!m6*s<>jz-RLE56zWN-c`TX^Tt@N-0M+qzQ5jyZ~|_oU_`-(Wn`>2ng=)%=t76fm~fxXhbXdb8#f&6MLTh<5qG|#vH`y4QKveG8xyo5+g>19I(Xa z!Qz_rm{srnR`SHBle)KyjGqtm?}bksxwN*9C~*JpFx=q+&?&dd2^TDKqa-g9OaBZ( zj3ZO_$Eu}$Lx`>;^lv2}{mlic-MH@88Y!$?dDurOrc>3kQ^$mV#e!optjLy5733@` zFkx{&$1pL6@Fea_cTs@HvH#h<#rww!GQielwlJt~He0+*)>UO!dFko9275`Zjz z-CZn-S!BCXS+oRdkarIe2}KyNv)bTUMaT$7GidMUL7K5kS)xrI+%&s-C#AR?8LDOetSZ90 z1@GSGq@-ca5Kd?^7uG*owf-=dVWVTVR0#(Vy-qSWt@r=0yyDXL(Thp=Q5O&SJCTD% zkDwcgx-DG@DL;8N3~x{Sz0YxBU)#PujR}_iUJ);wWDQcJizYDGNCM-)zC6k6s3#MX z5`Kgvp)Y7m6UHf!p0w$s`Bu++Q-ic}X!Lt}UO57GRCGF=L6o;v52;t?C0ogKPkh;+ zuUee5ecCYcPO}gidstUxH9p60t#i!gJAzq$64CF9kCOVr#R~hk`~bKF4Jqy0Kt`d{y&{N9`%bscJ{^PAJ`B5<761c2Du=>()Nfr$a+v4&6 z0xWyOB$1Hk-kGjRmR?KV)LpF488+v=8*N%@RSDqnYzeV&9V5G+PGqyT(;Fk%QoIiT zx?pLFRm$)R@hVn0=#ox068ba6GaIhfj0?Zie#XLFK7=HQmyn|1{roLjaQjC&A+HL@ zMCyp0;QQ!@+E7C1b1{kZyfQ#fwWqR>jlqBU&a@xvN+mk%LvBmIIHE7qZEGuDbWDEf z)8zth+i`IvgDk>%V6ZE%s`}cS%40RIox@05V}LYSC}RB8GIhRlh074A^=i;A*pKBZ z8mpEPI5NM;E0dC+-bt$%&X;K@UQ9gy!=V?pS&jDd$Ssa%=HjB6P_TC~*a9yDp9 z{Nq3M*Gyt(0M{B*{uDA~ytedwy+CFCc<;k`_P$be-LCflPC7IV0Co&LK=1ak{FRKe z5uz&t%hJjbpz{U*49_DFcN*kve}$5e+stI(?aX8u+fIx_Yd0TE?NPNDP&5S>z0yiY zi;!$+c0lbjX!^50PpT3{Hd}lG5GRf-9`gSJ9SY+0zZMmIeT^jEa$qUr7~-;#oO98*Ukp9 zx>9Hzqlj6L%{jS<+1Xj6C2P3z&O5cwKKp3Wq)9~#pC02+NsRCHMGxG}cx5I)O3I>4+naXsVx#H<8)-u;e^HV^{GWuL1!w zdraqrtXe#%oA242Z!OJ|Il2xTHcSsb_@L&SZ@%U`YvWGy8RJpTucl3#re&91HfS$h zkaV*RL|S_!9ZL-1ZL!4`I`hmkLn&)QIXr zVWG5TsasVD5D-az1__QTHKI8RXNfI&f%%u3Eth2{&T*IlyE5M~v_8g0LnvWPX%SGg zGzNrXg_R&XTQ@6S-co06+-&&>)3M@4f_$1ov{|Dqw2tox>%)MOp`D+Q-}F5FC2v(C zK%k@;1JNQolpNPg;-%BEo6i}4L`7xu#e#|E#~+ZTQu3{DdrNA`5iAW=k;x?bOO|2d zLw>wZX!o?7{$aZ$iSl@^J+z8{s%iuXLQB+Qn2$+nixiJo0VCQr)3TEAa*}PBf4OP6 zHpFb{ZLUQyFkU*_2aRwtR-?m#_5o~HgUQ-`FreWYO8i+uPOod1PR(7 z3}n<_I;M6OyxC_gEj*-a+KZ)D@3aQf>>N6EpZN!}eijL0VQ`R5j>d+zr)bt+(*oID zATObOE&c;rS@HwCDiffQAlYLmwb?bz&SM~)bVg$SaO_Ml=42Tct${yYJNvUw*8-bI z-tq;e3l!OwxcOe%zjL7T#`GFO)(~EsmT9)VxTsZ5^D|VdPJqmVGeJnOs|_RV{~7p` zjzAOyFnB3ARtG{tv{b@=dJa*-(>Vjs~F=tBTkmlJAr*(+o19cb;kK?Ah8 z;U@?Q_KdT%|3Nwe9Y6plXUMM-etwPc=_UeXuK0-@D(R43iXCpW$o{<}kOKmQiJV-k z4|P%%B0QR%Z?_Sk&6@-Y5MfVck!g0({XR#aMFhy|0`ZZkxT{*X5+I92DlJm+^jto{ zu6jd#GMz?%e14>)D{V@vEC6*g0fGxjRY&(~X6Y<1hCk~FNFtLnnb}L3%ruV`>UIJI zBhu?rh^1$1jO6j6cpp8705v=)P*%D$_Ia*EFAyMjQD_Mg?CENAdA>N_S63rI^)|?_ zR0WJ;Al@qk2zHd*q#!eHG9QfrruXRxR2%_P!{FvJu1uvXV`AD%1ZX&79DxKs6BAhP z+YzW30#795*OkVOf!3lI2@ovSleCYduU70Ob#Wbm7zo^4a~V5B(ycK%FLR&0N`R~h zDYYQMJ<7a5E}J6|8G&;W@~dn@gDm*;G690ytdjOjt|hYo>CZX>6+>W(q_dOj-HOSc zo4CDBfCeO6VKl)Fb3@iUasqG# zm2{q@XS#W!&CbmUaAu@Pu1e*()tUv-?{fqyfWUo{NMl73Q*Q|ZPJsQt3ZIpQ_YNI__y|l)$nOX7hpOfXPJoTq z4Q+7eB#dr)T}p3`K)Dg1ZYtqy7rE9q_(C}WHvb}TBMFUgo#r3$I~{?-5x89v8s9aA zk6gh4od8=~w8d-Iv+a`$tFbpnpezVr)x9*irj|vhvdiHF*xm}Qm5|`K+k4!fas+Zk z;IdkEM6O@v+G`JV<^-5+k#U*X&bP(cK=h~EMu6?^_G%xs%165HnHs9--GjyHf1NkXwd3QlD14PFV7hd z=Wgzqa+KF4*PY!w(N4|H2{1nLUzW6mq|GJGAAdL>p$iarMbdalKazAu7tE)$Q*Z(- zZfS6x*)+L^7B{SqR3QYOmvlpN-BlsED=VrKVDU@$c}d@p^mVUGQat!29g%t*Kaup~ zT29K}lvKv96w3*)bIbZENt;O8Skf0eCx(yQK?LrWbYpV;v4e8-?9fhtonQL7lcMbu zZYLx4$2u>POArx(HzbjfnZn~-uSXoqyKaDh6JWVsku{P^b?Zx7p&sJURT9S47o`mdvUDM9Fyj2VVC%}p= z*Z|_Lm{hT&k{y>PV~De^BqN}@nq9w(GmQ6I6ar3wMIl|>I2ce?k+f3H-2$g;E?jNA zwT=KTDmW%{Jt*k`H)OO9Vws?xrS6~76S`AEz#uA=KfI2)%av` zP3e;P`83)@zzMKT;CkS(kJM7Gf3j9(jOuq>7*>YW>dk&!64`9fBtK9R9Nw$dZbow> zo64VSSB}#0Vlu`Ek?yeSxC~NlKA8zT7_#qN%JHZam`$F zCT=<(mc%t%;?Qjb3W4E3{2xhgB^Pcc|ByuNEZ1wv^*2dGSaAI{xn4*> getAvailablePaymentTypes( - String fiatCurrency, String cryptoCurrency, bool isBuyAction) async => + String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async => []; Future?> fetchQuote( diff --git a/lib/buy/buy_quote.dart b/lib/buy/buy_quote.dart index 72ab7bd7d..8da70361d 100644 --- a/lib/buy/buy_quote.dart +++ b/lib/buy/buy_quote.dart @@ -289,6 +289,29 @@ class Quote extends SelectableOption { ); } + factory Quote.fromKryptonimJson( + Map json, bool isBuyAction, PaymentType paymentType) { + final fees = json['fees'] as Map; + final rate = _toDouble(json['rate']) ?? 0.0; + final limits = json['limits'] as Map; + final minLimit = _toDouble(limits['min_amount']) ?? 0.0; + final maxLimit = _toDouble(limits['max_amount']) ?? double.infinity; + final convertedAmount = _toDouble(json['converted_amount']) ?? 0.0; + final amount = _toDouble(json['amount']) ?? 0.0; + final calculatedRate = amount / convertedAmount; + return Quote( + rate: calculatedRate, + feeAmount: _toDouble(fees['totalFee']) ?? 0.0, + networkFee: _toDouble(fees['network_fee']) ?? 0.0, + transactionFee: _toDouble(fees['operation_fee']) ?? 0.0, + payout: _toDouble(json['amount']) ?? 0.0, + paymentType: paymentType, + recommendations: [], + provider: ProvidersHelper.getProviderByType(ProviderType.kriptonim)!, + isBuyAction: isBuyAction, + limits: Limits(min: minLimit, max: maxLimit)); + } + static double? _toDouble(dynamic value) { if (value is int) { return value.toDouble(); @@ -299,4 +322,7 @@ class Quote extends SelectableOption { } return null; } + + @override + String toString() => 'Quote: rate: $rate, feeAmount: $feeAmount, networkFee: $networkFee, transactionFee: $transactionFee, payout: $payout, paymentType: $paymentType, provider: $provider, quoteId: $quoteId, recommendations: $recommendations, isBuyAction: $isBuyAction, rampId: $rampId, rampName: $rampName, rampIconPath: $rampIconPath, [limits: min: ${limits?.min}, max: ${limits?.max}]'; } diff --git a/lib/buy/dfx/dfx_buy_provider.dart b/lib/buy/dfx/dfx_buy_provider.dart index eba48632b..ac50665f0 100644 --- a/lib/buy/dfx/dfx_buy_provider.dart +++ b/lib/buy/dfx/dfx_buy_provider.dart @@ -168,7 +168,7 @@ class DFXBuyProvider extends BuyProvider { } Future> getAvailablePaymentTypes( - String fiatCurrency, String cryptoCurrency, bool isBuyAction) async { + String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async { final List paymentMethods = []; if (isBuyAction) { @@ -190,7 +190,7 @@ class DFXBuyProvider extends BuyProvider { }); } } else { - final assetCredentials = await fetchAssetCredential(cryptoCurrency); + final assetCredentials = await fetchAssetCredential(cryptoCurrency.title); if (assetCredentials.isNotEmpty) { if (assetCredentials['sellable'] == true) { final availablePaymentTypes = [ diff --git a/lib/buy/kryptonim/kryptonim.dart b/lib/buy/kryptonim/kryptonim.dart new file mode 100644 index 000000000..c439fa2f7 --- /dev/null +++ b/lib/buy/kryptonim/kryptonim.dart @@ -0,0 +1,222 @@ +import 'dart:convert'; + +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/buy/buy_provider.dart'; +import 'package:cake_wallet/buy/buy_quote.dart'; +import 'package:cake_wallet/buy/payment_method.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:flutter/material.dart'; +import 'dart:developer'; +import 'package:http/http.dart' as http; +import 'package:url_launcher/url_launcher.dart'; + +class KryptonimBuyProvider extends BuyProvider { + KryptonimBuyProvider({required WalletBase wallet, bool isTestEnvironment = false}) + : super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: null); + + static const _isProduction = true; + + static const _baseUrl = _isProduction ? 'app.kryptonim.com' : 'intg-api.kryptonim.com'; + static const _baseWidgetUrl = _isProduction ? 'buy.kryptonim.com' : 'intg.kryptonim.com'; + static const _quotePath = '/v2/ramp/buy/quotes'; + static const _merchantId = 'a70fe053'; + + static String get _kryptonimApiKey => secrets.kryptonimApiKey; + + @override + String get title => 'Kryptonim'; + + @override + String get providerDescription => 'Kryptonim Buy Provider'; + + @override + String get lightIcon => 'assets/images/kryptonim_light.png'; + + @override + String get darkIcon => 'assets/images/kryptonim_dark.png'; + + @override + bool get isAggregator => false; + + Future> getExchangeRates( + {required CryptoCurrency cryptoCurrency, + required String fiatCurrency, + required double amount}) async { + final url = Uri.https(_baseUrl, _quotePath, {'m': _merchantId}); + + final headers = { + 'accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': _kryptonimApiKey, + }; + + final body = jsonEncode({ + 'amount': amount, + 'currency': fiatCurrency, + 'converted_currency': cryptoCurrency.title, + 'blockchain': _normalizeBlockChain(cryptoCurrency), + 'quote_currency': fiatCurrency, + }); + + try { + final response = await http.post(url, headers: headers, body: body); + + if (response.statusCode == 200 || response.statusCode == 201 || response.statusCode == 401) { + return jsonDecode(response.body) as Map; + } else { + return {}; + } + } catch (e) { + return {}; + } + } + + @override + Future> getAvailablePaymentTypes( + String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async { + + final data = await getExchangeRates( + cryptoCurrency: cryptoCurrency, + fiatCurrency: fiatCurrency, + amount: 100.0, + ); + + if (data.isEmpty || !data.containsKey('data')) return []; + + final paymentMethods = (data['data'] as List) + .map((e) => PaymentMethod.fromKryptonimJson(e as Map)) + .toList(); + + return paymentMethods; + } + + @override + Future?> fetchQuote({ + required CryptoCurrency cryptoCurrency, + required FiatCurrency fiatCurrency, + required double amount, + required bool isBuyAction, + required String walletAddress, + PaymentType? paymentType, + String? countryCode, + }) async { + log('Kryptonim: Fetching quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount'); + + final data = await getExchangeRates( + cryptoCurrency: cryptoCurrency, + fiatCurrency: fiatCurrency.toString(), + amount: amount, + ); + + if (!data.containsKey('data') || (data['data'] as List).isEmpty) { + return null; + } + + final quotesList = data['data'] as List; + + Map? selectedPaymentMethod; + + if (paymentType == PaymentType.all || paymentType == null) { + selectedPaymentMethod = quotesList.first as Map; + } else { + for (var quote in quotesList) { + final quotePaymentType = PaymentMethod.getPaymentTypeId(quote['payment_method'] as String?); + if (quotePaymentType == paymentType) { + selectedPaymentMethod = quote as Map; + break; + } + } + } + + if (selectedPaymentMethod == null) { + return null; + } + + final selectedPaymentType = + PaymentMethod.getPaymentTypeId(selectedPaymentMethod['payment_method'] as String?); + final quote = Quote.fromKryptonimJson(selectedPaymentMethod, isBuyAction, selectedPaymentType); + + quote.setFiatCurrency = fiatCurrency; + quote.setCryptoCurrency = cryptoCurrency; + + return [quote]; + } + + @override + Future? launchProvider( + {required BuildContext context, + required Quote quote, + required double amount, + required bool isBuyAction, + required String cryptoCurrencyAddress, + String? countryCode}) async { + final params = { + 'amount': amount.toInt().toString(), + 'currency': quote.fiatCurrency.name, + 'convertedCurrency': quote.cryptoCurrency.title, + 'blockchain': _normalizeBlockChain(quote.cryptoCurrency), + 'address': cryptoCurrencyAddress, + 'paymentMethod': normalizePaymentMethod(quote.paymentType), + }; + + final uri = Uri.https(_baseWidgetUrl, '/redirect-form', params); + + try { + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } else { + throw Exception('Could not launch URL'); + } + } catch (e) { + await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: "Kryptonim", + alertContent: "Payment provider is unavailable: $e", + buttonText: "OK", + buttonAction: () => Navigator.of(context).pop(), + ); + }, + ); + } + } + + String normalizePaymentMethod(PaymentType paymentType) { + switch (paymentType) { + case PaymentType.bankTransfer: + return 'bank'; + case PaymentType.creditCard: + case PaymentType.debitCard: + return 'card'; + default: + return paymentType.name.toLowerCase(); + } + } + + String _normalizeBlockChain(CryptoCurrency cur) { + String? blockchain = switch (cur.tag) { + 'ETH' => 'Ethereum', + 'POL' => 'Polygon', + 'AVAXC' => 'Avalanche', + 'SOL' => 'Solana', + _ => null, + }; + + if (blockchain == null) { + blockchain = switch (cur) { + CryptoCurrency.btc => 'Bitcoin', + CryptoCurrency.ltc => 'Litecoin', + CryptoCurrency.eth => 'Ethereum', + CryptoCurrency.maticpoly => 'Matic', + _ => null, + }; + } + + return blockchain ?? cur.fullName ?? ''; + } +} diff --git a/lib/buy/meld/meld_buy_provider.dart b/lib/buy/meld/meld_buy_provider.dart index a9759aab8..81ad35a40 100644 --- a/lib/buy/meld/meld_buy_provider.dart +++ b/lib/buy/meld/meld_buy_provider.dart @@ -53,7 +53,7 @@ class MeldBuyProvider extends BuyProvider { @override Future> getAvailablePaymentTypes( - String fiatCurrency, String cryptoCurrency, bool isBuyAction) async { + String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async { final params = {'fiatCurrencies': fiatCurrency, 'statuses': 'LIVE,RECENTLY_ADDED,BUILDING'}; final path = '$_providersProperties$_paymentMethodsPath'; diff --git a/lib/buy/moonpay/moonpay_provider.dart b/lib/buy/moonpay/moonpay_provider.dart index a6966db33..7bca7d405 100644 --- a/lib/buy/moonpay/moonpay_provider.dart +++ b/lib/buy/moonpay/moonpay_provider.dart @@ -126,11 +126,11 @@ class MoonPayProvider extends BuyProvider { } Future> getAvailablePaymentTypes( - String fiatCurrency, String cryptoCurrency, bool isBuyAction) async { + String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async { final List paymentMethods = []; if (isBuyAction) { - final fiatBuyCredentials = await fetchFiatCredentials(fiatCurrency, cryptoCurrency, null); + final fiatBuyCredentials = await fetchFiatCredentials(fiatCurrency, cryptoCurrency.title, null); if (fiatBuyCredentials.isNotEmpty) { final paymentMethod = fiatBuyCredentials['paymentMethod'] as String?; paymentMethods.add(PaymentMethod.fromMoonPayJson( diff --git a/lib/buy/onramper/onramper_buy_provider.dart b/lib/buy/onramper/onramper_buy_provider.dart index f229cb833..5480ab2cd 100644 --- a/lib/buy/onramper/onramper_buy_provider.dart +++ b/lib/buy/onramper/onramper_buy_provider.dart @@ -48,7 +48,7 @@ class OnRamperBuyProvider extends BuyProvider { bool get isAggregator => true; Future> getAvailablePaymentTypes( - String fiatCurrency, String cryptoCurrency, bool isBuyAction) async { + String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async { final params = { 'fiatCurrency': fiatCurrency, 'type': isBuyAction ? 'buy' : 'sell', diff --git a/lib/buy/payment_method.dart b/lib/buy/payment_method.dart index cf85c441b..14b119aa0 100644 --- a/lib/buy/payment_method.dart +++ b/lib/buy/payment_method.dart @@ -218,6 +218,15 @@ class PaymentMethod extends SelectableOption { customDescription: json['description'] as String?); } + factory PaymentMethod.fromKryptonimJson(Map json) { + final type = PaymentMethod.getPaymentTypeId(json['payment_method'] as String?); + return PaymentMethod( + paymentMethodType: type, + customTitle: json['payment_method'] as String? ?? 'Unknown', + customIconPath: 'assets/images/card.png', + ); + } + static PaymentType getPaymentTypeId(String? type) { switch (type?.toLowerCase()) { case 'banktransfer': diff --git a/lib/di.dart b/lib/di.dart index 174183f9a..83b3efaea 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -255,6 +255,7 @@ import 'package:get_it/get_it.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'buy/kryptonim/kryptonim.dart'; import 'buy/meld/meld_buy_provider.dart'; import 'src/screens/buy/buy_sell_page.dart'; import 'cake_pay/cake_pay_payment_credantials.dart'; @@ -1019,6 +1020,10 @@ Future setup({ wallet: getIt.get().wallet!, )); + getIt.registerFactory(() => KryptonimBuyProvider( + wallet: getIt.get().wallet!, + )); + getIt.registerFactoryParam((title, uri) => WebViewPage(title, uri)); getIt.registerFactory(() => ExchangeViewModel( diff --git a/lib/entities/provider_types.dart b/lib/entities/provider_types.dart index c65ac267b..5888970b0 100644 --- a/lib/entities/provider_types.dart +++ b/lib/entities/provider_types.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/buy/buy_provider.dart'; import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart'; +import 'package:cake_wallet/buy/kryptonim/kryptonim.dart'; import 'package:cake_wallet/buy/meld/meld_buy_provider.dart'; import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; @@ -8,7 +9,7 @@ import 'package:cake_wallet/di.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:http/http.dart'; -enum ProviderType { robinhood, dfx, onramper, moonpay, meld } +enum ProviderType { robinhood, dfx, onramper, moonpay, meld, kriptonim } extension ProviderTypeName on ProviderType { String get title { @@ -23,6 +24,8 @@ extension ProviderTypeName on ProviderType { return 'MoonPay'; case ProviderType.meld: return 'Meld'; + case ProviderType.kriptonim: + return 'Kriptonim'; } } @@ -38,6 +41,8 @@ extension ProviderTypeName on ProviderType { return 'moonpay_provider'; case ProviderType.meld: return 'meld_provider'; + case ProviderType.kriptonim: + return 'kriptonim_provider'; } } } @@ -59,6 +64,7 @@ class ProvidersHelper { ProviderType.dfx, ProviderType.robinhood, ProviderType.moonpay, + ProviderType.kriptonim ]; case WalletType.litecoin: case WalletType.bitcoinCash: @@ -66,13 +72,15 @@ class ProvidersHelper { return [ ProviderType.onramper, ProviderType.robinhood, - ProviderType.moonpay + ProviderType.moonpay, + ProviderType.kriptonim ]; case WalletType.tron: return [ ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay, + ProviderType.kriptonim ]; case WalletType.none: case WalletType.haven: @@ -127,6 +135,8 @@ class ProvidersHelper { return getIt.get(); case ProviderType.meld: return getIt.get(); + case ProviderType.kriptonim: + return getIt.get(); default: return null; } diff --git a/lib/view_model/buy/buy_sell_view_model.dart b/lib/view_model/buy/buy_sell_view_model.dart index 508d68a82..02b2b3576 100644 --- a/lib/view_model/buy/buy_sell_view_model.dart +++ b/lib/view_model/buy/buy_sell_view_model.dart @@ -351,7 +351,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S paymentMethodState = PaymentMethodLoading(); selectedPaymentMethod = null; final result = await Future.wait(providerList.map((element) => element - .getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency.title, isBuyAction) + .getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency, isBuyAction) .timeout( Duration(seconds: 10), onTimeout: () => [], diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index 0ef38e939..a8ebcc8cc 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -77,6 +77,7 @@ class SecretKey { SecretKey('moneroTestWalletBlockHeight', () => ''), SecretKey('chainflipApiKey', () => ''), SecretKey('chainflipAffiliateFee', () => ''), + SecretKey('kryptonimApiKey', () => ''), SecretKey('walletGroupSalt', () => hex.encode(encrypt.Key.fromSecureRandom(16).bytes)), ]; From c2996ac303ee34de675c70e4612081760186ffa1 Mon Sep 17 00:00:00 2001 From: Serhii Date: Thu, 6 Mar 2025 20:00:10 +0200 Subject: [PATCH 021/243] CW-983-SwapTrade-enhancements (#2057) * Update swaptrade_exchange_provider.dart * fix network and trade state issues * Update trade_filter_store.dart * hopefully final changes :3 * re-enable swaptrade [skip ci] * fix rate calculation issue * Add refund address --------- Co-authored-by: OmarHatem --- lib/entities/default_settings_migration.dart | 7 +++ .../provider/swaptrade_exchange_provider.dart | 44 ++++++++++++------- lib/exchange/trade_state.dart | 2 + lib/main.dart | 2 +- lib/store/dashboard/trade_filter_store.dart | 16 +++++-- .../dashboard/dashboard_view_model.dart | 5 +++ 6 files changed, 55 insertions(+), 21 deletions(-) diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index a02b40d44..e57d71174 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -401,6 +401,13 @@ Future defaultSettingsMigration( enabled: false, ); break; + case 48: + _changeExchangeProviderAvailability( + sharedPreferences, + providerName: "SwapTrade", + enabled: true, + ); + break; default: break; } diff --git a/lib/exchange/provider/swaptrade_exchange_provider.dart b/lib/exchange/provider/swaptrade_exchange_provider.dart index 9553c9559..d3f64b712 100644 --- a/lib/exchange/provider/swaptrade_exchange_provider.dart +++ b/lib/exchange/provider/swaptrade_exchange_provider.dart @@ -26,9 +26,11 @@ class SwapTradeExchangeProvider extends ExchangeProvider { CryptoCurrency.ltc, CryptoCurrency.ada, CryptoCurrency.bch, - CryptoCurrency.usdt, + CryptoCurrency.usdterc20, + CryptoCurrency.usdttrc20, CryptoCurrency.bnb, CryptoCurrency.xmr, + CryptoCurrency.zec, ].contains(element)) .toList()) ]; @@ -39,6 +41,7 @@ class SwapTradeExchangeProvider extends ExchangeProvider { static const getRate = '/api/swap/get-rate'; static const getCoins = '/api/swap/get-coins'; static const createOrder = '/api/swap/create-order'; + static const order = '/api/swap/order'; @override String get title => 'SwapTrade'; @@ -108,6 +111,7 @@ class SwapTradeExchangeProvider extends ExchangeProvider { final body = { 'coin_send': _normalizeCurrency(from), 'coin_receive': _normalizeCurrency(to), + 'amount': amount.toString(), 'ref': 'cake', }; @@ -120,7 +124,7 @@ class SwapTradeExchangeProvider extends ExchangeProvider { final data = responseBody['data'] as Map; double rate = double.parse(data['price'].toString()); - return rate; + return rate > 0 ? isFixedRateMode ? amount / rate : rate / amount : 0.0; } catch (e) { printV("error fetching rate: ${e.toString()}"); return 0.0; @@ -138,18 +142,16 @@ class SwapTradeExchangeProvider extends ExchangeProvider { final params = {}; var body = { 'coin_send': _normalizeCurrency(request.fromCurrency), + 'coin_send_network': _networkFor(request.fromCurrency), 'coin_receive': _normalizeCurrency(request.toCurrency), + 'coin_receive_network': _networkFor(request.toCurrency), 'amount_send': request.fromAmount, 'recipient': request.toAddress, 'ref': 'cake', 'markup': markup, + 'refund_address': request.refundAddress, }; - String? fromNetwork = _networkFor(request.fromCurrency); - String? toNetwork = _networkFor(request.toCurrency); - if (fromNetwork != null) body['coin_send_network'] = fromNetwork; - if (toNetwork != null) body['coin_receive_network'] = toNetwork; - final uri = Uri.https(apiAuthority, createOrder, params); final response = await post(uri, body: body, headers: headers); final responseBody = json.decode(response.body) as Map; @@ -193,7 +195,7 @@ class SwapTradeExchangeProvider extends ExchangeProvider { 'order_id': id, }; - final uri = Uri.https(apiAuthority, createOrder, params); + final uri = Uri.https(apiAuthority, order, params); final response = await post(uri, body: body, headers: headers); final responseBody = json.decode(response.body) as Map; @@ -211,10 +213,14 @@ class SwapTradeExchangeProvider extends ExchangeProvider { final toCurrency = responseData['coin_receive'] as String; final to = CryptoCurrency.fromString(toCurrency); final inputAddress = responseData['server_address'] as String; + final payoutAddress = responseData['recipient'] as String; final status = responseData['status'] as String; final state = TradeState.deserialize(raw: status); final response_id = responseData['order_id'] as String; final expectedSendAmount = responseData['amount_send'] as String; + final expectedReceiveAmount = responseData['amount_receive'] as String; + final memo = responseData['memo'] as String?; + final createdAt = responseData['created_at'] as String?; return Trade( id: response_id, @@ -223,7 +229,11 @@ class SwapTradeExchangeProvider extends ExchangeProvider { provider: description, inputAddress: inputAddress, amount: expectedSendAmount, + payoutAddress: payoutAddress, state: state, + receiveAmount: expectedReceiveAmount, + memo: memo, + createdAt: DateTime.tryParse(createdAt ?? ''), ); } catch (e) { printV("error getting trade: ${e.toString()}"); @@ -242,14 +252,14 @@ class SwapTradeExchangeProvider extends ExchangeProvider { } } - String? _networkFor(CryptoCurrency currency) { - switch (currency) { - case CryptoCurrency.usdt: - return "USDT_ERC20"; - case CryptoCurrency.bnb: - return "BNB_BSC"; - default: - return null; - } + String _networkFor(CryptoCurrency currency) { + final network = switch (currency) { + CryptoCurrency.eth => 'ETH', + CryptoCurrency.bnb => 'BNB_BSC', + CryptoCurrency.usdterc20 => 'USDT_ERC20', + CryptoCurrency.usdttrc20 => 'TRX_USDT_S2UZ', + _ => '', + }; + return network; } } diff --git a/lib/exchange/trade_state.dart b/lib/exchange/trade_state.dart index 6d2472a11..7bce8c7e7 100644 --- a/lib/exchange/trade_state.dart +++ b/lib/exchange/trade_state.dart @@ -131,6 +131,8 @@ class TradeState extends EnumerableItem with Serializable { case 'success': case 'done': return success; + case 'expired': + return expired; default: throw Exception('Unexpected token: $raw in TradeState deserialize'); } diff --git a/lib/main.dart b/lib/main.dart index 0e02ef97b..1eedfa6a9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -215,7 +215,7 @@ Future initializeAppConfigs() async { secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, havenSeedStore: havenSeedStore, - initialMigrationVersion: 47, + initialMigrationVersion: 48, ); } diff --git a/lib/store/dashboard/trade_filter_store.dart b/lib/store/dashboard/trade_filter_store.dart index a2c6e3646..6a98329d6 100644 --- a/lib/store/dashboard/trade_filter_store.dart +++ b/lib/store/dashboard/trade_filter_store.dart @@ -19,7 +19,8 @@ abstract class TradeFilterStoreBase with Store { displayChainflip = true, displayThorChain = true, displayLetsExchange = true, - displayStealthEx = true; + displayStealthEx = true, + displaySwapTrade = true; @observable bool displayXMRTO; @@ -54,6 +55,9 @@ abstract class TradeFilterStoreBase with Store { @observable bool displayStealthEx; + @observable + bool displaySwapTrade; + @computed bool get displayAllTrades => displayChangeNow && @@ -64,7 +68,8 @@ abstract class TradeFilterStoreBase with Store { displayChainflip && displayThorChain && displayLetsExchange && - displayStealthEx; + displayStealthEx && + displaySwapTrade; @action void toggleDisplayExchange(ExchangeProviderDescription provider) { @@ -102,6 +107,8 @@ abstract class TradeFilterStoreBase with Store { case ExchangeProviderDescription.stealthEx: displayStealthEx = !displayStealthEx; break; + case ExchangeProviderDescription.swapTrade: + displaySwapTrade = !displaySwapTrade; case ExchangeProviderDescription.all: if (displayAllTrades) { displayChangeNow = false; @@ -115,6 +122,7 @@ abstract class TradeFilterStoreBase with Store { displayThorChain = false; displayLetsExchange = false; displayStealthEx = false; + displaySwapTrade = false; } else { displayChangeNow = true; displaySideShift = true; @@ -127,6 +135,7 @@ abstract class TradeFilterStoreBase with Store { displayThorChain = true; displayLetsExchange = true; displayStealthEx = true; + displaySwapTrade = true; } break; } @@ -158,7 +167,8 @@ abstract class TradeFilterStoreBase with Store { item.trade.provider == ExchangeProviderDescription.thorChain) || (displayLetsExchange && item.trade.provider == ExchangeProviderDescription.letsExchange) || - (displayStealthEx && item.trade.provider == ExchangeProviderDescription.stealthEx)) + (displayStealthEx && item.trade.provider == ExchangeProviderDescription.stealthEx) || + (displaySwapTrade && item.trade.provider == ExchangeProviderDescription.swapTrade)) .toList() : _trades; } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 99283a409..40a07e666 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -152,6 +152,11 @@ abstract class DashboardViewModelBase with Store { caption: ExchangeProviderDescription.stealthEx.title, onChanged: () => tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.stealthEx)), + FilterItem( + value: () => tradeFilterStore.displaySwapTrade, + caption: ExchangeProviderDescription.swapTrade.title, + onChanged: () => tradeFilterStore + .toggleDisplayExchange(ExchangeProviderDescription.swapTrade)), ] }, subname = '', From 6cc9f4f757d41cee9deb48de9aea9dcb11f788ca Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Thu, 6 Mar 2025 10:00:44 -0800 Subject: [PATCH 022/243] CW-949 backup error messages (#2059) * [skip-ci] minor * dont initialize with restore button enabled + error handling updates --- .../restore_from_backup_view_model.dart | 33 +++++++++++++++---- lib/view_model/wallet_restore_view_model.dart | 1 - 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/lib/view_model/restore_from_backup_view_model.dart b/lib/view_model/restore_from_backup_view_model.dart index 247e6d43d..f9894b592 100644 --- a/lib/view_model/restore_from_backup_view_model.dart +++ b/lib/view_model/restore_from_backup_view_model.dart @@ -13,13 +13,12 @@ import 'package:cake_wallet/store/authentication_store.dart'; part 'restore_from_backup_view_model.g.dart'; -class RestoreFromBackupViewModel = RestoreFromBackupViewModelBase - with _$RestoreFromBackupViewModel; +class RestoreFromBackupViewModel = RestoreFromBackupViewModelBase with _$RestoreFromBackupViewModel; abstract class RestoreFromBackupViewModelBase with Store { RestoreFromBackupViewModelBase(this.backupService) - : state = InitialExecutionState(), - filePath = ''; + : state = InitialExecutionState(), + filePath = ''; final BackupService backupService; @@ -45,8 +44,14 @@ abstract class RestoreFromBackupViewModelBase with Store { final file = File(filePath); final data = await file.readAsBytes(); + await backupService.importBackup(data, password); - await initializeAppAtRoot(reInitializing: true); + + try { + await initializeAppAtRoot(reInitializing: true); + } catch (e, s) { + throw Exception('failed_app_initialization: $e $s'); + } final store = getIt.get(); ReactionDisposer? reaction; @@ -63,11 +68,25 @@ abstract class RestoreFromBackupViewModelBase with Store { state = ExecutedSuccessfullyState(); } catch (e, s) { - var msg = e.toString(); + var msg = e.toString().toLowerCase(); - if (msg.toLowerCase().contains("message authentication code (mac)")) { + // can't use a switch here because of .contains() / not an exact match + bool shouldBeMadeAware = false; + if (msg.contains("message authentication code (mac)")) { msg = 'Incorrect backup password'; + } else if (msg.contains("faileddecryption")) { + msg = 'Failed to decrypt backup file, please check you entered the right password'; + } else if (msg.contains("failed_to_decode")) { + msg = 'Failed to decode backup file, please try again'; + shouldBeMadeAware = true; + } else if (msg.contains("failed_app_initialization")) { + msg = 'Failed to initialize app, please try again'; + shouldBeMadeAware = true; } else { + shouldBeMadeAware = true; + } + + if (shouldBeMadeAware) { await ExceptionHandler.onError(FlutterErrorDetails( exception: e, stack: s, diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 8a497a605..3e5220447 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -67,7 +67,6 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { availableModes = [WalletRestoreMode.seed]; break; } - isButtonEnabled = !hasSeedLanguageSelector && !hasBlockchainHeightLanguageSelector; walletCreationService.changeWalletType(type: type); } From bcbc7ee130ec2a49a38637afde961e2053ea8cab Mon Sep 17 00:00:00 2001 From: Serhii Date: Thu, 6 Mar 2025 20:04:41 +0200 Subject: [PATCH 023/243] rename success rate label (#2055) * rename success rate label * move successRate quote to the recommended section --------- Co-authored-by: Omar Hatem --- lib/buy/buy_quote.dart | 2 +- lib/view_model/buy/buy_sell_view_model.dart | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/buy/buy_quote.dart b/lib/buy/buy_quote.dart index 8da70361d..1805b7e1a 100644 --- a/lib/buy/buy_quote.dart +++ b/lib/buy/buy_quote.dart @@ -16,7 +16,7 @@ extension RecommendationTitle on ProviderRecommendation { case ProviderRecommendation.lowKyc: return 'LOW KYC'; case ProviderRecommendation.successRate: - return 'SUCCESS RATE'; + return 'HIGHEST SUCCESS RATE'; } } } diff --git a/lib/view_model/buy/buy_sell_view_model.dart b/lib/view_model/buy/buy_sell_view_model.dart index 02b2b3576..436f66905 100644 --- a/lib/view_model/buy/buy_sell_view_model.dart +++ b/lib/view_model/buy/buy_sell_view_model.dart @@ -415,6 +415,17 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S return true; }).toList(); + final List successRateQuotes = validQuotes.where((element) => + element.provider is OnRamperBuyProvider && + element.recommendations.contains(ProviderRecommendation.successRate) + ).toList(); + + for (final quote in successRateQuotes) { + if (!uniqueProviderQuotes.contains(quote)) { + uniqueProviderQuotes.add(quote); + } + } + sortedRecommendedQuotes.addAll(uniqueProviderQuotes); sortedQuotes = ObservableList.of( From c7a03559f78e780067da383d77dfa6daba8012df Mon Sep 17 00:00:00 2001 From: tuxsudo Date: Thu, 6 Mar 2025 13:06:00 -0500 Subject: [PATCH 024/243] Update setting in privacy_page.dart (#2056) Co-authored-by: Omar Hatem --- lib/src/screens/settings/privacy_page.dart | 2 +- 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_fr.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_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 + 29 files changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/src/screens/settings/privacy_page.dart b/lib/src/screens/settings/privacy_page.dart index 8652c4af6..238e58eab 100644 --- a/lib/src/screens/settings/privacy_page.dart +++ b/lib/src/screens/settings/privacy_page.dart @@ -73,7 +73,7 @@ class PrivacyPage extends BasePage { _privacySettingsViewModel.setIsAppSecure(value); }), SettingsSwitcherCell( - title: S.current.disable_trade_option, + title: S.current.disable_exchange_option, value: _privacySettingsViewModel.disableTradeOption, onValueChange: (BuildContext _, bool value) { _privacySettingsViewModel.setDisableTradeOption(value); diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 0bffae6c4..84f0c17ce 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -226,6 +226,7 @@ "disable_buy": "تعطيل إجراء الشراء", "disable_cake_2fa": "تعطيل 2 عامل المصادقة", "disable_exchange": "تعطيل التبادل", + "disable_exchange_option": "تعطيل خيار التبادل", "disable_fee_api_warning": "من خلال إيقاف تشغيل هذا ، قد تكون معدلات الرسوم غير دقيقة في بعض الحالات ، لذلك قد ينتهي بك الأمر إلى دفع مبالغ زائدة أو دفع رسوم المعاملات الخاصة بك", "disable_fiat": "تعطيل fiat", "disable_sell": "قم بتعطيل إجراء البيع", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 5195ab39c..c67d8915a 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -226,6 +226,7 @@ "disable_buy": "Деактивирайте действието за покупка", "disable_cake_2fa": "Деактивирайте Cake 2FA", "disable_exchange": "Деактивиране на борса", + "disable_exchange_option": "Опция за деактивиране на обмен", "disable_fee_api_warning": "Като изключите това, таксите могат да бъдат неточни в някои случаи, така че може да се препланите или да не плащате таксите за вашите транзакции", "disable_fiat": "Деактивиране на fiat", "disable_sell": "Деактивирайте действието за продажба", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 4babb1070..349ae67bc 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -226,6 +226,7 @@ "disable_buy": "Zakázat akci nákupu", "disable_cake_2fa": "Zakázat Cake 2FA", "disable_exchange": "Zakázat směnárny", + "disable_exchange_option": "Zakázat možnost výměny", "disable_fee_api_warning": "Tímto vypnutím by sazby poplatků mohly být v některých případech nepřesné, takže byste mohli skončit přepláváním nebo nedoplatkem poplatků za vaše transakce", "disable_fiat": "Zakázat fiat", "disable_sell": "Zakázat akci prodeje", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 69c9e5783..895fa4183 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -226,6 +226,7 @@ "disable_buy": "Kaufaktion deaktivieren", "disable_cake_2fa": "Cake 2FA deaktivieren", "disable_exchange": "Exchange deaktivieren", + "disable_exchange_option": "Deaktivieren Sie die Exchange -Option", "disable_fee_api_warning": "Wenn dies ausgeschaltet wird, sind die Gebührenquoten in einigen Fällen möglicherweise ungenau, sodass Sie die Gebühren für Ihre Transaktionen möglicherweise überbezahlt oder unterzahlt", "disable_fiat": "Fiat deaktivieren", "disable_sell": "Verkaufsaktion deaktivieren", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 8fa1f92b5..c03841cb9 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -226,6 +226,7 @@ "disable_buy": "Disable buy action", "disable_cake_2fa": "Disable Cake 2FA", "disable_exchange": "Disable exchange", + "disable_exchange_option": "Disable Exchange option", "disable_fee_api_warning": "By turning this off, the fee rates might be inaccurate in some cases, so you might end up overpaying or underpaying the fees for your transactions", "disable_fiat": "Disable fiat", "disable_sell": "Disable sell action", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index c6363a15f..dcff955bb 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -226,6 +226,7 @@ "disable_buy": "Desactivar acción de compra", "disable_cake_2fa": "Desactivar 2FA", "disable_exchange": "Deshabilitar intercambio", + "disable_exchange_option": "Deshabilitar la opción de intercambio", "disable_fee_api_warning": "Al apagar esto, las tasas de tarifas pueden ser inexactas en algunos casos, por lo que puede terminar pagando en exceso o pagando menos las tarifas por sus transacciones", "disable_fiat": "Deshabilitar fiat", "disable_sell": "Desactivar acción de venta", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 41409968a..379290a29 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -226,6 +226,7 @@ "disable_buy": "Désactiver l'action d'achat", "disable_cake_2fa": "Désactiver Cake 2FA", "disable_exchange": "Désactiver l'échange", + "disable_exchange_option": "Désactiver l'option d'échange", "disable_fee_api_warning": "En désactivant cela, les taux de frais peuvent être inexacts dans certains cas, vous pourriez donc finir par payer trop ou sous-paiement les frais pour vos transactions", "disable_fiat": "Désactiver les montants en fiat", "disable_sell": "Désactiver l'action de vente", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index c528c58d2..125ceeaf5 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -226,6 +226,7 @@ "disable_buy": "Kashe alama", "disable_cake_2fa": "Musaki Cake 2FA", "disable_exchange": "Kashe musanya", + "disable_exchange_option": "Musaki zaɓi Canji", "disable_fee_api_warning": "Ta hanyar juya wannan kashe, kudaden da zai iya zama ba daidai ba a wasu halaye, saboda haka zaku iya ƙare da overpaying ko a ƙarƙashin kudaden don ma'amaloli", "disable_fiat": "Dakatar da fiat", "disable_sell": "Kashe karbuwa", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index cc1a7c6e1..074f3ba09 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -226,6 +226,7 @@ "disable_buy": "खरीद कार्रवाई अक्षम करें", "disable_cake_2fa": "केक 2FA अक्षम करें", "disable_exchange": "एक्सचेंज अक्षम करें", + "disable_exchange_option": "विनिमय विकल्प अक्षम करें", "disable_fee_api_warning": "इसे बंद करने से, कुछ मामलों में शुल्क दरें गलत हो सकती हैं, इसलिए आप अपने लेनदेन के लिए फीस को कम कर सकते हैं या कम कर सकते हैं", "disable_fiat": "िएट को अक्षम करें", "disable_sell": "बेचने की कार्रवाई अक्षम करें", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 9aae5501e..be447f24d 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -226,6 +226,7 @@ "disable_buy": "Onemogući kupnju", "disable_cake_2fa": "Onemogući Cake 2FA", "disable_exchange": "Onemogući exchange", + "disable_exchange_option": "Onemogući opciju razmjene", "disable_fee_api_warning": "Isključivanjem ovoga, stope naknade u nekim bi slučajevima mogle biti netočne, tako da biste mogli preplatiti ili predati naknadu za vaše transakcije", "disable_fiat": "Isključi, fiat", "disable_sell": "Onemogući akciju prodaje", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index bc99fda74..551e28cef 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -226,6 +226,7 @@ "disable_buy": "Անջատել գնում գործողությունը", "disable_cake_2fa": "Անջատել Cake 2FA", "disable_exchange": "Անջատել փոխանակումը", + "disable_exchange_option": "Անջատեք փոխանակման տարբերակը", "disable_fee_api_warning": "Դրանից անջատելով, վճարների տեմպերը որոշ դեպքերում կարող են անճիշտ լինել, այնպես որ դուք կարող եք վերջ տալ ձեր գործարքների համար վճարների գերավճարների կամ գերավճարների վրա", "disable_fiat": "Անջատել ֆիատ", "disable_sell": "Անջատել վաճառք գործողությունը", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 1458d69ef..a171f3243 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -226,6 +226,7 @@ "disable_buy": "Nonaktifkan tindakan beli", "disable_cake_2fa": "Nonaktifkan Kue 2FA", "disable_exchange": "Nonaktifkan pertukaran", + "disable_exchange_option": "Nonaktifkan opsi pertukaran", "disable_fee_api_warning": "Dengan mematikan ini, tarif biaya mungkin tidak akurat dalam beberapa kasus, jadi Anda mungkin akan membayar lebih atau membayar biaya untuk transaksi Anda", "disable_fiat": "Nonaktifkan fiat", "disable_sell": "Nonaktifkan aksi jual", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 0ec8de33a..f035c3493 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -226,6 +226,7 @@ "disable_buy": "Disabilita l'azione di acquisto", "disable_cake_2fa": "Disabilita Cake 2FA", "disable_exchange": "Disabilita scambio", + "disable_exchange_option": "Disabilita l'opzione di scambio", "disable_fee_api_warning": "Disattivando quest'opzione, i tassi delle commissioni potrebbero essere inaccurati in alcuni casi, quindi potresti finire per pagare troppo o troppo poco le commissioni per le tue transazioni", "disable_fiat": "Disabilita fiat", "disable_sell": "Disabilita l'azione di vendita", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index e4b16ec85..89fcc5391 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -226,6 +226,7 @@ "disable_buy": "購入アクションを無効にする", "disable_cake_2fa": "Cake 2FA を無効にする", "disable_exchange": "交換を無効にする", + "disable_exchange_option": "交換オプションを無効にします", "disable_fee_api_warning": "これをオフにすることで、料金金利は場合によっては不正確になる可能性があるため、取引の費用が過払いまたは不足している可能性があります", "disable_fiat": "フィアットを無効にする", "disable_sell": "販売アクションを無効にする", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 056a091c2..e8e23a1a2 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -226,6 +226,7 @@ "disable_buy": "구매 행동 비활성화", "disable_cake_2fa": "케이크 2FA 비활성화", "disable_exchange": "교환 비활성화", + "disable_exchange_option": "교환 옵션을 비활성화합니다", "disable_fee_api_warning": "이것을 끄면 경우에 따라 수수료가 부정확 할 수 있으므로 거래 수수료를 초과 지불하거나 지불 할 수 있습니다.", "disable_fiat": "법정화폐 비활성화", "disable_sell": "판매 조치 비활성화", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index a4a17760c..c21413a44 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -226,6 +226,7 @@ "disable_buy": "ဝယ်ယူမှု လုပ်ဆောင်ချက်ကို ပိတ်ပါ။", "disable_cake_2fa": "ကိတ်မုန့် 2FA ကို ပိတ်ပါ။", "disable_exchange": "လဲလှယ်မှုကို ပိတ်ပါ။", + "disable_exchange_option": "ငွေလဲရွေး option ကိုပိတ်ပါ", "disable_fee_api_warning": "ဤအရာကိုဖွင့်ခြင်းအားဖြင့်အချို့သောကိစ္စရပ်များတွင်အခကြေးငွေနှုန်းထားများသည်တိကျမှုရှိနိုင်သည်,", "disable_fiat": "Fiat ကိုပိတ်ပါ။", "disable_sell": "ရောင်းချခြင်းလုပ်ဆောင်ချက်ကို ပိတ်ပါ။", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index e950a75bd..0b32a7fc9 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -226,6 +226,7 @@ "disable_buy": "Koopactie uitschakelen", "disable_cake_2fa": "Taart 2FA uitschakelen", "disable_exchange": "Uitwisseling uitschakelen", + "disable_exchange_option": "Schakel Exchange -optie uit", "disable_fee_api_warning": "Door dit uit te schakelen, kunnen de tarieven in sommige gevallen onnauwkeurig zijn, dus u kunt de vergoedingen voor uw transacties te veel betalen of te weinig betalen", "disable_fiat": "Schakel Fiat uit", "disable_sell": "Verkoopactie uitschakelen", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index e4ac7286d..b5acc57ed 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -226,6 +226,7 @@ "disable_buy": "Wyłącz akcję kupna", "disable_cake_2fa": "Wyłącz Cake 2FA", "disable_exchange": "Wyłącz wymianę", + "disable_exchange_option": "Wyłącz opcję wymiany", "disable_fee_api_warning": "Wyłączając to, stawki opłaty mogą być w niektórych przypadkach niedokładne, więc możesz skończyć się przepłaceniem lub wynagrodzeniem opłat za transakcje", "disable_fiat": "Wyłącz waluty FIAT", "disable_sell": "Wyłącz akcję sprzedaży", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 5e0c246bd..8ec99f0f8 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -226,6 +226,7 @@ "disable_buy": "Desativar ação de compra", "disable_cake_2fa": "Desabilitar o Cake 2FA", "disable_exchange": "Desativar troca", + "disable_exchange_option": "Desativar opção de troca", "disable_fee_api_warning": "Ao desativar isso, as taxas de taxas podem ser imprecisas em alguns casos, para que você possa acabar pagando demais ou pagando as taxas por suas transações", "disable_fiat": "Desativar fiat", "disable_sell": "Desativar ação de venda", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index f09ca70e2..8ef7ac579 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -226,6 +226,7 @@ "disable_buy": "Отключить действие покупки", "disable_cake_2fa": "Отключить торт 2FA", "disable_exchange": "Отключить обмен", + "disable_exchange_option": "Отключить вариант обмена", "disable_fee_api_warning": "Выключив это, в некоторых случаях ставки платы могут быть неточными, так что вы можете в конечном итоге переплачивать или недоплачивать сборы за ваши транзакции", "disable_fiat": "Отключить фиат", "disable_sell": "Отключить действие продажи", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 5206db0bf..414bbe52f 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -226,6 +226,7 @@ "disable_buy": "ปิดการใช้งานการซื้อ", "disable_cake_2fa": "ปิดการใช้งานเค้ก 2FA", "disable_exchange": "ปิดใช้งานการแลกเปลี่ยน", + "disable_exchange_option": "ปิดการใช้งานตัวเลือกการแลกเปลี่ยน", "disable_fee_api_warning": "โดยการปิดสิ่งนี้อัตราค่าธรรมเนียมอาจไม่ถูกต้องในบางกรณีดังนั้นคุณอาจจบลงด้วยการจ่ายเงินมากเกินไปหรือจ่ายค่าธรรมเนียมสำหรับการทำธุรกรรมของคุณมากเกินไป", "disable_fiat": "ปิดใช้งานสกุลเงินตรา", "disable_sell": "ปิดการใช้งานการขาย", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 4befe732a..46a7540bd 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -226,6 +226,7 @@ "disable_buy": "Huwag paganahin ang pagkilos ng pagbili", "disable_cake_2fa": "Huwag paganahin ang Cake 2FA", "disable_exchange": "Huwag paganahin ang palitan", + "disable_exchange_option": "Huwag paganahin ang pagpipilian sa palitan", "disable_fee_api_warning": "Sa pamamagitan ng pag -off nito, ang mga rate ng bayad ay maaaring hindi tumpak sa ilang mga kaso, kaya maaari mong tapusin ang labis na bayad o pagsuporta sa mga bayarin para sa iyong mga transaksyon", "disable_fiat": "Huwag paganahin ang fiat", "disable_sell": "Huwag paganahin ang pagkilos ng pagbebenta", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index efb00c98f..8fcacb9ba 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -226,6 +226,7 @@ "disable_buy": "Satın alma işlemini devre dışı bırak", "disable_cake_2fa": "Cake 2FA'yı Devre Dışı Bırak", "disable_exchange": "Borsayı devre dışı bırak", + "disable_exchange_option": "Değişim seçeneğini devre dışı bırak", "disable_fee_api_warning": "Bunu kapatarak, ücret oranları bazı durumlarda yanlış olabilir, bu nedenle işlemleriniz için ücretleri fazla ödeyebilir veya az ödeyebilirsiniz.", "disable_fiat": "İtibari paraları devre dışı bırak", "disable_sell": "Satış işlemini devre dışı bırak", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index c670b174a..6b111f72f 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -226,6 +226,7 @@ "disable_buy": "Вимкнути дію покупки", "disable_cake_2fa": "Вимкнути Cake 2FA", "disable_exchange": "Вимкнути можливість обміну", + "disable_exchange_option": "Вимкнути варіант обміну", "disable_fee_api_warning": "Вимкнувши це, ставки плати в деяких випадках можуть бути неточними, тому ви можете переплатити або недооплатити плату за свої транзакції", "disable_fiat": "Вимкнути фиат", "disable_sell": "Вимкнути дію продажу", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 8a12b50dc..d1a2b4ca9 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -226,6 +226,7 @@ "disable_buy": "خرید ایکشن کو غیر فعال کریں۔", "disable_cake_2fa": "کیک 2FA کو غیر فعال کریں۔", "disable_exchange": "تبادلے کو غیر فعال کریں۔", + "disable_exchange_option": "ایکسچینج آپشن کو غیر فعال کریں", "disable_fee_api_warning": "اس کو بند کرنے سے ، کچھ معاملات میں فیس کی شرح غلط ہوسکتی ہے ، لہذا آپ اپنے لین دین کے لئے فیسوں کو زیادہ ادائیگی یا ادائیگی ختم کرسکتے ہیں۔", "disable_fiat": "فیاٹ کو غیر فعال کریں۔", "disable_sell": "فروخت کی کارروائی کو غیر فعال کریں۔", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 637e10a76..2aba61ef3 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -225,6 +225,7 @@ "disable_buy": "Vô hiệu hóa chức năng mua", "disable_cake_2fa": "Vô hiệu hóa 2FA Cake", "disable_exchange": "Vô hiệu hóa chức năng trao đổi", + "disable_exchange_option": "Tắt tùy chọn trao đổi", "disable_fee_api_warning": "Khi tắt chức năng này, tỉ lệ phí có thể không chính xác trong một số trường hợp, dẫn đến bạn trả quá hoặc không đủ phí cho giao dịch của mình.", "disable_fiat": "Vô hiệu hóa tiền tệ fiat", "disable_sell": "Vô hiệu hóa chức năng bán", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index ca2f6ade0..744159a30 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -226,6 +226,7 @@ "disable_buy": "Ko iṣọrọ ọja", "disable_cake_2fa": "Ko 2FA Cake sii", "disable_exchange": "Pa ilé pàṣípààrọ̀", + "disable_exchange_option": "Mu aṣayan paṣipaarọ", "disable_fee_api_warning": "Nipa yiyi eyi kuro, awọn oṣuwọn owo naa le jẹ aiṣe deede ni awọn ọrọ kan, nitorinaa o le pari apọju tabi awọn idiyele ti o ni agbara fun awọn iṣowo rẹ", "disable_fiat": "Pa owó tí ìjọba pàṣẹ wa lò", "disable_sell": "Ko iṣọrọ iṣọrọ", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index d272b08b2..a3b5ec421 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -226,6 +226,7 @@ "disable_buy": "禁用购买操作", "disable_cake_2fa": "禁用蛋糕 2FA", "disable_exchange": "禁用交换", + "disable_exchange_option": "禁用交换选项", "disable_fee_api_warning": "通过将其关闭,在某些情况下,收费率可能不准确,因此您最终可能会超额付款或支付交易费用", "disable_fiat": "禁用法令", "disable_sell": "禁用卖出操作", From 3dabad81f4207e81ccd59fb958d6a80dccc7eb38 Mon Sep 17 00:00:00 2001 From: tuxsudo Date: Thu, 6 Mar 2025 18:34:17 -0500 Subject: [PATCH 025/243] Fix whitespace (#2070) --- ios/Runner/InfoBase.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ios/Runner/InfoBase.plist b/ios/Runner/InfoBase.plist index aec00022b..69ab926b8 100644 --- a/ios/Runner/InfoBase.plist +++ b/ios/Runner/InfoBase.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + BGTaskSchedulerPermittedIdentifiers com.fotolockr.cakewallet.monero_sync_task From e56dd1256dbf4ca278e52811e2f728ead8366016 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Fri, 7 Mar 2025 02:48:22 +0200 Subject: [PATCH 026/243] V4.23.3 RC (#2069) * V4.23.3 RC * update release notes [skip ci] --- assets/text/Monerocom_Release_Notes.txt | 4 ++-- assets/text/Release_Notes.txt | 6 ++---- ios/Podfile.lock | 4 ++-- lib/buy/moonpay/moonpay_provider.dart | 26 ++++++++++++------------- scripts/android/app_env.sh | 8 ++++---- scripts/ios/app_env.sh | 8 ++++---- scripts/linux/app_env.sh | 4 ++-- scripts/macos/app_env.sh | 8 ++++---- scripts/windows/build_exe_installer.iss | 2 +- 9 files changed, 33 insertions(+), 37 deletions(-) diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index 011435baa..d1f91139b 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,3 +1,3 @@ -Ledger fixes -UI enhancements +UI/UX enhancements +Stability improvements Bug fixes \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index ca69e0b98..d1f91139b 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,5 +1,3 @@ -Zano enhancements -Ethereum enhancements -Ledger fixes -UI enhancements +UI/UX enhancements +Stability improvements Bug fixes \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d400d3f81..120194ee0 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -240,7 +240,7 @@ SPEC CHECKSUMS: connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483 cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a - cw_mweb: 87af74f9659fed0c1a2cbfb44413f1070e79e3ae + cw_mweb: 22cd01dfb8ad2d39b15332006f22046aaa8352a3 cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 @@ -277,4 +277,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: e448f662d4c41f0c0b1ccbb78afd57dbf895a597 -COCOAPODS: 1.15.2 \ No newline at end of file +COCOAPODS: 1.15.2 diff --git a/lib/buy/moonpay/moonpay_provider.dart b/lib/buy/moonpay/moonpay_provider.dart index 7bca7d405..10148dd30 100644 --- a/lib/buy/moonpay/moonpay_provider.dart +++ b/lib/buy/moonpay/moonpay_provider.dart @@ -130,7 +130,8 @@ class MoonPayProvider extends BuyProvider { final List paymentMethods = []; if (isBuyAction) { - final fiatBuyCredentials = await fetchFiatCredentials(fiatCurrency, cryptoCurrency.title, null); + final fiatBuyCredentials = + await fetchFiatCredentials(fiatCurrency, cryptoCurrency.title, null); if (fiatBuyCredentials.isNotEmpty) { final paymentMethod = fiatBuyCredentials['paymentMethod'] as String?; paymentMethods.add(PaymentMethod.fromMoonPayJson( @@ -223,7 +224,6 @@ class MoonPayProvider extends BuyProvider { required bool isBuyAction, required String cryptoCurrencyAddress, String? countryCode}) async { - final Map params = { 'theme': themeToMoonPayTheme(_settingsStore.currentTheme), 'language': _settingsStore.languageCode, @@ -246,19 +246,17 @@ class MoonPayProvider extends BuyProvider { if (!isBuyAction) params['quoteCurrencyCode'] = quote.cryptoCurrency.name; try { - { - final uri = await requestMoonPayUrl( - walletAddress: cryptoCurrencyAddress, - settingsStore: _settingsStore, - isBuyAction: isBuyAction, - amount: amount.toString(), - params: params); + final uri = await requestMoonPayUrl( + walletAddress: cryptoCurrencyAddress, + settingsStore: _settingsStore, + isBuyAction: isBuyAction, + amount: amount.toString(), + params: params); - if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } else { - throw Exception('Could not launch URL'); - } + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } else { + throw Exception('Could not launch URL'); } } catch (e) { if (context.mounted) { diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 8b1d46264..9419ae1c6 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -15,15 +15,15 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.20.2" -MONERO_COM_BUILD_NUMBER=114 +MONERO_COM_VERSION="1.20.3" +MONERO_COM_BUILD_NUMBER=115 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.23.2" -CAKEWALLET_BUILD_NUMBER=247 +CAKEWALLET_VERSION="4.23.3" +CAKEWALLET_BUILD_NUMBER=248 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index c6d3778f3..491b7d06a 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,13 +13,13 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.20.2" -MONERO_COM_BUILD_NUMBER=112 +MONERO_COM_VERSION="1.20.3" +MONERO_COM_BUILD_NUMBER=113 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.23.2" -CAKEWALLET_BUILD_NUMBER=298 +CAKEWALLET_VERSION="4.23.3" +CAKEWALLET_BUILD_NUMBER=301 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/linux/app_env.sh b/scripts/linux/app_env.sh index 325d2b335..91ab2cdeb 100755 --- a/scripts/linux/app_env.sh +++ b/scripts/linux/app_env.sh @@ -14,8 +14,8 @@ if [ -n "$1" ]; then fi CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.13.2" -CAKEWALLET_BUILD_NUMBER=47 +CAKEWALLET_VERSION="1.13.3" +CAKEWALLET_BUILD_NUMBER=48 if ! [[ " ${TYPES[*]} " =~ " ${APP_LINUX_TYPE} " ]]; then echo "Wrong app type." diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index f554d4d01..234d07c66 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -16,13 +16,13 @@ if [ -n "$1" ]; then fi MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.10.2" -MONERO_COM_BUILD_NUMBER=44 +MONERO_COM_VERSION="1.10.3" +MONERO_COM_BUILD_NUMBER=45 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.16.2" -CAKEWALLET_BUILD_NUMBER=105 +CAKEWALLET_VERSION="1.16.3" +CAKEWALLET_BUILD_NUMBER=106 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then diff --git a/scripts/windows/build_exe_installer.iss b/scripts/windows/build_exe_installer.iss index b2dd60130..3fb0cd1aa 100644 --- a/scripts/windows/build_exe_installer.iss +++ b/scripts/windows/build_exe_installer.iss @@ -1,5 +1,5 @@ #define MyAppName "Cake Wallet" -#define MyAppVersion "0.4.2" +#define MyAppVersion "0.4.3" #define MyAppPublisher "Cake Labs LLC" #define MyAppURL "https://cakewallet.com/" #define MyAppExeName "CakeWallet.exe" From 8009ee061c02c71e09d7e8a90b30bbd849e73eb4 Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 7 Mar 2025 14:48:36 +0200 Subject: [PATCH 027/243] Update dashboard_view_model.dart --- .../dashboard/dashboard_view_model.dart | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 2834be0e1..d070b2ff9 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -253,33 +253,34 @@ abstract class DashboardViewModelBase with Store { _checkMweb(); }); - connectMapToListWithTransform( - appStore.wallet!.transactionHistory.transactions, - transactions, - (TransactionInfo? transaction) => TransactionListItem( - transaction: transaction!, - balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore, - key: ValueKey( - '${_wallet.type.name}_transaction_history_item_${transaction.id}_key', + _transactionDisposer?.reaction.dispose(); + + _transactionDisposer = reaction( + (_) => appStore.wallet!.transactionHistory.transactions.values.toList(), + (List txs) { + + transactions.clear(); + + transactions.addAll( + txs.where((tx) { + if (wallet.type == WalletType.monero) { + return monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id; + } + if (wallet.type == WalletType.wownero) { + return wow.wownero!.getTransactionInfoAccountId(tx) == wow.wownero!.getCurrentAccount(wallet).id; + } + return true; + }).map( + (tx) => TransactionListItem( + transaction: tx, + balanceViewModel: balanceViewModel, + settingsStore: appStore.settingsStore, + key: ValueKey('${wallet.type.name}_transaction_history_item_${tx.id}_key'), ), - ), filter: (TransactionInfo? transaction) { - if (transaction == null) { - return false; - } - - final wallet = _wallet; - if (wallet.type == WalletType.monero) { - return monero!.getTransactionInfoAccountId(transaction) == - monero!.getCurrentAccount(wallet).id; - } - if (wallet.type == WalletType.wownero) { - return wow.wownero!.getTransactionInfoAccountId(transaction) == - wow.wownero!.getCurrentAccount(wallet).id; - } - - return true; - }); + ), + ); + } + ); if (hasSilentPayments) { silentPaymentsScanningActive = bitcoin!.getScanningActive(wallet); From be4e0d6ac8093d384bf1367c61013ffb606f79c3 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Sun, 9 Mar 2025 00:59:55 +0200 Subject: [PATCH 028/243] minor fix [skip ci] --- cw_polygon/lib/polygon_wallet.dart | 2 +- cw_solana/lib/solana_wallet.dart | 2 +- cw_tron/lib/tron_wallet.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cw_polygon/lib/polygon_wallet.dart b/cw_polygon/lib/polygon_wallet.dart index f9aff16c3..4db24b32a 100644 --- a/cw_polygon/lib/polygon_wallet.dart +++ b/cw_polygon/lib/polygon_wallet.dart @@ -121,7 +121,7 @@ class PolygonWallet extends EVMChainWallet { if (!hasKeysFile) rethrow; } - final balance = EVMChainERC20Balance.fromJSON(data?['balance'] as String) ?? + final balance = EVMChainERC20Balance.fromJSON(data?['balance'] as String?) ?? EVMChainERC20Balance(BigInt.zero); final WalletKeysData keysData; diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index 15c065918..248f1282b 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -407,7 +407,7 @@ abstract class SolanaWalletBase if (!hasKeysFile) rethrow; } - final balance = SolanaBalance.fromJSON(data?['balance'] as String) ?? SolanaBalance(0.0); + final balance = SolanaBalance.fromJSON(data?['balance'] as String?) ?? SolanaBalance(0.0); final WalletKeysData keysData; // Migrate wallet from the old scheme to then new .keys file scheme diff --git a/cw_tron/lib/tron_wallet.dart b/cw_tron/lib/tron_wallet.dart index cfa80f0d3..b8d5eceec 100644 --- a/cw_tron/lib/tron_wallet.dart +++ b/cw_tron/lib/tron_wallet.dart @@ -144,7 +144,7 @@ abstract class TronWalletBase if (!hasKeysFile) rethrow; } - final balance = TronBalance.fromJSON(data?['balance'] as String) ?? TronBalance(BigInt.zero); + final balance = TronBalance.fromJSON(data?['balance'] as String?) ?? TronBalance(BigInt.zero); final WalletKeysData keysData; // Migrate wallet from the old scheme to then new .keys file scheme From 1cb8651ffcde44d69c7b6660ac1fb8b029e219de Mon Sep 17 00:00:00 2001 From: Seth For Privacy <40500387+sethforprivacy@users.noreply.github.com> Date: Sun, 9 Mar 2025 17:10:12 -0400 Subject: [PATCH 029/243] Improve build docs and optimize Dockerfile for Android and Linux builds (#2016) * Update build docs and migrate Android builds to Docker * Update NDK and move steps directly into Dockerfile * Fix NDK installation via script and Dockerfile * Migrate to @MrCyjaneK's existing Dockerfile (with optimizations) * Add .dockerignore and migrate Dockerfile to root of project * Revert .gitignore changes * Update Android and Linux builds and resolve Linux build issue * Fix git repo for Linux and Android build instructions * Set branch to latest release in build docs * Ensure `flutter clean` is run before building * Fix Linux completion example * Don't build Zano for Linux * Apply suggestions from code review Co-authored-by: cyan * Explicitly add automake package to Dockerfile * Improve logging on Android and Linux builds via Docker * Improve Dockerfile comments and ordering * Fix issues in macOS and iOS builds docs (thanks @MrCyjaneK) * Update docs/builds/IOS.md [skip ci] * Update docs/builds/IOS.md [skip ci] * Update docs/builds/MACOS.md [skip ci] * Update docs/builds/IOS.md [skip ci] * Fix and improve iOS and macOS build docs * Windows build doc improvements Co-authored-by: cyan * add missing dependency to Dockerfile * Update Windows build docs * More fixes and optimizations to the Windows build docs * Add git config to Windows build doc * Fix shell location/commands in Windows build doc * Fix WSL commands in Windows build doc * Apply suggestions from code review Co-authored-by: cyan --------- Co-authored-by: cyan --- .dockerignore | 1 + scripts/linux/Dockerfile.linux => Dockerfile | 125 +++++++------ build-guide-linux.md | 176 ------------------ build-guide-win.md | 38 ---- .../NEW_WALLET_TYPES.md | 0 SECURITY.md => docs/SECURITY.md | 0 docs/builds/ANDROID.md | 60 ++++++ docs/builds/IOS.md | 143 ++++++++++++++ docs/builds/LINUX.md | 96 ++++++++++ docs/builds/MACOS.md | 135 ++++++++++++++ docs/builds/WINDOWS.md | 92 +++++++++ howto-build-android.md | 149 --------------- howto-build-ios.md | 101 ---------- howto-build-macos.md | 112 ----------- howto-build-windows.md | 57 ------ linux/CMakeLists.txt | 3 - scripts/android/install_ndk.sh | 4 +- scripts/linux/build_monero_all.sh | 2 +- 18 files changed, 594 insertions(+), 700 deletions(-) create mode 100644 .dockerignore rename scripts/linux/Dockerfile.linux => Dockerfile (62%) delete mode 100644 build-guide-linux.md delete mode 100644 build-guide-win.md rename how_to_add_new_wallet_type.md => docs/NEW_WALLET_TYPES.md (100%) rename SECURITY.md => docs/SECURITY.md (100%) create mode 100644 docs/builds/ANDROID.md create mode 100644 docs/builds/IOS.md create mode 100644 docs/builds/LINUX.md create mode 100644 docs/builds/MACOS.md create mode 100644 docs/builds/WINDOWS.md delete mode 100644 howto-build-android.md delete mode 100644 howto-build-ios.md delete mode 100644 howto-build-macos.md delete mode 100644 howto-build-windows.md diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..f59ec20aa --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +* \ No newline at end of file diff --git a/scripts/linux/Dockerfile.linux b/Dockerfile similarity index 62% rename from scripts/linux/Dockerfile.linux rename to Dockerfile index b41873cf5..dae174a61 100644 --- a/scripts/linux/Dockerfile.linux +++ b/Dockerfile @@ -1,83 +1,87 @@ # Usage: -# docker build . -f Dockerfile.linux -t ghcr.io/cake-tech/cake_wallet:main-linux +# docker build . -f Dockerfile -t ghcr.io/cake-tech/cake_wallet:main-linux # docker push ghcr.io/cake-tech/cake_wallet:main-linux -FROM --platform=linux/amd64 docker.io/debian:12 - -LABEL org.opencontainers.image.source=https://github.com/cake-tech/cake_wallet - -ENV GOLANG_VERSION=1.23.4 -# comes from https://developer.android.com/studio/#command-tools -ENV ANDROID_SDK_TOOLS_VERSION=11076708 -# https://developer.android.com/studio/releases/build-tools -ENV ANDROID_PLATFORM_VERSION=34 -ENV ANDROID_BUILD_TOOLS_VERSION=34.0.0 - -ENV FLUTTER_VERSION=3.24.0 - -# If we ever need to migrate the home directory... -RUN sed -i 's|^root:[^:]*:[^:]*:[^:]*:[^:]*:/root:|root:x:0:0:root:/root:|' /etc/passwd -# mkdir -p /root && rm -rf /root && cp -a /root /root -ENV HOME=/root # Heavily inspired by cirrusci images # https://github.com/cirruslabs/docker-images-android/blob/master/sdk/tools/Dockerfile # https://github.com/cirruslabs/docker-images-android/blob/master/sdk/34/Dockerfile # https://github.com/cirruslabs/docker-images-android/blob/master/sdk/34-ndk/Dockerfile # https://github.com/cirruslabs/docker-images-flutter/blob/master/sdk/Dockerfile +FROM --platform=linux/amd64 docker.io/debian:12 + +LABEL org.opencontainers.image.source=https://github.com/cake-tech/cake_wallet + +# Set necessary environment variables +# Set Go version to latest known-working version +ENV GOLANG_VERSION=1.23.4 + +# Pin Flutter version to latest known-working version +ENV FLUTTER_VERSION=3.24.4 + +# Pin Android Studio, platform, and build tools versions to latest known-working version +# Comes from https://developer.android.com/studio/#command-tools +ENV ANDROID_SDK_TOOLS_VERSION=11076708 +# Comes from https://developer.android.com/studio/releases/build-tools +ENV ANDROID_PLATFORM_VERSION=34 +ENV ANDROID_BUILD_TOOLS_VERSION=34.0.0 + +# If we ever need to migrate the home directory... +RUN sed -i 's|^root:[^:]*:[^:]*:[^:]*:[^:]*:/root:|root:x:0:0:root:/root:|' /etc/passwd +# mkdir -p /root && rm -rf /root && cp -a /root /root +ENV HOME=/root ENV ANDROID_HOME=/opt/android-sdk-linux \ LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 \ LANGUAGE=en_US:en +# Set Android SDK paths ENV ANDROID_SDK_ROOT=$ANDROID_HOME \ PATH=${PATH}:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/emulator +# Upgrade base image +RUN apt-get update \ + && apt-get upgrade -y + +# Install all build dependencies RUN set -o xtrace \ && cd /opt \ - && apt-get update \ - && apt-get upgrade -y \ - && apt-get install -y jq \ - && apt-get install -y default-jdk \ - && apt-get install -y sudo wget zip unzip git openssh-client curl bc software-properties-common build-essential ruby-full ruby-bundler libstdc++6 libpulse0 libglu1-mesa locales lcov libsqlite3-dev --no-install-recommends \ + && apt-get install -y --no-install-recommends --no-install-suggests \ + # Core dependencies + bc build-essential curl default-jdk git jq lcov libglu1-mesa libpulse0 libsqlite3-dev libstdc++6 locales openssh-client ruby-bundler ruby-full software-properties-common sudo unzip wget zip \ # for x86 emulators - && apt-get install -y libxtst6 libnss3-dev libnspr4 libxss1 libatk-bridge2.0-0 libgtk-3-0 libgdk-pixbuf2.0-0 \ - && apt-get install -y -qq xxd \ - && apt-get install -y lftp \ - && apt-get install -qq -y sqlite3 libsqlite3-dev \ - # linux desktop dependencies - && apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev \ + libatk-bridge2.0-0 libgdk-pixbuf2.0-0 libgtk-3-0 libnspr4 libnss3-dev libsqlite3-dev libxtst6 libxss1 lftp sqlite3 xxd \ + # Linux desktop dependencies + clang cmake libgtk-3-dev ninja-build pkg-config \ # monero_c dependencies - && apt-get install -y ccache build-essential autoconf libtool gperf llvm \ + autoconf automake build-essential ccache gperf libtool llvm \ # extra stuff for KVM - && apt-get install -y udev qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils \ - # for linux tests - && apt-get install -y xvfb network-manager ffmpeg x11-utils \ - # for aarch64-linux-gnu - && apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu \ - && rm -rf /var/lib/apt/lists/* \ + bridge-utils libvirt-clients libvirt-daemon-system qemu-kvm udev \ + # Linux test dependencies + ffmpeg network-manager x11-utils xvfb psmisc \ + # aarch64-linux-gnu dependencies + g++-aarch64-linux-gnu gcc-aarch64-linux-gnu \ + && apt clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ && sh -c 'echo "en_US.UTF-8 UTF-8" > /etc/locale.gen' \ && locale-gen \ && update-locale LANG=en_US.UTF-8 -# install nodejs for actions -RUN apt-get update && \ - apt-get install -y curl && \ - curl -fsSL https://deb.nodesource.com/setup_23.x | bash - && \ - apt-get install -y nodejs && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* - -RUN wget https://go.dev/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz &&\ - rm -rf /usr/local/go &&\ - tar -C /usr/local -xzf go${GOLANG_VERSION}.linux-amd64.tar.gz +# Install nodejs for Github Actions +RUN curl -fsSL https://deb.nodesource.com/setup_23.x | bash - && \ + apt-get install -y --no-install-recommends nodejs && \ + apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* +# Install Go ENV PATH=${PATH}:/usr/local/go/bin:${HOME}/go/bin ENV GOROOT=/usr/local/go ENV GOPATH=${HOME}/go -RUN go install golang.org/x/mobile/cmd/gomobile@latest -RUN gomobile init +RUN wget https://go.dev/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz &&\ + rm -rf /usr/local/go &&\ + tar -C /usr/local -xzf go${GOLANG_VERSION}.linux-amd64.tar.gz && \ + go install golang.org/x/mobile/cmd/gomobile@latest && \ + gomobile init +# Install Android SDK commandline tools and emulator RUN wget -q https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_SDK_TOOLS_VERSION}_latest.zip -O android-sdk-tools.zip \ && mkdir -p ${ANDROID_HOME}/cmdline-tools/ \ && unzip -q android-sdk-tools.zip -d ${ANDROID_HOME}/cmdline-tools/ \ @@ -94,10 +98,10 @@ RUN wget -q https://dl.google.com/android/repository/commandlinetools-linux-${AN && git config --global user.email "czarek@cakewallet.com" \ && git config --global user.name "CakeWallet CI" -# emulator is not available on linux/arm64 (https://issuetracker.google.com/issues/227219818) +# Handle emulator not being available on linux/arm64 (https://issuetracker.google.com/issues/227219818) RUN if [ $(uname -m) == "x86_64" ]; then sdkmanager emulator ; fi -# Extra dependencies to not download them for cake wallet build +# Pre-install extra Android SDK dependencies in order to not have to download them for each build RUN yes | sdkmanager \ "platforms;android-$ANDROID_PLATFORM_VERSION" \ "build-tools;$ANDROID_BUILD_TOOLS_VERSION" \ @@ -107,26 +111,26 @@ RUN yes | sdkmanager \ "build-tools;33.0.0" \ "build-tools;35.0.0" +# Install extra NDK dependency for sp_scanner ENV ANDROID_NDK_VERSION=27.2.12479018 - -# Extra ndk dependency for sp_scanner RUN yes | sdkmanager "ndk;$ANDROID_NDK_VERSION" \ "ndk;27.0.12077973" -# https://github.com/ReactiveCircus/android-emulator-runner dependencies for tests +# Install dependencies for tests +# Comes from https://github.com/ReactiveCircus/android-emulator-runner RUN yes | sdkmanager "system-images;android-29;default;x86" \ "system-images;android-29;default;x86_64" \ "system-images;android-31;default;x86_64" \ "platforms;android-29" -# fake the KVM status so android emulator doesn't complain (that much) +# Fake the KVM status so the Android emulator doesn't complain (that much) RUN (addgroup kvm || true) && \ adduser root kvm && \ mkdir -p /etc/udev/rules.d/ && \ echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | tee /etc/udev/rules.d/99-kvm4all.rules +# Install rustup, rust toolchains, and cargo-ndk ENV PATH=${HOME}/.cargo/bin:${PATH} - RUN curl https://sh.rustup.rs -sSf | bash -s -- -y && \ cargo install cargo-ndk && \ for target in aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android x86_64-unknown-linux-gnu; \ @@ -134,17 +138,16 @@ RUN curl https://sh.rustup.rs -sSf | bash -s -- -y && \ rustup target add --toolchain stable $target; \ done - +# Download and install Flutter ENV HOME=${HOME} ENV FLUTTER_HOME=${HOME}/sdks/flutter/${FLUTTER_VERSION} ENV FLUTTER_ROOT=$FLUTTER_HOME - ENV PATH=${PATH}:${FLUTTER_HOME}/bin:${FLUTTER_HOME}/bin/cache/dart-sdk/bin -RUN git clone --depth 1 --branch ${FLUTTER_VERSION} https://github.com/flutter/flutter.git ${FLUTTER_HOME} - -RUN yes | flutter doctor --android-licenses \ +RUN git clone --depth 1 --branch ${FLUTTER_VERSION} https://github.com/flutter/flutter.git ${FLUTTER_HOME} \ + && yes | flutter doctor --android-licenses \ && flutter doctor \ && chown -R root:root ${FLUTTER_HOME} +# Download and pre-cache necessary Flutter artifacts to speed up builds RUN flutter precache diff --git a/build-guide-linux.md b/build-guide-linux.md deleted file mode 100644 index df5f0f601..000000000 --- a/build-guide-linux.md +++ /dev/null @@ -1,176 +0,0 @@ -# Building CakeWallet for Linux - -## Requirements and Setup - -The following are the system requirements to build CakeWallet for your Linux device. - -``` -Ubuntu >= 16.04 -Flutter 3.10.x -``` - -## Building CakeWallet on Linux - -These steps will help you configure and execute a build of CakeWallet from its source code. - -### 1. Installing Package Dependencies - -CakeWallet requires some packages to be installed on your build system. You may easily install them on your build system with the following command: - -`$ sudo apt install build-essential cmake pkg-config git curl autoconf libtool` - -> [!WARNING] -> -> ### Check gcc version -> -> It is needed to use gcc 10 or 9 to successfully link dependencies with flutter.\ -> To check what gcc version you are using: -> -> ```bash -> $ gcc --version -> $ g++ --version -> ``` -> -> If you are using gcc version newer than 10, then you need to downgrade to version 10.4.0: -> -> ```bash -> $ sudo apt install gcc-10 g++-10 -> $ sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 10 -> $ sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-10 10 -> ``` - -> [!NOTE] -> -> Alternatively, you can use the [nix-shell](https://nixos.org/) with the `gcc10.nix` file\ -> present on `scripts/linux` like so: -> ```bash -> $ nix-shell gcc10.nix -> ``` -> This will get you in a nix environment with all the required dependencies that you can use to build the software from,\ -> and it works in any linux distro. - -### 2. Installing Flutter - -Need to install flutter. For this please check section [How to install flutter on Linux](https://docs.flutter.dev/get-started/install/linux). - -### 3. Verify Installations - -Verify that the Flutter has been correctly installed on your system with the following command: - -`$ flutter doctor` - -The output of this command will appear like this, indicating successful installations. If there are problems with your installation, they **must** be corrected before proceeding. - -``` -Doctor summary (to see all details, run flutter doctor -v): -[✓] Flutter (Channel stable, 3.10.x, on Linux, locale en_US.UTF-8) -``` - -### 4. Acquiring the CakeWallet Source Code - -Download CakeWallet source code - -`$ git clone https://github.com/cake-tech/cake_wallet.git --branch linux/password-direct-input` - -Proceed into the source code before proceeding with the next steps: - -`$ cd cake_wallet/scripts/linux/` - -To configure some project properties run: - -`$ ./cakewallet.sh` - -Build the Monero libraries and their dependencies: - -`$ ./build_all.sh` - -Now the dependencies need to be copied into the CakeWallet project with this command: - -`$ ./setup.sh` - -It is now time to change back to the base directory of the CakeWallet source code: - -`$ cd ../../` - -Install Flutter package dependencies with this command: - -`$ flutter pub get` - -> #### If you will get an error like: -> -> ``` -> The plugin `cw_shared_external` requires your app to be migrated to the Android embedding v2. Follow the steps on the migration doc above and re-run -> this command. -> ``` -> -> Then need to config Android project settings. For this open `scripts/android` (`$ cd scripts/android`) directory and run followed commands: -> -> ``` -> $ source ./app_env.sh cakewallet -> $ ./app_config.sh -> $ cd ../.. -> ``` -> -> Then re-configure Linux project again. For this open `scripts/linux` (`$cd scripts/linux`) directory and run: -> `$ ./cakewallet.sh` -> and back to project root directory: -> `$ cd ../..` -> and fetch dependencies again -> `$ flutter pub get` - -Your CakeWallet binary will be built with some specific keys for iterate with 3rd party services. You may generate these secret keys placeholders with the following command: - -`$ dart run tool/generate_new_secrets.dart` - -We will generate mobx models for the project. - -`$ ./model_generator.sh` - -Then we need to generate localization files. - -`$ dart run tool/generate_localization.dart` - -### 5. Build! - -`$ flutter build linux --release` - -Path to executable file will be: - -`build/linux/x64/release/bundle/cake_wallet` - -> ### Troubleshooting -> -> If you got an error while building the application with `$ flutter build linux --release` command, add `-v` argument to the command (`$ flutter build linux -v --release`) to get details.\ -> If you got in flutter build logs: undefined reference to `hid_free_enumeration`, or another error with undefined reference to `hid_*`, then rebuild monero lib without hidapi lib. Check does exists `libhidapi-dev` in your scope and remove it from your scope for build without it. - -# Flatpak - -For package the built application into flatpak you need firstly to install `flatpak` and `flatpak-builder`: - -`$ sudo apt install flatpak flatpak-builder` - -Then need to [add flathub](https://flatpak.org/setup/Ubuntu) (or just `$ flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo`). Then need to install freedesktop runtime and sdk: - -`$ flatpak install flathub org.freedesktop.Platform//22.08 org.freedesktop.Sdk//22.08` - -To build with using of `flatpak-build` directory run next: - -`$ flatpak-builder --force-clean flatpak-build com.cakewallet.CakeWallet.yml` - -And then export bundle: - -`$ flatpak build-export export flatpak-build` - -`$ flatpak build-bundle export cake_wallet.flatpak com.cakewallet.CakeWallet` - -Result file: `cake_wallet.flatpak` should be generated in the current directory. - -For install generated flatpak file use: - -`$ flatpak --user install cake_wallet.flatpak` - -For run the installed application run: - -`$ flatpak run com.cakewallet.CakeWallet` - -Copyright (c) 2023 Cake Technologies LLC. diff --git a/build-guide-win.md b/build-guide-win.md deleted file mode 100644 index 8cfd02c4c..000000000 --- a/build-guide-win.md +++ /dev/null @@ -1,38 +0,0 @@ -# Building CakeWallet for Windows - -## Requirements and Setup - -The following are the system requirements to build CakeWallet for your Windows PC. - -``` -Windows 10 or later (64-bit), x86-64 based -Flutter 3 or above -``` - -## Building CakeWallet on Windows - -These steps will help you configure and execute a build of CakeWallet from its source code. - -### 1. Installing Package Dependencies - -For build CakeWallet windows application from sources you will be needed to have: -> [Install Flutter]Follow installation guide (https://docs.flutter.dev/get-started/install/windows) and install do not miss to dev tools (install https://docs.flutter.dev/get-started/install/windows/desktop#development-tools) which are required for windows desktop development (need to install Git for Windows and Visual Studio 2022). Then install `Desktop development with C++` packages via GUI Visual Studio 2022, or Visual Studio Build Tools 2022 including: `C++ Build Tools core features`, `C++ 2022 Redistributable Update`, `C++ core desktop features`, `MVC v143 - VS 2022 C++ x64/x86 build tools`, `C++ CMake tools for Windows`, `Testing tools core features - Build Tools`, `C++ AddressSanitizer`. -> [Install WSL] for building monero dependencies need to install Windows WSL (https://learn.microsoft.com/en-us/windows/wsl/install) and required packages for WSL (Ubuntu): -`$ sudo apt update ` -`$ sudo apt build-essential cmake gcc-mingw-w64 g++-mingw-w64 autoconf libtool pkg-config` - -### 2. Pull CakeWallet source code - -You can download CakeWallet source code from our [GitHub repository](github.com/cake-tech/cake_wallet) via git by following next command: -`$ git clone https://github.com/cake-tech/cake_wallet.git --branch MrCyjaneK-cyjan-monerodart` -OR you can download it as [Zip archive](https://github.com/cake-tech/cake_wallet/archive/refs/heads/MrCyjaneK-cyjan-monerodart.zip) - -### 3. Build Monero, Monero_c and their dependencies - -For use monero in the application need to build Monero wrapper - Monero_C which will be used by monero.dart package. For that need to run shell (bash - typically same named utility should be available after WSL is enabled in your system) with previously installed WSL, then change current directory to the application project directory with your used shell and then change current directory to `scripts/windows`: `$ cd scripts/windows`. Run build script: `$ ./build_all.sh`. - -### 4. Configure and build CakeWallet application - -To configure the application open directory where you have downloaded or unarchived CakeWallet sources and run `cakewallet.bat`. -Or if you used WSL and have active shell session you can run `$ ./cakewallet.sh` script in `scripts/windows` which will run `cakewallet.bat` in WSL. -After execution of `cakewallet.bat` you should to get `Cake Wallet.zip` in project root directory which will contains `CakeWallet.exe` file and another needed files for run the application. Now you can extract files from `Cake Wallet.zip` archive and run the application. diff --git a/how_to_add_new_wallet_type.md b/docs/NEW_WALLET_TYPES.md similarity index 100% rename from how_to_add_new_wallet_type.md rename to docs/NEW_WALLET_TYPES.md diff --git a/SECURITY.md b/docs/SECURITY.md similarity index 100% rename from SECURITY.md rename to docs/SECURITY.md diff --git a/docs/builds/ANDROID.md b/docs/builds/ANDROID.md new file mode 100644 index 000000000..61d04185e --- /dev/null +++ b/docs/builds/ANDROID.md @@ -0,0 +1,60 @@ +# Building Cake Wallet for Android + +## Requirements and Setup + +As we use Docker with a custom Dockerfile to build Cake Wallet, the only dependency for building Cake on your local host is the Docker Engine. + +You can find the latest instructions for installing Docker on your given OS on the official website: + +- + +NOTE: If building on a Mac with an M-series CPU (arm64), you may encounter segmentation faults when building. If you do, simply retry the build. + +## Building Cake Wallet or Monero.com + +### Using the pre-built builder image + +In order to build the latest version of Cake Wallet, simply run the following: + +```bash +git clone --branch main https://github.com/cake-tech/cake_wallet.git +# NOTE: Replace `main` with the latest release tag available at https://github.com/cake-tech/cake_wallet/releases/latest. +cd cake_wallet +# docker build -t ghcr.io/cake-tech/cake_wallet:main-linux . # Uncomment to build the docker image yourself instead of pulling it from the registry +docker run -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:main-linux bash -x << EOF +set -x -e +pushd scripts/android + source ./app_env.sh cakewallet + # source ./app_env.sh monero.com # Uncomment this line to build monero.com + ./app_config.sh + ./build_monero_all.sh + ./build_mwebd.sh --dont-install +popd +pushd android/app + [[ -f key.jks ]] || keytool -genkey -v -keystore key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias testKey -noprompt -dname "CN=CakeWallet, OU=CakeWallet, O=CakeWallet, L=Florida, S=America, C=USA" -storepass hunter1 -keypass hunter1 +popd +flutter clean +./model_generator.sh +dart run tool/generate_android_key_properties.dart keyAlias=testKey storeFile=key.jks storePassword=hunter1 keyPassword=hunter1 +dart run tool/generate_localization.dart +dart run tool/generate_new_secrets.dart +flutter build apk --release --split-per-abi +EOF +``` + +You should see the command complete with similar output: + +```bash +Running Gradle task 'assembleRelease'... 519.1s +✓ Built build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk (56.3MB) +✓ Built build/app/outputs/flutter-apk/app-arm64-v8a-release.apk (55.8MB) +✓ Built build/app/outputs/flutter-apk/app-x86_64-release.apk (56.4MB) +``` + +Final builds can be found in `build/app/outputs/flutter-apk/` as seen above. + +## Signing builds + +While properly signing builds is outside of the scope of this guide (very few users want or need to run their own built APKs), to learn more about how to sign APKs you can check out the Zeus team's fantastic guide: + +- diff --git a/docs/builds/IOS.md b/docs/builds/IOS.md new file mode 100644 index 000000000..dd75bfed7 --- /dev/null +++ b/docs/builds/IOS.md @@ -0,0 +1,143 @@ +# Building Cake Wallet for iOS + +## Requirements and Setup + +The following are the system requirements to build Cake Wallet for your iOS device. + +```txt +macOS 15.3.1 +Xcode 16.2 +Flutter 3.24.4 +``` + +NOTE: Newer versions of macOS and Xcode may also work, but have not been confirmed to work by the Cake team. + +### 1. Installing dependencies + +For installing dependency tools you can use brew [Install brew](https://brew.sh). + +You may easily install them on your build system with the following command: + +```zsh +brew install automake ccache cmake cocoapods go libtool pkgconfig xz +sudo softwareupdate --install-rosetta --agree-to-license +``` + +### 2. Installing Xcode + +Download and install the latest version of [Xcode](https://developer.apple.com/xcode/) from macOS App Store. + +Run the following to properly initialize Xcode: + +```zsh +sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer +sudo xcodebuild -runFirstLaunch +``` + +To enable iOS build support for Xcode, perform the following: + +1. Open Xcode +2. Navigate to settings +3. Open Components tab +4. Click "Get" next to iOS 18.2 (or any other version that is showing up as default) + +### 3. Installing Flutter + +Install Flutter, specifically version `3.24.4` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). + +NOTE: as `3.24.4` is not the latest version, you'll need to download it from instead of the link in the docs above. + +### 4. Installing Rust + +Install Rust from the [rustup.rs](https://rustup.rs/) website. + +```zsh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +### 5. Verify Flutter and Xcode installation + +Verify that Flutter and Xcode have been correctly installed on your system with the following command: + +`flutter doctor` + +The output of this command should appear like this, indicating successful installations. If there are problems with your installation, they **must** be corrected before proceeding. + +```zsh +Doctor summary (to see all details, run flutter doctor -v): +[✓] Flutter (Channel stable, 3.24.4, on macOS 15.x.x) +[✓] Xcode - develop for iOS and macOS (Xcode 16.2) +``` + +### 6. Acquiring the Cake Wallet source code + +Download the latest release tag of Cake Wallet and enter the source code directory: + +```zsh +git clone https://github.com/cake-tech/cake_wallet.git --branch main +cd cake_wallet/scripts/ios/ +``` + +NOTE: Replace `main` with the latest release tag available at . + +### 7. Setup and build Cake Wallet from source + +We need to generate project settings like app name, app icon, package name, etc, including what specific variant of the app we want to build. + +To build Cake Wallet from source, run the following: + +```zsh +source ./app_env.sh cakewallet +``` + +For Monero.com, instead do: + +```zsh +source ./app_env.sh monero.com +``` + +Build the necessary libraries and their dependencies: + +```zsh +./build_monero_all.sh +./build_mwebd.sh +``` + +NOTE: This step will take quite a while, so be sure you grab a cup of coffee or a good book! + +Then run the configuration script to setup app name, app icon, etc: + +```zsh +./app_config.sh +``` + +### 8. Prepare Flutter + +Change back to the root directory of the Cake Wallet source code and install Flutter package dependencies: + +```zsh +cd ../../ +flutter pub get +``` + +Generate secrets as placeholders for official API keys etc. along with localization files and mobx models: + +```zsh +dart run tool/generate_new_secrets.dart +dart run tool/generate_localization.dart +./model_generator.sh +``` + +### 9. Build + +```zsh +flutter build ios --release --no-codesign +``` + +Then you can open `ios/Runner.xcworkspace` with Xcode to archive the application. + +If you want to run on a connected device, simply run: + +```zsh +flutter run +``` diff --git a/docs/builds/LINUX.md b/docs/builds/LINUX.md new file mode 100644 index 000000000..cd8466e6d --- /dev/null +++ b/docs/builds/LINUX.md @@ -0,0 +1,96 @@ +# Building Cake Wallet for Linux + +## Requirements and Setup + +As we use Docker with a custom Dockerfile to build Cake Wallet, the only dependency for building Cake on your local host is the Docker Engine. + +You can find the latest instructions for installing Docker on your given OS on the official website: + +- + +NOTE: If building on a Mac with an M-series CPU (arm64), you may encounter segmentation faults when building. If you do, simply retry the build. + +## Building Cake Wallet or Monero.com + +### Using the pre-built builder image + +In order to build the latest version of Cake Wallet, simply run the following: + +```bash +git clone --branch main https://github.com/cake-tech/cake_wallet.git +# NOTE: Replace `main` with the latest release tag available at https://github.com/cake-tech/cake_wallet/releases/latest. +cd cake_wallet +# docker build -t ghcr.io/cake-tech/cake_wallet:main-linux . # Uncomment to build the docker image yourself instead of pulling it from the registry +docker run -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:main-linux bash -x << EOF +set -x -e +pushd scripts + ./gen_android_manifest.sh +popd +pushd scripts/linux + source ./app_env.sh cakewallet + # source ./app_env.sh monero.com # Uncomment this line to build monero.com + ./app_config.sh + ./build_monero_all.sh +popd +flutter clean +./model_generator.sh +dart run tool/generate_localization.dart +dart run tool/generate_new_secrets.dart +flutter build linux +EOF +``` + +You should see the command complete with similar output: + +```bash ++ dart run tool/generate_localization.dart ++ dart run tool/generate_new_secrets.dart ++ flutter build linux + +Building Linux application... +✓ Built build/linux/x64/release/bundle/cake_wallet +``` + +Final builds can be found in `build/linux/x64/release/bundle/` as seen above. + + +## Flatpak (optional) + +To package the built binaries as a flatpak, you need first to install `flatpak` and `flatpak-builder`: + +```bash +sudo apt install flatpak flatpak-builder +``` + +Add the necessary Flathub: + +```bash +flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo +``` + +Then need to install freedesktop runtime and sdk: + +```bash +flatpak install flathub org.freedesktop.Platform//22.08 org.freedesktop.Sdk//22.08 +``` + +Next, build the flatpak bundle: + +```bash +flatpak-builder --force-clean flatpak-build com.cakewallet.CakeWallet.yml +``` + +And then export bundle: + +```bash +flatpak build-export export flatpak-build +flatpak build-bundle export cake_wallet.flatpak com.cakewallet.CakeWallet +``` + +The Flatpak file, `cake_wallet.flatpak`, should be generated in the current directory. + +To install the newly built Flatpak, run: + +```bash +flatpak --user install cake_wallet.flatpak +``` diff --git a/docs/builds/MACOS.md b/docs/builds/MACOS.md new file mode 100644 index 000000000..af3d8c1df --- /dev/null +++ b/docs/builds/MACOS.md @@ -0,0 +1,135 @@ +# Building Cake Wallet for macOS + +## Requirements and Setup + +The following are the system requirements to build Cake Wallet for your macOS device. + +```txt +macOS 15.3.1 +Xcode 16.2 +Flutter 3.24.4 +``` + +### 1. Installing dependencies + +For installing dependency tools you can use brew [Install brew](https://brew.sh). + +You may easily install them on your build system with the following command: + +```zsh +brew install autoconf automake binutils ccache cmake cocoapods go libtool pigz pkg-config +sudo softwareupdate --install-rosetta --agree-to-license +``` + +### 2. Installing Xcode + +Download and install the latest version of [Xcode](https://developer.apple.com/xcode/) from macOS App Store. + +Run the following to properly initialize Xcode: + +```zsh +sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer +sudo xcodebuild -runFirstLaunch +``` + +### 3. Installing Flutter + +Install Flutter, specifically version `3.24.4` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). + +NOTE: as `3.24.4` is not the latest version, you'll need to download it from instead of the link in the docs above. + +### 4. Installing Rust + +Install Rust from the [rustup.rs](https://rustup.rs/) website. + +```zsh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +### 5. Verify Flutter and Xcode installation + +Verify that Flutter and Xcode have been correctly installed on your system with the following command: + +`flutter doctor` + +The output of this command should appear like this, indicating successful installations. If there are problems with your installation of Flutter or Xcode, they **must** be corrected before proceeding. + +```zsh +Doctor summary (to see all details, run flutter doctor -v): +[✓] Flutter (Channel stable, 3.24.4, on macOS 15.x.x) +... +[✓] Xcode - develop for iOS and macOS (Xcode 16.2) +... +``` + +### 6. Acquiring the Cake Wallet source code + +Download the latest release tag of Cake Wallet and enter the source code directory: + +```zsh +git clone https://github.com/cake-tech/cake_wallet.git --branch main +cd cake_wallet/scripts/macos/ +``` + +NOTE: Replace `main` with the latest release tag available at . + +### 7. Setup and build Cake Wallet from source + +We need to generate project settings like app name, app icon, package name, etc, including what specific variant of the app we want to build. + +To build Cake Wallet from source, run the following: + +```zsh +source ./app_env.sh cakewallet +``` + +For Monero.com, instead do: + +```zsh +source ./app_env.sh monero.com +``` + +Build the necessary libraries and their dependencies: + +```zsh +./build_monero_all.sh +``` + +NOTE: This step will take quite a while, so be sure you grab a cup of coffee or a good book! + +Then run the configuration script to setup app name, app icon, etc: + +```zsh +./app_config.sh +``` + +### 8. Prepare Flutter + +Change back to the root directory of the Cake Wallet source code and install Flutter package dependencies: + +```zsh +cd ../../ +flutter pub get +``` + +Generate secrets as placeholders for official API keys etc. along with localization files and mobx models: + +```zsh +dart run tool/generate_new_secrets.dart +dart run tool/generate_localization.dart +./model_generator.sh +``` + +### 9. Build + +```zsh +flutter build macos --release +``` + +Then you can open `macos/Runner.xcworkspace` with Xcode to archive the application. + +If you want to run on a connected device, simply run: + +```zsh +flutter run +``` diff --git a/docs/builds/WINDOWS.md b/docs/builds/WINDOWS.md new file mode 100644 index 000000000..1b5d7a0e8 --- /dev/null +++ b/docs/builds/WINDOWS.md @@ -0,0 +1,92 @@ +# Building Cake Wallet for Windows + +## Requirements and Setup + +The following are the system requirements to build Cake Wallet for your Windows PC. + +```txt +Windows 10 or later (64-bit), x86-64 based +Flutter 3.24.4 +``` + +### 1. Installing Flutter + +Install Flutter, specifically version `3.24.4` by following the [official docs](https://docs.flutter.dev/get-started/install/windows). + +In order for Flutter to function, you'll also need to enable Developer Mode: + +Start Menu > search for "Run" > type `ms-settings:developers`, and turn on Developer Mode. + +NOTE: as `3.24.4` is not the latest version, you'll need to download it from instead of the link in the docs above. + +### 2. Install Development Tools + +Install Git for Windows and Visual Studio 2022: + +1. Follow the [Development Tools](https://docs.flutter.dev/get-started/install/windows/desktop#development-tools) installation instructions + 1. NOTE: Be sure to install the `Desktop Development with C++` workload in Visual Studio as outlined in the docs. +2. Add `git` to your path by going to Start Menu > search "environment" > Environment Variables > double-click Path > Add `C:\Program Files\Git\bin\` on a new line. + +Lastly, you'll need to install Nuget separately: + +1. Download the exe from +2. Create a new directory, `C:\Program Files\Nuget\` +3. Move or copy the `nuget.exe` binary you just downloaded into the newly created directory above. +4. Add `nuget` to your path by going to Start Menu > search "environment" > Environment Variables > double-click Path > Add `C:\Program Files\Nuget\` on a new line. + +### 3. Installing WSL (Windows Subsystem for Linux) + +For building Monero dependencies, it is required to install Windows [WSL](https://learn.microsoft.com/en-us/windows/wsl) and required packages for WSL (Ubuntu). + +1. Open a Powershell window by going to the Start Menu and searching for "Powershell" +2. Install WSL with the command `wsl --install` +3. Install the necessary Ubuntu dependencies + +```powershell +wsl --install +wsl sudo apt update +wsl sudo apt install -y autoconf build-essential ccache cmake curl gcc gcc-mingw-w64-x86-64 git g++ g++-mingw-w64-x86-64 gperf lbzip2 libtool make pkg-config pigz +``` + +### 4. Installing Rust + +Install Rust and other Rust-related dependencies using [rustup.rs](https://rustup.rs/#) by running the following command: + +```bash +wsl curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +### 5. Acquiring the Cake Wallet source code + +Download the latest release tag of Cake Wallet and enter the source code directory: + +```powershell +git clone https://github.com/cake-tech/cake_wallet.git --branch main +cd cake_wallet +``` + +NOTE: Replace `main` with the latest release tag available at . + +### 6. Build Monero, monero_c, and dependencies + +To use Monero in Cake Wallet, you must build the Monero_C wrapper which will be used by monero.dart package. + +Run the following in a WSL terminal window (set the Git username and email as desired): + +```powershell +wsl +git config --global user.email "builds@cakewallet.com" +git config --global user.name "builds" +./build_all.sh +``` + +### 7. Configure and build Cake Wallet application + +To configure the application, run the following: + +```powershell +exit +.\cakewallet.bat +``` + +After running the script above, you should get `Cake Wallet.zip` in the project's root directory which will contain `CakeWallet.exe` and other needed files for running the application. Now you can extract files from `Cake Wallet.zip` archive and run the application. diff --git a/howto-build-android.md b/howto-build-android.md deleted file mode 100644 index 5afecfba3..000000000 --- a/howto-build-android.md +++ /dev/null @@ -1,149 +0,0 @@ -# Building Cake Wallet for Android - -## Requirements and Setup - -The following are the system requirements to build Cake Wallet for your Android device. - -``` -Ubuntu >= 20.04 -Android SDK 29 or higher (better to have the latest one 33) -Android NDK 17c -Flutter 3.24.4 -``` - -### 1. Installing Package Dependencies - -CakeWallet cannot be built without the following packages installed on your system. - -- curl - -- unzip - -- automake - -- build-essential - -- file - -- pkg-config - -- git - -- python - -- libtool - -- libtinfo5 - -- cmake - -- openjdk-8-jre-headless - -- clang - -You may easily install them on your build system with the following command: - -`$ sudo apt-get install -y curl unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake openjdk-8-jre-headless clang` - -### 2. Installing Android Studio and Android toolchain - -You may download and install the latest version of Android Studio [here](https://developer.android.com/studio#downloads). After installing, start Android Studio, and go through the "Setup Wizard." This installs the latest Android SDK, Android SDK Command-line Tools, and Android SDK Build-Tools, which are required by Cake Wallet. **Be sure you are installing SDK version 28 or later when stepping through the wizard** - -### 3. Installing Flutter - -Install Flutter with version `3.24.4`. For this please check section [Install Flutter manually](https://docs.flutter.dev/get-started/install/linux#install-flutter-manually). - -### 4. Installing rustup - -Install rustup from the [rustup.rs](https://rustup.rs/) website. - -### 5. Verify Installations - -Verify that the Android toolchain, Flutter, and Android Studio have been correctly installed on your system with the following command: - -`$ flutter doctor` - -The output of this command will appear like this, indicating successful installations. If there are problems with your installation, they **must** be corrected before proceeding. -``` -Doctor summary (to see all details, run flutter doctor -v): -[✓] Flutter (Channel stable, 3.24.4, on Linux, locale en_US.UTF-8) -[✓] Android toolchain - develop for Android devices (Android SDK version 29 or higher) -[✓] Android Studio (version 4.0 or higher) -``` - -### 6. Generate a secure keystore for Android - -`$ keytool -genkey -v -keystore $HOME/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key` - -You will be prompted to create two passwords. First you will be prompted for the "store password", followed by a "key password" towards the end of the creation process. **TAKE NOTE OF THESE PASSWORDS!** You will need them in later steps. - -### 7. Acquiring the Cake Wallet Source Code - -Create the directory that will be use to store the Cake Wallet source... - -``` -$ sudo mkdir -p /opt/android -$ sudo chown $USER /opt/android -$ cd /opt/android -``` - -..and download the source code into that directory. - -`$ git clone https://github.com/cake-tech/cake_wallet.git --branch main` - -Proceed into the source code before proceeding with the next steps: - -`$ cd cake_wallet/scripts/android/` - -### 8. Installing Android NDK - -`$ ./install_ndk.sh` - -### 9. Execute Build & Setup Commands for Cak eWallet - -We need to generate project settings like app name, app icon, package name, etc. For this need to setup environment variables and configure project files. - -Please pick what app you want to build: cakewallet or monero.com. - -`$ source ./app_env.sh ` -(it should be like `$ source ./app_env.sh cakewallet` or `$ source ./app_env.sh monero.com`) - -Then run configuration script for setup app name, app icon and etc: - -`$ ./app_config.sh` - -Build the Monero libraries and their dependencies: - -`$ ./build_all.sh` - -It is now time to change back to the base directory of the Cake Wallet source code: - -`$ cd ../../` - -Install Flutter package dependencies with this command: - -`$ flutter pub get` - -Your Cake Wallet binary will be built with cryptographic salts, which are used for secure encryption of your data. You may generate these secret salts with the following command: - -`$ dart run tool/generate_new_secrets.dart` - -Next, we must generate key properties based on the secure keystore you generated for Android (in step 5). **MODIFY THE FOLLOWING COMMAND** with the "store password" and "key password" you assigned when creating your keystore (in step 5). - -`$ dart run tool/generate_android_key_properties.dart keyAlias=key storeFile=$HOME/key.jks storePassword= keyPassword=` - -**REMINDER:** The *above* command will **not** succeed unless you replaced the `storePassword` and `keyPassword` variables with the correct passwords for your keystore. - -Then we need to generate localization files. - -`$ dart run tool/generate_localization.dart` - -Finally build mobx models for the app: - -`$ ./model_generator.sh` - -### 10. Build! - -`$ flutter build apk --release` - -Copyright (c) 2024 Cake Labs LLC diff --git a/howto-build-ios.md b/howto-build-ios.md deleted file mode 100644 index 753e17e93..000000000 --- a/howto-build-ios.md +++ /dev/null @@ -1,101 +0,0 @@ -# Building Cake Wallet for iOS - -## Requirements and Setup - -The following are the system requirements to build Cake Wallet for your iOS device. - -``` -macOS >= 14.0 -Xcode 15.3 -Flutter 3.24.4 -``` - -### 1. Installing Package Dependencies - -Cake Wallet cannot be built without the following packages installed on your build system. - -For installing dependency tools you can use brew [Install brew](https://brew.sh). - -You may easily install them on your build system with the following command: - -`$ brew install cmake xz cocoapods` - -### 2. Installing Xcode - -You may download and install the latest version of [Xcode](https://developer.apple.com/xcode/) from macOS App Store. - -### 3. Installing Flutter - -Need to install flutter with version `3.24.4`. For this please check section [Install Flutter](https://docs.flutter.dev/get-started/install/macos/mobile-ios?tab=download). - -### 4. Installing rustup - -Install rustup from the [rustup.rs](https://rustup.rs/) website. - -### 5. Verify Installations - -Verify that the Flutter and Xcode have been correctly installed on your system with the following command: - -`$ flutter doctor` - -The output of this command will appear like this, indicating successful installations. If there are problems with your installation, they **must** be corrected before proceeding. -``` -Doctor summary (to see all details, run flutter doctor -v): -[✓] Flutter (Channel stable, 3.24.4, on macOS 14.x.x) -[✓] Xcode - develop for iOS and macOS (Xcode 15.3) -``` - -### 6. Acquiring the CakeWallet source code - -Download the source code. - -`$ git clone https://github.com/cake-tech/cake_wallet.git --branch main` - -Proceed into the source code before proceeding with the next steps: - -`$ cd cake_wallet/scripts/ios/` - -### 7. Execute Build & Setup Commands for Cake Wallet - -We need to generate project settings like app name, app icon, package name, etc. For this, we need to setup environment variables and configure project files. - -Please pick what app you want to build: cakewallet or monero.com. - -`$ source ./app_env.sh ` -(it should be like `$ source ./app_env.sh cakewallet` or `$ source ./app_env.sh monero.com`) - -Then run configuration script for setup app name, app icon and etc: - -`$ ./app_config.sh` - -Build the Monero libraries and their dependencies: - -`$ ./build_monero_all.sh` - -It is now time to change back to the base directory of the Cake Wallet source code: - -`$ cd ../../` - -Install Flutter package dependencies with this command: - -`$ flutter pub get` - -Your Cake Wallet binary will be built with cryptographic salts, which are used for secure encryption of your data. You may generate these secret salts with the following command: - -`$ dart run tool/generate_new_secrets.dart` - -Then we need to generate localization files and mobx models. - -`$ ./configure_cake_wallet.sh ios` - -### 8. Build! - -`$ flutter build ios --release` - -Then you can open `ios/Runner.xcworkspace` with Xcode and you can archive the application. - -Or if you want to run to connected device: - -`$ flutter run --release` - -Copyright (c) 2024 Cake Labs LLC diff --git a/howto-build-macos.md b/howto-build-macos.md deleted file mode 100644 index a497e1ffa..000000000 --- a/howto-build-macos.md +++ /dev/null @@ -1,112 +0,0 @@ -# Building Cake Wallet for macOS - -## Requirements and Setup - -The following are the system requirements to build Cake Wallet for your macOS device. - -``` -macOS >= 14.0 -Xcode 15.3 -Flutter 3.24.4 -``` - -### 1. Installing Package Dependencies - -Cake Wallet cannot be built without the following packages installed on your build system. - -For installing dependency tools you can use brew [Install brew](https://brew.sh). - -You may easily install them on your build system with the following command: - -`$ brew install cmake xz automake autoconf libtool boost@1.76 zmq cocoapods` - -`$ brew link boost@1.76` - -### 2. Installing Xcode - -You may download and install the latest version of [Xcode](https://developer.apple.com/xcode/) from macOS App Store. - -### 3. Installing Flutter - -Need to install flutter with version `3.24.4`. For this please check section [Install Flutter](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). - -### 4. Installing rustup - -Install rustup from the [rustup.rs](https://rustup.rs/) website. - -### 5. Verify Installations - -Verify that Flutter and Xcode have been correctly installed on your system with the following command: - -`$ flutter doctor` - -The output of this command will appear like this, indicating successful installations. If there are problems with your installation, they **must** be corrected before proceeding. -``` -Doctor summary (to see all details, run flutter doctor -v): -[✓] Flutter (Channel stable, 3.24.4, on macOS 14.x.x) -[✓] Xcode - develop for iOS and macOS (Xcode 15.3) -``` - -### 6. Acquiring the Cake Wallet source code - -Download the source code. - -`$ git clone https://github.com/cake-tech/cake_wallet.git --branch main` - -Proceed into the source code before proceeding with the next steps: - -`$ cd cake_wallet/scripts/macos/` - -### 7. Execute Build & Setup Commands for Cake Wallet - -We need to generate project settings like app name, app icon, package name, etc. For this need to setup environment variables and configure project files. - -Please pick what app you want to build: cakewallet or monero.com. - -`$ source ./app_env.sh ` -(it should be like `$ source ./app_env.sh cakewallet` or `$ source ./app_env.sh monero.com`) - -Then run configuration script for setup app name, app icon and etc: - -`$ ./app_config.sh` - -Build the Monero libraries and their dependencies: - -`$ ./build_monero_all.sh` - -If you be needed to build universal monero lib, then it will require additional steps. Steps for build universal monero lib on mac with Apple Silicon (arm64): - -- Need to install Rosetta: `$ softwareupdate --install-rosetta` -- Need to install [Brew](https://brew.sh/) with rosetta: `$ arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"` (or take another way to install brew, but be use that you have installed it into /usr/local as it's using for x86_64 macs) -- Install dependencies for build monero wallet lib for x86_64 with brew: `$ arch -x86_64 /usr/local/bin/brew install automake autoconf libtool openssl boost@1.76 zmq` and link installed boost@1.76 for x86_64 `$ arch -x86_64 /usr/local/bin/brew link boost@1.76` -- Run building script with additional argument: `$ ./build_monero_all.sh universal` - -If you will be needed to build monero wallet lib only for x86_64 on arm64 mac, then you need use steps above, but run build script with rosetta without arguments: `$ arch -x86_64 ./build_monero_all.sh`. - -It is now time to change back to the base directory of the Cake Wallet source code: - -`$ cd ../../` - -Install Flutter package dependencies with this command: - -`$ flutter pub get` - -Your Cake Wallet binary will be built with cryptographic salts, which are used for secure encryption of your data. You may generate these secret salts with the following command: - -`$ dart run tool/generate_new_secrets.dart` - -Then we need to generate localization files and mobx models. - -`$ ./configure_cake_wallet.sh macos` - -### 8. Build! - -`$ flutter build macos --release` - -Then you can open `macos/Runner.xcworkspace` with Xcode and you can to archive the application. - -Or if you want to run to connected device: - -`$ flutter run --release` - -Copyright (c) 2024 Cake Labs LLC diff --git a/howto-build-windows.md b/howto-build-windows.md deleted file mode 100644 index 3ebecaa61..000000000 --- a/howto-build-windows.md +++ /dev/null @@ -1,57 +0,0 @@ -# Building Cake Wallet for Windows - -## Requirements and Setup - -The following are the system requirements to build CakeWallet for your Windows PC. - -``` -Windows 10 or later (64-bit), x86-64 based -Flutter 3.24.4 -``` - -### 1. Installing Flutter - -Install Flutter with version `3.24.4`. Follow the Flutter [installation guide](https://docs.flutter.dev/get-started/install/windows). - -### 2. Install Development Tools - -Install Git for Windows and Visual Studio 2022. Follow the [Development Tools](https://docs.flutter.dev/get-started/install/windows/desktop#development-tools) installation instructions. - -Then install `Desktop development with C++` packages via Visual Studio 2022, or Visual Studio Build Tools 2022 including: -- `C++ Build Tools core features` -- `C++ 2022 Redistributable Update` -- `C++ core desktop features` -- `MVC v143 - VS 2022 C++ x64/x86 build tools` -- `C++ CMake tools for Windows` -- `Testing tools core features - Build Tools` -- `C++ AddressSanitizer`. - -### 3. Installing rustup - -Install rustup from the [rustup.rs](https://rustup.rs/#) website. Download and run the 64-bit rustup-init.exe - -### 4. Installing WSL (Windows Subsystem for Linux) - -For building monero dependencies, it is required to install Windows WSL (https://learn.microsoft.com/en-us/windows/wsl/install) and required packages for WSL (Ubuntu): -`$ sudo apt update ` -`$ sudo apt build-essential cmake gcc-mingw-w64 g++-mingw-w64 autoconf libtool pkg-config` - -### 5. Pull Cake Wallet source code - -You can download CakeWallet source code from our [GitHub repository](github.com/cake-tech/cake_wallet) via git: -`$ git clone https://github.com/cake-tech/cake_wallet.git --branch MrCyjaneK-cyjan-monerodart` -OR you can download it as [Zip archive](https://github.com/cake-tech/cake_wallet/archive/refs/heads/MrCyjaneK-cyjan-monerodart.zip) - -### 6. Build Monero, monero_c and their dependencies - -To use Monero in Cake Wallet, you must build the Monero_C wrapper which will be used by monero.dart package. - -For that you need to run the shell (bash - typically same named utility should be available after WSL is enabled in your system) with the previously installed WSL install, then change current directory to the application project directory with your shell then change current directory to `scripts/windows`: `$ cd scripts/windows`. Run build script: `$ ./build_all.sh`. - -### 7. Configure and build Cake Wallet application - -To configure the application, open the directory where you have downloaded or unarchived Cake Wallet sources and run `cakewallet.bat`. -Or if you used WSL and have active shell session you can run `$ ./cakewallet.sh` script in `scripts/windows` which will run `cakewallet.bat` in WSL. -After execution of `cakewallet.bat` you should to get `Cake Wallet.zip` in project root directory which will contain `CakeWallet.exe` file and another needed files for run the application. Now you can extract files from `Cake Wallet.zip` archive and run the application. - -Copyright (c) 2024 Cake Labs LLC. diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 0b6f32fd6..a87e938f3 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -119,9 +119,6 @@ install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/monero/${ install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/wownero/${LIB_TRIPLET}_libwallet2_api_c.so" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "wownero_libwallet2_api_c.so" COMPONENT Runtime) -install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/zano/${LIB_TRIPLET}_libwallet2_api_c.so" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "zano_libwallet2_api_c.so" - COMPONENT Runtime) - install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" COMPONENT Runtime) diff --git a/scripts/android/install_ndk.sh b/scripts/android/install_ndk.sh index ea131eb39..d6799dbd8 100755 --- a/scripts/android/install_ndk.sh +++ b/scripts/android/install_ndk.sh @@ -6,9 +6,9 @@ TOOLCHAIN_A32_DIR=${TOOLCHAIN_DIR}_aarch TOOLCHAIN_A64_DIR=${TOOLCHAIN_DIR}_aarch64 TOOLCHAIN_x86_DIR=${TOOLCHAIN_DIR}_i686 TOOLCHAIN_x86_64_DIR=${TOOLCHAIN_DIR}_x86_64 -ANDROID_NDK_SHA256="3f541adbd0330a9205ba12697f6d04ec90752c53d6b622101a2a8a856e816589" +ANDROID_NDK_SHA256="7a1302d9bfbc37d46be90b2285f4737508ffe08a346cf2424c5c6a744de2db22" - curl https://dl.google.com/android/repository/android-ndk-r17c-linux-x86_64.zip -o ${ANDROID_NDK_ZIP} + curl https://dl.google.com/android/repository/android-ndk-r27c-linux.zip -o ${ANDROID_NDK_ZIP} echo $ANDROID_NDK_SHA256 $ANDROID_NDK_ZIP | sha256sum -c || exit 1 unzip $ANDROID_NDK_ZIP -d $WORKDIR diff --git a/scripts/linux/build_monero_all.sh b/scripts/linux/build_monero_all.sh index 7113d88ef..7948d87a1 100755 --- a/scripts/linux/build_monero_all.sh +++ b/scripts/linux/build_monero_all.sh @@ -7,7 +7,7 @@ cd "$(dirname "$0")" ../prepare_moneroc.sh -for COIN in monero wownero zano; +for COIN in monero wownero; do pushd ../monero_c for target in x86_64-linux-gnu # aarch64-linux-gnu From c5f5d1dd4dc93b31b516b80e6b678e7f02ea0715 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Mon, 10 Mar 2025 11:49:16 +0200 Subject: [PATCH 030/243] fix edit token page minor fixes --- lib/src/screens/dashboard/dashboard_page.dart | 7 ++----- .../settings/widgets/settings_switcher_cell.dart | 11 ++++++++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index f219409da..cf1f6fa17 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -187,7 +187,6 @@ class _DashboardPageView extends BasePage { int get initialPage => dashboardViewModel.shouldShowMarketPlaceInDashboard ? 1 : 0; ObservableList pages = ObservableList(); bool _isEffectsInstalled = false; - StreamSubscription? _onInactiveSub; @override Widget body(BuildContext context) { @@ -275,7 +274,7 @@ class _DashboardPageView extends BasePage { } void _setEffects(BuildContext context) async { - if (_isEffectsInstalled) { + if (_isEffectsInstalled || !context.mounted) { return; } if (dashboardViewModel.shouldShowMarketPlaceInDashboard) { @@ -305,11 +304,9 @@ class _DashboardPageView extends BasePage { _showHavenPopup(context); var needToPresentYat = false; - var isInactive = false; - _onInactiveSub = rootKey.currentState?.isInactive.listen( + rootKey.currentState?.isInactive.listen( (inactive) { - isInactive = inactive; if (needToPresentYat) { Future.delayed(Duration(milliseconds: 500)).then( diff --git a/lib/src/screens/settings/widgets/settings_switcher_cell.dart b/lib/src/screens/settings/widgets/settings_switcher_cell.dart index bc3421ead..6173cb34d 100644 --- a/lib/src/screens/settings/widgets/settings_switcher_cell.dart +++ b/lib/src/screens/settings/widgets/settings_switcher_cell.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:cake_wallet/src/widgets/standard_list.dart'; import 'package:cake_wallet/src/widgets/standard_switch.dart'; import 'package:flutter/material.dart'; @@ -30,7 +29,13 @@ class SettingsSwitcherCell extends StandardListRow { height: 56, padding: EdgeInsets.only(left: 12, right: 12), child: TextButton( - onPressed: () => onValueChange?.call(context, !value), + onPressed: () { + if (onTap != null) { + onTap!.call(context); + } else { + onValueChange?.call(context, !value); + } + }, style: ButtonStyle( //backgroundColor: MaterialStateProperty.all(Theme.of(context).cardColor), shape: MaterialStateProperty.all( @@ -45,7 +50,7 @@ class SettingsSwitcherCell extends StandardListRow { children: [ if (leading != null) leading, buildCenter(context, hasLeftOffset: leading != null), - if (trailing != null) trailing, + trailing, ], ), ), From 1e4dbb5bc9a9e882d1ce0dbbc0c447fbe9d131fe Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Mon, 10 Mar 2025 14:13:21 +0200 Subject: [PATCH 031/243] Ignore packet loss errors [skip ci] --- lib/utils/exception_handler.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index 17e6daed3..547ffa571 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -217,6 +217,7 @@ class ExceptionHandler { "invalid signature", "invalid password", "NetworkImage._loadAsync", + "SSLV3_ALERT_BAD_RECORD_MAC", // Temporary ignored, More context: Flutter secure storage reads the values as null some times // probably when the device was locked and then opened on Cake // this is solved by a restart of the app From 1c8af1afae756306ca68a7c91fca6a8064812ee0 Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Mon, 10 Mar 2025 16:37:23 -0700 Subject: [PATCH 032/243] Mweb checkbox (#2000) * [skip-ci] wip * [skip-ci] styles still need updating * working but needs style updates * fix checkbox caption color * sort mweb coins to be last when selecting inputs * ui fixes * [skip-ci] default to mweb-checkbox being off * adaptable page view builder + workaround for keyboard actions * Fix checkbox themeing and send card sizing * Update lib/src/screens/send/widgets/send_card.dart --------- Co-authored-by: tuxpizza Co-authored-by: Omar Hatem --- cw_bitcoin/lib/electrum_wallet.dart | 4 +- lib/di.dart | 2 +- lib/src/screens/send/send_page.dart | 594 ++++++++++--------- lib/src/screens/send/widgets/send_card.dart | 620 +++++++++++--------- lib/src/widgets/adaptable_page_view.dart | 202 +++++++ lib/src/widgets/standard_checkbox.dart | 4 +- lib/view_model/send/send_view_model.dart | 12 +- 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_fr.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_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 + 35 files changed, 885 insertions(+), 581 deletions(-) create mode 100644 lib/src/widgets/adaptable_page_view.dart diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 64595b253..fd778571f 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -632,8 +632,8 @@ abstract class ElectrumWalletBase }).toList(); final unconfirmedCoins = availableInputs.where((utx) => utx.confirmations == 0).toList(); - // sort the unconfirmed coins so that mweb coins are first: - availableInputs.sort((a, b) => a.bitcoinAddressRecord.type == SegwitAddresType.mweb ? -1 : 1); + // sort the unconfirmed coins so that mweb coins are last: + availableInputs.sort((a, b) => a.bitcoinAddressRecord.type == SegwitAddresType.mweb ? 1 : -1); for (int i = 0; i < availableInputs.length; i++) { final utx = availableInputs[i]; diff --git a/lib/di.dart b/lib/di.dart index 83b3efaea..ccd0908d1 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -748,7 +748,7 @@ Future setup({ getIt.get(), _transactionDescriptionBox, getIt.get().wallet!.isHardwareWallet ? getIt.get() : null, - coinTypeToSpendFrom: coinTypeToSpendFrom ?? UnspentCoinType.any, + coinTypeToSpendFrom: coinTypeToSpendFrom ?? UnspentCoinType.nonMweb, getIt.get(param1: coinTypeToSpendFrom), ), ); diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index d3c741c0c..7e7080d0f 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -14,14 +14,17 @@ import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart' import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart'; import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart'; import 'package:cake_wallet/src/screens/send/widgets/send_card.dart'; +import 'package:cake_wallet/src/widgets/adaptable_page_view.dart'; import 'package:cake_wallet/src/widgets/add_template_button.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/template_tile.dart'; import 'package:cake_wallet/src/widgets/trail_button.dart'; +import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; import 'package:cake_wallet/themes/extensions/seed_widget_theme.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; import 'package:cake_wallet/themes/theme_base.dart'; @@ -38,6 +41,7 @@ import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:keyboard_actions/keyboard_actions.dart'; import 'package:mobx/mobx.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -93,7 +97,7 @@ class SendPage extends BasePage { return MergeSemantics( child: SizedBox( height: isMobileView ? 37 : 45, - width: isMobileView ? 37 : 45, + width: isMobileView ? 47: 45, child: ButtonTheme( minWidth: double.minPositive, child: Semantics( @@ -114,18 +118,6 @@ class SendPage extends BasePage { @override AppBarStyle get appBarStyle => AppBarStyle.transparent; - double _sendCardHeight(BuildContext context) { - double initialHeight = 480; - if (sendViewModel.hasCoinControl) { - initialHeight += 55; - } - - if (!responsiveLayoutUtil.shouldRenderMobileUI) { - return initialHeight - 66; - } - return initialHeight; - } - @override void onClose(BuildContext context) { sendViewModel.onClose(); @@ -174,285 +166,316 @@ class SendPage extends BasePage { Widget body(BuildContext context) { _setEffects(context); - return GestureDetector( - onLongPress: () => - sendViewModel.balanceViewModel.isReversing = !sendViewModel.balanceViewModel.isReversing, - onLongPressUp: () => - sendViewModel.balanceViewModel.isReversing = !sendViewModel.balanceViewModel.isReversing, - child: Form( - key: _formKey, - child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.only(bottom: 24), - content: FocusTraversalGroup( - policy: OrderedTraversalPolicy(), - child: Column( - children: [ - Container( - height: _sendCardHeight(context), - child: Observer( - builder: (_) { - return PageView.builder( - scrollDirection: Axis.horizontal, - controller: controller, - itemCount: sendViewModel.outputs.length, - itemBuilder: (context, index) { - final output = sendViewModel.outputs[index]; + return Observer(builder: (_) { + List sendCards = []; + List keyboardActions = []; + for (var output in sendViewModel.outputs) { + var cryptoAmountFocus = FocusNode(); + var fiatAmountFocus = FocusNode(); + sendCards.add(SendCard( + currentTheme: currentTheme, + key: output.key, + output: output, + sendViewModel: sendViewModel, + initialPaymentRequest: initialPaymentRequest, + cryptoAmountFocus: cryptoAmountFocus, + fiatAmountFocus: fiatAmountFocus, + )); + keyboardActions.add(KeyboardActionsItem( + focusNode: cryptoAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()])); + keyboardActions.add(KeyboardActionsItem( + focusNode: fiatAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()])); + } + return Stack( + children: [ + KeyboardActions( + config: KeyboardActionsConfig( + keyboardActionsPlatform: KeyboardActionsPlatform.ALL, + keyboardBarColor: Theme.of(context).extension()!.keyboardBarColor, + nextFocus: false, + actions: keyboardActions, + ), + child: Container( + height: 0, + color: Colors.transparent, + ), + ), + GestureDetector( + onLongPress: () => sendViewModel.balanceViewModel.isReversing = + !sendViewModel.balanceViewModel.isReversing, + onLongPressUp: () => sendViewModel.balanceViewModel.isReversing = + !sendViewModel.balanceViewModel.isReversing, + child: Form( + key: _formKey, + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.only(bottom: 24), + content: FocusTraversalGroup( + policy: OrderedTraversalPolicy(), + child: Column( + children: [ + PageViewHeightAdaptable( + controller: controller, + children: sendCards, + ), + SizedBox(height: 10), + Padding( + padding: EdgeInsets.only(left: 24, right: 24, bottom: 10), + child: Container( + height: 10, + child: Observer( + builder: (_) { + final count = sendViewModel.outputs.length; - return SendCard( - key: output.key, - output: output, - sendViewModel: sendViewModel, - initialPaymentRequest: initialPaymentRequest, + return count > 1 + ? Semantics( + label: 'Page Indicator', + hint: 'Swipe to change receiver', + excludeSemantics: true, + child: SmoothPageIndicator( + controller: controller, + count: count, + effect: ScrollingDotsEffect( + spacing: 6.0, + radius: 6.0, + dotWidth: 6.0, + dotHeight: 6.0, + dotColor: Theme.of(context) + .extension()! + .indicatorDotColor, + activeDotColor: Theme.of(context) + .extension()! + .templateBackgroundColor), + )) + : Offstage(); + }, + ), + ), + ), + Container( + height: 40, + width: double.infinity, + padding: EdgeInsets.only(left: 24), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Observer( + builder: (_) { + final templates = sendViewModel.templates; + final itemCount = templates.length; + + return Row( + children: [ + AddTemplateButton( + key: ValueKey('send_page_add_template_button_key'), + onTap: () => + Navigator.of(context).pushNamed(Routes.sendTemplate), + currentTemplatesLength: templates.length, + ), + ListView.builder( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: itemCount, + itemBuilder: (context, index) { + final template = templates[index]; + return TemplateTile( + key: UniqueKey(), + to: template.name, + hasMultipleRecipients: + template.additionalRecipients != null && + template.additionalRecipients!.length > 1, + amount: template.isCurrencySelected + ? template.amount + : template.amountFiat, + from: template.isCurrencySelected + ? template.cryptoCurrency + : template.fiatCurrency, + onTap: () async { + sendViewModel.state = IsExecutingState(); + if (template.additionalRecipients?.isNotEmpty ?? + false) { + sendViewModel.clearOutputs(); + + for (int i = 0; + i < template.additionalRecipients!.length; + i++) { + Output output; + try { + output = sendViewModel.outputs[i]; + } catch (e) { + sendViewModel.addOutput(); + output = sendViewModel.outputs[i]; + } + + await _setInputsFromTemplate( + context, + output: output, + template: template.additionalRecipients![i], + ); + } + } else { + final output = _defineCurrentOutput(); + await _setInputsFromTemplate( + context, + output: output, + template: template, + ); + } + sendViewModel.state = InitialExecutionState(); + }, + onRemove: () { + showPopUp( + context: context, + builder: (dialogContext) { + return AlertWithTwoActions( + alertTitle: S.of(context).template, + alertContent: + S.of(context).confirm_delete_template, + rightButtonText: S.of(context).delete, + leftButtonText: S.of(context).cancel, + actionRightButton: () { + Navigator.of(dialogContext).pop(); + sendViewModel.sendTemplateViewModel + .removeTemplate(template: template); + }, + actionLeftButton: () => + Navigator.of(dialogContext).pop()); + }, + ); + }, + ); + }, + ), + ], ); - }); - }, - )), - Padding( - padding: EdgeInsets.only(left: 24, right: 24, bottom: 10), - child: Container( - height: 10, - child: Observer( - builder: (_) { - final count = sendViewModel.outputs.length; - - return count > 1 - ? Semantics( - label: 'Page Indicator', - hint: 'Swipe to change receiver', - excludeSemantics: true, - child: SmoothPageIndicator( - controller: controller, - count: count, - effect: ScrollingDotsEffect( - spacing: 6.0, - radius: 6.0, - dotWidth: 6.0, - dotHeight: 6.0, - dotColor: Theme.of(context) - .extension()! - .indicatorDotColor, - activeDotColor: Theme.of(context) - .extension()! - .templateBackgroundColor), - )) - : Offstage(); - }, - ), + }, + ), + ), + ), + ], ), ), - Container( - height: 40, - width: double.infinity, - padding: EdgeInsets.only(left: 24), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Observer( + bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), + bottomSection: Column( + children: [ + if (sendViewModel.hasCurrecyChanger) + Observer( + builder: (_) => Padding( + padding: EdgeInsets.only(bottom: 12), + child: PrimaryButton( + key: ValueKey('send_page_change_asset_button_key'), + onPressed: () => presentCurrencyPicker(context), + text: 'Change your asset (${sendViewModel.selectedCryptoCurrency})', + color: Colors.transparent, + textColor: + Theme.of(context).extension()!.hintTextColor, + ), + ), + ), + if (sendViewModel.sendTemplateViewModel.hasMultiRecipient) + Padding( + padding: EdgeInsets.only(bottom: 12), + child: PrimaryButton( + key: ValueKey('send_page_add_receiver_button_key'), + onPressed: () { + sendViewModel.addOutput(); + Future.delayed(const Duration(milliseconds: 250), () { + controller.jumpToPage(sendViewModel.outputs.length - 1); + }); + }, + text: S.of(context).add_receiver, + color: Colors.transparent, + textColor: + Theme.of(context).extension()!.hintTextColor, + isDottedBorder: true, + borderColor: Theme.of(context) + .extension()! + .templateDottedBorderColor, + )), + Observer( builder: (_) { - final templates = sendViewModel.templates; - final itemCount = templates.length; + return LoadingPrimaryButton( + key: ValueKey('send_page_send_button_key'), + onPressed: () async { + if (sendViewModel.state is IsExecutingState) return; + if (_formKey.currentState != null && + !_formKey.currentState!.validate()) { + if (sendViewModel.outputs.length > 1) { + showErrorValidationAlert(context); + } - return Row( - children: [ - AddTemplateButton( - key: ValueKey('send_page_add_template_button_key'), - onTap: () => Navigator.of(context).pushNamed(Routes.sendTemplate), - currentTemplatesLength: templates.length, - ), - ListView.builder( - scrollDirection: Axis.horizontal, - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: itemCount, - itemBuilder: (context, index) { - final template = templates[index]; - return TemplateTile( - key: UniqueKey(), - to: template.name, - hasMultipleRecipients: template.additionalRecipients != null && - template.additionalRecipients!.length > 1, - amount: template.isCurrencySelected - ? template.amount - : template.amountFiat, - from: template.isCurrencySelected - ? template.cryptoCurrency - : template.fiatCurrency, - onTap: () async { - sendViewModel.state = IsExecutingState(); - if (template.additionalRecipients?.isNotEmpty ?? false) { - sendViewModel.clearOutputs(); + return; + } - for (int i = 0; - i < template.additionalRecipients!.length; - i++) { - Output output; - try { - output = sendViewModel.outputs[i]; - } catch (e) { - sendViewModel.addOutput(); - output = sendViewModel.outputs[i]; - } + final notValidItems = sendViewModel.outputs + .where( + (item) => item.address.isEmpty || item.cryptoAmount.isEmpty) + .toList(); - await _setInputsFromTemplate( - context, - output: output, - template: template.additionalRecipients![i], - ); - } - } else { - final output = _defineCurrentOutput(); - await _setInputsFromTemplate( - context, - output: output, - template: template, - ); - } - sendViewModel.state = InitialExecutionState(); - }, - onRemove: () { - showPopUp( - context: context, - builder: (dialogContext) { - return AlertWithTwoActions( - alertTitle: S.of(context).template, - alertContent: S.of(context).confirm_delete_template, - rightButtonText: S.of(context).delete, - leftButtonText: S.of(context).cancel, - actionRightButton: () { - Navigator.of(dialogContext).pop(); - sendViewModel.sendTemplateViewModel - .removeTemplate(template: template); - }, - actionLeftButton: () => - Navigator.of(dialogContext).pop()); + if (notValidItems.isNotEmpty) { + showErrorValidationAlert(context); + return; + } + + if (sendViewModel.wallet.isHardwareWallet) { + if (!sendViewModel.ledgerViewModel!.isConnected) { + await Navigator.of(context).pushNamed(Routes.connectDevices, + arguments: ConnectDevicePageParams( + walletType: sendViewModel.walletType, + onConnectDevice: (BuildContext context, _) { + sendViewModel.ledgerViewModel! + .setLedger(sendViewModel.wallet); + Navigator.of(context).pop(); }, - ); - }, - ); + )); + } else { + sendViewModel.ledgerViewModel!.setLedger(sendViewModel.wallet); + } + } + + if (sendViewModel.wallet.type == WalletType.monero) { + int amount = 0; + for (var item in sendViewModel.outputs) { + amount += item.formattedCryptoAmount; + } + if (monero!.needExportOutputs(sendViewModel.wallet, amount)) { + await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, + arguments: 'export-outputs'); + await Future.delayed( + Duration(seconds: 1)); // wait for monero to refresh the state + } + if (monero!.needExportOutputs(sendViewModel.wallet, amount)) { + return; + } + } + + final check = sendViewModel.shouldDisplayTotp(); + authService.authenticateAction( + context, + conditionToDetermineIfToUse2FA: check, + onAuthSuccess: (value) async { + if (value) { + await sendViewModel.createTransaction(); + } }, - ), - ], + ); + }, + text: S.of(context).send, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + isLoading: sendViewModel.state is IsExecutingState || + sendViewModel.state is TransactionCommitting || + sendViewModel.state is IsAwaitingDeviceResponseState, + isDisabled: !sendViewModel.isReadyForSend, ); }, - ), - ), - ), - ], - ), + ) + ], + )), ), - bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), - bottomSection: Column( - children: [ - if (sendViewModel.hasCurrecyChanger) - Observer( - builder: (_) => Padding( - padding: EdgeInsets.only(bottom: 12), - child: PrimaryButton( - key: ValueKey('send_page_change_asset_button_key'), - onPressed: () => presentCurrencyPicker(context), - text: 'Change your asset (${sendViewModel.selectedCryptoCurrency})', - color: Colors.transparent, - textColor: Theme.of(context).extension()!.hintTextColor, - ), - ), - ), - if (sendViewModel.sendTemplateViewModel.hasMultiRecipient) - Padding( - padding: EdgeInsets.only(bottom: 12), - child: PrimaryButton( - key: ValueKey('send_page_add_receiver_button_key'), - onPressed: () { - sendViewModel.addOutput(); - Future.delayed(const Duration(milliseconds: 250), () { - controller.jumpToPage(sendViewModel.outputs.length - 1); - }); - }, - text: S.of(context).add_receiver, - color: Colors.transparent, - textColor: Theme.of(context).extension()!.hintTextColor, - isDottedBorder: true, - borderColor: - Theme.of(context).extension()!.templateDottedBorderColor, - )), - Observer( - builder: (_) { - return LoadingPrimaryButton( - key: ValueKey('send_page_send_button_key'), - onPressed: () async { - if (sendViewModel.state is IsExecutingState) return; - if (_formKey.currentState != null && !_formKey.currentState!.validate()) { - if (sendViewModel.outputs.length > 1) { - showErrorValidationAlert(context); - } - - return; - } - - final notValidItems = sendViewModel.outputs - .where((item) => item.address.isEmpty || item.cryptoAmount.isEmpty) - .toList(); - - if (notValidItems.isNotEmpty) { - showErrorValidationAlert(context); - return; - } - - if (sendViewModel.wallet.isHardwareWallet) { - if (!sendViewModel.ledgerViewModel!.isConnected) { - await Navigator.of(context).pushNamed( - Routes.connectDevices, - arguments: ConnectDevicePageParams( - walletType: sendViewModel.walletType, - onConnectDevice: (BuildContext context, _) { - sendViewModel.ledgerViewModel! - .setLedger(sendViewModel.wallet); - Navigator.of(context).pop(); - }, - )); - } else { - sendViewModel.ledgerViewModel! - .setLedger(sendViewModel.wallet); - } - } - - if (sendViewModel.wallet.type == WalletType.monero) { - int amount = 0; - for (var item in sendViewModel.outputs) { - amount += item.formattedCryptoAmount; - } - if (monero!.needExportOutputs(sendViewModel.wallet, amount)) { - await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, arguments: 'export-outputs'); - await Future.delayed(Duration(seconds: 1)); // wait for monero to refresh the state - } - if (monero!.needExportOutputs(sendViewModel.wallet, amount)) { - return; - } - } - - final check = sendViewModel.shouldDisplayTotp(); - authService.authenticateAction( - context, - conditionToDetermineIfToUse2FA: check, - onAuthSuccess: (value) async { - if (value) { - await sendViewModel.createTransaction(); - } - }, - ); - }, - text: S.of(context).send, - color: Theme.of(context).primaryColor, - textColor: Colors.white, - isLoading: sendViewModel.state is IsExecutingState || - sendViewModel.state is TransactionCommitting || - sendViewModel.state is IsAwaitingDeviceResponseState, - isDisabled: !sendViewModel.isReadyForSend, - ); - }, - ) - ], - )), - ), - ); + ), + ], + ); + }); } BuildContext? dialogContext; @@ -525,13 +548,12 @@ class SendPage extends BasePage { if (state is TransactionCommitted) { WidgetsBinding.instance.addPostFrameCallback((_) async { - if (!context.mounted) { return; } - final successMessage = S.of(context).send_success( - sendViewModel.selectedCryptoCurrency.toString()); + final successMessage = + S.of(context).send_success(sendViewModel.selectedCryptoCurrency.toString()); final waitMessage = sendViewModel.walletType == WalletType.solana ? '. ${S.of(context).waitFewSecondForTxUpdate}' @@ -539,10 +561,8 @@ class SendPage extends BasePage { String alertContent = "$successMessage$waitMessage"; - await Navigator.of(context).pushNamed( - Routes.transactionSuccessPage, - arguments: alertContent - ); + await Navigator.of(context) + .pushNamed(Routes.transactionSuccessPage, arguments: alertContent); newContactAddress = newContactAddress ?? sendViewModel.newContactAddress(); if (newContactAddress?.address != null && isRegularElectrumAddress(newContactAddress!.address)) { @@ -562,7 +582,7 @@ class SendPage extends BasePage { leftButtonText: S.of(_dialogContext).ignor, alertLeftActionButtonKey: ValueKey('send_page_sent_dialog_ignore_button_key'), alertRightActionButtonKey: - ValueKey('send_page_sent_dialog_add_contact_button_key'), + ValueKey('send_page_sent_dialog_add_contact_button_key'), actionRightButton: () { Navigator.of(_dialogContext).pop(); RequestReviewHandler.requestReview(); diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 24cbd2061..f1cac5c9f 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dart'; import 'package:cake_wallet/src/widgets/picker.dart'; +import 'package:cake_wallet/src/widgets/standard_checkbox.dart'; import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; @@ -12,6 +13,7 @@ import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -24,40 +26,58 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; +import '../../../../themes/extensions/cake_text_theme.dart'; +import '../../../../themes/theme_base.dart'; + class SendCard extends StatefulWidget { SendCard({ Key? key, required this.output, required this.sendViewModel, + required this.currentTheme, this.initialPaymentRequest, + this.cryptoAmountFocus, + this.fiatAmountFocus, }) : super(key: key); final Output output; final SendViewModel sendViewModel; final PaymentRequest? initialPaymentRequest; + final FocusNode? cryptoAmountFocus; + final FocusNode? fiatAmountFocus; + final ThemeBase currentTheme; + @override SendCardState createState() => SendCardState( output: output, sendViewModel: sendViewModel, initialPaymentRequest: initialPaymentRequest, + currentTheme: currentTheme + // cryptoAmountFocus: cryptoAmountFocus ?? FocusNode(), + // fiatAmountFocus: fiatAmountFocus ?? FocusNode(), + // cryptoAmountFocus: FocusNode(), + // fiatAmountFocus: FocusNode(), ); } class SendCardState extends State with AutomaticKeepAliveClientMixin { - SendCardState({required this.output, required this.sendViewModel, this.initialPaymentRequest}) - : addressController = TextEditingController(), + SendCardState({ + required this.output, + required this.sendViewModel, + this.initialPaymentRequest, + required this.currentTheme, + }) : addressController = TextEditingController(), cryptoAmountController = TextEditingController(), fiatAmountController = TextEditingController(), noteController = TextEditingController(), extractedAddressController = TextEditingController(), - cryptoAmountFocus = FocusNode(), - fiatAmountFocus = FocusNode(), addressFocusNode = FocusNode(); static const prefixIconWidth = 34.0; static const prefixIconHeight = 34.0; + final ThemeBase currentTheme; final Output output; final SendViewModel sendViewModel; final PaymentRequest? initialPaymentRequest; @@ -67,8 +87,6 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin()!.keyboardBarColor, - nextFocus: false, - actions: [ - KeyboardActionsItem( - focusNode: cryptoAmountFocus, - toolbarButtons: [(_) => KeyboardDoneButton()], + // return Stack( + // children: [ + // return KeyboardActions( + // config: KeyboardActionsConfig( + // keyboardActionsPlatform: KeyboardActionsPlatform.IOS, + // keyboardBarColor: Theme.of(context).extension()!.keyboardBarColor, + // nextFocus: false, + // actions: [ + // KeyboardActionsItem( + // focusNode: cryptoAmountFocus, + // toolbarButtons: [(_) => KeyboardDoneButton()], + // ), + // KeyboardActionsItem( + // focusNode: fiatAmountFocus, + // toolbarButtons: [(_) => KeyboardDoneButton()], + // ) + // ], + // ), + // // child: Container( + // // height: 0, + // // color: Colors.transparent, + // // ), child: + // child: SizedBox( + // height: 100, + // width: 100, + // child: Text('Send Card'), + // ), + // ); + return Container( + decoration: responsiveLayoutUtil.shouldRenderMobileUI + ? BoxDecoration( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), + gradient: LinearGradient( + colors: [ + Theme.of(context).extension()!.firstGradientColor, + Theme.of(context).extension()!.secondGradientColor, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, ), - KeyboardActionsItem( - focusNode: fiatAmountFocus, - toolbarButtons: [(_) => KeyboardDoneButton()], - ) - ], - ), - child: Container( - height: 0, - color: Colors.transparent, - ), + ) + : null, + child: Padding( + padding: EdgeInsets.fromLTRB( + 24, + responsiveLayoutUtil.shouldRenderMobileUI ? 110 : 55, + 24, + responsiveLayoutUtil.shouldRenderMobileUI ? 32 : 0, ), - Container( - decoration: responsiveLayoutUtil.shouldRenderMobileUI - ? BoxDecoration( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), - gradient: LinearGradient( - colors: [ - Theme.of(context).extension()!.firstGradientColor, - Theme.of(context).extension()!.secondGradientColor, - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ) - : null, - child: Padding( - padding: EdgeInsets.fromLTRB( - 24, - responsiveLayoutUtil.shouldRenderMobileUI ? 110 : 55, - 24, - responsiveLayoutUtil.shouldRenderMobileUI ? 32 : 0, - ), - child: SingleChildScrollView( - child: Observer( - builder: (_) => Column( - mainAxisSize: MainAxisSize.min, - children: [ - Observer(builder: (_) { - final validator = output.isParsedAddress - ? sendViewModel.textValidator - : sendViewModel.addressValidator; + child: Observer( + builder: (_) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + Observer(builder: (_) { + final validator = output.isParsedAddress + ? sendViewModel.textValidator + : sendViewModel.addressValidator; - return AddressTextField( - addressKey: ValueKey('send_page_address_textfield_key'), - focusNode: addressFocusNode, - controller: addressController, - onURIScanned: (uri) { - final paymentRequest = PaymentRequest.fromUri(uri); - addressController.text = paymentRequest.address; - cryptoAmountController.text = paymentRequest.amount; - noteController.text = paymentRequest.note; - }, - options: [ - AddressTextFieldOption.paste, - AddressTextFieldOption.qrCode, - AddressTextFieldOption.addressBook - ], - buttonColor: - Theme.of(context).extension()!.textFieldButtonColor, + return AddressTextField( + addressKey: ValueKey('send_page_address_textfield_key'), + focusNode: addressFocusNode, + controller: addressController, + onURIScanned: (uri) { + final paymentRequest = PaymentRequest.fromUri(uri); + addressController.text = paymentRequest.address; + cryptoAmountController.text = paymentRequest.amount; + noteController.text = paymentRequest.note; + }, + options: [ + AddressTextFieldOption.paste, + AddressTextFieldOption.qrCode, + AddressTextFieldOption.addressBook + ], + buttonColor: Theme.of(context).extension()!.textFieldButtonColor, + borderColor: Theme.of(context).extension()!.textFieldBorderColor, + textStyle: + TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), + hintStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.textFieldHintColor), + onPushPasteButton: (context) async { + output.resetParsedAddress(); + await output.fetchParsedAddress(context); + }, + onPushAddressBookButton: (context) async { + output.resetParsedAddress(); + }, + onSelectedContact: (contact) { + output.loadContact(contact); + }, + validator: validator, + selectedCurrency: sendViewModel.selectedCryptoCurrency, + ); + }), + if (output.isParsedAddress) + Padding( + padding: const EdgeInsets.only(top: 20), + child: BaseTextFormField( + controller: extractedAddressController, + readOnly: true, borderColor: Theme.of(context).extension()!.textFieldBorderColor, textStyle: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), - hintStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: - Theme.of(context).extension()!.textFieldHintColor), - onPushPasteButton: (context) async { - output.resetParsedAddress(); - await output.fetchParsedAddress(context); - }, - onPushAddressBookButton: (context) async { - output.resetParsedAddress(); - }, - onSelectedContact: (contact) { - output.loadContact(contact); - }, - validator: validator, - selectedCurrency: sendViewModel.selectedCryptoCurrency, - ); - }), - if (output.isParsedAddress) - Padding( - padding: const EdgeInsets.only(top: 20), - child: BaseTextFormField( - controller: extractedAddressController, - readOnly: true, - borderColor: Theme.of(context) - .extension()! - .textFieldBorderColor, - textStyle: TextStyle( - fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), - validator: sendViewModel.addressValidator)), - CurrencyAmountTextField( - currencyPickerButtonKey: ValueKey('send_page_currency_picker_button_key'), - amountTextfieldKey: ValueKey('send_page_amount_textfield_key'), - sendAllButtonKey: ValueKey('send_page_send_all_button_key'), - currencyAmountTextFieldWidgetKey: - ValueKey('send_page_crypto_currency_amount_textfield_widget_key'), - selectedCurrency: sendViewModel.selectedCryptoCurrency.title, - amountFocusNode: cryptoAmountFocus, - amountController: cryptoAmountController, - isAmountEditable: true, - onTapPicker: () => _presentPicker(context), - isPickerEnable: sendViewModel.hasMultipleTokens, - tag: sendViewModel.selectedCryptoCurrency.tag, - allAmountButton: - !sendViewModel.isBatchSending && sendViewModel.shouldDisplaySendALL, - currencyValueValidator: output.sendAll - ? sendViewModel.allAmountValidator - : sendViewModel.amountValidator, - allAmountCallback: () async => output.setSendAll(sendViewModel.balance)), - Divider( - height: 1, - color: Theme.of(context).extension()!.textFieldHintColor), - Observer( - builder: (_) => Padding( - padding: EdgeInsets.only(top: 10), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - S.of(context).available_balance + ':', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Theme.of(context) - .extension()! - .textFieldHintColor), - ), - ), - Text( - sendViewModel.balance, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Theme.of(context) - .extension()! - .textFieldHintColor), - ) - ], + validator: sendViewModel.addressValidator)), + CurrencyAmountTextField( + currencyPickerButtonKey: ValueKey('send_page_currency_picker_button_key'), + amountTextfieldKey: ValueKey('send_page_amount_textfield_key'), + sendAllButtonKey: ValueKey('send_page_send_all_button_key'), + currencyAmountTextFieldWidgetKey: + ValueKey('send_page_crypto_currency_amount_textfield_widget_key'), + selectedCurrency: sendViewModel.selectedCryptoCurrency.title, + amountFocusNode: widget.cryptoAmountFocus, + amountController: cryptoAmountController, + isAmountEditable: true, + onTapPicker: () => _presentPicker(context), + isPickerEnable: sendViewModel.hasMultipleTokens, + tag: sendViewModel.selectedCryptoCurrency.tag, + allAmountButton: + !sendViewModel.isBatchSending && sendViewModel.shouldDisplaySendALL, + currencyValueValidator: output.sendAll + ? sendViewModel.allAmountValidator + : sendViewModel.amountValidator, + allAmountCallback: () async => output.setSendAll(sendViewModel.balance)), + Divider( + height: 1, + color: Theme.of(context).extension()!.textFieldHintColor), + Observer( + builder: (_) => Padding( + padding: EdgeInsets.only(top: 10), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + S.of(context).available_balance + ':', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: + Theme.of(context).extension()!.textFieldHintColor), ), ), - ), - if (!sendViewModel.isFiatDisabled) - CurrencyAmountTextField( - amountTextfieldKey: ValueKey('send_page_fiat_amount_textfield_key'), - currencyAmountTextFieldWidgetKey: - ValueKey('send_page_fiat_currency_amount_textfield_widget_key'), - selectedCurrency: sendViewModel.fiat.title, - amountFocusNode: fiatAmountFocus, - amountController: fiatAmountController, - hintText: '0.00', - isAmountEditable: true, - allAmountButton: false), - Divider( - height: 1, - color: Theme.of(context).extension()!.textFieldHintColor), - Padding( - padding: EdgeInsets.only(top: 20), - child: BaseTextFormField( - key: ValueKey('send_page_note_textfield_key'), - controller: noteController, - keyboardType: TextInputType.multiline, - maxLines: null, - borderColor: - Theme.of(context).extension()!.textFieldBorderColor, - textStyle: TextStyle( - fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), - hintText: S.of(context).note_optional, - placeholderTextStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, + Text( + sendViewModel.balance, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, color: Theme.of(context).extension()!.textFieldHintColor), - ), - ), - if (sendViewModel.hasFees) - Observer( - builder: (_) => GestureDetector( - key: ValueKey('send_page_select_fee_priority_button_key'), - onTap: sendViewModel.hasFeesPriority - ? () => pickTransactionPriority(context) - : () {}, - child: Container( - padding: EdgeInsets.only(top: 24), + ) + ], + ), + ), + ), + if (!sendViewModel.isFiatDisabled) + CurrencyAmountTextField( + amountTextfieldKey: ValueKey('send_page_fiat_amount_textfield_key'), + currencyAmountTextFieldWidgetKey: + ValueKey('send_page_fiat_currency_amount_textfield_widget_key'), + selectedCurrency: sendViewModel.fiat.title, + amountFocusNode: widget.fiatAmountFocus, + amountController: fiatAmountController, + hintText: '0.00', + isAmountEditable: true, + allAmountButton: false), + Divider( + height: 1, + color: Theme.of(context).extension()!.textFieldHintColor), + Padding( + padding: EdgeInsets.only(top: 20), + child: BaseTextFormField( + key: ValueKey('send_page_note_textfield_key'), + controller: noteController, + keyboardType: TextInputType.multiline, + maxLines: null, + borderColor: Theme.of(context).extension()!.textFieldBorderColor, + textStyle: + TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), + hintText: S.of(context).note_optional, + placeholderTextStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.textFieldHintColor), + ), + ), + if (sendViewModel.hasFees) + Observer( + builder: (_) => GestureDetector( + key: ValueKey('send_page_select_fee_priority_button_key'), + onTap: sendViewModel.hasFeesPriority + ? () => pickTransactionPriority(context) + : () {}, + child: Container( + padding: EdgeInsets.only(top: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context).send_estimated_fee, + style: TextStyle( + fontSize: 12, fontWeight: FontWeight.w500, color: Colors.white), + ), + Container( child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - S.of(context).send_estimated_fee, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: Colors.white), - ), - Container( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - output.estimatedFee.toString() + - ' ' + - sendViewModel.currency.toString(), - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - Padding( - padding: EdgeInsets.only(top: 5), - child: sendViewModel.isFiatDisabled - ? const SizedBox(height: 14) - : Text( - output.estimatedFeeFiatAmount + - ' ' + - sendViewModel.fiat.title, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Theme.of(context) - .extension()! - .textFieldHintColor, - ), - ), - ), - ], + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + output.estimatedFee.toString() + + ' ' + + sendViewModel.currency.toString(), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.white, ), - Padding( - padding: EdgeInsets.only(top: 2, left: 5), - child: Icon( - Icons.arrow_forward_ios, - size: 12, - color: Colors.white, - ), - ) - ], + ), + Padding( + padding: EdgeInsets.only(top: 5), + child: sendViewModel.isFiatDisabled + ? const SizedBox(height: 14) + : Text( + output.estimatedFeeFiatAmount + + ' ' + + sendViewModel.fiat.title, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .extension()! + .textFieldHintColor, + ), + ), + ), + ], + ), + Padding( + padding: EdgeInsets.only(top: 2, left: 5), + child: Icon( + Icons.arrow_forward_ios, + size: 12, + color: Colors.white, ), ) ], ), - ), - ), + ) + ], ), - if (sendViewModel.hasCoinControl) - Padding( - padding: EdgeInsets.only(top: 6), - child: GestureDetector( - key: ValueKey('send_page_unspent_coin_button_key'), - onTap: () => Navigator.of(context).pushNamed( - Routes.unspentCoinsList, - arguments: widget.sendViewModel.coinTypeToSpendFrom, - ), - child: Container( - color: Colors.transparent, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - S.of(context).coin_control, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Colors.white), - ), - Icon( - Icons.arrow_forward_ios, - size: 12, - color: Colors.white, - ), - ], - ), - ), - ), - ), - ], + ), + ), ), - ), - ), + if (sendViewModel.hasCoinControl) + Padding( + padding: EdgeInsets.only(top: 6), + child: GestureDetector( + key: ValueKey('send_page_unspent_coin_button_key'), + onTap: () => Navigator.of(context).pushNamed( + Routes.unspentCoinsList, + arguments: widget.sendViewModel.coinTypeToSpendFrom, + ), + child: Container( + color: Colors.transparent, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + S.of(context).coin_control, + style: TextStyle( + fontSize: 12, fontWeight: FontWeight.w600, color: Colors.white), + ), + Icon( + Icons.arrow_forward_ios, + size: 12, + color: Colors.white, + ), + ], + ), + ), + ), + ), + if (sendViewModel.currency == CryptoCurrency.ltc) + Observer( + builder: (_) => Padding( + padding: EdgeInsets.only(top: 14), + child: GestureDetector( + key: ValueKey('send_page_unspent_coin_button_key'), + onTap: () { + bool value = + widget.sendViewModel.coinTypeToSpendFrom == UnspentCoinType.any; + sendViewModel.setAllowMwebCoins(!value); + }, + child: Container( + color: Colors.transparent, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + StandardCheckbox( + caption: S.of(context).litecoin_mweb_allow_coins, + captionColor: Colors.white, + borderColor: currentTheme.type == ThemeType.bright + ? Colors.white + : Theme.of(context).extension()!.secondaryTextColor, + iconColor: currentTheme.type == ThemeType.bright + ? Colors.white + : Theme.of(context).primaryColor, + value: + widget.sendViewModel.coinTypeToSpendFrom == UnspentCoinType.any, + onChanged: (bool? value) { + sendViewModel.setAllowMwebCoins(value ?? false); + }, + ), + ], + ), + ), + ), + ), + ), + ], ), - ) - ], + ), + ), ); } diff --git a/lib/src/widgets/adaptable_page_view.dart b/lib/src/widgets/adaptable_page_view.dart new file mode 100644 index 000000000..c6800ae22 --- /dev/null +++ b/lib/src/widgets/adaptable_page_view.dart @@ -0,0 +1,202 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +const _firstLayoutMaxHeight = 10000.0; + +class PageViewHeightAdaptable extends StatefulWidget { + const PageViewHeightAdaptable({ + super.key, + required this.controller, + required this.children, + }) : assert(children.length > 0, 'children must not be empty'); + + final PageController controller; + final List children; + + @override + State createState() => _PageViewHeightAdaptableState(); +} + +class _PageViewHeightAdaptableState extends State { + final _sizes = {}; + + @override + void didUpdateWidget(PageViewHeightAdaptable oldWidget) { + super.didUpdateWidget(oldWidget); + + _sizes.clear(); + } + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: widget.controller, + builder: (context, child) => _SizingContainer( + sizes: _sizes, + page: widget.controller.hasClients ? widget.controller.page ?? 0 : 0, + child: child!, + ), + child: LayoutBuilder( + builder: (context, constraints) => PageView( + controller: widget.controller, + children: [ + for (final (i, child) in widget.children.indexed) + Stack( + alignment: Alignment.topCenter, + clipBehavior: Clip.hardEdge, + children: [ + SizedBox.fromSize(size: _sizes[i]), + Positioned( + left: 0, + top: 0, + right: 0, + child: _SizeAware( + child: child, + // don't setState, we'll use it in the layout phase + onSizeLaidOut: (size) { + _sizes[i] = size; + }, + ), + ), + ], + ), + ], + ), + ), + ); + } +} + +typedef _OnSizeLaidOutCallback = void Function(Size); + +class _SizingContainer extends SingleChildRenderObjectWidget { + const _SizingContainer({ + super.child, + required this.sizes, + required this.page, + }); + + final Map sizes; + final double page; + + @override + _RenderSizingContainer createRenderObject(BuildContext context) { + return _RenderSizingContainer( + sizes: sizes, + page: page, + ); + } + + @override + void updateRenderObject( + BuildContext context, + _RenderSizingContainer renderObject, + ) { + renderObject + ..sizes = sizes + ..page = page; + } +} + +class _RenderSizingContainer extends RenderProxyBox { + _RenderSizingContainer({ + RenderBox? child, + required Map sizes, + required double page, + }) : _sizes = sizes, + _page = page, + super(child); + + Map _sizes; + Map get sizes => _sizes; + set sizes(Map value) { + if (_sizes == value) return; + _sizes = value; + markNeedsLayout(); + } + + double _page; + double get page => _page; + set page(double value) { + if (_page == value) return; + _page = value; + markNeedsLayout(); + } + + @override + void performLayout() { + if (child case final child?) { + child.layout( + constraints.copyWith( + minWidth: constraints.maxWidth, + minHeight: 0, + maxHeight: constraints.hasBoundedHeight ? null : _firstLayoutMaxHeight, + ), + parentUsesSize: true, + ); + + final a = sizes[page.floor()]!; + final b = sizes[page.ceil()]!; + + final height = lerpDouble(a.height, b.height, page - page.floor()); + + child.layout( + constraints.copyWith(minHeight: height, maxHeight: height), + parentUsesSize: true, + ); + size = child.size; + } else { + size = computeSizeForNoChild(constraints); + } + } +} + +class _SizeAware extends SingleChildRenderObjectWidget { + const _SizeAware({ + required Widget child, + required this.onSizeLaidOut, + }) : super(child: child); + + final _OnSizeLaidOutCallback onSizeLaidOut; + + @override + _RenderSizeAware createRenderObject(BuildContext context) { + return _RenderSizeAware( + onSizeLaidOut: onSizeLaidOut, + ); + } + + @override + void updateRenderObject(BuildContext context, _RenderSizeAware renderObject) { + renderObject.onSizeLaidOut = onSizeLaidOut; + } +} + +class _RenderSizeAware extends RenderProxyBox { + _RenderSizeAware({ + RenderBox? child, + required _OnSizeLaidOutCallback onSizeLaidOut, + }) : _onSizeLaidOut = onSizeLaidOut, + super(child); + + _OnSizeLaidOutCallback? _onSizeLaidOut; + _OnSizeLaidOutCallback get onSizeLaidOut => _onSizeLaidOut!; + set onSizeLaidOut(_OnSizeLaidOutCallback value) { + if (_onSizeLaidOut == value) return; + _onSizeLaidOut = value; + markNeedsLayout(); + } + + @override + void performLayout() { + super.performLayout(); + + onSizeLaidOut( + getDryLayout( + constraints.copyWith(maxHeight: double.infinity), + ), + ); + } +} \ No newline at end of file diff --git a/lib/src/widgets/standard_checkbox.dart b/lib/src/widgets/standard_checkbox.dart index d61b84d1d..2b06ff1c6 100644 --- a/lib/src/widgets/standard_checkbox.dart +++ b/lib/src/widgets/standard_checkbox.dart @@ -9,6 +9,7 @@ class StandardCheckbox extends StatelessWidget { this.gradientBackground = false, this.borderColor, this.iconColor, + this.captionColor, required this.onChanged}); final bool value; @@ -16,6 +17,7 @@ class StandardCheckbox extends StatelessWidget { final bool gradientBackground; final Color? borderColor; final Color? iconColor; + final Color? captionColor; final Function(bool) onChanged; @override @@ -68,7 +70,7 @@ class StandardCheckbox extends StatelessWidget { fontSize: 16.0, fontFamily: 'Lato', fontWeight: FontWeight.normal, - color: Theme.of(context).extension()!.titleColor, + color: captionColor ?? Theme.of(context).extension()!.titleColor, decoration: TextDecoration.none, ), ), diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 998521ede..3dce212af 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -77,7 +77,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor this.transactionDescriptionBox, this.ledgerViewModel, this.unspentCoinsListViewModel, { - this.coinTypeToSpendFrom = UnspentCoinType.any, + this.coinTypeToSpendFrom = UnspentCoinType.nonMweb, }) : state = InitialExecutionState(), currencies = appStore.wallet!.balance.keys.toList(), selectedCryptoCurrency = appStore.wallet!.currency, @@ -112,7 +112,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor ObservableList outputs; - final UnspentCoinType coinTypeToSpendFrom; + @observable + UnspentCoinType coinTypeToSpendFrom; bool get showAddressBookPopup => _settingsStore.showAddressBookPopupEnabled; @@ -135,6 +136,13 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor addOutput(); } + @action + void setAllowMwebCoins(bool allow) { + if (wallet.type == WalletType.litecoin) { + coinTypeToSpendFrom = allow ? UnspentCoinType.any : UnspentCoinType.nonMweb; + } + } + @computed bool get isBatchSending => outputs.length > 1; diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 84f0c17ce..faf14e18d 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -394,6 +394,7 @@ "light_theme": "فاتح", "litecoin_enable_mweb_sync": "تمكين MWEB المسح الضوئي", "litecoin_mweb": "mweb", + "litecoin_mweb_allow_coins": "السماح للعملات المعدنية MWEB", "litecoin_mweb_always_scan": "اضبط MWEB دائمًا على المسح الضوئي", "litecoin_mweb_description": "MWEB هو بروتوكول جديد يجلب معاملات أسرع وأرخص وأكثر خصوصية إلى Litecoin", "litecoin_mweb_dismiss": "رفض", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index c67d8915a..7ac75f00e 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -394,6 +394,7 @@ "light_theme": "Светло", "litecoin_enable_mweb_sync": "Активирайте сканирането на MWeb", "litecoin_mweb": "Mweb", + "litecoin_mweb_allow_coins": "Позволете на MWeb монети", "litecoin_mweb_always_scan": "Задайте MWeb винаги сканиране", "litecoin_mweb_description": "MWeb е нов протокол, който носи по -бърз, по -евтин и повече частни транзакции на Litecoin", "litecoin_mweb_dismiss": "Уволнение", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 349ae67bc..f777ffe60 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -394,6 +394,7 @@ "light_theme": "Světlý", "litecoin_enable_mweb_sync": "Povolit skenování MWeb", "litecoin_mweb": "MWeb", + "litecoin_mweb_allow_coins": "Povolte mweb mince", "litecoin_mweb_always_scan": "Nastavit MWeb vždy skenování", "litecoin_mweb_description": "MWEB je nový protokol, který do Litecoin přináší rychlejší, levnější a více soukromých transakcí", "litecoin_mweb_dismiss": "Propustit", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 895fa4183..35012b20d 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -394,6 +394,7 @@ "light_theme": "Hell", "litecoin_enable_mweb_sync": "Aktivieren Sie das MWEB-Scannen", "litecoin_mweb": "MWeb", + "litecoin_mweb_allow_coins": "MWEB -Münzen zulassen", "litecoin_mweb_always_scan": "Setzen Sie MWeb immer scannen", "litecoin_mweb_description": "MWWB ist ein neues Protokoll, das schnellere, billigere und privatere Transaktionen zu Litecoin bringt", "litecoin_mweb_dismiss": "Zurückweisen", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index c03841cb9..6875794fd 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -394,6 +394,7 @@ "light_theme": "Light", "litecoin_enable_mweb_sync": "Enable MWEB scanning", "litecoin_mweb": "MWEB", + "litecoin_mweb_allow_coins": "Allow MWEB coins", "litecoin_mweb_always_scan": "Set MWEB always scanning", "litecoin_mweb_description": "MWEB is a new protocol that brings faster, cheaper, and more private transactions to Litecoin", "litecoin_mweb_dismiss": "Dismiss", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index dcff955bb..42e7eedc0 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -394,6 +394,7 @@ "light_theme": "Ligero", "litecoin_enable_mweb_sync": "Habilitar el escaneo mweb", "litecoin_mweb": "Mweb", + "litecoin_mweb_allow_coins": "Permitir monedas mweb", "litecoin_mweb_always_scan": "Establecer mweb siempre escaneo", "litecoin_mweb_description": "Mweb es un nuevo protocolo que trae transacciones más rápidas, más baratas y más privadas a Litecoin", "litecoin_mweb_dismiss": "Despedir", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 379290a29..4960d980a 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -394,6 +394,7 @@ "light_theme": "Clair", "litecoin_enable_mweb_sync": "Activer la numérisation MWEB", "litecoin_mweb": "Mweb", + "litecoin_mweb_allow_coins": "Autoriser les pièces MWeb", "litecoin_mweb_always_scan": "Définir MWEB Score Scanning", "litecoin_mweb_description": "MWEB est un nouveau protocole qui apporte des transactions plus rapides, moins chères et plus privées à Litecoin", "litecoin_mweb_dismiss": "Rejeter", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 125ceeaf5..068457568 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -394,6 +394,7 @@ "light_theme": "Haske", "litecoin_enable_mweb_sync": "Kunna binciken Mweb", "litecoin_mweb": "Mweb", + "litecoin_mweb_allow_coins": "Bada izinin Coins na Mweb", "litecoin_mweb_always_scan": "Saita Mweb koyaushe", "litecoin_mweb_description": "Mweb shine sabon tsarin yarjejeniya da ya kawo da sauri, mai rahusa, da kuma ma'amaloli masu zaman kansu zuwa Litecoin", "litecoin_mweb_dismiss": "Tuɓe \\ sallama", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 074f3ba09..248b0230c 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -394,6 +394,7 @@ "light_theme": "रोशनी", "litecoin_enable_mweb_sync": "MWEB स्कैनिंग सक्षम करें", "litecoin_mweb": "मावली", + "litecoin_mweb_allow_coins": "MWEB सिक्कों की अनुमति दें", "litecoin_mweb_always_scan": "MWEB हमेशा स्कैनिंग सेट करें", "litecoin_mweb_description": "MWEB एक नया प्रोटोकॉल है जो लिटकोइन के लिए तेजी से, सस्ता और अधिक निजी लेनदेन लाता है", "litecoin_mweb_dismiss": "नकार देना", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index be447f24d..5c83942a1 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -394,6 +394,7 @@ "light_theme": "Svijetla", "litecoin_enable_mweb_sync": "Omogućite MWEB skeniranje", "litecoin_mweb": "MWeb", + "litecoin_mweb_allow_coins": "Dopustite MWeb kovanice", "litecoin_mweb_always_scan": "Postavite MWeb uvijek skeniranje", "litecoin_mweb_description": "MWEB je novi protokol koji u Litecoin donosi brže, jeftinije i privatnije transakcije", "litecoin_mweb_dismiss": "Odbaciti", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 551e28cef..2719015cc 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -394,6 +394,7 @@ "light_theme": "Լուսավոր", "litecoin_enable_mweb_sync": "Միացնել MWEB սկան", "litecoin_mweb": "Մուեբ", + "litecoin_mweb_allow_coins": "Թույլ տվեք MWeb մետաղադրամներ", "litecoin_mweb_always_scan": "Սահմանեք Mweb Միշտ սկանավորում", "litecoin_mweb_description": "Mweb- ը նոր արձանագրություն է, որը բերում է ավելի արագ, ավելի էժան եւ ավելի մասնավոր գործարքներ դեպի LITECOIN", "litecoin_mweb_dismiss": "Հեռացնել", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index a171f3243..ccf4367e3 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -394,6 +394,7 @@ "light_theme": "Terang", "litecoin_enable_mweb_sync": "Aktifkan pemindaian MWEB", "litecoin_mweb": "Mweb", + "litecoin_mweb_allow_coins": "Izinkan koin mWeb", "litecoin_mweb_always_scan": "Atur mWeb selalu memindai", "litecoin_mweb_description": "MWEB adalah protokol baru yang membawa transaksi yang lebih cepat, lebih murah, dan lebih pribadi ke Litecoin", "litecoin_mweb_dismiss": "Membubarkan", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index f035c3493..a1682a11f 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -394,6 +394,7 @@ "light_theme": "Chiaro", "litecoin_enable_mweb_sync": "Abilita la scansione MWeb", "litecoin_mweb": "MWeb", + "litecoin_mweb_allow_coins": "Consenti monete mWeb", "litecoin_mweb_always_scan": "Imposta MWeb per scansionare sempre", "litecoin_mweb_description": "MWeb è un nuovo protocollo che porta transazioni più veloci, più economiche e più private a Litecoin", "litecoin_mweb_dismiss": "Chiudi", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 89fcc5391..f97baf7b4 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -395,6 +395,7 @@ "light_theme": "光", "litecoin_enable_mweb_sync": "MWEBスキャンを有効にします", "litecoin_mweb": "mweb", + "litecoin_mweb_allow_coins": "MWEBコインを許可します", "litecoin_mweb_always_scan": "MWEBを常にスキャンします", "litecoin_mweb_description": "MWEBは、Litecoinにより速く、より安価で、よりプライベートなトランザクションをもたらす新しいプロトコルです", "litecoin_mweb_dismiss": "却下する", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index e8e23a1a2..75af5c195 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -394,6 +394,7 @@ "light_theme": "빛", "litecoin_enable_mweb_sync": "mweb 스캔을 활성화합니다", "litecoin_mweb": "mweb", + "litecoin_mweb_allow_coins": "mweb 코인을 허용하십시오", "litecoin_mweb_always_scan": "mweb는 항상 스캔을 설정합니다", "litecoin_mweb_description": "MWEB는 Litecoin에 더 빠르고 저렴하며 개인 거래를 제공하는 새로운 프로토콜입니다.", "litecoin_mweb_dismiss": "해고하다", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index c21413a44..9f200a030 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -394,6 +394,7 @@ "light_theme": "အလင်း", "litecoin_enable_mweb_sync": "mweb scanning ဖွင့်ပါ", "litecoin_mweb": "မင်္ဂလာပါ", + "litecoin_mweb_allow_coins": "mweb ဒင်္ဂါးများကိုခွင့်ပြုပါ", "litecoin_mweb_always_scan": "Mweb အမြဲစကင်ဖတ်စစ်ဆေးပါ", "litecoin_mweb_description": "Mweb သည် Protocol အသစ်ဖြစ်ပြီး LitCoin သို့ပိုမိုဈေးချိုသာသော, စျေးသက်သက်သာသာသုံးခြင်းနှင့်ပိုမိုများပြားသောပုဂ္ဂလိကငွေပို့ဆောင်မှုများကိုဖြစ်ပေါ်စေသည်", "litecoin_mweb_dismiss": "ထုတ်ပစ်", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 0b32a7fc9..1b41eea22 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -394,6 +394,7 @@ "light_theme": "Licht", "litecoin_enable_mweb_sync": "MWEB -scanning inschakelen", "litecoin_mweb": "Mweb", + "litecoin_mweb_allow_coins": "Sta mweb munten toe", "litecoin_mweb_always_scan": "Stel mweb altijd op scannen", "litecoin_mweb_description": "MWEB is een nieuw protocol dat snellere, goedkopere en meer privé -transacties naar Litecoin brengt", "litecoin_mweb_dismiss": "Afwijzen", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index b5acc57ed..8b96bde04 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -394,6 +394,7 @@ "light_theme": "Jasny", "litecoin_enable_mweb_sync": "Włącz skanowanie MWEB", "litecoin_mweb": "MWEB", + "litecoin_mweb_allow_coins": "Zezwalaj na monety MWEB", "litecoin_mweb_always_scan": "Ustaw MWEB zawsze skanowanie", "litecoin_mweb_description": "MWEB to nowy protokół, który przynosi szybciej, tańsze i bardziej prywatne transakcje do Litecoin", "litecoin_mweb_dismiss": "Odrzucać", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 8ec99f0f8..096c3f413 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -394,6 +394,7 @@ "light_theme": "Luz", "litecoin_enable_mweb_sync": "Ativar digitalização do MWEB", "litecoin_mweb": "Mweb", + "litecoin_mweb_allow_coins": "Permitir moedas MWEB", "litecoin_mweb_always_scan": "Definir mweb sempre digitalizando", "litecoin_mweb_description": "MWEB é um novo protocolo que traz transações mais rápidas, baratas e mais privadas para o Litecoin", "litecoin_mweb_dismiss": "Liberar", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 8ef7ac579..6fe82abcf 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -394,6 +394,7 @@ "light_theme": "Светлая", "litecoin_enable_mweb_sync": "Включить MWEB сканирование", "litecoin_mweb": "Мвеб", + "litecoin_mweb_allow_coins": "Разрешить монеты MWEB", "litecoin_mweb_always_scan": "Установить MWEB всегда сканирование", "litecoin_mweb_description": "MWEB - это новый протокол, который приносит быстрее, дешевле и более частные транзакции в Litecoin", "litecoin_mweb_dismiss": "Увольнять", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 414bbe52f..35e94e508 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -394,6 +394,7 @@ "light_theme": "สว่าง", "litecoin_enable_mweb_sync": "เปิดใช้งานการสแกน MWEB", "litecoin_mweb": "mweb", + "litecoin_mweb_allow_coins": "อนุญาตให้เหรียญ MWEB", "litecoin_mweb_always_scan": "ตั้งค่าการสแกน MWEB เสมอ", "litecoin_mweb_description": "MWEB เป็นโปรโตคอลใหม่ที่นำการทำธุรกรรมที่เร็วกว่าราคาถูกกว่าและเป็นส่วนตัวมากขึ้นไปยัง Litecoin", "litecoin_mweb_dismiss": "อนุญาตให้ออกไป", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 46a7540bd..d9cdbbb3e 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -394,6 +394,7 @@ "light_theme": "Light", "litecoin_enable_mweb_sync": "Paganahin ang pag -scan ng MWeb", "litecoin_mweb": "Mweb", + "litecoin_mweb_allow_coins": "Payagan ang mga barya ng MWEB", "litecoin_mweb_always_scan": "Itakda ang MWeb na laging nag -scan", "litecoin_mweb_description": "Ang MWeb ay isang bagong protocol na nagdadala ng mas mabilis, mas mura, at mas maraming pribadong mga transaksyon sa Litecoin", "litecoin_mweb_dismiss": "Tanggalin", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 8fcacb9ba..5d7c77050 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -394,6 +394,7 @@ "light_theme": "Aydınlık", "litecoin_enable_mweb_sync": "MWEB taramasını etkinleştir", "litecoin_mweb": "Mweb", + "litecoin_mweb_allow_coins": "MWEB Coins'e izin ver", "litecoin_mweb_always_scan": "MWEB'i her zaman taramayı ayarlayın", "litecoin_mweb_description": "MWEB, Litecoin'e daha hızlı, daha ucuz ve daha fazla özel işlem getiren yeni bir protokoldür", "litecoin_mweb_dismiss": "Azletmek", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 6b111f72f..1a6d113b0 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -394,6 +394,7 @@ "light_theme": "Світла", "litecoin_enable_mweb_sync": "Увімкнути сканування MWEB", "litecoin_mweb": "Мвеб", + "litecoin_mweb_allow_coins": "Дозволити монети MWEB", "litecoin_mweb_always_scan": "Встановити mweb завжди сканувати", "litecoin_mweb_description": "MWEB - це новий протокол, який приносить швидкі, дешевші та більш приватні транзакції Litecoin", "litecoin_mweb_dismiss": "Звільнити", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index d1a2b4ca9..a7b70dae2 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -394,6 +394,7 @@ "light_theme": "روشنی", "litecoin_enable_mweb_sync": "MWEB اسکیننگ کو فعال کریں", "litecoin_mweb": "MWEB", + "litecoin_mweb_allow_coins": "MWEB سکے کی اجازت دیں", "litecoin_mweb_always_scan": "MWEB ہمیشہ اسکیننگ سیٹ کریں", "litecoin_mweb_description": "MWEB ایک نیا پروٹوکول ہے جو لیٹیکوئن میں تیز ، سستا اور زیادہ نجی لین دین لاتا ہے", "litecoin_mweb_dismiss": "خارج", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 2aba61ef3..ff547e77b 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -393,6 +393,7 @@ "light_theme": "Chủ đề sáng", "litecoin_enable_mweb_sync": "Bật quét MWEB", "litecoin_mweb": "Mweb", + "litecoin_mweb_allow_coins": "Cho phép tiền xu MWEB", "litecoin_mweb_always_scan": "Đặt MWEB luôn quét", "litecoin_mweb_description": "MWEB là một giao thức mới mang lại các giao dịch nhanh hơn, rẻ hơn và riêng tư hơn cho Litecoin", "litecoin_mweb_dismiss": "Miễn nhiệm", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 744159a30..6eb66754c 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -395,6 +395,7 @@ "light_theme": "Funfun bí eérú", "litecoin_enable_mweb_sync": "Mu mweb ọlọjẹ", "litecoin_mweb": "Mweb", + "litecoin_mweb_allow_coins": "Gba awọn owo Mweb gba", "litecoin_mweb_always_scan": "Ṣeto mweb nigbagbogbo n ṣayẹwo", "litecoin_mweb_description": "Mweb jẹ ilana ilana tuntun ti o mu iyara wa yiyara, din owo, ati awọn iṣowo ikọkọ diẹ sii si Livcoin", "litecoin_mweb_dismiss": "Tuka", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index a3b5ec421..ed890c17a 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -394,6 +394,7 @@ "light_theme": "艳丽", "litecoin_enable_mweb_sync": "启用MWEB扫描", "litecoin_mweb": "MWEB", + "litecoin_mweb_allow_coins": "允许MWEB硬币", "litecoin_mweb_always_scan": "设置MWEB总是扫描", "litecoin_mweb_description": "MWEB是一项新协议,它将更快,更便宜和更多的私人交易带给Litecoin", "litecoin_mweb_dismiss": "解雇", From dcde3fd2530f179a298c1bedc02600cfc355f9e9 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 11 Mar 2025 14:27:50 +0100 Subject: [PATCH 033/243] feat: Add fiat api and last used wallet type to support chat (#2078) --- lib/di.dart | 3 ++- lib/src/screens/support_chat/support_chat_page.dart | 2 ++ .../support_chat/widgets/chatwoot_widget.dart | 11 +++++++++-- lib/view_model/support_view_model.dart | 12 +++++++++--- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/di.dart b/lib/di.dart index ccd0908d1..c4ecb74ca 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1260,7 +1260,8 @@ Future setup({ getIt.registerFactoryParam( (Order order, _) => OrderDetailsPage(getIt.get(param1: order))); - getIt.registerFactory(() => SupportViewModel(getIt.get())); + getIt.registerFactory(() => + SupportViewModel(getIt.get(), getIt.get())); getIt.registerFactory(() => SupportPage(getIt.get())); diff --git a/lib/src/screens/support_chat/support_chat_page.dart b/lib/src/screens/support_chat/support_chat_page.dart index b66af5a59..23fcaca7d 100644 --- a/lib/src/screens/support_chat/support_chat_page.dart +++ b/lib/src/screens/support_chat/support_chat_page.dart @@ -26,6 +26,8 @@ class SupportChatPage extends BasePage { secureStorage, supportUrl: supportViewModel.fetchUrl(authToken: snapshot.data!), appVersion: supportViewModel.appVersion, + fiatApiMode: supportViewModel.fiatApiMode, + walletType: supportViewModel.walletType, ); return Container(); }, diff --git a/lib/src/screens/support_chat/widgets/chatwoot_widget.dart b/lib/src/screens/support_chat/widgets/chatwoot_widget.dart index 3b9c3301a..9056346b0 100644 --- a/lib/src/screens/support_chat/widgets/chatwoot_widget.dart +++ b/lib/src/screens/support_chat/widgets/chatwoot_widget.dart @@ -11,11 +11,15 @@ class ChatwootWidget extends StatefulWidget { this.secureStorage, { required this.supportUrl, required this.appVersion, + required this.fiatApiMode, + required this.walletType, }); final SecureStorage secureStorage; final String supportUrl; final String appVersion; + final String fiatApiMode; + final String walletType; @override ChatwootWidgetState createState() => ChatwootWidgetState(); @@ -43,8 +47,11 @@ class ChatwootWidgetState extends State { if (eventType == 'loaded') { final authToken = parsedMessage["config"]["authToken"]; _storeCookie(authToken as String); - _setCustomAttributes( - controller, {"app_version": widget.appVersion}); + _setCustomAttributes(controller, { + "app_version": widget.appVersion, + "fiat_api_mode": widget.fiatApiMode, + "wallet_type": widget.walletType, + }); } } }, diff --git a/lib/view_model/support_view_model.dart b/lib/view_model/support_view_model.dart index 766bcc9a2..524b3fb2e 100644 --- a/lib/view_model/support_view_model.dart +++ b/lib/view_model/support_view_model.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/view_model/settings/link_list_item.dart'; import 'package:cake_wallet/view_model/settings/settings_list_item.dart'; @@ -11,9 +12,10 @@ part 'support_view_model.g.dart'; class SupportViewModel = SupportViewModelBase with _$SupportViewModel; abstract class SupportViewModelBase with Store { - final SettingsStore settingsStore; + final SettingsStore _settingsStore; + final AppStore _appStore; - SupportViewModelBase(this.settingsStore) + SupportViewModelBase(this._settingsStore, this._appStore) : items = [ LinkListItem( title: 'Email', @@ -120,7 +122,11 @@ abstract class SupportViewModelBase with Store { } String get appVersion => - "${isMoneroOnly ? "Monero.com" : "Cake Wallet"} - ${settingsStore.appVersion}"; + "${isMoneroOnly ? "Monero.com" : "Cake Wallet"} - ${_settingsStore.appVersion}"; + + String get fiatApiMode => _settingsStore.fiatApiMode.title; + + String get walletType => _appStore.wallet?.type.name ?? 'Unknown'; List items; } From b387c6182ae504ae7a55f5ad8bfc7f88af79f1a3 Mon Sep 17 00:00:00 2001 From: Serhii Date: Tue, 11 Mar 2025 21:51:52 +0200 Subject: [PATCH 034/243] Update list of trocador providers to be fetched from the api (#1379) * fetch providers from Trocadore api * Update exchange_confirm_page.dart * Update exchange_view_model.dart * update trocador providers page * Revert "Update exchange_view_model.dart" This reverts commit 5722ed768a8a5e21deaaaec37d9436a7e3a2651f. * update exchange view model * pass the TrocadorExchangeProvider as a dependency [skip ci] * add missing localozation [skip ci] * minor fix * address review comments[skip ci] * update states when the provider is enabled * add kyc rating * sort providers by KYC * minor fix * Update exchange_view_model.dart * Update lib/exchange/provider/trocador_exchange_provider.dart Co-authored-by: Omar Hatem * backward compatibility for Trocador provider states * minor fix --------- Co-authored-by: Omar Hatem --- lib/di.dart | 10 +- lib/entities/preferences_key.dart | 1 + .../provider/trocador_exchange_provider.dart | 70 ++++++++++--- .../exchange_trade/exchange_confirm_page.dart | 47 ++++++--- .../settings/trocador_providers_page.dart | 97 ++++++++++++++++--- lib/store/settings_store.dart | 47 +++++++-- .../exchange/exchange_view_model.dart | 22 +++++ .../trocador_providers_view_model.dart | 35 ++++++- 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_fr.arb | 2 + res/values/strings_ha.arb | 2 + res/values/strings_hi.arb | 2 + res/values/strings_hr.arb | 2 + res/values/strings_id.arb | 2 + res/values/strings_it.arb | 2 + res/values/strings_ja.arb | 2 + res/values/strings_ko.arb | 2 + res/values/strings_my.arb | 2 + res/values/strings_nl.arb | 2 + res/values/strings_pl.arb | 2 + res/values/strings_pt.arb | 2 + res/values/strings_ru.arb | 2 + res/values/strings_th.arb | 2 + res/values/strings_tl.arb | 2 + res/values/strings_tr.arb | 2 + res/values/strings_uk.arb | 2 + res/values/strings_ur.arb | 2 + res/values/strings_yo.arb | 2 + res/values/strings_zh.arb | 2 + 34 files changed, 327 insertions(+), 54 deletions(-) diff --git a/lib/di.dart b/lib/di.dart index c4ecb74ca..7e844b37c 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -33,6 +33,11 @@ import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/hardware_wallet/require_hardware_wallet_connection.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; +import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; +import 'package:cake_wallet/view_model/link_view_model.dart'; +import 'package:cake_wallet/tron/tron.dart'; +import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; +import 'package:cw_core/receive_page_option.dart'; import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart'; import 'package:cake_wallet/entities/wallet_manager.dart'; import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart'; @@ -897,7 +902,10 @@ Future setup({ return PrivacySettingsViewModel(getIt.get(), getIt.get().wallet!); }); - getIt.registerFactory(() => TrocadorProvidersViewModel(getIt.get())); + getIt.registerFactory(() => TrocadorExchangeProvider()); + + getIt.registerFactory(() => TrocadorProvidersViewModel( + getIt.get(), getIt.get())); getIt.registerFactory(() { return OtherSettingsViewModel(getIt.get(), getIt.get().wallet!, diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 15baa02b8..7fbdb645a 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -86,6 +86,7 @@ class PreferencesKey { '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}'; static const exchangeProvidersSelection = 'exchange-providers-selection'; + static const trocadorProviderStatesKey = 'trocador_provider_states'; static const autoGenerateSubaddressStatusKey = 'auto_generate_subaddress_status'; static const moneroSeedType = 'monero_seed_type'; static const bitcoinSeedType = 'bitcoin_seed_type'; diff --git a/lib/exchange/provider/trocador_exchange_provider.dart b/lib/exchange/provider/trocador_exchange_provider.dart index 4d262049f..26a9b2e35 100644 --- a/lib/exchange/provider/trocador_exchange_provider.dart +++ b/lib/exchange/provider/trocador_exchange_provider.dart @@ -19,7 +19,7 @@ class TrocadorExchangeProvider extends ExchangeProvider { super(pairList: supportedPairs(_notSupported)); bool useTorOnly; - final Map providerStates; + Map providerStates; static const List availableProviders = [ 'Swapter', @@ -59,6 +59,8 @@ class TrocadorExchangeProvider extends ExchangeProvider { static const createTradePath = '/new_trade'; static const tradePath = '/trade'; static const coinPath = '/coin'; + static const providersListPath = '/exchanges'; + String _lastUsedRateId; List _provider; @@ -147,7 +149,14 @@ class TrocadorExchangeProvider extends ExchangeProvider { final rateId = responseJSON['trade_id'] as String? ?? ''; var quotes = responseJSON['quotes']['quotes'] as List; - _provider = quotes.map((quote) => quote['provider']).toList(); + _provider = quotes + .where((quote) => providerStates[quote['provider']] != false) + .map((quote) => quote['provider']) + .toList(); + + if (_provider.isEmpty) { + throw Exception('No enabled providers found for the selected trade.'); + } if (rateId.isNotEmpty) _lastUsedRateId = rateId; @@ -190,20 +199,11 @@ class TrocadorExchangeProvider extends ExchangeProvider { params['id'] = _lastUsedRateId; } - String firstAvailableProvider = ''; - - for (var provider in _provider) { - if (providerStates.containsKey(provider) && providerStates[provider] == true) { - firstAvailableProvider = provider as String; - break; - } - } - - if (firstAvailableProvider.isEmpty) { + if (_provider.isEmpty) { throw Exception('No available provider is enabled'); } - params['provider'] = firstAvailableProvider; + params['provider'] = _provider.first as String; final uri = await _getUri(createTradePath, params); final response = await get(uri, headers: {'API-Key': apiKey}); @@ -288,6 +288,24 @@ class TrocadorExchangeProvider extends ExchangeProvider { }); } + Future> fetchProviders() async { + final uri = await _getUri(providersListPath, {'api_key': apiKey}); + final response = await get(uri); + + if (response.statusCode != 200) + throw Exception('Unexpected http status: ${response.statusCode}'); + + final responseJSON = json.decode(response.body) as Map; + + final providersJsonList = responseJSON['list'] as List; + final filteredProvidersList = providersJsonList + .map((providerJson) => TrocadorPartners.fromJson(providerJson as Map)) + .where((provider) => provider.rating != 'D') + .toList(); + filteredProvidersList.sort((a, b) => a.rating.compareTo(b.rating)); + return filteredProvidersList; + } + String _networkFor(CryptoCurrency currency) { switch (currency) { case CryptoCurrency.eth: @@ -343,3 +361,29 @@ class TrocadorExchangeProvider extends ExchangeProvider { } } } + +class TrocadorPartners { + final String name; + final String rating; + final double? insurance; + final bool? enabledMarkup; + final double? eta; + + TrocadorPartners({ + required this.name, + required this.rating, + required this.insurance, + required this.enabledMarkup, + required this.eta, + }); + + factory TrocadorPartners.fromJson(Map json) { + return TrocadorPartners( + name: json['name'] as String? ?? '', + rating: json['rating'] as String? ?? 'N/A', + insurance: json['insurance'] as double?, + enabledMarkup: json['enabledmarkup'] as bool?, + eta: json['eta'] as double?, + ); + } +} diff --git a/lib/src/screens/exchange_trade/exchange_confirm_page.dart b/lib/src/screens/exchange_trade/exchange_confirm_page.dart index a179ee473..cbf29b19b 100644 --- a/lib/src/screens/exchange_trade/exchange_confirm_page.dart +++ b/lib/src/screens/exchange_trade/exchange_confirm_page.dart @@ -97,22 +97,39 @@ class ExchangeConfirmPage extends BasePage { ], ), ), - Flexible( - child: Center( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - (trade.provider.image?.isNotEmpty ?? false) - ? ImageUtil.getImageFromPath( - imagePath: trade.provider.image, width: 50) - : const SizedBox(), - if (!trade.provider.horizontalLogo) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Text(trade.provider.title), + Expanded( + child: Column( + children: [ + if (trade.provider == ExchangeProviderDescription.trocador) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + S.of(context).selected_trocador_provider +':${trade.providerName}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12.0, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.detailsTitlesColor), + ), + ), + Flexible( + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + (trade.provider.image?.isNotEmpty ?? false) + ? Image.asset(trade.provider.image, height: 50) + : const SizedBox(), + if (!trade.provider.horizontalLogo) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text(trade.provider.title), + ), + ], ), - ], - ), + ), + ), + ], ), ), ], diff --git a/lib/src/screens/settings/trocador_providers_page.dart b/lib/src/screens/settings/trocador_providers_page.dart index b972c22d5..d0646bb3a 100644 --- a/lib/src/screens/settings/trocador_providers_page.dart +++ b/lib/src/screens/settings/trocador_providers_page.dart @@ -1,4 +1,4 @@ -import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; import 'package:cake_wallet/view_model/settings/trocador_providers_view_model.dart'; @@ -15,23 +15,92 @@ class TrocadorProvidersPage extends BasePage { @override Widget body(BuildContext context) { - final availableProviders = TrocadorExchangeProvider.availableProviders; - final providerStates = trocadorProvidersViewModel.providerStates; return Container( padding: EdgeInsets.only(top: 10), - child: ListView.builder( - itemCount: availableProviders.length, - itemBuilder: (_, index) { - String provider = availableProviders[index]; - return Observer( - builder: (_) => SettingsSwitcherCell( - title: provider, - value: providerStates[provider] ?? false, - onValueChange: (BuildContext _, bool value) { - trocadorProvidersViewModel.toggleProviderState(provider); - })); + child: Observer( + builder: (_) { + if (trocadorProvidersViewModel.isLoading) { + return Center(child: CircularProgressIndicator()); + } + var providerStates = trocadorProvidersViewModel.providerStates; + final providerRatings = trocadorProvidersViewModel.providerRatings; + if (providerStates.isEmpty) { + return Center(child: Text(S.of(context).no_providers_available)); + } + return ListView.builder( + itemCount: providerStates.length, + itemBuilder: (_, index) { + final providerName = providerStates.keys.elementAt(index); + final providerEnabled = providerStates[providerName] ?? true; + return SettingsSwitcherCell( + title: providerName, + value: providerEnabled, + leading: Badge( + title: 'KYC \nRATING', + subTitle: providerRatings[providerName] ?? 'N/A', + textColor: Colors.white, + backgroundColor: Theme.of(context).primaryColor, + ), + onValueChange: (BuildContext _, value) => + trocadorProvidersViewModel.toggleProviderState(providerName), + ); + }, + ); }, ), ); } } + +class Badge extends StatelessWidget { + Badge({required this.textColor, required this.backgroundColor, required this.title, required this.subTitle}); + + final String title; + final String subTitle; + final Color textColor; + final Color backgroundColor; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(right: 8), + child: FittedBox( + fit: BoxFit.fitHeight, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(24)), color: backgroundColor), + alignment: Alignment.center, + child: IntrinsicHeight( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + title, + textAlign: TextAlign.center, + style: TextStyle( + color: textColor, + fontSize: 7, + fontWeight: FontWeight.w600, + ), + ), + VerticalDivider( + color: textColor, + thickness: 1, + ), + Text( + subTitle, + style: TextStyle( + color: textColor, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 800c3634e..ef9ab94a4 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; @@ -224,12 +225,11 @@ abstract class SettingsStoreBase with Store { if (initialZanoTransactionPriority != null) { priority[WalletType.zano] = initialZanoTransactionPriority; } + if (initialCakePayCountry != null) { selectedCakePayCountry = initialCakePayCountry; } - initializeTrocadorProviderStates(); - reaction( (_) => fiatCurrency, (FiatCurrency fiatCurrency) => sharedPreferences.setString( @@ -1708,16 +1708,47 @@ abstract class SettingsStoreBase with Store { powNodes[walletType] = node; } - void initializeTrocadorProviderStates() { - for (var provider in TrocadorExchangeProvider.availableProviders) { - final savedState = _sharedPreferences.getBool(provider) ?? true; - trocadorProviderStates[provider] = savedState; + @action + Future updateAllTrocadorProviderStates(List availableProviders) async { + final jsonKey = PreferencesKey.trocadorProviderStatesKey; + String? serializedData = await _sharedPreferences.getString(jsonKey); + + if (serializedData == null) { + final Map migratedStates = {}; + for (final provider in TrocadorExchangeProvider.availableProviders) { + final oldState = _sharedPreferences.getBool(provider) ?? true; + migratedStates[provider] = oldState; + } + + trocadorProviderStates + ..clear() + ..addAll(migratedStates); + + await saveMapToString(jsonKey, trocadorProviderStates); + } else { + final decodedMap = json.decode(serializedData) as Map; + final oldMap = decodedMap.map((k, v) => MapEntry(k, v == true)); + + final Map newStates = { + for (final provider in availableProviders) provider: oldMap[provider] ?? true + }; + + trocadorProviderStates + ..clear() + ..addAll(newStates); + await saveMapToString(jsonKey, trocadorProviderStates); } } - void saveTrocadorProviderState(String providerName, bool state) { - _sharedPreferences.setBool(providerName, state); + @action + Future setTrocadorProviderState(String providerName, bool state) async { trocadorProviderStates[providerName] = state; + await saveMapToString(PreferencesKey.trocadorProviderStatesKey, trocadorProviderStates); + } + + Future saveMapToString(String key, Map map) async { + String serializedData = json.encode(map); + await _sharedPreferences.setString(key, serializedData); } static Future _getDeviceName() async { diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index f10860c7d..6a3b32b9b 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -118,6 +118,16 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with _setAvailableProviders(); calculateBestRate(); + autorun((_) { + if (selectedProviders.any((provider) => provider is TrocadorExchangeProvider)) { + final trocadorProvider = + selectedProviders.firstWhere((provider) => provider is TrocadorExchangeProvider) + as TrocadorExchangeProvider; + + updateAllTrocadorProviderStates(trocadorProvider); + } + }); + bestRateSync = Timer.periodic(Duration(seconds: 10), (timer) => calculateBestRate()); isDepositAddressEnabled = !(depositCurrency == wallet.currency); @@ -183,6 +193,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates), ]; + @observable ExchangeProvider? provider; @@ -810,6 +821,17 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with ); } + @action + Future updateAllTrocadorProviderStates(TrocadorExchangeProvider trocadorProvider) async { + try { + var providers = await trocadorProvider.fetchProviders(); + var providerNames = providers.map((e) => e.name).toList(); + await _settingsStore.updateAllTrocadorProviderStates(providerNames); + } catch (e) { + printV('Error updating trocador provider states: $e'); + } + } + bool get isAvailableInSelected { final providersForPair = providersForCurrentPair(); return selectedProviders diff --git a/lib/view_model/settings/trocador_providers_view_model.dart b/lib/view_model/settings/trocador_providers_view_model.dart index 19204d1f9..98587da08 100644 --- a/lib/view_model/settings/trocador_providers_view_model.dart +++ b/lib/view_model/settings/trocador_providers_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:mobx/mobx.dart'; @@ -6,9 +7,37 @@ part 'trocador_providers_view_model.g.dart'; class TrocadorProvidersViewModel = TrocadorProvidersViewModelBase with _$TrocadorProvidersViewModel; abstract class TrocadorProvidersViewModelBase with Store { - TrocadorProvidersViewModelBase(this._settingsStore); + TrocadorProvidersViewModelBase(this._settingsStore, this.trocadorExchangeProvider) { + fetchTrocadorPartners(); + } final SettingsStore _settingsStore; + final TrocadorExchangeProvider trocadorExchangeProvider; + + @observable + ObservableFuture>? fetchProvidersFuture; + + Map providerRatings = {}; + + @computed + bool get isLoading => fetchProvidersFuture?.status == FutureStatus.pending; + + @action + Future fetchTrocadorPartners() async { + fetchProvidersFuture = + ObservableFuture(trocadorExchangeProvider.fetchProviders().then((providers) { + var providerNames = providers.map((e) => e.name).toList(); + + providerRatings = { + for (var provider in providers) + provider.name: provider.rating + }; + + return _settingsStore + .updateAllTrocadorProviderStates(providerNames) + .then((_) => _settingsStore.trocadorProviderStates); + })); + } @computed Map get providerStates => _settingsStore.trocadorProviderStates; @@ -16,6 +45,6 @@ abstract class TrocadorProvidersViewModelBase with Store { @action void toggleProviderState(String providerName) { final currentState = providerStates[providerName] ?? false; - _settingsStore.saveTrocadorProviderState(providerName, !currentState); + _settingsStore.setTrocadorProviderState(providerName, !currentState); } -} \ No newline at end of file +} diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index faf14e18d..ecbf2ad6f 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -459,6 +459,7 @@ "no_cards_found": "لم يتم العثور على بطاقات", "no_id_needed": "لا حاجة لID!", "no_id_required": "لا ID مطلوب. اشحن وانفق في أي مكان", + "no_providers_available": "لا مقدمي الخدمات المتاحة", "no_relay_on_domain": ".ﻡﺍﺪﺨﺘﺳﻼﻟ ﻊﺑﺎﺘﺘﻟﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ .ﺡﺎﺘﻣ ﺮﻴﻏ ﻞﻴﺣﺮﺘﻟﺍ ﻥﺃ ﻭﺃ ﻡﺪﺨﺘﺴﻤﻟﺍ ﻝﺎﺠﻤﻟ ﻞﻴﺣﺮﺗ ﺪ", "no_relays": "ﺕﻼﺣﺮﻤﻟﺍ ﻻ", "no_relays_message": ".ﻪﺑ ﺹﺎﺨﻟﺍ Nostr ﻞﺠﺳ ﻰﻟﺇ ﺕﻼﺣﺮﻤﻟﺍ ﺔﻓﺎﺿﻹ ﻢﻠﺘﺴﻤﻟﺍ ﺩﺎﺷﺭﺇ ﻰﺟﺮﻳ .ﺕﻼﺣﺮﻣ ﻱﺃ ﻰﻠﻋ ﻱﻮﺘﺤﻳ ﻻ", @@ -674,6 +675,7 @@ "select_destination": ".ﻲﻃﺎﻴﺘﺣﻻﺍ ﺦﺴﻨﻟﺍ ﻒﻠﻣ ﺔﻬﺟﻭ ﺪﻳﺪﺤﺗ ءﺎﺟﺮﻟﺍ", "select_hw_account_below": "الرجاء تحديد حساب الاستعادة أدناه:", "select_sell_provider_notice": ".ﻖﻴﺒﻄﺘﻟﺍ ﺕﺍﺩﺍﺪﻋﺇ ﻲﻓ ﻚﺑ ﺹﺎﺨﻟﺍ ﻲﺿﺍﺮﺘﻓﻻﺍ ﻊﻴﺒﻟﺍ ﺩﻭﺰﻣ ﻦﻴﻴﻌﺗ ﻖﻳﺮﻃ ﻦﻋ ﺔﺷﺎﺸﻟﺍ ﻩﺬﻫ ﻲﻄﺨﺗ", + "selected_trocador_provider": "مزود تروكادور المختار", "select_your_country": "الرجاء تحديد بلدك", "sell": "بيع", "sell_alert_content": ".ﺎﻬﻴﻟﺇ ﻞﻳﺪﺒﺘﻟﺍ ﻭﺃ Litecoin ﻭﺃ Ethereum ﻭﺃ Bitcoin ﺔﻈﻔﺤﻣ ءﺎﺸﻧﺇ ﻰﺟﺮﻳ .Litecoin ﻭ", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 7ac75f00e..e1959d195 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -459,6 +459,7 @@ "no_cards_found": "Не са намерени карти", "no_id_needed": "Без нужда от документ за самоличност!", "no_id_required": "Без нужда от документ за самоличност. Използвайте навсякъде", + "no_providers_available": "Няма налични доставчици", "no_relay_on_domain": "Няма реле за домейна на потребителя или релето не е налично. Моля, изберете реле, което да използвате.", "no_relays": "Без релета", "no_relays_message": "Намерихме запис Nostr NIP-05 за този потребител, но той не съдържа релета. Моля, инструктирайте получателя да добави релета към своя Nostr запис.", @@ -674,6 +675,7 @@ "select_destination": "Моля, изберете дестинация за архивния файл.", "select_hw_account_below": "Моля, изберете кой акаунт да възстановите по -долу:", "select_sell_provider_notice": "Изберете доставчик на продажба по-горе. Можете да пропуснете този екран, като зададете своя доставчик на продажба по подразбиране в настройките на приложението.", + "selected_trocador_provider": "Избран доставчик на трокадор", "select_your_country": "Моля, изберете вашата страна", "sell": "Продаване", "sell_alert_content": "В момента поддържаме само продажбата на Bitcoin, Ethereum и Litecoin. Моля, създайте или превключете към своя портфейл Bitcoin, Ethereum или Litecoin.", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index f777ffe60..bed37c456 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -459,6 +459,7 @@ "no_cards_found": "Žádné karty nenalezeny", "no_id_needed": "Žádné ID není potřeba!", "no_id_required": "Žádní ID není potřeba. Dobijte si a utrácejte kdekoliv", + "no_providers_available": "Žádní poskytovatelé jsou k dispozici", "no_relay_on_domain": "Pro doménu uživatele neexistuje přenos nebo je přenos nedostupný. Vyberte relé, které chcete použít.", "no_relays": "Žádná relé", "no_relays_message": "Pro tohoto uživatele jsme našli záznam Nostr NIP-05, který však neobsahuje žádná relé. Požádejte příjemce, aby přidal přenosy do svého záznamu Nostr.", @@ -674,6 +675,7 @@ "select_destination": "Vyberte cíl pro záložní soubor.", "select_hw_account_below": "Níže vyberte, který účet chcete obnovit:", "select_sell_provider_notice": "Výše vyberte poskytovatele prodeje. Tuto obrazovku můžete přeskočit nastavením výchozího poskytovatele prodeje v nastavení aplikace.", + "selected_trocador_provider": "Vybraný poskytovatel Trocador", "select_your_country": "Vyberte prosím svou zemi", "sell": "Prodat", "sell_alert_content": "V současné době podporujeme pouze prodej bitcoinů, etherea a litecoinů. Vytvořte nebo přepněte na svou bitcoinovou, ethereum nebo litecoinovou peněženku.", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 35012b20d..beca8957e 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -459,6 +459,7 @@ "no_cards_found": "Keine Karten gefunden", "no_id_needed": "Keine ID erforderlich!", "no_id_required": "Keine ID erforderlich. Upgraden und überall ausgeben", + "no_providers_available": "Keine Anbieter verfügbar", "no_relay_on_domain": "Es gibt kein Relay für die Domäne des Benutzers oder das Relay ist nicht verfügbar. Bitte wählen Sie ein zu verwendendes Relais aus.", "no_relays": "Keine Relais", "no_relays_message": "Wir haben einen Nostr NIP-05-Eintrag für diesen Benutzer gefunden, der jedoch keine Relays enthält. Bitte weisen Sie den Empfänger an, Relays zu seinem Nostr-Datensatz hinzuzufügen.", @@ -675,6 +676,7 @@ "select_destination": "Bitte wählen Sie das Ziel für die Sicherungsdatei aus.", "select_hw_account_below": "Bitte wählen Sie unten, welches Konto unten wiederhergestellt werden soll:", "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.", + "selected_trocador_provider": "Ausgewählter Trocador -Anbieter", "select_your_country": "Bitte wählen Sie Ihr Land aus", "sell": "Verkaufen", "sell_alert_content": "Wir unterstützen derzeit nur den Verkauf von Bitcoin, Ethereum und Litecoin. Bitte erstellen Sie Ihr Bitcoin-, Ethereum- oder Litecoin-Wallet oder wechseln Sie zu diesem.", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 6875794fd..0bd5d7138 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -459,6 +459,7 @@ "no_cards_found": "No cards found", "no_id_needed": "No ID needed!", "no_id_required": "No ID required. Top up and spend anywhere", + "no_providers_available": "No providers available", "no_relay_on_domain": "There isn't a relay for user's domain or the relay is unavailable. Please choose a relay to use.", "no_relays": "No relays", "no_relays_message": "We found a Nostr NIP-05 record for this user, but it does not contain any relays. Please instruct the recipient to add relays to their Nostr record.", @@ -675,6 +676,7 @@ "select_destination": "Please select destination for the backup file.", "select_hw_account_below": "Please select which account to restore below:", "select_sell_provider_notice": "Select a sell provider above. You can skip this screen by setting your default sell provider in app settings.", + "selected_trocador_provider": "selected Trocador provider", "select_your_country": "Please select your country", "sell": "Sell", "sell_alert_content": "We currently only support the sale of Bitcoin, Ethereum and Litecoin. Please create or switch to your Bitcoin, Ethereum or Litecoin wallet.", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 42e7eedc0..e2da76a97 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -459,6 +459,7 @@ "no_cards_found": "No se encuentran cartas", "no_id_needed": "¡No se necesita identificación!", "no_id_required": "No se requiere identificación. Recarga y gaste en cualquier lugar", + "no_providers_available": "No hay proveedores disponibles", "no_relay_on_domain": "No hay una retransmisión para el dominio del usuario o la retransmisión no está disponible. Elige un relay para usar.", "no_relays": "Sin relays", "no_relays_message": "Encontramos un registro Nostr NIP-05 para este usuario, pero no contiene ningún relay. Indica al destinatario que agregue retransmisiones a su registro Nostr.", @@ -676,6 +677,7 @@ "select_hw_account_below": "Seleccione qué cuenta restaurar a continuación:", "select_sell_provider_notice": "Selecciona un proveedor de venta arriba. Puede omitir esta pantalla configurando su proveedor de venta predeterminado en la configuración de la aplicación.", "select_your_country": "Seleccione su país", + "selected_trocador_provider": "Proveedor de Trocador seleccionado", "sell": "Vender", "sell_alert_content": "Actualmente solo admitimos la venta de Bitcoin, Ethereum y Litecoin. Cree o cambie a su billetera Bitcoin, Ethereum o Litecoin.", "sell_monero_com_alert_content": "Aún no se admite la venta de Monero", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 4960d980a..0e65bdb08 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -459,6 +459,7 @@ "no_cards_found": "Pas de cartes trouvées", "no_id_needed": "Aucune pièce d'identité nécessaire !", "no_id_required": "Aucune pièce d'identité requise. Rechargez et dépensez n'importe où", + "no_providers_available": "Aucun fournisseur disponible", "no_relay_on_domain": "Il n'existe pas de relais pour le domaine de l'utilisateur ou le relais n'est pas disponible. Veuillez choisir un relais à utiliser.", "no_relays": "Pas de relais", "no_relays_message": "Nous avons trouvé un enregistrement Nostr NIP-05 pour cet utilisateur, mais il ne contient aucun relais. Veuillez demander au destinataire d'ajouter des relais à son enregistrement Nostr.", @@ -674,6 +675,7 @@ "select_destination": "Veuillez sélectionner la destination du fichier de sauvegarde.", "select_hw_account_below": "Veuillez sélectionner le compte à restaurer ci-dessous:", "select_sell_provider_notice": "Sélectionnez un fournisseur de vente ci-dessus. Vous pouvez ignorer cet écran en définissant votre fournisseur de vente par défaut dans les paramètres de l'application.", + "selected_trocador_provider": "fournisseur de trocador sélectionné", "select_your_country": "Veuillez sélectionner votre pays", "sell": "Vendre", "sell_alert_content": "Nous ne prenons actuellement en charge que la vente de Bitcoin, Ethereum et Litecoin. Veuillez créer ou basculer vers votre portefeuille Bitcoin, Ethereum ou Litecoin.", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 068457568..53cfd164b 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -459,6 +459,7 @@ "no_cards_found": "Babu katunan da aka samo", "no_id_needed": "Babu ID da ake buƙata!", "no_id_required": "Babu ID da ake buƙata. Yi da kuma ciyar a ko'ina", + "no_providers_available": "Babu masu samar da wadatar", "no_relay_on_domain": "Babu gudun ba da sanda ga yankin mai amfani ko kuma ba a samu ba. Da fatan za a zaɓi gudun ba da sanda don amfani.", "no_relays": "Babu relays", "no_relays_message": "Mun sami rikodin Nostr NIP-05 don wannan mai amfani, amma ba ya ƙunshe da kowane relays. Da fatan za a umurci mai karɓa ya ƙara relays zuwa rikodin su na Nostr.", @@ -676,6 +677,7 @@ "select_destination": "Da fatan za a zaɓi wurin da za a yi wa madadin fayil ɗin.", "select_hw_account_below": "Da fatan za a zabi wanda asusun zai gyara a ƙasa:", "select_sell_provider_notice": "Zaɓi mai bada siyarwa a sama. Kuna iya tsallake wannan allon ta saita mai bada siyar da ku a cikin saitunan app.", + "selected_trocador_provider": "Zabi mai bada TORACAD", "select_your_country": "Da fatan za a zabi ƙasarku", "sell": "sayar", "sell_alert_content": "A halin yanzu muna tallafawa kawai siyar da Bitcoin, Ethereum da Litecoin. Da fatan za a ƙirƙiri ko canza zuwa walat ɗin ku na Bitcoin, Ethereum ko Litecoin.", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 248b0230c..002aa8246 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -459,6 +459,7 @@ "no_cards_found": "कोई कार्ड नहीं मिला", "no_id_needed": "कोई आईडी नहीं चाहिए!", "no_id_required": "कोई आईडी आवश्यक नहीं है। टॉप अप करें और कहीं भी खर्च करें", + "no_providers_available": "कोई प्रदाता उपलब्ध नहीं है", "no_relay_on_domain": "उपयोगकर्ता के डोमेन के लिए कोई रिले नहीं है या रिले अनुपलब्ध है। कृपया उपयोग करने के लिए एक रिले चुनें।", "no_relays": "कोई रिले नहीं", "no_relays_message": "हमें इस उपयोगकर्ता के लिए एक Nostr NIP-05 रिकॉर्ड मिला, लेकिन इसमें कोई रिले नहीं है। कृपया प्राप्तकर्ता को अपने नॉस्ट्र रिकॉर्ड में रिले जोड़ने का निर्देश दें।", @@ -676,6 +677,7 @@ "select_destination": "कृपया बैकअप फ़ाइल के लिए गंतव्य का चयन करें।", "select_hw_account_below": "कृपया नीचे पुनर्स्थापित करने के लिए कौन सा खाता चुनें:", "select_sell_provider_notice": "ऊपर एक विक्रय प्रदाता का चयन करें। आप ऐप सेटिंग में अपना डिफ़ॉल्ट विक्रय प्रदाता सेट करके इस स्क्रीन को छोड़ सकते हैं।", + "selected_trocador_provider": "चयनित ट्रोकैडर प्रदाता", "select_your_country": "कृपया अपने देश का चयन करें", "sell": "बेचना", "sell_alert_content": "हम वर्तमान में केवल बिटकॉइन, एथेरियम और लाइटकॉइन की बिक्री का समर्थन करते हैं। कृपया अपना बिटकॉइन, एथेरियम या लाइटकॉइन वॉलेट बनाएं या उसमें स्विच करें।", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 5c83942a1..45fc66005 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -459,6 +459,7 @@ "no_cards_found": "Nisu pronađene kartice", "no_id_needed": "Nije potreban ID!", "no_id_required": "Nije potreban ID. Nadopunite i potrošite bilo gdje", + "no_providers_available": "Nema dostupnih pružatelja usluga", "no_relay_on_domain": "Ne postoji relej za korisničku domenu ili je relej nedostupan. Odaberite relej za korištenje.", "no_relays": "Nema releja", "no_relays_message": "Pronašli smo zapis Nostr NIP-05 za ovog korisnika, ali on ne sadrži nikakve releje. Uputite primatelja da doda releje u svoj Nostr zapis.", @@ -674,6 +675,7 @@ "select_destination": "Odaberite odredište za datoteku sigurnosne kopije.", "select_hw_account_below": "Molimo odaberite koji će se račun vratiti u nastavku:", "select_sell_provider_notice": "Gore odaberite pružatelja usluga prodaje. Ovaj zaslon možete preskočiti postavljanjem zadanog pružatelja usluga prodaje u postavkama aplikacije.", + "selected_trocador_provider": "Odabrani pružatelj usluga trokadora", "select_your_country": "Odaberite svoju zemlju", "sell": "Prodavati", "sell_alert_content": "Trenutno podržavamo samo prodaju Bitcoina, Ethereuma i Litecoina. Izradite ili prijeđite na svoj Bitcoin, Ethereum ili Litecoin novčanik.", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index ccf4367e3..f5b0bb285 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -459,6 +459,7 @@ "no_cards_found": "Tidak ada kartu yang ditemukan", "no_id_needed": "Tidak perlu ID!", "no_id_required": "Tidak perlu ID. Isi ulang dan belanja di mana saja", + "no_providers_available": "Tidak ada penyedia yang tersedia", "no_relay_on_domain": "Tidak ada relai untuk domain pengguna atau relai tidak tersedia. Silakan pilih relai yang akan digunakan.", "no_relays": "Tidak ada relay", "no_relays_message": "Kami menemukan catatan Nostr NIP-05 untuk pengguna ini, tetapi tidak berisi relay apa pun. Harap instruksikan penerima untuk menambahkan relay ke catatan Nostr mereka.", @@ -677,6 +678,7 @@ "select_destination": "Silakan pilih tujuan untuk file cadangan.", "select_hw_account_below": "Pilih akun mana yang akan dikembalikan di bawah ini:", "select_sell_provider_notice": "Pilih penyedia jual di atas. Anda dapat melewati layar ini dengan mengatur penyedia penjualan default Anda di pengaturan aplikasi.", + "selected_trocador_provider": "Penyedia Trocador Terpilih", "select_your_country": "Pilih negara Anda", "sell": "Jual", "sell_alert_content": "Saat ini kami hanya mendukung penjualan Bitcoin, Ethereum, dan Litecoin. Harap buat atau alihkan ke dompet Bitcoin, Ethereum, atau Litecoin Anda.", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index a1682a11f..3c55f909d 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -459,6 +459,7 @@ "no_cards_found": "Nessuna carta trovata", "no_id_needed": "Nessun ID necessario!", "no_id_required": "Nessun ID richiesto. Ricarica e spendi ovunque", + "no_providers_available": "Nessun fornitore disponibile", "no_relay_on_domain": "Non esiste un inoltro per il dominio dell'utente oppure l'inoltro non è disponibile. Scegli un relay da utilizzare.", "no_relays": "Nessun relay", "no_relays_message": "Abbiamo trovato un record Nostr NIP-05 per questo utente, ma non contiene alcun relay. Indica al destinatario di aggiungere relay al proprio record Nostr.", @@ -673,6 +674,7 @@ "select_backup_file": "Seleziona file di backup", "select_buy_provider_notice": "Seleziona un provider di acquisto sopra. È possibile saltare questa schermata impostando il provider di acquisto predefinito nelle impostazioni dell'app.", "select_destination": "Seleziona la destinazione per il file di backup.", + "selected_trocador_provider": "Provider di Trocador selezionato", "select_hw_account_below": "Seleziona quale account ripristinare:", "select_sell_provider_notice": "Seleziona un provider di vendita sopra. Puoi saltare questa schermata impostando il tuo provider di vendita predefinito nelle impostazioni dell'app.", "select_your_country": "Seleziona il tuo paese", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index f97baf7b4..23414628a 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -460,6 +460,7 @@ "no_cards_found": "カードは見つかりません", "no_id_needed": "IDは必要ありません!", "no_id_required": "IDは必要ありません。どこにでも補充して使用できます", + "no_providers_available": "プロバイダーは利用できません", "no_relay_on_domain": "ユーザーのドメインのリレーが存在しないか、リレーが使用できません。使用するリレーを選択してください。", "no_relays": "リレーなし", "no_relays_message": "このユーザーの Nostr NIP-05 レコードが見つかりましたが、リレーは含まれていません。受信者に Nostr レコードにリレーを追加するよう指示してください。", @@ -675,6 +676,7 @@ "select_destination": "バックアップファイルの保存先を選択してください。", "select_hw_account_below": "以下に復元するアカウントを選択してください。", "select_sell_provider_notice": "上記の販売プロバイダーを選択してください。アプリ設定でデフォルトの販売プロバイダーを設定することで、この画面をスキップできます。", + "selected_trocador_provider": "選択したTrocadorプロバイダー", "select_your_country": "あなたの国を選択してください", "sell": "売る", "sell_alert_content": "現在、ビットコイン、イーサリアム、ライトコインの販売のみをサポートしています。ビットコイン、イーサリアム、またはライトコインのウォレットを作成するか、これらのウォレットに切り替えてください。", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 75af5c195..6b73f54b8 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -459,6 +459,7 @@ "no_cards_found": "카드를 찾지 못했습니다", "no_id_needed": "ID가 필요하지 않습니다!", "no_id_required": "신분증이 필요하지 않습니다. 충전하고 어디에서나 사용하세요", + "no_providers_available": "제공되는 제공자가 없습니다", "no_relay_on_domain": "사용자 도메인에 릴레이가 없거나 릴레이를 사용할 수 없습니다. 사용할 릴레이를 선택해주세요.", "no_relays": "릴레이 없음", "no_relays_message": "이 사용자에 대한 Nostr NIP-05 레코드를 찾았지만 릴레이가 포함되어 있지 않습니다. 수신자에게 Nostr 기록에 릴레이를 추가하도록 지시하십시오.", @@ -674,6 +675,7 @@ "select_destination": "백업 파일의 대상을 선택하십시오.", "select_hw_account_below": "아래를 복원 할 계정을 선택하십시오.", "select_sell_provider_notice": "위에서 판매 공급자를 선택하세요. 앱 설정에서 기본 판매 공급자를 설정하면 이 화면을 건너뛸 수 있습니다.", + "selected_trocador_provider": "선정 된 트로 코더 제공 업체", "select_your_country": "국가를 선택하십시오", "sell": "팔다", "sell_alert_content": "현재 Bitcoin, Ethereum 및 Litecoin의 판매만 지원합니다. Bitcoin, Ethereum 또는 Litecoin 지갑을 생성하거나 전환하십시오.", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 9f200a030..9a86dda6c 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -459,6 +459,7 @@ "no_cards_found": "ကဒ်များမရှိပါ", "no_id_needed": "ID မလိုအပ်ပါ။", "no_id_required": "ID မလိုအပ်ပါ။ ငွေဖြည့်ပြီး ဘယ်နေရာမဆို သုံးစွဲပါ။", + "no_providers_available": "မရရှိနိုင်ပါ", "no_relay_on_domain": "အသုံးပြုသူ၏ဒိုမိန်းအတွက် ထပ်ဆင့်လွှင့်ခြင်း မရှိပါ သို့မဟုတ် ထပ်ဆင့်လွှင့်ခြင်း မရနိုင်ပါ။ အသုံးပြုရန် relay ကိုရွေးချယ်ပါ။", "no_relays": "Relay မရှိပါ။", "no_relays_message": "ဤအသုံးပြုသူအတွက် Nostr NIP-05 မှတ်တမ်းကို ကျွန်ုပ်တို့တွေ့ရှိသော်လည်း ၎င်းတွင် မည်သည့် relays မှ မပါဝင်ပါ။ ကျေးဇူးပြု၍ လက်ခံသူကို ၎င်းတို့၏ Nostr မှတ်တမ်းတွင် ထပ်လောင်းထည့်ရန် ညွှန်ကြားပါ။", @@ -674,6 +675,7 @@ "select_destination": "အရန်ဖိုင်အတွက် ဦးတည်ရာကို ရွေးပါ။", "select_hw_account_below": "အောက်ဖော်ပြပါမည်သည့်အကောင့်ကိုရွေးပါ။", "select_sell_provider_notice": "အထက်ဖော်ပြပါ အရောင်းဝန်ဆောင်မှုပေးသူကို ရွေးပါ။ အက်ပ်ဆက်တင်များတွင် သင်၏မူလရောင်းချပေးသူကို သတ်မှတ်ခြင်းဖြင့် ဤစခရင်ကို ကျော်နိုင်သည်။", + "selected_trocador_provider": "ရွေးချယ်ထား Tracador ပံ့ပိုးပေး", "select_your_country": "ကျေးဇူးပြု. သင့်နိုင်ငံကိုရွေးချယ်ပါ", "sell": "ရောင်း", "sell_alert_content": "ကျွန်ုပ်တို့သည် လက်ရှိတွင် Bitcoin၊ Ethereum နှင့် Litecoin ရောင်းချခြင်းကိုသာ ပံ့ပိုးပေးပါသည်။ သင်၏ Bitcoin၊ Ethereum သို့မဟုတ် Litecoin ပိုက်ဆံအိတ်ကို ဖန်တီးပါ သို့မဟုတ် ပြောင်းပါ။", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 1b41eea22..69280dbbe 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -459,6 +459,7 @@ "no_cards_found": "Geen kaarten gevonden", "no_id_needed": "Geen ID nodig!", "no_id_required": "Geen ID vereist. Opwaarderen en overal uitgeven", + "no_providers_available": "Geen providers beschikbaar", "no_relay_on_domain": "Er is geen relay voor het domein van de gebruiker of de relay is niet beschikbaar. Kies een relais dat u wilt gebruiken.", "no_relays": "Geen relais", "no_relays_message": "We hebben een Nostr NIP-05-record voor deze gebruiker gevonden, maar deze bevat geen relays. Instrueer de ontvanger om relays toe te voegen aan zijn Nostr-record.", @@ -674,6 +675,7 @@ "select_destination": "Selecteer de bestemming voor het back-upbestand.", "select_hw_account_below": "Selecteer welk account u hieronder moet herstellen:", "select_sell_provider_notice": "Selecteer hierboven een verkoopaanbieder. U kunt dit scherm overslaan door uw standaardverkoopprovider in te stellen in de app-instellingen.", + "selected_trocador_provider": "Geselecteerde Trocador -provider", "select_your_country": "Selecteer uw land", "sell": "Verkopen", "sell_alert_content": "We ondersteunen momenteel alleen de verkoop van Bitcoin, Ethereum en Litecoin. Maak of schakel over naar uw Bitcoin-, Ethereum- of Litecoin-portemonnee.", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 8b96bde04..0e57ecf8c 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -459,6 +459,7 @@ "no_cards_found": "Nie znaleziono żadnych kart", "no_id_needed": "Nie potrzeba Dowodu!", "no_id_required": "Nie wymagamy Dowodu. Doładuj i wydawaj gdziekolwiek", + "no_providers_available": "Brak dostępnych dostawców", "no_relay_on_domain": "Brak przekaźnika dla domeny użytkownika lub przekaźnik jest niedostępny. Wybierz przekaźnik, którego chcesz użyć.", "no_relays": "Żadnych przekaźników", "no_relays_message": "Znaleźliśmy rekord Nostr NIP-05 dla tego użytkownika, ale nie zawiera on żadnych przekaźników. Poinstruuj odbiorcę, aby dodał przekaźniki do swojego rekordu Nostr.", @@ -674,6 +675,7 @@ "select_destination": "Wybierz miejsce docelowe dla pliku kopii zapasowej.", "select_hw_account_below": "Wybierz, które konto przywrócić poniżej:", "select_sell_provider_notice": "Wybierz dostawcę sprzedaży powyżej. Możesz pominąć ten ekran, ustawiając domyślnego dostawcę sprzedaży w ustawieniach aplikacji.", + "selected_trocador_provider": "Wybrany dostawca Trocador", "select_your_country": "Wybierz swój kraj", "sell": "Sprzedać", "sell_alert_content": "Obecnie obsługujemy tylko sprzedaż Bitcoin, Ethereum i Litecoin. Utwórz lub przełącz się na swój portfel Bitcoin, Ethereum lub Litecoin.", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 096c3f413..ccc87926d 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -460,6 +460,7 @@ "no_cards_found": "Nenhum cartão encontrado", "no_id_needed": "Nenhum ID necessário!", "no_id_required": "Não é necessário ID. Recarregue e gaste em qualquer lugar", + "no_providers_available": "Nenhum fornecedor disponível", "no_relay_on_domain": "Não há uma retransmissão para o domínio do usuário ou a retransmissão está indisponível. Escolha um relé para usar.", "no_relays": "Sem relés", "no_relays_message": "Encontramos um registro Nostr NIP-05 para este usuário, mas ele não contém nenhum relé. Instrua o destinatário a adicionar retransmissões ao seu registro Nostr.", @@ -676,6 +677,7 @@ "select_destination": "Selecione o destino para o arquivo de backup.", "select_hw_account_below": "Selecione qual conta para restaurar abaixo:", "select_sell_provider_notice": "Selecione um fornecedor de venda acima. Você pode pular esta tela definindo seu provedor de venda padrão nas configurações do aplicativo.", + "selected_trocador_provider": "Provedor de Trocador selecionado", "select_your_country": "Selecione seu país", "sell": "Vender", "sell_alert_content": "Atualmente, oferecemos suporte apenas à venda de Bitcoin, Ethereum e Litecoin. Crie ou troque para sua carteira Bitcoin, Ethereum ou Litecoin.", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 6fe82abcf..757d8b7bd 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -459,6 +459,7 @@ "no_cards_found": "Карт не найдено", "no_id_needed": "Идентификатор не нужен!", "no_id_required": "Идентификатор не требуется. Пополняйте и тратьте где угодно", + "no_providers_available": "Нет доступных поставщиков", "no_relay_on_domain": "Для домена пользователя реле не существует или реле недоступно. Пожалуйста, выберите реле для использования.", "no_relays": "Нет реле", "no_relays_message": "Мы нашли запись Nostr NIP-05 для этого пользователя, но она не содержит никаких реле. Попросите получателя добавить реле в свою запись Nostr.", @@ -675,6 +676,7 @@ "select_destination": "Пожалуйста, выберите место для файла резервной копии.", "select_hw_account_below": "Пожалуйста, выберите, какую учетную запись восстановить ниже:", "select_sell_provider_notice": "Выберите поставщика услуг продажи выше. Вы можете пропустить этот экран, установив поставщика услуг продаж по умолчанию в настройках приложения.", + "selected_trocador_provider": "Выбранный провайдер Трокадора", "select_your_country": "Пожалуйста, выберите свою страну", "sell": "Продавать", "sell_alert_content": "В настоящее время мы поддерживаем только продажу биткойнов, эфириума и лайткойна. Пожалуйста, создайте или переключитесь на свой кошелек Bitcoin, Ethereum или Litecoin.", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 35e94e508..0ac198a58 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -459,6 +459,7 @@ "no_cards_found": "ไม่พบการ์ด", "no_id_needed": "ไม่จำเป็นต้องใช้บัตรประชาชน!", "no_id_required": "ไม่จำเป็นต้องใช้บัตรประจำตัว ฝากเงินและใช้งานได้ทุกที่", + "no_providers_available": "ไม่มีผู้ให้บริการ", "no_relay_on_domain": "ไม่มีการส่งต่อสำหรับโดเมนของผู้ใช้ หรือการส่งต่อไม่พร้อมใช้งาน กรุณาเลือกรีเลย์ที่จะใช้", "no_relays": "ไม่มีรีเลย์", "no_relays_message": "เราพบบันทึก Nostr NIP-05 สำหรับผู้ใช้รายนี้ แต่ไม่มีรีเลย์ใดๆ โปรดแนะนำให้ผู้รับเพิ่มรีเลย์ลงในบันทึก Nostr ของตน", @@ -674,6 +675,7 @@ "select_destination": "โปรดเลือกปลายทางสำหรับไฟล์สำรอง", "select_hw_account_below": "กรุณาเลือกบัญชีที่จะกู้คืนด้านล่าง:", "select_sell_provider_notice": "เลือกผู้ให้บริการการขายด้านบน คุณสามารถข้ามหน้าจอนี้ได้โดยการตั้งค่าผู้ให้บริการการขายเริ่มต้นในการตั้งค่าแอป", + "selected_trocador_provider": "ผู้ให้บริการ Trocador ที่เลือก", "select_your_country": "กรุณาเลือกประเทศของคุณ", "sell": "ขาย", "sell_alert_content": "ขณะนี้เรารองรับการขาย Bitcoin, Ethereum และ Litecoin เท่านั้น โปรดสร้างหรือเปลี่ยนเป็นกระเป๋าเงิน Bitcoin, Ethereum หรือ Litecoin ของคุณ", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index d9cdbbb3e..6a17a4c9d 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -459,6 +459,7 @@ "no_cards_found": "Walang nahanap na mga card", "no_id_needed": "Hindi kailangan ng ID!", "no_id_required": "Hindi kailangan ng ID. I-top up at gumastos kahit saan", + "no_providers_available": "Walang magagamit na mga nagbibigay", "no_relay_on_domain": "Walang relay para sa domain ng user o hindi available ang relay. Mangyaring pumili ng relay na gagamitin.", "no_relays": "Walang mga relay", "no_relays_message": "Nakakita kami ng Nostr NIP-05 record para sa user na ito, ngunit hindi ito naglalaman ng anumang mga relay. Mangyaring atasan ang tatanggap na magdagdag ng mga relay sa kanilang Nostr record.", @@ -674,6 +675,7 @@ "select_destination": "Mangyaring piliin ang patutunguhan para sa backup na file.", "select_hw_account_below": "Mangyaring piliin kung aling account ang ibabalik sa ibaba:", "select_sell_provider_notice": "Pumili ng provider ng nagbebenta sa itaas. Maaari mong laktawan ang screen na ito sa pamamagitan ng pagtatakda ng iyong default na sell provider sa mga setting ng app.", + "selected_trocador_provider": "Napiling Trocador Provider", "select_your_country": "Mangyaring piliin ang iyong bansa", "sell": "Ibenta", "sell_alert_content": "Kasalukuyan lamang naming sinusuportahan ang pagbebenta ng Bitcoin, Ethereum at Litecoin. Mangyaring lumikha o lumipat sa iyong Bitcoin, Ethereum o Litecoin wallet.", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 5d7c77050..9f58e6861 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -459,6 +459,7 @@ "no_cards_found": "Kart bulunamadı", "no_id_needed": "Kimlik gerekmez!", "no_id_required": "Kimlik gerekmez. Para yükleyin ve istediğiniz yerde harcayın", + "no_providers_available": "Sağlayıcı yok", "no_relay_on_domain": "Kullanıcının alanı için bir geçiş yok veya geçiş kullanılamıyor. Lütfen kullanmak için bir röle seçin.", "no_relays": "Röle yok", "no_relays_message": "Bu kullanıcı için bir Nostr NIP-05 kaydı bulduk ancak bu kayıt herhangi bir aktarma içermiyor. Lütfen alıcıya Nostr kayıtlarına aktarma eklemesi talimatını verin.", @@ -674,6 +675,7 @@ "select_destination": "Lütfen yedekleme dosyası için hedef seçin.", "select_hw_account_below": "Lütfen aşağıda hangi hesabı geri yükleyeceğinizi seçin:", "select_sell_provider_notice": "Yukarıdan bir satış sağlayıcısı seçin. Uygulama ayarlarında varsayılan satış sağlayıcınızı ayarlayarak bu ekranı atlayabilirsiniz.", + "selected_trocador_provider": "Seçilmiş Trocador Sağlayıcı", "select_your_country": "Lütfen ülkenizi seçin", "sell": "Satış", "sell_alert_content": "Şu anda yalnızca Bitcoin, Ethereum ve Litecoin satışını destekliyoruz. Lütfen Bitcoin, Ethereum veya Litecoin cüzdanınızı oluşturun veya cüzdanınıza geçin.", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 1a6d113b0..af255c26a 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -459,6 +459,7 @@ "no_cards_found": "Карт не знайдено", "no_id_needed": "Ідентифікатор не потрібен!", "no_id_required": "Ідентифікатор не потрібен. Поповнюйте та витрачайте будь-де", + "no_providers_available": "Немає постачальників", "no_relay_on_domain": "Немає ретранслятора для домену користувача або ретранслятор недоступний. Будь ласка, виберіть реле для використання.", "no_relays": "Без реле", "no_relays_message": "Ми знайшли запис Nostr NIP-05 для цього користувача, але він не містить жодних реле. Будь ласка, попросіть одержувача додати реле до свого запису Nostr.", @@ -675,6 +676,7 @@ "select_destination": "Виберіть місце призначення для файлу резервної копії.", "select_hw_account_below": "Виберіть, який рахунок відновити нижче:", "select_sell_provider_notice": "Виберіть вище постачальника послуг продажу. Ви можете пропустити цей екран, встановивши постачальника послуг продажу за умовчанням у налаштуваннях програми.", + "selected_trocador_provider": "Вибраний постачальник Trocador", "select_your_country": "Будь ласка, виберіть свою країну", "sell": "Продати", "sell_alert_content": "Наразі ми підтримуємо лише продаж Bitcoin, Ethereum і Litecoin. Створіть або перейдіть на свій гаманець Bitcoin, Ethereum або Litecoin.", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index a7b70dae2..cb86f8af6 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -459,6 +459,7 @@ "no_cards_found": "کوئی کارڈ نہیں ملا", "no_id_needed": "شناخت کی ضرورت نہیں!", "no_id_required": "کوئی ID درکار نہیں۔ ٹاپ اپ اور کہیں بھی خرچ کریں۔", + "no_providers_available": "کوئی فراہم کنندہ دستیاب نہیں ہے", "no_relay_on_domain": "۔ﮟﯾﺮﮐ ﺏﺎﺨﺘﻧﺍ ﺎﮐ ﮯﻠﯾﺭ ﮯﯿﻟ ﮯﮐ ﮯﻧﺮﮐ ﻝﺎﻤﻌﺘﺳﺍ ﻡﺮﮐ ﮦﺍﺮﺑ ۔ﮯﮨ ﮟﯿﮩﻧ ﺏﺎﯿﺘﺳﺩ ﮯﻠﯾﺭ ﺎﯾ ﮯﮨ ﮟ", "no_relays": "۔ﮟﯿﮩﻧ ﮯﻠﯾﺭ ﯽﺋﻮﮐ", "no_relays_message": "۔ﮟﯾﺮﮐ ﻞﻣﺎﺷ ﮯﻠﯾﺭ ﮟﯿﻣ ﮈﺭﺎﮑﯾﺭ ﺮﭩﺳﻮﻧ ﮯﻨﭘﺍ ﮦﻭ ﮧﮐ ﮟﯾﺩ ﺖﯾﺍﺪﮨ ﻮﮐ ﮦﺪﻨﻨﮐ ﻝﻮﺻﻭ ﻡﺮﮐ ﮦﺍﺮﺑ ۔", @@ -676,6 +677,7 @@ "select_destination": "۔ﮟﯾﺮﮐ ﺏﺎﺨﺘﻧﺍ ﺎﮐ ﻝﺰﻨﻣ ﮯﯿﻟ ﮯﮐ ﻞﺋﺎﻓ ﭖﺍ ﮏﯿﺑ ﻡﺮﮐ ﮦﺍﺮﺑ", "select_hw_account_below": "براہ کرم ذیل میں کون سا اکاؤنٹ بحال کرنا ہے منتخب کریں:", "select_sell_provider_notice": "۔ﮟﯿﮨ ﮯﺘﮑﺳ ﮌﻮﮭﭼ ﻮﮐ ﻦﯾﺮﮑﺳﺍ ﺱﺍ ﺮﮐ ﮮﺩ ﺐﯿﺗﺮﺗ ﻮﮐ ﮦﺪﻨﻨﮐ ﻢﮨﺍﺮﻓ ﻞﯿﺳ ﭧﻟﺎﻔﯾﮈ ﮯﻨﭘﺍ ﮟﯿﻣ ﺕﺎﺒ", + "selected_trocador_provider": "منتخب کردہ ٹروکاڈور فراہم کنندہ", "select_your_country": "براہ کرم اپنے ملک کو منتخب کریں", "sell": "بیچنا", "sell_alert_content": "۔ﮟﯾﺮﮐ ﭻﺋﻮﺳ ﺮﭘ ﺱﺍ ﺎﯾ ﮟﯿﺋﺎﻨﺑ ﭧﯿﻟﺍﻭ Litecoin ﺎﯾ Bitcoin، Ethereum ﺎﻨﭘﺍ ﻡﺮﮐ ﮦﺍﺮﺑ ۔", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 6eb66754c..e5e94b01f 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -460,6 +460,7 @@ "no_cards_found": "Ko si awọn kaadi ti a rii", "no_id_needed": "Ẹ kò nílò àmì ìdánimọ̀!", "no_id_required": "Ẹ kò nílò àmì ìdánimọ̀. Ẹ lè fikún owó àti san níbikíbi", + "no_providers_available": "Ko si awọn olupese ti o wa", "no_relay_on_domain": "Ko si iṣipopada fun agbegbe olumulo tabi yiyi ko si. Jọwọ yan yii lati lo.", "no_relays": "Ko si relays", "no_relays_message": "A ri igbasilẹ Nostr NIP-05 fun olumulo yii, ṣugbọn ko ni eyikeyi awọn iṣipopada ninu. Jọwọ sọ fun olugba lati ṣafikun awọn isunmọ si igbasilẹ Nostr wọn.", @@ -675,6 +676,7 @@ "select_destination": "Jọwọ yan ibi ti o nlo fun faili afẹyinti.", "select_hw_account_below": "Jọwọ yan iru iroyin lati mu pada ni isalẹ:", "select_sell_provider_notice": "Yan olupese ti o ta loke. O le foju iboju yii nipa tito olupese iṣẹ tita aiyipada rẹ ni awọn eto app.", + "selected_trocador_provider": "olupese trocador ti a yan", "select_your_country": "Jọwọ yan orilẹ-ede rẹ", "sell": "Tà", "sell_alert_content": "Lọwọlọwọ a ṣe atilẹyin tita Bitcoin, Ethereum ati Litecoin nikan. Jọwọ ṣẹda tabi yipada si Bitcoin, Ethereum tabi apamọwọ Litecoin rẹ.", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index ed890c17a..eeba24191 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -459,6 +459,7 @@ "no_cards_found": "找不到卡", "no_id_needed": "不需要 ID!", "no_id_required": "不需要身份证。充值并在任何地方消费", + "no_providers_available": "没有提供商可用", "no_relay_on_domain": "用户域没有中继或中继不可用。请选择要使用的继电器。", "no_relays": "无继电器", "no_relays_message": "我们找到了该用户的 Nostr NIP-05 记录,但它不包含任何中继。请指示收件人将中继添加到他们的 Nostr 记录中。", @@ -674,6 +675,7 @@ "select_destination": "请选择备份文件的目的地。", "select_hw_account_below": "请在下面选择要还原的帐户:", "select_sell_provider_notice": "选择上面的销售提供商。您可以通过在应用程序设置中设置默认销售提供商来跳过此屏幕。", + "selected_trocador_provider": "选定的trocador提供商", "select_your_country": "请选择你的国家", "sell": "卖", "sell_alert_content": "我们目前仅支持比特币、以太坊和莱特币的销售。请创建或切换到您的比特币、以太坊或莱特币钱包。", From 4f35cc9b0fd24078efb68b8037ea1883704bbb79 Mon Sep 17 00:00:00 2001 From: Serhii Date: Thu, 13 Mar 2025 03:00:45 +0200 Subject: [PATCH 035/243] Cw 940 reduce impossible api calls to trocador (#2050) * skip Impossible API calls on exchange flow * call calculateBestRate from loadLimits --------- Co-authored-by: Omar Hatem --- .../exchange/exchange_view_model.dart | 96 ++++++++++++++----- 1 file changed, 71 insertions(+), 25 deletions(-) diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 6a3b32b9b..3a0ca3f1f 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -116,7 +116,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with .toList()); _setAvailableProviders(); - calculateBestRate(); autorun((_) { if (selectedProviders.any((provider) => provider is TrocadorExchangeProvider)) { @@ -154,9 +153,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with _defineIsReceiveAmountEditable(); loadLimits(); reaction((_) => isFixedRateMode, (Object _) { + bestRate = 0.0; loadLimits(); - bestRate = 0; - calculateBestRate(); }); if (isElectrumWallet) { @@ -210,6 +208,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with final List _tradeAvailableProviders = []; + Map _providerLimits = {}; + @observable ObservableList selectedProviders; @@ -433,9 +433,24 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with return true; } Future calculateBestRate() async { - final amount = double.tryParse(isFixedRateMode ? receiveAmount : depositAmount) ?? 1; + if (depositCurrency == receiveCurrency) { + bestRate = 0.0; + return; + } + final amount = double.tryParse(isFixedRateMode ? receiveAmount : depositAmount) + ?? initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency); - final _providers = _tradeAvailableProviders + final validProvidersForAmount = _tradeAvailableProviders.where((provider) { + final limits = _providerLimits[provider]; + + if (limits == null) return false; + if (limits.min != null && amount < limits.min!) return false; + if (limits.max != null && amount > limits.max!) return false; + + return true; + }).toList(); + + final _providers = validProvidersForAmount .where((element) => !isFixedRateMode || element.supportsFixedRate) .toList(); @@ -473,6 +488,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with @action Future loadLimits() async { + if (depositCurrency == receiveCurrency) { + limitsState = LimitsLoadedSuccessfully(limits: Limits(min: 0, max: 0)); + return; + }; if (selectedProviders.isEmpty) return; limitsState = LimitsIsLoading(); @@ -484,23 +503,27 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with double? highestMax = 0.0; try { - final result = await Future.wait( - selectedProviders.where((provider) => providersForCurrentPair().contains(provider)).map( - (provider) => provider - .fetchLimits( - from: from, - to: to, - isFixedRateMode: isFixedRateMode, - ) - .onError((error, stackTrace) => Limits(max: 0.0, min: double.maxFinite)) - .timeout( - Duration(seconds: 7), - onTimeout: () => Limits(max: 0.0, min: double.maxFinite), - ), - ), - ); + final futures = selectedProviders + .where((provider) => providersForCurrentPair().contains(provider)) + .map((provider) async { + final limits = await provider + .fetchLimits( + from: from, + to: to, + isFixedRateMode: isFixedRateMode, + ) + .onError((error, stackTrace) => Limits(max: 0.0, min: double.maxFinite)) + .timeout( + Duration(seconds: 7), + onTimeout: () => Limits(max: 0.0, min: double.maxFinite), + ); + return MapEntry(provider, limits); + }).toList(); - result.forEach((tempLimits) { + final entries = await Future.wait(futures); + _providerLimits = Map.fromEntries(entries); + + _providerLimits.values.forEach((tempLimits) { if (lowestMin != null && (tempLimits.min ?? -1) < lowestMin!) { lowestMin = tempLimits.min; } @@ -525,10 +548,18 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with } else { limitsState = LimitsLoadedFailure(error: 'Limits loading failed'); } + + calculateBestRate(); } @action Future createTrade() async { + if (depositCurrency == receiveCurrency) { + tradeState = TradeIsCreatedFailure( + title: S.current.trade_not_created, + error: 'Can\'t exchange the same currency'); + return; + } if (isSendAllEnabled) { await calculateDepositAllAmount(); final amount = double.tryParse(depositAmount); @@ -706,10 +737,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with void _onPairChange() { depositAmount = ''; receiveAmount = ''; + bestRate = 0.0; loadLimits(); _setAvailableProviders(); - bestRate = 0; - calculateBestRate(); } void _initialPairBasedOnWallet() { @@ -803,9 +833,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with receiveAmount = ''; isFixedRateMode = false; _defineIsReceiveAmountEditable(); + bestRate = 0.0; loadLimits(); - bestRate = 0; - calculateBestRate(); final Map exchangeProvidersSelection = json.decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") @@ -988,4 +1017,21 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with return false; } } + + double initialAmountByAssets (CryptoCurrency ticker) { + final amount = switch (ticker) { + CryptoCurrency.trx => 1000, + CryptoCurrency.nano => 10, + CryptoCurrency.zano => 10, + CryptoCurrency.wow => 1000, + CryptoCurrency.ada => 1000, + CryptoCurrency.dash => 10, + CryptoCurrency.rune => 10, + + _ => 1 + }; + return amount.toDouble(); + } + + } From 1c29be7993e397a3dda51008c1a42fef599fb569 Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 14 Mar 2025 03:02:39 +0200 Subject: [PATCH 036/243] CW-961-Integrate-xoswap (#2060) * integrate xoswap * fix network id * fix calculating amount with rate twice * minor: move xoswap a bit up [skip ci] * minor fix [skip ci] * add tracking url * improve fetching exchange rate * Update trade_filter_store.dart --------- Co-authored-by: OmarHatem --- assets/images/xoswap.svg | 26 ++ .../exchange_provider_description.dart | 4 + .../provider/xoswap_exchange_provider.dart | 309 ++++++++++++++++++ lib/exchange/trade_state.dart | 1 + .../widgets/sync_indicator_icon.dart | 2 + lib/store/dashboard/trade_filter_store.dart | 18 +- .../dashboard/dashboard_view_model.dart | 5 + .../exchange/exchange_trade_view_model.dart | 3 + .../exchange/exchange_view_model.dart | 2 + lib/view_model/trade_details_view_model.dart | 6 + 10 files changed, 373 insertions(+), 3 deletions(-) create mode 100644 assets/images/xoswap.svg create mode 100644 lib/exchange/provider/xoswap_exchange_provider.dart diff --git a/assets/images/xoswap.svg b/assets/images/xoswap.svg new file mode 100644 index 000000000..ef83c58b1 --- /dev/null +++ b/assets/images/xoswap.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/exchange/exchange_provider_description.dart b/lib/exchange/exchange_provider_description.dart index 8d1c75211..b3761ab73 100644 --- a/lib/exchange/exchange_provider_description.dart +++ b/lib/exchange/exchange_provider_description.dart @@ -34,6 +34,8 @@ class ExchangeProviderDescription extends EnumerableItem with Serializable< ExchangeProviderDescription(title: 'StealthEx', raw: 11, image: 'assets/images/stealthex.png'); static const chainflip = ExchangeProviderDescription(title: 'Chainflip', raw: 12, image: 'assets/images/chainflip.png'); + static const xoSwap = + ExchangeProviderDescription(title: 'XOSwap', raw: 13, image: 'assets/images/xoswap.svg'); static ExchangeProviderDescription deserialize({required int raw}) { switch (raw) { @@ -63,6 +65,8 @@ class ExchangeProviderDescription extends EnumerableItem with Serializable< return stealthEx; case 12: return chainflip; + case 13: + return xoSwap; default: throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize'); } diff --git a/lib/exchange/provider/xoswap_exchange_provider.dart b/lib/exchange/provider/xoswap_exchange_provider.dart new file mode 100644 index 000000000..0b3826aa9 --- /dev/null +++ b/lib/exchange/provider/xoswap_exchange_provider.dart @@ -0,0 +1,309 @@ +import 'dart:convert'; + +import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/exchange/limits.dart'; +import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; +import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/exchange/trade_not_created_exception.dart'; +import 'package:cake_wallet/exchange/trade_request.dart'; +import 'package:cake_wallet/exchange/trade_state.dart'; +import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:http/http.dart' as http; + +class XOSwapExchangeProvider extends ExchangeProvider { + XOSwapExchangeProvider() : super(pairList: supportedPairs(_notSupported)); + + static const List _notSupported = []; + + static const _apiAuthority = 'exchange.exodus.io'; + static const _apiPath = '/v3'; + static const _pairsPath = '/pairs'; + static const _ratePath = '/rates'; + static const _orders = '/orders'; + static const _assets = '/assets'; + + static const _headers = {'Content-Type': 'application/json', 'App-Name': 'cake-labs'}; + + final _networks = { + 'POL': 'matic', + 'ETH': 'ethereum', + 'BTC': 'bitcoin', + 'BSC': 'bsc', + 'SOL': 'solana', + 'TRX': 'tronmainnet', + 'ZEC': 'zcash', + 'ADA': 'cardano', + 'DOGE': 'dogecoin', + 'XMR': 'monero', + 'BCH': 'bcash', + 'BSV': 'bitcoinsv', + 'XRP': 'ripple', + 'LTC': 'litecoin', + 'EOS': 'eosio', + 'XLM': 'stellar', + }; + + @override + String get title => 'XOSwap'; + + @override + bool get isAvailable => true; + + @override + bool get isEnabled => true; + + @override + bool get supportsFixedRate => true; + + @override + ExchangeProviderDescription get description => ExchangeProviderDescription.xoSwap; + + @override + Future checkIsAvailable() async => true; + + Future _getAssets(CryptoCurrency currency) async { + if (currency.tag == null) return currency.title; + try { + final normalizedNetwork = _networks[currency.tag]; + if (normalizedNetwork == null) return null; + + final uri = Uri.https(_apiAuthority, _apiPath + _assets, + {'networks': normalizedNetwork, 'query': currency.title}); + + final response = await http.get(uri, headers: _headers); + if (response.statusCode != 200) { + throw Exception('Failed to fetch assets for ${currency.title} on ${currency.tag}'); + } + final assets = json.decode(response.body) as List; + + final asset = assets.firstWhere( + (asset) { + final assetSymbol = (asset['symbol'] as String).toUpperCase(); + return assetSymbol == currency.title.toUpperCase(); + }, + orElse: () => null, + ); + return asset != null ? asset['id'] as String : null; + } catch (e) { + printV(e.toString()); + return null; + } + } + + Future> getRatesForPair({ + required CryptoCurrency from, + required CryptoCurrency to, + }) async { + try { + final curFrom = await _getAssets(from); + final curTo = await _getAssets(to); + if (curFrom == null || curTo == null) return []; + final pairId = curFrom + '_' + curTo; + final uri = Uri.https(_apiAuthority, '$_apiPath$_pairsPath/$pairId$_ratePath'); + final response = await http.get(uri, headers: _headers); + if (response.statusCode != 200) return []; + return json.decode(response.body) as List; + } catch (e) { + printV(e.toString()); + return []; + } + } + + Future fetchLimits({ + required CryptoCurrency from, + required CryptoCurrency to, + required bool isFixedRateMode, + }) async { + final rates = await getRatesForPair(from: from, to: to); + if (rates.isEmpty) return Limits(min: 0, max: 0); + + double minLimit = double.infinity; + double maxLimit = 0; + + for (var rate in rates) { + final double currentMin = double.parse(rate['min']['value'].toString()); + final double currentMax = double.parse(rate['max']['value'].toString()); + if (currentMin < minLimit) minLimit = currentMin; + if (currentMax > maxLimit) maxLimit = currentMax; + } + return Limits(min: minLimit, max: maxLimit); + } + + Future fetchRate({ + required CryptoCurrency from, + required CryptoCurrency to, + required double amount, + required bool isFixedRateMode, + required bool isReceiveAmount, + }) async { + try { + final rates = await getRatesForPair(from: from, to: to); + if (rates.isEmpty) return 0; + + if (!isFixedRateMode) { + double bestOutput = 0.0; + for (var rate in rates) { + final double minVal = double.parse(rate['min']['value'].toString()); + final double maxVal = double.parse(rate['max']['value'].toString()); + if (amount >= minVal && amount <= maxVal) { + final double rateMultiplier = double.parse(rate['amount']['value'].toString()); + final double minerFee = double.parse(rate['minerFee']['value'].toString()); + final double outputAmount = (amount * rateMultiplier) - minerFee; + if (outputAmount > bestOutput) { + bestOutput = outputAmount; + } + } + } + return bestOutput > 0 ? (bestOutput / amount) : 0; + } else { + double bestInput = double.infinity; + for (var rate in rates) { + final double rateMultiplier = double.parse(rate['amount']['value'].toString()); + final double minerFee = double.parse(rate['minerFee']['value'].toString()); + final double minVal = double.parse(rate['min']['value'].toString()); + final double maxVal = double.parse(rate['max']['value'].toString()); + final double requiredSend = (amount + minerFee) / rateMultiplier; + if (requiredSend >= minVal && requiredSend <= maxVal) { + if (requiredSend < bestInput) { + bestInput = requiredSend; + } + } + } + return bestInput < double.infinity ? amount / bestInput : 0; + } + } catch (e) { + printV(e.toString()); + return 0; + } + } + + @override + Future createTrade({ + required TradeRequest request, + required bool isFixedRateMode, + required bool isSendAll, + }) async { + try { + final uri = Uri.https(_apiAuthority, '$_apiPath$_orders'); + + final payload = { + 'fromAmount': request.fromAmount, + 'fromAddress': request.refundAddress, + 'toAmount': request.toAmount, + 'toAddress': request.toAddress, + 'pairId': '${request.fromCurrency.title}_${request.toCurrency.title}', + }; + + final response = await http.post(uri, headers: _headers, body: json.encode(payload)); + if (response.statusCode != 201) { + final responseJSON = json.decode(response.body) as Map; + final error = responseJSON['error'] ?? 'Unknown error'; + final message = responseJSON['message'] ?? ''; + throw Exception('$error\n$message'); + } + final responseJSON = json.decode(response.body) as Map; + + final amount = responseJSON['amount'] as Map; + final toAmount = responseJSON['toAmount'] as Map; + final orderId = responseJSON['id'] as String; + final from = request.fromCurrency; + final to = request.toCurrency; + final payoutAddress = responseJSON['toAddress'] as String; + final depositAddress = responseJSON['payInAddress'] as String; + final refundAddress = responseJSON['fromAddress'] as String; + final depositAmount = _toDouble(amount['value']); + final receiveAmount = toAmount['value'] as String; + final status = responseJSON['status'] as String; + final createdAtString = responseJSON['createdAt'] as String; + final extraId = responseJSON['payInAddressTag'] as String?; + + final createdAt = DateTime.parse(createdAtString).toLocal(); + + return Trade( + id: orderId, + from: from, + to: to, + provider: description, + inputAddress: depositAddress, + refundAddress: refundAddress, + state: TradeState.deserialize(raw: status), + createdAt: createdAt, + amount: depositAmount.toString(), + receiveAmount: receiveAmount.toString(), + payoutAddress: payoutAddress, + extraId: extraId, + ); + } catch (e) { + printV(e.toString()); + throw TradeNotCreatedException(description); + } + } + + @override + Future findTradeById({required String id}) async { + try { + final uri = Uri.https(_apiAuthority, '$_apiPath$_orders/$id'); + final response = await http.get(uri, headers: _headers); + if (response.statusCode != 200) { + final responseJSON = json.decode(response.body) as Map; + if (responseJSON.containsKey('code') && responseJSON['code'] == 'NOT_FOUND') { + throw Exception('Trade not found'); + } + final error = responseJSON['error'] ?? 'Unknown error'; + final message = responseJSON['message'] ?? responseJSON['details'] ?? ''; + throw Exception('$error\n$message'); + } + final responseJSON = json.decode(response.body) as Map; + + final pairId = responseJSON['pairId'] as String; + final pairParts = pairId.split('_'); + final CryptoCurrency fromCurrency = + CryptoCurrency.fromString(pairParts.isNotEmpty ? pairParts[0] : ""); + final CryptoCurrency toCurrency = + CryptoCurrency.fromString(pairParts.length > 1 ? pairParts[1] : ""); + + final amount = responseJSON['amount'] as Map; + final toAmount = responseJSON['toAmount'] as Map; + final orderId = responseJSON['id'] as String; + final depositAmount = amount['value'] as String; + final receiveAmount = toAmount['value'] as String; + final depositAddress = responseJSON['payInAddress'] as String; + final payoutAddress = responseJSON['toAddress'] as String; + final refundAddress = responseJSON['fromAddress'] as String; + final status = responseJSON['status'] as String; + final createdAtString = responseJSON['createdAt'] as String; + final createdAt = DateTime.parse(createdAtString).toLocal(); + final extraId = responseJSON['payInAddressTag'] as String?; + + return Trade( + id: orderId, + from: fromCurrency, + to: toCurrency, + provider: description, + inputAddress: depositAddress, + refundAddress: refundAddress, + state: TradeState.deserialize(raw: status), + createdAt: createdAt, + amount: depositAmount, + receiveAmount: receiveAmount, + payoutAddress: payoutAddress, + extraId: extraId, + ); + } catch (e) { + printV(e.toString()); + throw TradeNotCreatedException(description); + } + } + + double _toDouble(dynamic value) { + if (value is int) { + return value.toDouble(); + } else if (value is double) { + return value; + } else { + return 0.0; + } + } +} diff --git a/lib/exchange/trade_state.dart b/lib/exchange/trade_state.dart index 7bce8c7e7..f63b41e5d 100644 --- a/lib/exchange/trade_state.dart +++ b/lib/exchange/trade_state.dart @@ -100,6 +100,7 @@ class TradeState extends EnumerableItem with Serializable { case 'waiting': return waiting; case 'processing': + case 'inProgress': return processing; case 'waitingPayment': return waitingPayment; diff --git a/lib/src/screens/dashboard/widgets/sync_indicator_icon.dart b/lib/src/screens/dashboard/widgets/sync_indicator_icon.dart index aca3231ec..c1202d3ff 100644 --- a/lib/src/screens/dashboard/widgets/sync_indicator_icon.dart +++ b/lib/src/screens/dashboard/widgets/sync_indicator_icon.dart @@ -21,6 +21,7 @@ class SyncIndicatorIcon extends StatelessWidget { static const String fetching = 'fetching'; static const String finished = 'finished'; static const String success = 'success'; + static const String complete = 'complete'; @override Widget build(BuildContext context) { @@ -47,6 +48,7 @@ class SyncIndicatorIcon extends StatelessWidget { break; case finished: case success: + case complete: indicatorColor = PaletteDark.brightGreen; break; default: diff --git a/lib/store/dashboard/trade_filter_store.dart b/lib/store/dashboard/trade_filter_store.dart index 6a98329d6..e3212fd11 100644 --- a/lib/store/dashboard/trade_filter_store.dart +++ b/lib/store/dashboard/trade_filter_store.dart @@ -20,6 +20,7 @@ abstract class TradeFilterStoreBase with Store { displayThorChain = true, displayLetsExchange = true, displayStealthEx = true, + displayXOSwap = true, displaySwapTrade = true; @observable @@ -45,7 +46,7 @@ abstract class TradeFilterStoreBase with Store { @observable bool displayChainflip; - + @observable bool displayThorChain; @@ -55,6 +56,9 @@ abstract class TradeFilterStoreBase with Store { @observable bool displayStealthEx; + @observable + bool displayXOSwap; + @observable bool displaySwapTrade; @@ -64,11 +68,12 @@ abstract class TradeFilterStoreBase with Store { displaySideShift && displaySimpleSwap && displayTrocador && - displayExolix && + displayExolix && displayChainflip && displayThorChain && displayLetsExchange && displayStealthEx && + displayXOSwap && displaySwapTrade; @action @@ -107,8 +112,12 @@ abstract class TradeFilterStoreBase with Store { case ExchangeProviderDescription.stealthEx: displayStealthEx = !displayStealthEx; break; - case ExchangeProviderDescription.swapTrade: + case ExchangeProviderDescription.xoSwap: + displayXOSwap = !displayXOSwap; + break; + case ExchangeProviderDescription.swapTrade: displaySwapTrade = !displaySwapTrade; + break; case ExchangeProviderDescription.all: if (displayAllTrades) { displayChangeNow = false; @@ -122,6 +131,7 @@ abstract class TradeFilterStoreBase with Store { displayThorChain = false; displayLetsExchange = false; displayStealthEx = false; + displayXOSwap = false; displaySwapTrade = false; } else { displayChangeNow = true; @@ -135,6 +145,7 @@ abstract class TradeFilterStoreBase with Store { displayThorChain = true; displayLetsExchange = true; displayStealthEx = true; + displayXOSwap = true; displaySwapTrade = true; } break; @@ -168,6 +179,7 @@ abstract class TradeFilterStoreBase with Store { (displayLetsExchange && item.trade.provider == ExchangeProviderDescription.letsExchange) || (displayStealthEx && item.trade.provider == ExchangeProviderDescription.stealthEx) || + (displayXOSwap && item.trade.provider == ExchangeProviderDescription.xoSwap) || (displaySwapTrade && item.trade.provider == ExchangeProviderDescription.swapTrade)) .toList() : _trades; diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 575193027..ee9c57b65 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -152,6 +152,11 @@ abstract class DashboardViewModelBase with Store { caption: ExchangeProviderDescription.stealthEx.title, onChanged: () => tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.stealthEx)), + FilterItem( + value: () => tradeFilterStore.displayXOSwap, + caption: ExchangeProviderDescription.xoSwap.title, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.xoSwap)), FilterItem( value: () => tradeFilterStore.displaySwapTrade, caption: ExchangeProviderDescription.swapTrade.title, diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index fd8c29f1f..d488447e2 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -11,6 +11,7 @@ import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart' import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_item.dart'; @@ -64,6 +65,8 @@ abstract class ExchangeTradeViewModelBase with Store { case ExchangeProviderDescription.chainflip: _provider = ChainflipExchangeProvider(tradesStore: trades); break; + case ExchangeProviderDescription.xoSwap: + _provider = XOSwapExchangeProvider(); } _updateItems(); diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 3a0ca3f1f..d3871ffbd 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/core/create_trade_result.dart'; import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -187,6 +188,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with SwapTradeExchangeProvider(), LetsExchangeExchangeProvider(), StealthExExchangeProvider(), + XOSwapExchangeProvider(), TrocadorExchangeProvider( useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates), ]; diff --git a/lib/view_model/trade_details_view_model.dart b/lib/view_model/trade_details_view_model.dart index 6b509dc22..a9a77fb54 100644 --- a/lib/view_model/trade_details_view_model.dart +++ b/lib/view_model/trade_details_view_model.dart @@ -12,6 +12,7 @@ import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart' import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/trade_details/track_trade_list_item.dart'; @@ -72,6 +73,9 @@ abstract class TradeDetailsViewModelBase with Store { case ExchangeProviderDescription.chainflip: _provider = ChainflipExchangeProvider(tradesStore: trades); break; + case ExchangeProviderDescription.xoSwap: + _provider = XOSwapExchangeProvider(); + break; } _updateItems(); @@ -104,6 +108,8 @@ abstract class TradeDetailsViewModelBase with Store { return 'https://stealthex.io/exchange/?id=${trade.id}'; case ExchangeProviderDescription.chainflip: return 'https://scan.chainflip.io/channels/${trade.id}'; + case ExchangeProviderDescription.xoSwap: + return 'https://orders.xoswap.com/${trade.id}'; } return null; } From 1b5be705f6d5f11f4508116ba9558e3178868d7d Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Fri, 14 Mar 2025 15:42:17 +0100 Subject: [PATCH 037/243] Solana Wallet New Implementation (#2011) * Feat: Implement Solana wallet using on_chain * v4.23.0 release candidate (#1974) * v4.23.0 release candidate * - Fix restoring zano from QR - Fix Zano confirmations count - Fix birdpay - Fix balance display * Fix Zano assets showing amount before they are added * - handle fetching token data while the API is busy - potential fix for duplicate transactions * fix receive confirmations, maybe * revert onChangeWallet cleanup * Fix confirmations not updating * improve zano wallet opening, fix CI commands and messages on slack (#1979) Co-authored-by: Omar Hatem * Cache wallet when creating/restoring as well * - hardcode Trocador Maximum limit for Zano temporarily - Configure Cake Zano node to use SSL * reformatting [skip ci] * revert to non-ssl * update build numbers [skip ci] * disable zano for desktop [skip ci] --------- Co-authored-by: cyan * CW-711 passphrase for XMR/WOWcreation (#1992) * add monero passphrase add wownero passphrase add passphrase to seed screen * obscure passphrase by default disable passphrase create for zano * Update lib/view_model/wallet_keys_view_model.dart [skip ci] * Update lib/src/screens/wallet_keys/wallet_keys_page.dart [skip ci] * Update lib/view_model/advanced_privacy_settings_view_model.dart * dynamic passphrase icon * fix polyseed not being encrypted by passphrase --------- Co-authored-by: Omar Hatem * show Zano keys properly in the keys tab (#2004) * fix: Switch private key hex encoding * Modified existing implementation to use older version of packages * fix: Fetch direct transaction history amounts instead of decimals, and add Create Account Instructions to Transaction History List * fix: Remove Create Account entries in Transaction History and disable activating token accounts of selected tokens * feat: Add passphrase support to Solana * fix: Issues with transaction amount and dissappearing transaction history items (very annoying bug) * fix: Issue with flipping transactions and incorrect transaction status * PR Review fixes --------- Co-authored-by: Omar Hatem Co-authored-by: cyan --- cw_core/lib/solana_rpc_http_service.dart | 26 + cw_core/pubspec.yaml | 4 + cw_solana/lib/default_spl_tokens.dart | 4 +- cw_solana/lib/pending_solana_transaction.dart | 7 +- cw_solana/lib/solana_client.dart | 895 +++++++++++------- cw_solana/lib/solana_transaction_info.dart | 4 +- cw_solana/lib/solana_wallet.dart | 158 ++-- cw_solana/lib/solana_wallet_service.dart | 2 + cw_solana/lib/spl_token.dart | 31 +- cw_solana/pubspec.yaml | 9 +- cw_tron/pubspec.yaml | 2 +- ios/Podfile.lock | 38 - .../solana/solana_chain_service.dart | 58 +- .../wallet_connect/web3wallet_service.dart | 18 +- lib/solana/cw_solana.dart | 7 +- .../screens/wallet_keys/wallet_keys_page.dart | 2 +- .../advanced_privacy_settings_view_model.dart | 1 + pubspec_base.yaml | 9 +- tool/configure.dart | 2 - 19 files changed, 716 insertions(+), 561 deletions(-) create mode 100644 cw_core/lib/solana_rpc_http_service.dart diff --git a/cw_core/lib/solana_rpc_http_service.dart b/cw_core/lib/solana_rpc_http_service.dart new file mode 100644 index 000000000..fbe9a29dc --- /dev/null +++ b/cw_core/lib/solana_rpc_http_service.dart @@ -0,0 +1,26 @@ +import 'dart:convert'; +import 'package:http/http.dart'; +import 'package:on_chain/solana/solana.dart'; + +class SolanaRPCHTTPService implements SolanaJSONRPCService { + SolanaRPCHTTPService( + {required this.url, Client? client, this.defaultRequestTimeout = const Duration(seconds: 30)}) + : client = client ?? Client(); + @override + final String url; + final Client client; + final Duration defaultRequestTimeout; + + @override + Future> call(SolanaRequestDetails params, [Duration? timeout]) async { + final response = await client.post( + Uri.parse(url), + body: params.toRequestBody(), + headers: { + 'Content-Type': 'application/json', + }, + ).timeout(timeout ?? defaultRequestTimeout); + final data = json.decode(response.body) as Map; + return data; + } +} diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml index d59440800..9b38b61a7 100644 --- a/cw_core/pubspec.yaml +++ b/cw_core/pubspec.yaml @@ -27,6 +27,10 @@ dependencies: version: 1.0.0 socks5_proxy: ^1.0.4 unorm_dart: ^0.3.0 + on_chain: + git: + url: https://github.com/cake-tech/on_chain.git + ref: cake-update-v2 # tor: # git: # url: https://github.com/cake-tech/tor.git diff --git a/cw_solana/lib/default_spl_tokens.dart b/cw_solana/lib/default_spl_tokens.dart index 21b5ef79d..f7ce60aac 100644 --- a/cw_solana/lib/default_spl_tokens.dart +++ b/cw_solana/lib/default_spl_tokens.dart @@ -26,7 +26,7 @@ class DefaultSPLTokens { decimal: 5, mint: 'Bonk', iconPath: 'assets/images/bonk_icon.png', - enabled: true, + enabled: false, ), SPLToken( name: 'Raydium', @@ -35,7 +35,7 @@ class DefaultSPLTokens { decimal: 6, mint: 'ray', iconPath: 'assets/images/ray_icon.png', - enabled: true, + enabled: false, ), SPLToken( name: 'Wrapped Ethereum (Sollet)', diff --git a/cw_solana/lib/pending_solana_transaction.dart b/cw_solana/lib/pending_solana_transaction.dart index e01446000..5102ea51f 100644 --- a/cw_solana/lib/pending_solana_transaction.dart +++ b/cw_solana/lib/pending_solana_transaction.dart @@ -1,9 +1,8 @@ import 'package:cw_core/pending_transaction.dart'; -import 'package:solana/encoder.dart'; class PendingSolanaTransaction with PendingTransaction { final double amount; - final SignedTx signedTransaction; + final String serializedTransaction; final String destinationAddress; final Function sendTransaction; final double fee; @@ -11,7 +10,7 @@ class PendingSolanaTransaction with PendingTransaction { PendingSolanaTransaction({ required this.fee, required this.amount, - required this.signedTransaction, + required this.serializedTransaction, required this.destinationAddress, required this.sendTransaction, }); @@ -36,7 +35,7 @@ class PendingSolanaTransaction with PendingTransaction { String get feeFormatted => fee.toString(); @override - String get hex => signedTransaction.encode(); + String get hex => serializedTransaction; @override String get id => ''; diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart index 431f5f7fb..0e08f9abb 100644 --- a/cw_solana/lib/solana_client.dart +++ b/cw_solana/lib/solana_client.dart @@ -4,54 +4,59 @@ import 'dart:math' as math; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/node.dart'; +import 'package:cw_core/solana_rpc_http_service.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_solana/pending_solana_transaction.dart'; import 'package:cw_solana/solana_balance.dart'; import 'package:cw_solana/solana_exceptions.dart'; import 'package:cw_solana/solana_transaction_model.dart'; +import 'package:cw_solana/spl_token.dart'; import 'package:http/http.dart' as http; -import 'package:solana/dto.dart'; -import 'package:solana/encoder.dart'; -import 'package:solana/solana.dart'; +import 'package:on_chain/solana/solana.dart'; +import 'package:on_chain/solana/src/models/pda/pda.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; import '.secrets.g.dart' as secrets; class SolanaWalletClient { final httpClient = http.Client(); - SolanaClient? _client; + SolanaRPC? _provider; bool connect(Node node) { try { - Uri rpcUri = node.uri; - String webSocketUrl = 'wss://${node.uriRaw}'; + String formattedUrl; + String protocolUsed = node.isSSL ? "https" : "http"; if (node.uriRaw == 'rpc.ankr.com') { String ankrApiKey = secrets.ankrApiKey; - rpcUri = Uri.https(node.uriRaw, '/solana/$ankrApiKey'); - webSocketUrl = 'wss://${node.uriRaw}/solana/ws/$ankrApiKey'; + formattedUrl = '$protocolUsed://${node.uriRaw}/$ankrApiKey'; } else if (node.uriRaw == 'solana-mainnet.core.chainstack.com') { String chainStackApiKey = secrets.chainStackApiKey; - rpcUri = Uri.https(node.uriRaw, '/$chainStackApiKey'); - webSocketUrl = 'wss://${node.uriRaw}/$chainStackApiKey'; + formattedUrl = '$protocolUsed://${node.uriRaw}/$chainStackApiKey'; + } else { + formattedUrl = '$protocolUsed://${node.uriRaw}'; } - _client = SolanaClient( - rpcUrl: rpcUri, - websocketUrl: Uri.parse(webSocketUrl), - timeout: const Duration(minutes: 2), - ); + _provider = SolanaRPC(SolanaRPCHTTPService(url: formattedUrl)); + return true; } catch (e) { return false; } } - Future getBalance(String address) async { + Future getBalance(String walletAddress) async { try { - final balance = await _client!.rpcClient.getBalance(address); + final balance = await _provider!.requestWithContext( + SolanaRPCGetBalance( + account: SolAddress(walletAddress), + ), + ); - final solBalance = balance.value / lamportsPerSol; + final balInLamp = balance.result.toDouble(); + + final solBalance = balInLamp / SolanaUtils.lamportsPerSol; return solBalance; } catch (_) { @@ -59,37 +64,42 @@ class SolanaWalletClient { } } - Future getSPLTokenAccounts(String mintAddress, String publicKey) async { + Future?> getSPLTokenAccounts( + String mintAddress, String publicKey) async { try { - final tokenAccounts = await _client!.rpcClient.getTokenAccountsByOwner( - publicKey, - TokenAccountsFilter.byMint(mintAddress), - commitment: Commitment.confirmed, - encoding: Encoding.jsonParsed, + final result = await _provider!.request( + SolanaRPCGetTokenAccountsByOwner( + account: SolAddress(publicKey), + mint: SolAddress(mintAddress), + commitment: Commitment.confirmed, + encoding: SolanaRPCEncoding.base64, + ), ); - return tokenAccounts; + + return result; } catch (e) { return null; } } - Future getSplTokenBalance(String mintAddress, String publicKey) async { + Future getSplTokenBalance(String mintAddress, String walletAddress) async { // Fetch the token accounts (a token can have multiple accounts for various uses) - final tokenAccounts = await getSPLTokenAccounts(mintAddress, publicKey); + final tokenAccounts = await getSPLTokenAccounts(mintAddress, walletAddress); // Handle scenario where there is no token account - if (tokenAccounts == null || tokenAccounts.value.isEmpty) { + if (tokenAccounts == null || tokenAccounts.isEmpty) { return null; } // Sum the balances of all accounts with the specified mint address double totalBalance = 0.0; - for (var programAccount in tokenAccounts.value) { - final tokenAmountResult = - await _client!.rpcClient.getTokenAccountBalance(programAccount.pubkey); + for (var tokenAccount in tokenAccounts) { + final tokenAmountResult = await _provider!.request( + SolanaRPCGetTokenAccountBalance(account: tokenAccount.pubkey), + ); - final balance = tokenAmountResult.value.uiAmountString; + final balance = tokenAmountResult.uiAmountString; final balanceAsDouble = double.tryParse(balance ?? '0.0') ?? 0.0; @@ -101,198 +111,318 @@ class SolanaWalletClient { Future getFeeForMessage(String message, Commitment commitment) async { try { - final feeForMessage = - await _client!.rpcClient.getFeeForMessage(message, commitment: commitment); - final fee = (feeForMessage ?? 0.0) / lamportsPerSol; + final feeForMessage = await _provider!.request( + SolanaRPCGetFeeForMessage( + encodedMessage: message, + commitment: commitment, + ), + ); + + final fee = (feeForMessage?.toDouble() ?? 0.0) / SolanaUtils.lamportsPerSol; return fee; } catch (_) { return 0.0; } } - Future getEstimatedFee(Ed25519HDKeyPair ownerKeypair) async { - const commitment = Commitment.confirmed; + Future getEstimatedFee(SolanaPublicKey publicKey, Commitment commitment) async { + final message = await _getMessageForNativeTransaction( + publicKey: publicKey, + destinationAddress: publicKey.toAddress().address, + lamports: SolanaUtils.lamportsPerSol, + commitment: commitment, + ); - final message = - _getMessageForNativeTransaction(ownerKeypair, ownerKeypair.address, lamportsPerSol); - - final latestBlockhash = await _getLatestBlockhash(commitment); - - final estimatedFee = _getFeeFromCompiledMessage( + final estimatedFee = await _getFeeFromCompiledMessage( message, - ownerKeypair.publicKey, - latestBlockhash, commitment, ); return estimatedFee; } + Future parseTransaction({ + VersionedTransactionResponse? txResponse, + required String walletAddress, + String? splTokenSymbol, + }) async { + if (txResponse == null) return null; + + try { + final blockTime = txResponse.blockTime; + final meta = txResponse.meta; + final transaction = txResponse.transaction; + + if (meta == null || transaction == null) return null; + + final int fee = meta.fee; + + final message = transaction.message; + final instructions = message.compiledInstructions; + + String sender = ""; + String receiver = ""; + + String signature = (txResponse.transaction?.signatures.isEmpty ?? true) + ? "" + : Base58Encoder.encode(txResponse.transaction!.signatures.first); + + for (final instruction in instructions) { + final programId = message.accountKeys[instruction.programIdIndex]; + + if (programId == SystemProgramConst.programId) { + // For native solana transactions + if (instruction.accounts.length < 2) continue; + final senderIndex = instruction.accounts[0]; + final receiverIndex = instruction.accounts[1]; + + sender = message.accountKeys[senderIndex].address; + receiver = message.accountKeys[receiverIndex].address; + + final feeForTx = fee / SolanaUtils.lamportsPerSol; + + final preBalances = meta.preBalances; + final postBalances = meta.postBalances; + + final amountInString = + (((preBalances[senderIndex] - postBalances[senderIndex]) / BigInt.from(1e9)) + .toDouble() - + feeForTx) + .toStringAsFixed(6); + + final amount = double.parse(amountInString); + + return SolanaTransactionModel( + isOutgoingTx: sender == walletAddress, + from: sender, + to: receiver, + id: signature, + amount: amount.abs(), + programId: SystemProgramConst.programId.address, + tokenSymbol: 'SOL', + blockTimeInInt: blockTime?.toInt() ?? 0, + fee: feeForTx, + ); + } else if (programId == SPLTokenProgramConst.tokenProgramId) { + // For SPL Token transactions + if (instruction.accounts.length < 2) continue; + + final preBalances = meta.preTokenBalances; + final postBalances = meta.postTokenBalances; + + double amount = 0.0; + bool isOutgoing = false; + String? mintAddress; + + double userPreAmount = 0.0; + if (preBalances != null && preBalances.isNotEmpty) { + for (final preBal in preBalances) { + if (preBal.owner?.address == walletAddress) { + userPreAmount = preBal.uiTokenAmount.uiAmount ?? 0.0; + + mintAddress = preBal.mint.address; + break; + } + } + } + + double userPostAmount = 0.0; + if (postBalances != null && postBalances.isNotEmpty) { + for (final postBal in postBalances) { + if (postBal.owner?.address == walletAddress) { + userPostAmount = postBal.uiTokenAmount.uiAmount ?? 0.0; + + mintAddress ??= postBal.mint.address; + break; + } + } + } + + final diff = userPreAmount - userPostAmount; + final rawAmount = diff.abs(); + + final amountInString = rawAmount.toStringAsFixed(6); + amount = double.parse(amountInString); + + isOutgoing = diff > 0; + + if (mintAddress == null && instruction.accounts.length >= 4) { + final mintIndex = instruction.accounts[3]; + mintAddress = message.accountKeys[mintIndex].address; + } + + final sender = message.accountKeys[instruction.accounts[0]].address; + final receiver = message.accountKeys[instruction.accounts[1]].address; + + String? tokenSymbol = splTokenSymbol; + if (tokenSymbol == null && mintAddress != null) { + final token = await fetchSPLTokenInfo(mintAddress); + tokenSymbol = token?.symbol; + } + + return SolanaTransactionModel( + isOutgoingTx: isOutgoing, + from: sender, + to: receiver, + id: signature, + amount: amount, + programId: SPLTokenProgramConst.tokenProgramId.address, + blockTimeInInt: blockTime?.toInt() ?? 0, + tokenSymbol: tokenSymbol ?? '', + fee: fee / SolanaUtils.lamportsPerSol, + ); + } else { + return null; + } + } + } catch (e, s) { + printV("Error parsing transaction: $e\n$s"); + } + + return null; + } + /// Load the Address's transactions into the account Future> fetchTransactions( - Ed25519HDPublicKey publicKey, { + SolAddress address, { String? splTokenSymbol, int? splTokenDecimal, + Commitment? commitment, + SolAddress? walletAddress, }) async { List transactions = []; try { - final signatures = await _client!.rpcClient.getSignaturesForAddress( - publicKey.toBase58(), - commitment: Commitment.confirmed, + final signatures = await _provider!.request( + SolanaRPCGetSignaturesForAddress( + account: address, + commitment: commitment, + ), ); - final List transactionDetails = []; + final List transactionDetails = []; + for (int i = 0; i < signatures.length; i += 20) { - final response = await _client!.rpcClient.getMultipleTransactions( - signatures.sublist(i, math.min(i + 20, signatures.length)), - commitment: Commitment.confirmed, - encoding: Encoding.jsonParsed, - ); - transactionDetails.addAll(response); + final batch = signatures.skip(i).take(20).toList(); // Get the next 20 signatures + + final batchResponses = await Future.wait(batch.map((signature) async { + try { + return await _provider!.request( + SolanaRPCGetTransaction( + transactionSignature: signature['signature'], + encoding: SolanaRPCEncoding.jsonParsed, + maxSupportedTransactionVersion: 0, + ), + ); + } catch (e) { + printV("Error fetching transaction: $e"); + return null; + } + })); + + transactionDetails.addAll(batchResponses.whereType()); // to avoid reaching the node RPS limit - await Future.delayed(Duration(milliseconds: 500)); + if (i + 20 < signatures.length) { + await Future.delayed(const Duration(milliseconds: 500)); + } } for (final tx in transactionDetails) { - if (tx.transaction is ParsedTransaction) { - final parsedTx = (tx.transaction as ParsedTransaction); - final message = parsedTx.message; - - final fee = (tx.meta?.fee ?? 0) / lamportsPerSol; - - for (final instruction in message.instructions) { - if (instruction is ParsedInstruction) { - instruction.map( - system: (systemData) { - systemData.parsed.map( - transfer: (transferData) { - ParsedSystemTransferInformation transfer = transferData.info; - bool isOutgoingTx = transfer.source == publicKey.toBase58(); - - double amount = transfer.lamports.toDouble() / lamportsPerSol; - - transactions.add( - SolanaTransactionModel( - id: parsedTx.signatures.first, - from: transfer.source, - to: transfer.destination, - amount: amount, - isOutgoingTx: isOutgoingTx, - blockTimeInInt: tx.blockTime!, - fee: fee, - programId: SystemProgram.programId, - tokenSymbol: 'SOL', - ), - ); - }, - transferChecked: (_) {}, - unsupported: (_) {}, - ); - }, - splToken: (splTokenData) { - if (splTokenSymbol != null) { - splTokenData.parsed.map( - transfer: (transferData) { - SplTokenTransferInfo transfer = transferData.info; - bool isOutgoingTx = transfer.source == publicKey.toBase58(); - - double amount = (double.tryParse(transfer.amount) ?? 0.0) / - math.pow(10, splTokenDecimal ?? 9); - - transactions.add( - SolanaTransactionModel( - id: parsedTx.signatures.first, - fee: fee, - from: transfer.source, - to: transfer.destination, - amount: amount, - isOutgoingTx: isOutgoingTx, - programId: TokenProgram.programId, - blockTimeInInt: tx.blockTime!, - tokenSymbol: splTokenSymbol, - ), - ); - }, - transferChecked: (transferCheckedData) { - SplTokenTransferCheckedInfo transfer = transferCheckedData.info; - bool isOutgoingTx = transfer.source == publicKey.toBase58(); - double amount = - double.tryParse(transfer.tokenAmount.uiAmountString ?? '0.0') ?? 0.0; - - transactions.add( - SolanaTransactionModel( - id: parsedTx.signatures.first, - fee: fee, - from: transfer.source, - to: transfer.destination, - amount: amount, - isOutgoingTx: isOutgoingTx, - programId: TokenProgram.programId, - blockTimeInInt: tx.blockTime!, - tokenSymbol: splTokenSymbol, - ), - ); - }, - generic: (genericData) {}, - ); - } - }, - memo: (_) {}, - unsupported: (a) {}, - ); - } - } + final parsedTx = await parseTransaction( + txResponse: tx, + splTokenSymbol: splTokenSymbol, + walletAddress: walletAddress?.address ?? address.address, + ); + if (parsedTx != null) { + transactions.add(parsedTx); } } return transactions; - } catch (err) { + } catch (err, s) { + printV('Error fetching transactions: $err \n$s'); return []; } } - Future> getSPLTokenTransfers( - String address, - String splTokenSymbol, - int splTokenDecimal, - Ed25519HDKeyPair ownerKeypair, - ) async { - final tokenMint = Ed25519HDPublicKey.fromBase58(address); - - ProgramAccount? associatedTokenAccount; - + Future> getSPLTokenTransfers({ + required String mintAddress, + required String splTokenSymbol, + required int splTokenDecimal, + required SolanaPrivateKey privateKey, + }) async { + ProgramDerivedAddress? associatedTokenAccount; + final ownerWalletAddress = privateKey.publicKey().toAddress(); try { - associatedTokenAccount = await _client!.getAssociatedTokenAccount( - mint: tokenMint, - owner: ownerKeypair.publicKey, - commitment: Commitment.confirmed, + associatedTokenAccount = await _getOrCreateAssociatedTokenAccount( + payerPrivateKey: privateKey, + mintAddress: SolAddress(mintAddress), + ownerAddress: ownerWalletAddress, + shouldCreateATA: false, ); - } catch (_) {} + } catch (e, s) { + printV('$e \n $s'); + } if (associatedTokenAccount == null) return []; - final accountPublicKey = Ed25519HDPublicKey.fromBase58(associatedTokenAccount.pubkey); + final accountPublicKey = associatedTokenAccount.address; final tokenTransactions = await fetchTransactions( accountPublicKey, splTokenSymbol: splTokenSymbol, splTokenDecimal: splTokenDecimal, + walletAddress: ownerWalletAddress, ); return tokenTransactions; } + Future fetchSPLTokenInfo(String mintAddress) async { + final programAddress = + MetaplexTokenMetaDataProgramUtils.findMetadataPda(mint: SolAddress(mintAddress)); + + final token = await _provider!.request( + SolanaRPCGetMetadataAccount( + account: programAddress.address, + commitment: Commitment.confirmed, + ), + ); + + if (token == null) { + return null; + } + + final metadata = token.data; + + String? iconPath; + //TODO(Further explore fetching images) + // try { + // iconPath = await _client.getIconImageFromTokenUri(metadata.uri); + // } catch (_) {} + + String filteredTokenSymbol = + metadata.symbol.replaceFirst(RegExp('^\\\$'), '').replaceAll('\u0000', ''); + + return SPLToken.fromMetadata( + name: metadata.name, + mint: metadata.symbol, + symbol: filteredTokenSymbol, + mintAddress: token.mint.address, + iconPath: iconPath, + ); + } + void stop() {} - SolanaClient? get getSolanaClient => _client; + SolanaRPC? get getSolanaProvider => _provider; Future signSolanaTransaction({ required String tokenTitle, required int tokenDecimals, required double inputAmount, required String destinationAddress, - required Ed25519HDKeyPair ownerKeypair, + required SolanaPrivateKey ownerPrivateKey, required bool isSendAll, required double solBalance, String? tokenMint, @@ -302,11 +432,9 @@ class SolanaWalletClient { if (tokenTitle == CryptoCurrency.sol.title) { final pendingNativeTokenTransaction = await _signNativeTokenTransaction( - tokenTitle: tokenTitle, - tokenDecimals: tokenDecimals, inputAmount: inputAmount, destinationAddress: destinationAddress, - ownerKeypair: ownerKeypair, + ownerPrivateKey: ownerPrivateKey, commitment: commitment, isSendAll: isSendAll, solBalance: solBalance, @@ -314,12 +442,11 @@ class SolanaWalletClient { return pendingNativeTokenTransaction; } else { final pendingSPLTokenTransaction = _signSPLTokenTransaction( - tokenTitle: tokenTitle, tokenDecimals: tokenDecimals, tokenMint: tokenMint!, inputAmount: inputAmount, + ownerPrivateKey: ownerPrivateKey, destinationAddress: destinationAddress, - ownerKeypair: ownerKeypair, commitment: commitment, solBalance: solBalance, ); @@ -327,47 +454,72 @@ class SolanaWalletClient { } } - Future _getLatestBlockhash(Commitment commitment) async { - final latestBlockHashResult = - await _client!.rpcClient.getLatestBlockhash(commitment: commitment).value; - - final latestBlockhash = LatestBlockhash( - blockhash: latestBlockHashResult.blockhash, - lastValidBlockHeight: latestBlockHashResult.lastValidBlockHeight, + Future _getLatestBlockhash(Commitment commitment) async { + final latestBlockhash = await _provider!.request( + const SolanaRPCGetLatestBlockhash(), ); - return latestBlockhash; + return latestBlockhash.blockhash; } - Message _getMessageForNativeTransaction( - Ed25519HDKeyPair ownerKeypair, - String destinationAddress, - int lamports, - ) { + Future _getMessageForNativeTransaction({ + required SolanaPublicKey publicKey, + required String destinationAddress, + required int lamports, + required Commitment commitment, + }) async { final instructions = [ - SystemInstruction.transfer( - fundingAccount: ownerKeypair.publicKey, - recipientAccount: Ed25519HDPublicKey.fromBase58(destinationAddress), - lamports: lamports, + SystemProgram.transfer( + from: publicKey.toAddress(), + layout: SystemTransferLayout(lamports: BigInt.from(lamports)), + to: SolAddress(destinationAddress), ), ]; - final message = Message(instructions: instructions); + final latestBlockhash = await _getLatestBlockhash(commitment); + + final message = Message.compile( + transactionInstructions: instructions, + payer: publicKey.toAddress(), + recentBlockhash: latestBlockhash, + ); return message; } - Future _getFeeFromCompiledMessage( - Message message, - Ed25519HDPublicKey feePayer, - LatestBlockhash latestBlockhash, - Commitment commitment, - ) async { - final compile = message.compile( - recentBlockhash: latestBlockhash.blockhash, - feePayer: feePayer, - ); + Future _getMessageForSPLTokenTransaction({ + required SolAddress ownerAddress, + required SolAddress destinationAddress, + required int tokenDecimals, + required SolAddress mintAddress, + required SolAddress sourceAccount, + required int amount, + required Commitment commitment, + }) async { + final instructions = [ + SPLTokenProgram.transferChecked( + layout: SPLTokenTransferCheckedLayout( + amount: BigInt.from(amount), + decimals: tokenDecimals, + ), + mint: mintAddress, + source: sourceAccount, + destination: destinationAddress, + owner: ownerAddress, + ) + ]; - final base64Message = base64Encode(compile.toByteArray().toList()); + final latestBlockhash = await _getLatestBlockhash(commitment); + + final message = Message.compile( + transactionInstructions: instructions, + payer: ownerAddress, + recentBlockhash: latestBlockhash, + ); + return message; + } + + Future _getFeeFromCompiledMessage(Message message, Commitment commitment) async { + final base64Message = base64Encode(message.serialize()); final fee = await getFeeForMessage(base64Message, commitment); @@ -379,43 +531,43 @@ class SolanaWalletClient { required double solBalance, required double fee, }) async { - return true; - // TODO: this is not doing what the name inclines - // final rent = - // await _client!.getMinimumBalanceForMintRentExemption(commitment: Commitment.confirmed); - // - // final rentInSol = (rent / lamportsPerSol).toDouble(); - // - // final remnant = solBalance - (inputAmount + fee); - // - // if (remnant > rentInSol) return true; - // - // return false; + final rent = await _provider!.request( + SolanaRPCGetMinimumBalanceForRentExemption( + size: SolanaTokenAccountUtils.accountSize, + ), + ); + + final rentInSol = (rent.toDouble() / SolanaUtils.lamportsPerSol).toDouble(); + + final remnant = solBalance - (inputAmount + fee); + + if (remnant > rentInSol) return true; + + return false; } Future _signNativeTokenTransaction({ - required String tokenTitle, - required int tokenDecimals, required double inputAmount, required String destinationAddress, - required Ed25519HDKeyPair ownerKeypair, + required SolanaPrivateKey ownerPrivateKey, required Commitment commitment, required bool isSendAll, required double solBalance, }) async { // Convert SOL to lamport - int lamports = (inputAmount * lamportsPerSol).toInt(); + int lamports = (inputAmount * SolanaUtils.lamportsPerSol).toInt(); - Message message = _getMessageForNativeTransaction(ownerKeypair, destinationAddress, lamports); + Message message = await _getMessageForNativeTransaction( + publicKey: ownerPrivateKey.publicKey(), + destinationAddress: destinationAddress, + lamports: lamports, + commitment: commitment, + ); - final signers = [ownerKeypair]; - - LatestBlockhash latestBlockhash = await _getLatestBlockhash(commitment); + SolAddress latestBlockhash = await _getLatestBlockhash(commitment); final fee = await _getFeeFromCompiledMessage( message, - signers.first.publicKey, - latestBlockhash, commitment, ); @@ -429,37 +581,44 @@ class SolanaWalletClient { throw SolanaSignNativeTokenTransactionRentException(); } - SignedTx signedTx; + String serializedTransaction; if (isSendAll) { - final feeInLamports = (fee * lamportsPerSol).toInt(); + final feeInLamports = (fee * SolanaUtils.lamportsPerSol).toInt(); final updatedLamports = lamports - feeInLamports; - final updatedMessage = - _getMessageForNativeTransaction(ownerKeypair, destinationAddress, updatedLamports); - - signedTx = await _signTransactionInternal( - message: updatedMessage, - signers: signers, - commitment: commitment, + final transaction = _constructNativeTransaction( + ownerPrivateKey: ownerPrivateKey, + destinationAddress: destinationAddress, latestBlockhash: latestBlockhash, + lamports: updatedLamports, + ); + + serializedTransaction = await _signTransactionInternal( + ownerPrivateKey: ownerPrivateKey, + transaction: transaction, ); } else { - signedTx = await _signTransactionInternal( - message: message, - signers: signers, - commitment: commitment, + final transaction = _constructNativeTransaction( + ownerPrivateKey: ownerPrivateKey, + destinationAddress: destinationAddress, latestBlockhash: latestBlockhash, + lamports: lamports, + ); + + serializedTransaction = await _signTransactionInternal( + ownerPrivateKey: ownerPrivateKey, + transaction: transaction, ); } sendTx() async => await sendTransaction( - signedTransaction: signedTx, + serializedTransaction: serializedTransaction, commitment: commitment, ); final pendingTransaction = PendingSolanaTransaction( amount: inputAmount, - signedTransaction: signedTx, + serializedTransaction: serializedTransaction, destinationAddress: destinationAddress, sendTransaction: sendTx, fee: fee, @@ -468,108 +627,170 @@ class SolanaWalletClient { return pendingTransaction; } + SolanaTransaction _constructNativeTransaction({ + required SolanaPrivateKey ownerPrivateKey, + required String destinationAddress, + required SolAddress latestBlockhash, + required int lamports, + }) { + final owner = ownerPrivateKey.publicKey().toAddress(); + + /// Create a transfer instruction to move funds from the owner to the receiver. + final transferInstruction = SystemProgram.transfer( + from: owner, + layout: SystemTransferLayout(lamports: BigInt.from(lamports)), + to: SolAddress(destinationAddress), + ); + + /// Construct a Solana transaction with the transfer instruction. + return SolanaTransaction( + instructions: [transferInstruction], + recentBlockhash: latestBlockhash, + payerKey: ownerPrivateKey.publicKey().toAddress(), + type: TransactionType.v0, + ); + } + + Future _getOrCreateAssociatedTokenAccount({ + required SolanaPrivateKey payerPrivateKey, + required SolAddress ownerAddress, + required SolAddress mintAddress, + required bool shouldCreateATA, + }) async { + final associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount( + mint: mintAddress, + owner: ownerAddress, + ); + + SolanaAccountInfo? accountInfo; + try { + accountInfo = await _provider!.request( + SolanaRPCGetAccountInfo(account: associatedTokenAccount.address), + ); + } catch (e) { + accountInfo = null; + } + + // If aacountInfo is null, signifies that the associatedTokenAccount has only been created locally and not been broadcasted to the blockchain. + if (accountInfo != null) return associatedTokenAccount; + + if (!shouldCreateATA) return null; + + final createAssociatedTokenAccount = AssociatedTokenAccountProgram.associatedTokenAccount( + payer: payerPrivateKey.publicKey().toAddress(), + associatedToken: associatedTokenAccount.address, + owner: ownerAddress, + mint: mintAddress, + ); + + final blockhash = await _getLatestBlockhash(Commitment.confirmed); + + final transaction = SolanaTransaction( + payerKey: payerPrivateKey.publicKey().toAddress(), + instructions: [createAssociatedTokenAccount], + recentBlockhash: blockhash, + ); + + transaction.sign([payerPrivateKey]); + + await sendTransaction( + serializedTransaction: transaction.serializeString(), + commitment: Commitment.confirmed, + ); + + // Delay for propagation on the blockchain for newly created associated token addresses + await Future.delayed(const Duration(seconds: 2)); + + return associatedTokenAccount; + } + Future _signSPLTokenTransaction({ - required String tokenTitle, required int tokenDecimals, required String tokenMint, required double inputAmount, required String destinationAddress, - required Ed25519HDKeyPair ownerKeypair, + required SolanaPrivateKey ownerPrivateKey, required Commitment commitment, required double solBalance, }) async { - final destinationOwner = Ed25519HDPublicKey.fromBase58(destinationAddress); - final mint = Ed25519HDPublicKey.fromBase58(tokenMint); + final mintAddress = SolAddress(tokenMint); // Input by the user final amount = (inputAmount * math.pow(10, tokenDecimals)).toInt(); - - ProgramAccount? associatedRecipientAccount; - ProgramAccount? associatedSenderAccount; - - associatedRecipientAccount = await _client!.getAssociatedTokenAccount( - mint: mint, - owner: destinationOwner, - commitment: commitment, - ); - - associatedSenderAccount = await _client!.getAssociatedTokenAccount( - owner: ownerKeypair.publicKey, - mint: mint, - commitment: commitment, - ); + ProgramDerivedAddress? associatedSenderAccount; + try { + associatedSenderAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount( + mint: mintAddress, + owner: ownerPrivateKey.publicKey().toAddress(), + ); + } catch (e) { + associatedSenderAccount = null; + } // Throw an appropriate exception if the sender has no associated // token account if (associatedSenderAccount == null) { - throw SolanaNoAssociatedTokenAccountException(ownerKeypair.address, mint.toBase58()); + throw SolanaNoAssociatedTokenAccountException( + ownerPrivateKey.publicKey().toAddress().address, + mintAddress.address, + ); } + ProgramDerivedAddress? associatedRecipientAccount; try { - if (associatedRecipientAccount == null) { - final derivedAddress = await findAssociatedTokenAddress( - owner: destinationOwner, - mint: mint, - ); - - final instruction = AssociatedTokenAccountInstruction.createAccount( - mint: mint, - address: derivedAddress, - owner: destinationOwner, - funder: ownerKeypair.publicKey, - ); - - final _signedTx = await _signTransactionInternal( - message: Message.only(instruction), - signers: [ownerKeypair], - commitment: commitment, - latestBlockhash: await _getLatestBlockhash(commitment), - ); - - await sendTransaction( - signedTransaction: _signedTx, - commitment: commitment, - ); - - associatedRecipientAccount = ProgramAccount( - pubkey: derivedAddress.toBase58(), - account: Account( - owner: destinationOwner.toBase58(), - lamports: 0, - executable: false, - rentEpoch: BigInt.zero, - data: null, - ), - ); - - await Future.delayed(Duration(seconds: 5)); - } + associatedRecipientAccount = await _getOrCreateAssociatedTokenAccount( + payerPrivateKey: ownerPrivateKey, + mintAddress: mintAddress, + ownerAddress: SolAddress(destinationAddress), + shouldCreateATA: true, + ); } catch (e) { - throw SolanaCreateAssociatedTokenAccountException(e.toString()); + associatedRecipientAccount = null; + + throw SolanaCreateAssociatedTokenAccountException( + 'Error fetching recipient associated token account: ${e.toString()}', + ); } - final instruction = TokenInstruction.transfer( - source: Ed25519HDPublicKey.fromBase58(associatedSenderAccount.pubkey), - destination: Ed25519HDPublicKey.fromBase58(associatedRecipientAccount.pubkey), - owner: ownerKeypair.publicKey, + if (associatedRecipientAccount == null) { + throw SolanaCreateAssociatedTokenAccountException( + 'Error fetching recipient associated token account', + ); + } + + final transferInstructions = SPLTokenProgram.transferChecked( + layout: SPLTokenTransferCheckedLayout( + amount: BigInt.from(amount), + decimals: tokenDecimals, + ), + mint: mintAddress, + source: associatedSenderAccount.address, + destination: associatedRecipientAccount.address, + owner: ownerPrivateKey.publicKey().toAddress(), + ); + + final latestBlockHash = await _getLatestBlockhash(commitment); + + final transaction = SolanaTransaction( + payerKey: ownerPrivateKey.publicKey().toAddress(), + instructions: [transferInstructions], + recentBlockhash: latestBlockHash, + ); + + final message = await _getMessageForSPLTokenTransaction( + ownerAddress: ownerPrivateKey.publicKey().toAddress(), + tokenDecimals: tokenDecimals, + mintAddress: mintAddress, + destinationAddress: associatedRecipientAccount.address, + sourceAccount: associatedSenderAccount.address, amount: amount, + commitment: commitment, ); - final message = Message(instructions: [instruction]); - - final signers = [ownerKeypair]; - - LatestBlockhash latestBlockhash = await _getLatestBlockhash(commitment); - - final fee = await _getFeeFromCompiledMessage( - message, - signers.first.publicKey, - latestBlockhash, - commitment, - ); + final fee = await _getFeeFromCompiledMessage(message, commitment); bool hasSufficientFundsLeft = await hasSufficientFundsLeftForRent( - inputAmount: inputAmount, + inputAmount: 0, fee: fee, solBalance: solBalance, ); @@ -578,25 +799,19 @@ class SolanaWalletClient { throw SolanaSignSPLTokenTransactionRentException(); } - final signedTx = await _signTransactionInternal( - message: message, - signers: signers, - commitment: commitment, - latestBlockhash: latestBlockhash, + final serializedTransaction = await _signTransactionInternal( + ownerPrivateKey: ownerPrivateKey, + transaction: transaction, ); - sendTx() async { - await Future.delayed(Duration(seconds: 3)); - - return await sendTransaction( - signedTransaction: signedTx, + sendTx() async => await sendTransaction( + serializedTransaction: serializedTransaction, commitment: commitment, ); - } final pendingTransaction = PendingSolanaTransaction( amount: inputAmount, - signedTransaction: signedTx, + serializedTransaction: serializedTransaction, destinationAddress: destinationAddress, sendTransaction: sendTx, fee: fee, @@ -604,37 +819,41 @@ class SolanaWalletClient { return pendingTransaction; } - Future _signTransactionInternal({ - required Message message, - required List signers, - required Commitment commitment, - required LatestBlockhash latestBlockhash, + Future _signTransactionInternal({ + required SolanaPrivateKey ownerPrivateKey, + required SolanaTransaction transaction, }) async { - final signedTx = await signTransaction(latestBlockhash, message, signers); + /// Sign the transaction with the owner's private key. + final ownerSignature = ownerPrivateKey.sign(transaction.serializeMessage()); + transaction.addSignature(ownerPrivateKey.publicKey().toAddress(), ownerSignature); - return signedTx; + /// Serialize the transaction. + final serializedTransaction = transaction.serializeString(); + + return serializedTransaction; } Future sendTransaction({ - required SignedTx signedTransaction, + required String serializedTransaction, required Commitment commitment, }) async { try { - final signature = await _client!.rpcClient.sendTransaction( - signedTransaction.encode(), - preflightCommitment: commitment, + /// Send the transaction to the Solana network. + final signature = await _provider!.request( + SolanaRPCSendTransaction( + encodedTransaction: serializedTransaction, + commitment: commitment, + ), ); - - _client!.waitForSignatureStatus(signature, status: commitment); - return signature; } catch (e) { - printV('Error while sending transaction: ${e.toString()}'); throw Exception(e); } } Future getIconImageFromTokenUri(String uri) async { + if (uri.isEmpty || uri == '…') return null; + try { final response = await httpClient.get(Uri.parse(uri)); diff --git a/cw_solana/lib/solana_transaction_info.dart b/cw_solana/lib/solana_transaction_info.dart index 7a0844e52..f51c55dad 100644 --- a/cw_solana/lib/solana_transaction_info.dart +++ b/cw_solana/lib/solana_transaction_info.dart @@ -34,7 +34,9 @@ class SolanaTransactionInfo extends TransactionInfo { @override String amountFormatted() { String stringBalance = solAmount.toString(); - + if (stringBalance.toString().length >= 12) { + stringBalance = stringBalance.substring(0, 12); + } return '$stringBalance $tokenSymbol'; } diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index 248f1282b..e6888cf91 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -30,9 +30,9 @@ import 'package:hex/hex.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:solana/base58.dart'; -import 'package:solana/metaplex.dart' as metaplex; -import 'package:solana/solana.dart'; +import 'package:on_chain/solana/solana.dart' hide Store; +import 'package:bip39/bip39.dart' as bip39; +import 'package:blockchain_utils/blockchain_utils.dart'; part 'solana_wallet.g.dart'; @@ -77,14 +77,6 @@ abstract class SolanaWalletBase final String? _hexPrivateKey; final EncryptionFileUtils encryptionFileUtils; - // The Solana WalletPair - Ed25519HDKeyPair? _walletKeyPair; - - Ed25519HDKeyPair? get walletKeyPair => _walletKeyPair; - - // To access the privateKey bytes. - Ed25519HDKeyPairData? _keyPairData; - late final SolanaWalletClient _client; @observable @@ -108,29 +100,23 @@ abstract class SolanaWalletBase final Completer _sharedPrefs = Completer(); @override - Ed25519HDKeyPairData get keys { - if (_keyPairData == null) { - return Ed25519HDKeyPairData([], publicKey: const Ed25519HDPublicKey([])); - } + Object get keys => throw UnimplementedError("keys"); - return _keyPairData!; - } + late final SolanaPrivateKey _solanaPrivateKey; + + late final SolanaPublicKey _solanaPublicKey; + + SolanaPublicKey get solanaPublicKey => _solanaPublicKey; + + SolanaPrivateKey get solanaPrivateKey => _solanaPrivateKey; + + String get solanaAddress => _solanaPublicKey.toAddress().address; @override String? get seed => _mnemonic; @override - String get privateKey { - final privateKeyBytes = _keyPairData!.bytes; - - final publicKeyBytes = _keyPairData!.publicKey.bytes; - - final encodedBytes = privateKeyBytes + publicKeyBytes; - - final privateKey = base58encode(encodedBytes); - - return privateKey; - } + String get privateKey => _solanaPrivateKey.seedHex(); @override WalletKeysData get walletKeysData => WalletKeysData(mnemonic: _mnemonic, privateKey: privateKey); @@ -140,35 +126,47 @@ abstract class SolanaWalletBase splTokensBox = await CakeHive.openBox(boxName); - // Create WalletPair using either the mnemonic or the privateKey - _walletKeyPair = await getWalletPair( + // Create the privatekey using either the mnemonic or the privateKey + _solanaPrivateKey = await getPrivateKey( mnemonic: _mnemonic, privateKey: _hexPrivateKey, + passphrase: passphrase, ); - // Extract the keyPairData containing both the privateKey bytes and the publicKey hex. - _keyPairData = await _walletKeyPair!.extract(); + // Extract the public key and wallet address + _solanaPublicKey = _solanaPrivateKey.publicKey(); - walletInfo.address = _walletKeyPair!.address; + walletInfo.address = _solanaPublicKey.toAddress().address; await walletAddresses.init(); await transactionHistory.init(); await save(); } - Future getWalletPair({String? mnemonic, String? privateKey}) async { + Future getPrivateKey({ + String? mnemonic, + String? privateKey, + String? passphrase, + }) async { assert(mnemonic != null || privateKey != null); if (mnemonic != null) { - return Wallet.fromMnemonic(mnemonic, account: 0, change: 0); + final seed = bip39.mnemonicToSeed(mnemonic, passphrase: passphrase ?? ''); + + // Derive a Solana private key from the seed + final bip44 = Bip44.fromSeed(seed, Bip44Coins.solana); + + final childKey = bip44.deriveDefaultPath.change(Bip44Changes.chainExt); + + return SolanaPrivateKey.fromSeed(childKey.privateKey.raw); } try { - final privateKeyBytes = base58decode(privateKey!); - return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes.take(32).toList()); + final keypairBytes = Base58Decoder.decode(privateKey!); + return SolanaPrivateKey.fromSeed(keypairBytes); } catch (_) { final privateKeyBytes = HEX.decode(privateKey!); - return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes); + return SolanaPrivateKey.fromBytes(privateKeyBytes); } } @@ -206,7 +204,8 @@ abstract class SolanaWalletBase Future _getEstimatedFees() async { try { - estimatedFee = await _client.getEstimatedFee(_walletKeyPair!); + estimatedFee = await _client.getEstimatedFee(_solanaPublicKey, Commitment.confirmed); + printV(estimatedFee.toString()); } catch (e) { estimatedFee = 0.0; } @@ -274,7 +273,7 @@ abstract class SolanaWalletBase tokenMint: tokenMint, tokenTitle: transactionCurrency.title, inputAmount: totalAmount, - ownerKeypair: _walletKeyPair!, + ownerPrivateKey: _solanaPrivateKey, tokenDecimals: transactionCurrency.decimals, destinationAddress: solCredentials.outputs.first.isParsedAddress ? solCredentials.outputs.first.extractedAddress! @@ -291,9 +290,7 @@ abstract class SolanaWalletBase /// Fetches the native SOL transactions linked to the wallet Public Key Future _updateNativeSOLTransactions() async { - final address = Ed25519HDPublicKey.fromBase58(_walletKeyPair!.address); - - final transactions = await _client.fetchTransactions(address); + final transactions = await _client.fetchTransactions(_solanaPublicKey.toAddress()); await _addTransactionsToTransactionHistory(transactions); } @@ -308,10 +305,10 @@ abstract class SolanaWalletBase for (var token in tokenKeys) { if (token is SPLToken) { final tokenTxs = await _client.getSPLTokenTransfers( - token.mintAddress, - token.symbol, - token.decimal, - _walletKeyPair!, + mintAddress: token.mintAddress, + splTokenSymbol: token.symbol, + splTokenDecimal: token.decimal, + privateKey: _solanaPrivateKey, ); // splTokenTransactions.addAll(tokenTxs); @@ -387,6 +384,7 @@ abstract class SolanaWalletBase 'mnemonic': _mnemonic, 'private_key': _hexPrivateKey, 'balance': balance[currency]!.toJSON(), + 'passphrase': passphrase, }); static Future open({ @@ -414,8 +412,9 @@ abstract class SolanaWalletBase if (!hasKeysFile) { final mnemonic = data!['mnemonic'] as String?; final privateKey = data['private_key'] as String?; + final passphrase = data['passphrase'] as String?; - keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey); + keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey, passphrase: passphrase); } else { keysData = await WalletKeysFile.readKeysFile( name, @@ -428,6 +427,7 @@ abstract class SolanaWalletBase return SolanaWallet( walletInfo: walletInfo, password: password, + passphrase: keysData.passphrase, mnemonic: keysData.mnemonic, privateKey: keysData.privateKey, initialBalance: balance, @@ -442,7 +442,7 @@ abstract class SolanaWalletBase } Future _fetchSOLBalance() async { - final balance = await _client.getBalance(_walletKeyPair!.address); + final balance = await _client.getBalance(solanaAddress); return SolanaBalance(balance); } @@ -451,10 +451,9 @@ abstract class SolanaWalletBase for (var token in splTokensBox.values) { if (token.enabled) { try { - final tokenBalance = - await _client.getSplTokenBalance(token.mintAddress, _walletKeyPair!.address) ?? - balance[token] ?? - SolanaBalance(0.0); + final tokenBalance = await _client.getSplTokenBalance(token.mintAddress, solanaAddress) ?? + balance[token] ?? + SolanaBalance(0.0); balance[token] = tokenBalance; } catch (e) { printV('Error fetching spl token (${token.symbol}) balance ${e.toString()}'); @@ -482,10 +481,9 @@ abstract class SolanaWalletBase await splTokensBox.put(token.mintAddress, token); if (token.enabled) { - final tokenBalance = - await _client.getSplTokenBalance(token.mintAddress, _walletKeyPair!.address) ?? - balance[token] ?? - SolanaBalance(0.0); + final tokenBalance = await _client.getSplTokenBalance(token.mintAddress, solanaAddress) ?? + balance[token] ?? + SolanaBalance(0.0); balance[token] = tokenBalance; } else { @@ -507,37 +505,10 @@ abstract class SolanaWalletBase } Future getSPLToken(String mintAddress) async { - // Convert SPL token mint address to public key - final Ed25519HDPublicKey mintPublicKey; try { - mintPublicKey = Ed25519HDPublicKey.fromBase58(mintAddress); - } catch (_) { - return null; - } - - // Fetch token's metadata account - try { - final token = await solanaClient!.rpcClient.getMetadata(mint: mintPublicKey); - - if (token == null) { - return null; - } - - String? iconPath; - try { - iconPath = await _client.getIconImageFromTokenUri(token.uri); - } catch (_) {} - - String filteredTokenSymbol = token.symbol.replaceFirst(RegExp('^\\\$'), ''); - - return SPLToken.fromMetadata( - name: token.name, - mint: token.mint, - symbol: filteredTokenSymbol, - mintAddress: mintAddress, - iconPath: iconPath, - ); - } catch (e) { + return await _client.fetchSPLTokenInfo(mintAddress); + } catch (e, s) { + printV('Error fetching token: ${e.toString()}, ${s.toString()}'); return null; } } @@ -582,7 +553,7 @@ abstract class SolanaWalletBase final messageBytes = utf8.encode(message); // Sign the message bytes with the wallet's private key - final signature = (await _walletKeyPair!.sign(messageBytes)).toString(); + final signature = (_solanaPrivateKey.sign(messageBytes)).toString(); return HEX.encode(utf8.encode(signature)).toUpperCase(); } @@ -596,7 +567,7 @@ abstract class SolanaWalletBase final base58EncodedPublicKeyString = match.group(2)!; final sigBytes = bytesString.split(', ').map(int.parse).toList(); - List pubKeyBytes = base58decode(base58EncodedPublicKeyString); + List pubKeyBytes = SolAddrDecoder().decodeAddr(base58EncodedPublicKeyString); return [sigBytes, pubKeyBytes]; } else { @@ -619,19 +590,18 @@ abstract class SolanaWalletBase } // make sure the address derived from the public key provided matches the one we expect - final pub = Ed25519HDPublicKey(pubKeyBytes); - if (address != pub.toBase58()) { + final pub = SolanaPublicKey.fromBytes(pubKeyBytes); + if (address != pub.toAddress().address) { return false; } - return await verifySignature( + return pub.verify( message: messageBytes, signature: sigBytes, - publicKey: Ed25519HDPublicKey(pubKeyBytes), ); } - SolanaClient? get solanaClient => _client.getSolanaClient; + SolanaRPC? get solanaProvider => _client.getSolanaProvider; @override String get password => _password; diff --git a/cw_solana/lib/solana_wallet_service.dart b/cw_solana/lib/solana_wallet_service.dart index aff75373e..a33cebb3a 100644 --- a/cw_solana/lib/solana_wallet_service.dart +++ b/cw_solana/lib/solana_wallet_service.dart @@ -33,6 +33,7 @@ class SolanaWalletService extends WalletService mintAddress.hashCode; } - -class NFT extends SPLToken { - final ImageInfo? imageInfo; - - NFT( - String mint, - String name, - String symbol, - String mintAddress, - int decimal, - String iconPath, - this.imageInfo, - ) : super( - name: name, - symbol: symbol, - mintAddress: mintAddress, - decimal: decimal, - mint: mint, - iconPath: iconPath, - ); -} - -class ImageInfo { - final String uri; - final OffChainMetadata? data; - - const ImageInfo(this.uri, this.data); -} diff --git a/cw_solana/pubspec.yaml b/cw_solana/pubspec.yaml index 807acdca8..82b5a2bc0 100644 --- a/cw_solana/pubspec.yaml +++ b/cw_solana/pubspec.yaml @@ -11,7 +11,6 @@ environment: dependencies: flutter: sdk: flutter - solana: ^0.31.0+1 cw_core: path: ../cw_core http: ^1.1.0 @@ -21,6 +20,14 @@ dependencies: shared_preferences: ^2.0.15 bip32: ^2.0.0 hex: ^0.2.0 + on_chain: + git: + url: https://github.com/cake-tech/on_chain.git + ref: cake-update-v2 + blockchain_utils: + git: + url: https://github.com/cake-tech/blockchain_utils + ref: cake-update-v2 dev_dependencies: flutter_test: diff --git a/cw_tron/pubspec.yaml b/cw_tron/pubspec.yaml index e69fd7ca0..80ea7ee51 100644 --- a/cw_tron/pubspec.yaml +++ b/cw_tron/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: path: ../cw_evm on_chain: git: - url: https://github.com/cake-tech/On_chain + url: https://github.com/cake-tech/on_chain.git ref: cake-update-v2 blockchain_utils: git: diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 120194ee0..5f8a14daa 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3,38 +3,8 @@ PODS: - Flutter - ReachabilitySwift - CryptoSwift (1.8.3) - - cw_haven (0.0.1): - - cw_haven/Boost (= 0.0.1) - - cw_haven/Haven (= 0.0.1) - - cw_haven/OpenSSL (= 0.0.1) - - cw_haven/Sodium (= 0.0.1) - - cw_shared_external - - Flutter - - cw_haven/Boost (0.0.1): - - cw_shared_external - - Flutter - - cw_haven/Haven (0.0.1): - - cw_shared_external - - Flutter - - cw_haven/OpenSSL (0.0.1): - - cw_shared_external - - Flutter - - cw_haven/Sodium (0.0.1): - - cw_shared_external - - Flutter - cw_mweb (0.0.1): - Flutter - - cw_shared_external (0.0.1): - - cw_shared_external/Boost (= 0.0.1) - - cw_shared_external/OpenSSL (= 0.0.1) - - cw_shared_external/Sodium (= 0.0.1) - - Flutter - - cw_shared_external/Boost (0.0.1): - - Flutter - - cw_shared_external/OpenSSL (0.0.1): - - Flutter - - cw_shared_external/Sodium (0.0.1): - - Flutter - device_display_brightness (0.0.1): - Flutter - device_info_plus (0.0.1): @@ -136,9 +106,7 @@ PODS: DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - CryptoSwift - - cw_haven (from `.symlinks/plugins/cw_haven/ios`) - cw_mweb (from `.symlinks/plugins/cw_mweb/ios`) - - cw_shared_external (from `.symlinks/plugins/cw_shared_external/ios`) - device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - devicelocale (from `.symlinks/plugins/devicelocale/ios`) @@ -179,12 +147,8 @@ SPEC REPOS: EXTERNAL SOURCES: connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" - cw_haven: - :path: ".symlinks/plugins/cw_haven/ios" cw_mweb: :path: ".symlinks/plugins/cw_mweb/ios" - cw_shared_external: - :path: ".symlinks/plugins/cw_shared_external/ios" device_display_brightness: :path: ".symlinks/plugins/device_display_brightness/ios" device_info_plus: @@ -239,9 +203,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483 - cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a cw_mweb: 22cd01dfb8ad2d39b15332006f22046aaa8352a3 - cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 devicelocale: 35ba84dc7f45f527c3001535d8c8d104edd5d926 diff --git a/lib/core/wallet_connect/chain_service/solana/solana_chain_service.dart b/lib/core/wallet_connect/chain_service/solana/solana_chain_service.dart index d7fe53c73..ca4017b98 100644 --- a/lib/core/wallet_connect/chain_service/solana/solana_chain_service.dart +++ b/lib/core/wallet_connect/chain_service/solana/solana_chain_service.dart @@ -1,5 +1,7 @@ +import 'dart:convert'; import 'dart:developer'; +import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cake_wallet/core/wallet_connect/chain_service/solana/entities/solana_sign_message.dart'; import 'package:cake_wallet/core/wallet_connect/chain_service/solana/solana_chain_id.dart'; import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; @@ -8,9 +10,9 @@ import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_w import 'package:cake_wallet/core/wallet_connect/models/connection_model.dart'; import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_widget.dart'; import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart'; +import 'package:cw_core/solana_rpc_http_service.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:solana/base58.dart'; -import 'package:solana/solana.dart'; +import 'package:on_chain/solana/solana.dart'; import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; import '../chain_service.dart'; import '../../wallet_connect_key_service.dart'; @@ -27,25 +29,19 @@ class SolanaChainServiceImpl implements ChainService { final SolanaChainId reference; - final SolanaClient solanaClient; + final SolanaRPC solanaProvider; - final Ed25519HDKeyPair? ownerKeyPair; + final SolanaPrivateKey? ownerPrivateKey; SolanaChainServiceImpl({ required this.reference, required this.wcKeyService, required this.bottomSheetService, required this.wallet, - required this.ownerKeyPair, - required String webSocketUrl, - required Uri rpcUrl, - SolanaClient? solanaClient, - }) : solanaClient = solanaClient ?? - SolanaClient( - rpcUrl: rpcUrl, - websocketUrl: Uri.parse(webSocketUrl), - timeout: const Duration(minutes: 5), - ) { + required this.ownerPrivateKey, + required String formattedRPCUrl, + SolanaRPC? solanaProvider, + }) : solanaProvider = solanaProvider ?? SolanaRPC(SolanaRPCHTTPService(url: formattedRPCUrl)) { for (final String event in getEvents()) { wallet.registerEventEmitter(chainId: getChainId(), event: event); } @@ -110,26 +106,20 @@ class SolanaChainServiceImpl implements ChainService { } try { - final message = - await solanaClient.rpcClient.getMessageFromEncodedTx(solanaSignTx.transaction); + // Convert transaction string to bytes + List transactionBytes = base64Decode(solanaSignTx.transaction); - final sign = await ownerKeyPair?.signMessage( - message: message, - recentBlockhash: solanaSignTx.recentBlockhash ?? '', + final message = SolanaTransactionUtils.deserializeMessageLegacy(transactionBytes); + + final sign = ownerPrivateKey!.sign(message.serialize()); + + final signature = solanaProvider.request( + SolanaRPCSendTransaction( + encodedTransaction: Base58Encoder.encode(sign), + commitment: Commitment.confirmed, + ), ); - if (sign == null) { - return ''; - } - - String signature = await solanaClient.sendAndConfirmTransaction( - message: message, - signers: [ownerKeyPair!], - commitment: Commitment.confirmed, - ); - - printV(signature); - bottomSheetService.queueBottomSheet( isModalDismissible: true, widget: BottomSheetMessageDisplayWidget( @@ -161,10 +151,10 @@ class SolanaChainServiceImpl implements ChainService { if (authError != null) { return authError; } - Signature? sign; + List? sign; try { - sign = await ownerKeyPair?.sign(base58decode(solanaSignMessage.message)); + sign = ownerPrivateKey!.sign(Base58Decoder.decode(solanaSignMessage.message)); } catch (e) { printV(e); } @@ -173,7 +163,7 @@ class SolanaChainServiceImpl implements ChainService { return ''; } - String signature = sign.toBase58(); + final signature = Base58Encoder.encode(sign); return signature; } diff --git a/lib/core/wallet_connect/web3wallet_service.dart b/lib/core/wallet_connect/web3wallet_service.dart index 3740d3dfe..898433c62 100644 --- a/lib/core/wallet_connect/web3wallet_service.dart +++ b/lib/core/wallet_connect/web3wallet_service.dart @@ -22,6 +22,7 @@ import 'package:cw_core/wallet_type.dart'; import 'package:eth_sig_util/eth_sig_util.dart'; import 'package:flutter/material.dart'; import 'package:mobx/mobx.dart'; +import 'package:on_chain/solana/solana.dart' hide Store; import 'package:shared_preferences/shared_preferences.dart'; import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; @@ -140,29 +141,28 @@ abstract class Web3WalletServiceBase with Store { for (final cId in SolanaChainId.values) { final node = appStore.settingsStore.getCurrentNode(appStore.wallet!.type); - Uri rpcUri = node.uri; - String webSocketUrl = 'wss://${node.uriRaw}'; + String formattedUrl; + String protocolUsed = node.isSSL ? "https" : "http"; if (node.uriRaw == 'rpc.ankr.com') { String ankrApiKey = secrets.ankrApiKey; - rpcUri = Uri.https(node.uriRaw, '/solana/$ankrApiKey'); - webSocketUrl = 'wss://${node.uriRaw}/solana/ws/$ankrApiKey'; + formattedUrl = '$protocolUsed://${node.uriRaw}/$ankrApiKey'; } else if (node.uriRaw == 'solana-mainnet.core.chainstack.com') { String chainStackApiKey = secrets.chainStackApiKey; - rpcUri = Uri.https(node.uriRaw, '/$chainStackApiKey'); - webSocketUrl = 'wss://${node.uriRaw}/$chainStackApiKey'; + formattedUrl = '$protocolUsed://${node.uriRaw}/$chainStackApiKey'; + } else { + formattedUrl = '$protocolUsed://${node.uriRaw}'; } SolanaChainServiceImpl( reference: cId, - rpcUrl: rpcUri, - webSocketUrl: webSocketUrl, + formattedRPCUrl: formattedUrl, wcKeyService: walletKeyService, bottomSheetService: _bottomSheetHandler, wallet: _web3Wallet, - ownerKeyPair: solana!.getWalletKeyPair(appStore.wallet!), + ownerPrivateKey: SolanaPrivateKey.fromSeedHex(solana!.getPrivateKey(appStore.wallet!)), ); } } diff --git a/lib/solana/cw_solana.dart b/lib/solana/cw_solana.dart index f0df5fba1..937b5b3bf 100644 --- a/lib/solana/cw_solana.dart +++ b/lib/solana/cw_solana.dart @@ -52,11 +52,8 @@ class CWSolana extends Solana { String getPrivateKey(WalletBase wallet) => (wallet as SolanaWallet).privateKey; @override - String getPublicKey(WalletBase wallet) => (wallet as SolanaWallet).keys.publicKey.toBase58(); - - @override - Ed25519HDKeyPair? getWalletKeyPair(WalletBase wallet) => (wallet as SolanaWallet).walletKeyPair; - + String getPublicKey(WalletBase wallet) => + (wallet as SolanaWallet).solanaPublicKey.toAddress().address; Object createSolanaTransactionCredentials( List outputs, { required CryptoCurrency currency, diff --git a/lib/src/screens/wallet_keys/wallet_keys_page.dart b/lib/src/screens/wallet_keys/wallet_keys_page.dart index ab6762f8d..be1972106 100644 --- a/lib/src/screens/wallet_keys/wallet_keys_page.dart +++ b/lib/src/screens/wallet_keys/wallet_keys_page.dart @@ -326,7 +326,7 @@ class _WalletKeysPageBodyState extends State ), ); } - + Widget _buildBottomActionPanel({ required String titleForClipboard, required String dataToCopy, diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index 14d7ad566..3959afe01 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -78,6 +78,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { WalletType.ethereum, WalletType.polygon, WalletType.tron, + WalletType.solana, WalletType.monero, WalletType.wownero, WalletType.zano, diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 72c17a079..767f0b1f3 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -106,12 +106,19 @@ dependencies: flutter_svg: ^2.0.9 polyseed: ^0.0.6 nostr_tools: ^1.0.9 - solana: ^0.31.0+1 ledger_flutter_plus: git: url: https://github.com/vespr-wallet/ledger-flutter-plus ref: c2e341d8038f1108690ad6f80f7b4b7156aacc76 hashlib: ^1.19.2 + on_chain: + git: + url: https://github.com/cake-tech/on_chain.git + ref: cake-update-v2 + blockchain_utils: + git: + url: https://github.com/cake-tech/blockchain_utils + ref: cake-update-v2 dev_dependencies: flutter_test: diff --git a/tool/configure.dart b/tool/configure.dart index c26e3f44b..259a5d9a0 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -1261,7 +1261,6 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:hive/hive.dart'; -import 'package:solana/solana.dart'; """; const solanaCWHeaders = """ @@ -1289,7 +1288,6 @@ abstract class Solana { String getAddress(WalletBase wallet); String getPrivateKey(WalletBase wallet); String getPublicKey(WalletBase wallet); - Ed25519HDKeyPair? getWalletKeyPair(WalletBase wallet); Object createSolanaTransactionCredentials( List outputs, { From 0f301a71a22ce7543375d82f16c09fdc1dc41819 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Fri, 14 Mar 2025 17:30:19 +0100 Subject: [PATCH 038/243] feat: Enable NFTs for Solana wallet (#2082) * feat: Enable NFTs for Solana wallet * Add localization for texts * Adjust file naming * Adjust file naming --- lib/entities/solana_nft_asset_model.dart | 130 +++++++++ lib/reactions/wallet_connect.dart | 13 +- lib/router.dart | 2 +- .../dashboard/pages/balance/balance_page.dart | 10 +- .../dashboard/pages/nft_details_page.dart | 272 ++++++++++++------ .../dashboard/pages/nft_import_page.dart | 83 +++--- .../dashboard/pages/nft_listing_page.dart | 112 +++++--- .../dashboard/widgets/nft_tile_widget.dart | 10 +- .../widgets/solana_nft_tile_widget.dart | 97 +++++++ lib/view_model/dashboard/nft_view_model.dart | 140 ++++++--- res/values/strings_ar.arb | 7 +- res/values/strings_bg.arb | 7 +- res/values/strings_cs.arb | 7 +- res/values/strings_de.arb | 7 +- res/values/strings_en.arb | 7 +- res/values/strings_es.arb | 5 + res/values/strings_fr.arb | 7 +- res/values/strings_ha.arb | 7 +- res/values/strings_hi.arb | 9 +- res/values/strings_hr.arb | 7 +- res/values/strings_hy.arb | 5 + res/values/strings_id.arb | 7 +- res/values/strings_it.arb | 7 +- res/values/strings_ja.arb | 7 +- res/values/strings_ko.arb | 7 +- res/values/strings_my.arb | 7 +- res/values/strings_nl.arb | 7 +- res/values/strings_pl.arb | 7 +- res/values/strings_pt.arb | 7 +- res/values/strings_ru.arb | 7 +- res/values/strings_th.arb | 7 +- res/values/strings_tl.arb | 7 +- res/values/strings_tr.arb | 7 +- res/values/strings_uk.arb | 7 +- res/values/strings_ur.arb | 7 +- res/values/strings_vi.arb | 5 + res/values/strings_yo.arb | 7 +- res/values/strings_zh.arb | 7 +- 38 files changed, 816 insertions(+), 245 deletions(-) create mode 100644 lib/entities/solana_nft_asset_model.dart create mode 100644 lib/src/screens/dashboard/widgets/solana_nft_tile_widget.dart diff --git a/lib/entities/solana_nft_asset_model.dart b/lib/entities/solana_nft_asset_model.dart new file mode 100644 index 000000000..51f43c583 --- /dev/null +++ b/lib/entities/solana_nft_asset_model.dart @@ -0,0 +1,130 @@ +class SolanaNFTAssetModel { + String? address; + String? mint; + String? standard; + String? name; + String? symbol; + String? description; + String? imageOriginalUrl; + String? externalUrl; + String? metadataOriginalUrl; + String? totalSupply; + Metaplex? metaplex; + Collection? collection; + Contract? contract; + + SolanaNFTAssetModel({ + this.address, + this.mint, + this.standard, + this.name, + this.symbol, + this.description, + this.imageOriginalUrl, + this.externalUrl, + this.metadataOriginalUrl, + this.totalSupply, + this.metaplex, + this.collection, + this.contract, + }); + + factory SolanaNFTAssetModel.fromJson(Map json) { + return SolanaNFTAssetModel( + address: json['address'] as String?, + mint: json['mint'] as String?, + standard: json['standard'] as String?, + name: json['name'] as String?, + symbol: json['symbol'] as String?, + description: json['description'] as String?, + imageOriginalUrl: json['imageOriginalUrl'] as String?, + externalUrl: json['externalUrl'] as String?, + metadataOriginalUrl: json['metadataOriginalUrl'] as String?, + totalSupply: json['totalSupply'] as String?, + metaplex: json['metaplex'] != null + ? Metaplex.fromJson(json['metaplex'] as Map) + : null, + collection: json['collection'] != null + ? Collection.fromJson(json['collection'] as Map) + : null, + contract: json['contract'] != null + ? Contract.fromJson(json['contract'] as Map) + : null, + ); + } +} + +class Metaplex { + String? metadataUri; + String? updateAuthority; + int? sellerFeeBasisPoints; + int? primarySaleHappened; + bool? isMutable; + bool? masterEdition; + + Metaplex( + {this.metadataUri, + this.updateAuthority, + this.sellerFeeBasisPoints, + this.primarySaleHappened, + this.isMutable, + this.masterEdition}); + + factory Metaplex.fromJson(Map json) { + return Metaplex( + metadataUri: json['metadataUri'] as String?, + updateAuthority: json['updateAuthority'] as String?, + sellerFeeBasisPoints: json['sellerFeeBasisPoints'] as int?, + primarySaleHappened: json['primarySaleHappened'] as int?, + isMutable: json['isMutable'] as bool?, + masterEdition: json['masterEdition'] as bool?, + ); + } +} + +class Collection { + String? collectionAddress; + String? name; + String? description; + String? imageOriginalUrl; + String? externalUrl; + String? metaplexMint; + int? sellerFeeBasisPoints; + + Collection( + {this.collectionAddress, + this.name, + this.description, + this.imageOriginalUrl, + this.externalUrl, + this.metaplexMint, + this.sellerFeeBasisPoints}); + + factory Collection.fromJson(Map json) { + return Collection( + collectionAddress: json['collectionAddress'] as String?, + name: json['name'] as String?, + description: json['description'] as String?, + imageOriginalUrl: json['imageOriginalUrl'] as String?, + externalUrl: json['externalUrl'] as String?, + metaplexMint: json['metaplexMint'] as String?, + sellerFeeBasisPoints: json['sellerFeeBasisPoints'] as int?, + ); + } +} + +class Contract { + String? type; + String? name; + String? symbol; + + Contract({this.type, this.name, this.symbol}); + + factory Contract.fromJson(Map json) { + return Contract( + type: json['type'] as String?, + name: json['name'] as String?, + symbol: json['symbol'] as String?, + ); + } +} diff --git a/lib/reactions/wallet_connect.dart b/lib/reactions/wallet_connect.dart index ca908bc65..217664eaa 100644 --- a/lib/reactions/wallet_connect.dart +++ b/lib/reactions/wallet_connect.dart @@ -12,6 +12,17 @@ bool isEVMCompatibleChain(WalletType walletType) { } } +bool isNFTACtivatedChain(WalletType walletType) { + switch (walletType) { + case WalletType.polygon: + case WalletType.ethereum: + case WalletType.solana: + return true; + default: + return false; + } +} + bool isWalletConnectCompatibleChain(WalletType walletType) { switch (walletType) { case WalletType.polygon: @@ -55,7 +66,7 @@ String getChainNameBasedOnWalletType(WalletType walletType) { case WalletType.polygon: return 'polygon'; case WalletType.solana: - return 'solana'; + return 'mainnet'; default: return ''; } diff --git a/lib/router.dart b/lib/router.dart index 72fc06e40..9026919d6 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -771,7 +771,7 @@ Route createRoute(RouteSettings settings) { case Routes.nftDetailsPage: return MaterialPageRoute( builder: (_) => NFTDetailsPage( - nftAsset: settings.arguments as NFTAssetModel, + arguments: settings.arguments as NFTDetailsPageArguments, dashboardViewModel: getIt.get(), ), ); diff --git a/lib/src/screens/dashboard/pages/balance/balance_page.dart b/lib/src/screens/dashboard/pages/balance/balance_page.dart index e9c2115a9..8577ba0ad 100644 --- a/lib/src/screens/dashboard/pages/balance/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance/balance_page.dart @@ -23,13 +23,13 @@ class BalancePage extends StatelessWidget { Widget build(BuildContext context) { return Observer( builder: (context) { - final isEVMCompatible = isEVMCompatibleChain(dashboardViewModel.type); + final isNFTActivated = isNFTACtivatedChain(dashboardViewModel.type); return DefaultTabController( - key: ValueKey(isEVMCompatible), - length: isEVMCompatible ? 2 : 1, + key: ValueKey(isNFTActivated), + length: isNFTActivated ? 2 : 1, child: Column( children: [ - if (isEVMCompatible) + if (isNFTActivated) Align( alignment: Alignment.centerLeft, child: Padding( @@ -76,7 +76,7 @@ class BalancePage extends StatelessWidget { physics: NeverScrollableScrollPhysics(), children: [ CryptoBalanceWidget(dashboardViewModel: dashboardViewModel), - if (isEVMCompatible) NFTListingPage(nftViewModel: nftViewModel) + if (isNFTActivated) NFTListingPage(nftViewModel: nftViewModel) ], ), ), diff --git a/lib/src/screens/dashboard/pages/nft_details_page.dart b/lib/src/screens/dashboard/pages/nft_details_page.dart index b8352a672..c82c5ce90 100644 --- a/lib/src/screens/dashboard/pages/nft_details_page.dart +++ b/lib/src/screens/dashboard/pages/nft_details_page.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; +import 'package:cake_wallet/entities/solana_nft_asset_model.dart'; import 'package:cake_wallet/entities/wallet_nft_response.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; @@ -8,21 +10,23 @@ import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; -import 'package:flutter/material.dart'; class NFTDetailsPage extends BasePage { - NFTDetailsPage({required this.dashboardViewModel, required this.nftAsset}); + NFTDetailsPage({ + required this.dashboardViewModel, + required this.arguments, + Key? key, + }); final DashboardViewModel dashboardViewModel; - final NFTAssetModel nftAsset; + final NFTDetailsPageArguments arguments; @override bool get gradientBackground => true; @override Widget Function(BuildContext, Widget) get rootWrapper => - (BuildContext context, Widget scaffold) => - GradientBackground(scaffold: scaffold); + (BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold); @override bool get resizeToAvoidBottomInset => false; @@ -30,15 +34,14 @@ class NFTDetailsPage extends BasePage { @override Widget get endDrawer => MenuWidget( dashboardViewModel, - ValueKey('nft_details_page_menu_widget_key'), + const ValueKey('nft_details_page_menu_widget_key'), ); @override Widget trailing(BuildContext context) { final menuButton = Image.asset( 'assets/images/menu.png', - color: - Theme.of(context).extension()!.pageTitleTextColor, + color: Theme.of(context).extension()!.pageTitleTextColor, ); return Container( @@ -58,119 +61,200 @@ class NFTDetailsPage extends BasePage { @override Widget body(BuildContext context) { return SingleChildScrollView( - child: Column( - children: [ - Container( - width: double.infinity, - margin: const EdgeInsets.all(16), - padding: const EdgeInsets.symmetric(vertical: 16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30.0), - border: Border.all( - color: Theme.of(context) - .extension()! - .cardBorderColor, - width: 1, - ), - color: Theme.of(context) - .extension()! - .syncedBackgroundColor, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: MediaQuery.sizeOf(context).height / 2.5, - width: double.infinity, - clipBehavior: Clip.hardEdge, - margin: const EdgeInsets.all(8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16.0), - border: Border.all( - color: Theme.of(context) - .extension()! - .cardBorderColor, - width: 1, - ), - color: Theme.of(context) - .extension()! - .syncedBackgroundColor, - - ), - child: CakeImageWidget( - imageUrl: nftAsset.normalizedMetadata?.imageUrl, - ), - ), - SizedBox(height: 16), - _NFTSingleInfoTile( - infoType: S.current.name, - infoValue: nftAsset.normalizedMetadata?.name ?? '---', - ), - - if (nftAsset.normalizedMetadata?.description != null) ...[ - SizedBox(height: 16), - _NFTSingleInfoTile( - infoType: S.current.description, - infoValue: nftAsset.normalizedMetadata?.description ?? '---', - ), - ], - - SizedBox(height: 16), - _NFTSingleInfoTile( - infoType: S.current.contractName, - infoValue: nftAsset.name ?? '---', - ), - SizedBox(height: 8), - _NFTSingleInfoTile( - infoType: S.current.contractSymbol, - infoValue: nftAsset.symbol ?? '---', - ), - ], - ), + child: Container( + width: double.infinity, + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30.0), + border: Border.all( + color: Theme.of(context).extension()!.cardBorderColor, + width: 1, ), - ], + color: Theme.of(context).extension()!.syncedBackgroundColor, + ), + child: arguments.isSolanaNFT + ? SolanaNFTDetailsWidget( + solanaNftAsset: arguments.solanaNFTAssetModel, + ) + : EVMChainNFTDetailsWidget( + nftAsset: arguments.nftAsset, + ), ), ); } } +class _NFTImageWidget extends StatelessWidget { + final String? imageUrl; + + const _NFTImageWidget({Key? key, this.imageUrl}) : super(key: key); + + @override + Widget build(BuildContext context) { + final balanceTheme = Theme.of(context).extension()!; + final syncTheme = Theme.of(context).extension()!; + + return Container( + height: MediaQuery.sizeOf(context).height / 2.5, + width: double.infinity, + margin: const EdgeInsets.all(8), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16.0), + border: Border.all( + color: balanceTheme.cardBorderColor, + width: 1, + ), + color: syncTheme.syncedBackgroundColor, + ), + child: CakeImageWidget(imageUrl: imageUrl), + ); + } +} + +class EVMChainNFTDetailsWidget extends StatelessWidget { + final NFTAssetModel? nftAsset; + + const EVMChainNFTDetailsWidget({Key? key, this.nftAsset}) : super(key: key); + + @override + Widget build(BuildContext context) { + if (nftAsset == null) { + return Center(child: Text(S.current.no_extra_detail)); + } + + final metadata = nftAsset!.normalizedMetadata; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _NFTImageWidget(imageUrl: metadata?.imageUrl), + const SizedBox(height: 16), + _NFTSingleInfoTile( + infoType: S.current.name, + infoValue: metadata?.name ?? '---', + ), + if (metadata?.description != null) ...[ + const SizedBox(height: 16), + _NFTSingleInfoTile( + infoType: S.current.description, + infoValue: metadata!.description ?? '---', + ), + ], + const SizedBox(height: 16), + _NFTSingleInfoTile( + infoType: S.current.contractName, + infoValue: nftAsset!.name ?? '---', + ), + const SizedBox(height: 8), + _NFTSingleInfoTile( + infoType: S.current.contractSymbol, + infoValue: nftAsset!.symbol ?? '---', + ), + ], + ); + } +} + +class SolanaNFTDetailsWidget extends StatelessWidget { + final SolanaNFTAssetModel? solanaNftAsset; + + const SolanaNFTDetailsWidget({Key? key, this.solanaNftAsset}) : super(key: key); + + @override + Widget build(BuildContext context) { + if (solanaNftAsset == null) { + return Center(child: Text(S.current.no_extra_detail)); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _NFTImageWidget(imageUrl: solanaNftAsset?.imageOriginalUrl), + const SizedBox(height: 16), + _NFTSingleInfoTile( + infoType: S.current.name, + infoValue: solanaNftAsset?.name ?? '---', + ), + if (solanaNftAsset?.description != null) ...[ + const SizedBox(height: 16), + _NFTSingleInfoTile( + infoType: S.current.description, + infoValue: solanaNftAsset!.description ?? '---', + ), + ], + const SizedBox(height: 16), + _NFTSingleInfoTile( + infoType: S.current.mint_address, + infoValue: solanaNftAsset?.mint ?? '---', + ), + const SizedBox(height: 16), + _NFTSingleInfoTile( + infoType: S.current.contractName, + infoValue: solanaNftAsset?.contract?.name ?? '---', + ), + const SizedBox(height: 16), + _NFTSingleInfoTile( + infoType: S.current.contractSymbol, + infoValue: solanaNftAsset?.contract?.symbol ?? '---', + ), + const SizedBox(height: 16), + _NFTSingleInfoTile( + infoType: S.current.collection_name, + infoValue: solanaNftAsset?.collection?.name ?? '---', + ), + const SizedBox(height: 16), + _NFTSingleInfoTile( + infoType: S.current.collection_description, + infoValue: solanaNftAsset?.collection?.description ?? '---', + ), + const SizedBox(height: 16), + _NFTSingleInfoTile( + infoType: S.current.collection_address, + infoValue: solanaNftAsset?.collection?.collectionAddress ?? '---', + ), + const SizedBox(height: 16), + ], + ); + } +} + class _NFTSingleInfoTile extends StatelessWidget { + final String infoType; + final String infoValue; + const _NFTSingleInfoTile({ required this.infoType, required this.infoValue, }); - final String infoType; - final String infoValue; @override Widget build(BuildContext context) { + final balanceTheme = Theme.of(context).extension()!; return Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( -infoType, + infoType, style: TextStyle( fontSize: 12, fontFamily: 'Lato', fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .labelTextColor, + color: balanceTheme.labelTextColor, height: 1, ), ), - SizedBox(height: 8), + const SizedBox(height: 8), Text( infoValue, style: TextStyle( fontSize: 16, fontFamily: 'Lato', fontWeight: FontWeight.w600, - color: Theme.of(context) - .extension()! - .assetTitleColor, + color: balanceTheme.assetTitleColor, height: 1, ), ), @@ -179,3 +263,15 @@ infoType, ); } } + +class NFTDetailsPageArguments { + NFTDetailsPageArguments({ + this.nftAsset, + this.solanaNFTAssetModel, + required this.isSolanaNFT, + }); + + final NFTAssetModel? nftAsset; + final SolanaNFTAssetModel? solanaNFTAssetModel; + final bool isSolanaNFT; +} diff --git a/lib/src/screens/dashboard/pages/nft_import_page.dart b/lib/src/screens/dashboard/pages/nft_import_page.dart index 6b40b46cc..3ed30d6f8 100644 --- a/lib/src/screens/dashboard/pages/nft_import_page.dart +++ b/lib/src/screens/dashboard/pages/nft_import_page.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/seed_widget_theme.dart'; import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -68,7 +69,6 @@ class _ImportNFTPage extends BasePage { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( S.current.address, textAlign: TextAlign.center, @@ -76,8 +76,7 @@ class _ImportNFTPage extends BasePage { fontSize: 16, fontFamily: 'Lato', fontWeight: FontWeight.w800, - color: - Theme.of(context).extension()!.hintTextColor, + color: Theme.of(context).extension()!.hintTextColor, height: 1, ), ), @@ -92,9 +91,7 @@ class _ImportNFTPage extends BasePage { tokenAddressController.text = tokenAddress; } }, - borderColor: Theme.of(context) - .extension()! - .textfieldUnderlineColor, + borderColor: Theme.of(context).extension()!.textfieldUnderlineColor, iconColor: Theme.of(context).primaryColor, placeholder: '0x...', textStyle: TextStyle( @@ -108,46 +105,45 @@ class _ImportNFTPage extends BasePage { color: PaletteDark.darkCyanBlue, ), ), - - SizedBox(height: 48), - Text( - S.current.tokenID, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - fontFamily: 'Lato', - fontWeight: FontWeight.w800, - color: Theme.of(context).extension()!.hintTextColor, - height: 1, + if (nftViewModel.appStore.wallet!.type != WalletType.solana) ...[ + SizedBox(height: 48), + Text( + S.current.tokenID, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontFamily: 'Lato', + fontWeight: FontWeight.w800, + color: Theme.of(context).extension()!.hintTextColor, + height: 1, + ), ), - ), - AddressTextField( - controller: tokenIDController, - options: [AddressTextFieldOption.paste], - onPushPasteButton: (context) async { - final clipboard = await Clipboard.getData('text/plain'); - final tokenID = clipboard?.text ?? ''; + AddressTextField( + controller: tokenIDController, + options: [AddressTextFieldOption.paste], + onPushPasteButton: (context) async { + final clipboard = await Clipboard.getData('text/plain'); + final tokenID = clipboard?.text ?? ''; - if (tokenID.isNotEmpty) { - tokenIDController.text = tokenID; - } - }, - borderColor: Theme.of(context) - .extension()! - .textfieldUnderlineColor, - iconColor: Theme.of(context).primaryColor, - placeholder: S.current.enterTokenID, - textStyle: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - color: PaletteDark.darkCyanBlue, + if (tokenID.isNotEmpty) { + tokenIDController.text = tokenID; + } + }, + borderColor: Theme.of(context).extension()!.textfieldUnderlineColor, + iconColor: Theme.of(context).primaryColor, + placeholder: S.current.enterTokenID, + textStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: PaletteDark.darkCyanBlue, + ), + hintStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: PaletteDark.darkCyanBlue, + ), ), - hintStyle: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - color: PaletteDark.darkCyanBlue, - ), - ), + ], Spacer(), Observer(builder: (context) { return LoadingPrimaryButton( @@ -161,6 +157,7 @@ class _ImportNFTPage extends BasePage { }, ); }), + SizedBox(height: 16), ], ), ); diff --git a/lib/src/screens/dashboard/pages/nft_listing_page.dart b/lib/src/screens/dashboard/pages/nft_listing_page.dart index 46690a969..8da63fce6 100644 --- a/lib/src/screens/dashboard/pages/nft_listing_page.dart +++ b/lib/src/screens/dashboard/pages/nft_listing_page.dart @@ -1,32 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/nft_tile_widget.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/solana_nft_tile_widget.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:cw_core/wallet_type.dart'; class NFTListingPage extends StatelessWidget { final NFTViewModel nftViewModel; const NFTListingPage({super.key, required this.nftViewModel}); + @override Widget build(BuildContext context) { + final dashboardTheme = Theme.of(context).extension()!; + final syncIndicatorTheme = Theme.of(context).extension()!; + final exchangeTheme = Theme.of(context).extension()!; + return Observer( builder: (context) { return Column( children: [ - SizedBox(height: 16), + const SizedBox(height: 16), Padding( - padding: EdgeInsets.only(left: 16, right: 16, bottom: 16), + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), child: PrimaryButton( text: S.current.import, - color: Theme.of(context) - .extension()! - .syncedBackgroundColor, + color: syncIndicatorTheme.syncedBackgroundColor, textColor: Colors.white, onPressed: () => Navigator.pushNamed( context, @@ -39,46 +44,75 @@ class NFTListingPage extends StatelessWidget { Expanded( child: Center( child: CircularProgressIndicator( - backgroundColor: Theme.of(context).extension()!.textColor, + backgroundColor: dashboardTheme.textColor, valueColor: AlwaysStoppedAnimation( - Theme.of(context) - .extension()! - .firstGradientBottomPanelColor, + exchangeTheme.firstGradientBottomPanelColor, ), ), ), - ), - if (!nftViewModel.isLoading) - Expanded( - child: nftViewModel.nftAssetByWalletModels.isEmpty - ? Center( - child: Text( - S.current.noNFTYet, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 20, - fontFamily: 'Lato', - fontWeight: FontWeight.w600, - color: Theme.of(context) - .extension()! - .pageTitleTextColor, - height: 1, - ), - ), - ) - : ListView.separated( - padding: EdgeInsets.symmetric(horizontal: 4, vertical: 16), - separatorBuilder: (context, index) => SizedBox(height: 8), - itemCount: nftViewModel.nftAssetByWalletModels.length, - itemBuilder: (context, index) { - final nftAsset = nftViewModel.nftAssetByWalletModels[index]; - return NFTTileWidget(nftAsset: nftAsset); - }, - ), ) + else + Expanded( + child: NFTListWidget(nftViewModel: nftViewModel), + ), ], ); }, ); } } + +class NFTListWidget extends StatelessWidget { + const NFTListWidget({required this.nftViewModel, super.key}); + + final NFTViewModel nftViewModel; + + @override + Widget build(BuildContext context) { + return Observer( + builder: (context) { + final isSolana = nftViewModel.appStore.wallet!.type == WalletType.solana; + + final emptyMessage = Center( + child: Text( + S.current.noNFTYet, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).extension()!.pageTitleTextColor, + height: 1, + ), + ), + ); + + if (isSolana) { + if (nftViewModel.solanaNftAssetModels.isEmpty) return emptyMessage; + + return ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 16), + separatorBuilder: (context, index) => const SizedBox(height: 8), + itemCount: nftViewModel.solanaNftAssetModels.length, + itemBuilder: (context, index) { + final nftAsset = nftViewModel.solanaNftAssetModels[index]; + return SolanaNFTTileWidget(nftAsset: nftAsset); + }, + ); + } else { + if (nftViewModel.nftAssetByWalletModels.isEmpty) return emptyMessage; + + return ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 16), + separatorBuilder: (context, index) => const SizedBox(height: 8), + itemCount: nftViewModel.nftAssetByWalletModels.length, + itemBuilder: (context, index) { + final nftAsset = nftViewModel.nftAssetByWalletModels[index]; + return NFTTileWidget(nftAsset: nftAsset); + }, + ); + } + }, + ); + } +} diff --git a/lib/src/screens/dashboard/widgets/nft_tile_widget.dart b/lib/src/screens/dashboard/widgets/nft_tile_widget.dart index 4c4d214e7..c7d79bfa7 100644 --- a/lib/src/screens/dashboard/widgets/nft_tile_widget.dart +++ b/lib/src/screens/dashboard/widgets/nft_tile_widget.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/entities/wallet_nft_response.dart'; import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/nft_details_page.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; @@ -12,7 +13,14 @@ class NFTTileWidget extends StatelessWidget { @override Widget build(BuildContext context) { return InkWell( - onTap: () => Navigator.pushNamed(context, Routes.nftDetailsPage, arguments: nftAsset), + onTap: () => Navigator.pushNamed( + context, + Routes.nftDetailsPage, + arguments: NFTDetailsPageArguments( + isSolanaNFT: false, + nftAsset: nftAsset, + ), + ), child: Container( width: double.infinity, margin: const EdgeInsets.only(left: 16, right: 16), diff --git a/lib/src/screens/dashboard/widgets/solana_nft_tile_widget.dart b/lib/src/screens/dashboard/widgets/solana_nft_tile_widget.dart new file mode 100644 index 000000000..bd134d7b5 --- /dev/null +++ b/lib/src/screens/dashboard/widgets/solana_nft_tile_widget.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:cake_wallet/entities/solana_nft_asset_model.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/nft_details_page.dart'; +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; +import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; + +class SolanaNFTTileWidget extends StatelessWidget { + const SolanaNFTTileWidget({super.key, required this.nftAsset}); + + final SolanaNFTAssetModel nftAsset; + + @override + Widget build(BuildContext context) { + final balanceTheme = Theme.of(context).extension()!; + final syncTheme = Theme.of(context).extension()!; + + return InkWell( + onTap: () { + Navigator.pushNamed( + context, + Routes.nftDetailsPage, + arguments: NFTDetailsPageArguments( + isSolanaNFT: true, + solanaNFTAssetModel: nftAsset, + ), + ); + }, + child: Container( + width: double.infinity, + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16.0), + border: Border.all( + color: balanceTheme.cardBorderColor, + width: 1, + ), + color: syncTheme.syncedBackgroundColor, + ), + child: Row( + children: [ + Container( + height: 100, + width: 100, + margin: const EdgeInsets.all(8), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16.0), + border: Border.all( + color: balanceTheme.cardBorderColor, + width: 1, + ), + color: syncTheme.syncedBackgroundColor, + ), + child: CakeImageWidget( + imageUrl: nftAsset.imageOriginalUrl, + ), + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Symbol: ${nftAsset.symbol ?? '---'}', + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: balanceTheme.labelTextColor, + height: 1, + ), + ), + const SizedBox(height: 8), + Text( + (nftAsset.name?.isNotEmpty ?? false) + ? nftAsset.name! + : (nftAsset.symbol ?? '---'), + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w900, + color: balanceTheme.assetTitleColor, + height: 1, + ), + ), + ], + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/view_model/dashboard/nft_view_model.dart b/lib/view_model/dashboard/nft_view_model.dart index f00f929a3..b0474604b 100644 --- a/lib/view_model/dashboard/nft_view_model.dart +++ b/lib/view_model/dashboard/nft_view_model.dart @@ -2,8 +2,10 @@ import 'dart:convert'; import 'dart:developer'; import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; +import 'package:cake_wallet/entities/solana_nft_asset_model.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_widget.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:http/http.dart' as http; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; @@ -19,7 +21,8 @@ abstract class NFTViewModelBase with Store { NFTViewModelBase(this.appStore, this.bottomSheetService) : isLoading = false, isImportNFTLoading = false, - nftAssetByWalletModels = ObservableList() { + nftAssetByWalletModels = ObservableList(), + solanaNftAssetModels = ObservableList() { getNFTAssetByWallet(); reaction((_) => appStore.wallet, (_) => getNFTAssetByWallet()); @@ -36,31 +39,44 @@ abstract class NFTViewModelBase with Store { ObservableList nftAssetByWalletModels; + ObservableList solanaNftAssetModels; + @action Future getNFTAssetByWallet() async { - if (!isEVMCompatibleChain(appStore.wallet!.type)) return; + final walletType = appStore.wallet!.type; + + if (!isNFTACtivatedChain(walletType)) return; final walletAddress = appStore.wallet!.walletInfo.address; log('Fetching wallet NFTs for $walletAddress'); - final chainName = getChainNameBasedOnWalletType(appStore.wallet!.type); + final chainName = getChainNameBasedOnWalletType(walletType); // the [chain] refers to the chain network that the nft is on // the [format] refers to the number format type of the responses // the [normalizedMetadata] field is a boolean that determines if // the response would include a json string of the NFT Metadata that can be decoded // and used within the wallet // the [excludeSpam] field is a boolean that determines if spam nfts be excluded from the response. - final uri = Uri.https( - 'deep-index.moralis.io', - '/api/v2.2/$walletAddress/nft', - { - "chain": chainName, - "format": "decimal", - "media_items": "false", - "exclude_spam": "true", - "normalizeMetadata": "true", - }, - ); + + Uri uri; + if (walletType == WalletType.solana) { + uri = Uri.https( + 'solana-gateway.moralis.io', + '/account/$chainName/$walletAddress/nft', + ); + } else { + uri = Uri.https( + 'deep-index.moralis.io', + '/api/v2.2/$walletAddress/nft', + { + "chain": chainName, + "format": "decimal", + "media_items": "false", + "exclude_spam": "true", + "normalizeMetadata": "true", + }, + ); + } try { isLoading = true; @@ -73,13 +89,30 @@ abstract class NFTViewModelBase with Store { }, ); - final decodedResponse = jsonDecode(response.body) as Map; + final decodedResponse = jsonDecode(response.body); - final result = WalletNFTsResponseModel.fromJson(decodedResponse).result ?? []; + if (walletType == WalletType.solana) { + final results = await Future.wait( + (decodedResponse as List).map( + (x) { + final data = x as Map; + final mint = data['mint'] as String? ?? ''; + return getSolanaNFTDetails(mint, chainName); + }, + ).toList(), + ); - nftAssetByWalletModels.clear(); + solanaNftAssetModels.clear(); - nftAssetByWalletModels.addAll(result); + solanaNftAssetModels.addAll(results); + } else { + final result = + WalletNFTsResponseModel.fromJson(decodedResponse as Map).result ?? []; + + nftAssetByWalletModels.clear(); + + nftAssetByWalletModels.addAll(result); + } isLoading = false; } catch (e) { @@ -94,51 +127,76 @@ abstract class NFTViewModelBase with Store { } } + Future getSolanaNFTDetails(String address, String chainName) async { + final uri = Uri.https( + 'solana-gateway.moralis.io', + '/nft/$chainName/$address/metadata', + ); + + final response = await http.get( + uri, + headers: { + "Accept": "application/json", + "X-API-Key": secrets.moralisApiKey, + }, + ); + + final decodedResponse = jsonDecode(response.body) as Map; + + return SolanaNFTAssetModel.fromJson(decodedResponse); + } + @action - Future importNFT(String tokenAddress, String tokenId) async { + Future importNFT(String tokenAddress, String? tokenId) async { final chainName = getChainNameBasedOnWalletType(appStore.wallet!.type); // the [chain] refers to the chain network that the nft is on // the [format] refers to the number format type of the responses // the [normalizedMetadata] field is a boolean that determines if // the response would include a json string of the NFT Metadata that can be decoded // and used within the wallet - final uri = Uri.https( - 'deep-index.moralis.io', - '/api/v2.2/nft/$tokenAddress/$tokenId', - { - "chain": chainName, - "format": "decimal", - "media_items": "false", - "normalizeMetadata": "true", - }, - ); try { isImportNFTLoading = true; - final response = await http.get( - uri, - headers: { - "Accept": "application/json", - "X-API-Key": secrets.moralisApiKey, - }, - ); + if (appStore.wallet!.type == WalletType.solana) { + final result = await getSolanaNFTDetails(tokenAddress, chainName); - final decodedResponse = jsonDecode(response.body) as Map; + solanaNftAssetModels.add(result); + } else { + final uri = Uri.https( + 'deep-index.moralis.io', + '/api/v2.2/nft/$tokenAddress/$tokenId', + { + "chain": chainName, + "format": "decimal", + "media_items": "false", + "normalizeMetadata": "true", + }, + ); - final nftAsset = NFTAssetModel.fromJson(decodedResponse); + final response = await http.get( + uri, + headers: { + "Accept": "application/json", + "X-API-Key": secrets.moralisApiKey, + }, + ); - nftAssetByWalletModels.add(nftAsset); + final decodedResponse = jsonDecode(response.body) as Map; - isImportNFTLoading = false; + final nftAsset = NFTAssetModel.fromJson(decodedResponse); + + nftAssetByWalletModels.add(nftAsset); + } } catch (e) { - isImportNFTLoading = false; bottomSheetService.queueBottomSheet( isModalDismissible: true, widget: BottomSheetMessageDisplayWidget( message: e.toString(), ), ); + } finally { + isImportNFTLoading = false; } } } diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index ecbf2ad6f..ec09b2b3d 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -148,6 +148,9 @@ "close": "يغلق", "coin_control": "التحكم في العملة (اختياري)", "cold_or_recover_wallet": "أضف محفظة للقراءة فقط من Cupcake أو محفظة باردة أو استعاد محفظة ورقية", + "collection_address": "عنوان التجميع", + "collection_description": "وصف المجموعة", + "collection_name": "اسم المجموعة", "color_theme": "سمة اللون", "commit_transaction_amount_fee": "تنفيذ الصفقة\nالمبلغ: ${amount}\nالرسوم: ${fee}", "confirm": "تأكيد", @@ -430,6 +433,7 @@ "methods": " ﻕﺮﻃُ", "min_amount": "الحد الأدنى: ${value}", "min_value": "الحد الأدنى: ${value} ${currency}", + "mint_address": "عنوان النعناع", "minutes_to_pin_code": "${minutes} دقيقة", "mm": "MM", "modify_2fa": "تعديل 2 عامل المصادقة", @@ -457,6 +461,7 @@ "new_wallet": "إنشاء محفظة جديدة", "newConnection": "ﺪﻳﺪﺟ ﻝﺎﺼﺗﺍ", "no_cards_found": "لم يتم العثور على بطاقات", + "no_extra_detail": "لا توجد تفاصيل إضافية متاحة", "no_id_needed": "لا حاجة لID!", "no_id_required": "لا ID مطلوب. اشحن وانفق في أي مكان", "no_providers_available": "لا مقدمي الخدمات المتاحة", @@ -675,8 +680,8 @@ "select_destination": ".ﻲﻃﺎﻴﺘﺣﻻﺍ ﺦﺴﻨﻟﺍ ﻒﻠﻣ ﺔﻬﺟﻭ ﺪﻳﺪﺤﺗ ءﺎﺟﺮﻟﺍ", "select_hw_account_below": "الرجاء تحديد حساب الاستعادة أدناه:", "select_sell_provider_notice": ".ﻖﻴﺒﻄﺘﻟﺍ ﺕﺍﺩﺍﺪﻋﺇ ﻲﻓ ﻚﺑ ﺹﺎﺨﻟﺍ ﻲﺿﺍﺮﺘﻓﻻﺍ ﻊﻴﺒﻟﺍ ﺩﻭﺰﻣ ﻦﻴﻴﻌﺗ ﻖﻳﺮﻃ ﻦﻋ ﺔﺷﺎﺸﻟﺍ ﻩﺬﻫ ﻲﻄﺨﺗ", - "selected_trocador_provider": "مزود تروكادور المختار", "select_your_country": "الرجاء تحديد بلدك", + "selected_trocador_provider": "مزود تروكادور المختار", "sell": "بيع", "sell_alert_content": ".ﺎﻬﻴﻟﺇ ﻞﻳﺪﺒﺘﻟﺍ ﻭﺃ Litecoin ﻭﺃ Ethereum ﻭﺃ Bitcoin ﺔﻈﻔﺤﻣ ءﺎﺸﻧﺇ ﻰﺟﺮﻳ .Litecoin ﻭ", "sell_monero_com_alert_content": "بيع Monero غير مدعوم حتى الآن", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index e1959d195..31fd57c27 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -148,6 +148,9 @@ "close": "затвори", "coin_control": "Управление на монетите (не е задължително)", "cold_or_recover_wallet": "Добавете портфейл само за четене от Cupcake или студен портфейл или възстановете хартиен портфейл", + "collection_address": "Адрес на колекцията", + "collection_description": "Описание на колекцията", + "collection_name": "Име на колекцията", "color_theme": "Цвят", "commit_transaction_amount_fee": "Изпълняване на транзакция\nСума: ${amount}\nТакса: ${fee}", "confirm": "Потвърждаване", @@ -430,6 +433,7 @@ "methods": "Методи", "min_amount": "Мин: ${value}", "min_value": "Мин: ${value} ${currency}", + "mint_address": "Адрес на мента", "minutes_to_pin_code": "${minute} минути", "mm": "мм", "modify_2fa": "Модифициране на тортата 2FA", @@ -457,6 +461,7 @@ "new_wallet": "Нов портфейл", "newConnection": "Нова връзка", "no_cards_found": "Не са намерени карти", + "no_extra_detail": "Няма налични допълнителни подробности", "no_id_needed": "Без нужда от документ за самоличност!", "no_id_required": "Без нужда от документ за самоличност. Използвайте навсякъде", "no_providers_available": "Няма налични доставчици", @@ -675,8 +680,8 @@ "select_destination": "Моля, изберете дестинация за архивния файл.", "select_hw_account_below": "Моля, изберете кой акаунт да възстановите по -долу:", "select_sell_provider_notice": "Изберете доставчик на продажба по-горе. Можете да пропуснете този екран, като зададете своя доставчик на продажба по подразбиране в настройките на приложението.", - "selected_trocador_provider": "Избран доставчик на трокадор", "select_your_country": "Моля, изберете вашата страна", + "selected_trocador_provider": "Избран доставчик на трокадор", "sell": "Продаване", "sell_alert_content": "В момента поддържаме само продажбата на Bitcoin, Ethereum и Litecoin. Моля, създайте или превключете към своя портфейл Bitcoin, Ethereum или Litecoin.", "sell_monero_com_alert_content": "Продажбата на Monero все още не се поддържа", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index bed37c456..fd66fdcae 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -148,6 +148,9 @@ "close": "zavřít", "coin_control": "Volba mincí (nepovinné)", "cold_or_recover_wallet": "Přidejte peněženku pouze pro čtení z Cupcake nebo studené peněženky nebo obnovte papírovou peněženku", + "collection_address": "Sběrná adresa", + "collection_description": "Popis sbírky", + "collection_name": "Název sbírky", "color_theme": "Barevný motiv", "commit_transaction_amount_fee": "Odeslat transakci\nČástka: ${amount}\nPoplatek: ${fee}", "confirm": "Potvrdit", @@ -430,6 +433,7 @@ "methods": "Metody", "min_amount": "Min: ${value}", "min_value": "Min: ${value} ${currency}", + "mint_address": "Adresa máty", "minutes_to_pin_code": "${minute} minutách", "mm": "MM", "modify_2fa": "Upravte Cake 2FA", @@ -457,6 +461,7 @@ "new_wallet": "Nová peněženka", "newConnection": "Nové připojení", "no_cards_found": "Žádné karty nenalezeny", + "no_extra_detail": "K dispozici nejsou žádné další podrobnosti", "no_id_needed": "Žádné ID není potřeba!", "no_id_required": "Žádní ID není potřeba. Dobijte si a utrácejte kdekoliv", "no_providers_available": "Žádní poskytovatelé jsou k dispozici", @@ -675,8 +680,8 @@ "select_destination": "Vyberte cíl pro záložní soubor.", "select_hw_account_below": "Níže vyberte, který účet chcete obnovit:", "select_sell_provider_notice": "Výše vyberte poskytovatele prodeje. Tuto obrazovku můžete přeskočit nastavením výchozího poskytovatele prodeje v nastavení aplikace.", - "selected_trocador_provider": "Vybraný poskytovatel Trocador", "select_your_country": "Vyberte prosím svou zemi", + "selected_trocador_provider": "Vybraný poskytovatel Trocador", "sell": "Prodat", "sell_alert_content": "V současné době podporujeme pouze prodej bitcoinů, etherea a litecoinů. Vytvořte nebo přepněte na svou bitcoinovou, ethereum nebo litecoinovou peněženku.", "sell_monero_com_alert_content": "Prodej Monero zatím není podporován", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index beca8957e..36f5ecfa8 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -148,6 +148,9 @@ "close": "Schließen", "coin_control": "Coin Control (optional)", "cold_or_recover_wallet": "Fügen Sie eine schreibgeschützte Wallet von Cupcake, eine Cold-Wallet oder eine Papier-Wallet hinzu.", + "collection_address": "Sammeladresse", + "collection_description": "Sammlung Beschreibung", + "collection_name": "Sammlungsname", "color_theme": "Farbthema", "commit_transaction_amount_fee": "Transaktion absenden\nBetrag: ${amount}\nGebühr: ${fee}", "confirm": "Bestätigen", @@ -430,6 +433,7 @@ "methods": "Methoden", "min_amount": "Min: ${value}", "min_value": "Min: ${value} ${currency}", + "mint_address": "Minzadresse", "minutes_to_pin_code": "${minute} Minuten", "mm": "MM", "modify_2fa": "Cake 2FA ändern", @@ -457,6 +461,7 @@ "new_wallet": "Neue Wallet", "newConnection": "Neue Verbindung", "no_cards_found": "Keine Karten gefunden", + "no_extra_detail": "Keine zusätzlichen Details verfügbar", "no_id_needed": "Keine ID erforderlich!", "no_id_required": "Keine ID erforderlich. Upgraden und überall ausgeben", "no_providers_available": "Keine Anbieter verfügbar", @@ -676,8 +681,8 @@ "select_destination": "Bitte wählen Sie das Ziel für die Sicherungsdatei aus.", "select_hw_account_below": "Bitte wählen Sie unten, welches Konto unten wiederhergestellt werden soll:", "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.", - "selected_trocador_provider": "Ausgewählter Trocador -Anbieter", "select_your_country": "Bitte wählen Sie Ihr Land aus", + "selected_trocador_provider": "Ausgewählter Trocador -Anbieter", "sell": "Verkaufen", "sell_alert_content": "Wir unterstützen derzeit nur den Verkauf von Bitcoin, Ethereum und Litecoin. Bitte erstellen Sie Ihr Bitcoin-, Ethereum- oder Litecoin-Wallet oder wechseln Sie zu diesem.", "sell_monero_com_alert_content": "Der Verkauf von Monero wird noch nicht unterstützt", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 0bd5d7138..1278aaf04 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -148,6 +148,9 @@ "close": "Close", "coin_control": "Coin control (optional)", "cold_or_recover_wallet": "Add a read-only wallet from Cupcake or a cold wallet or recover a paper wallet", + "collection_address": "Collection Address", + "collection_description": "Collection Description", + "collection_name": "Collection Name", "color_theme": "Color theme", "commit_transaction_amount_fee": "Commit transaction\nAmount: ${amount}\nFee: ${fee}", "confirm": "Confirm", @@ -430,6 +433,7 @@ "methods": "Methods", "min_amount": "Min: ${value}", "min_value": "Min: ${value} ${currency}", + "mint_address": "Mint Address", "minutes_to_pin_code": "${minute} minutes", "mm": "MM", "modify_2fa": "Modify Cake 2FA", @@ -457,6 +461,7 @@ "new_wallet": "New Wallet", "newConnection": "New Connection", "no_cards_found": "No cards found", + "no_extra_detail": "No extra details available", "no_id_needed": "No ID needed!", "no_id_required": "No ID required. Top up and spend anywhere", "no_providers_available": "No providers available", @@ -676,8 +681,8 @@ "select_destination": "Please select destination for the backup file.", "select_hw_account_below": "Please select which account to restore below:", "select_sell_provider_notice": "Select a sell provider above. You can skip this screen by setting your default sell provider in app settings.", - "selected_trocador_provider": "selected Trocador provider", "select_your_country": "Please select your country", + "selected_trocador_provider": "selected Trocador provider", "sell": "Sell", "sell_alert_content": "We currently only support the sale of Bitcoin, Ethereum and Litecoin. Please create or switch to your Bitcoin, Ethereum or Litecoin wallet.", "sell_monero_com_alert_content": "Selling Monero is not supported yet", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index e2da76a97..1849a2f60 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -148,6 +148,9 @@ "close": "Cerca", "coin_control": "Control de monedas (opcional)", "cold_or_recover_wallet": "Agregue una billetera de solo lectura de Cupcake o una billetera en frío o recupere una billetera de papel", + "collection_address": "Dirección de recolección", + "collection_description": "Descripción de la colección", + "collection_name": "Nombre de colección", "color_theme": "Tema de color", "commit_transaction_amount_fee": "Confirmar transacción\nCantidad: ${amount}\nCuota: ${fee}", "confirm": "Confirmar", @@ -430,6 +433,7 @@ "methods": "Métodos", "min_amount": "Mínimo: ${value}", "min_value": "Min: ${value} ${currency}", + "mint_address": "Dirección menta", "minutes_to_pin_code": "${minute} minutos", "mm": "mm", "modify_2fa": "Modificar 2FA", @@ -457,6 +461,7 @@ "new_wallet": "Nueva billetera", "newConnection": "Nueva conexión", "no_cards_found": "No se encuentran cartas", + "no_extra_detail": "No hay detalles adicionales disponibles", "no_id_needed": "¡No se necesita identificación!", "no_id_required": "No se requiere identificación. Recarga y gaste en cualquier lugar", "no_providers_available": "No hay proveedores disponibles", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 0e65bdb08..bcd513f67 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -148,6 +148,9 @@ "close": "Fermer", "coin_control": "Contrôle optionnel des pièces (coins)", "cold_or_recover_wallet": "Ajoutez un portefeuille en lecture seule de Cupcake ou d'un portefeuille froid ou récupérez un portefeuille en papier", + "collection_address": "Adresse de collecte", + "collection_description": "Description de la collection", + "collection_name": "Nom de collection", "color_theme": "Thème", "commit_transaction_amount_fee": "Valider la transaction\nMontant : ${amount}\nFrais : ${fee}", "confirm": "Confirmer", @@ -430,6 +433,7 @@ "methods": "Méthodes", "min_amount": "Min : ${value}", "min_value": "Min: ${value} ${currency}", + "mint_address": "Adresse de la menthe", "minutes_to_pin_code": "${minute} minutes", "mm": "MM", "modify_2fa": "Modifier les paramètres Cake 2FA", @@ -457,6 +461,7 @@ "new_wallet": "Nouveau Portefeuille (Wallet)", "newConnection": "Nouvelle connexion", "no_cards_found": "Pas de cartes trouvées", + "no_extra_detail": "Aucun détail supplémentaire disponible", "no_id_needed": "Aucune pièce d'identité nécessaire !", "no_id_required": "Aucune pièce d'identité requise. Rechargez et dépensez n'importe où", "no_providers_available": "Aucun fournisseur disponible", @@ -675,8 +680,8 @@ "select_destination": "Veuillez sélectionner la destination du fichier de sauvegarde.", "select_hw_account_below": "Veuillez sélectionner le compte à restaurer ci-dessous:", "select_sell_provider_notice": "Sélectionnez un fournisseur de vente ci-dessus. Vous pouvez ignorer cet écran en définissant votre fournisseur de vente par défaut dans les paramètres de l'application.", - "selected_trocador_provider": "fournisseur de trocador sélectionné", "select_your_country": "Veuillez sélectionner votre pays", + "selected_trocador_provider": "fournisseur de trocador sélectionné", "sell": "Vendre", "sell_alert_content": "Nous ne prenons actuellement en charge que la vente de Bitcoin, Ethereum et Litecoin. Veuillez créer ou basculer vers votre portefeuille Bitcoin, Ethereum ou Litecoin.", "sell_monero_com_alert_content": "La vente de Monero n'est pas encore prise en charge", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 53cfd164b..0e30d2aff 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -148,6 +148,9 @@ "close": "Rufa", "coin_control": "Sarrafa tsabar kuɗi (na zaɓi)", "cold_or_recover_wallet": "Aara wani walat mai karanta-kawai Cupcake ko walat ɗin mai sanyi ko murmurewa takarda takarda", + "collection_address": "Adireshin tarin tarin", + "collection_description": "Bayanin tarin", + "collection_name": "Sunan tattara", "color_theme": "Jigon launi", "commit_transaction_amount_fee": "Aikata ciniki\nAdadi: ${amount}\nKuda: ${fee}", "confirm": "Tabbatar", @@ -430,6 +433,7 @@ "methods": "Hanyoyin", "min_amount": "Min: ${value}", "min_value": "Min: ${value} ${currency}", + "mint_address": "Adireshin Mint", "minutes_to_pin_code": "${minute} minti", "mm": "MM", "modify_2fa": "Gyara Cake 2FA", @@ -457,6 +461,7 @@ "new_wallet": "Sabuwar Wallet", "newConnection": "Sabuwar Haɗi", "no_cards_found": "Babu katunan da aka samo", + "no_extra_detail": "Babu ƙarin cikakkun bayanai", "no_id_needed": "Babu ID da ake buƙata!", "no_id_required": "Babu ID da ake buƙata. Yi da kuma ciyar a ko'ina", "no_providers_available": "Babu masu samar da wadatar", @@ -677,8 +682,8 @@ "select_destination": "Da fatan za a zaɓi wurin da za a yi wa madadin fayil ɗin.", "select_hw_account_below": "Da fatan za a zabi wanda asusun zai gyara a ƙasa:", "select_sell_provider_notice": "Zaɓi mai bada siyarwa a sama. Kuna iya tsallake wannan allon ta saita mai bada siyar da ku a cikin saitunan app.", - "selected_trocador_provider": "Zabi mai bada TORACAD", "select_your_country": "Da fatan za a zabi ƙasarku", + "selected_trocador_provider": "Zabi mai bada TORACAD", "sell": "sayar", "sell_alert_content": "A halin yanzu muna tallafawa kawai siyar da Bitcoin, Ethereum da Litecoin. Da fatan za a ƙirƙiri ko canza zuwa walat ɗin ku na Bitcoin, Ethereum ko Litecoin.", "sell_monero_com_alert_content": "Selling Monero bai sami ƙarshen mai bukatar samun ba", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 002aa8246..d309c7f89 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -148,6 +148,9 @@ "close": "बंद करना", "coin_control": "सिक्का नियंत्रण (वैकल्पिक)", "cold_or_recover_wallet": "Cupcake या एक कोल्ड वॉलेट से एक रीड-ओनली वॉलेट जोड़ें या एक पेपर वॉलेट को पुनर्प्राप्त करें", + "collection_address": "संग्रह पता", + "collection_description": "संग्रह विवरण", + "collection_name": "संग्रह नाम", "color_theme": "रंग विषय", "commit_transaction_amount_fee": "लेन-देन करें\nरकम: ${amount}\nशुल्क: ${fee}", "confirm": "की पुष्टि करें", @@ -430,6 +433,7 @@ "methods": "तरीकों", "min_amount": "न्यूनतम: ${value}", "min_value": "मिन: ${value} ${currency}", + "mint_address": "टकसाल पता", "minutes_to_pin_code": "${minute} मिनट", "mm": "एमएम", "modify_2fa": "केक 2FA संशोधित करें", @@ -457,6 +461,7 @@ "new_wallet": "नया बटुआ", "newConnection": "नया कनेक्शन", "no_cards_found": "कोई कार्ड नहीं मिला", + "no_extra_detail": "कोई अतिरिक्त विवरण उपलब्ध नहीं है", "no_id_needed": "कोई आईडी नहीं चाहिए!", "no_id_required": "कोई आईडी आवश्यक नहीं है। टॉप अप करें और कहीं भी खर्च करें", "no_providers_available": "कोई प्रदाता उपलब्ध नहीं है", @@ -509,8 +514,8 @@ "paste": "पेस्ट करें", "pause_wallet_creation": "हेवन वॉलेट बनाने की क्षमता फिलहाल रुकी हुई है।", "payment_id": "भुगतान ID: ", - "payment_was_received": "आपका भुगतान प्राप्त हुआ था।", "Payment_was_received": "आपका भुगतान प्राप्त हो गया था।", + "payment_was_received": "आपका भुगतान प्राप्त हुआ था।", "pending": " (अपूर्ण)", "percentageOf": "${amount} का", "pin_at_top": "शीर्ष पर ${token} पिन करें", @@ -677,8 +682,8 @@ "select_destination": "कृपया बैकअप फ़ाइल के लिए गंतव्य का चयन करें।", "select_hw_account_below": "कृपया नीचे पुनर्स्थापित करने के लिए कौन सा खाता चुनें:", "select_sell_provider_notice": "ऊपर एक विक्रय प्रदाता का चयन करें। आप ऐप सेटिंग में अपना डिफ़ॉल्ट विक्रय प्रदाता सेट करके इस स्क्रीन को छोड़ सकते हैं।", - "selected_trocador_provider": "चयनित ट्रोकैडर प्रदाता", "select_your_country": "कृपया अपने देश का चयन करें", + "selected_trocador_provider": "चयनित ट्रोकैडर प्रदाता", "sell": "बेचना", "sell_alert_content": "हम वर्तमान में केवल बिटकॉइन, एथेरियम और लाइटकॉइन की बिक्री का समर्थन करते हैं। कृपया अपना बिटकॉइन, एथेरियम या लाइटकॉइन वॉलेट बनाएं या उसमें स्विच करें।", "sell_monero_com_alert_content": "मोनेरो बेचना अभी तक समर्थित नहीं है", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 45fc66005..4040513cb 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -148,6 +148,9 @@ "close": "Zatvoriti", "coin_control": "Kontrola novca (nije obavezno)", "cold_or_recover_wallet": "Dodajte novčanik samo za čitanje od Cupcake ili hladnog novčanika ili oporavite papirni novčanik", + "collection_address": "Adresa prikupljanja", + "collection_description": "Zbirka Opis opisa", + "collection_name": "Naziv kolekcije", "color_theme": "Shema boja", "commit_transaction_amount_fee": "Izvrši transakciju \nAmount: ${amount}\nFee: ${fee}", "confirm": "Potvrdi", @@ -430,6 +433,7 @@ "methods": "Metode", "min_amount": "Minimalno: ${value}", "min_value": "Min.: ${value} ${currency}", + "mint_address": "Adresa metvice", "minutes_to_pin_code": "${minute} minuta", "mm": "MM", "modify_2fa": "Izmijenite tortu 2FA", @@ -457,6 +461,7 @@ "new_wallet": "Novi novčanik", "newConnection": "Nova veza", "no_cards_found": "Nisu pronađene kartice", + "no_extra_detail": "Nema dostupnih dodatnih detalja", "no_id_needed": "Nije potreban ID!", "no_id_required": "Nije potreban ID. Nadopunite i potrošite bilo gdje", "no_providers_available": "Nema dostupnih pružatelja usluga", @@ -675,8 +680,8 @@ "select_destination": "Odaberite odredište za datoteku sigurnosne kopije.", "select_hw_account_below": "Molimo odaberite koji će se račun vratiti u nastavku:", "select_sell_provider_notice": "Gore odaberite pružatelja usluga prodaje. Ovaj zaslon možete preskočiti postavljanjem zadanog pružatelja usluga prodaje u postavkama aplikacije.", - "selected_trocador_provider": "Odabrani pružatelj usluga trokadora", "select_your_country": "Odaberite svoju zemlju", + "selected_trocador_provider": "Odabrani pružatelj usluga trokadora", "sell": "Prodavati", "sell_alert_content": "Trenutno podržavamo samo prodaju Bitcoina, Ethereuma i Litecoina. Izradite ili prijeđite na svoj Bitcoin, Ethereum ili Litecoin novčanik.", "sell_monero_com_alert_content": "Prodaja Monera još nije podržana", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 2719015cc..e8ab6ca99 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -148,6 +148,9 @@ "close": "Փակել", "coin_control": "Մետաղադրամի վերահսկում (ըստ ցանկության)", "cold_or_recover_wallet": "Cupcake կամ ցուրտ դրամապանակից ավելացնել միայն ընթերցված դրամապանակ կամ վերականգնել թղթի դրամապանակը", + "collection_address": "Հավաքածուի հասցե", + "collection_description": "Հավաքածուի նկարագրությունը", + "collection_name": "Հավաքածուի անուն", "color_theme": "Գույների տեսք", "commit_transaction_amount_fee": "Հաստատել գործարքը\nՍկզբնական գումար. ${amount}\nՄիջնորդավճար. ${fee}", "confirm": "Հաստատել", @@ -430,6 +433,7 @@ "methods": "Մեթոդներ", "min_amount": "Նվազը: ${value}", "min_value": "Նվազը: ${value} ${currency}", + "mint_address": "Անանուխի հասցե", "minutes_to_pin_code": "${minute} րոպե", "mm": "ԱԱ", "modify_2fa": "Փոփոխել Cake 2FA", @@ -457,6 +461,7 @@ "new_wallet": "Նոր դրամապանակ", "newConnection": "Նոր կապ", "no_cards_found": "Ոչ մի քարտ չի գտնվել", + "no_extra_detail": "Լրացուցիչ մանրամասներ մատչելի չեն", "no_id_needed": "Ոչ մի փաստաթուղթ չի պահանջվում!", "no_id_required": "Ոչ մի փաստաթուղթ չի պահանջվում։ Լրացրեք և ծախսեք ամենուր", "no_relay_on_domain": "Տիրույթի համար ընդունող չկա կամ անհասանելի է։ Խնդրում ենք ընտրել ընդունող", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index f5b0bb285..db2383f17 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -148,6 +148,9 @@ "close": "Menutup", "coin_control": "Kontrol koin (opsional)", "cold_or_recover_wallet": "Tambahkan dompet hanya baca dari Cupcake atau dompet dingin atau memulihkan dompet kertas", + "collection_address": "Alamat Koleksi", + "collection_description": "Deskripsi Koleksi", + "collection_name": "Nama Koleksi", "color_theme": "Tema warna", "commit_transaction_amount_fee": "Lakukan transaksi\nJumlah: ${amount}\nBiaya: ${fee}", "confirm": "Konfirmasi", @@ -430,6 +433,7 @@ "methods": "Metode", "min_amount": "Min: ${value}", "min_value": "Min: ${value} ${currency}", + "mint_address": "Alamat mint", "minutes_to_pin_code": "${minute} menit", "mm": "MM", "modify_2fa": "Ubah Kue 2FA", @@ -457,6 +461,7 @@ "new_wallet": "Dompet Baru", "newConnection": "Koneksi Baru", "no_cards_found": "Tidak ada kartu yang ditemukan", + "no_extra_detail": "Tidak ada detail tambahan yang tersedia", "no_id_needed": "Tidak perlu ID!", "no_id_required": "Tidak perlu ID. Isi ulang dan belanja di mana saja", "no_providers_available": "Tidak ada penyedia yang tersedia", @@ -678,8 +683,8 @@ "select_destination": "Silakan pilih tujuan untuk file cadangan.", "select_hw_account_below": "Pilih akun mana yang akan dikembalikan di bawah ini:", "select_sell_provider_notice": "Pilih penyedia jual di atas. Anda dapat melewati layar ini dengan mengatur penyedia penjualan default Anda di pengaturan aplikasi.", - "selected_trocador_provider": "Penyedia Trocador Terpilih", "select_your_country": "Pilih negara Anda", + "selected_trocador_provider": "Penyedia Trocador Terpilih", "sell": "Jual", "sell_alert_content": "Saat ini kami hanya mendukung penjualan Bitcoin, Ethereum, dan Litecoin. Harap buat atau alihkan ke dompet Bitcoin, Ethereum, atau Litecoin Anda.", "sell_monero_com_alert_content": "Menjual Monero belum didukung", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 3c55f909d..5ebbcece8 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -148,6 +148,9 @@ "close": "Chiudi", "coin_control": "Controllo valute (opzionale)", "cold_or_recover_wallet": "Aggiungi un portafoglio di sola lettura da Cupcake o un cold wallet o recupera un portafoglio di carta", + "collection_address": "Indirizzo di raccolta", + "collection_description": "Descrizione della raccolta", + "collection_name": "Nome della raccolta", "color_theme": "Colore tema", "commit_transaction_amount_fee": "Invia transazione\nAmmontare: ${amount}\nCommissione: ${fee}", "confirm": "Conferma", @@ -430,6 +433,7 @@ "methods": "Metodi", "min_amount": "Min: ${value}", "min_value": "Min: ${value} ${currency}", + "mint_address": "Indirizzo di menta", "minutes_to_pin_code": "${minute} minuti", "mm": "mm", "modify_2fa": "Modifica Cake 2FA", @@ -457,6 +461,7 @@ "new_wallet": "Nuovo portafoglio", "newConnection": "Nuova connessione", "no_cards_found": "Nessuna carta trovata", + "no_extra_detail": "Nessun dettaglio extra disponibile", "no_id_needed": "Nessun ID necessario!", "no_id_required": "Nessun ID richiesto. Ricarica e spendi ovunque", "no_providers_available": "Nessun fornitore disponibile", @@ -674,10 +679,10 @@ "select_backup_file": "Seleziona file di backup", "select_buy_provider_notice": "Seleziona un provider di acquisto sopra. È possibile saltare questa schermata impostando il provider di acquisto predefinito nelle impostazioni dell'app.", "select_destination": "Seleziona la destinazione per il file di backup.", - "selected_trocador_provider": "Provider di Trocador selezionato", "select_hw_account_below": "Seleziona quale account ripristinare:", "select_sell_provider_notice": "Seleziona un provider di vendita sopra. Puoi saltare questa schermata impostando il tuo provider di vendita predefinito nelle impostazioni dell'app.", "select_your_country": "Seleziona il tuo paese", + "selected_trocador_provider": "Provider di Trocador selezionato", "sell": "Vendi", "sell_alert_content": "Al momento supportiamo solo la vendita di Bitcoin, Ethereum e Litecoin. Crea o passa al tuo portafoglio Bitcoin, Ethereum o Litecoin.", "sell_monero_com_alert_content": "La vendita di Monero non è ancora supportata", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 23414628a..9fe48e0b3 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -148,6 +148,9 @@ "close": "近い", "coin_control": "コインコントロール(オプション)", "cold_or_recover_wallet": "Cupcakeまたはコールドウォレットから読み取り専用ウォレットを追加するか、紙の財布を回収する", + "collection_address": "コレクションアドレス", + "collection_description": "コレクションの説明", + "collection_name": "コレクション名", "color_theme": "カラーテーマ", "commit_transaction_amount_fee": "トランザクションをコミット\n量: ${amount}\n費用: ${fee}", "confirm": "確認する", @@ -431,6 +434,7 @@ "methods": "メソッド", "min_amount": "最小: ${value}", "min_value": "分: ${value} ${currency}", + "mint_address": "ミントアドレス", "minutes_to_pin_code": "${minute} 分", "mm": "んん", "modify_2fa": "ケーキの 2FA を変更する", @@ -458,6 +462,7 @@ "new_wallet": "新しいウォレット", "newConnection": "新しい接続", "no_cards_found": "カードは見つかりません", + "no_extra_detail": "追加の詳細はありません", "no_id_needed": "IDは必要ありません!", "no_id_required": "IDは必要ありません。どこにでも補充して使用できます", "no_providers_available": "プロバイダーは利用できません", @@ -676,8 +681,8 @@ "select_destination": "バックアップファイルの保存先を選択してください。", "select_hw_account_below": "以下に復元するアカウントを選択してください。", "select_sell_provider_notice": "上記の販売プロバイダーを選択してください。アプリ設定でデフォルトの販売プロバイダーを設定することで、この画面をスキップできます。", - "selected_trocador_provider": "選択したTrocadorプロバイダー", "select_your_country": "あなたの国を選択してください", + "selected_trocador_provider": "選択したTrocadorプロバイダー", "sell": "売る", "sell_alert_content": "現在、ビットコイン、イーサリアム、ライトコインの販売のみをサポートしています。ビットコイン、イーサリアム、またはライトコインのウォレットを作成するか、これらのウォレットに切り替えてください。", "sell_monero_com_alert_content": "モネロの販売はまだサポートされていません", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 6b73f54b8..c498b2438 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -148,6 +148,9 @@ "close": "닫다", "coin_control": "코인 제어 (옵션)", "cold_or_recover_wallet": "Cupcake 또는 차가운 지갑에서 읽기 전용 지갑을 추가하거나 종이 지갑을 복구하십시오.", + "collection_address": "수집 주소", + "collection_description": "컬렉션 설명", + "collection_name": "수집 이름", "color_theme": "색상 테마", "commit_transaction_amount_fee": "커밋 거래\n양: ${amount}\n보수: ${fee}", "confirm": "확인", @@ -430,6 +433,7 @@ "methods": "행동 양식", "min_amount": "최소: ${value}", "min_value": "최소: ${value} ${currency}", + "mint_address": "민트 주소", "minutes_to_pin_code": "${minute}분", "mm": "mm", "modify_2fa": "수정 케이크 2FA", @@ -457,6 +461,7 @@ "new_wallet": "새 월렛", "newConnection": "새로운 연결", "no_cards_found": "카드를 찾지 못했습니다", + "no_extra_detail": "추가 세부 정보가 없습니다", "no_id_needed": "ID가 필요하지 않습니다!", "no_id_required": "신분증이 필요하지 않습니다. 충전하고 어디에서나 사용하세요", "no_providers_available": "제공되는 제공자가 없습니다", @@ -675,8 +680,8 @@ "select_destination": "백업 파일의 대상을 선택하십시오.", "select_hw_account_below": "아래를 복원 할 계정을 선택하십시오.", "select_sell_provider_notice": "위에서 판매 공급자를 선택하세요. 앱 설정에서 기본 판매 공급자를 설정하면 이 화면을 건너뛸 수 있습니다.", - "selected_trocador_provider": "선정 된 트로 코더 제공 업체", "select_your_country": "국가를 선택하십시오", + "selected_trocador_provider": "선정 된 트로 코더 제공 업체", "sell": "팔다", "sell_alert_content": "현재 Bitcoin, Ethereum 및 Litecoin의 판매만 지원합니다. Bitcoin, Ethereum 또는 Litecoin 지갑을 생성하거나 전환하십시오.", "sell_monero_com_alert_content": "지원되지 않습니다.", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 9a86dda6c..5c166afca 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -148,6 +148,9 @@ "close": "အနီးကပ်", "coin_control": "အကြွေစေ့ထိန်းချုပ်မှု (ချန်လှပ်ထားနိုင်သည်)", "cold_or_recover_wallet": "Cupcake သို့မဟုတ်အအေးပိုက်ဆံအိတ်မှဖတ်ရန်သာပိုက်ဆံအိတ်တစ်ခုထည့်ပါသို့မဟုတ်စက္ကူပိုက်ဆံအိတ်ကိုပြန်လည်ရယူပါ", + "collection_address": "စုဆောင်းမှုလိပ်စာ", + "collection_description": "စုဆောင်းခြင်းဖော်ပြချက်", + "collection_name": "စုဆောင်းခြင်းအမည်", "color_theme": "အရောင်အပြင်အဆင်", "commit_transaction_amount_fee": "ငွေလွှဲခြင်း\nပမာဏ- ${amount}\nအခကြေးငွေ- ${fee}", "confirm": "အတည်ပြုပါ။", @@ -430,6 +433,7 @@ "methods": "နည်းလမ်းများ", "min_amount": "အနည်းဆုံး- ${value}", "min_value": "အနည်းဆုံး- ${value} ${currency}", + "mint_address": "Mint လိပ်စာ", "minutes_to_pin_code": "${minute} မိနစ်", "mm": "MM", "modify_2fa": "ကိတ်မုန့် 2FA ကို ပြင်ဆင်ပါ။", @@ -457,6 +461,7 @@ "new_wallet": "ပိုက်ဆံအိတ်အသစ်", "newConnection": "ချိတ်ဆက်မှုအသစ်", "no_cards_found": "ကဒ်များမရှိပါ", + "no_extra_detail": "အဘယ်သူမျှမအပိုအသေးစိတ်ကိုရရှိနိုင်", "no_id_needed": "ID မလိုအပ်ပါ။", "no_id_required": "ID မလိုအပ်ပါ။ ငွေဖြည့်ပြီး ဘယ်နေရာမဆို သုံးစွဲပါ။", "no_providers_available": "မရရှိနိုင်ပါ", @@ -675,8 +680,8 @@ "select_destination": "အရန်ဖိုင်အတွက် ဦးတည်ရာကို ရွေးပါ။", "select_hw_account_below": "အောက်ဖော်ပြပါမည်သည့်အကောင့်ကိုရွေးပါ။", "select_sell_provider_notice": "အထက်ဖော်ပြပါ အရောင်းဝန်ဆောင်မှုပေးသူကို ရွေးပါ။ အက်ပ်ဆက်တင်များတွင် သင်၏မူလရောင်းချပေးသူကို သတ်မှတ်ခြင်းဖြင့် ဤစခရင်ကို ကျော်နိုင်သည်။", - "selected_trocador_provider": "ရွေးချယ်ထား Tracador ပံ့ပိုးပေး", "select_your_country": "ကျေးဇူးပြု. သင့်နိုင်ငံကိုရွေးချယ်ပါ", + "selected_trocador_provider": "ရွေးချယ်ထား Tracador ပံ့ပိုးပေး", "sell": "ရောင်း", "sell_alert_content": "ကျွန်ုပ်တို့သည် လက်ရှိတွင် Bitcoin၊ Ethereum နှင့် Litecoin ရောင်းချခြင်းကိုသာ ပံ့ပိုးပေးပါသည်။ သင်၏ Bitcoin၊ Ethereum သို့မဟုတ် Litecoin ပိုက်ဆံအိတ်ကို ဖန်တီးပါ သို့မဟုတ် ပြောင်းပါ။", "sell_monero_com_alert_content": "Monero ရောင်းချခြင်းကို မပံ့ပိုးရသေးပါ။", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 69280dbbe..d1b43bbdc 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -148,6 +148,9 @@ "close": "Dichtbij", "coin_control": "Muntcontrole (optioneel)", "cold_or_recover_wallet": "Voeg een alleen-lezen portemonnee toe van Cupcake of een koude portemonnee of herstel een papieren portemonnee", + "collection_address": "Verzameladres", + "collection_description": "Verzamelingsbeschrijving", + "collection_name": "Verzamelnaam", "color_theme": "Kleur thema", "commit_transaction_amount_fee": "Verricht transactie\nBedrag: ${amount}\nhonorarium: ${fee}", "confirm": "Bevestigen", @@ -430,6 +433,7 @@ "methods": "Methoden", "min_amount": "Min: ${value}", "min_value": "Min: ${value} ${currency}", + "mint_address": "Muntadres", "minutes_to_pin_code": "${minute} minuten", "mm": "MM", "modify_2fa": "Wijzig Cake 2FA", @@ -457,6 +461,7 @@ "new_wallet": "Nieuwe portemonnee", "newConnection": "Nieuwe verbinding", "no_cards_found": "Geen kaarten gevonden", + "no_extra_detail": "Geen extra details beschikbaar", "no_id_needed": "Geen ID nodig!", "no_id_required": "Geen ID vereist. Opwaarderen en overal uitgeven", "no_providers_available": "Geen providers beschikbaar", @@ -675,8 +680,8 @@ "select_destination": "Selecteer de bestemming voor het back-upbestand.", "select_hw_account_below": "Selecteer welk account u hieronder moet herstellen:", "select_sell_provider_notice": "Selecteer hierboven een verkoopaanbieder. U kunt dit scherm overslaan door uw standaardverkoopprovider in te stellen in de app-instellingen.", - "selected_trocador_provider": "Geselecteerde Trocador -provider", "select_your_country": "Selecteer uw land", + "selected_trocador_provider": "Geselecteerde Trocador -provider", "sell": "Verkopen", "sell_alert_content": "We ondersteunen momenteel alleen de verkoop van Bitcoin, Ethereum en Litecoin. Maak of schakel over naar uw Bitcoin-, Ethereum- of Litecoin-portemonnee.", "sell_monero_com_alert_content": "Het verkopen van Monero wordt nog niet ondersteund", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 0e57ecf8c..e0514d481 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -148,6 +148,9 @@ "close": "Zamknąć", "coin_control": "Kontrola monet (opcjonalnie)", "cold_or_recover_wallet": "Dodaj portfel tylko do odczytu od Cupcake lub zimnego portfela lub odzyskaj papierowy portfel", + "collection_address": "Adres kolekcji", + "collection_description": "Opis kolekcji", + "collection_name": "Nazwa kolekcji", "color_theme": "Motyw kolorystyczny", "commit_transaction_amount_fee": "Zatwierdź transakcję\nIlość: ${amount}\nOpłata: ${fee}", "confirm": "Potwierdzać", @@ -430,6 +433,7 @@ "methods": "Metody", "min_amount": "Min: ${value}", "min_value": "Min: ${value} ${currency}", + "mint_address": "Adres mięty", "minutes_to_pin_code": "${minute} minut", "mm": "MM", "modify_2fa": "Zmodyfikuj ciasto 2FA", @@ -457,6 +461,7 @@ "new_wallet": "Nowy portfel", "newConnection": "Nowe połączenie", "no_cards_found": "Nie znaleziono żadnych kart", + "no_extra_detail": "Brak dodatkowych szczegółów", "no_id_needed": "Nie potrzeba Dowodu!", "no_id_required": "Nie wymagamy Dowodu. Doładuj i wydawaj gdziekolwiek", "no_providers_available": "Brak dostępnych dostawców", @@ -675,8 +680,8 @@ "select_destination": "Wybierz miejsce docelowe dla pliku kopii zapasowej.", "select_hw_account_below": "Wybierz, które konto przywrócić poniżej:", "select_sell_provider_notice": "Wybierz dostawcę sprzedaży powyżej. Możesz pominąć ten ekran, ustawiając domyślnego dostawcę sprzedaży w ustawieniach aplikacji.", - "selected_trocador_provider": "Wybrany dostawca Trocador", "select_your_country": "Wybierz swój kraj", + "selected_trocador_provider": "Wybrany dostawca Trocador", "sell": "Sprzedać", "sell_alert_content": "Obecnie obsługujemy tylko sprzedaż Bitcoin, Ethereum i Litecoin. Utwórz lub przełącz się na swój portfel Bitcoin, Ethereum lub Litecoin.", "sell_monero_com_alert_content": "Sprzedaż Monero nie jest jeszcze obsługiwana", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index ccc87926d..f6f4a1295 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -148,6 +148,9 @@ "close": "Fechar", "coin_control": "Controle de moedas (opcional)", "cold_or_recover_wallet": "Adicione uma carteira somente leitura de Cupcake ou uma carteira fria ou recupere uma carteira de papel", + "collection_address": "Endereço de coleção", + "collection_description": "Descrição da coleção", + "collection_name": "Nome da coleção", "color_theme": "Tema de cor", "commit_transaction_amount_fee": "Confirmar transação\nQuantia: ${amount}\nTaxa: ${fee}", "confirm": "Confirmar", @@ -431,6 +434,7 @@ "methods": "Métodos", "min_amount": "Mínimo: ${valor}", "min_value": "Mín: ${value} ${currency}", + "mint_address": "Endereço da hortelã", "minutes_to_pin_code": "${minute} minutos", "mm": "MM", "modify_2fa": "Modificar o Cake 2FA", @@ -458,6 +462,7 @@ "new_wallet": "Nova carteira", "newConnection": "Nova conexão", "no_cards_found": "Nenhum cartão encontrado", + "no_extra_detail": "Sem detalhes extras disponíveis", "no_id_needed": "Nenhum ID necessário!", "no_id_required": "Não é necessário ID. Recarregue e gaste em qualquer lugar", "no_providers_available": "Nenhum fornecedor disponível", @@ -677,8 +682,8 @@ "select_destination": "Selecione o destino para o arquivo de backup.", "select_hw_account_below": "Selecione qual conta para restaurar abaixo:", "select_sell_provider_notice": "Selecione um fornecedor de venda acima. Você pode pular esta tela definindo seu provedor de venda padrão nas configurações do aplicativo.", - "selected_trocador_provider": "Provedor de Trocador selecionado", "select_your_country": "Selecione seu país", + "selected_trocador_provider": "Provedor de Trocador selecionado", "sell": "Vender", "sell_alert_content": "Atualmente, oferecemos suporte apenas à venda de Bitcoin, Ethereum e Litecoin. Crie ou troque para sua carteira Bitcoin, Ethereum ou Litecoin.", "sell_monero_com_alert_content": "A venda de Monero ainda não é suportada", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 757d8b7bd..e2a718cb0 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -148,6 +148,9 @@ "close": "Закрывать", "coin_control": "Контроль монет (необязательно)", "cold_or_recover_wallet": "Добавить кошелек только для чтения из Cupcake или холодный кошелек или восстановить бумажный кошелек", + "collection_address": "Адрес сбора", + "collection_description": "Описание коллекции", + "collection_name": "Название коллекции", "color_theme": "Цветовая тема", "commit_transaction_amount_fee": "Подтвердить транзакцию \nСумма: ${amount}\nКомиссия: ${fee}", "confirm": "Подтвердить", @@ -430,6 +433,7 @@ "methods": "Методы", "min_amount": "Минимум: ${value}", "min_value": "Мин: ${value} ${currency}", + "mint_address": "Мятный адрес", "minutes_to_pin_code": "${minute} минут", "mm": "ММ", "modify_2fa": "Изменить торт 2FA", @@ -457,6 +461,7 @@ "new_wallet": "Новый кошелёк", "newConnection": "Новое соединение", "no_cards_found": "Карт не найдено", + "no_extra_detail": "Нет дополнительных деталей", "no_id_needed": "Идентификатор не нужен!", "no_id_required": "Идентификатор не требуется. Пополняйте и тратьте где угодно", "no_providers_available": "Нет доступных поставщиков", @@ -676,8 +681,8 @@ "select_destination": "Пожалуйста, выберите место для файла резервной копии.", "select_hw_account_below": "Пожалуйста, выберите, какую учетную запись восстановить ниже:", "select_sell_provider_notice": "Выберите поставщика услуг продажи выше. Вы можете пропустить этот экран, установив поставщика услуг продаж по умолчанию в настройках приложения.", - "selected_trocador_provider": "Выбранный провайдер Трокадора", "select_your_country": "Пожалуйста, выберите свою страну", + "selected_trocador_provider": "Выбранный провайдер Трокадора", "sell": "Продавать", "sell_alert_content": "В настоящее время мы поддерживаем только продажу биткойнов, эфириума и лайткойна. Пожалуйста, создайте или переключитесь на свой кошелек Bitcoin, Ethereum или Litecoin.", "sell_monero_com_alert_content": "Продажа Monero пока не поддерживается", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 0ac198a58..ff950f3b7 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -148,6 +148,9 @@ "close": "ปิด", "coin_control": "การควบคุมเหรียญ (ตัวเลือก)", "cold_or_recover_wallet": "เพิ่มกระเป๋าเงินแบบอ่านอย่างเดียวจาก Cupcake หรือกระเป๋าเงินเย็นหรือกู้คืนกระเป๋ากระดาษ", + "collection_address": "ที่อยู่คอลเลกชัน", + "collection_description": "คำอธิบายการรวบรวม", + "collection_name": "ชื่อคอลเลกชัน", "color_theme": "ธีมสี", "commit_transaction_amount_fee": "ยืนยันธุรกรรม\nจำนวน: ${amount}\nค่าธรรมเนียม: ${fee}", "confirm": "ยืนยัน", @@ -430,6 +433,7 @@ "methods": "วิธีการ", "min_amount": "จำนวนขั้นต่ำ: ${value}", "min_value": "ขั้นต่ำ: ${value} ${currency}", + "mint_address": "ที่อยู่มินต์", "minutes_to_pin_code": "${minute} นาที", "mm": "เดือน", "modify_2fa": "แก้ไขเค้ก 2FA", @@ -457,6 +461,7 @@ "new_wallet": "กระเป๋าใหม่", "newConnection": "การเชื่อมต่อใหม่", "no_cards_found": "ไม่พบการ์ด", + "no_extra_detail": "ไม่มีรายละเอียดเพิ่มเติม", "no_id_needed": "ไม่จำเป็นต้องใช้บัตรประชาชน!", "no_id_required": "ไม่จำเป็นต้องใช้บัตรประจำตัว ฝากเงินและใช้งานได้ทุกที่", "no_providers_available": "ไม่มีผู้ให้บริการ", @@ -675,8 +680,8 @@ "select_destination": "โปรดเลือกปลายทางสำหรับไฟล์สำรอง", "select_hw_account_below": "กรุณาเลือกบัญชีที่จะกู้คืนด้านล่าง:", "select_sell_provider_notice": "เลือกผู้ให้บริการการขายด้านบน คุณสามารถข้ามหน้าจอนี้ได้โดยการตั้งค่าผู้ให้บริการการขายเริ่มต้นในการตั้งค่าแอป", - "selected_trocador_provider": "ผู้ให้บริการ Trocador ที่เลือก", "select_your_country": "กรุณาเลือกประเทศของคุณ", + "selected_trocador_provider": "ผู้ให้บริการ Trocador ที่เลือก", "sell": "ขาย", "sell_alert_content": "ขณะนี้เรารองรับการขาย Bitcoin, Ethereum และ Litecoin เท่านั้น โปรดสร้างหรือเปลี่ยนเป็นกระเป๋าเงิน Bitcoin, Ethereum หรือ Litecoin ของคุณ", "sell_monero_com_alert_content": "ยังไม่รองรับการขาย Monero", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 6a17a4c9d..fe065080f 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -148,6 +148,9 @@ "close": "Isara", "coin_control": "Coin control (opsyonal)", "cold_or_recover_wallet": "Magdagdag ng isang basahin lamang na pitaka mula sa Cupcake o isang malamig na pitaka o mabawi ang isang wallet ng papel", + "collection_address": "Address ng koleksyon", + "collection_description": "Paglalarawan ng Koleksyon", + "collection_name": "Pangalan ng Koleksyon", "color_theme": "Color theme", "commit_transaction_amount_fee": "Gumawa ng transaksyon\nHalaga: ${amount}\nFee: ${fee}", "confirm": "Kumpirmahin", @@ -430,6 +433,7 @@ "methods": "Mga Paraan", "min_amount": "Min: ${value}", "min_value": "Min: ${value} ${currency}", + "mint_address": "Mint Address", "minutes_to_pin_code": "${minute} minuto", "mm": "MM", "modify_2fa": "Baguhin ang Cake 2FA", @@ -457,6 +461,7 @@ "new_wallet": "Bagong Wallet", "newConnection": "Bagong Koneksyon", "no_cards_found": "Walang nahanap na mga card", + "no_extra_detail": "Walang magagamit na mga dagdag na detalye", "no_id_needed": "Hindi kailangan ng ID!", "no_id_required": "Hindi kailangan ng ID. I-top up at gumastos kahit saan", "no_providers_available": "Walang magagamit na mga nagbibigay", @@ -675,8 +680,8 @@ "select_destination": "Mangyaring piliin ang patutunguhan para sa backup na file.", "select_hw_account_below": "Mangyaring piliin kung aling account ang ibabalik sa ibaba:", "select_sell_provider_notice": "Pumili ng provider ng nagbebenta sa itaas. Maaari mong laktawan ang screen na ito sa pamamagitan ng pagtatakda ng iyong default na sell provider sa mga setting ng app.", - "selected_trocador_provider": "Napiling Trocador Provider", "select_your_country": "Mangyaring piliin ang iyong bansa", + "selected_trocador_provider": "Napiling Trocador Provider", "sell": "Ibenta", "sell_alert_content": "Kasalukuyan lamang naming sinusuportahan ang pagbebenta ng Bitcoin, Ethereum at Litecoin. Mangyaring lumikha o lumipat sa iyong Bitcoin, Ethereum o Litecoin wallet.", "sell_monero_com_alert_content": "Ang pagbebenta ng Monero ay hindi pa suportado", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 9f58e6861..9e9ac3726 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -148,6 +148,9 @@ "close": "Kapalı", "coin_control": "Koin kontrolü (isteğe bağlı)", "cold_or_recover_wallet": "Cupcake veya soğuk bir cüzdandan salt okunur bir cüzdan ekleyin veya bir kağıt cüzdanı kurtar", + "collection_address": "Toplama adresi", + "collection_description": "Koleksiyon Açıklama", + "collection_name": "Toplama adı", "color_theme": "Renk teması", "commit_transaction_amount_fee": "Transferi gerçekleştir\nMiktar: ${amount}\nKomisyon: ${fee}", "confirm": "Onayla", @@ -430,6 +433,7 @@ "methods": "Yöntemler", "min_amount": "Min: ${value}", "min_value": "En az: ${value} ${currency}", + "mint_address": "Nane adresi", "minutes_to_pin_code": "${minute} dakika", "mm": "AA", "modify_2fa": "Cake 2FA'yı Değiştirin", @@ -457,6 +461,7 @@ "new_wallet": "Yeni Cüzdan", "newConnection": "Yeni bağlantı", "no_cards_found": "Kart bulunamadı", + "no_extra_detail": "Ekstra ayrıntı yok", "no_id_needed": "Kimlik gerekmez!", "no_id_required": "Kimlik gerekmez. Para yükleyin ve istediğiniz yerde harcayın", "no_providers_available": "Sağlayıcı yok", @@ -675,8 +680,8 @@ "select_destination": "Lütfen yedekleme dosyası için hedef seçin.", "select_hw_account_below": "Lütfen aşağıda hangi hesabı geri yükleyeceğinizi seçin:", "select_sell_provider_notice": "Yukarıdan bir satış sağlayıcısı seçin. Uygulama ayarlarında varsayılan satış sağlayıcınızı ayarlayarak bu ekranı atlayabilirsiniz.", - "selected_trocador_provider": "Seçilmiş Trocador Sağlayıcı", "select_your_country": "Lütfen ülkenizi seçin", + "selected_trocador_provider": "Seçilmiş Trocador Sağlayıcı", "sell": "Satış", "sell_alert_content": "Şu anda yalnızca Bitcoin, Ethereum ve Litecoin satışını destekliyoruz. Lütfen Bitcoin, Ethereum veya Litecoin cüzdanınızı oluşturun veya cüzdanınıza geçin.", "sell_monero_com_alert_content": "Monero satışı henüz desteklenmiyor", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index af255c26a..6b321cb78 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -148,6 +148,9 @@ "close": "Закрити", "coin_control": "Контроль монет (необов’язково)", "cold_or_recover_wallet": "Додайте гаманець лише для читання від Cupcake або холодного гаманця або відновіть паперовий гаманець", + "collection_address": "Адреса колекції", + "collection_description": "Опис колекції", + "collection_name": "Назва колекції", "color_theme": "Кольорова тема", "commit_transaction_amount_fee": "Підтвердити транзакцію \nСума: ${amount}\nКомісія: ${fee}", "confirm": "Підтвердити", @@ -430,6 +433,7 @@ "methods": "методи", "min_amount": "Мінімум: ${value}", "min_value": "Мін: ${value} ${currency}", + "mint_address": "Адреса м'яти", "minutes_to_pin_code": "${minute} хвилин", "mm": "MM", "modify_2fa": "Змінити торт 2FA", @@ -457,6 +461,7 @@ "new_wallet": "Новий гаманець", "newConnection": "Нове підключення", "no_cards_found": "Карт не знайдено", + "no_extra_detail": "Немає додаткових деталей", "no_id_needed": "Ідентифікатор не потрібен!", "no_id_required": "Ідентифікатор не потрібен. Поповнюйте та витрачайте будь-де", "no_providers_available": "Немає постачальників", @@ -676,8 +681,8 @@ "select_destination": "Виберіть місце призначення для файлу резервної копії.", "select_hw_account_below": "Виберіть, який рахунок відновити нижче:", "select_sell_provider_notice": "Виберіть вище постачальника послуг продажу. Ви можете пропустити цей екран, встановивши постачальника послуг продажу за умовчанням у налаштуваннях програми.", - "selected_trocador_provider": "Вибраний постачальник Trocador", "select_your_country": "Будь ласка, виберіть свою країну", + "selected_trocador_provider": "Вибраний постачальник Trocador", "sell": "Продати", "sell_alert_content": "Наразі ми підтримуємо лише продаж Bitcoin, Ethereum і Litecoin. Створіть або перейдіть на свій гаманець Bitcoin, Ethereum або Litecoin.", "sell_monero_com_alert_content": "Продаж Monero ще не підтримується", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index cb86f8af6..3ed3f5949 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -148,6 +148,9 @@ "close": "بند کریں", "coin_control": "سکے کنٹرول (اختیاری)", "cold_or_recover_wallet": "Cupcake یا سرد بٹوے سے صرف ایک پڑھنے والا پرس شامل کریں یا کاغذ کا پرس بازیافت کریں", + "collection_address": "جمع کرنے کا پتہ", + "collection_description": "جمع کرنے کی تفصیل", + "collection_name": "جمع کرنے کا نام", "color_theme": "رنگین تھیم", "commit_transaction_amount_fee": "لین دین کا ارتکاب کریں\\nرقم: ${amount}\\nفیس: ${fee}", "confirm": "تصدیق کریں۔", @@ -430,6 +433,7 @@ "methods": "ﮯﻘﯾﺮﻃ", "min_amount": "کم سے کم: ${value}", "min_value": "کم سے کم: ${value} ${currency}", + "mint_address": "ٹکسال کا پتہ", "minutes_to_pin_code": "${minute} منٹ", "mm": "MM", "modify_2fa": "کیک 2FA میں ترمیم کریں۔", @@ -457,6 +461,7 @@ "new_wallet": "نیا پرس", "newConnection": "ﻦﺸﮑﻨﮐ ﺎﯿﻧ", "no_cards_found": "کوئی کارڈ نہیں ملا", + "no_extra_detail": "کوئی اضافی تفصیلات دستیاب نہیں ہیں", "no_id_needed": "شناخت کی ضرورت نہیں!", "no_id_required": "کوئی ID درکار نہیں۔ ٹاپ اپ اور کہیں بھی خرچ کریں۔", "no_providers_available": "کوئی فراہم کنندہ دستیاب نہیں ہے", @@ -677,8 +682,8 @@ "select_destination": "۔ﮟﯾﺮﮐ ﺏﺎﺨﺘﻧﺍ ﺎﮐ ﻝﺰﻨﻣ ﮯﯿﻟ ﮯﮐ ﻞﺋﺎﻓ ﭖﺍ ﮏﯿﺑ ﻡﺮﮐ ﮦﺍﺮﺑ", "select_hw_account_below": "براہ کرم ذیل میں کون سا اکاؤنٹ بحال کرنا ہے منتخب کریں:", "select_sell_provider_notice": "۔ﮟﯿﮨ ﮯﺘﮑﺳ ﮌﻮﮭﭼ ﻮﮐ ﻦﯾﺮﮑﺳﺍ ﺱﺍ ﺮﮐ ﮮﺩ ﺐﯿﺗﺮﺗ ﻮﮐ ﮦﺪﻨﻨﮐ ﻢﮨﺍﺮﻓ ﻞﯿﺳ ﭧﻟﺎﻔﯾﮈ ﮯﻨﭘﺍ ﮟﯿﻣ ﺕﺎﺒ", - "selected_trocador_provider": "منتخب کردہ ٹروکاڈور فراہم کنندہ", "select_your_country": "براہ کرم اپنے ملک کو منتخب کریں", + "selected_trocador_provider": "منتخب کردہ ٹروکاڈور فراہم کنندہ", "sell": "بیچنا", "sell_alert_content": "۔ﮟﯾﺮﮐ ﭻﺋﻮﺳ ﺮﭘ ﺱﺍ ﺎﯾ ﮟﯿﺋﺎﻨﺑ ﭧﯿﻟﺍﻭ Litecoin ﺎﯾ Bitcoin، Ethereum ﺎﻨﭘﺍ ﻡﺮﮐ ﮦﺍﺮﺑ ۔", "sell_monero_com_alert_content": "Monero فروخت کرنا ابھی تک تعاون یافتہ نہیں ہے۔", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index ff547e77b..787115f44 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -146,6 +146,9 @@ "close": "Đóng", "coin_control": "Kiểm soát đồng xu (tùy chọn)", "cold_or_recover_wallet": "Thêm ví chỉ đọc từ Cupcake hoặc ví lạnh hoặc thu hồi ví giấy", + "collection_address": "Địa chỉ bộ sưu tập", + "collection_description": "Mô tả bộ sưu tập", + "collection_name": "Tên bộ sưu tập", "color_theme": "Chủ đề màu sắc", "commit_transaction_amount_fee": "Cam kết giao dịch\nSố tiền: ${amount}\nPhí: ${fee}", "confirm": "Xác nhận", @@ -429,6 +432,7 @@ "methods": "Phương pháp", "min_amount": "Tối thiểu: ${value}", "min_value": "Tối thiểu: ${value} ${currency}", + "mint_address": "Địa chỉ bạc hà", "minutes_to_pin_code": "${minute} phút", "mm": "MM", "modify_2fa": "Chỉnh sửa Cake 2FA", @@ -456,6 +460,7 @@ "new_wallet": "Ví mới", "newConnection": "Kết nối mới", "no_cards_found": "Không tìm thấy thẻ", + "no_extra_detail": "Không có thêm chi tiết có sẵn", "no_id_needed": "Không cần ID!", "no_id_required": "Không yêu cầu ID. Nạp tiền và chi tiêu ở bất kỳ đâu", "no_relay_on_domain": "Không có relay cho miền của người dùng hoặc relay không khả dụng. Vui lòng chọn một relay để sử dụng.", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index e5e94b01f..ac9f30013 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -148,6 +148,9 @@ "close": "sunmo", "coin_control": "Ìdarí owó ẹyọ (ìyàn nìyí)", "cold_or_recover_wallet": "Ṣafikun apamọwọ kika-nikan lati Cupcake tabi apamọwọ tutu tabi gba owo apamọwọ iwe kan", + "collection_address": "Adirẹsi", + "collection_description": "Ijuwe akopọ", + "collection_name": "Orukọ ikojọpọ", "color_theme": "Àwọn ààtò àwọ̀", "commit_transaction_amount_fee": "Jẹ́rìí sí àránṣẹ́\nOwó: ${amount}\nIye àfikún: ${fee}", "confirm": "Jẹ́rìísí", @@ -431,6 +434,7 @@ "methods": "Awọn ọna", "min_amount": "kò kéré ju: ${value}", "min_value": "kò gbọ́dọ̀ kéré ju ${value} ${currency}", + "mint_address": "Adirẹsi Mint", "minutes_to_pin_code": "${minute} ìṣẹ́jú", "mm": "Os", "modify_2fa": "Fi iṣiro 2FA sii Cake", @@ -458,6 +462,7 @@ "new_wallet": "Àpamọ́wọ́ títun", "newConnection": "Tuntun Asopọ", "no_cards_found": "Ko si awọn kaadi ti a rii", + "no_extra_detail": "Ko si awọn alaye afikun ti o wa", "no_id_needed": "Ẹ kò nílò àmì ìdánimọ̀!", "no_id_required": "Ẹ kò nílò àmì ìdánimọ̀. Ẹ lè fikún owó àti san níbikíbi", "no_providers_available": "Ko si awọn olupese ti o wa", @@ -676,8 +681,8 @@ "select_destination": "Jọwọ yan ibi ti o nlo fun faili afẹyinti.", "select_hw_account_below": "Jọwọ yan iru iroyin lati mu pada ni isalẹ:", "select_sell_provider_notice": "Yan olupese ti o ta loke. O le foju iboju yii nipa tito olupese iṣẹ tita aiyipada rẹ ni awọn eto app.", - "selected_trocador_provider": "olupese trocador ti a yan", "select_your_country": "Jọwọ yan orilẹ-ede rẹ", + "selected_trocador_provider": "olupese trocador ti a yan", "sell": "Tà", "sell_alert_content": "Lọwọlọwọ a ṣe atilẹyin tita Bitcoin, Ethereum ati Litecoin nikan. Jọwọ ṣẹda tabi yipada si Bitcoin, Ethereum tabi apamọwọ Litecoin rẹ.", "sell_monero_com_alert_content": "Kọ ju lọwọ Monero ko ṣe ni ibamu", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index eeba24191..1eb3e7d7a 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -148,6 +148,9 @@ "close": "关闭", "coin_control": "硬幣控制(可選)", "cold_or_recover_wallet": "从Cupcake或冷钱包中添加只读的钱包或恢复纸钱包", + "collection_address": "收集地址", + "collection_description": "集合描述", + "collection_name": "收集名称", "color_theme": "主题", "commit_transaction_amount_fee": "提交交易\n金额: ${amount}\n手续费: ${fee}", "confirm": "确认", @@ -430,6 +433,7 @@ "methods": "方法", "min_amount": "最小值: ${value}", "min_value": "最小: ${value} ${currency}", + "mint_address": "薄荷地址", "minutes_to_pin_code": "${minute} 分钟", "mm": "毫米", "modify_2fa": "修改蛋糕2FA", @@ -457,6 +461,7 @@ "new_wallet": "新钱包", "newConnection": "新连接", "no_cards_found": "找不到卡", + "no_extra_detail": "没有其他详细信息", "no_id_needed": "不需要 ID!", "no_id_required": "不需要身份证。充值并在任何地方消费", "no_providers_available": "没有提供商可用", @@ -675,8 +680,8 @@ "select_destination": "请选择备份文件的目的地。", "select_hw_account_below": "请在下面选择要还原的帐户:", "select_sell_provider_notice": "选择上面的销售提供商。您可以通过在应用程序设置中设置默认销售提供商来跳过此屏幕。", - "selected_trocador_provider": "选定的trocador提供商", "select_your_country": "请选择你的国家", + "selected_trocador_provider": "选定的trocador提供商", "sell": "卖", "sell_alert_content": "我们目前仅支持比特币、以太坊和莱特币的销售。请创建或切换到您的比特币、以太坊或莱特币钱包。", "sell_monero_com_alert_content": "尚不支持出售门罗币", From 9f3078740e86a0463e8f999cd919a51768fc117b Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Fri, 14 Mar 2025 17:31:31 +0100 Subject: [PATCH 039/243] CW-942-New-Swap-UI-Flow (#2036) * feat: Implement New Swap UI including fee items and coin control and extract common fee logic to a feeViewModel, use that in SendViewModel and ExchangeViewModel * feat: Implement New Swap UI with fee items and coin control and extract common fee logic to a feeViewModel for use in SendViewModel and ExchangeViewModel * Update colors * Show warning if user choose a below default transaction priority * fix: Add warning when user select less than default transaction priority and add condition to show extraId * Complete Swap UI flow * Handle edgecases for XRP * Switch swap page button texts * Handle edgecases for XLM * Switch prompt text and handle edgecases --------- Co-authored-by: tuxpizza Co-authored-by: Omar Hatem --- integration_test/funds_related_tests.dart | 2 +- .../robots/exchange_trade_page_robot.dart | 8 +- integration_test/robots/send_page_robot.dart | 10 +- lib/di.dart | 29 +- lib/router.dart | 4 + lib/routes.dart | 1 + lib/src/screens/exchange/exchange_page.dart | 6 +- .../widgets/present_provider_picker.dart | 3 +- .../exchange_trade/exchange_confirm_page.dart | 4 +- .../exchange_trade_external_send_page.dart | 143 ++++++++ .../exchange_trade/exchange_trade_item.dart | 6 +- .../exchange_trade/exchange_trade_page.dart | 251 +++++++------- .../exchange_trade_card_item_widget.dart | 312 ++++++++++++++++++ lib/src/screens/send/widgets/send_card.dart | 18 +- lib/src/widgets/list_row.dart | 31 +- .../exchange/exchange_trade_view_model.dart | 78 +++-- .../exchange/exchange_view_model.dart | 63 +--- lib/view_model/send/fees_view_model.dart | 191 +++++++++++ lib/view_model/send/send_view_model.dart | 88 +---- .../transaction_details_view_model.dart | 15 +- res/values/strings_ar.arb | 4 + res/values/strings_bg.arb | 4 + res/values/strings_cs.arb | 4 + res/values/strings_de.arb | 4 + res/values/strings_en.arb | 4 + res/values/strings_es.arb | 4 + res/values/strings_fr.arb | 4 + res/values/strings_ha.arb | 4 + res/values/strings_hi.arb | 4 + res/values/strings_hr.arb | 4 + res/values/strings_hy.arb | 4 + res/values/strings_id.arb | 4 + 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_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 + 48 files changed, 1048 insertions(+), 327 deletions(-) create mode 100644 lib/src/screens/exchange_trade/exchange_trade_external_send_page.dart create mode 100644 lib/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart create mode 100644 lib/view_model/send/fees_view_model.dart diff --git a/integration_test/funds_related_tests.dart b/integration_test/funds_related_tests.dart index 27187dc2f..c56520ec2 100644 --- a/integration_test/funds_related_tests.dart +++ b/integration_test/funds_related_tests.dart @@ -84,7 +84,7 @@ void main() { exchangeTradePageRobot.hasInformationDialog(); await exchangeTradePageRobot.onGotItButtonPressed(); - await exchangeTradePageRobot.onConfirmSendingButtonPressed(); + await exchangeTradePageRobot.onSendFromCakeButtonPressed(); await exchangeTradePageRobot.handleConfirmSendResult(); diff --git a/integration_test/robots/exchange_trade_page_robot.dart b/integration_test/robots/exchange_trade_page_robot.dart index 5708b6fae..4dc688ae7 100644 --- a/integration_test/robots/exchange_trade_page_robot.dart +++ b/integration_test/robots/exchange_trade_page_robot.dart @@ -27,11 +27,11 @@ class ExchangeTradePageRobot { await commonTestCases.defaultSleepTime(); } - Future onConfirmSendingButtonPressed() async { - tester.printToConsole('Now confirming sending'); + Future onSendFromCakeButtonPressed() async { + tester.printToConsole('Now sending from cake'); await commonTestCases.tapItemByKey( - 'exchange_trade_page_confirm_sending_button_key', + 'exchange_trade_page_send_from_cake_button_key', shouldPumpAndSettle: false, ); @@ -134,7 +134,7 @@ class ExchangeTradePageRobot { await commonTestCases.defaultSleepTime(); - await onConfirmSendingButtonPressed(); + await onSendFromCakeButtonPressed(); tester.printToConsole('Confirm sending button tapped'); hasError = await hasErrorWhileSending(); diff --git a/integration_test/robots/send_page_robot.dart b/integration_test/robots/send_page_robot.dart index b705c803f..e74f03c76 100644 --- a/integration_test/robots/send_page_robot.dart +++ b/integration_test/robots/send_page_robot.dart @@ -51,7 +51,7 @@ class SendPageRobot { commonTestCases.hasValueKey('send_page_fiat_amount_textfield_key'); } - if (sendViewModel.hasFees) { + if (sendViewModel.feesViewModel.hasFees) { commonTestCases.hasValueKey('send_page_select_fee_priority_button_key'); } @@ -104,12 +104,12 @@ class SendPageRobot { SendPage sendPage = tester.widget(find.byType(SendPage)); final sendViewModel = sendPage.sendViewModel; - if (!sendViewModel.hasFees || priority == null) return; + if (!sendViewModel.feesViewModel.hasFees || priority == null) return; final transactionPriorityPickerKey = 'send_page_select_fee_priority_button_key'; await commonTestCases.tapItemByKey(transactionPriorityPickerKey); - if (priority == sendViewModel.transactionPriority) { + if (priority == sendViewModel.feesViewModel.transactionPriority) { await commonTestCases .tapItemByKey('picker_items_index_${priority.title}_selected_item_button_key'); return; @@ -188,8 +188,8 @@ class SendPageRobot { await authPageRobot.enterPinCode(CommonTestConstants.pin); } - final onAuthPageDesktop = authPageRobot.onAuthPageDesktop(); - if (onAuthPageDesktop) { + final onAuthPageDesktop = authPageRobot.onAuthPageDesktop(); + if (onAuthPageDesktop) { await authPageRobot.enterPassword(CommonTestConstants.pin.join("")); } } diff --git a/lib/di.dart b/lib/di.dart index 7e844b37c..f3de2bc96 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -42,6 +42,7 @@ import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart'; import 'package:cake_wallet/entities/wallet_manager.dart'; import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart'; import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart'; +import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_external_send_page.dart'; import 'package:cake_wallet/src/screens/receive/address_list_page.dart'; import 'package:cake_wallet/src/screens/seed/seed_verification/seed_verification_page.dart'; import 'package:cake_wallet/src/screens/send/transaction_success_info_page.dart'; @@ -53,6 +54,7 @@ import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart'; +import 'package:cake_wallet/view_model/send/fees_view_model.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/qr_view_data.dart'; @@ -755,6 +757,7 @@ Future setup({ getIt.get().wallet!.isHardwareWallet ? getIt.get() : null, coinTypeToSpendFrom: coinTypeToSpendFrom ?? UnspentCoinType.nonMweb, getIt.get(param1: coinTypeToSpendFrom), + getIt.get(), ), ); @@ -1034,20 +1037,35 @@ Future setup({ getIt.registerFactoryParam((title, uri) => WebViewPage(title, uri)); - getIt.registerFactory(() => ExchangeViewModel( + getIt.registerFactory( + () => ExchangeViewModel( getIt.get(), _tradesSource, getIt.get(), getIt.get(), getIt.get().settingsStore, getIt.get(), - getIt.get())); + getIt.get(), + getIt.get(), + ), + ); - getIt.registerFactory(() => ExchangeTradeViewModel( + getIt.registerFactory( + () => FeesViewModel( + getIt.get(), + getIt.get(), + ), + ); + + getIt.registerFactory( + () => ExchangeTradeViewModel( wallet: getIt.get().wallet!, trades: _tradesSource, tradesStore: getIt.get(), - sendViewModel: getIt.get())); + sendViewModel: getIt.get(), + feesViewModel: getIt.get(), + ), + ); getIt.registerFactoryParam( (PaymentRequest? paymentRequest, __) { @@ -1059,6 +1077,9 @@ Future setup({ getIt.registerFactory( () => ExchangeTradePage(exchangeTradeViewModel: getIt.get())); + getIt.registerFactory( + () => ExchangeTradeExternalSendPage(exchangeTradeViewModel: getIt.get())); + getIt.registerFactory(() => ExchangeTemplatePage(getIt.get())); getIt.registerFactoryParam((WalletType param1, __) { diff --git a/lib/router.dart b/lib/router.dart index 9026919d6..497bcdaa1 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -40,6 +40,7 @@ import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart'; import 'package:cake_wallet/src/screens/exchange/exchange_page.dart'; import 'package:cake_wallet/src/screens/exchange/exchange_template_page.dart'; import 'package:cake_wallet/src/screens/exchange_trade/exchange_confirm_page.dart'; +import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_external_send_page.dart'; import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_page.dart'; import 'package:cake_wallet/src/screens/faq/faq_page.dart'; import 'package:cake_wallet/src/screens/monero_accounts/monero_account_edit_or_create_page.dart'; @@ -813,6 +814,9 @@ Route createRoute(RouteSettings settings) { builder: (_) => getIt.get(), ); + case Routes.exchangeTradeExternalSendPage: + return MaterialPageRoute(builder: (_) => getIt.get(),); + default: return MaterialPageRoute( builder: (_) => Scaffold( diff --git a/lib/routes.dart b/lib/routes.dart index 4b397c57c..e2b4fdb34 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -118,4 +118,5 @@ class Routes { static const walletGroupDescription = '/wallet_group_description'; static const walletGroupExistingSeedDescriptionPage = '/wallet_group_existing_seed_description_page'; static const walletSeedVerificationPage = '/wallet_seed_verification_page'; + static const exchangeTradeExternalSendPage = '/exchange_trade_external_send_page'; } diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index f11a50bfe..e5027198b 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -248,7 +248,7 @@ class ExchangePage extends BasePage { Observer( builder: (_) => LoadingPrimaryButton( key: ValueKey('exchange_page_exchange_button_key'), - text: exchangeViewModel.isAvailableInSelected ? S.of(context).exchange : S.of(context).change_selected_exchanges, + text: exchangeViewModel.isAvailableInSelected ? S.of(context).swap : S.of(context).change_selected_exchanges, onPressed: exchangeViewModel.isAvailableInSelected ? () { FocusScope.of(context).unfocus(); @@ -377,7 +377,7 @@ class ExchangePage extends BasePage { return; } - if (exchangeViewModel.isLowFee) { + if (exchangeViewModel.feesViewModel.isLowFee) { _showFeeAlert(context); } @@ -640,7 +640,7 @@ class ExchangePage extends BasePage { }) ?? false; if (confirmed) { - exchangeViewModel.setDefaultTransactionPriority(); + exchangeViewModel.feesViewModel .setDefaultTransactionPriority(); } } diff --git a/lib/src/screens/exchange/widgets/present_provider_picker.dart b/lib/src/screens/exchange/widgets/present_provider_picker.dart index c79325910..e496796d0 100644 --- a/lib/src/screens/exchange/widgets/present_provider_picker.dart +++ b/lib/src/screens/exchange/widgets/present_provider_picker.dart @@ -35,10 +35,11 @@ class PresentProviderPicker extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - Text(S.of(context).exchange, + Text(S.of(context).swap, style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.w600, + color: Colors.white)), Observer( builder: (_) => Text( diff --git a/lib/src/screens/exchange_trade/exchange_confirm_page.dart b/lib/src/screens/exchange_trade/exchange_confirm_page.dart index cbf29b19b..60b534c6e 100644 --- a/lib/src/screens/exchange_trade/exchange_confirm_page.dart +++ b/lib/src/screens/exchange_trade/exchange_confirm_page.dart @@ -21,7 +21,7 @@ class ExchangeConfirmPage extends BasePage { final Trade trade; @override - String get title => S.current.copy_id; + String get title => S.current.swap; @override Widget body(BuildContext context) { @@ -40,7 +40,7 @@ class ExchangeConfirmPage extends BasePage { style: TextStyle( fontSize: 18.0, fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.titleColor), + color: Theme.of(context).extension()!.detailsTitlesColor), ), )), Container( diff --git a/lib/src/screens/exchange_trade/exchange_trade_external_send_page.dart b/lib/src/screens/exchange_trade/exchange_trade_external_send_page.dart new file mode 100644 index 000000000..5ed82e71c --- /dev/null +++ b/lib/src/screens/exchange_trade/exchange_trade_external_send_page.dart @@ -0,0 +1,143 @@ +import 'package:cake_wallet/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart'; +import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart'; +import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart'; +import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; + +class ExchangeTradeExternalSendPage extends BasePage { + ExchangeTradeExternalSendPage({required this.exchangeTradeViewModel}); + + final ExchangeTradeViewModel exchangeTradeViewModel; + @override + String get title => S.current.swap; + + @override + bool get gradientBackground => true; + + @override + bool get gradientAll => true; + + @override + bool get resizeToAvoidBottomInset => false; + + @override + bool get extendBodyBehindAppBar => true; + + @override + AppBarStyle get appBarStyle => AppBarStyle.transparent; + + final fetchingLabel = S.current.fetching; + + @override + Widget body(BuildContext context) { + final copyImage = Image.asset( + 'assets/images/copy_content.png', + height: 16, + width: 16, + color: Theme.of(context).extension()!.estimatedFeeColor, + ); + return Container( + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.only(top: 36, bottom: 24), + content: Observer( + builder: (_) { + return Column( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), + gradient: LinearGradient( + colors: [ + Theme.of(context).extension()!.firstGradientColor, + Theme.of(context).extension()!.secondGradientColor, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + padding: EdgeInsets.fromLTRB(24, 110, 24, 32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Spacer(flex: 3), + Flexible( + flex: 6, + child: Center( + child: AspectRatio( + aspectRatio: 1.0, + child: Container( + padding: EdgeInsets.all(5), + decoration: BoxDecoration( + border: Border.all( + width: 3, + color: Theme.of(context) + .extension()! + .qrCodeColor, + ), + ), + child: QrImage( + data: + exchangeTradeViewModel.trade.inputAddress ?? fetchingLabel, + ), + ), + ), + ), + ), + Spacer(flex: 3) + ], + ), + SizedBox(height: 24), + ...exchangeTradeViewModel.items + .where((item) => item.isExternalSendDetail) + .map( + (item) => TradeItemRowWidget( + currentTheme: currentTheme, + title: item.title, + value: item.data, + isCopied: true, + copyImage: copyImage, + ), + ) + .toList(), + ], + ), + ), + ], + ); + }, + ), + bottomSection: Observer( + builder: (_) { + final trade = exchangeTradeViewModel.trade; + final sendingState = exchangeTradeViewModel.sendViewModel.state; + + return exchangeTradeViewModel.isSendable && !(sendingState is TransactionCommitted) + ? LoadingPrimaryButton( + key: ValueKey('exchange_trade_external_send_page_continue_button_key'), + isDisabled: trade.inputAddress == null || trade.inputAddress!.isEmpty, + isLoading: sendingState is IsExecutingState, + onPressed: () { + Navigator.of(context).popUntil((route) => route.isFirst); + }, + text: S.current.continue_text, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + ) + : Offstage(); + }, + ), + ), + ); + } +} diff --git a/lib/src/screens/exchange_trade/exchange_trade_item.dart b/lib/src/screens/exchange_trade/exchange_trade_item.dart index 007a89e25..007427eb4 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_item.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_item.dart @@ -1,13 +1,15 @@ -import 'package:flutter/cupertino.dart'; - class ExchangeTradeItem { ExchangeTradeItem({ required this.title, required this.data, required this.isCopied, + required this.isReceiveDetail, + required this.isExternalSendDetail, }); String title; String data; bool isCopied; + bool isReceiveDetail; + bool isExternalSendDetail; } \ No newline at end of file diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index 130ffa6b0..6adb08de8 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -1,22 +1,22 @@ import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart'; +import 'package:cake_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart'; +import 'package:cake_wallet/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; import 'dart:ui'; -import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; import 'package:cake_wallet/utils/request_review_handler.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:mobx/mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart'; import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart'; -import 'package:cake_wallet/src/widgets/list_row.dart'; -import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; -import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/exchange_trade/widgets/timer_widget.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; @@ -30,8 +30,7 @@ void showInformation( final walletName = exchangeTradeViewModel.wallet.name; final information = exchangeTradeViewModel.isSendable - ? S.current.exchange_result_confirm( - trade.amount, trade.from.toString(), walletName) + + ? S.current.exchange_trade_result_confirm(trade.amount, trade.from.toString(), walletName) + exchangeTradeViewModel.extraInfo : S.current.exchange_result_description( trade.amount, trade.from.toString()) + @@ -50,7 +49,22 @@ class ExchangeTradePage extends BasePage { final ExchangeTradeViewModel exchangeTradeViewModel; @override - String get title => S.current.exchange; + String get title => S.current.swap; + + @override + bool get gradientBackground => true; + + @override + bool get gradientAll => true; + + @override + bool get resizeToAvoidBottomInset => false; + + @override + bool get extendBodyBehindAppBar => true; + + @override + AppBarStyle get appBarStyle => AppBarStyle.transparent; @override Widget trailing(BuildContext context) { @@ -74,14 +88,20 @@ class ExchangeTradePage extends BasePage { } @override - Widget body(BuildContext context) => - ExchangeTradeForm(exchangeTradeViewModel); + Widget body(BuildContext context) => ExchangeTradeForm( + exchangeTradeViewModel, + currentTheme, + ); } class ExchangeTradeForm extends StatefulWidget { - ExchangeTradeForm(this.exchangeTradeViewModel); + ExchangeTradeForm( + this.exchangeTradeViewModel, + this.currentTheme, + ); final ExchangeTradeViewModel exchangeTradeViewModel; + final ThemeBase currentTheme; @override ExchangeTradeState createState() => ExchangeTradeState(); @@ -115,119 +135,77 @@ class ExchangeTradeState extends State { @override Widget build(BuildContext context) { - final copyImage = Image.asset('assets/images/copy_content.png', - height: 16, - width: 16, - color: Theme.of(context).extension()!.detailsTitlesColor); - _setEffects(); return Container( child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.only(top: 10, bottom: 16), - content: Observer(builder: (_) { - final trade = widget.exchangeTradeViewModel.trade; + contentPadding: EdgeInsets.only(top: 10, bottom: 16), + content: Observer(builder: (_) { + final trade = widget.exchangeTradeViewModel.trade; - return Column( - children: [ - trade.expiredAt != null - ? Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - S.of(context).offer_expires_in, - style: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.detailsTitlesColor), - ), - if (trade.expiredAt != null) - TimerWidget(trade.expiredAt!, - color: Theme.of(context).extension()!.titleColor) - ]) - : Offstage(), - Padding( - padding: EdgeInsets.only(top: 32), - child: Row(children: [ - Spacer(flex: 3), - Flexible( - flex: 4, - child: Center( - child: AspectRatio( - aspectRatio: 1.0, - child: Container( - padding: EdgeInsets.all(5), - decoration: BoxDecoration( - border: Border.all( - width: 3, - color: Theme.of(context).extension()!.qrCodeColor - ) - ), - child: QrImage(data: trade.inputAddress ?? fetchingLabel), - )))), - Spacer(flex: 3) - ]), - ), - Padding( - padding: EdgeInsets.only(top: 16), - child: ListView.separated( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: widget.exchangeTradeViewModel.items.length, - separatorBuilder: (context, index) => Container( - height: 1, - color: Theme.of(context).extension()!.dividerCodeColor, - ), - itemBuilder: (context, index) { - final item = widget.exchangeTradeViewModel.items[index]; - final value = item.data; + return Column( + children: [ + trade.expiredAt != null + ? Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + S.of(context).offer_expires_in, + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .extension()! + .detailsTitlesColor), + ), + if (trade.expiredAt != null) + TimerWidget(trade.expiredAt!, + color: Theme.of(context).extension()!.titleColor) + ]) + : Offstage(), + _ExchangeTradeItemsCardSection( + viewModel: widget.exchangeTradeViewModel, + currentTheme: widget.currentTheme, + ), + ], + ); + }), + bottomSectionPadding: EdgeInsets.fromLTRB(24, 0, 24, 24), + bottomSection: Column( + children: [ + PrimaryButton( + key: ValueKey('exchange_trade_page_send_from_external_button_key'), + text: S.current.send_from_external_wallet, + onPressed: () async { + Navigator.of(context).pushNamed(Routes.exchangeTradeExternalSendPage); + }, + color: Theme.of(context).cardColor, + textColor: Theme.of(context).extension()!.buttonTextColor, + ), + SizedBox(height: 16), + Observer( + builder: (_) { + final trade = widget.exchangeTradeViewModel.trade; + final sendingState = widget.exchangeTradeViewModel.sendViewModel.state; - final content = ListRow( - title: item.title, - value: value, - valueFontSize: 14, - image: item.isCopied ? copyImage : null, - ); - - return item.isCopied - ? Builder( - builder: (context) => GestureDetector( - onTap: () { - Clipboard.setData( - ClipboardData(text: value)); - showBar(context, - S.of(context).copied_to_clipboard); - }, - child: content, - )) - : content; - }, - ), - ), - ], - ); - }), - bottomSectionPadding: EdgeInsets.fromLTRB(24, 0, 24, 24), - bottomSection: Observer(builder: (_) { - final trade = widget.exchangeTradeViewModel.trade; - final sendingState = - widget.exchangeTradeViewModel.sendViewModel.state; - - return widget.exchangeTradeViewModel.isSendable && - !(sendingState is TransactionCommitted) - ? LoadingPrimaryButton( - key: ValueKey('exchange_trade_page_confirm_sending_button_key'), - isDisabled: trade.inputAddress == null || - trade.inputAddress!.isEmpty, - isLoading: sendingState is IsExecutingState, - onPressed: () => - widget.exchangeTradeViewModel.confirmSending(), - text: S.of(context).confirm, - color: Theme.of(context).primaryColor, - textColor: Colors.white) - : Offstage(); - })), + return widget.exchangeTradeViewModel.isSendable && + !(sendingState is TransactionCommitted) + ? LoadingPrimaryButton( + key: ValueKey('exchange_trade_page_send_from_cake_button_key'), + isDisabled: trade.inputAddress == null || trade.inputAddress!.isEmpty, + isLoading: sendingState is IsExecutingState, + onPressed: () => widget.exchangeTradeViewModel.confirmSending(), + text:S.current.send_from_cake_wallet, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + ) + : Offstage(); + }, + ), + ], + ), + ), ); } @@ -412,3 +390,40 @@ class ExchangeTradeState extends State { } } } + +class _ExchangeTradeItemsCardSection extends StatelessWidget { + const _ExchangeTradeItemsCardSection({ + required this.viewModel, + required this.currentTheme, + }); + + final ExchangeTradeViewModel viewModel; + final ThemeBase currentTheme; + + @override + Widget build(BuildContext context) { + final firstExchangeCard = ExchangeTradeCardItemWidget( + currentTheme: currentTheme, + isReceiveDetailsCard: true, + exchangeTradeViewModel: viewModel, + ); + + final secondExchangeCard = ExchangeTradeCardItemWidget( + currentTheme: currentTheme, + isReceiveDetailsCard: false, + exchangeTradeViewModel: viewModel, + ); + + if (responsiveLayoutUtil.shouldRenderMobileUI) { + return MobileExchangeCardsSection( + firstExchangeCard: firstExchangeCard, + secondExchangeCard: secondExchangeCard, + ); + } + + return DesktopExchangeCardsSection( + firstExchangeCard: firstExchangeCard, + secondExchangeCard: secondExchangeCard, + ); + } +} diff --git a/lib/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart b/lib/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart new file mode 100644 index 000000000..9b07023c2 --- /dev/null +++ b/lib/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart @@ -0,0 +1,312 @@ +import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/src/widgets/list_row.dart'; +import 'package:cake_wallet/src/widgets/picker.dart'; +import 'package:cake_wallet/utils/show_bar.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart'; +import 'package:cake_wallet/view_model/send/fees_view_model.dart'; +import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import '../../../../themes/extensions/cake_text_theme.dart'; +import '../../../../themes/extensions/transaction_trade_theme.dart'; +import '../../../../themes/theme_base.dart'; + +class ExchangeTradeCardItemWidget extends StatelessWidget { + ExchangeTradeCardItemWidget({ + required this.isReceiveDetailsCard, + required this.exchangeTradeViewModel, + required this.currentTheme, + Key? key, + }) : feesViewModel = exchangeTradeViewModel.feesViewModel, + output = exchangeTradeViewModel.output; + + final Output output; + final bool isReceiveDetailsCard; + final FeesViewModel feesViewModel; + final ExchangeTradeViewModel exchangeTradeViewModel; + final ThemeBase currentTheme; + + @override + Widget build(BuildContext context) { + final copyImage = Image.asset( + 'assets/images/copy_content.png', + height: 16, + width: 16, + color: Theme.of(context).extension()!.estimatedFeeColor, + ); + + return Container( + width: double.infinity, + color: Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 10), + ...exchangeTradeViewModel.items + .where((item) => item.isReceiveDetail == isReceiveDetailsCard) + .map( + (item) => TradeItemRowWidget( + currentTheme: currentTheme, + title: item.title, + value: item.data, + isCopied: item.isCopied, + copyImage: copyImage, + ), + ) + .toList(), + if (!isReceiveDetailsCard && exchangeTradeViewModel.isSendable) ...[ + if (feesViewModel.hasFees) + FeeSelectionWidget( + feesViewModel: feesViewModel, + output: output, + onTap: () => pickTransactionPriority(context), + ), + if (exchangeTradeViewModel.sendViewModel.hasCoinControl) + CoinControlWidget( + onTap: () => Navigator.of(context).pushNamed( + Routes.unspentCoinsList, + arguments: exchangeTradeViewModel.sendViewModel.coinTypeToSpendFrom, + ), + ), + ], + ], + ), + ); + } + + Future pickTransactionPriority(BuildContext context) async { + final items = priorityForWalletType(feesViewModel.walletType); + final selectedItem = items.indexOf(feesViewModel.transactionPriority); + final customItemIndex = feesViewModel.getCustomPriorityIndex(items); + final isBitcoinWallet = feesViewModel.walletType == WalletType.bitcoin; + final maxCustomFeeRate = feesViewModel.maxCustomFeeRate?.toDouble(); + double? customFeeRate = isBitcoinWallet ? feesViewModel.customBitcoinFeeRate.toDouble() : null; + + await showPopUp( + context: context, + builder: (BuildContext context) { + int selectedIdx = selectedItem; + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Picker( + items: items, + displayItem: (TransactionPriority priority) => + feesViewModel.displayFeeRate(priority, customFeeRate?.round()), + selectedAtIndex: selectedIdx, + customItemIndex: customItemIndex, + maxValue: maxCustomFeeRate, + title: S.of(context).please_select, + headerEnabled: !isBitcoinWallet, + closeOnItemSelected: !isBitcoinWallet, + mainAxisAlignment: MainAxisAlignment.center, + sliderValue: customFeeRate, + onSliderChanged: (double newValue) => setState(() => customFeeRate = newValue), + onItemSelected: (TransactionPriority priority) { + feesViewModel.setTransactionPriority(priority); + setState(() => selectedIdx = items.indexOf(priority)); + + if (feesViewModel.isLowFee) { + _showFeeAlert(context); + } + }, + ); + }, + ); + }, + ); + if (isBitcoinWallet) { + feesViewModel.customBitcoinFeeRate = customFeeRate!.round(); + if (feesViewModel.showAlertForCustomFeeRate()) { + _showFeeAlert(context); + } + } + } + + void _showFeeAlert(BuildContext context) async { + final confirmed = await showPopUp( + context: context, + builder: (dialogContext) { + return AlertWithTwoActions( + alertTitle: S.of(context).low_fee, + alertContent: S.of(context).low_fee_alert, + leftButtonText: S.of(context).ignor, + rightButtonText: S.of(context).use_suggested, + actionLeftButton: () => Navigator.of(dialogContext).pop(false), + actionRightButton: () => Navigator.of(dialogContext).pop(true)); + }) ?? + false; + if (confirmed) { + feesViewModel.setDefaultTransactionPriority(); + } + } +} + +class TradeItemRowWidget extends StatelessWidget { + final String title; + final String value; + final bool isCopied; + final Image copyImage; + final ThemeBase currentTheme; + + const TradeItemRowWidget({ + required this.title, + required this.value, + required this.isCopied, + required this.copyImage, + required this.currentTheme, + }); + + @override + Widget build(BuildContext context) { + + final hintTextColor = currentTheme == ThemeType.bright + ? Theme.of(context).extension()!.detailsTitlesColor + : Colors.white.withAlpha(175); + + final mainTextColor = currentTheme == ThemeType.bright + ? Theme.of(context).extension()!.titleColor + : Colors.white; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: GestureDetector( + onTap: () { + if (!isCopied) return; + Clipboard.setData(ClipboardData(text: value)); + showBar(context, S.of(context).transaction_details_copied(title)); + }, + child: ListRow( + padding: EdgeInsets.zero, + title: title, + value: value, + image: isCopied ? copyImage : null, + color: Colors.transparent, + hintTextColor: hintTextColor, + mainTextColor: mainTextColor, + ), + ), + ); + } +} + +class FeeSelectionWidget extends StatelessWidget { + final FeesViewModel feesViewModel; + final Output output; + final VoidCallback onTap; + + const FeeSelectionWidget({ + required this.feesViewModel, + required this.output, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Observer( + builder: (_) => GestureDetector( + key: ValueKey('exchange_trade_card_item_widget_select_fee_priority_button_key'), + onTap: feesViewModel.hasFeesPriority ? onTap : () {}, + child: Container( + padding: EdgeInsets.only(top: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context).send_estimated_fee, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '${output.estimatedFee} ${feesViewModel.currency}', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + if (!feesViewModel.isFiatDisabled) + Padding( + padding: EdgeInsets.only(top: 5), + child: Text( + '${output.estimatedFeeFiatAmount} ${feesViewModel.fiat.title}', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: + Theme.of(context).extension()!.textFieldHintColor, + ), + ), + ), + ], + ), + Padding( + padding: EdgeInsets.only(top: 2, left: 5), + child: Icon( + Icons.arrow_forward_ios, + size: 12, + color: Colors.white, + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} + +class CoinControlWidget extends StatelessWidget { + final VoidCallback onTap; + + const CoinControlWidget({required this.onTap}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + key: ValueKey('exchange_trade_card_item_widget_unspent_coin_button_key'), + onTap: onTap, + child: Container( + color: Colors.transparent, + padding: EdgeInsets.only(top: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + S.of(context).coin_control, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + Icon( + Icons.arrow_forward_ios, + size: 12, + color: Colors.white, + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index f1cac5c9f..7b1ae9098 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -308,11 +308,11 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin()!.textFieldHintColor), ), ), - if (sendViewModel.hasFees) + if (sendViewModel.feesViewModel.hasFees) Observer( builder: (_) => GestureDetector( key: ValueKey('send_page_select_fee_priority_button_key'), - onTap: sendViewModel.hasFeesPriority + onTap: sendViewModel.feesViewModel.hasFeesPriority ? () => pickTransactionPriority(context) : () {}, child: Container( @@ -570,11 +570,11 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin pickTransactionPriority(BuildContext context) async { final items = priorityForWalletType(sendViewModel.walletType); - final selectedItem = items.indexOf(sendViewModel.transactionPriority); - final customItemIndex = sendViewModel.getCustomPriorityIndex(items); + final selectedItem = items.indexOf(sendViewModel.feesViewModel.transactionPriority); + final customItemIndex = sendViewModel.feesViewModel.getCustomPriorityIndex(items); final isBitcoinWallet = sendViewModel.walletType == WalletType.bitcoin; - final maxCustomFeeRate = sendViewModel.maxCustomFeeRate?.toDouble(); - double? customFeeRate = isBitcoinWallet ? sendViewModel.customBitcoinFeeRate.toDouble() : null; + final maxCustomFeeRate = sendViewModel.feesViewModel.maxCustomFeeRate?.toDouble(); + double? customFeeRate = isBitcoinWallet ? sendViewModel.feesViewModel.customBitcoinFeeRate.toDouble() : null; await showPopUp( context: context, @@ -585,7 +585,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin - sendViewModel.displayFeeRate(priority, customFeeRate?.round()), + sendViewModel.feesViewModel.displayFeeRate(priority, customFeeRate?.round()), selectedAtIndex: selectedIdx, customItemIndex: customItemIndex, maxValue: maxCustomFeeRate, @@ -596,7 +596,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin setState(() => customFeeRate = newValue), onItemSelected: (TransactionPriority priority) { - sendViewModel.setTransactionPriority(priority); + sendViewModel.feesViewModel.setTransactionPriority(priority); setState(() => selectedIdx = items.indexOf(priority)); }, ); @@ -604,7 +604,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin[ @@ -32,7 +39,7 @@ class ListRow extends StatelessWidget { style: TextStyle( fontSize: titleFontSize, fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.detailsTitlesColor), + color: hintTextColor ?? Theme.of(context).extension()!.detailsTitlesColor), textAlign: TextAlign.left), Padding( padding: const EdgeInsets.only(top: 12), @@ -46,7 +53,7 @@ class ListRow extends StatelessWidget { style: TextStyle( fontSize: valueFontSize, fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.titleColor)), + color: mainTextColor ?? Theme.of(context).extension()!.titleColor)), ), image != null ? Padding( diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index d488447e2..fc7a5429b 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -16,6 +16,8 @@ import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_item.dart'; import 'package:cake_wallet/store/dashboard/trades_store.dart'; +import 'package:cake_wallet/view_model/send/fees_view_model.dart'; +import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -28,14 +30,16 @@ part 'exchange_trade_view_model.g.dart'; class ExchangeTradeViewModel = ExchangeTradeViewModelBase with _$ExchangeTradeViewModel; abstract class ExchangeTradeViewModelBase with Store { - ExchangeTradeViewModelBase( - {required this.wallet, - required this.trades, - required this.tradesStore, - required this.sendViewModel}) - : trade = tradesStore.trade!, + ExchangeTradeViewModelBase({ + required this.wallet, + required this.trades, + required this.tradesStore, + required this.sendViewModel, + required this.feesViewModel, + }) : trade = tradesStore.trade!, isSendable = _checkIfCanSend(tradesStore, wallet), items = ObservableList() { + setUpOutput(); switch (trade.provider) { case ExchangeProviderDescription.changeNow: _provider = @@ -81,6 +85,9 @@ abstract class ExchangeTradeViewModelBase with Store { final Box trades; final TradesStore tradesStore; final SendViewModel sendViewModel; + final FeesViewModel feesViewModel; + + late Output output; @observable Trade trade; @@ -112,16 +119,18 @@ abstract class ExchangeTradeViewModelBase with Store { Timer? timer; - @action - Future confirmSending() async { - if (!isSendable) return; - + void setUpOutput() { sendViewModel.clearOutputs(); - final output = sendViewModel.outputs.first; + output = sendViewModel.outputs.first; output.address = trade.inputAddress ?? ''; output.setCryptoAmount(trade.amount); if (_provider is ThorChainExchangeProvider) output.memo = trade.memo; if (trade.isSendAll == true) output.sendAll = true; + } + + @action + Future confirmSending() async { + if (!isSendable) return; sendViewModel.selectedCryptoCurrency = trade.from; final pendingTransaction = await sendViewModel.createTransaction(provider: _provider); if (_provider is ThorChainExchangeProvider) { @@ -154,7 +163,7 @@ abstract class ExchangeTradeViewModelBase with Store { void _updateItems() { final tagFrom = - tradesStore.trade!.from.tag != null ? '${tradesStore.trade!.from.tag}' + ' ' : ''; + tradesStore.trade!.from.tag != null ? '${tradesStore.trade!.from.tag}' + ' ' : ''; final tagTo = tradesStore.trade!.to.tag != null ? '${tradesStore.trade!.to.tag}' + ' ' : ''; items.clear(); @@ -164,6 +173,8 @@ abstract class ExchangeTradeViewModelBase with Store { title: "${trade.provider.title} ${S.current.id}", data: '${trade.id}', isCopied: true, + isReceiveDetail: true, + isExternalSendDetail: false, ), ); @@ -171,28 +182,51 @@ abstract class ExchangeTradeViewModelBase with Store { ExchangeTradeItem( title: S.current.amount, data: '${trade.amount} ${trade.from}', - isCopied: true, + isCopied: false, + isReceiveDetail: false, + isExternalSendDetail: true, ), ExchangeTradeItem( - title: S.current.estimated_receive_amount + ':', + title: S.current.you_will_receive_estimated_amount + ':', data: '${tradesStore.trade?.receiveAmount} ${trade.to}', isCopied: true, + isReceiveDetail: true, + isExternalSendDetail: false, ), ExchangeTradeItem( title: S.current.send_to_this_address('${tradesStore.trade!.from}', tagFrom) + ':', data: trade.inputAddress ?? '', - isCopied: true, + isCopied: false, + isReceiveDetail: false, + isExternalSendDetail: true, ), ]); if (trade.extraId != null) { - final title = trade.from == CryptoCurrency.xrp - ? S.current.destination_tag - : trade.from == CryptoCurrency.xlm - ? S.current.memo - : S.current.extra_id; + final shouldAddExtraId = trade.from == CryptoCurrency.xrp || trade.from == CryptoCurrency.xlm; - items.add(ExchangeTradeItem(title: title, data: '${trade.extraId}', isCopied: true)); + if (shouldAddExtraId) { + final title = trade.from == CryptoCurrency.xrp + ? S.current.destination_tag + : trade.from == CryptoCurrency.xlm + ? S.current.memo + : S.current.extra_id; + + items.add( + ExchangeTradeItem( + title: title, + data: '${trade.extraId}', + isCopied: true, + isReceiveDetail: (trade.from == CryptoCurrency.xrp || trade.from == CryptoCurrency.xlm) + ? false + : true, + isExternalSendDetail: + (trade.from == CryptoCurrency.xrp || trade.from == CryptoCurrency.xlm) + ? true + : false, + ), + ); + } } items.add( @@ -200,6 +234,8 @@ abstract class ExchangeTradeViewModelBase with Store { title: S.current.arrive_in_this_address('${tradesStore.trade!.to}', tagTo) + ':', data: trade.payoutAddress ?? '', isCopied: true, + isReceiveDetail: true, + isExternalSendDetail: false, ), ); } diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index d3871ffbd..6ea9c811f 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/core/create_trade_result.dart'; import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart'; +import 'package:cake_wallet/view_model/send/fees_view_model.dart'; import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/sync_status.dart'; @@ -22,12 +23,10 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/wallet_contact.dart'; -import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/exchange/exchange_template.dart'; import 'package:cake_wallet/exchange/exchange_trade_state.dart'; @@ -44,8 +43,6 @@ import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/trades_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -72,6 +69,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with this._settingsStore, this.sharedPreferences, this.contactListViewModel, + this.feesViewModel, ) : _cryptoNumberFormat = NumberFormat(), isSendAllEnabled = false, isFixedRateMode = false, @@ -169,8 +167,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with wallet.type == WalletType.bitcoinCash; bool get hideAddressAfterExchange => - wallet.type == WalletType.monero || - wallet.type == WalletType.wownero; + wallet.type == WalletType.monero || wallet.type == WalletType.wownero; bool _useTorOnly; final Box trades; @@ -319,27 +316,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with bool get isMoneroWallet => wallet.type == WalletType.monero; - bool get isLowFee { - switch (wallet.type) { - case WalletType.monero: - case WalletType.wownero: - case WalletType.haven: - case WalletType.zano: - return transactionPriority == monero!.getMoneroTransactionPrioritySlow(); - case WalletType.bitcoin: - return transactionPriority == bitcoin!.getBitcoinTransactionPrioritySlow(); - case WalletType.litecoin: - return transactionPriority == bitcoin!.getLitecoinTransactionPrioritySlow(); - case WalletType.ethereum: - return transactionPriority == ethereum!.getEthereumTransactionPrioritySlow(); - case WalletType.bitcoinCash: - return transactionPriority == bitcoinCash!.getBitcoinCashTransactionPrioritySlow(); - case WalletType.polygon: - return transactionPriority == polygon!.getPolygonTransactionPrioritySlow(); - default: - return false; - } - } + List receiveCurrencies; @@ -351,6 +328,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with final ContactListViewModel contactListViewModel; + final FeesViewModel feesViewModel; + @observable double bestRate = 0.0; @@ -434,6 +413,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with return true; } + Future calculateBestRate() async { if (depositCurrency == receiveCurrency) { bestRate = 0.0; @@ -876,35 +856,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with selectedProviders.where((provider) => providersForCurrentPair().contains(provider))); } - @action - void setDefaultTransactionPriority() { - switch (wallet.type) { - case WalletType.monero: - case WalletType.haven: - case WalletType.wownero: - case WalletType.zano: - _settingsStore.priority[wallet.type] = monero!.getMoneroTransactionPriorityAutomatic(); - break; - case WalletType.bitcoin: - _settingsStore.priority[wallet.type] = bitcoin!.getBitcoinTransactionPriorityMedium(); - break; - case WalletType.litecoin: - _settingsStore.priority[wallet.type] = bitcoin!.getLitecoinTransactionPriorityMedium(); - break; - case WalletType.ethereum: - _settingsStore.priority[wallet.type] = ethereum!.getDefaultTransactionPriority(); - break; - case WalletType.bitcoinCash: - _settingsStore.priority[wallet.type] = bitcoinCash!.getDefaultTransactionPriority(); - break; - case WalletType.polygon: - _settingsStore.priority[wallet.type] = polygon!.getDefaultTransactionPriority(); - break; - default: - break; - } - } - void _setProviders() { if (_settingsStore.exchangeStatus == ExchangeApiMode.torOnly) providerList = _allProviders.where((provider) => provider.supportsOnionAddress).toList(); diff --git a/lib/view_model/send/fees_view_model.dart b/lib/view_model/send/fees_view_model.dart new file mode 100644 index 000000000..088c66039 --- /dev/null +++ b/lib/view_model/send/fees_view_model.dart @@ -0,0 +1,191 @@ +import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; +import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; +import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:collection/collection.dart'; + +part 'fees_view_model.g.dart'; + +class FeesViewModel = FeesViewModelBase with _$FeesViewModel; + +abstract class FeesViewModelBase extends WalletChangeListenerViewModel with Store { + FeesViewModelBase( + AppStore appStore, + this.balanceViewModel, + ) : _settingsStore = appStore.settingsStore, + super(appStore: appStore) { + if (wallet.type == WalletType.bitcoin && + _settingsStore.priority[wallet.type] == bitcoinTransactionPriorityCustom) { + setTransactionPriority(bitcoinTransactionPriorityMedium); + } + final priority = _settingsStore.priority[wallet.type]; + final priorities = priorityForWalletType(wallet.type); + if (!priorityForWalletType(wallet.type).contains(priority) && priorities.isNotEmpty) { + _settingsStore.priority[wallet.type] = priorities.first; + } + } + + @computed + WalletType get walletType => wallet.type; + CryptoCurrency get currency => wallet.currency; + FiatCurrency get fiat => _settingsStore.fiatCurrency; + bool get isFiatDisabled => balanceViewModel.isFiatDisabled; + + final BalanceViewModel balanceViewModel; + + TransactionPriority get transactionPriority { + final priority = _settingsStore.priority[wallet.type]; + + if (priority == null) { + throw Exception('Unexpected type ${wallet.type}'); + } + + return priority; + } + + int? getCustomPriorityIndex(List priorities) { + if (wallet.type == WalletType.bitcoin) { + final customItem = priorities + .firstWhereOrNull((element) => element == bitcoin!.getBitcoinTransactionPriorityCustom()); + + return customItem != null ? priorities.indexOf(customItem) : null; + } + return null; + } + + int? get maxCustomFeeRate { + if (wallet.type == WalletType.bitcoin) { + return bitcoin!.getMaxCustomFeeRate(wallet); + } + return null; + } + + bool get isLowFee { + switch (wallet.type) { + case WalletType.monero: + case WalletType.wownero: + case WalletType.haven: + case WalletType.zano: + return transactionPriority == monero!.getMoneroTransactionPrioritySlow(); + case WalletType.bitcoin: + return transactionPriority == bitcoin!.getBitcoinTransactionPrioritySlow(); + case WalletType.litecoin: + return transactionPriority == bitcoin!.getLitecoinTransactionPrioritySlow(); + case WalletType.ethereum: + return transactionPriority == ethereum!.getEthereumTransactionPrioritySlow(); + case WalletType.bitcoinCash: + return transactionPriority == bitcoinCash!.getBitcoinCashTransactionPrioritySlow(); + case WalletType.polygon: + return transactionPriority == polygon!.getPolygonTransactionPrioritySlow(); + default: + return false; + } + } + + @computed + int get customBitcoinFeeRate => _settingsStore.customBitcoinFeeRate; + + void set customBitcoinFeeRate(int value) => _settingsStore.customBitcoinFeeRate = value; + + @computed + bool get hasFees => wallet.type != WalletType.nano && wallet.type != WalletType.banano; + + @computed + bool get hasFeesPriority => + wallet.type != WalletType.nano && + wallet.type != WalletType.banano && + wallet.type != WalletType.solana && + wallet.type != WalletType.tron; + + @computed + bool get isElectrumWallet => + wallet.type == WalletType.bitcoin || + wallet.type == WalletType.litecoin || + wallet.type == WalletType.bitcoinCash; + + String? get walletCurrencyName => wallet.currency.fullName?.toLowerCase() ?? wallet.currency.name; + + @computed + FiatCurrency get fiatCurrency => _settingsStore.fiatCurrency; + + final SettingsStore _settingsStore; + + @action + void setTransactionPriority(TransactionPriority priority) => + _settingsStore.priority[wallet.type] = priority; + + bool showAlertForCustomFeeRate() { + if (wallet.type != WalletType.bitcoin || isLowFee) { + return false; + } + + if (transactionPriority != bitcoinTransactionPriorityCustom) { + return false; + } + + final mediumRate = bitcoin!.getFeeRate(wallet, bitcoinTransactionPriorityMedium); + return customBitcoinFeeRate < mediumRate; + } + + String displayFeeRate(dynamic priority, int? customValue) { + final _priority = priority as TransactionPriority; + + if (wallet.type == WalletType.bitcoin) { + final rate = bitcoin!.getFeeRate(wallet, _priority); + return bitcoin!.bitcoinTransactionPriorityWithLabel(_priority, rate, customRate: customValue); + } + + if (isElectrumWallet) { + final rate = bitcoin!.getFeeRate(wallet, _priority); + return bitcoin!.bitcoinTransactionPriorityWithLabel(_priority, rate); + } + + return priority.toString(); + } + + TransactionPriority get bitcoinTransactionPriorityCustom => + bitcoin!.getBitcoinTransactionPriorityCustom(); + + TransactionPriority get bitcoinTransactionPriorityMedium => + bitcoin!.getBitcoinTransactionPriorityMedium(); + + @action + void setDefaultTransactionPriority() { + switch (wallet.type) { + case WalletType.monero: + case WalletType.haven: + case WalletType.wownero: + case WalletType.zano: + _settingsStore.priority[wallet.type] = monero!.getMoneroTransactionPriorityAutomatic(); + break; + case WalletType.bitcoin: + _settingsStore.priority[wallet.type] = bitcoin!.getBitcoinTransactionPriorityMedium(); + break; + case WalletType.litecoin: + _settingsStore.priority[wallet.type] = bitcoin!.getLitecoinTransactionPriorityMedium(); + break; + case WalletType.ethereum: + _settingsStore.priority[wallet.type] = ethereum!.getDefaultTransactionPriority(); + break; + case WalletType.bitcoinCash: + _settingsStore.priority[wallet.type] = bitcoinCash!.getDefaultTransactionPriority(); + break; + case WalletType.polygon: + _settingsStore.priority[wallet.type] = polygon!.getDefaultTransactionPriority(); + break; + default: + break; + } + } +} diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 3dce212af..1cd6b8966 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -18,6 +18,7 @@ import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; +import 'package:cake_wallet/view_model/send/fees_view_model.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; @@ -76,7 +77,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor this.contactListViewModel, this.transactionDescriptionBox, this.ledgerViewModel, - this.unspentCoinsListViewModel, { + this.unspentCoinsListViewModel, + this.feesViewModel, { this.coinTypeToSpendFrom = UnspentCoinType.nonMweb, }) : state = InitialExecutionState(), currencies = appStore.wallet!.balance.keys.toList(), @@ -89,16 +91,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor _settingsStore = appStore.settingsStore, fiatFromSettings = appStore.settingsStore.fiatCurrency, super(appStore: appStore) { - if (wallet.type == WalletType.bitcoin && - _settingsStore.priority[wallet.type] == bitcoinTransactionPriorityCustom) { - setTransactionPriority(bitcoinTransactionPriorityMedium); - } - final priority = _settingsStore.priority[wallet.type]; - final priorities = priorityForWalletType(wallet.type); - if (!priorityForWalletType(wallet.type).contains(priority) && priorities.isNotEmpty) { - _settingsStore.priority[wallet.type] = priorities.first; - } - outputs .add(Output(wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency)); @@ -205,38 +197,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor FiatCurrency get fiat => _settingsStore.fiatCurrency; - TransactionPriority get transactionPriority { - final priority = _settingsStore.priority[wallet.type]; - - if (priority == null) { - throw Exception('Unexpected type ${wallet.type}'); - } - - return priority; - } - - int? getCustomPriorityIndex(List priorities) { - if (wallet.type == WalletType.bitcoin) { - final customItem = priorities - .firstWhereOrNull((element) => element == bitcoin!.getBitcoinTransactionPriorityCustom()); - - return customItem != null ? priorities.indexOf(customItem) : null; - } - return null; - } - - int? get maxCustomFeeRate { - if (wallet.type == WalletType.bitcoin) { - return bitcoin!.getMaxCustomFeeRate(wallet); - } - return null; - } - - @computed - int get customBitcoinFeeRate => _settingsStore.customBitcoinFeeRate; - - void set customBitcoinFeeRate(int value) => _settingsStore.customBitcoinFeeRate = value; - CryptoCurrency get currency => wallet.currency; Validator get amountValidator => @@ -299,16 +259,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor wallet.type == WalletType.litecoin || wallet.type == WalletType.bitcoinCash; - @computed - bool get hasFees => wallet.type != WalletType.nano && wallet.type != WalletType.banano; - - @computed - bool get hasFeesPriority => - wallet.type != WalletType.nano && - wallet.type != WalletType.banano && - wallet.type != WalletType.solana && - wallet.type != WalletType.tron; - @observable CryptoCurrency selectedCryptoCurrency; @@ -331,6 +281,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor final BalanceViewModel balanceViewModel; final ContactListViewModel contactListViewModel; final LedgerViewModel? ledgerViewModel; + final FeesViewModel feesViewModel; final FiatConversionStore _fiatConversationStore; final Box transactionDescriptionBox; @@ -531,10 +482,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor } } - @action - void setTransactionPriority(TransactionPriority priority) => - _settingsStore.priority[wallet.type] = priority; - Object _credentials([ExchangeProvider? provider]) { final priority = _settingsStore.priority[wallet.type]; @@ -552,14 +499,14 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor return bitcoin!.createBitcoinTransactionCredentials( outputs, priority: priority!, - feeRate: customBitcoinFeeRate, + feeRate:feesViewModel. customBitcoinFeeRate, coinTypeToSpendFrom: coinTypeToSpendFrom, ); case WalletType.litecoin: return bitcoin!.createBitcoinTransactionCredentials( outputs, priority: priority!, - feeRate: customBitcoinFeeRate, + feeRate:feesViewModel. customBitcoinFeeRate, // if it's an exchange flow then disable sending from mweb coins coinTypeToSpendFrom: provider != null ? UnspentCoinType.nonMweb : coinTypeToSpendFrom, ); @@ -597,32 +544,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor } } - String displayFeeRate(dynamic priority, int? customValue) { - final _priority = priority as TransactionPriority; - - if (walletType == WalletType.bitcoin) { - final rate = bitcoin!.getFeeRate(wallet, _priority); - return bitcoin!.bitcoinTransactionPriorityWithLabel(_priority, rate, customRate: customValue); - } - - if (isElectrumWallet) { - final rate = bitcoin!.getFeeRate(wallet, _priority); - return bitcoin!.bitcoinTransactionPriorityWithLabel(_priority, rate); - } - - return priority.toString(); - } - bool _isEqualCurrency(String currency) => wallet.balance.keys.any((e) => currency.toLowerCase() == e.title.toLowerCase()); - TransactionPriority get bitcoinTransactionPriorityCustom => - bitcoin!.getBitcoinTransactionPriorityCustom(); - - TransactionPriority get bitcoinTransactionPriorityMedium => - bitcoin!.getBitcoinTransactionPriorityMedium(); - - @action void onClose() => _settingsStore.fiatCurrency = fiatFromSettings; @action diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index e93c6f8ff..2dc6478f9 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -576,11 +576,11 @@ abstract class TransactionDetailsViewModelBase with Store { } final priorities = priorityForWalletType(wallet.type); - final selectedItem = priorities.indexOf(sendViewModel.transactionPriority); - final customItem = priorities - .firstWhereOrNull((element) => element == sendViewModel.bitcoinTransactionPriorityCustom); + final selectedItem = priorities.indexOf(sendViewModel.feesViewModel.transactionPriority); + final customItem = priorities.firstWhereOrNull( + (element) => element == sendViewModel.feesViewModel.bitcoinTransactionPriorityCustom); final customItemIndex = customItem != null ? priorities.indexOf(customItem) : null; - final maxCustomFeeRate = sendViewModel.maxCustomFeeRate?.toDouble(); + final maxCustomFeeRate = sendViewModel.feesViewModel.maxCustomFeeRate?.toDouble(); RBFListItems.add( StandardPickerListItem( @@ -594,7 +594,7 @@ abstract class TransactionDetailsViewModelBase with Store { selectedIdx: selectedItem, customItemIndex: customItemIndex ?? 0, displayItem: (dynamic priority, double sliderValue) => - sendViewModel.displayFeeRate(priority, sliderValue.round()), + sendViewModel.feesViewModel.displayFeeRate(priority, sliderValue.round()), onSliderChanged: (double newValue) => setNewFee(value: newValue, priority: transactionPriority!), onItemSelected: (dynamic item, double sliderValue) { @@ -788,7 +788,8 @@ abstract class TransactionDetailsViewModelBase with Store { final comment = tx.additionalInfo['comment'] as String?; items.addAll([ StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id), - StandartListItem(title: 'Asset ID', value: tx.additionalInfo['assetId'] as String? ?? "Unknown asset id"), + StandartListItem( + title: 'Asset ID', value: tx.additionalInfo['assetId'] as String? ?? "Unknown asset id"), StandartListItem( title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'), @@ -798,5 +799,5 @@ abstract class TransactionDetailsViewModelBase with Store { if (comment != null && comment.isNotEmpty) StandartListItem(title: S.current.transaction_details_title, value: comment), ]); - } + } } diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index ec09b2b3d..9f1235919 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "* يرجى نسخ أو كتابة معرف هويتك الأعلى لحفظة.", "exchange_result_write_down_trade_id": "يرجى نسخ أو كتابة معرّف العملية للمتابعة.", "exchange_sync_alert_content": "يرجى الانتظار حتى تتم مزامنة محفظتك", + "exchange_trade_result_confirm": "بالضغط على إرسال من Cake Wallet ، ستقوم بإرسال ${fetchingLabel} ${from} من محفظتك التي تسمى ${walletName} إلى العنوان الموضح أدناه. أو يمكنك إرسالها من محفظتك الخارجية إلى العنوان / QR رمز الإرسال من صفحة تفاصيل المحفظة الخارجية. \n\n يرجى الضغط على أي من الأزرار للمتابعة أو العودة لتغيير المبالغ.", "expired": "منتهي الصلاحية", "expires": "تنتهي", "expiresOn": "ﻲﻓ ﻪﺘﻴﺣﻼﺻ ﻲﻬﺘﻨﺗ", @@ -694,6 +695,8 @@ "send_error_minimum_value": "الحد الأدنى لقيمة المبلغ هو 0.01", "send_estimated_fee": "الرسوم المقدرة:", "send_fee": "الرسوم:", + "send_from_cake_wallet": "أرسل من محفظة الكيك", + "send_from_external_wallet": "أرسل من محفظة خارجية", "send_name": "الأسم", "send_new": "جديد", "send_payment_id": "معرف عملية الدفع (اختياري)", @@ -1008,6 +1011,7 @@ "you_now_have_debit_card": "لديك الآن بطاقة ائتمان", "you_pay": "انت تدفع", "you_will_get": "حول الى", + "you_will_receive_estimated_amount": "سوف تتلقى(ooded )", "you_will_send": "تحويل من", "yy": "YY" } \ No newline at end of file diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 31fd57c27..190dd07d8 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "*Please copy or write down your ID shown above.", "exchange_result_write_down_trade_id": "Моля, запишете trade ID-то, за да продължите.", "exchange_sync_alert_content": "Моля, изчакайте синхронизирането на Вашия портфейл", + "exchange_trade_result_confirm": "Натискайки изпращане от портфейла за торта, ще изпращате ${fetchingLabel} ${from} от вашия портфейл, наречен ${walletName} на адреса, показан по -долу. Или можете да изпратите от външния си портфейл до адреса / QR код на страницата за изпращане от външния портфейл.", "expired": "Изтекло", "expires": "Изтича", "expiresOn": "Изтича на", @@ -694,6 +695,8 @@ "send_error_minimum_value": "Минималната сума е 0.01", "send_estimated_fee": "Изчислена такса:", "send_fee": "Такса:", + "send_from_cake_wallet": "Изпратете от портфейла за торта", + "send_from_external_wallet": "Изпратете от външен портфейл", "send_name": "Име", "send_new": "Ново", "send_payment_id": "Payment ID (не е задължително)", @@ -1008,6 +1011,7 @@ "you_now_have_debit_card": "Вече имате дебитна карта", "you_pay": "Вие плащате", "you_will_get": "Обръщане в", + "you_will_receive_estimated_amount": "Ще получите(прогнозно )", "you_will_send": "Обръщане от", "yy": "гг" } \ No newline at end of file diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index fd66fdcae..923db16fe 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "*Prosím zkopírujte si, nebo zapište si výše uvedené ID.", "exchange_result_write_down_trade_id": "Prosím zkopírujte si, nebo zapište si ID transakce (trade ID) pro pokračování.", "exchange_sync_alert_content": "Prosím počkejte, dokud nebude vaše peněženka synchronizována", + "exchange_trade_result_confirm": "Stisknutím odeslání z peněženky Cake Wallet posíláte ${fetchingLabel} ${from} z peněženky nazvané ${walletName} na níže uvedenou adresu. Nebo můžete odeslat z externí peněženky na adresu / qr kód na stránce Odeslat ze stránky externích detailů peněženky. \n\n Stisknutím některého z tlačítek pokračujte nebo se vrátíte a změňte částky.", "expired": "Vypršelo", "expires": "Vyprší", "expiresOn": "Vyprší dne", @@ -694,6 +695,8 @@ "send_error_minimum_value": "Minimální částka je 0,01", "send_estimated_fee": "Odhadovaný poplatek:", "send_fee": "Poplatek:", + "send_from_cake_wallet": "Odeslat z peněženky", + "send_from_external_wallet": "Odeslat z externí peněženky", "send_name": "Název", "send_new": "Nová", "send_payment_id": "ID platby (nepovinné)", @@ -1008,6 +1011,7 @@ "you_now_have_debit_card": "Nyní máte debetní kartu", "you_pay": "Zaplatíte", "you_will_get": "Směnit na", + "you_will_receive_estimated_amount": "Obdržíte(odhadovaný )", "you_will_send": "Směnit z", "yy": "YY" } \ No newline at end of file diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 36f5ecfa8..39e3113c1 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "*Bitte kopieren oder notieren Sie sich die oben gezeigte ID.", "exchange_result_write_down_trade_id": "Bitte kopieren oder notieren Sie die Handels-ID, um fortzufahren.", "exchange_sync_alert_content": "Bitte warten Sie, bis Ihre Wallet synchronisiert ist", + "exchange_trade_result_confirm": "Durch Drücken von Send von Cake Wallet senden Sie ${fetchingLabel} ${from} von Ihrer Brieftasche mit dem Namen ${walletName} an die unten gezeigte Adresse. Oder Sie können von Ihrem externen Brieftaschen an den Adresse \n\nqR auf der Seite \"Senden von externen Brieftaschen\" senden.", "expired": "Abgelaufen", "expires": "Läuft ab", "expiresOn": "Läuft aus am", @@ -695,6 +696,8 @@ "send_error_minimum_value": "Der Mindestbetrag ist 0,01", "send_estimated_fee": "Geschätzte Gebühr:", "send_fee": "Gebühr:", + "send_from_cake_wallet": "Senden Sie aus Kuchenbrieftasche", + "send_from_external_wallet": "Senden Sie aus der Außenschreibe", "send_name": "Name", "send_new": "Neu", "send_payment_id": "Zahlungs-ID (optional)", @@ -1011,6 +1014,7 @@ "you_now_have_debit_card": "Sie haben jetzt eine Debitkarte", "you_pay": "Sie bezahlen", "you_will_get": "Konvertieren zu", + "you_will_receive_estimated_amount": "Sie erhalten(geschätzt )", "you_will_send": "Konvertieren von", "yy": "YY" } \ No newline at end of file diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 1278aaf04..921375c3c 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "*Please copy or write down your ID shown above.", "exchange_result_write_down_trade_id": "Please copy or write down the trade ID to continue.", "exchange_sync_alert_content": "Please wait until your wallet is synchronized", + "exchange_trade_result_confirm": "By pressing send from cake wallet, you will be sending ${fetchingLabel} ${from} from your wallet called ${walletName} to the address shown below. Or you can send from your external wallet to the address/QR code on the send from external wallet details page.\n\nPlease press either of the buttons to continue or go back to change the amounts.", "expired": "Expired", "expires": "Expires", "expiresOn": "Expires on", @@ -695,6 +696,8 @@ "send_error_minimum_value": "Minimum value of amount is 0.01", "send_estimated_fee": "Estimated fee:", "send_fee": "Fee:", + "send_from_cake_wallet": "Send from Cake Wallet", + "send_from_external_wallet": "Send from External Wallet", "send_name": "Name", "send_new": "New", "send_payment_id": "Payment ID (optional)", @@ -1009,6 +1012,7 @@ "you_now_have_debit_card": "You now have a debit card", "you_pay": "You Pay", "you_will_get": "Convert to", + "you_will_receive_estimated_amount": "You will receive (estimated)", "you_will_send": "Convert from", "yy": "YY" } \ No newline at end of file diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 1849a2f60..ad96c2a93 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "*Copie o escriba su identificación que se muestra arriba.", "exchange_result_write_down_trade_id": "Por favor, copia o escribe el ID.", "exchange_sync_alert_content": "Espere hasta que su billetera esté sincronizada", + "exchange_trade_result_confirm": "Al presionar el envío de la billetera de la torta, enviará ${fetchingLabel} ${from} desde su billetera llamada ${walletName} a la dirección que se muestra a continuación. O puede enviar desde su billetera externa a la dirección / Código QR en la página Enviar desde la página Detalles de la billetera externa. \n\n Por favor presione cualquiera de los botones para continuar o regrese para cambiar las cantidades.", "expired": "Muerto", "expires": "Caduca", "expiresOn": "Expira el", @@ -695,6 +696,8 @@ "send_error_minimum_value": "El valor mínimo de la cantidad es 0.01", "send_estimated_fee": "Tarifa estimada:", "send_fee": "Cuota:", + "send_from_cake_wallet": "Enviar desde la billetera de pastel", + "send_from_external_wallet": "Enviar desde la billetera externa", "send_name": "Nombre", "send_new": "Nuevo", "send_payment_id": "ID de pago (opcional)", @@ -1009,6 +1012,7 @@ "you_now_have_debit_card": "Ahora tienes una tarjeta de débito", "you_pay": "Tú pagas", "you_will_get": "Convertir a", + "you_will_receive_estimated_amount": "Recibirá(estimado )", "you_will_send": "Convertir de", "yy": "YY" } \ No newline at end of file diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index bcd513f67..ccd12b1c6 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "*Merci de copier ou écrire votre ID présenté ci-dessus.", "exchange_result_write_down_trade_id": "Merci de copier ou d'écrire l'ID d'échange pour continuer.", "exchange_sync_alert_content": "Merci d'attendre que votre portefeuille (wallet) soit synchronisé", + "exchange_trade_result_confirm": "En appuyant sur Send de Cake Wallet, vous enverrez ${fetchingLabel} ${from} de votre portefeuille appelé ${walletName} à l'adresse ci-dessous. Ou vous pouvez envoyer à partir de votre portefeuille externe à l'adresse / QR Code sur la page Send from External Wallet Détails. \n\n Veuillez appuyer sur l'un des boutons pour continuer ou revenir pour modifier les montants.", "expired": "Expirée", "expires": "Expire", "expiresOn": "Expire le", @@ -694,6 +695,8 @@ "send_error_minimum_value": "La valeur minimale du montant est 0.01", "send_estimated_fee": "Estimation des frais :", "send_fee": "Frais:", + "send_from_cake_wallet": "Envoyer du portefeuille à gâteau", + "send_from_external_wallet": "Envoyer du portefeuille externe", "send_name": "Nom", "send_new": "Nouveau", "send_payment_id": "ID de paiement (optionnel)", @@ -1008,6 +1011,7 @@ "you_now_have_debit_card": "Vous avez maintenant une carte de débit", "you_pay": "Vous payez", "you_will_get": "Convertir vers", + "you_will_receive_estimated_amount": "Vous recevrez ( estimé )", "you_will_send": "Convertir depuis", "yy": "AA" } \ No newline at end of file diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 0e30d2aff..af931e4df 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "*Don Allah kwafi ko rubuta ID dake nuna sama.", "exchange_result_write_down_trade_id": "Da fatan za a kwafa ko rubuta ID ɗin ciniki don ci gaba.", "exchange_sync_alert_content": "Da fatan za a jira har sai an daidaita walat ɗin ku", + "exchange_trade_result_confirm": "Ta latsa Aika daga Wallet Caza, zaku aika ${fetchingLabel} ${from} daga walat ɗinku da ake kira ${walletName} zuwa adireshin da aka nuna a ƙasa. Ko zaka iya aika daga walat na waje zuwa adireshin / QR Code a kan Shafin Wallake Daga Shafin Tallar waje. \n\n Latsa don Allah a danna Duk da haka.", "expired": "Karewa", "expires": "Ya ƙare", "expiresOn": "Yana ƙarewa", @@ -696,6 +697,8 @@ "send_error_minimum_value": "Mafi ƙarancin ƙimar adadin shine 0.01", "send_estimated_fee": "Ƙimar kuɗi:", "send_fee": "Kudin:", + "send_from_cake_wallet": "Aika daga Wallet Caza", + "send_from_external_wallet": "Aika daga walat na waje", "send_name": "Sunan", "send_new": "Sabon", "send_payment_id": "ID na biyan kuɗi (optional)", @@ -1010,6 +1013,7 @@ "you_now_have_debit_card": "Yanzu kana da katin zare kudi", "you_pay": "Ka Bayar", "you_will_get": "Maida zuwa", + "you_will_receive_estimated_amount": "Za ku (karɓi )", "you_will_send": "Maida daga", "yy": "YY" } \ No newline at end of file diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index d309c7f89..449a3f278 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "*कृपया ऊपर दिखाए गए अपने ID को कॉपी या लिख लें.", "exchange_result_write_down_trade_id": "जारी रखने के लिए कृपया ट्रेड ID की प्रतिलिपि बनाएँ या लिखें.", "exchange_sync_alert_content": "कृपया प्रतीक्षा करें जब तक आपका बटुआ सिंक्रनाइज़ नहीं किया जाता है", + "exchange_trade_result_confirm": "केक वॉलेट से भेजने से, आप नीचे दिखाए गए पते पर ${walletName} नामक अपने वॉलेट से ${fetchingLabel} ${from} भेजेंगे। या आप अपने बाहरी वॉलेट से एक्सटर्नल वॉलेट विवरण पृष्ठ से भेजने पर पते / QR कोड पर भेज सकते हैं। \n\n कृपया जारी रखने के लिए या वापस जाने के लिए वापस बटन में से किसी एक को दबाएं।", "expired": "समय सीमा समाप्त", "expires": "समाप्त हो जाता है", "expiresOn": "पर समय सीमा समाप्त", @@ -696,6 +697,8 @@ "send_error_minimum_value": "राशि का न्यूनतम मूल्य 0.01 है", "send_estimated_fee": "अनुमानित शुल्क:", "send_fee": "शुल्क:", + "send_from_cake_wallet": "केक वॉलेट से भेजें", + "send_from_external_wallet": "बाहरी बटुए से भेजें", "send_name": "नाम", "send_new": "नया", "send_payment_id": "भुगतान ID (ऐच्छिक)", @@ -1010,6 +1013,7 @@ "you_now_have_debit_card": "अब आपके पास डेबिट कार्ड है", "you_pay": "आप भुगतान करते हैं", "you_will_get": "में बदलें", + "you_will_receive_estimated_amount": "आपको#अनुमानित ( प्राप्त होगा)", "you_will_send": "से रूपांतरित करें", "yy": "वाईवाई" } \ No newline at end of file diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 4040513cb..25d90e711 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "*Molimo kopirajte ili zapišite svoj ID prikazan ispod.", "exchange_result_write_down_trade_id": "Molimo kopirajte ili zapišite transakcijski ID za nastavak.", "exchange_sync_alert_content": "Molimo pričekajte dok se Vaš novčanik ne sinkronizira.", + "exchange_trade_result_confirm": "Pritiskom na WALLET WALLET, šaljet ćete ${fetchingLabel} ${from} iz vašeg novčanika nazvanu ${walletName} na adresu prikazanu u nastavku. Ili možete poslati iz svog vanjskog novčanika na adresu / QR kôd na stranici Slanje s Detalji vanjskih novčanika. \n\n Pritisnite bilo koji od gumba da biste nastavili ili se vratili da biste promijenili količine.", "expired": "Isteklo", "expires": "Ističe", "expiresOn": "Istječe", @@ -694,6 +695,8 @@ "send_error_minimum_value": "Minimalna vrijednost iznosa je 0.01", "send_estimated_fee": "Procijenjena naknada:", "send_fee": "Naknada:", + "send_from_cake_wallet": "Pošaljite iz novčanika", + "send_from_external_wallet": "Pošaljite iz vanjskog novčanika", "send_name": "Ime", "send_new": "Novi", "send_payment_id": "ID plaćanja (nije obvezno)", @@ -1008,6 +1011,7 @@ "you_now_have_debit_card": "Sada imate debitnu karticu", "you_pay": "Vi plaćate", "you_will_get": "Razmijeni u", + "you_will_receive_estimated_amount": "Primit ćete(procijenjeno )", "you_will_send": "Razmijeni iz", "yy": "GG" } \ No newline at end of file diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index e8ab6ca99..6c35b2be1 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "*Խնդրում ենք պատճենել կամ գրել ձեր ID-ն վերևում", "exchange_result_write_down_trade_id": "Խնդրում ենք պատճենել կամ գրել առևտրի ID-ն շարունակելու համար", "exchange_sync_alert_content": "Խնդրում ենք սպասել մինչև ձեր հաշվեհամարը համաժամացվի", + "exchange_trade_result_confirm": "Տորթի դրամապանակից ուղարկելու միջոցով ձեր դրամապանակից ${fetchingLabel} կուղարկեք ${from} անունով ${walletName}, ստորեւ նշված հասցեով: Կամ դուք կարող եք ձեր արտաքին դրամապանակից ուղարկել / QR կոդ, արտաքին դրամապանակի մանրամասների էջից ուղարկելու համար: \n\n Խնդրում ենք սեղմել կոճակներից կամ վերադառնալ, գումարները շարունակելու կամ վերադառնալու համար:", "expired": "Վավերականությունը լրացել է", "expires": "Վավերականությունը լրանում է", "expiresOn": "Վավերականությունը լրանում է", @@ -692,6 +693,8 @@ "send_error_minimum_value": "Քանակի նվազագույն արժեքը 0.01 է", "send_estimated_fee": "Գնահատված վարձը՝", "send_fee": "Վարձը՝", + "send_from_cake_wallet": "Ուղարկել տորթի դրամապանակից", + "send_from_external_wallet": "Ուղարկել արտաքին դրամապանակից", "send_name": "Անվանում", "send_new": "Նոր", "send_payment_id": "Վճարման ID (կամավոր)", @@ -1006,6 +1009,7 @@ "you_now_have_debit_card": "Դուք այժմ ունեք դեբետային քարտ", "you_pay": "Դուք վճարում եք", "you_will_get": "Ստացեք", + "you_will_receive_estimated_amount": "Դուք կստանաք ( գնահատված )", "you_will_send": "Փոխանակեք", "yy": "ՏՏ" } \ No newline at end of file diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index db2383f17..87e45da1c 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "*Silakan salin atau tulis ID Anda yang ditampilkan di atas.", "exchange_result_write_down_trade_id": "Silakan salin atau tulis ID perdagangan untuk melanjutkan.", "exchange_sync_alert_content": "Silakan tunggu sampai dompet Anda tersinkronisasi", + "exchange_trade_result_confirm": "Dengan menekan kirim dari dompet kue, Anda akan mengirim ${fetchingLabel} ${from} dari dompet Anda yang disebut ${walletName} ke alamat yang ditunjukkan di bawah ini. Atau Anda dapat mengirim dari dompet eksternal Anda ke alamat / Kode QR pada halaman Dompet Dompet Kirim dari Eksternal. \n\n Tekan salah satu tombol untuk melanjutkan atau kembali untuk mengubah jumlah.", "expired": "Kedaluwarsa", "expires": "Kadaluarsa", "expiresOn": "Kadaluarsa pada", @@ -697,6 +698,8 @@ "send_error_minimum_value": "Nilai minimum jumlah adalah 0.01", "send_estimated_fee": "Biaya yang diperkirakan:", "send_fee": "Biaya:", + "send_from_cake_wallet": "Kirim dari Dompet Kue", + "send_from_external_wallet": "Kirim dari dompet eksternal", "send_name": "Nama", "send_new": "Baru", "send_payment_id": "ID Pembayaran (opsional)", @@ -1011,6 +1014,7 @@ "you_now_have_debit_card": "Anda sekarang memiliki kartu debit", "you_pay": "Anda Membayar", "you_will_get": "Konversi ke", + "you_will_receive_estimated_amount": "Anda akan menerima(estimasi )", "you_will_send": "Konversi dari", "yy": "YY" } \ No newline at end of file diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 5ebbcece8..a476bbc0c 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "*Copia o trascrivi il tuo ID mostrato in alto.", "exchange_result_write_down_trade_id": "Copia o trascrivi l'ID dello scambio per continuare.", "exchange_sync_alert_content": "Gentilmente aspetta che il tuo portafoglio venga sincronizzato", + "exchange_trade_result_confirm": "Premendo Invia dal portafoglio Cake, invierai ${fetchingLabel} ${from} dal tuo portafoglio chiamato ${walletName} all'indirizzo mostrato di seguito. Oppure puoi inviare dal tuo portafoglio esterno all'indirizzo / QR Codice sulla pagina Invio da Dettagli del portafoglio esterno. \n\n Premere uno dei pulsanti per continuare o tornare a modificare gli importi.", "expired": "Scaduto", "expires": "Scade", "expiresOn": "Scade il", @@ -695,6 +696,8 @@ "send_error_minimum_value": "L'importo minimo è 0.01", "send_estimated_fee": "Commissione stimata:", "send_fee": "Commissione:", + "send_from_cake_wallet": "Invia dal portafoglio Cake", + "send_from_external_wallet": "Invia dal portafoglio esterno", "send_name": "Nome", "send_new": "Nuovo", "send_payment_id": "ID Pagamento (opzionale)", @@ -1010,6 +1013,7 @@ "you_now_have_debit_card": "Ora hai una carta di debito", "you_pay": "Tu paghi", "you_will_get": "Converti a", + "you_will_receive_estimated_amount": "Riceverai(stimato )", "you_will_send": "Conveti da", "yy": "YY" } \ No newline at end of file diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 9fe48e0b3..10350d7be 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "*上記のIDをコピーまたは書き留めてください.", "exchange_result_write_down_trade_id": "続行するには、取引IDをコピーまたは書き留めてください.", "exchange_sync_alert_content": "ウォレットが同期されるまでお待ちください", + "exchange_trade_result_confirm": "Cake Walletから送信を押すことにより、${walletName}というウォレットから ${fetchingLabel} ${from}を以下に示すアドレスに送信します。または、外部ウォレットから外部ウォレットの詳細ページから送信のアドレス/ QRコードに送信することもできます。", "expired": "期限切れ", "expires": "Expires", "expiresOn": "有効期限は次のとおりです", @@ -695,6 +696,8 @@ "send_error_minimum_value": "金額の最小値は0.01です", "send_estimated_fee": "見積手数料:", "send_fee": "費用:", + "send_from_cake_wallet": "ケーキウォレットから送信します", + "send_from_external_wallet": "外部ウォレットから送信します", "send_name": "名前", "send_new": "新着", "send_payment_id": "支払いID (オプショナル)", @@ -1009,6 +1012,7 @@ "you_now_have_debit_card": "デビットカードができました", "you_pay": "あなたが支払う", "you_will_get": "に変換", + "you_will_receive_estimated_amount": "あなたは(推定)を受け取ります", "you_will_send": "から変換", "yy": "YY" } \ No newline at end of file diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index c498b2438..a76318ad0 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "*위에 표시된 ID를 복사하거나 적어 두십시오.", "exchange_result_write_down_trade_id": "계속하려면 거래 ID를 복사하거나 적어 두십시오..", "exchange_sync_alert_content": "지갑이 동기화 될 때까지 기다리십시오", + "exchange_trade_result_confirm": "Cake 지갑에서 보내기를 누르면 ${walletName}이라는 지갑에서 아래에 표시된 주소로 ${fetchingLabel} ${from}을 보내는 것입니다. 또는 외부 지갑에서 외부 지갑 세부 정보 페이지에서 주소 / QR 코드로 보낼 수 있습니다.", "expired": "만료", "expires": "만료", "expiresOn": "만료 날짜", @@ -694,6 +695,8 @@ "send_error_minimum_value": "금액의 최소값은 0.01입니다", "send_estimated_fee": "예상 수수료:", "send_fee": "회비:", + "send_from_cake_wallet": "케이크 지갑에서 보내십시오", + "send_from_external_wallet": "외부 지갑에서 보내십시오", "send_name": "이름", "send_new": "새로운", "send_payment_id": "지불 ID (optional)", @@ -1008,6 +1011,7 @@ "you_now_have_debit_card": "이제 직불카드가 있습니다.", "you_pay": "당신이 지불합니다", "you_will_get": "로 변환하다", + "you_will_receive_estimated_amount": "(추정 )을 받게됩니다.", "you_will_send": "다음에서 변환", "YY": "YY", "yy": "YY" diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 5c166afca..ac6749a7f 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "* ကျေးဇူးပြု၍ အထက်ဖော်ပြပါ သင်၏ ID ကို ကော်ပီ သို့မဟုတ် ရေးမှတ်ပါ။", "exchange_result_write_down_trade_id": "ရှေ့ဆက်ရန် ကုန်သွယ်မှု ID ကို ကူးယူ သို့မဟုတ် ချရေးပါ။", "exchange_sync_alert_content": "သင့်ပိုက်ဆံအိတ်ကို စင့်ခ်လုပ်ထားသည့်အချိန်အထိ စောင့်ပါ။", + "exchange_trade_result_confirm": "Cake Wallet မှ Send ကိုနှိပ်ခြင်းအားဖြင့်သင်သည် ${walletName} ဟုခေါ်သည့်သင်၏ပိုက်ဆံအိတ်မှ ${fetchingLabel} ကို ${from} ကိုအောက်တွင်ဖော်ပြထားသောလိပ်စာသို့ပို့လိမ့်မည်။ သို့မဟုတ်ပြင်ပပိုက်ဆံအိတ်အသေးစိတ်စာမျက်နှာမှ Send People ၏ Send People ၏ Send People မှသင်၏ပြင်ပပိုက်ဆံအိတ်မှလိပ်စာသို့သင်ပို့နိုင်သည်။ \n\n ဆက်လုပ်ရန်သို့မဟုတ်ပမာဏကိုပြောင်းလဲရန်ပြန်သွားပါ။", "expired": "သက်တမ်းကုန်သွားပြီ", "expires": "သက်တမ်းကုန်သည်။", "expiresOn": "သက်တမ်းကုန်သည်။", @@ -694,6 +695,8 @@ "send_error_minimum_value": "ပမာဏ၏ အနည်းဆုံးတန်ဖိုးမှာ 0.01 ဖြစ်သည်။", "send_estimated_fee": "ခန့်မှန်းကြေး-", "send_fee": "အခကြေးငွေ-", + "send_from_cake_wallet": "ကိတ်မုန့်ပိုက်ဆံအိတ်မှပေးပို့ပါ", + "send_from_external_wallet": "ပြင်ပပိုက်ဆံအိတ်မှပေးပို့ပါ", "send_name": "နာမည်", "send_new": "အသစ်", "send_payment_id": "ငွေပေးချေမှု ID (ချန်လှပ်ထား)", @@ -1008,6 +1011,7 @@ "you_now_have_debit_card": "ယခု သင့်တွင် ဒက်ဘစ်ကတ်တစ်ခုရှိသည်။", "you_pay": "သင်ပေးချေပါ။", "you_will_get": "သို့ပြောင်းပါ။", + "you_will_receive_estimated_amount": "သင် ( ခန့်မှန်းခြေ ) လက်ခံရရှိလိမ့်မည်", "you_will_send": "မှပြောင်းပါ။", "yy": "YY" } \ No newline at end of file diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index d1b43bbdc..e38e1ddfd 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "*Kopieer of noteer uw hierboven getoonde ID.", "exchange_result_write_down_trade_id": "Kopieer of noteer de handels-ID om door te gaan.", "exchange_sync_alert_content": "Wacht tot uw portemonnee is gesynchroniseerd", + "exchange_trade_result_confirm": "Door te drukken op verzenden vanaf cake -portemonnee, verzendt u ${fetchingLabel} ${from} vanuit uw portemonnee genaamd ${walletName} naar het hieronder getoonde adres. Of u kunt van uw externe portemonnee verzenden naar het adres / QR -code op de pagina Verzenden van externe portemonnee -details. \n\n Druk op een van de knoppen om door te gaan of terug te gaan om de bedragen te wijzigen.", "expired": "Verlopen", "expires": "Verloopt", "expiresOn": "Verloopt op", @@ -694,6 +695,8 @@ "send_error_minimum_value": "Minimale waarde van bedrag is 0,01", "send_estimated_fee": "Geschatte vergoeding:", "send_fee": "Vergoeding:", + "send_from_cake_wallet": "Stuur vanaf cake -portemonnee", + "send_from_external_wallet": "Stuur vanuit een externe portemonnee", "send_name": "Naam", "send_new": "Nieuw", "send_payment_id": "Betaling ID (facultatief)", @@ -1009,6 +1012,7 @@ "you_now_have_debit_card": "Je hebt nu een debetkaart", "you_pay": "U betaalt", "you_will_get": "Converteren naar", + "you_will_receive_estimated_amount": "U ontvangt(geschat )", "you_will_send": "Converteren van", "yy": "JJ" } \ No newline at end of file diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index e0514d481..9ec7db278 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "*Skopiuj lub zanotuj identyfikator transakcji pokazany powyżej.", "exchange_result_write_down_trade_id": "Skopiuj lub zanotuj identyfikator transakcji (ID), aby kontynuować.", "exchange_sync_alert_content": "Poczekaj, aż portfel zostanie zsynchronizowany", + "exchange_trade_result_confirm": "Naciskając wyślij z portfela ciasta, wyślesz ${fetchingLabel} ${from} z portfela o nazwie ${walletName} na adres pokazany poniżej. Lub możesz wysłać z portfela zewnętrznego na adres / qr kod na stronie Wyślij ze strony szczegółów portfela zewnętrznego. \n\n naciśnij którykolwiek z przycisków, aby kontynuować lub wróć, aby zmienić kwoty.", "expired": "Przedawniony", "expires": "Wygasa", "expiresOn": "Upływa w dniu", @@ -694,6 +695,8 @@ "send_error_minimum_value": "Minimalna wartość to 0,01", "send_estimated_fee": "Szacowana opłata:", "send_fee": "Opłata:", + "send_from_cake_wallet": "Wyślij z portfela ciasta", + "send_from_external_wallet": "Wyślij z portfela zewnętrznego", "send_name": "Imię", "send_new": "Nowy", "send_payment_id": "Identyfikator płatności (opcjonalny)", @@ -1008,6 +1011,7 @@ "you_now_have_debit_card": "Masz teraz kartę debetową", "you_pay": "Płacisz", "you_will_get": "Konwertuj na", + "you_will_receive_estimated_amount": "Otrzymasz(oszacowane )", "you_will_send": "Konwertuj z", "yy": "RR" } \ No newline at end of file diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index f6f4a1295..0c0ed2515 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "*Copie ou anote seu ID mostrado acima.", "exchange_result_write_down_trade_id": "Copie ou anote o ID da troca para continuar.", "exchange_sync_alert_content": "Por favor, espere até que sua carteira seja sincronizada", + "exchange_trade_result_confirm": "Ao pressionar o envio da carteira de bolo, você estará enviando ${fetchingLabel} ${from} da sua carteira chamada ${walletName} para o endereço mostrado abaixo. Ou você pode enviar da sua carteira externa para o endereço / QR Código na página Enviar da carteira externa. \n\n Pressione qualquer um dos botões para continuar ou voltar para alterar os valores.", "expired": "Expirada", "expires": "Expira", "expiresOn": "Expira em", @@ -696,6 +697,8 @@ "send_error_minimum_value": "O valor mínimo da quantia é 0,01", "send_estimated_fee": "Taxa estimada:", "send_fee": "Taxa:", + "send_from_cake_wallet": "Enviar da carteira de bolo", + "send_from_external_wallet": "Enviar da carteira externa", "send_name": "Nome", "send_new": "Novo", "send_payment_id": "ID de pagamento (opcional)", @@ -1011,6 +1014,7 @@ "you_now_have_debit_card": "Agora você tem um cartão de débito", "you_pay": "Você paga", "you_will_get": "Converter para", + "you_will_receive_estimated_amount": "Você receberá(estimado )", "you_will_send": "Converter de", "yy": "aa" } \ No newline at end of file diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index e2a718cb0..0b511b016 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "*Пожалуйста, скопируйте или запишите ID, указанный выше.", "exchange_result_write_down_trade_id": "Пожалуйста, скопируйте или запишите ID сделки.", "exchange_sync_alert_content": "Подождите, пока ваш кошелек синхронизируется", + "exchange_trade_result_confirm": "Нажав отправить из кошелька для торта, вы отправите ${fetchingLabel} ${from} из своего кошелька под названием ${walletName} на адрес, показанный ниже. Или вы можете отправить из своего внешнего кошелька на адрес / QR -код на странице «Отправить с внешнего кошелька». \n\n Нажмите любую кнопки, чтобы продолжить или вернуться, чтобы изменить суммы.", "expired": "Истекает", "expires": "Истекает", "expiresOn": "Годен до", @@ -695,6 +696,8 @@ "send_error_minimum_value": "Mинимальная сумма 0.01", "send_estimated_fee": "Предполагаемая комиссия:", "send_fee": "Комиссия:", + "send_from_cake_wallet": "Отправить с кошелька для торта", + "send_from_external_wallet": "Отправить с внешнего кошелька", "send_name": "Имя", "send_new": "Новый", "send_payment_id": "ID платежа (опционально)", @@ -1009,6 +1012,7 @@ "you_now_have_debit_card": "Теперь у вас есть дебетовая карта", "you_pay": "Вы платите", "you_will_get": "Конвертировать в", + "you_will_receive_estimated_amount": "Вы получите(Оценку )", "you_will_send": "Конвертировать из", "yy": "ГГ" } \ No newline at end of file diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index ff950f3b7..bbbcf7227 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "*โปรดคัดลอกหรือเขียนรหัสของคุณด้านบน", "exchange_result_write_down_trade_id": "โปรดคัดลอกหรือบันทึก ID ของการซื้อขายเพื่อดำเนินการต่อไป", "exchange_sync_alert_content": "โปรดรอจนกว่ากระเป๋าของคุณจะถูกซิงค์", + "exchange_trade_result_confirm": "โดยการกดส่งจาก Cake Wallet คุณจะส่ง ${fetchingLabel} ${from} จากกระเป๋าเงินของคุณที่เรียกว่า ${walletName} ไปยังที่อยู่ที่แสดงด้านล่าง หรือคุณสามารถส่งจากกระเป๋าเงินภายนอกของคุณไปยังที่อยู่ / รหัส QR ในหน้าส่งจากรายละเอียดกระเป๋าเงินภายนอก \n\n โปรดกดปุ่มใดปุ่มหนึ่งเพื่อดำเนินการต่อหรือกลับไปเปลี่ยนจำนวนเงิน", "expired": "หมดอายุ", "expires": "หมดอายุ", "expiresOn": "หมดอายุวันที่", @@ -694,6 +695,8 @@ "send_error_minimum_value": "จำนวนขั้นต่ำของจำนวนเงินคือ 0.01", "send_estimated_fee": "ค่าธรรมเนียมที่คาดการณ์:", "send_fee": "ค่าธรรมเนียม:", + "send_from_cake_wallet": "ส่งจากกระเป๋าเงินเค้ก", + "send_from_external_wallet": "ส่งจากกระเป๋าเงินภายนอก", "send_name": "ชื่อ", "send_new": "ใหม่", "send_payment_id": "ID การชำระเงิน (ไม่จำเป็น)", @@ -1008,6 +1011,7 @@ "you_now_have_debit_card": "ขณะนี้คุณมีบัตรเดบิต", "you_pay": "คุณจ่าย", "you_will_get": "แปลงเป็น", + "you_will_receive_estimated_amount": "คุณจะได้รับ(โดยประมาณ )", "you_will_send": "แปลงจาก", "yy": "ปี" } \ No newline at end of file diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index fe065080f..7cdbd0d8b 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "*Mangyaring kopyahin o isulat ang inyong ID na ipinapakita sa itaas.", "exchange_result_write_down_trade_id": "Mangyaring kopyahin o isulat ang trade ID upang magpatuloy.", "exchange_sync_alert_content": "Mangyaring maghintay hanggang ang iyong wallet ay naka-synchronize", + "exchange_trade_result_confirm": "Sa pamamagitan ng pagpindot sa pagpapadala mula sa cake wallet, magpapadala ka ng ${fetchingLabel} ${from} mula sa iyong pitaka na tinatawag na ${walletName} sa address na ipinakita sa ibaba. O maaari kang magpadala mula sa iyong panlabas na pitaka sa address / qr code sa pagpapadala mula sa pahina ng Mga Detalye ng Panlabas na Wallet. \n\n Mangyaring pindutin ang alinman sa mga pindutan upang magpatuloy o bumalik upang baguhin ang mga halaga.", "expired": "Nag-expire na", "expires": "Mag-e-expire", "expiresOn": "Mag-e-expire sa", @@ -694,6 +695,8 @@ "send_error_minimum_value": "Ang minimum na halaga ay 0.01", "send_estimated_fee": "Tinatayang fee:", "send_fee": "Fee:", + "send_from_cake_wallet": "Magpadala mula sa cake wallet", + "send_from_external_wallet": "Magpadala mula sa panlabas na pitaka", "send_name": "Pangalan", "send_new": "Bago", "send_payment_id": "Payment ID (opsyonal)", @@ -1008,6 +1011,7 @@ "you_now_have_debit_card": "Mayroon ka na ngayong debit card", "you_pay": "Magbayad ka", "you_will_get": "I-convert sa", + "you_will_receive_estimated_amount": "Makakatanggap ka ng(tinantyang)", "you_will_send": "I-convert mula sa", "yy": "YY" } \ No newline at end of file diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 9e9ac3726..bfc87a7e7 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "*Lütfen yukarıda gösterilen ID'ni kopyala veya bir yere yaz.", "exchange_result_write_down_trade_id": "Devam etmek için lütfen ID'yi kopyala veya bir yere yaz.", "exchange_sync_alert_content": "Lütfen cüzdanın senkronize olana kadar bekle", + "exchange_trade_result_confirm": "Kek cüzdanından Gönder'e basarak, ${walletName} adlı cüzdanınızdan aşağıda gösterilen adrese ${fetchingLabel} ${from} göndereceksiniz. Veya harici cüzdanınızdan harici cüzdan detaylarından gönderme gönderisindeki / qr koduna gönderebilirsiniz.", "expired": "Süresi doldu", "expires": "Son kullanma tarihi", "expiresOn": "Tarihinde sona eriyor", @@ -694,6 +695,8 @@ "send_error_minimum_value": "Minimum tutar değeri 0.01'dir", "send_estimated_fee": "Tahmini komisyon:", "send_fee": "Komisyon:", + "send_from_cake_wallet": "Kek cüzdanından gönder", + "send_from_external_wallet": "Harici cüzdandan gönder", "send_name": "İsim", "send_new": "Yeni", "send_payment_id": "Ödeme ID'si (isteğe bağlı)", @@ -1008,6 +1011,7 @@ "you_now_have_debit_card": "Artık bir ön ödemeli kartın var", "you_pay": "Şu kadar ödeyeceksin: ", "you_will_get": "Biçimine dönüştür:", + "you_will_receive_estimated_amount": "(Tahmini ) alacaksınız", "you_will_send": "Biçiminden dönüştür:", "yy": "YY" } \ No newline at end of file diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 6b321cb78..4b7818347 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "*Будь ласка, скопіюйте або запишіть ID, вказаний вище.", "exchange_result_write_down_trade_id": "Будь ласка, скопіюйте або запишіть ID операції.", "exchange_sync_alert_content": "Зачекайте, поки ваш гаманець не синхронізується", + "exchange_trade_result_confirm": "Натиснувши надсилання з гаманця тортів, ви будете відправляти ${fetchingLabel} ${from} зі свого гаманця під назвою ${walletName} на адресу, показану нижче. Або ви можете надіслати зі свого зовнішнього гаманця на адресу / QR -код на сторінці \"Надіслати з зовнішнього гаманця\".", "expired": "Закінчується", "expires": "Закінчується", "expiresOn": "Термін дії закінчується", @@ -695,6 +696,8 @@ "send_error_minimum_value": "Мінімальна сума 0.01", "send_estimated_fee": "Ймовірна комісія:", "send_fee": "Комісія:", + "send_from_cake_wallet": "Надіслати з гаманця торта", + "send_from_external_wallet": "Надіслати із зовнішнього гаманця", "send_name": "Ім'я", "send_new": "Новий", "send_payment_id": "ID платежу (опційно)", @@ -1009,6 +1012,7 @@ "you_now_have_debit_card": "Тепер у вас є дебетова картка", "you_pay": "Ви платите", "you_will_get": "Конвертувати в", + "you_will_receive_estimated_amount": "Ви отримаєте(оцінюється )", "you_will_send": "Конвертувати з", "yy": "YY" } \ No newline at end of file diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 3ed3f5949..f6b92ca36 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "*براہ کرم اوپر دکھائی گئی اپنی ID کاپی کریں یا لکھیں۔", "exchange_result_write_down_trade_id": "جاری رکھنے کے لیے براہ کرم تجارتی ID کاپی کریں یا لکھیں۔", "exchange_sync_alert_content": "براہ کرم اس وقت تک انتظار کریں جب تک آپ کا بٹوہ مطابقت پذیر نہ ہو جائے۔", + "exchange_trade_result_confirm": "کیک پرس سے بھیجیں دبانے سے ، آپ اپنے بٹوے سے ${fetchingLabel} ${from} کہتے ہیں ${walletName} نیچے دیئے گئے پتے پر بھیج رہے ہوں گے۔ یا آپ اپنے بیرونی پرس سے بیرونی والیٹ کی تفصیلات کے صفحے پر بھیجنے والے ایڈریس \n\n QR کوڈ پر بھیج سکتے ہیں۔", "expired": "میعاد ختم", "expires": "میعاد ختم", "expiresOn": "ﺩﺎﻌﯿﻣ ﯽﻣﺎﺘﺘﺧﺍ", @@ -696,6 +697,8 @@ "send_error_minimum_value": "رقم کی کم از کم قیمت 0.01 ہے۔", "send_estimated_fee": "تخمینی فیس:", "send_fee": "فیس:", + "send_from_cake_wallet": "کیک پرس سے بھیجیں", + "send_from_external_wallet": "بیرونی پرس سے بھیجیں", "send_name": "نام", "send_new": "نئی", "send_payment_id": "ادائیگی کی شناخت (اختیاری)", @@ -1010,6 +1013,7 @@ "you_now_have_debit_card": "اب آپ کے پاس ڈیبٹ کارڈ ہے۔", "you_pay": "تم ادا کرو", "you_will_get": "میں تبدیل کریں۔", + "you_will_receive_estimated_amount": "آپ(تخمینہ ) وصول کریں گے", "you_will_send": "سے تبدیل کریں۔", "yy": "YY" } \ No newline at end of file diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 787115f44..a759e87c9 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -307,6 +307,7 @@ "exchange_result_write_down_ID": "*Vui lòng sao chép hoặc ghi lại ID hiển thị ở trên.", "exchange_result_write_down_trade_id": "Vui lòng sao chép hoặc ghi lại ID giao dịch để tiếp tục.", "exchange_sync_alert_content": "Vui lòng chờ cho đến khi ví của bạn được đồng bộ hóa", + "exchange_trade_result_confirm": "Bằng cách nhấn gửi từ ví bánh, bạn sẽ gửi ${fetchingLabel} ${from} từ ví của bạn có tên là ${walletName} đến địa chỉ được hiển thị bên dưới. Hoặc bạn có thể gửi từ ví bên ngoài của mình đến địa chỉ / Mã QR trên trang gửi từ chi tiết ví bên ngoài. \n\n Vui lòng nhấn một trong hai nút để tiếp tục hoặc quay lại để thay đổi số tiền.", "expired": "Đã hết hạn", "expires": "Hết hạn", "expiresOn": "Hết hạn vào", @@ -691,6 +692,8 @@ "send_error_minimum_value": "Giá trị tối thiểu của số tiền là 0.01", "send_estimated_fee": "Phí ước lượng:", "send_fee": "Phí:", + "send_from_cake_wallet": "Gửi từ ví bánh", + "send_from_external_wallet": "Gửi từ ví bên ngoài", "send_name": "Tên", "send_new": "Mới", "send_payment_id": "ID thanh toán (tùy chọn)", @@ -1005,6 +1008,7 @@ "you_now_have_debit_card": "Bạn hiện có một thẻ ghi nợ", "you_pay": "Bạn thanh toán", "you_will_get": "Chuyển đổi thành", + "you_will_receive_estimated_amount": "Bạn sẽ nhận được(ước tính )", "you_will_send": "Chuyển đổi từ", "yy": "YY" } \ No newline at end of file diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index ac9f30013..7d9017d0c 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -309,6 +309,7 @@ "exchange_result_write_down_ID": "*Ẹ jọ̀wọ́, ṣẹ̀dà àmì ìdánimọ̀ yín tó ṣàfihàn òun lókè.", "exchange_result_write_down_trade_id": "Ẹ jọ̀wọ́, kọ àmì ìdánimọ̀ pàṣípààrọ̀ sílẹ̀ kí tẹ̀síwájú.", "exchange_sync_alert_content": "Ẹ jọ̀wọ́ dúró kí a ti múdọ́gba àpamọ́wọ́ yín", + "exchange_trade_result_confirm": "Nipa titẹ-titẹ lati apamọwọ akara oyinbo, iwọ yoo firanṣẹ ${fetchingLabel} ${from} lati apamọwọ rẹ ti a pe ni ${walletName} si adirẹsi ti o han ni isalẹ. Tabi o le firanṣẹ lati apamọwọ ti ita rẹ si adirẹsi / Kọlẹ oju-iwe lori Firanṣẹ lati oju opo wẹẹbu ita. \n\n Jọwọ tẹ awọn bọtini lati tẹsiwaju lati yi awọn oye silẹ.", "expired": "Kíkú", "expires": "Ó parí", "expiresOn": "Ipari lori", @@ -695,6 +696,8 @@ "send_error_minimum_value": "Ránṣẹ́ owó kò kéré dé 0.01", "send_estimated_fee": "Iye àfikún l'a fojú díwọ̀n:", "send_fee": "Owó àfikún:", + "send_from_cake_wallet": "Firanṣẹ lati apamọwọ akara oyinbo", + "send_from_external_wallet": "Firanṣẹ lati apamọwọ ita", "send_name": "Orúkọ", "send_new": "Títun", "send_payment_id": "Àmì ìdánimọ̀ àránṣẹ́ (ìyàn nìyí)", @@ -1009,6 +1012,7 @@ "you_now_have_debit_card": "Ẹ ni káàdì ìrajà lọ́wọ́lọ́wọ́", "you_pay": "Ẹ sàn", "you_will_get": "Ṣe pàṣípààrọ̀ sí", + "you_will_receive_estimated_amount": "Iwọ yoo gba ( excimated )", "you_will_send": "Ṣe pàṣípààrọ̀ láti", "yy": "Ọd" } \ No newline at end of file diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 1eb3e7d7a..5e009f841 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -308,6 +308,7 @@ "exchange_result_write_down_ID": "*请复制或写下您上面显示的ID.", "exchange_result_write_down_trade_id": "请复制或写下交易编号以继续.", "exchange_sync_alert_content": "请等待,直到您的钱包同步", + "exchange_trade_result_confirm": "通过按Cake Wallet的发送发送,您将从钱包中发送 ${fetchingLabel} ${from}将###发送到下面所示的地址。或者,您可以从外部钱包${walletName} QR码从外部钱包详细信息页面发送到地址/ QR码。\n\n请按任何按钮继续或返回以更改金额。", "expired": "已过期", "expires": "过期", "expiresOn": "到期", @@ -694,6 +695,8 @@ "send_error_minimum_value": "最小金额为0.01", "send_estimated_fee": "预估费用:", "send_fee": "手续费:", + "send_from_cake_wallet": "从蛋糕钱包发送", + "send_from_external_wallet": "从外部钱包发送", "send_name": "名称", "send_new": "新建", "send_payment_id": "付款编号 (可选的)", @@ -1008,6 +1011,7 @@ "you_now_have_debit_card": "你现在有一张借记卡", "you_pay": "你付钱", "you_will_get": "转换到", + "you_will_receive_estimated_amount": "您将收到(估计的)", "you_will_send": "转换自", "yy": "YY" } \ No newline at end of file From 6abefc25a7fb75e1d3958c7a5b3e737645d6d775 Mon Sep 17 00:00:00 2001 From: Serhii Date: Sat, 15 Mar 2025 01:45:46 +0200 Subject: [PATCH 040/243] Cw 773 restoring from qr gives no visual feedback of progress (#1944) * init commit * fix for passphrase * update restore from keys flow * remove WalletRestorationFromQRVM * update creation vm * fix for blockchainHeightKey * fix merge conflicts * Update wallet_restore_page.dart --------- Co-authored-by: Omar Hatem --- lib/di.dart | 23 +- lib/router.dart | 17 +- .../screens/restore/restore_options_page.dart | 43 ++-- .../wallet_restore_from_keys_form.dart | 37 ++- .../wallet_restore_from_seed_form.dart | 4 + .../screens/restore/wallet_restore_page.dart | 27 ++- lib/src/widgets/seed_widget.dart | 18 +- .../restore/restore_from_qr_vm.dart | 219 ------------------ .../restore/wallet_restore_from_qr_code.dart | 31 ++- lib/view_model/wallet_creation_vm.dart | 20 +- lib/view_model/wallet_restore_view_model.dart | 11 +- 11 files changed, 129 insertions(+), 321 deletions(-) delete mode 100644 lib/view_model/restore/restore_from_qr_vm.dart diff --git a/lib/di.dart b/lib/di.dart index f3de2bc96..c8d664b6e 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -54,6 +54,7 @@ import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart'; +import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:cake_wallet/view_model/send/fees_view_model.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; @@ -173,7 +174,6 @@ import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart'; import 'package:cake_wallet/view_model/wallet_groups_display_view_model.dart'; import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart'; -import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart'; import 'package:cake_wallet/view_model/settings/display_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/mweb_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart'; @@ -475,14 +475,6 @@ Future setup({ walletType: args.walletType ?? currentWalletType); }); - getIt.registerFactoryParam((WalletType type, _) => - WalletRestorationFromQRVM( - getIt.get(), - getIt.get(param1: type), - _walletInfoSource, - type, - getIt.get())); - getIt.registerFactoryParam((type, _) => WalletHardwareRestoreViewModel( getIt.get(), @@ -1144,13 +1136,18 @@ Future setup({ getIt.registerFactory(() => FaqPage(getIt.get())); - getIt.registerFactoryParam((type, _) => + getIt.registerFactoryParam((WalletType type, + restoredWallet) => WalletRestoreViewModel(getIt.get(), getIt.get(param1: type), _walletInfoSource, getIt.get(), - type: type)); + type: type, restoredWallet: restoredWallet)); - getIt.registerFactoryParam((type, _) => WalletRestorePage( - getIt.get(param1: type), getIt.get())); + getIt.registerFactoryParam((WalletType type, + restoredWallet) { + return WalletRestorePage( + getIt.get(param1: type, param2: restoredWallet), + getIt.get()); + }); getIt.registerFactoryParam, void>( (derivations, _) => WalletRestoreChooseDerivationViewModel(derivationInfos: derivations)); diff --git a/lib/router.dart b/lib/router.dart index 497bcdaa1..54cbb1531 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -120,6 +120,7 @@ import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; +import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:cake_wallet/view_model/wallet_groups_display_view_model.dart'; import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart'; @@ -235,8 +236,9 @@ Route createRoute(RouteSettings settings) { return CupertinoPageRoute( builder: (_) => getIt.get( param1: NewWalletTypeArguments( - onTypeSelected: (BuildContext context, WalletType type) => - Navigator.of(context).pushNamed(Routes.restoreWallet, arguments: type), + onTypeSelected: (BuildContext context, WalletType type) { + final arg = {'walletType': type}; + Navigator.of(context).pushNamed(Routes.restoreWallet, arguments: arg);}, isCreate: false, isHardwareWallet: false, ), @@ -261,8 +263,9 @@ Route createRoute(RouteSettings settings) { return CupertinoPageRoute( builder: (_) => getIt.get( param1: NewWalletTypeArguments( - onTypeSelected: (BuildContext context, WalletType type) => - Navigator.of(context).pushNamed(Routes.restoreWallet, arguments: type), + onTypeSelected: (BuildContext context, WalletType type) { + final arg = {'walletType': type}; + Navigator.of(context).pushNamed(Routes.restoreWallet, arguments: arg);}, isCreate: false, isHardwareWallet: false, ), @@ -318,8 +321,11 @@ Route createRoute(RouteSettings settings) { builder: (_) => getIt.get(param1: settings.arguments as bool)); case Routes.restoreWallet: + final args = settings.arguments as Map?; + final walletType = args?['walletType'] as WalletType; + final restoredWallet = args?['restoredWallet'] as RestoredWallet?; return MaterialPageRoute( - builder: (_) => getIt.get(param1: settings.arguments as WalletType)); + builder: (_) => getIt.get(param1: walletType, param2: restoredWallet)); case Routes.restoreWalletChooseDerivation: return MaterialPageRoute( @@ -677,6 +683,7 @@ Route createRoute(RouteSettings settings) { final isChildWallet = args['isChildWallet'] as bool? ?? false; final useTestnet = args['useTestnet'] as bool; final toggleTestnet = args['toggleTestnet'] as Function(bool? val); + final restoredWallet = args['restoredWallet'] as RestoredWallet?; return CupertinoPageRoute( builder: (_) => AdvancedPrivacySettingsPage( diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index 2e9e04acd..ba1669fe5 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -1,18 +1,14 @@ import 'dart:io'; -import 'package:cake_wallet/core/execution_state.dart'; -import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/option_tile.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/permission_handler.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart'; import 'package:cake_wallet/view_model/restore/wallet_restore_from_qr_code.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cw_core/hardware/device_connection_type.dart'; @@ -131,10 +127,8 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { ); } - void _onWalletCreateFailure(BuildContext context, String error) { - setState(() { - isRestoring = false; - }); + void _showQRScanError(BuildContext context, String error) { + setState(() => isRestoring = false); WidgetsBinding.instance.addPostFrameCallback((_) { showPopUp( @@ -151,28 +145,23 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { Future _onScanQRCode(BuildContext context) async { final isCameraPermissionGranted = - await PermissionHandler.checkPermission(Permission.camera, context); + await PermissionHandler.checkPermission(Permission.camera, context); if (!isCameraPermissionGranted) return; - try { - if (isRestoring) { - return; - } - setState(() { - isRestoring = true; - }); - final restoreWallet = await WalletRestoreFromQRCode.scanQRCodeForRestoring(context); + try { + if (isRestoring) return; - final restoreFromQRViewModel = - getIt.get(param1: restoreWallet.type); + setState(() => isRestoring = true); - await restoreFromQRViewModel.create(restoreWallet: restoreWallet); - if (restoreFromQRViewModel.state is FailureState) { - _onWalletCreateFailure(context, - 'Create wallet state: ${(restoreFromQRViewModel.state as FailureState).error}'); - } - } catch (e) { - _onWalletCreateFailure(context, e.toString()); - } + final restoredWallet = await WalletRestoreFromQRCode.scanQRCodeForRestoring(context); + + final params = {'walletType': restoredWallet.type, 'restoredWallet': restoredWallet}; + + Navigator.pushNamed(context, Routes.restoreWallet, arguments: params).then((_) { + if (mounted) setState(() => isRestoring = false); + }); + } catch (e) { + _showQRScanError(context, e.toString()); } + } } diff --git a/lib/src/screens/restore/wallet_restore_from_keys_form.dart b/lib/src/screens/restore/wallet_restore_from_keys_form.dart index 0c9af6910..3dc312702 100644 --- a/lib/src/screens/restore/wallet_restore_from_keys_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_keys_form.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/src/widgets/address_text_field.dart'; +import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:cake_wallet/view_model/wallet_restore_view_model.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; @@ -10,14 +11,15 @@ import 'package:cake_wallet/entities/generate_name.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; import 'package:flutter/services.dart'; -class WalletRestoreFromKeysFrom extends StatefulWidget { - WalletRestoreFromKeysFrom({ +class WalletRestoreFromKeysForm extends StatefulWidget { + WalletRestoreFromKeysForm({ required this.walletRestoreViewModel, required this.onPrivateKeyChange, required this.displayPrivateKeyField, required this.onHeightOrDateEntered, required this.displayWalletPassword, required this.onRepeatedPasswordChange, + this.restoredWallet, this.onPasswordChange, Key? key, }) : super(key: key); @@ -27,23 +29,32 @@ class WalletRestoreFromKeysFrom extends StatefulWidget { final void Function(String)? onPrivateKeyChange; final bool displayPrivateKeyField; final bool displayWalletPassword; + final RestoredWallet? restoredWallet; final void Function(String)? onPasswordChange; final void Function(String)? onRepeatedPasswordChange; @override - WalletRestoreFromKeysFromState createState() => - WalletRestoreFromKeysFromState(displayWalletPassword: displayWalletPassword); + WalletRestoreFromKeysFormState createState() => + WalletRestoreFromKeysFormState(displayWalletPassword: displayWalletPassword, restoredWallet: restoredWallet); } -class WalletRestoreFromKeysFromState extends State { - WalletRestoreFromKeysFromState({required bool displayWalletPassword}) +class WalletRestoreFromKeysFormState extends State { + WalletRestoreFromKeysFormState({required bool displayWalletPassword, RestoredWallet? restoredWallet}) : formKey = GlobalKey(), blockchainHeightKey = GlobalKey(), nameController = TextEditingController(), - addressController = TextEditingController(), - viewKeyController = TextEditingController(), - spendKeyController = TextEditingController(), - privateKeyController = TextEditingController(), + addressController = restoredWallet != null + ? TextEditingController(text: restoredWallet.address) + : TextEditingController(), + viewKeyController = restoredWallet != null + ? TextEditingController(text: restoredWallet.viewKey) + : TextEditingController(), + spendKeyController = restoredWallet != null + ? TextEditingController(text: restoredWallet.spendKey) + : TextEditingController(), + privateKeyController = restoredWallet != null + ? TextEditingController(text: restoredWallet.privateKey) + : TextEditingController(), nameTextEditingController = TextEditingController(), passwordTextEditingController = displayWalletPassword ? TextEditingController() : null, repeatedPasswordTextEditingController = displayWalletPassword ? TextEditingController() : null; @@ -80,6 +91,12 @@ class WalletRestoreFromKeysFromState extends State { } widget.onPrivateKeyChange?.call(privateKeyController.text); }); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (widget.restoredWallet?.height != null) { + blockchainHeightKey.currentState?.restoreHeightController.text = widget.restoredWallet!.height.toString(); + } + }); } @override diff --git a/lib/src/screens/restore/wallet_restore_from_seed_form.dart b/lib/src/screens/restore/wallet_restore_from_seed_form.dart index f22deb3e1..d089f8c1c 100644 --- a/lib/src/screens/restore/wallet_restore_from_seed_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_seed_form.dart @@ -9,6 +9,7 @@ import 'package:cake_wallet/src/widgets/seed_language_picker.dart'; import 'package:cake_wallet/src/widgets/seed_widget.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; @@ -23,6 +24,7 @@ class WalletRestoreFromSeedForm extends StatefulWidget { required this.displayWalletPassword, required this.seedSettingsViewModel, this.blockHeightFocusNode, + this.restoredWallet, this.onHeightOrDateEntered, this.onSeedChange, this.onLanguageChange, @@ -36,6 +38,7 @@ class WalletRestoreFromSeedForm extends StatefulWidget { final bool displayWalletPassword; final SeedSettingsViewModel seedSettingsViewModel; final FocusNode? blockHeightFocusNode; + final RestoredWallet? restoredWallet; final Function(bool)? onHeightOrDateEntered; final void Function(String)? onSeedChange; final void Function(String)? onLanguageChange; @@ -184,6 +187,7 @@ class WalletRestoreFromSeedFormState extends State { Container(height: 20), SeedWidget( key: seedWidgetStateKey, + initialSeed: widget.restoredWallet?.mnemonicSeed, language: language, type: widget.type, onSeedChange: onSeedChange, diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index dcaf2f4c7..31f02617d 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -24,13 +24,13 @@ import 'package:mobx/mobx.dart'; class WalletRestorePage extends BasePage { WalletRestorePage(this.walletRestoreViewModel, this.seedSettingsViewModel) : walletRestoreFromSeedFormKey = GlobalKey(), - walletRestoreFromKeysFormKey = GlobalKey(), + walletRestoreFromKeysFormKey = GlobalKey(), _blockHeightFocusNode = FocusNode(); final WalletRestoreViewModel walletRestoreViewModel; final SeedSettingsViewModel seedSettingsViewModel; final GlobalKey walletRestoreFromSeedFormKey; - final GlobalKey walletRestoreFromKeysFormKey; + final GlobalKey walletRestoreFromKeysFormKey; final FocusNode _blockHeightFocusNode; bool _formProcessing = false; @@ -52,6 +52,10 @@ class WalletRestorePage extends BasePage { // String? derivationPath = null; DerivationInfo? derivationInfo; + + @override + Function(BuildContext)? get popWidget => (context) => seedSettingsViewModel.setPassphrase(null); + @override Function(BuildContext)? get pushToNextWidget => (context) { FocusScopeNode currentFocus = FocusScope.of(context); @@ -283,7 +287,7 @@ class _WalletRestorePageBody extends StatefulWidget { final WalletRestoreViewModel walletRestoreViewModel; final SeedSettingsViewModel seedSettingsViewModel; final GlobalKey walletRestoreFromSeedFormKey; - final GlobalKey walletRestoreFromKeysFormKey; + final GlobalKey walletRestoreFromKeysFormKey; final FocusNode blockHeightFocusNode; final DerivationInfo? derivationInfo; final void Function(DerivationInfo?) onDerivationInfoChanged; @@ -311,7 +315,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> final WalletRestoreViewModel walletRestoreViewModel; final SeedSettingsViewModel seedSettingsViewModel; final GlobalKey walletRestoreFromSeedFormKey; - final GlobalKey walletRestoreFromKeysFormKey; + final GlobalKey walletRestoreFromKeysFormKey; final FocusNode blockHeightFocusNode; DerivationInfo? derivationInfo; @@ -325,7 +329,14 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> _hasKeysTab = widget.walletRestoreViewModel.availableModes.contains(WalletRestoreMode.keys); final tabCount = _hasKeysTab ? 2 : 1; - _tabController = TabController(length: tabCount, vsync: this); + + final initialIndex = walletRestoreViewModel.mode == WalletRestoreMode.seed + ? 0 + : _hasKeysTab + ? 1 + : 0; + + _tabController = TabController(length: tabCount, vsync: this, initialIndex: initialIndex); _tabController.addListener(() { if (!_tabController.indexIsChanging) { @@ -442,9 +453,10 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> ); } - WalletRestoreFromKeysFrom _buildWalletRestoreFromKeysTab() { - return WalletRestoreFromKeysFrom( + WalletRestoreFromKeysForm _buildWalletRestoreFromKeysTab() { + return WalletRestoreFromKeysForm( key: widget.walletRestoreFromKeysFormKey, + restoredWallet: walletRestoreViewModel.restoredWallet, walletRestoreViewModel: widget.walletRestoreViewModel, displayPrivateKeyField: widget.walletRestoreViewModel.hasRestoreFromPrivateKey, displayWalletPassword: widget.walletRestoreViewModel.hasWalletPassword, @@ -466,6 +478,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> WalletRestoreFromSeedForm _buildWalletRestoreFromSeedTab() { return WalletRestoreFromSeedForm( key: widget.walletRestoreFromSeedFormKey, + restoredWallet: walletRestoreViewModel.restoredWallet, seedSettingsViewModel: widget.seedSettingsViewModel, displayBlockHeightSelector: widget.walletRestoreViewModel.hasBlockchainHeightLanguageSelector, displayLanguageSelector: widget.walletRestoreViewModel.hasSeedLanguageSelector, diff --git a/lib/src/widgets/seed_widget.dart b/lib/src/widgets/seed_widget.dart index 1c94718f1..7747c5fb6 100644 --- a/lib/src/widgets/seed_widget.dart +++ b/lib/src/widgets/seed_widget.dart @@ -14,6 +14,7 @@ class SeedWidget extends StatefulWidget { this.onSeedChange, this.pasteButtonKey, this.seedTextFieldKey, + this.initialSeed, super.key, }); final Key? seedTextFieldKey; @@ -21,17 +22,18 @@ class SeedWidget extends StatefulWidget { final String language; final WalletType type; final void Function(String)? onSeedChange; + final String? initialSeed; @override - SeedWidgetState createState() => SeedWidgetState(language, type); + SeedWidgetState createState() => SeedWidgetState(language, type, initialSeed); } class SeedWidgetState extends State { - SeedWidgetState(String language, this.type) - : controller = TextEditingController(), + SeedWidgetState(String language, this.type, String? initialSeed) + : controller = TextEditingController(text: initialSeed ?? ''), focusNode = FocusNode(), words = SeedValidator.getWordList(type: type, language: language), - _showPlaceholder = false { + _showPlaceholder = initialSeed == null || initialSeed.isEmpty { focusNode.addListener(() { setState(() { if (!focusNode.hasFocus && controller.text.isEmpty) { @@ -57,8 +59,12 @@ class SeedWidgetState extends State { @override void initState() { super.initState(); - _showPlaceholder = true; - controller.addListener(() => widget.onSeedChange?.call(text)); + controller.addListener(() { + setState(() { + _showPlaceholder = controller.text.isEmpty; + }); + widget.onSeedChange?.call(text); + }); } void changeSeedLanguage(String language) { diff --git a/lib/view_model/restore/restore_from_qr_vm.dart b/lib/view_model/restore/restore_from_qr_vm.dart deleted file mode 100644 index f31c93911..000000000 --- a/lib/view_model/restore/restore_from_qr_vm.dart +++ /dev/null @@ -1,219 +0,0 @@ -import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; -import 'package:cake_wallet/ethereum/ethereum.dart'; -import 'package:cake_wallet/nano/nano.dart'; -import 'package:cake_wallet/polygon/polygon.dart'; -import 'package:cake_wallet/solana/solana.dart'; -import 'package:cake_wallet/tron/tron.dart'; -import 'package:cake_wallet/view_model/restore/restore_mode.dart'; -import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; -import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; -import 'package:cake_wallet/wownero/wownero.dart'; -import 'package:cake_wallet/zano/zano.dart'; -import 'package:hive/hive.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/store/app_store.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cake_wallet/core/generate_wallet_password.dart'; -import 'package:cake_wallet/core/wallet_creation_service.dart'; -import 'package:cw_core/wallet_credentials.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; -import 'package:cw_core/wallet_info.dart'; - -part 'restore_from_qr_vm.g.dart'; - -class WalletRestorationFromQRVM = WalletRestorationFromQRVMBase with _$WalletRestorationFromQRVM; - -abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store { - WalletRestorationFromQRVMBase( - AppStore appStore, - WalletCreationService walletCreationService, - Box walletInfoSource, - WalletType type, - SeedSettingsViewModel seedSettingsViewModel) - : height = 0, - viewKey = '', - spendKey = '', - wif = '', - address = '', - super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, - type: type, isRecovery: true); - - @observable - int height; - - @observable - String viewKey; - - @observable - String spendKey; - - @observable - String wif; - - @observable - String address; - - bool get hasRestorationHeight => type == WalletType.monero || type == WalletType.wownero; - - @override - Future getWalletCredentialsFromQRCredentials( - RestoredWallet restoreWallet) async { - final password = generateWalletPassword(); - - switch (restoreWallet.restoreMode) { - case WalletRestoreMode.keys: - switch (restoreWallet.type) { - case WalletType.monero: - return monero!.createMoneroRestoreWalletFromKeysCredentials( - name: name, - password: password, - language: 'English', - address: restoreWallet.address ?? '', - viewKey: restoreWallet.viewKey ?? '', - spendKey: restoreWallet.spendKey ?? '', - height: restoreWallet.height ?? 0); - case WalletType.wownero: - return wownero!.createWowneroRestoreWalletFromKeysCredentials( - name: name, - password: password, - language: 'English', - address: restoreWallet.address ?? '', - viewKey: restoreWallet.viewKey ?? '', - spendKey: restoreWallet.spendKey ?? '', - height: restoreWallet.height ?? 0); - case WalletType.bitcoin: - case WalletType.litecoin: - return bitcoin!.createBitcoinRestoreWalletFromWIFCredentials( - name: name, password: password, wif: wif); - case WalletType.ethereum: - return ethereum!.createEthereumRestoreWalletFromPrivateKey( - name: name, password: password, privateKey: restoreWallet.privateKey!); - case WalletType.polygon: - return polygon!.createPolygonRestoreWalletFromPrivateKey( - name: name, password: password, privateKey: restoreWallet.privateKey!); - case WalletType.solana: - return solana!.createSolanaRestoreWalletFromPrivateKey( - name: name, password: password, privateKey: restoreWallet.privateKey!); - case WalletType.tron: - return tron!.createTronRestoreWalletFromPrivateKey( - name: name, password: password, privateKey: restoreWallet.privateKey!); - default: - throw Exception('Unexpected type: ${restoreWallet.type.toString()}'); - } - case WalletRestoreMode.seed: - switch (restoreWallet.type) { - case WalletType.monero: - return monero!.createMoneroRestoreWalletFromSeedCredentials( - name: name, - height: restoreWallet.height ?? 0, - mnemonic: restoreWallet.mnemonicSeed ?? '', - password: password, - passphrase: restoreWallet.passphrase ?? '', - ); - case WalletType.bitcoin: - case WalletType.litecoin: - - final derivationInfoList = await getDerivationInfoFromQRCredentials(restoreWallet); - DerivationInfo derivationInfo; - if (derivationInfoList.isEmpty) { - derivationInfo = getDefaultCreateDerivation()!; - } else { - derivationInfo = derivationInfoList.first; - } - return bitcoin!.createBitcoinRestoreWalletFromSeedCredentials( - name: name, - mnemonic: restoreWallet.mnemonicSeed ?? '', - password: password, - passphrase: restoreWallet.passphrase, - derivationType: derivationInfo.derivationType!, - derivationPath: derivationInfo.derivationPath!, - ); - case WalletType.bitcoinCash: - return bitcoinCash!.createBitcoinCashRestoreWalletFromSeedCredentials( - name: name, - mnemonic: restoreWallet.mnemonicSeed ?? '', - password: password, - passphrase: restoreWallet.passphrase, - ); - case WalletType.ethereum: - return ethereum!.createEthereumRestoreWalletFromSeedCredentials( - name: name, - mnemonic: restoreWallet.mnemonicSeed ?? '', - password: password, - passphrase: restoreWallet.passphrase, - ); - case WalletType.nano: - final derivationInfo = - (await getDerivationInfoFromQRCredentials(restoreWallet)).first; - return nano!.createNanoRestoreWalletFromSeedCredentials( - name: name, - mnemonic: restoreWallet.mnemonicSeed ?? '', - password: password, - derivationType: derivationInfo.derivationType!, - passphrase: restoreWallet.passphrase, - ); - case WalletType.polygon: - return polygon!.createPolygonRestoreWalletFromSeedCredentials( - name: name, - mnemonic: restoreWallet.mnemonicSeed ?? '', - password: password, - passphrase: restoreWallet.passphrase, - ); - case WalletType.solana: - return solana!.createSolanaRestoreWalletFromSeedCredentials( - name: name, - mnemonic: restoreWallet.mnemonicSeed ?? '', - password: password, - passphrase: restoreWallet.passphrase, - ); - case WalletType.tron: - return tron!.createTronRestoreWalletFromSeedCredentials( - name: name, - mnemonic: restoreWallet.mnemonicSeed ?? '', - password: password, - passphrase: restoreWallet.passphrase, - ); - case WalletType.wownero: - return wownero!.createWowneroRestoreWalletFromSeedCredentials( - name: name, - height: restoreWallet.height ?? 0, - mnemonic: restoreWallet.mnemonicSeed ?? '', - password: password, - passphrase: restoreWallet.passphrase ?? '', - ); - case WalletType.zano: - return zano!.createZanoRestoreWalletFromSeedCredentials( - name: name, - password: password, - height: height, - mnemonic: restoreWallet.mnemonicSeed ?? '', - passphrase: restoreWallet.passphrase ?? '', - ); - default: - throw Exception('Unexpected type: ${type.toString()}'); - } - default: - throw Exception('Unexpected type: ${type.toString()}'); - } - } - - @override - Future processFromRestoredWallet(WalletCredentials credentials, - RestoredWallet restoreWallet) async { - try { - switch (restoreWallet.restoreMode) { - case WalletRestoreMode.keys: - return walletCreationService.restoreFromKeys(credentials); - case WalletRestoreMode.seed: - return walletCreationService.restoreFromSeed(credentials); - default: - throw Exception('Unexpected restore mode: ${restoreWallet.restoreMode.toString()}'); - } - } catch (e) { - throw Exception('Unexpected restore mode: ${e.toString()}'); - } - } -} diff --git a/lib/view_model/restore/wallet_restore_from_qr_code.dart b/lib/view_model/restore/wallet_restore_from_qr_code.dart index e28a429a4..2e4e2da83 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -46,8 +46,6 @@ class WalletRestoreFromQRCode { 'zano_wallet': WalletType.zano, }; - static bool _containsAssetSpecifier(String code) => _extractWalletType(code) != null; - static WalletType? _extractWalletType(String code) { final sortedKeys = _walletTypeMap.keys.toList()..sort((a, b) => b.length.compareTo(a.length)); @@ -55,15 +53,18 @@ class WalletRestoreFromQRCode { if (extracted == null) { // Special case for view-only monero wallet - final codeParsed = json.decode(code); - if (codeParsed["version"] == 0 && - codeParsed["primaryAddress"] != null && - codeParsed["privateViewKey"] != null && - codeParsed["restoreHeight"] != null) { - return WalletType.monero; + try { + final codeParsed = json.decode(code); + if (codeParsed["version"] == 0 && + codeParsed["primaryAddress"] != null && + codeParsed["privateViewKey"] != null && + codeParsed["restoreHeight"] != null) { + return WalletType.monero; + } + } catch (e) { + return null; } } - return _walletTypeMap[extracted]; } @@ -93,10 +94,10 @@ class WalletRestoreFromQRCode { if (code == null) throw Exception("Unexpected scan QR code value: aborted"); if (code.isEmpty) throw Exception('Unexpected scan QR code value: value is empty'); - WalletType? walletType; String formattedUri = ''; + WalletType? walletType = _extractWalletType(code); - if (!_containsAssetSpecifier(code)) { + if (walletType == null) { await _specifyWalletAssets(context, "Can't determine wallet type, please pick it manually"); walletType = await Navigator.pushNamed(context, Routes.restoreWalletTypeFromQR) as WalletType?; @@ -108,7 +109,6 @@ class WalletRestoreFromQRCode { ? '$walletType:?seed=$seedPhrase' : throw Exception('Failed to determine valid seed phrase'); } else { - walletType = _extractWalletType(code); final index = code.indexOf(':'); final query = code.substring(index + 1).replaceAll('?', '&'); formattedUri = '$walletType:?$query'; @@ -118,12 +118,11 @@ class WalletRestoreFromQRCode { Map queryParameters = {...uri.queryParameters}; if (queryParameters['seed'] == null) { - queryParameters['seed'] = _extractSeedPhraseFromUrl(code, walletType!); + queryParameters['seed'] = _extractSeedPhraseFromUrl(code, walletType); } if (queryParameters['address'] == null) { - try { - queryParameters['address'] = _extractAddressFromUrl(code, walletType!); - } catch (_) {} + queryParameters['address'] = _extractAddressFromUrl(code, walletType); + } Map credentials = {'type': walletType, ...queryParameters, 'raw_qr': code}; diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index edaff441d..4ee8069b3 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -67,8 +67,8 @@ abstract class WalletCreationVMBase with Store { bool typeExists(WalletType type) => walletCreationService.typeExists(type); - Future create({dynamic options, RestoredWallet? restoreWallet}) async { - final type = restoreWallet?.type ?? this.type; + Future create({dynamic options}) async { + final type = this.type; try { state = IsExecutingState(); if (name.isEmpty) { @@ -87,9 +87,7 @@ abstract class WalletCreationVMBase with Store { final dirPath = await pathForWalletDir(name: name, type: type); final path = await pathForWallet(name: name, type: type); - final credentials = restoreWallet != null - ? await getWalletCredentialsFromQRCredentials(restoreWallet) - : getCredentials(options); + final credentials = getCredentials(options); final walletInfo = WalletInfo.external( id: WalletBase.idFor(name, type), @@ -107,9 +105,7 @@ abstract class WalletCreationVMBase with Store { ); credentials.walletInfo = walletInfo; - final wallet = restoreWallet != null - ? await processFromRestoredWallet(credentials, restoreWallet) - : await process(credentials); + final wallet = await process(credentials); final isNonSeedWallet = isRecovery ? wallet.seed == null : false; walletInfo.isNonSeedWallet = isNonSeedWallet; @@ -234,14 +230,6 @@ abstract class WalletCreationVMBase with Store { Future process(WalletCredentials credentials) => throw UnimplementedError(); - Future getWalletCredentialsFromQRCredentials( - RestoredWallet restoreWallet) async => - throw UnimplementedError(); - - Future processFromRestoredWallet( - WalletCredentials credentials, RestoredWallet restoreWallet) => - throw UnimplementedError(); - @action void toggleUseTestnet(bool? value) { _useTestnet = value ?? !_useTestnet; diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 3e5220447..e7df1c221 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -12,6 +12,7 @@ import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/view_model/restore/restore_mode.dart'; +import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; import 'package:cake_wallet/wownero/wownero.dart'; @@ -30,7 +31,7 @@ class WalletRestoreViewModel = WalletRestoreViewModelBase with _$WalletRestoreVi abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { WalletRestoreViewModelBase(AppStore appStore, WalletCreationService walletCreationService, Box walletInfoSource, SeedSettingsViewModel seedSettingsViewModel, - {required WalletType type}) + {required WalletType type, this.restoredWallet}) : hasSeedLanguageSelector = type == WalletType.monero || type == WalletType.haven || type == WalletType.wownero, hasBlockchainHeightLanguageSelector = @@ -42,7 +43,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { type == WalletType.solana || type == WalletType.tron, isButtonEnabled = false, - mode = WalletRestoreMode.seed, + mode = restoredWallet?.restoreMode ?? WalletRestoreMode.seed, super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, type: type, isRecovery: true) { switch (type) { @@ -68,6 +69,11 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { break; } walletCreationService.changeWalletType(type: type); + if (restoredWallet != null) { + if(restoredWallet!.restoreMode == WalletRestoreMode.seed) { + seedSettingsViewModel.setPassphrase(restoredWallet!.passphrase); + } + } } static const moneroSeedMnemonicLength = 25; @@ -76,6 +82,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { final bool hasSeedLanguageSelector; final bool hasBlockchainHeightLanguageSelector; final bool hasRestoreFromPrivateKey; + final RestoredWallet? restoredWallet; @observable WalletRestoreMode mode; From dda3b49da677add97d676a7f560f2e510cad02fe Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Sun, 16 Mar 2025 04:23:45 +0200 Subject: [PATCH 041/243] Fix LTC not saving passphrase (#2086) --- cw_bitcoin/lib/litecoin_wallet.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 5bd25785f..5e71bfd6d 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -77,6 +77,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { }) : super( mnemonic: mnemonic, password: password, + passphrase: passphrase, xpub: xpub, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, From b0269a7f14d6981fc94a04032a1865297c985e57 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Sun, 16 Mar 2025 04:45:46 +0200 Subject: [PATCH 042/243] Fix not saving passphrase --- cw_evm/lib/evm_chain_wallet.dart | 6 +++++- cw_nano/lib/nano_wallet.dart | 6 +++++- cw_solana/lib/solana_wallet.dart | 6 +++++- cw_tron/lib/tron_wallet.dart | 6 +++++- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index 9691993ad..d3efdaa7c 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -555,7 +555,11 @@ abstract class EVMChainWalletBase : null; @override - WalletKeysData get walletKeysData => WalletKeysData(mnemonic: _mnemonic, privateKey: privateKey); + WalletKeysData get walletKeysData => WalletKeysData( + mnemonic: _mnemonic, + privateKey: privateKey, + passphrase: passphrase, + ); String toJSON() => json.encode({ 'mnemonic': _mnemonic, diff --git a/cw_nano/lib/nano_wallet.dart b/cw_nano/lib/nano_wallet.dart index b48335857..eb2d2581c 100644 --- a/cw_nano/lib/nano_wallet.dart +++ b/cw_nano/lib/nano_wallet.dart @@ -337,7 +337,11 @@ abstract class NanoWalletBase String get hexSeed => _hexSeed!; @override - WalletKeysData get walletKeysData => WalletKeysData(mnemonic: _mnemonic, altMnemonic: hexSeed); + WalletKeysData get walletKeysData => WalletKeysData( + mnemonic: _mnemonic, + altMnemonic: hexSeed, + passphrase: passphrase, + ); String get representative => _representativeAddress ?? ""; diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index e6888cf91..1495189db 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -119,7 +119,11 @@ abstract class SolanaWalletBase String get privateKey => _solanaPrivateKey.seedHex(); @override - WalletKeysData get walletKeysData => WalletKeysData(mnemonic: _mnemonic, privateKey: privateKey); + WalletKeysData get walletKeysData => WalletKeysData( + mnemonic: _mnemonic, + privateKey: privateKey, + passphrase: passphrase, + ); Future init() async { final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${SPLToken.boxName}"; diff --git a/cw_tron/lib/tron_wallet.dart b/cw_tron/lib/tron_wallet.dart index b8d5eceec..c6175dd32 100644 --- a/cw_tron/lib/tron_wallet.dart +++ b/cw_tron/lib/tron_wallet.dart @@ -462,7 +462,11 @@ abstract class TronWalletBase String get privateKey => _tronPrivateKey.toHex(); @override - WalletKeysData get walletKeysData => WalletKeysData(mnemonic: _mnemonic, privateKey: privateKey); + WalletKeysData get walletKeysData => WalletKeysData( + mnemonic: _mnemonic, + privateKey: privateKey, + passphrase: passphrase, + ); String toJSON() => json.encode({ 'mnemonic': _mnemonic, From fee523cd811199ccdd738c333ebcd9483f02ed78 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Tue, 18 Mar 2025 17:33:38 +0200 Subject: [PATCH 043/243] update only zano on 4.23.3 (#2091) * update only zano on 4.23.3 * update versions [skip ci] --- assets/text/Release_Notes.txt | 4 +--- cw_haven/lib/haven_wallet.dart | 3 +++ cw_monero/pubspec.lock | 4 ++-- cw_monero/pubspec.yaml | 2 +- cw_wownero/pubspec.lock | 4 ++-- cw_wownero/pubspec.yaml | 2 +- cw_zano/pubspec.lock | 4 ++-- cw_zano/pubspec.yaml | 2 +- ios/Podfile.lock | 2 +- ios/Runner.xcodeproj/project.pbxproj | 4 ++++ macos/Podfile.lock | 2 +- scripts/android/app_env.sh | 4 ++-- scripts/ios/app_env.sh | 4 ++-- scripts/linux/app_env.sh | 4 ++-- scripts/macos/app_env.sh | 4 ++-- scripts/prepare_moneroc.sh | 2 +- scripts/windows/build_exe_installer.iss | 2 +- 17 files changed, 29 insertions(+), 24 deletions(-) diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index d1f91139b..0f8118b4e 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,3 +1 @@ -UI/UX enhancements -Stability improvements -Bug fixes \ No newline at end of file +Update for Zano's Hard fork \ No newline at end of file diff --git a/cw_haven/lib/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart index 515fa624e..6316f5a62 100644 --- a/cw_haven/lib/haven_wallet.dart +++ b/cw_haven/lib/haven_wallet.dart @@ -72,6 +72,9 @@ abstract class HavenWalletBase @override String get seed => haven_wallet.getSeed(); + @override + bool get hasRescan => true; + @override MoneroWalletKeys get keys => MoneroWalletKeys( primaryAddress: haven_wallet.getAddress(accountIndex: 0, addressIndex: 0), diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index f1510b4bc..56451735a 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -511,8 +511,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: "65608c09e9093f1cd42c6afd8e9131016c82574b" - resolved-ref: "65608c09e9093f1cd42c6afd8e9131016c82574b" + ref: "84e52393e395d75f449bcd81e23028889538118f" + resolved-ref: "84e52393e395d75f449bcd81e23028889538118f" url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index 6a96e41cc..58c5fc968 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -25,7 +25,7 @@ dependencies: monero: git: url: https://github.com/mrcyjanek/monero_c - ref: 65608c09e9093f1cd42c6afd8e9131016c82574b + ref: 84e52393e395d75f449bcd81e23028889538118f path: impls/monero.dart mutex: ^3.1.0 ledger_flutter_plus: ^1.4.1 diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index 44182ed9f..645beb141 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -471,8 +471,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: "65608c09e9093f1cd42c6afd8e9131016c82574b" - resolved-ref: "65608c09e9093f1cd42c6afd8e9131016c82574b" + ref: "84e52393e395d75f449bcd81e23028889538118f" + resolved-ref: "84e52393e395d75f449bcd81e23028889538118f" url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" diff --git a/cw_wownero/pubspec.yaml b/cw_wownero/pubspec.yaml index a57c2f25b..d81cda250 100644 --- a/cw_wownero/pubspec.yaml +++ b/cw_wownero/pubspec.yaml @@ -25,7 +25,7 @@ dependencies: monero: git: url: https://github.com/mrcyjanek/monero_c - ref: 65608c09e9093f1cd42c6afd8e9131016c82574b # monero_c hash + ref: 84e52393e395d75f449bcd81e23028889538118f # monero_c hash path: impls/monero.dart mutex: ^3.1.0 diff --git a/cw_zano/pubspec.lock b/cw_zano/pubspec.lock index 74a87487e..55246ad3a 100644 --- a/cw_zano/pubspec.lock +++ b/cw_zano/pubspec.lock @@ -476,8 +476,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: "65608c09e9093f1cd42c6afd8e9131016c82574b" - resolved-ref: "65608c09e9093f1cd42c6afd8e9131016c82574b" + ref: "84e52393e395d75f449bcd81e23028889538118f" + resolved-ref: "84e52393e395d75f449bcd81e23028889538118f" url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" diff --git a/cw_zano/pubspec.yaml b/cw_zano/pubspec.yaml index 111731f4f..b036a874f 100644 --- a/cw_zano/pubspec.yaml +++ b/cw_zano/pubspec.yaml @@ -26,7 +26,7 @@ dependencies: monero: git: url: https://github.com/mrcyjanek/monero_c - ref: 65608c09e9093f1cd42c6afd8e9131016c82574b # monero_c hash + ref: 84e52393e395d75f449bcd81e23028889538118f # monero_c hash path: impls/monero.dart dev_dependencies: flutter_test: diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5f8a14daa..57d4589c9 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -239,4 +239,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: e448f662d4c41f0c0b1ccbb78afd57dbf895a597 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 864d6c420..d9ff6b66e 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -486,6 +486,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; @@ -633,6 +634,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; @@ -657,6 +659,7 @@ "$(PROJECT_DIR)/Flutter", ); MARKETING_VERSION = 1.0.1; + ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -672,6 +675,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 4445e5976..689f0ea03 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -129,4 +129,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 65ec1541137fb5b35d00490dec1bb48d4d9586bb -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 9419ae1c6..41095ef26 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -22,8 +22,8 @@ MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.23.3" -CAKEWALLET_BUILD_NUMBER=248 +CAKEWALLET_VERSION="4.23.4" +CAKEWALLET_BUILD_NUMBER=249 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 491b7d06a..7d5bfff38 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -18,8 +18,8 @@ MONERO_COM_BUILD_NUMBER=113 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.23.3" -CAKEWALLET_BUILD_NUMBER=301 +CAKEWALLET_VERSION="4.23.4" +CAKEWALLET_BUILD_NUMBER=302 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/linux/app_env.sh b/scripts/linux/app_env.sh index 91ab2cdeb..ef1747ba6 100755 --- a/scripts/linux/app_env.sh +++ b/scripts/linux/app_env.sh @@ -14,8 +14,8 @@ if [ -n "$1" ]; then fi CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.13.3" -CAKEWALLET_BUILD_NUMBER=48 +CAKEWALLET_VERSION="1.13.4" +CAKEWALLET_BUILD_NUMBER=49 if ! [[ " ${TYPES[*]} " =~ " ${APP_LINUX_TYPE} " ]]; then echo "Wrong app type." diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index 234d07c66..7410ee3a3 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -21,8 +21,8 @@ MONERO_COM_BUILD_NUMBER=45 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.16.3" -CAKEWALLET_BUILD_NUMBER=106 +CAKEWALLET_VERSION="1.16.4" +CAKEWALLET_BUILD_NUMBER=107 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then diff --git a/scripts/prepare_moneroc.sh b/scripts/prepare_moneroc.sh index ec2cb4908..133c0ab2f 100755 --- a/scripts/prepare_moneroc.sh +++ b/scripts/prepare_moneroc.sh @@ -8,7 +8,7 @@ if [[ ! -d "monero_c/.git" ]]; then git clone https://github.com/mrcyjanek/monero_c --branch master monero_c cd monero_c - git checkout 65608c09e9093f1cd42c6afd8e9131016c82574b + git checkout 84e52393e395d75f449bcd81e23028889538118f git reset --hard git submodule update --init --force --recursive ./apply_patches.sh monero diff --git a/scripts/windows/build_exe_installer.iss b/scripts/windows/build_exe_installer.iss index 3fb0cd1aa..211017eff 100644 --- a/scripts/windows/build_exe_installer.iss +++ b/scripts/windows/build_exe_installer.iss @@ -1,5 +1,5 @@ #define MyAppName "Cake Wallet" -#define MyAppVersion "0.4.3" +#define MyAppVersion "0.4.4" #define MyAppPublisher "Cake Labs LLC" #define MyAppURL "https://cakewallet.com/" #define MyAppExeName "CakeWallet.exe" From cec414e44bd3dea69c36816fa4621768b3a9a6d8 Mon Sep 17 00:00:00 2001 From: cyan Date: Wed, 19 Mar 2025 01:01:04 +0100 Subject: [PATCH 044/243] add ci check for restricted imports that would prevent monero.com from building (#2093) cherry picked from old bgsync pr --- .github/workflows/no_restricted_imports.yaml | 47 ++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/no_restricted_imports.yaml diff --git a/.github/workflows/no_restricted_imports.yaml b/.github/workflows/no_restricted_imports.yaml new file mode 100644 index 000000000..4b17de31a --- /dev/null +++ b/.github/workflows/no_restricted_imports.yaml @@ -0,0 +1,47 @@ +name: No restricted imports in lib directory + +on: [pull_request] + +jobs: + check_restricted_imports: + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v4 + - name: Check for restricted imports in lib directory + if: github.event_name == 'pull_request' + run: | + RESTRICTED_PACKAGES=( + "cw_bitcoin" + "cw_bitcoin_cash" + "cw_ethereum" + "cw_evm" + "cw_haven" + "cw_mweb" + "cw_nano" + "cw_polygon" + "cw_solana" + "cw_tron" + "cw_wownero" + "cw_zano" + ) + + FOUND_RESTRICTED=false + + for package in "${RESTRICTED_PACKAGES[@]}"; do + GREP_RESULT=$(find lib -type f -name "*.dart" -exec grep -l "import.*package:$package" {} \; || true) + + if [ -n "$GREP_RESULT" ]; then + echo "Found restricted import of '$package' in the following files:" + echo "$GREP_RESULT" + FOUND_RESTRICTED=true + fi + done + + if [ "$FOUND_RESTRICTED" = true ]; then + echo "Error: Restricted package imports found in lib/ directory" + echo "Please remove these imports as they are not allowed in the lib/ directory" + exit 1 + else + echo "No restricted imports found. All good!" + fi \ No newline at end of file From 2c68c57d10fe3ee4977ba1681e67b158666f8ea1 Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Tue, 18 Mar 2025 19:52:22 -0700 Subject: [PATCH 045/243] Don't export / import pin codes when making / restoring backups (#2089) * initial commit * remove print --- lib/core/backup_service.dart | 8 +------- lib/src/screens/pin_code/pin_code_widget.dart | 6 +++--- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 03f20363d..df9442306 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -449,8 +449,6 @@ class BackupService { final keychainJSON = json.decode(utf8.decode(decryptedKeychainDumpFileData)) as Map; final keychainWalletsInfo = keychainJSON['wallets'] as List; - final decodedPin = keychainJSON['pin'] as String; - final pinCodeKey = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword); final backupPassword = keychainJSON[backupPasswordKey] as String; @@ -461,8 +459,6 @@ class BackupService { await importWalletKeychainInfo(info); }); - await _secureStorage.write(key: pinCodeKey, value: encodedPinCode(pin: decodedPin)); - keychainDumpFile.deleteSync(); } @@ -481,8 +477,6 @@ class BackupService { Future _exportKeychainDumpV2(String password, {String keychainSalt = secrets.backupKeychainSalt}) async { final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); - final encodedPin = await _secureStorage.read(key: key); - final decodedPin = decodedPinCode(pin: encodedPin!); final wallets = await Future.wait(_walletInfoSource.values.map((walletInfo) async { return { 'name': walletInfo.name, @@ -493,7 +487,7 @@ class BackupService { final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword); final backupPassword = await _secureStorage.read(key: backupPasswordKey); final data = utf8.encode( - json.encode({'pin': decodedPin, 'wallets': wallets, backupPasswordKey: backupPassword})); + json.encode({'wallets': wallets, backupPasswordKey: backupPassword})); final encrypted = await _encryptV2(Uint8List.fromList(data), '$keychainSalt$password'); return encrypted; diff --git a/lib/src/screens/pin_code/pin_code_widget.dart b/lib/src/screens/pin_code/pin_code_widget.dart index dcd875d7a..f6249576d 100644 --- a/lib/src/screens/pin_code/pin_code_widget.dart +++ b/lib/src/screens/pin_code/pin_code_widget.dart @@ -119,11 +119,11 @@ class PinCodeState extends State { color: Theme.of(context).extension()!.titleColor, ); - return RawKeyboardListener( + return KeyboardListener( focusNode: FocusNode(), autofocus: true, - onKey: (keyEvent) { - if (keyEvent is RawKeyDownEvent) { + onKeyEvent: (keyEvent) { + if (keyEvent is KeyDownEvent) { if (keyEvent.logicalKey.keyLabel == "Backspace") { _pop(); return; From 52a39e29d457f2060512216ffda622287e54c9a1 Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Wed, 19 Mar 2025 10:09:12 -0700 Subject: [PATCH 046/243] dont export/import pinlength (#2100) --- lib/core/backup_service.dart | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index df9442306..76f54aa14 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -275,7 +275,6 @@ class BackupService { final currentLanguageCode = data[PreferencesKey.currentLanguageCode] as String?; final displayActionListMode = data[PreferencesKey.displayActionListModeKey] as int?; final fiatApiMode = data[PreferencesKey.currentFiatApiModeKey] as int?; - final currentPinLength = data[PreferencesKey.currentPinLength] as int?; final currentTheme = data[PreferencesKey.currentTheme] as int?; final exchangeStatus = data[PreferencesKey.exchangeStatusKey] as int?; final currentDefaultSettingsMigrationVersion = @@ -345,9 +344,6 @@ class BackupService { await _sharedPreferences.setInt( PreferencesKey.autoGenerateSubaddressStatusKey, autoGenerateSubaddressStatus); - if (currentPinLength != null) - await _sharedPreferences.setInt(PreferencesKey.currentPinLength, currentPinLength); - if (currentTheme != null && DeviceInfo.instance.isMobile) { await _sharedPreferences.setInt(PreferencesKey.currentTheme, currentTheme); // enforce dark theme on desktop platforms until the design is ready: @@ -506,7 +502,6 @@ class BackupService { PreferencesKey.shouldSaveRecipientAddressKey: _sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey), PreferencesKey.disableTradeOption: _sharedPreferences.getBool(PreferencesKey.disableTradeOption), - PreferencesKey.currentPinLength: _sharedPreferences.getInt(PreferencesKey.currentPinLength), PreferencesKey.currentTransactionPriorityKeyLegacy: _sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy), PreferencesKey.currentBitcoinElectrumSererIdKey: From 0ba54fa60283b34f03ddac061911c84bd342ef89 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Fri, 21 Mar 2025 04:18:47 +0200 Subject: [PATCH 047/243] Add decred (#1938) * decred: Add decred. (#1322) * multi: Add initial decred screens. (#1165) Use a mock libwallet for now. * cw_decred: add libdcrwallet dependency and link library for android, ios and macos (#1240) * change cw_decred from package to plugin * add libdcrwallet dependency and link library for android, ios and macos * remove spvwallet, make some libdcrwallet fns async, light refactor * libdcrwallet: use json payload returns * use specific libwallet commit hash * decred: fix Rename wallet. --------- Co-authored-by: JoeGruff * decred: Add sync. * decred: Add send transaction. * decred: Fix fee estimation. * decred: List transactions. * decred: Add rescan. * decred: Sign message. * decred: Add new addr and addrs. * decred: Add change wallet pass. * decred: Add restore from seed. * decred: Add watching only wallets. * decred: Enable mainnet. * decred: Allow using blank node address. This allows a persistent peer to be unset, falling back to decred seeders. * decred: Rescan from wallet birthday. * add and update macos build scripts, update build readme, gitignore macos project.pbxproj Signed-off-by: Philemon Ukane * multi: hide decred rescan page if it's not ready - move hasRescan method to WalletBase and implement for decred Signed-off-by: Philemon Ukane * cw_decred: fix bug where decred wallets are not loaded after app restart Signed-off-by: Philemon Ukane * add buy and sell for decred via onramp Signed-off-by: Philemon Ukane * bug-fix: account for other send outputs that are part of the same tx Signed-off-by: Philemon Ukane * decred: Return address with no peers. * decred: Update pubspec. * decred: Add verify message. * upgrade hive_generator dep in cw_decred * decred: Clean up code. --------- Signed-off-by: Philemon Ukane Co-authored-by: Wisdom Arerosuoghene Co-authored-by: Philemon Ukane * fix extracted addresses not used fix conflicts with main * remove print [skip ci] * minor formatting * fix initial migration version * add build decred script to workflow * install go before build decred fix switch cases * trial 2 to fix decred build * re-install go * revert build script change * refactor/clean nodes functions * Fix address book issue Fix send ALL (to be continued with the fees point) * Fix transactions display issues Add missing file * Fix unconfirmed balance not displayed Change Wallet order Minor cleanup * Fix workflow * Fix workflow * Fix workflow * test * hardcode path for now * fix + cleanup decred build script to work on mac and linux * Update decred build script * Run actions on pull requests, extract commit message * run after checkout * add safe directory * Get commit message from base.sha instead of last commit * base -> head * Do not merge main branch into pr * [skip slack] [run tests] clone by sha * Proper name for decred library in the build script * Throw an error when ANDROID_HOME or ANDROID_NDK_VERSION is missing * Fix conflicts with main * minor code enhancement * decred: Add used address history. (#1941) * decred: Update pubspec. * decred testnet * decred: Add used address history. * decred: Remove default node list. * populate transaction history before sync begins * decred: Add some awaits. * decred: Fix send all. * decred: Add clang export to build script. * decred: Update logo colors. * cleanup cw_decred.dart * make decred wallet addresses selectable in receive page * decred: Always set default addr when used. * decred: Add back default node list. * decred: Allow creating addresses manually. --------- Co-authored-by: Wisdom Arerosuoghene Co-authored-by: Omar Hatem * minor fixes and cleanup * minor fix, feel free to test now * - Fix transaction details - Fix Nodes - Add processing sync status * Add decred info card * push missing file * Add missing text for decred info card * minor: change docs link [skip ci] * decred: Update derivation info. (#2013) * decred: Update derivation info. * decred: Allow unsynced unused addresses. * decred: Update dcrwallet dep to 4.3.0. * Merge main and fix conflicts * Merge main and fix conflicts * decred: Fix background sync panic. (#2080) * decred: Run libwallet in isolate. (#2077) * decred: Fix contact save inquiry. (#2083) Also fix tx time and the fee shown on pending transactions. * Disable send button in view only decred wallets * - Fix frozen coins - Add URI support - Fix fees in tx details - Handle empty coins send - Handle wallets in address book * Merge main * remove print [skip ci] * Fix restore from QR * minor improvement for QR restore * minor fixes [skip ci] * decred: Get slip44 addrs before sync completes. (#2092) * - Fix loading wallet more than one time - Fix minor UI issue --------- Signed-off-by: Philemon Ukane Co-authored-by: JoeGruffins <34998433+JoeGruffins@users.noreply.github.com> Co-authored-by: Wisdom Arerosuoghene Co-authored-by: Philemon Ukane Co-authored-by: Czarek Nakamoto --- .github/workflows/no_print_in_dart.yaml | 4 +- .github/workflows/pr_test_build_android.yml | 12 +- .github/workflows/pr_test_build_linux.yml | 1 + .gitignore | 3 + android/app/src/main/AndroidManifestBase.xml | 3 + assets/decred_node_list.yml | 6 + assets/images/2.0x/decred.png | Bin 0 -> 2112 bytes assets/images/2.0x/decred_menu.png | Bin 0 -> 1871 bytes assets/images/3.0x/decred.png | Bin 0 -> 2918 bytes assets/images/3.0x/decred_menu.png | Bin 0 -> 1423 bytes assets/images/dcr_icon.png | Bin 117789 -> 91254 bytes assets/images/decred.png | Bin 0 -> 1237 bytes assets/images/decred_icon.png | Bin 0 -> 1542 bytes assets/images/decred_menu.png | Bin 0 -> 1482 bytes cw_bitcoin/lib/bitcoin_wallet.dart | 3 + cw_bitcoin/lib/litecoin_wallet.dart | 4 +- cw_core/lib/amount_converter.dart | 1 + cw_core/lib/currency_for_wallet_type.dart | 7 +- cw_core/lib/node.dart | 18 + cw_core/lib/receive_page_option.dart | 1 + cw_core/lib/sync_status.dart | 10 + cw_core/lib/wallet_base.dart | 4 + cw_core/lib/wallet_type.dart | 15 +- cw_core/pubspec.lock | 18 + cw_decred/.gitignore | 39 + cw_decred/.metadata | 36 + cw_decred/CHANGELOG.md | 3 + cw_decred/LICENSE | 1 + cw_decred/README.md | 3 + cw_decred/analysis_options.yaml | 4 + cw_decred/android/.gitignore | 9 + cw_decred/android/build.gradle | 59 + cw_decred/android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 4 + .../cakewallet/cw_decred/CwDecredPlugin.kt | 35 + cw_decred/ios/.gitignore | 38 + cw_decred/ios/Assets/.gitkeep | 0 cw_decred/ios/Classes/CwDecredPlugin.swift | 19 + cw_decred/ios/cw_decred.podspec | 22 + cw_decred/lib/amount_format.dart | 26 + cw_decred/lib/api/libdcrwallet.dart | 693 ++++++ cw_decred/lib/api/util.dart | 64 + cw_decred/lib/balance.dart | 25 + cw_decred/lib/mnemonic.dart | 2050 +++++++++++++++++ cw_decred/lib/pending_transaction.dart | 39 + cw_decred/lib/transaction_credentials.dart | 10 + cw_decred/lib/transaction_history.dart | 31 + cw_decred/lib/transaction_info.dart | 45 + cw_decred/lib/transaction_priority.dart | 69 + cw_decred/lib/wallet.dart | 729 ++++++ cw_decred/lib/wallet_addresses.dart | 137 ++ .../lib/wallet_creation_credentials.dart | 40 + cw_decred/lib/wallet_service.dart | 186 ++ cw_decred/macos/Classes/CwDecredPlugin.swift | 19 + cw_decred/macos/cw_decred.podspec | 22 + cw_decred/pubspec.lock | 852 +++++++ cw_decred/pubspec.yaml | 84 + cw_monero/lib/monero_wallet.dart | 3 + cw_wownero/lib/wownero_wallet.dart | 3 + ios/Podfile.lock | 43 + ios/Runner/InfoBase.plist | 40 + lib/core/address_validator.dart | 2 +- lib/core/node_address_validator.dart | 11 + lib/core/seed_validator.dart | 3 + lib/core/sync_status_title.dart | 4 + lib/core/wallet_creation_service.dart | 1 + lib/decred/cw_decred.dart | 114 + lib/di.dart | 6 +- lib/entities/default_settings_migration.dart | 851 +++---- lib/entities/main_actions.dart | 1 + lib/entities/node_list.dart | 262 +-- lib/entities/preferences_key.dart | 3 + lib/entities/priority_for_wallet_type.dart | 3 + lib/entities/provider_types.dart | 2 + lib/main.dart | 2 +- lib/reactions/bip39_wallet_utils.dart | 1 + lib/reactions/check_connection.dart | 3 + lib/reactions/fiat_rate_update.dart | 1 - lib/reactions/on_current_wallet_change.dart | 3 +- .../on_wallet_sync_status_change.dart | 2 +- .../desktop_wallet_selection_dropdown.dart | 3 + .../pages/balance/crypto_balance_widget.dart | 106 +- .../dashboard/pages/navigation_dock.dart | 6 +- .../screens/dashboard/widgets/info_card.dart | 88 + .../dashboard/widgets/menu_widget.dart | 6 +- .../dashboard/widgets/sync_indicator.dart | 4 +- .../advanced_privacy_settings_page.dart | 7 +- .../nodes/node_create_or_edit_page.dart | 5 +- lib/src/screens/nodes/widgets/node_form.dart | 5 +- lib/src/screens/rescan/rescan_page.dart | 71 +- .../wallet_restore_from_keys_form.dart | 19 + .../screens/restore/wallet_restore_page.dart | 27 +- .../screens/wallet_keys/wallet_keys_page.dart | 30 +- .../screens/wallet_list/wallet_list_page.dart | 15 +- lib/src/widgets/blockchain_height_widget.dart | 7 +- lib/src/widgets/seed_widget.dart | 3 + lib/store/settings_store.dart | 38 +- .../advanced_privacy_settings_view_model.dart | 1 + .../contact_list/contact_list_view_model.dart | 12 +- .../contact_list/contact_view_model.dart | 12 +- .../dashboard/balance_view_model.dart | 1 + .../dashboard/dashboard_view_model.dart | 66 +- .../dashboard/home_settings_view_model.dart | 1 + .../dashboard/receive_option_view_model.dart | 15 +- .../dashboard/transaction_list_item.dart | 13 +- .../exchange/exchange_view_model.dart | 11 +- .../node_create_or_edit_view_model.dart | 7 +- .../node_list/node_list_view_model.dart | 48 +- .../restore/wallet_restore_from_qr_code.dart | 12 +- lib/view_model/send/fees_view_model.dart | 9 +- lib/view_model/send/output.dart | 8 + lib/view_model/send/send_view_model.dart | 95 +- .../settings/privacy_settings_view_model.dart | 3 +- .../transaction_details_view_model.dart | 56 +- .../unspent_coins_list_view_model.dart | 9 +- ...let_address_edit_or_create_view_model.dart | 68 +- .../wallet_address_list_view_model.dart | 32 +- lib/view_model/wallet_keys_view_model.dart | 9 + lib/view_model/wallet_new_vm.dart | 5 + lib/view_model/wallet_restore_view_model.dart | 24 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile.lock | 8 + model_generator.sh | 8 +- pubspec_base.yaml | 1 + res/values/strings_ar.arb | 3 + res/values/strings_bg.arb | 3 + res/values/strings_cs.arb | 3 + res/values/strings_de.arb | 3 + res/values/strings_en.arb | 3 + res/values/strings_es.arb | 3 + res/values/strings_fr.arb | 3 + res/values/strings_ha.arb | 3 + res/values/strings_hi.arb | 3 + res/values/strings_hr.arb | 3 + res/values/strings_hy.arb | 3 + res/values/strings_id.arb | 3 + res/values/strings_it.arb | 3 + res/values/strings_ja.arb | 3 + res/values/strings_ko.arb | 3 + res/values/strings_my.arb | 3 + res/values/strings_nl.arb | 3 + res/values/strings_pl.arb | 3 + res/values/strings_pt.arb | 3 + res/values/strings_ru.arb | 3 + res/values/strings_th.arb | 3 + res/values/strings_tl.arb | 3 + res/values/strings_tr.arb | 3 + res/values/strings_uk.arb | 3 + res/values/strings_ur.arb | 3 + res/values/strings_vi.arb | 3 + res/values/strings_yo.arb | 3 + res/values/strings_zh.arb | 3 + scripts/android/.gitignore | 3 +- scripts/android/build_all.sh | 3 +- scripts/android/build_decred.sh | 84 + scripts/android/pubspec_gen.sh | 4 +- scripts/ios/app_config.sh | 8 +- scripts/ios/build_all.sh | 2 +- scripts/ios/build_decred.sh | 35 + scripts/ios/build_zmq.sh | 2 +- scripts/macos/app_config.sh | 2 +- scripts/macos/build_all.sh | 2 +- scripts/macos/build_boost_arm64.sh | 1 + scripts/macos/build_boost_common.sh | 8 +- scripts/macos/build_decred.sh | 34 + scripts/macos/build_expat.sh | 4 + scripts/macos/build_monero.sh | 4 + scripts/macos/build_openssl_arm64.sh | 1 + scripts/macos/build_openssl_common.sh | 4 +- scripts/macos/build_sodium.sh | 4 +- scripts/macos/build_unbound.sh | 5 +- scripts/macos/build_zmq.sh | 4 +- scripts/macos/gen.sh | 6 + scripts/macos/gen_common.sh | 4 +- tool/configure.dart | 108 +- 175 files changed, 7145 insertions(+), 1115 deletions(-) create mode 100644 assets/decred_node_list.yml create mode 100644 assets/images/2.0x/decred.png create mode 100644 assets/images/2.0x/decred_menu.png create mode 100644 assets/images/3.0x/decred.png create mode 100644 assets/images/3.0x/decred_menu.png create mode 100644 assets/images/decred.png create mode 100644 assets/images/decred_icon.png create mode 100644 assets/images/decred_menu.png create mode 100644 cw_decred/.gitignore create mode 100644 cw_decred/.metadata create mode 100644 cw_decred/CHANGELOG.md create mode 100644 cw_decred/LICENSE create mode 100644 cw_decred/README.md create mode 100644 cw_decred/analysis_options.yaml create mode 100644 cw_decred/android/.gitignore create mode 100644 cw_decred/android/build.gradle create mode 100644 cw_decred/android/settings.gradle create mode 100644 cw_decred/android/src/main/AndroidManifest.xml create mode 100644 cw_decred/android/src/main/kotlin/com/cakewallet/cw_decred/CwDecredPlugin.kt create mode 100644 cw_decred/ios/.gitignore create mode 100644 cw_decred/ios/Assets/.gitkeep create mode 100644 cw_decred/ios/Classes/CwDecredPlugin.swift create mode 100644 cw_decred/ios/cw_decred.podspec create mode 100644 cw_decred/lib/amount_format.dart create mode 100644 cw_decred/lib/api/libdcrwallet.dart create mode 100644 cw_decred/lib/api/util.dart create mode 100644 cw_decred/lib/balance.dart create mode 100644 cw_decred/lib/mnemonic.dart create mode 100644 cw_decred/lib/pending_transaction.dart create mode 100644 cw_decred/lib/transaction_credentials.dart create mode 100644 cw_decred/lib/transaction_history.dart create mode 100644 cw_decred/lib/transaction_info.dart create mode 100644 cw_decred/lib/transaction_priority.dart create mode 100644 cw_decred/lib/wallet.dart create mode 100644 cw_decred/lib/wallet_addresses.dart create mode 100644 cw_decred/lib/wallet_creation_credentials.dart create mode 100644 cw_decred/lib/wallet_service.dart create mode 100644 cw_decred/macos/Classes/CwDecredPlugin.swift create mode 100644 cw_decred/macos/cw_decred.podspec create mode 100644 cw_decred/pubspec.lock create mode 100644 cw_decred/pubspec.yaml create mode 100644 lib/decred/cw_decred.dart create mode 100644 lib/src/screens/dashboard/widgets/info_card.dart create mode 100755 scripts/android/build_decred.sh create mode 100755 scripts/ios/build_decred.sh create mode 100755 scripts/macos/build_decred.sh create mode 100755 scripts/macos/gen.sh diff --git a/.github/workflows/no_print_in_dart.yaml b/.github/workflows/no_print_in_dart.yaml index 8cd24edfe..b321a9cc9 100644 --- a/.github/workflows/no_print_in_dart.yaml +++ b/.github/workflows/no_print_in_dart.yaml @@ -1,8 +1,6 @@ name: No print statements in dart files -on: - pull_request: - branches: [main] +on: [pull_request] jobs: PR_test_build: diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 762144ac1..61fb67ee2 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -47,6 +47,7 @@ jobs: echo "message<> $GITHUB_ENV echo "$FULL_MESSAGE" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV + - name: Add secrets run: | touch lib/.secrets.g.dart @@ -243,6 +244,13 @@ jobs: ./build_mwebd.sh --dont-install popd + - name: Build Decred + run: | + set -x -e + pushd scripts/android + ./build_decred.sh + popd + - name: Build generated code run: | ./model_generator.sh async @@ -281,7 +289,7 @@ jobs: set -x apk_file=$(ls build/app/outputs/flutter-apk/test-apk/${BRANCH_NAME}.apk || exit 1) echo "APK_FILE=$apk_file" >> $GITHUB_ENV - + - name: Upload artifact to slack if: ${{ !contains(env.message, 'skip slack') }} continue-on-error: true @@ -294,7 +302,7 @@ jobs: - name: cleanup run: rm -rf build/app/outputs/flutter-apk/test-apk/ - + - name: Upload Artifact to github uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index c5cb26dd9..53904149f 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -32,6 +32,7 @@ jobs: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} + repository: ${{ github.event.pull_request.head.repo.full_name }} - name: configure git run: | git config --global --add safe.directory '*' diff --git a/.gitignore b/.gitignore index c431a7f60..e78b1c4e6 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ .history .svn/ .fvm/ +.fvmrc # IntelliJ related *.iml @@ -138,6 +139,7 @@ lib/solana/solana.dart lib/tron/tron.dart lib/wownero/wownero.dart lib/zano/zano.dart +lib/decred/decred.dart ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png @@ -171,6 +173,7 @@ macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png macos/Runner/Configs/AppInfo.xcconfig +macos/Runner.xcodeproj/project.pbxproj integration_test/playground.dart diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index 5a1824a17..9a324edf3 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -92,6 +92,9 @@ + + + diff --git a/assets/decred_node_list.yml b/assets/decred_node_list.yml new file mode 100644 index 000000000..cb171e701 --- /dev/null +++ b/assets/decred_node_list.yml @@ -0,0 +1,6 @@ +- + uri: default-spv-nodes + is_default: true +- + uri: dcrd.sethforprivacy.com:9108 + useSSL: true \ No newline at end of file diff --git a/assets/images/2.0x/decred.png b/assets/images/2.0x/decred.png new file mode 100644 index 0000000000000000000000000000000000000000..2f4919cecda0ccdff5eed70d5b53938be2e5ea27 GIT binary patch literal 2112 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 QBLDyZ07*qoM6N<$f<`ZExc~qF literal 0 HcmV?d00001 diff --git a/assets/images/3.0x/decred_menu.png b/assets/images/3.0x/decred_menu.png new file mode 100644 index 0000000000000000000000000000000000000000..e55b3fb5c843a0d48b496d8a90a4de1868af8e2b GIT binary patch literal 1423 zcmV;A1#tR_P)EX>4Tx04R}tkv&MmKpe$iTeYQBL@X%ckfA!+MMWHI6^c+H)C#RSm|XfHG-*gu zTpR`0f`cE6RRU7TlmpZjz4Dg~1PK7lyFbi*QEC!XH4 zbk6(4QC5}|;&bA0gDyz?$aUG}H_j!8{X8>jWHWQbQDULg!Ey()vY`@B5yuo&qkMnP zWrgz=XSGset$XqpMhe>U64z;tAcX}iLWBSrHEf^)3vpUCQcPrMKk4BgcKjl_WO8kQ zkz*cJsE`~#_#gc4)+|m=xJi*D(EVcDAEQ8U7iiXP`}^3onpV2qvfq`2fyz2JW+Q;bwkfpBDH^9Lm zFjk`MHIH|PI(z%~Osl^iI~sD3>NA5u00006VoOIv00000008+zyMF)x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru=LiZ0J0zTBU1R_N02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00U=9L_t(|+U?!hQ6xbO1kjaY?86g$7$5Ki2jPo|{Q!dL z(UF;zZ2*+&wd&}pBmN0IulM(lj=%eMe+l-w34>~bhe5T$!>HQWVOVYGFswFo7*-oP z466+thSi1+<7y*^VYQ*dxZ22JSZ(Mqt~PQQR~tEutBoAS)kY5EY9ohnwUNWP+Q?yC zZR9YnHgXtO8#!HN+D}wvZ{_6tnO36n<2_32^+=3S6armW%;0XY%xmmQxjBvsW{7Tjs&eS_2z)xD~q@!J}wD#AA?jr8$F*Ok76!CK?%eqHGYrOj|SA z%9JOqFg&d+qdOZArZou2(`87-3_&dC>;3&><9|a9vQ)QwZod4bJN&DFRCYUV!nW-2 zuL_7gled~8$Esyjl)n>4IU}D2H;WP0KM#tVaZ@ApajnvSk`mbB*w#%YKW# z+J~zlXIEjE2QSRXqo>U>pXREP$#F_wbi%;vyRv%8j(8@AYG9{j^6ITCUjcdGHUGlL zWKc-ijB?LI_m4ahcQ$yQd@+M3Fj#L0Ww|*fP_m5?8Uguf;Eg7A3mXCXk#dw=5WU@R zPGU1P*QHmyqYRAvGy)r$5BFhUBXi-d&4c~SJ-+WxID0Px8<}S|0dob6pPp%$j~^7q zEde$%E^ZmHkuh;gfsKrZTMlewEZi1gBjez<0UH?uw-wmXyRc)xM&83612*(L>eMOa8fVjQNkxI!ao{# d3S8kV?N6GS<`Mcb7Cry~002ovPDHLkV1kl0ii`jN literal 0 HcmV?d00001 diff --git a/assets/images/dcr_icon.png b/assets/images/dcr_icon.png index 609873611f9ed85a3d6df68753e4a6afbe628b5d..757cd03882756113d81edee26b6349fa38e144d5 100644 GIT binary patch literal 91254 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 z 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 literal 117789 zcmeFZi9giq`#)|;Dxp#-YrE4Tb%e5a;y5atlO+2oAz4Nt8OCU%m6M7PBa-Z8&oWa~ z%1(A631i>JHq6ZTzF)7|)cf}j{2rh8<8dD2ywA(^y6@|HUeD)sUH3h1|81xzyh36H z4-b#9{$DyLcz6VB;6KZjg8wJt;CpZIvE1!1Gj|>yAu;$5FHgkN4Lm%Xc=UA+pS;pK z)Q)qrx{2#Mv(Gx_zrRENuDjvVdSgeseum>3+j4~vU%S8;cA<_DI+-O^0ZWQ_b4DK9 zeGW;pDQ=Hawre%a%xZCNX}{c-WoKbmyuLitF0#?}Y|#d(n#cb*>xT+&e|Dp0$#2b# zheE3c%GO|bNfxtTCw(2F_aE61d2nG|ZO(0^=(jtucflIr|2Fk6>_C6w@%I;D{=}!s z{9zfH`NO6de)OmRd+7iC#Pk1q7mTRzGJ*ep*}?OFx&MDI07Ai0fd9)59-jYGMveIJk}{mQ`KLPI!?buYTfz@X@`$D=Oaoe1R%U zbv9v)lst@ekHZ|DN-V2>(b4qMd$(|*dG+!JVtl@letTl8&)#8G`P}M=(S@v1!*=Pu zhTQ7gYDT5s5@$;%&we=cda}ZIv_3`MR?Xgk*57P8CW^UD(WlYoG8y&fI_j@Gbl=q1 z(i9MsyKgv1$a0uy>IiR{`8NGoi(a5V9R5s@;oDGieMWf*KNhX;r849*MmjLm+1R=- ztLWlHdU7P?Lq$zaXGE5ICT6gZ@~P8?Ak)9_MnCM$%L?#6!@{N|l;ZX2D;e6_;xz&u zyjI}j;i?*;qnTZ=3^-#l@M4OQ_ zSt;l^wd?tVfG)wAa{)OR!a_GjX{E+2{*X20`o;ydtBHb%kD3?1yb zQWCB1nRHmed$5!}4|t1SZSVg!?d=uN&6n@LH%5pIKCFj$aYvP({-qS?bw~(2wKrMf z$}GPL4L838T`g-#n_>v}#a|T9(}y*LOWI^Vt;8_C1UwVOJ2cRTlO?7x^VP!hIe1l# z`l;lp`P?EE@t)qiR`A}G^L*GdcHvvg`#Lu+<$5oL7iX?p-i$Nk2eB?6k(#Yia~;H= z4`ePZ*bVU)ZaqJ>f1}@6>HU(n=>sAzqoqj4N(AQ6#*?0XovOc#EYOqM^)S)WBU|xZMa9{PlU^4a;tJdszf2>5m-bvDAV)fU z;i5-FP2;cv=>rjOVQ1gj=zZU4df}dJ4e*}5MvM`|&V7CQGM~j8ib;|8?Rw!%*_?0< zY}~GahBGmt74Rbf6Y#(=gL|Bpwyij zogt6AXp@{^Yw+^ocEviB{Oes_D;XsO+!c8%8h_p!BqCuML<|!+dt>Cb#SjjriDD$J zlnOn4y2o;SBm#3A4d}E-IYanw!)bpiDk`cu0Myk{$GiYbaDE$GD@fV?@Mz|{_Zgw% zYEM^!mxMJ;Yf&B~(c6O&SaM#X9;25W(0Lk|mra*w$#-fPYxI$0NhFd7m_kTD86UpYxr~+H!y0vINpRQDL_%loU*&0 zekJGPmA0VIS~Cp|C2hnk?74Y4-?fDRTK;}z7KIak%`K>v>}UEId?--;HC-GBn{%Iv zz%Af56G#&|^ma+6@oE7JMMbt5o9gtX zqT;wVzT|O}-l&4vYP!y1t~il;7{5!r((?~M6;h{KI{ZGn&vy?y9g ze@)t#3bT{4LkMM|?%cM7l6Wgx*ygyBo~-9l$Z@9{Nb|WnikOK47i^_Hvozn|uY->o zS(MnZe{y`(#mE^OSY8ikXf-cw7SJaB_##(?r3l5?IpiEr-0}T0Rqca%$Ai zobw#wd_uNA9b5W>XpB4Grm_R?d|sl}Is)Q89xVCPa{$T-%9X)FwUXsr(VX_zl&+Nv z&AsRFbS)Cki=)wl$yBdVB#L0Ut4Eml$LMS0w<;J=FL(ddG@2sON|o$?P?c<%3XrIa zAn|o|hns^N*Xr+T#8kgLFH9Us*{7J9H&k_Xv@mWL$tQu^%g}WSxBTguL&gyY&e`>rX0&3bDqIM)t*6Pq->5y*irWVcNX>xVqd5icJ)c*W5Z-)Xag9bz+9sna&l!WJM&dXAp%Co72p-PHWUnsU!#HpHd!aTNoAagtmbIo_Ir^(A|Cg zFvr(Me`phe$kn0gcm3XlBd`N@ZIVVtg*Z%vBB^-^rOC&BYOHa(SaKkF+GTFLcv^UP zi-1&(L(X%}LK{xtfvzTOr9Qah{P*{I=;RbtM$Nq#>k2;A=R;VH$%z!4TwHA1@$ zWQ)A%clL25V5}(l%P#YeBkq?WET(Vz(#wExl$l`K!KFaiySqPz@yJ0!FQ53&Y~D;` z?XcFMgtDR{uLjo1uPY;fY`cATaM5pzF_D-PqxJZ1uc{XChe)zBhV(p%H0s1H4S>TT z$D69E+0% zBs2BcA7ZO(Y)1d>9t-u6)XV7@D)>?fsFM(Ha_Y~py+;GR7bENrxn=BTWJYb^)6Iy) zP-gscUXVs#F_Vt<5uN8x-(?}9BHf%(1f2WqjK}=EyzqgK!{*)Uov;sUrZAab74~t(MVlgFvrN zH-}9ex9ZA*ny_i9Sh@MfQMVxA0Ou{#wh+g}_Hz+$_stvKO? zL9LSET9{}TmQr1lt7Zg#@j!>iK+u8v9+y)zYrLIC5`%VIHXsY_==}cl!_+&&U$OP! zq&wvwhdZzUGvO~HBvs95$Z=xId_h9`pd3-Ly`o|XMX@=A`dW*PWdRD1$!jb@GhBzz zz@#WXOSN}{QZ|9eJ@wV&b5d?iAM4S_y zS{QHJtU6$5NEZm!>xvuzm*{b#8GXaw`tNArz}^O3(}OlhhE~le9hX4dj+RSsx3K!4 zdb>UMK6lJ#@~^(i$N$W4Ps1oG9`n^!*z@@AUK>kUKchuB0^*Wc-l*Gj=nP`CYzCLM zqtgqm!X!nEsEo9`PxmA2^kaVgsh)X1 zlx$VFY75lv<8t>&6+o^4c%6w7avE4o5dnc-|J4xSHG|1Pys?3cXD3OKRp0##_Opn zfJ~%kv()ZpTs0)Z8kuMhH~A65 z9Mlm>1HLv~l+8pO=CC$lzw(q-f1DLz*TUWCG!I3^-L3xCr|s-K`oDV|>=N;LhL5Vw9%54=7^(6gnr6%c@f~;w2l1>$x4C+ zWtM}0JF&HCd&KBOmLh`YZZiZxkb;vWsU*?X-v0G%a_|rqV?Z1&;G`O}p7G;d0tzbU z5sd1q_`NeHOV_1c20~v%&eY7x(e?XPfjm@2o-NG{ofjoRq!!#OIp6 zXdIB=f*3f_f>t^>7LANlPs;`QFbbM?O9a8nhgT zar(*_tTwa!wbvpl`|fX$!qqQ7hs278i*K0gaA4&Vdz3@_LztX?$)k>t4#reBj&%T5 z4xT#t<(&;za}HX<{^uEM^9&wB83*kqY2| z(TV~WxhHp<25~yzaTls$><9&s#mY0gH;VfsBa7=AqbZ+fd@T^@a}y}`I3G2ua!yH5 z6;nCfyuZl=1Gv7_HD0)M7brr4xVqQZf|QNxTD}dUtE}4^71r>>hryM|Ob6Jmt#MHA zIFSG585|ac6@i9cCjD0Q^4`bwvTCUF5!itYjh8I`?XmFXd>#<;KaF0CH~sKoO>Q{A zm&x?k?0%y^lY%WA)Clr{^|G=><67GHV(M5a=P@WnuRL519!TGtU%c*L*j*bC6Y_U- zN^;!Itm?28eAIh!4~eyx+{)AQMk;~+o(3}1k@He>!#cJWz=EOw7JVqgv(_^WM#ey= zv(3wPSbKDm^Hye7%9QwP+7>#VbJv?3>s=#GgnJI`j6QVt%TJ|@!f!U>GKrs-$~1#5 zD<|W!=DU^VI%I!c;&FazdE-6jj>ZFpZ@xZ;L9rRL2w6_gT}&H#6x;GvFxuor6n*Mb z+DzjQA6cY?qwV3N+9E40m5+f1Gp>;z8|Lp9%}-4px&Ug(mUzVMx7E19xnJ>>FNhU~KkU?9c0V$>!IHL=_3?X`rmba>jZIuZFq%Lg`52e=@q_1+&ndLYES$wgj`k zWdk^_vPPzmZOu3oKnQJop7BGF&CVJ4Z+ADxkeodxSiJsBM16x(mSZ7Lypob+z%jewA0NEZqNb4NC3= zNlA)1Rky0h=ZxylsuXHpB4_-Dd;v8N7`_?+RFuQ8EI6$epltLJ+FbPe1=9N-#Hag` zQ||4xdMYx*84V!QpSgUrN3!Gvj6}FaEJ)eWIj?~Dh){lu2`1!&DgZic7p!w7x!0ob zfh&yv-yr@!_eTj^v4|2aq7Labzm)LSzPz}B`C1_@KTPG}PSl<@XpQhcNoHeUce%y& zHY*;;m`ELRVM3#GJv%8_^=9l!a{85*bN}{LMgxF=fSx=S1>{#Saw8e?;O%Ww9YZLH+^a3Z9$wob~Z;;e3Pns-J z0*S~qjbO6bUG*68<0%`5oiT^rm#%Q*1`Spo$N z80?cysnyIE{<>dpP{&v=>vt)M)cof77MAPDeolRR3+}R1VZzT1YuU^Vos{P~0AlJ}E8B$X0yVLViGP@`5UznbY6+N2hiEg{Av;$Q&mS z?q|zwS!3WC?`g6DiX3Dj0+MGdz#?}A0|ysIe`ynP`aks68hGWxVRQ5f7$Y+qGg`B0 zZ~97v5)j|QlG8(na5`GLI*&sez^#=W7GPpLI>z7c>s14-^M_s~rop^04Pe1w>qvHpGAsW3BuCe7l zSy-=3{6(<@0E%$6}ReIfy}~obk58HU#lz7y(6pr>!tAMR>}Smgb8Cu_ZuW|7n$ABHSGhLzAGVnm%#kUwgj%%8wY@ z(+~asvgVxtexSk>f0Qm74E(r8g;f!dEWySRs^I2e%8jLzjauf4t+oHPy|ztDSOWGw zT$p}9Y$1_@V_2~+lAgk2ZDb9( zatD+aJ0**6pR~9V29x`H2Ia3ly9OU0Mmpz}%t*_r#~2Ng&-?jy&ui>_67KKX=b30y zs%ab|a)k40cvg@a>oTM%4);p#pZgx+i{UCaSxH9OX)7M?(r?Bf@t+bJ|qP`S!42qi?X z2icuFnGq5|ZogaiPLcCF%z|b1+%9peVH7wyq`ogeWvjtnvHu8A&=F9ug?2UY6*WbW zvKMTTk?2p);E(iDmi7H+%d&oQZ@#a->W0M_x;Ek2C8S~(C;A4EDmZLxX`6W+aWz^F z?0#`;4rzqsT-c*ss*(XapWE*$mUyTs?5?~qQ;j4x5=_|nB;F!wt_;nUDykKL6;_Kh zO`{0_Q-+gXY$c~D`4!q{#XW<`ZG0X2wkmg<*upbTL?MV;Rs|_kFU>`BI~k0@9GsS7 zb6~8Qg)b+^NmcKH^vw0tj%p|&arf;Jem6Y%rHgfV0t}1ev9)0gn4v z9S& zq|;HlWp=fZ2uznl7D;>a?a!z#IB?(%dz;hw$_t0#K@wW(4tGR<$o46~b2aNMq){bT zn2TKg`cTptADJKsv-xRC#_#za`a6cWf(xf(055m#TF50#Nfh+)dvh66&6Npkw#5v5 zi>M8XRM$Ya93MVLpJTlbP^Ui}k!)UBo~fsC5*&v)=UnO>@oE?urU$W{#>MY)b+xal zZ#!w%C>hrFav9b(f>R~&bJ}>Mc%kG8Q0@WK)mZIOK(WFkZBjJCp)^M~@rJ3?uqh3yY- zz&uuFWLB9yU4D6SZQEjXILqUR+w(0xdGCcei>Yza!!5WO zQu!BTsULS^HejL%B63_sN;5z8k%FLDd24ZjB~s@0TpoI2p;j}=rR!{X5`f&y_bJ6m zYV|o5Lsp!;6)iJAbj@SYc_S!vEkdOWsuytu*MyJdSxV4DwdU2?GDGJvnr@8j=p7JN zimkB?@8piwb)Dh3A7=Q=(<+Tx-UAgE(a=*nMilBrDEBxHVzwXPSa-r-b65{-o$(pn z_Mf{QZ*>&?CZ@@Cc!(f%3LUfUMnra?6dP6#lJWru9_$2904J+z^{>5$!H{uW2MNzK zJXzcx?#5Lk!?8_hny7W&PD$J+NJL**=kHPn^?wl1 zXVo3QLgKs~0{#(Z*)_=~8M6_Dz@2-D?W0MRGef_WDPU^z2(-wZ_dBNFzwo+}v`>+@ z-O#5;xXYB~Y~(Tut}LwdV_5DI8xN#Io{m4Fwl`gxJZHi2HJb#Dz#P`6uxH$0UGy#( zm@n35-B-F7Jh-R);Or>jo^E+#5G@Bn@+_s^@1hk^?bkMhkLoBuxpq7By5wl3M#IIM zf4)scyYa3+euBo=g%!Q zS_jp^F*Ri~^|XF(^Ib0Xg){lrt|{=?5LKH3jn2tIP**5$N-mC3b8w{Xys`a?m+LFq z38VGU>Q2l>caP!XRvJol1GgKRi zCc4RR=hQT)faE#O;w1WI(1LnaKC9Fk?MD*5=W%@L&xaCQr~$8U_q2@PL+R*ikhJ^Y zQtSwaKy9GOw z(R%gg8%l_6za6}|G9i~hH(2DcdjKeiNV|ZoDtprIvbdvrc+&JPV`2lO}TqTv*?8XwGJMRMhh?isL zH*;RhPW0-7F-EN(FQ?wV_z=Ahkb0)Ex%ZJVaSkd#X|B1hmLw%(k>fhF9%&-%Y(8O% zGJwI+|Fy>5XWGu|?FHBHu9P9$&W{h@SFlO!ryeKc>!aIuvb&a|1|JU(ht-RCFd+RB z+R)(-_2$M}z+X6JJ}{{RZQK5s>cacP)Va5`CM?tPN$k)ha46S2UTzaOKKNOF=HUas zCQjzQ{PB696!GZBAEw3be3H9QOolVzL1rU&rfg~AJzHek?Et5nwQE;9ToBB+fh3Gp z9Z+Ur595$#Sy2+`QfeL&qJ>J2{jAH4;z;DnLtN2Z3H5+S^GJ1r`?_ zwL^eb?}`zZa?zk@Ur#`w`FwZz5{A=N76*U;g3!Eia`yFBo{dF0qwL+gV}ldzom>?h zXN4x(TCS9<^A>t-_{N6;fGjT;KTarlArQXkxKJ}jHke$Sk!?#Fh9bFvro|@?ieYz8 z{&q3AO60m18|(s@i&qeouMRKtV zM}|%TZF8Y8!ogg+0@?a>@9H9OcjccWKdBJdJC_KRUzqp8JqPjO>H^s;=O9@l)OnXU zUH||;I{Ey}j+n@z{$0D{jvX_-$8PS7|>!!~uauGZw3#5Jy3AGwX(PyNJ;hjiE9eMy+E8lScVFC67o&)U4bbUUS27=l0JL&%PY@d2`#PC1iVf zz>UvY;T{LFiu5C2+_u|zPp0oLzrU2*L}m09`#nOuUqwtC9vO+cEm5w##6meMXB+<) zwdc0~1-;(h5^C_*Cc90K{_P)oSb<=o-e_vbY0tCB)XKf z8rFOHDf+yW;+5$KuZ)f!a)!1X*X7>8kKLxatwh%io;2c(DeW07&P!5S2olx~dGwsP z(0twIblv>F%h(%WtxwBITcKjM?7GGEV$!RDtj)Y0{RYV?ve8-zejZCW7NP5k=5c(j^B8xX8hD+lbw=;^Nd3Y!pmchVOl*^N=+X(=vDJV z$0A#CFCa|GiOJ69F%K#Bz1ISr71HKl4*B>I93Y zRbjGtL1gN~d}NbsfFn2j;z(KwZ_z<9tr?5MX}HiY0ZVG)RZ=s~P!!!IVwbT)tdF!F7Udj0a4UDVyn2Z>DPR zvY*20cWUDsPG?r8tt%-tcZv!jfK>gqMUuVfPK5}Y6?(zlzu1uH3IT>Ta8hON!MRDevXIwer5Y) z@ttQrnmx=wrw6NQ^0FhHz*8fXx#?DMlF3$;W}a>O`iVQjBD2As+j-h|e+Ap*&Y5jI zFo>t5Td8&k#^v-VAIVM}4@&UR4#xJCil;6MYXi2HQw`q>9K-tX`Hw7JWJpz;utZy- zs&V}Gaw#TYsAJ`Xm%$%-SkF;TEUcucRJ>15cNHQz^8dGOdtz&;%tFcbBA-8g-KQBp z_TniiVxK~&`lTi{1cXJ0VpW7qtJ8uxj~6|{#E>LvlUx7143~HN4~kwvau{V~ZYTFH zkrmK}z=e6|mf8&bU^T*q*e|;-bO%?Y21j!oa*Vo^n*8E|!If{VoBYDieAc9X)3T{6 z=v$^evk_rwO6FmhA_;soYKS*)^H5UZ7ho9fQ_oVvShgj`~b%FY_Ta2^B z+aO;y)?PoKg|(yq$)SiJuQfWCkgdBAVw_pwJH6)3XE*K-6%V_j|7Jk}=Jy<_Nme@CUn-Ps4eCH+z{)zzl zcLAP5X@F$LaJziu;8Hfs!zP3&rbZ@Cv(j*(X19oanPToMS?*(PS)`^~0x+5CxO><^ zM^v_^O4hkc#;UUc!c6Ifj*W-azpUS+fAr|FOB$I@J~QgVHSO%Dn4;hyOuo61@Z_yN zqRj;(y|YQ)!nN)k7cM6i(;l12egu(@dwke85y8-a%;4PP)((wemijGPo>7GcUUh4b zf?MlQ>Q0!B=QNRhz3DQ-1-N~g~@{;BIZ<`v}wy)r?PLu<3kg0UA zaPTb35~+on3qtIqEvg4us6IGhd)Gow3j;HoT(CiW&+zbacJi|Y<{=;XrVVNLhURIR zKq%63sTq#WABFW%NI)2n$HZTa%aO&}`*U zb4i;SEr+JMe&cwrPNl}?Ps+YD*Sqwxr69g*V`nrxAPVt3WKEE9{FE#{syqmx(V-YG zKDg3j>tU6Z-a~E_J;X$fK^F#4MG&Ww*e*(z15DwE0Y$+%A+YDAwX(P|LeuofAwCp0 z5ZfXw2pN@0xk~2u?UEA)CV-v}P-iDHsAF;*f`#Xma_5AVz-<^alP_*lPXm5i!XeZtgiw=m9<;+6BqYhlqxvbxT%$#+ zLjI*1>({E96<@LQ2=v&g==)mAxo0R2?AI}?kJL)pJmp)Rc1(z%L@>{rZ<@E*BqcM_ zBruom+*6|=6!rU(n(ak)b}h5~lM->VoCmk#_-Q7_Z%a}3+IOCm0)g|p!mJVm$2gR!*(kLM&^kZwZo$F=w( z4PJIGvPx@YWpgHiqjOnDkd^6CUXZI=gg3LFv<@L$rSDL*E>bnOMQR0L$0lVM%u3i) z!g_J_2C%G*3+Ky)4L9(SLppMj0>tro-)-SI+vaU^{{3UoBhE zle~U>WM+>?F@)Rnt&R=yPuf=Xu)RPzfx9^`xe90CH+pYVv?GjZ{PtiCic@xxh8{cl z*ei5cNxtcQajK?qmmGq(w&I@g2KUHV0%sF2w|c5|ZNy|!SuKgjBwa%`X?S>!S6fS3 z2O)JMBlwBi#Zx4P=7-T8a6*I? zi|Hgs4Cz%)Zl4xsJKQmu{L*+NQmgC6X8*nYk9J`y7^aJs_jGFR^@r3rLN2E;umKdn z5i!*3w0jU$yG1)DFD3|qq)n*57I3~{=p&b4;CE}M|0bZV6+|8{&RIYT7k&BRLHcm3 zWi^K?Sj~*WwMiB%j3i=8lLK?;v^&=={3`qZ03q3F@_2cARNDq={zW!nZOKuLcm=JF z$=dC>c7A3^SOx(g{iyzazB%gczt@jfKiv!HAnHU@dXUz^^!?Q3Y#+hWrJ^{y_Tl63 zSy~PZ2{rEP5%5LfDGwH=Iqs}pRwFc7lJm?b?NUBKx5rVFrQ;taCF3~T!9oc#$^7^1 zuIpnrCrrkb5R>MbuZE+taT*K-_IufB3}UnIdUBvLt_I!pV%-4E_LkW6;9|h0lIV0{ z2H}IN)u<&SIc<^vkl&qx6hjmmTE<{Qw(rD-!wy*jhEes@slL~tB+1qUFkz6@qb$tE zHgsKrWuJb-^vGOXD>1qT(OO4cX7S?ADlZT8`Xy-2g!UWl&_44odHQ~PxQz(dR8)v!L4H8rwH#9kHIaZg#sxL}z?Tmp-l zd~W(kz;rr1TT$FNy8^K(Up}1wTsgOI{%2V8Q@-$1zDu=!kE22SSt}`xr_34D+OnQ& zM+0CZ-anPwi;fdur$DPCM(_Vw#X(%8AhTrcPOrwVVR#GG-)hXEe<0X~*JrIXt#rII!SWU_7I=Q2 zP}7f@s%789(aZuZeuF1YLy#37PSPeQDWpjZJQ+l{XpyH*#JU<&j9FN-?t-b`p<^S- zB^zJ5@$?}CC=(dF4I1a~T??^News$jb?YQgeTyDdY!UTz2h4 zth=lyK$pHgt#T9;1b3a`j1nmb%v*E&M*jy;_{bLD8}yVFyh0#`Kgw6UH?4BI6V1{x zfMF!#bN@6tW&eGq4H-O`xi)P-=2I{_33_?JaEoeDj3`-5G3UbhL5HYqaI+Ud7yEhP zVJ(Uf*3qimliLD*cR1jcWmd98~@$jdIfCL28+9rluGR(;+9j> zDg^-G_}C`c#h69aYoto#0WNbqk#zo!YA?sLJ7EEVx%FqV#iU9r9Aze@rOzJ&i;8n! zftF#E=e&zscV4Oo_)`rFFFoDL6;w3<6G!aPVc4?Y3(MFiWx`!oJT0<5A~68g(g{!c znDBtdF)XA|9#(YTSFu`jc5Ki44#dYZLY%B)^hy;B6zadwf_ z6o^PE%LGRe)4};u-!n#DPhcedwlTD)pIW%Hkfk^%jW`_P)`_seM6=c=jg`MNdU%>M z{cyL~uvTCFCI~x=nnxs@p)qT|)b0=yrWqqHCkryQ@Eubo3d%yKJsBFmCWEj_Eot~Y zv{MJ}^)UW5+#$tPK!8&cmr}fCmhuJI=YV?Tjg_o*Yg4=EuZwDzYB3*03{9-oP_+Cf zbfYxOr+~$Qq1$E~esGR5^83dHJ94wqeMEmT~2 z6z5EuYP@{+o^EYhS8t#iiv%!14t{E`^Y2)WZd+fFQvBoERhQ9=|3V7cOZ2VSgIVFl zinbR&%u^kT@IR-EF1=4Cg30~`Gn<*PL2CY1L}Ac55vw$$_4NM+F6|o%K|#=87d}?a z+69Z)K614Tpfe;A0guSC-N7zW6%6!}A6^5{a&NqLy#9Ak3bNWK5P(E*BC*5IaH@4I zQW+Ty4YyC?8>c)O%}99<=i*%Dy-=pNbHjrmBek8d8gwwR|5M92^ftR$;9*ID8dD06 z&1ib)TG=!)E+Hc@u3ZAFD&oCwa^fP>FQ0(ks!BS&k^ZNdF-H#4-4wjFwe|~PxMof9 z3L~A9lRysISk7R1SCD+O|H^^c+9G1^R-jW~O{`>XG!?`&Sci{jDQL%A+jO(qH(!ZZ zj7$&VFu7sMvznu17b;)3K+Xs; zZ)`KMX=>8=6D4q><#LB}qJ;?5&BNUDCBe%@q`gf=f zsoCy$G%0U8{VxCtcJG?*1}`4i^vm4P+kJiZ1k&r_y2jG(hV@{HoCzR`3hV`5-1$lJ zz*gzIQ6fC~+YIytbe=S=mHD}h94v9boWGwA&*Ilm+GB-6d-4%cr&5;wTs3EblgiQT zx&B7Ek)q-*o|t2Gk4_qkRY!EL9bs7&sR7&}w+i;}1db2LAtpBHqtqTRZO>s`?~X7DJrC*RUb%zP4ylMH3r4#^WU?5QZ6-=eBFLZ3WrI3M~6_+qtJsqvm1~EU=nbF1=aeFU=uYS*r zd9}ly?;Or|_H8IUPLVcJ;2`xFS9T)+FKk_?gqDu(tiBP*rWV*do=Iv9R5&S!j*YGnBR$?_SepJFHpnJXR&lE#@2|Q4 zpTP$~b0SMXEUQYxUWE4;{4{KQdLNSikB65L!pKP76*=~jRa!Edvg|MJzbasBl${C`IK@`4Z)xE7kiXP!&SP~7t@t` z+>jrx>e%C!*X!IfInAR#?9ArJNjX>8%|LH{IzX#5=Rg*92uNZ-UscyjYk0^Z9vw*+ zKH+orhy|fjQrK)Elbu^cx8YB&NK*j`<7U2P6XTavsq)|^KMj;b;@kCM26-o+wVRc+ z3nw$@q%eq}qXnJ7CVUJy1Ij@lF4gt@05T>|dE$KM7Mv+!?5LyiJJqE~aJ`@Y3NZ?R)Rke`gpRIZiOi}RVP!Ht=bpd= zkkOZ2Y*09H{1LMNg2gJB#}uD*0W+ee!r6K3>+>H0z0f`4{nu1b|_n`XnU;BK91S3 z!%8GfHHLJcAveGaC7xcIl_?;eY0g<8`eN%0fQsL9@W4-BVg*_r;7!;C0ki^&VWG@@ zpMW~NS474yCudGq`wUKdj`ICfSr^^l#hn9p4P-NE{2uxd>mNIG2Uk|#qjTNFLav-y z9Xu`eJ`A~Q0Pld9gbmW!BRAGQx33_J;YFo!@46WCXWO&@931~KQ>MQKM6njd3p^%- zzSk1R;bz%&|23kR=l@rJ7uADFS8AXN{=;`sz_G26_IFkP8O%%oFr#IbcU_-6Q%lPr z^T46bl%VIEG`4)P7HCAuUtrw@hzBgBn1em<^f*-(WJ6X`?9PEFOeq6!q^yM#ES28U z^|?WQ9cXvwZY2bGgX;zuGZtq-A1HT9W{&RTLW6o@^0l-o%5~&i z(Y0)Nt|8xY=9g7gmrCsz{>XM&trkdehycnb=K83lLJD z@ioonG$1rQ;*aX3wY*^3w1PEl0;B(Y_j8k^>~R=dKlBJnit7jpnF<4(2w$7sf{41E zZ2R~~%i3ieL_IEBGl(F)<3>47>k{pnYkr ztB8L~zi&d|ccF!tMzwpB#vXIAEFXjS8U4;pu8GtH(Ceq#VeGw?Ssfw_DbSURu6K(E zavm79@*=X1D_~3kBH~>0Vs_YJd@)89le zrZPI3f59xx3AyaYnry&T0YhS6#&s>=-W;v$j?gr4n6*Xdedc8ZwxDFXS<$t@Z3JiJSTe%WwK3!C z2MHEl&wrGEq5KZ8buO|-ZpvlR&z-Me9OF2RWpUIWqN`MG4d z!H+GZehTf({f5|D=lb2`9P2!ZW$DEQ+k=)xy&r;+(IarvyioZiE6zbL6J7cOl*Z!Z zuY|m&4@*JtZqH^M%v6^IcUzEkF=2I!oU0etkUA#`Z#r%BG)^nP0?tlgfe8(fbd22E z8X@=eN|~5rC(fGxx18a;7iBW2mc~5f($yML>DH3yjij4oA3smCG#_vg@t12Lz#hR; zTVbJSAsRK7bpGD&?h%W}gS7ZWqEaC`&pQV0Yq)_Gtp*`Rrbz6wD5EU^oQlZ6+#V$2 z&i#)aCP^lZQq18XyikR5q9@nzCnI>|$_#+|F9SWfRW;oWfoeIe0|27ilQIHkoBEXW z6c+QVi4Mi>)s9;V=#@f5zKQLO)gXpYmN|C;!R=J*r(30!aPI<5?!mAWO5Nk{f4Ahfj4`G?w z1dqSasn33?H)&I+!{L?(~Jl}xZOEmkYbN=EDn9^BcFr=(jBR4YUGC*Go$F52M1 z+`ewzToKGG^U9yP;pU|EFTB)Tr>SEQ+L z7@Y9AzKQq=X~LD>d`+=?)6O20u*MV7%d}+&4mQFujj}gPpVaT6vs)*BujR6a)K54Z z<;T=vU&QS(qyAQIUx-#Dz7<73tEzQ&TZ}h8F+6;*Y?bQ$fL~Cnn zX)?lw`xe0T+i`;UylPK%0EZaiG;F+DH<&FlAullZjnpab_i3UFM{$gxJ5-vQmMO6O zI9KY*w@CQsRHAQ#zkYw)TY?L(-~fpr5`BJV>D$R?8eoSza)JJ|aZt94hgQW0u05qY z2_e^>LP9LSS%Hc6^ldYbV+{Ud1M{+Z?q^%&dLK~vXJlC;L|D6fgVAU5S4PE)->1M> zL{(7!)4kN_HmUn)u292yumZr<`g|0%V8(c?QkM!iY~Q9IZ~DrqNw-{(P8|rK&ok_B z3|eI_rIXQ@E7d%gJ0&=R=bm)*N!65E1iL1^dT`tYEWYX=p$I*vGV@rbS|MdF_&a7d z$nTJzMgIi$g2dPHyHhmLJ+94o{1MvKsLrHLJVSIq^eaJ-9KV7q?lPdT#mv3U@oH6n zu@|7T?^RjF*n)Ru+*Xcv;CM69T6QEI9D0H3cbk5{^XIL>g>ilie%XSaxBU3OfpNkC z%IMbyw}1QGIj8)BWN^!7d>~FbHRoQU5U2_djlf11NE)-zW!0~%akgwp+oq3A{P?a) zRAsu8;d6i9i|z>4FRCKd4DRmnGV2L`iZMUsw)e#k@1|`s{yA;{yz(LjOh2EiOUf*+ zcJ>LW1e7#5Suf-PHru9;IR)d|NwU3sgh4Bp>s1pLyez}MKMPWp_8k0eHO;zlQ{`KU z;0k12J??XqS+p=StKo$#*)3o`65^5HPMPH&P3|O3sz{0q zEqFOB8Q_HDHZS%4kOjhX+n=_9=|)uGuE^31yGm9&jC7vTH8ylhibY8QIF|)Wj5MFd z(B?kU1=p{0eMzX>%^4i8+egzDB6RS39@8^UX=E1X#4PCw}5HKfUOiFq$g&eRHAz7F25?Lk z8JB<}0$!enS~&ZLED5Gy%^MIg)USK9D|0kGea}}z%ur`b2Uw~A$KoR>JEj`7uiuI> zzC!YmfW46Uf48aVv<9h5k^0*O^g{1m!}?o4q{{P{pT}r{gy_&hlbE~L5k?i)=za|k zC2#6{X-Bc-Y4v_NEwIopF9lj&#vuZWsxc~z(QIK1*=ArOaQ>y18)M-kT^iq7L+VBc zY;d92v*6nGkx0z62VntDu`+i>#-+1ca|DS`)h5PcEdr5m6NDUS5V*K6b6nfO`v@aQ zEBuKZCtu$4QXd34n9}lSvOY;ht}tQF2t^btgk|fnXJW|rMxsjb>AJZAo!LNbxAps) zyrop!j_enkI(y%lEZnY!vgI02AB;_mjb}^wUBwdQ^sv1<|TR6FWiO zM63W}8}hurCPcPY-wpG*>XB;LI7wW_Zkg#7i~XuxKW=6;_!+RliQkEq)hVAA z>l5gKxa8@8bot7Wyy^FS@mlCmE6Qx-iGpZConqAqz7Bj7CP?<;%pQm_e&I3$rPboX@p=WjsIUhs6 z1Od*+2v1-LVZpS`F~6w>&(Lxj>hyL1o_F9tVv?6EHu*J4_Htph@!Syb{iC<*&y{HH zaavx=2@mLK)1PWnJh!4ea`caPT~^{M(&PmifU6b{d-{>E0hGOZs`R3S@fHD4#XU1Q z7qGiG_g)ZELLfJ*kja{Hk>*#Mjqk-_#{s^PH8Ag0;X2eg&NzNtbB=uk#iPLsrjT7= zX})2p(22u#P6Rbsf+hf2VwHr{<0`uY- zWSc%PD$4xuWd^Y|w(I(A$eqxI#aA+)+|0enr4;(RA!H?*&AtFcaGs<|fxlvU`P`Q+ zMjeU(UMC!V4-w32rC7}n$XYPC)zu+wl}NRYB-Mh0Q*cS7@!%`>!@&7Ia`v37^aN*Z zj7G-X=(U4S`fvLz{mxgjJ<-GbKVjC06a)$#t5v!A{<8`dhDSdn1l6>@f)yFb*>f;K zP}d&4)fRMl;hEfUzaX&l9-u4xQ8xOOFv<@;qzK`mJ>r4)Sw)MPvBXvej?UkF!LN4h zBb}SZKJ!pOD5EPhv%A})GZMssVy#Kp^^h@C*z2|yNps{2Mu5SK_n3=os&Bnj6nY$p z%St*udw90wCvb9jVXZq$8HVD-Q|5Mfm*DSSj6`qs&z^7zS9Bsku&g+ZI6$F zZ?-jF{A<+r`vM2v=9wi zIadtm?w}C@3o_UJYqZn4t)A2!h`bFlJ#}YwZIIT?T8(V5v-p<+m0eAJIh^V{UX=Xd zjiRhVz9%yB0tMu0-a18^TIhi(FHO!&f>3-fO*NxO!l))!6kxL~sUjfTZZ~=*KOec+ z2?t)gOYy-USPY+dvRpI2yB(%X1XYc|S$HQ_8CU&qau_CGagupJa$mL%G8sVoW-!?n zN_Qtb`7k)iYJozYpGe)`eM2^;*-=`&xh3uAw2B zfNBr3f%Sc+dJESP2EulfSn)Q;38W2)byy=EL*lIsb~gW^?>zy%T?oJTcP;e10x%61RHp*@{ za_ESPAH41WZim@;IL!)6!V;wJ1Uy7=m1veZ0DQv&iIG~2m(;TFR^r~O#Qgx#SOb&F zF7fhHf1>h^ED1EN_XlTK z9e=dbX0--K6cYsCc+q0E-t+JRZvnEg`U!CDpkn3F>6QaEbBe4|Es>v+=W>2h)?0#T zs1E{`SOf0I-T^7nK6&b%$oxv!zryn*P*W}eD%vR{Vw&zf-mT0Jk}WX&C->EK=Rl+FGr8RTAb|l8Y%cN$eXRkfmazpnTf?@np~6h3 zW785Cz1NFD6(a~F>R*5oa}T5-VzE7}@U7)u`v*&V7xV>Mrh=qqbD_so^QBd@CNf!1 zjBsw_ap7E^D@AAF+8pmGBH(t>Ji0O{qiDUZUo-cBuy2GwM@lXR9@ybQJpH($hjYNj zfT?2A$u;jM^msdhO#HoSsQ=w&f+x@gk`#2OWBKgB;|~kgS%mrE9P{hbeY@r+U4BA{ z?SD0-B?vyxhg1YOODQQbDAL+M>b;f#LrFhm{N87hQp{Q^tfpR0B^{!(#l4YX4Q6JTKQLW`JTCTLRiZdF`-Bsf_Ijg|N z3qQdmm3L~kKO3A>7i=M6{#See!E{;`F0UOFeOTb|EETe|dP?7rB`XF)>X$Ib{p?2F zCxf3;&JFrwuAUMA`nZS_B_8-4^U~tPc!G+%gk+WL#jdUe7Y-VQ2BCg*+3hm7KZ%Z@ z$RJXaI3bdo{%Wa9B)CLnKm# zgC;Zt2g=pJ16=4;@bbx9EJKTOYYKH^7f7rfMxu7~f_xUxI zx{86E#bZ@Y$YrXv?w)sdA|FDsUF9p??phsBa`lW&8r|u*gsaTh{-hZ)b7xkm73c-@ z)X74zVn1h_HgFyE);Ds8gKWW-T)k0x`2A0-A#~`I`$FCkphgoy|3pR-eIxRQoBb!|xR z8f|Y_U^*m zlVX=Dp)r#rUQQEB)NHv0GqMb?hQujWvD||=&NAZNWkcU!)_{2@KhEH0 zHLe%y7qx6y4RufDE?9BiR-=-$ELB1w=5@LLHd?7!}-I=gxRT( z*ONZ}oEGJ9pzNo+CEGE2;yC~&$W&|F+i_j-v1pf(i#5VT6$g=At3ws07>i7G2ti~d zhZs4g4jpy1PT4<7dwyIqZ5F%Si|f z2Bogo=<8QO*xm{o#`4XvQ5WNbj$nu7>Xd;Ia?PSdT`qs~4$Bkch*;P;A?@?grGv4& zy(xl2qj-BPO9VD%Yu>bKuC&O7RnoDX3$VOW%Pmgr$sl*ftDq|B4G1SvyLuDdCASH- zlym3+L@^a3gd=&_U)su%ES{Em(~hy%Uz?b(kDN!<95ssi2cc|3yU>04{4Y;#z+4Au zsyfeRePOOJY_5bL0?E_s$bm=t?jWl4)JwKq;pWE^!+PT-P(Ot0{Ry&OFJeLFT}|To zaB~$RuZ@xN@Ab9RpUgU8(9$ad{?_9`W>1>Ys5OwxDY0L+`|ZZen%w-RlL1xHA9iw$ z4TiSx5wES6wLVS??+Uo#Y69CMClAcYTR8~lPGWr#1~f>6?#%2nH5}auGbR@R+Me9C zVTNq2d8uj@N;?#?s+WO-TJxngqZjK1aILx3C^G7xsl{QirCiaxB6vF;*gF`rGmybd z2-06QRpoCuxEh&1_f5}J7i&#zp9Tz)kPBHrEQ2tPX^=?`nOY-zm!n-sx1$3)uLFyF zW&>b4xxtg0a(cxGQMU_HGn3{$0yW8h)?)S?1@Z*ALs}Jg`1dx=v4bs~xtcQtTP!gT z+Q2k8@Oa&CP>uUTSY(}ApJU7 zQxhZ6rtVj0Pect7#dx>n-Ph8J54qg*9HL|^sqSx(hkeG({4OBdI}Elon|hX&0m3{W z?#Gb|uCM16Z1)_wN*m~iR2SGK1h)yVLu`*eZNH>|iVz&7%nEf{IZ-OtggC0akJhqTP`{76p_k78f< z+Jx0OqFG|9%bOrKyAA9n;pMfY!if)YH6>8Dq%-@m}lz= z-8V7!O!C%GAI3A*n{-z(y$2Dg!V1&y&n3E-hZiT%|F928t~BEU(TCit+&_Xl?c&rt zaaZ5SKbRZufhc#)$OD_QotnlR5`kq&$6TR62}t+b zgu4}NeN-(1_W_zMqAzwB7)a^q`fvLmb_ue&$0MLy^~d?0Eh!vkHHEaeV|}qmno={{ z$=57F99pW8Hm4<^(GP=Bg~r5_vK&(PL3hJtgvw-H;t(o>)yzxB1Xuqxc%NzEJ~QJ# z>&t-jI34C>MU4GM)_)5%kh;V%N_=K|)!>Rtqwq#J7`--s+8FB?QG?!vpkKSx@zBZM zLAr3la(j5YhLY5^5(QqPJ;$%avve+op2mPf%g3zv1_a%yVwITVBD<770Lwzhrr6z3aM!>~ush z_wCI8(SM}aodWJzgFLWTH%E}U9!3}`o$or1a(dboWdtEfDvhqq##|j<`Fw3hDJeSj5 z(wOMIZoX7l1$bF`=jq2A2ikAQxB6fmO029!taY*Rc1VHm*EfyOlGt}Q=gHRMKLd|E z^R0S~2`i_64OzWU2x-$EW#4GawT2m;US4^r)Eu-~$B%?PJI69{JS#TqpK$O^V<0jc zctJT44v8lxS~^j1;ED(YVs^oEwcz$FowR}r=$f`LUostuQ4T;Pz{~arT4re|lP2+S z&BwKKZ0hq@lg?6}3xx4Q-xayAly|_CDD$u4lTW}1<-v4_!o#x!C-pEbnZ2T9qnF(m za&RgJT0cx|K3%m>>(vZRdJy`*>eP&|b$BVaRkbAp7*X5)rmQ7sr_Qs%13bVueWAE< zDYz4f@C4Z0bH}&@ORnUNQ~fA=v`G1JTMwcW9EyVv^xzS^S!XV^%RWK2W%u^OWLjoO%YGi5 z&OKg@v_h_r*l<^5G)dG>VNZ4c!=PL`P`H&?#da?sjU?Ur4MgWfgQO{;&YGu|*17jchf+`qi# z<|fMd+=5B-2yulw6>M>FQJQJQII=Kq*@G$)446g=+Y zwW~XKggNF>%9BSWZu;akBy>E#w0Hf(cJaDVcpT_b@6mTIiPvP4(Bs@{D)-eSc(d|o zdh<3iv8`d`g~O)>J{>~CYEkB`zl#n0a?iSZrI4C+nq`DZ&x5U|EO-;* z=5k_pZjFDGyIA5S%ySm>Lick~h;?vpiN-3Ys$%or3?v%LX?_`|`0OD}#B0f{KKs&F z{|vJY3~|>f_&lR^iRw(=rT?7mq6q7Ce)^7FP1~YU`D_rKQ#ln$r0M*sb4g{`o~qB1 z!8+`%&t5-ZU97PxZAUz&s=fjoQwo_MwMzLHCkHF-weoL8LaI7FQhzRD{=M}M_Bzag z=-u7{>4fEnH_8&tN;LdVHZ^RC;W>IJh4Acr96L^JzO^|vMD1A`XQtfrkx!Cd5^}tk zG6E->^jrNQzqq1*jrOGy%RCL;uS@=SOVCXu$b4{Ou)S+flS?*3J{LLK)2i;T3fO)e z6NpM|HzDTDKW(z9XPo^$T3}v3u1yrRYzb?rQ9Yk$J8!8rG0i5Mfzqi5_o{sy<{9*c z)o=DU2?32CpJHCgNbO%-GI@dL9r90O`V;%yhcIW~{ljM(A>_PoqcS}uC~#95ZJLF3NrK}#?* zy1Mrk_yt8#Z}!V<_gYjQnIOtjb>2h{IN&^eN3l=!`whNb$kB3gB|d)E78&C-%p+m9 zyLNSsf&rT|mqo|wO=9l(*nhKJjoE?tk9wNlsCHG5c2M{HW)Dv;$Ah$IY%tq_wGgRK(>Y>`JdX*UGEB1=W3p z9>47mh;?g-M-ln4`7gWFw5X;O8479SWM9_qp?8SNt3=qDwpzcMSH-BTbyr3`ynVh7 z=yUh>DFbU#lQ5dA~dsVg<~i!n~nSs9Z9Ki7<$i{%T<2r^QSn$uZ|e1mIV6T zWloC&JdgPmuh)H6v`#Kz>wiU2Wf4b+_KXH9wufxPUDbI-+M zHRu?O2tkl&wqc~7=Uj>~G4&va;wj5(+$9e*!j~A^iA4yNl3AlkT9{iA)b>}?xpnT4 z$$ev2mwW6WAko*%{{RS%{ybR!BGY$KccoUCPFp3yFlj`P;cT!6u7(7)%|KhS`3YUc z{@+3CRb>BrF7OH;q?~m>O)DfLV76PnjFj)yLYU*V%+S~p>_)&2k6F^$U6ccU*^hZl z;{XQ|7s&~->!q@Ms$V@F&#V!coI=Pmp&dR+W6He}!61kfP3?WX4pN=#D^p)~gtupT zI8Rm78ZMj+(`rvqrY~K$a{7{Be^$-Dp$UXw$jjv}*8WB#tvUWlzWM!7!!y&cVk?l@esjFXFGd}CwiX74lD*4GOtV)@$kh`5D9ng6+_+hpkw+y`+=7Z z@}=)p79fpN!UGqEZixd)40-5aIwGWl6uB&%WfcNORQt}&G~&@cPsUZ~dd|bkXa^nt z8UIl1@Bj+~d6o=EZ@|sI+dizOf4g$O*x+(xFIT;1CR^v<@4W7h=Mu?Tj*&!EA_vLC zi(*8zpkZAwMzWtw|D$!KsDXR2he0|scrUXBD)C+u29i!BpZ zu%gbeLx)szrg`$eu!x&2|2xoV2-X)*YU z4bQw?>N2mL9=NhVA z$#Dta3`n04Fa2MQ$FtLZpde0_j|`%xDFsXBhv%;=yh`8}fmmyVSSNd>4p<6$J$h-A z^W|gAO8KO9b^pbBZ9>rOfS7`bLz39Qc7#=`4ogUOw-jE5qvtY#Jq;6a#MFCcarVTw zxqo%5Kj-Q?%nH*JnU3Sxa< zbUi*gf~-B@sw4g7A!_)9?%9uy_yY5N^}dTrvL3HnrJ};)F%~mH%7d~YaK2e@U3t~p z{bB>Z0efzLtQ9;kFU7T?jjN~!3NKun83yj$T+@6xPqlw>V&(!_(Ao9EaRzba@Y-)1nCB5&gd|OGlPGfb@;8xJqjS*Kj|K7Ar*T<44Ls+%|ik{L8rQWKpsb)R<*n#s1 zg>Kh|&Y+KQl5+&v8Bzz_fIh}HAVT(^B1Z~)|N4&&7Gxb4WS1TZI#K;f&u0&!8@uBK zNfiHr>+||tDq5*SA+e$ZY`Ji{+trMR&XS}Zj(pppaC;vl7`G&u4*-;l0hAH_FkF-< zh$)aRY#b zH&^s#O6oq@LVRU_rr0f(GHa?j*D2zdwYoVT{jYLVw4a}D<^d7r(P1zHRVMfc&o5M6ArN-8o zuH`yNMeIes*V;)(u3KnF4{o2_L5S*kmXdDo%CQj(nz|6g(F_qotW#Fwt#vgc&lA~= z3x0NVW*WGL*88kJskAEfUbez{IbA!&Tvg~~`5!+OPIU^Ir||q}Pi$G7&u=i#C1-G$ z(at~CXCYQQ+*NVfJW@nqNja4Atkr@LBJ4O7Gr1@s`xdebxqiRL^KpP=Xuw6=kLZQ( zBC1iu%W+lco5Bh-AbSg;mK1!OmhL=xA;^uBKiv_zdPf1OnQ1E+LD#P;{Jxlxyu_kp zy{-+v`0|vq8OM6GfXhhc>C%z(@^HJm3c74eLw6GrraJmgLxbB!#v9dA(rtM@4dr=U zDLBelAu~^?T66g7=DfGaneG}&_GqIPXAPdx$mkrz5jPIc3rRm7SUmUiCq8N6nPCkP zrd5ahUgzP(`(q}H%^gd?X}|yMHf83V7_bO^=Gsb;IvKc}H*L)+1!(qg1Wf98?rkMq;0xP%7JJ-;9_Oo0O3i6+EOSmnxyY`pXx-E0(z%tYzH7Z3eNjhZJ>@M5} zdQj;InWG;8l+pKI)5I8(y`bYEpS6?nXeT8TRQVKY0lTW#gVjNwo zCUa`PlsK*)+8Tlqd|$AFLjrR;91bY--9sfva#ZvAF;}1aO>RgPc-$i8`ZW67nUxfE zSnl&6z|AIJ%3;APmSFUX0T5?&`Mr&QYU1MxgJYr+nKMJv!=kb15B;XAS~Pp#IW&abKoZ+d|oyy|k| zcwOj>0cuMkaO7;~DMk=%UDmvDHGOu$U6ZHsKk7HpL7}i z&xm z`{nw2#Q)Hyqw$yv)(e2p>32c)J0}*URB3Pq&S>7Z4aoxa?H=066&6!ns^pizrPQ<& zwk7&xXQKb{L)3iAz^9$_GFjCUJ#G)jz_wGYo^FooF#3LHlChk2{KgktLqg;!IMHWCwt6QCz<1bt*AJ29C^TH5&+e zt$%WI7tw#}mCJWE;npI^ECaX#Vcz&e>9S&h;Sog6rJ$zaZ;paBl9E#o>q=M`j~O zhTojylPD|~D8Y~tWhUpR?Z_SK=@R$(8}u@DZ(nI^6jx;RYJ&9xrnV5QOtLfereZz5 z3sc}?8EAi{b*oN4C;D8188+%rB|kQz+V$L&8YD|i`<&KcHlU)!2P|CJ0kk70;%6Q9 zhTBXCk)GH?@!ZTB(NDpAwERZ)z|*zf=r>Ic;|o%AUtX*6e|wehB<^T(2JSwgfC;oT~EbG`;vfl4!hy$LC1F>t34P`vg8^L-PgNywiBC@l*DN(#8 zRHeCWn0}s5e)wPRcYxlhTg*I>>VRV;uW0#)AVMiEkxIoL)&MomOlb`9OEa zGYx;QIUg~&tJ8uHdKT;jJg~76Oy0GTM`HD*qG!DuW-L*K=+*TNHN3#uk1=ttMN<Nx z)dg8^@E9;30>{rQ!$H`?AH@nM4yDy_1%Hz4ou3UZWZC4Y3yuhM;G7|-&1Uq>q8STw zWSU&2*Vfnm8H{7VY7$PeO> z+I^J(QxPRJ+=skaEGgjiLb5tPg*4`VMp$k6Pc>D15e;T51=z)GIEx-h-U)2tK)cf zCsYAmJOqnlyJd?iA0BZD09FoiI|jjXHLrtS`n{clEV3hzoOK~s|Fo9|O710$F#i%k z#>&{wE$8j|GJp%w5MnI9LTME=cB+V&4y3WOP%niWTbGZ}iX^gwGpoEIzRq^ZtTMy?k(Th}P)8#wcEiua5>uq+%J+N#=KjP}q= z_v;)3=irNOd(e86Pm39(9cq`VM3^ykpPSE@zu^b61%$?vb@6ns<&-W1_jWDFAwPl5 zYzvv4N1bJO_Trc6clY34e@vETZYW80p;*KvEC-)pC(tRhmOTfViZYNoNXh5RbI%*U zg3z%u;J7@Y@veD?^xQ6MK6}GA1GxwSXg5)XgGk;ODe0feir+m)N9R}tZ(OtpGb~19 zwD)I_nx(HTB$IG$Sm6T7AMo|oih`C9NBt*i1r-udKYygzzwsmb)hrgccmD<~p{6CK zH?5)jTDP41&4-V!3mzQ!If7vQ9H-32A$^>A1WK`~^lc&eu|RRyvfkR@xjYk@Aasq^%v)6+-R$SSE2E129PD<{R2VHz9#YbfD?b>xmbz&yN z<#g0!Ri7ZO0I73GEe|aZ?8QN)K{GgGtn=*jtLEhNP86IubMT9-!2>Xwi^^qy&w~$v zqjw~5!4KgOdN^$f}$2$MFcI)}GEEmD=UuGJR#{-)>~kxJF@;&|6$^@7$a8(?=q z|C)**VXl`ag<}SZF)S`53aKiNg~X;IGwUvjmJYiO*8_NEX0{5sKpo+ zVHHEs41y158tvW(YG&Z7=ME1pt5axWQ2R8M{CegEu1=N)`z zjeDb%BXklBaB-j{{f+~@c_1$<1=z>j(3iZ+la$3&=VlIhvsLAkzi+$2^un{7 z8mf8Hif>LNh5CE|($p_vlIGAS4#IUHg4Zq*qdLdz6Z_o!{6We$_{}fDQL>C!1L?i0 zc|uAVulLKegnNPFJ#hT4=aGLdnLsIz(uUBA>wnDcQ^YFx! zqna>N+a3vIH@iy)jqLwMiaWR{&Cw4P38tdXm&wWSR{N;UkaZq_Ki8Jwjt_!+Qk< zT`4}O;qm~LC1|NV;%PhbD>U*PYeY6uR1iM-0L7Xr@QteBNmH`DuDiUGUF{lv5JqK> zgD6kImRkEB7^x7M*ca7AM>dl-P!`r6qx>-t%@;fsf^?LD0FzAgj;m>j$_-uTShird z<*C!$n^v4z^%arSU#OafqTiVtNMnRq=9{G-i&4YZ6~hUIV!2a*?uG_RzL;{ApmPifIJMnFtpU-oT>S{-&D?ctv(XyTiq)`-_o&P%Iuae3kzIAbONk4vQJ-9zYCH1y(Wr-x^k5NI`)*`eVT!h9b7H;jVB zQu&6UUJfMd3L%rM2Fr=vy8=E*0&yNc)zjT!`+wn$y(ABfA0TfVjXp}dK99a6S-ye+ z6=;oy)N47PcLd0gQNJ98W35p} zs$1;}`XmYd{qQObFh4mFN8g?o#%ZO{TMI{unco&VhjlG%!c(42ZVa|tm+m4qmwEIX z!QrEf5`3#VnkZLW#T1vxP(m#vNqB(^!$oizc~?B?LZJ zbP2TQmbt<~JHJVp@EcyZBmh}RPpgrn9v9Zz_)W-;@Kl+x67;g`R^4+7-b)qP%E5N~ zG_qyf{rGFxd}0Ft7C01a7ZbAUeWp80+Z5^K*=SpCKZ|{BXNLg$T4}K_p0!aqXDzv< zw&4l(T?T~L{J}S#y%g?{+YE_vuvpg#*q-VVy?fplHC8^5iae=IpxuO+WuCQ>pFaG| z!n0ZU*>{gy6a#AOgaNk<1;|5a7np(;p@Wh*Ne;FX(#XB)T`ci`fCLy`dxCrPlzP9! z6}jOTrh^bBi=) z^3?w?v;e%ZYJi6m9&F_Mj3AA6qH>&S;R9MV)vA$;N{Bp$4U zNz_&J?#vT;I|+C98)?tOu@>bdYRpXR{KmoN4~0aS5%4v5aFwP$LMOx{C25$djyCQ$ zzl1l{fu?)YK@wD@`|RAB@C5#>&$pYRcX5)xLe(TlBC|dMk)6n#HX)SjZkQJ-da^Qw zNjI`B`#*eJd1^UQdmGyF>;$AHlvW{IQaSR9f_~7Tdl9qHRL)M2A;q}VaN5u28$9JV zS$H-)ndYQ#DM#}C@jX-d0_0n=akIP7g8>0&JfR+EZ)fl9_dksW-^y}cFl4adD%kuh zz4#LVJaOxwTu_X@B=M9SIz7_aA;jF8!TLP0KKB1|*rBrn`dNRNZMb0{e+rod-n1T@ zMoD8Ctnc+qidz0`WqRpF{$_=5tQF+@L>LcxCO+IS*DUy_P8<|M9mhU4G>=xUey0dh z9{Q>g!kxajcAi_!m&JzHL$H1h&WhuR9IuY1v+E=ZjL7d&Xl5l>!f z+q7n`|M54j8^4<*$nwLHorWZyM^cg+&vpS=uwtOnYP?np^RK@OvoE&m62h&ork93A zRY?DzFsPsiuv311G?HE_ZK{v#h`3<;-WNRw&w_?0Uomt@x*VB%Vclge`BBv2R}1Jd znXiVM0vaOcP5lJmISH>dz;mpb$K?=I^YoW8?9Gv=oWB`Eo@yme)p7_KedJUjY;V%E z#c%MwPncSIvwV?X!`G0VcU(eyQ~WAFM^{yPZt*{%>71hO-+OGLGB6{4xQP^P?a!9G z6O!*B(bKT;PK%vhWyGD9-eW@n<4($rtt%6(wPJVrWk}6EbeMl{5`P|~h<>!-dLjC$ z^=?6CScy2p-i*@nsvz?ljRzlfvgWQi!FJW`*Y;9MK6Wwa^I(G8|m>)jiGk zFOp48(o|o4ONfr;m%pOZ@L z#l@!@FJCrHa6FR$ZDlY!s|WIsiro4VYZadSjy%IOjYQ@Tug?=sQ2r(~qFC1L*lJQ( z_Qc6?H&=1NCQ(ba`w&I2S4+yNmnd4vHzLg>*)J5j(mr-*S=YPRG;p_IHrKXyL zixo}2EJ~mk=uDk9I^gxFa4Cm!_m9_GywXFzo%9sXk_rpT$=1ZPc4lO#&SYKUlQg@l z!Nn@8oST>F$9)#PmQ23nUv!?xIoTuaH%lkx*P&6J5W_cNM^Q0nlE+VHkK6NC0BHgQ z_tu@{Z8_^^df!cq45g{pc=#f(0l}}{`ox00m4s}IrIV`w)ARjU!wB!K032Mp6|V68 zYuzLH&OA$R5np196unVCgfoGe>pP7*^Gnfv^sF4(LswKzH^EV^oqB(?m2`&BOM~29 zvQ&==bbJ2l9TvpM)GMeUA`N+9_FbVO--crIDE^JZLwl?jfK}|8oC5-|>yda(OQf3w z=d&#whlDPTr~hHKqaj;Q?vS_0Wcp{($b5A!x6B?@5cJVd^$fi6+>w{XA?fw)aCfU4Z! zpS)6l+&yvn=X&`73L?M#jR)Ml>miK=bJThnO3D!zB&D<6-^f{tKszSx+BwLdy|fbo zU!g~@^l81${tVw_bhOK7?YU$U_azqAdmMoNv>`J5w@0cq>y5O+7M^X7KTdG}<=)q8 zBQFkaA69Vk3FXdUmj4-PdgJa@VI5&u6`{iJ`kV_v6Z{T1)JRNpp37K%Sut(a$E0L@ z=A0io3F1tHzI34J&Vb@`?f%&aBbK~-r?AO7Q~7VMXAhKM{fsAfU#nTBR0$avIQsS| za3`0c)EA*n%B!Rs;phVOIl`tzd|o?il>oWy+OcdW|0&-`LtQ~0Lg>klMk%v`&`&Jc zdkDAw`O#baZtRtX__W9UFq@Xq?ec%{6VF zxr{Tz7k0e^mY*1TVead9|EVfDJPot=)vCoIKxlvbDomCT*cSf3YU}U@ak3jJNZColM}4j z45XR?Kf~R8aj7U(GNNI9u-%22-1wsw0v=q30IpN?ar8g@r*@|*O(jKMW13kxaP|%# z`}A7ttB9h(B1XbSgl$XHPl}h&(H=T*5#Uh`_W{rs-|#l3;U?~(??ed@{wRSOBjZEVAX@fjJj;5e z>hBF}^syV};7qJx^!xsW@ayLsRdQ@UQl*1mD$-nxdc*btOoLxw*A(gcysLpTUVH%d zR-7r`Qs!vo9lF|z$UU!xee}83&8VD;QcHfbiNdj$a&lP(I~S50EDzJ!Yp;i64Jh8e zBY#*OMF)FEp9?_W9EW9OpI@#q<1$jXtJQ`ZNq&TuBd+f)$n4PbT1H6N|5huk zdLf?eUBdh#RfhHbS=FNj$^oeF58`ehOVyfKwC54)m9~{By3PP7>AVQfdg~<2-GV;( z>U*4!P>hEWG$O&BDN6c1ZgpXe1=1x0KkdnFg83a7sNW?7rOxjT4vds`{d@D#0;lcV z#1s-%bS!4{`#(QsVRgFkZ0|m?qZ(Z4x!)JD`6D>nuruh~!?Uh+?lBNzRGU$3{g_92 zxmWTaj!ZG#690KgxE*lLg_w`}GQH;0Wi8=ujNo|ko=N7T;k#V)JGjvivKeAL!rDR_{vagZ z>Rlz~{#;0!(+hC)Vx_;$qCfl&b&;+&&^?E&7Ew~cE2{~z?&K9{6#U7jQRG1Z@*bR0v5}O*NI;ve z-UkTIcJ6_PLTFA%2(PY3!2(phrr`E>D|O$suBNxuBi-xpi-`q~JDq!Q1zAFPCXmoM z)meN#Foj1#c%!bJAvJf*yYbbRjPu-awXKn>Us2F@1^}8Q#GEBq?(z4k?=sniy(&uA z#8Dhf!62+`)jG5D=!cCcQ7S3K&{nKq1O~+nF(?l#8rSGe zV>(_L0D0EjJN7x;z2lzWt}VFzz5LuHN6PAvH-_NXbr{-_*P!D*E@B@+Rx~xw+e+?8 zyqdKTJ&Z15vExr~j}V`HW~0CdA7a8Na|pt4kpqbabgl@e9UJ4`YU zZiad0U`tv-rwD4=5Jv;dZR%NqY0kcqB0u{dFuZ~Am((P6CAr{|zsoM}UhhvuDt`=H z@G(wv2GX3xOSUc~ZPSDqvV~=x&i;v$&4FB}^5E#^Bd_#OleQJl3N!3CGZ7?r4<8t| zF51eAx$NYFi!*iP+zmx0n|!d5?nsN>HeQnzxF#isZDLP>QPdO)nq9b+Z@QhT6Qz}u#Yfvzx%;6}Lr-w;V4Jk1~EJP`3+6%(_G`Rf^#4DLKyKRwse(+P? znH;VNA6VvsvZnxK4yp~EkHBTdOO>61v;_+f7l|U;l3u*b=n8YuKuPrx+9PC8Z#lrO zGjM9Iw%Pnva4`pbOgb^}L0+x4xBBh(EIo3VD`cmTg>P4Ai{+BYGwuwg4?n_@{ULZ7c z$QC?TOg9NL$wjh#$k4I3D?DV;MK=c*PygHLj~o_Hrr3_;YyHa$W1kAMQZhztAHho% zuE<4C_?gRtX}}`ZN>T8H1e0?f!QG8nHZ$EwD-_R_Q9-4*q*MA<>g>m?>UJ1Dz&PM)?|oEMm@kY++^X9xEt=Cm3Z1Zxuz-;NV&=Z z*s}m|*NpyD<(KWg``z=da^oy+JA)Od28@JU(eUiAFl;r!{r!rGyUTcHvs@55Z#P6J z2R2VYDjhjx>JUib8Psh8b6_erR|$Mk4=xVyoZ`Kw;wMO^`0gNkYfn8%=SG`1&1V5q zdQr-1Dltr1lqcDm^^2%lZc76DAM;=ksp{U-nfYUnqgRCM!dS(i59b@PJYKaTbyz_^ zz{H7?DoP*~D+i#pT0A)bPk*E`1D2e4yoDPM{#?I5&NQH}4LvH&>e*!argT#aNv4Ud zX@<5E4+!gcyRxT1~Um;L#4`Ep1rCD3)aPvl# zAvsnjw#>_-t0}<+tN>Ij$m-DB{`FB4wA&Umj8s>x=JBOk6jAdIB7GhCfhso=ziD3Ru;i;L+N5{|M`0EH zqFs`$Z=p(kpt)a0wS5JzCJ-(OiMjJ(X3%Rrcn+1_7T^y0$-nZ&jD$Eisx-sOZOPo* zY@W+T-iOzV5Y>0IMJfBp96;Lq*Mtw z;u`Bhzl9qJbeG`v>!Jx9%BExMHbyE`ps#tsH4t{yy>?SmSXG3H*fk<%hAHhh`c`#v zK+Of_?iL(p$Iw0sH0noPzB?4b*`y=R<`#JNXYrbq;0rCoAmJ;X_0sEf#b60~QuT<{ z%y~6TW~G>6zQ{3!V8ODvg>*K{8c(*@o_cH>gs$cYr4N7f>u~lH;w;$*4G&ZCEF}Cr zUUw3E@tNX=r;L3(Yvq^M0__2u6?@H6uFIl5U%$seWq-2tkw)q%96clFnWN=`aWgZZ!z@!EPEW_{W7LC&N2xg0h1^@t($xzYZqx3Z!P57iSxj=?GxG|{wGkTvSU`sev0 z?~S9u@y$QbU0=WC1!Lq-c=}(xSBXlpZ9V9|V-7}_&R|ex2bQU|uiv~2{6*ENqt-la zVO!g^C`em*i(GE)!Uj4)*8>Ul98M@P6m=+ZhPqU>mR0fC3$S+91lsT#`Gd(xllgRR zO62DDI?e^rBd@Na+6_5d#DXUjTdrTju*wy-eY0>&uVX4_Fd7=yU27uThPiRW&y^|h z<tYF11x=Wa^t7}Y!GW%F%CwlP@X%Ht<8kymidHb!NK0N#tBTZXz7Gy#Mk zBBd=CW5P{3(x!XZhaXsPaAeVHTH7-ETUR9p4NSUQ zFE$by^|Vk?AJsV*IMs;kSClCpTV~v}5G7@2s%+Bq+HaK~fu7X73Oax2JAlflztW21 zH2FQCSzTPj`V@N}z3nE#ba**ladK=N;N&G?XM?1$r58 ziD~x9Y*$4dqJiIi!-&-t6g2+;pF5>g7x~Z);kf{@NS8IL+*}GI5vt#Lhk|>4faW>P zP5l4-=qA{RAe1Gm;tBJ;jTu%K1z7B>ow>a1HM~2}=+#Y&D z;9yL|DuVmWEK~2Y3g$7}tqZo(+G-NlxjAnTU9%@KqjY8iiYlKt{uGUvwOo+-CH8sQ zWz31IUQ;bw_`I+!6VtIY5Lidl&qz^XXwiKt4VO~Y+Ex_vfZC_-wnPcjhntT-SbG%w zd8~&Q`dp^>4u3%w!ZWX?zmPP?(24#APv0s(mu~Bgp3bu=x18MpW6@8N>P6B{2aUa& zpK111Sr%-Ur#!U*qf2!6**zO}+<3EJtStlE=i36|X_|pAA8`h{Nkt!X`Yi|WdO2GQ zP67y_ksqF2F;b*2rSEIv?DceEed389^Uxc~_M%KBazap&x5@;F&ymsGeTvb7*wph0 zbpP#X+RK|0ZZbvORh(~{J+>Uj0WMQc{VBa6TOLpU+fe#=hXa_=k(zvOq-F8-sz_4%JG3!!Yua0hv>;A zD#jEbuM%XxEVCVob#V*Tr>eP0C@fY~0ZeVGLT$_y={9jbpnr0o2r|0ot@qaq zp}Nbx0?2nOer0g}db$&bH@W(SIwzW(kt*Pkg-9;E4=6rk}3jVzh+G`LAIU zD~HPD8%;T5WHy{nS{Nh1zRCJEtX>WobH_dUUPdf>h_W>dgEJpf4DP!4glbZ?vaih4 zyyn}bGF9vNdBFw`&RXpehP^qG-m?TaJPg+>qMa^z^vBrfN=Tm8$pzSQ|22`2Kqk1K zlq~MJtx7zp&OKX8!R|?#sX;tY6ggcrk!Z9QBCU@}D3`UOi!r%|a<0`lD^pvwdt064rutSxqc2FetaW`^M!b!R5+40?t z>rncwm0;>QD*UlljA`O*Bty#GxQcUrLFf%bNhsaCQfE$0&VTv-{6HuMMzJ;8GqJ5?L+I$a3eNYx4~V!e%8zu_V#OakoJ z!FDZna%m;S&t9#OKvwQdnu#GTSH!NvD;o{d}?J&GMwtv*jA-1y4ic=`tpeSXi8DHuZaPSTSw|8J)% z$)H6}%!EL57Iot(q>_R6;I7l3KG(*kcW}o!Ncfaw&>0%2+9`}FF${uot&t^nEYnry zao=OZT$4%P4TH=;?(rJz-cQstz3@cUIDauyE8U=Flh{52&0Te~wzDoO0JJX1NGNH#-4J*zVFw?!<^;$lQxKuGC^g5*6Zck+C=E9Gf7j^CWv z-6)01+8>h4rq0R|pWL(3mx7C#M``ZZQt-fZl;|a^+=%vLUxbn6p1ZY`W~4bPVmwk& zV1(GumsU6RG!==klNtYXaS1eIEQ=f^Fa1#f4|6cn%L@>AiW)iZ-<`8`L~UF`(f zw@Do+_w9thMq0Ew)N>d4IlAO)6r8OarTpQ=%trC~AewgL(b&9TyIY&)Uf%9T6_#8f zNG5LB(F};@(#9QrocM=rW&kF$bhv9=cqH0+T$<5ubBQF4*$C6SDLLyDvrloDeYnn! zS+EB^*Bbbkf-`r7n!7~ckC-fqB)>F1eOZGeP=Ci%lnyn5@V`W%aoyM=wqwe+mY(%g zQuIdpsNWNGCE8f9U5A2hu9s7vK$<2(k{>h%_q5o&k>yFpDn^)&rLjY9r4i73HNCUe z0zG&=7#Q`^sfgFqh8S^V=-$I89y&ijCFHmOJ0#YbyC#^wYm2$cYvoUHHgsCzOJ8CxW}^?49(C(Pq1tWlN9$kAa*>s_}ThZ zE4bUB^#m9HY%$~5ufZXQMfB0?SU@{2yET*R<*{A>F=mY@Q$9$)ezn#EJf*L;Rw!!y zc@)1`r^K0GqCZSaB~1ok3-|4wOfcWT6W@*04pzql?e3oK)W$q%scIf-oDySd43jpn472sj1;D^p?Uh}4?+WsQMx*C=@5LP2J2iVMlqa}00_4hn^MBG~2c~fe<Sy$r8k^H7W`3STj&r4WnGOJQ=SwvgsfEjWana3 zH`A2Cg^+R2HshS@%=L`{-koAR00A|+ac!cns_2pkt03}5N5UpdfXX!>qB;K{CNo*a zHjTr(9j{gtxc`7E6MP13s+aCupiYAzKKJZ>QS*bguxLVccW455%d!>-Bl{ ze=*&B21J=SRv64{QS|Y1(!x->L?nX!&t9l&Q3vpoS}3kH>Lf zv+Jk07E5(&Os&Ir6FU6--d>2VD83}jis|XXG`ju=M*p@_b(5yrP)jZYHrIZ2)j}QJ z1DfC?6*+Inq9x)fe|T>B2|brMBv>gfao*_e`W2Z0CMk?BS5U4(M!|35L&IgqZSxzY z!KuD11{oaGZ|q)xQN5IotpZ0X@z2HFZH03X9i^y0`H2fcVwNk* zv0-uy=h{-gBLys<{_V&He^3JSqhXZSrYbg%$s&u1GQX(42#;66Wb1$)RVyXLEe#zM z-I{Auy%Ia}9tB2%-I>PTK%W#(Sd^Z-^F8J#^#H8B~FM4;Q47!^nIS3h^ zbx$-^lmZ;>t1OjV*rse8P_o8_k6$S{T|xw`}?Q#$outrzMl8>dOdU5f(`p8 zG7V$);EW>aNm496+v{}tM3XR?`}wiPL4j{^033?mj_UMU1Tm!8Q;O}QPe2MLF=0I* z$&$US)20Qh-WooWDKr2-Z>c{W&gbXVh3?F2aLC|U+ex}kd~7=4X_*}qUbQkJQhWb;G*W=4|Eg{KPyYVRRDewLV6 zsUF>Lwce3r`(94(XKcMP&HG-|+qF7g_n(VtxvgIH^7GknEyDX>2++Ia<#W1-hZt$}dru1O! z2N;6Ka@miah?D|aAkLRCG9+QXLN2yXUBDxUuX_cGYpyWo_t8#gb=5Ne^$gY4m|#Ur zK+2Hd?nXc(cBD$DcmvF-mb8Rm;+7g?eS`mcW0LKo`|KwoX;+mnylaWDIUYmSj(q9S z%%*TKA4cB;Od^9F*&Th$ejdxy@oIyKCN6Rz`c&u+MtJeB2NnA*vM}7G$CDJ>FYL=d zed?$X+1Z$m`>M&{)k_2%zrp9GBstRr4Cs*c)X>wCE=A9*+@HW*s{_A`XJSjm3Xu<< zUhj;fZOi*MRg84W5CfLV`@PdvE?-19X4*!~#T`dbZ;o{(b!Dex4)K*^7$h5-_uebS zOoBq1GQE6!l37-vL)A=Ipz!tz*zyQO4{OF{F&&~AbuT^+5{TXj z!~(HUa4`3UE{oitW_=u&lJG&_!7BoGNei;sYj~lnyj)w|L7pz7yvrS1@%%iNaqa8C z$Fn(|jVBiLFB7>pzlByD6=h#a`{O=_4VZ$wFG*{({X?QfV-nwd8Zir}Cx-?6XF%ji zh3HQ2&UffZ2hLx=6IeK4oXz`$rKu>|Y zaC^n%!5QU0##9yr`;X_c438aap1QxMqI!iGyE&q!8uQV>+%zNwpPZ$}Wuk)H1}l~p z;JKZym2^${-bSmG6*^rIW6dpIg|Y4k1kw^Z8FiQvpWmiiC5Ia-yT$aZjQ0!U&ZBx5 zmz1W_JD!>XCw4%>&VCs9hxC10wK?eJCn1CVfgZm@D@CYBB&;A); z`2U(j*I$c4!VD_z{k=>$JT`I5e3rPHohokj6zGu;cNsb*@DgnLJl=n=xWQq5TPW`7 z?*vqyKeOR<-&0}E{T*(2x)G|>Mr#_KGWhtX>NB|Ey@J5;Z-a})-HObUxhA?)+xWTh zO9bw7R+N^7lCf*fVJzNw>rHbwEkI^8k;1)|F&KAkLMJ1r{mxqJrya9hVKSva3)}Jj zhRLDaNc(SXIF(f9vrKd3o2F{>?>?@BzCX29iUsBvUcfIkI_FgM0gH`1iYK!+Pech^ zi{C}rmOba*m_QPEYpDOP*hg2v3zE9N!TMOii*x+2x(P{}I(%!x!<=*TSuvM&4_ADM zjXO<}9KhqIlMstbdmqg}*=22c#1kfN|5G7O_mvJjLA_R# zoo4nx@sSN5IT0H?GW1vAa+jt=RqTD{x4{AV=#TJ{XBgAI7Xr`8=M4tqQO*ks#+@o} zeJq`3bIFv-3(b$sgRyfX8LxMS2~Pb#G4>mCx?K@Uz+ckLb=1QKn|uX69ug^ZmfU>p zu6rSQC0(i{0ZTRP-bB?*j8Dls!G|&Yt*2&!z;91mMK!#wW?!999;Mq2DNZ>&L9|;9 z(M4tH4X8#jPrO{fxck=I5c6^%c%_tfs%ch?w8mW^*~YGwz*U$Uc+6LM=U(%H>}^bI zS!|tq7uPU30 zGb3f$QrOYPm`+-?j4pd{mF=U`eAI*iB3#uy0=ek|{vIH5e`gL>mMx5jPo3vY<7u^X zL~dJ3ZPu|IlxO}1lesN+ivU<%s$pEfr#F$+@ZB+z=~6ri848P-iWxq^_p2D0+O>wE zk4Hq^L`t83W#>8(_Rh3F@R;YpQh@Pw))D?rYu_rg`kv5XP#AXv9>#THPEu!8Xhp+% z0MhoE%5vP1*h9+FUPjMSoxbeJ&lcSVK^m+(cmvNne-&r_3NBw(T-H(ls-WWIJA3UN z1pf*7de4-O^H@4eDaEScL$CiB&cq+Y*sJ98Dw%o)4sa$pO6UwB?NpwMgP;7n;MfkK&dE4w-oU&&m^y0)YQ&o0A87MW_9xp?w9^HEngq_gOz z0#%_R!7A~m6?5;pi?T&jugyx1YNQ;0zRt3M{uzbxPiw$R_*(%DM?~3G(;F+8*`h#u zW13f$z;N6K|4K#SDYj+gTki~b4HHjlHwOj$ZRflEH2bJ9xit0TxtZg=$*rkAQiP&% zqp_rydh{1>fnL(1%47vT8rC>*x6T3bbMS5hZljmbv#hMi*&`^s%uUN(MaeShE|iQ{ z!&%$nEHp)xryDquvd&6O(qf2m^mEKN6K>O4?49Y3w?CAszs``-?*YTJD{Huq5d=*jMqn>>k75Ni& z+Zsn~Yx4&bO$6R2WXcl8$JE5@T9NDKNZnB&Y8*YtKA8{wrGW-0b7Njj$H{<28#7TB z;KzbA=>NR`pPtCeXT9Kg`-Y4S0eI9i9~fNa*;^Gp)O%7qY!Dvk73lZW`7H3W*ZFr% z?j;uava>Ot7ZU6HoOd(qWceka)p3JOTf=bnu-S)r=Q^2lTc!ZoETg3~JkEw_Jx1l` zvDbVSFzO~}nK1o!VT2Z8XUB_0LbY|R)0Qa+FwfuD!Xuecgx@5NmhmZaM)5N@+@qNJ z_{u@dC4i!x|2A>&Df{Kj@fC=JG4v-U`i;Vz+vZ<)scIuyc#&v>xOb6C+@QAGFF0}* zupD&zLY;hfFL#)LPwmqLX=14Myvcnqm>YpoW!U+(wKvyk4a2=p5QbQj49h1>jxZ-4 zLNMpp%S;xyx6E}w)oXVABI{2JH&4XNnh2Db5GShYa_7y_M^(c{!sLVHEHRApN)_mR z2Va+BrP*TpZ~FboxD-RxZK1>Z$>U|}mpzkAp$~V)m_m4}x#pT}ZZsdzb=EAuKpn;w z6~6Neb?J%T;rNbLQs+H{Q|C=lX6Mqwzz zaPF{E0vNNcGJ{wG6Y6zYM$%=l43-r?Jmt<>!$I_A0Wpr zbv)~^7xLNh4XfRYXmM3zbLwWHbvVm)aJ=pB3T~Y++5OQz0hr+0ev(Y@M?NGaCJyUc zn8Z4q7I@Md1vIkAv81=v2eK7<#Y^xXpD^me6ktECJG+pH5{YL%UDUr#;`lc0n zGa^=9T+CH}u$8AgkM;TlW)B`glc~qh9i(PMApHH%qqwgh_($@z3~mzOafB_!U;l}) zy;&>ob_`8+V`U13dNgPBs2Js+@LE(~PQ-_t7Sv7Q+rava1BGHH3s?&*Zr~NJ5Kz5F zGPF~dP_?CUPrqUKT9m)(J-r!9GPXYN=3QyFMs|F(8Se^^bR@a{E}fP02Hml{UdN-n z8V)%Ysg57g!k1+D{wwRVRkiVbFU45M%~(Wii1NbOGNBYy-v;ZG;pooq*2N9d5T$9` zj&qnz8)kI4P@-@CaDzgw}?)F|q5<%3Rx{>_&1Y@S(fNCJVcGl-0gLwTX z6KV&Z%b1|)VqE({o@aZi4_$}e!}` zD7|H%s*h~BUjoyb`Gps7JQbxTka)+<2SW}*+~7{f!UUj8``v}p701y1D(WBm#J}?# zOmPwb+8i|9ChbibaCku8ji5-XWW&rM)0z|VU!LQCx(o5qK-ao<3!?FGQTFz@TLC|P zy926ZQ#6)V1j5)BJoUIZqJV&UHw7uK0{f0t?h|bKE=b{&;+OH)YZ=nXklVh};fEQq z6AM{6DII6~&{J+a_|~7N4xa|Q3pg-sGnKJ)Wb)%3Bz5Csh6&4csF59#d-4%wg)O&M zeh#oxVT2{L2yjXPO5UncIzZb3)Bx^Vh8eh0xHlg5D@{WY1vT^3y~&Ie|LV^7vAB+B zr!HWPz3yxW2AJ(3Yyzge>HjVjMa|ybYZ|=IsxXF&1n#RfDa(h%^N}042@M@3S8K~p zFU4hu`4d$$N1hVu$=@^e{c&#qcDPYcD}!&(ka*`gw`6SDR%U{?GHfUD>R>{%_9Bq@Ib2tDw>2a0X zO9PUL-}GO@(}x)9g~+dkIO8LdUd0+4#{PK=y)-)YxRdaPpFZtJx2;kNA3Umm{>>VJ z$1Oy@B*eLQvt6rANg2}p#$R1$q4#!)cltE4>5CQ6*rc?eC_(3-7Ur1B&{bvV4T_(A z0-tBzae#`o@mJ#+PPmcMk!FDS7X(7sTO@r{KFU0wWvt2$bv`gQ8GgW=h97EiOTavPj#Y`uDN8y$Ws~YoPAKs^CFiQnb z%!M+Iu7P4hVb7`W(tNq7`W}6Zewx}+TiYX}X&_ZAr|Ma|wz4qi z<)H_7y5x}v5+ya1q0;xa%@wY$`krnwO~)6PcNseKXKLf3OG|f`O$yu`9^9a?w^g#A%9i$--~<} z^f_z=%kg*E+FOvJgeEw97;fJB^+olaJZUMfRa0;RZR3D<%~yD9fXwD%{C&?~NZ+#HpE8s`|Dufww*>wi-q)s4gu?}p zAAOfE<^k zr!+eFd~Nt3H!mt=)^UY6H6tN9HnwVGbpUeMaxNZ;jSG>#J_<#tyT~x*;#s0Kt{VcdzBBl?=&{=P0!20C+n<_m z2Nwm#z8f((DzNVxs1_Tjt{Lw%MGR%>!LBp>Z+lC@va3r3`EExY0IO=2-yWD@H3O{b z3M8vpL9X&=kA8rG(h!cutgQo0Pd%i zX4~6bVM88e;H9AP(&m=?Gj|IOhC`V!jQa zWm~|q59Cb!&h$&2FwupkCZ#_VaRfo%hlxm+0qRJTo@G>xOj%ucl($7&y;IQ8nDt^} z6cO`7!)>Eqq*x`mcYb0%OQW~uekf{j7uh&Z6pw);E-63DfLRG~DjUovxuaI3K-{Y^ zC+bV<^{<+XP~^|*f|oZf*0>k(*s~}C^}f7z>weP&MR&NuVI{4nhE+F`kjlQo@Bx(d zUV_2&b=}K)uy|v+xZ<{m#{m1RtF(aE@@^shYH{D||9fw}2w&K;PQaEJYYrpI0 zA@5o`UmCDA?co$p_|LF9e3%zdUdK$9XZMA|upqwZW7@)1FBQ6NM^4rsJ6?`$lP; z_cIaGF)NZf&UNdl-YZb#?APuU=DP0b;pE2a&)CGrFi$XVTfVrdw;t6NM@()|{6d#) z{H>CU=ryZ-f1zvnt0=mB;v&<>g)O;qb zjCUfu1CM@vK+|7{Z`qs*JkXal)8xT;)3Eb?i)W!e5Q zNl%w{5v`|^e-9L#*@y;wNsHAojaw!-7n)4!8md2>$9j)re+xDFD$j}kU3RBG zRWnBJ%jDM;v$r!1kjShWdH%{-#}ibo5FRS(U!2~FLK<`~VrAoKvt8$N=SPE&-7gDl zHIn{JS8V6(c6r~c4MK+Rsmc$>(M|W?sGXQLZWf*7rNYl( zOMS2J=sY&wDga&Wb~=rGu=nq47IpC}ZMI_YoMgZFea?5o%I3&l**=^pmApQ-{@DC6 zyi&Gbgnf#BmcP6B=KOfgJn>N;_myu?^6XPrfAR4DAg;Tm8g7)~HBv-dFaH7*CA?~e z{SuvpTU=pJ#W<}*sonfI5n(*&gb3aTEulS0aZwO)9Ka$-b0}7?aMDDD_OCdz-shiQ zjE(_A(5C&j26oQkh$ay}`(4tFNh@G)F?4Tw_YrTqgp;TG6fQBfgCpgP~ z;gvL%<}35dtsr&mCNY3RXNwD?38CIhI^~P>0AmyX^HlTVz>;j5t4-{&8O;h`in4Fs zvvbuuzo5D*o2W0)=b0AILO=0U#QoJpS-fu0Mcq4&@ zB43gIx$gpXkd`Ap%58f!5#U?u+bB#<;Cs(tAliebMKnDo^d{$!XR?@(BvkRKMYZZr z3+thLcE05}?`p0UPd@xgPy8$flA|Y}DMFXNZ@vTb*s~mzhKIq6`BFE56iZ!!<1JcV zjLHHM5;A95Hxc%VIgmLyo9E@LFGCN+O>q)rQLN?oHtMcpMlko9$@BX~yw3dD1Tc6I z6z~&L;dB}NrUM?5@*i-`3Z+lTI;!F%gHOPom(3;eJPm|(%16X2Ot%UYwFtWxu1tjF zKCf1@34nlRpZixl_WWCj^U8PdbFJ!iv^`VhXN~vVH}xgcdjS#p!-|zqg_*>3nh4uX zgzfAy{LTkFN2I6dv=wAV+s6uUUPOv*conTY z8OBzq3@Q$IHhk~PgBbM-T^3akzO=|WVQ7N}Hrv|^Gd0o9C#}f$FRB_cK$b5x@K@lF5h-Sj zqgZaJ!oOgm1qH;YQNP7=#o4#iXV9bh@Q)1sxynwB4*Fa7UFZly6#o+&aL|Uxm1?htjsh-N0cYe%I_j>#NQ@q1O4PfEf?i!b=w|sw>XM< z$!R%koW(R+V!*J0F1mMRtEWXd|0rWyY?q-QED1tSv|k=I78ke;PsLq$>{t1Y{Vzrz zpWQvn;)I?+`jGqFIJc%FTbA{_frxlkk_l1}krKrYDEGXo``qg6>Z1RM8`><&;d%GH z7!^A$T`t1bp39=(MB_ZyzZfvAu`MJYyzhp*rG~fzRuB}wl6wV(Y7qrVN1T&yJizce z?&sLjF%F6!Goyu1~EiGNeqY%JC^P{9`!ETY+{bb>_f@ zuxp|EM~i!2jMnD}>$=2__i?2hJU-sJ8K)gOOV=u71qH-8+YQJ|vMe+r+hXH1=Z)IhxOdd6-qbfT z_{vPG7uKEjt$cCQb?+3nbjyuTLItyI+!HZ2&)Gf75YkPvi-3@BI`#G1E~JU-MO2P_ z{%~UD7-RI!hq1vUt)nEKhNI+{Biqhzd#cblJth^{-U=wO2HYs5?D%PhqalXk~&NqDB>PMHQLmR3|H&n7c})V+K-Ad zN2Vhka!&PK zluyi#zc`^g!#Ge6+ybrk8*+ZG%z}JzO=`zHOoZ)&NeE;YjwA`%GDT z?X-6R#WuIX?pC|=-kX$PudZ^77rPl3m=KxGiDl-#ixWPm>saOE68E`D^Za~6&&$eH zp*C*u@mh&d@%0C9;{u zsw%noKt%SrR0p1Ub%W}KCHC63F-`e_=aPNjI51=5B6?$g1iN_UjH3p+&kLL^@rjtg zx0;GSLk8f=NxTCL1bdDA_2nN3gydshWAV}Ueq(x=bj@;4ar%&*gvi%i^?qAMm(i3N z!;S3*@XeKwvwD=%C``+U{DEm0KBx8`RwtFsQ-DmP%%b|k( zWc+;BO2Y9A#)Hp?6U#@Z%a+a71o#^Ko4~79&<%TAZqOkQA&UoZNL}PXf<`8hP{nyW z@Z5gv%UcZA8CML`p5RcW1O)_+b}-f1uXp{9WbyxpwaiJvtnaYb=$V+KH59*0>s)VB z3{5}6G)tJnN-C9KK#5uW@w*1dSlUH{3-o8Agu6g2Z<=3IyFOl+{SiFvkO4T(Or~60 z3>Wqiv^WYFi$cnlV?Oloyf|T_XG445qOG_X5wd<>(O;DBd|;M+Fx_n zcFWQizQO1URV}oK7Fo*Hi&-V34w5MPe=`76pDO9&>Gw)#c2ZSn04+AZi!n5w5DD)V)eLe&-+;_cVI_Ua>BU$|VfJ6_6|q)` zqcM5-mp_p5P9vT_fnrw7{Hp*EDe4za-k^UuoJt&7=E(cW*aPYN%pq{@sQm1IJku-#31d^qBbc1A&8)$B<>{3 zy^a7hYI=!j?N^>W`g}|Z>cnKK$R#d3?pW|Stfl|p~ z<|uMg=G(sQ3*q`euE#yhH-mz?v(9pjTp~fGuX)moW;fB^#&3EYlf?obU1}Ea30qro zM$kC#tHNX@T`AeF%TF$$haP>Dsx>3J0bjOetSf3`i4y)!xA$gzT~IzYP|WxGO!*1i z*#ZZQz}Z?|k`eeN=F(jecDd98XlFr<1KPVF+92CTAcW_A%q+C=?JsWA#X}^)(FClJ z@Nq>uIooyV$>m69dvInID19kBkR=>Nr_oh`mf5n>+dolH(Uro`8ocKf%!wqa45sX# zjiT(2vkL#w9ve@32oox{@1QwU*LF{O8S-xs%s6MWPG>mcr1gHHp;-H`^APVy=wRw$ zyByuvA}8XxkO<+p!umpQ)5>oYKLyk>v`O(1h+zQx%TT5|4`b|=uYj?$BnZD{PnV?PiLx`m*(%F5iiw2ZzFztE+;Ob`-T!4{$(|zYDlxPh zvv~R6_rY2Hz&bD^Xm$=psn6(h#h_#Ww6ZP)+GFkf0^SoyUfNI8&Ut!9 z!oW4PJH)4AfzE#l6aFkK8y%R)Ky$qStNWAnX<_|GzQJ9u#;%h12vb@G*MC6Q8jBpcI8kWV^z&M9$| z1Ywa0a*e~kzt~#y|8qJHoR0R*i1rSygrwG<#7PJJjHl?X*Wq9-fF4WbDt=Sl57adq zE;Dpy_&u4_wC#`zfNtePV`CXS(1M0l=xe`mo)9#52itFKy>+qc*cMbp%@TG22S7V} zphsIt2CqrkkH~!nx4iJXAAo3Zj1?y(1Fl~n1Px6#QoQRoF!GoftId*mY%$~!0@`#( zz2x`~b!vhKwB+FF*a{+d+_AN8mEz`iM8f?O6VtkkC%~5c9*G4_HaqZMfTEZGv|s>aCcYlg#!p(L9W z=Y!xNg}9NF4T3lf9lT(h6vtvG zCk}Jx@1Hno-o}_1+s$)N8(63~dLEt`Tgq@T9RH`_Mro)X`Y60Nf85Rh%#g%>l%UHj zpyw$DQkA9bGrjgSs0xwqNsE_YFUiU1QZ1fdt{&HdXW6cL$6N^YKi)-OrHnuKg}?F0 zpHRZyNnj^$X7m-e_gZDmW2wrPDq_#0KNjYsMy+$a(f)ZAJoB}#o${@tK=yJG0zS$vQgTVxJ_!e?Np4r3~ zSxpK3+W-4}LiED;0$Tl9t4IQ?6Q3Olul|2nGF=qv0S~yHy7TsQq{~X3A#MKqX0rya zpd&IU21H6v^@L@zoAnZ^MN+9D_UyD4f$ZD&DU|XMUJ+ADyHP4O(5&Tg9D50l4=z5P z0$%HPBryV}F1otvKkgbaVHB)}=CbHb`c_$?$fb*Z z5gX9+Z!~i+!1SB1VlS{RMk(U?W_?4{eXuXM=b#A^N|9|y2AXJx_JOk{!XUfRyEg)ybk7>^Rg~%1_ zXG}EnK9e%EiR`OALtFS;Daer29p`OVL|(>Ata)=!IA)u;{9Jtd?}627GkBD;jh=1iO?NvyV?WDIiWs4_u9{+rNRv^ayoQuE(IS7oC%9Q5xGYi zYUF9Ghv^f-?4(E7BYH4SEnMwH9)Ahdu*wZ>z=7`Qx44oJFrpH?j^r{~Hpv+LGu>B& zeKl$041!=v%pLwB8D6YgYB853_M%#;4SNo7W)T>F46-ow*TKg<^7OQAA@&%$lu7D) zJrU9LMNZ?~HPmr=A1<5!StXIkYDplwW!F7gHizyo{V2_1>z{Rkg|g1gEB z=4bo0NWnt8j(uiz5j>-oQ=q;_B0rIF+cf`v=UXOKwH`XQzPf+u&>q+6&j2J zy*xHK%PF)z0_H>Z`HxT%CJ+*4yzDU*5`~WM5fP5Wy-k;GpY# z=n*XkeXfNKi~61gDF+A?^67wvUUNWscW;L}|JX2(*I;txSp86dA{V&0pb<5gcXfwW ze=pRS*nlQBNj7^e1do@jF%-o#xd#!Wp&7g9Z2H?&H1o48~EHnEUTRor#sgcCSb zfu9l8buj6qI7=hy%nFQoR?rt5%+?r}cO=09EzC+wO1fh%i^7mffq=~(C~3QlYS=RI zURX7DgR~#09(R=~?8))66?D~O!K><#C(bjUwn$1^6`uD&4WTBVJ!VT;5kWX^z84~L%CfM$?|n=JL3r)vrZ2Vowg)HheU5(-F5p0rg2#l{^ZB83%qg!F_SVu?xfNt{W~bdoTpx?Fn=DzN zC~_qLkgbu3WS(=MG{s+3{*IjkPn@1UW^HnP&sIbc)5I{T5<{fwY#%}%_cp{>=H!>~ zz2Sc!r@E@h9hq3FAUK0eQqRpRjHgSG{TnnE_6Iul4|V*^zQ z_s~+c^Lvoz$4?HMV`mK`u%t6OPpBf342BONGMP~+Kfd_~sv+_hs;lmF$4&8?n+@9X zsjFdFMLkPRbtRu4^{0bu?&iR3r^m|>cubQAl{~jkB2KA{8Z{JE0bQajWWBvC`$L--1z9{f;VLJSPgD&Zod8W4rI&`>b>$c1@@z<1l3Th zF~EnJW8$xJ79{eZN-Q?Bb~S^*$td4PttZTTgs2whL!2>=5L%3zWY z;CJLG`UgFP?NVy;AIJEjY`T(ak$YJ=eLe!f;k+9TpiCtEu_*PsC9c6NAMt7ty84so zy-&ZG(ZsN&ugTLFWEa?f)J$K1tnu+J#i)%wjm_))&TCfROf7XMvW=bXKi3xHpLY>h z;m#!o>yhC!%L)S8mQ}jvdl8JVh&yX@e}qdOdG)wvZAAM)WQ_A zH^U-N8=d8JhHAk2F~QqTa3}Ex)0T;p+gO5N<^bO|p%Pmjb+b#C z_lp#y-U^nGLVKpw$;LWU${3AQsF8V>s;m9|$$tUYo7EhOkzJow*>CtG-!yWwb(gN8 zA7Xz}Z}-fjmq)+ErtHK+n6vkg<9I^GA_N{gfM*bptA}}>2pj^}K;{y&%sZgjXiqde z5^^TG6`R~(B+7nL|F**-`hn)rRS0t5)SBv%t({#0vFTznE5LHJcYD3%TRwcAqohWl z%GfwR|3#V~audmBL~g4Zdop7&0*NDE5Jv-%^hEwn6bSP9Ul1fOAhP_>DDtfk`)yU1 zuB-Qj+shWxvkJ}~(pipW-&R9XvEB{ZCfOU(b^uU2{Xg59X+AXs%lF@O1zV;vDzi&B zu=JwQvEy*G_$eA+?)BdW1q>|XFf5gjbBzM68>;rs{lOBET2w{G#Dl8GVy>QhZZ9L! z8MF^AmN%Q%92zVTv^D|H`!VH3vBQ42%Eea zCc(OtQoQBw95ax}r6DUNBsOHpHuJOqt+TyRAeoIyP;no9v__24xplQPk!rC`q>|4L zHuE7WUuIw%b|vDNDj??S_B=&}XSpulIi)z~NJ+9npUU+Xx3BwMWeF+k?T+%pTQ6fd zk9ZG+`TAbjTB6GsM>%<&Z5w*Qs=9Hi=jEYWW|)f2u1XF=HnE;9H@(sY(FTN zaL*aDP11G(H0M(hE7&$PjRlpnE!cemH)}raZ2gtpD!+k`Kff3o)}BaU)7rT}4uCccK@f%uFaUCluXR^M8XbAlGtN@%g$FFNf-@6PVJx|nLQ^%9oxR9AvL<^tsgryCKLTM!!Cd4}qbUgosE;Np(o zEY>C02y?=mdE+k%V^2N=N8?=&YoF@MQVe_Q(m-Z=KOcHDf|~1rCm%iBkOz{8MdGs( zhv2J9ddc^5cZSHUyBuV2jg>Owhn;OtP;qJy%eiDt8hW-&uTz1})-Hi_XRm@SD6^QV zIN1JpQhzPjuP_(j*Yzdq{OVMFlE)5EB zUGvVW|KTsM<>(gMb`N6dSu-^Ma!K-tCW070r0_unO3fi*wTc--P<$rHVAF8*H=|^! zbwBgZgZO5!ek54zbCUO?BkH6N-vl^5+92_vXFfMn*aHje_)vi1;rt=Y*@7nG&3{o^ zNH4FI;;TPG(R*;Xn+AWAq`7t@Dq&t*fKNwr!L1UT|1b^b4fb@&g&<#1c6k%*LvULkFI|y0Tq`NXly6oFmPrps_UK3~v6Kkgf=JL@5Qz z97kc5|H=_ZT9^~66Y9-VG+ALbkI1*~Sg-@aY%44w`w zWBcC}uWhTxjX_RUR21_}^1PHkiAA5QSn}D&u@s`xLYBRA$$KQVbr=YCd&zj+WhGeX zzx57ITGc7$c&3EdTo@3ayIC3=f5kQvg(T$D%Z$9M2y)(kzAP5~`NHH6!e^NZ-WRRn zK8x6utAnP)-qn`KVCwEYnvg27J+>QAvgcgRs^Gr93&P0+@-1P0abm^**pf(wtpWaH z>bh#b6Szu;G~M)CANA8J>Zy8Eu?In|S<9W4v5N&(ym|jk7&3Cw?3~J~b(LTd z`$8qkysYzlzGVL?#L2hFH!M7{-Qz|M9O&W0dL%fscIiMX95WIvK{sAXPi`smqO3vu zGM)bsII4$hmQIQD#afu{o^=7{86SmIsL`Gae7ksxDns?Vgt0i^86saHa_Z!}3LA76 z11uV8siEx{WeD8Ye<@65en5a&9f<^m-J&pD`(Ig0uzG`fxsHDir?l~28j*xpGHvfX z#<$~Q?~wdlZDGCZwRRVMkb!l*A9^@>@4H2lh_ep@|1gDTbv*;~N5mv~vB`n+sJ3R( zl_fIxZ_yyr6Sg};8I}nD?Z$R@L%bJYB%e#Oj+DL2_iqILAwmB{kb>8BPe$(*TL=X{ zG<&eI_(Y5)^ZXq*`!F({ch5b(M0Ttujmh&<;;O$QYsd)E$Iwt+-DP?=voSc2TnpZ} zbk1gZ0|9P!)-5x!$ca1Zun&MrS=uZxl2~#W5h4DMo5a|~&SkV7RUIXG8Z|sQn8x=E zA^%*Pawf((F@d^95n9K;vR)DqBQP)(!11$QyV(8So~oXbzqEFuby6 zlFz{4%T|ot%e%xoeh2}^yuewnFnxpOkTlW)hAxF@P}Hj;Ung*zsvcYOs_LG@BF`;r zJCW23$_whT(e*=ymKFet%Dn{h^=JPXaUk$*D{2b(Kb8^bv^FpHMQ`7~04dMSYqox6 z;9%Hl**nlAxfU77nD%fp@l}(KeeS|P<7rPkek@u}EE;aiRChsY%<-)-_}#Ur$FY@9 z?V=qZjor?jp$)D#+D|$o2fCB0*vmUZap}B* zVR%N#Vn>Ak6A#$N{}v$5Ng}y3ygzEtlU7of4?sM9QqRiJ+=jcBuukVBy(Rd}zLrm- zeJC{bNZ{zadMfOc}5edmCi(^Ecsv5RT%HAP z2SRPdBq~znyS#ZKjJ?-=;}!TyDsK+`TI!RejuxExY~|?=i;L98Rnzt&1Ut4pla^$l zIkfOUaXuTW+>Zxbb9h}6cr_)Js_BH z@%1SrFzu|%3N3t7Bh1-BHW3mu!9qE}&(i&!?iy+}BlXdU-pOfslU@GVT^NidN`Jnd zKz|H?S<1fYlh>lAZfcUFBbF{8Wi`u1;5s52t#BP(}}Hq7mi?Ve{w525Vsq-4(N~@Y6M7Mz@-b76p{L6if)PuV&*GHy|P5-%l zQj&Ga#VABTZd)O0_1>d?8*>n|8;wc@noNb2&j0H;#!yn#3H4;O_&*(U&|hH;1O6pE(K#H*O&5b>-u%);nk8W5$&<_ z1tu?2l;yBkf9k+KV~-+FFj)`H)FH8*Q7-O})jM$xR8|1{4N5(WZ}pBF!N|zp?0czr zd54oIjF72x5NV~b>jsF7E)DJ+WSAw$-u+jW2EOcydr(;K^Evzu+#&ouF7p{a$eCl0 z5WD5&VpUUEK*zGpjfVtJm~)AH!!Xf(P^-S^0hn=G$5{(ABS^xB@9V_EV+nfJ+#2tr zey~7o(>eEG`;q+Ve+d+ny{Ch3ezm3z%(JMql9nZ@GvubUabKi7;mOe*lGCl9kvZyB z5%B861xhAwIdH%Z6apFL`+(u%MQA>deR2*Pu?O$X%~N5sd6_g^P7ET=dZYDV&NlIr2p5s z=A_!gQ~=5&moJo)cq9&VMu~n6q0;U)oye`U*YoLV(aHhBxENXeMAouy+xFG69$A4M zmheBm&HT={A=e0T9@jr^%OksmZ{?MhuC?L4P_646#@A;OYII+wL}xe{I7uUZqx$=e zPqK0Y9vc2D@w`)*RR@-?4O&@z9wZ+!T5#YCq3ERhOx4tVOSf~$rws31c}GGJLM_m^ z{>9SPur@IDA%TKT`{uJ=(OM$q>F^ov8_MQJ3y#7vh)1rOgpKKhzO75F{>~`0hO-2! z%@1W#tHoQ77}oMA{|X3YMCid$6AzF8O8KDC0&7@`p6am?PuWp6xZV#jv}st$y`8fb zXjQZy2`xufA)zHs1_TXk5fL`8UAw^VQt|<#YllG7To^VN#(2l$FKF&sUFpr2#fP+i z$zruGmFM1aWGn*J4w$nG(@*ZN{R6u{e7ii0#s?L2ppeB1?K5KX@9P^q6 zgqex!{Bdk>rU;yghWxB11+~1*2ops{AjOcCyEtOFC&EV?gmoU1XU1eKcPoyD-@1MB zz!zeXp8nSa{I_6kJ0OeMfP}x>(1pBOe7ug@1u!h0zr;NrRN}by%;PO*T=SE68J$-d zMfGyqo%w6{JP%k`1jpyO{?z#oJ*>9F$%xFCm|8?^{lwYwFE5ODYC3n7$6`ma9rMG(Xz2tu>fg3!^IzoOm9AJ)6JD{ znb>9;a{J@4UkF7PjM*23_TwA_Q^yl_vTwTGzpr}a>?MS)Wa{rKj_2K2#jVc&ZmlKo z)_AU6p{oAkJy-p%5ZJ$^MgNg3z>y(>qerRz6*`XQut>opU;xQ&*V$Mj6($Mli%EfR z`kIxLQeF|r_6O;BYa-g`Ue9&tUpy!ZPGN9ZbQ-p&5$1CvL* zlP1m#R+FEE-C=|1TzVJ%2Soso0=@inQmR^`V-70%w%ap#QsS$)Qak7UK*rd$bJ%Z3 zYuW;9bT~tn`jtj1;m~!U8uMTbM}e&zBDQ`$&k!H{B#I{EZq7Rp4pWSS5N{4iN7@ls zwqTgKznZ$;-jnf)NWOQdvQuDsWyI*!KXb+$kKOOfIEo_5Y@$RKmia#0t8PZBMh^K` z@;30Se}T(wXTRAA&L)ZE@)u8^u{LJgq#Z`NhbLUq!iCBNXxK90dkS8-r!gTr#XBu{W*MlfS zq?dwN47PYVFw!W_@Zm2&BaVAn+5}sE#hlAML6mzDEdoKG6 zv>IR8+7d_!F7hqhoKZVT?qK{&V7ny?Sr*oZ;7L_A9IlT9cJ^Nia_Mh(7-!g`2L$Ii z!*zMDA=VDUN2>u$f1z(0-G510-H6=11iEp&ceobMiks9WfCCOWjK?90TlL8QEwc%B zq`;??FZXidb65A~7;gprZ!9Lr4_p(YmlwSJ`^hT*%p^6AMg1d0tDQjF@EStd`m=PA zM^M1)AcJQ2zxx{p#4UE#EHOQL+vB{F!mMmE6k4pHba1uG?Mc#>##7R$0q{Z^3S@VW z>f>{;QH>iA6*L{%|DdpM+YVhD=G#5DH)OmL2G;C-`v7OFYYXVrrk;L7B7OYBAvX8t zf7x8NF@YV{Z&#?h8Ip(bNvnImsmvo~ImJ)G(0bhaT7raytMtsnuC$*~LqR&6(AQGC z@rf(hhRAno6f{W;P-RGRS|P}$A}AnzETrE~KdR?Jk?S`$bUYnKz1iNONx`a8bO)MD z(H8&QCmNif4@kDc`pEXo!7j*HnEa~&!*(;Xd`R;PthLuGh zQX>#9KgPM@YSjRUdV#jWqj|eFarSK?_ED`t|8ryNJ}aS*CF3CiP4AGvxBe&w=~R!) zQjRUkhNrC&jH_o1JT7bBpU^T!nB|^GNw9YN#k&|bhY^{#pSp$1Q7xS2AyrKjGx`c( z2jV(C4)uKseEbB}Z~>{=4}WfeKpCe3kqpi23G>|s4)x!Dx_oyAl!l<2f!*Wbg9|P+ zdT^)4?&pQ%M^(Fti6ZDpK36f;0G{mvoC2VF;^&eAQ|;3ojC=grT!-$gGP-u~EojQC zm0<#n8!AIh@JpPGLi!F*Q23`haf-o-(w-%^Y#$+V%bDsU#HyIk(dl$$S)*gEdzR2G z-tTmMrY(ONhr3IJWte}XeQ~?%CH_KSCW8^q;kWnj&u$c{}w}>B6cYU?= zZY9u`@9Ih{aL~)7K=n2)4)C^iWE&sO2cofm>)k%dXxjKh9cvo?f9<{bKh*30KTayA zgieae);TQ>9jPRhWwalu=9HueZL(9@GK^_CQi)DF5@Mu9_GByTiEv8xkaa>ah#|u; z7@x=WoaH%n&iniQ1K#h~FRxy&o7*+lbv+)B`{VxDt|$6UW{f!VQvJg`pXE~@IvaIrBxCkkBeOR9`hD39BS|Avn=qK868NU(zHIY z_1iW*X9#m zcSWSS)^kty&VTZ4k`<~k;S^s-TBE|&sjkc;XLU4A$LE*k=7-N&-&rY9wj70l_s!n>F!ZUAr+gt(M^@0xwM^6 z?2e3DyF&FSQ1=imKMG$+dR}0H7C^;z53X!R`IYS6cogF<9iGUCcWd~&7iC+Q-%&rM zXKv;Xt!l(YFi0c9etE{_fwslCx*$y3`2)QhU0Q6N#j8E$D3jp(^FnpQ&+PTW+*ytl zK3ZQLm68QmXM4h*^yiv$1~4nJ5{2Xp1OQPD?IJK(FPrqo+S@B$oCvT`2r15I z+FyQnVn1JPfNM@o>N1a%(YL!>)~+#pyu@HS?ya`J9{==rr0LzU*~@hYSC>~uI2xcu z&O?Bz?D!8uho8ZNgwNV&s-0qAYxS1Q?!&~{!H1eenbi11EmB`d%QlS9f+8qspahcX z;zOQ)^k<@kz@C>^mtQG^?Fhm(ar(;H^oL2O@)Xr~XQ!|>EHhM_S!MO)dHD%#KEbFF zWeywH{TOuwrFmCDnc`raM`>rHwssebEF*|CHAD?)I>X(`l{>-`^hAFPwp1{ngxUAT z^ua)`G5>5v5&$o4i}p686DQqKkLz+EuW<{@pe-1wIp5i^;OF$>~`34nIw50f_j1MR2$wzL0?KLxuGi(EoMm6zlACquyBJwZz?cu+(E2o?-*%7YiJ<~F zA37)ZlCN*!-PEYr=2pRO`WHEBCL6>ld@agL7oGYt^T0lD1POW79@tIQLFy!TT>%4Y zXy;a7NZX*7huP9MQMJ<0PlTNSMB;91%R&KGYq%Q_bV4HA!|IYwgi5M{wH;g?QrT8@ zkH*5=;-iikP{UH&6wQfWF66=OJXSQI1ir16nVut%2xZPA&p(JMPGMvF%uyWv_z>oP z?=j@@+LR1@sKn-3pDjb9X)^dwwkR`zez!~Dd7&QE5%P zCggf$=F$DePc*0Mg~%4H2(NT8@9!}Z zBEoo$wW3hB`z2bgYj4?y?FH6-MhERsN8;P}uU*=pHHpWN3T6Ou-*9Sw4W<1F`}Nfa zn~mXm3^hV3eP67jdGcY8yVRNN5nEzOedwbaC;>uw#+GEhgpJPc+XN0x`gTsFA5r)3HEVd`r4QAAJ(9(s*#=ea{Vf{)WVu0U}^y8S1(w zwMs1~0?zLre3UEv##DCwjq`Ak#LD8~YNp|u+)ga7&>!d{ta@45-6!dB2C zHM1tCkP_cBZ)4*lIM*I~EKdbG_!Dx>LWTJ~O2A~v>7Fl6B~DP%##gGogzn44C#oby zV9#0H85Z1+YmwE@$#%6*2vh@YTyYmFuN%822C$Y7#&QXui>6E zB2()jDa7Vy1H_oC!)826>A7d0VFJeW*Xx7+?92^a6WpRJ!fWj+nYo|<2bzymrZlpH z|H>6%Tc>k^=>5WkLS$-BZ2W4U63<6KCRDF;EtH{yRY1AnDNJr@ygS`o9hIJ>Vfbcl zwAC5wh$|X6_PT0?G#7T8dhTz_Lgj4r)o`9iD7~2rg2(wWl37HQx8{K^`v|Mx;2#YN z-p&0x^|`7pzt{@78`Vdi`(|rJwohq=(aSTF09}KBSEJ@VSogh|?qsGop|aw}f)&Mt zi@vi=Hu$RxWXks0Tby}39>SZPWG(hec~t5=moo6%D7(amhLc;Kx$idHt?Zis<~}49 zZFK_7gMgcIxAnl>hd27cyPA%%9JUm$$I7PX1~P}fJU^7rND%xaSB-XgmztF*`=+49 zBw?|T2x3YeKSK|Vu_^`x!Xi!3$;nr}LC;!8Axwq#qy-r(LKtPZ%Qu|GbRFK?T-|0L zU=`d!aulVT9y{vFZ*vFzrVD3@AhSu|hdoZt4 z;NUoE2gKT&kNNDg8GMW;mYSR3q-rI|z8`%hCN>oABh1ACK-up@VGJA${-d zYTsD=af4e;yS$bD@kyDGM7OqEgd7zQ$r;m)`}FxlSTVYE4lHKUT%=hSxs?`P@kVkZ zj57zzCS)&f>;rWc!>#q8DA^-uRH23R$onN2Yk;x=wKQIv=-a`YQ2?^1_v9jH!QO(^ z^D1e3H6~ZBGe`525{;+8+elfh%kB@ zOsnpt9?Mf#Fss$7GeaqkX3!10OTIV0vwRpzDZFnl-#`AW7&hg_6rm3{g?G^{w6*+< zg*FczNwhFu)A=SY(v;t8m-yJZ+dM1mLc?)B4dA$qK2K~Dwl>RqXV3IHkqyo6i?C-L zO5H3p?Gv@bfHLO^b*hcoZJ%A}lGJ~1tUs*uW`f=<=*_d!-}5<5dH-i&o;_z4^QqIB zrrq8H>baMS!SVV@Mr#=OF>Y8iE)d(!56)szqk3}+jkTH!ch|dJT@np-s7Gb}XiEIa*9j&3=kLsgf-jj>ty4;BI|Y)S z6t9wd7SE=)YHJDM=@y7QntNF9d@1a(M)}_6RSfgiMTyK$XqqShjXv$`X0?IJ3VI6w z?bB)}ih@%8)xgV#(?CY1jS4=q1Y8GtnJ)Eu`fVK<{rI8RsXZus^rPot$W;ukK;ti_ ztKFZv7^xLtlp3qsd5Ynwh_%+)9LesTrn_y6!$16fTBYb+qVt8%-T5 zAN|;zkaL+}mDA7c4ciT&h6PB!r}*(OS(*5mB}n>9VRRx4<$_X2R@(W_pa=Xua`%m- zKr^A#|1~aG=YDd&&IJ!xwJO}HeJUYKJPxJoFEl|1p;~|Vu6&cIw+uaeYCvUSRb{1> zY<|gWko!wer-Pr%+X=Wk1kj#8#kwr;r^N1dlpuUQzS0hYoPa||9hmhI5rsHpLmgC_uAyzh+PkC9 zQ@pZaVZLeJYM%@H8eS`>4BQ(VLGxt9RbcyX693I05KAA7d?;N|g!VosTH|C$L{W|? zj}wFOL!pEd|H9XE?NL|HK?3YN=)I%Fv~zjp$I5z-y_2IU9y=LBub~BeAp@n*jXtw{ zd2OYz2ytz6Ye$MjJ-4J`HaVeD zA#b|ti4a+ZyrTp?B+Qf=Ur1@wTxa4=yg-s|Vn~%ZWD<2P)AS}ua2}&9k6D!LOwBK5 zwTmVP!YefU)N*22b*E#rNekfi%swm~CcDQ?w&6EV{22)0o6*!(Rq!`!2p2793P*k@ zBQ^$KOWt3{*tesO^~W6()*r9lQ8+KyQ6I;{yt2EZKAtXbZNCmeJLNDiS1PGd|F#v` z^z27?mSXf7E4XW5<~zM&Ee8$H^>gldui~)d_qt`odO@o^7u8r%VA^^ELeEiP$;|#z z(m?r+B{mxkXV6op3fg<25Thxi;Tr0|Nqlh_#ye%p$@j+w zCad9JR&9y-e;l2dXum!ZMpuvOUTJtP!RwQ`@?MZGO26^V6rRU)<`g8Uc;vPLdS6s3 zix02lJ;}i@YMY|Q338AqdBo5R}96-YLCP>Yc5= zooOpXnPVp4xh}XG3(~eo%emc#_eIDaoC_z}+dYX|7ve)#6emG4bVDS~TlqUE)@|L` zX6`g>1k*(pi#+!}7P1s>HUNgUEALNbxEIJx#>J8h4=NZb_BID^N#|w^uu2Cv7M(c4 zn1)ysEd+3Zy)D8!AGRi}<9dzrfas^ATW1v9abydPHT3>Lo}f0*W3QQ(WG$gffVTRD3cE8p*B2 zlq}%8g}Cw#K)&@>;qiV88~rbHtl*Rw94R(UnBb>;bx|qfRzJtGj6SdkSQy-z+nbd0 z{KOb09QYr3A4JHq)hbU2%(%F86(7Tc^C+au6(N;paHoqXuvhZP5!P;XUu0)bb2tA3 zV98$r6Qq)Z!SuUEg3Z3JcU12}(5ciXD(v1G|4~7L9rK4-;r7i30B`?^ofM|&t@jz}f*N(|EW4^rh*1Gc{j$0hn#LlouQvst70JGleWUA{ zun)!vg_5@cLE8Zy3OfMlC9fK!=v}v6$ZN^A49{;)*VyB=Y&dT~`vZ5a08+>|-PmY* z>qMA^iBoQ8&rd1ua$!Bd0$2+bI;i)cTKp%TCg2eG@0z5@KwxV;4n_Z&^Pi=sgAVLze&%7_{=Hcsq2xvm{Pp zdm1zU+_T;LTLV10L%DUT4)r{7mr`?)N<(K7R#TO^u%#xNbWq|yJ6s0eqbrY)9?8=I zaJVD*TWrj59yXHJ6*{xZnukpBA!9v{w)lXYrG4G54=xi5U$8Ih*s*Rg1U!1Vm`1tRu#$sYA$YG$E zMfolA?mTJ1LN+#TEUVKnX#}u!F*)YxdDjm@1wl-tq+rp!L5D2g-gj*Y>#OVbi$LcS8wDOdh4iej40y$jF}>SBxA z*V{hMT*mX-x>Gww`9TVB0hBxPAYC0$AP*{H``ej_q+a{C*(uwVc`1LW%gYpQ{=9zH&5;3hJLSBT z9kI11PnmzUb|L3G=&kU*b zR+Wnd3K(_JKx5x~(#JR*NMwmvbhD>3(zj_KaF z`BA3?@+IvNC7+ZUXj<_4l_N63hRq4tYx28F*tWtQnE9x+i#<<-`Bm9UX5sWZLs;s! z9hD4=7td3=8GI!XW4?zEtW2);eANFC6Tj+j)CwY{AtSvZ>U6qZx;|~;m3m6~2*n35pUp=cCfkB7F zhaT@2gM_WS_k->mA$fWfJTk*>{qfx^m6P!Z1N-i$pWLbI=}Ts=65J=xWoyo=D!Z1i z>NC-`jB{V+(h5~Ovg_0Cm=N*=M<{8(E;_U;u>J}mJ{_S<!erJE!k#qMmT zQaH;osirjrjtrG{Ywg%A5Io~FI)B1CKL2)aE9Cz(p z`kia}IU|1$P%)H_tczcIOFchdf9dxx>X3JdvOm3V;i`&?7^n){gHma8c(lI{M}1gM zCaurKT>1;!8dKq;N$X|w<_Zcrym?IWsu?7^yH3|Ek1D+m)meh9bCkk7mi;`-MgtDa zo8cxR(bm;z=($H?nv$6L?5AG*x40)pOD(C^zRGx?d_Cpddbqo!=qx8^3tBz{pF!0t zzGxZlX)ZmG4cVocRqpj_Wa+7rt8@#22UO&Z=4k#{Wt7Iu|0QwGRfkA;lxNOlTw7vx zTh#G?8GQL~Wv?vUWfAnJ9d;+{GT3Ze-g9Eu&bam^BT9aAgpt{-Zk^99#Njz+5 zZ8VFC+8YK%HowACE<=?L19J-c+?GM-qkEqB z-NY|_UJ?SUCc-N@#J)=w&6iC*Ex4ezTHW>sK_?5}W9pw0ol^c$kJQuy;PJZGJ151W zu|X4uzSu=oD%~#iN{-Wyu$#YWs`%QqOMAyO3cdK2K*ng#5()YpV^^1Rl(ih6FMq+d z%sfX&j~%7ahGhf?z^?y_x_=Z~3sZ_EDbLsBv#(zL1$U>s*5Jz11{&qh1FI9VHWZs~ zS_z6#-(ShdevzMIBQE&qsFMa7WSOb*y6rjf7)`WMs@5z{;rBK3cc@&sZbb z;Sjk1>V^(GTRhs|a>(Wa8pvEIMzXu1F7D~uiYZ2k`G@%&RlzUufk?5c%++eKy3|z{ zk3Gmbv-6|gjolPsy3HV=R&)5wn+_~VpoxI3cNQofn_rp+X*UrIoAmK8TM>bH#OYk{ zzA4VEX|YmB$kR#m#MTSR^QPn^yU-5s#!f(Yn19V+PYly8w`IP>EM4=k`^J1X%k6Ie z*0D~}saMwtnO#wXtjL9M%r6xM&7Ncnt4asHs6DBf|3I1i3K^1CKX54ye`buQSb$<24V%W2 zvmdjU<$s9V8vIv!xD`G3QN9a93df2Jcx=EWOQ~smCHJZhgYuNSTx-KQyx@R@k2!St zB5ix^64T9_5mwxI(O(!c<;{C53;isdvSvquzX!P)+g6f^?`qDMK*{BJJ(Hgwm=c>$ zcD;}|+QxErJC7N;5AjJdg@I>*faX!AbLi%mH{HVsaCm0?`sJ&gZG|7kq!L{`;n3-}{P&)ZNwlyd9w>xM;4UT;6>Z~}jZ+^xm zbiNOfegl1dY*s!5tpo!X5cyQ17zqI*2%tup)!;Cj%9+Kzwv4qh?n1iH<7kBAO5yjc zS$|M^(XLK5a-u5Vk1+bVv!qxCk+?cK)W){KNL{>{QYYA$pqpKkFH5F0-0XYDUj_=6 zT_al&ga6V6@1Pf&{Dsko7veCk7ic0*jej=2^5U9(Mh&e>R=Nb4JiI+h%#Fsq5(<20 z-6h6lo|V~;ua#Xd3mo}$8#r>ldLVJm%ToF<0*+(`y(T^Vkv=CxUh@E64i~%`Mp%s4 z<7S`XuE3$#$Ysct+jS&nbM&@aOt-D$FDRn-7_cn04hWiT!OW`1c5&-n6Te>FyFr+< zaQ}E35~1Za6GiP!AM(?{|9b4(U#$ z9Ro9e0`2%89J#5fV$(>(Ms!_P!5>g8>+k zpX6dXlR1N3QInh2`*%gkhsgy7p-&FE&!;$U-OiL1*oeS3i)?zH(Es_mEWgDNZYuro zYbUwnFSMLl(NqbP%bs!+!OTE}d~#O_#T<BI;(hN8RZrK|AHdivL0mpSw;{w0H|; zt9~eW?KyetmS`Ej7r=cyYA>kBkC-p!=61jrcpX-5dAai1l}7B~waM{AA3Efh79J2N z5_jeTG=XqEg3j_k(cfC6wSNKP-6X5ZB=9Y_W}qBB*Q|kHm1(BCIt=zE2RQl8i8hM8 zMVDM|Rq)^^VcyM$fIIiZ15RnZqGv^}z})*b)(EU;CB_djAUKDCGn)pAi?3a?iX*n3 z#m?<+a{s=_M?dOWdJxY&yz|Ej)$7(1-ulK3S+PR-H;w4f?vLRQEE+J8>{qVVY^Mmb zG}9NDvJhpfi(ag{=1a$J&og(dv+VFF3q5AW0~+Ib^v7PVBUQ=JmIDfBm-Y0gCcGtw zuW*EMzLLbw0VSDBYVN8ZVN8AN()Xo~9G`IFUc2*&jV(gm&HUG0sFS}oFPw7onr}6* z0mS){_%k?k#+=>>mS6-PTmfL@%_RLQrN2g_K7)ev{FNZpu~}k$^^Za_94uqX#QV|*8Ry6tDdg@P}j-V@WAe#D$Di=D7%8N0O0CPrJJ-~2dCjA?8D zzTj*RyFKFmFMO9y{>G)-15@3u!#eGsX=RV>e9LDw`Obtc>s@U^QJ6p*W-@DZe^*F& zY2)@-#t+0;$MUc;rxkb#^c)lrFn}yz zHz%-r_M!Qp8_YCOx*}2%W?Xqr(smsR_+^o7x3?x6Zu(AKMX!?a@PRi;0qaT7X<_%m z1KT-^w(2&-@y)-U#b}*@Jt%?&S{V2Ht*k0}HYSa@<)n@FFY$B-X2!^i5aHRlw9V{B z^?gH&_Af#h9?E$0p2b+g9jE8IK-K8b*v}s;O0PskZRTEI{CXMT26mo5BJ4C!rdqT7 z8TGS=Qglp!;9jSbTeDzmo97B`4IKRrrVWpL#}KVx z5=9tl2!ev-C_YEY(@}YRdlx0y!)G}GBgZ}Z9zPZ;AW2hrm-U&;l? z9*f;6p4k|+oQzsIGOp)>TVu~G=FpO+5mO$61IRSY(3P{&*N~qg3KWHht-L)d-`r)- zSmRsQgK-Cl;gjIVLNPpKH_x0czobmAz%=Ecx@{0)-6HPHJO*<-9-@B-WjLk%PGzum z^+m41JWN~E>b?oqZzr&gU5R7< z`{lsM>L<5#uN)z_>a-vLmI z`IR1ph!!q7*X%bk)%sl&UvgMFL!2uOdqd;N90DcEc;M9vZ{ww;vQ(tv|Wr^Kk{dswbo${(ZUlP1Mz;t8c+g4RgfuyRHB zmg)%Ux?c7u^Bb-EI_cK`z@oqoqnEP()()vE!fBZUA{3)D%4Y7{C;NuiFBh^fzGCQS#J!9iv)R8G6hoGoS)RmTaD+U* z84Q(A{L{N#hXD!Qi)+ZXCSikYxGX#$EwHTEYjpX535_2*8uu>=el(La)9vu~>8R*@ z%D1_B>itNF+0HuO0le&p@9Xr^_7y>OQ2e5>{{gop)fryiereIx$#t>l_QCCg;rkNt z=z^#beXH`7B9Rxi!GeWq>xWz>hh2mg#Bpvm6d^pU#-z2yhq~{t$9;dLP&f~`c{0jL zK-^RknfsXIlGS*G!ZjB#2m%>0x@@ubvrdC;Gq4RND1XV%)Gx64rRQQB^cb*@iZiiD znsgpAQR4OU`m>>p9c}XN`_h8-8uEF%i5u|>BRSAS;R^l4Sy4spo9m;RT}^$WUH zFwe?up+MP!@~R5$lGwM{iYIrt+#u*dB!}1N1doGJ^?^j{6klk}E8V!s8X4KJL}cju z?i|l=G6rv!IO~1zh2k?%u|G_b15(WX3~0lZnEzH7Q&ogrBvX*m)i$Vs-Jv67@lKHQ zy#p{V$_==|l^JPn)W5`C3?W_WGP!S}Ozvr^Gop$6%S$>q%dT~Avg6x7m8RK&O$KwR z1W>#Cij?yc!?!5VJqFj;67O;=<;b6bmRv|en#03RsjAr7^v?hQLEKa}ED0RIZ@|27 z*gFVMNCLW#4i&To4IIbPZ(y~Z%SjOIoY+Xh8?$h=o=vyjQJ~?|eRo-Yi6N56dKC9d zcw6FZ5KEeQ_DON*wHW4gdbIWr6-%72Uj1(**4ILMbLGGhnRyLcc!wVDy*e4CFE9Zt zLrcw2u$|_5+;irsKS$-PmCwih3EyC-OF_C1IzD{$-d)iD2#XUD{a^akdlWw~iqms* zx{gm2aGAj3YS#mJ*@OSG*hT%FCL z7>hCA#5Eql!dDeZJ`!yI4hgbKfxu>prwn9&=t7}Z&AEhsZcd_l5?2NT0>pDi0_dXG znO`28i>;SX#yDE@C&RaIatHd+%GuFe+c1`bT$@dEVqdfCZvZXKc&c6)T)((1U}lOge3g0>f<^gXz-7m!B_hin5-7+hYGMd%9EOUF8luiz|?5 z?G?I~{8iRwbVjr7`8t~VxGN`-MDz44HQ#gOxP(q5`;l~F=9+xc)-@z~f$j*NMWsd^ z%~XA7DxRub0@i*svkIQ{&Y!^V1BD_}omN_lJSzL$4 z%U#U1X1*jC;KY5!AY1CnW& zFp=AL`iH^6Z3SED1FZBnI-^^QTH3>M?K3g|;RUHbgAIT6E+U+*0aC+MNl9DE_7s;K zAHe?><@BO5{5?1as%kY$zhalBULiYOp1XGw@2zCPd13HnOSFdkbc4gjc??&yWX4s_ zfXjwYIRQ4ZXrPxMf3c01u2i|F82RdBt#a1*tg4md;~|0Va@{t&g<^EvD?o%}>}s{? zti^n*FX;#*-N*HemVRw@K`*oLTwAOhWcLcQLp_dJ^YmmxG1U^BAf9j$I6*8bVMtp| zzbzRv-Y*XZaksB^V*IKPi~sx%bmpM?2hmj7emVxTEiuS%+){t%w_WJ4T*)!9<&=N8 zRo6Dr7Kkn9VFOWrm=DVpu@~MsV8L4r3icG6{s*Jb*oDlWWtG+#aK0V)9D|+9;GhBf{6!_zMy_8`)O}=OFAo z=xiG%eTb)TCW3@?HsL zo|dhDGPq-am>;i|fD3*&avY^f5el1pDirRG<1u4napw9M!eS|k>JQx*a(+YQclv2zM^i1rAb?K&iCaW`;Vj*tRz)x&$vFhb{9O`OKqyKr_&U{W=r==eP^?^ zbLfmpN*)%+djpD+Djkig0@-8Wda31@Hws zCxuE|{2ws{_3HTk#r18P$2u_P+S4EGOsSjJc~o{f>4q-)J<-hImXwy4LlF`6%)=0rhi(r@J>$A=-_GN9zX zZ@~%dzHzSqYR4`VEMa!Peth77u<9zMC^n_4UvO_I-iU$7XaV`ZmLgr`&|kY~&=fK@ zqj<&)`TSKhvJi$q)vS9ksNksgv#C4WrIw>Y5Z_S>s32q@m8(=@JzURG=D&8mkNoPd zrXO6lKY1o>?im+-pX!R6OF2zIoeRwU+-i!Po;H48SU(eXWx!NX%hfI4Y~5=bo)}hU z`*hcZO%C7?jY(3wLCN0A5%ba~76rD;@R5-ZLy*Y8!UVB)ovfKdS}34i;ZYhuw7C0* zF(8x)hOCWhvR-bdrxP+!mBe_v4W96UL4LVRNcm%>0VyfJsI8&7y$giG@y{sFcV;+; zf_KAXdXed2>wR{sLgn)*uKGV2Ynt_v?#>qz7L#GBgyP@IqT&YGEAfMHIW6@B{jbkrd_%B2Wy-g3XDq+ibsqjM?4Bd$8gZV1Z7Gth#VvQT&7F0EIC-}24 z_0PKgo^CU*5GgGfgg-FZ7&O_Q+hUks!^e?q7Xk5~4%Os1)H%+Dt}8gIJ@WS;RiX7z z2FKFY%eqek3y~nb_bzp(#L|aZ{JZBLQ=D$RmDx^4DbYuFSATi=lko>(rjO{`Y6t&J z$}4PF?bnoE%YkDi4+|WmAe@QS(~1c2t4YhiQgU^lby6x8dI+_7VKJK~XTk;ZDUpXl zUOu(+U9%LoWB%5ttB@n$gg}k_K9>3olA~t-s`)GjuL5Ancr)$y948Em3{A}CtkRyX zBk_w8?3d!qJ$!4uE{0JCC36y^nww1imF^S~q5kDNu3tHpf>l#7?sjL~yztA7lELd< zfr7paAzhP-AuI@NNk4bm4<+~?5Y(eW)W1l9OizjM#YHj$8Dvgj?XGtblHZB{*>tjh zRv?~76T*?=ap%Pix=VxVoNw0a9K|BS2NUvPg##$VAA5rz+$b2(EW-;dH9w3>V32;t z-fK<1&wuJ1R7g0S_s(;ttKL91o@@DqnC0_hRU}-aKIzP9=z0Yg_$a|KXn@+VSg3-= zKMYZ_%Ba91$L<0Z)qvtY&u01Rn|~1Q#72FQf$rkXtGO6#;eJZj<%BWbK+Ef>Ku!=o zazl#rwi{S2n=H{05cw;L<0>G#ujz+{`22Pi)CPC8bb2HO)8PG|UF6c>%O@&bT7LXp z@f8NjnWD^^z82+q+?4mRZ_S_YPQIgZvF|&+{1Ym-F);T(GLZ1fQswsQRE=5fx>!BP z{KqVdd+J|&@mvL{U;%;RgJtAK#p%IrH74RiJ45zW*ZzlXs7TWvN6@9ABTGJ@pz{#2 zS;_I_y-%Kb3;FAH(YL0}nmx0WY$v=Tl%tJmR87Jp3f2{k4(I&D)c{9L-1@2UR?V+orLGzw~_ z@;f*ZmoH?>pz`7WfD1WYgPAEGCPgfQ_rF~Ea?E&9-~l{4z~9@{ClT_D>Y~sDGXzDhrl>$4qN zxqn2?L%wBV-0=kKC&5P=xWRu!pMNHdnxTu!>3uSGV`pG>8vkl&s#k2|V9l|a$ShP%>Nb2GnN|LuX#Z*-s7XiYKw$iR=mIlT4`sYP zEny+#zfrvIAmv1pndemS9tmQ@gz3n*+0`-&wc!XM)89{j6@M$NKMt6Q~q60i=&3=|18ZZj2J z^2I1;T z|MlIEdPFA(u>HD7nk?oq|ZG$KeV^nqV-{n&@Sns!m zYoxEboVUjS`N6O}+@Mea5IXFEi8iyuQyiyOVxA-YaNXk8jA$?6J~LhzI(?_%C1Gh$ zTl!CvcV(`CGq)a&GKvs*P)B{{rM}3)JoZGXzt~k5!UdRRZ5vK!rX?P3u^!I&#&k4B^jaA zm?-;@2zf!ZgSIro)eN241XTbeEWWD%?C86i2~D&J^3E*F^GZK)!qtuEpmAi;hgS3{ z(`=e@SHhLCpJQjC$_Z)!`eI;;kE%jek$UfeBGyJbA?9WnJ{o`J!v4DW+oj#w-;C{G zYF+6D#ObSlVZpR^M2D3d+98$=Y~0o@ME~N!Z|F3g-`%8dkCxoBTs`g<<#CKEzgbLB zk`*C85hIPSVtW>PI$RlR!20F?jR+)BvUe`0Dw9Y45X5L6HZdOZl-;;eol zroq3+c3f|{6*eD`;rL*<-Z8&Vka$6cXXR;YZf9xcdc3*Y5t*J%`r9WRS7#-}@a$OV z7Eb{n-MY$6m;dlT!2;^T2=zRK`SA+dvWAj^Bk)>^cZp>W52x@A5@@32vQH_g>n5XQ zE`t_gyg)Wv=+Q5X|25?0PI+?omK*RNJcgln2WEf{=5!Xv zt|^mp8~LVWccHc2=~SPzizIio$Gs&TP66Q$Q6vmP74$p7+gzIrD3k$diH2E2#n^NV zjtG^qyqXwTIbj0wpBgSx8d-$tvE7aobbXKGpFFjG70+*?5DLSWz$-9bYkj6x%Xbnt z!*J08w#>lM#%(r$tbsx%f>O~ISRU^y((1$61EXUdH@XI|kC)>qGrqsmoLf~IJNjVo zNPixqm3|r$zxIunl3l<~cJgC!46KcYPxf8Pw93TzP{!TlK*G^#g@K3$-mILE9(IX9 zpOG>zUmgc@I7fC=daijRn+@rIzUUAi+K`g0mp-D6!L?1s&hqxakn#gGAMVC*C}hur z;X5B~g@o3{NuEC=J8n5%+MVW?yq!+~Fm|Bn6hlr4G!-^%s6!+ z?-F58WK}aHls%XJ&zNvCk&y267$dnY1|;M6&(E6h$5Vu~SwHhy^0KJ@ z4XE)P%NKl45*U?tmr31k6#*uBr@*=**-_2;$Oi?*?^&~6KVp93H#96z6OROPOQ`Sv zi-9m8yoGs}iLaSSFPA$hXG|e-dIzD5iSgJ!m%I4AB!Y5{C@CDPUd*9AC2z`qi$*Hn zpBJ+T`(n`WjSWK;_d(LM-UY177L>4iK@H(>wSxXSiisctCKQrFGoOEv)!ag_{i4W< z)!o{{73o{}sWQ+PG-ma8_yd&`j3-3a%5Es7K6MO=ifUK@nu>b_lWKjsuQ4Oymo^^<6-Mz1cTjN7~I^VO{u&76Mtv?94T|h4ECwd zG;NRCtG^5~H9)1^$14)j#4PagI;Mcd8U9(cGuVO}X3x|(+djwBi^sWYB`E$%Rne8o854f*elVZ8?gMQVt;+w7N@D%J@ZF;wMh73Vp5dWZok_-Xh>_@u9Q%k42epVIvV@sXVUs77 zJE-KfVxCKrn`2xo83mSII-#FtZ=vDEK*XcR8?!^Hlj<1p@M^3n8FRJ%&B}f5pBOp| zO4IRgLJL#8ZHw~+$GJew@)@McvMLU zlkX7#2Tx(BDIhuZ;ujvBsghA&VMV99s$0qLBt3-Q=t4D3&Gq_{T-j!$ve%)F{?nCn zTpnSPz)cQNor;VO!W!4H?x;anFKOJb!sxk}V!z;*LI0+v8lOY$zY2@{EwkL5Cf~$& z*)|uNaNlPU1cY#Ay>p-qWekl0>B|>_&?6xWR-f3``5i9gF&n&|tfX;VGm%>#&hK-+ zrIBo=^>wV!q#A!;jpywQA-%4%ZRnZ+DJjgk$ zmvi3aTv3ckwLEqb;ct-2%tF}-+8i3Bzfy*dA*%x_lLR^_6p>&V#g}~_GpCCXc|2P76lKz-c>e$dkkz2v=ph4tANEO7~y%ObM#BWTP&Kx>M974TAr^$y;K@=hB zCX}ADgE~MX+w@gJPO=C+_|d0Rq*Fr)Aqg>9okb{JEKcuU9^K%paWj;#FpmUbYr%viahgj`)N?>Imlj{_ws`IPNQ$8eUNOJ!!uxL`thdG$C4-V zV%-6|%(a*gSG!(`{4R#L8Aud?rb=N(CpAg zDK~wg7<&nHN}JoP9g9^419@~PEB*6_j<C(X z1$^IKqEp_V2{vDz?0dMG*a17tWDgpLex9985-2_o6ed3bDLeyYrsmxrOrDe-=SX8x z{>GV{f(*Q8wYDGSqv|mbGa8(Jxvg6UXWW1a`j_bD)OU(@(5)4^qMYt*DM!h+HyPWp z(}5|?)fOJ^quZHdpfUZDCCM0OsvGWcJg1wGTD1fmaw*osj-48!I*rOQbE>3e)94&H zbj<}LGk=#j`|D8_FSabjv_aV9^qpXf`Jog}uwvFkTgjg+?-*zhxczzJm` z7tq?C1l_xOUXrj)yXT8mcf6EIxp3=Ws;zGUf@b{T@?KxT>T{eq3L!My)XOE^ghvPw z#|+E~a!i0~vqfcG!ruZ!d zS)w#u&_e@>1P9eVFS%g3q}Kc)OCUBLUQwY$P#_=p-tkvXP^{zsFC?)4l+~7_j#+Y0V3C2fvZ0%f! zGUMydP8ZhwJDQ8tjVL0F90Ix8qx7P@c$Ak=u|W$`JY!Z5b4CoBE739ckN*AQ=O}Z8 zFOHJZzaCTMadSSB1DMvNxKW~COr!WF&J3doieW=bgzx~(xI??H^V9|qY0tt0=NlMXq?ClXBDfrN3Ps!HApN7!;OPy@^-!yA0g-%NVrPKOx0pPzer=6Z~IR7Y%FJPJ|3*YBz>35uh3hiCwkFRXjYloBhI8z zC)cpa?Nsk=cUP>;+!R%JYUY`!R*+w$&$iP zwIFPrH-q`+D8~S62+>tU?xgc?O`4+u9iJ=5%0N=dg!KLfBo~{w8qeC$jdCstj&oS* zW-^MPXhKO9F=m)3)_Sq!IW&c^4#8di8Y35Vv}se-5I$Rrbqhv#7Ch%3T_OFEmf7!h zOZ5*lE>%4GY?V+W9d=pJ!Vb~Xf_(CD*O+V6A0TTio+#N?Dl}${!p{y1|E#Gs=r$D& zBcI}1H_=P?yP+l2Oos_PRvIa}?UBUR3yC!{&p<#)OWD;SfmbSZ@dg2|yl<;yp(i!a z(Yv=AMRd}Rv@F#>$o3!=hhtA8;q^|4Lb0PKZp!rOq-W`k?1Ae<(+3+Qb<^-qU=TH- zQ!2CIhpU`VKO@ueI$R$AG(MY|&8n-uz0(0pGz+t0nR!qdQlh2zd^khQM&1enl4s{W zh6xoV+@L7ISr45=ixB(_r+MQ{8mC;R4#=_(42UC0N=?t~{ z9Yx=-I};||#EZJGEQ{)Va~MI9hTd?Bytqyf{(V98eg|h2yUg6m%JYTOk02}66Y59h zD80%qPg4RpXlzoLo(wC%(AV$G+C^J^UdA59&Dj< z3)B`oHBFIXnIv9s!;1=-Hg*35#(tZdxYv7ygyfn(+*|T0NFR@tFa`b4>Q7lL0w8=4 zRVs=*>e?gglB#Q`<)7j+U6{*Zzl8B#AMLrJE{pTu7$+cg(w}^9{qj|i5`Gp?CzYOvFMWz%)fh(^LQm1XmuDKJ> z#}aWJp-&_lOVVX(m(1hzlp>^4O*wDjQ#X-9PB|u@muYJOBu$vCz#!cmB>ukEbg!{> z#3#XzUdxuIEfuHE5%^K&Ut;VrhWA%LK8LisYLd1p-YnPS(@?KgJLg75HQ`r0Q;6Z& z)H_l3#5G^qpgu7Msw)kf2H$Ce~^zFn-wT0d%~-)#@8M2 z?x|yhI6OS{?cvEwMak!#Nx!rd{CusOG$RCs9@4sL>F1txs^bwCdSJk_UfJvG8L+2d zPAZD3*N}-*QxUx(pCfvI`tGyLL{Vh?XUAC6UDac9dgqV^I6vzg4!ol1k6!`GO2>?7 z5WK`;uh47uHF+?gE~!|u&Y_TO(ihX9R_~7o&D46&G{?_3TK)IM4zdrYbIeCWA;)Xj_KM1GM4F%z;sZz#7YQjI#%u?C$KTjAOeXfr zAxI1#-J@L}nyBcMWGUm6VkR?UnjFAsx5}NX6it{`Q04FjGKL zu2x>$)_wi=;J-czFbDr}tRpFP;B!(4z{842*JD=oTfTlS?%TysZ9SG^DVKL7V-0)a z#>jyqLjD$d+TCCnD?(|mH!3I*DPSl+lOA+l1*B-$zetc%6l2Z=3%$zN@_fVrTRtl! zKPx{y) z`J162%KPU`H{L(ySMmNY0|Rm1|0SyUe}D9USMa|^3wHCgT}1GY|20SdKMeRk82tbA s1g$^vROD~|KmN~6i2U!pf)~Xz^?up%SflJDhLwgp_wPvFcKq`H1AryqO#lD@ diff --git a/assets/images/decred.png b/assets/images/decred.png new file mode 100644 index 0000000000000000000000000000000000000000..0b12f2ef012e54d7d0e2f9c88843720bc29fcc83 GIT binary patch literal 1237 zcmV;`1SEX>4Tx04R}tkv&MmP!xqvQ>CI62aAe01guVWQ4z;lg(6f4wL+^7CNKSiCJjl8 zi=*ILaPVib>fqw6tAnc`2>yV$3r>nIQsR9{p+$@r9`ED4dk*j22MF~N)2xmtpy?Ge z8IOtS%&Hi8g%1Ph!5~IuW*Kvmlz?Y_-BUN!U4&kZ*5YKE@ z49@$+Ay$x8;&b8&lP*a7$aTf#H_my71)do)Q^|Sa5V4qVW2KE*!PJPSiNmU?RNSu0gp^Pc>L!K}Wr%yn8Lh+`2;kRU=q6-AU#L44YK10|AXJNwQ^IFZc;D?biUa3#~2XU1sXNm{yw(t#tGnm2Cnp$zg!1qKS{5( zwD1wow+&oew=`uBxZDATo($QP9m!8i$mM|dGy0|s(0>bbueoz;p5ycZNYShkH^9Lm zFrKIEb&q#y{D4^000SaNLh0L z01m?d01m?e$8V@)00007bV*G`2k8M83nwU6s5EB)000?uMObu0Z*6U5Zgc=ca%Ew3 zWn>_CX>@2HM@dakSAh-}00084Nkl&DbOuj*a7>6N-W{ALK%4 znszoK6qQK}t4)J_xlAG`sKo>s1TCf@+B8%aWFH74xoF|wt}+`JE~c~y8nH%3Q%TH} zY;o^6Z$2fl4m{rF-1Gkbd(Qozd!Ircvv;hN0Y?E3P%9sRG5Lgm?ucKR%!Q%k0JZ>~ zz;U2}{I&#i0hc0vCAKDkyrcmNprJDUN#5x>%_b*gFLXMo^3z>(hsB}Qfja^*ob z#ttPiQ8YDwzs{BCDwi$dS7LhU=uChsz{Ar}?||iHxY-SXcOvLoGqmhUqQqz5%?MoY zgZN@bxjKPUij?Abx>_D-NuEGpNcgWFfWwWc%Z+}6wp%cfWoqopx8hxBfZY`t z1hx8X1T78XJv|GT127QHEf>?4g&=+_b0+SIe zr2>3m5xQQ$jT{6zv)fk&?9+Pd%Anc`u+s?@n?xTBYe=jX(JwvWtjG8XUHtWxd9&MB1=J^I zek?=pAe0-xTp}b0V6O(N0s22klIgr8_alDg@jpN8Q~-GsdD%GgCT_RYK9J{k`v!ow zYj3sxdu3`eu>k?8gtcr*#@f=sbN*Fqlh00000NkvXXu0mjfp@b+! literal 0 HcmV?d00001 diff --git a/assets/images/decred_icon.png b/assets/images/decred_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9391abc3d8247f4c60d3d226bc80723f00629855 GIT binary patch literal 1542 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 true; + static Future create({ required String mnemonic, required String password, diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 5e71bfd6d..d54303075 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:typed_data'; import 'package:convert/convert.dart' as convert; import 'dart:math'; @@ -156,6 +155,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @observable SyncStatus mwebSyncStatus = NotConnectedSyncStatus(); + @override + bool get hasRescan => true; + List get scanSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; List get spendSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000001)).privateKey.privKey.raw; diff --git a/cw_core/lib/amount_converter.dart b/cw_core/lib/amount_converter.dart index 71d0cef42..e01c3834a 100644 --- a/cw_core/lib/amount_converter.dart +++ b/cw_core/lib/amount_converter.dart @@ -29,6 +29,7 @@ class AmountConverter { case CryptoCurrency.btc: case CryptoCurrency.bch: case CryptoCurrency.ltc: + case CryptoCurrency.dcr: return _bitcoinAmountToString(amount); case CryptoCurrency.xhv: case CryptoCurrency.xag: diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index bd1c224a3..0f913cb79 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -32,9 +32,10 @@ CryptoCurrency currencyForWalletType(WalletType type, {bool? isTestnet}) { return CryptoCurrency.wow; case WalletType.zano: return CryptoCurrency.zano; + case WalletType.decred: + return CryptoCurrency.dcr; case WalletType.none: throw Exception( - 'Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType'); } } @@ -65,6 +66,10 @@ WalletType? walletTypeForCurrency(CryptoCurrency currency) { return WalletType.tron; case CryptoCurrency.wow: return WalletType.wownero; + case CryptoCurrency.zano: + return WalletType.zano; + case CryptoCurrency.dcr: + return WalletType.decred; default: return null; } diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index c984cd03b..38fcde9e1 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -103,6 +103,7 @@ class Node extends HiveObject with Keyable { case WalletType.solana: case WalletType.tron: case WalletType.zano: + case WalletType.decred: return Uri.parse( "http${isSSL ? "s" : ""}://$uriRaw${path!.startsWith("/") || path!.isEmpty ? path : "/$path"}"); case WalletType.none: @@ -167,6 +168,8 @@ class Node extends HiveObject with Keyable { return requestElectrumServer(); case WalletType.zano: return requestZanoNode(); + case WalletType.decred: + return requestDecredNode(); case WalletType.none: return false; } @@ -355,6 +358,21 @@ class Node extends HiveObject with Keyable { return false; } } + + Future requestDecredNode() async { + if (uri.host == "default-spv-nodes") { + // Just show default port as ok. The wallet will connect to a list of known + // nodes automatically. + return true; + } + try { + final socket = await Socket.connect(uri.host, uri.port, timeout: Duration(seconds: 5)); + socket.destroy(); + return true; + } catch (_) { + return false; + } + } } /// https://github.com/ManyMath/digest_auth/ diff --git a/cw_core/lib/receive_page_option.dart b/cw_core/lib/receive_page_option.dart index 786d07bc5..f7d69bf0a 100644 --- a/cw_core/lib/receive_page_option.dart +++ b/cw_core/lib/receive_page_option.dart @@ -2,6 +2,7 @@ import 'package:cw_core/enumerate.dart'; class ReceivePageOption implements Enumerate { static const mainnet = ReceivePageOption._('mainnet'); + static const testnet = ReceivePageOption._('testnet'); static const anonPayInvoice = ReceivePageOption._('anonPayInvoice'); static const anonPayDonationLink = ReceivePageOption._('anonPayDonationLink'); diff --git a/cw_core/lib/sync_status.dart b/cw_core/lib/sync_status.dart index 7d6b0a285..3183d9d27 100644 --- a/cw_core/lib/sync_status.dart +++ b/cw_core/lib/sync_status.dart @@ -34,6 +34,16 @@ class SyncingSyncStatus extends SyncStatus { } } +class ProcessingSyncStatus extends SyncStatus { + final String? message; + + ProcessingSyncStatus({this.message}); + + @override + double progress() => 0.99; + +} + class SyncedSyncStatus extends SyncStatus { @override double progress() => 1.0; diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 16c794a25..42bd66da0 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -60,6 +60,8 @@ abstract class WalletBase walletInfo.isHardwareWallet; + bool get hasRescan => false; + Future connectToNode({required Node node}); // there is a default definition here because only coins with a pow node (nano based) need to override this @@ -100,4 +102,6 @@ abstract class WalletBase verifyMessage(String message, String signature, {String? address = null}); bool isTestnet = false; + + bool canSend() => true; } diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index 79d2b002d..5ae1c1290 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -17,6 +17,7 @@ const walletTypes = [ WalletType.solana, WalletType.tron, WalletType.zano, + WalletType.decred, ]; @HiveType(typeId: WALLET_TYPE_TYPE_ID) @@ -60,9 +61,11 @@ enum WalletType { @HiveField(12) wownero, - @HiveField(13) + @HiveField(13) zano, + @HiveField(14) + decred } int serializeToInt(WalletType type) { @@ -93,6 +96,8 @@ int serializeToInt(WalletType type) { return 11; case WalletType.zano: return 12; + case WalletType.decred: + return 13; case WalletType.none: return -1; } @@ -126,6 +131,8 @@ WalletType deserializeFromInt(int raw) { return WalletType.wownero; case 12: return WalletType.zano; + case 13: + return WalletType.decred; default: throw Exception( 'Unexpected token: $raw for WalletType deserializeFromInt'); @@ -160,6 +167,8 @@ String walletTypeToString(WalletType type) { return 'Wownero'; case WalletType.zano: return 'Zano'; + case WalletType.decred: + return 'Decred'; case WalletType.none: return ''; } @@ -193,6 +202,8 @@ String walletTypeToDisplayName(WalletType type) { return 'Wownero (WOW)'; case WalletType.zano: return 'Zano (ZANO)'; + case WalletType.decred: + return 'Decred (DCR)'; case WalletType.none: return ''; } @@ -229,6 +240,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type, {bool isTestnet = fal return CryptoCurrency.wow; case WalletType.zano: return CryptoCurrency.zano; + case WalletType.decred: + return CryptoCurrency.dcr; case WalletType.none: throw Exception( 'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency'); diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index f023dc153..da7768ee0 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -46,6 +46,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + blockchain_utils: + dependency: transitive + description: + path: "." + ref: cake-update-v2 + resolved-ref: "59fdf29d72068e0522a96a8953ed7272833a9f57" + url: "https://github.com/cake-tech/blockchain_utils" + source: git + version: "3.3.0" boolean_selector: dependency: transitive description: @@ -465,6 +474,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + on_chain: + dependency: "direct main" + description: + path: "." + ref: cake-update-v2 + resolved-ref: "93440dc5126369b873ca1fccc13c3c1240b1c5c2" + url: "https://github.com/cake-tech/on_chain.git" + source: git + version: "3.7.0" package_config: dependency: transitive description: diff --git a/cw_decred/.gitignore b/cw_decred/.gitignore new file mode 100644 index 000000000..d8452de53 --- /dev/null +++ b/cw_decred/.gitignore @@ -0,0 +1,39 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ + +android/.externalNativeBuild/ +android/.cxx/ +android/libs +ios/External/ +macos/External/ + +*libdcrwallet.h +libdcrwallet_bindings.dart diff --git a/cw_decred/.metadata b/cw_decred/.metadata new file mode 100644 index 000000000..fa060de4b --- /dev/null +++ b/cw_decred/.metadata @@ -0,0 +1,36 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + channel: unknown + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: android + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: ios + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: macos + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/cw_decred/CHANGELOG.md b/cw_decred/CHANGELOG.md new file mode 100644 index 000000000..ac071598e --- /dev/null +++ b/cw_decred/CHANGELOG.md @@ -0,0 +1,3 @@ +## [0.0.1] - TODO: Add release date. + +* TODO: Describe initial release. diff --git a/cw_decred/LICENSE b/cw_decred/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_decred/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_decred/README.md b/cw_decred/README.md new file mode 100644 index 000000000..d24bc80a4 --- /dev/null +++ b/cw_decred/README.md @@ -0,0 +1,3 @@ +# cw_decred + +TODO: Fill this out. diff --git a/cw_decred/analysis_options.yaml b/cw_decred/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_decred/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/cw_decred/android/.gitignore b/cw_decred/android/.gitignore new file mode 100644 index 000000000..161bdcdaf --- /dev/null +++ b/cw_decred/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/cw_decred/android/build.gradle b/cw_decred/android/build.gradle new file mode 100644 index 000000000..0fb2f3cb8 --- /dev/null +++ b/cw_decred/android/build.gradle @@ -0,0 +1,59 @@ +group 'com.cakewallet.cw_decred' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '2.0.21' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:8.7.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + if (project.android.hasProperty("namespace")) { + namespace 'com.cakewallet.cw_decred' + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = '17' + } + sourceSets { + main { + java.srcDirs += 'src/main/kotlin' + jniLibs.srcDirs 'libs' // contains libdcrwallet.so shared libraries + } + } + defaultConfig { + minSdkVersion 21 + } + externalNativeBuild { + cmake { + } + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/cw_decred/android/settings.gradle b/cw_decred/android/settings.gradle new file mode 100644 index 000000000..1c81706ad --- /dev/null +++ b/cw_decred/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'cw_decred' diff --git a/cw_decred/android/src/main/AndroidManifest.xml b/cw_decred/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..ea58d3c72 --- /dev/null +++ b/cw_decred/android/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + diff --git a/cw_decred/android/src/main/kotlin/com/cakewallet/cw_decred/CwDecredPlugin.kt b/cw_decred/android/src/main/kotlin/com/cakewallet/cw_decred/CwDecredPlugin.kt new file mode 100644 index 000000000..4bd6f3395 --- /dev/null +++ b/cw_decred/android/src/main/kotlin/com/cakewallet/cw_decred/CwDecredPlugin.kt @@ -0,0 +1,35 @@ +package com.cakewallet.cw_decred + +import androidx.annotation.NonNull + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +/** CwDecredPlugin */ +class CwDecredPlugin: FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel : MethodChannel + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "cw_decred") + channel.setMethodCallHandler(this) + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + if (call.method == "getPlatformVersion") { + result.success("Android ${android.os.Build.VERSION.RELEASE}") + } else { + result.notImplemented() + } + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } +} diff --git a/cw_decred/ios/.gitignore b/cw_decred/ios/.gitignore new file mode 100644 index 000000000..0c885071e --- /dev/null +++ b/cw_decred/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/cw_decred/ios/Assets/.gitkeep b/cw_decred/ios/Assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/cw_decred/ios/Classes/CwDecredPlugin.swift b/cw_decred/ios/Classes/CwDecredPlugin.swift new file mode 100644 index 000000000..c38d15516 --- /dev/null +++ b/cw_decred/ios/Classes/CwDecredPlugin.swift @@ -0,0 +1,19 @@ +import Flutter +import UIKit + +public class CwDecredPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "cw_decred", binaryMessenger: registrar.messenger()) + let instance = CwDecredPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getPlatformVersion": + result("iOS " + UIDevice.current.systemVersion) + default: + result(FlutterMethodNotImplemented) + } + } +} diff --git a/cw_decred/ios/cw_decred.podspec b/cw_decred/ios/cw_decred.podspec new file mode 100644 index 000000000..b36789e08 --- /dev/null +++ b/cw_decred/ios/cw_decred.podspec @@ -0,0 +1,22 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint cw_decred.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'cw_decred' + s.version = '0.0.1' + s.summary = 'Cake Wallet Decred' + s.description = 'Cake Wallet wrapper over Decred project' + s.homepage = 'http://cakewallet.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Cake Wallet' => 'support@cakewallet.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '11.0' + + s.vendored_libraries = 'External/lib/libdcrwallet.a' + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', "OTHER_LDFLAGS" => "-force_load $(PODS_TARGET_SRCROOT)/External/lib/libdcrwallet.a -lstdc++" } + s.swift_version = '5.0' +end diff --git a/cw_decred/lib/amount_format.dart b/cw_decred/lib/amount_format.dart new file mode 100644 index 000000000..c09f76b3b --- /dev/null +++ b/cw_decred/lib/amount_format.dart @@ -0,0 +1,26 @@ +import 'package:intl/intl.dart'; +import 'package:cw_core/crypto_amount_format.dart'; + +const decredAmountLength = 8; +const decredAmountDivider = 100000000; +final decredAmountFormat = NumberFormat() + ..maximumFractionDigits = decredAmountLength + ..minimumFractionDigits = 1; + +String decredAmountToString({required int amount}) => + decredAmountFormat.format(cryptoAmountToDouble(amount: amount, divider: decredAmountDivider)); + +double decredAmountToDouble({required int amount}) => + cryptoAmountToDouble(amount: amount, divider: decredAmountDivider); + +int stringDoubleToDecredAmount(String amount) { + int result = 0; + + try { + result = (double.parse(amount) * decredAmountDivider).round(); + } catch (e) { + result = 0; + } + + return result; +} diff --git a/cw_decred/lib/api/libdcrwallet.dart b/cw_decred/lib/api/libdcrwallet.dart new file mode 100644 index 000000000..6a26e64c6 --- /dev/null +++ b/cw_decred/lib/api/libdcrwallet.dart @@ -0,0 +1,693 @@ +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:io'; +import 'dart:async'; +import 'dart:isolate'; +import 'package:flutter/foundation.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_decred/api/libdcrwallet_bindings.dart'; +import 'package:cw_decred/api/util.dart'; + +final int ErrCodeNotSynced = 1; + +final String libraryName = Platform.isAndroid || Platform.isLinux // TODO: Linux. + ? 'libdcrwallet.so' + : 'cw_decred.framework/cw_decred'; + +class Libwallet { + final SendPort _commands; + final ReceivePort _responses; + final Map> _activeRequests = {}; + int _idCounter = 0; + bool _closed = false; + + static Future spawn() async { + // Create a receive port and add its initial message handler. + final initPort = RawReceivePort(); + final connection = Completer<(ReceivePort, SendPort)>.sync(); + initPort.handler = (initialMessage) { + final commandPort = initialMessage as SendPort; + connection.complete(( + ReceivePort.fromRawReceivePort(initPort), + commandPort, + )); + }; + // Spawn the isolate. + try { + await Isolate.spawn(_startRemoteIsolate, (initPort.sendPort)); + } on Object { + initPort.close(); + rethrow; + } + + final (ReceivePort receivePort, SendPort sendPort) = await connection.future; + + return Libwallet._(receivePort, sendPort); + } + + Libwallet._(this._responses, this._commands) { + _responses.listen(_handleResponsesFromIsolate); + } + + void _handleResponsesFromIsolate(dynamic message) { + final (int id, Object? response) = message as (int, Object?); + final completer = _activeRequests.remove(id)!; + + if (response is RemoteError) { + completer.completeError(response); + } else { + completer.complete(response); + } + + if (_closed && _activeRequests.isEmpty) _responses.close(); + } + + static void _handleCommandsToIsolate( + ReceivePort receivePort, + SendPort sendPort, + ) { + final dcrwalletApi = libdcrwallet(DynamicLibrary.open(libraryName)); + receivePort.listen((message) { + if (message == 'shutdown') { + receivePort.close(); + return; + } + final (int id, Map args) = message as (int, Map); + var res = PayloadResult("", "", 0); + final method = args["method"] ?? ""; + try { + switch (method) { + case "initlibdcrwallet": + final logDir = args["logdir"] ?? ""; + final cLogDir = logDir.toCString(); + executePayloadFn( + fn: () => dcrwalletApi.initialize(cLogDir), + ptrsToFree: [cLogDir], + ); + break; + case "createwallet": + final config = args["config"] ?? ""; + final cConfig = config.toCString(); + executePayloadFn( + fn: () => dcrwalletApi.createWallet(cConfig), + ptrsToFree: [cConfig], + ); + break; + case "createwatchonlywallet": + final config = args["config"] ?? ""; + final cConfig = config.toCString(); + executePayloadFn( + fn: () => dcrwalletApi.createWatchOnlyWallet(cConfig), + ptrsToFree: [cConfig], + ); + break; + case "loadwallet": + final config = args["config"] ?? ""; + final cConfig = config.toCString(); + executePayloadFn( + fn: () => dcrwalletApi.loadWallet(cConfig), + ptrsToFree: [cConfig], + ); + break; + case "startsync": + final name = args["name"] ?? ""; + final peers = args["peers"] ?? ""; + final cName = name.toCString(); + final cPeers = peers.toCString(); + executePayloadFn( + fn: () => dcrwalletApi.syncWallet(cName, cPeers), + ptrsToFree: [cName, cPeers], + ); + break; + case "closewallet": + final name = args["name"] ?? ""; + final cName = name.toCString(); + executePayloadFn( + fn: () => dcrwalletApi.closeWallet(cName), + ptrsToFree: [cName], + ); + break; + case "changewalletpassword": + final name = args["name"] ?? ""; + final oldPass = args["oldpass"] ?? ""; + final newPass = args["newpass"] ?? ""; + final cName = name.toCString(); + final cOldPass = oldPass.toCString(); + final cNewPass = newPass.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.changePassphrase(cName, cOldPass, cNewPass), + ptrsToFree: [cName, cOldPass, cNewPass], + ); + break; + case "walletseed": + final name = args["name"] ?? ""; + final pass = args["pass"] ?? ""; + final cName = name.toCString(); + final cPass = pass.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.walletSeed(cName, cPass), + ptrsToFree: [cName, cPass], + ); + break; + case "syncstatus": + final name = args["name"] ?? ""; + final cName = name.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.syncWalletStatus(cName), + ptrsToFree: [cName], + ); + break; + case "balance": + final name = args["name"] ?? ""; + final cName = name.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.walletBalance(cName), + ptrsToFree: [cName], + ); + break; + case "estimatefee": + final name = args["name"] ?? ""; + final numBlocks = args["numblocks"] ?? ""; + final cName = name.toCString(); + final cNumBlocks = numBlocks.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.estimateFee(cName, cNumBlocks), + ptrsToFree: [cName, cNumBlocks], + ); + break; + case "createsignedtransaction": + final name = args["name"] ?? ""; + final signReq = args["signreq"] ?? ""; + final cName = name.toCString(); + final cSignReq = signReq.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.createSignedTransaction(cName, cSignReq), + ptrsToFree: [cName, cSignReq], + ); + break; + case "sendrawtransaction": + final name = args["name"] ?? ""; + final txHex = args["txhex"] ?? ""; + final cName = name.toCString(); + final cTxHex = txHex.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.sendRawTransaction(cName, cTxHex), + ptrsToFree: [cName, cTxHex], + ); + break; + case "listtransactions": + final name = args["name"] ?? ""; + final from = args["from"] ?? ""; + final count = args["count"] ?? ""; + final cName = name.toCString(); + final cFrom = from.toCString(); + final cCount = count.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.listTransactions(cName, cFrom, cCount), + ptrsToFree: [cName, cFrom, cCount], + ); + break; + case "bestblock": + final name = args["name"] ?? ""; + final cName = name.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.bestBlock(cName), + ptrsToFree: [cName], + ); + break; + case "listunspents": + final name = args["name"] ?? ""; + final cName = name.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.listUnspents(cName), + ptrsToFree: [cName], + ); + break; + case "rescanfromheight": + final name = args["name"] ?? ""; + final height = args["height"] ?? ""; + final cName = name.toCString(); + final cHeight = height.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.rescanFromHeight(cName, cHeight), + ptrsToFree: [cName, cHeight], + ); + break; + case "signmessage": + final name = args["name"] ?? ""; + final message = args["message"] ?? ""; + final address = args["address"] ?? ""; + final pass = args["pass"] ?? ""; + final cName = name.toCString(); + final cMessage = message.toCString(); + final cAddress = address.toCString(); + final cPass = pass.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.signMessage(cName, cMessage, cAddress, cPass), + ptrsToFree: [cName, cMessage, cAddress, cPass], + ); + break; + case "verifymessage": + final name = args["name"] ?? ""; + final message = args["message"] ?? ""; + final address = args["address"] ?? ""; + final sig = args["sig"] ?? ""; + final cName = name.toCString(); + final cMessage = message.toCString(); + final cAddress = address.toCString(); + final cSig = sig.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.verifyMessage(cName, cMessage, cAddress, cSig), + ptrsToFree: [cName, cMessage, cAddress, cSig], + ); + break; + case "newexternaladdress": + final name = args["name"] ?? ""; + final cName = name.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.newExternalAddress(cName), + ptrsToFree: [cName], + skipErrorCheck: true, + ); + break; + case "defaultpubkey": + final name = args["name"] ?? ""; + final cName = name.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.defaultPubkey(cName), + ptrsToFree: [cName], + ); + break; + case "addresses": + final name = args["name"] ?? ""; + final nUsed = args["nused"] ?? ""; + final nUnused = args["nunused"] ?? ""; + final cName = name.toCString(); + final cNUsed = nUsed.toCString(); + final cNUnused = nUnused.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.addresses(cName, cNUsed, cNUnused), + ptrsToFree: [cName, cNUsed, cNUnused], + ); + break; + case "birthstate": + final name = args["name"] ?? ""; + final cName = name.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.birthState(cName), + ptrsToFree: [cName], + ); + break; + case "shutdown": + final name = args["name"] ?? ""; + final cName = name.toCString(); + executePayloadFn( + fn: () => dcrwalletApi.shutdown(), + ptrsToFree: [], + ); + break; + default: + res = PayloadResult("", "unknown libwallet method ${method}", 0); + } + sendPort.send((id, res)); + } catch (e) { + final errMsg = e.toString(); + printV("decred libwallet returned an error for method ${method}: ${errMsg}"); + sendPort.send((id, PayloadResult("", errMsg, 0))); + } + }); + } + + static void _startRemoteIsolate(SendPort sendPort) { + final receivePort = ReceivePort(); + sendPort.send(receivePort.sendPort); + _handleCommandsToIsolate(receivePort, sendPort); + } + + // initLibdcrwallet initializes libdcrwallet using the provided logDir and gets + // it ready for use. This must be done before attempting to create, load or use + // a wallet. + Future initLibdcrwallet(String logDir) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "initlibdcrwallet", + "logdir": logDir, + }; + _commands.send((id, req)); + await completer.future; + } + + Future createWallet(String config) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "createwallet", + "config": config, + }; + _commands.send((id, req)); + await completer.future; + } + + Future createWatchOnlyWallet(String config) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "createwatchonlywallet", + "config": config, + }; + _commands.send((id, req)); + await completer.future; + } + + Future loadWallet(String config) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "loadwallet", + "config": config, + }; + _commands.send((id, req)); + await completer.future; + } + + Future startSync(String walletName, String peers) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "startsync", + "name": walletName, + "peers": peers, + }; + _commands.send((id, req)); + await completer.future; + } + + Future closeWallet(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "closewallet", + "name": walletName, + }; + _commands.send((id, req)); + await completer.future; + } + + Future changeWalletPassword( + String walletName, String currentPassword, String newPassword) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "changewalletpassword", + "name": walletName, + "oldpass": currentPassword, + "newpass": newPassword + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future walletSeed(String walletName, String walletPassword) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "walletseed", + "name": walletName, + "pass": walletPassword, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future syncStatus(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "syncstatus", + "name": walletName, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future balance(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "balance", + "name": walletName, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return jsonDecode(res.payload); + } + + Future estimateFee(String walletName, int numBlocks) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "estimatefee", + "name": walletName, + "numblocks": numBlocks.toString(), + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future createSignedTransaction( + String walletName, String createSignedTransactionReq) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "createsignedtransaction", + "name": walletName, + "signreq": createSignedTransactionReq, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future sendRawTransaction(String walletName, String txHex) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "sendrawtransaction", + "name": walletName, + "txhex": txHex, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future listTransactions(String walletName, String from, String count) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "listtransactions", + "name": walletName, + "from": from, + "count": count, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future bestBlock(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "bestblock", + "name": walletName, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future listUnspents(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "listunspents", + "name": walletName, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future rescanFromHeight(String walletName, String height) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "rescanfromheight", + "name": walletName, + "height": height, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future signMessage( + String walletName, String message, String address, String walletPass) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "signmessage", + "name": walletName, + "message": message, + "address": address, + "pass": walletPass, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future verifyMessage( + String walletName, String message, String address, String sig) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "verifymessage", + "name": walletName, + "message": message, + "address": address, + "sig": sig, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future newExternalAddress(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "newexternaladdress", + "name": walletName, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + if (res.errCode == ErrCodeNotSynced) { + // Wallet is not synced. We do not want to give out a used address so give + // nothing. + return null; + } + checkErr(res.err); + return res.payload; + } + + Future defaultPubkey(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "defaultpubkey", + "name": walletName, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future addresses(String walletName, String nUsed, String nUnused) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "addresses", + "name": walletName, + "nused": nUsed, + "nunused": nUnused, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future birthState(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "birthstate", + "name": walletName, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future shutdown() async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "shutdown", + }; + _commands.send((id, req)); + await completer.future as PayloadResult; + } + + void close() { + if (!_closed) { + _closed = true; + _commands.send('shutdown'); + if (_activeRequests.isEmpty) _responses.close(); + } + } +} diff --git a/cw_decred/lib/api/util.dart b/cw_decred/lib/api/util.dart new file mode 100644 index 000000000..42c3def70 --- /dev/null +++ b/cw_decred/lib/api/util.dart @@ -0,0 +1,64 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import 'dart:convert'; + +class PayloadResult { + final String payload; + final String err; + final int errCode; + + const PayloadResult(this.payload, this.err, this.errCode); +} + +// Executes the provided fn and converts the string response to a PayloadResult. +// Returns payload, error code, and error. +PayloadResult executePayloadFn({ + required Pointer fn(), + required List ptrsToFree, + bool skipErrorCheck = false, +}) { + final jsonStr = fn().toDartString(); + freePointers(ptrsToFree); + if (jsonStr == null) throw Exception("no json return from wallet library"); + final decoded = json.decode(jsonStr); + + final err = decoded["error"] ?? ""; + if (!skipErrorCheck) { + checkErr(err); + } + + final payload = decoded["payload"] ?? ""; + final errCode = decoded["errorcode"] ?? -1; + return new PayloadResult(payload, err, errCode); +} + +void freePointers(List ptrsToFree) { + for (final ptr in ptrsToFree) { + malloc.free(ptr); + } +} + +void checkErr(String err) { + if (err == "") return; + throw Exception(err); +} + +extension StringUtil on String { + Pointer toCString() => toNativeUtf8().cast(); +} + +extension CStringUtil on Pointer { + bool get isNull => address == nullptr.address; + + free() { + malloc.free(this); + } + + String? toDartString() { + if (isNull) return null; + + final str = cast().toDartString(); + free(); + return str; + } +} diff --git a/cw_decred/lib/balance.dart b/cw_decred/lib/balance.dart new file mode 100644 index 000000000..a88098a9f --- /dev/null +++ b/cw_decred/lib/balance.dart @@ -0,0 +1,25 @@ +import 'package:cw_decred/amount_format.dart'; +import 'package:cw_core/balance.dart'; + +class DecredBalance extends Balance { + const DecredBalance({required this.confirmed, required this.unconfirmed, required this.frozen}) + : super(confirmed, unconfirmed); + + factory DecredBalance.zero() => DecredBalance(confirmed: 0, unconfirmed: 0, frozen: 0); + + final int confirmed; + final int unconfirmed; + final int frozen; + + @override + String get formattedAvailableBalance => decredAmountToString(amount: confirmed - frozen); + + @override + String get formattedAdditionalBalance => decredAmountToString(amount: unconfirmed); + + @override + String get formattedUnAvailableBalance { + final frozenFormatted = decredAmountToString(amount: frozen); + return frozenFormatted == '0.0' ? '' : frozenFormatted; + } +} diff --git a/cw_decred/lib/mnemonic.dart b/cw_decred/lib/mnemonic.dart new file mode 100644 index 000000000..bd39e2a18 --- /dev/null +++ b/cw_decred/lib/mnemonic.dart @@ -0,0 +1,2050 @@ +final wordlist = [ + "abandon", + "ability", + "able", + "about", + "above", + "absent", + "absorb", + "abstract", + "absurd", + "abuse", + "access", + "accident", + "account", + "accuse", + "achieve", + "acid", + "acoustic", + "acquire", + "across", + "act", + "action", + "actor", + "actress", + "actual", + "adapt", + "add", + "addict", + "address", + "adjust", + "admit", + "adult", + "advance", + "advice", + "aerobic", + "affair", + "afford", + "afraid", + "again", + "age", + "agent", + "agree", + "ahead", + "aim", + "air", + "airport", + "aisle", + "alarm", + "album", + "alcohol", + "alert", + "alien", + "all", + "alley", + "allow", + "almost", + "alone", + "alpha", + "already", + "also", + "alter", + "always", + "amateur", + "amazing", + "among", + "amount", + "amused", + "analyst", + "anchor", + "ancient", + "anger", + "angle", + "angry", + "animal", + "ankle", + "announce", + "annual", + "another", + "answer", + "antenna", + "antique", + "anxiety", + "any", + "apart", + "apology", + "appear", + "apple", + "approve", + "april", + "arch", + "arctic", + "area", + "arena", + "argue", + "arm", + "armed", + "armor", + "army", + "around", + "arrange", + "arrest", + "arrive", + "arrow", + "art", + "artefact", + "artist", + "artwork", + "ask", + "aspect", + "assault", + "asset", + "assist", + "assume", + "asthma", + "athlete", + "atom", + "attack", + "attend", + "attitude", + "attract", + "auction", + "audit", + "august", + "aunt", + "author", + "auto", + "autumn", + "average", + "avocado", + "avoid", + "awake", + "aware", + "away", + "awesome", + "awful", + "awkward", + "axis", + "baby", + "bachelor", + "bacon", + "badge", + "bag", + "balance", + "balcony", + "ball", + "bamboo", + "banana", + "banner", + "bar", + "barely", + "bargain", + "barrel", + "base", + "basic", + "basket", + "battle", + "beach", + "bean", + "beauty", + "because", + "become", + "beef", + "before", + "begin", + "behave", + "behind", + "believe", + "below", + "belt", + "bench", + "benefit", + "best", + "betray", + "better", + "between", + "beyond", + "bicycle", + "bid", + "bike", + "bind", + "biology", + "bird", + "birth", + "bitter", + "black", + "blade", + "blame", + "blanket", + "blast", + "bleak", + "bless", + "blind", + "blood", + "blossom", + "blouse", + "blue", + "blur", + "blush", + "board", + "boat", + "body", + "boil", + "bomb", + "bone", + "bonus", + "book", + "boost", + "border", + "boring", + "borrow", + "boss", + "bottom", + "bounce", + "box", + "boy", + "bracket", + "brain", + "brand", + "brass", + "brave", + "bread", + "breeze", + "brick", + "bridge", + "brief", + "bright", + "bring", + "brisk", + "broccoli", + "broken", + "bronze", + "broom", + "brother", + "brown", + "brush", + "bubble", + "buddy", + "budget", + "buffalo", + "build", + "bulb", + "bulk", + "bullet", + "bundle", + "bunker", + "burden", + "burger", + "burst", + "bus", + "business", + "busy", + "butter", + "buyer", + "buzz", + "cabbage", + "cabin", + "cable", + "cactus", + "cage", + "cake", + "call", + "calm", + "camera", + "camp", + "can", + "canal", + "cancel", + "candy", + "cannon", + "canoe", + "canvas", + "canyon", + "capable", + "capital", + "captain", + "car", + "carbon", + "card", + "cargo", + "carpet", + "carry", + "cart", + "case", + "cash", + "casino", + "castle", + "casual", + "cat", + "catalog", + "catch", + "category", + "cattle", + "caught", + "cause", + "caution", + "cave", + "ceiling", + "celery", + "cement", + "census", + "century", + "cereal", + "certain", + "chair", + "chalk", + "champion", + "change", + "chaos", + "chapter", + "charge", + "chase", + "chat", + "cheap", + "check", + "cheese", + "chef", + "cherry", + "chest", + "chicken", + "chief", + "child", + "chimney", + "choice", + "choose", + "chronic", + "chuckle", + "chunk", + "churn", + "cigar", + "cinnamon", + "circle", + "citizen", + "city", + "civil", + "claim", + "clap", + "clarify", + "claw", + "clay", + "clean", + "clerk", + "clever", + "click", + "client", + "cliff", + "climb", + "clinic", + "clip", + "clock", + "clog", + "close", + "cloth", + "cloud", + "clown", + "club", + "clump", + "cluster", + "clutch", + "coach", + "coast", + "coconut", + "code", + "coffee", + "coil", + "coin", + "collect", + "color", + "column", + "combine", + "come", + "comfort", + "comic", + "common", + "company", + "concert", + "conduct", + "confirm", + "congress", + "connect", + "consider", + "control", + "convince", + "cook", + "cool", + "copper", + "copy", + "coral", + "core", + "corn", + "correct", + "cost", + "cotton", + "couch", + "country", + "couple", + "course", + "cousin", + "cover", + "coyote", + "crack", + "cradle", + "craft", + "cram", + "crane", + "crash", + "crater", + "crawl", + "crazy", + "cream", + "credit", + "creek", + "crew", + "cricket", + "crime", + "crisp", + "critic", + "crop", + "cross", + "crouch", + "crowd", + "crucial", + "cruel", + "cruise", + "crumble", + "crunch", + "crush", + "cry", + "crystal", + "cube", + "culture", + "cup", + "cupboard", + "curious", + "current", + "curtain", + "curve", + "cushion", + "custom", + "cute", + "cycle", + "dad", + "damage", + "damp", + "dance", + "danger", + "daring", + "dash", + "daughter", + "dawn", + "day", + "deal", + "debate", + "debris", + "decade", + "december", + "decide", + "decline", + "decorate", + "decrease", + "deer", + "defense", + "define", + "defy", + "degree", + "delay", + "deliver", + "demand", + "demise", + "denial", + "dentist", + "deny", + "depart", + "depend", + "deposit", + "depth", + "deputy", + "derive", + "describe", + "desert", + "design", + "desk", + "despair", + "destroy", + "detail", + "detect", + "develop", + "device", + "devote", + "diagram", + "dial", + "diamond", + "diary", + "dice", + "diesel", + "diet", + "differ", + "digital", + "dignity", + "dilemma", + "dinner", + "dinosaur", + "direct", + "dirt", + "disagree", + "discover", + "disease", + "dish", + "dismiss", + "disorder", + "display", + "distance", + "divert", + "divide", + "divorce", + "dizzy", + "doctor", + "document", + "dog", + "doll", + "dolphin", + "domain", + "donate", + "donkey", + "donor", + "door", + "dose", + "double", + "dove", + "draft", + "dragon", + "drama", + "drastic", + "draw", + "dream", + "dress", + "drift", + "drill", + "drink", + "drip", + "drive", + "drop", + "drum", + "dry", + "duck", + "dumb", + "dune", + "during", + "dust", + "dutch", + "duty", + "dwarf", + "dynamic", + "eager", + "eagle", + "early", + "earn", + "earth", + "easily", + "east", + "easy", + "echo", + "ecology", + "economy", + "edge", + "edit", + "educate", + "effort", + "egg", + "eight", + "either", + "elbow", + "elder", + "electric", + "elegant", + "element", + "elephant", + "elevator", + "elite", + "else", + "embark", + "embody", + "embrace", + "emerge", + "emotion", + "employ", + "empower", + "empty", + "enable", + "enact", + "end", + "endless", + "endorse", + "enemy", + "energy", + "enforce", + "engage", + "engine", + "enhance", + "enjoy", + "enlist", + "enough", + "enrich", + "enroll", + "ensure", + "enter", + "entire", + "entry", + "envelope", + "episode", + "equal", + "equip", + "era", + "erase", + "erode", + "erosion", + "error", + "erupt", + "escape", + "essay", + "essence", + "estate", + "eternal", + "ethics", + "evidence", + "evil", + "evoke", + "evolve", + "exact", + "example", + "excess", + "exchange", + "excite", + "exclude", + "excuse", + "execute", + "exercise", + "exhaust", + "exhibit", + "exile", + "exist", + "exit", + "exotic", + "expand", + "expect", + "expire", + "explain", + "expose", + "express", + "extend", + "extra", + "eye", + "eyebrow", + "fabric", + "face", + "faculty", + "fade", + "faint", + "faith", + "fall", + "false", + "fame", + "family", + "famous", + "fan", + "fancy", + "fantasy", + "farm", + "fashion", + "fat", + "fatal", + "father", + "fatigue", + "fault", + "favorite", + "feature", + "february", + "federal", + "fee", + "feed", + "feel", + "female", + "fence", + "festival", + "fetch", + "fever", + "few", + "fiber", + "fiction", + "field", + "figure", + "file", + "film", + "filter", + "final", + "find", + "fine", + "finger", + "finish", + "fire", + "firm", + "first", + "fiscal", + "fish", + "fit", + "fitness", + "fix", + "flag", + "flame", + "flash", + "flat", + "flavor", + "flee", + "flight", + "flip", + "float", + "flock", + "floor", + "flower", + "fluid", + "flush", + "fly", + "foam", + "focus", + "fog", + "foil", + "fold", + "follow", + "food", + "foot", + "force", + "forest", + "forget", + "fork", + "fortune", + "forum", + "forward", + "fossil", + "foster", + "found", + "fox", + "fragile", + "frame", + "frequent", + "fresh", + "friend", + "fringe", + "frog", + "front", + "frost", + "frown", + "frozen", + "fruit", + "fuel", + "fun", + "funny", + "furnace", + "fury", + "future", + "gadget", + "gain", + "galaxy", + "gallery", + "game", + "gap", + "garage", + "garbage", + "garden", + "garlic", + "garment", + "gas", + "gasp", + "gate", + "gather", + "gauge", + "gaze", + "general", + "genius", + "genre", + "gentle", + "genuine", + "gesture", + "ghost", + "giant", + "gift", + "giggle", + "ginger", + "giraffe", + "girl", + "give", + "glad", + "glance", + "glare", + "glass", + "glide", + "glimpse", + "globe", + "gloom", + "glory", + "glove", + "glow", + "glue", + "goat", + "goddess", + "gold", + "good", + "goose", + "gorilla", + "gospel", + "gossip", + "govern", + "gown", + "grab", + "grace", + "grain", + "grant", + "grape", + "grass", + "gravity", + "great", + "green", + "grid", + "grief", + "grit", + "grocery", + "group", + "grow", + "grunt", + "guard", + "guess", + "guide", + "guilt", + "guitar", + "gun", + "gym", + "habit", + "hair", + "half", + "hammer", + "hamster", + "hand", + "happy", + "harbor", + "hard", + "harsh", + "harvest", + "hat", + "have", + "hawk", + "hazard", + "head", + "health", + "heart", + "heavy", + "hedgehog", + "height", + "hello", + "helmet", + "help", + "hen", + "hero", + "hidden", + "high", + "hill", + "hint", + "hip", + "hire", + "history", + "hobby", + "hockey", + "hold", + "hole", + "holiday", + "hollow", + "home", + "honey", + "hood", + "hope", + "horn", + "horror", + "horse", + "hospital", + "host", + "hotel", + "hour", + "hover", + "hub", + "huge", + "human", + "humble", + "humor", + "hundred", + "hungry", + "hunt", + "hurdle", + "hurry", + "hurt", + "husband", + "hybrid", + "ice", + "icon", + "idea", + "identify", + "idle", + "ignore", + "ill", + "illegal", + "illness", + "image", + "imitate", + "immense", + "immune", + "impact", + "impose", + "improve", + "impulse", + "inch", + "include", + "income", + "increase", + "index", + "indicate", + "indoor", + "industry", + "infant", + "inflict", + "inform", + "inhale", + "inherit", + "initial", + "inject", + "injury", + "inmate", + "inner", + "innocent", + "input", + "inquiry", + "insane", + "insect", + "inside", + "inspire", + "install", + "intact", + "interest", + "into", + "invest", + "invite", + "involve", + "iron", + "island", + "isolate", + "issue", + "item", + "ivory", + "jacket", + "jaguar", + "jar", + "jazz", + "jealous", + "jeans", + "jelly", + "jewel", + "job", + "join", + "joke", + "journey", + "joy", + "judge", + "juice", + "jump", + "jungle", + "junior", + "junk", + "just", + "kangaroo", + "keen", + "keep", + "ketchup", + "key", + "kick", + "kid", + "kidney", + "kind", + "kingdom", + "kiss", + "kit", + "kitchen", + "kite", + "kitten", + "kiwi", + "knee", + "knife", + "knock", + "know", + "lab", + "label", + "labor", + "ladder", + "lady", + "lake", + "lamp", + "language", + "laptop", + "large", + "later", + "latin", + "laugh", + "laundry", + "lava", + "law", + "lawn", + "lawsuit", + "layer", + "lazy", + "leader", + "leaf", + "learn", + "leave", + "lecture", + "left", + "leg", + "legal", + "legend", + "leisure", + "lemon", + "lend", + "length", + "lens", + "leopard", + "lesson", + "letter", + "level", + "liar", + "liberty", + "library", + "license", + "life", + "lift", + "light", + "like", + "limb", + "limit", + "link", + "lion", + "liquid", + "list", + "little", + "live", + "lizard", + "load", + "loan", + "lobster", + "local", + "lock", + "logic", + "lonely", + "long", + "loop", + "lottery", + "loud", + "lounge", + "love", + "loyal", + "lucky", + "luggage", + "lumber", + "lunar", + "lunch", + "luxury", + "lyrics", + "machine", + "mad", + "magic", + "magnet", + "maid", + "mail", + "main", + "major", + "make", + "mammal", + "man", + "manage", + "mandate", + "mango", + "mansion", + "manual", + "maple", + "marble", + "march", + "margin", + "marine", + "market", + "marriage", + "mask", + "mass", + "master", + "match", + "material", + "math", + "matrix", + "matter", + "maximum", + "maze", + "meadow", + "mean", + "measure", + "meat", + "mechanic", + "medal", + "media", + "melody", + "melt", + "member", + "memory", + "mention", + "menu", + "mercy", + "merge", + "merit", + "merry", + "mesh", + "message", + "metal", + "method", + "middle", + "midnight", + "milk", + "million", + "mimic", + "mind", + "minimum", + "minor", + "minute", + "miracle", + "mirror", + "misery", + "miss", + "mistake", + "mix", + "mixed", + "mixture", + "mobile", + "model", + "modify", + "mom", + "moment", + "monitor", + "monkey", + "monster", + "month", + "moon", + "moral", + "more", + "morning", + "mosquito", + "mother", + "motion", + "motor", + "mountain", + "mouse", + "move", + "movie", + "much", + "muffin", + "mule", + "multiply", + "muscle", + "museum", + "mushroom", + "music", + "must", + "mutual", + "myself", + "mystery", + "myth", + "naive", + "name", + "napkin", + "narrow", + "nasty", + "nation", + "nature", + "near", + "neck", + "need", + "negative", + "neglect", + "neither", + "nephew", + "nerve", + "nest", + "net", + "network", + "neutral", + "never", + "news", + "next", + "nice", + "night", + "noble", + "noise", + "nominee", + "noodle", + "normal", + "north", + "nose", + "notable", + "note", + "nothing", + "notice", + "novel", + "now", + "nuclear", + "number", + "nurse", + "nut", + "oak", + "obey", + "object", + "oblige", + "obscure", + "observe", + "obtain", + "obvious", + "occur", + "ocean", + "october", + "odor", + "off", + "offer", + "office", + "often", + "oil", + "okay", + "old", + "olive", + "olympic", + "omit", + "once", + "one", + "onion", + "online", + "only", + "open", + "opera", + "opinion", + "oppose", + "option", + "orange", + "orbit", + "orchard", + "order", + "ordinary", + "organ", + "orient", + "original", + "orphan", + "ostrich", + "other", + "outdoor", + "outer", + "output", + "outside", + "oval", + "oven", + "over", + "own", + "owner", + "oxygen", + "oyster", + "ozone", + "pact", + "paddle", + "page", + "pair", + "palace", + "palm", + "panda", + "panel", + "panic", + "panther", + "paper", + "parade", + "parent", + "park", + "parrot", + "party", + "pass", + "patch", + "path", + "patient", + "patrol", + "pattern", + "pause", + "pave", + "payment", + "peace", + "peanut", + "pear", + "peasant", + "pelican", + "pen", + "penalty", + "pencil", + "people", + "pepper", + "perfect", + "permit", + "person", + "pet", + "phone", + "photo", + "phrase", + "physical", + "piano", + "picnic", + "picture", + "piece", + "pig", + "pigeon", + "pill", + "pilot", + "pink", + "pioneer", + "pipe", + "pistol", + "pitch", + "pizza", + "place", + "planet", + "plastic", + "plate", + "play", + "please", + "pledge", + "pluck", + "plug", + "plunge", + "poem", + "poet", + "point", + "polar", + "pole", + "police", + "pond", + "pony", + "pool", + "popular", + "portion", + "position", + "possible", + "post", + "potato", + "pottery", + "poverty", + "powder", + "power", + "practice", + "praise", + "predict", + "prefer", + "prepare", + "present", + "pretty", + "prevent", + "price", + "pride", + "primary", + "print", + "priority", + "prison", + "private", + "prize", + "problem", + "process", + "produce", + "profit", + "program", + "project", + "promote", + "proof", + "property", + "prosper", + "protect", + "proud", + "provide", + "public", + "pudding", + "pull", + "pulp", + "pulse", + "pumpkin", + "punch", + "pupil", + "puppy", + "purchase", + "purity", + "purpose", + "purse", + "push", + "put", + "puzzle", + "pyramid", + "quality", + "quantum", + "quarter", + "question", + "quick", + "quit", + "quiz", + "quote", + "rabbit", + "raccoon", + "race", + "rack", + "radar", + "radio", + "rail", + "rain", + "raise", + "rally", + "ramp", + "ranch", + "random", + "range", + "rapid", + "rare", + "rate", + "rather", + "raven", + "raw", + "razor", + "ready", + "real", + "reason", + "rebel", + "rebuild", + "recall", + "receive", + "recipe", + "record", + "recycle", + "reduce", + "reflect", + "reform", + "refuse", + "region", + "regret", + "regular", + "reject", + "relax", + "release", + "relief", + "rely", + "remain", + "remember", + "remind", + "remove", + "render", + "renew", + "rent", + "reopen", + "repair", + "repeat", + "replace", + "report", + "require", + "rescue", + "resemble", + "resist", + "resource", + "response", + "result", + "retire", + "retreat", + "return", + "reunion", + "reveal", + "review", + "reward", + "rhythm", + "rib", + "ribbon", + "rice", + "rich", + "ride", + "ridge", + "rifle", + "right", + "rigid", + "ring", + "riot", + "ripple", + "risk", + "ritual", + "rival", + "river", + "road", + "roast", + "robot", + "robust", + "rocket", + "romance", + "roof", + "rookie", + "room", + "rose", + "rotate", + "rough", + "round", + "route", + "royal", + "rubber", + "rude", + "rug", + "rule", + "run", + "runway", + "rural", + "sad", + "saddle", + "sadness", + "safe", + "sail", + "salad", + "salmon", + "salon", + "salt", + "salute", + "same", + "sample", + "sand", + "satisfy", + "satoshi", + "sauce", + "sausage", + "save", + "say", + "scale", + "scan", + "scare", + "scatter", + "scene", + "scheme", + "school", + "science", + "scissors", + "scorpion", + "scout", + "scrap", + "screen", + "script", + "scrub", + "sea", + "search", + "season", + "seat", + "second", + "secret", + "section", + "security", + "seed", + "seek", + "segment", + "select", + "sell", + "seminar", + "senior", + "sense", + "sentence", + "series", + "service", + "session", + "settle", + "setup", + "seven", + "shadow", + "shaft", + "shallow", + "share", + "shed", + "shell", + "sheriff", + "shield", + "shift", + "shine", + "ship", + "shiver", + "shock", + "shoe", + "shoot", + "shop", + "short", + "shoulder", + "shove", + "shrimp", + "shrug", + "shuffle", + "shy", + "sibling", + "sick", + "side", + "siege", + "sight", + "sign", + "silent", + "silk", + "silly", + "silver", + "similar", + "simple", + "since", + "sing", + "siren", + "sister", + "situate", + "six", + "size", + "skate", + "sketch", + "ski", + "skill", + "skin", + "skirt", + "skull", + "slab", + "slam", + "sleep", + "slender", + "slice", + "slide", + "slight", + "slim", + "slogan", + "slot", + "slow", + "slush", + "small", + "smart", + "smile", + "smoke", + "smooth", + "snack", + "snake", + "snap", + "sniff", + "snow", + "soap", + "soccer", + "social", + "sock", + "soda", + "soft", + "solar", + "soldier", + "solid", + "solution", + "solve", + "someone", + "song", + "soon", + "sorry", + "sort", + "soul", + "sound", + "soup", + "source", + "south", + "space", + "spare", + "spatial", + "spawn", + "speak", + "special", + "speed", + "spell", + "spend", + "sphere", + "spice", + "spider", + "spike", + "spin", + "spirit", + "split", + "spoil", + "sponsor", + "spoon", + "sport", + "spot", + "spray", + "spread", + "spring", + "spy", + "square", + "squeeze", + "squirrel", + "stable", + "stadium", + "staff", + "stage", + "stairs", + "stamp", + "stand", + "start", + "state", + "stay", + "steak", + "steel", + "stem", + "step", + "stereo", + "stick", + "still", + "sting", + "stock", + "stomach", + "stone", + "stool", + "story", + "stove", + "strategy", + "street", + "strike", + "strong", + "struggle", + "student", + "stuff", + "stumble", + "style", + "subject", + "submit", + "subway", + "success", + "such", + "sudden", + "suffer", + "sugar", + "suggest", + "suit", + "summer", + "sun", + "sunny", + "sunset", + "super", + "supply", + "supreme", + "sure", + "surface", + "surge", + "surprise", + "surround", + "survey", + "suspect", + "sustain", + "swallow", + "swamp", + "swap", + "swarm", + "swear", + "sweet", + "swift", + "swim", + "swing", + "switch", + "sword", + "symbol", + "symptom", + "syrup", + "system", + "table", + "tackle", + "tag", + "tail", + "talent", + "talk", + "tank", + "tape", + "target", + "task", + "taste", + "tattoo", + "taxi", + "teach", + "team", + "tell", + "ten", + "tenant", + "tennis", + "tent", + "term", + "test", + "text", + "thank", + "that", + "theme", + "then", + "theory", + "there", + "they", + "thing", + "this", + "thought", + "three", + "thrive", + "throw", + "thumb", + "thunder", + "ticket", + "tide", + "tiger", + "tilt", + "timber", + "time", + "tiny", + "tip", + "tired", + "tissue", + "title", + "toast", + "tobacco", + "today", + "toddler", + "toe", + "together", + "toilet", + "token", + "tomato", + "tomorrow", + "tone", + "tongue", + "tonight", + "tool", + "tooth", + "top", + "topic", + "topple", + "torch", + "tornado", + "tortoise", + "toss", + "total", + "tourist", + "toward", + "tower", + "town", + "toy", + "track", + "trade", + "traffic", + "tragic", + "train", + "transfer", + "trap", + "trash", + "travel", + "tray", + "treat", + "tree", + "trend", + "trial", + "tribe", + "trick", + "trigger", + "trim", + "trip", + "trophy", + "trouble", + "truck", + "true", + "truly", + "trumpet", + "trust", + "truth", + "try", + "tube", + "tuition", + "tumble", + "tuna", + "tunnel", + "turkey", + "turn", + "turtle", + "twelve", + "twenty", + "twice", + "twin", + "twist", + "two", + "type", + "typical", + "ugly", + "umbrella", + "unable", + "unaware", + "uncle", + "uncover", + "under", + "undo", + "unfair", + "unfold", + "unhappy", + "uniform", + "unique", + "unit", + "universe", + "unknown", + "unlock", + "until", + "unusual", + "unveil", + "update", + "upgrade", + "uphold", + "upon", + "upper", + "upset", + "urban", + "urge", + "usage", + "use", + "used", + "useful", + "useless", + "usual", + "utility", + "vacant", + "vacuum", + "vague", + "valid", + "valley", + "valve", + "van", + "vanish", + "vapor", + "various", + "vast", + "vault", + "vehicle", + "velvet", + "vendor", + "venture", + "venue", + "verb", + "verify", + "version", + "very", + "vessel", + "veteran", + "viable", + "vibrant", + "vicious", + "victory", + "video", + "view", + "village", + "vintage", + "violin", + "virtual", + "virus", + "visa", + "visit", + "visual", + "vital", + "vivid", + "vocal", + "voice", + "void", + "volcano", + "volume", + "vote", + "voyage", + "wage", + "wagon", + "wait", + "walk", + "wall", + "walnut", + "want", + "warfare", + "warm", + "warrior", + "wash", + "wasp", + "waste", + "water", + "wave", + "way", + "wealth", + "weapon", + "wear", + "weasel", + "weather", + "web", + "wedding", + "weekend", + "weird", + "welcome", + "west", + "wet", + "whale", + "what", + "wheat", + "wheel", + "when", + "where", + "whip", + "whisper", + "wide", + "width", + "wife", + "wild", + "will", + "win", + "window", + "wine", + "wing", + "wink", + "winner", + "winter", + "wire", + "wisdom", + "wise", + "wish", + "witness", + "wolf", + "woman", + "wonder", + "wood", + "wool", + "word", + "work", + "world", + "worry", + "worth", + "wrap", + "wreck", + "wrestle", + "wrist", + "write", + "wrong", + "yard", + "year", + "yellow", + "you", + "young", + "youth", + "zebra", + "zero", + "zone", + "zoo", +]; diff --git a/cw_decred/lib/pending_transaction.dart b/cw_decred/lib/pending_transaction.dart new file mode 100644 index 000000000..63162bf40 --- /dev/null +++ b/cw_decred/lib/pending_transaction.dart @@ -0,0 +1,39 @@ +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_decred/amount_format.dart'; + +class DecredPendingTransaction with PendingTransaction { + DecredPendingTransaction( + {required this.txid, + required this.amount, + required this.fee, + required this.rawHex, + required this.send}); + + final int amount; + final int fee; + final String txid; + final String rawHex; + final Future Function() send; + + @override + String get id => txid; + + @override + String get amountFormatted => decredAmountToString(amount: amount); + + @override + String get feeFormatted => decredAmountToString(amount: fee); + + @override + String get hex => rawHex; + + @override + Future commit() async { + return send(); + } + + @override + Future commitUR() { + throw UnimplementedError(); + } +} diff --git a/cw_decred/lib/transaction_credentials.dart b/cw_decred/lib/transaction_credentials.dart new file mode 100644 index 000000000..5ace384f4 --- /dev/null +++ b/cw_decred/lib/transaction_credentials.dart @@ -0,0 +1,10 @@ +import 'package:cw_decred/transaction_priority.dart'; +import 'package:cw_core/output_info.dart'; + +class DecredTransactionCredentials { + DecredTransactionCredentials(this.outputs, {required this.priority, this.feeRate}); + + final List outputs; + final DecredTransactionPriority? priority; + final int? feeRate; +} diff --git a/cw_decred/lib/transaction_history.dart b/cw_decred/lib/transaction_history.dart new file mode 100644 index 000000000..02227aa9c --- /dev/null +++ b/cw_decred/lib/transaction_history.dart @@ -0,0 +1,31 @@ +import 'package:mobx/mobx.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/transaction_history.dart'; + +class DecredTransactionHistory extends TransactionHistoryBase { + DecredTransactionHistory() { + transactions = ObservableMap(); + } + + @override + void addOne(TransactionInfo transaction) => transactions[transaction.id] = transaction; + + @override + void addMany(Map transactions) => this.transactions.addAll(transactions); + + @override + Future save() async {} + + // update returns true if a known transaction that is not pending was found. + bool update(Map txs) { + var foundOldTx = false; + txs.forEach((_, tx) { + if (!this.transactions.containsKey(tx.id) || this.transactions[tx.id]!.isPending) { + this.transactions[tx.id] = tx; + } else { + foundOldTx = true; + } + }); + return foundOldTx; + } +} diff --git a/cw_decred/lib/transaction_info.dart b/cw_decred/lib/transaction_info.dart new file mode 100644 index 000000000..4afe09e1b --- /dev/null +++ b/cw_decred/lib/transaction_info.dart @@ -0,0 +1,45 @@ +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_decred/amount_format.dart'; + +class DecredTransactionInfo extends TransactionInfo { + DecredTransactionInfo({ + required String id, + required int amount, + required int fee, + required TransactionDirection direction, + required bool isPending, + required DateTime date, + required int height, + required int confirmations, + required String to, + }) { + this.id = id; + this.amount = amount; + this.fee = fee; + this.height = height; + this.direction = direction; + this.date = date; + this.isPending = isPending; + this.confirmations = confirmations; + this.to = to; + } + + String? _fiatAmount; + + @override + String amountFormatted() => + '${formatAmount(decredAmountToString(amount: amount))} ${walletTypeToCryptoCurrency(WalletType.decred).title}'; + + @override + String? feeFormatted() => + '${formatAmount(decredAmountToString(amount: fee ?? 0))} ${walletTypeToCryptoCurrency(WalletType.decred).title}'; + + @override + String fiatAmount() => _fiatAmount ?? ''; + + @override + void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); +} diff --git a/cw_decred/lib/transaction_priority.dart b/cw_decred/lib/transaction_priority.dart new file mode 100644 index 000000000..80a9c7e3a --- /dev/null +++ b/cw_decred/lib/transaction_priority.dart @@ -0,0 +1,69 @@ +import 'package:cw_core/transaction_priority.dart'; + +class DecredTransactionPriority extends TransactionPriority { + const DecredTransactionPriority({required String title, required int raw}) + : super(title: title, raw: raw); + + static const List all = [fast, medium, slow]; + static const DecredTransactionPriority slow = DecredTransactionPriority(title: 'Slow', raw: 0); + static const DecredTransactionPriority medium = + DecredTransactionPriority(title: 'Medium', raw: 1); + static const DecredTransactionPriority fast = DecredTransactionPriority(title: 'Fast', raw: 2); + + static DecredTransactionPriority deserialize({required int raw}) { + switch (raw) { + case 0: + return slow; + case 1: + return medium; + case 2: + return fast; + default: + throw Exception('Unexpected token: $raw for DecredTransactionPriority deserialize'); + } + } + + String get units => 'atom'; + + @override + String toString() { + var label = ''; + + switch (this) { + case DecredTransactionPriority.slow: + label = 'Slow ~24hrs'; // '${S.current.transaction_priority_slow} ~24hrs'; + break; + case DecredTransactionPriority.medium: + label = 'Medium'; // S.current.transaction_priority_medium; + break; + case DecredTransactionPriority.fast: + label = 'Fast'; // S.current.transaction_priority_fast; + break; + default: + break; + } + + return label; + } + + String labelWithRate(int rate) => '${toString()} ($rate ${units}/byte)'; +} + +class FeeCache { + int _feeRate; + DateTime stamp; + FeeCache(this._feeRate) : this.stamp = DateTime(0, 0, 0, 0, 0, 0, 0, 0); + + bool isOld() { + return this.stamp.add(const Duration(minutes: 30)).isBefore(DateTime.now()); + } + + void update(int feeRate) { + this._feeRate = feeRate; + this.stamp = DateTime.now(); + } + + int feeRate() { + return this._feeRate; + } +} diff --git a/cw_decred/lib/wallet.dart b/cw_decred/lib/wallet.dart new file mode 100644 index 000000000..028ba6cda --- /dev/null +++ b/cw_decred/lib/wallet.dart @@ -0,0 +1,729 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:cw_core/exceptions.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_decred/pending_transaction.dart'; +import 'package:cw_decred/transaction_credentials.dart'; +import 'package:flutter/foundation.dart'; +import 'package:mobx/mobx.dart'; +import 'package:hive/hive.dart'; + +import 'package:cw_decred/api/libdcrwallet.dart'; +import 'package:cw_decred/transaction_history.dart'; +import 'package:cw_decred/wallet_addresses.dart'; +import 'package:cw_decred/transaction_priority.dart'; +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/wallet_info.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/unspent_transaction_output.dart'; + +part 'wallet.g.dart'; + +class DecredWallet = DecredWalletBase with _$DecredWallet; + +abstract class DecredWalletBase + extends WalletBase with Store { + DecredWalletBase(WalletInfo walletInfo, String password, Box unspentCoinsInfo, + Libwallet libwallet, Function() closeLibwallet) + : _password = password, + _libwallet = libwallet, + _closeLibwallet = closeLibwallet, + this.syncStatus = NotConnectedSyncStatus(), + this.unspentCoinsInfo = unspentCoinsInfo, + this.watchingOnly = + walletInfo.derivationInfo?.derivationPath == DecredWalletService.pubkeyRestorePath || + walletInfo.derivationInfo?.derivationPath == + DecredWalletService.pubkeyRestorePathTestnet, + this.balance = ObservableMap.of({CryptoCurrency.dcr: DecredBalance.zero()}), + this.isTestnet = walletInfo.derivationInfo?.derivationPath == + DecredWalletService.seedRestorePathTestnet || + walletInfo.derivationInfo?.derivationPath == + DecredWalletService.pubkeyRestorePathTestnet, + super(walletInfo) { + walletAddresses = DecredWalletAddresses(walletInfo, libwallet); + transactionHistory = DecredTransactionHistory(); + + reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) { + this.walletAddresses.isEnabledAutoGenerateSubaddress = enabled; + }); + } + + // NOTE: Hitting this max fee would be unexpected with current on chain use + // but this may need to be updated in the future. + final maxFeeRate = 100000; + + // syncIntervalSyncing is used up until synced, then transactions are checked + // every syncIntervalSynced. + final syncIntervalSyncing = 5; // seconds + final syncIntervalSynced = 30; // seconds + static final defaultFeeRate = 10000; + final String _password; + final Libwallet _libwallet; + final Function() _closeLibwallet; + final idPrefix = "decred_"; + + // TODO: Encrypt this. + var _seed = ""; + var _pubkey = ""; + var _unspents = []; + + // synced is used to set the syncTimer interval. + bool synced = false; + bool watchingOnly; + bool connecting = false; + String persistantPeer = "default-spv-nodes"; + FeeCache feeRateFast = FeeCache(defaultFeeRate); + FeeCache feeRateMedium = FeeCache(defaultFeeRate); + FeeCache feeRateSlow = FeeCache(defaultFeeRate); + Timer? syncTimer; + Box unspentCoinsInfo; + + @override + @observable + bool isEnabledAutoGenerateSubaddress = true; + + @override + @observable + SyncStatus syncStatus; + + @override + @observable + late ObservableMap balance; + + @override + late DecredWalletAddresses walletAddresses; + + @override + String? get seed { + if (watchingOnly) { + return null; + } + return _seed; + } + + @override + Object get keys => {}; + + @override + bool isTestnet; + + String get pubkey { + return _pubkey; + } + + Future init() async { + final getSeed = () async { + if (!watchingOnly) { + _seed = await _libwallet.walletSeed(walletInfo.name, _password) ?? ""; + } + _pubkey = await _libwallet.defaultPubkey(walletInfo.name); + }; + await Future.wait([ + updateBalance(), + updateTransactionHistory(), + walletAddresses.init(), + fetchTransactions(), + updateFees(), + fetchUnspents(), + getSeed(), + ]); + } + + Future performBackgroundTasks() async { + if (!await checkSync()) { + if (synced == true) { + synced = false; + if (syncTimer != null) { + syncTimer!.cancel(); + } + syncTimer = Timer.periodic( + Duration(seconds: syncIntervalSyncing), (Timer t) => performBackgroundTasks()); + } + return; + } + // Set sync check interval lower since we are synced. + if (synced == false) { + synced = true; + if (syncTimer != null) { + syncTimer!.cancel(); + } + syncTimer = Timer.periodic( + Duration(seconds: syncIntervalSynced), (Timer t) => performBackgroundTasks()); + } + await Future.wait([ + updateTransactionHistory(), + updateFees(), + fetchUnspents(), + updateBalance(), + walletAddresses.updateAddressesInBox(), + ]); + } + + Future updateFees() async { + final feeForNb = (int nb) async { + try { + final feeStr = await _libwallet.estimateFee(walletInfo.name, nb); + var fee = int.parse(feeStr); + if (fee > maxFeeRate) { + throw "dcr fee returned from estimate fee was over max"; + } else if (fee <= 0) { + throw "dcr fee returned from estimate fee was zero"; + } + return fee; + } catch (e) { + printV(e); + return defaultFeeRate; + } + }; + if (feeRateSlow.isOld()) { + feeRateSlow.update(await feeForNb(4)); + } + if (feeRateMedium.isOld()) { + feeRateMedium.update(await feeForNb(2)); + } + if (feeRateFast.isOld()) { + feeRateFast.update(await feeForNb(1)); + } + } + + Future updateTransactionHistory() async { + // from is the number of transactions skipped from most recent, not block + // height. + var from = 0; + while (true) { + // Transactions are returned from newest to oldest. Loop fetching 5 txn + // at a time until we find a batch with txn that no longer need to be + // updated. + final txs = await this.fetchFiveTransactions(from); + if (txs.length == 0) { + return; + } + if (this.transactionHistory.update(txs)) { + return; + } + from += 5; + } + } + + Future checkSync() async { + final syncStatusJSON = await _libwallet.syncStatus(walletInfo.name); + final decoded = json.decode(syncStatusJSON); + + final syncStatusCode = decoded["syncstatuscode"] ?? 0; + // final syncStatusStr = decoded["syncstatus"] ?? ""; + final targetHeight = decoded["targetheight"] ?? 1; + final numPeers = decoded["numpeers"] ?? 0; + // final cFiltersHeight = decoded["cfiltersheight"] ?? 0; + final headersHeight = decoded["headersheight"] ?? 0; + final rescanHeight = decoded["rescanheight"] ?? 0; + + if (numPeers == 0) { + syncStatus = NotConnectedSyncStatus(); + return false; + } + + // Sync codes: + // NotStarted = 0 + // FetchingCFilters = 1 + // FetchingHeaders = 2 + // DiscoveringAddrs = 3 + // Rescanning = 4 + // Complete = 5 + + if (syncStatusCode > 4) { + syncStatus = SyncedSyncStatus(); + return true; + } + + if (syncStatusCode == 0) { + syncStatus = ConnectedSyncStatus(); + return false; + } + + if (syncStatusCode == 1) { + syncStatus = SyncingSyncStatus(targetHeight, 0.0); + return false; + } + + if (syncStatusCode == 2) { + final headersProg = headersHeight / targetHeight; + // Only allow headers progress to go up half way. + syncStatus = SyncingSyncStatus(targetHeight - headersHeight, headersProg); + return false; + } + + // TODO: This step takes a while so should really get more info to the UI + // that we are discovering addresses. + if (syncStatusCode == 3) { + // Hover at half. + syncStatus = ProcessingSyncStatus(); + return false; + } + + if (syncStatusCode == 4) { + // Start at 75%. + final rescanProg = rescanHeight / targetHeight / 4; + syncStatus = SyncingSyncStatus(targetHeight - rescanHeight, .75 + rescanProg); + return false; + } + return false; + } + + @action + @override + Future connectToNode({required Node node}) async { + if (connecting) { + return; + } + connecting = true; + String addr = "default-spv-nodes"; + if (node.uri.host != addr) { + addr = node.uri.host; + if (node.uri.port != "") { + addr += ":" + node.uri.port.toString(); + } + } + if (addr != persistantPeer) { + if (syncTimer != null) { + syncTimer!.cancel(); + syncTimer = null; + } + persistantPeer = addr; + await _libwallet.closeWallet(walletInfo.name); + final network = isTestnet ? "testnet" : "mainnet"; + final config = { + "name": walletInfo.name, + "datadir": walletInfo.dirPath, + "net": network, + "unsyncedaddrs": true, + }; + await _libwallet.loadWallet(jsonEncode(config)); + } + await this._startSync(); + connecting = false; + } + + @action + @override + Future startSync() async { + if (connecting) { + return; + } + connecting = true; + await this._startSync(); + connecting = false; + } + + Future _startSync() async { + if (syncTimer != null) { + return; + } + try { + syncStatus = ConnectingSyncStatus(); + await _libwallet.startSync( + walletInfo.name, + persistantPeer == "default-spv-nodes" ? "" : persistantPeer, + ); + syncTimer = Timer.periodic( + Duration(seconds: syncIntervalSyncing), (Timer t) => performBackgroundTasks()); + } catch (e) { + printV(e.toString()); + syncStatus = FailedSyncStatus(); + } + } + + @override + Future createTransaction(Object credentials) async { + if (watchingOnly) { + return DecredPendingTransaction( + txid: "", + amount: 0, + fee: 0, + rawHex: "", + send: () async { + throw "unable to send with watching only wallet"; + }); + } + var totalIn = 0; + final ignoreInputs = []; + this.unspentCoinsInfo.values.forEach((unspent) { + if (unspent.isFrozen || !unspent.isSending) { + final input = {"txid": unspent.hash, "vout": unspent.vout}; + ignoreInputs.add(input); + return; + } + totalIn += unspent.value; + }); + + final creds = credentials as DecredTransactionCredentials; + var totalAmt = 0; + var sendAll = false; + final outputs = []; + for (final out in creds.outputs) { + var amt = 0; + if (out.sendAll) { + if (creds.outputs.length != 1) { + throw "can only send all to one output"; + } + sendAll = true; + totalAmt = totalIn; + } else if (out.cryptoAmount != null) { + final coins = double.parse(out.cryptoAmount!); + amt = (coins * 1e8).toInt(); + } + totalAmt += amt; + final o = { + "address": out.isParsedAddress ? out.extractedAddress! : out.address, + "amount": amt + }; + outputs.add(o); + } + + // throw exception if no selected coins under coin control + // or if the total coins selected, is less than the amount the user wants to spend + if (ignoreInputs.length == unspentCoinsInfo.values.length || totalIn < totalAmt) { + throw TransactionNoInputsException(); + } + + // The inputs are always used. Currently we don't have use for this + // argument. sendall ingores output value and sends everything. + final signReq = { + // "inputs": inputs, + "ignoreInputs": ignoreInputs, + "outputs": outputs, + "feerate": creds.feeRate ?? defaultFeeRate, + "password": _password, + "sendall": sendAll, + }; + final res = await _libwallet.createSignedTransaction(walletInfo.name, jsonEncode(signReq)); + final decoded = json.decode(res); + final signedHex = decoded["signedhex"]; + final send = () async { + await _libwallet.sendRawTransaction(walletInfo.name, signedHex); + await updateBalance(); + }; + final fee = decoded["fee"] ?? 0; + if (sendAll) { + totalAmt = (totalAmt - fee).toInt(); + } + return DecredPendingTransaction( + txid: decoded["txid"] ?? "", amount: totalAmt, fee: fee, rawHex: signedHex, send: send); + } + + int feeRate(TransactionPriority priority) { + if (!(priority is DecredTransactionPriority)) { + return defaultFeeRate; + } + final p = priority; + switch (p) { + case DecredTransactionPriority.slow: + return feeRateSlow.feeRate(); + case DecredTransactionPriority.medium: + return feeRateMedium.feeRate(); + case DecredTransactionPriority.fast: + return feeRateFast.feeRate(); + } + return defaultFeeRate; + } + + @override + int calculateEstimatedFee(TransactionPriority priority, int? amount) { + if (priority is DecredTransactionPriority) { + final P2PKHOutputSize = + 36; // 8 bytes value + 2 bytes version + at least 1 byte varint script size + P2PKHPkScriptSize + // MsgTxOverhead is 4 bytes version (lower 2 bytes for the real transaction + // version and upper 2 bytes for the serialization type) + 4 bytes locktime + // + 4 bytes expiry + 3 bytes of varints for the number of transaction + // inputs (x2 for witness and prefix) and outputs + final MsgTxOverhead = 15; + // TxInOverhead is the overhead for a wire.TxIn with a scriptSig length < + // 254. prefix (41 bytes) + ValueIn (8 bytes) + BlockHeight (4 bytes) + + // BlockIndex (4 bytes) + sig script var int (at least 1 byte) + final TxInOverhead = 57; + final P2PKHInputSize = + TxInOverhead + 109; // TxInOverhead (57) + var int (1) + P2PKHSigScriptSize (108) + + int inputsCount = 1; + if (amount != null) { + inputsCount += _unspents.where((e) { + amount = (amount!) - e.value; + return (amount!) > 0; + }).length; + } + + // Estimate using a transaction consuming inoutsCount and paying to one address with change. + return (this.feeRate(priority) / 1000).round() * + (MsgTxOverhead + P2PKHInputSize * inputsCount + P2PKHOutputSize * 2); + } + return 0; + } + + @override + Future> fetchTransactions() async { + return this.fetchFiveTransactions(0); + } + + Future> fetchFiveTransactions(int from) async { + final res = await _libwallet.listTransactions(walletInfo.name, from.toString(), "5"); + final decoded = json.decode(res); + var txs = {}; + for (final d in decoded) { + final txid = uniqueTxID(d["txid"] ?? "", d["vout"] ?? 0); + var direction = TransactionDirection.outgoing; + if (d["category"] == "receive") { + direction = TransactionDirection.incoming; + } + final amountDouble = d["amount"] ?? 0.0; + final amount = (amountDouble * 1e8).toInt().abs(); + final feeDouble = d["fee"] ?? 0.0; + final fee = (feeDouble * 1e8).toInt().abs(); + final confs = d["confirmations"] ?? 0; + final sendTime = d["time"] ?? 0; + final height = d["height"] ?? 0; + final txInfo = DecredTransactionInfo( + id: txid, + amount: amount, + fee: fee, + direction: direction, + isPending: confs == 0, + date: DateTime.fromMillisecondsSinceEpoch(sendTime * 1000, isUtc: false), + height: height, + confirmations: confs, + to: d["address"] ?? "", + ); + txs[txid] = txInfo; + } + return txs; + } + + // uniqueTxID combines the tx id and vout to create a unique id. + String uniqueTxID(String id, int vout) { + return id + ":" + vout.toString(); + } + + @override + Future save() async {} + + @override + bool get hasRescan => walletBirthdayBlockHeight() != -1; + + @override + Future rescan({required int height}) async { + // The required height is not used. A birthday time is recorded in the + // mnemonic. As long as not private data is imported into the wallet, we + // can always rescan from there. + var rescanHeight = 0; + if (!watchingOnly) { + rescanHeight = await walletBirthdayBlockHeight(); + // Sync has not yet reached the birthday block. + if (rescanHeight == -1) { + return; + } + } + await _libwallet.rescanFromHeight(walletInfo.name, rescanHeight.toString()); + } + + @override + Future close({bool shouldCleanup = false}) async { + if (syncTimer != null) { + syncTimer!.cancel(); + syncTimer = null; + } + await _libwallet.closeWallet(walletInfo.name); + if (shouldCleanup) { + await _libwallet.shutdown(); + _closeLibwallet(); + } + } + + @override + Future changePassword(String password) async { + if (watchingOnly) { + return; + } + return () async { + await _libwallet.changeWalletPassword(walletInfo.name, _password, password); + }(); + } + + @override + Future updateBalance() async { + final balanceMap = await _libwallet.balance(walletInfo.name); + + var totalFrozen = 0; + + unspentCoinsInfo.values.forEach((info) { + _unspents.forEach((element) { + if (element.hash == info.hash && + element.vout == info.vout && + info.isFrozen && + element.value == info.value) { + totalFrozen += element.value; + } + }); + }); + + balance[CryptoCurrency.dcr] = DecredBalance( + confirmed: balanceMap["confirmed"] ?? 0, + unconfirmed: balanceMap["unconfirmed"] ?? 0, + frozen: totalFrozen, + ); + } + + @override + void setExceptionHandler(void Function(FlutterErrorDetails) onError) => onError; + + Future renameWalletFiles(String newWalletName) async { + final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type); + + final newDirPath = await pathForWalletDir(name: newWalletName, type: type); + + if (File(newDirPath).existsSync()) { + throw "wallet already exists at $newDirPath"; + } + + await Directory(currentDirPath).rename(newDirPath); + } + + @override + Future signMessage(String message, {String? address = null}) async { + if (watchingOnly) { + throw "a watching only wallet cannot sign"; + } + var addr = address; + if (addr == null) { + addr = walletAddresses.address; + } + if (addr == "") { + throw "unable to get an address from unsynced wallet"; + } + return await _libwallet.signMessage(walletInfo.name, message, addr, _password); + } + + Future fetchUnspents() async { + final res = await _libwallet.listUnspents(walletInfo.name); + final decoded = json.decode(res); + var unspents = []; + for (final d in decoded) { + final spendable = d["spendable"] ?? false; + if (!spendable) { + continue; + } + final amountDouble = d["amount"] ?? 0.0; + final amount = (amountDouble * 1e8).toInt().abs(); + final utxo = Unspent(d["address"] ?? "", d["txid"] ?? "", amount, d["vout"] ?? 0, null); + utxo.isChange = d["ischange"] ?? false; + unspents.add(utxo); + } + _unspents = unspents; + } + + List unspents() { + this.updateUnspents(_unspents); + return _unspents; + } + + void updateUnspents(List unspentCoins) { + if (this.unspentCoinsInfo.isEmpty) { + unspentCoins.forEach((coin) => this.addCoinInfo(coin)); + return; + } + + if (unspentCoins.isEmpty) { + this.unspentCoinsInfo.clear(); + return; + } + + final walletID = idPrefix + walletInfo.name; + if (unspentCoins.isNotEmpty) { + unspentCoins.forEach((coin) { + final coinInfoList = this.unspentCoinsInfo.values.where((element) => + element.walletId == walletID && element.hash == coin.hash && element.vout == coin.vout); + + if (coinInfoList.isEmpty) { + this.addCoinInfo(coin); + } else { + final coinInfo = coinInfoList.first; + + coin.isFrozen = coinInfo.isFrozen; + coin.isSending = coinInfo.isSending; + coin.note = coinInfo.note; + } + }); + } + + final List keys = []; + this.unspentCoinsInfo.values.forEach((element) { + final existUnspentCoins = unspentCoins.where((coin) => element.hash.contains(coin.hash)); + + if (existUnspentCoins.isEmpty) { + keys.add(element.key); + } + }); + + if (keys.isNotEmpty) { + unspentCoinsInfo.deleteAll(keys); + } + } + + void addCoinInfo(Unspent coin) { + final newInfo = UnspentCoinsInfo( + walletId: idPrefix + walletInfo.name, + hash: coin.hash, + isFrozen: false, + isSending: coin.isSending, + noteRaw: "", + address: coin.address, + value: coin.value, + vout: coin.vout, + isChange: coin.isChange, + keyImage: coin.keyImage, + ); + + unspentCoinsInfo.add(newInfo); + } + + // walletBirthdayBlockHeight checks if the wallet birthday is set and returns + // it. Returns -1 if not. + Future walletBirthdayBlockHeight() async { + final res = await _libwallet.birthState(walletInfo.name); + final decoded = json.decode(res); + // Having these values set indicates that sync has not reached the birthday + // yet, so no birthday is set. + if (decoded["setfromheight"] == true || decoded["setfromtime"] == true) { + return -1; + } + return decoded["height"] ?? 0; + } + + Future verifyMessage(String message, String signature, {String? address = null}) async { + var addr = address; + if (addr == null) { + throw "an address is required to verify message"; + } + return () async { + final verified = await _libwallet.verifyMessage(walletInfo.name, message, addr, signature); + if (verified == "true") { + return true; + } + return false; + }(); + } + + @override + String get password => _password; + + @override + bool canSend() => seed != null; +} diff --git a/cw_decred/lib/wallet_addresses.dart b/cw_decred/lib/wallet_addresses.dart new file mode 100644 index 000000000..10970b2d6 --- /dev/null +++ b/cw_decred/lib/wallet_addresses.dart @@ -0,0 +1,137 @@ +import 'dart:convert'; +import 'package:mobx/mobx.dart'; + +import 'package:cw_core/address_info.dart'; +import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_decred/api/libdcrwallet.dart'; + +part 'wallet_addresses.g.dart'; + +class DecredWalletAddresses = DecredWalletAddressesBase with _$DecredWalletAddresses; + +abstract class DecredWalletAddressesBase extends WalletAddresses with Store { + DecredWalletAddressesBase(WalletInfo walletInfo, Libwallet libwallet) + : _libwallet = libwallet, + super(walletInfo); + final Libwallet _libwallet; + String currentAddr = ''; + + @observable + bool isEnabledAutoGenerateSubaddress = true; + + @observable + String selectedAddr = ''; + + @override + @computed + String get address { + return selectedAddr; + } + + @override + set address(value) { + selectedAddr = value; + } + + @override + Future init() async { + if (walletInfo.addresses != null) { + addressesMap = walletInfo.addresses!; + } + if (walletInfo.addressInfos != null) { + addressInfos = walletInfo.addressInfos!; + } + if (walletInfo.usedAddresses != null) { + usedAddresses = {...walletInfo.usedAddresses!}; + } + await updateAddressesInBox(); + } + + @override + Future updateAddressesInBox() async { + final addrs = await libAddresses(); + final allAddrs = new List.from(addrs.usedAddrs)..addAll(addrs.unusedAddrs); + + // Add all addresses. + allAddrs.forEach((addr) { + if (addressesMap.containsKey(addr)) { + return; + } + addressesMap[addr] = ""; + addressInfos[0] ??= []; + addressInfos[0]?.add(AddressInfo(address: addr, label: "", accountIndex: 0)); + }); + + // Add used addresses. + addrs.usedAddrs.forEach((addr) { + if (!usedAddresses.contains(addr)) { + usedAddresses.add(addr); + } + }); + + if (addrs.unusedAddrs.length > 0 && addrs.unusedAddrs[0] != currentAddr) { + currentAddr = addrs.unusedAddrs[0]; + selectedAddr = currentAddr; + } + + await saveAddressesInBox(); + } + + List getAddressInfos() { + if (addressInfos.containsKey(0)) { + return addressInfos[0]!; + } + return []; + } + + Future updateAddress(String address, String label) async { + if (!addressInfos.containsKey(0)) { + return; + } + addressInfos[0]!.forEach((info) { + if (info.address == address) { + info.label = label; + } + }); + await saveAddressesInBox(); + } + + Future libAddresses() async { + final nUsed = "10"; + var nUnused = "1"; + if (this.isEnabledAutoGenerateSubaddress) { + nUnused = "3"; + } + final res = await _libwallet.addresses(walletInfo.name, nUsed, nUnused); + final decoded = json.decode(res); + final usedAddrs = List.from(decoded["used"] ?? []); + final unusedAddrs = List.from(decoded["unused"] ?? []); + // index is the index of the first unused address. + final index = decoded["index"] ?? 0; + return new LibAddresses(usedAddrs, unusedAddrs, index); + } + + Future generateNewAddress(String label) async { + // NOTE: This will ignore the gap limit and may cause problems when restoring from seed if too + // many addresses are taken and not used. + final addr = await _libwallet.newExternalAddress(walletInfo.name) ?? ''; + if (addr == "") { + return; + } + if (!addressesMap.containsKey(addr)) { + addressesMap[addr] = ""; + addressInfos[0] ??= []; + addressInfos[0]?.add(AddressInfo(address: addr, label: label, accountIndex: 0)); + } + selectedAddr = addr; + await saveAddressesInBox(); + } +} + +class LibAddresses { + final List usedAddrs, unusedAddrs; + final int firstUnusedAddrIndex; + + LibAddresses(this.usedAddrs, this.unusedAddrs, this.firstUnusedAddrIndex); +} diff --git a/cw_decred/lib/wallet_creation_credentials.dart b/cw_decred/lib/wallet_creation_credentials.dart new file mode 100644 index 000000000..ca0451447 --- /dev/null +++ b/cw_decred/lib/wallet_creation_credentials.dart @@ -0,0 +1,40 @@ +import 'package:cw_core/wallet_credentials.dart'; +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); +} + +class DecredRestoreWalletFromSeedCredentials extends WalletCredentials { + DecredRestoreWalletFromSeedCredentials( + {required String name, + required String password, + required this.mnemonic, + WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String mnemonic; +} + +class DecredRestoreWalletFromPubkeyCredentials extends WalletCredentials { + DecredRestoreWalletFromPubkeyCredentials( + {required String name, + required String password, + required String this.pubkey, + WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String pubkey; +} + +class DecredRestoreWalletFromHardwareCredentials extends WalletCredentials { + DecredRestoreWalletFromHardwareCredentials( + {required String name, required this.hwAccountData, WalletInfo? walletInfo}) + : t = throw UnimplementedError(), + super(name: name, walletInfo: walletInfo); + + final HardwareAccountData hwAccountData; + final void t; +} diff --git a/cw_decred/lib/wallet_service.dart b/cw_decred/lib/wallet_service.dart new file mode 100644 index 000000000..a54833321 --- /dev/null +++ b/cw_decred/lib/wallet_service.dart @@ -0,0 +1,186 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:cw_decred/api/libdcrwallet.dart'; +import 'package:cw_decred/wallet_creation_credentials.dart'; +import 'package:cw_decred/wallet.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:hive/hive.dart'; +import 'package:collection/collection.dart'; +import 'package:cw_core/unspent_coins_info.dart'; + +class DecredWalletService extends WalletService< + DecredNewWalletCredentials, + DecredRestoreWalletFromSeedCredentials, + DecredRestoreWalletFromPubkeyCredentials, + DecredRestoreWalletFromHardwareCredentials> { + DecredWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); + + final Box walletInfoSource; + final Box unspentCoinsInfoSource; + final seedRestorePath = "m/44'/42'"; + static final seedRestorePathTestnet = "m/44'/1'"; + static final pubkeyRestorePath = "m/44'/42'/0'"; + static final pubkeyRestorePathTestnet = "m/44'/1'/0'"; + final mainnet = "mainnet"; + final testnet = "testnet"; + Libwallet? libwallet; + + Future init() async { + if (libwallet != null) { + return; + } + libwallet = await Libwallet.spawn(); + // Use the general path for all dcr wallets as the general log directory. + // Individual wallet paths may be removed if the wallet is deleted. + final dcrLogDir = await pathForWalletDir(name: '', type: WalletType.decred); + libwallet!.initLibdcrwallet(dcrLogDir); + } + + void closeLibwallet() { + if (libwallet == null) { + return; + } + libwallet!.close(); + libwallet = null; + } + + @override + WalletType getType() => WalletType.decred; + + @override + Future isWalletExit(String name) async => + File(await pathForWallet(name: name, type: getType())).existsSync(); + + @override + Future create(DecredNewWalletCredentials credentials, {bool? isTestnet}) async { + await this.init(); + final config = { + "name": credentials.walletInfo!.name, + "datadir": credentials.walletInfo!.dirPath, + "pass": credentials.password!, + "net": isTestnet == true ? testnet : mainnet, + "unsyncedaddrs": true, + }; + await libwallet!.createWallet(jsonEncode(config)); + final di = DerivationInfo( + derivationPath: isTestnet == true ? seedRestorePathTestnet : seedRestorePath); + credentials.walletInfo!.derivationInfo = di; + final wallet = DecredWallet(credentials.walletInfo!, credentials.password!, + this.unspentCoinsInfoSource, libwallet!, closeLibwallet); + await wallet.init(); + return wallet; + } + + @override + Future openWallet(String name, String password) async { + final walletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!; + final network = walletInfo.derivationInfo?.derivationPath == seedRestorePathTestnet || + walletInfo.derivationInfo?.derivationPath == pubkeyRestorePathTestnet + ? testnet + : mainnet; + + await this.init(); + final walletDirExists = Directory(walletInfo.dirPath).existsSync(); + if (!walletDirExists) { + walletInfo.dirPath = await pathForWalletDir(name: name, type: getType()); + } + + final config = { + "name": walletInfo.name, + "datadir": walletInfo.dirPath, + "net": network, + "unsyncedaddrs": true, + }; + await libwallet!.loadWallet(jsonEncode(config)); + final wallet = + DecredWallet(walletInfo, password, this.unspentCoinsInfoSource, libwallet!, closeLibwallet); + await wallet.init(); + return wallet; + } + + @override + Future remove(String wallet) async { + File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true); + final walletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; + await walletInfoSource.delete(walletInfo.key); + } + + @override + Future rename(String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(currentName, getType()))!; + final network = currentWalletInfo.derivationInfo?.derivationPath == seedRestorePathTestnet || + currentWalletInfo.derivationInfo?.derivationPath == pubkeyRestorePathTestnet + ? testnet + : mainnet; + final currentWallet = DecredWallet( + currentWalletInfo, password, this.unspentCoinsInfoSource, libwallet!, closeLibwallet); + + await currentWallet.renameWalletFiles(newName); + + final newDirPath = await pathForWalletDir(name: newName, type: getType()); + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + newWalletInfo.dirPath = newDirPath; + newWalletInfo.network = network; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); + } + + @override + Future restoreFromSeed(DecredRestoreWalletFromSeedCredentials credentials, + {bool? isTestnet}) async { + await this.init(); + final config = { + "name": credentials.walletInfo!.name, + "datadir": credentials.walletInfo!.dirPath, + "pass": credentials.password!, + "mnemonic": credentials.mnemonic, + "net": isTestnet == true ? testnet : mainnet, + "unsyncedaddrs": true, + }; + await libwallet!.createWallet(jsonEncode(config)); + final di = DerivationInfo( + derivationPath: isTestnet == true ? seedRestorePathTestnet : seedRestorePath); + credentials.walletInfo!.derivationInfo = di; + final wallet = DecredWallet(credentials.walletInfo!, credentials.password!, + this.unspentCoinsInfoSource, libwallet!, closeLibwallet); + await wallet.init(); + return wallet; + } + + // restoreFromKeys only supports restoring a watch only wallet from an account + // pubkey. + @override + Future restoreFromKeys(DecredRestoreWalletFromPubkeyCredentials credentials, + {bool? isTestnet}) async { + await this.init(); + final config = { + "name": credentials.walletInfo!.name, + "datadir": credentials.walletInfo!.dirPath, + "pubkey": credentials.pubkey, + "net": isTestnet == true ? testnet : mainnet, + "unsyncedaddrs": true, + }; + await libwallet!.createWatchOnlyWallet(jsonEncode(config)); + final di = DerivationInfo( + derivationPath: isTestnet == true ? pubkeyRestorePathTestnet : pubkeyRestorePath); + credentials.walletInfo!.derivationInfo = di; + final wallet = DecredWallet(credentials.walletInfo!, credentials.password!, + this.unspentCoinsInfoSource, libwallet!, closeLibwallet); + await wallet.init(); + return wallet; + } + + @override + Future restoreFromHardwareWallet( + DecredRestoreWalletFromHardwareCredentials credentials) async => + throw UnimplementedError(); +} diff --git a/cw_decred/macos/Classes/CwDecredPlugin.swift b/cw_decred/macos/Classes/CwDecredPlugin.swift new file mode 100644 index 000000000..72dae36f4 --- /dev/null +++ b/cw_decred/macos/Classes/CwDecredPlugin.swift @@ -0,0 +1,19 @@ +import Cocoa +import FlutterMacOS + +public class CwDecredPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "cw_decred", binaryMessenger: registrar.messenger) + let instance = CwDecredPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getPlatformVersion": + result("macOS " + ProcessInfo.processInfo.operatingSystemVersionString) + default: + result(FlutterMethodNotImplemented) + } + } +} diff --git a/cw_decred/macos/cw_decred.podspec b/cw_decred/macos/cw_decred.podspec new file mode 100644 index 000000000..87d82f238 --- /dev/null +++ b/cw_decred/macos/cw_decred.podspec @@ -0,0 +1,22 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint cw_decred.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'cw_decred' + s.version = '0.0.1' + s.summary = 'Cake Wallet Decred' + s.description = 'Cake Wallet wrapper over Decred project' + s.homepage = 'http://cakewallet.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Cake Wallet' => 'support@cakewallet.com' } + + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'FlutterMacOS' + + s.platform = :osx, '10.11' + s.vendored_libraries = 'External/lib/libdcrwallet.a' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', "OTHER_LDFLAGS" => "-force_load $(PODS_TARGET_SRCROOT)/External/lib/libdcrwallet.a -lstdc++" } + s.swift_version = '5.0' +end diff --git a/cw_decred/pubspec.lock b/cw_decred/pubspec.lock new file mode 100644 index 000000000..e3d39372b --- /dev/null +++ b/cw_decred/pubspec.lock @@ -0,0 +1,852 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + url: "https://pub.dev" + source: hosted + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + url: "https://pub.dev" + source: hosted + version: "6.7.0" + args: + dependency: transitive + description: + name: args + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + url: "https://pub.dev" + source: hosted + version: "2.6.0" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" + url: "https://pub.dev" + source: hosted + version: "1.5.8" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + blockchain_utils: + dependency: transitive + description: + path: "." + ref: cake-update-v2 + resolved-ref: "59fdf29d72068e0522a96a8953ed7272833a9f57" + url: "https://github.com/cake-tech/blockchain_utils" + source: git + version: "3.3.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + build_resolvers: + dependency: "direct dev" + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + url: "https://pub.dev" + source: hosted + version: "2.4.13" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + url: "https://pub.dev" + source: hosted + version: "7.3.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 + url: "https://pub.dev" + source: hosted + version: "8.9.5" + cake_backup: + dependency: transitive + description: + path: "." + ref: main + resolved-ref: "3aba867dcab6737f6707782f5db15d71f303db38" + url: "https://github.com/cake-tech/cake_backup.git" + source: git + version: "1.0.0+1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + url: "https://pub.dev" + source: hosted + version: "4.10.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + cryptography: + dependency: transitive + description: + name: cryptography + sha256: d146b76d33d94548cf035233fbc2f4338c1242fa119013bead807d033fc4ae05 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + cupertino_icons: + dependency: transitive + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + cw_core: + dependency: "direct main" + description: + path: "../cw_core" + relative: true + source: path + version: "0.0.1" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + url: "https://pub.dev" + source: hosted + version: "2.3.7" + decimal: + dependency: transitive + description: + name: decimal + sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + encrypt: + dependency: transitive + description: + name: encrypt + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + ffigen: + dependency: "direct dev" + description: + name: ffigen + sha256: "2119b4fe3aad0db94dc9531b90283c4640a6231070e613c400b426a4da08c704" + url: "https://pub.dev" + source: hosted + version: "16.1.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_mobx: + dependency: transitive + description: + name: flutter_mobx + sha256: ba5e93467866a2991259dc51cffd41ef45f695c667c2b8e7b087bf24118b50fe + url: "https://pub.dev" + source: hosted + version: "2.3.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + hive: + dependency: transitive + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + http: + dependency: transitive + description: + name: http + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + url: "https://pub.dev" + source: hosted + version: "1.3.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + intl: + dependency: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + url: "https://pub.dev" + source: hosted + version: "10.0.5" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.dev" + source: hosted + version: "1.15.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mobx: + dependency: transitive + description: + name: mobx + sha256: bf1a90e5bcfd2851fc6984e20eef69557c65d9e4d0a88f5be4cf72c9819ce6b0 + url: "https://pub.dev" + source: hosted + version: "2.5.0" + mobx_codegen: + dependency: "direct dev" + description: + name: mobx_codegen + sha256: "990da80722f7d7c0017dec92040b31545d625b15d40204c36a1e63d167c73cdc" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + on_chain: + dependency: transitive + description: + path: "." + ref: cake-update-v2 + resolved-ref: "93440dc5126369b873ca1fccc13c3c1240b1c5c2" + url: "https://github.com/cake-tech/on_chain.git" + source: git + version: "3.7.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + url: "https://pub.dev" + source: hosted + version: "2.2.15" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + rational: + dependency: transitive + description: + name: rational + sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336 + url: "https://pub.dev" + source: hosted + version: "2.2.3" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + socks5_proxy: + dependency: transitive + description: + name: socks5_proxy + sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" + url: "https://pub.dev" + source: hosted + version: "1.0.6" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + url: "https://pub.dev" + source: hosted + version: "1.3.5" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + unorm_dart: + dependency: transitive + description: + name: unorm_dart + sha256: "23d8bf65605401a6a32cff99435fed66ef3dab3ddcad3454059165df46496a3b" + url: "https://pub.dev" + source: hosted + version: "0.3.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + url: "https://pub.dev" + source: hosted + version: "14.2.4" + watcher: + dependency: transitive + description: + name: watcher + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + sha256: fb38626579fb345ad00e674e2af3a5c9b0cc4b9bfb8fd7f7ff322c7c9e62aef5 + url: "https://pub.dev" + source: hosted + version: "2.2.2" +sdks: + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/cw_decred/pubspec.yaml b/cw_decred/pubspec.yaml new file mode 100644 index 000000000..fcb2ac5ec --- /dev/null +++ b/cw_decred/pubspec.yaml @@ -0,0 +1,84 @@ +name: cw_decred +description: A new Flutter plugin project. +version: 0.0.1 +publish_to: none +author: Cake Wallet +homepage: https://cakewallet.com + +environment: + sdk: '>=3.2.0-0 <4.0.0' + flutter: ">=3.19.0" + + +dependencies: + flutter: + sdk: flutter + cw_core: + path: ../cw_core + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.1.11 + build_resolvers: ^2.0.9 + mobx_codegen: ^2.0.7 + hive_generator: ^2.0.1 + ffigen: ^16.1.0 + +ffigen: + name: libdcrwallet + description: Bindings for dcrwallet go library. + output: "lib/api/libdcrwallet_bindings.dart" + headers: + entry-points: + - "lib/api/libdcrwallet.h" + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The androidPackage and pluginClass identifiers should not ordinarily + # be modified. They are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: com.cakewallet.cw_decred + pluginClass: CwDecredPlugin + ios: + pluginClass: CwDecredPlugin + macos: + pluginClass: CwDecredPlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 15e8f684e..ebad95f8e 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -115,6 +115,9 @@ abstract class MoneroWalletBase extends WalletBase balance; + @override + bool get hasRescan => true; + @override String get seed => monero_wallet.getSeed(); String seedLegacy(String? language) => monero_wallet.getSeedLegacy(language); diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index 67a9bbb45..6f4630b60 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -123,6 +123,9 @@ abstract class WowneroWalletBase String _password; + @override + bool get hasRescan => true; + @override MoneroWalletKeys get keys => MoneroWalletKeys( primaryAddress: wownero_wallet.getAddress(accountIndex: 0, addressIndex: 0), diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 57d4589c9..f621ab9b2 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3,8 +3,39 @@ PODS: - Flutter - ReachabilitySwift - CryptoSwift (1.8.3) + - cw_haven (0.0.1): + - cw_haven/Boost (= 0.0.1) + - cw_haven/Haven (= 0.0.1) + - cw_haven/OpenSSL (= 0.0.1) + - cw_haven/Sodium (= 0.0.1) + - cw_shared_external + - Flutter + - cw_haven/Boost (0.0.1): + - cw_shared_external + - Flutter + - cw_haven/Haven (0.0.1): + - cw_shared_external + - Flutter + - cw_haven/OpenSSL (0.0.1): + - cw_shared_external + - Flutter + - cw_haven/Sodium (0.0.1): + - cw_shared_external + - Flutter - cw_mweb (0.0.1): - Flutter + - cw_decred (0.0.1): + - cw_shared_external (0.0.1): + - cw_shared_external/Boost (= 0.0.1) + - cw_shared_external/OpenSSL (= 0.0.1) + - cw_shared_external/Sodium (= 0.0.1) + - Flutter + - cw_shared_external/Boost (0.0.1): + - Flutter + - cw_shared_external/OpenSSL (0.0.1): + - Flutter + - cw_shared_external/Sodium (0.0.1): + - Flutter - device_display_brightness (0.0.1): - Flutter - device_info_plus (0.0.1): @@ -106,7 +137,10 @@ PODS: DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - CryptoSwift + - cw_haven (from `.symlinks/plugins/cw_haven/ios`) - cw_mweb (from `.symlinks/plugins/cw_mweb/ios`) + - cw_shared_external (from `.symlinks/plugins/cw_shared_external/ios`) + - cw_decred (from `.symlinks/plugins/cw_decred/ios`) - device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - devicelocale (from `.symlinks/plugins/devicelocale/ios`) @@ -147,8 +181,14 @@ SPEC REPOS: EXTERNAL SOURCES: connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" + cw_haven: + :path: ".symlinks/plugins/cw_haven/ios" cw_mweb: :path: ".symlinks/plugins/cw_mweb/ios" + cw_shared_external: + :path: ".symlinks/plugins/cw_shared_external/ios" + cw_decred: + :path: ".symlinks/plugins/cw_decred/ios" device_display_brightness: :path: ".symlinks/plugins/device_display_brightness/ios" device_info_plus: @@ -203,7 +243,10 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483 + cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a cw_mweb: 22cd01dfb8ad2d39b15332006f22046aaa8352a3 + cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 + cw_decred: 9c0e1df74745b51a1289ec5e91fb9e24b68fa14a device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 devicelocale: 35ba84dc7f45f527c3001535d8c8d104edd5d926 diff --git a/ios/Runner/InfoBase.plist b/ios/Runner/InfoBase.plist index 69ab926b8..f27ef8d4f 100644 --- a/ios/Runner/InfoBase.plist +++ b/ios/Runner/InfoBase.plist @@ -242,6 +242,46 @@ wownero-wallet + + CFBundleTypeRole + Viewer + CFBundleURLName + zano + CFBundleURLSchemes + + zano + + + + CFBundleTypeRole + Viewer + CFBundleURLName + zano-wallet + CFBundleURLSchemes + + zano-wallet + + + + CFBundleTypeRole + Viewer + CFBundleURLName + decred + CFBundleURLSchemes + + decred + + + + CFBundleTypeRole + Viewer + CFBundleURLName + decred-wallet + CFBundleURLSchemes + + decred-wallet + + CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 888be1768..911e939d1 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -117,7 +117,7 @@ class AddressValidator extends TextValidator { case CryptoCurrency.zec: pattern = 't1[0-9a-zA-Z]{33}|t3[0-9a-zA-Z]{33}'; case CryptoCurrency.dcr: - pattern = 'D[ksecS]([0-9a-zA-Z])+'; + pattern = '(D|T|S)[ksecS]([0-9a-zA-Z])+'; case CryptoCurrency.rvn: pattern = '[Rr]([1-9a-km-zA-HJ-NP-Z]){33}'; case CryptoCurrency.near: diff --git a/lib/core/node_address_validator.dart b/lib/core/node_address_validator.dart index 0c8a0c37c..68478d501 100644 --- a/lib/core/node_address_validator.dart +++ b/lib/core/node_address_validator.dart @@ -17,3 +17,14 @@ class NodePathValidator extends TextValidator { isAutovalidate: true, ); } + +// NodeAddressValidatorDecredBlankException allows decred to send a blank ip +// address which effectively clears the current set persistant peer. +class NodeAddressValidatorDecredBlankException extends TextValidator { + NodeAddressValidatorDecredBlankException() + : super( + errorMessage: S.current.error_text_node_address, + isAutovalidate: true, + pattern: + '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\$|^[0-9a-zA-Z.\-]+\$'); +} diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index d963fb523..5356ed875 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -10,6 +10,7 @@ import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/utils/language_list.dart'; import 'package:cw_core/wallet_type.dart'; @@ -50,6 +51,8 @@ class SeedValidator extends Validator { return wownero!.getWowneroWordList(language); case WalletType.zano: return zano!.getWordList(language); + case WalletType.decred: + return decred!.getDecredWordList(); case WalletType.none: return []; } diff --git a/lib/core/sync_status_title.dart b/lib/core/sync_status_title.dart index 46dd62c3a..cf4cb8e88 100644 --- a/lib/core/sync_status_title.dart +++ b/lib/core/sync_status_title.dart @@ -63,5 +63,9 @@ String syncStatusTitle(SyncStatus syncStatus) { return S.current.sync_status_attempting_scan; } + if (syncStatus is ProcessingSyncStatus) { + return syncStatus.message ?? S.current.processing; + } + return ''; } diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index 3fb4b5b1d..b44e56a98 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -90,6 +90,7 @@ class WalletCreationService { case WalletType.nano: 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 new file mode 100644 index 000000000..283895936 --- /dev/null +++ b/lib/decred/cw_decred.dart @@ -0,0 +1,114 @@ +part of 'decred.dart'; + +class CWDecred extends Decred { + CWDecred() {} + + @override + WalletCredentials createDecredNewWalletCredentials( + {required String name, WalletInfo? walletInfo}) => + DecredNewWalletCredentials(name: name, walletInfo: walletInfo); + + @override + WalletCredentials createDecredRestoreWalletFromSeedCredentials( + {required String name, required String mnemonic, required String password}) => + DecredRestoreWalletFromSeedCredentials(name: name, mnemonic: mnemonic, password: password); + + @override + WalletCredentials createDecredRestoreWalletFromPubkeyCredentials( + {required String name, required String pubkey, required String password}) => + DecredRestoreWalletFromPubkeyCredentials(name: name, pubkey: pubkey, password: password); + + @override + WalletService createDecredWalletService( + Box walletInfoSource, Box unspentCoinSource) { + return DecredWalletService(walletInfoSource, unspentCoinSource); + } + + @override + List getTransactionPriorities() => DecredTransactionPriority.all; + + @override + TransactionPriority getDecredTransactionPriorityMedium() => DecredTransactionPriority.medium; + + @override + TransactionPriority getDecredTransactionPrioritySlow() => DecredTransactionPriority.slow; + + @override + TransactionPriority deserializeDecredTransactionPriority(int raw) => + DecredTransactionPriority.deserialize(raw: raw); + + @override + Object createDecredTransactionCredentials(List outputs, TransactionPriority priority) => + DecredTransactionCredentials( + outputs + .map((out) => OutputInfo( + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmount, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + formattedCryptoAmount: out.formattedCryptoAmount)) + .toList(), + priority: priority as DecredTransactionPriority); + + List getAddressInfos(Object wallet) { + final decredWallet = wallet as DecredWallet; + return decredWallet.walletAddresses.getAddressInfos(); + } + + @override + Future updateAddress(Object wallet, String address, String label) async { + final decredWallet = wallet as DecredWallet; + await decredWallet.walletAddresses.updateAddress(address, label); + } + + @override + Future generateNewAddress(Object wallet, String label) async { + final decredWallet = wallet as DecredWallet; + await decredWallet.walletAddresses.generateNewAddress(label); + } + + @override + String formatterDecredAmountToString({required int amount}) => + decredAmountToString(amount: amount); + + @override + double formatterDecredAmountToDouble({required int amount}) => + decredAmountToDouble(amount: amount); + + @override + int formatterStringDoubleToDecredAmount(String amount) => stringDoubleToDecredAmount(amount); + + @override + List getUnspents(Object wallet) { + final decredWallet = wallet as DecredWallet; + return decredWallet.unspents(); + } + + @override + void updateUnspents(Object wallet) { + final decredWallet = wallet as DecredWallet; + decredWallet.unspents(); + } + + @override + int heightByDate(DateTime date) { + final genesisBlocktime = DateTime.fromMillisecondsSinceEpoch(1454954400 * 1000); + final minutesDiff = date.difference(genesisBlocktime).inMinutes; + // Decred has five minute blocks on mainnet. + // NOTE: This is off by about a day but is currently unused by decred as we + // rescan from the wallet birthday. + return minutesDiff ~/ 5; + } + + @override + List getDecredWordList() => wordlist; + + @override + String pubkey(Object wallet) { + final decredWallet = wallet as DecredWallet; + return decredWallet.pubkey; + } +} diff --git a/lib/di.dart b/lib/di.dart index c8d664b6e..5c6d11e4b 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -69,6 +69,7 @@ import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/reactions/on_authentication_state_change.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/solana/solana.dart'; @@ -990,7 +991,8 @@ Future setup({ (Node? editingNode, bool? isSelected) => NodeCreateOrEditPage( nodeCreateOrEditViewModel: getIt.get(param2: false), editingNode: editingNode, - isSelected: isSelected)); + isSelected: isSelected, + type: getIt.get().wallet!.type)); getIt.registerFactoryParam( (Node? editingNode, bool? isSelected) => PowNodeCreateOrEditPage( @@ -1115,6 +1117,8 @@ Future setup({ return wownero!.createWowneroWalletService(_walletInfoSource, _unspentCoinsInfoSource); case WalletType.zano: return zano!.createZanoWalletService(_walletInfoSource); + case WalletType.decred: + return decred!.createDecredWalletService(_walletInfoSource, _unspentCoinsInfoSource); case WalletType.none: throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService'); } diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index e57d71174..5ed35b5a1 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -46,6 +46,7 @@ const newCakeWalletBitcoinUri = 'btc-electrum.cakewallet.com:50002'; const wowneroDefaultNodeUri = 'node3.monerodevs.org:34568'; const zanoDefaultNodeUri = 'zano.cakewallet.com:11211'; const moneroWorldNodeUri = '.moneroworld.com'; +const decredDefaultUri = "default-spv-nodes"; Future defaultSettingsMigration( {required int version, @@ -96,30 +97,67 @@ Future defaultSettingsMigration( PreferencesKey.currentBalanceDisplayModeKey, BalanceDisplayMode.availableBalance.raw); await sharedPreferences.setBool('save_recipient_address', true); await resetToDefault(nodes); - await changeMoneroCurrentNodeToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); - await changeBitcoinCurrentElectrumServerToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); - await changeLitecoinCurrentElectrumServerToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); - await changeHavenCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); - await changeBitcoinCashCurrentNodeToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.monero, + currentNodePreferenceKey: PreferencesKey.currentNodeIdKey, + useSSL: true, + trusted: true, + ); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.bitcoin, + currentNodePreferenceKey: PreferencesKey.currentBitcoinElectrumSererIdKey, + useSSL: true, + ); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.litecoin, + currentNodePreferenceKey: PreferencesKey.currentLitecoinElectrumSererIdKey, + useSSL: true, + ); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.haven, + currentNodePreferenceKey: PreferencesKey.currentHavenNodeIdKey, + ); break; case 2: await replaceNodesMigration(nodes: nodes); - await replaceDefaultNode(sharedPreferences: sharedPreferences, nodes: nodes); - + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.monero, + newDefaultUri: newCakeWalletMoneroUri, + currentNodePreferenceKey: PreferencesKey.currentNodeIdKey, + useSSL: true, + trusted: true, + oldUri: [ + 'xmr-node-uk.cakewallet.com:18081', + 'eu-node.cakewallet.io:18081', + 'node.cakewallet.io:18081' + ], + ); break; case 3: await updateNodeTypes(nodes: nodes); - await addBitcoinElectrumServerList(nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.bitcoin); break; case 4: - await changeBitcoinCurrentElectrumServerToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.bitcoin, + newDefaultUri: newCakeWalletBitcoinUri, + currentNodePreferenceKey: PreferencesKey.currentBitcoinElectrumSererIdKey, + useSSL: true, + ); break; case 5: @@ -139,7 +177,15 @@ Future defaultSettingsMigration( break; case 11: - await changeDefaultMoneroNode(nodes, sharedPreferences); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.monero, + newDefaultUri: newCakeWalletMoneroUri, + currentNodePreferenceKey: PreferencesKey.currentNodeIdKey, + trusted: true, + oldUri: ['.cakewallet.com'], + ); break; case 12: @@ -151,28 +197,38 @@ Future defaultSettingsMigration( break; case 15: - await addLitecoinElectrumServerList(nodes: nodes); - await changeLitecoinCurrentElectrumServerToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.litecoin); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.litecoin, + currentNodePreferenceKey: PreferencesKey.currentLitecoinElectrumSererIdKey, + ); await checkCurrentNodes(nodes, powNodes, sharedPreferences); break; case 16: - await addHavenNodeList(nodes: nodes); - await changeHavenCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.haven); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.haven, + currentNodePreferenceKey: PreferencesKey.currentHavenNodeIdKey, + ); await checkCurrentNodes(nodes, powNodes, sharedPreferences); break; case 17: - await changeDefaultHavenNode(nodes); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.haven, + currentNodePreferenceKey: PreferencesKey.currentHavenNodeIdKey, + ); break; case 18: - await updateWalletTypeNodesWithNewNode( - nodes: nodes, - newNodeUri: "cakexmrl7bonq7ovjka5kuwuyd3f7qnkz6z6s6dmsy3uckwra7bvggyd.onion:18081", - type: WalletType.monero, - ); + addWalletNodeList(nodes: nodes, type: WalletType.monero); break; case 19: @@ -182,47 +238,82 @@ Future defaultSettingsMigration( await migrateExchangeStatus(sharedPreferences); break; case 21: - await addEthereumNodeList(nodes: nodes); - await changeEthereumCurrentNodeToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.ethereum); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.ethereum, + currentNodePreferenceKey: PreferencesKey.currentEthereumNodeIdKey, + ); break; case 22: - await addNanoNodeList(nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.nano); await addNanoPowNodeList(nodes: powNodes); - await changeNanoCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); - await changeNanoCurrentPowNodeToDefault( - sharedPreferences: sharedPreferences, nodes: powNodes); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.nano, + currentNodePreferenceKey: PreferencesKey.currentNanoNodeIdKey, + ); + await _changeDefaultNode( + nodes: powNodes, + sharedPreferences: sharedPreferences, + type: WalletType.nano, + currentNodePreferenceKey: PreferencesKey.currentNanoPowNodeIdKey, + newDefaultUri: nanoDefaultPowNodeUri, + ); break; case 23: - await addBitcoinCashElectrumServerList(nodes: nodes); - await changeBitcoinCurrentElectrumServerToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.bitcoinCash); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.bitcoinCash, + currentNodePreferenceKey: PreferencesKey.currentBitcoinCashNodeIdKey, + ); break; case 24: - await addPolygonNodeList(nodes: nodes); - await changePolygonCurrentNodeToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.polygon); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.polygon, + currentNodePreferenceKey: PreferencesKey.currentPolygonNodeIdKey, + ); break; case 25: await rewriteSecureStoragePin(secureStorage: secureStorage); break; case 26: - /// commented out as it was a probable cause for some users to have white screen issues - /// maybe due to multiple access on Secure Storage at once - /// or long await time on start of the app - // await insecureStorageMigration(secureStorage: secureStorage, sharedPreferences: sharedPreferences); + /// commented out as it was a probable cause for some users to have white screen issues + /// maybe due to multiple access on Secure Storage at once + /// or long await time on start of the app + // await insecureStorageMigration(secureStorage: secureStorage, sharedPreferences: sharedPreferences); + break; case 27: - await addSolanaNodeList(nodes: nodes); - await changeSolanaCurrentNodeToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.solana); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.solana, + currentNodePreferenceKey: PreferencesKey.currentSolanaNodeIdKey, + ); break; case 28: await _updateMoneroPriority(sharedPreferences); break; case 29: - await changeDefaultBitcoinNode(nodes, sharedPreferences); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.bitcoin, + newDefaultUri: newCakeWalletBitcoinUri, + currentNodePreferenceKey: PreferencesKey.currentBitcoinElectrumSererIdKey, + useSSL: true, + oldUri: ['.cakewallet.com'], + ); break; case 30: await disableServiceStatusFiatDisabled(sharedPreferences); @@ -234,28 +325,50 @@ Future defaultSettingsMigration( await updateBtcNanoWalletInfos(walletInfoSource); break; case 33: - await addTronNodeList(nodes: nodes); - await changeTronCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.tron); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.tron, + currentNodePreferenceKey: PreferencesKey.currentTronNodeIdKey, + ); break; case 34: - await _addElectRsNode(nodes, sharedPreferences); + addWalletNodeList(nodes: nodes, type: WalletType.bitcoin); case 35: - await _switchElectRsNode(nodes, sharedPreferences); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.bitcoin, + newDefaultUri: newCakeWalletBitcoinUri, + currentNodePreferenceKey: PreferencesKey.currentBitcoinElectrumSererIdKey, + useSSL: true, + oldUri: ['electrs.cakewallet.com'], + ); break; case 36: - await addWowneroNodeList(nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.wownero); await changeWowneroCurrentNodeToDefault( sharedPreferences: sharedPreferences, nodes: nodes); break; case 37: - await replaceTronDefaultNode(sharedPreferences: sharedPreferences, nodes: nodes); + // removed as it would be replaced again anyway + // await replaceTronDefaultNode(sharedPreferences: sharedPreferences, nodes: nodes); break; case 38: await fixBtcDerivationPaths(walletInfoSource); break; case 39: _fixNodesUseSSLFlag(nodes); - await changeDefaultNanoNode(nodes, sharedPreferences); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.nano, + newDefaultUri: nanoDefaultNodeUri, + currentNodePreferenceKey: PreferencesKey.currentNanoNodeIdKey, + useSSL: true, + oldUri: ['rpc.nano.to'], + ); break; case 40: await removeMoneroWorld(sharedPreferences: sharedPreferences, nodes: nodes); @@ -266,8 +379,8 @@ Future defaultSettingsMigration( providerName: "SwapTrade", enabled: false, ); - await _addSethNode(nodes, sharedPreferences); - await updateTronNodesWithNowNodes(sharedPreferences: sharedPreferences, nodes: nodes); + addWalletNodeList(nodes: nodes, type: WalletType.bitcoin); + addWalletNodeList(nodes: nodes, type: WalletType.tron); break; case 42: _fixNodesUseSSLFlag(nodes); @@ -312,19 +425,8 @@ Future defaultSettingsMigration( case 45: await _backupHavenSeeds(havenSeedStore); - updateWalletTypeNodesWithNewNode( - newNodeUri: 'matic.nownodes.io', - nodes: nodes, - type: WalletType.polygon, - useSSL: true, - ); - updateWalletTypeNodesWithNewNode( - newNodeUri: 'eth.nownodes.io', - nodes: nodes, - type: WalletType.ethereum, - useSSL: true, - ); - + addWalletNodeList(nodes: nodes, type: WalletType.polygon); + addWalletNodeList(nodes: nodes, type: WalletType.ethereum); _changeDefaultNode( nodes: nodes, sharedPreferences: sharedPreferences, @@ -349,18 +451,7 @@ Future defaultSettingsMigration( break; case 46: await _fixNodesUseSSLFlag(nodes); - await updateWalletTypeNodesWithNewNode( - newNodeUri: 'litecoin.stackwallet.com:20063', - nodes: nodes, - type: WalletType.litecoin, - useSSL: true, - ); - await updateWalletTypeNodesWithNewNode( - newNodeUri: 'electrum-ltc.bysh.me:50002', - nodes: nodes, - type: WalletType.litecoin, - useSSL: true, - ); + await addWalletNodeList(nodes: nodes, type: WalletType.litecoin); await _changeDefaultNode( nodes: nodes, sharedPreferences: sharedPreferences, @@ -386,10 +477,14 @@ Future defaultSettingsMigration( newUri: "polygon-bor-rpc.publicnode.com", useSSL: true, ); - break; case 47: - await addZanoNodeList(nodes: nodes); - await changeZanoCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.zano); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.zano, + currentNodePreferenceKey: PreferencesKey.currentZanoNodeIdKey, + ); _changeExchangeProviderAvailability( sharedPreferences, providerName: "SimpleSwap", @@ -400,8 +495,17 @@ Future defaultSettingsMigration( providerName: "SwapTrade", enabled: false, ); - break; + break; case 48: + await addWalletNodeList(nodes: nodes, type: WalletType.decred); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.decred, + currentNodePreferenceKey: PreferencesKey.currentDecredNodeIdKey, + ); + break; + case 49: _changeExchangeProviderAvailability( sharedPreferences, providerName: "SwapTrade", @@ -414,8 +518,9 @@ Future defaultSettingsMigration( await sharedPreferences.setInt( PreferencesKey.currentDefaultSettingsMigrationVersion, version); - } catch (e) { + } catch (e, s) { printV('Migration error: ${e.toString()}'); + printV('Migration error: ${s}'); } }); @@ -455,17 +560,24 @@ Future _changeDefaultNode({ required Box nodes, required SharedPreferences sharedPreferences, required WalletType type, - required String newDefaultUri, required String currentNodePreferenceKey, - required bool useSSL, - required List - oldUri, // leave empty if you want to force replace the node regardless of the user's current node + bool useSSL = true, + bool trusted = false, + String? newDefaultUri, // ignore, if you want to use the default node uri + List? + oldUri, // ignore, if you want to force replace the node regardless of the user's current node }) async { final currentNodeId = sharedPreferences.getInt(currentNodePreferenceKey); - final currentNode = nodes.values.firstWhere((node) => node.key == currentNodeId); - final shouldReplace = oldUri.any((e) => currentNode.uriRaw.contains(e)); + final bool shouldReplace; + if (currentNodeId == null) { + shouldReplace = true; + } else { + final currentNode = nodes.values.firstWhere((node) => node.key == currentNodeId); + shouldReplace = oldUri?.any((e) => currentNode.uriRaw.contains(e)) ?? true; + } if (shouldReplace) { + newDefaultUri ??= _getDefaultNodeUri(type); var newNodeId = nodes.values.firstWhereOrNull((element) => element.uriRaw == newDefaultUri)?.key; @@ -475,6 +587,7 @@ Future _changeDefaultNode({ uri: newDefaultUri, type: type, useSSL: useSSL, + trusted: trusted, ); await nodes.add(newNode); @@ -485,23 +598,38 @@ Future _changeDefaultNode({ } } -/// Generic function for adding a new Node for a Wallet Type. -Future updateWalletTypeNodesWithNewNode({ - required Box nodes, - required WalletType type, - required String newNodeUri, - bool? useSSL, -}) async { - // If it already exists in the box of nodes, no need to add it annymore. - if (nodes.values.any((node) => node.uriRaw == newNodeUri)) return; - - await nodes.add( - Node( - uri: newNodeUri, - type: type, - useSSL: useSSL, - ), - ); +String _getDefaultNodeUri(WalletType type) { + switch (type) { + case WalletType.monero: + return newCakeWalletMoneroUri; + case WalletType.bitcoin: + return newCakeWalletBitcoinUri; + case WalletType.litecoin: + return cakeWalletLitecoinElectrumUri; + case WalletType.haven: + return havenDefaultNodeUri; + case WalletType.ethereum: + return ethereumDefaultNodeUri; + case WalletType.nano: + return nanoDefaultNodeUri; + case WalletType.bitcoinCash: + return cakeWalletBitcoinCashDefaultNodeUri; + case WalletType.polygon: + return polygonDefaultNodeUri; + case WalletType.solana: + return solanaDefaultNodeUri; + case WalletType.tron: + return tronDefaultNodeUri; + case WalletType.wownero: + return wowneroDefaultNodeUri; + case WalletType.zano: + return zanoDefaultNodeUri; + case WalletType.decred: + return decredDefaultUri; + case WalletType.banano: + case WalletType.none: + return ''; + } } void _changeExchangeProviderAvailability(SharedPreferences sharedPreferences, @@ -533,7 +661,7 @@ Future _fixNodesUseSSLFlag(Box nodes) async { } Future updateNanoNodeList({required Box nodes}) async { - final nodeList = await loadDefaultNanoNodes(); + final nodeList = await loadDefaultNodes(WalletType.nano); var listOfNewEndpoints = [ "app.natrium.io", "rainstorm.city", @@ -682,87 +810,16 @@ Future replaceNodesMigration({required Box nodes}) async { }); } -Future changeMoneroCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getMoneroDefaultNode(nodes: nodes); - final nodeId = node.key as int? ?? 0; // 0 - England - - await sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, nodeId); -} - -Node? getBitcoinDefaultElectrumServer({required Box nodes}) { - return nodes.values - .firstWhereOrNull((Node node) => node.uriRaw == cakeWalletBitcoinElectrumUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.bitcoin); -} - Node? getBitcoinTestnetDefaultElectrumServer({required Box nodes}) { return nodes.values .firstWhereOrNull((Node node) => node.uriRaw == publicBitcoinTestnetElectrumUri) ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.bitcoin); } -Node? getLitecoinDefaultElectrumServer({required Box nodes}) { - return nodes.values - .firstWhereOrNull((Node node) => node.uriRaw == cakeWalletLitecoinElectrumUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.litecoin); -} - -Node? getHavenDefaultNode({required Box nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == havenDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.haven); -} - -Node? getEthereumDefaultNode({required Box nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == ethereumDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.ethereum); -} - -Node? getPolygonDefaultNode({required Box nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == polygonDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.polygon); -} - -Node? getNanoDefaultNode({required Box nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == nanoDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.nano); -} - -Node? getNanoDefaultPowNode({required Box nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == nanoDefaultPowNodeUri) ?? - nodes.values.firstWhereOrNull((node) => (node.type == WalletType.nano)); -} - -Node? getBitcoinCashDefaultElectrumServer({required Box nodes}) { - return nodes.values - .firstWhereOrNull((Node node) => node.uriRaw == cakeWalletBitcoinCashDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.bitcoinCash); -} - -Node? getZanoDefaultNode({required Box nodes}) { - return nodes.values.firstWhereOrNull( - (Node node) => node.uriRaw == zanoDefaultNodeUri) - ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.zano); -} - -Node getMoneroDefaultNode({required Box nodes}) { - var nodeUri = newCakeWalletMoneroUri; - - try { - return nodes.values.firstWhere((Node node) => node.uriRaw == nodeUri); - } catch (_) { - return nodes.values.first; - } -} - -Node? getSolanaDefaultNode({required Box nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == solanaDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.solana); -} - -Node? getTronDefaultNode({required Box nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == tronDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.tron); +Node? getDefaultNode({required Box nodes, required WalletType type}) { + final defaultUri = _getDefaultNodeUri(type); + return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == defaultUri) ?? + nodes.values.firstWhereOrNull((node) => node.type == type); } Node getWowneroDefaultNode({required Box nodes}) { @@ -899,63 +956,6 @@ Future rewriteSecureStoragePin({required SecureStorage secureStorage}) asy ); } -Future changeBitcoinCurrentElectrumServerToDefault( - {required SharedPreferences sharedPreferences, - required Box nodes, - bool? isTestnet}) async { - Node? server; - if (isTestnet == true) { - server = getBitcoinTestnetDefaultElectrumServer(nodes: nodes); - } else { - server = getBitcoinDefaultElectrumServer(nodes: nodes); - } - final serverId = server?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentBitcoinElectrumSererIdKey, serverId); -} - -Future changeLitecoinCurrentElectrumServerToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final server = getLitecoinDefaultElectrumServer(nodes: nodes); - final serverId = server?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentLitecoinElectrumSererIdKey, serverId); -} - -Future changeBitcoinCashCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final server = getBitcoinCashDefaultElectrumServer(nodes: nodes); - final serverId = server?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentBitcoinCashNodeIdKey, serverId); -} - -Future changeHavenCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getHavenDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentHavenNodeIdKey, nodeId); -} - -Future replaceDefaultNode( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - const nodesForReplace = [ - 'xmr-node-uk.cakewallet.com:18081', - 'eu-node.cakewallet.io:18081', - 'node.cakewallet.io:18081' - ]; - final currentNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); - final currentNode = nodes.values.firstWhereOrNull((Node node) => node.key == currentNodeId); - final needToReplace = currentNode == null ? true : nodesForReplace.contains(currentNode.uriRaw); - - if (!needToReplace) { - return; - } - - await changeMoneroCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); -} - Future updateNodeTypes({required Box nodes}) async { nodes.values.forEach((node) async { if (node.type == null) { @@ -965,42 +965,6 @@ Future updateNodeTypes({required Box nodes}) async { }); } -Future addBitcoinElectrumServerList({required Box nodes}) async { - final serverList = await loadBitcoinElectrumServerList(); - for (var node in serverList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - -Future addLitecoinElectrumServerList({required Box nodes}) async { - final serverList = await loadLitecoinElectrumServerList(); - for (var node in serverList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - -Future addBitcoinCashElectrumServerList({required Box nodes}) async { - final serverList = await loadBitcoinCashElectrumServerList(); - for (var node in serverList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - -Future addHavenNodeList({required Box nodes}) async { - final nodeList = await loadDefaultHavenNodes(); - for (var node in nodeList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - Future addAddressesForMoneroWallets(Box walletInfoSource) async { final moneroWalletsInfo = walletInfoSource.values.where((info) => info.type == WalletType.monero); moneroWalletsInfo.forEach((info) async { @@ -1050,31 +1014,6 @@ Future changeTransactionPriorityAndFeeRateKeys(SharedPreferences sharedPre bitcoin!.getMediumTransactionPriority().serialize()); } -Future changeDefaultMoneroNode( - Box nodeSource, SharedPreferences sharedPreferences) async { - const cakeWalletMoneroNodeUriPattern = '.cakewallet.com'; - final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); - final currentMoneroNode = nodeSource.values.firstWhere((node) => node.key == currentMoneroNodeId); - final needToReplaceCurrentMoneroNode = - currentMoneroNode.uri.toString().contains(cakeWalletMoneroNodeUriPattern); - - nodeSource.values.forEach((node) async { - if (node.type == WalletType.monero && - node.uri.toString().contains(cakeWalletMoneroNodeUriPattern)) { - await node.delete(); - } - }); - - final newCakeWalletNode = - Node(uri: newCakeWalletMoneroUri, type: WalletType.monero, trusted: true); - - await nodeSource.add(newCakeWalletNode); - - if (needToReplaceCurrentMoneroNode) { - await sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, newCakeWalletNode.key as int); - } -} - Future fixBtcDerivationPaths(Box walletsInfoSource) async { for (WalletInfo walletInfo in walletsInfoSource.values) { if (walletInfo.type == WalletType.bitcoin || @@ -1102,128 +1041,6 @@ Future updateBtcNanoWalletInfos(Box walletsInfoSource) async { } } -Future changeDefaultNanoNode( - Box nodeSource, SharedPreferences sharedPreferences) async { - const oldNanoNodeUriPattern = 'rpc.nano.to'; - final currentNanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); - final currentNanoNode = nodeSource.values.firstWhere((node) => node.key == currentNanoNodeId); - - final newCakeWalletNode = Node( - uri: nanoDefaultNodeUri, - type: WalletType.nano, - useSSL: true, - ); - - await nodeSource.add(newCakeWalletNode); - - if (currentNanoNode.uri.toString().contains(oldNanoNodeUriPattern)) { - await sharedPreferences.setInt( - PreferencesKey.currentNanoNodeIdKey, newCakeWalletNode.key as int); - } -} - -Future changeDefaultBitcoinNode( - Box nodeSource, SharedPreferences sharedPreferences) async { - const cakeWalletBitcoinNodeUriPattern = '.cakewallet.com'; - final currentBitcoinNodeId = - sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); - final currentBitcoinNode = - nodeSource.values.firstWhere((node) => node.key == currentBitcoinNodeId); - final needToReplaceCurrentBitcoinNode = - currentBitcoinNode.uri.toString().contains(cakeWalletBitcoinNodeUriPattern); - - final newCakeWalletBitcoinNode = - Node(uri: newCakeWalletBitcoinUri, type: WalletType.bitcoin, useSSL: true); - - if (!nodeSource.values.any((element) => element.uriRaw == newCakeWalletBitcoinUri)) { - await nodeSource.add(newCakeWalletBitcoinNode); - } - - if (needToReplaceCurrentBitcoinNode) { - await sharedPreferences.setInt( - PreferencesKey.currentBitcoinElectrumSererIdKey, newCakeWalletBitcoinNode.key as int); - } -} - -Future _addSethNode(Box nodeSource, SharedPreferences sharedPreferences) async { - _addBitcoinNode( - nodeSource: nodeSource, - sharedPreferences: sharedPreferences, - nodeUri: "fulcrum.sethforprivacy.com:50002", - useSSL: false, - ); -} - -Future _addElectRsNode(Box nodeSource, SharedPreferences sharedPreferences) async { - _addBitcoinNode( - nodeSource: nodeSource, - sharedPreferences: sharedPreferences, - nodeUri: cakeWalletSilentPaymentsElectrsUri, - ); -} - -Future _addBitcoinNode({ - required Box nodeSource, - required SharedPreferences sharedPreferences, - required String nodeUri, - bool replaceExisting = false, - bool useSSL = false, -}) async { - bool isNodeExists = nodeSource.values.any((element) => element.uriRaw == nodeUri); - if (isNodeExists) { - return; - } - const cakeWalletBitcoinNodeUriPattern = '.cakewallet.com'; - final currentBitcoinNodeId = - sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); - final currentBitcoinNode = - nodeSource.values.firstWhere((node) => node.key == currentBitcoinNodeId); - final needToReplaceCurrentBitcoinNode = - currentBitcoinNode.uri.toString().contains(cakeWalletBitcoinNodeUriPattern); - - final newElectRsBitcoinNode = Node(uri: nodeUri, type: WalletType.bitcoin, useSSL: useSSL); - - await nodeSource.add(newElectRsBitcoinNode); - - if (needToReplaceCurrentBitcoinNode && replaceExisting) { - await sharedPreferences.setInt( - PreferencesKey.currentBitcoinElectrumSererIdKey, newElectRsBitcoinNode.key as int); - } -} - -Future _switchElectRsNode(Box nodeSource, SharedPreferences sharedPreferences) async { - final currentBitcoinNodeId = - sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); - final currentBitcoinNode = - nodeSource.values.firstWhere((node) => node.key == currentBitcoinNodeId); - final needToReplaceCurrentBitcoinNode = - currentBitcoinNode.uri.toString().contains('electrs.cakewallet.com'); - - if (!needToReplaceCurrentBitcoinNode) return; - - final btcElectrumNode = nodeSource.values.firstWhereOrNull( - (node) => node.uri.toString().contains('btc-electrum.cakewallet.com'), - ); - - if (btcElectrumNode == null) { - final newBtcElectrumBitcoinNode = Node( - uri: newCakeWalletBitcoinUri, - type: WalletType.bitcoin, - useSSL: false, - ); - await nodeSource.add(newBtcElectrumBitcoinNode); - await sharedPreferences.setInt( - PreferencesKey.currentBitcoinElectrumSererIdKey, - newBtcElectrumBitcoinNode.key as int, - ); - } else { - await sharedPreferences.setInt( - PreferencesKey.currentBitcoinElectrumSererIdKey, - btcElectrumNode.key as int, - ); - } -} - Future checkCurrentNodes( Box nodeSource, Box powNodeSource, SharedPreferences sharedPreferences) async { final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); @@ -1236,6 +1053,7 @@ Future checkCurrentNodes( final currentPolygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey); final currentNanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final currentNanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey); + final currentDecredNodeId = sharedPreferences.getInt(PreferencesKey.currentDecredNodeIdKey); final currentBitcoinCashNodeId = sharedPreferences.getInt(PreferencesKey.currentBitcoinCashNodeIdKey); final currentSolanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); @@ -1256,6 +1074,8 @@ Future checkCurrentNodes( nodeSource.values.firstWhereOrNull((node) => node.key == currentPolygonNodeId); final currentNanoNodeServer = nodeSource.values.firstWhereOrNull((node) => node.key == currentNanoNodeId); + final currentDecredNodeServer = + nodeSource.values.firstWhereOrNull((node) => node.key == currentDecredNodeId); final currentNanoPowNodeServer = powNodeSource.values.firstWhereOrNull((node) => node.key == currentNanoPowNodeId); final currentBitcoinCashNodeServer = @@ -1266,7 +1086,8 @@ Future checkCurrentNodes( nodeSource.values.firstWhereOrNull((node) => node.key == currentTronNodeId); final currentWowneroNodeServer = nodeSource.values.firstWhereOrNull((node) => node.key == currentWowneroNodeId); - final currentZanoNode = nodeSource.values.firstWhereOrNull((node) => node.key == currentZanoNodeId); + final currentZanoNode = + nodeSource.values.firstWhereOrNull((node) => node.key == currentZanoNodeId); if (currentMoneroNode == null) { final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero); @@ -1357,6 +1178,12 @@ Future checkCurrentNodes( await nodeSource.add(node); await sharedPreferences.setInt(PreferencesKey.currentZanoNodeIdKey, node.key as int); } + + if (currentDecredNodeServer == null) { + final node = Node(uri: decredDefaultUri, type: WalletType.decred); + await nodeSource.add(node); + await sharedPreferences.setInt(PreferencesKey.currentDecredNodeIdKey, node.key as int); + } } Future resetBitcoinElectrumServer( @@ -1385,15 +1212,6 @@ Future resetBitcoinElectrumServer( await oldElectrumServer?.delete(); } -Future changeDefaultHavenNode(Box nodeSource) async { - const previousHavenDefaultNodeUri = 'vault.havenprotocol.org:443'; - final havenNodes = nodeSource.values.where((node) => node.uriRaw == previousHavenDefaultNodeUri); - havenNodes.forEach((node) async { - node.uriRaw = havenDefaultNodeUri; - await node.save(); - }); -} - Future migrateExchangeStatus(SharedPreferences sharedPreferences) async { final isExchangeDisabled = sharedPreferences.getBool(PreferencesKey.disableExchangeKey); if (isExchangeDisabled == null) { @@ -1406,65 +1224,14 @@ Future migrateExchangeStatus(SharedPreferences sharedPreferences) async { await sharedPreferences.remove(PreferencesKey.disableExchangeKey); } -Future addEthereumNodeList({required Box nodes}) async { - final nodeList = await loadDefaultEthereumNodes(); - for (var node in nodeList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - -Future changeEthereumCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getEthereumDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentEthereumNodeIdKey, nodeId); -} - -Future addWowneroNodeList({required Box nodes}) async { - final nodeList = await loadDefaultWowneroNodes(); - for (var node in nodeList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - -Future addZanoNodeList({required Box nodes}) async { - final nodeList = await loadDefaultZanoNodes(); - for (var node in nodeList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - Future changeWowneroCurrentNodeToDefault( {required SharedPreferences sharedPreferences, required Box nodes}) async { final node = getWowneroDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; + final nodeId = node.key as int? ?? 0; await sharedPreferences.setInt(PreferencesKey.currentWowneroNodeIdKey, nodeId); } -Future changeZanoCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getZanoDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - await sharedPreferences.setInt(PreferencesKey.currentZanoNodeIdKey, nodeId); -} - -Future addNanoNodeList({required Box nodes}) async { - final nodeList = await loadDefaultNanoNodes(); - for (var node in nodeList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - Future addNanoPowNodeList({required Box nodes}) async { final nodeList = await loadDefaultNanoPowNodes(); for (var node in nodeList) { @@ -1474,23 +1241,13 @@ Future addNanoPowNodeList({required Box nodes}) async { } } -Future changeNanoCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getNanoDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentNanoNodeIdKey, nodeId); +Node? getNanoDefaultPowNode({required Box nodes}) { + return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == nanoDefaultPowNodeUri) ?? + nodes.values.firstWhereOrNull((node) => (node.type == WalletType.nano)); } -Future changeNanoCurrentPowNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getNanoDefaultPowNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - await sharedPreferences.setInt(PreferencesKey.currentNanoPowNodeIdKey, nodeId); -} - -Future addPolygonNodeList({required Box nodes}) async { - final nodeList = await loadDefaultPolygonNodes(); +Future addWalletNodeList({required Box nodes, required WalletType type}) async { + final List nodeList = await loadDefaultNodes(type); for (var node in nodeList) { if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { await nodes.add(node); @@ -1498,74 +1255,6 @@ Future addPolygonNodeList({required Box nodes}) async { } } -Future changePolygonCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getPolygonDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentPolygonNodeIdKey, nodeId); -} - -Future addSolanaNodeList({required Box nodes}) async { - final nodeList = await loadDefaultSolanaNodes(); - for (var node in nodeList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - -Future changeSolanaCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getSolanaDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, nodeId); -} - -Future addTronNodeList({required Box nodes}) async { - final nodeList = await loadDefaultTronNodes(); - for (var node in nodeList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - -Future changeTronCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getTronDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentTronNodeIdKey, nodeId); -} - -Future replaceTronDefaultNode({ - required SharedPreferences sharedPreferences, - required Box nodes, -}) async { - // Get the currently active node - final currentTronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); - final currentTronNode = - nodes.values.firstWhereOrNull((Node node) => node.key == currentTronNodeId); - - //Confirm if this node is part of the default nodes from CakeWallet - final tronDefaultNodeList = [ - 'tron-rpc.publicnode.com:443', - 'api.trongrid.io', - ]; - bool needsToBeReplaced = - currentTronNode == null ? true : tronDefaultNodeList.contains(currentTronNode.uriRaw); - - // If it's a custom node, return. We don't want to switch users from their custom nodes - if (!needsToBeReplaced) { - return; - } - - // If it's not, we switch user to the new default node: NowNodes - await changeTronCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); -} - Future removeMoneroWorld( {required SharedPreferences sharedPreferences, required Box nodes}) async { const cakeWalletMoneroNodeUriPattern = '.moneroworld.com'; @@ -1582,19 +1271,13 @@ Future removeMoneroWorld( }); if (needToReplaceCurrentMoneroNode) { - await changeMoneroCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.monero, + newDefaultUri: newCakeWalletMoneroUri, + currentNodePreferenceKey: PreferencesKey.currentNodeIdKey, + trusted: true, + ); } } - -Future updateTronNodesWithNowNodes({ - required SharedPreferences sharedPreferences, - required Box nodes, -}) async { - final tronNowNodesUri = 'trx.nownodes.io'; - - if (nodes.values.any((node) => node.uriRaw == tronNowNodesUri)) return; - - await nodes.add(Node(uri: tronNowNodesUri, type: WalletType.tron)); - - await replaceTronDefaultNode(sharedPreferences: sharedPreferences, nodes: nodes); -} diff --git a/lib/entities/main_actions.dart b/lib/entities/main_actions.dart index 68f599718..2e633bce8 100644 --- a/lib/entities/main_actions.dart +++ b/lib/entities/main_actions.dart @@ -59,6 +59,7 @@ class MainActions { static MainActions sendAction = MainActions._( name: (context) => S.of(context).send, image: 'assets/images/upload.png', + isEnabled: (viewModel) => viewModel.canSend, onTap: (BuildContext context, DashboardViewModel viewModel) async { Navigator.pushNamed(context, Routes.send); }, diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index 5147aa614..bb489e715 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -4,111 +4,62 @@ import "package:yaml/yaml.dart"; import 'package:cw_core/node.dart'; import 'package:cw_core/wallet_type.dart'; -Future> loadDefaultNodes() async { - final nodesRaw = await rootBundle.loadString('assets/node_list.yml'); +Future> loadDefaultNodes(WalletType type) async { + String path; + switch (type) { + case WalletType.monero: + path = 'assets/node_list.yml'; + break; + case WalletType.bitcoin: + path = 'assets/bitcoin_electrum_server_list.yml'; + break; + case WalletType.litecoin: + path = 'assets/litecoin_electrum_server_list.yml'; + break; + case WalletType.haven: + path = 'assets/haven_node_list.yml'; + break; + case WalletType.ethereum: + path = 'assets/ethereum_server_list.yml'; + break; + case WalletType.nano: + path = 'assets/nano_node_list.yml'; + break; + case WalletType.bitcoinCash: + path = 'assets/bitcoin_cash_electrum_server_list.yml'; + break; + case WalletType.polygon: + path = 'assets/polygon_node_list.yml'; + break; + case WalletType.solana: + path = 'assets/solana_node_list.yml'; + break; + case WalletType.tron: + path = 'assets/tron_node_list.yml'; + break; + case WalletType.wownero: + path = 'assets/wownero_node_list.yml'; + break; + case WalletType.zano: + path = 'assets/zano_node_list.yml'; + break; + case WalletType.decred: + path = 'assets/decred_node_list.yml'; + break; + case WalletType.banano: + case WalletType.none: + path = ''; + break; + } + + final nodesRaw = await rootBundle.loadString(path); final loadedNodes = loadYaml(nodesRaw) as YamlList; final nodes = []; for (final raw in loadedNodes) { if (raw is Map) { final node = Node.fromMap(Map.from(raw)); - node.type = WalletType.monero; - nodes.add(node); - } - } - - return nodes; -} - -Future> loadBitcoinElectrumServerList() async { - final serverListRaw = await rootBundle.loadString('assets/bitcoin_electrum_server_list.yml'); - final loadedServerList = loadYaml(serverListRaw) as YamlList; - final serverList = []; - - for (final raw in loadedServerList) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - node.type = WalletType.bitcoin; - serverList.add(node); - } - } - - return serverList; -} - -Future> loadLitecoinElectrumServerList() async { - final serverListRaw = await rootBundle.loadString('assets/litecoin_electrum_server_list.yml'); - final loadedServerList = loadYaml(serverListRaw) as YamlList; - final serverList = []; - - for (final raw in loadedServerList) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - node.type = WalletType.litecoin; - serverList.add(node); - } - } - - return serverList; -} - -Future> loadDefaultHavenNodes() async { - final nodesRaw = await rootBundle.loadString('assets/haven_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = []; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - node.type = WalletType.haven; - nodes.add(node); - } - } - - return nodes; -} - -Future> loadDefaultEthereumNodes() async { - final nodesRaw = await rootBundle.loadString('assets/ethereum_server_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = []; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - node.type = WalletType.ethereum; - nodes.add(node); - } - } - - return nodes; -} - -Future> loadBitcoinCashElectrumServerList() async { - final serverListRaw = await rootBundle.loadString('assets/bitcoin_cash_electrum_server_list.yml'); - final loadedServerList = loadYaml(serverListRaw) as YamlList; - final serverList = []; - - for (final raw in loadedServerList) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - node.type = WalletType.bitcoinCash; - serverList.add(node); - } - } - - return serverList; -} - -Future> loadDefaultNanoNodes() async { - final nodesRaw = await rootBundle.loadString('assets/nano_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = []; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - node.type = WalletType.nano; + node.type = type; nodes.add(node); } } @@ -132,103 +83,19 @@ Future> loadDefaultNanoPowNodes() async { return nodes; } -Future> loadDefaultPolygonNodes() async { - final nodesRaw = await rootBundle.loadString('assets/polygon_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = []; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - - node.type = WalletType.polygon; - nodes.add(node); - } - } - - return nodes; -} - -Future> loadDefaultSolanaNodes() async { - final nodesRaw = await rootBundle.loadString('assets/solana_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = []; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - - node.type = WalletType.solana; - nodes.add(node); - } - } - - return nodes; -} - -Future> loadDefaultTronNodes() async { - final nodesRaw = await rootBundle.loadString('assets/tron_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = []; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - - node.type = WalletType.tron; - nodes.add(node); - } - } - - return nodes; -} - -Future> loadDefaultWowneroNodes() async { - final nodesRaw = await rootBundle.loadString('assets/wownero_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = []; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - - node.type = WalletType.wownero; - nodes.add(node); - } - } - - return nodes; -} - -Future> loadDefaultZanoNodes() async { - final nodesRaw = await rootBundle.loadString('assets/zano_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = []; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - - node.type = WalletType.zano; - nodes.add(node); - } - } - - return nodes; -} - Future resetToDefault(Box nodeSource) async { - final moneroNodes = await loadDefaultNodes(); - final bitcoinElectrumServerList = await loadBitcoinElectrumServerList(); - final litecoinElectrumServerList = await loadLitecoinElectrumServerList(); - final bitcoinCashElectrumServerList = await loadBitcoinCashElectrumServerList(); - final havenNodes = await loadDefaultHavenNodes(); - final ethereumNodes = await loadDefaultEthereumNodes(); - final nanoNodes = await loadDefaultNanoNodes(); - final polygonNodes = await loadDefaultPolygonNodes(); - final solanaNodes = await loadDefaultSolanaNodes(); - final tronNodes = await loadDefaultTronNodes(); - final zanoNodes = await loadDefaultZanoNodes(); + final moneroNodes = await loadDefaultNodes(WalletType.monero); + final bitcoinElectrumServerList = await loadDefaultNodes(WalletType.bitcoin); + final litecoinElectrumServerList = await loadDefaultNodes(WalletType.litecoin); + final bitcoinCashElectrumServerList = await loadDefaultNodes(WalletType.bitcoinCash); + final havenNodes = await loadDefaultNodes(WalletType.haven); + final ethereumNodes = await loadDefaultNodes(WalletType.ethereum); + final nanoNodes = await loadDefaultNodes(WalletType.nano); + final polygonNodes = await loadDefaultNodes(WalletType.polygon); + final solanaNodes = await loadDefaultNodes(WalletType.solana); + final tronNodes = await loadDefaultNodes(WalletType.tron); + final decredNodes = await loadDefaultNodes(WalletType.decred); + final zanoNodes = await loadDefaultNodes(WalletType.zano); final nodes = moneroNodes + bitcoinElectrumServerList + @@ -238,7 +105,10 @@ Future resetToDefault(Box nodeSource) async { bitcoinCashElectrumServerList + nanoNodes + polygonNodes + - solanaNodes + tronNodes + zanoNodes; + solanaNodes + + tronNodes + + zanoNodes + + decredNodes; await nodeSource.clear(); await nodeSource.addAll(nodes); diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 7fbdb645a..9e889ff46 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -10,6 +10,7 @@ class PreferencesKey { static const currentPolygonNodeIdKey = 'current_node_id_matic'; static const currentNanoNodeIdKey = 'current_node_id_nano'; static const currentNanoPowNodeIdKey = 'current_node_id_nano_pow'; + static const currentDecredNodeIdKey = 'current_node_id_decred'; static const currentBananoNodeIdKey = 'current_node_id_banano'; static const currentBananoPowNodeIdKey = 'current_node_id_banano_pow'; static const currentFiatCurrencyKey = 'current_fiat_currency'; @@ -48,6 +49,7 @@ class PreferencesKey { static const bitcoinCashTransactionPriority = 'current_fee_priority_bitcoin_cash'; static const zanoTransactionPriority = 'current_fee_priority_zano'; static const wowneroTransactionPriority = 'current_fee_priority_wownero'; + static const decredTransactionPriority = 'current_fee_priority_decred'; static const customBitcoinFeeRate = 'custom_electrum_fee_rate'; static const silentPaymentsCardDisplay = 'silentPaymentsCardDisplay'; static const silentPaymentsAlwaysScan = 'silentPaymentsAlwaysScan'; @@ -81,6 +83,7 @@ class PreferencesKey { static const lookupsENS = 'looks_up_ens'; static const lookupsWellKnown = 'looks_up_well_known'; static const showCameraConsent = 'show_camera_consent'; + static const showDecredInfoCard = 'show_decred_info_card'; static String moneroWalletUpdateV1Key(String name) => '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}'; diff --git a/lib/entities/priority_for_wallet_type.dart b/lib/entities/priority_for_wallet_type.dart index bbd98d17d..f7165500f 100644 --- a/lib/entities/priority_for_wallet_type.dart +++ b/lib/entities/priority_for_wallet_type.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_type.dart'; @@ -35,6 +36,8 @@ List priorityForWalletType(WalletType type) { return []; case WalletType.zano: return zano!.getTransactionPriorities(); + case WalletType.decred: + return decred!.getTransactionPriorities(); default: return []; } diff --git a/lib/entities/provider_types.dart b/lib/entities/provider_types.dart index 5888970b0..2d2a98379 100644 --- a/lib/entities/provider_types.dart +++ b/lib/entities/provider_types.dart @@ -82,6 +82,7 @@ class ProvidersHelper { ProviderType.moonpay, ProviderType.kriptonim ]; + case WalletType.decred: case WalletType.none: case WalletType.haven: case WalletType.zano: @@ -113,6 +114,7 @@ class ProvidersHelper { ]; case WalletType.monero: return [ProviderType.dfx]; + case WalletType.decred: case WalletType.nano: case WalletType.banano: case WalletType.none: diff --git a/lib/main.dart b/lib/main.dart index 1eedfa6a9..2bf0f269d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -215,7 +215,7 @@ Future initializeAppConfigs() async { secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, havenSeedStore: havenSeedStore, - initialMigrationVersion: 48, + initialMigrationVersion: 49, ); } diff --git a/lib/reactions/bip39_wallet_utils.dart b/lib/reactions/bip39_wallet_utils.dart index 8b99331ce..0c58bc76f 100644 --- a/lib/reactions/bip39_wallet_utils.dart +++ b/lib/reactions/bip39_wallet_utils.dart @@ -16,6 +16,7 @@ bool isBIP39Wallet(WalletType walletType) { case WalletType.wownero: case WalletType.haven: case WalletType.zano: + case WalletType.decred: case WalletType.none: return false; } diff --git a/lib/reactions/check_connection.dart b/lib/reactions/check_connection.dart index d60037543..1e8fa88fa 100644 --- a/lib/reactions/check_connection.dart +++ b/lib/reactions/check_connection.dart @@ -18,6 +18,9 @@ void startCheckConnectionReaction(WalletBase wallet, SettingsStore settingsStore if (wallet.type == WalletType.bitcoin && wallet.syncStatus is SyncingSyncStatus) { return; } + if (wallet.type == WalletType.decred && wallet.syncStatus is ProcessingSyncStatus) { + return; + } try { final connectivityResult = await (Connectivity().checkConnectivity()); diff --git a/lib/reactions/fiat_rate_update.dart b/lib/reactions/fiat_rate_update.dart index de3dea4a2..2ddb1c6f2 100644 --- a/lib/reactions/fiat_rate_update.dart +++ b/lib/reactions/fiat_rate_update.dart @@ -10,7 +10,6 @@ import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index a6475571d..513c97c4e 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -74,7 +74,8 @@ void startCurrentWalletChangeReaction( wallet.type == WalletType.wownero || wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin || - wallet.type == WalletType.bitcoinCash) { + wallet.type == WalletType.bitcoinCash || + wallet.type == WalletType.decred) { _setAutoGenerateSubaddressStatus(wallet, settingsStore); } diff --git a/lib/reactions/on_wallet_sync_status_change.dart b/lib/reactions/on_wallet_sync_status_change.dart index 072602e5f..fc618dabd 100644 --- a/lib/reactions/on_wallet_sync_status_change.dart +++ b/lib/reactions/on_wallet_sync_status_change.dart @@ -25,7 +25,7 @@ void startWalletSyncStatusChangeReaction( await updateHavenRate(fiatConversionStore); } } - if (status is SyncingSyncStatus) { + if (status is SyncingSyncStatus || status is ProcessingSyncStatus) { await WakelockPlus.enable(); } if (status is SyncedSyncStatus || status is FailedSyncStatus) { diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart index 6804467f7..fed9cb6fe 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart @@ -47,6 +47,7 @@ class _DesktopWalletSelectionDropDownState extends State Image.asset( @@ -181,6 +182,8 @@ class _DesktopWalletSelectionDropDownState extends State launchUrl( - Uri.parse("https://docs.cakewallet.com/cryptos/litecoin/#mweb"), - mode: LaunchMode.externalApplication, - ), - child: Text( - S.of(context).learn_more, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: - Theme.of(context).extension()!.labelTextColor, - height: 1, - ), - softWrap: true, - ), - ), - SizedBox(height: 8), - Row( - children: [ - Expanded( - child: ElevatedButton( - onPressed: () => _dismissMweb(context), - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).primaryColor, - ), - child: Text( - S.of(context).litecoin_mweb_dismiss, - style: TextStyle(color: Colors.white), - ), - ), - ), - const SizedBox(width: 8), - Expanded( - child: ElevatedButton( - onPressed: () => _enableMweb(context), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - foregroundColor: Colors.black, - ), - child: Text( - S.of(context).enable, - maxLines: 1, - ), - ), - ), - ], - ), - ], - ), - icon: Container( - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, + description: S.of(context).litecoin_mweb_description, + leftButtonTitle: S.of(context).litecoin_mweb_dismiss, + rightButtonTitle: S.of(context).enable, + image: 'assets/images/mweb_logo.png', + leftButtonAction: () => _dismissMweb(context), + rightButtonAction: () => _enableMweb(context), + hintWidget: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => launchUrl( + Uri.parse("https://docs.cakewallet.com/cryptos/litecoin/#mweb"), + mode: LaunchMode.externalApplication, ), - child: ImageIcon( - AssetImage('assets/images/mweb_logo.png'), - color: Color.fromARGB(255, 11, 70, 129), - size: 40, + child: Text( + S.of(context).learn_more, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.labelTextColor, + height: 1, + ), + softWrap: true, ), ), ), ), ], + if (dashboardViewModel.showDecredInfoCard) ...[ + SizedBox(height: 10), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: InfoCard( + title: S.of(context).decred_info_title, + description: S.of(context).decred_info_card_details, + image: 'assets/images/dcr_icon.png', + leftButtonTitle: S.of(context).litecoin_mweb_dismiss, + rightButtonTitle: S.of(context).learn_more, + leftButtonAction: () => dashboardViewModel.dismissDecredInfoCard(), + rightButtonAction: () => launchUrl(Uri.parse("https://docs.cakewallet.com/cryptos/decred/#spv-sync")), + ), + ), + ], ], ); }), diff --git a/lib/src/screens/dashboard/pages/navigation_dock.dart b/lib/src/screens/dashboard/pages/navigation_dock.dart index 52b39cdf7..4eda169d3 100644 --- a/lib/src/screens/dashboard/pages/navigation_dock.dart +++ b/lib/src/screens/dashboard/pages/navigation_dock.dart @@ -75,8 +75,10 @@ class NavigationDock extends StatelessWidget { .labelTextColor, ), title: action.name(context), - onClick: () async => - await action.onTap(context, dashboardViewModel), + onClick: (action.isEnabled?.call(dashboardViewModel) ?? true) + ? () async => + await action.onTap(context, dashboardViewModel) + : null, textColor: action.isEnabled?.call(dashboardViewModel) ?? true ? null : Theme.of(context) diff --git a/lib/src/screens/dashboard/widgets/info_card.dart b/lib/src/screens/dashboard/widgets/info_card.dart new file mode 100644 index 000000000..1bf8a11bf --- /dev/null +++ b/lib/src/screens/dashboard/widgets/info_card.dart @@ -0,0 +1,88 @@ +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; +import 'package:cake_wallet/src/widgets/dashboard_card_widget.dart'; +import 'package:flutter/material.dart'; + +class InfoCard extends StatelessWidget { + final String leftButtonTitle; + final String rightButtonTitle; + final String title; + final String description; + final String image; + + final Function() leftButtonAction; + final Function() rightButtonAction; + + final Widget? hintWidget; + + const InfoCard({ + Key? key, + required this.title, + required this.description, + required this.leftButtonTitle, + required this.rightButtonTitle, + required this.leftButtonAction, + required this.rightButtonAction, + required this.image, + this.hintWidget, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return DashBoardRoundedCardWidget( + marginH: 0, + marginV: 0, + customBorder: 30, + title: title, + subTitle: description, + hint: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (hintWidget != null) hintWidget!, + if (hintWidget != null) SizedBox(height: 8), + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: leftButtonAction, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColor, + ), + child: Text( + leftButtonTitle, + style: TextStyle(color: Colors.white), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton( + onPressed: rightButtonAction, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + ), + child: Text( + rightButtonTitle, + maxLines: 1, + ), + ), + ), + ], + ), + ], + ), + onTap: () => {}, + icon: Container( + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: CakeImageWidget( + imageUrl: image, + height: 40, + width: 40, + ), + ), + ); + } +} diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index 16e0f88aa..dbf55d046 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -38,7 +38,8 @@ class MenuWidgetState extends State { this.solanaIcon = Image.asset('assets/images/sol_icon.png'), this.tronIcon = Image.asset('assets/images/trx_icon.png'), this.wowneroIcon = Image.asset('assets/images/wownero_icon.png'), - this.zanoIcon = Image.asset('assets/images/zano_icon.png'); + this.zanoIcon = Image.asset('assets/images/zano_icon.png'), + this.decredIcon = Image.asset('assets/images/decred_menu.png'); final largeScreen = 731; @@ -64,6 +65,7 @@ class MenuWidgetState extends State { Image tronIcon; Image wowneroIcon; Image zanoIcon; + Image decredIcon; @override void initState() { @@ -250,6 +252,8 @@ class MenuWidgetState extends State { return wowneroIcon; case WalletType.zano: return zanoIcon; + case WalletType.decred: + return decredIcon; default: throw Exception('No icon for ${type.toString()}'); } diff --git a/lib/src/screens/dashboard/widgets/sync_indicator.dart b/lib/src/screens/dashboard/widgets/sync_indicator.dart index 27b3d0109..860747d1e 100644 --- a/lib/src/screens/dashboard/widgets/sync_indicator.dart +++ b/lib/src/screens/dashboard/widgets/sync_indicator.dart @@ -22,8 +22,8 @@ class SyncIndicator extends StatelessWidget { builder: (_) { final syncIndicatorWidth = 237.0; final status = dashboardViewModel.status; - final statusText = status != null ? syncStatusTitle(status) : ''; - final progress = status != null ? status.progress() : 0.0; + final statusText = syncStatusTitle(status); + final progress = status.progress(); final indicatorOffset = progress * syncIndicatorWidth; final indicatorWidth = progress < 1 ? indicatorOffset > 0 ? indicatorOffset : 0.0 diff --git a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart index e5853570e..f8901918f 100644 --- a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart +++ b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart @@ -274,7 +274,8 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo ], ); }), - if (widget.privacySettingsViewModel.type == WalletType.bitcoin) + if (widget.privacySettingsViewModel.type == WalletType.bitcoin || + widget.privacySettingsViewModel.type == WalletType.decred) Builder(builder: (_) { final val = testnetValue ?? false; return SettingsSwitcherCell( @@ -301,7 +302,9 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo widget.nodeViewModel.save(); } - if (testnetValue == true) { + if (testnetValue == true && + widget.privacySettingsViewModel.type == + WalletType.bitcoin) { // TODO: add type (mainnet/testnet) to Node class so when switching wallets the node can be switched to a matching type // Currently this is so you can create a working testnet wallet but you need to keep switching back the node if you use multiple wallets at once widget.nodeViewModel.address = publicBitcoinTestnetElectrumAddress; diff --git a/lib/src/screens/nodes/node_create_or_edit_page.dart b/lib/src/screens/nodes/node_create_or_edit_page.dart index 53c34f302..fc6ac07e0 100644 --- a/lib/src/screens/nodes/node_create_or_edit_page.dart +++ b/lib/src/screens/nodes/node_create_or_edit_page.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cw_core/node.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; @@ -15,7 +16,7 @@ import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; class NodeCreateOrEditPage extends BasePage { - NodeCreateOrEditPage({required this.nodeCreateOrEditViewModel,this.editingNode, this.isSelected}) + NodeCreateOrEditPage({required this.nodeCreateOrEditViewModel,this.editingNode, this.isSelected, this.type}) : _formKey = GlobalKey(), _addressController = TextEditingController(), _pathController = TextEditingController(), @@ -86,6 +87,7 @@ class NodeCreateOrEditPage extends BasePage { final NodeCreateOrEditViewModel nodeCreateOrEditViewModel; final Node? editingNode; final bool? isSelected; + final WalletType? type; @override Widget body(BuildContext context) { @@ -130,6 +132,7 @@ class NodeCreateOrEditPage extends BasePage { formKey: _formKey, nodeViewModel: nodeCreateOrEditViewModel, editingNode: editingNode, + type: type, ), bottomSectionPadding: EdgeInsets.only(bottom: 24), bottomSection: Observer( diff --git a/lib/src/screens/nodes/widgets/node_form.dart b/lib/src/screens/nodes/widgets/node_form.dart index 22a38f423..eeda073af 100644 --- a/lib/src/screens/nodes/widgets/node_form.dart +++ b/lib/src/screens/nodes/widgets/node_form.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/widgets/standard_checkbox.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; import 'package:cw_core/node.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -15,6 +16,7 @@ class NodeForm extends StatelessWidget { required this.nodeViewModel, required this.formKey, this.editingNode, + this.type, }) : _addressController = TextEditingController(text: editingNode?.uri.host.toString()), _pathController = TextEditingController(text: editingNode?.path.toString()), _portController = TextEditingController(text: editingNode?.uri.port.toString()), @@ -76,6 +78,7 @@ class NodeForm extends StatelessWidget { final NodeCreateOrEditViewModel nodeViewModel; final GlobalKey formKey; final Node? editingNode; + final WalletType? type; final TextEditingController _addressController; final TextEditingController _pathController; @@ -96,7 +99,7 @@ class NodeForm extends StatelessWidget { child: BaseTextFormField( controller: _addressController, hintText: S.of(context).node_address, - validator: NodeAddressValidator(), + validator: type == WalletType.decred ? NodeAddressValidatorDecredBlankException() : NodeAddressValidator(), ), ) ], diff --git a/lib/src/screens/rescan/rescan_page.dart b/lib/src/screens/rescan/rescan_page.dart index 2c1c213c1..63f13c5df 100644 --- a/lib/src/screens/rescan/rescan_page.dart +++ b/lib/src/screens/rescan/rescan_page.dart @@ -9,41 +9,47 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/blockchain_height_widget.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cw_core/wallet_type.dart'; class RescanPage extends BasePage { RescanPage(this._rescanViewModel) : _blockchainHeightWidgetKey = GlobalKey(); @override - String get title => - _rescanViewModel.isSilentPaymentsScan ? S.current.silent_payments_scanning : S.current.rescan; + String get title => _rescanViewModel.isSilentPaymentsScan + ? S.current.silent_payments_scanning + : S.current.rescan; final GlobalKey _blockchainHeightWidgetKey; final RescanViewModel _rescanViewModel; @override Widget body(BuildContext context) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => FocusScope.of(context).unfocus(), - child: Padding( + Widget child; + if (_rescanViewModel.wallet.type != WalletType.decred) { + child = Padding( padding: EdgeInsets.only(left: 24, right: 24, bottom: 24), - child: Column(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + child: + Column(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Observer( builder: (_) => BlockchainHeightWidget( key: _blockchainHeightWidgetKey, - onHeightOrDateEntered: (value) => _rescanViewModel.isButtonEnabled = value, + onHeightOrDateEntered: (value) => + _rescanViewModel.isButtonEnabled = value, isSilentPaymentsScan: _rescanViewModel.isSilentPaymentsScan, isMwebScan: _rescanViewModel.isMwebScan, doSingleScan: _rescanViewModel.doSingleScan, - hasDatePicker: !_rescanViewModel.isMwebScan,// disable date picker for mweb for now - toggleSingleScan: () => - _rescanViewModel.doSingleScan = !_rescanViewModel.doSingleScan, + hasDatePicker: !_rescanViewModel + .isMwebScan, // disable date picker for mweb for now + toggleSingleScan: () => _rescanViewModel.doSingleScan = + !_rescanViewModel.doSingleScan, walletType: _rescanViewModel.wallet.type, - bitcoinMempoolAPIEnabled: _rescanViewModel.isBitcoinMempoolAPIEnabled, + bitcoinMempoolAPIEnabled: + _rescanViewModel.isBitcoinMempoolAPIEnabled, )), Observer( builder: (_) => LoadingPrimaryButton( - isLoading: _rescanViewModel.state == RescanWalletState.rescaning, + isLoading: + _rescanViewModel.state == RescanWalletState.rescaning, text: S.of(context).rescan, onPressed: () async { if (_rescanViewModel.isSilentPaymentsScan) { @@ -51,7 +57,8 @@ class RescanPage extends BasePage { } _rescanViewModel.rescanCurrentWallet( - restoreHeight: _blockchainHeightWidgetKey.currentState!.height); + restoreHeight: + _blockchainHeightWidgetKey.currentState!.height); Navigator.of(context).pop(); }, @@ -60,7 +67,35 @@ class RescanPage extends BasePage { isDisabled: !_rescanViewModel.isButtonEnabled, )) ]), - ), + ); + } else { + child = Center( + child: Padding( + padding: EdgeInsets.only(left: 24, right: 24, bottom: 24), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Spacer(), + Observer( + builder: (_) => LoadingPrimaryButton( + isLoading: _rescanViewModel.state == + RescanWalletState.rescaning, + text: S.of(context).rescan, + onPressed: () async { + await _rescanViewModel.rescanCurrentWallet( + restoreHeight: 0); + Navigator.of(context).pop(); + }, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + )) + ]), + )); + } + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => FocusScope.of(context).unfocus(), + child: child, ); } @@ -70,14 +105,16 @@ class RescanPage extends BasePage { Navigator.of(context).pop(); final needsToSwitch = - await bitcoin!.getNodeIsElectrsSPEnabled(_rescanViewModel.wallet) == false; + await bitcoin!.getNodeIsElectrsSPEnabled(_rescanViewModel.wallet) == + false; if (needsToSwitch) { return showPopUp( context: navigatorKey.currentState!.context, builder: (BuildContext _dialogContext) => AlertWithTwoActions( alertTitle: S.of(_dialogContext).change_current_node_title, - alertContent: S.of(_dialogContext).confirm_silent_payments_switch_node, + alertContent: + S.of(_dialogContext).confirm_silent_payments_switch_node, rightButtonText: S.of(_dialogContext).confirm, leftButtonText: S.of(_dialogContext).cancel, actionRightButton: () async { diff --git a/lib/src/screens/restore/wallet_restore_from_keys_form.dart b/lib/src/screens/restore/wallet_restore_from_keys_form.dart index 3dc312702..b95fcb4c6 100644 --- a/lib/src/screens/restore/wallet_restore_from_keys_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_keys_form.dart @@ -15,6 +15,7 @@ class WalletRestoreFromKeysForm extends StatefulWidget { WalletRestoreFromKeysForm({ required this.walletRestoreViewModel, required this.onPrivateKeyChange, + required this.onViewKeyEntered, required this.displayPrivateKeyField, required this.onHeightOrDateEntered, required this.displayWalletPassword, @@ -27,6 +28,7 @@ class WalletRestoreFromKeysForm extends StatefulWidget { final Function(bool) onHeightOrDateEntered; final WalletRestoreViewModel walletRestoreViewModel; final void Function(String)? onPrivateKeyChange; + final void Function(bool)? onViewKeyEntered; final bool displayPrivateKeyField; final bool displayWalletPassword; final RestoredWallet? restoredWallet; @@ -97,6 +99,10 @@ class WalletRestoreFromKeysFormState extends State { blockchainHeightKey.currentState?.restoreHeightController.text = widget.restoredWallet!.height.toString(); } }); + + viewKeyController.addListener(() { + widget.onViewKeyEntered?.call(viewKeyController.text.isNotEmpty); + }); } @override @@ -187,6 +193,19 @@ class WalletRestoreFromKeysFormState extends State { } Widget _restoreFromKeysFormFields() { + // Decred can only restore a view only wallet with an account pubkey. Other + // fields are not used. + if (widget.walletRestoreViewModel.type == WalletType.decred) { + return Column( + children: [ + BaseTextFormField( + controller: viewKeyController, + hintText: S.of(context).view_key_public, + maxLines: null, + )], + ); + } + if (widget.displayPrivateKeyField) { // the term "private key" isn't actually what we're accepting here, and it's confusing to // users of the nano community, what this form actually accepts (when importing for nano) is a nano seed in it's hex form, referred to in code as a "seed key" diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 31f02617d..ce8595ba4 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -168,14 +168,16 @@ class WalletRestorePage extends BasePage { credentials['name'] = walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text; } else { - credentials['address'] = walletRestoreFromKeysFormKey.currentState!.addressController.text; - credentials['viewKey'] = walletRestoreFromKeysFormKey.currentState!.viewKeyController.text; - credentials['spendKey'] = - walletRestoreFromKeysFormKey.currentState!.spendKeyController.text; - credentials['height'] = - walletRestoreFromKeysFormKey.currentState!.blockchainHeightKey.currentState!.height; credentials['name'] = walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text; + credentials['viewKey'] = walletRestoreFromKeysFormKey.currentState!.viewKeyController.text; + if (walletRestoreViewModel.type != WalletType.decred) { + credentials['address'] = walletRestoreFromKeysFormKey.currentState!.addressController.text; + credentials['spendKey'] = + walletRestoreFromKeysFormKey.currentState!.spendKeyController.text; + credentials['height'] = + walletRestoreFromKeysFormKey.currentState!.blockchainHeightKey.currentState!.height; + } } } @@ -467,6 +469,11 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> widget.walletRestoreViewModel.isButtonEnabled = _isValidSeedKey(); } }, + onViewKeyEntered: (bool entered) { + if (walletRestoreViewModel.type == WalletType.decred) { + walletRestoreViewModel.isButtonEnabled = entered; + } + }, onPasswordChange: (String password) => widget.walletRestoreViewModel.walletPassword = password, onRepeatedPasswordChange: (String repeatedPassword) => @@ -538,13 +545,19 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> // bip39: final validBip39SeedLengths = [12, 18, 24]; - final nonBip39WalletTypes = [WalletType.monero, WalletType.wownero, WalletType.haven]; + final nonBip39WalletTypes = [WalletType.monero, WalletType.wownero, WalletType.haven, WalletType.decred]; // if it's a bip39 wallet and the length is not valid return false if (!nonBip39WalletTypes.contains(walletRestoreViewModel.type) && !(validBip39SeedLengths.contains(seedWords.length))) { return false; } + if ((walletRestoreViewModel.type == WalletType.decred) && + seedWords.length != + WalletRestoreViewModelBase.decredSeedMnemonicLength) { + return false; + } + final words = walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.words.toSet(); return seedWords.toSet().difference(words).toSet().isEmpty; diff --git a/lib/src/screens/wallet_keys/wallet_keys_page.dart b/lib/src/screens/wallet_keys/wallet_keys_page.dart index be1972106..29a1bfb6f 100644 --- a/lib/src/screens/wallet_keys/wallet_keys_page.dart +++ b/lib/src/screens/wallet_keys/wallet_keys_page.dart @@ -72,6 +72,10 @@ class _WalletKeysPageBodyState extends State late bool showLegacySeedTab; late bool isLegacySeedOnly; + bool get _hasSeeds => + widget.walletKeysViewModel.legacySeedSplit.length > 10 || + widget.walletKeysViewModel.seedSplit.length > 10; + @override void initState() { super.initState(); @@ -160,11 +164,10 @@ class _WalletKeysPageBodyState extends State Widget _buildSeedTab(BuildContext context, bool isLegacySeed) { return Column( children: [ - if (isLegacySeedOnly || isLegacySeed) - ...[ - _buildHeightBox(), - const SizedBox(height: 20), - ], + if (isLegacySeedOnly || isLegacySeed) ...[ + _buildHeightBox(), + const SizedBox(height: 20), + ], (_buildPassphraseBox() ?? Container()), if (widget.walletKeysViewModel.passphrase.isNotEmpty) const SizedBox(height: 20), Expanded( @@ -175,13 +178,14 @@ class _WalletKeysPageBodyState extends State ), ), const SizedBox(height: 10), - _buildBottomActionPanel( - titleForClipboard: S.of(context).wallet_seed.toLowerCase(), - dataToCopy: isLegacySeed - ? widget.walletKeysViewModel.legacySeed - : widget.walletKeysViewModel.seed, - onShowQR: () async => _showQR(context), - ), + if (_hasSeeds) + _buildBottomActionPanel( + titleForClipboard: S.of(context).wallet_seed.toLowerCase(), + dataToCopy: isLegacySeed + ? widget.walletKeysViewModel.legacySeed + : widget.walletKeysViewModel.seed, + onShowQR: () async => _showQR(context), + ), ], ); } @@ -326,7 +330,7 @@ class _WalletKeysPageBodyState extends State ), ); } - + Widget _buildBottomActionPanel({ required String titleForClipboard, required String dataToCopy, diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index 569dce958..62f79bdc4 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -122,6 +122,7 @@ class WalletListBodyState extends State { final bitcoinIcon = Image.asset('assets/images/bitcoin.png', height: 24, width: 24); final tBitcoinIcon = Image.asset('assets/images/tbtc.png', height: 24, width: 24); final litecoinIcon = Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24); + final decredIcon = Image.asset('assets/images/decred_icon.png', height: 24, width: 24); final nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24); final havenIcon = Image.asset('assets/images/haven_logo.png', height: 24, width: 24); final ethereumIcon = Image.asset('assets/images/eth_icon.png', height: 24, width: 24); @@ -136,6 +137,8 @@ class WalletListBodyState extends State { final double tileHeight = 60; Flushbar? _progressBar; + bool _loadingWallet = false; + @override Widget build(BuildContext context) { final newWalletImage = Image.asset('assets/images/new_wallet.png', @@ -480,6 +483,10 @@ class WalletListBodyState extends State { } Future _loadWallet(WalletListItem wallet) async { + if (_loadingWallet) return; + + _loadingWallet = true; + if (SettingsStoreBase.walletPasswordDirectInput) { Navigator.of(context).pushNamed(Routes.walletUnlockLoadable, arguments: WalletUnlockArguments( @@ -492,13 +499,17 @@ class WalletListBodyState extends State { }, walletName: wallet.name, walletType: wallet.type)); + _loadingWallet = false; return; } await widget.authService.authenticateAction( context, onAuthSuccess: (isAuthenticatedSuccessfully) async { - if (!isAuthenticatedSuccessfully) return; + if (!isAuthenticatedSuccessfully) { + _loadingWallet = false; + return; + } try { final requireHardwareWalletConnection = widget.walletListViewModel @@ -555,6 +566,8 @@ class WalletListBodyState extends State { .of(context) .wallet_list_failed_to_load(wallet.name, e.toString())); } + } finally { + _loadingWallet = false; } }, conditionToDetermineIfToUse2FA: diff --git a/lib/src/widgets/blockchain_height_widget.dart b/lib/src/widgets/blockchain_height_widget.dart index 650ee684d..605ae4484 100644 --- a/lib/src/widgets/blockchain_height_widget.dart +++ b/lib/src/widgets/blockchain_height_widget.dart @@ -9,6 +9,9 @@ import 'package:intl/intl.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:cake_wallet/decred/decred.dart'; +import 'package:cw_core/wallet_type.dart'; + class BlockchainHeightWidget extends StatefulWidget { BlockchainHeightWidget({ @@ -183,7 +186,9 @@ class BlockchainHeightState extends State { bitcoinMempoolAPIEnabled: await widget.bitcoinMempoolAPIEnabled, ); } else { - if (widget.walletType == WalletType.monero) { + if (widget.walletType == WalletType.decred) { + height = decred!.heightByDate(date); + } else if (widget.walletType == WalletType.monero) { height = monero!.getHeightByDate(date: date); } else { assert(widget.walletType == WalletType.wownero, diff --git a/lib/src/widgets/seed_widget.dart b/lib/src/widgets/seed_widget.dart index 7747c5fb6..9b3726afb 100644 --- a/lib/src/widgets/seed_widget.dart +++ b/lib/src/widgets/seed_widget.dart @@ -65,6 +65,9 @@ class SeedWidgetState extends State { }); widget.onSeedChange?.call(text); }); + Future.delayed(Duration.zero, () { + widget.onSeedChange?.call(text); + }); } void changeSeedLanguage(String language) { diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index ef9ab94a4..d43550806 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/di.dart'; @@ -139,6 +140,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? initialPolygonTransactionPriority, TransactionPriority? initialBitcoinCashTransactionPriority, TransactionPriority? initialZanoTransactionPriority, + TransactionPriority? initialDecredTransactionPriority, Country? initialCakePayCountry}) : nodes = ObservableMap.of(nodes), powNodes = ObservableMap.of(powNodes), @@ -225,6 +227,9 @@ abstract class SettingsStoreBase with Store { if (initialZanoTransactionPriority != null) { priority[WalletType.zano] = initialZanoTransactionPriority; } + if (initialDecredTransactionPriority != null) { + priority[WalletType.decred] = initialDecredTransactionPriority; + } if (initialCakePayCountry != null) { selectedCakePayCountry = initialCakePayCountry; @@ -280,6 +285,9 @@ abstract class SettingsStoreBase with Store { case WalletType.zano: key = PreferencesKey.zanoTransactionPriority; break; + case WalletType.decred: + key = PreferencesKey.decredTransactionPriority; + break; default: key = null; } @@ -898,6 +906,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? bitcoinCashTransactionPriority; TransactionPriority? wowneroTransactionPriority; TransactionPriority? zanoTransactionPriority; + TransactionPriority? decredTransactionPriority; if (sharedPreferences.getInt(PreferencesKey.havenTransactionPriority) != null) { havenTransactionPriority = monero?.deserializeMoneroTransactionPriority( @@ -927,6 +936,10 @@ abstract class SettingsStoreBase with Store { zanoTransactionPriority = monero?.deserializeMoneroTransactionPriority( raw: sharedPreferences.getInt(PreferencesKey.zanoTransactionPriority)!); } + if (sharedPreferences.getInt(PreferencesKey.decredTransactionPriority) != null) { + decredTransactionPriority = decred?.deserializeDecredTransactionPriority( + sharedPreferences.getInt(PreferencesKey.decredTransactionPriority)!); + } moneroTransactionPriority ??= monero?.getDefaultTransactionPriority(); bitcoinTransactionPriority ??= bitcoin?.getMediumTransactionPriority(); @@ -935,6 +948,7 @@ abstract class SettingsStoreBase with Store { ethereumTransactionPriority ??= ethereum?.getDefaultTransactionPriority(); bitcoinCashTransactionPriority ??= bitcoinCash?.getDefaultTransactionPriority(); wowneroTransactionPriority ??= wownero?.getDefaultTransactionPriority(); + decredTransactionPriority ??= decred?.getDecredTransactionPriorityMedium(); polygonTransactionPriority ??= polygon?.getDefaultTransactionPriority(); zanoTransactionPriority ??= zano?.getDefaultTransactionPriority(); @@ -1038,7 +1052,7 @@ abstract class SettingsStoreBase with Store { final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); final wowneroNodeId = sharedPreferences.getInt(PreferencesKey.currentWowneroNodeIdKey); final zanoNodeId = sharedPreferences.getInt(PreferencesKey.currentZanoNodeIdKey); - + final decredNodeId = sharedPreferences.getInt(PreferencesKey.currentDecredNodeIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); @@ -1047,6 +1061,7 @@ abstract class SettingsStoreBase with Store { final polygonNode = nodeSource.get(polygonNodeId); final bitcoinCashElectrumServer = nodeSource.get(bitcoinCashElectrumServerId); final nanoNode = nodeSource.get(nanoNodeId); + final decredNode = nodeSource.get(decredNodeId); final nanoPowNode = powNodeSource.get(nanoPowNodeId); final solanaNode = nodeSource.get(solanaNodeId); final tronNode = nodeSource.get(tronNodeId); @@ -1137,6 +1152,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.zano] = zanoNode; } + if (decredNode != null) { + nodes[WalletType.decred] = decredNode; + } + final savedSyncMode = SyncMode.all.firstWhere((element) { return element.type.index == (sharedPreferences.getInt(PreferencesKey.syncModeKey) ?? 0); }); @@ -1304,6 +1323,7 @@ abstract class SettingsStoreBase with Store { initialHavenTransactionPriority: havenTransactionPriority, initialLitecoinTransactionPriority: litecoinTransactionPriority, initialBitcoinCashTransactionPriority: bitcoinCashTransactionPriority, + initialDecredTransactionPriority: decredTransactionPriority, initialShouldRequireTOTP2FAForAccessingWallet: shouldRequireTOTP2FAForAccessingWallet, initialShouldRequireTOTP2FAForSendsToContact: shouldRequireTOTP2FAForSendsToContact, initialShouldRequireTOTP2FAForSendsToNonContact: shouldRequireTOTP2FAForSendsToNonContact, @@ -1378,6 +1398,11 @@ abstract class SettingsStoreBase with Store { priority[WalletType.zano] = zano!.deserializeMoneroTransactionPriority( raw: sharedPreferences.getInt(PreferencesKey.zanoTransactionPriority)!); } + if (decred != null && + sharedPreferences.getInt(PreferencesKey.decredTransactionPriority) != null) { + priority[WalletType.decred] = decred!.deserializeDecredTransactionPriority( + sharedPreferences.getInt(PreferencesKey.decredTransactionPriority)!); + } final generateSubaddresses = sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey); @@ -1489,6 +1514,7 @@ abstract class SettingsStoreBase with Store { final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); final wowneroNodeId = sharedPreferences.getInt(PreferencesKey.currentWowneroNodeIdKey); final zanoNodeId = sharedPreferences.getInt(PreferencesKey.currentZanoNodeIdKey); + final decredNodeId = sharedPreferences.getInt(PreferencesKey.currentDecredNodeIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); @@ -1496,11 +1522,12 @@ abstract class SettingsStoreBase with Store { final ethereumNode = nodeSource.get(ethereumNodeId); final polygonNode = nodeSource.get(polygonNodeId); final bitcoinCashNode = nodeSource.get(bitcoinCashElectrumServerId); - final nanoNode = nodeSource.get(nanoNodeId); + final nanoNode = nodeSource.get(nanoNodeId); final solanaNode = nodeSource.get(solanaNodeId); final tronNode = nodeSource.get(tronNodeId); final wowneroNode = nodeSource.get(wowneroNodeId); final zanoNode = nodeSource.get(zanoNodeId); + final decredNode = nodeSource.get(decredNodeId); if (moneroNode != null) { nodes[WalletType.monero] = moneroNode; @@ -1551,6 +1578,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.zano] = zanoNode; } + if (decredNode != null) { + nodes[WalletType.decred] = decredNode; + } + // MIGRATED: useTOTP2FA = await SecureKey.getBool( @@ -1687,6 +1718,9 @@ abstract class SettingsStoreBase with Store { case WalletType.wownero: await _sharedPreferences.setInt(PreferencesKey.currentWowneroNodeIdKey, node.key as int); break; + case WalletType.decred: + await _sharedPreferences.setInt(PreferencesKey.currentDecredNodeIdKey, node.key as int); + break; case WalletType.zano: await _sharedPreferences.setInt(PreferencesKey.currentZanoNodeIdKey, node.key as int); default: diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index 3959afe01..803744590 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -55,6 +55,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { case WalletType.none: case WalletType.haven: case WalletType.zano: + case WalletType.decred: return false; } } diff --git a/lib/view_model/contact_list/contact_list_view_model.dart b/lib/view_model/contact_list/contact_list_view_model.dart index eb3fb837e..730d0735c 100644 --- a/lib/view_model/contact_list/contact_list_view_model.dart +++ b/lib/view_model/contact_list/contact_list_view_model.dart @@ -9,7 +9,6 @@ import 'package:cake_wallet/entities/wallet_list_order_types.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/mobx.dart'; -import 'package:collection/collection.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -43,7 +42,8 @@ abstract class ContactListViewModelBase with Store { } } } else if (info.addresses?.isNotEmpty == true && info.addresses!.length > 1) { - if ([WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type)) { + if ([WalletType.monero, WalletType.wownero, WalletType.haven, WalletType.decred] + .contains(info.type)) { final address = info.address; final name = _createName(info.name, "", key: 0); walletContacts.add(WalletContact( @@ -129,11 +129,9 @@ abstract class ContactListViewModelBase with Store { (element.type == CryptoCurrency.btc || element.type == CryptoCurrency.ltc)) return false; return element.type == _currency || - (element.type.tag != null && - _currency?.tag != null && - element.type.tag == _currency?.tag) || - _currency?.toString() == element.type.tag || - _currency?.tag == element.type.toString(); + (element.type.tag != null && _currency.tag != null && element.type.tag == _currency.tag) || + _currency.toString() == element.type.tag || + _currency.tag == element.type.toString(); } void dispose() => _subscription?.cancel(); diff --git a/lib/view_model/contact_list/contact_view_model.dart b/lib/view_model/contact_list/contact_view_model.dart index 93abfb11c..efa1f09f4 100644 --- a/lib/view_model/contact_list/contact_view_model.dart +++ b/lib/view_model/contact_list/contact_view_model.dart @@ -62,12 +62,12 @@ abstract class ContactViewModelBase with Store { return; } - if (_contact != null && _contact!.original.isInBox) { - _contact?.name = name; - _contact?.address = address; - _contact?.type = currency!; - _contact?.lastChange = now; - await _contact?.save(); + if (_contact != null && _contact.original.isInBox) { + _contact.name = name; + _contact.address = address; + _contact.type = currency!; + _contact.lastChange = now; + await _contact.save(); } else { await _contacts .add(Contact(name: name, address: address, type: currency!, lastChange: now)); diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 730f07a93..ef5676138 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -306,6 +306,7 @@ abstract class BalanceViewModelBase with Store { case WalletType.monero: case WalletType.wownero: case WalletType.zano: + case WalletType.decred: return true; default: return false; diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index ee9c57b65..49c0dbc59 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -23,7 +23,6 @@ import 'package:cake_wallet/store/dashboard/trades_store.dart'; import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; -import 'package:cake_wallet/utils/mobx.dart'; import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; @@ -169,6 +168,9 @@ abstract class DashboardViewModelBase with Store { type = appStore.wallet!.type, transactions = ObservableList(), wallet = appStore.wallet! { + showDecredInfoCard = wallet.type == WalletType.decred && + (sharedPreferences.getBool(PreferencesKey.showDecredInfoCard) ?? true); + name = wallet.name; type = wallet.type; isShowFirstYatIntroduction = false; @@ -261,6 +263,7 @@ abstract class DashboardViewModelBase with Store { reaction((_) => appStore.wallet, (wallet) { _onWalletChange(wallet); _checkMweb(); + showDecredInfoCard = wallet?.type == WalletType.decred; }); _transactionDisposer?.reaction.dispose(); @@ -350,6 +353,10 @@ abstract class DashboardViewModelBase with Store { statusText = S.current.please_try_to_connect_to_another_node; } + if (status is ProcessingSyncStatus) { + statusText = (status as ProcessingSyncStatus).message ?? S.current.processing; + } + return statusText; } @@ -399,13 +406,7 @@ abstract class DashboardViewModelBase with Store { bool get isTestnet => wallet.type == WalletType.bitcoin && bitcoin!.isTestnet(wallet); @computed - bool get hasRescan => [ - WalletType.bitcoin, - WalletType.monero, - WalletType.litecoin, - WalletType.wownero, - WalletType.haven - ].contains(wallet.type); + bool get hasRescan => wallet.hasRescan; @computed bool get isMoneroViewOnly { @@ -490,6 +491,9 @@ abstract class DashboardViewModelBase with Store { @observable bool mwebEnabled = false; + @observable + late bool showDecredInfoCard; + @computed bool get hasEnabledMwebBefore => settingsStore.hasEnabledMwebBefore; @@ -543,6 +547,12 @@ abstract class DashboardViewModelBase with Store { bitcoin!.setMwebEnabled(wallet, false); } + @action + void dismissDecredInfoCard() { + showDecredInfoCard = false; + sharedPreferences.setBool(PreferencesKey.showDecredInfoCard, false); + } + BalanceViewModel balanceViewModel; AppStore appStore; @@ -573,6 +583,9 @@ abstract class DashboardViewModelBase with Store { @computed bool get isEnabledSwapAction => settingsStore.exchangeStatus != ExchangeApiMode.disabled; + @computed + bool get canSend => wallet.canSend(); + @observable bool hasSwapAction; @@ -596,21 +609,28 @@ abstract class DashboardViewModelBase with Store { @computed bool get hasSignMessages { - if (wallet.isHardwareWallet) return false; - - return [ - WalletType.monero, - WalletType.litecoin, - WalletType.bitcoin, - WalletType.bitcoinCash, - WalletType.ethereum, - WalletType.polygon, - WalletType.solana, - WalletType.nano, - WalletType.banano, - WalletType.tron, - WalletType.wownero - ].contains(wallet.type); + if (wallet.isHardwareWallet) { + return false; + } + switch (wallet.type) { + case WalletType.monero: + case WalletType.litecoin: + case WalletType.bitcoin: + case WalletType.bitcoinCash: + case WalletType.ethereum: + case WalletType.polygon: + case WalletType.solana: + case WalletType.nano: + case WalletType.banano: + case WalletType.tron: + case WalletType.wownero: + case WalletType.decred: + return true; + case WalletType.zano: + case WalletType.haven: + case WalletType.none: + return false; + } } bool get showRepWarning { diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index e3e02a045..4a85ec89a 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -214,6 +214,7 @@ abstract class HomeSettingsViewModelBase with Store { case WalletType.nano: case WalletType.wownero: case WalletType.bitcoinCash: + case WalletType.decred: return false; } diff --git a/lib/view_model/dashboard/receive_option_view_model.dart b/lib/view_model/dashboard/receive_option_view_model.dart index 744e4c58d..f15d7dad6 100644 --- a/lib/view_model/dashboard/receive_option_view_model.dart +++ b/lib/view_model/dashboard/receive_option_view_model.dart @@ -14,7 +14,9 @@ abstract class ReceiveOptionViewModelBase with Store { (_wallet.type == WalletType.bitcoin || _wallet.type == WalletType.litecoin ? bitcoin!.getSelectedAddressType(_wallet) - : ReceivePageOption.mainnet), + : (_wallet.type == WalletType.decred && _wallet.isTestnet) + ? ReceivePageOption.testnet + : ReceivePageOption.mainnet), _options = [] { final walletType = _wallet.type; switch (walletType) { @@ -33,6 +35,17 @@ abstract class ReceiveOptionViewModelBase with Store { case WalletType.haven: _options = [ReceivePageOption.mainnet]; break; + case WalletType.decred: + if (_wallet.isTestnet) { + _options = [ + ReceivePageOption.testnet, + ...ReceivePageOptions.where( + (element) => element != ReceivePageOption.mainnet) + ]; + } else { + _options = ReceivePageOptions; + } + break; default: _options = ReceivePageOptions; } diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 864448293..83953f9f1 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; @@ -82,7 +83,9 @@ class TransactionListItem extends ActionListItem with Keyable { if (transaction.confirmations <= 0) { str = S.current.pending; } - if ((isPegOut || fromPegOut) && transaction.confirmations >= 0 && transaction.confirmations < 6) { + if ((isPegOut || fromPegOut) && + transaction.confirmations >= 0 && + transaction.confirmations < 6) { str = " (${transaction.confirmations}/6)"; } if (isPegIn) { @@ -224,7 +227,13 @@ class TransactionListItem extends ActionListItem with Keyable { cryptoAmount: zano!.formatterIntAmountToDouble(amount: transaction.amount, currency: asset, forFee: false), price: price); break; - default: + case WalletType.decred: + amount = calculateFiatAmountRaw( + cryptoAmount: decred!.formatterDecredAmountToDouble(amount: transaction.amount), + price: price); + break; + case WalletType.none: + case WalletType.banano: break; } diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 6ea9c811f..8b7349e9a 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -23,6 +23,8 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; @@ -167,7 +169,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with wallet.type == WalletType.bitcoinCash; bool get hideAddressAfterExchange => - wallet.type == WalletType.monero || wallet.type == WalletType.wownero; + wallet.type == WalletType.monero || + wallet.type == WalletType.wownero; bool _useTorOnly; final Box trades; @@ -316,8 +319,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with bool get isMoneroWallet => wallet.type == WalletType.monero; - - List receiveCurrencies; List depositCurrencies; @@ -778,6 +779,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with depositCurrency = CryptoCurrency.zano; receiveCurrency = CryptoCurrency.xmr; break; + case WalletType.decred: + depositCurrency = CryptoCurrency.dcr; + receiveCurrency = CryptoCurrency.xmr; + break; case WalletType.none: break; } diff --git a/lib/view_model/node_list/node_create_or_edit_view_model.dart b/lib/view_model/node_list/node_create_or_edit_view_model.dart index 71f996aff..8fbe174e3 100644 --- a/lib/view_model/node_list/node_create_or_edit_view_model.dart +++ b/lib/view_model/node_list/node_create_or_edit_view_model.dart @@ -63,13 +63,13 @@ abstract class NodeCreateOrEditViewModelBase with Store { String socksProxyAddress; @computed - bool get isReady => address.isNotEmpty && port.isNotEmpty; + bool get isReady => + (address.isNotEmpty && port.isNotEmpty) || + _walletType == WalletType.decred; // Allow an empty address. bool get hasAuthCredentials => _walletType == WalletType.monero || _walletType == WalletType.wownero || _walletType == WalletType.haven; - bool get hasTestnetSupport => _walletType == WalletType.bitcoin; - bool get hasPathSupport { switch (_walletType) { case WalletType.ethereum: @@ -87,6 +87,7 @@ abstract class NodeCreateOrEditViewModelBase with Store { case WalletType.bitcoinCash: case WalletType.bitcoin: case WalletType.zano: + case WalletType.decred: return false; } } diff --git a/lib/view_model/node_list/node_list_view_model.dart b/lib/view_model/node_list/node_list_view_model.dart index 71e77eb12..9df5f2980 100644 --- a/lib/view_model/node_list/node_list_view_model.dart +++ b/lib/view_model/node_list/node_list_view_model.dart @@ -49,50 +49,10 @@ abstract class NodeListViewModelBase with Store { await resetToDefault(_nodeSource); Node node; - - switch (_appStore.wallet!.type) { - case WalletType.bitcoin: - if (_appStore.wallet!.isTestnet) { - node = getBitcoinTestnetDefaultElectrumServer(nodes: _nodeSource)!; - } else { - node = getBitcoinDefaultElectrumServer(nodes: _nodeSource)!; - } - break; - case WalletType.monero: - node = getMoneroDefaultNode(nodes: _nodeSource); - break; - case WalletType.litecoin: - node = getLitecoinDefaultElectrumServer(nodes: _nodeSource)!; - break; - case WalletType.haven: - node = getHavenDefaultNode(nodes: _nodeSource)!; - break; - case WalletType.ethereum: - node = getEthereumDefaultNode(nodes: _nodeSource)!; - break; - case WalletType.bitcoinCash: - node = getBitcoinCashDefaultElectrumServer(nodes: _nodeSource)!; - break; - case WalletType.nano: - node = getNanoDefaultNode(nodes: _nodeSource)!; - break; - case WalletType.polygon: - node = getPolygonDefaultNode(nodes: _nodeSource)!; - break; - case WalletType.solana: - node = getSolanaDefaultNode(nodes: _nodeSource)!; - break; - case WalletType.tron: - node = getTronDefaultNode(nodes: _nodeSource)!; - break; - case WalletType.wownero: - node = getWowneroDefaultNode(nodes: _nodeSource); - break; - case WalletType.zano: - node = getZanoDefaultNode(nodes: _nodeSource)!; - break; - default: - throw Exception('Unexpected wallet type: ${_appStore.wallet!.type}'); + if (_appStore.wallet!.type == WalletType.bitcoin && _appStore.wallet!.isTestnet) { + node = getBitcoinTestnetDefaultElectrumServer(nodes: _nodeSource)!; + } else { + node = getDefaultNode(nodes: _nodeSource, type: _appStore.wallet!.type)!; } await setAsCurrent(node); diff --git a/lib/view_model/restore/wallet_restore_from_qr_code.dart b/lib/view_model/restore/wallet_restore_from_qr_code.dart index 2e4e2da83..c8ff81acc 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -44,6 +44,9 @@ class WalletRestoreFromQRCode { 'zano': WalletType.zano, 'zano-wallet': WalletType.zano, 'zano_wallet': WalletType.zano, + 'decred': WalletType.decred, + 'decred-wallet': WalletType.decred, + 'decred_wallet': WalletType.decred, }; static WalletType? _extractWalletType(String code) { @@ -69,8 +72,12 @@ class WalletRestoreFromQRCode { } static String? _extractAddressFromUrl(String rawString, WalletType type) { - return AddressResolver.extractAddressByType( - raw: rawString, type: walletTypeToCryptoCurrency(type)); + try { + return AddressResolver.extractAddressByType( + raw: rawString, type: walletTypeToCryptoCurrency(type)); + } catch (_) { + return null; + } } static String? _extractSeedPhraseFromUrl(String rawString, WalletType walletType) { @@ -122,7 +129,6 @@ class WalletRestoreFromQRCode { } if (queryParameters['address'] == null) { queryParameters['address'] = _extractAddressFromUrl(code, walletType); - } Map credentials = {'type': walletType, ...queryParameters, 'raw_qr': code}; diff --git a/lib/view_model/send/fees_view_model.dart b/lib/view_model/send/fees_view_model.dart index 088c66039..f6dd0f201 100644 --- a/lib/view_model/send/fees_view_model.dart +++ b/lib/view_model/send/fees_view_model.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; @@ -88,7 +89,13 @@ abstract class FeesViewModelBase extends WalletChangeListenerViewModel with Stor return transactionPriority == bitcoinCash!.getBitcoinCashTransactionPrioritySlow(); case WalletType.polygon: return transactionPriority == polygon!.getPolygonTransactionPrioritySlow(); - default: + case WalletType.decred: + return transactionPriority == decred!.getDecredTransactionPrioritySlow(); + case WalletType.none: + case WalletType.nano: + case WalletType.banano: + case WalletType.solana: + case WalletType.tron: return false; } } diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 121ffa693..fdaaeba0d 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; @@ -101,6 +102,9 @@ abstract class OutputBase with Store { case WalletType.bitcoinCash: _amount = bitcoin!.formatterStringDoubleToBitcoinAmount(_cryptoAmount); break; + case WalletType.decred: + _amount = decred!.formatterStringDoubleToDecredAmount(_cryptoAmount); + break; case WalletType.haven: _amount = haven!.formatterMoneroParseAmount(amount: _cryptoAmount); break; @@ -188,6 +192,10 @@ abstract class OutputBase with Store { if (_wallet.type == WalletType.zano) { return zano!.formatterIntAmountToDouble(amount: fee, currency: cryptoCurrencyHandler(), forFee: true); } + + if (_wallet.type == WalletType.decred) { + return decred!.formatterDecredAmountToDouble(amount: fee); + } } catch (e) { printV(e.toString()); } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 1cd6b8966..3bf85ef2f 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; import 'package:cake_wallet/nano/nano.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/wallet_contact.dart'; @@ -251,6 +252,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor wallet.type == WalletType.litecoin || wallet.type == WalletType.monero || wallet.type == WalletType.wownero || + wallet.type == WalletType.decred || wallet.type == WalletType.bitcoinCash; @computed @@ -539,6 +541,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor case WalletType.zano: return zano!.createZanoTransactionCredentials( outputs: outputs, priority: priority!, currency: selectedCryptoCurrency); + case WalletType.decred: + this.coinTypeToSpendFrom = UnspentCoinType.any; + return decred!.createDecredTransactionCredentials(outputs, priority!); default: throw Exception('Unexpected wallet type: ${wallet.type}'); } @@ -681,55 +686,51 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor } } - if (walletType == WalletType.bitcoin || - walletType == WalletType.litecoin || - walletType == WalletType.bitcoinCash) { - if (error is TransactionWrongBalanceException) { - if (error.amount != null) - return S.current - .tx_wrong_balance_with_amount_exception(currency.toString(), error.amount.toString()); + if (error is TransactionWrongBalanceException) { + if (error.amount != null) + return S.current + .tx_wrong_balance_with_amount_exception(currency.toString(), error.amount.toString()); - return S.current.tx_wrong_balance_exception(currency.toString()); - } - if (error is TransactionNoInputsException) { - return S.current.tx_not_enough_inputs_exception; - } - if (error is TransactionNoFeeException) { - return S.current.tx_zero_fee_exception; - } - if (error is TransactionNoDustException) { - return S.current.tx_no_dust_exception; - } - if (error is TransactionCommitFailed) { - if (error.errorMessage != null && error.errorMessage!.contains("no peers replied")) { - return S.current.tx_commit_failed_no_peers; - } - return "${S.current.tx_commit_failed}${error.errorMessage != null ? "\n\n${error.errorMessage}" : ""}"; - } - if (error is TransactionCommitFailedDustChange) { - return S.current.tx_rejected_dust_change; - } - if (error is TransactionCommitFailedDustOutput) { - return S.current.tx_rejected_dust_output; - } - if (error is TransactionCommitFailedDustOutputSendAll) { - return S.current.tx_rejected_dust_output_send_all; - } - if (error is TransactionCommitFailedVoutNegative) { - return S.current.tx_rejected_vout_negative; - } - if (error is TransactionCommitFailedBIP68Final) { - return S.current.tx_rejected_bip68_final; - } - if (error is TransactionCommitFailedLessThanMin) { - return S.current.fee_less_than_min; - } - if (error is TransactionNoDustOnChangeException) { - return S.current.tx_commit_exception_no_dust_on_change(error.min, error.max); - } - if (error is TransactionInputNotSupported) { - return S.current.tx_invalid_input; + return S.current.tx_wrong_balance_exception(currency.toString()); + } + if (error is TransactionNoInputsException) { + return S.current.tx_not_enough_inputs_exception; + } + if (error is TransactionNoFeeException) { + return S.current.tx_zero_fee_exception; + } + if (error is TransactionNoDustException) { + return S.current.tx_no_dust_exception; + } + if (error is TransactionCommitFailed) { + if (error.errorMessage != null && error.errorMessage!.contains("no peers replied")) { + return S.current.tx_commit_failed_no_peers; } + return "${S.current.tx_commit_failed}${error.errorMessage != null ? "\n\n${error.errorMessage}" : ""}"; + } + if (error is TransactionCommitFailedDustChange) { + return S.current.tx_rejected_dust_change; + } + if (error is TransactionCommitFailedDustOutput) { + return S.current.tx_rejected_dust_output; + } + if (error is TransactionCommitFailedDustOutputSendAll) { + return S.current.tx_rejected_dust_output_send_all; + } + if (error is TransactionCommitFailedVoutNegative) { + return S.current.tx_rejected_vout_negative; + } + if (error is TransactionCommitFailedBIP68Final) { + return S.current.tx_rejected_bip68_final; + } + if (error is TransactionCommitFailedLessThanMin) { + return S.current.fee_less_than_min; + } + if (error is TransactionNoDustOnChangeException) { + return S.current.tx_commit_exception_no_dust_on_change(error.min, error.max); + } + if (error is TransactionInputNotSupported) { + return S.current.tx_invalid_input; } return errorMessage; diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index e2c977590..62b656dca 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -44,7 +44,8 @@ abstract class PrivacySettingsViewModelBase with Store { _wallet.type == WalletType.wownero || _wallet.type == WalletType.bitcoin || _wallet.type == WalletType.litecoin || - _wallet.type == WalletType.bitcoinCash; + _wallet.type == WalletType.bitcoinCash || + _wallet.type == WalletType.decred; bool get isMoneroWallet => _wallet.type == WalletType.monero; diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 2dc6478f9..067ca73f9 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -48,6 +48,7 @@ abstract class TransactionDetailsViewModelBase with Store { final dateFormat = DateFormatter.withCurrentLocal(); final tx = transactionInfo; + // TODO: can be cleaned further switch (wallet.type) { case WalletType.monero: _addMoneroListItems(tx, dateFormat); @@ -84,8 +85,12 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.zano: _addZanoListItems(tx, dateFormat); break; - default: + case WalletType.decred: + _addDecredListItems(tx, dateFormat); break; + case WalletType.none: + case WalletType.banano: + break; } final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}'; @@ -186,6 +191,8 @@ abstract class TransactionDetailsViewModelBase with Store { return 'https://explore.wownero.com/tx/${txId}'; case WalletType.zano: return 'https://explorer.zano.org/transaction/${txId}'; + case WalletType.decred: + return 'https://${wallet.isTestnet ? "testnet" : "dcrdata"}.decred.org/tx/${txId.split(':')[0]}'; case WalletType.none: return ''; } @@ -218,6 +225,8 @@ abstract class TransactionDetailsViewModelBase with Store { return S.current.view_transaction_on + 'Wownero.com'; case WalletType.zano: return S.current.view_transaction_on + 'explorer.zano.org'; + case WalletType.decred: + return S.current.view_transaction_on + 'dcrdata.decred.org'; case WalletType.none: return ''; } @@ -672,6 +681,51 @@ abstract class TransactionDetailsViewModelBase with Store { items.addAll(_items); } + void _addDecredListItems(TransactionInfo tx, DateFormat dateFormat) { + final _items = [ + StandartListItem( + title: S.current.transaction_details_transaction_id, + value: tx.txHash, + key: ValueKey('standard_list_item_transaction_details_id_key'), + ), + StandartListItem( + title: S.current.transaction_details_date, + value: dateFormat.format(tx.date), + key: ValueKey('standard_list_item_transaction_details_date_key'), + ), + StandartListItem( + title: S.current.transaction_details_height, + value: '${tx.height}', + key: ValueKey('standard_list_item_transaction_details_height_key'), + ), + StandartListItem( + title: S.current.transaction_details_amount, + value: tx.amountFormatted(), + key: ValueKey('standard_list_item_transaction_details_amount_key'), + ), + if (tx.feeFormatted()?.isNotEmpty ?? false) + StandartListItem( + title: S.current.transaction_details_fee, + value: tx.feeFormatted()!, + key: ValueKey('standard_list_item_transaction_details_fee_key'), + ), + if (showRecipientAddress && tx.to != null) + StandartListItem( + title: S.current.transaction_details_recipient_address, + value: tx.to!, + key: ValueKey('standard_list_item_transaction_details_recipient_address_key'), + ), + if (tx.from != null) + StandartListItem( + title: S.current.transaction_details_source_address, + value: tx.from!, + key: ValueKey('standard_list_item_transaction_details_source_address_key'), + ), + ]; + + items.addAll(_items); + } + @action Future _checkForRBF(TransactionInfo tx) async { if (wallet.type == WalletType.bitcoin && diff --git a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart index 9a8a4a8f2..d4fadb2f1 100644 --- a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart +++ b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/unspent_coin_type.dart'; @@ -92,6 +93,8 @@ abstract class UnspentCoinsListViewModelBase with Store { return wownero!.formatterWowneroAmountToString(amount: fullBalance); if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type)) return bitcoin!.formatterBitcoinAmountToString(amount: fullBalance); + if (wallet.type == WalletType.decred) + return decred!.formatterDecredAmountToString(amount: fullBalance); return ''; } @@ -105,7 +108,9 @@ abstract class UnspentCoinsListViewModelBase with Store { if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type)) { await bitcoin!.updateUnspents(wallet); } - + if (wallet.type == WalletType.decred) { + decred!.updateUnspents(wallet); + } _updateUnspentCoinsInfo(); } @@ -119,6 +124,8 @@ abstract class UnspentCoinsListViewModelBase with Store { case WalletType.litecoin: case WalletType.bitcoinCash: return bitcoin!.getUnspents(wallet, coinTypeToSpendFrom: coinTypeToSpendFrom); + case WalletType.decred: + return decred!.getUnspents(wallet); default: return List.empty(); } diff --git a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart index d365c8e00..3a678ff0f 100644 --- a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart @@ -5,6 +5,7 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/haven/haven.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cw_core/wallet_type.dart'; part 'wallet_address_edit_or_create_view_model.g.dart'; @@ -27,13 +28,12 @@ class AddressEditOrCreateStateFailure extends AddressEditOrCreateState { } abstract class WalletAddressEditOrCreateViewModelBase with Store { - WalletAddressEditOrCreateViewModelBase( - {required WalletBase wallet, WalletAddressListItem? item}) + WalletAddressEditOrCreateViewModelBase({required WalletBase wallet, WalletAddressListItem? item}) : isEdit = item != null, state = AddressEditOrCreateStateInitial(), label = item?.name ?? '', _item = item, - _wallet = wallet; + _wallet = wallet; @observable AddressEditOrCreateState state; @@ -46,7 +46,8 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { final WalletAddressListItem? _item; final WalletBase _wallet; - bool get isElectrum => _wallet.type == WalletType.bitcoin || + bool get isElectrum => + _wallet.type == WalletType.bitcoin || _wallet.type == WalletType.bitcoinCash || _wallet.type == WalletType.litecoin; @@ -69,39 +70,46 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { Future _createNew() async { final wallet = _wallet; - if (isElectrum) await bitcoin!.generateNewAddress(wallet, label); + if (isElectrum) { + await bitcoin!.generateNewAddress(wallet, label); + await wallet.save(); + } + + if (wallet.type == WalletType.decred) { + await decred!.generateNewAddress(wallet, label); + await wallet.save(); + } if (wallet.type == WalletType.monero) { - await monero - !.getSubaddressList(wallet) - .addSubaddress( - wallet, - accountIndex: monero!.getCurrentAccount(wallet).id, - label: label); - final addr = await monero!.getSubaddressList(wallet).subaddresses.first.address; // first because the order is reversed + await monero! + .getSubaddressList(wallet) + .addSubaddress(wallet, accountIndex: monero!.getCurrentAccount(wallet).id, label: label); + final addr = await monero! + .getSubaddressList(wallet) + .subaddresses + .first + .address; // first because the order is reversed wallet.walletAddresses.manualAddresses.add(addr); await wallet.save(); } if (wallet.type == WalletType.wownero) { - await wownero - !.getSubaddressList(wallet) - .addSubaddress( - wallet, - accountIndex: wownero!.getCurrentAccount(wallet).id, - label: label); - final addr = await wownero!.getSubaddressList(wallet).subaddresses.first.address; // first because the order is reversed + await wownero! + .getSubaddressList(wallet) + .addSubaddress(wallet, accountIndex: wownero!.getCurrentAccount(wallet).id, label: label); + final addr = await wownero! + .getSubaddressList(wallet) + .subaddresses + .first + .address; // first because the order is reversed wallet.walletAddresses.manualAddresses.add(addr); await wallet.save(); } if (wallet.type == WalletType.haven) { - await haven - !.getSubaddressList(wallet) - .addSubaddress( - wallet, - accountIndex: haven!.getCurrentAccount(wallet).id, - label: label); + await haven! + .getSubaddressList(wallet) + .addSubaddress(wallet, accountIndex: haven!.getCurrentAccount(wallet).id, label: label); await wallet.save(); } } @@ -111,6 +119,12 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { if (isElectrum) await bitcoin!.updateAddress(wallet, _item!.address, label); + if (wallet.type == WalletType.decred) { + await decred!.updateAddress(wallet, _item!.address, label); + await wallet.save(); + return; + } + final index = _item?.id; if (index != null) { if (wallet.type == WalletType.monero) { @@ -125,9 +139,7 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { } if (wallet.type == WalletType.haven) { await haven!.getSubaddressList(wallet).setLabelSubaddress(wallet, - accountIndex: haven!.getCurrentAccount(wallet).id, - addressIndex: index, - label: label); + accountIndex: haven!.getCurrentAccount(wallet).id, addressIndex: index, label: label); await wallet.save(); } } diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index ef1c52f31..0b0873a60 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -12,6 +12,7 @@ import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -222,6 +223,22 @@ class ZanoURI extends PaymentURI { } } +class DecredURI extends PaymentURI { + DecredURI({required String amount, required String address}) + : super(amount: amount, address: address); + + @override + String toString() { + var base = 'decred:' + address; + + if (amount.isNotEmpty) { + base += '?amount=${amount.replaceAll(',', '.')}'; + } + + return base; + } +} + abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store { WalletAddressListViewModelBase({ required AppStore appStore, @@ -313,7 +330,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo case WalletType.wownero: return WowneroURI(amount: amount, address: address.address); case WalletType.zano: - return ZanoURI(amount: amount, address: address.address); + return ZanoURI(amount: amount, address: address.address); + case WalletType.decred: + return DecredURI(amount: amount, address: address.address); case WalletType.none: throw Exception('Unexpected type: ${type.toString()}'); } @@ -468,6 +487,14 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } + if (wallet.type == WalletType.decred) { + final addrInfos = decred!.getAddressInfos(wallet); + addrInfos.forEach((info) { + addressList.add(new WalletAddressListItem(isPrimary: false, address: info.address, + name: info.label)); + }); + } + for (var i = 0; i < addressList.length; i++) { if (!(addressList[i] is WalletAddressListItem)) continue; (addressList[i] as WalletAddressListItem).isHidden = wallet.walletAddresses.hiddenAddresses @@ -561,7 +588,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo WalletType.haven, WalletType.bitcoinCash, WalletType.bitcoin, - WalletType.litecoin + WalletType.litecoin, + WalletType.decred ].contains(wallet.type); @computed diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index 402764c40..467b7c3a5 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -12,6 +12,7 @@ import 'package:cw_core/wallet_type.dart'; import 'package:cw_monero/monero_wallet.dart'; import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:polyseed/polyseed.dart'; part 'wallet_keys_view_model.g.dart'; @@ -155,6 +156,12 @@ abstract class WalletKeysViewModelBase with Store { ), ]); break; + case WalletType.decred: + final pubkey = decred!.pubkey(_appStore.wallet!); + items.addAll([ + StandartListItem(title: S.current.view_key_public, value: pubkey), + ]); + break; case WalletType.bitcoin: case WalletType.litecoin: case WalletType.bitcoinCash: @@ -248,6 +255,8 @@ abstract class WalletKeysViewModelBase with Store { return 'wownero-wallet'; case WalletType.zano: return 'zano-wallet'; + case WalletType.decred: + return 'decred-wallet'; default: throw Exception('Unexpected wallet type: ${_wallet.type.toString()}'); } diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index 0cd730028..e82ae8773 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -21,6 +21,7 @@ import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; @@ -83,6 +84,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { return 25; case WalletType.zano: return 26; + case WalletType.decred: + return 15; } } @@ -170,6 +173,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { password: walletPassword, passphrase: passphrase, ); + case WalletType.decred: + return decred!.createDecredNewWalletCredentials(name: name); case WalletType.none: 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 e7df1c221..3e005e9bc 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -11,6 +11,7 @@ import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/tron/tron.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/view_model/restore/restore_mode.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; @@ -58,6 +59,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { case WalletType.haven: case WalletType.ethereum: case WalletType.polygon: + case WalletType.decred: availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys]; break; case WalletType.bitcoin: @@ -77,6 +79,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { } static const moneroSeedMnemonicLength = 25; + static const decredSeedMnemonicLength = 15; late List availableModes; final bool hasSeedLanguageSelector; @@ -171,11 +174,18 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { ); case WalletType.zano: return zano!.createZanoRestoreWalletFromSeedCredentials( - name: name, - password: password, - height: height, + name: name, + password: password, + height: height, passphrase: passphrase??'', - mnemonic: seed); + mnemonic: seed, + ); + case WalletType.decred: + return decred!.createDecredRestoreWalletFromSeedCredentials( + name: name, + mnemonic: seed, + password: password, + ); case WalletType.none: break; } @@ -251,6 +261,12 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { password: password, language: 'English', ); + case WalletType.decred: + return decred!.createDecredRestoreWalletFromPubkeyCredentials( + name: name, + password: password, + pubkey: viewKey!, + ); default: break; } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 42b9fa84c..528de8c42 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import connectivity_plus +import cw_decred import cw_mweb import device_info_plus import devicelocale @@ -24,6 +25,7 @@ import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) + CwDecredPlugin.register(with: registry.registrar(forPlugin: "CwDecredPlugin")) CwMwebPlugin.register(with: registry.registrar(forPlugin: "CwMwebPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DevicelocalePlugin.register(with: registry.registrar(forPlugin: "DevicelocalePlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 689f0ea03..cc6ae6e3b 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -3,6 +3,7 @@ PODS: - FlutterMacOS - ReachabilitySwift - cw_mweb (0.0.1): + - cw_decred (0.0.1): - FlutterMacOS - device_info_plus (0.0.1): - FlutterMacOS @@ -45,6 +46,7 @@ PODS: DEPENDENCIES: - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) - cw_mweb (from `Flutter/ephemeral/.symlinks/plugins/cw_mweb/macos`) + - cw_decred (from `Flutter/ephemeral/.symlinks/plugins/cw_decred/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - devicelocale (from `Flutter/ephemeral/.symlinks/plugins/devicelocale/macos`) - fast_scanner (from `Flutter/ephemeral/.symlinks/plugins/fast_scanner/macos`) @@ -72,6 +74,10 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos cw_mweb: :path: Flutter/ephemeral/.symlinks/plugins/cw_mweb/macos + cw_decred: + :path: Flutter/ephemeral/.symlinks/plugins/cw_decred/macos + cw_monero: + :path: Flutter/ephemeral/.symlinks/plugins/cw_monero/macos device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos devicelocale: @@ -108,6 +114,8 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 cw_mweb: 7440b12ead811dda972a9918442ea2a458e8742c + cw_monero: ec03de55a19c4a2b174ea687e0f4202edc716fa4 + cw_decred: 0c93fbeb31bd97a6ad4ec5680960af0943bfca78 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f devicelocale: 9f0f36ac651cabae2c33f32dcff4f32b61c38225 fast_scanner: d31bae07e2653403a69dac99fb710c1722b16a97 diff --git a/model_generator.sh b/model_generator.sh index f3950e2b1..56b891903 100755 --- a/model_generator.sh +++ b/model_generator.sh @@ -1,22 +1,22 @@ #!/bin/bash set -x -e -for cwcoin in cw_{core,evm,monero,bitcoin,haven,nano,bitcoin_cash,solana,tron,wownero,zano} +for cwcoin in cw_{core,evm,monero,bitcoin,haven,nano,bitcoin_cash,solana,tron,wownero,zano,decred} do if [[ "x$1" == "xasync" ]]; then bash -c "cd $cwcoin; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .." & else - bash -c "cd $cwcoin; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .." + cd $cwcoin; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .. fi done for cwcoin in cw_{polygon,ethereum,mweb}; -do +do if [[ "x$1" == "xasync" ]]; then bash -c "cd $cwcoin; flutter pub get; cd .." & else - bash -c "cd $cwcoin; flutter pub get; cd .." + cd $cwcoin; flutter pub get; cd .. fi done diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 767f0b1f3..7e700e588 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -189,6 +189,7 @@ flutter: - assets/tron_node_list.yml - assets/wownero_node_list.yml - assets/zano_node_list.yml + - assets/decred_node_list.yml - assets/text/ - assets/faq/ - assets/animation/ diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 9f1235919..6072b046c 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -208,6 +208,8 @@ "debit_card_terms": "يخضع تخزين واستخدام رقم بطاقة الدفع الخاصة بك (وبيانات الاعتماد المقابلة لرقم بطاقة الدفع الخاصة بك) في هذه المحفظة الرقمية لشروط وأحكام اتفاقية حامل البطاقة المعمول بها مع جهة إصدار بطاقة الدفع ، كما هو معمول به من وقت لآخر.", "decimal_places_error": "عدد كبير جدًا من المنازل العشرية", "decimals_cannot_be_zero": "الرمز العشري لا يمكن أن يكون الصفر.", + "decred_info_card_details": "يستخدم Decred طريقة متزامنة لا مركزية وحفاظ على الخصوصية المعروفة باسم \"SPV\" ، والتي تستغرق وقتًا أطول من محفظة Bitcoin العادية. لمعرفة المزيد ، انقر أدناه.", + "decred_info_title": "التزامن في Decred", "default_buy_provider": "مزود شراء الافتراضي", "default_sell_provider": "ﻲﺿﺍﺮﺘﻓﻻﺍ ﻊﻴﺒﻟﺍ ﺩﻭﺰﻣ", "delete": "حذف", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "إذا لم تستمر الشاشة بعد دقيقة واحدة ، فتحقق من بريدك الإلكتروني.", "proceed_on_device": "تابع جهازك", "proceed_on_device_description": "يرجى اتباع الإرشادات المطلوبة على محفظة الأجهزة الخاصة بك", + "processing": "يعالج", "profile": "حساب تعريفي", "provider_error": "خطأ ${provider}", "public_key": "مفتاح عمومي", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 190dd07d8..62f1bbf18 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Съхранението и използването на данните от вашата платежна карта в този дигитален портфейл подлежат на условията на съответното съгласие за картодържец от издателя на картата.", "decimal_places_error": "Твърде много знаци след десетичната запетая", "decimals_cannot_be_zero": "Десетичната точка не може да бъде нула.", + "decred_info_card_details": "DeCred използва децентрализиран и консервиращ метод за синхронизиране, известен като „SPV“, който отнема повече време от нормалния портфейл с биткойн. За да научите повече, докоснете по -долу.", + "decred_info_title": "Синхронизация в Decred", "default_buy_provider": "Доставчик по подразбиране купува", "default_sell_provider": "Доставчик за продажба по подразбиране", "delete": "Изтрий", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Ако процесът продължи повече от 1 минута, проверете своя имейл.", "proceed_on_device": "Продължете на вашето устройство", "proceed_on_device_description": "Моля, следвайте инструкциите, подканени на вашия хардуер", + "processing": "Обработка", "profile": "Профил", "provider_error": "Грешка на ${provider} ", "public_key": "Публичен ключ", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 923db16fe..92eed7a3d 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Uložení a použití vašeho čísla platební karty (a přihlašovací údaje k vašemu číslu karty) v této digitální peněžence se řídí Obchodními podmínkami smlouvy příslušného držitele karty s vydavatelem karty (v jejich nejaktuálnější verzi).", "decimal_places_error": "Příliš mnoho desetinných míst", "decimals_cannot_be_zero": "Desetinná desetinná škola nemůže být nulová.", + "decred_info_card_details": "Decred používá decentralizovanou a synchronizační metodu zachovávající soukromí známou jako „SPV“, která trvá déle než normální bitcoinová peněženka. Chcete -li se dozvědět více, klepněte na níže.", + "decred_info_title": "Synchronizace v Decred", "default_buy_provider": "Výchozí poskytovatel nákupu", "default_sell_provider": "Výchozí poskytovatel prodeje", "delete": "Smazat", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Pokud proces nepokročí během 1 minuty, zkontrolujte svůj e-mail.", "proceed_on_device": "Pokračujte ve svém zařízení", "proceed_on_device_description": "Postupujte podle pokynů na výzvu na vaší hardwarové peněžence", + "processing": "Zpracování", "profile": "Profil", "provider_error": "${provider} chyba", "public_key": "Veřejný klíč", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 39e3113c1..43db11f18 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Die Speicherung und Nutzung Ihrer Zahlungskartennummer (und Ihrer Zahlungskartennummer entsprechenden Anmeldeinformationen) in dieser digitalen Wallet unterliegt den Allgemeinen Geschäftsbedingungen des geltenden Karteninhabervertrags mit dem Zahlungskartenaussteller, gültig ab von Zeit zu Zeit.", "decimal_places_error": "Zu viele Nachkommastellen", "decimals_cannot_be_zero": "Token-Dezimalzahl kann nicht Null sein.", + "decred_info_card_details": "Decred verwendet eine dezentrale und für Privatsphäre erziehende Synchronisation, die als „SPV“ bezeichnet wird und länger als eine normale Bitcoin-Brieftasche dauert. Um mehr zu erfahren, tippen Sie unten.", + "decred_info_title": "Synchronisation in Decred", "default_buy_provider": "Standard-Kaufanbieter", "default_sell_provider": "Standard-Verkaufsanbieter", "delete": "Löschen", @@ -547,6 +549,7 @@ "proceed_after_one_minute": "Wenn der Bildschirm nach 1 Minute nicht weitergeht, überprüfen Sie bitte Ihre E-Mail.", "proceed_on_device": "Fahren Sie auf Ihrem Gerät fort", "proceed_on_device_description": "Bitte befolgen Sie die Anweisungen, die auf Ihrer Hardware-Wallet angezeigt werden", + "processing": "Verarbeitung", "profile": "Profil", "provider_error": "${provider}-Fehler", "public_key": "Öffentlicher Schlüssel", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 921375c3c..b6023d86e 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -208,6 +208,8 @@ "debit_card_terms": "The storage and usage of your payment card number (and credentials corresponding to your payment card number) in this digital wallet are subject to the Terms and Conditions of the applicable cardholder agreement with the payment card issuer, as in effect from time to time.", "decimal_places_error": "Too many decimal places", "decimals_cannot_be_zero": "Token decimal cannot be zero.", + "decred_info_card_details": "Decred uses a decentralized and privacy-preserving sync method known as “SPV”, which takes longer than a normal Bitcoin wallet. To learn more, tap below.", + "decred_info_title": "Synchronization in Decred", "default_buy_provider": "Default Buy Provider", "default_sell_provider": "Default Sell Provider", "delete": "Delete", @@ -547,6 +549,7 @@ "proceed_after_one_minute": "If the screen doesn’t proceed after 1 minute, check your email.", "proceed_on_device": "Proceed on your device", "proceed_on_device_description": "Please follow the instructions prompted on your hardware wallet", + "processing": "Processing", "profile": "Profile", "provider_error": "${provider} error", "public_key": "Public key", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index ad96c2a93..c0474d672 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -208,6 +208,8 @@ "debit_card_terms": "El almacenamiento y el uso de su número de tarjeta de pago (y las credenciales correspondientes a su número de tarjeta de pago) en esta billetera digital están sujetos a los Términos y condiciones del acuerdo del titular de la tarjeta aplicable con el emisor de la tarjeta de pago, en vigor desde tiempo al tiempo.", "decimal_places_error": "Demasiados lugares decimales", "decimals_cannot_be_zero": "Token Decimal no puede ser cero.", + "decred_info_card_details": "Decred utiliza un método de sincronización descentralizado y que presenta la privacidad conocido como \"SPV\", que lleva más tiempo que una billetera Bitcoin normal. Para obtener más información, toque a continuación.", + "decred_info_title": "Sincronización en Decred", "default_buy_provider": "Proveedor de compra predeterminado", "default_sell_provider": "Proveedor de venta predeterminado", "delete": "Borrar", @@ -547,6 +549,7 @@ "proceed_after_one_minute": "Si la pantalla no continúa después de 1 minuto, revisa tu correo electrónico.", "proceed_on_device": "Continúa con tu dispositivo", "proceed_on_device_description": "Sigue las instrucciones solicitadas en su billetera de hardware", + "processing": "Tratamiento", "profile": "Perfil", "provider_error": "${provider} error", "public_key": "Clave pública", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index ccd12b1c6..d779a22f5 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Le stockage et l'utilisation de votre numéro de carte de paiement (et des informations d'identification correspondant à votre numéro de carte de paiement) dans ce portefeuille (wallet) numérique peuvent être soumis aux conditions générales de l'accord du titulaire de carte parfois en vigueur avec l'émetteur de la carte de paiement.", "decimal_places_error": "Trop de décimales", "decimals_cannot_be_zero": "La décimale du jeton ne peut pas être nulle.", + "decred_info_card_details": "Decred utilise une méthode de synchronisation décentralisée et préservée de confidentialité connue sous le nom de «SPV», qui prend plus de temps qu'un portefeuille Bitcoin normal. Pour en savoir plus, appuyez sur ci-dessous.", + "decred_info_title": "Synchronisation dans Decred", "default_buy_provider": "Fournisseur d'achat par défaut", "default_sell_provider": "Fournisseur de vente par défaut", "delete": "Effacer", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Si l'écran ne s'affiche pas après 1 minute, vérifiez vos e-mails.", "proceed_on_device": "Continuez sur votre appareil", "proceed_on_device_description": "Veuillez suivre les instructions affichées sur votre portefeuille physique.", + "processing": "Traitement", "profile": "Profil", "provider_error": "Erreur de ${provider}", "public_key": "Clef publique", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index af931e4df..13cbe1828 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Adana da amfani da lambar katin kuɗin ku (da takaddun shaida masu dacewa da lambar katin kuɗin ku) a cikin wannan walat ɗin dijital suna ƙarƙashin Sharuɗɗa da Sharuɗɗa na yarjejeniya mai amfani da katin tare da mai fitar da katin biyan kuɗi, kamar yadda yake aiki daga lokaci zuwa lokaci.", "decimal_places_error": "Wadannan suna da tsawon harsuna", "decimals_cannot_be_zero": "Alamar alama ba zata iya zama sifili ba.", + "decred_info_card_details": "An yanke amfani da shi da ingantaccen tsari da kuma tsarin adana Siscyc na sirri da aka sani da \"SPV\", wanda ke ɗaukar tsayi fiye da walatic na al'ada. Don ƙarin koyo, matsa ƙasa.", + "decred_info_title": "Aiki tare a Decred", "default_buy_provider": "Tsohuwar Siyarwa", "default_sell_provider": "Tsohuwar Mai Bayar Siyarwa", "delete": "Share", @@ -548,6 +550,7 @@ "proceed_after_one_minute": "Idan allon bai ci gaba ba bayan minti 1, duba imel ɗin ku.", "proceed_on_device": "Ci gaba akan na'urarka", "proceed_on_device_description": "Da fatan za a bi umarnin akan walatware", + "processing": "Aiki", "profile": "Rabin fuska", "provider_error": "${provider} kuskure", "public_key": "Maɓallin jama'a", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 449a3f278..409e0fd07 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -208,6 +208,8 @@ "debit_card_terms": "इस डिजिटल वॉलेट में आपके भुगतान कार्ड नंबर (और आपके भुगतान कार्ड नंबर से संबंधित क्रेडेंशियल) का भंडारण और उपयोग भुगतान कार्ड जारीकर्ता के साथ लागू कार्डधारक समझौते के नियमों और शर्तों के अधीन है, जैसा कि प्रभावी है समय - समय पर।", "decimal_places_error": "बहुत अधिक दशमलव स्थान", "decimals_cannot_be_zero": "टोकन दशमलव शून्य नहीं हो सकता।", + "decred_info_card_details": "डिक्रेड एक विकेन्द्रीकृत और गोपनीयता-संरक्षण सिंक विधि का उपयोग करता है जिसे \"एसपीवी\" के रूप में जाना जाता है, जो एक सामान्य बिटकॉइन वॉलेट से अधिक समय लेता है। अधिक जानने के लिए, नीचे टैप करें।", + "decred_info_title": "डिक्रेड में सिंक्रनाइज़ेशन", "default_buy_provider": "डिफ़ॉल्ट खरीद प्रदाता", "default_sell_provider": "डिफ़ॉल्ट विक्रय प्रदाता", "delete": "हटाएं", @@ -547,6 +549,7 @@ "proceed_after_one_minute": "यदि 1 मिनट के बाद भी स्क्रीन आगे नहीं बढ़ती है, तो अपना ईमेल देखें।", "proceed_on_device": "अपने डिवाइस पर आगे बढ़ें", "proceed_on_device_description": "कृपया अपने हार्डवेयर वॉलेट पर दिए गए निर्देशों का पालन करें", + "processing": "प्रसंस्करण", "profile": "प्रोफ़ाइल", "provider_error": "${provider} त्रुटि", "public_key": "सार्वजनिक कुंजी", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 25d90e711..f93f86d1d 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Pohranjivanje i korištenje broja vaše platne kartice (i vjerodajnica koje odgovaraju broju vaše platne kartice) u ovom digitalnom novčaniku podliježu Uvjetima i odredbama važećeg ugovora vlasnika kartice s izdavateljem platne kartice, koji su na snazi ​​od S vremena na vrijeme.", "decimal_places_error": "Previše decimalnih mjesta", "decimals_cannot_be_zero": "Token Decimal ne može biti nula.", + "decred_info_card_details": "DECRED koristi decentraliziranu metodu sinkronizacije za očuvanje privatnosti poznatu kao \"SPV\", koja traje duže od normalnog Bitcoin novčanika. Da biste saznali više, dodirnite u nastavku.", + "decred_info_title": "Sinkronizacija u DECRED", "default_buy_provider": "Zadani davatelj kupnje", "default_sell_provider": "Zadani dobavljač prodaje", "delete": "Izbriši", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Ako se zaslon ne nastavi nakon 1 minute, provjerite svoju e-poštu.", "proceed_on_device": "Nastavite na svom uređaju", "proceed_on_device_description": "Slijedite upute zatražene na vašem hardverskom novčaniku", + "processing": "Obrada", "profile": "Profil", "provider_error": "${provider} greška", "public_key": "Javni ključ", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 6c35b2be1..3378f1a0d 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Ձեր վճարային քարտի համարի (և ձեր վճարային քարտի համարի համապատասխան վկայականներ) պահպանումն ու օգտագործումը այս թվային դրամապանակում ենթակա են վճարային քարտ թողարկող կողմի գործող պայմանների և պայմանագրի", "decimal_places_error": "Խմբակային տեղերի սխալ", "decimals_cannot_be_zero": "Խմբակային տեղերը չեն կարող լինել զրո", + "decred_info_card_details": "DECREDE- ն օգտագործում է ապակենտրոնացված եւ գաղտնիության պահպանման համաժամացման համաժամացման մեթոդը, որը հայտնի է որպես «SPV», որը տեւում է ավելի երկար, քան նորմալ Bitcoin դրամապանակը: Ավելին իմանալու համար հպեք ստորեւ:", + "decred_info_title": "Համաժամացումը DEPRED- ում", "default_buy_provider": "Լռելյայն գնման մատակարար", "default_sell_provider": "Լռելյայն վաճառքի մատակարար", "delete": "Ջնջել", @@ -545,6 +547,7 @@ "proceed_after_one_minute": "Եթե էկրանը 1 րոպեից ավել չի անցնում, ստուգեք ձեր էլեկտրոնային փոստը", "proceed_on_device": "Շարունակեք ձեր սարքի վրա", "proceed_on_device_description": "Խնդրում ենք հետևել ձեր սարքի վրա ցուցադրվող հրահանգներին", + "processing": "Վերամշակում", "profile": "Պրոֆիլ", "provider_error": "${provider} սխալ", "public_key": "Հանրային բանալի", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 87e45da1c..22558854f 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Penyimpanan dan penggunaan nomor kartu pembayaran Anda (dan kredensial yang sesuai dengan nomor kartu pembayaran Anda) dalam dompet digital ini tertakluk pada Syarat dan Ketentuan persetujuan pemegang kartu yang berlaku dengan penerbit kartu pembayaran, seperti yang berlaku dari waktu ke waktu.", "decimal_places_error": "Terlalu banyak tempat desimal", "decimals_cannot_be_zero": "Token desimal tidak bisa nol.", + "decred_info_card_details": "Decred menggunakan metode sinkronisasi yang terdesentralisasi dan mempertahankan privasi yang dikenal sebagai \"SPV\", yang membutuhkan waktu lebih lama dari dompet Bitcoin normal. Untuk mempelajari lebih lanjut, ketuk di bawah ini.", + "decred_info_title": "Sinkronisasi dalam dekred", "default_buy_provider": "Penyedia beli default", "default_sell_provider": "Penyedia Penjualan Default", "delete": "Hapus", @@ -548,6 +550,7 @@ "proceed_after_one_minute": "Jika layar tidak bergerak setelah 1 menit, periksa email Anda.", "proceed_on_device": "Lanjutkan di perangkat Anda", "proceed_on_device_description": "Harap ikuti instruksi yang diminta di dompet perangkat keras Anda", + "processing": "Pengolahan", "profile": "Profil", "provider_error": "${provider} error", "public_key": "Kunci publik", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index a476bbc0c..a30944901 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -208,6 +208,8 @@ "debit_card_terms": "L'archiviazione e l'utilizzo del numero della carta di pagamento (e delle credenziali corrispondenti al numero della carta di pagamento) in questo portafoglio digitale sono soggetti ai Termini e condizioni del contratto applicabile con il titolare della carta con l'emittente della carta di pagamento, come in vigore di tanto in tanto.", "decimal_places_error": "Troppe cifre decimali", "decimals_cannot_be_zero": "Il decimale token non può essere zero.", + "decred_info_card_details": "Decred utilizza un metodo di sincronizzazione decentralizzato e di conservazione della privacy noto come \"SPV\", che richiede più tempo di un normale portafoglio Bitcoin. Per saperne di più, tocca sotto.", + "decred_info_title": "Sincronizzazione in decred", "default_buy_provider": "Provider di acquisto predefinito", "default_sell_provider": "Provider di vendita predefinito", "delete": "Elimina", @@ -547,6 +549,7 @@ "proceed_after_one_minute": "Se la schermata non procede dopo 1 minuto, controlla la tua email.", "proceed_on_device": "Procedi sul tuo dispositivo", "proceed_on_device_description": "Segui le istruzioni richieste sul portafoglio hardware", + "processing": "Elaborazione", "profile": "Profilo", "provider_error": "${provider} errore", "public_key": "Chiave pubblica", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 10350d7be..25b16a92a 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -208,6 +208,8 @@ "debit_card_terms": "このデジタルウォレットでの支払いカード番号(および支払いカード番号に対応する資格情報)の保存と使用には、支払いカード発行者との該当するカード所有者契約の利用規約が適用されます。時々。", "decimal_places_error": "小数点以下の桁数が多すぎる", "decimals_cannot_be_zero": "トークン小数はゼロにすることはできません。", + "decred_info_card_details": "Decredは、「SPV」と呼ばれる分散型およびプライバシーを摂取する同期メソッドを使用します。これには、通常のビットコインウォレットよりも時間がかかります。詳細については、以下をタップしてください。", + "decred_info_title": "デコロードの同期", "default_buy_provider": "デフォルトの購入プロバイダー", "default_sell_provider": "デフォルトの販売プロバイダー", "delete": "削除する", @@ -547,6 +549,7 @@ "proceed_after_one_minute": "1分経っても画面が進まない場合は、メールを確認してください。", "proceed_on_device": "デバイスに進みます", "proceed_on_device_description": "ハードウェアウォレットにプロンプ​​トされた指示に従ってください", + "processing": "処理", "profile": "プロフィール", "provider_error": "${provider} エラー", "public_key": "公開鍵", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index a76318ad0..9849ccd09 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -208,6 +208,8 @@ "debit_card_terms": "이 디지털 지갑에 있는 귀하의 지불 카드 번호(및 귀하의 지불 카드 번호에 해당하는 자격 증명)의 저장 및 사용은 부터 발효되는 지불 카드 발행자와의 해당 카드 소지자 계약의 이용 약관을 따릅니다. 수시로.", "decimal_places_error": "소수점 이하 자릿수가 너무 많습니다.", "decimals_cannot_be_zero": "토큰 소수점은 0이 될 수 없습니다.", + "decred_info_card_details": "Decred는 정상적인 비트 코인 지갑보다 더 오래 걸리는 \"SPV\"로 알려진 분산 및 개인 정보 보호 동기화 방법을 사용합니다. 자세한 내용은 아래를 누릅니다.", + "decred_info_title": "Decred의 동기화", "default_buy_provider": "기본 구매 제공자", "default_sell_provider": "기본 판매 공급자", "delete": "지우다", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "1분 후에도 화면이 진행되지 않으면 이메일을 확인하세요.", "proceed_on_device": "장치를 진행하십시오", "proceed_on_device_description": "하드웨어 지갑에 표시된 지침을 따르십시오", + "processing": "처리", "profile": "프로필", "provider_error": "${provider} 오류", "public_key": "공개 키", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index ac6749a7f..a8f57a824 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -208,6 +208,8 @@ "debit_card_terms": "ဤဒစ်ဂျစ်တယ်ပိုက်ဆံအိတ်ရှိ သင့်ငွေပေးချေမှုကတ်နံပါတ် (နှင့် သင့်ငွေပေးချေကတ်နံပါတ်နှင့် သက်ဆိုင်သောအထောက်အထားများ) ၏ သိုလှောင်မှုနှင့် အသုံးပြုမှုသည် အချိန်အခါနှင့်အမျှ သက်ရောက်မှုရှိသကဲ့သို့ ကတ်ကိုင်ဆောင်ထားသူ၏ သဘောတူညီချက်၏ စည်းကမ်းသတ်မှတ်ချက်များနှင့် ကိုက်ညီပါသည်။", "decimal_places_error": "ဒဿမနေရာများ များလွန်းသည်။", "decimals_cannot_be_zero": "တိုကင်ဒ decimal မသုညမဖြစ်နိုင်ပါ။", + "decred_info_card_details": "ပုံမှန် bitcoin ပိုက်ဆံအိတ်ထက်ပိုရှည်သော \"SPV\" ဟုလူသိများသောဗဟိုချုပ်ကိုင်မှုလျှော့ချခြင်းနှင့်လုံခြုံမှုထိန်းသိမ်းခြင်းကိုထိန်းသိမ်းထားသည့်ထပ်တူပြုခြင်းနည်းလမ်းကိုအသုံးပြုသည်။ ပိုမိုလေ့လာရန်အောက်ပါကိုအသာပုတ်ပါ။", + "decred_info_title": "ဒီဇင်ဘာလ၌ထပ်တူပြုခြင်း", "default_buy_provider": "Default Provider ကိုဝယ်ပါ", "default_sell_provider": "ပုံသေရောင်းချပေးသူ", "delete": "ဖျက်ပါ။", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "မျက်နှာပြင်သည် ၁ မိနစ်အကြာတွင် ဆက်လက်မလုပ်ဆောင်ပါက သင့်အီးမေးလ်ကို စစ်ဆေးပါ။", "proceed_on_device": "သင့်စက်ပေါ်တွင်ဆက်လက်ဆောင်ရွက်ပါ", "proceed_on_device_description": "သင်၏ hardware ပိုက်ဆံအိတ်ပေါ်ရှိညွှန်ကြားချက်များကိုလိုက်နာပါ", + "processing": "လုပ်ကိုင်ခြင်း", "profile": "ကိုယ်ရေးအကျဉ်း", "provider_error": "${provider} အမှား", "public_key": "အများသူငှာသော့", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index e38e1ddfd..a4f2bb9a1 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -208,6 +208,8 @@ "debit_card_terms": "De opslag en het gebruik van uw betaalkaartnummer (en inloggegevens die overeenkomen met uw betaalkaartnummer) in deze digitale portemonnee zijn onderworpen aan de Algemene voorwaarden van de toepasselijke kaarthouderovereenkomst met de uitgever van de betaalkaart, zoals van kracht vanaf tijd tot tijd.", "decimal_places_error": "Te veel decimalen", "decimals_cannot_be_zero": "Token decimaal kan niet nul zijn.", + "decred_info_card_details": "Decred maakt gebruik van een gedecentraliseerde en privacy-behouds-synchronisatiemethode bekend als \"SPV\", die langer duurt dan een normale Bitcoin-portemonnee. Voor meer informatie, tik hieronder.", + "decred_info_title": "Synchronisatie in Decred", "default_buy_provider": "Standaard Koopprovider", "default_sell_provider": "Standaard verkoopaanbieder", "delete": "Delete", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Als het scherm na 1 minuut niet verder gaat, controleer dan uw e-mail.", "proceed_on_device": "Ga verder met uw apparaat", "proceed_on_device_description": "Volg de instructies die zijn aangevraagd op uw hardware -portemonnee", + "processing": "Verwerking", "profile": "Profiel", "provider_error": "${provider} fout", "public_key": "Publieke sleutel", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 9ec7db278..12e867518 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Przechowywanie i używanie numeru karty płatniczej (oraz danych uwierzytelniających odpowiadających numerowi karty płatniczej) w tym portfelu cyfrowym podlega Warunkom odpowiedniej umowy posiadacza karty z wydawcą karty płatniczej, zgodnie z obowiązującym od od czasu do czasu.", "decimal_places_error": "Za dużo miejsc dziesiętnych", "decimals_cannot_be_zero": "Token dziesiętny nie może być zerowy.", + "decred_info_card_details": "Decred używa zdecentralizowanej i zachowującej prywatność metodę synchronizacji znanej jako „SPV”, która trwa dłużej niż normalny portfel bitcoin. Aby dowiedzieć się więcej, dotknij poniżej.", + "decred_info_title": "Synchronizacja w dekred", "default_buy_provider": "Domyślny dostawca zakupu", "default_sell_provider": "Domyślny dostawca sprzedaży", "delete": "Skasuj", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Jeśli ekran nie przejdzie dalej po 1 minucie, sprawdź pocztę.", "proceed_on_device": "Kontynuuj swoje urządzenie", "proceed_on_device_description": "Postępuj zgodnie z instrukcjami wyświetlonymi w portfelu sprzętowym", + "processing": "Przetwarzanie", "profile": "Profil", "provider_error": "${provider} pomyłka", "public_key": "Klucz publiczny", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 0c0ed2515..7d60a3d79 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -208,6 +208,8 @@ "debit_card_terms": "O armazenamento e uso do número do cartão de pagamento (e credenciais correspondentes ao número do cartão de pagamento) nesta carteira digital estão sujeitos aos Termos e Condições do contrato do titular do cartão aplicável com o emissor do cartão de pagamento, em vigor a partir de tempo ao tempo.", "decimal_places_error": "Muitas casas decimais", "decimals_cannot_be_zero": "Decimal de token não pode ser zero.", + "decred_info_card_details": "O Decred usa um método de sincronização descentralizado e de preservação de privacidade, conhecido como \"SPV\", que leva mais tempo do que uma carteira normal de Bitcoin. Para saber mais, toque abaixo.", + "decred_info_title": "Sincronização em Decred", "default_buy_provider": "Provedor de compra padrão", "default_sell_provider": "Provedor de venda padrão", "delete": "Excluir", @@ -548,6 +550,7 @@ "proceed_after_one_minute": "Se a tela não prosseguir após 1 minuto, verifique seu e-mail.", "proceed_on_device": "Prossiga no seu dispositivo", "proceed_on_device_description": "Siga as instruções solicitadas em sua carteira de hardware", + "processing": "Processamento", "profile": "Perfil", "provider_error": "${provider} erro", "public_key": "Chave pública", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 0b511b016..9c80e483f 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Хранение и использование номера вашей платежной карты (и учетных данных, соответствующих номеру вашей платежной карты) в этом цифровом кошельке регулируются положениями и условиями применимого соглашения держателя карты с эмитентом платежной карты, действующим с время от времени.", "decimal_places_error": "Слишком много десятичных знаков", "decimals_cannot_be_zero": "Десятичный токен не может быть нулевым.", + "decred_info_card_details": "DepRed использует децентрализованный метод синхронизации и сохраняющего конфиденциальность, известный как «SPV», который занимает больше времени, чем обычный биткойн-кошелек. Чтобы узнать больше, нажмите ниже.", + "decred_info_title": "Синхронизация в декорации", "default_buy_provider": "По умолчанию поставщик покупки", "default_sell_provider": "Поставщик продаж по умолчанию", "delete": "Удалить", @@ -547,6 +549,7 @@ "proceed_after_one_minute": "Если через 1 минуту экран не отображается, проверьте свою электронную почту.", "proceed_on_device": "Пройдите на свое устройство", "proceed_on_device_description": "Пожалуйста, следуйте инструкциям, представленным на вашем аппаратном кошельке", + "processing": "Обработка", "profile": "Профиль", "provider_error": "${provider} ошибка", "public_key": "Публичный ключ", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index bbbcf7227..874e9f390 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -208,6 +208,8 @@ "debit_card_terms": "การเก็บรักษาและใช้หมายเลขบัตรจ่ายเงิน (และข้อมูลประจำตัวที่เกี่ยวข้องกับหมายเลขบัตรจ่ายเงิน) ในกระเป๋าดิจิทัลนี้ จะต้องยึดถือข้อกำหนดและเงื่อนไขของข้อตกลงผู้ใช้บัตรของผู้ถือบัตรที่เกี่ยวข้องกับบัตรผู้ถือบัตร ซึ่งจะมีผลตั้งแต่เวลานั้น", "decimal_places_error": "ทศนิยมมากเกินไป", "decimals_cannot_be_zero": "ทศนิยมโทเค็นไม่สามารถเป็นศูนย์ได้", + "decred_info_card_details": "Decred ใช้วิธีการซิงค์การกระจายอำนาจและความเป็นส่วนตัวที่เรียกว่า \"SPV\" ซึ่งใช้เวลานานกว่ากระเป๋าเงิน bitcoin ปกติ หากต้องการเรียนรู้เพิ่มเติมแตะด้านล่าง", + "decred_info_title": "การซิงโครไนซ์ใน decred", "default_buy_provider": "ผู้ให้บริการซื้อเริ่มต้น", "default_sell_provider": "ผู้ให้บริการการขายเริ่มต้น", "delete": "ลบ", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "หากหน้าจอไม่ดำเนินการหลังจาก 1 นาทีโปรดตรวจสอบอีเมลของคุณ", "proceed_on_device": "ดำเนินการบนอุปกรณ์ของคุณ", "proceed_on_device_description": "โปรดทำตามคำแนะนำที่ได้รับแจ้งไว้ในกระเป๋าเงินฮาร์ดแวร์ของคุณ", + "processing": "กำลังประมวลผล", "profile": "ประวัติโดยย่อ", "provider_error": "ข้อผิดพลาด ${provider}", "public_key": "คีย์สาธารณะ", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 7cdbd0d8b..31683c4fc 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Ang pag-iimbak at paggamit ng iyong numero sa card (at mga kredensyal na nauugnay sa numero ng iyong card sa pagbabayad) sa pagbabayad sa digital wallet na ito ay napapailalim sa mga tuntunin at kundisyon ng naaangkop na kasunduan sa may-ari ng card kasama ang nagbigay ng card ng pagbabayad, na may bisa sa pana-panahon.", "decimal_places_error": "Masyadong maraming mga lugar na desimal", "decimals_cannot_be_zero": "Ang token decimal ay hindi maaaring maging zero.", + "decred_info_card_details": "Ang DECRED ay gumagamit ng isang desentralisado at privacy-pagpapanatili ng pamamaraan ng pag-sync na kilala bilang \"SPV\", na mas matagal kaysa sa isang normal na pitaka ng bitcoin. Upang malaman ang higit pa, mag -tap sa ibaba.", + "decred_info_title": "Pag -synchronise sa Decred", "default_buy_provider": "Default na Buy Provider", "default_sell_provider": "Default na Sell Provider", "delete": "Tanggalin", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Kung ang screen ay hindi magpapatuloy pagkatapos ng 1 minuto, suriin ang iyong email.", "proceed_on_device": "Magpatuloy sa iyong hardware wallet", "proceed_on_device_description": "Mangyaring sundin ang mga tagubilin na sinenyasan sa iyong hardware wallet", + "processing": "Pagproseso", "profile": "Profile", "provider_error": "${provider} error", "public_key": "Public key", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index bfc87a7e7..f7553d5c1 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Ödeme kartı numaranızın (ve kart numaranıza karşılık gelen kimlik bilgilerinin) bu dijital cüzdanda saklanması ve kullanılması, zaman zaman yürürlükte olan ödeme kartı veren kuruluşla yapılan ilgili kart sahibi sözleşmesinin Hüküm ve Koşullarına tabidir.", "decimal_places_error": "Çok fazla ondalık basamak", "decimals_cannot_be_zero": "Token oncial sıfır olamaz.", + "decred_info_card_details": "Decred, normal bir bitcoin cüzdanından daha uzun süren “SPV” olarak bilinen merkezi olmayan ve gizliliği koruyan bir senkronizasyon yöntemi kullanır. Daha fazla bilgi edinmek için aşağıya dokunun.", + "decred_info_title": "Senkronizasyon Dekred", "default_buy_provider": "Varsayılan Satın Alma Sağlayıcısı", "default_sell_provider": "Varsayılan Satış Sağlayıcısı", "delete": "Sil", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Ekran 1 dakika sonra ilerlemezse, e-postanızı kontrol edin.", "proceed_on_device": "Cihazınıza devam edin", "proceed_on_device_description": "Lütfen donanım cüzdanınızda istenen talimatları izleyin", + "processing": "İşleme", "profile": "Profil", "provider_error": "${provider} hatası", "public_key": "Genel Anahtar", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 4b7818347..e3a150bca 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Зберігання та використання номера вашої платіжної картки (та облікових даних, які відповідають номеру вашої платіжної картки) у цьому цифровому гаманці регулюються Умовами відповідної угоди власника картки з емітентом платіжної картки, що діє з час від часу.", "decimal_places_error": "Забагато знаків після коми", "decimals_cannot_be_zero": "Десятковий знак не може бути нульовим.", + "decred_info_card_details": "Decred використовує децентралізований метод синхронізації, що зберігає конфіденційність, відомий як \"SPV\", який займає більше часу, ніж звичайний гаманець Bitcoin. Щоб дізнатися більше, торкніться нижче.", + "decred_info_title": "Синхронізація в Decred", "default_buy_provider": "Постачальник покупки за замовчуванням", "default_sell_provider": "Постачальник продажу за замовчуванням", "delete": "Видалити", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Якщо екран не продовжується через 1 хвилину, перевірте свою електронну пошту.", "proceed_on_device": "Продовжуйте свій пристрій", "proceed_on_device_description": "Будь ласка, дотримуйтесь інструкцій, підказаних на вашому апаратному гаманці", + "processing": "Обробка", "profile": "Профіль", "provider_error": "${provider} помилка", "public_key": "Публічний ключ", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index f6b92ca36..d95bb9237 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -208,6 +208,8 @@ "debit_card_terms": "اس ڈیجیٹل والیٹ میں آپ کے ادائیگی کارڈ نمبر (اور آپ کے ادائیگی کارڈ نمبر سے متعلقہ اسناد) کا ذخیرہ اور استعمال ادائیگی کارڈ جاری کنندہ کے ساتھ قابل اطلاق کارڈ ہولڈر کے معاہدے کی شرائط و ضوابط کے ساتھ مشروط ہے، جیسا کہ وقتاً فوقتاً نافذ ہوتا ہے۔", "decimal_places_error": "بہت زیادہ اعشاریہ جگہیں۔", "decimals_cannot_be_zero": "ٹوکن اعشاریہ صفر نہیں ہوسکتا۔", + "decred_info_card_details": "ڈیکریڈ ایک विकेंद्रीकृत اور رازداری سے محفوظ رکھنے والا مطابقت پذیری کا طریقہ استعمال کرتا ہے جسے \"ایس پی وی\" کہا جاتا ہے ، جو عام بٹ کوائن پرس سے زیادہ وقت لگتا ہے۔ مزید جاننے کے لئے ، نیچے ٹیپ کریں۔", + "decred_info_title": "فیصلہ شدہ میں ہم آہنگی", "default_buy_provider": "پہلے سے طے شدہ خریدنے والا", "default_sell_provider": " ﮦﺪﻨﻨﮐ ﻢﮨﺍﺮﻓ ﻞﯿﺳ ﭧﻟﺎﻔﯾﮈ", "delete": "حذف کریں۔", @@ -548,6 +550,7 @@ "proceed_after_one_minute": "اگر اسکرین 1 منٹ کے بعد آگے نہیں بڑھتی ہے تو اپنا ای میل چیک کریں۔", "proceed_on_device": "اپنے آلے پر آگے بڑھیں", "proceed_on_device_description": "براہ کرم اپنے ہارڈ ویئر پرس پر آنے والی ہدایات پر عمل کریں", + "processing": "پروسیسنگ", "profile": "پروفائل", "provider_error": "${provider} خرابی۔", "public_key": "عوامی کلید", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index a759e87c9..3ae00e89f 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -207,6 +207,8 @@ "debit_card_terms": "Việc lưu trữ và sử dụng số thẻ thanh toán của bạn (và thông tin xác thực tương ứng với số thẻ thanh toán của bạn) trong ví điện tử này phải tuân theo Điều khoản và Điều kiện của thỏa thuận chủ thẻ hiện hành với tổ chức phát hành thẻ thanh toán, theo thời gian.", "decimal_places_error": "Quá nhiều chữ số thập phân", "decimals_cannot_be_zero": "Chữ số thập phân không thể là số không.", + "decred_info_card_details": "Decred sử dụng một phương pháp đồng bộ hóa bảo tồn và bảo tồn quyền riêng tư được gọi là SPV SPV, mất nhiều thời gian hơn ví Bitcoin bình thường. Để tìm hiểu thêm, nhấn bên dưới.", + "decred_info_title": "Đồng bộ hóa trong Decred", "default_buy_provider": "Nhà cung cấp Mua mặc định", "default_sell_provider": "Nhà cung cấp Bán mặc định", "delete": "Xóa", @@ -544,6 +546,7 @@ "proceed_after_one_minute": "Nếu màn hình không tiếp tục sau 1 phút, hãy kiểm tra email của bạn.", "proceed_on_device": "Tiếp tục trên thiết bị của bạn", "proceed_on_device_description": "Vui lòng làm theo các hướng dẫn được nhắc trên ví phần cứng của bạn", + "processing": "Xử lý", "profile": "Hồ sơ", "provider_error": "Lỗi ${provider}", "public_key": "Khóa công khai", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 7d9017d0c..3502629fc 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Òfin ti olùṣe àjọrò káàdì ìrajà bójú irú ọ̀nà t'á pamọ́ àti a lo òǹkà ti káàdì ìrajà yín (àti ọ̀rọ̀ ìdánimọ̀ tí káàdì náà) nínú àpamọ́wọ́ yìí.", "decimal_places_error": "Oọ̀rọ̀ ayipada ti o wa ni o dara julọ", "decimals_cannot_be_zero": "Token eleemel ko le jẹ odo.", + "decred_info_card_details": "Devend nlo ọna ti o ni itọju ati ti itọju-itọju-itọju ti o mọ bi \"SPV\", eyiti o gba to gun ju apamọwọ Bitcoin deede. Lati kọ ẹkọ diẹ sii, tẹ ni isalẹ.", + "decred_info_title": "Imuṣiṣẹpọ ni devitered", "default_buy_provider": "Aiyipada Ra Olupese", "default_sell_provider": "Aiyipada Olupese Tita", "delete": "Pa á", @@ -547,6 +549,7 @@ "proceed_after_one_minute": "Tí aṣàfihàn kò bá tẹ̀síwájú l'áàárín ìṣẹ́jú kan, ẹ tọ́ ímeèlì yín wò.", "proceed_on_device": "Tẹsiwaju lori ẹrọ rẹ", "proceed_on_device_description": "Jọwọ tẹle awọn ilana ti a ṣe lori apamọwọ ohun elo rẹ", + "processing": "Iṣaayan", "profile": "profaili", "provider_error": "Àṣìṣe ${provider}", "public_key": "Kọ́kọ́rọ́ tó kò àdáni", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 5e009f841..741266f90 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -208,6 +208,8 @@ "debit_card_terms": "您的支付卡号(以及与您的支付卡号对应的凭证)在此数字钱包中的存储和使用受适用的持卡人与支付卡发卡机构签订的协议的条款和条件的约束,自时不时。", "decimal_places_error": "小数位太多", "decimals_cannot_be_zero": "代币十进制不能为零。", + "decred_info_card_details": "DECRED使用称为“ SPV”的分散和隐私的同步方法,该方法比普通的比特币钱包更长。要了解更多信息,请点击下面。", + "decred_info_title": "在Decred中同步", "default_buy_provider": "默认购买提供商", "default_sell_provider": "默认销售提供商", "delete": "删除", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "如果屏幕在 1 分钟后没有继续,请检查您的电子邮件。", "proceed_on_device": "在设备上继续", "proceed_on_device_description": "请按照您的硬件钱包上提示的说明进行操作", + "processing": "加工", "profile": "轮廓", "provider_error": "${provider} 错误", "public_key": "公钥", diff --git a/scripts/android/.gitignore b/scripts/android/.gitignore index f7e94b7c0..3a2b4c98b 100644 --- a/scripts/android/.gitignore +++ b/scripts/android/.gitignore @@ -1 +1,2 @@ -mwebd \ No newline at end of file +mwebd +decred \ No newline at end of file diff --git a/scripts/android/build_all.sh b/scripts/android/build_all.sh index ad4ec984b..dc730af55 100755 --- a/scripts/android/build_all.sh +++ b/scripts/android/build_all.sh @@ -11,6 +11,7 @@ case $APP_ANDROID_TYPE in "monero.com") $DIR/build_monero_all.sh ;; "cakewallet") $DIR/build_monero_all.sh $DIR/build_haven_all.sh - $DIR/build_mwebd.sh ;; + $DIR/build_mwebd.sh + $DIR/build_decred.sh ;; "haven") $DIR/build_haven_all.sh ;; esac diff --git a/scripts/android/build_decred.sh b/scripts/android/build_decred.sh new file mode 100755 index 000000000..d7b2fdb6b --- /dev/null +++ b/scripts/android/build_decred.sh @@ -0,0 +1,84 @@ +#!/bin/sh + +set -e +cd "$(dirname "$0")" +# . ./config.sh + +CW_DECRED_DIR=$(realpath ../..)/cw_decred +LIBWALLET_PATH="${PWD}/decred/libwallet" +LIBWALLET_URL="https://github.com/decred/libwallet.git" +LIBWALLET_VERSION="87b2769538db3065b334d247b25774593fc6443d" + +if [ -e $LIBWALLET_PATH ]; then + rm -fr $LIBWALLET_PATH/{*,.*} || true +fi +mkdir -p $LIBWALLET_PATH || true + +git clone $LIBWALLET_URL $LIBWALLET_PATH +cd $LIBWALLET_PATH +git checkout $LIBWALLET_VERSION + +if [[ "x$ANDROID_HOME" == "x" ]]; +then + echo "ANDROID_HOME is missing, please declare it before building (on macos it is usually $HOME/Library/Android/sdk)" + echo "echo > ~/.zprofile" + echo "echo 'export ANDROID_HOME=\"\$HOME/Library/Android/sdk\" > ~/.zprofile" + exit 1 +fi + +if [[ "x$ANDROID_NDK_VERSION" == "x" ]]; +then + echo "ANDROID_NDK_VERSION is missing, please declare it before building" + echo "You have these versions installed on your system currently:" + ls ${ANDROID_HOME}/ndk/ | cat | awk '{ print "- " $1 }' + echo "echo > ~/.zprofile" + echo "echo 'export ANDROID_NDK_CERSION=..... > ~/.zprofile" + exit 1 +fi + +export NDK_BIN_PATH="${ANDROID_HOME}/ndk/${ANDROID_NDK_VERSION}/toolchains/llvm/prebuilt/$(uname | tr '[:upper:]' '[:lower:]')-x86_64/bin" +export ANDROID_API_VERSION=21 +# export CPATH="$(clang -v 2>&1 | grep "Selected GCC installation" | rev | cut -d' ' -f1 | rev)/include" + +for arch in "aarch" "aarch64" "x86_64" +do + TRIPLET="" + TARGET="" + ARCH_ABI="" + + case $arch in + "aarch") + TRIPLET="armv7a-linux-androideabi" + TARGET="arm" + ARCH_ABI="armeabi-v7a";; + "aarch64") + TRIPLET="aarch64-linux-android" + TARGET="arm64" + ARCH_ABI="arm64-v8a";; + "x86_64") + TRIPLET="x86_64-linux-android" + TARGET="amd64" + ARCH_ABI="x86_64";; + *) + echo "Unknown arch: $arch" + exit 1;; + esac + + # PATH="${TOOLCHAIN_BASE_DIR}_${arch}/bin:${ORIGINAL_PATH}" + if [ -e ./build ]; then + rm -fr ./build + fi + + CLANG_PATH="${NDK_BIN_PATH}/${TRIPLET}${ANDROID_API_VERSION}-clang" + CGO_ENABLED=1 GOOS=android GOARCH=${TARGET} CC=${CLANG_PATH} CXX=${CLANG_PATH}++ \ + go build -v -buildmode=c-shared -o ./build/${TRIPLET}-libdcrwallet.so ./cgo + + DEST_LIB_DIR=${CW_DECRED_DIR}/android/libs/${ARCH_ABI} + mkdir -p $DEST_LIB_DIR + cp ${LIBWALLET_PATH}/build/${TRIPLET}-libdcrwallet.so $DEST_LIB_DIR/libdcrwallet.so +done + +HEADER_DIR=$CW_DECRED_DIR/lib/api +cp ${LIBWALLET_PATH}/build/${TRIPLET}-libdcrwallet.h $HEADER_DIR/libdcrwallet.h +cd $CW_DECRED_DIR +dart run ffigen diff --git a/scripts/android/pubspec_gen.sh b/scripts/android/pubspec_gen.sh index 5d6a24722..d3341f63d 100755 --- a/scripts/android/pubspec_gen.sh +++ b/scripts/android/pubspec_gen.sh @@ -10,7 +10,7 @@ case $APP_ANDROID_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano" + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred" if [ "$CW_WITH_HAVEN" = true ];then CONFIG_ARGS="$CONFIG_ARGS --haven" fi @@ -26,4 +26,4 @@ flutter pub get dart run tool/generate_pubspec.dart flutter pub get dart run tool/configure.dart $CONFIG_ARGS -cd scripts/android \ No newline at end of file +cd scripts/android diff --git a/scripts/ios/app_config.sh b/scripts/ios/app_config.sh index d118370b5..32d6d5edd 100755 --- a/scripts/ios/app_config.sh +++ b/scripts/ios/app_config.sh @@ -27,18 +27,18 @@ universal_sed "s/PRODUCT_BUNDLE_IDENTIFIER = .*;/PRODUCT_BUNDLE_IDENTIFIER = $AP CONFIG_ARGS="" case $APP_IOS_TYPE in - $MONERO_COM) + $MONERO_COM) CONFIG_ARGS="--monero" ;; + $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano" + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred" if [ "$CW_WITH_HAVEN" = true ];then CONFIG_ARGS="$CONFIG_ARGS --haven" fi ;; + $HAVEN) - - CONFIG_ARGS="--haven" ;; esac diff --git a/scripts/ios/build_all.sh b/scripts/ios/build_all.sh index ba5c55a1f..f5b58d6fa 100755 --- a/scripts/ios/build_all.sh +++ b/scripts/ios/build_all.sh @@ -9,6 +9,6 @@ DIR=$(dirname "$0") case $APP_IOS_TYPE in "monero.com") $DIR/build_monero_all.sh ;; - "cakewallet") $DIR/build_monero_all.sh && $DIR/build_haven.sh && $DIR/build_mwebd.sh ;; + "cakewallet") $DIR/build_monero_all.sh && $DIR/build_haven.sh && $DIR/build_mwebd.sh && $DIR/build_decred.sh ;; "haven") $DIR/build_haven_all.sh ;; esac diff --git a/scripts/ios/build_decred.sh b/scripts/ios/build_decred.sh new file mode 100755 index 000000000..e6b13d0da --- /dev/null +++ b/scripts/ios/build_decred.sh @@ -0,0 +1,35 @@ +#!/bin/sh +set -e +. ./config.sh +LIBWALLET_PATH="${EXTERNAL_IOS_SOURCE_DIR}/libwallet" +LIBWALLET_URL="https://github.com/decred/libwallet.git" +LIBWALLET_VERSION="87b2769538db3065b334d247b25774593fc6443d" + +if [ -e $LIBWALLET_PATH ]; then + rm -fr $LIBWALLET_PATH +fi +mkdir -p $LIBWALLET_PATH +git clone $LIBWALLET_URL $LIBWALLET_PATH +cd $LIBWALLET_PATH +git checkout $LIBWALLET_VERSION + +SYSROOT=`xcrun --sdk iphoneos --show-sdk-path` +CLANG="clang -target arm64-apple-ios -isysroot ${SYSROOT}" +CLANGXX="clang++ -target arm64-apple-ios -isysroot ${SYSROOT}" + +if [ -e ./build ]; then + rm -fr ./build +fi +CGO_ENABLED=1 GOOS=ios GOARCH=arm64 CC=$CLANG CXX=$CLANGXX \ +go build -v -buildmode=c-archive -o ./build/libdcrwallet.a ./cgo || exit 1 + +CW_DECRED_DIR=${CW_ROOT}/cw_decred +HEADER_DIR=$CW_DECRED_DIR/lib/api +mv ${LIBWALLET_PATH}/build/libdcrwallet.h $HEADER_DIR + +DEST_LIB_DIR=${CW_DECRED_DIR}/ios/External/lib +mkdir -p $DEST_LIB_DIR +mv ${LIBWALLET_PATH}/build/libdcrwallet.a $DEST_LIB_DIR + +cd $CW_DECRED_DIR +dart run ffigen diff --git a/scripts/ios/build_zmq.sh b/scripts/ios/build_zmq.sh index e2fd7caae..d72f322a3 100755 --- a/scripts/ios/build_zmq.sh +++ b/scripts/ios/build_zmq.sh @@ -1,4 +1,4 @@ -g#!/bin/sh +#!/bin/sh . ./config.sh diff --git a/scripts/macos/app_config.sh b/scripts/macos/app_config.sh index bb4750803..ad1f04b3e 100755 --- a/scripts/macos/app_config.sh +++ b/scripts/macos/app_config.sh @@ -36,7 +36,7 @@ case $APP_MACOS_TYPE in $MONERO_COM) CONFIG_ARGS="--monero";; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero";; #--haven + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --decred";; #--haven esac cp -rf pubspec_description.yaml pubspec.yaml diff --git a/scripts/macos/build_all.sh b/scripts/macos/build_all.sh index 030617f7d..7929bb719 100755 --- a/scripts/macos/build_all.sh +++ b/scripts/macos/build_all.sh @@ -1,3 +1,3 @@ #!/bin/sh -./build_monero_all.sh universal \ No newline at end of file +./build_monero_all.sh universal && $DIR/build_decred.sh diff --git a/scripts/macos/build_boost_arm64.sh b/scripts/macos/build_boost_arm64.sh index 11f26040f..b5761b7a2 100755 --- a/scripts/macos/build_boost_arm64.sh +++ b/scripts/macos/build_boost_arm64.sh @@ -1,4 +1,5 @@ #!/bin/sh +. ./config.sh . ./build_boost_common.sh build_boost_arm64 \ No newline at end of file diff --git a/scripts/macos/build_boost_common.sh b/scripts/macos/build_boost_common.sh index 0c75be2bd..5aa17bf7c 100755 --- a/scripts/macos/build_boost_common.sh +++ b/scripts/macos/build_boost_common.sh @@ -35,6 +35,10 @@ BOOST_B2_LINKFLAGS_X86_64="-arch x86_64" BOOST_B2_BUILD_DIR_X86_64=macos-x86_64 build_boost_init_common() { + echo " + ============================ BOOST ============================ + " + CXXFLAGS=$1 CFLAGS=$2 LINKFLAGS=$3 @@ -157,8 +161,8 @@ build_boost_compile_universal() { build_boost_install_common() { ARCH=$1 LIB_DIR="" - mkdir $EXTERNAL_MACOS_LIB_DIR - mkdir $EXTERNAL_MACOS_INCLUDE_DIR + mkdir -p $EXTERNAL_MACOS_LIB_DIR + mkdir -p $EXTERNAL_MACOS_INCLUDE_DIR case $ARCH in arm64) LIB_DIR="${BOOST_B2_BUILD_DIR_ARM_64}/stage/lib";; diff --git a/scripts/macos/build_decred.sh b/scripts/macos/build_decred.sh new file mode 100755 index 000000000..d4b7d4d65 --- /dev/null +++ b/scripts/macos/build_decred.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +. ./config.sh + +LIBWALLET_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/libwallet" +LIBWALLET_URL="https://github.com/decred/libwallet.git" +LIBWALLET_VERSION="87b2769538db3065b334d247b25774593fc6443d" + +echo "======================= DECRED LIBWALLET =========================" + +echo "Cloning DECRED LIBWALLET from - $LIBWALLET_URL" +if [ -e $LIBWALLET_PATH ]; then + rm -fr $LIBWALLET_PATH +fi +mkdir -p $LIBWALLET_PATH +git clone $LIBWALLET_URL $LIBWALLET_PATH +cd $LIBWALLET_PATH +git checkout $LIBWALLET_VERSION + +if [ -e ./build ]; then + rm -fr ./build +fi +go build -buildmode=c-archive -o ./build/libdcrwallet.a ./cgo + +CW_DECRED_DIR=${CW_ROOT}/cw_decred +HEADER_DIR=$CW_DECRED_DIR/lib/api +mv ${LIBWALLET_PATH}/build/libdcrwallet.h $HEADER_DIR + +DEST_LIB_DIR=${CW_DECRED_DIR}/macos/External/lib +mkdir -p $DEST_LIB_DIR +mv ${LIBWALLET_PATH}/build/libdcrwallet.a $DEST_LIB_DIR + +cd $CW_DECRED_DIR +dart run ffigen diff --git a/scripts/macos/build_expat.sh b/scripts/macos/build_expat.sh index 0c5857907..8730e6992 100755 --- a/scripts/macos/build_expat.sh +++ b/scripts/macos/build_expat.sh @@ -6,6 +6,10 @@ EXPAT_VERSION=R_2_4_8 EXPAT_HASH="3bab6c09bbe8bf42d84b81563ddbcf4cca4be838" EXPAT_SRC_DIR=${EXTERNAL_MACOS_SOURCE_DIR}/libexpat +echo " +============================ EXPAT ============================ +" + git clone https://github.com/libexpat/libexpat.git -b ${EXPAT_VERSION} ${EXPAT_SRC_DIR} cd $EXPAT_SRC_DIR test `git rev-parse HEAD` = ${EXPAT_HASH} || exit 1 diff --git a/scripts/macos/build_monero.sh b/scripts/macos/build_monero.sh index 1af7ae0e3..a3b0381e3 100755 --- a/scripts/macos/build_monero.sh +++ b/scripts/macos/build_monero.sh @@ -11,6 +11,10 @@ DEST_LIB_DIR=${EXTERNAL_MACOS_LIB_DIR}/monero DEST_INCLUDE_DIR=${EXTERNAL_MACOS_INCLUDE_DIR}/monero ARCH=`uname -m` +echo " +============================ MONERO ============================ +" + echo "Cloning monero from - $MONERO_URL to - $MONERO_DIR_PATH" git clone $MONERO_URL $MONERO_DIR_PATH cd $MONERO_DIR_PATH diff --git a/scripts/macos/build_openssl_arm64.sh b/scripts/macos/build_openssl_arm64.sh index fd8d7b2f5..d320ef7fa 100755 --- a/scripts/macos/build_openssl_arm64.sh +++ b/scripts/macos/build_openssl_arm64.sh @@ -1,4 +1,5 @@ #!/bin/sh +. ./config.sh . ./build_openssl_common.sh build_openssl_arm64 \ No newline at end of file diff --git a/scripts/macos/build_openssl_common.sh b/scripts/macos/build_openssl_common.sh index 27cb1ef8c..1e4ac6f54 100755 --- a/scripts/macos/build_openssl_common.sh +++ b/scripts/macos/build_openssl_common.sh @@ -13,7 +13,9 @@ build_openssl_init_common() { # Use 1.1.1s because of https://github.com/openssl/openssl/issues/18720 OPENSSL_VERSION="1.1.1s" - echo "============================ OpenSSL ============================" + echo " + ============================ OPENSSL ============================ + " cd $EXTERNAL_MACOS_SOURCE_DIR curl -O https://www.openssl.org/source/openssl-$OPENSSL_VERSION.tar.gz diff --git a/scripts/macos/build_sodium.sh b/scripts/macos/build_sodium.sh index 19aad3c97..d754ce8e4 100755 --- a/scripts/macos/build_sodium.sh +++ b/scripts/macos/build_sodium.sh @@ -5,7 +5,9 @@ SODIUM_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/libsodium" SODIUM_URL="https://github.com/jedisct1/libsodium.git" -echo "============================ SODIUM ============================" +echo " +============================ SODIUM ============================ +" echo "Cloning SODIUM from - $SODIUM_URL" git clone $SODIUM_URL $SODIUM_PATH --branch stable diff --git a/scripts/macos/build_unbound.sh b/scripts/macos/build_unbound.sh index ed115d464..6580ebc7c 100755 --- a/scripts/macos/build_unbound.sh +++ b/scripts/macos/build_unbound.sh @@ -7,7 +7,10 @@ UNBOUND_HASH="cbed768b8ff9bfcf11089a5f1699b7e5707f1ea5" UNBOUND_URL="https://www.nlnetlabs.nl/downloads/unbound/unbound-${UNBOUND_VERSION}.tar.gz" UNBOUND_DIR_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/unbound-1.16.2" -echo "============================ Unbound ============================" +echo " +============================ Unbound ============================ +" + rm -rf ${UNBOUND_DIR_PATH} git clone https://github.com/NLnetLabs/unbound.git -b ${UNBOUND_VERSION} ${UNBOUND_DIR_PATH} cd $UNBOUND_DIR_PATH diff --git a/scripts/macos/build_zmq.sh b/scripts/macos/build_zmq.sh index dd5623f06..15c31f248 100755 --- a/scripts/macos/build_zmq.sh +++ b/scripts/macos/build_zmq.sh @@ -5,7 +5,9 @@ ZMQ_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/libzmq" ZMQ_URL="https://github.com/zeromq/libzmq.git" -echo "============================ ZMQ ============================" +echo " +============================ ZMQ ============================ +" echo "Cloning ZMQ from - $ZMQ_URL" git clone $ZMQ_URL $ZMQ_PATH diff --git a/scripts/macos/gen.sh b/scripts/macos/gen.sh new file mode 100755 index 000000000..3d602c130 --- /dev/null +++ b/scripts/macos/gen.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +. ./gen_common.sh + +ARCH=`uname -m` +gen $ARCH \ No newline at end of file diff --git a/scripts/macos/gen_common.sh b/scripts/macos/gen_common.sh index 72ff638b6..95f74e748 100755 --- a/scripts/macos/gen_common.sh +++ b/scripts/macos/gen_common.sh @@ -15,6 +15,8 @@ gen_podspec() { gen_project() { ARCH=$1 CW_DIR="`pwd`/../../macos/Runner.xcodeproj" + BASE_FILENAME="project_base.pbxproj" + BASE_FILE_PATH="${CW_DIR}/${BASE_FILENAME}" DEFAULT_FILENAME="project.pbxproj" DEFAULT_FILE_PATH="${CW_DIR}/${DEFAULT_FILENAME}" universal_sed "s/ARCHS =.*/ARCHS = \"${ARCH}\";/g" $DEFAULT_FILE_PATH @@ -24,4 +26,4 @@ gen() { ARCH=$1 gen_podspec "${ARCH}" gen_project "${ARCH}" -} \ No newline at end of file +} diff --git a/tool/configure.dart b/tool/configure.dart index 259a5d9a0..d1c9326c8 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -11,6 +11,7 @@ const solanaOutputPath = 'lib/solana/solana.dart'; const tronOutputPath = 'lib/tron/tron.dart'; const wowneroOutputPath = 'lib/wownero/wownero.dart'; const zanoOutputPath = 'lib/zano/zano.dart'; +const decredOutputPath = 'lib/decred/decred.dart'; const walletTypesPath = 'lib/wallet_types.g.dart'; const secureStoragePath = 'lib/core/secure_storage.dart'; const pubspecDefaultPath = 'pubspec_default.yaml'; @@ -30,6 +31,7 @@ Future main(List args) async { final hasTron = args.contains('${prefix}tron'); final hasWownero = args.contains('${prefix}wownero'); final hasZano = args.contains('${prefix}zano'); + final hasDecred = args.contains('${prefix}decred'); final excludeFlutterSecureStorage = args.contains('${prefix}excludeFlutterSecureStorage'); await generateBitcoin(hasBitcoin); @@ -44,6 +46,7 @@ Future main(List args) async { await generateWownero(hasWownero); await generateZano(hasZano); // await generateBanano(hasEthereum); + await generateDecred(hasDecred); await generatePubspec( hasMonero: hasMonero, @@ -59,6 +62,7 @@ Future main(List args) async { hasTron: hasTron, hasWownero: hasWownero, hasZano: hasZano, + hasDecred: hasDecred, ); await generateWalletTypes( hasMonero: hasMonero, @@ -73,6 +77,7 @@ Future main(List args) async { hasTron: hasTron, hasWownero: hasWownero, hasZano: hasZano, + hasDecred: hasDecred, ); await injectSecureStorage(!excludeFlutterSecureStorage); } @@ -1093,12 +1098,15 @@ abstract class BitcoinCash { """; const bitcoinCashEmptyDefinition = 'BitcoinCash? bitcoinCash;\n'; - const bitcoinCashCWDefinition = 'BitcoinCash? bitcoinCash = CWBitcoinCash();\n'; + const bitcoinCashCWDefinition = + 'BitcoinCash? bitcoinCash = CWBitcoinCash();\n'; final output = '$bitcoinCashCommonHeaders\n' + (hasImplementation ? '$bitcoinCashCWHeaders\n' : '\n') + (hasImplementation ? '$bitcoinCashCwPart\n\n' : '\n') + - (hasImplementation ? bitcoinCashCWDefinition : bitcoinCashEmptyDefinition) + + (hasImplementation + ? bitcoinCashCWDefinition + : bitcoinCashEmptyDefinition) + '\n' + bitcoinCashContent; @@ -1233,7 +1241,8 @@ abstract class NanoUtil { """; const nanoEmptyDefinition = 'Nano? nano;\nNanoUtil? nanoUtil;\n'; - const nanoCWDefinition = 'Nano? nano = CWNano();\nNanoUtil? nanoUtil = CWNanoUtil();\n'; + const nanoCWDefinition = + 'Nano? nano = CWNano();\nNanoUtil? nanoUtil = CWNanoUtil();\n'; final output = '$nanoCommonHeaders\n' + (hasImplementation ? '$nanoCWHeaders\n' : '\n') + @@ -1482,6 +1491,85 @@ abstract class Zano { await outputFile.writeAsString(output); } +Future generateDecred(bool hasImplementation) async { + final outputFile = File(decredOutputPath); + const decredCommonHeaders = """ +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/address_info.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/output_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/unspent_transaction_output.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:hive/hive.dart'; +"""; + const decredCWHeaders = """ +import 'package:cw_decred/transaction_priority.dart'; +import 'package:cw_decred/wallet.dart'; +import 'package:cw_decred/wallet_service.dart'; +import 'package:cw_decred/wallet_creation_credentials.dart'; +import 'package:cw_decred/amount_format.dart'; +import 'package:cw_decred/transaction_credentials.dart'; +import 'package:cw_decred/mnemonic.dart'; +"""; + const decredCwPart = "part 'cw_decred.dart';"; + const decredContent = """ + +abstract class Decred { + WalletCredentials createDecredNewWalletCredentials( + {required String name, WalletInfo? walletInfo}); + WalletCredentials createDecredRestoreWalletFromSeedCredentials( + {required String name, required String mnemonic, required String password}); + WalletCredentials createDecredRestoreWalletFromPubkeyCredentials( + {required String name, required String pubkey, required String password}); + WalletService createDecredWalletService( + Box walletInfoSource, Box unspentCoinSource); + + List getTransactionPriorities(); + TransactionPriority getDecredTransactionPriorityMedium(); + TransactionPriority getDecredTransactionPrioritySlow(); + TransactionPriority deserializeDecredTransactionPriority(int raw); + + Object createDecredTransactionCredentials(List outputs, TransactionPriority priority); + + List getAddressInfos(Object wallet); + Future updateAddress(Object wallet, String address, String label); + Future generateNewAddress(Object wallet, String label); + + String formatterDecredAmountToString({required int amount}); + double formatterDecredAmountToDouble({required int amount}); + int formatterStringDoubleToDecredAmount(String amount); + + List getUnspents(Object wallet); + void updateUnspents(Object wallet); + + int heightByDate(DateTime date); + + List getDecredWordList(); + + String pubkey(Object wallet); +} +"""; + + const decredEmptyDefinition = 'Decred? decred;\n'; + const decredCWDefinition = 'Decred? decred = CWDecred();\n'; + + final output = '$decredCommonHeaders\n' + + (hasImplementation ? '$decredCWHeaders\n' : '\n') + + (hasImplementation ? '$decredCwPart\n\n' : '\n') + + (hasImplementation ? decredCWDefinition : decredEmptyDefinition) + + '\n' + + decredContent; + + if (outputFile.existsSync()) { + await outputFile.delete(); + } + + await outputFile.writeAsString(output); +} + Future generatePubspec({ required bool hasMonero, required bool hasBitcoin, @@ -1496,6 +1584,7 @@ Future generatePubspec({ required bool hasTron, required bool hasWownero, required bool hasZano, + required bool hasDecred, }) async { const cwCore = """ cw_core: @@ -1564,6 +1653,10 @@ Future generatePubspec({ cw_zano: path: ./cw_zano """; + const cwDecred = """ + cw_decred: + path: ./cw_decred + """; final inputFile = File(pubspecOutputPath); final inputText = await inputFile.readAsString(); final inputLines = inputText.split('\n'); @@ -1613,6 +1706,10 @@ Future generatePubspec({ output += '\n$cwSharedExternal\n$cwHaven'; } + if (hasDecred) { + output += '\n$cwDecred'; + } + if (hasFlutterSecureStorage) { output += '\n$flutterSecureStorage\n'; } @@ -1654,6 +1751,7 @@ Future generateWalletTypes({ required bool hasTron, required bool hasWownero, required bool hasZano, + required bool hasDecred, }) async { final walletTypesFile = File(walletTypesPath); @@ -1709,6 +1807,10 @@ Future generateWalletTypes({ outputContent += '\tWalletType.banano,\n'; } + if (hasDecred) { + outputContent += '\tWalletType.decred,\n'; + } + if (hasWownero) { outputContent += '\tWalletType.wownero,\n'; } From a34cf248978f34647e7dd4111f60514cabc19db5 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Fri, 21 Mar 2025 04:52:05 +0200 Subject: [PATCH 048/243] Remove haven (#2085) * decred: Add decred. (#1322) * multi: Add initial decred screens. (#1165) Use a mock libwallet for now. * cw_decred: add libdcrwallet dependency and link library for android, ios and macos (#1240) * change cw_decred from package to plugin * add libdcrwallet dependency and link library for android, ios and macos * remove spvwallet, make some libdcrwallet fns async, light refactor * libdcrwallet: use json payload returns * use specific libwallet commit hash * decred: fix Rename wallet. --------- Co-authored-by: JoeGruff * decred: Add sync. * decred: Add send transaction. * decred: Fix fee estimation. * decred: List transactions. * decred: Add rescan. * decred: Sign message. * decred: Add new addr and addrs. * decred: Add change wallet pass. * decred: Add restore from seed. * decred: Add watching only wallets. * decred: Enable mainnet. * decred: Allow using blank node address. This allows a persistent peer to be unset, falling back to decred seeders. * decred: Rescan from wallet birthday. * add and update macos build scripts, update build readme, gitignore macos project.pbxproj Signed-off-by: Philemon Ukane * multi: hide decred rescan page if it's not ready - move hasRescan method to WalletBase and implement for decred Signed-off-by: Philemon Ukane * cw_decred: fix bug where decred wallets are not loaded after app restart Signed-off-by: Philemon Ukane * add buy and sell for decred via onramp Signed-off-by: Philemon Ukane * bug-fix: account for other send outputs that are part of the same tx Signed-off-by: Philemon Ukane * decred: Return address with no peers. * decred: Update pubspec. * decred: Add verify message. * upgrade hive_generator dep in cw_decred * decred: Clean up code. --------- Signed-off-by: Philemon Ukane Co-authored-by: Wisdom Arerosuoghene Co-authored-by: Philemon Ukane * fix extracted addresses not used fix conflicts with main * remove print [skip ci] * minor formatting * fix initial migration version * add build decred script to workflow * install go before build decred fix switch cases * trial 2 to fix decred build * re-install go * revert build script change * refactor/clean nodes functions * Fix address book issue Fix send ALL (to be continued with the fees point) * Fix transactions display issues Add missing file * Fix unconfirmed balance not displayed Change Wallet order Minor cleanup * Fix workflow * Fix workflow * Fix workflow * test * hardcode path for now * fix + cleanup decred build script to work on mac and linux * Update decred build script * Run actions on pull requests, extract commit message * run after checkout * add safe directory * Get commit message from base.sha instead of last commit * base -> head * Do not merge main branch into pr * [skip slack] [run tests] clone by sha * Proper name for decred library in the build script * Throw an error when ANDROID_HOME or ANDROID_NDK_VERSION is missing * Fix conflicts with main * minor code enhancement * decred: Add used address history. (#1941) * decred: Update pubspec. * decred testnet * decred: Add used address history. * decred: Remove default node list. * populate transaction history before sync begins * decred: Add some awaits. * decred: Fix send all. * decred: Add clang export to build script. * decred: Update logo colors. * cleanup cw_decred.dart * make decred wallet addresses selectable in receive page * decred: Always set default addr when used. * decred: Add back default node list. * decred: Allow creating addresses manually. --------- Co-authored-by: Wisdom Arerosuoghene Co-authored-by: Omar Hatem * minor fixes and cleanup * minor fix, feel free to test now * - Fix transaction details - Fix Nodes - Add processing sync status * Add decred info card * push missing file * Add missing text for decred info card * minor: change docs link [skip ci] * decred: Update derivation info. (#2013) * decred: Update derivation info. * decred: Allow unsynced unused addresses. * decred: Update dcrwallet dep to 4.3.0. * Merge main and fix conflicts * Merge main and fix conflicts * decred: Fix background sync panic. (#2080) * decred: Run libwallet in isolate. (#2077) * decred: Fix contact save inquiry. (#2083) Also fix tx time and the fee shown on pending transactions. * Disable send button in view only decred wallets * - Fix frozen coins - Add URI support - Fix fees in tx details - Handle empty coins send - Handle wallets in address book * Merge main * remove print [skip ci] * Fix restore from QR * minor improvement for QR restore * Remove Haven Wallet * Remove haven scripts * minor fixes [skip ci] * decred: Get slip44 addrs before sync completes. (#2092) * - Fix loading wallet more than one time - Fix minor UI issue * fix merge issue * fix merge issue --------- Signed-off-by: Philemon Ukane Co-authored-by: JoeGruffins <34998433+JoeGruffins@users.noreply.github.com> Co-authored-by: Wisdom Arerosuoghene Co-authored-by: Philemon Ukane Co-authored-by: Czarek Nakamoto --- cw_haven/.gitignore | 7 - cw_haven/.metadata | 10 - cw_haven/CHANGELOG.md | 3 - cw_haven/LICENSE | 1 - cw_haven/README.md | 15 - cw_haven/android/.gitignore | 8 - cw_haven/android/CMakeLists.txt | 220 --- cw_haven/android/build.gradle | 56 - cw_haven/android/gradle.properties | 3 - .../gradle/wrapper/gradle-wrapper.properties | 5 - cw_haven/android/settings.gradle | 1 - cw_haven/android/src/main/AndroidManifest.xml | 3 - .../com/cakewallet/cw_haven/CwHavenPlugin.kt | 36 - cw_haven/ios/.gitignore | 37 - cw_haven/ios/Assets/.gitkeep | 0 cw_haven/ios/Classes/CwHavenPlugin.h | 4 - cw_haven/ios/Classes/CwHavenPlugin.m | 15 - cw_haven/ios/Classes/SwiftCwHavenPlugin.swift | 14 - cw_haven/ios/Classes/haven_api.cpp | 942 ---------- cw_haven/ios/cw_haven.podspec | 50 - cw_haven/lib/api/account_list.dart | 81 - cw_haven/lib/api/asset_types.dart | 23 - cw_haven/lib/api/balance_list.dart | 58 - cw_haven/lib/api/convert_utf8_to_string.dart | 8 - cw_haven/lib/api/cw_haven.dart | 14 - .../connection_to_node_exception.dart | 5 - .../creation_transaction_exception.dart | 8 - .../exceptions/setup_wallet_exception.dart | 10 - .../exceptions/wallet_creation_exception.dart | 8 - .../exceptions/wallet_opening_exception.dart | 8 - .../wallet_restore_from_keys_exception.dart | 10 - .../wallet_restore_from_seed_exception.dart | 5 - cw_haven/lib/api/haven_api.dart | 6 - cw_haven/lib/api/monero_output.dart | 8 - cw_haven/lib/api/signatures.dart | 144 -- cw_haven/lib/api/structs/account_row.dart | 12 - .../lib/api/structs/haven_balance_row.dart | 12 - cw_haven/lib/api/structs/haven_rate.dart | 12 - .../lib/api/structs/pending_transaction.dart | 27 - cw_haven/lib/api/structs/subaddress_row.dart | 15 - .../lib/api/structs/transaction_info_row.dart | 44 - cw_haven/lib/api/structs/ut8_box.dart | 8 - cw_haven/lib/api/subaddress_list.dart | 97 - cw_haven/lib/api/transaction_history.dart | 246 --- cw_haven/lib/api/types.dart | 142 -- cw_haven/lib/api/wallet.dart | 376 ---- cw_haven/lib/api/wallet_manager.dart | 248 --- cw_haven/lib/haven_account_list.dart | 85 - cw_haven/lib/haven_balance.dart | 34 - cw_haven/lib/haven_subaddress_list.dart | 87 - ...aven_transaction_creation_credentials.dart | 13 - .../haven_transaction_creation_exception.dart | 8 - cw_haven/lib/haven_transaction_history.dart | 27 - cw_haven/lib/haven_transaction_info.dart | 73 - cw_haven/lib/haven_wallet.dart | 435 ----- cw_haven/lib/haven_wallet_addresses.dart | 96 - cw_haven/lib/haven_wallet_service.dart | 259 --- .../lib/mnemonics/chinese_simplified.dart | 1630 ----------------- cw_haven/lib/mnemonics/dutch.dart | 1630 ----------------- cw_haven/lib/mnemonics/english.dart | 1630 ----------------- cw_haven/lib/mnemonics/french.dart | 1630 ----------------- cw_haven/lib/mnemonics/german.dart | 1630 ----------------- cw_haven/lib/mnemonics/italian.dart | 1630 ----------------- cw_haven/lib/mnemonics/japanese.dart | 1630 ----------------- cw_haven/lib/mnemonics/portuguese.dart | 1630 ----------------- cw_haven/lib/mnemonics/russian.dart | 1630 ----------------- cw_haven/lib/mnemonics/spanish.dart | 1630 ----------------- cw_haven/lib/pending_haven_transaction.dart | 56 - cw_haven/lib/update_haven_rate.dart | 15 - cw_haven/pubspec.lock | 789 -------- cw_haven/pubspec.yaml | 81 - integration_test/helpers/test_helpers.dart | 1 - .../robots/create_pin_welcome_page_robot.dart | 8 - .../restore_from_seed_or_key_robot.dart | 2 - .../test_suites/confirm_seeds_flow_test.dart | 2 - ...estore_wallet_through_seeds_flow_test.dart | 2 - lib/anypay/any_pay_payment.dart | 1 - .../any_pay_payment_committed_info.dart | 1 - lib/anypay/any_pay_payment_instruction.dart | 1 - lib/buy/buy_amount.dart | 1 - lib/buy/buy_exception.dart | 2 - lib/buy/get_buy_provider_icon.dart | 18 +- lib/core/monero_account_label_validator.dart | 2 - lib/core/seed_validator.dart | 4 +- lib/di.dart | 9 +- lib/entities/default_settings_migration.dart | 11 +- lib/entities/priority_for_wallet_type.dart | 6 +- lib/entities/qr_scanner.dart | 2 - lib/entities/seed_phrase_length.dart | 1 - lib/entities/update_haven_rate.dart | 26 - lib/reactions/fiat_rate_update.dart | 15 +- lib/reactions/on_current_wallet_change.dart | 5 - .../on_wallet_sync_status_change.dart | 6 - .../screens/buy/widgets/buy_list_item.dart | 1 - .../welcome/create_pin_welcome_page.dart | 8 - .../dashboard/dashboard_view_model.dart | 12 +- .../dashboard/transaction_list_item.dart | 9 +- ...ero_account_edit_or_create_view_model.dart | 39 +- .../monero_account_list_view_model.dart | 18 - lib/view_model/send/output.dart | 15 +- lib/view_model/send/send_view_model.dart | 8 - ...let_address_edit_or_create_view_model.dart | 13 - .../wallet_address_list_view_model.dart | 22 - lib/view_model/wallet_keys_view_model.dart | 12 +- lib/view_model/wallet_new_vm.dart | 12 +- lib/view_model/wallet_restore_view_model.dart | 16 +- lib/wallet_type_utils.dart | 9 - model_generator.sh | 2 +- scripts/android/app_env.sh | 8 +- scripts/android/app_icon.sh | 5 - scripts/android/build_all.sh | 2 - scripts/android/build_haven.sh | 70 - scripts/android/build_haven_all.sh | 8 - scripts/android/pubspec_gen.sh | 6 - scripts/docker/Dockerfile | 4 - scripts/docker/build_all.sh | 32 +- scripts/docker/build_haven.sh | 71 - scripts/docker/build_haven_all.sh | 9 - scripts/docker/copy_haven_deps.sh | 46 - scripts/docker/entrypoint.sh | 2 - scripts/ios/app_config.sh | 8 - scripts/ios/app_env.sh | 7 +- scripts/ios/build_all.sh | 3 +- scripts/ios/build_haven.sh | 64 - scripts/ios/build_haven_all.sh | 9 - scripts/macos/app_config.sh | 2 +- scripts/macos/build_haven.sh | 50 - tool/configure.dart | 211 --- 128 files changed, 52 insertions(+), 22310 deletions(-) delete mode 100644 cw_haven/.gitignore delete mode 100644 cw_haven/.metadata delete mode 100644 cw_haven/CHANGELOG.md delete mode 100644 cw_haven/LICENSE delete mode 100644 cw_haven/README.md delete mode 100644 cw_haven/android/.gitignore delete mode 100644 cw_haven/android/CMakeLists.txt delete mode 100644 cw_haven/android/build.gradle delete mode 100644 cw_haven/android/gradle.properties delete mode 100644 cw_haven/android/gradle/wrapper/gradle-wrapper.properties delete mode 100644 cw_haven/android/settings.gradle delete mode 100644 cw_haven/android/src/main/AndroidManifest.xml delete mode 100644 cw_haven/android/src/main/kotlin/com/cakewallet/cw_haven/CwHavenPlugin.kt delete mode 100644 cw_haven/ios/.gitignore delete mode 100644 cw_haven/ios/Assets/.gitkeep delete mode 100644 cw_haven/ios/Classes/CwHavenPlugin.h delete mode 100644 cw_haven/ios/Classes/CwHavenPlugin.m delete mode 100644 cw_haven/ios/Classes/SwiftCwHavenPlugin.swift delete mode 100644 cw_haven/ios/Classes/haven_api.cpp delete mode 100644 cw_haven/ios/cw_haven.podspec delete mode 100644 cw_haven/lib/api/account_list.dart delete mode 100644 cw_haven/lib/api/asset_types.dart delete mode 100644 cw_haven/lib/api/balance_list.dart delete mode 100644 cw_haven/lib/api/convert_utf8_to_string.dart delete mode 100644 cw_haven/lib/api/cw_haven.dart delete mode 100644 cw_haven/lib/api/exceptions/connection_to_node_exception.dart delete mode 100644 cw_haven/lib/api/exceptions/creation_transaction_exception.dart delete mode 100644 cw_haven/lib/api/exceptions/setup_wallet_exception.dart delete mode 100644 cw_haven/lib/api/exceptions/wallet_creation_exception.dart delete mode 100644 cw_haven/lib/api/exceptions/wallet_opening_exception.dart delete mode 100644 cw_haven/lib/api/exceptions/wallet_restore_from_keys_exception.dart delete mode 100644 cw_haven/lib/api/exceptions/wallet_restore_from_seed_exception.dart delete mode 100644 cw_haven/lib/api/haven_api.dart delete mode 100644 cw_haven/lib/api/monero_output.dart delete mode 100644 cw_haven/lib/api/signatures.dart delete mode 100644 cw_haven/lib/api/structs/account_row.dart delete mode 100644 cw_haven/lib/api/structs/haven_balance_row.dart delete mode 100644 cw_haven/lib/api/structs/haven_rate.dart delete mode 100644 cw_haven/lib/api/structs/pending_transaction.dart delete mode 100644 cw_haven/lib/api/structs/subaddress_row.dart delete mode 100644 cw_haven/lib/api/structs/transaction_info_row.dart delete mode 100644 cw_haven/lib/api/structs/ut8_box.dart delete mode 100644 cw_haven/lib/api/subaddress_list.dart delete mode 100644 cw_haven/lib/api/transaction_history.dart delete mode 100644 cw_haven/lib/api/types.dart delete mode 100644 cw_haven/lib/api/wallet.dart delete mode 100644 cw_haven/lib/api/wallet_manager.dart delete mode 100644 cw_haven/lib/haven_account_list.dart delete mode 100644 cw_haven/lib/haven_balance.dart delete mode 100644 cw_haven/lib/haven_subaddress_list.dart delete mode 100644 cw_haven/lib/haven_transaction_creation_credentials.dart delete mode 100644 cw_haven/lib/haven_transaction_creation_exception.dart delete mode 100644 cw_haven/lib/haven_transaction_history.dart delete mode 100644 cw_haven/lib/haven_transaction_info.dart delete mode 100644 cw_haven/lib/haven_wallet.dart delete mode 100644 cw_haven/lib/haven_wallet_addresses.dart delete mode 100644 cw_haven/lib/haven_wallet_service.dart delete mode 100644 cw_haven/lib/mnemonics/chinese_simplified.dart delete mode 100644 cw_haven/lib/mnemonics/dutch.dart delete mode 100644 cw_haven/lib/mnemonics/english.dart delete mode 100644 cw_haven/lib/mnemonics/french.dart delete mode 100644 cw_haven/lib/mnemonics/german.dart delete mode 100644 cw_haven/lib/mnemonics/italian.dart delete mode 100644 cw_haven/lib/mnemonics/japanese.dart delete mode 100644 cw_haven/lib/mnemonics/portuguese.dart delete mode 100644 cw_haven/lib/mnemonics/russian.dart delete mode 100644 cw_haven/lib/mnemonics/spanish.dart delete mode 100644 cw_haven/lib/pending_haven_transaction.dart delete mode 100644 cw_haven/lib/update_haven_rate.dart delete mode 100644 cw_haven/pubspec.lock delete mode 100644 cw_haven/pubspec.yaml delete mode 100644 lib/entities/update_haven_rate.dart delete mode 100755 scripts/android/build_haven.sh delete mode 100755 scripts/android/build_haven_all.sh delete mode 100755 scripts/docker/build_haven.sh delete mode 100755 scripts/docker/build_haven_all.sh delete mode 100755 scripts/docker/copy_haven_deps.sh delete mode 100755 scripts/ios/build_haven.sh delete mode 100755 scripts/ios/build_haven_all.sh delete mode 100755 scripts/macos/build_haven.sh diff --git a/cw_haven/.gitignore b/cw_haven/.gitignore deleted file mode 100644 index e9dc58d3d..000000000 --- a/cw_haven/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -.DS_Store -.dart_tool/ - -.packages -.pub/ - -build/ diff --git a/cw_haven/.metadata b/cw_haven/.metadata deleted file mode 100644 index cb1a29e7c..000000000 --- a/cw_haven/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: 4d7946a68d26794349189cf21b3f68cc6fe61dcb - channel: stable - -project_type: plugin diff --git a/cw_haven/CHANGELOG.md b/cw_haven/CHANGELOG.md deleted file mode 100644 index 41cc7d819..000000000 --- a/cw_haven/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -## 0.0.1 - -* TODO: Describe initial release. diff --git a/cw_haven/LICENSE b/cw_haven/LICENSE deleted file mode 100644 index ba75c69f7..000000000 --- a/cw_haven/LICENSE +++ /dev/null @@ -1 +0,0 @@ -TODO: Add your license here. diff --git a/cw_haven/README.md b/cw_haven/README.md deleted file mode 100644 index 150aebc42..000000000 --- a/cw_haven/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# cw_haven - -A new flutter plugin project. - -## Getting Started - -This project is a starting point for a Flutter -[plug-in package](https://flutter.dev/developing-packages/), -a specialized package that includes platform-specific implementation code for -Android and/or iOS. - -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. - diff --git a/cw_haven/android/.gitignore b/cw_haven/android/.gitignore deleted file mode 100644 index c6cbe562a..000000000 --- a/cw_haven/android/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures diff --git a/cw_haven/android/CMakeLists.txt b/cw_haven/android/CMakeLists.txt deleted file mode 100644 index 04faca0e6..000000000 --- a/cw_haven/android/CMakeLists.txt +++ /dev/null @@ -1,220 +0,0 @@ -cmake_minimum_required(VERSION 3.4.1) - -add_library( cw_haven - SHARED - ../ios/Classes/haven_api.cpp) - - find_library( log-lib log ) - -set(EXTERNAL_LIBS_DIR ${CMAKE_SOURCE_DIR}/../../cw_shared_external/ios/External/android) - -############ -# libsodium -############ - -add_library(sodium STATIC IMPORTED) -set_target_properties(sodium PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libsodium.a) - -############ -# OpenSSL -############ - -add_library(crypto STATIC IMPORTED) -set_target_properties(crypto PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libcrypto.a) - -add_library(ssl STATIC IMPORTED) -set_target_properties(ssl PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libssl.a) - -############ -# Boost -############ - -add_library(boost_chrono STATIC IMPORTED) -set_target_properties(boost_chrono PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_chrono.a) - -add_library(boost_date_time STATIC IMPORTED) -set_target_properties(boost_date_time PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_date_time.a) - -add_library(boost_filesystem STATIC IMPORTED) -set_target_properties(boost_filesystem PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_filesystem.a) - -add_library(boost_program_options STATIC IMPORTED) -set_target_properties(boost_program_options PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_program_options.a) - -add_library(boost_regex STATIC IMPORTED) -set_target_properties(boost_regex PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_regex.a) - -add_library(boost_serialization STATIC IMPORTED) -set_target_properties(boost_serialization PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_serialization.a) - -add_library(boost_system STATIC IMPORTED) -set_target_properties(boost_system PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_system.a) - -add_library(boost_thread STATIC IMPORTED) -set_target_properties(boost_thread PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_thread.a) - -add_library(boost_wserialization STATIC IMPORTED) -set_target_properties(boost_wserialization PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_wserialization.a) - -############# -# Haven -############# - -add_library(wallet_api STATIC IMPORTED) -set_target_properties(wallet_api PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libwallet_api.a) - -add_library(wallet STATIC IMPORTED) -set_target_properties(wallet PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libwallet.a) - -add_library(cryptonote_core STATIC IMPORTED) -set_target_properties(cryptonote_core PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libcryptonote_core.a) - -add_library(cryptonote_basic STATIC IMPORTED) -set_target_properties(cryptonote_basic PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libcryptonote_basic.a) - -add_library(mnemonics STATIC IMPORTED) -set_target_properties(mnemonics PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libmnemonics.a) - -add_library(common STATIC IMPORTED) -set_target_properties(common PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libcommon.a) - -add_library(cncrypto STATIC IMPORTED) -set_target_properties(cncrypto PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libcncrypto.a) - -add_library(ringct STATIC IMPORTED) -set_target_properties(ringct PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libringct.a) - -add_library(ringct_basic STATIC IMPORTED) -set_target_properties(ringct_basic PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libringct_basic.a) - -add_library(blockchain_db STATIC IMPORTED) -set_target_properties(blockchain_db PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libblockchain_db.a) - -add_library(lmdb STATIC IMPORTED) -set_target_properties(lmdb PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/liblmdb.a) - -add_library(easylogging STATIC IMPORTED) -set_target_properties(easylogging PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libeasylogging.a) - -add_library(unbound STATIC IMPORTED) -set_target_properties(unbound PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libunbound.a) - -add_library(epee STATIC IMPORTED) -set_target_properties(epee PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libepee.a) - -add_library(checkpoints STATIC IMPORTED) -set_target_properties(checkpoints PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libcheckpoints.a) - -add_library(device STATIC IMPORTED) -set_target_properties(device PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libdevice.a) - -add_library(device_trezor STATIC IMPORTED) -set_target_properties(device_trezor PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libdevice_trezor.a) - -add_library(multisig STATIC IMPORTED) -set_target_properties(multisig PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libmultisig.a) - -add_library(version STATIC IMPORTED) -set_target_properties(version PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libversion.a) - -add_library(net STATIC IMPORTED) -set_target_properties(net PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libnet.a) - -add_library(hardforks STATIC IMPORTED) -set_target_properties(hardforks PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libhardforks.a) - -add_library(randomx STATIC IMPORTED) -set_target_properties(randomx PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/librandomx.a) - -add_library(offshore STATIC IMPORTED) -set_target_properties(offshore PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/liboffshore.a) - - -add_library(rpc_base STATIC IMPORTED) -set_target_properties(rpc_base PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/librpc_base.a) - -add_library(wallet-crypto STATIC IMPORTED) -set_target_properties(wallet-crypto PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libwallet-crypto.a) - -include_directories( ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/include ) - -target_link_libraries( cw_haven - - wallet_api - wallet - cryptonote_core - cryptonote_basic - mnemonics - ringct - ringct_basic - net - common - cncrypto - blockchain_db - lmdb - easylogging - unbound - epee - checkpoints - device - device_trezor - multisig - version - randomx - offshore - hardforks - rpc_base - - boost_chrono - boost_date_time - boost_filesystem - boost_program_options - boost_regex - boost_serialization - boost_system - boost_thread - boost_wserialization - - ssl - crypto - - sodium - - ${log-lib} ) \ No newline at end of file diff --git a/cw_haven/android/build.gradle b/cw_haven/android/build.gradle deleted file mode 100644 index 8eb728a67..000000000 --- a/cw_haven/android/build.gradle +++ /dev/null @@ -1,56 +0,0 @@ -group 'com.cakewallet.cw_haven' -version '1.0-SNAPSHOT' - -buildscript { - ext.kotlin_version = '2.0.21' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:8.7.1' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' - -android { - compileSdkVersion 33 - - if (project.android.hasProperty("namespace")) { - namespace 'com.cakewallet.cw_haven' - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = '17' - } - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - defaultConfig { - minSdkVersion 21 - } - externalNativeBuild { - cmake { - } - } -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} diff --git a/cw_haven/android/gradle.properties b/cw_haven/android/gradle.properties deleted file mode 100644 index 94adc3a3f..000000000 --- a/cw_haven/android/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.useAndroidX=true -android.enableJetifier=true diff --git a/cw_haven/android/gradle/wrapper/gradle-wrapper.properties b/cw_haven/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 3c9d0852b..000000000 --- a/cw_haven/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/cw_haven/android/settings.gradle b/cw_haven/android/settings.gradle deleted file mode 100644 index 2a0a2fea8..000000000 --- a/cw_haven/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'cw_haven' diff --git a/cw_haven/android/src/main/AndroidManifest.xml b/cw_haven/android/src/main/AndroidManifest.xml deleted file mode 100644 index b72b21d5d..000000000 --- a/cw_haven/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/cw_haven/android/src/main/kotlin/com/cakewallet/cw_haven/CwHavenPlugin.kt b/cw_haven/android/src/main/kotlin/com/cakewallet/cw_haven/CwHavenPlugin.kt deleted file mode 100644 index b31493c6e..000000000 --- a/cw_haven/android/src/main/kotlin/com/cakewallet/cw_haven/CwHavenPlugin.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.cakewallet.cw_haven - -import androidx.annotation.NonNull - -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result -import io.flutter.plugin.common.PluginRegistry.Registrar - -/** CwHavenPlugin */ -class CwHavenPlugin: FlutterPlugin, MethodCallHandler { - /// The MethodChannel that will the communication between Flutter and native Android - /// - /// This local reference serves to register the plugin with the Flutter Engine and unregister it - /// when the Flutter Engine is detached from the Activity - private lateinit var channel : MethodChannel - - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - channel = MethodChannel(flutterPluginBinding.binaryMessenger, "cw_haven") - channel.setMethodCallHandler(this) - } - - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { - if (call.method == "getPlatformVersion") { - result.success("Android ${android.os.Build.VERSION.RELEASE}") - } else { - result.notImplemented() - } - } - - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) - } -} diff --git a/cw_haven/ios/.gitignore b/cw_haven/ios/.gitignore deleted file mode 100644 index aa479fd3c..000000000 --- a/cw_haven/ios/.gitignore +++ /dev/null @@ -1,37 +0,0 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -.generated/ - -*.pbxuser -*.mode1v3 -*.mode2v3 -*.perspectivev3 - -!default.pbxuser -!default.mode1v3 -!default.mode2v3 -!default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/Generated.xcconfig -/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/cw_haven/ios/Assets/.gitkeep b/cw_haven/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/cw_haven/ios/Classes/CwHavenPlugin.h b/cw_haven/ios/Classes/CwHavenPlugin.h deleted file mode 100644 index 8a4195232..000000000 --- a/cw_haven/ios/Classes/CwHavenPlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface CwHavenPlugin : NSObject -@end diff --git a/cw_haven/ios/Classes/CwHavenPlugin.m b/cw_haven/ios/Classes/CwHavenPlugin.m deleted file mode 100644 index 4683f4b68..000000000 --- a/cw_haven/ios/Classes/CwHavenPlugin.m +++ /dev/null @@ -1,15 +0,0 @@ -#import "CwHavenPlugin.h" -#if __has_include() -#import -#else -// Support project import fallback if the generated compatibility header -// is not copied when this plugin is created as a library. -// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 -#import "cw_haven-Swift.h" -#endif - -@implementation CwHavenPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - [SwiftCwHavenPlugin registerWithRegistrar:registrar]; -} -@end diff --git a/cw_haven/ios/Classes/SwiftCwHavenPlugin.swift b/cw_haven/ios/Classes/SwiftCwHavenPlugin.swift deleted file mode 100644 index ddee88ae7..000000000 --- a/cw_haven/ios/Classes/SwiftCwHavenPlugin.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Flutter -import UIKit - -public class SwiftCwHavenPlugin: NSObject, FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "cw_haven", binaryMessenger: registrar.messenger()) - let instance = SwiftCwHavenPlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - result("iOS " + UIDevice.current.systemVersion) - } -} diff --git a/cw_haven/ios/Classes/haven_api.cpp b/cw_haven/ios/Classes/haven_api.cpp deleted file mode 100644 index aecaf0016..000000000 --- a/cw_haven/ios/Classes/haven_api.cpp +++ /dev/null @@ -1,942 +0,0 @@ -#include -#include "cstdlib" -#include -#include -#include -#include -#include -#include "thread" -#if __APPLE__ -// Fix for randomx on ios -void __clear_cache(void* start, void* end) { } -#include "../External/ios/include/wallet2_api.h" -#else -#include "../External/android/include/wallet2_api.h" -#endif - -using namespace std::chrono_literals; - -#ifdef __cplusplus -extern "C" -{ -#endif - const uint64_t MONERO_BLOCK_SIZE = 1000; - - struct Utf8Box - { - char *value; - - Utf8Box(char *_value) - { - value = _value; - } - }; - - - struct SubaddressRow - { - uint64_t id; - char *address; - char *label; - - SubaddressRow(std::size_t _id, char *_address, char *_label) - { - id = static_cast(_id); - address = _address; - label = _label; - } - }; - - struct AccountRow - { - uint64_t id; - char *label; - - AccountRow(std::size_t _id, char *_label) - { - id = static_cast(_id); - label = _label; - } - }; - - struct HavenBalance - { - uint64_t amount; - char *assetType; - - HavenBalance(char *_assetType, uint64_t _amount) - { - amount = _amount; - assetType = _assetType; - } - }; - - struct HavenRate - { - uint64_t rate; - char *assetType; - - HavenRate(char *_assetType, uint64_t _rate) - { - rate = _rate; - assetType = _assetType; - } - }; - - struct MoneroWalletListener : Monero::WalletListener - { - uint64_t m_height; - bool m_need_to_refresh; - bool m_new_transaction; - - MoneroWalletListener() - { - m_height = 0; - m_need_to_refresh = false; - m_new_transaction = false; - } - - void moneySpent(const std::string &txId, uint64_t amount, std::string assetType) - { - m_new_transaction = true; - } - - void moneyReceived(const std::string &txId, uint64_t amount, std::string assetType) - { - m_new_transaction = true; - } - - void unconfirmedMoneyReceived(const std::string &txId, uint64_t amount) - { - m_new_transaction = true; - } - - void newBlock(uint64_t height) - { - m_height = height; - } - - void updated() - { - m_new_transaction = true; - } - - void refreshed() - { - m_need_to_refresh = true; - } - - void resetNeedToRefresh() - { - m_need_to_refresh = false; - } - - bool isNeedToRefresh() - { - return m_need_to_refresh; - } - - bool isNewTransactionExist() - { - return m_new_transaction; - } - - void resetIsNewTransactionExist() - { - m_new_transaction = false; - } - - uint64_t height() - { - return m_height; - } - }; - - struct TransactionInfoRow - { - uint64_t amount; - uint64_t fee; - uint64_t blockHeight; - uint64_t confirmations; - uint32_t subaddrAccount; - int8_t direction; - int8_t isPending; - uint32_t subaddrIndex; - - char *hash; - char *paymentId; - char *assetType; - - int64_t datetime; - - TransactionInfoRow(Monero::TransactionInfo *transaction) - { - amount = transaction->amount(); - fee = transaction->fee(); - blockHeight = transaction->blockHeight(); - subaddrAccount = transaction->subaddrAccount(); - std::set::iterator it = transaction->subaddrIndex().begin(); - subaddrIndex = *it; - confirmations = transaction->confirmations(); - datetime = static_cast(transaction->timestamp()); - direction = transaction->direction(); - isPending = static_cast(transaction->isPending()); - std::string *hash_str = new std::string(transaction->hash()); - hash = strdup(hash_str->c_str()); - paymentId = strdup(transaction->paymentId().c_str()); - assetType = strdup(transaction->assetType().c_str()); - } - }; - - struct PendingTransactionRaw - { - uint64_t amount; - uint64_t fee; - char *hash; - Monero::PendingTransaction *transaction; - - PendingTransactionRaw(Monero::PendingTransaction *_transaction) - { - transaction = _transaction; - amount = _transaction->amount(); - fee = _transaction->fee(); - hash = strdup(_transaction->txid()[0].c_str()); - } - }; - - Monero::Wallet *m_wallet; - Monero::TransactionHistory *m_transaction_history; - MoneroWalletListener *m_listener; - Monero::Subaddress *m_subaddress; - Monero::SubaddressAccount *m_account; - uint64_t m_last_known_wallet_height; - uint64_t m_cached_syncing_blockchain_height = 0; - std::mutex store_lock; - bool is_storing = false; - - void change_current_wallet(Monero::Wallet *wallet) - { - m_wallet = wallet; - m_listener = nullptr; - - - if (wallet != nullptr) - { - m_transaction_history = wallet->history(); - } - else - { - m_transaction_history = nullptr; - } - - if (wallet != nullptr) - { - m_account = wallet->subaddressAccount(); - } - else - { - m_account = nullptr; - } - - if (wallet != nullptr) - { - m_subaddress = wallet->subaddress(); - } - else - { - m_subaddress = nullptr; - } - } - - Monero::Wallet *get_current_wallet() - { - return m_wallet; - } - - bool create_wallet(char *path, char *password, char *language, int32_t networkType, char *error) - { - Monero::WalletManagerFactory::setLogLevel(4); - - Monero::NetworkType _networkType = static_cast(networkType); - Monero::WalletManager *walletManager = Monero::WalletManagerFactory::getWalletManager(); - Monero::Wallet *wallet = walletManager->createWallet(path, password, language, _networkType); - - int status; - std::string errorString; - - wallet->statusWithErrorString(status, errorString); - - if (wallet->status() != Monero::Wallet::Status_Ok) - { - error = strdup(wallet->errorString().c_str()); - return false; - } - - change_current_wallet(wallet); - - return true; - } - - bool restore_wallet_from_seed(char *path, char *password, char *seed, int32_t networkType, uint64_t restoreHeight, char *error) - { - Monero::NetworkType _networkType = static_cast(networkType); - Monero::Wallet *wallet = Monero::WalletManagerFactory::getWalletManager()->recoveryWallet( - std::string(path), - std::string(password), - std::string(seed), - _networkType, - (uint64_t)restoreHeight); - - int status; - std::string errorString; - - wallet->statusWithErrorString(status, errorString); - - if (status != Monero::Wallet::Status_Ok || !errorString.empty()) - { - error = strdup(errorString.c_str()); - return false; - } - - change_current_wallet(wallet); - return true; - } - - bool restore_wallet_from_keys(char *path, char *password, char *language, char *address, char *viewKey, char *spendKey, int32_t networkType, uint64_t restoreHeight, char *error) - { - Monero::NetworkType _networkType = static_cast(networkType); - Monero::Wallet *wallet = Monero::WalletManagerFactory::getWalletManager()->createWalletFromKeys( - std::string(path), - std::string(password), - std::string(language), - _networkType, - (uint64_t)restoreHeight, - std::string(address), - std::string(viewKey), - std::string(spendKey)); - - int status; - std::string errorString; - - wallet->statusWithErrorString(status, errorString); - - if (status != Monero::Wallet::Status_Ok || !errorString.empty()) - { - error = strdup(errorString.c_str()); - return false; - } - - change_current_wallet(wallet); - return true; - } - - bool load_wallet(char *path, char *password, int32_t nettype) - { - nice(19); - Monero::NetworkType networkType = static_cast(nettype); - Monero::WalletManager *walletManager = Monero::WalletManagerFactory::getWalletManager(); - Monero::Wallet *wallet = walletManager->openWallet(std::string(path), std::string(password), networkType); - int status; - std::string errorString; - - wallet->statusWithErrorString(status, errorString); - change_current_wallet(wallet); - - return !(status != Monero::Wallet::Status_Ok || !errorString.empty()); - } - - char *error_string() { - return strdup(get_current_wallet()->errorString().c_str()); - } - - - bool is_wallet_exist(char *path) - { - return Monero::WalletManagerFactory::getWalletManager()->walletExists(std::string(path)); - } - - void close_current_wallet() - { - Monero::WalletManagerFactory::getWalletManager()->closeWallet(get_current_wallet()); - change_current_wallet(nullptr); - } - - char *get_filename() - { - return strdup(get_current_wallet()->filename().c_str()); - } - - char *secret_view_key() - { - return strdup(get_current_wallet()->secretViewKey().c_str()); - } - - char *public_view_key() - { - return strdup(get_current_wallet()->publicViewKey().c_str()); - } - - char *secret_spend_key() - { - return strdup(get_current_wallet()->secretSpendKey().c_str()); - } - - char *public_spend_key() - { - return strdup(get_current_wallet()->publicSpendKey().c_str()); - } - - char *get_address(uint32_t account_index, uint32_t address_index) - { - return strdup(get_current_wallet()->address(account_index, address_index).c_str()); - } - - - const char *seed() - { - return strdup(get_current_wallet()->seed().c_str()); - } - - int64_t *get_full_balance(uint32_t account_index) - { - std::map accountBalance; - std::map> balanceSubaddresses = get_current_wallet()->balance(account_index); - std::vector assetList = Monero::Assets::list(); - //prefill balances - for (const auto &asset_type : assetList) { - - accountBalance[asset_type] = 0; - } - // balances are mapped to their subaddress - // we compute total balances of account - for (auto const& balanceSubaddress : balanceSubaddresses) - { - - std::map balanceOfSubaddress = balanceSubaddress.second; - - for (auto const& balance : balanceOfSubaddress) - { - - const std::string &assetType = balance.first; - const uint64_t &amount = balance.second; - accountBalance[assetType] +=amount; - } - } - - size_t size = accountBalance.size(); - int64_t *balanceAddresses = (int64_t *)malloc(size * sizeof(int64_t)); - int i = 0; - - for (auto const& balance : accountBalance) - { - char *assetType = strdup(balance.first.c_str()); - HavenBalance *hb = new HavenBalance(assetType, balance.second); - balanceAddresses[i] = reinterpret_cast(hb); - i++; - } - return balanceAddresses; - } - - int64_t *get_unlocked_balance(uint32_t account_index) - { - std::map accountBalance; - std::map> balanceSubaddresses = get_current_wallet()->unlockedBalance(account_index); - std::vector assetList = Monero::Assets::list(); - - //prefill balances - for (const auto &asset_type : assetList) { - - accountBalance[asset_type] = 0; - } - // balances are mapped to their subaddress - // we compute total balances of account - for (auto const& balanceSubaddress : balanceSubaddresses) - { - - std::map balanceOfSubaddress = balanceSubaddress.second; - - for (auto const& balance : balanceOfSubaddress) - { - - const std::string &assetType = balance.first; - const uint64_t &amount = balance.second; - accountBalance[assetType] +=amount; - } - } - - size_t size = accountBalance.size(); - int64_t *balanceAddresses = (int64_t *)malloc(size * sizeof(int64_t)); - int i = 0; - - for (auto const& balance : accountBalance) - { - char *assetType = strdup(balance.first.c_str()); - HavenBalance *hb = new HavenBalance(assetType, balance.second); - balanceAddresses[i] = reinterpret_cast(hb); - i++; - } - return balanceAddresses; - } - - uint64_t get_current_height() - { - return get_current_wallet()->blockChainHeight(); - } - - uint64_t get_node_height() - { - return get_current_wallet()->daemonBlockChainHeight(); - } - - bool connect_to_node(char *error) - { - nice(19); - bool is_connected = get_current_wallet()->connectToDaemon(); - - if (!is_connected) - { - error = strdup(get_current_wallet()->errorString().c_str()); - } - - return is_connected; - } - - bool setup_node(char *address, char *login, char *password, bool use_ssl, bool is_light_wallet, char *error) - { - nice(19); - Monero::Wallet *wallet = get_current_wallet(); - - std::string _login = ""; - std::string _password = ""; - - if (login != nullptr) - { - _login = std::string(login); - } - - if (password != nullptr) - { - _password = std::string(password); - } - - bool inited = wallet->init(std::string(address), 0, _login, _password, use_ssl, is_light_wallet); - - if (!inited) - { - error = strdup(wallet->errorString().c_str()); - } else if (!wallet->connectToDaemon()) { - error = strdup(wallet->errorString().c_str()); - } - - return inited; - } - - bool is_connected() - { - return get_current_wallet()->connected(); - } - - void start_refresh() - { - get_current_wallet()->refreshAsync(); - get_current_wallet()->startRefresh(); - } - - void set_refresh_from_block_height(uint64_t height) - { - get_current_wallet()->setRefreshFromBlockHeight(height); - } - - void set_recovering_from_seed(bool is_recovery) - { - get_current_wallet()->setRecoveringFromSeed(is_recovery); - } - - void store(char *path) - { - store_lock.lock(); - if (is_storing) { - return; - } - - is_storing = true; - get_current_wallet()->store(std::string(path)); - is_storing = false; - store_lock.unlock(); - } - - bool set_password(char *password, Utf8Box &error) { - bool is_changed = get_current_wallet()->setPassword(std::string(password)); - - if (!is_changed) { - error = Utf8Box(strdup(get_current_wallet()->errorString().c_str())); - } - - return is_changed; - } - - bool transaction_create(char *address, char *asset_type, char *payment_id, char *amount, - uint8_t priority_raw, uint32_t subaddr_account, Utf8Box &error, PendingTransactionRaw &pendingTransaction) - { - nice(19); - - auto priority = static_cast(priority_raw); - std::string _payment_id; - Monero::PendingTransaction *transaction; - - if (payment_id != nullptr) - { - _payment_id = std::string(payment_id); - } - - if (amount != nullptr) - { - uint64_t _amount = Monero::Wallet::amountFromString(std::string(amount)); - transaction = m_wallet->createTransaction(std::string(address), _payment_id, _amount, std::string(asset_type), std::string(asset_type), m_wallet->defaultMixin(), priority, subaddr_account, {}); - } - else - { - transaction = m_wallet->createTransaction(std::string(address), _payment_id, Monero::optional(),std::string(asset_type), std::string(asset_type), m_wallet->defaultMixin(), priority, subaddr_account, {}); - } - - int status = transaction->status(); - - if (status == Monero::PendingTransaction::Status::Status_Error || status == Monero::PendingTransaction::Status::Status_Critical) - { - error = Utf8Box(strdup(transaction->errorString().c_str())); - return false; - } - - if (m_listener != nullptr) { - m_listener->m_new_transaction = true; - } - - pendingTransaction = PendingTransactionRaw(transaction); - return true; - } - - bool transaction_create_mult_dest(char **addresses, char *asset_type, char *payment_id, char **amounts, uint32_t size, - uint8_t priority_raw, uint32_t subaddr_account, Utf8Box &error, PendingTransactionRaw &pendingTransaction) - { - nice(19); - - std::vector _addresses; - std::vector _amounts; - - for (int i = 0; i < size; i++) { - _addresses.push_back(std::string(*addresses)); - _amounts.push_back(Monero::Wallet::amountFromString(std::string(*amounts))); - addresses++; - amounts++; - } - - auto priority = static_cast(priority_raw); - std::string _payment_id; - Monero::PendingTransaction *transaction; - - if (payment_id != nullptr) - { - _payment_id = std::string(payment_id); - } - - transaction = m_wallet->createTransactionMultDest(_addresses, _payment_id, _amounts, - std::string(asset_type), std::string(asset_type), m_wallet->defaultMixin(), priority, subaddr_account,{}); - - int status = transaction->status(); - - if (status == Monero::PendingTransaction::Status::Status_Error || status == Monero::PendingTransaction::Status::Status_Critical) - { - error = Utf8Box(strdup(transaction->errorString().c_str())); - return false; - } - - if (m_listener != nullptr) { - m_listener->m_new_transaction = true; - } - - pendingTransaction = PendingTransactionRaw(transaction); - return true; - } - - bool transaction_commit(PendingTransactionRaw *transaction, Utf8Box &error) - { - bool committed = transaction->transaction->commit(); - - if (!committed) - { - error = Utf8Box(strdup(transaction->transaction->errorString().c_str())); - } else if (m_listener != nullptr) { - m_listener->m_new_transaction = true; - } - - return committed; - } - - uint64_t get_node_height_or_update(uint64_t base_eight) - { - if (m_cached_syncing_blockchain_height < base_eight) { - m_cached_syncing_blockchain_height = base_eight; - } - - return m_cached_syncing_blockchain_height; - } - - uint64_t get_syncing_height() - { - if (m_listener == nullptr) { - return 0; - } - - uint64_t height = m_listener->height(); - - if (height <= 1) { - return 0; - } - - if (height != m_last_known_wallet_height) - { - m_last_known_wallet_height = height; - } - - return height; - } - - uint64_t is_needed_to_refresh() - { - if (m_listener == nullptr) { - return false; - } - - bool should_refresh = m_listener->isNeedToRefresh(); - - if (should_refresh) { - m_listener->resetNeedToRefresh(); - } - - return should_refresh; - } - - uint8_t is_new_transaction_exist() - { - if (m_listener == nullptr) { - return false; - } - - bool is_new_transaction_exist = m_listener->isNewTransactionExist(); - - if (is_new_transaction_exist) - { - m_listener->resetIsNewTransactionExist(); - } - - return is_new_transaction_exist; - } - - void set_listener() - { - m_last_known_wallet_height = 0; - - if (m_listener != nullptr) - { - free(m_listener); - } - - m_listener = new MoneroWalletListener(); - get_current_wallet()->setListener(m_listener); - } - - int64_t *subaddrress_get_all() - { - std::vector _subaddresses = m_subaddress->getAll(); - size_t size = _subaddresses.size(); - int64_t *subaddresses = (int64_t *)malloc(size * sizeof(int64_t)); - - for (int i = 0; i < size; i++) - { - Monero::SubaddressRow *row = _subaddresses[i]; - SubaddressRow *_row = new SubaddressRow(row->getRowId(), strdup(row->getAddress().c_str()), strdup(row->getLabel().c_str())); - subaddresses[i] = reinterpret_cast(_row); - } - - return subaddresses; - } - - int32_t subaddrress_size() - { - std::vector _subaddresses = m_subaddress->getAll(); - return _subaddresses.size(); - } - - void subaddress_add_row(uint32_t accountIndex, char *label) - { - m_subaddress->addRow(accountIndex, std::string(label)); - } - - void subaddress_set_label(uint32_t accountIndex, uint32_t addressIndex, char *label) - { - m_subaddress->setLabel(accountIndex, addressIndex, std::string(label)); - } - - void subaddress_refresh(uint32_t accountIndex) - { - m_subaddress->refresh(accountIndex); - } - - int32_t account_size() - { - std::vector _accocunts = m_account->getAll(); - return _accocunts.size(); - } - - int64_t *account_get_all() - { - std::vector _accocunts = m_account->getAll(); - size_t size = _accocunts.size(); - int64_t *accocunts = (int64_t *)malloc(size * sizeof(int64_t)); - - for (int i = 0; i < size; i++) - { - Monero::SubaddressAccountRow *row = _accocunts[i]; - AccountRow *_row = new AccountRow(row->getRowId(), strdup(row->getLabel().c_str())); - accocunts[i] = reinterpret_cast(_row); - } - - return accocunts; - } - - void account_add_row(char *label) - { - m_account->addRow(std::string(label)); - } - - void account_set_label_row(uint32_t account_index, char *label) - { - m_account->setLabel(account_index, label); - } - - void account_refresh() - { - m_account->refresh(); - } - - int64_t *transactions_get_all() - { - std::vector transactions = m_transaction_history->getAll(); - size_t size = transactions.size(); - int64_t *transactionAddresses = (int64_t *)malloc(size * sizeof(int64_t)); - - for (int i = 0; i < size; i++) - { - Monero::TransactionInfo *row = transactions[i]; - TransactionInfoRow *tx = new TransactionInfoRow(row); - transactionAddresses[i] = reinterpret_cast(tx); - } - - return transactionAddresses; - } - - void transactions_refresh() - { - m_transaction_history->refresh(); - } - - int64_t transactions_count() - { - return m_transaction_history->count(); - } - - int LedgerExchange( - unsigned char *command, - unsigned int cmd_len, - unsigned char *response, - unsigned int max_resp_len) - { - return -1; - } - - int LedgerFind(char *buffer, size_t len) - { - return -1; - } - - void on_startup() - { - Monero::Utils::onStartup(); - Monero::WalletManagerFactory::setLogLevel(4); - } - - void rescan_blockchain() - { - m_wallet->rescanBlockchainAsync(); - } - - char * get_tx_key(char * txId) - { - return strdup(m_wallet->getTxKey(std::string(txId)).c_str()); - } - - int32_t asset_types_size() - { - return Monero::Assets::list().size(); - } - - char **asset_types() - { - size_t size = Monero::Assets::list().size(); - std::vector assetList = Monero::Assets::list(); - char **assetTypesPts; - assetTypesPts = (char **) malloc( size * sizeof(char*)); - - for (int i = 0; i < size; i++) - { - - std::string asset = assetList[i]; - //assetTypes[i] = (char *)malloc( 5 * sizeof(char)); - assetTypesPts[i] = strdup(asset.c_str()); - } - - return assetTypesPts; - } - - std::map rates; - - void update_rate() - { - rates = get_current_wallet()->oracleRates(); - } - - int64_t *get_rate() - { - size_t size = rates.size(); - int64_t *havenRates = (int64_t *)malloc(size * sizeof(int64_t)); - int i = 0; - - for (auto const& rate : rates) - { - char *assetType = strdup(rate.first.c_str()); - HavenRate *havenRate = new HavenRate(assetType, rate.second); - havenRates[i] = reinterpret_cast(havenRate); - i++; - } - - return havenRates; - } - - int32_t size_of_rate() - { - return static_cast(rates.size()); - } - - void set_trusted_daemon(bool arg) - { - m_wallet->setTrustedDaemon(arg); - } - - bool trusted_daemon() - { - return m_wallet->trustedDaemon(); - } - -#ifdef __cplusplus -} -#endif diff --git a/cw_haven/ios/cw_haven.podspec b/cw_haven/ios/cw_haven.podspec deleted file mode 100644 index 4a9267d15..000000000 --- a/cw_haven/ios/cw_haven.podspec +++ /dev/null @@ -1,50 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint cw_haven.podspec` to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'cw_haven' - s.version = '0.0.1' - s.summary = 'Cake Wallet Haven' - s.description = 'Cake Wallet wrapper over Haven project' - s.homepage = 'http://cakewallet.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Cake Wallet' => 'support@cakewallet.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h, Classes/*.h, ../shared_external/ios/libs/monero/include/src/**/*.h, ../shared_external/ios/libs/monero/include/contrib/**/*.h, ../shared_external/ios/libs/monero/include/../shared_external/ios/**/*.h' - s.dependency 'Flutter' - s.dependency 'cw_shared_external' - s.platform = :ios, '10.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS' => 'arm64', 'ENABLE_BITCODE' => 'NO' } - s.swift_version = '5.0' - s.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/Classes/*.h" } - - s.subspec 'OpenSSL' do |openssl| - openssl.preserve_paths = '../../../../../cw_shared_external/ios/External/ios/include/**/*.h' - openssl.vendored_libraries = '../../../../../cw_shared_external/ios/External/ios/lib/libcrypto.a', '../../../../../cw_shared_external/ios/External/ios/lib/libssl.a' - openssl.libraries = 'ssl', 'crypto' - openssl.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/include/**" } - end - - s.subspec 'Sodium' do |sodium| - sodium.preserve_paths = '../../../../../cw_shared_external/ios/External/ios/include/**/*.h' - sodium.vendored_libraries = '../../../../../cw_shared_external/ios/External/ios/lib/libsodium.a' - sodium.libraries = 'sodium' - sodium.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/include/**" } - end - - s.subspec 'Boost' do |boost| - boost.preserve_paths = '../../../../../cw_shared_external/ios/External/ios/include/**/*.h', - boost.vendored_libraries = '../../../../../cw_shared_external/ios/External/ios/lib/libboost.a', - boost.libraries = 'boost' - boost.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/include/**" } - end - - s.subspec 'Haven' do |haven| - haven.preserve_paths = 'External/ios/include/**/*.h' - haven.vendored_libraries = 'External/ios/lib/libhaven.a' - haven.libraries = 'haven' - haven.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/include" } - end -end diff --git a/cw_haven/lib/api/account_list.dart b/cw_haven/lib/api/account_list.dart deleted file mode 100644 index 87f036206..000000000 --- a/cw_haven/lib/api/account_list.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; -import 'package:cw_haven/api/signatures.dart'; -import 'package:cw_haven/api/types.dart'; -import 'package:cw_haven/api/haven_api.dart'; -import 'package:cw_haven/api/structs/account_row.dart'; -import 'package:cw_haven/api/wallet.dart'; - -final accountSizeNative = havenApi - .lookup>('account_size') - .asFunction(); - -final accountRefreshNative = havenApi - .lookup>('account_refresh') - .asFunction(); - -final accountGetAllNative = havenApi - .lookup>('account_get_all') - .asFunction(); - -final accountAddNewNative = havenApi - .lookup>('account_add_row') - .asFunction(); - -final accountSetLabelNative = havenApi - .lookup>('account_set_label_row') - .asFunction(); - -bool isUpdating = false; - -void refreshAccounts() { - try { - isUpdating = true; - accountRefreshNative(); - isUpdating = false; - } catch (e) { - isUpdating = false; - rethrow; - } -} - -List getAllAccount() { - final size = accountSizeNative(); - final accountAddressesPointer = accountGetAllNative(); - final accountAddresses = accountAddressesPointer.asTypedList(size); - - return accountAddresses - .map((addr) => Pointer.fromAddress(addr).ref) - .toList(); -} - -void addAccountSync({required String label}) { - final labelPointer = label.toNativeUtf8(); - accountAddNewNative(labelPointer); - calloc.free(labelPointer); -} - -void setLabelForAccountSync({required int accountIndex, required String label}) { - final labelPointer = label.toNativeUtf8(); - accountSetLabelNative(accountIndex, labelPointer); - calloc.free(labelPointer); -} - -void _addAccount(String label) => addAccountSync(label: label); - -void _setLabelForAccount(Map args) { - final label = args['label'] as String; - final accountIndex = args['accountIndex'] as int; - - setLabelForAccountSync(label: label, accountIndex: accountIndex); -} - -Future addAccount({required String label}) async { - _addAccount(label); - await store(); -} - -Future setLabelForAccount({required int accountIndex, required String label}) async { - _setLabelForAccount({'accountIndex': accountIndex, 'label': label}); - await store(); -} \ No newline at end of file diff --git a/cw_haven/lib/api/asset_types.dart b/cw_haven/lib/api/asset_types.dart deleted file mode 100644 index f57b10f78..000000000 --- a/cw_haven/lib/api/asset_types.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'dart:ffi'; -import 'package:cw_haven/api/convert_utf8_to_string.dart'; -import 'package:cw_haven/api/signatures.dart'; -import 'package:cw_haven/api/types.dart'; -import 'package:cw_haven/api/haven_api.dart'; -import 'package:ffi/ffi.dart'; - -final assetTypesSizeNative = havenApi - .lookup>('asset_types_size') - .asFunction(); - -final getAssetTypesNative = havenApi - .lookup>('asset_types') - .asFunction(); - -List getAssetTypes() { - List assetTypes = []; - Pointer> assetTypePointers = getAssetTypesNative(); - Pointer assetpointer = assetTypePointers.elementAt(0)[0]; - String asset = convertUTF8ToString(pointer: assetpointer); - - return assetTypes; -} diff --git a/cw_haven/lib/api/balance_list.dart b/cw_haven/lib/api/balance_list.dart deleted file mode 100644 index 3488a6583..000000000 --- a/cw_haven/lib/api/balance_list.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'dart:ffi'; -import 'package:cw_haven/api/signatures.dart'; -import 'package:cw_haven/api/types.dart'; -import 'package:cw_haven/api/haven_api.dart'; -import 'package:cw_haven/api/structs/haven_balance_row.dart'; -import 'package:cw_haven/api/structs/haven_rate.dart'; -import 'asset_types.dart'; - -List getHavenFullBalance({int accountIndex = 0}) { - final size = assetTypesSizeNative(); - final balanceAddressesPointer = getHavenFullBalanceNative(accountIndex); - final balanceAddresses = balanceAddressesPointer.asTypedList(size); - - return balanceAddresses - .map((addr) => Pointer.fromAddress(addr).ref) - .toList(); -} - -List getHavenUnlockedBalance({int accountIndex = 0}) { - final size = assetTypesSizeNative(); - final balanceAddressesPointer = getHavenUnlockedBalanceNative(accountIndex); - final balanceAddresses = balanceAddressesPointer.asTypedList(size); - - return balanceAddresses - .map((addr) => Pointer.fromAddress(addr).ref) - .toList(); -} - -List getRate() { - updateRateNative(); - final size = sizeOfRateNative(); - final ratePointer = getRateNative(); - final rate = ratePointer.asTypedList(size); - - return rate - .map((addr) => Pointer.fromAddress(addr).ref) - .toList(); -} - -final getHavenFullBalanceNative = havenApi - .lookup>('get_full_balance') - .asFunction(); - -final getHavenUnlockedBalanceNative = havenApi - .lookup>('get_unlocked_balance') - .asFunction(); - -final getRateNative = havenApi - .lookup>('get_rate') - .asFunction(); - -final sizeOfRateNative = havenApi - .lookup>('size_of_rate') - .asFunction(); - -final updateRateNative = havenApi - .lookup>('update_rate') - .asFunction(); \ No newline at end of file diff --git a/cw_haven/lib/api/convert_utf8_to_string.dart b/cw_haven/lib/api/convert_utf8_to_string.dart deleted file mode 100644 index 41a6b648a..000000000 --- a/cw_haven/lib/api/convert_utf8_to_string.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; - -String convertUTF8ToString({required Pointer pointer}) { - final str = pointer.toDartString(); - calloc.free(pointer); - return str; -} \ No newline at end of file diff --git a/cw_haven/lib/api/cw_haven.dart b/cw_haven/lib/api/cw_haven.dart deleted file mode 100644 index 0e48276d9..000000000 --- a/cw_haven/lib/api/cw_haven.dart +++ /dev/null @@ -1,14 +0,0 @@ - -import 'dart:async'; - -import 'package:flutter/services.dart'; - -class CwHaven { - static const MethodChannel _channel = - const MethodChannel('cw_haven'); - - static Future get platformVersion async { - final String version = await _channel.invokeMethod('getPlatformVersion') ?? ''; - return version; - } -} diff --git a/cw_haven/lib/api/exceptions/connection_to_node_exception.dart b/cw_haven/lib/api/exceptions/connection_to_node_exception.dart deleted file mode 100644 index 483b0a174..000000000 --- a/cw_haven/lib/api/exceptions/connection_to_node_exception.dart +++ /dev/null @@ -1,5 +0,0 @@ -class ConnectionToNodeException implements Exception { - ConnectionToNodeException({required this.message}); - - final String message; -} \ No newline at end of file diff --git a/cw_haven/lib/api/exceptions/creation_transaction_exception.dart b/cw_haven/lib/api/exceptions/creation_transaction_exception.dart deleted file mode 100644 index 7b55ec074..000000000 --- a/cw_haven/lib/api/exceptions/creation_transaction_exception.dart +++ /dev/null @@ -1,8 +0,0 @@ -class CreationTransactionException implements Exception { - CreationTransactionException({required this.message}); - - final String message; - - @override - String toString() => message; -} \ No newline at end of file diff --git a/cw_haven/lib/api/exceptions/setup_wallet_exception.dart b/cw_haven/lib/api/exceptions/setup_wallet_exception.dart deleted file mode 100644 index 9d985665b..000000000 --- a/cw_haven/lib/api/exceptions/setup_wallet_exception.dart +++ /dev/null @@ -1,10 +0,0 @@ -class SetupWalletException implements Exception { - SetupWalletException({required this.message}); - - final String message; - - @override - String toString() { - return message; - } -} \ No newline at end of file diff --git a/cw_haven/lib/api/exceptions/wallet_creation_exception.dart b/cw_haven/lib/api/exceptions/wallet_creation_exception.dart deleted file mode 100644 index 6052366b9..000000000 --- a/cw_haven/lib/api/exceptions/wallet_creation_exception.dart +++ /dev/null @@ -1,8 +0,0 @@ -class WalletCreationException implements Exception { - WalletCreationException({required this.message}); - - final String message; - - @override - String toString() => message; -} \ No newline at end of file diff --git a/cw_haven/lib/api/exceptions/wallet_opening_exception.dart b/cw_haven/lib/api/exceptions/wallet_opening_exception.dart deleted file mode 100644 index df7a850a4..000000000 --- a/cw_haven/lib/api/exceptions/wallet_opening_exception.dart +++ /dev/null @@ -1,8 +0,0 @@ -class WalletOpeningException implements Exception { - WalletOpeningException({required this.message}); - - final String message; - - @override - String toString() => message; -} \ No newline at end of file diff --git a/cw_haven/lib/api/exceptions/wallet_restore_from_keys_exception.dart b/cw_haven/lib/api/exceptions/wallet_restore_from_keys_exception.dart deleted file mode 100644 index 3ff5f2438..000000000 --- a/cw_haven/lib/api/exceptions/wallet_restore_from_keys_exception.dart +++ /dev/null @@ -1,10 +0,0 @@ -class WalletRestoreFromKeysException implements Exception { - WalletRestoreFromKeysException({required this.message}); - - final String message; - - @override - String toString() { - return message; - } -} \ No newline at end of file diff --git a/cw_haven/lib/api/exceptions/wallet_restore_from_seed_exception.dart b/cw_haven/lib/api/exceptions/wallet_restore_from_seed_exception.dart deleted file mode 100644 index 004cd7958..000000000 --- a/cw_haven/lib/api/exceptions/wallet_restore_from_seed_exception.dart +++ /dev/null @@ -1,5 +0,0 @@ -class WalletRestoreFromSeedException implements Exception { - WalletRestoreFromSeedException({required this.message}); - - final String message; -} \ No newline at end of file diff --git a/cw_haven/lib/api/haven_api.dart b/cw_haven/lib/api/haven_api.dart deleted file mode 100644 index 41b50d9bc..000000000 --- a/cw_haven/lib/api/haven_api.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'dart:ffi'; -import 'dart:io'; - -final DynamicLibrary havenApi = Platform.isAndroid - ? DynamicLibrary.open("libcw_haven.so") - : DynamicLibrary.open("cw_haven.framework/cw_haven"); \ No newline at end of file diff --git a/cw_haven/lib/api/monero_output.dart b/cw_haven/lib/api/monero_output.dart deleted file mode 100644 index a6d735bd3..000000000 --- a/cw_haven/lib/api/monero_output.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:flutter/foundation.dart'; - -class MoneroOutput { - MoneroOutput({required this.address, required this.amount}); - - final String address; - final String amount; -} \ No newline at end of file diff --git a/cw_haven/lib/api/signatures.dart b/cw_haven/lib/api/signatures.dart deleted file mode 100644 index ae95b62dd..000000000 --- a/cw_haven/lib/api/signatures.dart +++ /dev/null @@ -1,144 +0,0 @@ -import 'dart:ffi'; -import 'package:cw_haven/api/structs/pending_transaction.dart'; -import 'package:cw_haven/api/structs/ut8_box.dart'; -import 'package:ffi/ffi.dart'; - -typedef create_wallet = Int8 Function( - Pointer, Pointer, Pointer, Int32, Pointer); - -typedef restore_wallet_from_seed = Int8 Function( - Pointer, Pointer, Pointer, Int32, Int64, Pointer); - -typedef restore_wallet_from_keys = Int8 Function(Pointer, Pointer, - Pointer, Pointer, Pointer, Pointer, Int32, Int64, Pointer); - -typedef is_wallet_exist = Int8 Function(Pointer); - -typedef load_wallet = Int8 Function(Pointer, Pointer, Int8); - -typedef error_string = Pointer Function(); - -typedef get_filename = Pointer Function(); - -typedef get_seed = Pointer Function(); - -typedef get_address = Pointer Function(Int32, Int32); - -typedef get_full_balance = Pointer Function(Int32); - -typedef get_unlocked_balance = Pointer Function(Int32); - -typedef get_full_balanace = Int64 Function(Int32); - -typedef get_unlocked_balanace = Int64 Function(Int32); - -typedef get_current_height = Int64 Function(); - -typedef get_node_height = Int64 Function(); - -typedef is_connected = Int8 Function(); - -typedef setup_node = Int8 Function( - Pointer, Pointer?, Pointer?, Int8, Int8, Pointer?, Pointer); - -typedef start_refresh = Void Function(); - -typedef connect_to_node = Int8 Function(); - -typedef set_refresh_from_block_height = Void Function(Int64); - -typedef set_recovering_from_seed = Void Function(Int8); - -typedef store_c = Void Function(Pointer); - -typedef set_password = Int8 Function(Pointer password, Pointer error); - -typedef set_listener = Void Function(); - -typedef get_syncing_height = Int64 Function(); - -typedef is_needed_to_refresh = Int8 Function(); - -typedef is_new_transaction_exist = Int8 Function(); - -typedef subaddrress_size = Int32 Function(); - -typedef subaddrress_refresh = Void Function(Int32); - -typedef subaddress_get_all = Pointer Function(); - -typedef subaddress_add_new = Void Function( - Int32 accountIndex, Pointer label); - -typedef subaddress_set_label = Void Function( - Int32 accountIndex, Int32 addressIndex, Pointer label); - -typedef account_size = Int32 Function(); - -typedef account_refresh = Void Function(); - -typedef account_get_all = Pointer Function(); - -typedef account_add_new = Void Function(Pointer label); - -typedef account_set_label = Void Function( - Int32 accountIndex, Pointer label); - -typedef transactions_refresh = Void Function(); - -typedef get_tx_key = Pointer? Function(Pointer txId); - -typedef transactions_count = Int64 Function(); - -typedef transactions_get_all = Pointer Function(); - -typedef transaction_create = Int8 Function( - Pointer address, - Pointer assetType, - Pointer paymentId, - Pointer amount, - Int8 priorityRaw, - Int32 subaddrAccount, - Pointer error, - Pointer pendingTransaction); - -typedef transaction_create_mult_dest = Int8 Function( - Pointer> addresses, - Pointer assetType, - Pointer paymentId, - Pointer> amounts, - Int32 size, - Int8 priorityRaw, - Int32 subaddrAccount, - Pointer error, - Pointer pendingTransaction); - -typedef transaction_commit = Int8 Function(Pointer, Pointer); - -typedef secret_view_key = Pointer Function(); - -typedef public_view_key = Pointer Function(); - -typedef secret_spend_key = Pointer Function(); - -typedef public_spend_key = Pointer Function(); - -typedef close_current_wallet = Void Function(); - -typedef on_startup = Void Function(); - -typedef rescan_blockchain = Void Function(); - -typedef asset_types = Pointer> Function(); - -typedef asset_types_size = Int32 Function(); - -typedef get_rate = Pointer Function(); - -typedef size_of_rate = Int32 Function(); - -typedef update_rate = Void Function(); - -typedef set_trusted_daemon = Void Function(Int8 trusted); - -typedef trusted_daemon = Int8 Function(); \ No newline at end of file diff --git a/cw_haven/lib/api/structs/account_row.dart b/cw_haven/lib/api/structs/account_row.dart deleted file mode 100644 index aa492ee0f..000000000 --- a/cw_haven/lib/api/structs/account_row.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; - -class AccountRow extends Struct { - @Int64() - external int id; - - external Pointer label; - - String getLabel() => label.toDartString(); - int getId() => id; -} diff --git a/cw_haven/lib/api/structs/haven_balance_row.dart b/cw_haven/lib/api/structs/haven_balance_row.dart deleted file mode 100644 index b0f657bca..000000000 --- a/cw_haven/lib/api/structs/haven_balance_row.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; - -class HavenBalanceRow extends Struct { - @Int64() - external int amount; - - external Pointer assetType; - - int getAmount() => amount; - String getAssetType() => assetType.toDartString(); -} diff --git a/cw_haven/lib/api/structs/haven_rate.dart b/cw_haven/lib/api/structs/haven_rate.dart deleted file mode 100644 index 48f188135..000000000 --- a/cw_haven/lib/api/structs/haven_rate.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; - -class HavenRate extends Struct { - @Int64() - external int rate; - - external Pointer assetType; - - int getRate() => rate; - String getAssetType() => assetType.toDartString(); -} diff --git a/cw_haven/lib/api/structs/pending_transaction.dart b/cw_haven/lib/api/structs/pending_transaction.dart deleted file mode 100644 index 12e5233f1..000000000 --- a/cw_haven/lib/api/structs/pending_transaction.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; - -class PendingTransactionRaw extends Struct { - @Int64() - external int amount; - - @Int64() - external int fee; - - external Pointer hash; - - String getHash() => hash.toDartString(); -} - -class PendingTransactionDescription { - PendingTransactionDescription({ - required this.amount, - required this.fee, - required this.hash, - required this.pointerAddress}); - - final int amount; - final int fee; - final String hash; - final int pointerAddress; -} \ No newline at end of file diff --git a/cw_haven/lib/api/structs/subaddress_row.dart b/cw_haven/lib/api/structs/subaddress_row.dart deleted file mode 100644 index d593a793d..000000000 --- a/cw_haven/lib/api/structs/subaddress_row.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; - -class SubaddressRow extends Struct { - @Int64() - external int id; - - external Pointer address; - - external Pointer label; - - String getLabel() => label.toDartString(); - String getAddress() => address.toDartString(); - int getId() => id; -} \ No newline at end of file diff --git a/cw_haven/lib/api/structs/transaction_info_row.dart b/cw_haven/lib/api/structs/transaction_info_row.dart deleted file mode 100644 index 177cdfde7..000000000 --- a/cw_haven/lib/api/structs/transaction_info_row.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; - -class TransactionInfoRow extends Struct { - @Uint64() - external int amount; - - @Uint64() - external int fee; - - @Uint64() - external int blockHeight; - - @Uint64() - external int confirmations; - - @Uint32() - external int subaddrAccount; - - @Int8() - external int direction; - - @Int8() - external int isPending; - - @Uint32() - external int subaddrIndex; - - external Pointer hash; - - external Pointer paymentId; - - external Pointer assetType; - - @Int64() - external int datetime; - - int getDatetime() => datetime; - int getAmount() => amount >= 0 ? amount : amount * -1; - bool getIsPending() => isPending != 0; - String getHash() => hash.toDartString(); - String getPaymentId() => paymentId.toDartString(); - String getAssetType() => assetType.toDartString(); -} diff --git a/cw_haven/lib/api/structs/ut8_box.dart b/cw_haven/lib/api/structs/ut8_box.dart deleted file mode 100644 index 53e678c88..000000000 --- a/cw_haven/lib/api/structs/ut8_box.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; - -class Utf8Box extends Struct { - external Pointer value; - - String getValue() => value.toDartString(); -} diff --git a/cw_haven/lib/api/subaddress_list.dart b/cw_haven/lib/api/subaddress_list.dart deleted file mode 100644 index 39dbeab78..000000000 --- a/cw_haven/lib/api/subaddress_list.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; -import 'package:flutter/foundation.dart'; -import 'package:cw_haven/api/signatures.dart'; -import 'package:cw_haven/api/types.dart'; -import 'package:cw_haven/api/haven_api.dart'; -import 'package:cw_haven/api/structs/subaddress_row.dart'; -import 'package:cw_haven/api/wallet.dart'; - -final subaddressSizeNative = havenApi - .lookup>('subaddrress_size') - .asFunction(); - -final subaddressRefreshNative = havenApi - .lookup>('subaddress_refresh') - .asFunction(); - -final subaddrressGetAllNative = havenApi - .lookup>('subaddrress_get_all') - .asFunction(); - -final subaddrressAddNewNative = havenApi - .lookup>('subaddress_add_row') - .asFunction(); - -final subaddrressSetLabelNative = havenApi - .lookup>('subaddress_set_label') - .asFunction(); - -bool isUpdating = false; - -void refreshSubaddresses({required int accountIndex}) { - try { - isUpdating = true; - subaddressRefreshNative(accountIndex); - isUpdating = false; - } catch (e) { - isUpdating = false; - rethrow; - } -} - -List getAllSubaddresses() { - final size = subaddressSizeNative(); - final subaddressAddressesPointer = subaddrressGetAllNative(); - final subaddressAddresses = subaddressAddressesPointer.asTypedList(size); - - return subaddressAddresses - .map((addr) => Pointer.fromAddress(addr).ref) - .toList(); -} - -void addSubaddressSync({required int accountIndex, required String label}) { - final labelPointer = label.toNativeUtf8(); - subaddrressAddNewNative(accountIndex, labelPointer); - calloc.free(labelPointer); -} - -void setLabelForSubaddressSync( - {required int accountIndex, required int addressIndex, required String label}) { - final labelPointer = label.toNativeUtf8(); - - subaddrressSetLabelNative(accountIndex, addressIndex, labelPointer); - calloc.free(labelPointer); -} - -void _addSubaddress(Map args) { - final label = args['label'] as String; - final accountIndex = args['accountIndex'] as int; - - addSubaddressSync(accountIndex: accountIndex, label: label); -} - -void _setLabelForSubaddress(Map args) { - final label = args['label'] as String; - final accountIndex = args['accountIndex'] as int; - final addressIndex = args['addressIndex'] as int; - - setLabelForSubaddressSync( - accountIndex: accountIndex, addressIndex: addressIndex, label: label); -} - -Future addSubaddress({required int accountIndex, required String label}) async { - await compute, void>( - _addSubaddress, {'accountIndex': accountIndex, 'label': label}); - await store(); -} - -Future setLabelForSubaddress( - {required int accountIndex, required int addressIndex, required String label}) async { - await compute, void>(_setLabelForSubaddress, { - 'accountIndex': accountIndex, - 'addressIndex': addressIndex, - 'label': label - }); - await store(); -} diff --git a/cw_haven/lib/api/transaction_history.dart b/cw_haven/lib/api/transaction_history.dart deleted file mode 100644 index f658133e1..000000000 --- a/cw_haven/lib/api/transaction_history.dart +++ /dev/null @@ -1,246 +0,0 @@ -import 'dart:ffi'; -import 'package:cw_haven/api/convert_utf8_to_string.dart'; -import 'package:cw_haven/api/monero_output.dart'; -import 'package:cw_haven/api/structs/ut8_box.dart'; -import 'package:ffi/ffi.dart'; -import 'package:flutter/foundation.dart'; -import 'package:cw_haven/api/signatures.dart'; -import 'package:cw_haven/api/types.dart'; -import 'package:cw_haven/api/haven_api.dart'; -import 'package:cw_haven/api/structs/transaction_info_row.dart'; -import 'package:cw_haven/api/structs/pending_transaction.dart'; -import 'package:cw_haven/api/exceptions/creation_transaction_exception.dart'; - -final transactionsRefreshNative = havenApi - .lookup>('transactions_refresh') - .asFunction(); - -final transactionsCountNative = havenApi - .lookup>('transactions_count') - .asFunction(); - -final transactionsGetAllNative = havenApi - .lookup>('transactions_get_all') - .asFunction(); - -final transactionCreateNative = havenApi - .lookup>('transaction_create') - .asFunction(); - -final transactionCreateMultDestNative = havenApi - .lookup>('transaction_create_mult_dest') - .asFunction(); - -final transactionCommitNative = havenApi - .lookup>('transaction_commit') - .asFunction(); - -final getTxKeyNative = havenApi - .lookup>('get_tx_key') - .asFunction(); - -String getTxKey(String txId) { - final txIdPointer = txId.toNativeUtf8(); - final keyPointer = getTxKeyNative(txIdPointer); - - calloc.free(txIdPointer); - - if (keyPointer != null) { - return convertUTF8ToString(pointer: keyPointer); - } - - return ''; -} - -void refreshTransactions() => transactionsRefreshNative(); - -int countOfTransactions() => transactionsCountNative(); - -List getAllTransations() { - final size = transactionsCountNative(); - final transactionsPointer = transactionsGetAllNative(); - final transactionsAddresses = transactionsPointer.asTypedList(size); - - return transactionsAddresses - .map((addr) => Pointer.fromAddress(addr).ref) - .toList(); -} - -PendingTransactionDescription createTransactionSync( - {required String address, - required String assetType, - required String paymentId, - required int priorityRaw, - String? amount, - int accountIndex = 0}) { - final addressPointer = address.toNativeUtf8(); - final assetTypePointer = assetType.toNativeUtf8(); - final paymentIdPointer = paymentId.toNativeUtf8(); - final amountPointer = amount != null ? amount.toNativeUtf8() : nullptr; - final errorMessagePointer = calloc(); - final pendingTransactionRawPointer = calloc(); - final created = transactionCreateNative( - addressPointer, - assetTypePointer, - paymentIdPointer, - amountPointer, - priorityRaw, - accountIndex, - errorMessagePointer, - pendingTransactionRawPointer) != - 0; - - calloc.free(addressPointer); - calloc.free(assetTypePointer); - calloc.free(paymentIdPointer); - - if (amountPointer != nullptr) { - calloc.free(amountPointer); - } - - if (!created) { - final message = errorMessagePointer.ref.getValue(); - calloc.free(errorMessagePointer); - throw CreationTransactionException(message: message); - } - - return PendingTransactionDescription( - amount: pendingTransactionRawPointer.ref.amount, - fee: pendingTransactionRawPointer.ref.fee, - hash: pendingTransactionRawPointer.ref.getHash(), - pointerAddress: pendingTransactionRawPointer.address); -} - -PendingTransactionDescription createTransactionMultDestSync( - {required List outputs, - required String assetType, - required String paymentId, - required int priorityRaw, - int accountIndex = 0}) { - final int size = outputs.length; - final List> addressesPointers = outputs.map((output) => - output.address.toNativeUtf8()).toList(); - final Pointer> addressesPointerPointer = calloc(size); - final List> amountsPointers = outputs.map((output) => - output.amount.toNativeUtf8()).toList(); - final Pointer> amountsPointerPointer = calloc( size); - - for (int i = 0; i < size; i++) { - addressesPointerPointer[i] = addressesPointers[i]; - amountsPointerPointer[i] = amountsPointers[i]; - } - - final assetTypePointer = assetType.toNativeUtf8(); - final paymentIdPointer = paymentId.toNativeUtf8(); - final errorMessagePointer = calloc(); - final pendingTransactionRawPointer = calloc(); - final created = transactionCreateMultDestNative( - addressesPointerPointer, - assetTypePointer, - paymentIdPointer, - amountsPointerPointer, - size, - priorityRaw, - accountIndex, - errorMessagePointer, - pendingTransactionRawPointer) != - 0; - - calloc.free(addressesPointerPointer); - calloc.free(assetTypePointer); - calloc.free(amountsPointerPointer); - - addressesPointers.forEach((element) => calloc.free(element)); - amountsPointers.forEach((element) => calloc.free(element)); - - calloc.free(paymentIdPointer); - - if (!created) { - final message = errorMessagePointer.ref.getValue(); - calloc.free(errorMessagePointer); - throw CreationTransactionException(message: message); - } - - return PendingTransactionDescription( - amount: pendingTransactionRawPointer.ref.amount, - fee: pendingTransactionRawPointer.ref.fee, - hash: pendingTransactionRawPointer.ref.getHash(), - pointerAddress: pendingTransactionRawPointer.address); -} - -void commitTransactionFromPointerAddress({required int address}) => commitTransaction( - transactionPointer: Pointer.fromAddress(address)); - -void commitTransaction({required Pointer transactionPointer}) { - final errorMessagePointer = calloc(); - final isCommited = - transactionCommitNative(transactionPointer, errorMessagePointer) != 0; - - if (!isCommited) { - final message = errorMessagePointer.ref.getValue(); - calloc.free(errorMessagePointer); - throw CreationTransactionException(message: message); - } -} - -PendingTransactionDescription _createTransactionSync(Map args) { - final address = args['address'] as String; - final assetType = args['assetType'] as String; - final paymentId = args['paymentId'] as String; - final amount = args['amount'] as String; - final priorityRaw = args['priorityRaw'] as int; - final accountIndex = args['accountIndex'] as int; - - return createTransactionSync( - address: address, - assetType: assetType, - paymentId: paymentId, - amount: amount, - priorityRaw: priorityRaw, - accountIndex: accountIndex); -} - -PendingTransactionDescription _createTransactionMultDestSync(Map args) { - final outputs = args['outputs'] as List; - final assetType = args['assetType'] as String; - final paymentId = args['paymentId'] as String; - final priorityRaw = args['priorityRaw'] as int; - final accountIndex = args['accountIndex'] as int; - - return createTransactionMultDestSync( - outputs: outputs, - assetType: assetType, - paymentId: paymentId, - priorityRaw: priorityRaw, - accountIndex: accountIndex); -} - -Future createTransaction( - {required String address, - required String assetType, - required int priorityRaw, - String? amount, - String paymentId = '', - int accountIndex = 0}) => - compute(_createTransactionSync, { - 'address': address, - 'assetType': assetType, - 'paymentId': paymentId, - 'amount': amount, - 'priorityRaw': priorityRaw, - 'accountIndex': accountIndex - }); - -Future createTransactionMultDest( - {required List outputs, - required int priorityRaw, - String? assetType, - String paymentId = '', - int accountIndex = 0}) => - compute(_createTransactionMultDestSync, { - 'outputs': outputs, - 'assetType': assetType, - 'paymentId': paymentId, - 'priorityRaw': priorityRaw, - 'accountIndex': accountIndex - }); diff --git a/cw_haven/lib/api/types.dart b/cw_haven/lib/api/types.dart deleted file mode 100644 index 8c9dfdab2..000000000 --- a/cw_haven/lib/api/types.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'dart:ffi'; -import 'package:cw_haven/api/structs/pending_transaction.dart'; -import 'package:cw_haven/api/structs/ut8_box.dart'; -import 'package:ffi/ffi.dart'; - -typedef CreateWallet = int Function( - Pointer, Pointer, Pointer, int, Pointer); - -typedef RestoreWalletFromSeed = int Function( - Pointer, Pointer, Pointer, int, int, Pointer); - -typedef RestoreWalletFromKeys = int Function(Pointer, Pointer, - Pointer, Pointer, Pointer, Pointer, int, int, Pointer); - -typedef IsWalletExist = int Function(Pointer); - -typedef LoadWallet = int Function(Pointer, Pointer, int); - -typedef ErrorString = Pointer Function(); - -typedef GetFilename = Pointer Function(); - -typedef GetSeed = Pointer Function(); - -typedef GetAddress = Pointer Function(int, int); - -typedef GetHavenFullBalance = Pointer Function(int); - -typedef GetHavenUnlockedBalance = Pointer Function(int); - -typedef GetFullBalance = int Function(int); - -typedef GetUnlockedBalance = int Function(int); - -typedef GetCurrentHeight = int Function(); - -typedef GetNodeHeight = int Function(); - -typedef IsConnected = int Function(); - -typedef SetupNode = int Function( - Pointer, Pointer?, Pointer?, int, int, Pointer?, Pointer); - -typedef StartRefresh = void Function(); - -typedef ConnectToNode = int Function(); - -typedef SetRefreshFromBlockHeight = void Function(int); - -typedef SetRecoveringFromSeed = void Function(int); - -typedef Store = void Function(Pointer); - -typedef SetPassword = int Function(Pointer password, Pointer error); - -typedef SetListener = void Function(); - -typedef GetSyncingHeight = int Function(); - -typedef IsNeededToRefresh = int Function(); - -typedef IsNewTransactionExist = int Function(); - -typedef SubaddressSize = int Function(); - -typedef SubaddressRefresh = void Function(int); - -typedef SubaddressGetAll = Pointer Function(); - -typedef SubaddressAddNew = void Function(int accountIndex, Pointer label); - -typedef SubaddressSetLabel = void Function( - int accountIndex, int addressIndex, Pointer label); - -typedef AccountSize = int Function(); - -typedef AccountRefresh = void Function(); - -typedef AccountGetAll = Pointer Function(); - -typedef AccountAddNew = void Function(Pointer label); - -typedef AccountSetLabel = void Function(int accountIndex, Pointer label); - -typedef TransactionsRefresh = void Function(); - -typedef GetTxKey = Pointer? Function(Pointer txId); - -typedef TransactionsCount = int Function(); - -typedef TransactionsGetAll = Pointer Function(); - -typedef TransactionCreate = int Function( - Pointer address, - Pointer assetType, - Pointer paymentId, - Pointer amount, - int priorityRaw, - int subaddrAccount, - Pointer error, - Pointer pendingTransaction); - -typedef TransactionCreateMultDest = int Function( - Pointer> addresses, - Pointer assetType, - Pointer paymentId, - Pointer> amounts, - int size, - int priorityRaw, - int subaddrAccount, - Pointer error, - Pointer pendingTransaction); - -typedef TransactionCommit = int Function(Pointer, Pointer); - -typedef SecretViewKey = Pointer Function(); - -typedef PublicViewKey = Pointer Function(); - -typedef SecretSpendKey = Pointer Function(); - -typedef PublicSpendKey = Pointer Function(); - -typedef CloseCurrentWallet = void Function(); - -typedef OnStartup = void Function(); - -typedef RescanBlockchainAsync = void Function(); - -typedef AssetTypes = Pointer> Function(); - -typedef AssetTypesSize = int Function(); - -typedef GetRate = Pointer Function(); - -typedef SizeOfRate = int Function(); - -typedef UpdateRate = void Function(); - -typedef SetTrustedDaemon = void Function(int); - -typedef TrustedDaemon = int Function(); \ No newline at end of file diff --git a/cw_haven/lib/api/wallet.dart b/cw_haven/lib/api/wallet.dart deleted file mode 100644 index e6b75c0cc..000000000 --- a/cw_haven/lib/api/wallet.dart +++ /dev/null @@ -1,376 +0,0 @@ -import 'dart:async'; -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; -import 'package:cw_haven/api/structs/ut8_box.dart'; -import 'package:cw_haven/api/convert_utf8_to_string.dart'; -import 'package:cw_haven/api/signatures.dart'; -import 'package:cw_haven/api/types.dart'; -import 'package:cw_haven/api/haven_api.dart'; -import 'package:cw_haven/api/exceptions/setup_wallet_exception.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; - -int _boolToInt(bool value) => value ? 1 : 0; - -final getFileNameNative = havenApi - .lookup>('get_filename') - .asFunction(); - -final getSeedNative = - havenApi.lookup>('seed').asFunction(); - -final getAddressNative = havenApi - .lookup>('get_address') - .asFunction(); - -final getFullBalanceNative = havenApi - .lookup>('get_full_balance') - .asFunction(); - -final getUnlockedBalanceNative = havenApi - .lookup>('get_unlocked_balance') - .asFunction(); - -final getCurrentHeightNative = havenApi - .lookup>('get_current_height') - .asFunction(); - -final getNodeHeightNative = havenApi - .lookup>('get_node_height') - .asFunction(); - -final isConnectedNative = havenApi - .lookup>('is_connected') - .asFunction(); - -final setupNodeNative = havenApi - .lookup>('setup_node') - .asFunction(); - -final startRefreshNative = havenApi - .lookup>('start_refresh') - .asFunction(); - -final connecToNodeNative = havenApi - .lookup>('connect_to_node') - .asFunction(); - -final setRefreshFromBlockHeightNative = havenApi - .lookup>( - 'set_refresh_from_block_height') - .asFunction(); - -final setRecoveringFromSeedNative = havenApi - .lookup>( - 'set_recovering_from_seed') - .asFunction(); - -final storeNative = - havenApi.lookup>('store').asFunction(); - -final setPasswordNative = - havenApi.lookup>('set_password').asFunction(); - -final setListenerNative = havenApi - .lookup>('set_listener') - .asFunction(); - -final getSyncingHeightNative = havenApi - .lookup>('get_syncing_height') - .asFunction(); - -final isNeededToRefreshNative = havenApi - .lookup>('is_needed_to_refresh') - .asFunction(); - -final isNewTransactionExistNative = havenApi - .lookup>( - 'is_new_transaction_exist') - .asFunction(); - -final getSecretViewKeyNative = havenApi - .lookup>('secret_view_key') - .asFunction(); - -final getPublicViewKeyNative = havenApi - .lookup>('public_view_key') - .asFunction(); - -final getSecretSpendKeyNative = havenApi - .lookup>('secret_spend_key') - .asFunction(); - -final getPublicSpendKeyNative = havenApi - .lookup>('public_spend_key') - .asFunction(); - -final closeCurrentWalletNative = havenApi - .lookup>('close_current_wallet') - .asFunction(); - -final onStartupNative = havenApi - .lookup>('on_startup') - .asFunction(); - -final rescanBlockchainAsyncNative = havenApi - .lookup>('rescan_blockchain') - .asFunction(); - -final setTrustedDaemonNative = havenApi - .lookup>('set_trusted_daemon') - .asFunction(); - -final trustedDaemonNative = havenApi - .lookup>('trusted_daemon') - .asFunction(); - -int getSyncingHeight() => getSyncingHeightNative(); - -bool isNeededToRefresh() => isNeededToRefreshNative() != 0; - -bool isNewTransactionExist() => isNewTransactionExistNative() != 0; - -String getFilename() => convertUTF8ToString(pointer: getFileNameNative()); - -String getSeed() => convertUTF8ToString(pointer: getSeedNative()); - -String getAddress({int accountIndex = 0, int addressIndex = 0}) => - convertUTF8ToString(pointer: getAddressNative(accountIndex, addressIndex)); - -int getFullBalance({int accountIndex = 0}) => - getFullBalanceNative(accountIndex); - -int getUnlockedBalance({int accountIndex = 0}) => - getUnlockedBalanceNative(accountIndex); - -int getCurrentHeight() => getCurrentHeightNative(); - -int getNodeHeightSync() => getNodeHeightNative(); - -bool isConnectedSync() => isConnectedNative() != 0; - -bool setupNodeSync( - {required String address, - String? login, - String? password, - bool useSSL = false, - bool isLightWallet = false, - String? socksProxyAddress}) { - final addressPointer = address.toNativeUtf8(); - Pointer? loginPointer; - Pointer? socksProxyAddressPointer; - Pointer? passwordPointer; - - if (login != null) { - loginPointer = login.toNativeUtf8(); - } - - if (password != null) { - passwordPointer = password.toNativeUtf8(); - } - - if (socksProxyAddress != null) { - socksProxyAddressPointer = socksProxyAddress.toNativeUtf8(); - } - - final errorMessagePointer = ''.toNativeUtf8(); - final isSetupNode = setupNodeNative( - addressPointer, - loginPointer, - passwordPointer, - _boolToInt(useSSL), - _boolToInt(isLightWallet), - socksProxyAddressPointer, - errorMessagePointer) != - 0; - - calloc.free(addressPointer); - - if (loginPointer != null) { - calloc.free(loginPointer); - } - - if (passwordPointer != null) { - calloc.free(passwordPointer); - } - - if (!isSetupNode) { - throw SetupWalletException( - message: convertUTF8ToString(pointer: errorMessagePointer)); - } - - return isSetupNode; -} - -void startRefreshSync() => startRefreshNative(); - -Future connectToNode() async => connecToNodeNative() != 0; - -void setRefreshFromBlockHeight({required int height}) => - setRefreshFromBlockHeightNative(height); - -void setRecoveringFromSeed({required bool isRecovery}) => - setRecoveringFromSeedNative(_boolToInt(isRecovery)); - -void storeSync() { - final pathPointer = ''.toNativeUtf8(); - storeNative(pathPointer); - calloc.free(pathPointer); -} - -void setPasswordSync(String password) { - final passwordPointer = password.toNativeUtf8(); - final errorMessagePointer = calloc(); - final changed = setPasswordNative(passwordPointer, errorMessagePointer) != 0; - calloc.free(passwordPointer); - - if (!changed) { - final message = errorMessagePointer.ref.getValue(); - calloc.free(errorMessagePointer); - throw Exception(message); - } - - calloc.free(errorMessagePointer); -} - -void closeCurrentWallet() => closeCurrentWalletNative(); - -String getSecretViewKey() => - convertUTF8ToString(pointer: getSecretViewKeyNative()); - -String getPublicViewKey() => - convertUTF8ToString(pointer: getPublicViewKeyNative()); - -String getSecretSpendKey() => - convertUTF8ToString(pointer: getSecretSpendKeyNative()); - -String getPublicSpendKey() => - convertUTF8ToString(pointer: getPublicSpendKeyNative()); - -class SyncListener { - SyncListener(this.onNewBlock, this.onNewTransaction) - : _cachedBlockchainHeight = 0, - _lastKnownBlockHeight = 0, - _initialSyncHeight = 0; - - void Function(int, int, double) onNewBlock; - void Function() onNewTransaction; - - Timer? _updateSyncInfoTimer; - int _cachedBlockchainHeight; - int _lastKnownBlockHeight; - int _initialSyncHeight; - - Future getNodeHeightOrUpdate(int baseHeight) async { - if (_cachedBlockchainHeight < baseHeight || _cachedBlockchainHeight == 0) { - _cachedBlockchainHeight = await getNodeHeight(); - } - - return _cachedBlockchainHeight; - } - - void start() { - _cachedBlockchainHeight = 0; - _lastKnownBlockHeight = 0; - _initialSyncHeight = 0; - _updateSyncInfoTimer ??= - Timer.periodic(Duration(milliseconds: 1200), (_) async { - if (isNewTransactionExist()) { - onNewTransaction?.call(); - } - - var syncHeight = getSyncingHeight(); - - if (syncHeight <= 0) { - syncHeight = getCurrentHeight(); - } - - if (_initialSyncHeight <= 0) { - _initialSyncHeight = syncHeight; - } - - final bchHeight = await getNodeHeightOrUpdate(syncHeight); - - if (_lastKnownBlockHeight == syncHeight || syncHeight == null) { - return; - } - - _lastKnownBlockHeight = syncHeight; - final track = bchHeight - _initialSyncHeight; - final diff = track - (bchHeight - syncHeight); - final ptc = diff <= 0 ? 0.0 : diff / track; - final left = bchHeight - syncHeight; - - if (syncHeight < 0 || left < 0) { - return; - } - - // 1. Actual new height; 2. Blocks left to finish; 3. Progress in percents; - onNewBlock?.call(syncHeight, left, ptc); - }); - } - - void stop() => _updateSyncInfoTimer?.cancel(); -} - -SyncListener setListeners(void Function(int, int, double) onNewBlock, - void Function() onNewTransaction) { - final listener = SyncListener(onNewBlock, onNewTransaction); - setListenerNative(); - return listener; -} - -void onStartup() => onStartupNative(); - -void _storeSync(Object _) => storeSync(); - -bool _setupNodeSync(Map args) { - final address = args['address'] as String; - final login = (args['login'] ?? '') as String; - final password = (args['password'] ?? '') as String; - final useSSL = args['useSSL'] as bool; - final isLightWallet = args['isLightWallet'] as bool; - final socksProxyAddress = (args['socksProxyAddress'] ?? '') as String; - - return setupNodeSync( - address: address, - login: login, - password: password, - useSSL: useSSL, - isLightWallet: isLightWallet, - socksProxyAddress: socksProxyAddress); -} - -bool _isConnected(Object _) => isConnectedSync(); - -int _getNodeHeight(Object _) => getNodeHeightSync(); - -void startRefresh() => startRefreshSync(); - -Future setupNode( - {required String address, - String? login, - String? password, - bool useSSL = false, - String? socksProxyAddress, - bool isLightWallet = false}) => - compute, void>(_setupNodeSync, { - 'address': address, - 'login': login, - 'password': password, - 'useSSL': useSSL, - 'isLightWallet': isLightWallet, - 'socksProxyAddress': socksProxyAddress - }); - -Future store() => compute(_storeSync, 0); - -Future isConnected() => compute(_isConnected, 0); - -Future getNodeHeight() => compute(_getNodeHeight, 0); - -void rescanBlockchainAsync() => rescanBlockchainAsyncNative(); - -Future setTrustedDaemon(bool trusted) async => setTrustedDaemonNative(_boolToInt(trusted)); - -Future trustedDaemon() async => trustedDaemonNative() != 0; diff --git a/cw_haven/lib/api/wallet_manager.dart b/cw_haven/lib/api/wallet_manager.dart deleted file mode 100644 index 627fc226a..000000000 --- a/cw_haven/lib/api/wallet_manager.dart +++ /dev/null @@ -1,248 +0,0 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; -import 'package:flutter/foundation.dart'; -import 'package:cw_haven/api/convert_utf8_to_string.dart'; -import 'package:cw_haven/api/signatures.dart'; -import 'package:cw_haven/api/types.dart'; -import 'package:cw_haven/api/haven_api.dart'; -import 'package:cw_haven/api/wallet.dart'; -import 'package:cw_haven/api/exceptions/wallet_opening_exception.dart'; -import 'package:cw_haven/api/exceptions/wallet_creation_exception.dart'; -import 'package:cw_haven/api/exceptions/wallet_restore_from_keys_exception.dart'; -import 'package:cw_haven/api/exceptions/wallet_restore_from_seed_exception.dart'; - -final createWalletNative = havenApi - .lookup>('create_wallet') - .asFunction(); - -final restoreWalletFromSeedNative = havenApi - .lookup>( - 'restore_wallet_from_seed') - .asFunction(); - -final restoreWalletFromKeysNative = havenApi - .lookup>( - 'restore_wallet_from_keys') - .asFunction(); - -final isWalletExistNative = havenApi - .lookup>('is_wallet_exist') - .asFunction(); - -final loadWalletNative = havenApi - .lookup>('load_wallet') - .asFunction(); - -final errorStringNative = havenApi - .lookup>('error_string') - .asFunction(); - -void createWalletSync( - {required String path, required String password, required String language, int nettype = 0}) { - final pathPointer = path.toNativeUtf8(); - final passwordPointer = password.toNativeUtf8(); - final languagePointer = language.toNativeUtf8(); - final errorMessagePointer = ''.toNativeUtf8(); - final isWalletCreated = createWalletNative(pathPointer, passwordPointer, - languagePointer, nettype, errorMessagePointer) != - 0; - - calloc.free(pathPointer); - calloc.free(passwordPointer); - calloc.free(languagePointer); - - if (!isWalletCreated) { - throw WalletCreationException( - message: convertUTF8ToString(pointer: errorMessagePointer)); - } - - // setupNodeSync(address: "node.moneroworld.com:18089"); -} - -bool isWalletExistSync({required String path}) { - final pathPointer = path.toNativeUtf8(); - final isExist = isWalletExistNative(pathPointer) != 0; - - calloc.free(pathPointer); - - return isExist; -} - -void restoreWalletFromSeedSync( - {required String path, - required String password, - required String seed, - int nettype = 0, - int restoreHeight = 0}) { - final pathPointer = path.toNativeUtf8(); - final passwordPointer = password.toNativeUtf8(); - final seedPointer = seed.toNativeUtf8(); - final errorMessagePointer = ''.toNativeUtf8(); - final isWalletRestored = restoreWalletFromSeedNative( - pathPointer, - passwordPointer, - seedPointer, - nettype, - restoreHeight, - errorMessagePointer) != - 0; - - calloc.free(pathPointer); - calloc.free(passwordPointer); - calloc.free(seedPointer); - - if (!isWalletRestored) { - throw WalletRestoreFromSeedException( - message: convertUTF8ToString(pointer: errorMessagePointer)); - } -} - -void restoreWalletFromKeysSync( - {required String path, - required String password, - required String language, - required String address, - required String viewKey, - required String spendKey, - int nettype = 0, - int restoreHeight = 0}) { - final pathPointer = path.toNativeUtf8(); - final passwordPointer = password.toNativeUtf8(); - final languagePointer = language.toNativeUtf8(); - final addressPointer = address.toNativeUtf8(); - final viewKeyPointer = viewKey.toNativeUtf8(); - final spendKeyPointer = spendKey.toNativeUtf8(); - final errorMessagePointer = ''.toNativeUtf8(); - final isWalletRestored = restoreWalletFromKeysNative( - pathPointer, - passwordPointer, - languagePointer, - addressPointer, - viewKeyPointer, - spendKeyPointer, - nettype, - restoreHeight, - errorMessagePointer) != - 0; - - calloc.free(pathPointer); - calloc.free(passwordPointer); - calloc.free(languagePointer); - calloc.free(addressPointer); - calloc.free(viewKeyPointer); - calloc.free(spendKeyPointer); - - if (!isWalletRestored) { - throw WalletRestoreFromKeysException( - message: convertUTF8ToString(pointer: errorMessagePointer)); - } -} - -void loadWallet({required String path, required String password, int nettype = 0}) { - final pathPointer = path.toNativeUtf8(); - final passwordPointer = password.toNativeUtf8(); - final loaded = loadWalletNative(pathPointer, passwordPointer, nettype) != 0; - calloc.free(pathPointer); - calloc.free(passwordPointer); - - if (!loaded) { - throw WalletOpeningException( - message: convertUTF8ToString(pointer: errorStringNative())); - } -} - -void _createWallet(Map args) { - final path = args['path'] as String; - final password = args['password'] as String; - final language = args['language'] as String; - - createWalletSync(path: path, password: password, language: language); -} - -void _restoreFromSeed(Map args) { - final path = args['path'] as String; - final password = args['password'] as String; - final seed = args['seed'] as String; - final restoreHeight = args['restoreHeight'] as int; - - restoreWalletFromSeedSync( - path: path, password: password, seed: seed, restoreHeight: restoreHeight); -} - -void _restoreFromKeys(Map args) { - final path = args['path'] as String; - final password = args['password'] as String; - final language = args['language'] as String; - final restoreHeight = args['restoreHeight'] as int; - final address = args['address'] as String; - final viewKey = args['viewKey'] as String; - final spendKey = args['spendKey'] as String; - - restoreWalletFromKeysSync( - path: path, - password: password, - language: language, - restoreHeight: restoreHeight, - address: address, - viewKey: viewKey, - spendKey: spendKey); -} - -Future _openWallet(Map args) async => - loadWallet(path: args['path'] as String, password: args['password'] as String); - -bool _isWalletExist(String path) => isWalletExistSync(path: path); - -void openWallet({required String path, required String password, int nettype = 0}) async => - loadWallet(path: path, password: password, nettype: nettype); - -Future openWalletAsync(Map args) async => - compute(_openWallet, args); - -Future createWallet( - {required String path, - required String password, - required String language, - int nettype = 0}) async => - compute(_createWallet, { - 'path': path, - 'password': password, - 'language': language, - 'nettype': nettype - }); - -Future restoreFromSeed( - {required String path, - required String password, - required String seed, - int nettype = 0, - int restoreHeight = 0}) async => - compute, void>(_restoreFromSeed, { - 'path': path, - 'password': password, - 'seed': seed, - 'nettype': nettype, - 'restoreHeight': restoreHeight - }); - -Future restoreFromKeys( - {required String path, - required String password, - required String language, - required String address, - required String viewKey, - required String spendKey, - int nettype = 0, - int restoreHeight = 0}) async => - compute, void>(_restoreFromKeys, { - 'path': path, - 'password': password, - 'language': language, - 'address': address, - 'viewKey': viewKey, - 'spendKey': spendKey, - 'nettype': nettype, - 'restoreHeight': restoreHeight - }); - -Future isWalletExist({required String path}) => compute(_isWalletExist, path); diff --git a/cw_haven/lib/haven_account_list.dart b/cw_haven/lib/haven_account_list.dart deleted file mode 100644 index 41e3bbf57..000000000 --- a/cw_haven/lib/haven_account_list.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cw_core/account.dart'; -import 'package:cw_core/account_list.dart'; -import 'package:cw_haven/api/account_list.dart' as account_list; - -part 'haven_account_list.g.dart'; - -class HavenAccountList = HavenAccountListBase with _$HavenAccountList; - -abstract class HavenAccountListBase extends AccountList with Store { - HavenAccountListBase() - : accounts = ObservableList(), - _isRefreshing = false, - _isUpdating = false { - refresh(); - } - - @override - @observable - ObservableList accounts; - bool _isRefreshing; - bool _isUpdating; - - @override - void update() async { - if (_isUpdating) { - return; - } - - try { - _isUpdating = true; - refresh(); - final accounts = getAll(); - - if (accounts.isNotEmpty) { - this.accounts.clear(); - this.accounts.addAll(accounts); - } - - _isUpdating = false; - } catch (e) { - _isUpdating = false; - rethrow; - } - } - - @override - List getAll() => account_list - .getAllAccount() - .map((accountRow) => Account( - id: accountRow.getId(), - label: accountRow.getLabel())) - .toList(); - - @override - Future addAccount({required String label}) async { - await account_list.addAccount(label: label); - update(); - } - - @override - Future setLabelAccount({required int accountIndex, required String label}) async { - await account_list.setLabelForAccount( - accountIndex: accountIndex, label: label); - update(); - } - - @override - void refresh() { - if (_isRefreshing) { - return; - } - - try { - _isRefreshing = true; - account_list.refreshAccounts(); - _isRefreshing = false; - } catch (e) { - _isRefreshing = false; - printV(e); - rethrow; - } - } -} diff --git a/cw_haven/lib/haven_balance.dart b/cw_haven/lib/haven_balance.dart deleted file mode 100644 index 7d257ded9..000000000 --- a/cw_haven/lib/haven_balance.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/monero_balance.dart'; -import 'package:cw_haven/api/balance_list.dart'; -import 'package:cw_haven/api/structs/haven_balance_row.dart'; - -const inactiveBalances = [ - CryptoCurrency.xcad, - CryptoCurrency.xjpy, - CryptoCurrency.xnok, - CryptoCurrency.xnzd]; - -Map getHavenBalance({required int accountIndex}) { - final fullBalances = getHavenFullBalance(accountIndex: accountIndex); - final unlockedBalances = getHavenUnlockedBalance(accountIndex: accountIndex); - final havenBalances = {}; - final balancesLength = fullBalances.length; - - for (int i = 0; i < balancesLength; i++) { - final assetType = fullBalances[i].getAssetType(); - final fullBalance = fullBalances[i].getAmount(); - final unlockedBalance = unlockedBalances[i].getAmount(); - final moneroBalance = MoneroBalance( - fullBalance: fullBalance, unlockedBalance: unlockedBalance); - final currency = CryptoCurrency.fromString(assetType); - - if (inactiveBalances.indexOf(currency) >= 0) { - continue; - } - - havenBalances[currency] = moneroBalance; - } - - return havenBalances; -} \ No newline at end of file diff --git a/cw_haven/lib/haven_subaddress_list.dart b/cw_haven/lib/haven_subaddress_list.dart deleted file mode 100644 index 8a5125a55..000000000 --- a/cw_haven/lib/haven_subaddress_list.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_haven/api/structs/subaddress_row.dart'; -import 'package:flutter/services.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cw_haven/api/subaddress_list.dart' as subaddress_list; -import 'package:cw_core/subaddress.dart'; - -part 'haven_subaddress_list.g.dart'; - -class HavenSubaddressList = HavenSubaddressListBase - with _$HavenSubaddressList; - -abstract class HavenSubaddressListBase with Store { - HavenSubaddressListBase() - : _isRefreshing = false, - _isUpdating = false, - subaddresses = ObservableList(); - - @observable - ObservableList subaddresses; - - bool _isRefreshing; - bool _isUpdating; - - void update({required int accountIndex}) { - if (_isUpdating) { - return; - } - - try { - _isUpdating = true; - refresh(accountIndex: accountIndex); - subaddresses.clear(); - subaddresses.addAll(getAll()); - _isUpdating = false; - } catch (e) { - _isUpdating = false; - rethrow; - } - } - - List getAll() { - var subaddresses = subaddress_list.getAllSubaddresses(); - - if (subaddresses.length > 2) { - final primary = subaddresses.first; - final rest = subaddresses.sublist(1).reversed; - subaddresses = [primary] + rest.toList(); - } - - return subaddresses - .map((subaddressRow) => Subaddress( - id: subaddressRow.getId(), - address: subaddressRow.getAddress(), - label: subaddressRow.getLabel())) - .toList(); - } - - Future addSubaddress({required int accountIndex, required String label}) async { - await subaddress_list.addSubaddress( - accountIndex: accountIndex, label: label); - update(accountIndex: accountIndex); - } - - Future setLabelSubaddress( - {required int accountIndex, required int addressIndex, required String label}) async { - await subaddress_list.setLabelForSubaddress( - accountIndex: accountIndex, addressIndex: addressIndex, label: label); - update(accountIndex: accountIndex); - } - - void refresh({required int accountIndex}) { - if (_isRefreshing) { - return; - } - - try { - _isRefreshing = true; - subaddress_list.refreshSubaddresses(accountIndex: accountIndex); - _isRefreshing = false; - } on PlatformException catch (e) { - _isRefreshing = false; - printV(e); - rethrow; - } - } -} diff --git a/cw_haven/lib/haven_transaction_creation_credentials.dart b/cw_haven/lib/haven_transaction_creation_credentials.dart deleted file mode 100644 index 4de41a504..000000000 --- a/cw_haven/lib/haven_transaction_creation_credentials.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:cw_core/monero_transaction_priority.dart'; -import 'package:cw_core/output_info.dart'; - -class HavenTransactionCreationCredentials { - HavenTransactionCreationCredentials({ - required this.outputs, - required this.priority, - required this.assetType}); - - final List outputs; - final MoneroTransactionPriority priority; - final String assetType; -} diff --git a/cw_haven/lib/haven_transaction_creation_exception.dart b/cw_haven/lib/haven_transaction_creation_exception.dart deleted file mode 100644 index 768abe7fa..000000000 --- a/cw_haven/lib/haven_transaction_creation_exception.dart +++ /dev/null @@ -1,8 +0,0 @@ -class HavenTransactionCreationException implements Exception { - HavenTransactionCreationException(this.message); - - final String message; - - @override - String toString() => message; -} \ No newline at end of file diff --git a/cw_haven/lib/haven_transaction_history.dart b/cw_haven/lib/haven_transaction_history.dart deleted file mode 100644 index b456174fe..000000000 --- a/cw_haven/lib/haven_transaction_history.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'dart:core'; -import 'package:mobx/mobx.dart'; -import 'package:cw_core/transaction_history.dart'; -import 'package:cw_haven/haven_transaction_info.dart'; - -part 'haven_transaction_history.g.dart'; - -class HavenTransactionHistory = HavenTransactionHistoryBase - with _$HavenTransactionHistory; - -abstract class HavenTransactionHistoryBase - extends TransactionHistoryBase with Store { - HavenTransactionHistoryBase() { - transactions = ObservableMap(); - } - - @override - Future save() async {} - - @override - void addOne(HavenTransactionInfo transaction) => - transactions[transaction.id] = transaction; - - @override - void addMany(Map transactions) => - this.transactions.addAll(transactions); -} diff --git a/cw_haven/lib/haven_transaction_info.dart b/cw_haven/lib/haven_transaction_info.dart deleted file mode 100644 index 6ffb60e6b..000000000 --- a/cw_haven/lib/haven_transaction_info.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:cw_core/transaction_info.dart'; -import 'package:cw_core/monero_amount_format.dart'; -import 'package:cw_haven/api/structs/transaction_info_row.dart'; -import 'package:cw_core/parseBoolFromString.dart'; -import 'package:cw_core/transaction_direction.dart'; -import 'package:cw_core/format_amount.dart'; -import 'package:cw_haven/api/transaction_history.dart'; - -class HavenTransactionInfo extends TransactionInfo { - HavenTransactionInfo(this.id, this.height, this.direction, this.date, - this.isPending, this.amount, this.accountIndex, this.addressIndex, this.fee, - this.confirmations); - - HavenTransactionInfo.fromMap(Map map) - : id = (map['hash'] ?? '') as String, - height = (map['height'] ?? 0) as int, - direction = - parseTransactionDirectionFromNumber(map['direction'] as String) ?? - TransactionDirection.incoming, - date = DateTime.fromMillisecondsSinceEpoch( - int.parse(map['timestamp'] as String? ?? '0') * 1000), - isPending = parseBoolFromString(map['isPending'] as String), - amount = map['amount'] as int, - accountIndex = int.parse(map['accountIndex'] as String), - addressIndex = map['addressIndex'] as int, - confirmations = map['confirmations'] as int, - key = getTxKey((map['hash'] ?? '') as String), - fee = map['fee'] as int? ?? 0; - - HavenTransactionInfo.fromRow(TransactionInfoRow row) - : id = row.getHash(), - height = row.blockHeight, - direction = parseTransactionDirectionFromInt(row.direction) ?? - TransactionDirection.incoming, - date = DateTime.fromMillisecondsSinceEpoch(row.getDatetime() * 1000), - isPending = row.isPending != 0, - amount = row.getAmount(), - accountIndex = row.subaddrAccount, - addressIndex = row.subaddrIndex, - confirmations = row.confirmations, - key = null, //getTxKey(row.getHash()), - fee = row.fee, - assetType = row.getAssetType(); - - final String id; - final int height; - final TransactionDirection direction; - final DateTime date; - final int accountIndex; - final bool isPending; - final int amount; - final int fee; - final int addressIndex; - final int confirmations; - late String recipientAddress; - late String assetType; - String? _fiatAmount; - String? key; - - @override - String amountFormatted() => - '${formatAmount(moneroAmountToString(amount: amount))} $assetType'; - - @override - String fiatAmount() => _fiatAmount ?? ''; - - @override - void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); - - @override - String feeFormatted() => - '${formatAmount(moneroAmountToString(amount: fee))} $assetType'; -} diff --git a/cw_haven/lib/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart deleted file mode 100644 index 6316f5a62..000000000 --- a/cw_haven/lib/haven_wallet.dart +++ /dev/null @@ -1,435 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/pathForWallet.dart'; -import 'package:cw_core/transaction_priority.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_haven/haven_transaction_creation_credentials.dart'; -import 'package:cw_core/monero_amount_format.dart'; -import 'package:cw_haven/haven_transaction_creation_exception.dart'; -import 'package:cw_haven/haven_transaction_info.dart'; -import 'package:cw_haven/haven_wallet_addresses.dart'; -import 'package:cw_core/monero_wallet_utils.dart'; -import 'package:cw_haven/api/structs/pending_transaction.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cw_haven/api/transaction_history.dart' as haven_transaction_history; -import 'package:cw_haven/api/wallet.dart' as haven_wallet; -import 'package:cw_haven/api/transaction_history.dart' as transaction_history; -import 'package:cw_haven/api/monero_output.dart'; -import 'package:cw_haven/pending_haven_transaction.dart'; -import 'package:cw_core/monero_wallet_keys.dart'; -import 'package:cw_core/monero_balance.dart'; -import 'package:cw_haven/haven_transaction_history.dart'; -import 'package:cw_core/account.dart'; -import 'package:cw_core/pending_transaction.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/sync_status.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/node.dart'; -import 'package:cw_core/monero_transaction_priority.dart'; -import 'package:cw_haven/haven_balance.dart'; - -part 'haven_wallet.g.dart'; - -const moneroBlockSize = 1000; - -class HavenWallet = HavenWalletBase with _$HavenWallet; - -abstract class HavenWalletBase - extends WalletBase with Store { - HavenWalletBase({required WalletInfo walletInfo, String? password}) - : balance = ObservableMap.of(getHavenBalance(accountIndex: 0)), - _isTransactionUpdating = false, - _password = password ?? '', - _hasSyncAfterStartup = false, - walletAddresses = HavenWalletAddresses(walletInfo), - syncStatus = NotConnectedSyncStatus(), - super(walletInfo) { - transactionHistory = HavenTransactionHistory(); - _onAccountChangeReaction = reaction((_) => walletAddresses.account, (Account? account) { - if (account == null) { - return; - } - balance.addAll(getHavenBalance(accountIndex: account.id)); - walletAddresses.updateSubaddressList(accountIndex: account.id); - }); - } - - static const int _autoSaveInterval = 30; - final String _password; - - @override - HavenWalletAddresses walletAddresses; - - @override - @observable - SyncStatus syncStatus; - - @override - @observable - ObservableMap balance; - - @override - String get seed => haven_wallet.getSeed(); - - @override - bool get hasRescan => true; - - @override - MoneroWalletKeys get keys => MoneroWalletKeys( - primaryAddress: haven_wallet.getAddress(accountIndex: 0, addressIndex: 0), - privateSpendKey: haven_wallet.getSecretSpendKey(), - privateViewKey: haven_wallet.getSecretViewKey(), - publicSpendKey: haven_wallet.getPublicSpendKey(), - publicViewKey: haven_wallet.getPublicViewKey(), - passphrase: ""); - - haven_wallet.SyncListener? _listener; - ReactionDisposer? _onAccountChangeReaction; - bool _isTransactionUpdating; - bool _hasSyncAfterStartup; - Timer? _autoSaveTimer; - - Future init() async { - await walletAddresses.init(); - balance.addAll(getHavenBalance(accountIndex: walletAddresses.account?.id ?? 0)); - _setListeners(); - await updateTransactions(); - - if (walletInfo.isRecovery) { - haven_wallet.setRecoveringFromSeed(isRecovery: walletInfo.isRecovery); - - if (haven_wallet.getCurrentHeight() <= 1) { - haven_wallet.setRefreshFromBlockHeight(height: walletInfo.restoreHeight); - } - } - - _autoSaveTimer = - Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save()); - } - - @override - Future? updateBalance() => null; - - @override - Future close({bool shouldCleanup = false}) async { - _listener?.stop(); - _onAccountChangeReaction?.reaction.dispose(); - _autoSaveTimer?.cancel(); - } - - @override - Future connectToNode({required Node node}) async { - try { - syncStatus = ConnectingSyncStatus(); - await haven_wallet.setupNode( - address: node.uriRaw, - login: node.login, - password: node.password, - useSSL: node.useSSL ?? false, - isLightWallet: false, - // FIXME: hardcoded value - socksProxyAddress: node.socksProxyAddress); - - haven_wallet.setTrustedDaemon(node.trusted); - syncStatus = ConnectedSyncStatus(); - } catch (e) { - syncStatus = FailedSyncStatus(); - printV(e); - } - } - - @override - Future startSync() async { - try { - _setInitialHeight(); - } catch (_) {} - - try { - syncStatus = AttemptingSyncStatus(); - haven_wallet.startRefresh(); - _setListeners(); - _listener?.start(); - } catch (e) { - syncStatus = FailedSyncStatus(); - printV(e); - rethrow; - } - } - - @override - Future createTransaction(Object credentials) async { - final _credentials = credentials as HavenTransactionCreationCredentials; - final outputs = _credentials.outputs; - final hasMultiDestination = outputs.length > 1; - final assetType = CryptoCurrency.fromString(_credentials.assetType.toLowerCase()); - final balances = getHavenBalance(accountIndex: walletAddresses.account!.id); - final unlockedBalance = balances[assetType]!.unlockedBalance; - - PendingTransactionDescription pendingTransactionDescription; - - if (!(syncStatus is SyncedSyncStatus)) { - throw HavenTransactionCreationException('The wallet is not synced.'); - } - - if (hasMultiDestination) { - if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { - throw HavenTransactionCreationException( - 'You do not have enough coins to send this amount.'); - } - - final int totalAmount = - outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); - - if (unlockedBalance < totalAmount) { - throw HavenTransactionCreationException( - 'You do not have enough coins to send this amount.'); - } - - final moneroOutputs = outputs - .map((output) => MoneroOutput( - address: output.address, amount: output.cryptoAmount!.replaceAll(',', '.'))) - .toList(); - - pendingTransactionDescription = await transaction_history.createTransactionMultDest( - outputs: moneroOutputs, - priorityRaw: _credentials.priority.serialize(), - accountIndex: walletAddresses.account!.id); - } else { - final output = outputs.first; - final address = output.isParsedAddress && (output.extractedAddress?.isNotEmpty ?? false) - ? output.extractedAddress! - : output.address; - final amount = output.sendAll ? null : output.cryptoAmount!.replaceAll(',', '.'); - final int? formattedAmount = output.sendAll ? null : output.formattedCryptoAmount; - - if ((formattedAmount != null && unlockedBalance < formattedAmount) || - (formattedAmount == null && unlockedBalance <= 0)) { - final formattedBalance = moneroAmountToString(amount: unlockedBalance); - - throw HavenTransactionCreationException( - 'You do not have enough unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.'); - } - - pendingTransactionDescription = await transaction_history.createTransaction( - address: address, - assetType: _credentials.assetType, - amount: amount, - priorityRaw: _credentials.priority.serialize(), - accountIndex: walletAddresses.account!.id); - } - - return PendingHavenTransaction(pendingTransactionDescription, assetType); - } - - @override - int calculateEstimatedFee(TransactionPriority priority, int? amount) { - // FIXME: hardcoded value; - - if (priority is MoneroTransactionPriority) { - switch (priority) { - case MoneroTransactionPriority.slow: - return 24590000; - case MoneroTransactionPriority.automatic: - return 123050000; - case MoneroTransactionPriority.medium: - return 245029999; - case MoneroTransactionPriority.fast: - return 614530000; - case MoneroTransactionPriority.fastest: - return 26021600000; - } - } - - return 0; - } - - @override - Future save() async { - await walletAddresses.updateAddressesInBox(); - await backupWalletFiles(name); - await haven_wallet.store(); - } - - @override - Future renameWalletFiles(String newWalletName) async { - final currentWalletPath = await pathForWallet(name: name, type: type); - final currentCacheFile = File(currentWalletPath); - final currentKeysFile = File('$currentWalletPath.keys'); - final currentAddressListFile = File('$currentWalletPath.address.txt'); - - final newWalletPath = await pathForWallet(name: newWalletName, type: type); - - // Copies current wallet files into new wallet name's dir and files - if (currentCacheFile.existsSync()) { - await currentCacheFile.copy(newWalletPath); - } - if (currentKeysFile.existsSync()) { - await currentKeysFile.copy('$newWalletPath.keys'); - } - if (currentAddressListFile.existsSync()) { - await currentAddressListFile.copy('$newWalletPath.address.txt'); - } - - // Delete old name's dir and files - await Directory(currentWalletPath).delete(recursive: true); - } - - @override - Future changePassword(String password) async { - haven_wallet.setPasswordSync(password); - } - - Future getNodeHeight() async => haven_wallet.getNodeHeight(); - - Future isConnected() async => haven_wallet.isConnected(); - - Future setAsRecovered() async { - walletInfo.isRecovery = false; - await walletInfo.save(); - } - - @override - Future rescan({required int height}) async { - walletInfo.restoreHeight = height; - walletInfo.isRecovery = true; - haven_wallet.setRefreshFromBlockHeight(height: height); - haven_wallet.rescanBlockchainAsync(); - await startSync(); - _askForUpdateBalance(); - walletAddresses.accountList.update(); - await _askForUpdateTransactionHistory(); - await save(); - await walletInfo.save(); - } - - String getTransactionAddress(int accountIndex, int addressIndex) => - haven_wallet.getAddress(accountIndex: accountIndex, addressIndex: addressIndex); - - @override - Future> fetchTransactions() async { - haven_transaction_history.refreshTransactions(); - return _getAllTransactions(null) - .fold>({}, - (Map acc, HavenTransactionInfo tx) { - acc[tx.id] = tx; - return acc; - }); - } - - Future updateTransactions() async { - try { - if (_isTransactionUpdating) { - return; - } - - _isTransactionUpdating = true; - final transactions = await fetchTransactions(); - transactionHistory.addMany(transactions); - await transactionHistory.save(); - _isTransactionUpdating = false; - } catch (e) { - printV(e); - _isTransactionUpdating = false; - } - } - - List _getAllTransactions(dynamic _) => haven_transaction_history - .getAllTransations() - .map((row) => HavenTransactionInfo.fromRow(row)) - .toList(); - - void _setListeners() { - _listener?.stop(); - _listener = haven_wallet.setListeners(_onNewBlock, _onNewTransaction); - } - - void _setInitialHeight() { - if (walletInfo.isRecovery) { - return; - } - - final currentHeight = haven_wallet.getCurrentHeight(); - - if (currentHeight <= 1) { - final height = _getHeightByDate(walletInfo.date); - haven_wallet.setRecoveringFromSeed(isRecovery: true); - haven_wallet.setRefreshFromBlockHeight(height: height); - } - } - - int _getHeightDistance(DateTime date) { - final distance = DateTime.now().millisecondsSinceEpoch - date.millisecondsSinceEpoch; - final daysTmp = (distance / 86400).round(); - final days = daysTmp < 1 ? 1 : daysTmp; - - return days * 1000; - } - - int _getHeightByDate(DateTime date) { - final nodeHeight = haven_wallet.getNodeHeightSync(); - final heightDistance = _getHeightDistance(date); - - if (nodeHeight <= 0) { - return 0; - } - - return nodeHeight - heightDistance; - } - - void _askForUpdateBalance() => - balance.addAll(getHavenBalance(accountIndex: walletAddresses.account!.id)); - - Future _askForUpdateTransactionHistory() async => await updateTransactions(); - - void _onNewBlock(int height, int blocksLeft, double ptc) async { - try { - if (walletInfo.isRecovery) { - await _askForUpdateTransactionHistory(); - _askForUpdateBalance(); - walletAddresses.accountList.update(); - } - - if (blocksLeft < 1000) { - await _askForUpdateTransactionHistory(); - _askForUpdateBalance(); - walletAddresses.accountList.update(); - syncStatus = SyncedSyncStatus(); - - if (!_hasSyncAfterStartup) { - _hasSyncAfterStartup = true; - await save(); - } - - if (walletInfo.isRecovery) { - await setAsRecovered(); - } - } else { - syncStatus = SyncingSyncStatus(blocksLeft, ptc); - } - } catch (e) { - printV(e.toString()); - } - } - - void _onNewTransaction() async { - try { - await _askForUpdateTransactionHistory(); - _askForUpdateBalance(); - await Future.delayed(Duration(seconds: 1)); - } catch (e) { - printV(e.toString()); - } - } - - @override - String get password => _password; - - @override - Future signMessage(String message, {String? address = null}) => - throw UnimplementedError(); - - @override - Future verifyMessage(String message, String signature, {String? address = null}) => - throw UnimplementedError(); -} diff --git a/cw_haven/lib/haven_wallet_addresses.dart b/cw_haven/lib/haven_wallet_addresses.dart deleted file mode 100644 index 192c09ef7..000000000 --- a/cw_haven/lib/haven_wallet_addresses.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_addresses_with_account.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/account.dart'; -import 'package:cw_haven/api/wallet.dart'; -import 'package:cw_haven/haven_account_list.dart'; -import 'package:cw_haven/haven_subaddress_list.dart'; -import 'package:cw_core/subaddress.dart'; -import 'package:mobx/mobx.dart'; - -part 'haven_wallet_addresses.g.dart'; - -class HavenWalletAddresses = HavenWalletAddressesBase - with _$HavenWalletAddresses; - -abstract class HavenWalletAddressesBase extends WalletAddressesWithAccount with Store { - HavenWalletAddressesBase(WalletInfo walletInfo) - : accountList = HavenAccountList(), - subaddressList = HavenSubaddressList(), - address = '', - super(walletInfo); - - @override - @observable - String address; - - String get primaryAddress => address; - - // @override - @observable - Account? account; - - @observable - Subaddress? subaddress; - - HavenSubaddressList subaddressList; - - HavenAccountList accountList; - - @override - Future init() async { - accountList.update(); - account = accountList.accounts.isEmpty ? Account(id: 0, label: "Primary address") : accountList.accounts.first; - updateSubaddressList(accountIndex: account?.id ?? 0); - await updateAddressesInBox(); - } - - @override - Future updateAddressesInBox() async { - try { - final _subaddressList = HavenSubaddressList(); - - addressesMap.clear(); - - accountList.accounts.forEach((account) { - _subaddressList.update(accountIndex: account.id); - _subaddressList.subaddresses.forEach((subaddress) { - addressesMap[subaddress.address] = subaddress.label; - }); - }); - - await saveAddressesInBox(); - } catch (e) { - printV(e.toString()); - } - } - - bool validate() { - accountList.update(); - final accountListLength = accountList.accounts.length; - - if (accountListLength <= 0) { - return false; - } - - subaddressList.update(accountIndex: accountList.accounts.first.id); - final subaddressListLength = subaddressList.subaddresses.length; - - if (subaddressListLength <= 0) { - return false; - } - - return true; - } - - void updateSubaddressList({required int accountIndex}) { - subaddressList.update(accountIndex: accountIndex); - address = subaddressList.subaddresses.isNotEmpty - ? subaddressList.subaddresses.first.address - : getAddress(); - } - - @override - bool containsAddress(String address) => - addressInfos[account?.id ?? 0]?.any((it) => it.address == address) ?? false; -} diff --git a/cw_haven/lib/haven_wallet_service.dart b/cw_haven/lib/haven_wallet_service.dart deleted file mode 100644 index dec5f5564..000000000 --- a/cw_haven/lib/haven_wallet_service.dart +++ /dev/null @@ -1,259 +0,0 @@ -import 'dart:io'; -import 'package:collection/collection.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/monero_wallet_utils.dart'; -import 'package:hive/hive.dart'; -import 'package:cw_haven/api/wallet_manager.dart' as haven_wallet_manager; -import 'package:cw_haven/api/wallet.dart' as haven_wallet; -import 'package:cw_haven/api/exceptions/wallet_opening_exception.dart'; -import 'package:cw_haven/haven_wallet.dart'; -import 'package:cw_core/wallet_credentials.dart'; -import 'package:cw_core/wallet_service.dart'; -import 'package:cw_core/pathForWallet.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/wallet_type.dart'; - -class HavenNewWalletCredentials extends WalletCredentials { - HavenNewWalletCredentials({required String name, required this.language, String? password}) - : super(name: name, password: password); - - final String language; -} - -class HavenRestoreWalletFromSeedCredentials extends WalletCredentials { - HavenRestoreWalletFromSeedCredentials( - {required String name, - required String password, - required int height, - required this.mnemonic}) - : super(name: name, password: password, height: height); - - final String mnemonic; -} - -class HavenWalletLoadingException implements Exception { - @override - String toString() => 'Failure to load the wallet.'; -} - -class HavenRestoreWalletFromKeysCredentials extends WalletCredentials { - HavenRestoreWalletFromKeysCredentials( - {required String name, - required String password, - required this.language, - required this.address, - required this.viewKey, - required this.spendKey, - required int height}) - : super(name: name, password: password, height: height); - - final String language; - final String address; - final String viewKey; - final String spendKey; -} - -class HavenWalletService extends WalletService< - HavenNewWalletCredentials, - HavenRestoreWalletFromSeedCredentials, - HavenRestoreWalletFromKeysCredentials, - HavenNewWalletCredentials> { - HavenWalletService(this.walletInfoSource); - - final Box walletInfoSource; - - static bool walletFilesExist(String path) => - !File(path).existsSync() && !File('$path.keys').existsSync(); - - @override - WalletType getType() => WalletType.haven; - - @override - Future create(HavenNewWalletCredentials credentials, {bool? isTestnet}) async { - try { - final path = await pathForWallet(name: credentials.name, type: getType()); - await haven_wallet_manager.createWallet( - path: path, - password: credentials.password!, - language: credentials.language); - final wallet = HavenWallet(walletInfo: credentials.walletInfo!); - await wallet.init(); - return wallet; - } catch (e) { - // TODO: Implement Exception for wallet list service. - printV('HavenWalletsManager Error: ${e.toString()}'); - rethrow; - } - } - - @override - Future isWalletExit(String name) async { - try { - final path = await pathForWallet(name: name, type: getType()); - return haven_wallet_manager.isWalletExist(path: path); - } catch (e) { - // TODO: Implement Exception for wallet list service. - printV('HavenWalletsManager Error: $e'); - rethrow; - } - } - - @override - Future openWallet(String name, String password) async { - try { - final path = await pathForWallet(name: name, type: getType()); - - if (walletFilesExist(path)) { - await repairOldAndroidWallet(name); - } - - await haven_wallet_manager - .openWalletAsync({'path': path, 'password': password}); - final walletInfo = walletInfoSource.values.firstWhereOrNull( - (info) => info.id == WalletBase.idFor(name, getType()))!; - final wallet = HavenWallet(walletInfo: walletInfo); - final isValid = wallet.walletAddresses.validate(); - - if (!isValid) { - await restoreOrResetWalletFiles(name); - wallet.close(shouldCleanup: false); - return openWallet(name, password); - } - - await wallet.init(); - - return wallet; - } catch (e) { - // TODO: Implement Exception for wallet list service. - - if ((e.toString().contains('bad_alloc') || - (e is WalletOpeningException && - (e.message == 'std::bad_alloc' || - e.message.contains('bad_alloc')))) || - (e.toString().contains('does not correspond') || - (e is WalletOpeningException && - e.message.contains('does not correspond')))) { - await restoreOrResetWalletFiles(name); - return openWallet(name, password); - } - - rethrow; - } - } - - @override - Future remove(String wallet) async { - final path = await pathForWalletDir(name: wallet, type: getType()); - final file = Directory(path); - final isExist = file.existsSync(); - - if (isExist) { - await file.delete(recursive: true); - } - - final walletInfo = walletInfoSource.values - .firstWhere((info) => info.id == WalletBase.idFor(wallet, getType())); - await walletInfoSource.delete(walletInfo.key); - } - - @override - Future rename( - String currentName, String password, String newName) async { - final currentWalletInfo = walletInfoSource.values.firstWhere( - (info) => info.id == WalletBase.idFor(currentName, getType())); - final currentWallet = HavenWallet(walletInfo: currentWalletInfo); - - await currentWallet.renameWalletFiles(newName); - await saveBackup(newName); - - final newWalletInfo = currentWalletInfo; - newWalletInfo.id = WalletBase.idFor(newName, getType()); - newWalletInfo.name = newName; - - await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); - } - - @override - Future restoreFromHardwareWallet(HavenNewWalletCredentials credentials) { - throw UnimplementedError("Restoring a Haven wallet from a hardware wallet is not yet supported!"); - } - - @override - Future restoreFromKeys( - HavenRestoreWalletFromKeysCredentials credentials, {bool? isTestnet}) async { - try { - final path = await pathForWallet(name: credentials.name, type: getType()); - await haven_wallet_manager.restoreFromKeys( - path: path, - password: credentials.password!, - language: credentials.language, - restoreHeight: credentials.height!, - address: credentials.address, - viewKey: credentials.viewKey, - spendKey: credentials.spendKey); - final wallet = HavenWallet(walletInfo: credentials.walletInfo!); - await wallet.init(); - - return wallet; - } catch (e) { - // TODO: Implement Exception for wallet list service. - printV('HavenWalletsManager Error: $e'); - rethrow; - } - } - - @override - Future restoreFromSeed( - HavenRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { - try { - final path = await pathForWallet(name: credentials.name, type: getType()); - await haven_wallet_manager.restoreFromSeed( - path: path, - password: credentials.password!, - seed: credentials.mnemonic, - restoreHeight: credentials.height!); - final wallet = HavenWallet(walletInfo: credentials.walletInfo!); - await wallet.init(); - - return wallet; - } catch (e) { - // TODO: Implement Exception for wallet list service. - printV('HavenWalletsManager Error: $e'); - rethrow; - } - } - - Future repairOldAndroidWallet(String name) async { - try { - if (!Platform.isAndroid) { - return; - } - - final oldAndroidWalletDirPath = - await outdatedAndroidPathForWalletDir(name: name); - final dir = Directory(oldAndroidWalletDirPath); - - if (!dir.existsSync()) { - return; - } - - final newWalletDirPath = - await pathForWalletDir(name: name, type: getType()); - - dir.listSync().forEach((f) { - final file = File(f.path); - final name = f.path.split('/').last; - final newPath = newWalletDirPath + '/$name'; - final newFile = File(newPath); - - if (!newFile.existsSync()) { - newFile.createSync(); - } - newFile.writeAsBytesSync(file.readAsBytesSync()); - }); - } catch (e) { - printV(e.toString()); - } - } -} diff --git a/cw_haven/lib/mnemonics/chinese_simplified.dart b/cw_haven/lib/mnemonics/chinese_simplified.dart deleted file mode 100644 index da3225041..000000000 --- a/cw_haven/lib/mnemonics/chinese_simplified.dart +++ /dev/null @@ -1,1630 +0,0 @@ -class ChineseSimplifiedMnemonics { - static const words = [ - "的", - "一", - "是", - "在", - "不", - "了", - "有", - "和", - "人", - "这", - "中", - "大", - "为", - "上", - "个", - "国", - "我", - "以", - "要", - "他", - "时", - "来", - "用", - "们", - "生", - "到", - "作", - "地", - "于", - "出", - "就", - "分", - "对", - "成", - "会", - "可", - "主", - "发", - "年", - "动", - "同", - "工", - "也", - "能", - "下", - "过", - "子", - "说", - "产", - "种", - "面", - "而", - "方", - "后", - "多", - "定", - "行", - "学", - "法", - "所", - "民", - "得", - "经", - "十", - "三", - "之", - "进", - "着", - "等", - "部", - "度", - "家", - "电", - "力", - "里", - "如", - "水", - "化", - "高", - "自", - "二", - "理", - "起", - "小", - "物", - "现", - "实", - "加", - "量", - "都", - "两", - "体", - "制", - "机", - "当", - "使", - "点", - "从", - "业", - "本", - "去", - "把", - "性", - "好", - "应", - "开", - "它", - "合", - "还", - "因", - "由", - "其", - "些", - "然", - "前", - "外", - "天", - "政", - "四", - "日", - "那", - "社", - "义", - "事", - "平", - "形", - "相", - "全", - "表", - "间", - "样", - "与", - "关", - "各", - "重", - "新", - "线", - "内", - "数", - "正", - "心", - "反", - "你", - "明", - "看", - "原", - "又", - "么", - "利", - "比", - "或", - "但", - "质", - "气", - "第", - "向", - "道", - "命", - "此", - "变", - "条", - "只", - "没", - "结", - "解", - "问", - "意", - "建", - "月", - "公", - "无", - "系", - "军", - "很", - "情", - "者", - "最", - "立", - "代", - "想", - "已", - "通", - "并", - "提", - "直", - "题", - "党", - "程", - "展", - "五", - "果", - "料", - "象", - "员", - "革", - "位", - "入", - "常", - "文", - "总", - "次", - "品", - "式", - "活", - "设", - "及", - "管", - "特", - "件", - "长", - "求", - "老", - "头", - "基", - "资", - "边", - "流", - "路", - "级", - "少", - "图", - "山", - "统", - "接", - "知", - "较", - "将", - "组", - "见", - "计", - "别", - "她", - "手", - "角", - "期", - "根", - "论", - "运", - "农", - "指", - "几", - "九", - "区", - "强", - "放", - "决", - "西", - "被", - "干", - "做", - "必", - "战", - "先", - "回", - "则", - "任", - "取", - "据", - "处", - "队", - "南", - "给", - "色", - "光", - "门", - "即", - "保", - "治", - "北", - "造", - "百", - "规", - "热", - "领", - "七", - "海", - "口", - "东", - "导", - "器", - "压", - "志", - "世", - "金", - "增", - "争", - "济", - "阶", - "油", - "思", - "术", - "极", - "交", - "受", - "联", - "什", - "认", - "六", - "共", - "权", - "收", - "证", - "改", - "清", - "美", - "再", - "采", - "转", - "更", - "单", - "风", - "切", - "打", - "白", - "教", - "速", - "花", - "带", - "安", - "场", - "身", - "车", - "例", - "真", - "务", - "具", - "万", - "每", - "目", - "至", - "达", - "走", - "积", - "示", - "议", - "声", - "报", - "斗", - "完", - "类", - "八", - "离", - "华", - "名", - "确", - "才", - "科", - "张", - "信", - "马", - "节", - "话", - "米", - "整", - "空", - "元", - "况", - "今", - "集", - "温", - "传", - "土", - "许", - "步", - "群", - "广", - "石", - "记", - "需", - "段", - "研", - "界", - "拉", - "林", - "律", - "叫", - "且", - "究", - "观", - "越", - "织", - "装", - "影", - "算", - "低", - "持", - "音", - "众", - "书", - "布", - "复", - "容", - "儿", - "须", - "际", - "商", - "非", - "验", - "连", - "断", - "深", - "难", - "近", - "矿", - "千", - "周", - "委", - "素", - "技", - "备", - "半", - "办", - "青", - "省", - "列", - "习", - "响", - "约", - "支", - "般", - "史", - "感", - "劳", - "便", - "团", - "往", - "酸", - "历", - "市", - "克", - "何", - "除", - "消", - "构", - "府", - "称", - "太", - "准", - "精", - "值", - "号", - "率", - "族", - "维", - "划", - "选", - "标", - "写", - "存", - "候", - "毛", - "亲", - "快", - "效", - "斯", - "院", - "查", - "江", - "型", - "眼", - "王", - "按", - "格", - "养", - "易", - "置", - "派", - "层", - "片", - "始", - "却", - "专", - "状", - "育", - "厂", - "京", - "识", - "适", - "属", - "圆", - "包", - "火", - "住", - "调", - "满", - "县", - "局", - "照", - "参", - "红", - "细", - "引", - "听", - "该", - "铁", - "价", - "严", - "首", - "底", - "液", - "官", - "德", - "随", - "病", - "苏", - "失", - "尔", - "死", - "讲", - "配", - "女", - "黄", - "推", - "显", - "谈", - "罪", - "神", - "艺", - "呢", - "席", - "含", - "企", - "望", - "密", - "批", - "营", - "项", - "防", - "举", - "球", - "英", - "氧", - "势", - "告", - "李", - "台", - "落", - "木", - "帮", - "轮", - "破", - "亚", - "师", - "围", - "注", - "远", - "字", - "材", - "排", - "供", - "河", - "态", - "封", - "另", - "施", - "减", - "树", - "溶", - "怎", - "止", - "案", - "言", - "士", - "均", - "武", - "固", - "叶", - "鱼", - "波", - "视", - "仅", - "费", - "紧", - "爱", - "左", - "章", - "早", - "朝", - "害", - "续", - "轻", - "服", - "试", - "食", - "充", - "兵", - "源", - "判", - "护", - "司", - "足", - "某", - "练", - "差", - "致", - "板", - "田", - "降", - "黑", - "犯", - "负", - "击", - "范", - "继", - "兴", - "似", - "余", - "坚", - "曲", - "输", - "修", - "故", - "城", - "夫", - "够", - "送", - "笔", - "船", - "占", - "右", - "财", - "吃", - "富", - "春", - "职", - "觉", - "汉", - "画", - "功", - "巴", - "跟", - "虽", - "杂", - "飞", - "检", - "吸", - "助", - "升", - "阳", - "互", - "初", - "创", - "抗", - "考", - "投", - "坏", - "策", - "古", - "径", - "换", - "未", - "跑", - "留", - "钢", - "曾", - "端", - "责", - "站", - "简", - "述", - "钱", - "副", - "尽", - "帝", - "射", - "草", - "冲", - "承", - "独", - "令", - "限", - "阿", - "宣", - "环", - "双", - "请", - "超", - "微", - "让", - "控", - "州", - "良", - "轴", - "找", - "否", - "纪", - "益", - "依", - "优", - "顶", - "础", - "载", - "倒", - "房", - "突", - "坐", - "粉", - "敌", - "略", - "客", - "袁", - "冷", - "胜", - "绝", - "析", - "块", - "剂", - "测", - "丝", - "协", - "诉", - "念", - "陈", - "仍", - "罗", - "盐", - "友", - "洋", - "错", - "苦", - "夜", - "刑", - "移", - "频", - "逐", - "靠", - "混", - "母", - "短", - "皮", - "终", - "聚", - "汽", - "村", - "云", - "哪", - "既", - "距", - "卫", - "停", - "烈", - "央", - "察", - "烧", - "迅", - "境", - "若", - "印", - "洲", - "刻", - "括", - "激", - "孔", - "搞", - "甚", - "室", - "待", - "核", - "校", - "散", - "侵", - "吧", - "甲", - "游", - "久", - "菜", - "味", - "旧", - "模", - "湖", - "货", - "损", - "预", - "阻", - "毫", - "普", - "稳", - "乙", - "妈", - "植", - "息", - "扩", - "银", - "语", - "挥", - "酒", - "守", - "拿", - "序", - "纸", - "医", - "缺", - "雨", - "吗", - "针", - "刘", - "啊", - "急", - "唱", - "误", - "训", - "愿", - "审", - "附", - "获", - "茶", - "鲜", - "粮", - "斤", - "孩", - "脱", - "硫", - "肥", - "善", - "龙", - "演", - "父", - "渐", - "血", - "欢", - "械", - "掌", - "歌", - "沙", - "刚", - "攻", - "谓", - "盾", - "讨", - "晚", - "粒", - "乱", - "燃", - "矛", - "乎", - "杀", - "药", - "宁", - "鲁", - "贵", - "钟", - "煤", - "读", - "班", - "伯", - "香", - "介", - "迫", - "句", - "丰", - "培", - "握", - "兰", - "担", - "弦", - "蛋", - "沉", - "假", - "穿", - "执", - "答", - "乐", - "谁", - "顺", - "烟", - "缩", - "征", - "脸", - "喜", - "松", - "脚", - "困", - "异", - "免", - "背", - "星", - "福", - "买", - "染", - "井", - "概", - "慢", - "怕", - "磁", - "倍", - "祖", - "皇", - "促", - "静", - "补", - "评", - "翻", - "肉", - "践", - "尼", - "衣", - "宽", - "扬", - "棉", - "希", - "伤", - "操", - "垂", - "秋", - "宜", - "氢", - "套", - "督", - "振", - "架", - "亮", - "末", - "宪", - "庆", - "编", - "牛", - "触", - "映", - "雷", - "销", - "诗", - "座", - "居", - "抓", - "裂", - "胞", - "呼", - "娘", - "景", - "威", - "绿", - "晶", - "厚", - "盟", - "衡", - "鸡", - "孙", - "延", - "危", - "胶", - "屋", - "乡", - "临", - "陆", - "顾", - "掉", - "呀", - "灯", - "岁", - "措", - "束", - "耐", - "剧", - "玉", - "赵", - "跳", - "哥", - "季", - "课", - "凯", - "胡", - "额", - "款", - "绍", - "卷", - "齐", - "伟", - "蒸", - "殖", - "永", - "宗", - "苗", - "川", - "炉", - "岩", - "弱", - "零", - "杨", - "奏", - "沿", - "露", - "杆", - "探", - "滑", - "镇", - "饭", - "浓", - "航", - "怀", - "赶", - "库", - "夺", - "伊", - "灵", - "税", - "途", - "灭", - "赛", - "归", - "召", - "鼓", - "播", - "盘", - "裁", - "险", - "康", - "唯", - "录", - "菌", - "纯", - "借", - "糖", - "盖", - "横", - "符", - "私", - "努", - "堂", - "域", - "枪", - "润", - "幅", - "哈", - "竟", - "熟", - "虫", - "泽", - "脑", - "壤", - "碳", - "欧", - "遍", - "侧", - "寨", - "敢", - "彻", - "虑", - "斜", - "薄", - "庭", - "纳", - "弹", - "饲", - "伸", - "折", - "麦", - "湿", - "暗", - "荷", - "瓦", - "塞", - "床", - "筑", - "恶", - "户", - "访", - "塔", - "奇", - "透", - "梁", - "刀", - "旋", - "迹", - "卡", - "氯", - "遇", - "份", - "毒", - "泥", - "退", - "洗", - "摆", - "灰", - "彩", - "卖", - "耗", - "夏", - "择", - "忙", - "铜", - "献", - "硬", - "予", - "繁", - "圈", - "雪", - "函", - "亦", - "抽", - "篇", - "阵", - "阴", - "丁", - "尺", - "追", - "堆", - "雄", - "迎", - "泛", - "爸", - "楼", - "避", - "谋", - "吨", - "野", - "猪", - "旗", - "累", - "偏", - "典", - "馆", - "索", - "秦", - "脂", - "潮", - "爷", - "豆", - "忽", - "托", - "惊", - "塑", - "遗", - "愈", - "朱", - "替", - "纤", - "粗", - "倾", - "尚", - "痛", - "楚", - "谢", - "奋", - "购", - "磨", - "君", - "池", - "旁", - "碎", - "骨", - "监", - "捕", - "弟", - "暴", - "割", - "贯", - "殊", - "释", - "词", - "亡", - "壁", - "顿", - "宝", - "午", - "尘", - "闻", - "揭", - "炮", - "残", - "冬", - "桥", - "妇", - "警", - "综", - "招", - "吴", - "付", - "浮", - "遭", - "徐", - "您", - "摇", - "谷", - "赞", - "箱", - "隔", - "订", - "男", - "吹", - "园", - "纷", - "唐", - "败", - "宋", - "玻", - "巨", - "耕", - "坦", - "荣", - "闭", - "湾", - "键", - "凡", - "驻", - "锅", - "救", - "恩", - "剥", - "凝", - "碱", - "齿", - "截", - "炼", - "麻", - "纺", - "禁", - "废", - "盛", - "版", - "缓", - "净", - "睛", - "昌", - "婚", - "涉", - "筒", - "嘴", - "插", - "岸", - "朗", - "庄", - "街", - "藏", - "姑", - "贸", - "腐", - "奴", - "啦", - "惯", - "乘", - "伙", - "恢", - "匀", - "纱", - "扎", - "辩", - "耳", - "彪", - "臣", - "亿", - "璃", - "抵", - "脉", - "秀", - "萨", - "俄", - "网", - "舞", - "店", - "喷", - "纵", - "寸", - "汗", - "挂", - "洪", - "贺", - "闪", - "柬", - "爆", - "烯", - "津", - "稻", - "墙", - "软", - "勇", - "像", - "滚", - "厘", - "蒙", - "芳", - "肯", - "坡", - "柱", - "荡", - "腿", - "仪", - "旅", - "尾", - "轧", - "冰", - "贡", - "登", - "黎", - "削", - "钻", - "勒", - "逃", - "障", - "氨", - "郭", - "峰", - "币", - "港", - "伏", - "轨", - "亩", - "毕", - "擦", - "莫", - "刺", - "浪", - "秘", - "援", - "株", - "健", - "售", - "股", - "岛", - "甘", - "泡", - "睡", - "童", - "铸", - "汤", - "阀", - "休", - "汇", - "舍", - "牧", - "绕", - "炸", - "哲", - "磷", - "绩", - "朋", - "淡", - "尖", - "启", - "陷", - "柴", - "呈", - "徒", - "颜", - "泪", - "稍", - "忘", - "泵", - "蓝", - "拖", - "洞", - "授", - "镜", - "辛", - "壮", - "锋", - "贫", - "虚", - "弯", - "摩", - "泰", - "幼", - "廷", - "尊", - "窗", - "纲", - "弄", - "隶", - "疑", - "氏", - "宫", - "姐", - "震", - "瑞", - "怪", - "尤", - "琴", - "循", - "描", - "膜", - "违", - "夹", - "腰", - "缘", - "珠", - "穷", - "森", - "枝", - "竹", - "沟", - "催", - "绳", - "忆", - "邦", - "剩", - "幸", - "浆", - "栏", - "拥", - "牙", - "贮", - "礼", - "滤", - "钠", - "纹", - "罢", - "拍", - "咱", - "喊", - "袖", - "埃", - "勤", - "罚", - "焦", - "潜", - "伍", - "墨", - "欲", - "缝", - "姓", - "刊", - "饱", - "仿", - "奖", - "铝", - "鬼", - "丽", - "跨", - "默", - "挖", - "链", - "扫", - "喝", - "袋", - "炭", - "污", - "幕", - "诸", - "弧", - "励", - "梅", - "奶", - "洁", - "灾", - "舟", - "鉴", - "苯", - "讼", - "抱", - "毁", - "懂", - "寒", - "智", - "埔", - "寄", - "届", - "跃", - "渡", - "挑", - "丹", - "艰", - "贝", - "碰", - "拔", - "爹", - "戴", - "码", - "梦", - "芽", - "熔", - "赤", - "渔", - "哭", - "敬", - "颗", - "奔", - "铅", - "仲", - "虎", - "稀", - "妹", - "乏", - "珍", - "申", - "桌", - "遵", - "允", - "隆", - "螺", - "仓", - "魏", - "锐", - "晓", - "氮", - "兼", - "隐", - "碍", - "赫", - "拨", - "忠", - "肃", - "缸", - "牵", - "抢", - "博", - "巧", - "壳", - "兄", - "杜", - "讯", - "诚", - "碧", - "祥", - "柯", - "页", - "巡", - "矩", - "悲", - "灌", - "龄", - "伦", - "票", - "寻", - "桂", - "铺", - "圣", - "恐", - "恰", - "郑", - "趣", - "抬", - "荒", - "腾", - "贴", - "柔", - "滴", - "猛", - "阔", - "辆", - "妻", - "填", - "撤", - "储", - "签", - "闹", - "扰", - "紫", - "砂", - "递", - "戏", - "吊", - "陶", - "伐", - "喂", - "疗", - "瓶", - "婆", - "抚", - "臂", - "摸", - "忍", - "虾", - "蜡", - "邻", - "胸", - "巩", - "挤", - "偶", - "弃", - "槽", - "劲", - "乳", - "邓", - "吉", - "仁", - "烂", - "砖", - "租", - "乌", - "舰", - "伴", - "瓜", - "浅", - "丙", - "暂", - "燥", - "橡", - "柳", - "迷", - "暖", - "牌", - "秧", - "胆", - "详", - "簧", - "踏", - "瓷", - "谱", - "呆", - "宾", - "糊", - "洛", - "辉", - "愤", - "竞", - "隙", - "怒", - "粘", - "乃", - "绪", - "肩", - "籍", - "敏", - "涂", - "熙", - "皆", - "侦", - "悬", - "掘", - "享", - "纠", - "醒", - "狂", - "锁", - "淀", - "恨", - "牲", - "霸", - "爬", - "赏", - "逆", - "玩", - "陵", - "祝", - "秒", - "浙", - "貌" - ]; -} \ No newline at end of file diff --git a/cw_haven/lib/mnemonics/dutch.dart b/cw_haven/lib/mnemonics/dutch.dart deleted file mode 100644 index 3a1d00cfc..000000000 --- a/cw_haven/lib/mnemonics/dutch.dart +++ /dev/null @@ -1,1630 +0,0 @@ -class DutchMnemonics { - static const words = [ - "aalglad", - "aalscholver", - "aambeeld", - "aangeef", - "aanlandig", - "aanvaard", - "aanwakker", - "aapmens", - "aarten", - "abdicatie", - "abnormaal", - "abrikoos", - "accu", - "acuut", - "adjudant", - "admiraal", - "advies", - "afbidding", - "afdracht", - "affaire", - "affiche", - "afgang", - "afkick", - "afknap", - "aflees", - "afmijner", - "afname", - "afpreekt", - "afrader", - "afspeel", - "aftocht", - "aftrek", - "afzijdig", - "ahornboom", - "aktetas", - "akzo", - "alchemist", - "alcohol", - "aldaar", - "alexander", - "alfabet", - "alfredo", - "alice", - "alikruik", - "allrisk", - "altsax", - "alufolie", - "alziend", - "amai", - "ambacht", - "ambieer", - "amina", - "amnestie", - "amok", - "ampul", - "amuzikaal", - "angela", - "aniek", - "antje", - "antwerpen", - "anya", - "aorta", - "apache", - "apekool", - "appelaar", - "arganolie", - "argeloos", - "armoede", - "arrenslee", - "artritis", - "arubaan", - "asbak", - "ascii", - "asgrauw", - "asjes", - "asml", - "aspunt", - "asurn", - "asveld", - "aterling", - "atomair", - "atrium", - "atsma", - "atypisch", - "auping", - "aura", - "avifauna", - "axiaal", - "azoriaan", - "azteek", - "azuur", - "bachelor", - "badderen", - "badhotel", - "badmantel", - "badsteden", - "balie", - "ballans", - "balvers", - "bamibal", - "banneling", - "barracuda", - "basaal", - "batelaan", - "batje", - "beambte", - "bedlamp", - "bedwelmd", - "befaamd", - "begierd", - "begraaf", - "behield", - "beijaard", - "bejaagd", - "bekaaid", - "beks", - "bektas", - "belaad", - "belboei", - "belderbos", - "beloerd", - "beluchten", - "bemiddeld", - "benadeeld", - "benijd", - "berechten", - "beroemd", - "besef", - "besseling", - "best", - "betichten", - "bevind", - "bevochten", - "bevraagd", - "bewust", - "bidplaats", - "biefstuk", - "biemans", - "biezen", - "bijbaan", - "bijeenkom", - "bijfiguur", - "bijkaart", - "bijlage", - "bijpaard", - "bijtgaar", - "bijweg", - "bimmel", - "binck", - "bint", - "biobak", - "biotisch", - "biseks", - "bistro", - "bitter", - "bitumen", - "bizar", - "blad", - "bleken", - "blender", - "bleu", - "blief", - "blijven", - "blozen", - "bock", - "boef", - "boei", - "boks", - "bolder", - "bolus", - "bolvormig", - "bomaanval", - "bombarde", - "bomma", - "bomtapijt", - "bookmaker", - "boos", - "borg", - "bosbes", - "boshuizen", - "bosloop", - "botanicus", - "bougie", - "bovag", - "boxspring", - "braad", - "brasem", - "brevet", - "brigade", - "brinckman", - "bruid", - "budget", - "buffel", - "buks", - "bulgaar", - "buma", - "butaan", - "butler", - "buuf", - "cactus", - "cafeetje", - "camcorder", - "cannabis", - "canyon", - "capoeira", - "capsule", - "carkit", - "casanova", - "catalaan", - "ceintuur", - "celdeling", - "celplasma", - "cement", - "censeren", - "ceramisch", - "cerberus", - "cerebraal", - "cesium", - "cirkel", - "citeer", - "civiel", - "claxon", - "clenbuterol", - "clicheren", - "clijsen", - "coalitie", - "coassistentschap", - "coaxiaal", - "codetaal", - "cofinanciering", - "cognac", - "coltrui", - "comfort", - "commandant", - "condensaat", - "confectie", - "conifeer", - "convector", - "copier", - "corfu", - "correct", - "coup", - "couvert", - "creatie", - "credit", - "crematie", - "cricket", - "croupier", - "cruciaal", - "cruijff", - "cuisine", - "culemborg", - "culinair", - "curve", - "cyrano", - "dactylus", - "dading", - "dagblind", - "dagje", - "daglicht", - "dagprijs", - "dagranden", - "dakdekker", - "dakpark", - "dakterras", - "dalgrond", - "dambord", - "damkat", - "damlengte", - "damman", - "danenberg", - "debbie", - "decibel", - "defect", - "deformeer", - "degelijk", - "degradant", - "dejonghe", - "dekken", - "deppen", - "derek", - "derf", - "derhalve", - "detineren", - "devalueer", - "diaken", - "dicht", - "dictaat", - "dief", - "digitaal", - "dijbreuk", - "dijkmans", - "dimbaar", - "dinsdag", - "diode", - "dirigeer", - "disbalans", - "dobermann", - "doenbaar", - "doerak", - "dogma", - "dokhaven", - "dokwerker", - "doling", - "dolphijn", - "dolven", - "dombo", - "dooraderd", - "dopeling", - "doping", - "draderig", - "drama", - "drenkbak", - "dreumes", - "drol", - "drug", - "duaal", - "dublin", - "duplicaat", - "durven", - "dusdanig", - "dutchbat", - "dutje", - "dutten", - "duur", - "duwwerk", - "dwaal", - "dweil", - "dwing", - "dyslexie", - "ecostroom", - "ecotaks", - "educatie", - "eeckhout", - "eede", - "eemland", - "eencellig", - "eeneiig", - "eenruiter", - "eenwinter", - "eerenberg", - "eerrover", - "eersel", - "eetmaal", - "efteling", - "egaal", - "egtberts", - "eickhoff", - "eidooier", - "eiland", - "eind", - "eisden", - "ekster", - "elburg", - "elevatie", - "elfkoppig", - "elfrink", - "elftal", - "elimineer", - "elleboog", - "elma", - "elodie", - "elsa", - "embleem", - "embolie", - "emoe", - "emonds", - "emplooi", - "enduro", - "enfin", - "engageer", - "entourage", - "entstof", - "epileer", - "episch", - "eppo", - "erasmus", - "erboven", - "erebaan", - "erelijst", - "ereronden", - "ereteken", - "erfhuis", - "erfwet", - "erger", - "erica", - "ermitage", - "erna", - "ernie", - "erts", - "ertussen", - "eruitzien", - "ervaar", - "erven", - "erwt", - "esbeek", - "escort", - "esdoorn", - "essing", - "etage", - "eter", - "ethanol", - "ethicus", - "etholoog", - "eufonisch", - "eurocent", - "evacuatie", - "exact", - "examen", - "executant", - "exen", - "exit", - "exogeen", - "exotherm", - "expeditie", - "expletief", - "expres", - "extase", - "extinctie", - "faal", - "faam", - "fabel", - "facultair", - "fakir", - "fakkel", - "faliekant", - "fallisch", - "famke", - "fanclub", - "fase", - "fatsoen", - "fauna", - "federaal", - "feedback", - "feest", - "feilbaar", - "feitelijk", - "felblauw", - "figurante", - "fiod", - "fitheid", - "fixeer", - "flap", - "fleece", - "fleur", - "flexibel", - "flits", - "flos", - "flow", - "fluweel", - "foezelen", - "fokkelman", - "fokpaard", - "fokvee", - "folder", - "follikel", - "folmer", - "folteraar", - "fooi", - "foolen", - "forfait", - "forint", - "formule", - "fornuis", - "fosfaat", - "foxtrot", - "foyer", - "fragiel", - "frater", - "freak", - "freddie", - "fregat", - "freon", - "frijnen", - "fructose", - "frunniken", - "fuiven", - "funshop", - "furieus", - "fysica", - "gadget", - "galder", - "galei", - "galg", - "galvlieg", - "galzuur", - "ganesh", - "gaswet", - "gaza", - "gazelle", - "geaaid", - "gebiecht", - "gebufferd", - "gedijd", - "geef", - "geflanst", - "gefreesd", - "gegaan", - "gegijzeld", - "gegniffel", - "gegraaid", - "gehikt", - "gehobbeld", - "gehucht", - "geiser", - "geiten", - "gekaakt", - "gekheid", - "gekijf", - "gekmakend", - "gekocht", - "gekskap", - "gekte", - "gelubberd", - "gemiddeld", - "geordend", - "gepoederd", - "gepuft", - "gerda", - "gerijpt", - "geseald", - "geshockt", - "gesierd", - "geslaagd", - "gesnaaid", - "getracht", - "getwijfel", - "geuit", - "gevecht", - "gevlagd", - "gewicht", - "gezaagd", - "gezocht", - "ghanees", - "giebelen", - "giechel", - "giepmans", - "gips", - "giraal", - "gistachtig", - "gitaar", - "glaasje", - "gletsjer", - "gleuf", - "glibberen", - "glijbaan", - "gloren", - "gluipen", - "gluren", - "gluur", - "gnoe", - "goddelijk", - "godgans", - "godschalk", - "godzalig", - "goeierd", - "gogme", - "goklustig", - "gokwereld", - "gonggrijp", - "gonje", - "goor", - "grabbel", - "graf", - "graveer", - "grif", - "grolleman", - "grom", - "groosman", - "grubben", - "gruijs", - "grut", - "guacamole", - "guido", - "guppy", - "haazen", - "hachelijk", - "haex", - "haiku", - "hakhout", - "hakken", - "hanegem", - "hans", - "hanteer", - "harrie", - "hazebroek", - "hedonist", - "heil", - "heineken", - "hekhuis", - "hekman", - "helbig", - "helga", - "helwegen", - "hengelaar", - "herkansen", - "hermafrodiet", - "hertaald", - "hiaat", - "hikspoors", - "hitachi", - "hitparade", - "hobo", - "hoeve", - "holocaust", - "hond", - "honnepon", - "hoogacht", - "hotelbed", - "hufter", - "hugo", - "huilbier", - "hulk", - "humus", - "huwbaar", - "huwelijk", - "hype", - "iconisch", - "idema", - "ideogram", - "idolaat", - "ietje", - "ijker", - "ijkheid", - "ijklijn", - "ijkmaat", - "ijkwezen", - "ijmuiden", - "ijsbox", - "ijsdag", - "ijselijk", - "ijskoud", - "ilse", - "immuun", - "impliceer", - "impuls", - "inbijten", - "inbuigen", - "indijken", - "induceer", - "indy", - "infecteer", - "inhaak", - "inkijk", - "inluiden", - "inmijnen", - "inoefenen", - "inpolder", - "inrijden", - "inslaan", - "invitatie", - "inwaaien", - "ionisch", - "isaac", - "isolatie", - "isotherm", - "isra", - "italiaan", - "ivoor", - "jacobs", - "jakob", - "jammen", - "jampot", - "jarig", - "jehova", - "jenever", - "jezus", - "joana", - "jobdienst", - "josua", - "joule", - "juich", - "jurk", - "juut", - "kaas", - "kabelaar", - "kabinet", - "kagenaar", - "kajuit", - "kalebas", - "kalm", - "kanjer", - "kapucijn", - "karregat", - "kart", - "katvanger", - "katwijk", - "kegelaar", - "keiachtig", - "keizer", - "kenletter", - "kerdijk", - "keus", - "kevlar", - "kezen", - "kickback", - "kieviet", - "kijken", - "kikvors", - "kilheid", - "kilobit", - "kilsdonk", - "kipschnitzel", - "kissebis", - "klad", - "klagelijk", - "klak", - "klapbaar", - "klaver", - "klene", - "klets", - "klijnhout", - "klit", - "klok", - "klonen", - "klotefilm", - "kluif", - "klumper", - "klus", - "knabbel", - "knagen", - "knaven", - "kneedbaar", - "knmi", - "knul", - "knus", - "kokhals", - "komiek", - "komkommer", - "kompaan", - "komrij", - "komvormig", - "koning", - "kopbal", - "kopklep", - "kopnagel", - "koppejan", - "koptekst", - "kopwand", - "koraal", - "kosmisch", - "kostbaar", - "kram", - "kraneveld", - "kras", - "kreling", - "krengen", - "kribbe", - "krik", - "kruid", - "krulbol", - "kuijper", - "kuipbank", - "kuit", - "kuiven", - "kutsmoes", - "kuub", - "kwak", - "kwatong", - "kwetsbaar", - "kwezelaar", - "kwijnen", - "kwik", - "kwinkslag", - "kwitantie", - "lading", - "lakbeits", - "lakken", - "laklaag", - "lakmoes", - "lakwijk", - "lamheid", - "lamp", - "lamsbout", - "lapmiddel", - "larve", - "laser", - "latijn", - "latuw", - "lawaai", - "laxeerpil", - "lebberen", - "ledeboer", - "leefbaar", - "leeman", - "lefdoekje", - "lefhebber", - "legboor", - "legsel", - "leguaan", - "leiplaat", - "lekdicht", - "lekrijden", - "leksteen", - "lenen", - "leraar", - "lesbienne", - "leugenaar", - "leut", - "lexicaal", - "lezing", - "lieten", - "liggeld", - "lijdzaam", - "lijk", - "lijmstang", - "lijnschip", - "likdoorn", - "likken", - "liksteen", - "limburg", - "link", - "linoleum", - "lipbloem", - "lipman", - "lispelen", - "lissabon", - "litanie", - "liturgie", - "lochem", - "loempia", - "loesje", - "logheid", - "lonen", - "lonneke", - "loom", - "loos", - "losbaar", - "loslaten", - "losplaats", - "loting", - "lotnummer", - "lots", - "louie", - "lourdes", - "louter", - "lowbudget", - "luijten", - "luikenaar", - "luilak", - "luipaard", - "luizenbos", - "lulkoek", - "lumen", - "lunzen", - "lurven", - "lutjeboer", - "luttel", - "lutz", - "luuk", - "luwte", - "luyendijk", - "lyceum", - "lynx", - "maakbaar", - "magdalena", - "malheid", - "manchet", - "manfred", - "manhaftig", - "mank", - "mantel", - "marion", - "marxist", - "masmeijer", - "massaal", - "matsen", - "matverf", - "matze", - "maude", - "mayonaise", - "mechanica", - "meifeest", - "melodie", - "meppelink", - "midvoor", - "midweeks", - "midzomer", - "miezel", - "mijnraad", - "minus", - "mirck", - "mirte", - "mispakken", - "misraden", - "miswassen", - "mitella", - "moker", - "molecule", - "mombakkes", - "moonen", - "mopperaar", - "moraal", - "morgana", - "mormel", - "mosselaar", - "motregen", - "mouw", - "mufheid", - "mutueel", - "muzelman", - "naaidoos", - "naald", - "nadeel", - "nadruk", - "nagy", - "nahon", - "naima", - "nairobi", - "napalm", - "napels", - "napijn", - "napoleon", - "narigheid", - "narratief", - "naseizoen", - "nasibal", - "navigatie", - "nawijn", - "negatief", - "nekletsel", - "nekwervel", - "neolatijn", - "neonataal", - "neptunus", - "nerd", - "nest", - "neuzelaar", - "nihiliste", - "nijenhuis", - "nijging", - "nijhoff", - "nijl", - "nijptang", - "nippel", - "nokkenas", - "noordam", - "noren", - "normaal", - "nottelman", - "notulant", - "nout", - "nuance", - "nuchter", - "nudorp", - "nulde", - "nullijn", - "nulmeting", - "nunspeet", - "nylon", - "obelisk", - "object", - "oblie", - "obsceen", - "occlusie", - "oceaan", - "ochtend", - "ockhuizen", - "oerdom", - "oergezond", - "oerlaag", - "oester", - "okhuijsen", - "olifant", - "olijfboer", - "omaans", - "ombudsman", - "omdat", - "omdijken", - "omdoen", - "omgebouwd", - "omkeer", - "omkomen", - "ommegaand", - "ommuren", - "omroep", - "omruil", - "omslaan", - "omsmeden", - "omvaar", - "onaardig", - "onedel", - "onenig", - "onheilig", - "onrecht", - "onroerend", - "ontcijfer", - "onthaal", - "ontvallen", - "ontzadeld", - "onzacht", - "onzin", - "onzuiver", - "oogappel", - "ooibos", - "ooievaar", - "ooit", - "oorarts", - "oorhanger", - "oorijzer", - "oorklep", - "oorschelp", - "oorworm", - "oorzaak", - "opdagen", - "opdien", - "opdweilen", - "opel", - "opgebaard", - "opinie", - "opjutten", - "opkijken", - "opklaar", - "opkuisen", - "opkwam", - "opnaaien", - "opossum", - "opsieren", - "opsmeer", - "optreden", - "opvijzel", - "opvlammen", - "opwind", - "oraal", - "orchidee", - "orkest", - "ossuarium", - "ostendorf", - "oublie", - "oudachtig", - "oudbakken", - "oudnoors", - "oudshoorn", - "oudtante", - "oven", - "over", - "oxidant", - "pablo", - "pacht", - "paktafel", - "pakzadel", - "paljas", - "panharing", - "papfles", - "paprika", - "parochie", - "paus", - "pauze", - "paviljoen", - "peek", - "pegel", - "peigeren", - "pekela", - "pendant", - "penibel", - "pepmiddel", - "peptalk", - "periferie", - "perron", - "pessarium", - "peter", - "petfles", - "petgat", - "peuk", - "pfeifer", - "picknick", - "pief", - "pieneman", - "pijlkruid", - "pijnacker", - "pijpelink", - "pikdonker", - "pikeer", - "pilaar", - "pionier", - "pipet", - "piscine", - "pissebed", - "pitchen", - "pixel", - "plamuren", - "plan", - "plausibel", - "plegen", - "plempen", - "pleonasme", - "plezant", - "podoloog", - "pofmouw", - "pokdalig", - "ponywagen", - "popachtig", - "popidool", - "porren", - "positie", - "potten", - "pralen", - "prezen", - "prijzen", - "privaat", - "proef", - "prooi", - "prozawerk", - "pruik", - "prul", - "publiceer", - "puck", - "puilen", - "pukkelig", - "pulveren", - "pupil", - "puppy", - "purmerend", - "pustjens", - "putemmer", - "puzzelaar", - "queenie", - "quiche", - "raam", - "raar", - "raat", - "raes", - "ralf", - "rally", - "ramona", - "ramselaar", - "ranonkel", - "rapen", - "rapunzel", - "rarekiek", - "rarigheid", - "rattenhol", - "ravage", - "reactie", - "recreant", - "redacteur", - "redster", - "reewild", - "regie", - "reijnders", - "rein", - "replica", - "revanche", - "rigide", - "rijbaan", - "rijdansen", - "rijgen", - "rijkdom", - "rijles", - "rijnwijn", - "rijpma", - "rijstafel", - "rijtaak", - "rijzwepen", - "rioleer", - "ripdeal", - "riphagen", - "riskant", - "rits", - "rivaal", - "robbedoes", - "robot", - "rockact", - "rodijk", - "rogier", - "rohypnol", - "rollaag", - "rolpaal", - "roltafel", - "roof", - "roon", - "roppen", - "rosbief", - "rosharig", - "rosielle", - "rotan", - "rotleven", - "rotten", - "rotvaart", - "royaal", - "royeer", - "rubato", - "ruby", - "ruche", - "rudge", - "ruggetje", - "rugnummer", - "rugpijn", - "rugtitel", - "rugzak", - "ruilbaar", - "ruis", - "ruit", - "rukwind", - "rulijs", - "rumoeren", - "rumsdorp", - "rumtaart", - "runnen", - "russchen", - "ruwkruid", - "saboteer", - "saksisch", - "salade", - "salpeter", - "sambabal", - "samsam", - "satelliet", - "satineer", - "saus", - "scampi", - "scarabee", - "scenario", - "schobben", - "schubben", - "scout", - "secessie", - "secondair", - "seculair", - "sediment", - "seeland", - "settelen", - "setwinst", - "sheriff", - "shiatsu", - "siciliaan", - "sidderaal", - "sigma", - "sijben", - "silvana", - "simkaart", - "sinds", - "situatie", - "sjaak", - "sjardijn", - "sjezen", - "sjor", - "skinhead", - "skylab", - "slamixen", - "sleijpen", - "slijkerig", - "slordig", - "slowaak", - "sluieren", - "smadelijk", - "smiecht", - "smoel", - "smos", - "smukken", - "snackcar", - "snavel", - "sneaker", - "sneu", - "snijdbaar", - "snit", - "snorder", - "soapbox", - "soetekouw", - "soigneren", - "sojaboon", - "solo", - "solvabel", - "somber", - "sommatie", - "soort", - "soppen", - "sopraan", - "soundbar", - "spanen", - "spawater", - "spijgat", - "spinaal", - "spionage", - "spiraal", - "spleet", - "splijt", - "spoed", - "sporen", - "spul", - "spuug", - "spuw", - "stalen", - "standaard", - "star", - "stefan", - "stencil", - "stijf", - "stil", - "stip", - "stopdas", - "stoten", - "stoven", - "straat", - "strobbe", - "strubbel", - "stucadoor", - "stuif", - "stukadoor", - "subhoofd", - "subregent", - "sudoku", - "sukade", - "sulfaat", - "surinaams", - "suus", - "syfilis", - "symboliek", - "sympathie", - "synagoge", - "synchroon", - "synergie", - "systeem", - "taanderij", - "tabak", - "tachtig", - "tackelen", - "taiwanees", - "talman", - "tamheid", - "tangaslip", - "taps", - "tarkan", - "tarwe", - "tasman", - "tatjana", - "taxameter", - "teil", - "teisman", - "telbaar", - "telco", - "telganger", - "telstar", - "tenant", - "tepel", - "terzet", - "testament", - "ticket", - "tiesinga", - "tijdelijk", - "tika", - "tiksel", - "tilleman", - "timbaal", - "tinsteen", - "tiplijn", - "tippelaar", - "tjirpen", - "toezeggen", - "tolbaas", - "tolgeld", - "tolhek", - "tolo", - "tolpoort", - "toltarief", - "tolvrij", - "tomaat", - "tondeuse", - "toog", - "tooi", - "toonbaar", - "toos", - "topclub", - "toppen", - "toptalent", - "topvrouw", - "toque", - "torment", - "tornado", - "tosti", - "totdat", - "toucheer", - "toulouse", - "tournedos", - "tout", - "trabant", - "tragedie", - "trailer", - "traject", - "traktaat", - "trauma", - "tray", - "trechter", - "tred", - "tref", - "treur", - "troebel", - "tros", - "trucage", - "truffel", - "tsaar", - "tucht", - "tuenter", - "tuitelig", - "tukje", - "tuktuk", - "tulp", - "tuma", - "tureluurs", - "twijfel", - "twitteren", - "tyfoon", - "typograaf", - "ugandees", - "uiachtig", - "uier", - "uisnipper", - "ultiem", - "unitair", - "uranium", - "urbaan", - "urendag", - "ursula", - "uurcirkel", - "uurglas", - "uzelf", - "vaat", - "vakantie", - "vakleraar", - "valbijl", - "valpartij", - "valreep", - "valuatie", - "vanmiddag", - "vanonder", - "varaan", - "varken", - "vaten", - "veenbes", - "veeteler", - "velgrem", - "vellekoop", - "velvet", - "veneberg", - "venlo", - "vent", - "venusberg", - "venw", - "veredeld", - "verf", - "verhaaf", - "vermaak", - "vernaaid", - "verraad", - "vers", - "veruit", - "verzaagd", - "vetachtig", - "vetlok", - "vetmesten", - "veto", - "vetrek", - "vetstaart", - "vetten", - "veurink", - "viaduct", - "vibrafoon", - "vicariaat", - "vieux", - "vieveen", - "vijfvoud", - "villa", - "vilt", - "vimmetje", - "vindbaar", - "vips", - "virtueel", - "visdieven", - "visee", - "visie", - "vlaag", - "vleugel", - "vmbo", - "vocht", - "voesenek", - "voicemail", - "voip", - "volg", - "vork", - "vorselaar", - "voyeur", - "vracht", - "vrekkig", - "vreten", - "vrije", - "vrozen", - "vrucht", - "vucht", - "vugt", - "vulkaan", - "vulmiddel", - "vulva", - "vuren", - "waas", - "wacht", - "wadvogel", - "wafel", - "waffel", - "walhalla", - "walnoot", - "walraven", - "wals", - "walvis", - "wandaad", - "wanen", - "wanmolen", - "want", - "warklomp", - "warm", - "wasachtig", - "wasteil", - "watt", - "webhandel", - "weblog", - "webpagina", - "webzine", - "wedereis", - "wedstrijd", - "weeda", - "weert", - "wegmaaien", - "wegscheer", - "wekelijks", - "wekken", - "wekroep", - "wektoon", - "weldaad", - "welwater", - "wendbaar", - "wenkbrauw", - "wens", - "wentelaar", - "wervel", - "wesseling", - "wetboek", - "wetmatig", - "whirlpool", - "wijbrands", - "wijdbeens", - "wijk", - "wijnbes", - "wijting", - "wild", - "wimpelen", - "wingebied", - "winplaats", - "winter", - "winzucht", - "wipstaart", - "wisgerhof", - "withaar", - "witmaker", - "wokkel", - "wolf", - "wonenden", - "woning", - "worden", - "worp", - "wortel", - "wrat", - "wrijf", - "wringen", - "yoghurt", - "ypsilon", - "zaaijer", - "zaak", - "zacharias", - "zakelijk", - "zakkam", - "zakwater", - "zalf", - "zalig", - "zaniken", - "zebracode", - "zeeblauw", - "zeef", - "zeegaand", - "zeeuw", - "zege", - "zegje", - "zeil", - "zesbaans", - "zesenhalf", - "zeskantig", - "zesmaal", - "zetbaas", - "zetpil", - "zeulen", - "ziezo", - "zigzag", - "zijaltaar", - "zijbeuk", - "zijlijn", - "zijmuur", - "zijn", - "zijwaarts", - "zijzelf", - "zilt", - "zimmerman", - "zinledig", - "zinnelijk", - "zionist", - "zitdag", - "zitruimte", - "zitzak", - "zoal", - "zodoende", - "zoekbots", - "zoem", - "zoiets", - "zojuist", - "zondaar", - "zotskap", - "zottebol", - "zucht", - "zuivel", - "zulk", - "zult", - "zuster", - "zuur", - "zweedijk", - "zwendel", - "zwepen", - "zwiep", - "zwijmel", - "zworen" - ]; -} \ No newline at end of file diff --git a/cw_haven/lib/mnemonics/english.dart b/cw_haven/lib/mnemonics/english.dart deleted file mode 100644 index fb464d04e..000000000 --- a/cw_haven/lib/mnemonics/english.dart +++ /dev/null @@ -1,1630 +0,0 @@ -class EnglishMnemonics { - static const words = [ - "abbey", - "abducts", - "ability", - "ablaze", - "abnormal", - "abort", - "abrasive", - "absorb", - "abyss", - "academy", - "aces", - "aching", - "acidic", - "acoustic", - "acquire", - "across", - "actress", - "acumen", - "adapt", - "addicted", - "adept", - "adhesive", - "adjust", - "adopt", - "adrenalin", - "adult", - "adventure", - "aerial", - "afar", - "affair", - "afield", - "afloat", - "afoot", - "afraid", - "after", - "against", - "agenda", - "aggravate", - "agile", - "aglow", - "agnostic", - "agony", - "agreed", - "ahead", - "aided", - "ailments", - "aimless", - "airport", - "aisle", - "ajar", - "akin", - "alarms", - "album", - "alchemy", - "alerts", - "algebra", - "alkaline", - "alley", - "almost", - "aloof", - "alpine", - "already", - "also", - "altitude", - "alumni", - "always", - "amaze", - "ambush", - "amended", - "amidst", - "ammo", - "amnesty", - "among", - "amply", - "amused", - "anchor", - "android", - "anecdote", - "angled", - "ankle", - "annoyed", - "answers", - "antics", - "anvil", - "anxiety", - "anybody", - "apart", - "apex", - "aphid", - "aplomb", - "apology", - "apply", - "apricot", - "aptitude", - "aquarium", - "arbitrary", - "archer", - "ardent", - "arena", - "argue", - "arises", - "army", - "around", - "arrow", - "arsenic", - "artistic", - "ascend", - "ashtray", - "aside", - "asked", - "asleep", - "aspire", - "assorted", - "asylum", - "athlete", - "atlas", - "atom", - "atrium", - "attire", - "auburn", - "auctions", - "audio", - "august", - "aunt", - "austere", - "autumn", - "avatar", - "avidly", - "avoid", - "awakened", - "awesome", - "awful", - "awkward", - "awning", - "awoken", - "axes", - "axis", - "axle", - "aztec", - "azure", - "baby", - "bacon", - "badge", - "baffles", - "bagpipe", - "bailed", - "bakery", - "balding", - "bamboo", - "banjo", - "baptism", - "basin", - "batch", - "bawled", - "bays", - "because", - "beer", - "befit", - "begun", - "behind", - "being", - "below", - "bemused", - "benches", - "berries", - "bested", - "betting", - "bevel", - "beware", - "beyond", - "bias", - "bicycle", - "bids", - "bifocals", - "biggest", - "bikini", - "bimonthly", - "binocular", - "biology", - "biplane", - "birth", - "biscuit", - "bite", - "biweekly", - "blender", - "blip", - "bluntly", - "boat", - "bobsled", - "bodies", - "bogeys", - "boil", - "boldly", - "bomb", - "border", - "boss", - "both", - "bounced", - "bovine", - "bowling", - "boxes", - "boyfriend", - "broken", - "brunt", - "bubble", - "buckets", - "budget", - "buffet", - "bugs", - "building", - "bulb", - "bumper", - "bunch", - "business", - "butter", - "buying", - "buzzer", - "bygones", - "byline", - "bypass", - "cabin", - "cactus", - "cadets", - "cafe", - "cage", - "cajun", - "cake", - "calamity", - "camp", - "candy", - "casket", - "catch", - "cause", - "cavernous", - "cease", - "cedar", - "ceiling", - "cell", - "cement", - "cent", - "certain", - "chlorine", - "chrome", - "cider", - "cigar", - "cinema", - "circle", - "cistern", - "citadel", - "civilian", - "claim", - "click", - "clue", - "coal", - "cobra", - "cocoa", - "code", - "coexist", - "coffee", - "cogs", - "cohesive", - "coils", - "colony", - "comb", - "cool", - "copy", - "corrode", - "costume", - "cottage", - "cousin", - "cowl", - "criminal", - "cube", - "cucumber", - "cuddled", - "cuffs", - "cuisine", - "cunning", - "cupcake", - "custom", - "cycling", - "cylinder", - "cynical", - "dabbing", - "dads", - "daft", - "dagger", - "daily", - "damp", - "dangerous", - "dapper", - "darted", - "dash", - "dating", - "dauntless", - "dawn", - "daytime", - "dazed", - "debut", - "decay", - "dedicated", - "deepest", - "deftly", - "degrees", - "dehydrate", - "deity", - "dejected", - "delayed", - "demonstrate", - "dented", - "deodorant", - "depth", - "desk", - "devoid", - "dewdrop", - "dexterity", - "dialect", - "dice", - "diet", - "different", - "digit", - "dilute", - "dime", - "dinner", - "diode", - "diplomat", - "directed", - "distance", - "ditch", - "divers", - "dizzy", - "doctor", - "dodge", - "does", - "dogs", - "doing", - "dolphin", - "domestic", - "donuts", - "doorway", - "dormant", - "dosage", - "dotted", - "double", - "dove", - "down", - "dozen", - "dreams", - "drinks", - "drowning", - "drunk", - "drying", - "dual", - "dubbed", - "duckling", - "dude", - "duets", - "duke", - "dullness", - "dummy", - "dunes", - "duplex", - "duration", - "dusted", - "duties", - "dwarf", - "dwelt", - "dwindling", - "dying", - "dynamite", - "dyslexic", - "each", - "eagle", - "earth", - "easy", - "eating", - "eavesdrop", - "eccentric", - "echo", - "eclipse", - "economics", - "ecstatic", - "eden", - "edgy", - "edited", - "educated", - "eels", - "efficient", - "eggs", - "egotistic", - "eight", - "either", - "eject", - "elapse", - "elbow", - "eldest", - "eleven", - "elite", - "elope", - "else", - "eluded", - "emails", - "ember", - "emerge", - "emit", - "emotion", - "empty", - "emulate", - "energy", - "enforce", - "enhanced", - "enigma", - "enjoy", - "enlist", - "enmity", - "enough", - "enraged", - "ensign", - "entrance", - "envy", - "epoxy", - "equip", - "erase", - "erected", - "erosion", - "error", - "eskimos", - "espionage", - "essential", - "estate", - "etched", - "eternal", - "ethics", - "etiquette", - "evaluate", - "evenings", - "evicted", - "evolved", - "examine", - "excess", - "exhale", - "exit", - "exotic", - "exquisite", - "extra", - "exult", - "fabrics", - "factual", - "fading", - "fainted", - "faked", - "fall", - "family", - "fancy", - "farming", - "fatal", - "faulty", - "fawns", - "faxed", - "fazed", - "feast", - "february", - "federal", - "feel", - "feline", - "females", - "fences", - "ferry", - "festival", - "fetches", - "fever", - "fewest", - "fiat", - "fibula", - "fictional", - "fidget", - "fierce", - "fifteen", - "fight", - "films", - "firm", - "fishing", - "fitting", - "five", - "fixate", - "fizzle", - "fleet", - "flippant", - "flying", - "foamy", - "focus", - "foes", - "foggy", - "foiled", - "folding", - "fonts", - "foolish", - "fossil", - "fountain", - "fowls", - "foxes", - "foyer", - "framed", - "friendly", - "frown", - "fruit", - "frying", - "fudge", - "fuel", - "fugitive", - "fully", - "fuming", - "fungal", - "furnished", - "fuselage", - "future", - "fuzzy", - "gables", - "gadget", - "gags", - "gained", - "galaxy", - "gambit", - "gang", - "gasp", - "gather", - "gauze", - "gave", - "gawk", - "gaze", - "gearbox", - "gecko", - "geek", - "gels", - "gemstone", - "general", - "geometry", - "germs", - "gesture", - "getting", - "geyser", - "ghetto", - "ghost", - "giant", - "giddy", - "gifts", - "gigantic", - "gills", - "gimmick", - "ginger", - "girth", - "giving", - "glass", - "gleeful", - "glide", - "gnaw", - "gnome", - "goat", - "goblet", - "godfather", - "goes", - "goggles", - "going", - "goldfish", - "gone", - "goodbye", - "gopher", - "gorilla", - "gossip", - "gotten", - "gourmet", - "governing", - "gown", - "greater", - "grunt", - "guarded", - "guest", - "guide", - "gulp", - "gumball", - "guru", - "gusts", - "gutter", - "guys", - "gymnast", - "gypsy", - "gyrate", - "habitat", - "hacksaw", - "haggled", - "hairy", - "hamburger", - "happens", - "hashing", - "hatchet", - "haunted", - "having", - "hawk", - "haystack", - "hazard", - "hectare", - "hedgehog", - "heels", - "hefty", - "height", - "hemlock", - "hence", - "heron", - "hesitate", - "hexagon", - "hickory", - "hiding", - "highway", - "hijack", - "hiker", - "hills", - "himself", - "hinder", - "hippo", - "hire", - "history", - "hitched", - "hive", - "hoax", - "hobby", - "hockey", - "hoisting", - "hold", - "honked", - "hookup", - "hope", - "hornet", - "hospital", - "hotel", - "hounded", - "hover", - "howls", - "hubcaps", - "huddle", - "huge", - "hull", - "humid", - "hunter", - "hurried", - "husband", - "huts", - "hybrid", - "hydrogen", - "hyper", - "iceberg", - "icing", - "icon", - "identity", - "idiom", - "idled", - "idols", - "igloo", - "ignore", - "iguana", - "illness", - "imagine", - "imbalance", - "imitate", - "impel", - "inactive", - "inbound", - "incur", - "industrial", - "inexact", - "inflamed", - "ingested", - "initiate", - "injury", - "inkling", - "inline", - "inmate", - "innocent", - "inorganic", - "input", - "inquest", - "inroads", - "insult", - "intended", - "inundate", - "invoke", - "inwardly", - "ionic", - "irate", - "iris", - "irony", - "irritate", - "island", - "isolated", - "issued", - "italics", - "itches", - "items", - "itinerary", - "itself", - "ivory", - "jabbed", - "jackets", - "jaded", - "jagged", - "jailed", - "jamming", - "january", - "jargon", - "jaunt", - "javelin", - "jaws", - "jazz", - "jeans", - "jeers", - "jellyfish", - "jeopardy", - "jerseys", - "jester", - "jetting", - "jewels", - "jigsaw", - "jingle", - "jittery", - "jive", - "jobs", - "jockey", - "jogger", - "joining", - "joking", - "jolted", - "jostle", - "journal", - "joyous", - "jubilee", - "judge", - "juggled", - "juicy", - "jukebox", - "july", - "jump", - "junk", - "jury", - "justice", - "juvenile", - "kangaroo", - "karate", - "keep", - "kennel", - "kept", - "kernels", - "kettle", - "keyboard", - "kickoff", - "kidneys", - "king", - "kiosk", - "kisses", - "kitchens", - "kiwi", - "knapsack", - "knee", - "knife", - "knowledge", - "knuckle", - "koala", - "laboratory", - "ladder", - "lagoon", - "lair", - "lakes", - "lamb", - "language", - "laptop", - "large", - "last", - "later", - "launching", - "lava", - "lawsuit", - "layout", - "lazy", - "lectures", - "ledge", - "leech", - "left", - "legion", - "leisure", - "lemon", - "lending", - "leopard", - "lesson", - "lettuce", - "lexicon", - "liar", - "library", - "licks", - "lids", - "lied", - "lifestyle", - "light", - "likewise", - "lilac", - "limits", - "linen", - "lion", - "lipstick", - "liquid", - "listen", - "lively", - "loaded", - "lobster", - "locker", - "lodge", - "lofty", - "logic", - "loincloth", - "long", - "looking", - "lopped", - "lordship", - "losing", - "lottery", - "loudly", - "love", - "lower", - "loyal", - "lucky", - "luggage", - "lukewarm", - "lullaby", - "lumber", - "lunar", - "lurk", - "lush", - "luxury", - "lymph", - "lynx", - "lyrics", - "macro", - "madness", - "magically", - "mailed", - "major", - "makeup", - "malady", - "mammal", - "maps", - "masterful", - "match", - "maul", - "maverick", - "maximum", - "mayor", - "maze", - "meant", - "mechanic", - "medicate", - "meeting", - "megabyte", - "melting", - "memoir", - "menu", - "merger", - "mesh", - "metro", - "mews", - "mice", - "midst", - "mighty", - "mime", - "mirror", - "misery", - "mittens", - "mixture", - "moat", - "mobile", - "mocked", - "mohawk", - "moisture", - "molten", - "moment", - "money", - "moon", - "mops", - "morsel", - "mostly", - "motherly", - "mouth", - "movement", - "mowing", - "much", - "muddy", - "muffin", - "mugged", - "mullet", - "mumble", - "mundane", - "muppet", - "mural", - "musical", - "muzzle", - "myriad", - "mystery", - "myth", - "nabbing", - "nagged", - "nail", - "names", - "nanny", - "napkin", - "narrate", - "nasty", - "natural", - "nautical", - "navy", - "nearby", - "necklace", - "needed", - "negative", - "neither", - "neon", - "nephew", - "nerves", - "nestle", - "network", - "neutral", - "never", - "newt", - "nexus", - "nibs", - "niche", - "niece", - "nifty", - "nightly", - "nimbly", - "nineteen", - "nirvana", - "nitrogen", - "nobody", - "nocturnal", - "nodes", - "noises", - "nomad", - "noodles", - "northern", - "nostril", - "noted", - "nouns", - "novelty", - "nowhere", - "nozzle", - "nuance", - "nucleus", - "nudged", - "nugget", - "nuisance", - "null", - "number", - "nuns", - "nurse", - "nutshell", - "nylon", - "oaks", - "oars", - "oasis", - "oatmeal", - "obedient", - "object", - "obliged", - "obnoxious", - "observant", - "obtains", - "obvious", - "occur", - "ocean", - "october", - "odds", - "odometer", - "offend", - "often", - "oilfield", - "ointment", - "okay", - "older", - "olive", - "olympics", - "omega", - "omission", - "omnibus", - "onboard", - "oncoming", - "oneself", - "ongoing", - "onion", - "online", - "onslaught", - "onto", - "onward", - "oozed", - "opacity", - "opened", - "opposite", - "optical", - "opus", - "orange", - "orbit", - "orchid", - "orders", - "organs", - "origin", - "ornament", - "orphans", - "oscar", - "ostrich", - "otherwise", - "otter", - "ouch", - "ought", - "ounce", - "ourselves", - "oust", - "outbreak", - "oval", - "oven", - "owed", - "owls", - "owner", - "oxidant", - "oxygen", - "oyster", - "ozone", - "pact", - "paddles", - "pager", - "pairing", - "palace", - "pamphlet", - "pancakes", - "paper", - "paradise", - "pastry", - "patio", - "pause", - "pavements", - "pawnshop", - "payment", - "peaches", - "pebbles", - "peculiar", - "pedantic", - "peeled", - "pegs", - "pelican", - "pencil", - "people", - "pepper", - "perfect", - "pests", - "petals", - "phase", - "pheasants", - "phone", - "phrases", - "physics", - "piano", - "picked", - "pierce", - "pigment", - "piloted", - "pimple", - "pinched", - "pioneer", - "pipeline", - "pirate", - "pistons", - "pitched", - "pivot", - "pixels", - "pizza", - "playful", - "pledge", - "pliers", - "plotting", - "plus", - "plywood", - "poaching", - "pockets", - "podcast", - "poetry", - "point", - "poker", - "polar", - "ponies", - "pool", - "popular", - "portents", - "possible", - "potato", - "pouch", - "poverty", - "powder", - "pram", - "present", - "pride", - "problems", - "pruned", - "prying", - "psychic", - "public", - "puck", - "puddle", - "puffin", - "pulp", - "pumpkins", - "punch", - "puppy", - "purged", - "push", - "putty", - "puzzled", - "pylons", - "pyramid", - "python", - "queen", - "quick", - "quote", - "rabbits", - "racetrack", - "radar", - "rafts", - "rage", - "railway", - "raking", - "rally", - "ramped", - "randomly", - "rapid", - "rarest", - "rash", - "rated", - "ravine", - "rays", - "razor", - "react", - "rebel", - "recipe", - "reduce", - "reef", - "refer", - "regular", - "reheat", - "reinvest", - "rejoices", - "rekindle", - "relic", - "remedy", - "renting", - "reorder", - "repent", - "request", - "reruns", - "rest", - "return", - "reunion", - "revamp", - "rewind", - "rhino", - "rhythm", - "ribbon", - "richly", - "ridges", - "rift", - "rigid", - "rims", - "ringing", - "riots", - "ripped", - "rising", - "ritual", - "river", - "roared", - "robot", - "rockets", - "rodent", - "rogue", - "roles", - "romance", - "roomy", - "roped", - "roster", - "rotate", - "rounded", - "rover", - "rowboat", - "royal", - "ruby", - "rudely", - "ruffled", - "rugged", - "ruined", - "ruling", - "rumble", - "runway", - "rural", - "rustled", - "ruthless", - "sabotage", - "sack", - "sadness", - "safety", - "saga", - "sailor", - "sake", - "salads", - "sample", - "sanity", - "sapling", - "sarcasm", - "sash", - "satin", - "saucepan", - "saved", - "sawmill", - "saxophone", - "sayings", - "scamper", - "scenic", - "school", - "science", - "scoop", - "scrub", - "scuba", - "seasons", - "second", - "sedan", - "seeded", - "segments", - "seismic", - "selfish", - "semifinal", - "sensible", - "september", - "sequence", - "serving", - "session", - "setup", - "seventh", - "sewage", - "shackles", - "shelter", - "shipped", - "shocking", - "shrugged", - "shuffled", - "shyness", - "siblings", - "sickness", - "sidekick", - "sieve", - "sifting", - "sighting", - "silk", - "simplest", - "sincerely", - "sipped", - "siren", - "situated", - "sixteen", - "sizes", - "skater", - "skew", - "skirting", - "skulls", - "skydive", - "slackens", - "sleepless", - "slid", - "slower", - "slug", - "smash", - "smelting", - "smidgen", - "smog", - "smuggled", - "snake", - "sneeze", - "sniff", - "snout", - "snug", - "soapy", - "sober", - "soccer", - "soda", - "software", - "soggy", - "soil", - "solved", - "somewhere", - "sonic", - "soothe", - "soprano", - "sorry", - "southern", - "sovereign", - "sowed", - "soya", - "space", - "speedy", - "sphere", - "spiders", - "splendid", - "spout", - "sprig", - "spud", - "spying", - "square", - "stacking", - "stellar", - "stick", - "stockpile", - "strained", - "stunning", - "stylishly", - "subtly", - "succeed", - "suddenly", - "suede", - "suffice", - "sugar", - "suitcase", - "sulking", - "summon", - "sunken", - "superior", - "surfer", - "sushi", - "suture", - "swagger", - "swept", - "swiftly", - "sword", - "swung", - "syllabus", - "symptoms", - "syndrome", - "syringe", - "system", - "taboo", - "tacit", - "tadpoles", - "tagged", - "tail", - "taken", - "talent", - "tamper", - "tanks", - "tapestry", - "tarnished", - "tasked", - "tattoo", - "taunts", - "tavern", - "tawny", - "taxi", - "teardrop", - "technical", - "tedious", - "teeming", - "tell", - "template", - "tender", - "tepid", - "tequila", - "terminal", - "testing", - "tether", - "textbook", - "thaw", - "theatrics", - "thirsty", - "thorn", - "threaten", - "thumbs", - "thwart", - "ticket", - "tidy", - "tiers", - "tiger", - "tilt", - "timber", - "tinted", - "tipsy", - "tirade", - "tissue", - "titans", - "toaster", - "tobacco", - "today", - "toenail", - "toffee", - "together", - "toilet", - "token", - "tolerant", - "tomorrow", - "tonic", - "toolbox", - "topic", - "torch", - "tossed", - "total", - "touchy", - "towel", - "toxic", - "toyed", - "trash", - "trendy", - "tribal", - "trolling", - "truth", - "trying", - "tsunami", - "tubes", - "tucks", - "tudor", - "tuesday", - "tufts", - "tugs", - "tuition", - "tulips", - "tumbling", - "tunnel", - "turnip", - "tusks", - "tutor", - "tuxedo", - "twang", - "tweezers", - "twice", - "twofold", - "tycoon", - "typist", - "tyrant", - "ugly", - "ulcers", - "ultimate", - "umbrella", - "umpire", - "unafraid", - "unbending", - "uncle", - "under", - "uneven", - "unfit", - "ungainly", - "unhappy", - "union", - "unjustly", - "unknown", - "unlikely", - "unmask", - "unnoticed", - "unopened", - "unplugs", - "unquoted", - "unrest", - "unsafe", - "until", - "unusual", - "unveil", - "unwind", - "unzip", - "upbeat", - "upcoming", - "update", - "upgrade", - "uphill", - "upkeep", - "upload", - "upon", - "upper", - "upright", - "upstairs", - "uptight", - "upwards", - "urban", - "urchins", - "urgent", - "usage", - "useful", - "usher", - "using", - "usual", - "utensils", - "utility", - "utmost", - "utopia", - "uttered", - "vacation", - "vague", - "vain", - "value", - "vampire", - "vane", - "vapidly", - "vary", - "vastness", - "vats", - "vaults", - "vector", - "veered", - "vegan", - "vehicle", - "vein", - "velvet", - "venomous", - "verification", - "vessel", - "veteran", - "vexed", - "vials", - "vibrate", - "victim", - "video", - "viewpoint", - "vigilant", - "viking", - "village", - "vinegar", - "violin", - "vipers", - "virtual", - "visited", - "vitals", - "vivid", - "vixen", - "vocal", - "vogue", - "voice", - "volcano", - "vortex", - "voted", - "voucher", - "vowels", - "voyage", - "vulture", - "wade", - "waffle", - "wagtail", - "waist", - "waking", - "wallets", - "wanted", - "warped", - "washing", - "water", - "waveform", - "waxing", - "wayside", - "weavers", - "website", - "wedge", - "weekday", - "weird", - "welders", - "went", - "wept", - "were", - "western", - "wetsuit", - "whale", - "when", - "whipped", - "whole", - "wickets", - "width", - "wield", - "wife", - "wiggle", - "wildly", - "winter", - "wipeout", - "wiring", - "wise", - "withdrawn", - "wives", - "wizard", - "wobbly", - "woes", - "woken", - "wolf", - "womanly", - "wonders", - "woozy", - "worry", - "wounded", - "woven", - "wrap", - "wrist", - "wrong", - "yacht", - "yahoo", - "yanks", - "yard", - "yawning", - "yearbook", - "yellow", - "yesterday", - "yeti", - "yields", - "yodel", - "yoga", - "younger", - "yoyo", - "zapped", - "zeal", - "zebra", - "zero", - "zesty", - "zigzags", - "zinger", - "zippers", - "zodiac", - "zombie", - "zones", - "zoom" - ]; -} diff --git a/cw_haven/lib/mnemonics/french.dart b/cw_haven/lib/mnemonics/french.dart deleted file mode 100644 index 76d556f6a..000000000 --- a/cw_haven/lib/mnemonics/french.dart +++ /dev/null @@ -1,1630 +0,0 @@ -class FrenchMnemonics { - static const words = [ - "abandon", - "abattre", - "aboi", - "abolir", - "aborder", - "abri", - "absence", - "absolu", - "abuser", - "acacia", - "acajou", - "accent", - "accord", - "accrocher", - "accuser", - "acerbe", - "achat", - "acheter", - "acide", - "acier", - "acquis", - "acte", - "action", - "adage", - "adepte", - "adieu", - "admettre", - "admis", - "adorer", - "adresser", - "aduler", - "affaire", - "affirmer", - "afin", - "agacer", - "agent", - "agir", - "agiter", - "agonie", - "agrafe", - "agrume", - "aider", - "aigle", - "aigre", - "aile", - "ailleurs", - "aimant", - "aimer", - "ainsi", - "aise", - "ajouter", - "alarme", - "album", - "alcool", - "alerte", - "algue", - "alibi", - "aller", - "allumer", - "alors", - "amande", - "amener", - "amie", - "amorcer", - "amour", - "ample", - "amuser", - "ananas", - "ancien", - "anglais", - "angoisse", - "animal", - "anneau", - "annoncer", - "apercevoir", - "apparence", - "appel", - "apporter", - "apprendre", - "appuyer", - "arbre", - "arcade", - "arceau", - "arche", - "ardeur", - "argent", - "argile", - "aride", - "arme", - "armure", - "arracher", - "arriver", - "article", - "asile", - "aspect", - "assaut", - "assez", - "assister", - "assurer", - "astre", - "astuce", - "atlas", - "atroce", - "attacher", - "attente", - "attirer", - "aube", - "aucun", - "audace", - "auparavant", - "auquel", - "aurore", - "aussi", - "autant", - "auteur", - "autoroute", - "autre", - "aval", - "avant", - "avec", - "avenir", - "averse", - "aveu", - "avide", - "avion", - "avis", - "avoir", - "avouer", - "avril", - "azote", - "azur", - "badge", - "bagage", - "bague", - "bain", - "baisser", - "balai", - "balcon", - "balise", - "balle", - "bambou", - "banane", - "banc", - "bandage", - "banjo", - "banlieue", - "bannir", - "banque", - "baobab", - "barbe", - "barque", - "barrer", - "bassine", - "bataille", - "bateau", - "battre", - "baver", - "bavoir", - "bazar", - "beau", - "beige", - "berger", - "besoin", - "beurre", - "biais", - "biceps", - "bidule", - "bien", - "bijou", - "bilan", - "billet", - "blanc", - "blason", - "bleu", - "bloc", - "blond", - "bocal", - "boire", - "boiserie", - "boiter", - "bonbon", - "bondir", - "bonheur", - "bordure", - "borgne", - "borner", - "bosse", - "bouche", - "bouder", - "bouger", - "boule", - "bourse", - "bout", - "boxe", - "brader", - "braise", - "branche", - "braquer", - "bras", - "brave", - "brebis", - "brevet", - "brider", - "briller", - "brin", - "brique", - "briser", - "broche", - "broder", - "bronze", - "brosser", - "brouter", - "bruit", - "brute", - "budget", - "buffet", - "bulle", - "bureau", - "buriner", - "buste", - "buter", - "butiner", - "cabas", - "cabinet", - "cabri", - "cacao", - "cacher", - "cadeau", - "cadre", - "cage", - "caisse", - "caler", - "calme", - "camarade", - "camion", - "campagne", - "canal", - "canif", - "capable", - "capot", - "carat", - "caresser", - "carie", - "carpe", - "cartel", - "casier", - "casque", - "casserole", - "cause", - "cavale", - "cave", - "ceci", - "cela", - "celui", - "cendre", - "cent", - "cependant", - "cercle", - "cerise", - "cerner", - "certes", - "cerveau", - "cesser", - "chacun", - "chair", - "chaleur", - "chamois", - "chanson", - "chaque", - "charge", - "chasse", - "chat", - "chaud", - "chef", - "chemin", - "cheveu", - "chez", - "chicane", - "chien", - "chiffre", - "chiner", - "chiot", - "chlore", - "choc", - "choix", - "chose", - "chou", - "chute", - "cibler", - "cidre", - "ciel", - "cigale", - "cinq", - "cintre", - "cirage", - "cirque", - "ciseau", - "citation", - "citer", - "citron", - "civet", - "clairon", - "clan", - "classe", - "clavier", - "clef", - "climat", - "cloche", - "cloner", - "clore", - "clos", - "clou", - "club", - "cobra", - "cocon", - "coiffer", - "coin", - "colline", - "colon", - "combat", - "comme", - "compte", - "conclure", - "conduire", - "confier", - "connu", - "conseil", - "contre", - "convenir", - "copier", - "cordial", - "cornet", - "corps", - "cosmos", - "coton", - "couche", - "coude", - "couler", - "coupure", - "cour", - "couteau", - "couvrir", - "crabe", - "crainte", - "crampe", - "cran", - "creuser", - "crever", - "crier", - "crime", - "crin", - "crise", - "crochet", - "croix", - "cruel", - "cuisine", - "cuite", - "culot", - "culte", - "cumul", - "cure", - "curieux", - "cuve", - "dame", - "danger", - "dans", - "davantage", - "debout", - "dedans", - "dehors", - "delta", - "demain", - "demeurer", - "demi", - "dense", - "dent", - "depuis", - "dernier", - "descendre", - "dessus", - "destin", - "dette", - "deuil", - "deux", - "devant", - "devenir", - "devin", - "devoir", - "dicton", - "dieu", - "difficile", - "digestion", - "digue", - "diluer", - "dimanche", - "dinde", - "diode", - "dire", - "diriger", - "discours", - "disposer", - "distance", - "divan", - "divers", - "docile", - "docteur", - "dodu", - "dogme", - "doigt", - "dominer", - "donation", - "donjon", - "donner", - "dopage", - "dorer", - "dormir", - "doseur", - "douane", - "double", - "douche", - "douleur", - "doute", - "doux", - "douzaine", - "draguer", - "drame", - "drap", - "dresser", - "droit", - "duel", - "dune", - "duper", - "durant", - "durcir", - "durer", - "eaux", - "effacer", - "effet", - "effort", - "effrayant", - "elle", - "embrasser", - "emmener", - "emparer", - "empire", - "employer", - "emporter", - "enclos", - "encore", - "endive", - "endormir", - "endroit", - "enduit", - "enfant", - "enfermer", - "enfin", - "enfler", - "enfoncer", - "enfuir", - "engager", - "engin", - "enjeu", - "enlever", - "ennemi", - "ennui", - "ensemble", - "ensuite", - "entamer", - "entendre", - "entier", - "entourer", - "entre", - "envelopper", - "envie", - "envoyer", - "erreur", - "escalier", - "espace", - "espoir", - "esprit", - "essai", - "essor", - "essuyer", - "estimer", - "exact", - "examiner", - "excuse", - "exemple", - "exiger", - "exil", - "exister", - "exode", - "expliquer", - "exposer", - "exprimer", - "extase", - "fable", - "facette", - "facile", - "fade", - "faible", - "faim", - "faire", - "fait", - "falloir", - "famille", - "faner", - "farce", - "farine", - "fatigue", - "faucon", - "faune", - "faute", - "faux", - "faveur", - "favori", - "faxer", - "feinter", - "femme", - "fendre", - "fente", - "ferme", - "festin", - "feuille", - "feutre", - "fiable", - "fibre", - "ficher", - "fier", - "figer", - "figure", - "filet", - "fille", - "filmer", - "fils", - "filtre", - "final", - "finesse", - "finir", - "fiole", - "firme", - "fixe", - "flacon", - "flair", - "flamme", - "flan", - "flaque", - "fleur", - "flocon", - "flore", - "flot", - "flou", - "fluide", - "fluor", - "flux", - "focus", - "foin", - "foire", - "foison", - "folie", - "fonction", - "fondre", - "fonte", - "force", - "forer", - "forger", - "forme", - "fort", - "fosse", - "fouet", - "fouine", - "foule", - "four", - "foyer", - "frais", - "franc", - "frapper", - "freiner", - "frimer", - "friser", - "frite", - "froid", - "froncer", - "fruit", - "fugue", - "fuir", - "fuite", - "fumer", - "fureur", - "furieux", - "fuser", - "fusil", - "futile", - "futur", - "gagner", - "gain", - "gala", - "galet", - "galop", - "gamme", - "gant", - "garage", - "garde", - "garer", - "gauche", - "gaufre", - "gaule", - "gaver", - "gazon", - "geler", - "genou", - "genre", - "gens", - "gercer", - "germer", - "geste", - "gibier", - "gicler", - "gilet", - "girafe", - "givre", - "glace", - "glisser", - "globe", - "gloire", - "gluant", - "gober", - "golf", - "gommer", - "gorge", - "gosier", - "goutte", - "grain", - "gramme", - "grand", - "gras", - "grave", - "gredin", - "griffure", - "griller", - "gris", - "gronder", - "gros", - "grotte", - "groupe", - "grue", - "guerrier", - "guetter", - "guider", - "guise", - "habiter", - "hache", - "haie", - "haine", - "halte", - "hamac", - "hanche", - "hangar", - "hanter", - "haras", - "hareng", - "harpe", - "hasard", - "hausse", - "haut", - "havre", - "herbe", - "heure", - "hibou", - "hier", - "histoire", - "hiver", - "hochet", - "homme", - "honneur", - "honte", - "horde", - "horizon", - "hormone", - "houle", - "housse", - "hublot", - "huile", - "huit", - "humain", - "humble", - "humide", - "humour", - "hurler", - "idole", - "igloo", - "ignorer", - "illusion", - "image", - "immense", - "immobile", - "imposer", - "impression", - "incapable", - "inconnu", - "index", - "indiquer", - "infime", - "injure", - "inox", - "inspirer", - "instant", - "intention", - "intime", - "inutile", - "inventer", - "inviter", - "iode", - "iris", - "issue", - "ivre", - "jade", - "jadis", - "jamais", - "jambe", - "janvier", - "jardin", - "jauge", - "jaunisse", - "jeter", - "jeton", - "jeudi", - "jeune", - "joie", - "joindre", - "joli", - "joueur", - "journal", - "judo", - "juge", - "juillet", - "juin", - "jument", - "jungle", - "jupe", - "jupon", - "jurer", - "juron", - "jury", - "jusque", - "juste", - "kayak", - "ketchup", - "kilo", - "kiwi", - "koala", - "label", - "lacet", - "lacune", - "laine", - "laisse", - "lait", - "lame", - "lancer", - "lande", - "laque", - "lard", - "largeur", - "larme", - "larve", - "lasso", - "laver", - "lendemain", - "lentement", - "lequel", - "lettre", - "leur", - "lever", - "levure", - "liane", - "libre", - "lien", - "lier", - "lieutenant", - "ligne", - "ligoter", - "liguer", - "limace", - "limer", - "limite", - "lingot", - "lion", - "lire", - "lisser", - "litre", - "livre", - "lobe", - "local", - "logis", - "loin", - "loisir", - "long", - "loque", - "lors", - "lotus", - "louer", - "loup", - "lourd", - "louve", - "loyer", - "lubie", - "lucide", - "lueur", - "luge", - "luire", - "lundi", - "lune", - "lustre", - "lutin", - "lutte", - "luxe", - "machine", - "madame", - "magie", - "magnifique", - "magot", - "maigre", - "main", - "mairie", - "maison", - "malade", - "malheur", - "malin", - "manche", - "manger", - "manier", - "manoir", - "manquer", - "marche", - "mardi", - "marge", - "mariage", - "marquer", - "mars", - "masque", - "masse", - "matin", - "mauvais", - "meilleur", - "melon", - "membre", - "menacer", - "mener", - "mensonge", - "mentir", - "menu", - "merci", - "merlu", - "mesure", - "mettre", - "meuble", - "meunier", - "meute", - "miche", - "micro", - "midi", - "miel", - "miette", - "mieux", - "milieu", - "mille", - "mimer", - "mince", - "mineur", - "ministre", - "minute", - "mirage", - "miroir", - "miser", - "mite", - "mixte", - "mobile", - "mode", - "module", - "moins", - "mois", - "moment", - "momie", - "monde", - "monsieur", - "monter", - "moquer", - "moral", - "morceau", - "mordre", - "morose", - "morse", - "mortier", - "morue", - "motif", - "motte", - "moudre", - "moule", - "mourir", - "mousse", - "mouton", - "mouvement", - "moyen", - "muer", - "muette", - "mugir", - "muguet", - "mulot", - "multiple", - "munir", - "muret", - "muse", - "musique", - "muter", - "nacre", - "nager", - "nain", - "naissance", - "narine", - "narrer", - "naseau", - "nasse", - "nation", - "nature", - "naval", - "navet", - "naviguer", - "navrer", - "neige", - "nerf", - "nerveux", - "neuf", - "neutre", - "neuve", - "neveu", - "niche", - "nier", - "niveau", - "noble", - "noce", - "nocif", - "noir", - "nomade", - "nombre", - "nommer", - "nord", - "norme", - "notaire", - "notice", - "notre", - "nouer", - "nougat", - "nourrir", - "nous", - "nouveau", - "novice", - "noyade", - "noyer", - "nuage", - "nuance", - "nuire", - "nuit", - "nulle", - "nuque", - "oasis", - "objet", - "obliger", - "obscur", - "observer", - "obtenir", - "obus", - "occasion", - "occuper", - "ocre", - "octet", - "odeur", - "odorat", - "offense", - "officier", - "offrir", - "ogive", - "oiseau", - "olive", - "ombre", - "onctueux", - "onduler", - "ongle", - "onze", - "opter", - "option", - "orageux", - "oral", - "orange", - "orbite", - "ordinaire", - "ordre", - "oreille", - "organe", - "orgie", - "orgueil", - "orient", - "origan", - "orner", - "orteil", - "ortie", - "oser", - "osselet", - "otage", - "otarie", - "ouate", - "oublier", - "ouest", - "ours", - "outil", - "outre", - "ouvert", - "ouvrir", - "ovale", - "ozone", - "pacte", - "page", - "paille", - "pain", - "paire", - "paix", - "palace", - "palissade", - "palmier", - "palpiter", - "panda", - "panneau", - "papa", - "papier", - "paquet", - "parc", - "pardi", - "parfois", - "parler", - "parmi", - "parole", - "partir", - "parvenir", - "passer", - "pastel", - "patin", - "patron", - "paume", - "pause", - "pauvre", - "paver", - "pavot", - "payer", - "pays", - "peau", - "peigne", - "peinture", - "pelage", - "pelote", - "pencher", - "pendre", - "penser", - "pente", - "percer", - "perdu", - "perle", - "permettre", - "personne", - "perte", - "peser", - "pesticide", - "petit", - "peuple", - "peur", - "phase", - "photo", - "phrase", - "piano", - "pied", - "pierre", - "pieu", - "pile", - "pilier", - "pilote", - "pilule", - "piment", - "pincer", - "pinson", - "pinte", - "pion", - "piquer", - "pirate", - "pire", - "piste", - "piton", - "pitre", - "pivot", - "pizza", - "placer", - "plage", - "plaire", - "plan", - "plaque", - "plat", - "plein", - "pleurer", - "pliage", - "plier", - "plonger", - "plot", - "pluie", - "plume", - "plus", - "pneu", - "poche", - "podium", - "poids", - "poil", - "point", - "poire", - "poison", - "poitrine", - "poivre", - "police", - "pollen", - "pomme", - "pompier", - "poncer", - "pondre", - "pont", - "portion", - "poser", - "position", - "possible", - "poste", - "potage", - "potin", - "pouce", - "poudre", - "poulet", - "poumon", - "poupe", - "pour", - "pousser", - "poutre", - "pouvoir", - "prairie", - "premier", - "prendre", - "presque", - "preuve", - "prier", - "primeur", - "prince", - "prison", - "priver", - "prix", - "prochain", - "produire", - "profond", - "proie", - "projet", - "promener", - "prononcer", - "propre", - "prose", - "prouver", - "prune", - "public", - "puce", - "pudeur", - "puiser", - "pull", - "pulpe", - "puma", - "punir", - "purge", - "putois", - "quand", - "quartier", - "quasi", - "quatre", - "quel", - "question", - "queue", - "quiche", - "quille", - "quinze", - "quitter", - "quoi", - "rabais", - "raboter", - "race", - "racheter", - "racine", - "racler", - "raconter", - "radar", - "radio", - "rafale", - "rage", - "ragot", - "raideur", - "raie", - "rail", - "raison", - "ramasser", - "ramener", - "rampe", - "rance", - "rang", - "rapace", - "rapide", - "rapport", - "rarement", - "rasage", - "raser", - "rasoir", - "rassurer", - "rater", - "ratio", - "rature", - "ravage", - "ravir", - "rayer", - "rayon", - "rebond", - "recevoir", - "recherche", - "record", - "reculer", - "redevenir", - "refuser", - "regard", - "regretter", - "rein", - "rejeter", - "rejoindre", - "relation", - "relever", - "religion", - "remarquer", - "remettre", - "remise", - "remonter", - "remplir", - "remuer", - "rencontre", - "rendre", - "renier", - "renoncer", - "rentrer", - "renverser", - "repas", - "repli", - "reposer", - "reproche", - "requin", - "respect", - "ressembler", - "reste", - "retard", - "retenir", - "retirer", - "retour", - "retrouver", - "revenir", - "revoir", - "revue", - "rhume", - "ricaner", - "riche", - "rideau", - "ridicule", - "rien", - "rigide", - "rincer", - "rire", - "risquer", - "rituel", - "rivage", - "rive", - "robe", - "robot", - "robuste", - "rocade", - "roche", - "rodeur", - "rogner", - "roman", - "rompre", - "ronce", - "rondeur", - "ronger", - "roque", - "rose", - "rosir", - "rotation", - "rotule", - "roue", - "rouge", - "rouler", - "route", - "ruban", - "rubis", - "ruche", - "rude", - "ruelle", - "ruer", - "rugby", - "rugir", - "ruine", - "rumeur", - "rural", - "ruse", - "rustre", - "sable", - "sabot", - "sabre", - "sacre", - "sage", - "saint", - "saisir", - "salade", - "salive", - "salle", - "salon", - "salto", - "salut", - "salve", - "samba", - "sandale", - "sanguin", - "sapin", - "sarcasme", - "satisfaire", - "sauce", - "sauf", - "sauge", - "saule", - "sauna", - "sauter", - "sauver", - "savoir", - "science", - "scoop", - "score", - "second", - "secret", - "secte", - "seigneur", - "sein", - "seize", - "selle", - "selon", - "semaine", - "sembler", - "semer", - "semis", - "sensuel", - "sentir", - "sept", - "serpe", - "serrer", - "sertir", - "service", - "seuil", - "seulement", - "short", - "sien", - "sigle", - "signal", - "silence", - "silo", - "simple", - "singe", - "sinon", - "sinus", - "sioux", - "sirop", - "site", - "situation", - "skier", - "snob", - "sobre", - "social", - "socle", - "sodium", - "soigner", - "soir", - "soixante", - "soja", - "solaire", - "soldat", - "soleil", - "solide", - "solo", - "solvant", - "sombre", - "somme", - "somnoler", - "sondage", - "songeur", - "sonner", - "sorte", - "sosie", - "sottise", - "souci", - "soudain", - "souffrir", - "souhaiter", - "soulever", - "soumettre", - "soupe", - "sourd", - "soustraire", - "soutenir", - "souvent", - "soyeux", - "spectacle", - "sport", - "stade", - "stagiaire", - "stand", - "star", - "statue", - "stock", - "stop", - "store", - "style", - "suave", - "subir", - "sucre", - "suer", - "suffire", - "suie", - "suite", - "suivre", - "sujet", - "sulfite", - "supposer", - "surf", - "surprendre", - "surtout", - "surveiller", - "tabac", - "table", - "tabou", - "tache", - "tacler", - "tacot", - "tact", - "taie", - "taille", - "taire", - "talon", - "talus", - "tandis", - "tango", - "tanin", - "tant", - "taper", - "tapis", - "tard", - "tarif", - "tarot", - "tarte", - "tasse", - "taureau", - "taux", - "taverne", - "taxer", - "taxi", - "tellement", - "temple", - "tendre", - "tenir", - "tenter", - "tenu", - "terme", - "ternir", - "terre", - "test", - "texte", - "thym", - "tibia", - "tiers", - "tige", - "tipi", - "tique", - "tirer", - "tissu", - "titre", - "toast", - "toge", - "toile", - "toiser", - "toiture", - "tomber", - "tome", - "tonne", - "tonte", - "toque", - "torse", - "tortue", - "totem", - "toucher", - "toujours", - "tour", - "tousser", - "tout", - "toux", - "trace", - "train", - "trame", - "tranquille", - "travail", - "trembler", - "trente", - "tribu", - "trier", - "trio", - "tripe", - "triste", - "troc", - "trois", - "tromper", - "tronc", - "trop", - "trotter", - "trouer", - "truc", - "truite", - "tuba", - "tuer", - "tuile", - "turbo", - "tutu", - "tuyau", - "type", - "union", - "unique", - "unir", - "unisson", - "untel", - "urne", - "usage", - "user", - "usiner", - "usure", - "utile", - "vache", - "vague", - "vaincre", - "valeur", - "valoir", - "valser", - "valve", - "vampire", - "vaseux", - "vaste", - "veau", - "veille", - "veine", - "velours", - "velu", - "vendre", - "venir", - "vent", - "venue", - "verbe", - "verdict", - "version", - "vertige", - "verve", - "veste", - "veto", - "vexer", - "vice", - "victime", - "vide", - "vieil", - "vieux", - "vigie", - "vigne", - "ville", - "vingt", - "violent", - "virer", - "virus", - "visage", - "viser", - "visite", - "visuel", - "vitamine", - "vitrine", - "vivant", - "vivre", - "vocal", - "vodka", - "vogue", - "voici", - "voile", - "voir", - "voisin", - "voiture", - "volaille", - "volcan", - "voler", - "volt", - "votant", - "votre", - "vouer", - "vouloir", - "vous", - "voyage", - "voyou", - "vrac", - "vrai", - "yacht", - "yeti", - "yeux", - "yoga", - "zeste", - "zinc", - "zone", - "zoom" - ]; -} \ No newline at end of file diff --git a/cw_haven/lib/mnemonics/german.dart b/cw_haven/lib/mnemonics/german.dart deleted file mode 100644 index 1491c9b0e..000000000 --- a/cw_haven/lib/mnemonics/german.dart +++ /dev/null @@ -1,1630 +0,0 @@ -class GermanMnemonics { - static const words = [ - "Abakus", - "Abart", - "abbilden", - "Abbruch", - "Abdrift", - "Abendrot", - "Abfahrt", - "abfeuern", - "Abflug", - "abfragen", - "Abglanz", - "abhärten", - "abheben", - "Abhilfe", - "Abitur", - "Abkehr", - "Ablauf", - "ablecken", - "Ablösung", - "Abnehmer", - "abnutzen", - "Abonnent", - "Abrasion", - "Abrede", - "abrüsten", - "Absicht", - "Absprung", - "Abstand", - "absuchen", - "Abteil", - "Abundanz", - "abwarten", - "Abwurf", - "Abzug", - "Achse", - "Achtung", - "Acker", - "Aderlass", - "Adler", - "Admiral", - "Adresse", - "Affe", - "Affront", - "Afrika", - "Aggregat", - "Agilität", - "ähneln", - "Ahnung", - "Ahorn", - "Akazie", - "Akkord", - "Akrobat", - "Aktfoto", - "Aktivist", - "Albatros", - "Alchimie", - "Alemanne", - "Alibi", - "Alkohol", - "Allee", - "Allüre", - "Almosen", - "Almweide", - "Aloe", - "Alpaka", - "Alpental", - "Alphabet", - "Alpinist", - "Alraune", - "Altbier", - "Alter", - "Altflöte", - "Altruist", - "Alublech", - "Aludose", - "Amateur", - "Amazonas", - "Ameise", - "Amnesie", - "Amok", - "Ampel", - "Amphibie", - "Ampulle", - "Amsel", - "Amulett", - "Anakonda", - "Analogie", - "Ananas", - "Anarchie", - "Anatomie", - "Anbau", - "Anbeginn", - "anbieten", - "Anblick", - "ändern", - "andocken", - "Andrang", - "anecken", - "Anflug", - "Anfrage", - "Anführer", - "Angebot", - "Angler", - "Anhalter", - "Anhöhe", - "Animator", - "Anis", - "Anker", - "ankleben", - "Ankunft", - "Anlage", - "anlocken", - "Anmut", - "Annahme", - "Anomalie", - "Anonymus", - "Anorak", - "anpeilen", - "Anrecht", - "Anruf", - "Ansage", - "Anschein", - "Ansicht", - "Ansporn", - "Anteil", - "Antlitz", - "Antrag", - "Antwort", - "Anwohner", - "Aorta", - "Apfel", - "Appetit", - "Applaus", - "Aquarium", - "Arbeit", - "Arche", - "Argument", - "Arktis", - "Armband", - "Aroma", - "Asche", - "Askese", - "Asphalt", - "Asteroid", - "Ästhetik", - "Astronom", - "Atelier", - "Athlet", - "Atlantik", - "Atmung", - "Audienz", - "aufatmen", - "Auffahrt", - "aufholen", - "aufregen", - "Aufsatz", - "Auftritt", - "Aufwand", - "Augapfel", - "Auktion", - "Ausbruch", - "Ausflug", - "Ausgabe", - "Aushilfe", - "Ausland", - "Ausnahme", - "Aussage", - "Autobahn", - "Avocado", - "Axthieb", - "Bach", - "backen", - "Badesee", - "Bahnhof", - "Balance", - "Balkon", - "Ballett", - "Balsam", - "Banane", - "Bandage", - "Bankett", - "Barbar", - "Barde", - "Barett", - "Bargeld", - "Barkasse", - "Barriere", - "Bart", - "Bass", - "Bastler", - "Batterie", - "Bauch", - "Bauer", - "Bauholz", - "Baujahr", - "Baum", - "Baustahl", - "Bauteil", - "Bauweise", - "Bazar", - "beachten", - "Beatmung", - "beben", - "Becher", - "Becken", - "bedanken", - "beeilen", - "beenden", - "Beere", - "befinden", - "Befreier", - "Begabung", - "Begierde", - "begrüßen", - "Beiboot", - "Beichte", - "Beifall", - "Beigabe", - "Beil", - "Beispiel", - "Beitrag", - "beizen", - "bekommen", - "beladen", - "Beleg", - "bellen", - "belohnen", - "Bemalung", - "Bengel", - "Benutzer", - "Benzin", - "beraten", - "Bereich", - "Bergluft", - "Bericht", - "Bescheid", - "Besitz", - "besorgen", - "Bestand", - "Besuch", - "betanken", - "beten", - "betören", - "Bett", - "Beule", - "Beute", - "Bewegung", - "bewirken", - "Bewohner", - "bezahlen", - "Bezug", - "biegen", - "Biene", - "Bierzelt", - "bieten", - "Bikini", - "Bildung", - "Billard", - "binden", - "Biobauer", - "Biologe", - "Bionik", - "Biotop", - "Birke", - "Bison", - "Bitte", - "Biwak", - "Bizeps", - "blasen", - "Blatt", - "Blauwal", - "Blende", - "Blick", - "Blitz", - "Blockade", - "Blödelei", - "Blondine", - "Blues", - "Blume", - "Blut", - "Bodensee", - "Bogen", - "Boje", - "Bollwerk", - "Bonbon", - "Bonus", - "Boot", - "Bordarzt", - "Börse", - "Böschung", - "Boudoir", - "Boxkampf", - "Boykott", - "Brahms", - "Brandung", - "Brauerei", - "Brecher", - "Breitaxt", - "Bremse", - "brennen", - "Brett", - "Brief", - "Brigade", - "Brillanz", - "bringen", - "brodeln", - "Brosche", - "Brötchen", - "Brücke", - "Brunnen", - "Brüste", - "Brutofen", - "Buch", - "Büffel", - "Bugwelle", - "Bühne", - "Buletten", - "Bullauge", - "Bumerang", - "bummeln", - "Buntglas", - "Bürde", - "Burgherr", - "Bursche", - "Busen", - "Buslinie", - "Bussard", - "Butangas", - "Butter", - "Cabrio", - "campen", - "Captain", - "Cartoon", - "Cello", - "Chalet", - "Charisma", - "Chefarzt", - "Chiffon", - "Chipsatz", - "Chirurg", - "Chor", - "Chronik", - "Chuzpe", - "Clubhaus", - "Cockpit", - "Codewort", - "Cognac", - "Coladose", - "Computer", - "Coupon", - "Cousin", - "Cracking", - "Crash", - "Curry", - "Dach", - "Dackel", - "daddeln", - "daliegen", - "Dame", - "Dammbau", - "Dämon", - "Dampflok", - "Dank", - "Darm", - "Datei", - "Datsche", - "Datteln", - "Datum", - "Dauer", - "Daunen", - "Deckel", - "Decoder", - "Defekt", - "Degen", - "Dehnung", - "Deiche", - "Dekade", - "Dekor", - "Delfin", - "Demut", - "denken", - "Deponie", - "Design", - "Desktop", - "Dessert", - "Detail", - "Detektiv", - "Dezibel", - "Diadem", - "Diagnose", - "Dialekt", - "Diamant", - "Dichter", - "Dickicht", - "Diesel", - "Diktat", - "Diplom", - "Direktor", - "Dirne", - "Diskurs", - "Distanz", - "Docht", - "Dohle", - "Dolch", - "Domäne", - "Donner", - "Dorade", - "Dorf", - "Dörrobst", - "Dorsch", - "Dossier", - "Dozent", - "Drachen", - "Draht", - "Drama", - "Drang", - "Drehbuch", - "Dreieck", - "Dressur", - "Drittel", - "Drossel", - "Druck", - "Duell", - "Duft", - "Düne", - "Dünung", - "dürfen", - "Duschbad", - "Düsenjet", - "Dynamik", - "Ebbe", - "Echolot", - "Echse", - "Eckball", - "Edding", - "Edelweiß", - "Eden", - "Edition", - "Efeu", - "Effekte", - "Egoismus", - "Ehre", - "Eiablage", - "Eiche", - "Eidechse", - "Eidotter", - "Eierkopf", - "Eigelb", - "Eiland", - "Eilbote", - "Eimer", - "einatmen", - "Einband", - "Eindruck", - "Einfall", - "Eingang", - "Einkauf", - "einladen", - "Einöde", - "Einrad", - "Eintopf", - "Einwurf", - "Einzug", - "Eisbär", - "Eisen", - "Eishöhle", - "Eismeer", - "Eiweiß", - "Ekstase", - "Elan", - "Elch", - "Elefant", - "Eleganz", - "Element", - "Elfe", - "Elite", - "Elixier", - "Ellbogen", - "Eloquenz", - "Emigrant", - "Emission", - "Emotion", - "Empathie", - "Empfang", - "Endzeit", - "Energie", - "Engpass", - "Enkel", - "Enklave", - "Ente", - "entheben", - "Entität", - "entladen", - "Entwurf", - "Episode", - "Epoche", - "erachten", - "Erbauer", - "erblühen", - "Erdbeere", - "Erde", - "Erdgas", - "Erdkunde", - "Erdnuss", - "Erdöl", - "Erdteil", - "Ereignis", - "Eremit", - "erfahren", - "Erfolg", - "erfreuen", - "erfüllen", - "Ergebnis", - "erhitzen", - "erkalten", - "erkennen", - "erleben", - "Erlösung", - "ernähren", - "erneuern", - "Ernte", - "Eroberer", - "eröffnen", - "Erosion", - "Erotik", - "Erpel", - "erraten", - "Erreger", - "erröten", - "Ersatz", - "Erstflug", - "Ertrag", - "Eruption", - "erwarten", - "erwidern", - "Erzbau", - "Erzeuger", - "erziehen", - "Esel", - "Eskimo", - "Eskorte", - "Espe", - "Espresso", - "essen", - "Etage", - "Etappe", - "Etat", - "Ethik", - "Etikett", - "Etüde", - "Eule", - "Euphorie", - "Europa", - "Everest", - "Examen", - "Exil", - "Exodus", - "Extrakt", - "Fabel", - "Fabrik", - "Fachmann", - "Fackel", - "Faden", - "Fagott", - "Fahne", - "Faible", - "Fairness", - "Fakt", - "Fakultät", - "Falke", - "Fallobst", - "Fälscher", - "Faltboot", - "Familie", - "Fanclub", - "Fanfare", - "Fangarm", - "Fantasie", - "Farbe", - "Farmhaus", - "Farn", - "Fasan", - "Faser", - "Fassung", - "fasten", - "Faulheit", - "Fauna", - "Faust", - "Favorit", - "Faxgerät", - "Fazit", - "fechten", - "Federboa", - "Fehler", - "Feier", - "Feige", - "feilen", - "Feinripp", - "Feldbett", - "Felge", - "Fellpony", - "Felswand", - "Ferien", - "Ferkel", - "Fernweh", - "Ferse", - "Fest", - "Fettnapf", - "Feuer", - "Fiasko", - "Fichte", - "Fiktion", - "Film", - "Filter", - "Filz", - "Finanzen", - "Findling", - "Finger", - "Fink", - "Finnwal", - "Fisch", - "Fitness", - "Fixpunkt", - "Fixstern", - "Fjord", - "Flachbau", - "Flagge", - "Flamenco", - "Flanke", - "Flasche", - "Flaute", - "Fleck", - "Flegel", - "flehen", - "Fleisch", - "fliegen", - "Flinte", - "Flirt", - "Flocke", - "Floh", - "Floskel", - "Floß", - "Flöte", - "Flugzeug", - "Flunder", - "Flusstal", - "Flutung", - "Fockmast", - "Fohlen", - "Föhnlage", - "Fokus", - "folgen", - "Foliant", - "Folklore", - "Fontäne", - "Förde", - "Forelle", - "Format", - "Forscher", - "Fortgang", - "Forum", - "Fotograf", - "Frachter", - "Fragment", - "Fraktion", - "fräsen", - "Frauenpo", - "Freak", - "Fregatte", - "Freiheit", - "Freude", - "Frieden", - "Frohsinn", - "Frosch", - "Frucht", - "Frühjahr", - "Fuchs", - "Fügung", - "fühlen", - "Füller", - "Fundbüro", - "Funkboje", - "Funzel", - "Furnier", - "Fürsorge", - "Fusel", - "Fußbad", - "Futteral", - "Gabelung", - "gackern", - "Gage", - "gähnen", - "Galaxie", - "Galeere", - "Galopp", - "Gameboy", - "Gamsbart", - "Gandhi", - "Gang", - "Garage", - "Gardine", - "Garküche", - "Garten", - "Gasthaus", - "Gattung", - "gaukeln", - "Gazelle", - "Gebäck", - "Gebirge", - "Gebräu", - "Geburt", - "Gedanke", - "Gedeck", - "Gedicht", - "Gefahr", - "Gefieder", - "Geflügel", - "Gefühl", - "Gegend", - "Gehirn", - "Gehöft", - "Gehweg", - "Geige", - "Geist", - "Gelage", - "Geld", - "Gelenk", - "Gelübde", - "Gemälde", - "Gemeinde", - "Gemüse", - "genesen", - "Genuss", - "Gepäck", - "Geranie", - "Gericht", - "Germane", - "Geruch", - "Gesang", - "Geschenk", - "Gesetz", - "Gesindel", - "Gesöff", - "Gespan", - "Gestade", - "Gesuch", - "Getier", - "Getränk", - "Getümmel", - "Gewand", - "Geweih", - "Gewitter", - "Gewölbe", - "Geysir", - "Giftzahn", - "Gipfel", - "Giraffe", - "Gitarre", - "glänzen", - "Glasauge", - "Glatze", - "Gleis", - "Globus", - "Glück", - "glühen", - "Glutofen", - "Goldzahn", - "Gondel", - "gönnen", - "Gottheit", - "graben", - "Grafik", - "Grashalm", - "Graugans", - "greifen", - "Grenze", - "grillen", - "Groschen", - "Grotte", - "Grube", - "Grünalge", - "Gruppe", - "gruseln", - "Gulasch", - "Gummibär", - "Gurgel", - "Gürtel", - "Güterzug", - "Haarband", - "Habicht", - "hacken", - "hadern", - "Hafen", - "Hagel", - "Hähnchen", - "Haifisch", - "Haken", - "Halbaffe", - "Halsader", - "halten", - "Halunke", - "Handbuch", - "Hanf", - "Harfe", - "Harnisch", - "härten", - "Harz", - "Hasenohr", - "Haube", - "hauchen", - "Haupt", - "Haut", - "Havarie", - "Hebamme", - "hecheln", - "Heck", - "Hedonist", - "Heiler", - "Heimat", - "Heizung", - "Hektik", - "Held", - "helfen", - "Helium", - "Hemd", - "hemmen", - "Hengst", - "Herd", - "Hering", - "Herkunft", - "Hermelin", - "Herrchen", - "Herzdame", - "Heulboje", - "Hexe", - "Hilfe", - "Himbeere", - "Himmel", - "Hingabe", - "hinhören", - "Hinweis", - "Hirsch", - "Hirte", - "Hitzkopf", - "Hobel", - "Hochform", - "Hocker", - "hoffen", - "Hofhund", - "Hofnarr", - "Höhenzug", - "Hohlraum", - "Hölle", - "Holzboot", - "Honig", - "Honorar", - "horchen", - "Hörprobe", - "Höschen", - "Hotel", - "Hubraum", - "Hufeisen", - "Hügel", - "huldigen", - "Hülle", - "Humbug", - "Hummer", - "Humor", - "Hund", - "Hunger", - "Hupe", - "Hürde", - "Hurrikan", - "Hydrant", - "Hypnose", - "Ibis", - "Idee", - "Idiot", - "Igel", - "Illusion", - "Imitat", - "impfen", - "Import", - "Inferno", - "Ingwer", - "Inhalte", - "Inland", - "Insekt", - "Ironie", - "Irrfahrt", - "Irrtum", - "Isolator", - "Istwert", - "Jacke", - "Jade", - "Jagdhund", - "Jäger", - "Jaguar", - "Jahr", - "Jähzorn", - "Jazzfest", - "Jetpilot", - "jobben", - "Jochbein", - "jodeln", - "Jodsalz", - "Jolle", - "Journal", - "Jubel", - "Junge", - "Junimond", - "Jupiter", - "Jutesack", - "Juwel", - "Kabarett", - "Kabine", - "Kabuff", - "Käfer", - "Kaffee", - "Kahlkopf", - "Kaimauer", - "Kajüte", - "Kaktus", - "Kaliber", - "Kaltluft", - "Kamel", - "kämmen", - "Kampagne", - "Kanal", - "Känguru", - "Kanister", - "Kanone", - "Kante", - "Kanu", - "kapern", - "Kapitän", - "Kapuze", - "Karneval", - "Karotte", - "Käsebrot", - "Kasper", - "Kastanie", - "Katalog", - "Kathode", - "Katze", - "kaufen", - "Kaugummi", - "Kauz", - "Kehle", - "Keilerei", - "Keksdose", - "Kellner", - "Keramik", - "Kerze", - "Kessel", - "Kette", - "keuchen", - "kichern", - "Kielboot", - "Kindheit", - "Kinnbart", - "Kinosaal", - "Kiosk", - "Kissen", - "Klammer", - "Klang", - "Klapprad", - "Klartext", - "kleben", - "Klee", - "Kleinod", - "Klima", - "Klingel", - "Klippe", - "Klischee", - "Kloster", - "Klugheit", - "Klüngel", - "kneten", - "Knie", - "Knöchel", - "knüpfen", - "Kobold", - "Kochbuch", - "Kohlrabi", - "Koje", - "Kokosöl", - "Kolibri", - "Kolumne", - "Kombüse", - "Komiker", - "kommen", - "Konto", - "Konzept", - "Kopfkino", - "Kordhose", - "Korken", - "Korsett", - "Kosename", - "Krabbe", - "Krach", - "Kraft", - "Krähe", - "Kralle", - "Krapfen", - "Krater", - "kraulen", - "Kreuz", - "Krokodil", - "Kröte", - "Kugel", - "Kuhhirt", - "Kühnheit", - "Künstler", - "Kurort", - "Kurve", - "Kurzfilm", - "kuscheln", - "küssen", - "Kutter", - "Labor", - "lachen", - "Lackaffe", - "Ladeluke", - "Lagune", - "Laib", - "Lakritze", - "Lammfell", - "Land", - "Langmut", - "Lappalie", - "Last", - "Laterne", - "Latzhose", - "Laubsäge", - "laufen", - "Laune", - "Lausbub", - "Lavasee", - "Leben", - "Leder", - "Leerlauf", - "Lehm", - "Lehrer", - "leihen", - "Lektüre", - "Lenker", - "Lerche", - "Leseecke", - "Leuchter", - "Lexikon", - "Libelle", - "Libido", - "Licht", - "Liebe", - "liefern", - "Liftboy", - "Limonade", - "Lineal", - "Linoleum", - "List", - "Liveband", - "Lobrede", - "locken", - "Löffel", - "Logbuch", - "Logik", - "Lohn", - "Loipe", - "Lokal", - "Lorbeer", - "Lösung", - "löten", - "Lottofee", - "Löwe", - "Luchs", - "Luder", - "Luftpost", - "Luke", - "Lümmel", - "Lunge", - "lutschen", - "Luxus", - "Macht", - "Magazin", - "Magier", - "Magnet", - "mähen", - "Mahlzeit", - "Mahnmal", - "Maibaum", - "Maisbrei", - "Makel", - "malen", - "Mammut", - "Maniküre", - "Mantel", - "Marathon", - "Marder", - "Marine", - "Marke", - "Marmor", - "Märzluft", - "Maske", - "Maßanzug", - "Maßkrug", - "Mastkorb", - "Material", - "Matratze", - "Mauerbau", - "Maulkorb", - "Mäuschen", - "Mäzen", - "Medium", - "Meinung", - "melden", - "Melodie", - "Mensch", - "Merkmal", - "Messe", - "Metall", - "Meteor", - "Methode", - "Metzger", - "Mieze", - "Milchkuh", - "Mimose", - "Minirock", - "Minute", - "mischen", - "Missetat", - "mitgehen", - "Mittag", - "Mixtape", - "Möbel", - "Modul", - "mögen", - "Möhre", - "Molch", - "Moment", - "Monat", - "Mondflug", - "Monitor", - "Monokini", - "Monster", - "Monument", - "Moorhuhn", - "Moos", - "Möpse", - "Moral", - "Mörtel", - "Motiv", - "Motorrad", - "Möwe", - "Mühe", - "Mulatte", - "Müller", - "Mumie", - "Mund", - "Münze", - "Muschel", - "Muster", - "Mythos", - "Nabel", - "Nachtzug", - "Nackedei", - "Nagel", - "Nähe", - "Nähnadel", - "Namen", - "Narbe", - "Narwal", - "Nasenbär", - "Natur", - "Nebel", - "necken", - "Neffe", - "Neigung", - "Nektar", - "Nenner", - "Neptun", - "Nerz", - "Nessel", - "Nestbau", - "Netz", - "Neubau", - "Neuerung", - "Neugier", - "nicken", - "Niere", - "Nilpferd", - "nisten", - "Nocke", - "Nomade", - "Nordmeer", - "Notdurft", - "Notstand", - "Notwehr", - "Nudismus", - "Nuss", - "Nutzhanf", - "Oase", - "Obdach", - "Oberarzt", - "Objekt", - "Oboe", - "Obsthain", - "Ochse", - "Odyssee", - "Ofenholz", - "öffnen", - "Ohnmacht", - "Ohrfeige", - "Ohrwurm", - "Ökologie", - "Oktave", - "Ölberg", - "Olive", - "Ölkrise", - "Omelett", - "Onkel", - "Oper", - "Optiker", - "Orange", - "Orchidee", - "ordnen", - "Orgasmus", - "Orkan", - "Ortskern", - "Ortung", - "Ostasien", - "Ozean", - "Paarlauf", - "Packeis", - "paddeln", - "Paket", - "Palast", - "Pandabär", - "Panik", - "Panorama", - "Panther", - "Papagei", - "Papier", - "Paprika", - "Paradies", - "Parka", - "Parodie", - "Partner", - "Passant", - "Patent", - "Patzer", - "Pause", - "Pavian", - "Pedal", - "Pegel", - "peilen", - "Perle", - "Person", - "Pfad", - "Pfau", - "Pferd", - "Pfleger", - "Physik", - "Pier", - "Pilotwal", - "Pinzette", - "Piste", - "Plakat", - "Plankton", - "Platin", - "Plombe", - "plündern", - "Pobacke", - "Pokal", - "polieren", - "Popmusik", - "Porträt", - "Posaune", - "Postamt", - "Pottwal", - "Pracht", - "Pranke", - "Preis", - "Primat", - "Prinzip", - "Protest", - "Proviant", - "Prüfung", - "Pubertät", - "Pudding", - "Pullover", - "Pulsader", - "Punkt", - "Pute", - "Putsch", - "Puzzle", - "Python", - "quaken", - "Qualle", - "Quark", - "Quellsee", - "Querkopf", - "Quitte", - "Quote", - "Rabauke", - "Rache", - "Radclub", - "Radhose", - "Radio", - "Radtour", - "Rahmen", - "Rampe", - "Randlage", - "Ranzen", - "Rapsöl", - "Raserei", - "rasten", - "Rasur", - "Rätsel", - "Raubtier", - "Raumzeit", - "Rausch", - "Reaktor", - "Realität", - "Rebell", - "Rede", - "Reetdach", - "Regatta", - "Regen", - "Rehkitz", - "Reifen", - "Reim", - "Reise", - "Reizung", - "Rekord", - "Relevanz", - "Rennboot", - "Respekt", - "Restmüll", - "retten", - "Reue", - "Revolte", - "Rhetorik", - "Rhythmus", - "Richtung", - "Riegel", - "Rindvieh", - "Rippchen", - "Ritter", - "Robbe", - "Roboter", - "Rockband", - "Rohdaten", - "Roller", - "Roman", - "röntgen", - "Rose", - "Rosskur", - "Rost", - "Rotahorn", - "Rotglut", - "Rotznase", - "Rubrik", - "Rückweg", - "Rufmord", - "Ruhe", - "Ruine", - "Rumpf", - "Runde", - "Rüstung", - "rütteln", - "Saaltür", - "Saatguts", - "Säbel", - "Sachbuch", - "Sack", - "Saft", - "sagen", - "Sahneeis", - "Salat", - "Salbe", - "Salz", - "Sammlung", - "Samt", - "Sandbank", - "Sanftmut", - "Sardine", - "Satire", - "Sattel", - "Satzbau", - "Sauerei", - "Saum", - "Säure", - "Schall", - "Scheitel", - "Schiff", - "Schlager", - "Schmied", - "Schnee", - "Scholle", - "Schrank", - "Schulbus", - "Schwan", - "Seeadler", - "Seefahrt", - "Seehund", - "Seeufer", - "segeln", - "Sehnerv", - "Seide", - "Seilzug", - "Senf", - "Sessel", - "Seufzer", - "Sexgott", - "Sichtung", - "Signal", - "Silber", - "singen", - "Sinn", - "Sirup", - "Sitzbank", - "Skandal", - "Skikurs", - "Skipper", - "Skizze", - "Smaragd", - "Socke", - "Sohn", - "Sommer", - "Songtext", - "Sorte", - "Spagat", - "Spannung", - "Spargel", - "Specht", - "Speiseöl", - "Spiegel", - "Sport", - "spülen", - "Stadtbus", - "Stall", - "Stärke", - "Stativ", - "staunen", - "Stern", - "Stiftung", - "Stollen", - "Strömung", - "Sturm", - "Substanz", - "Südalpen", - "Sumpf", - "surfen", - "Tabak", - "Tafel", - "Tagebau", - "takeln", - "Taktung", - "Talsohle", - "Tand", - "Tanzbär", - "Tapir", - "Tarantel", - "Tarnname", - "Tasse", - "Tatnacht", - "Tatsache", - "Tatze", - "Taube", - "tauchen", - "Taufpate", - "Taumel", - "Teelicht", - "Teich", - "teilen", - "Tempo", - "Tenor", - "Terrasse", - "Testflug", - "Theater", - "Thermik", - "ticken", - "Tiefflug", - "Tierart", - "Tigerhai", - "Tinte", - "Tischler", - "toben", - "Toleranz", - "Tölpel", - "Tonband", - "Topf", - "Topmodel", - "Torbogen", - "Torlinie", - "Torte", - "Tourist", - "Tragesel", - "trampeln", - "Trapez", - "Traum", - "treffen", - "Trennung", - "Treue", - "Trick", - "trimmen", - "Trödel", - "Trost", - "Trumpf", - "tüfteln", - "Turban", - "Turm", - "Übermut", - "Ufer", - "Uhrwerk", - "umarmen", - "Umbau", - "Umfeld", - "Umgang", - "Umsturz", - "Unart", - "Unfug", - "Unimog", - "Unruhe", - "Unwucht", - "Uranerz", - "Urlaub", - "Urmensch", - "Utopie", - "Vakuum", - "Valuta", - "Vandale", - "Vase", - "Vektor", - "Ventil", - "Verb", - "Verdeck", - "Verfall", - "Vergaser", - "verhexen", - "Verlag", - "Vers", - "Vesper", - "Vieh", - "Viereck", - "Vinyl", - "Virus", - "Vitrine", - "Vollblut", - "Vorbote", - "Vorrat", - "Vorsicht", - "Vulkan", - "Wachstum", - "Wade", - "Wagemut", - "Wahlen", - "Wahrheit", - "Wald", - "Walhai", - "Wallach", - "Walnuss", - "Walzer", - "wandeln", - "Wanze", - "wärmen", - "Warnruf", - "Wäsche", - "Wasser", - "Weberei", - "wechseln", - "Wegegeld", - "wehren", - "Weiher", - "Weinglas", - "Weißbier", - "Weitwurf", - "Welle", - "Weltall", - "Werkbank", - "Werwolf", - "Wetter", - "wiehern", - "Wildgans", - "Wind", - "Wohl", - "Wohnort", - "Wolf", - "Wollust", - "Wortlaut", - "Wrack", - "Wunder", - "Wurfaxt", - "Wurst", - "Yacht", - "Yeti", - "Zacke", - "Zahl", - "zähmen", - "Zahnfee", - "Zäpfchen", - "Zaster", - "Zaumzeug", - "Zebra", - "zeigen", - "Zeitlupe", - "Zellkern", - "Zeltdach", - "Zensor", - "Zerfall", - "Zeug", - "Ziege", - "Zielfoto", - "Zimteis", - "Zobel", - "Zollhund", - "Zombie", - "Zöpfe", - "Zucht", - "Zufahrt", - "Zugfahrt", - "Zugvogel", - "Zündung", - "Zweck", - "Zyklop" - ]; -} \ No newline at end of file diff --git a/cw_haven/lib/mnemonics/italian.dart b/cw_haven/lib/mnemonics/italian.dart deleted file mode 100644 index 275f85bf4..000000000 --- a/cw_haven/lib/mnemonics/italian.dart +++ /dev/null @@ -1,1630 +0,0 @@ -class ItalianMnemonics { - static const words = [ - "abbinare", - "abbonato", - "abisso", - "abitare", - "abominio", - "accadere", - "accesso", - "acciaio", - "accordo", - "accumulo", - "acido", - "acqua", - "acrobata", - "acustico", - "adattare", - "addetto", - "addio", - "addome", - "adeguato", - "aderire", - "adorare", - "adottare", - "adozione", - "adulto", - "aereo", - "aerobica", - "affare", - "affetto", - "affidare", - "affogato", - "affronto", - "africano", - "afrodite", - "agenzia", - "aggancio", - "aggeggio", - "aggiunta", - "agio", - "agire", - "agitare", - "aglio", - "agnello", - "agosto", - "aiutare", - "albero", - "albo", - "alce", - "alchimia", - "alcool", - "alfabeto", - "algebra", - "alimento", - "allarme", - "alleanza", - "allievo", - "alloggio", - "alluce", - "alpi", - "alterare", - "altro", - "aluminio", - "amante", - "amarezza", - "ambiente", - "ambrosia", - "america", - "amico", - "ammalare", - "ammirare", - "amnesia", - "amnistia", - "amore", - "ampliare", - "amputare", - "analisi", - "anamnesi", - "ananas", - "anarchia", - "anatra", - "anca", - "ancorato", - "andare", - "androide", - "aneddoto", - "anello", - "angelo", - "angolino", - "anguilla", - "anidride", - "anima", - "annegare", - "anno", - "annuncio", - "anomalia", - "antenna", - "anticipo", - "aperto", - "apostolo", - "appalto", - "appello", - "appiglio", - "applauso", - "appoggio", - "appurare", - "aprile", - "aquila", - "arabo", - "arachidi", - "aragosta", - "arancia", - "arbitrio", - "archivio", - "arco", - "argento", - "argilla", - "aria", - "ariete", - "arma", - "armonia", - "aroma", - "arrivare", - "arrosto", - "arsenale", - "arte", - "artiglio", - "asfalto", - "asfissia", - "asino", - "asparagi", - "aspirina", - "assalire", - "assegno", - "assolto", - "assurdo", - "asta", - "astratto", - "atlante", - "atletica", - "atomo", - "atropina", - "attacco", - "attesa", - "attico", - "atto", - "attrarre", - "auguri", - "aula", - "aumento", - "aurora", - "auspicio", - "autista", - "auto", - "autunno", - "avanzare", - "avarizia", - "avere", - "aviatore", - "avido", - "avorio", - "avvenire", - "avviso", - "avvocato", - "azienda", - "azione", - "azzardo", - "azzurro", - "babbuino", - "bacio", - "badante", - "baffi", - "bagaglio", - "bagliore", - "bagno", - "balcone", - "balena", - "ballare", - "balordo", - "balsamo", - "bambola", - "bancomat", - "banda", - "barato", - "barba", - "barista", - "barriera", - "basette", - "basilico", - "bassista", - "bastare", - "battello", - "bavaglio", - "beccare", - "beduino", - "bellezza", - "bene", - "benzina", - "berretto", - "bestia", - "bevitore", - "bianco", - "bibbia", - "biberon", - "bibita", - "bici", - "bidone", - "bilancia", - "biliardo", - "binario", - "binocolo", - "biologia", - "biondina", - "biopsia", - "biossido", - "birbante", - "birra", - "biscotto", - "bisogno", - "bistecca", - "bivio", - "blindare", - "bloccare", - "bocca", - "bollire", - "bombola", - "bonifico", - "borghese", - "borsa", - "bottino", - "botulino", - "braccio", - "bradipo", - "branco", - "bravo", - "bresaola", - "bretelle", - "brevetto", - "briciola", - "brigante", - "brillare", - "brindare", - "brivido", - "broccoli", - "brontolo", - "bruciare", - "brufolo", - "bucare", - "buddista", - "budino", - "bufera", - "buffo", - "bugiardo", - "buio", - "buono", - "burrone", - "bussola", - "bustina", - "buttare", - "cabernet", - "cabina", - "cacao", - "cacciare", - "cactus", - "cadavere", - "caffe", - "calamari", - "calcio", - "caldaia", - "calmare", - "calunnia", - "calvario", - "calzone", - "cambiare", - "camera", - "camion", - "cammello", - "campana", - "canarino", - "cancello", - "candore", - "cane", - "canguro", - "cannone", - "canoa", - "cantare", - "canzone", - "caos", - "capanna", - "capello", - "capire", - "capo", - "capperi", - "capra", - "capsula", - "caraffa", - "carbone", - "carciofo", - "cardigan", - "carenza", - "caricare", - "carota", - "carrello", - "carta", - "casa", - "cascare", - "caserma", - "cashmere", - "casino", - "cassetta", - "castello", - "catalogo", - "catena", - "catorcio", - "cattivo", - "causa", - "cauzione", - "cavallo", - "caverna", - "caviglia", - "cavo", - "cazzotto", - "celibato", - "cemento", - "cenare", - "centrale", - "ceramica", - "cercare", - "ceretta", - "cerniera", - "certezza", - "cervello", - "cessione", - "cestino", - "cetriolo", - "chiave", - "chiedere", - "chilo", - "chimera", - "chiodo", - "chirurgo", - "chitarra", - "chiudere", - "ciabatta", - "ciao", - "cibo", - "ciccia", - "cicerone", - "ciclone", - "cicogna", - "cielo", - "cifra", - "cigno", - "ciliegia", - "cimitero", - "cinema", - "cinque", - "cintura", - "ciondolo", - "ciotola", - "cipolla", - "cippato", - "circuito", - "cisterna", - "citofono", - "ciuccio", - "civetta", - "civico", - "clausola", - "cliente", - "clima", - "clinica", - "cobra", - "coccole", - "cocktail", - "cocomero", - "codice", - "coesione", - "cogliere", - "cognome", - "colla", - "colomba", - "colpire", - "coltello", - "comando", - "comitato", - "commedia", - "comodino", - "compagna", - "comune", - "concerto", - "condotto", - "conforto", - "congiura", - "coniglio", - "consegna", - "conto", - "convegno", - "coperta", - "copia", - "coprire", - "corazza", - "corda", - "corleone", - "cornice", - "corona", - "corpo", - "corrente", - "corsa", - "cortesia", - "corvo", - "coso", - "costume", - "cotone", - "cottura", - "cozza", - "crampo", - "cratere", - "cravatta", - "creare", - "credere", - "crema", - "crescere", - "crimine", - "criterio", - "croce", - "crollare", - "cronaca", - "crostata", - "croupier", - "cubetto", - "cucciolo", - "cucina", - "cultura", - "cuoco", - "cuore", - "cupido", - "cupola", - "cura", - "curva", - "cuscino", - "custode", - "danzare", - "data", - "decennio", - "decidere", - "decollo", - "dedicare", - "dedurre", - "definire", - "delegare", - "delfino", - "delitto", - "demone", - "dentista", - "denuncia", - "deposito", - "derivare", - "deserto", - "designer", - "destino", - "detonare", - "dettagli", - "diagnosi", - "dialogo", - "diamante", - "diario", - "diavolo", - "dicembre", - "difesa", - "digerire", - "digitare", - "diluvio", - "dinamica", - "dipinto", - "diploma", - "diramare", - "dire", - "dirigere", - "dirupo", - "discesa", - "disdetta", - "disegno", - "disporre", - "dissenso", - "distacco", - "dito", - "ditta", - "diva", - "divenire", - "dividere", - "divorare", - "docente", - "dolcetto", - "dolore", - "domatore", - "domenica", - "dominare", - "donatore", - "donna", - "dorato", - "dormire", - "dorso", - "dosaggio", - "dottore", - "dovere", - "download", - "dragone", - "dramma", - "dubbio", - "dubitare", - "duetto", - "durata", - "ebbrezza", - "eccesso", - "eccitare", - "eclissi", - "economia", - "edera", - "edificio", - "editore", - "edizione", - "educare", - "effetto", - "egitto", - "egiziano", - "elastico", - "elefante", - "eleggere", - "elemento", - "elenco", - "elezione", - "elmetto", - "elogio", - "embrione", - "emergere", - "emettere", - "eminenza", - "emisfero", - "emozione", - "empatia", - "energia", - "enfasi", - "enigma", - "entrare", - "enzima", - "epidemia", - "epilogo", - "episodio", - "epoca", - "equivoco", - "erba", - "erede", - "eroe", - "erotico", - "errore", - "eruzione", - "esaltare", - "esame", - "esaudire", - "eseguire", - "esempio", - "esigere", - "esistere", - "esito", - "esperto", - "espresso", - "essere", - "estasi", - "esterno", - "estrarre", - "eterno", - "etica", - "euforico", - "europa", - "evacuare", - "evasione", - "evento", - "evidenza", - "evitare", - "evolvere", - "fabbrica", - "facciata", - "fagiano", - "fagotto", - "falco", - "fame", - "famiglia", - "fanale", - "fango", - "fantasia", - "farfalla", - "farmacia", - "faro", - "fase", - "fastidio", - "faticare", - "fatto", - "favola", - "febbre", - "femmina", - "femore", - "fenomeno", - "fermata", - "feromoni", - "ferrari", - "fessura", - "festa", - "fiaba", - "fiamma", - "fianco", - "fiat", - "fibbia", - "fidare", - "fieno", - "figa", - "figlio", - "figura", - "filetto", - "filmato", - "filosofo", - "filtrare", - "finanza", - "finestra", - "fingere", - "finire", - "finta", - "finzione", - "fiocco", - "fioraio", - "firewall", - "firmare", - "fisico", - "fissare", - "fittizio", - "fiume", - "flacone", - "flagello", - "flirtare", - "flusso", - "focaccia", - "foglio", - "fognario", - "follia", - "fonderia", - "fontana", - "forbici", - "forcella", - "foresta", - "forgiare", - "formare", - "fornace", - "foro", - "fortuna", - "forzare", - "fosforo", - "fotoni", - "fracasso", - "fragola", - "frantumi", - "fratello", - "frazione", - "freccia", - "freddo", - "frenare", - "fresco", - "friggere", - "frittata", - "frivolo", - "frizione", - "fronte", - "frullato", - "frumento", - "frusta", - "frutto", - "fucile", - "fuggire", - "fulmine", - "fumare", - "funzione", - "fuoco", - "furbizia", - "furgone", - "furia", - "furore", - "fusibile", - "fuso", - "futuro", - "gabbiano", - "galassia", - "gallina", - "gamba", - "gancio", - "garanzia", - "garofano", - "gasolio", - "gatto", - "gazebo", - "gazzetta", - "gelato", - "gemelli", - "generare", - "genitori", - "gennaio", - "geologia", - "germania", - "gestire", - "gettare", - "ghepardo", - "ghiaccio", - "giaccone", - "giaguaro", - "giallo", - "giappone", - "giardino", - "gigante", - "gioco", - "gioiello", - "giorno", - "giovane", - "giraffa", - "giudizio", - "giurare", - "giusto", - "globo", - "gloria", - "glucosio", - "gnocca", - "gocciola", - "godere", - "gomito", - "gomma", - "gonfiare", - "gorilla", - "governo", - "gradire", - "graffiti", - "granchio", - "grappolo", - "grasso", - "grattare", - "gridare", - "grissino", - "grondaia", - "grugnito", - "gruppo", - "guadagno", - "guaio", - "guancia", - "guardare", - "gufo", - "guidare", - "guscio", - "gusto", - "icona", - "idea", - "identico", - "idolo", - "idoneo", - "idrante", - "idrogeno", - "igiene", - "ignoto", - "imbarco", - "immagine", - "immobile", - "imparare", - "impedire", - "impianto", - "importo", - "impresa", - "impulso", - "incanto", - "incendio", - "incidere", - "incontro", - "incrocia", - "incubo", - "indagare", - "indice", - "indotto", - "infanzia", - "inferno", - "infinito", - "infranto", - "ingerire", - "inglese", - "ingoiare", - "ingresso", - "iniziare", - "innesco", - "insalata", - "inserire", - "insicuro", - "insonnia", - "insulto", - "interno", - "introiti", - "invasori", - "inverno", - "invito", - "invocare", - "ipnosi", - "ipocrita", - "ipotesi", - "ironia", - "irrigare", - "iscritto", - "isola", - "ispirare", - "isterico", - "istinto", - "istruire", - "italiano", - "jazz", - "labbra", - "labrador", - "ladro", - "lago", - "lamento", - "lampone", - "lancetta", - "lanterna", - "lapide", - "larva", - "lasagne", - "lasciare", - "lastra", - "latte", - "laurea", - "lavagna", - "lavorare", - "leccare", - "legare", - "leggere", - "lenzuolo", - "leone", - "lepre", - "letargo", - "lettera", - "levare", - "levitare", - "lezione", - "liberare", - "libidine", - "libro", - "licenza", - "lievito", - "limite", - "lince", - "lingua", - "liquore", - "lire", - "listino", - "litigare", - "litro", - "locale", - "lottare", - "lucciola", - "lucidare", - "luglio", - "luna", - "macchina", - "madama", - "madre", - "maestro", - "maggio", - "magico", - "maglione", - "magnolia", - "mago", - "maialino", - "maionese", - "malattia", - "male", - "malloppo", - "mancare", - "mandorla", - "mangiare", - "manico", - "manopola", - "mansarda", - "mantello", - "manubrio", - "manzo", - "mappa", - "mare", - "margine", - "marinaio", - "marmotta", - "marocco", - "martello", - "marzo", - "maschera", - "matrice", - "maturare", - "mazzetta", - "meandri", - "medaglia", - "medico", - "medusa", - "megafono", - "melone", - "membrana", - "menta", - "mercato", - "meritare", - "merluzzo", - "mese", - "mestiere", - "metafora", - "meteo", - "metodo", - "mettere", - "miele", - "miglio", - "miliardo", - "mimetica", - "minatore", - "minuto", - "miracolo", - "mirtillo", - "missile", - "mistero", - "misura", - "mito", - "mobile", - "moda", - "moderare", - "moglie", - "molecola", - "molle", - "momento", - "moneta", - "mongolia", - "monologo", - "montagna", - "morale", - "morbillo", - "mordere", - "mosaico", - "mosca", - "mostro", - "motivare", - "moto", - "mulino", - "mulo", - "muovere", - "muraglia", - "muscolo", - "museo", - "musica", - "mutande", - "nascere", - "nastro", - "natale", - "natura", - "nave", - "navigare", - "negare", - "negozio", - "nemico", - "nero", - "nervo", - "nessuno", - "nettare", - "neutroni", - "neve", - "nevicare", - "nicotina", - "nido", - "nipote", - "nocciola", - "noleggio", - "nome", - "nonno", - "norvegia", - "notare", - "notizia", - "nove", - "nucleo", - "nuda", - "nuotare", - "nutrire", - "obbligo", - "occhio", - "occupare", - "oceano", - "odissea", - "odore", - "offerta", - "officina", - "offrire", - "oggetto", - "oggi", - "olfatto", - "olio", - "oliva", - "ombelico", - "ombrello", - "omuncolo", - "ondata", - "onore", - "opera", - "opinione", - "opuscolo", - "opzione", - "orario", - "orbita", - "orchidea", - "ordine", - "orecchio", - "orgasmo", - "orgoglio", - "origine", - "orologio", - "oroscopo", - "orso", - "oscurare", - "ospedale", - "ospite", - "ossigeno", - "ostacolo", - "ostriche", - "ottenere", - "ottimo", - "ottobre", - "ovest", - "pacco", - "pace", - "pacifico", - "padella", - "pagare", - "pagina", - "pagnotta", - "palazzo", - "palestra", - "palpebre", - "pancetta", - "panfilo", - "panino", - "pannello", - "panorama", - "papa", - "paperino", - "paradiso", - "parcella", - "parente", - "parlare", - "parodia", - "parrucca", - "partire", - "passare", - "pasta", - "patata", - "patente", - "patogeno", - "patriota", - "pausa", - "pazienza", - "peccare", - "pecora", - "pedalare", - "pelare", - "pena", - "pendenza", - "penisola", - "pennello", - "pensare", - "pentirsi", - "percorso", - "perdono", - "perfetto", - "perizoma", - "perla", - "permesso", - "persona", - "pesare", - "pesce", - "peso", - "petardo", - "petrolio", - "pezzo", - "piacere", - "pianeta", - "piastra", - "piatto", - "piazza", - "piccolo", - "piede", - "piegare", - "pietra", - "pigiama", - "pigliare", - "pigrizia", - "pilastro", - "pilota", - "pinguino", - "pioggia", - "piombo", - "pionieri", - "piovra", - "pipa", - "pirata", - "pirolisi", - "piscina", - "pisolino", - "pista", - "pitone", - "piumino", - "pizza", - "plastica", - "platino", - "poesia", - "poiana", - "polaroid", - "polenta", - "polimero", - "pollo", - "polmone", - "polpetta", - "poltrona", - "pomodoro", - "pompa", - "popolo", - "porco", - "porta", - "porzione", - "possesso", - "postino", - "potassio", - "potere", - "poverino", - "pranzo", - "prato", - "prefisso", - "prelievo", - "premio", - "prendere", - "prestare", - "pretesa", - "prezzo", - "primario", - "privacy", - "problema", - "processo", - "prodotto", - "profeta", - "progetto", - "promessa", - "pronto", - "proposta", - "proroga", - "prossimo", - "proteina", - "prova", - "prudenza", - "pubblico", - "pudore", - "pugilato", - "pulire", - "pulsante", - "puntare", - "pupazzo", - "puzzle", - "quaderno", - "qualcuno", - "quarzo", - "quercia", - "quintale", - "rabbia", - "racconto", - "radice", - "raffica", - "ragazza", - "ragione", - "rammento", - "ramo", - "rana", - "randagio", - "rapace", - "rapinare", - "rapporto", - "rasatura", - "ravioli", - "reagire", - "realista", - "reattore", - "reazione", - "recitare", - "recluso", - "record", - "recupero", - "redigere", - "regalare", - "regina", - "regola", - "relatore", - "reliquia", - "remare", - "rendere", - "reparto", - "resina", - "resto", - "rete", - "retorica", - "rettile", - "revocare", - "riaprire", - "ribadire", - "ribelle", - "ricambio", - "ricetta", - "richiamo", - "ricordo", - "ridurre", - "riempire", - "riferire", - "riflesso", - "righello", - "rilancio", - "rilevare", - "rilievo", - "rimanere", - "rimborso", - "rinforzo", - "rinuncia", - "riparo", - "ripetere", - "riposare", - "ripulire", - "risalita", - "riscatto", - "riserva", - "riso", - "rispetto", - "ritaglio", - "ritmo", - "ritorno", - "ritratto", - "rituale", - "riunione", - "riuscire", - "riva", - "robotica", - "rondine", - "rosa", - "rospo", - "rosso", - "rotonda", - "rotta", - "roulotte", - "rubare", - "rubrica", - "ruffiano", - "rumore", - "ruota", - "ruscello", - "sabbia", - "sacco", - "saggio", - "sale", - "salire", - "salmone", - "salto", - "salutare", - "salvia", - "sangue", - "sanzioni", - "sapere", - "sapienza", - "sarcasmo", - "sardine", - "sartoria", - "sbalzo", - "sbarcare", - "sberla", - "sborsare", - "scadenza", - "scafo", - "scala", - "scambio", - "scappare", - "scarpa", - "scatola", - "scelta", - "scena", - "sceriffo", - "scheggia", - "schiuma", - "sciarpa", - "scienza", - "scimmia", - "sciopero", - "scivolo", - "sclerare", - "scolpire", - "sconto", - "scopa", - "scordare", - "scossa", - "scrivere", - "scrupolo", - "scuderia", - "scultore", - "scuola", - "scusare", - "sdraiare", - "secolo", - "sedativo", - "sedere", - "sedia", - "segare", - "segreto", - "seguire", - "semaforo", - "seme", - "senape", - "seno", - "sentiero", - "separare", - "sepolcro", - "sequenza", - "serata", - "serpente", - "servizio", - "sesso", - "seta", - "settore", - "sfamare", - "sfera", - "sfidare", - "sfiorare", - "sfogare", - "sgabello", - "sicuro", - "siepe", - "sigaro", - "silenzio", - "silicone", - "simbiosi", - "simpatia", - "simulare", - "sinapsi", - "sindrome", - "sinergia", - "sinonimo", - "sintonia", - "sirena", - "siringa", - "sistema", - "sito", - "smalto", - "smentire", - "smontare", - "soccorso", - "socio", - "soffitto", - "software", - "soggetto", - "sogliola", - "sognare", - "soldi", - "sole", - "sollievo", - "solo", - "sommario", - "sondare", - "sonno", - "sorpresa", - "sorriso", - "sospiro", - "sostegno", - "sovrano", - "spaccare", - "spada", - "spagnolo", - "spalla", - "sparire", - "spavento", - "spazio", - "specchio", - "spedire", - "spegnere", - "spendere", - "speranza", - "spessore", - "spezzare", - "spiaggia", - "spiccare", - "spiegare", - "spiffero", - "spingere", - "sponda", - "sporcare", - "spostare", - "spremuta", - "spugna", - "spumante", - "spuntare", - "squadra", - "squillo", - "staccare", - "stadio", - "stagione", - "stallone", - "stampa", - "stancare", - "starnuto", - "statura", - "stella", - "stendere", - "sterzo", - "stilista", - "stimolo", - "stinco", - "stiva", - "stoffa", - "storia", - "strada", - "stregone", - "striscia", - "studiare", - "stufa", - "stupendo", - "subire", - "successo", - "sudare", - "suono", - "superare", - "supporto", - "surfista", - "sussurro", - "svelto", - "svenire", - "sviluppo", - "svolta", - "svuotare", - "tabacco", - "tabella", - "tabu", - "tacchino", - "tacere", - "taglio", - "talento", - "tangente", - "tappeto", - "tartufo", - "tassello", - "tastiera", - "tavolo", - "tazza", - "teatro", - "tedesco", - "telaio", - "telefono", - "tema", - "temere", - "tempo", - "tendenza", - "tenebre", - "tensione", - "tentare", - "teologia", - "teorema", - "termica", - "terrazzo", - "teschio", - "tesi", - "tesoro", - "tessera", - "testa", - "thriller", - "tifoso", - "tigre", - "timbrare", - "timido", - "tinta", - "tirare", - "tisana", - "titano", - "titolo", - "toccare", - "togliere", - "topolino", - "torcia", - "torrente", - "tovaglia", - "traffico", - "tragitto", - "training", - "tramonto", - "transito", - "trapezio", - "trasloco", - "trattore", - "trazione", - "treccia", - "tregua", - "treno", - "triciclo", - "tridente", - "trilogia", - "tromba", - "troncare", - "trota", - "trovare", - "trucco", - "tubo", - "tulipano", - "tumulto", - "tunisia", - "tuono", - "turista", - "tuta", - "tutelare", - "tutore", - "ubriaco", - "uccello", - "udienza", - "udito", - "uffa", - "umanoide", - "umore", - "unghia", - "unguento", - "unicorno", - "unione", - "universo", - "uomo", - "uragano", - "uranio", - "urlare", - "uscire", - "utente", - "utilizzo", - "vacanza", - "vacca", - "vaglio", - "vagonata", - "valle", - "valore", - "valutare", - "valvola", - "vampiro", - "vaniglia", - "vanto", - "vapore", - "variante", - "vasca", - "vaselina", - "vassoio", - "vedere", - "vegetale", - "veglia", - "veicolo", - "vela", - "veleno", - "velivolo", - "velluto", - "vendere", - "venerare", - "venire", - "vento", - "veranda", - "verbo", - "verdura", - "vergine", - "verifica", - "vernice", - "vero", - "verruca", - "versare", - "vertebra", - "vescica", - "vespaio", - "vestito", - "vesuvio", - "veterano", - "vetro", - "vetta", - "viadotto", - "viaggio", - "vibrare", - "vicenda", - "vichingo", - "vietare", - "vigilare", - "vigneto", - "villa", - "vincere", - "violino", - "vipera", - "virgola", - "virtuoso", - "visita", - "vita", - "vitello", - "vittima", - "vivavoce", - "vivere", - "viziato", - "voglia", - "volare", - "volpe", - "volto", - "volume", - "vongole", - "voragine", - "vortice", - "votare", - "vulcano", - "vuotare", - "zabaione", - "zaffiro", - "zainetto", - "zampa", - "zanzara", - "zattera", - "zavorra", - "zenzero", - "zero", - "zingaro", - "zittire", - "zoccolo", - "zolfo", - "zombie", - "zucchero" - ]; -} \ No newline at end of file diff --git a/cw_haven/lib/mnemonics/japanese.dart b/cw_haven/lib/mnemonics/japanese.dart deleted file mode 100644 index 5d17fdb14..000000000 --- a/cw_haven/lib/mnemonics/japanese.dart +++ /dev/null @@ -1,1630 +0,0 @@ -class JapaneseMnemonics { - static const words = [ - "あいこくしん", - "あいさつ", - "あいだ", - "あおぞら", - "あかちゃん", - "あきる", - "あけがた", - "あける", - "あこがれる", - "あさい", - "あさひ", - "あしあと", - "あじわう", - "あずかる", - "あずき", - "あそぶ", - "あたえる", - "あたためる", - "あたりまえ", - "あたる", - "あつい", - "あつかう", - "あっしゅく", - "あつまり", - "あつめる", - "あてな", - "あてはまる", - "あひる", - "あぶら", - "あぶる", - "あふれる", - "あまい", - "あまど", - "あまやかす", - "あまり", - "あみもの", - "あめりか", - "あやまる", - "あゆむ", - "あらいぐま", - "あらし", - "あらすじ", - "あらためる", - "あらゆる", - "あらわす", - "ありがとう", - "あわせる", - "あわてる", - "あんい", - "あんがい", - "あんこ", - "あんぜん", - "あんてい", - "あんない", - "あんまり", - "いいだす", - "いおん", - "いがい", - "いがく", - "いきおい", - "いきなり", - "いきもの", - "いきる", - "いくじ", - "いくぶん", - "いけばな", - "いけん", - "いこう", - "いこく", - "いこつ", - "いさましい", - "いさん", - "いしき", - "いじゅう", - "いじょう", - "いじわる", - "いずみ", - "いずれ", - "いせい", - "いせえび", - "いせかい", - "いせき", - "いぜん", - "いそうろう", - "いそがしい", - "いだい", - "いだく", - "いたずら", - "いたみ", - "いたりあ", - "いちおう", - "いちじ", - "いちど", - "いちば", - "いちぶ", - "いちりゅう", - "いつか", - "いっしゅん", - "いっせい", - "いっそう", - "いったん", - "いっち", - "いってい", - "いっぽう", - "いてざ", - "いてん", - "いどう", - "いとこ", - "いない", - "いなか", - "いねむり", - "いのち", - "いのる", - "いはつ", - "いばる", - "いはん", - "いびき", - "いひん", - "いふく", - "いへん", - "いほう", - "いみん", - "いもうと", - "いもたれ", - "いもり", - "いやがる", - "いやす", - "いよかん", - "いよく", - "いらい", - "いらすと", - "いりぐち", - "いりょう", - "いれい", - "いれもの", - "いれる", - "いろえんぴつ", - "いわい", - "いわう", - "いわかん", - "いわば", - "いわゆる", - "いんげんまめ", - "いんさつ", - "いんしょう", - "いんよう", - "うえき", - "うえる", - "うおざ", - "うがい", - "うかぶ", - "うかべる", - "うきわ", - "うくらいな", - "うくれれ", - "うけたまわる", - "うけつけ", - "うけとる", - "うけもつ", - "うける", - "うごかす", - "うごく", - "うこん", - "うさぎ", - "うしなう", - "うしろがみ", - "うすい", - "うすぎ", - "うすぐらい", - "うすめる", - "うせつ", - "うちあわせ", - "うちがわ", - "うちき", - "うちゅう", - "うっかり", - "うつくしい", - "うったえる", - "うつる", - "うどん", - "うなぎ", - "うなじ", - "うなずく", - "うなる", - "うねる", - "うのう", - "うぶげ", - "うぶごえ", - "うまれる", - "うめる", - "うもう", - "うやまう", - "うよく", - "うらがえす", - "うらぐち", - "うらない", - "うりあげ", - "うりきれ", - "うるさい", - "うれしい", - "うれゆき", - "うれる", - "うろこ", - "うわき", - "うわさ", - "うんこう", - "うんちん", - "うんてん", - "うんどう", - "えいえん", - "えいが", - "えいきょう", - "えいご", - "えいせい", - "えいぶん", - "えいよう", - "えいわ", - "えおり", - "えがお", - "えがく", - "えきたい", - "えくせる", - "えしゃく", - "えすて", - "えつらん", - "えのぐ", - "えほうまき", - "えほん", - "えまき", - "えもじ", - "えもの", - "えらい", - "えらぶ", - "えりあ", - "えんえん", - "えんかい", - "えんぎ", - "えんげき", - "えんしゅう", - "えんぜつ", - "えんそく", - "えんちょう", - "えんとつ", - "おいかける", - "おいこす", - "おいしい", - "おいつく", - "おうえん", - "おうさま", - "おうじ", - "おうせつ", - "おうたい", - "おうふく", - "おうべい", - "おうよう", - "おえる", - "おおい", - "おおう", - "おおどおり", - "おおや", - "おおよそ", - "おかえり", - "おかず", - "おがむ", - "おかわり", - "おぎなう", - "おきる", - "おくさま", - "おくじょう", - "おくりがな", - "おくる", - "おくれる", - "おこす", - "おこなう", - "おこる", - "おさえる", - "おさない", - "おさめる", - "おしいれ", - "おしえる", - "おじぎ", - "おじさん", - "おしゃれ", - "おそらく", - "おそわる", - "おたがい", - "おたく", - "おだやか", - "おちつく", - "おっと", - "おつり", - "おでかけ", - "おとしもの", - "おとなしい", - "おどり", - "おどろかす", - "おばさん", - "おまいり", - "おめでとう", - "おもいで", - "おもう", - "おもたい", - "おもちゃ", - "おやつ", - "おやゆび", - "およぼす", - "おらんだ", - "おろす", - "おんがく", - "おんけい", - "おんしゃ", - "おんせん", - "おんだん", - "おんちゅう", - "おんどけい", - "かあつ", - "かいが", - "がいき", - "がいけん", - "がいこう", - "かいさつ", - "かいしゃ", - "かいすいよく", - "かいぜん", - "かいぞうど", - "かいつう", - "かいてん", - "かいとう", - "かいふく", - "がいへき", - "かいほう", - "かいよう", - "がいらい", - "かいわ", - "かえる", - "かおり", - "かかえる", - "かがく", - "かがし", - "かがみ", - "かくご", - "かくとく", - "かざる", - "がぞう", - "かたい", - "かたち", - "がちょう", - "がっきゅう", - "がっこう", - "がっさん", - "がっしょう", - "かなざわし", - "かのう", - "がはく", - "かぶか", - "かほう", - "かほご", - "かまう", - "かまぼこ", - "かめれおん", - "かゆい", - "かようび", - "からい", - "かるい", - "かろう", - "かわく", - "かわら", - "がんか", - "かんけい", - "かんこう", - "かんしゃ", - "かんそう", - "かんたん", - "かんち", - "がんばる", - "きあい", - "きあつ", - "きいろ", - "ぎいん", - "きうい", - "きうん", - "きえる", - "きおう", - "きおく", - "きおち", - "きおん", - "きかい", - "きかく", - "きかんしゃ", - "ききて", - "きくばり", - "きくらげ", - "きけんせい", - "きこう", - "きこえる", - "きこく", - "きさい", - "きさく", - "きさま", - "きさらぎ", - "ぎじかがく", - "ぎしき", - "ぎじたいけん", - "ぎじにってい", - "ぎじゅつしゃ", - "きすう", - "きせい", - "きせき", - "きせつ", - "きそう", - "きぞく", - "きぞん", - "きたえる", - "きちょう", - "きつえん", - "ぎっちり", - "きつつき", - "きつね", - "きてい", - "きどう", - "きどく", - "きない", - "きなが", - "きなこ", - "きぬごし", - "きねん", - "きのう", - "きのした", - "きはく", - "きびしい", - "きひん", - "きふく", - "きぶん", - "きぼう", - "きほん", - "きまる", - "きみつ", - "きむずかしい", - "きめる", - "きもだめし", - "きもち", - "きもの", - "きゃく", - "きやく", - "ぎゅうにく", - "きよう", - "きょうりゅう", - "きらい", - "きらく", - "きりん", - "きれい", - "きれつ", - "きろく", - "ぎろん", - "きわめる", - "ぎんいろ", - "きんかくじ", - "きんじょ", - "きんようび", - "ぐあい", - "くいず", - "くうかん", - "くうき", - "くうぐん", - "くうこう", - "ぐうせい", - "くうそう", - "ぐうたら", - "くうふく", - "くうぼ", - "くかん", - "くきょう", - "くげん", - "ぐこう", - "くさい", - "くさき", - "くさばな", - "くさる", - "くしゃみ", - "くしょう", - "くすのき", - "くすりゆび", - "くせげ", - "くせん", - "ぐたいてき", - "くださる", - "くたびれる", - "くちこみ", - "くちさき", - "くつした", - "ぐっすり", - "くつろぐ", - "くとうてん", - "くどく", - "くなん", - "くねくね", - "くのう", - "くふう", - "くみあわせ", - "くみたてる", - "くめる", - "くやくしょ", - "くらす", - "くらべる", - "くるま", - "くれる", - "くろう", - "くわしい", - "ぐんかん", - "ぐんしょく", - "ぐんたい", - "ぐんて", - "けあな", - "けいかく", - "けいけん", - "けいこ", - "けいさつ", - "げいじゅつ", - "けいたい", - "げいのうじん", - "けいれき", - "けいろ", - "けおとす", - "けおりもの", - "げきか", - "げきげん", - "げきだん", - "げきちん", - "げきとつ", - "げきは", - "げきやく", - "げこう", - "げこくじょう", - "げざい", - "けさき", - "げざん", - "けしき", - "けしごむ", - "けしょう", - "げすと", - "けたば", - "けちゃっぷ", - "けちらす", - "けつあつ", - "けつい", - "けつえき", - "けっこん", - "けつじょ", - "けっせき", - "けってい", - "けつまつ", - "げつようび", - "げつれい", - "けつろん", - "げどく", - "けとばす", - "けとる", - "けなげ", - "けなす", - "けなみ", - "けぬき", - "げねつ", - "けねん", - "けはい", - "げひん", - "けぶかい", - "げぼく", - "けまり", - "けみかる", - "けむし", - "けむり", - "けもの", - "けらい", - "けろけろ", - "けわしい", - "けんい", - "けんえつ", - "けんお", - "けんか", - "げんき", - "けんげん", - "けんこう", - "けんさく", - "けんしゅう", - "けんすう", - "げんそう", - "けんちく", - "けんてい", - "けんとう", - "けんない", - "けんにん", - "げんぶつ", - "けんま", - "けんみん", - "けんめい", - "けんらん", - "けんり", - "こあくま", - "こいぬ", - "こいびと", - "ごうい", - "こうえん", - "こうおん", - "こうかん", - "ごうきゅう", - "ごうけい", - "こうこう", - "こうさい", - "こうじ", - "こうすい", - "ごうせい", - "こうそく", - "こうたい", - "こうちゃ", - "こうつう", - "こうてい", - "こうどう", - "こうない", - "こうはい", - "ごうほう", - "ごうまん", - "こうもく", - "こうりつ", - "こえる", - "こおり", - "ごかい", - "ごがつ", - "ごかん", - "こくご", - "こくさい", - "こくとう", - "こくない", - "こくはく", - "こぐま", - "こけい", - "こける", - "ここのか", - "こころ", - "こさめ", - "こしつ", - "こすう", - "こせい", - "こせき", - "こぜん", - "こそだて", - "こたい", - "こたえる", - "こたつ", - "こちょう", - "こっか", - "こつこつ", - "こつばん", - "こつぶ", - "こてい", - "こてん", - "ことがら", - "ことし", - "ことば", - "ことり", - "こなごな", - "こねこね", - "このまま", - "このみ", - "このよ", - "ごはん", - "こひつじ", - "こふう", - "こふん", - "こぼれる", - "ごまあぶら", - "こまかい", - "ごますり", - "こまつな", - "こまる", - "こむぎこ", - "こもじ", - "こもち", - "こもの", - "こもん", - "こやく", - "こやま", - "こゆう", - "こゆび", - "こよい", - "こよう", - "こりる", - "これくしょん", - "ころっけ", - "こわもて", - "こわれる", - "こんいん", - "こんかい", - "こんき", - "こんしゅう", - "こんすい", - "こんだて", - "こんとん", - "こんなん", - "こんびに", - "こんぽん", - "こんまけ", - "こんや", - "こんれい", - "こんわく", - "ざいえき", - "さいかい", - "さいきん", - "ざいげん", - "ざいこ", - "さいしょ", - "さいせい", - "ざいたく", - "ざいちゅう", - "さいてき", - "ざいりょう", - "さうな", - "さかいし", - "さがす", - "さかな", - "さかみち", - "さがる", - "さぎょう", - "さくし", - "さくひん", - "さくら", - "さこく", - "さこつ", - "さずかる", - "ざせき", - "さたん", - "さつえい", - "ざつおん", - "ざっか", - "ざつがく", - "さっきょく", - "ざっし", - "さつじん", - "ざっそう", - "さつたば", - "さつまいも", - "さてい", - "さといも", - "さとう", - "さとおや", - "さとし", - "さとる", - "さのう", - "さばく", - "さびしい", - "さべつ", - "さほう", - "さほど", - "さます", - "さみしい", - "さみだれ", - "さむけ", - "さめる", - "さやえんどう", - "さゆう", - "さよう", - "さよく", - "さらだ", - "ざるそば", - "さわやか", - "さわる", - "さんいん", - "さんか", - "さんきゃく", - "さんこう", - "さんさい", - "ざんしょ", - "さんすう", - "さんせい", - "さんそ", - "さんち", - "さんま", - "さんみ", - "さんらん", - "しあい", - "しあげ", - "しあさって", - "しあわせ", - "しいく", - "しいん", - "しうち", - "しえい", - "しおけ", - "しかい", - "しかく", - "じかん", - "しごと", - "しすう", - "じだい", - "したうけ", - "したぎ", - "したて", - "したみ", - "しちょう", - "しちりん", - "しっかり", - "しつじ", - "しつもん", - "してい", - "してき", - "してつ", - "じてん", - "じどう", - "しなぎれ", - "しなもの", - "しなん", - "しねま", - "しねん", - "しのぐ", - "しのぶ", - "しはい", - "しばかり", - "しはつ", - "しはらい", - "しはん", - "しひょう", - "しふく", - "じぶん", - "しへい", - "しほう", - "しほん", - "しまう", - "しまる", - "しみん", - "しむける", - "じむしょ", - "しめい", - "しめる", - "しもん", - "しゃいん", - "しゃうん", - "しゃおん", - "じゃがいも", - "しやくしょ", - "しゃくほう", - "しゃけん", - "しゃこ", - "しゃざい", - "しゃしん", - "しゃせん", - "しゃそう", - "しゃたい", - "しゃちょう", - "しゃっきん", - "じゃま", - "しゃりん", - "しゃれい", - "じゆう", - "じゅうしょ", - "しゅくはく", - "じゅしん", - "しゅっせき", - "しゅみ", - "しゅらば", - "じゅんばん", - "しょうかい", - "しょくたく", - "しょっけん", - "しょどう", - "しょもつ", - "しらせる", - "しらべる", - "しんか", - "しんこう", - "じんじゃ", - "しんせいじ", - "しんちく", - "しんりん", - "すあげ", - "すあし", - "すあな", - "ずあん", - "すいえい", - "すいか", - "すいとう", - "ずいぶん", - "すいようび", - "すうがく", - "すうじつ", - "すうせん", - "すおどり", - "すきま", - "すくう", - "すくない", - "すける", - "すごい", - "すこし", - "ずさん", - "すずしい", - "すすむ", - "すすめる", - "すっかり", - "ずっしり", - "ずっと", - "すてき", - "すてる", - "すねる", - "すのこ", - "すはだ", - "すばらしい", - "ずひょう", - "ずぶぬれ", - "すぶり", - "すふれ", - "すべて", - "すべる", - "ずほう", - "すぼん", - "すまい", - "すめし", - "すもう", - "すやき", - "すらすら", - "するめ", - "すれちがう", - "すろっと", - "すわる", - "すんぜん", - "すんぽう", - "せあぶら", - "せいかつ", - "せいげん", - "せいじ", - "せいよう", - "せおう", - "せかいかん", - "せきにん", - "せきむ", - "せきゆ", - "せきらんうん", - "せけん", - "せこう", - "せすじ", - "せたい", - "せたけ", - "せっかく", - "せっきゃく", - "ぜっく", - "せっけん", - "せっこつ", - "せっさたくま", - "せつぞく", - "せつだん", - "せつでん", - "せっぱん", - "せつび", - "せつぶん", - "せつめい", - "せつりつ", - "せなか", - "せのび", - "せはば", - "せびろ", - "せぼね", - "せまい", - "せまる", - "せめる", - "せもたれ", - "せりふ", - "ぜんあく", - "せんい", - "せんえい", - "せんか", - "せんきょ", - "せんく", - "せんげん", - "ぜんご", - "せんさい", - "せんしゅ", - "せんすい", - "せんせい", - "せんぞ", - "せんたく", - "せんちょう", - "せんてい", - "せんとう", - "せんぬき", - "せんねん", - "せんぱい", - "ぜんぶ", - "ぜんぽう", - "せんむ", - "せんめんじょ", - "せんもん", - "せんやく", - "せんゆう", - "せんよう", - "ぜんら", - "ぜんりゃく", - "せんれい", - "せんろ", - "そあく", - "そいとげる", - "そいね", - "そうがんきょう", - "そうき", - "そうご", - "そうしん", - "そうだん", - "そうなん", - "そうび", - "そうめん", - "そうり", - "そえもの", - "そえん", - "そがい", - "そげき", - "そこう", - "そこそこ", - "そざい", - "そしな", - "そせい", - "そせん", - "そそぐ", - "そだてる", - "そつう", - "そつえん", - "そっかん", - "そつぎょう", - "そっけつ", - "そっこう", - "そっせん", - "そっと", - "そとがわ", - "そとづら", - "そなえる", - "そなた", - "そふぼ", - "そぼく", - "そぼろ", - "そまつ", - "そまる", - "そむく", - "そむりえ", - "そめる", - "そもそも", - "そよかぜ", - "そらまめ", - "そろう", - "そんかい", - "そんけい", - "そんざい", - "そんしつ", - "そんぞく", - "そんちょう", - "ぞんび", - "ぞんぶん", - "そんみん", - "たあい", - "たいいん", - "たいうん", - "たいえき", - "たいおう", - "だいがく", - "たいき", - "たいぐう", - "たいけん", - "たいこ", - "たいざい", - "だいじょうぶ", - "だいすき", - "たいせつ", - "たいそう", - "だいたい", - "たいちょう", - "たいてい", - "だいどころ", - "たいない", - "たいねつ", - "たいのう", - "たいはん", - "だいひょう", - "たいふう", - "たいへん", - "たいほ", - "たいまつばな", - "たいみんぐ", - "たいむ", - "たいめん", - "たいやき", - "たいよう", - "たいら", - "たいりょく", - "たいる", - "たいわん", - "たうえ", - "たえる", - "たおす", - "たおる", - "たおれる", - "たかい", - "たかね", - "たきび", - "たくさん", - "たこく", - "たこやき", - "たさい", - "たしざん", - "だじゃれ", - "たすける", - "たずさわる", - "たそがれ", - "たたかう", - "たたく", - "ただしい", - "たたみ", - "たちばな", - "だっかい", - "だっきゃく", - "だっこ", - "だっしゅつ", - "だったい", - "たてる", - "たとえる", - "たなばた", - "たにん", - "たぬき", - "たのしみ", - "たはつ", - "たぶん", - "たべる", - "たぼう", - "たまご", - "たまる", - "だむる", - "ためいき", - "ためす", - "ためる", - "たもつ", - "たやすい", - "たよる", - "たらす", - "たりきほんがん", - "たりょう", - "たりる", - "たると", - "たれる", - "たれんと", - "たろっと", - "たわむれる", - "だんあつ", - "たんい", - "たんおん", - "たんか", - "たんき", - "たんけん", - "たんご", - "たんさん", - "たんじょうび", - "だんせい", - "たんそく", - "たんたい", - "だんち", - "たんてい", - "たんとう", - "だんな", - "たんにん", - "だんねつ", - "たんのう", - "たんぴん", - "だんぼう", - "たんまつ", - "たんめい", - "だんれつ", - "だんろ", - "だんわ", - "ちあい", - "ちあん", - "ちいき", - "ちいさい", - "ちえん", - "ちかい", - "ちから", - "ちきゅう", - "ちきん", - "ちけいず", - "ちけん", - "ちこく", - "ちさい", - "ちしき", - "ちしりょう", - "ちせい", - "ちそう", - "ちたい", - "ちたん", - "ちちおや", - "ちつじょ", - "ちてき", - "ちてん", - "ちぬき", - "ちぬり", - "ちのう", - "ちひょう", - "ちへいせん", - "ちほう", - "ちまた", - "ちみつ", - "ちみどろ", - "ちめいど", - "ちゃんこなべ", - "ちゅうい", - "ちゆりょく", - "ちょうし", - "ちょさくけん", - "ちらし", - "ちらみ", - "ちりがみ", - "ちりょう", - "ちるど", - "ちわわ", - "ちんたい", - "ちんもく", - "ついか", - "ついたち", - "つうか", - "つうじょう", - "つうはん", - "つうわ", - "つかう", - "つかれる", - "つくね", - "つくる", - "つけね", - "つける", - "つごう", - "つたえる", - "つづく", - "つつじ", - "つつむ", - "つとめる", - "つながる", - "つなみ", - "つねづね", - "つのる", - "つぶす", - "つまらない", - "つまる", - "つみき", - "つめたい", - "つもり", - "つもる", - "つよい", - "つるぼ", - "つるみく", - "つわもの", - "つわり", - "てあし", - "てあて", - "てあみ", - "ていおん", - "ていか", - "ていき", - "ていけい", - "ていこく", - "ていさつ", - "ていし", - "ていせい", - "ていたい", - "ていど", - "ていねい", - "ていひょう", - "ていへん", - "ていぼう", - "てうち", - "ておくれ", - "てきとう", - "てくび", - "でこぼこ", - "てさぎょう", - "てさげ", - "てすり", - "てそう", - "てちがい", - "てちょう", - "てつがく", - "てつづき", - "でっぱ", - "てつぼう", - "てつや", - "でぬかえ", - "てぬき", - "てぬぐい", - "てのひら", - "てはい", - "てぶくろ", - "てふだ", - "てほどき", - "てほん", - "てまえ", - "てまきずし", - "てみじか", - "てみやげ", - "てらす", - "てれび", - "てわけ", - "てわたし", - "でんあつ", - "てんいん", - "てんかい", - "てんき", - "てんぐ", - "てんけん", - "てんごく", - "てんさい", - "てんし", - "てんすう", - "でんち", - "てんてき", - "てんとう", - "てんない", - "てんぷら", - "てんぼうだい", - "てんめつ", - "てんらんかい", - "でんりょく", - "でんわ", - "どあい", - "といれ", - "どうかん", - "とうきゅう", - "どうぐ", - "とうし", - "とうむぎ", - "とおい", - "とおか", - "とおく", - "とおす", - "とおる", - "とかい", - "とかす", - "ときおり", - "ときどき", - "とくい", - "とくしゅう", - "とくてん", - "とくに", - "とくべつ", - "とけい", - "とける", - "とこや", - "とさか", - "としょかん", - "とそう", - "とたん", - "とちゅう", - "とっきゅう", - "とっくん", - "とつぜん", - "とつにゅう", - "とどける", - "ととのえる", - "とない", - "となえる", - "となり", - "とのさま", - "とばす", - "どぶがわ", - "とほう", - "とまる", - "とめる", - "ともだち", - "ともる", - "どようび", - "とらえる", - "とんかつ", - "どんぶり", - "ないかく", - "ないこう", - "ないしょ", - "ないす", - "ないせん", - "ないそう", - "なおす", - "ながい", - "なくす", - "なげる", - "なこうど", - "なさけ", - "なたでここ", - "なっとう", - "なつやすみ", - "ななおし", - "なにごと", - "なにもの", - "なにわ", - "なのか", - "なふだ", - "なまいき", - "なまえ", - "なまみ", - "なみだ", - "なめらか", - "なめる", - "なやむ", - "ならう", - "ならび", - "ならぶ", - "なれる", - "なわとび", - "なわばり", - "にあう", - "にいがた", - "にうけ", - "におい", - "にかい", - "にがて", - "にきび", - "にくしみ", - "にくまん", - "にげる", - "にさんかたんそ", - "にしき", - "にせもの", - "にちじょう", - "にちようび", - "にっか", - "にっき", - "にっけい", - "にっこう", - "にっさん", - "にっしょく", - "にっすう", - "にっせき", - "にってい", - "になう", - "にほん", - "にまめ", - "にもつ", - "にやり", - "にゅういん", - "にりんしゃ", - "にわとり", - "にんい", - "にんか", - "にんき", - "にんげん", - "にんしき", - "にんずう", - "にんそう", - "にんたい", - "にんち", - "にんてい", - "にんにく", - "にんぷ", - "にんまり", - "にんむ", - "にんめい", - "にんよう", - "ぬいくぎ", - "ぬかす", - "ぬぐいとる", - "ぬぐう", - "ぬくもり", - "ぬすむ", - "ぬまえび", - "ぬめり", - "ぬらす", - "ぬんちゃく", - "ねあげ", - "ねいき", - "ねいる", - "ねいろ", - "ねぐせ", - "ねくたい", - "ねくら", - "ねこぜ", - "ねこむ", - "ねさげ", - "ねすごす", - "ねそべる", - "ねだん", - "ねつい", - "ねっしん", - "ねつぞう", - "ねったいぎょ", - "ねぶそく", - "ねふだ", - "ねぼう", - "ねほりはほり", - "ねまき", - "ねまわし", - "ねみみ", - "ねむい", - "ねむたい", - "ねもと", - "ねらう", - "ねわざ", - "ねんいり", - "ねんおし", - "ねんかん", - "ねんきん", - "ねんぐ", - "ねんざ", - "ねんし", - "ねんちゃく", - "ねんど", - "ねんぴ", - "ねんぶつ", - "ねんまつ", - "ねんりょう", - "ねんれい", - "のいず", - "のおづま", - "のがす", - "のきなみ", - "のこぎり", - "のこす", - "のこる", - "のせる", - "のぞく", - "のぞむ", - "のたまう", - "のちほど", - "のっく", - "のばす", - "のはら", - "のべる", - "のぼる", - "のみもの", - "のやま", - "のらいぬ", - "のらねこ", - "のりもの", - "のりゆき", - "のれん", - "のんき", - "ばあい", - "はあく", - "ばあさん", - "ばいか", - "ばいく", - "はいけん", - "はいご", - "はいしん", - "はいすい", - "はいせん", - "はいそう", - "はいち", - "ばいばい", - "はいれつ", - "はえる", - "はおる", - "はかい", - "ばかり", - "はかる", - "はくしゅ", - "はけん", - "はこぶ", - "はさみ", - "はさん", - "はしご", - "ばしょ", - "はしる", - "はせる", - "ぱそこん", - "はそん", - "はたん", - "はちみつ", - "はつおん", - "はっかく", - "はづき", - "はっきり", - "はっくつ", - "はっけん", - "はっこう", - "はっさん", - "はっしん", - "はったつ", - "はっちゅう", - "はってん", - "はっぴょう", - "はっぽう", - "はなす", - "はなび", - "はにかむ", - "はぶらし", - "はみがき", - "はむかう", - "はめつ", - "はやい", - "はやし", - "はらう", - "はろうぃん", - "はわい", - "はんい", - "はんえい", - "はんおん", - "はんかく", - "はんきょう", - "ばんぐみ", - "はんこ", - "はんしゃ", - "はんすう", - "はんだん", - "ぱんち", - "ぱんつ", - "はんてい", - "はんとし", - "はんのう", - "はんぱ", - "はんぶん", - "はんぺん", - "はんぼうき", - "はんめい", - "はんらん", - "はんろん", - "ひいき", - "ひうん", - "ひえる", - "ひかく", - "ひかり", - "ひかる", - "ひかん", - "ひくい", - "ひけつ", - "ひこうき", - "ひこく", - "ひさい", - "ひさしぶり", - "ひさん", - "びじゅつかん", - "ひしょ" - ]; -} \ No newline at end of file diff --git a/cw_haven/lib/mnemonics/portuguese.dart b/cw_haven/lib/mnemonics/portuguese.dart deleted file mode 100644 index bdd63d3b2..000000000 --- a/cw_haven/lib/mnemonics/portuguese.dart +++ /dev/null @@ -1,1630 +0,0 @@ -class PortugueseMnemonics { - static const words = [ - "abaular", - "abdominal", - "abeto", - "abissinio", - "abjeto", - "ablucao", - "abnegar", - "abotoar", - "abrutalhar", - "absurdo", - "abutre", - "acautelar", - "accessorios", - "acetona", - "achocolatado", - "acirrar", - "acne", - "acovardar", - "acrostico", - "actinomicete", - "acustico", - "adaptavel", - "adeus", - "adivinho", - "adjunto", - "admoestar", - "adnominal", - "adotivo", - "adquirir", - "adriatico", - "adsorcao", - "adutora", - "advogar", - "aerossol", - "afazeres", - "afetuoso", - "afixo", - "afluir", - "afortunar", - "afrouxar", - "aftosa", - "afunilar", - "agentes", - "agito", - "aglutinar", - "aiatola", - "aimore", - "aino", - "aipo", - "airoso", - "ajeitar", - "ajoelhar", - "ajudante", - "ajuste", - "alazao", - "albumina", - "alcunha", - "alegria", - "alexandre", - "alforriar", - "alguns", - "alhures", - "alivio", - "almoxarife", - "alotropico", - "alpiste", - "alquimista", - "alsaciano", - "altura", - "aluviao", - "alvura", - "amazonico", - "ambulatorio", - "ametodico", - "amizades", - "amniotico", - "amovivel", - "amurada", - "anatomico", - "ancorar", - "anexo", - "anfora", - "aniversario", - "anjo", - "anotar", - "ansioso", - "anturio", - "anuviar", - "anverso", - "anzol", - "aonde", - "apaziguar", - "apito", - "aplicavel", - "apoteotico", - "aprimorar", - "aprumo", - "apto", - "apuros", - "aquoso", - "arauto", - "arbusto", - "arduo", - "aresta", - "arfar", - "arguto", - "aritmetico", - "arlequim", - "armisticio", - "aromatizar", - "arpoar", - "arquivo", - "arrumar", - "arsenio", - "arturiano", - "aruaque", - "arvores", - "asbesto", - "ascorbico", - "aspirina", - "asqueroso", - "assustar", - "astuto", - "atazanar", - "ativo", - "atletismo", - "atmosferico", - "atormentar", - "atroz", - "aturdir", - "audivel", - "auferir", - "augusto", - "aula", - "aumento", - "aurora", - "autuar", - "avatar", - "avexar", - "avizinhar", - "avolumar", - "avulso", - "axiomatico", - "azerbaijano", - "azimute", - "azoto", - "azulejo", - "bacteriologista", - "badulaque", - "baforada", - "baixote", - "bajular", - "balzaquiana", - "bambuzal", - "banzo", - "baoba", - "baqueta", - "barulho", - "bastonete", - "batuta", - "bauxita", - "bavaro", - "bazuca", - "bcrepuscular", - "beato", - "beduino", - "begonia", - "behaviorista", - "beisebol", - "belzebu", - "bemol", - "benzido", - "beocio", - "bequer", - "berro", - "besuntar", - "betume", - "bexiga", - "bezerro", - "biatlon", - "biboca", - "bicuspide", - "bidirecional", - "bienio", - "bifurcar", - "bigorna", - "bijuteria", - "bimotor", - "binormal", - "bioxido", - "bipolarizacao", - "biquini", - "birutice", - "bisturi", - "bituca", - "biunivoco", - "bivalve", - "bizarro", - "blasfemo", - "blenorreia", - "blindar", - "bloqueio", - "blusao", - "boazuda", - "bofete", - "bojudo", - "bolso", - "bombordo", - "bonzo", - "botina", - "boquiaberto", - "bostoniano", - "botulismo", - "bourbon", - "bovino", - "boximane", - "bravura", - "brevidade", - "britar", - "broxar", - "bruno", - "bruxuleio", - "bubonico", - "bucolico", - "buda", - "budista", - "bueiro", - "buffer", - "bugre", - "bujao", - "bumerangue", - "burundines", - "busto", - "butique", - "buzios", - "caatinga", - "cabuqui", - "cacunda", - "cafuzo", - "cajueiro", - "camurca", - "canudo", - "caquizeiro", - "carvoeiro", - "casulo", - "catuaba", - "cauterizar", - "cebolinha", - "cedula", - "ceifeiro", - "celulose", - "cerzir", - "cesto", - "cetro", - "ceus", - "cevar", - "chavena", - "cheroqui", - "chita", - "chovido", - "chuvoso", - "ciatico", - "cibernetico", - "cicuta", - "cidreira", - "cientistas", - "cifrar", - "cigarro", - "cilio", - "cimo", - "cinzento", - "cioso", - "cipriota", - "cirurgico", - "cisto", - "citrico", - "ciumento", - "civismo", - "clavicula", - "clero", - "clitoris", - "cluster", - "coaxial", - "cobrir", - "cocota", - "codorniz", - "coexistir", - "cogumelo", - "coito", - "colusao", - "compaixao", - "comutativo", - "contentamento", - "convulsivo", - "coordenativa", - "coquetel", - "correto", - "corvo", - "costureiro", - "cotovia", - "covil", - "cozinheiro", - "cretino", - "cristo", - "crivo", - "crotalo", - "cruzes", - "cubo", - "cucuia", - "cueiro", - "cuidar", - "cujo", - "cultural", - "cunilingua", - "cupula", - "curvo", - "custoso", - "cutucar", - "czarismo", - "dablio", - "dacota", - "dados", - "daguerreotipo", - "daiquiri", - "daltonismo", - "damista", - "dantesco", - "daquilo", - "darwinista", - "dasein", - "dativo", - "deao", - "debutantes", - "decurso", - "deduzir", - "defunto", - "degustar", - "dejeto", - "deltoide", - "demover", - "denunciar", - "deputado", - "deque", - "dervixe", - "desvirtuar", - "deturpar", - "deuteronomio", - "devoto", - "dextrose", - "dezoito", - "diatribe", - "dicotomico", - "didatico", - "dietista", - "difuso", - "digressao", - "diluvio", - "diminuto", - "dinheiro", - "dinossauro", - "dioxido", - "diplomatico", - "dique", - "dirimivel", - "disturbio", - "diurno", - "divulgar", - "dizivel", - "doar", - "dobro", - "docura", - "dodoi", - "doer", - "dogue", - "doloso", - "domo", - "donzela", - "doping", - "dorsal", - "dossie", - "dote", - "doutro", - "doze", - "dravidico", - "dreno", - "driver", - "dropes", - "druso", - "dubnio", - "ducto", - "dueto", - "dulija", - "dundum", - "duodeno", - "duquesa", - "durou", - "duvidoso", - "duzia", - "ebano", - "ebrio", - "eburneo", - "echarpe", - "eclusa", - "ecossistema", - "ectoplasma", - "ecumenismo", - "eczema", - "eden", - "editorial", - "edredom", - "edulcorar", - "efetuar", - "efigie", - "efluvio", - "egiptologo", - "egresso", - "egua", - "einsteiniano", - "eira", - "eivar", - "eixos", - "ejetar", - "elastomero", - "eldorado", - "elixir", - "elmo", - "eloquente", - "elucidativo", - "emaranhar", - "embutir", - "emerito", - "emfa", - "emitir", - "emotivo", - "empuxo", - "emulsao", - "enamorar", - "encurvar", - "enduro", - "enevoar", - "enfurnar", - "enguico", - "enho", - "enigmista", - "enlutar", - "enormidade", - "enpreendimento", - "enquanto", - "enriquecer", - "enrugar", - "entusiastico", - "enunciar", - "envolvimento", - "enxuto", - "enzimatico", - "eolico", - "epiteto", - "epoxi", - "epura", - "equivoco", - "erario", - "erbio", - "ereto", - "erguido", - "erisipela", - "ermo", - "erotizar", - "erros", - "erupcao", - "ervilha", - "esburacar", - "escutar", - "esfuziante", - "esguio", - "esloveno", - "esmurrar", - "esoterismo", - "esperanca", - "espirito", - "espurio", - "essencialmente", - "esturricar", - "esvoacar", - "etario", - "eterno", - "etiquetar", - "etnologo", - "etos", - "etrusco", - "euclidiano", - "euforico", - "eugenico", - "eunuco", - "europio", - "eustaquio", - "eutanasia", - "evasivo", - "eventualidade", - "evitavel", - "evoluir", - "exaustor", - "excursionista", - "exercito", - "exfoliado", - "exito", - "exotico", - "expurgo", - "exsudar", - "extrusora", - "exumar", - "fabuloso", - "facultativo", - "fado", - "fagulha", - "faixas", - "fajuto", - "faltoso", - "famoso", - "fanzine", - "fapesp", - "faquir", - "fartura", - "fastio", - "faturista", - "fausto", - "favorito", - "faxineira", - "fazer", - "fealdade", - "febril", - "fecundo", - "fedorento", - "feerico", - "feixe", - "felicidade", - "felpudo", - "feltro", - "femur", - "fenotipo", - "fervura", - "festivo", - "feto", - "feudo", - "fevereiro", - "fezinha", - "fiasco", - "fibra", - "ficticio", - "fiduciario", - "fiesp", - "fifa", - "figurino", - "fijiano", - "filtro", - "finura", - "fiorde", - "fiquei", - "firula", - "fissurar", - "fitoteca", - "fivela", - "fixo", - "flavio", - "flexor", - "flibusteiro", - "flotilha", - "fluxograma", - "fobos", - "foco", - "fofura", - "foguista", - "foie", - "foliculo", - "fominha", - "fonte", - "forum", - "fosso", - "fotossintese", - "foxtrote", - "fraudulento", - "frevo", - "frivolo", - "frouxo", - "frutose", - "fuba", - "fucsia", - "fugitivo", - "fuinha", - "fujao", - "fulustreco", - "fumo", - "funileiro", - "furunculo", - "fustigar", - "futurologo", - "fuxico", - "fuzue", - "gabriel", - "gado", - "gaelico", - "gafieira", - "gaguejo", - "gaivota", - "gajo", - "galvanoplastico", - "gamo", - "ganso", - "garrucha", - "gastronomo", - "gatuno", - "gaussiano", - "gaviao", - "gaxeta", - "gazeteiro", - "gear", - "geiser", - "geminiano", - "generoso", - "genuino", - "geossinclinal", - "gerundio", - "gestual", - "getulista", - "gibi", - "gigolo", - "gilete", - "ginseng", - "giroscopio", - "glaucio", - "glacial", - "gleba", - "glifo", - "glote", - "glutonia", - "gnostico", - "goela", - "gogo", - "goitaca", - "golpista", - "gomo", - "gonzo", - "gorro", - "gostou", - "goticula", - "gourmet", - "governo", - "gozo", - "graxo", - "grevista", - "grito", - "grotesco", - "gruta", - "guaxinim", - "gude", - "gueto", - "guizo", - "guloso", - "gume", - "guru", - "gustativo", - "grelhado", - "gutural", - "habitue", - "haitiano", - "halterofilista", - "hamburguer", - "hanseniase", - "happening", - "harpista", - "hastear", - "haveres", - "hebreu", - "hectometro", - "hedonista", - "hegira", - "helena", - "helminto", - "hemorroidas", - "henrique", - "heptassilabo", - "hertziano", - "hesitar", - "heterossexual", - "heuristico", - "hexagono", - "hiato", - "hibrido", - "hidrostatico", - "hieroglifo", - "hifenizar", - "higienizar", - "hilario", - "himen", - "hino", - "hippie", - "hirsuto", - "historiografia", - "hitlerista", - "hodometro", - "hoje", - "holograma", - "homus", - "honroso", - "hoquei", - "horto", - "hostilizar", - "hotentote", - "huguenote", - "humilde", - "huno", - "hurra", - "hutu", - "iaia", - "ialorixa", - "iambico", - "iansa", - "iaque", - "iara", - "iatista", - "iberico", - "ibis", - "icar", - "iceberg", - "icosagono", - "idade", - "ideologo", - "idiotice", - "idoso", - "iemenita", - "iene", - "igarape", - "iglu", - "ignorar", - "igreja", - "iguaria", - "iidiche", - "ilativo", - "iletrado", - "ilharga", - "ilimitado", - "ilogismo", - "ilustrissimo", - "imaturo", - "imbuzeiro", - "imerso", - "imitavel", - "imovel", - "imputar", - "imutavel", - "inaveriguavel", - "incutir", - "induzir", - "inextricavel", - "infusao", - "ingua", - "inhame", - "iniquo", - "injusto", - "inning", - "inoxidavel", - "inquisitorial", - "insustentavel", - "intumescimento", - "inutilizavel", - "invulneravel", - "inzoneiro", - "iodo", - "iogurte", - "ioio", - "ionosfera", - "ioruba", - "iota", - "ipsilon", - "irascivel", - "iris", - "irlandes", - "irmaos", - "iroques", - "irrupcao", - "isca", - "isento", - "islandes", - "isotopo", - "isqueiro", - "israelita", - "isso", - "isto", - "iterbio", - "itinerario", - "itrio", - "iuane", - "iugoslavo", - "jabuticabeira", - "jacutinga", - "jade", - "jagunco", - "jainista", - "jaleco", - "jambo", - "jantarada", - "japones", - "jaqueta", - "jarro", - "jasmim", - "jato", - "jaula", - "javel", - "jazz", - "jegue", - "jeitoso", - "jejum", - "jenipapo", - "jeova", - "jequitiba", - "jersei", - "jesus", - "jetom", - "jiboia", - "jihad", - "jilo", - "jingle", - "jipe", - "jocoso", - "joelho", - "joguete", - "joio", - "jojoba", - "jorro", - "jota", - "joule", - "joviano", - "jubiloso", - "judoca", - "jugular", - "juizo", - "jujuba", - "juliano", - "jumento", - "junto", - "jururu", - "justo", - "juta", - "juventude", - "labutar", - "laguna", - "laico", - "lajota", - "lanterninha", - "lapso", - "laquear", - "lastro", - "lauto", - "lavrar", - "laxativo", - "lazer", - "leasing", - "lebre", - "lecionar", - "ledo", - "leguminoso", - "leitura", - "lele", - "lemure", - "lento", - "leonardo", - "leopardo", - "lepton", - "leque", - "leste", - "letreiro", - "leucocito", - "levitico", - "lexicologo", - "lhama", - "lhufas", - "liame", - "licoroso", - "lidocaina", - "liliputiano", - "limusine", - "linotipo", - "lipoproteina", - "liquidos", - "lirismo", - "lisura", - "liturgico", - "livros", - "lixo", - "lobulo", - "locutor", - "lodo", - "logro", - "lojista", - "lombriga", - "lontra", - "loop", - "loquaz", - "lorota", - "losango", - "lotus", - "louvor", - "luar", - "lubrificavel", - "lucros", - "lugubre", - "luis", - "luminoso", - "luneta", - "lustroso", - "luto", - "luvas", - "luxuriante", - "luzeiro", - "maduro", - "maestro", - "mafioso", - "magro", - "maiuscula", - "majoritario", - "malvisto", - "mamute", - "manutencao", - "mapoteca", - "maquinista", - "marzipa", - "masturbar", - "matuto", - "mausoleu", - "mavioso", - "maxixe", - "mazurca", - "meandro", - "mecha", - "medusa", - "mefistofelico", - "megera", - "meirinho", - "melro", - "memorizar", - "menu", - "mequetrefe", - "mertiolate", - "mestria", - "metroviario", - "mexilhao", - "mezanino", - "miau", - "microssegundo", - "midia", - "migratorio", - "mimosa", - "minuto", - "miosotis", - "mirtilo", - "misturar", - "mitzvah", - "miudos", - "mixuruca", - "mnemonico", - "moagem", - "mobilizar", - "modulo", - "moer", - "mofo", - "mogno", - "moita", - "molusco", - "monumento", - "moqueca", - "morubixaba", - "mostruario", - "motriz", - "mouse", - "movivel", - "mozarela", - "muarra", - "muculmano", - "mudo", - "mugir", - "muitos", - "mumunha", - "munir", - "muon", - "muquira", - "murros", - "musselina", - "nacoes", - "nado", - "naftalina", - "nago", - "naipe", - "naja", - "nalgum", - "namoro", - "nanquim", - "napolitano", - "naquilo", - "nascimento", - "nautilo", - "navios", - "nazista", - "nebuloso", - "nectarina", - "nefrologo", - "negus", - "nelore", - "nenufar", - "nepotismo", - "nervura", - "neste", - "netuno", - "neutron", - "nevoeiro", - "newtoniano", - "nexo", - "nhenhenhem", - "nhoque", - "nigeriano", - "niilista", - "ninho", - "niobio", - "niponico", - "niquelar", - "nirvana", - "nisto", - "nitroglicerina", - "nivoso", - "nobreza", - "nocivo", - "noel", - "nogueira", - "noivo", - "nojo", - "nominativo", - "nonuplo", - "noruegues", - "nostalgico", - "noturno", - "nouveau", - "nuanca", - "nublar", - "nucleotideo", - "nudista", - "nulo", - "numismatico", - "nunquinha", - "nupcias", - "nutritivo", - "nuvens", - "oasis", - "obcecar", - "obeso", - "obituario", - "objetos", - "oblongo", - "obnoxio", - "obrigatorio", - "obstruir", - "obtuso", - "obus", - "obvio", - "ocaso", - "occipital", - "oceanografo", - "ocioso", - "oclusivo", - "ocorrer", - "ocre", - "octogono", - "odalisca", - "odisseia", - "odorifico", - "oersted", - "oeste", - "ofertar", - "ofidio", - "oftalmologo", - "ogiva", - "ogum", - "oigale", - "oitavo", - "oitocentos", - "ojeriza", - "olaria", - "oleoso", - "olfato", - "olhos", - "oliveira", - "olmo", - "olor", - "olvidavel", - "ombudsman", - "omeleteira", - "omitir", - "omoplata", - "onanismo", - "ondular", - "oneroso", - "onomatopeico", - "ontologico", - "onus", - "onze", - "opalescente", - "opcional", - "operistico", - "opio", - "oposto", - "oprobrio", - "optometrista", - "opusculo", - "oratorio", - "orbital", - "orcar", - "orfao", - "orixa", - "orla", - "ornitologo", - "orquidea", - "ortorrombico", - "orvalho", - "osculo", - "osmotico", - "ossudo", - "ostrogodo", - "otario", - "otite", - "ouro", - "ousar", - "outubro", - "ouvir", - "ovario", - "overnight", - "oviparo", - "ovni", - "ovoviviparo", - "ovulo", - "oxala", - "oxente", - "oxiuro", - "oxossi", - "ozonizar", - "paciente", - "pactuar", - "padronizar", - "paete", - "pagodeiro", - "paixao", - "pajem", - "paludismo", - "pampas", - "panturrilha", - "papudo", - "paquistanes", - "pastoso", - "patua", - "paulo", - "pauzinhos", - "pavoroso", - "paxa", - "pazes", - "peao", - "pecuniario", - "pedunculo", - "pegaso", - "peixinho", - "pejorativo", - "pelvis", - "penuria", - "pequno", - "petunia", - "pezada", - "piauiense", - "pictorico", - "pierro", - "pigmeu", - "pijama", - "pilulas", - "pimpolho", - "pintura", - "piorar", - "pipocar", - "piqueteiro", - "pirulito", - "pistoleiro", - "pituitaria", - "pivotar", - "pixote", - "pizzaria", - "plistoceno", - "plotar", - "pluviometrico", - "pneumonico", - "poco", - "podridao", - "poetisa", - "pogrom", - "pois", - "polvorosa", - "pomposo", - "ponderado", - "pontudo", - "populoso", - "poquer", - "porvir", - "posudo", - "potro", - "pouso", - "povoar", - "prazo", - "prezar", - "privilegios", - "proximo", - "prussiano", - "pseudopode", - "psoriase", - "pterossauros", - "ptialina", - "ptolemaico", - "pudor", - "pueril", - "pufe", - "pugilista", - "puir", - "pujante", - "pulverizar", - "pumba", - "punk", - "purulento", - "pustula", - "putsch", - "puxe", - "quatrocentos", - "quetzal", - "quixotesco", - "quotizavel", - "rabujice", - "racista", - "radonio", - "rafia", - "ragu", - "rajado", - "ralo", - "rampeiro", - "ranzinza", - "raptor", - "raquitismo", - "raro", - "rasurar", - "ratoeira", - "ravioli", - "razoavel", - "reavivar", - "rebuscar", - "recusavel", - "reduzivel", - "reexposicao", - "refutavel", - "regurgitar", - "reivindicavel", - "rejuvenescimento", - "relva", - "remuneravel", - "renunciar", - "reorientar", - "repuxo", - "requisito", - "resumo", - "returno", - "reutilizar", - "revolvido", - "rezonear", - "riacho", - "ribossomo", - "ricota", - "ridiculo", - "rifle", - "rigoroso", - "rijo", - "rimel", - "rins", - "rios", - "riqueza", - "respeito", - "rissole", - "ritualistico", - "rivalizar", - "rixa", - "robusto", - "rococo", - "rodoviario", - "roer", - "rogo", - "rojao", - "rolo", - "rompimento", - "ronronar", - "roqueiro", - "rorqual", - "rosto", - "rotundo", - "rouxinol", - "roxo", - "royal", - "ruas", - "rucula", - "rudimentos", - "ruela", - "rufo", - "rugoso", - "ruivo", - "rule", - "rumoroso", - "runico", - "ruptura", - "rural", - "rustico", - "rutilar", - "saariano", - "sabujo", - "sacudir", - "sadomasoquista", - "safra", - "sagui", - "sais", - "samurai", - "santuario", - "sapo", - "saquear", - "sartriano", - "saturno", - "saude", - "sauva", - "saveiro", - "saxofonista", - "sazonal", - "scherzo", - "script", - "seara", - "seborreia", - "secura", - "seduzir", - "sefardim", - "seguro", - "seja", - "selvas", - "sempre", - "senzala", - "sepultura", - "sequoia", - "sestercio", - "setuplo", - "seus", - "seviciar", - "sezonismo", - "shalom", - "siames", - "sibilante", - "sicrano", - "sidra", - "sifilitico", - "signos", - "silvo", - "simultaneo", - "sinusite", - "sionista", - "sirio", - "sisudo", - "situar", - "sivan", - "slide", - "slogan", - "soar", - "sobrio", - "socratico", - "sodomizar", - "soerguer", - "software", - "sogro", - "soja", - "solver", - "somente", - "sonso", - "sopro", - "soquete", - "sorveteiro", - "sossego", - "soturno", - "sousafone", - "sovinice", - "sozinho", - "suavizar", - "subverter", - "sucursal", - "sudoriparo", - "sufragio", - "sugestoes", - "suite", - "sujo", - "sultao", - "sumula", - "suntuoso", - "suor", - "supurar", - "suruba", - "susto", - "suturar", - "suvenir", - "tabuleta", - "taco", - "tadjique", - "tafeta", - "tagarelice", - "taitiano", - "talvez", - "tampouco", - "tanzaniano", - "taoista", - "tapume", - "taquion", - "tarugo", - "tascar", - "tatuar", - "tautologico", - "tavola", - "taxionomista", - "tchecoslovaco", - "teatrologo", - "tectonismo", - "tedioso", - "teflon", - "tegumento", - "teixo", - "telurio", - "temporas", - "tenue", - "teosofico", - "tepido", - "tequila", - "terrorista", - "testosterona", - "tetrico", - "teutonico", - "teve", - "texugo", - "tiara", - "tibia", - "tiete", - "tifoide", - "tigresa", - "tijolo", - "tilintar", - "timpano", - "tintureiro", - "tiquete", - "tiroteio", - "tisico", - "titulos", - "tive", - "toar", - "toboga", - "tofu", - "togoles", - "toicinho", - "tolueno", - "tomografo", - "tontura", - "toponimo", - "toquio", - "torvelinho", - "tostar", - "toto", - "touro", - "toxina", - "trazer", - "trezentos", - "trivialidade", - "trovoar", - "truta", - "tuaregue", - "tubular", - "tucano", - "tudo", - "tufo", - "tuiste", - "tulipa", - "tumultuoso", - "tunisino", - "tupiniquim", - "turvo", - "tutu", - "ucraniano", - "udenista", - "ufanista", - "ufologo", - "ugaritico", - "uiste", - "uivo", - "ulceroso", - "ulema", - "ultravioleta", - "umbilical", - "umero", - "umido", - "umlaut", - "unanimidade", - "unesco", - "ungulado", - "unheiro", - "univoco", - "untuoso", - "urano", - "urbano", - "urdir", - "uretra", - "urgente", - "urinol", - "urna", - "urologo", - "urro", - "ursulina", - "urtiga", - "urupe", - "usavel", - "usbeque", - "usei", - "usineiro", - "usurpar", - "utero", - "utilizar", - "utopico", - "uvular", - "uxoricidio", - "vacuo", - "vadio", - "vaguear", - "vaivem", - "valvula", - "vampiro", - "vantajoso", - "vaporoso", - "vaquinha", - "varziano", - "vasto", - "vaticinio", - "vaudeville", - "vazio", - "veado", - "vedico", - "veemente", - "vegetativo", - "veio", - "veja", - "veludo", - "venusiano", - "verdade", - "verve", - "vestuario", - "vetusto", - "vexatorio", - "vezes", - "viavel", - "vibratorio", - "victor", - "vicunha", - "vidros", - "vietnamita", - "vigoroso", - "vilipendiar", - "vime", - "vintem", - "violoncelo", - "viquingue", - "virus", - "visualizar", - "vituperio", - "viuvo", - "vivo", - "vizir", - "voar", - "vociferar", - "vodu", - "vogar", - "voile", - "volver", - "vomito", - "vontade", - "vortice", - "vosso", - "voto", - "vovozinha", - "voyeuse", - "vozes", - "vulva", - "vupt", - "western", - "xadrez", - "xale", - "xampu", - "xango", - "xarope", - "xaual", - "xavante", - "xaxim", - "xenonio", - "xepa", - "xerox", - "xicara", - "xifopago", - "xiita", - "xilogravura", - "xinxim", - "xistoso", - "xixi", - "xodo", - "xogum", - "xucro", - "zabumba", - "zagueiro", - "zambiano", - "zanzar", - "zarpar", - "zebu", - "zefiro", - "zeloso", - "zenite", - "zumbi" - ]; -} \ No newline at end of file diff --git a/cw_haven/lib/mnemonics/russian.dart b/cw_haven/lib/mnemonics/russian.dart deleted file mode 100644 index f10af0ff6..000000000 --- a/cw_haven/lib/mnemonics/russian.dart +++ /dev/null @@ -1,1630 +0,0 @@ -class RussianMnemonics { - static const words = [ - "абажур", - "абзац", - "абонент", - "абрикос", - "абсурд", - "авангард", - "август", - "авиация", - "авоська", - "автор", - "агат", - "агент", - "агитатор", - "агнец", - "агония", - "агрегат", - "адвокат", - "адмирал", - "адрес", - "ажиотаж", - "азарт", - "азбука", - "азот", - "аист", - "айсберг", - "академия", - "аквариум", - "аккорд", - "акробат", - "аксиома", - "актер", - "акула", - "акция", - "алгоритм", - "алебарда", - "аллея", - "алмаз", - "алтарь", - "алфавит", - "алхимик", - "алый", - "альбом", - "алюминий", - "амбар", - "аметист", - "амнезия", - "ампула", - "амфора", - "анализ", - "ангел", - "анекдот", - "анимация", - "анкета", - "аномалия", - "ансамбль", - "антенна", - "апатия", - "апельсин", - "апофеоз", - "аппарат", - "апрель", - "аптека", - "арабский", - "арбуз", - "аргумент", - "арест", - "ария", - "арка", - "армия", - "аромат", - "арсенал", - "артист", - "архив", - "аршин", - "асбест", - "аскетизм", - "аспект", - "ассорти", - "астроном", - "асфальт", - "атака", - "ателье", - "атлас", - "атом", - "атрибут", - "аудитор", - "аукцион", - "аура", - "афера", - "афиша", - "ахинея", - "ацетон", - "аэропорт", - "бабушка", - "багаж", - "бадья", - "база", - "баклажан", - "балкон", - "бампер", - "банк", - "барон", - "бассейн", - "батарея", - "бахрома", - "башня", - "баян", - "бегство", - "бедро", - "бездна", - "бекон", - "белый", - "бензин", - "берег", - "беседа", - "бетонный", - "биатлон", - "библия", - "бивень", - "бигуди", - "бидон", - "бизнес", - "бикини", - "билет", - "бинокль", - "биология", - "биржа", - "бисер", - "битва", - "бицепс", - "благо", - "бледный", - "близкий", - "блок", - "блуждать", - "блюдо", - "бляха", - "бобер", - "богатый", - "бодрый", - "боевой", - "бокал", - "большой", - "борьба", - "босой", - "ботинок", - "боцман", - "бочка", - "боярин", - "брать", - "бревно", - "бригада", - "бросать", - "брызги", - "брюки", - "бублик", - "бугор", - "будущее", - "буква", - "бульвар", - "бумага", - "бунт", - "бурный", - "бусы", - "бутылка", - "буфет", - "бухта", - "бушлат", - "бывалый", - "быль", - "быстрый", - "быть", - "бюджет", - "бюро", - "бюст", - "вагон", - "важный", - "ваза", - "вакцина", - "валюта", - "вампир", - "ванная", - "вариант", - "вассал", - "вата", - "вафля", - "вахта", - "вдова", - "вдыхать", - "ведущий", - "веер", - "вежливый", - "везти", - "веко", - "великий", - "вена", - "верить", - "веселый", - "ветер", - "вечер", - "вешать", - "вещь", - "веяние", - "взаимный", - "взбучка", - "взвод", - "взгляд", - "вздыхать", - "взлетать", - "взмах", - "взнос", - "взор", - "взрыв", - "взывать", - "взятка", - "вибрация", - "визит", - "вилка", - "вино", - "вирус", - "висеть", - "витрина", - "вихрь", - "вишневый", - "включать", - "вкус", - "власть", - "влечь", - "влияние", - "влюблять", - "внешний", - "внимание", - "внук", - "внятный", - "вода", - "воевать", - "вождь", - "воздух", - "войти", - "вокзал", - "волос", - "вопрос", - "ворота", - "восток", - "впадать", - "впускать", - "врач", - "время", - "вручать", - "всадник", - "всеобщий", - "вспышка", - "встреча", - "вторник", - "вулкан", - "вурдалак", - "входить", - "въезд", - "выбор", - "вывод", - "выгодный", - "выделять", - "выезжать", - "выживать", - "вызывать", - "выигрыш", - "вылезать", - "выносить", - "выпивать", - "высокий", - "выходить", - "вычет", - "вышка", - "выяснять", - "вязать", - "вялый", - "гавань", - "гадать", - "газета", - "гаишник", - "галстук", - "гамма", - "гарантия", - "гастроли", - "гвардия", - "гвоздь", - "гектар", - "гель", - "генерал", - "геолог", - "герой", - "гешефт", - "гибель", - "гигант", - "гильза", - "гимн", - "гипотеза", - "гитара", - "глаз", - "глина", - "глоток", - "глубокий", - "глыба", - "глядеть", - "гнать", - "гнев", - "гнить", - "гном", - "гнуть", - "говорить", - "годовой", - "голова", - "гонка", - "город", - "гость", - "готовый", - "граница", - "грех", - "гриб", - "громкий", - "группа", - "грызть", - "грязный", - "губа", - "гудеть", - "гулять", - "гуманный", - "густой", - "гуща", - "давать", - "далекий", - "дама", - "данные", - "дарить", - "дать", - "дача", - "дверь", - "движение", - "двор", - "дебют", - "девушка", - "дедушка", - "дежурный", - "дезертир", - "действие", - "декабрь", - "дело", - "демократ", - "день", - "депутат", - "держать", - "десяток", - "детский", - "дефицит", - "дешевый", - "деятель", - "джаз", - "джинсы", - "джунгли", - "диалог", - "диван", - "диета", - "дизайн", - "дикий", - "динамика", - "диплом", - "директор", - "диск", - "дитя", - "дичь", - "длинный", - "дневник", - "добрый", - "доверие", - "договор", - "дождь", - "доза", - "документ", - "должен", - "домашний", - "допрос", - "дорога", - "доход", - "доцент", - "дочь", - "дощатый", - "драка", - "древний", - "дрожать", - "друг", - "дрянь", - "дубовый", - "дуга", - "дудка", - "дукат", - "дуло", - "думать", - "дупло", - "дурак", - "дуть", - "духи", - "душа", - "дуэт", - "дымить", - "дыня", - "дыра", - "дыханье", - "дышать", - "дьявол", - "дюжина", - "дюйм", - "дюна", - "дядя", - "дятел", - "егерь", - "единый", - "едкий", - "ежевика", - "ежик", - "езда", - "елка", - "емкость", - "ерунда", - "ехать", - "жадный", - "жажда", - "жалеть", - "жанр", - "жара", - "жать", - "жгучий", - "ждать", - "жевать", - "желание", - "жемчуг", - "женщина", - "жертва", - "жесткий", - "жечь", - "живой", - "жидкость", - "жизнь", - "жилье", - "жирный", - "житель", - "журнал", - "жюри", - "забывать", - "завод", - "загадка", - "задача", - "зажечь", - "зайти", - "закон", - "замечать", - "занимать", - "западный", - "зарплата", - "засыпать", - "затрата", - "захват", - "зацепка", - "зачет", - "защита", - "заявка", - "звать", - "звезда", - "звонить", - "звук", - "здание", - "здешний", - "здоровье", - "зебра", - "зевать", - "зеленый", - "земля", - "зенит", - "зеркало", - "зефир", - "зигзаг", - "зима", - "зиять", - "злак", - "злой", - "змея", - "знать", - "зной", - "зодчий", - "золотой", - "зомби", - "зона", - "зоопарк", - "зоркий", - "зрачок", - "зрение", - "зритель", - "зубной", - "зыбкий", - "зять", - "игла", - "иголка", - "играть", - "идея", - "идиот", - "идол", - "идти", - "иерархия", - "избрать", - "известие", - "изгонять", - "издание", - "излагать", - "изменять", - "износ", - "изоляция", - "изрядный", - "изучать", - "изымать", - "изящный", - "икона", - "икра", - "иллюзия", - "имбирь", - "иметь", - "имидж", - "иммунный", - "империя", - "инвестор", - "индивид", - "инерция", - "инженер", - "иномарка", - "институт", - "интерес", - "инфекция", - "инцидент", - "ипподром", - "ирис", - "ирония", - "искать", - "история", - "исходить", - "исчезать", - "итог", - "июль", - "июнь", - "кабинет", - "кавалер", - "кадр", - "казарма", - "кайф", - "кактус", - "калитка", - "камень", - "канал", - "капитан", - "картина", - "касса", - "катер", - "кафе", - "качество", - "каша", - "каюта", - "квартира", - "квинтет", - "квота", - "кедр", - "кекс", - "кенгуру", - "кепка", - "керосин", - "кетчуп", - "кефир", - "кибитка", - "кивнуть", - "кидать", - "километр", - "кино", - "киоск", - "кипеть", - "кирпич", - "кисть", - "китаец", - "класс", - "клетка", - "клиент", - "клоун", - "клуб", - "клык", - "ключ", - "клятва", - "книга", - "кнопка", - "кнут", - "князь", - "кобура", - "ковер", - "коготь", - "кодекс", - "кожа", - "козел", - "койка", - "коктейль", - "колено", - "компания", - "конец", - "копейка", - "короткий", - "костюм", - "котел", - "кофе", - "кошка", - "красный", - "кресло", - "кричать", - "кровь", - "крупный", - "крыша", - "крючок", - "кубок", - "кувшин", - "кудрявый", - "кузов", - "кукла", - "культура", - "кумир", - "купить", - "курс", - "кусок", - "кухня", - "куча", - "кушать", - "кювет", - "лабиринт", - "лавка", - "лагерь", - "ладонь", - "лазерный", - "лайнер", - "лакей", - "лампа", - "ландшафт", - "лапа", - "ларек", - "ласковый", - "лауреат", - "лачуга", - "лаять", - "лгать", - "лебедь", - "левый", - "легкий", - "ледяной", - "лежать", - "лекция", - "лента", - "лепесток", - "лесной", - "лето", - "лечь", - "леший", - "лживый", - "либерал", - "ливень", - "лига", - "лидер", - "ликовать", - "лиловый", - "лимон", - "линия", - "липа", - "лирика", - "лист", - "литр", - "лифт", - "лихой", - "лицо", - "личный", - "лишний", - "лобовой", - "ловить", - "логика", - "лодка", - "ложка", - "лозунг", - "локоть", - "ломать", - "лоно", - "лопата", - "лорд", - "лось", - "лоток", - "лохматый", - "лошадь", - "лужа", - "лукавый", - "луна", - "лупить", - "лучший", - "лыжный", - "лысый", - "львиный", - "льгота", - "льдина", - "любить", - "людской", - "люстра", - "лютый", - "лягушка", - "магазин", - "мадам", - "мазать", - "майор", - "максимум", - "мальчик", - "манера", - "март", - "масса", - "мать", - "мафия", - "махать", - "мачта", - "машина", - "маэстро", - "маяк", - "мгла", - "мебель", - "медведь", - "мелкий", - "мемуары", - "менять", - "мера", - "место", - "метод", - "механизм", - "мечтать", - "мешать", - "миграция", - "мизинец", - "микрофон", - "миллион", - "минута", - "мировой", - "миссия", - "митинг", - "мишень", - "младший", - "мнение", - "мнимый", - "могила", - "модель", - "мозг", - "мойка", - "мокрый", - "молодой", - "момент", - "монах", - "море", - "мост", - "мотор", - "мохнатый", - "мочь", - "мошенник", - "мощный", - "мрачный", - "мстить", - "мудрый", - "мужчина", - "музыка", - "мука", - "мумия", - "мундир", - "муравей", - "мусор", - "мутный", - "муфта", - "муха", - "мучить", - "мушкетер", - "мыло", - "мысль", - "мыть", - "мычать", - "мышь", - "мэтр", - "мюзикл", - "мягкий", - "мякиш", - "мясо", - "мятый", - "мячик", - "набор", - "навык", - "нагрузка", - "надежда", - "наемный", - "нажать", - "называть", - "наивный", - "накрыть", - "налог", - "намерен", - "наносить", - "написать", - "народ", - "натура", - "наука", - "нация", - "начать", - "небо", - "невеста", - "негодяй", - "неделя", - "нежный", - "незнание", - "нелепый", - "немалый", - "неправда", - "нервный", - "нести", - "нефть", - "нехватка", - "нечистый", - "неясный", - "нива", - "нижний", - "низкий", - "никель", - "нирвана", - "нить", - "ничья", - "ниша", - "нищий", - "новый", - "нога", - "ножницы", - "ноздря", - "ноль", - "номер", - "норма", - "нота", - "ночь", - "ноша", - "ноябрь", - "нрав", - "нужный", - "нутро", - "нынешний", - "нырнуть", - "ныть", - "нюанс", - "нюхать", - "няня", - "оазис", - "обаяние", - "обвинять", - "обгонять", - "обещать", - "обжигать", - "обзор", - "обида", - "область", - "обмен", - "обнимать", - "оборона", - "образ", - "обучение", - "обходить", - "обширный", - "общий", - "объект", - "обычный", - "обязать", - "овальный", - "овес", - "овощи", - "овраг", - "овца", - "овчарка", - "огненный", - "огонь", - "огромный", - "огурец", - "одежда", - "одинокий", - "одобрить", - "ожидать", - "ожог", - "озарение", - "озеро", - "означать", - "оказать", - "океан", - "оклад", - "окно", - "округ", - "октябрь", - "окурок", - "олень", - "опасный", - "операция", - "описать", - "оплата", - "опора", - "оппонент", - "опрос", - "оптимизм", - "опускать", - "опыт", - "орать", - "орбита", - "орган", - "орден", - "орел", - "оригинал", - "оркестр", - "орнамент", - "оружие", - "осадок", - "освещать", - "осень", - "осина", - "осколок", - "осмотр", - "основной", - "особый", - "осуждать", - "отбор", - "отвечать", - "отдать", - "отец", - "отзыв", - "открытие", - "отмечать", - "относить", - "отпуск", - "отрасль", - "отставка", - "оттенок", - "отходить", - "отчет", - "отъезд", - "офицер", - "охапка", - "охота", - "охрана", - "оценка", - "очаг", - "очередь", - "очищать", - "очки", - "ошейник", - "ошибка", - "ощущение", - "павильон", - "падать", - "паек", - "пакет", - "палец", - "память", - "панель", - "папка", - "партия", - "паспорт", - "патрон", - "пауза", - "пафос", - "пахнуть", - "пациент", - "пачка", - "пашня", - "певец", - "педагог", - "пейзаж", - "пельмень", - "пенсия", - "пепел", - "период", - "песня", - "петля", - "пехота", - "печать", - "пешеход", - "пещера", - "пианист", - "пиво", - "пиджак", - "пиковый", - "пилот", - "пионер", - "пирог", - "писать", - "пить", - "пицца", - "пишущий", - "пища", - "план", - "плечо", - "плита", - "плохой", - "плыть", - "плюс", - "пляж", - "победа", - "повод", - "погода", - "подумать", - "поехать", - "пожимать", - "позиция", - "поиск", - "покой", - "получать", - "помнить", - "пони", - "поощрять", - "попадать", - "порядок", - "пост", - "поток", - "похожий", - "поцелуй", - "почва", - "пощечина", - "поэт", - "пояснить", - "право", - "предмет", - "проблема", - "пруд", - "прыгать", - "прямой", - "психолог", - "птица", - "публика", - "пугать", - "пудра", - "пузырь", - "пуля", - "пункт", - "пурга", - "пустой", - "путь", - "пухлый", - "пучок", - "пушистый", - "пчела", - "пшеница", - "пыль", - "пытка", - "пыхтеть", - "пышный", - "пьеса", - "пьяный", - "пятно", - "работа", - "равный", - "радость", - "развитие", - "район", - "ракета", - "рамка", - "ранний", - "рапорт", - "рассказ", - "раунд", - "рация", - "рвать", - "реальный", - "ребенок", - "реветь", - "регион", - "редакция", - "реестр", - "режим", - "резкий", - "рейтинг", - "река", - "религия", - "ремонт", - "рента", - "реплика", - "ресурс", - "реформа", - "рецепт", - "речь", - "решение", - "ржавый", - "рисунок", - "ритм", - "рифма", - "робкий", - "ровный", - "рогатый", - "родитель", - "рождение", - "розовый", - "роковой", - "роль", - "роман", - "ронять", - "рост", - "рота", - "роща", - "рояль", - "рубль", - "ругать", - "руда", - "ружье", - "руины", - "рука", - "руль", - "румяный", - "русский", - "ручка", - "рыба", - "рывок", - "рыдать", - "рыжий", - "рынок", - "рысь", - "рыть", - "рыхлый", - "рыцарь", - "рычаг", - "рюкзак", - "рюмка", - "рябой", - "рядовой", - "сабля", - "садовый", - "сажать", - "салон", - "самолет", - "сани", - "сапог", - "сарай", - "сатира", - "сауна", - "сахар", - "сбегать", - "сбивать", - "сбор", - "сбыт", - "свадьба", - "свет", - "свидание", - "свобода", - "связь", - "сгорать", - "сдвигать", - "сеанс", - "северный", - "сегмент", - "седой", - "сезон", - "сейф", - "секунда", - "сельский", - "семья", - "сентябрь", - "сердце", - "сеть", - "сечение", - "сеять", - "сигнал", - "сидеть", - "сизый", - "сила", - "символ", - "синий", - "сирота", - "система", - "ситуация", - "сиять", - "сказать", - "скважина", - "скелет", - "скидка", - "склад", - "скорый", - "скрывать", - "скучный", - "слава", - "слеза", - "слияние", - "слово", - "случай", - "слышать", - "слюна", - "смех", - "смирение", - "смотреть", - "смутный", - "смысл", - "смятение", - "снаряд", - "снег", - "снижение", - "сносить", - "снять", - "событие", - "совет", - "согласие", - "сожалеть", - "сойти", - "сокол", - "солнце", - "сомнение", - "сонный", - "сообщать", - "соперник", - "сорт", - "состав", - "сотня", - "соус", - "социолог", - "сочинять", - "союз", - "спать", - "спешить", - "спина", - "сплошной", - "способ", - "спутник", - "средство", - "срок", - "срывать", - "стать", - "ствол", - "стена", - "стихи", - "сторона", - "страна", - "студент", - "стыд", - "субъект", - "сувенир", - "сугроб", - "судьба", - "суета", - "суждение", - "сукно", - "сулить", - "сумма", - "сунуть", - "супруг", - "суровый", - "сустав", - "суть", - "сухой", - "суша", - "существо", - "сфера", - "схема", - "сцена", - "счастье", - "счет", - "считать", - "сшивать", - "съезд", - "сынок", - "сыпать", - "сырье", - "сытый", - "сыщик", - "сюжет", - "сюрприз", - "таблица", - "таежный", - "таинство", - "тайна", - "такси", - "талант", - "таможня", - "танец", - "тарелка", - "таскать", - "тахта", - "тачка", - "таять", - "тварь", - "твердый", - "творить", - "театр", - "тезис", - "текст", - "тело", - "тема", - "тень", - "теория", - "теплый", - "терять", - "тесный", - "тетя", - "техника", - "течение", - "тигр", - "типичный", - "тираж", - "титул", - "тихий", - "тишина", - "ткань", - "товарищ", - "толпа", - "тонкий", - "топливо", - "торговля", - "тоска", - "точка", - "тощий", - "традиция", - "тревога", - "трибуна", - "трогать", - "труд", - "трюк", - "тряпка", - "туалет", - "тугой", - "туловище", - "туман", - "тундра", - "тупой", - "турнир", - "тусклый", - "туфля", - "туча", - "туша", - "тыкать", - "тысяча", - "тьма", - "тюльпан", - "тюрьма", - "тяга", - "тяжелый", - "тянуть", - "убеждать", - "убирать", - "убогий", - "убыток", - "уважение", - "уверять", - "увлекать", - "угнать", - "угол", - "угроза", - "удар", - "удивлять", - "удобный", - "уезд", - "ужас", - "ужин", - "узел", - "узкий", - "узнавать", - "узор", - "уйма", - "уклон", - "укол", - "уксус", - "улетать", - "улица", - "улучшать", - "улыбка", - "уметь", - "умиление", - "умный", - "умолять", - "умысел", - "унижать", - "уносить", - "уныние", - "упасть", - "уплата", - "упор", - "упрекать", - "упускать", - "уран", - "урна", - "уровень", - "усадьба", - "усердие", - "усилие", - "ускорять", - "условие", - "усмешка", - "уснуть", - "успеть", - "усыпать", - "утешать", - "утка", - "уточнять", - "утро", - "утюг", - "уходить", - "уцелеть", - "участие", - "ученый", - "учитель", - "ушко", - "ущерб", - "уютный", - "уяснять", - "фабрика", - "фаворит", - "фаза", - "файл", - "факт", - "фамилия", - "фантазия", - "фара", - "фасад", - "февраль", - "фельдшер", - "феномен", - "ферма", - "фигура", - "физика", - "фильм", - "финал", - "фирма", - "фишка", - "флаг", - "флейта", - "флот", - "фокус", - "фольклор", - "фонд", - "форма", - "фото", - "фраза", - "фреска", - "фронт", - "фрукт", - "функция", - "фуражка", - "футбол", - "фыркать", - "халат", - "хамство", - "хаос", - "характер", - "хата", - "хватать", - "хвост", - "хижина", - "хилый", - "химия", - "хирург", - "хитрый", - "хищник", - "хлам", - "хлеб", - "хлопать", - "хмурый", - "ходить", - "хозяин", - "хоккей", - "холодный", - "хороший", - "хотеть", - "хохотать", - "храм", - "хрен", - "хриплый", - "хроника", - "хрупкий", - "художник", - "хулиган", - "хутор", - "царь", - "цвет", - "цель", - "цемент", - "центр", - "цепь", - "церковь", - "цикл", - "цилиндр", - "циничный", - "цирк", - "цистерна", - "цитата", - "цифра", - "цыпленок", - "чадо", - "чайник", - "часть", - "чашка", - "человек", - "чемодан", - "чепуха", - "черный", - "честь", - "четкий", - "чехол", - "чиновник", - "число", - "читать", - "членство", - "чреватый", - "чтение", - "чувство", - "чугунный", - "чудо", - "чужой", - "чукча", - "чулок", - "чума", - "чуткий", - "чучело", - "чушь", - "шаблон", - "шагать", - "шайка", - "шакал", - "шалаш", - "шампунь", - "шанс", - "шапка", - "шарик", - "шасси", - "шатер", - "шахта", - "шашлык", - "швейный", - "швырять", - "шевелить", - "шедевр", - "шейка", - "шелковый", - "шептать", - "шерсть", - "шестерка", - "шикарный", - "шинель", - "шипеть", - "широкий", - "шить", - "шишка", - "шкаф", - "школа", - "шкура", - "шланг", - "шлем", - "шлюпка", - "шляпа", - "шнур", - "шоколад", - "шорох", - "шоссе", - "шофер", - "шпага", - "шпион", - "шприц", - "шрам", - "шрифт", - "штаб", - "штора", - "штраф", - "штука", - "штык", - "шуба", - "шуметь", - "шуршать", - "шутка", - "щадить", - "щедрый", - "щека", - "щель", - "щенок", - "щепка", - "щетка", - "щука", - "эволюция", - "эгоизм", - "экзамен", - "экипаж", - "экономия", - "экран", - "эксперт", - "элемент", - "элита", - "эмблема", - "эмигрант", - "эмоция", - "энергия", - "эпизод", - "эпоха", - "эскиз", - "эссе", - "эстрада", - "этап", - "этика", - "этюд", - "эфир", - "эффект", - "эшелон", - "юбилей", - "юбка", - "южный", - "юмор", - "юноша", - "юрист", - "яблоко", - "явление", - "ягода", - "ядерный", - "ядовитый", - "ядро", - "язва", - "язык", - "яйцо", - "якорь", - "январь", - "японец", - "яркий", - "ярмарка", - "ярость", - "ярус", - "ясный", - "яхта", - "ячейка", - "ящик" - ]; -} \ No newline at end of file diff --git a/cw_haven/lib/mnemonics/spanish.dart b/cw_haven/lib/mnemonics/spanish.dart deleted file mode 100644 index 531eafd35..000000000 --- a/cw_haven/lib/mnemonics/spanish.dart +++ /dev/null @@ -1,1630 +0,0 @@ -class SpanishMnemonics { - static const words = [ - "ábaco", - "abdomen", - "abeja", - "abierto", - "abogado", - "abono", - "aborto", - "abrazo", - "abrir", - "abuelo", - "abuso", - "acabar", - "academia", - "acceso", - "acción", - "aceite", - "acelga", - "acento", - "aceptar", - "ácido", - "aclarar", - "acné", - "acoger", - "acoso", - "activo", - "acto", - "actriz", - "actuar", - "acudir", - "acuerdo", - "acusar", - "adicto", - "admitir", - "adoptar", - "adorno", - "aduana", - "adulto", - "aéreo", - "afectar", - "afición", - "afinar", - "afirmar", - "ágil", - "agitar", - "agonía", - "agosto", - "agotar", - "agregar", - "agrio", - "agua", - "agudo", - "águila", - "aguja", - "ahogo", - "ahorro", - "aire", - "aislar", - "ajedrez", - "ajeno", - "ajuste", - "alacrán", - "alambre", - "alarma", - "alba", - "álbum", - "alcalde", - "aldea", - "alegre", - "alejar", - "alerta", - "aleta", - "alfiler", - "alga", - "algodón", - "aliado", - "aliento", - "alivio", - "alma", - "almeja", - "almíbar", - "altar", - "alteza", - "altivo", - "alto", - "altura", - "alumno", - "alzar", - "amable", - "amante", - "amapola", - "amargo", - "amasar", - "ámbar", - "ámbito", - "ameno", - "amigo", - "amistad", - "amor", - "amparo", - "amplio", - "ancho", - "anciano", - "ancla", - "andar", - "andén", - "anemia", - "ángulo", - "anillo", - "ánimo", - "anís", - "anotar", - "antena", - "antiguo", - "antojo", - "anual", - "anular", - "anuncio", - "añadir", - "añejo", - "año", - "apagar", - "aparato", - "apetito", - "apio", - "aplicar", - "apodo", - "aporte", - "apoyo", - "aprender", - "aprobar", - "apuesta", - "apuro", - "arado", - "araña", - "arar", - "árbitro", - "árbol", - "arbusto", - "archivo", - "arco", - "arder", - "ardilla", - "arduo", - "área", - "árido", - "aries", - "armonía", - "arnés", - "aroma", - "arpa", - "arpón", - "arreglo", - "arroz", - "arruga", - "arte", - "artista", - "asa", - "asado", - "asalto", - "ascenso", - "asegurar", - "aseo", - "asesor", - "asiento", - "asilo", - "asistir", - "asno", - "asombro", - "áspero", - "astilla", - "astro", - "astuto", - "asumir", - "asunto", - "atajo", - "ataque", - "atar", - "atento", - "ateo", - "ático", - "atleta", - "átomo", - "atraer", - "atroz", - "atún", - "audaz", - "audio", - "auge", - "aula", - "aumento", - "ausente", - "autor", - "aval", - "avance", - "avaro", - "ave", - "avellana", - "avena", - "avestruz", - "avión", - "aviso", - "ayer", - "ayuda", - "ayuno", - "azafrán", - "azar", - "azote", - "azúcar", - "azufre", - "azul", - "baba", - "babor", - "bache", - "bahía", - "baile", - "bajar", - "balanza", - "balcón", - "balde", - "bambú", - "banco", - "banda", - "baño", - "barba", - "barco", - "barniz", - "barro", - "báscula", - "bastón", - "basura", - "batalla", - "batería", - "batir", - "batuta", - "baúl", - "bazar", - "bebé", - "bebida", - "bello", - "besar", - "beso", - "bestia", - "bicho", - "bien", - "bingo", - "blanco", - "bloque", - "blusa", - "boa", - "bobina", - "bobo", - "boca", - "bocina", - "boda", - "bodega", - "boina", - "bola", - "bolero", - "bolsa", - "bomba", - "bondad", - "bonito", - "bono", - "bonsái", - "borde", - "borrar", - "bosque", - "bote", - "botín", - "bóveda", - "bozal", - "bravo", - "brazo", - "brecha", - "breve", - "brillo", - "brinco", - "brisa", - "broca", - "broma", - "bronce", - "brote", - "bruja", - "brusco", - "bruto", - "buceo", - "bucle", - "bueno", - "buey", - "bufanda", - "bufón", - "búho", - "buitre", - "bulto", - "burbuja", - "burla", - "burro", - "buscar", - "butaca", - "buzón", - "caballo", - "cabeza", - "cabina", - "cabra", - "cacao", - "cadáver", - "cadena", - "caer", - "café", - "caída", - "caimán", - "caja", - "cajón", - "cal", - "calamar", - "calcio", - "caldo", - "calidad", - "calle", - "calma", - "calor", - "calvo", - "cama", - "cambio", - "camello", - "camino", - "campo", - "cáncer", - "candil", - "canela", - "canguro", - "canica", - "canto", - "caña", - "cañón", - "caoba", - "caos", - "capaz", - "capitán", - "capote", - "captar", - "capucha", - "cara", - "carbón", - "cárcel", - "careta", - "carga", - "cariño", - "carne", - "carpeta", - "carro", - "carta", - "casa", - "casco", - "casero", - "caspa", - "castor", - "catorce", - "catre", - "caudal", - "causa", - "cazo", - "cebolla", - "ceder", - "cedro", - "celda", - "célebre", - "celoso", - "célula", - "cemento", - "ceniza", - "centro", - "cerca", - "cerdo", - "cereza", - "cero", - "cerrar", - "certeza", - "césped", - "cetro", - "chacal", - "chaleco", - "champú", - "chancla", - "chapa", - "charla", - "chico", - "chiste", - "chivo", - "choque", - "choza", - "chuleta", - "chupar", - "ciclón", - "ciego", - "cielo", - "cien", - "cierto", - "cifra", - "cigarro", - "cima", - "cinco", - "cine", - "cinta", - "ciprés", - "circo", - "ciruela", - "cisne", - "cita", - "ciudad", - "clamor", - "clan", - "claro", - "clase", - "clave", - "cliente", - "clima", - "clínica", - "cobre", - "cocción", - "cochino", - "cocina", - "coco", - "código", - "codo", - "cofre", - "coger", - "cohete", - "cojín", - "cojo", - "cola", - "colcha", - "colegio", - "colgar", - "colina", - "collar", - "colmo", - "columna", - "combate", - "comer", - "comida", - "cómodo", - "compra", - "conde", - "conejo", - "conga", - "conocer", - "consejo", - "contar", - "copa", - "copia", - "corazón", - "corbata", - "corcho", - "cordón", - "corona", - "correr", - "coser", - "cosmos", - "costa", - "cráneo", - "cráter", - "crear", - "crecer", - "creído", - "crema", - "cría", - "crimen", - "cripta", - "crisis", - "cromo", - "crónica", - "croqueta", - "crudo", - "cruz", - "cuadro", - "cuarto", - "cuatro", - "cubo", - "cubrir", - "cuchara", - "cuello", - "cuento", - "cuerda", - "cuesta", - "cueva", - "cuidar", - "culebra", - "culpa", - "culto", - "cumbre", - "cumplir", - "cuna", - "cuneta", - "cuota", - "cupón", - "cúpula", - "curar", - "curioso", - "curso", - "curva", - "cutis", - "dama", - "danza", - "dar", - "dardo", - "dátil", - "deber", - "débil", - "década", - "decir", - "dedo", - "defensa", - "definir", - "dejar", - "delfín", - "delgado", - "delito", - "demora", - "denso", - "dental", - "deporte", - "derecho", - "derrota", - "desayuno", - "deseo", - "desfile", - "desnudo", - "destino", - "desvío", - "detalle", - "detener", - "deuda", - "día", - "diablo", - "diadema", - "diamante", - "diana", - "diario", - "dibujo", - "dictar", - "diente", - "dieta", - "diez", - "difícil", - "digno", - "dilema", - "diluir", - "dinero", - "directo", - "dirigir", - "disco", - "diseño", - "disfraz", - "diva", - "divino", - "doble", - "doce", - "dolor", - "domingo", - "don", - "donar", - "dorado", - "dormir", - "dorso", - "dos", - "dosis", - "dragón", - "droga", - "ducha", - "duda", - "duelo", - "dueño", - "dulce", - "dúo", - "duque", - "durar", - "dureza", - "duro", - "ébano", - "ebrio", - "echar", - "eco", - "ecuador", - "edad", - "edición", - "edificio", - "editor", - "educar", - "efecto", - "eficaz", - "eje", - "ejemplo", - "elefante", - "elegir", - "elemento", - "elevar", - "elipse", - "élite", - "elixir", - "elogio", - "eludir", - "embudo", - "emitir", - "emoción", - "empate", - "empeño", - "empleo", - "empresa", - "enano", - "encargo", - "enchufe", - "encía", - "enemigo", - "enero", - "enfado", - "enfermo", - "engaño", - "enigma", - "enlace", - "enorme", - "enredo", - "ensayo", - "enseñar", - "entero", - "entrar", - "envase", - "envío", - "época", - "equipo", - "erizo", - "escala", - "escena", - "escolar", - "escribir", - "escudo", - "esencia", - "esfera", - "esfuerzo", - "espada", - "espejo", - "espía", - "esposa", - "espuma", - "esquí", - "estar", - "este", - "estilo", - "estufa", - "etapa", - "eterno", - "ética", - "etnia", - "evadir", - "evaluar", - "evento", - "evitar", - "exacto", - "examen", - "exceso", - "excusa", - "exento", - "exigir", - "exilio", - "existir", - "éxito", - "experto", - "explicar", - "exponer", - "extremo", - "fábrica", - "fábula", - "fachada", - "fácil", - "factor", - "faena", - "faja", - "falda", - "fallo", - "falso", - "faltar", - "fama", - "familia", - "famoso", - "faraón", - "farmacia", - "farol", - "farsa", - "fase", - "fatiga", - "fauna", - "favor", - "fax", - "febrero", - "fecha", - "feliz", - "feo", - "feria", - "feroz", - "fértil", - "fervor", - "festín", - "fiable", - "fianza", - "fiar", - "fibra", - "ficción", - "ficha", - "fideo", - "fiebre", - "fiel", - "fiera", - "fiesta", - "figura", - "fijar", - "fijo", - "fila", - "filete", - "filial", - "filtro", - "fin", - "finca", - "fingir", - "finito", - "firma", - "flaco", - "flauta", - "flecha", - "flor", - "flota", - "fluir", - "flujo", - "flúor", - "fobia", - "foca", - "fogata", - "fogón", - "folio", - "folleto", - "fondo", - "forma", - "forro", - "fortuna", - "forzar", - "fosa", - "foto", - "fracaso", - "frágil", - "franja", - "frase", - "fraude", - "freír", - "freno", - "fresa", - "frío", - "frito", - "fruta", - "fuego", - "fuente", - "fuerza", - "fuga", - "fumar", - "función", - "funda", - "furgón", - "furia", - "fusil", - "fútbol", - "futuro", - "gacela", - "gafas", - "gaita", - "gajo", - "gala", - "galería", - "gallo", - "gamba", - "ganar", - "gancho", - "ganga", - "ganso", - "garaje", - "garza", - "gasolina", - "gastar", - "gato", - "gavilán", - "gemelo", - "gemir", - "gen", - "género", - "genio", - "gente", - "geranio", - "gerente", - "germen", - "gesto", - "gigante", - "gimnasio", - "girar", - "giro", - "glaciar", - "globo", - "gloria", - "gol", - "golfo", - "goloso", - "golpe", - "goma", - "gordo", - "gorila", - "gorra", - "gota", - "goteo", - "gozar", - "grada", - "gráfico", - "grano", - "grasa", - "gratis", - "grave", - "grieta", - "grillo", - "gripe", - "gris", - "grito", - "grosor", - "grúa", - "grueso", - "grumo", - "grupo", - "guante", - "guapo", - "guardia", - "guerra", - "guía", - "guiño", - "guion", - "guiso", - "guitarra", - "gusano", - "gustar", - "haber", - "hábil", - "hablar", - "hacer", - "hacha", - "hada", - "hallar", - "hamaca", - "harina", - "haz", - "hazaña", - "hebilla", - "hebra", - "hecho", - "helado", - "helio", - "hembra", - "herir", - "hermano", - "héroe", - "hervir", - "hielo", - "hierro", - "hígado", - "higiene", - "hijo", - "himno", - "historia", - "hocico", - "hogar", - "hoguera", - "hoja", - "hombre", - "hongo", - "honor", - "honra", - "hora", - "hormiga", - "horno", - "hostil", - "hoyo", - "hueco", - "huelga", - "huerta", - "hueso", - "huevo", - "huida", - "huir", - "humano", - "húmedo", - "humilde", - "humo", - "hundir", - "huracán", - "hurto", - "icono", - "ideal", - "idioma", - "ídolo", - "iglesia", - "iglú", - "igual", - "ilegal", - "ilusión", - "imagen", - "imán", - "imitar", - "impar", - "imperio", - "imponer", - "impulso", - "incapaz", - "índice", - "inerte", - "infiel", - "informe", - "ingenio", - "inicio", - "inmenso", - "inmune", - "innato", - "insecto", - "instante", - "interés", - "íntimo", - "intuir", - "inútil", - "invierno", - "ira", - "iris", - "ironía", - "isla", - "islote", - "jabalí", - "jabón", - "jamón", - "jarabe", - "jardín", - "jarra", - "jaula", - "jazmín", - "jefe", - "jeringa", - "jinete", - "jornada", - "joroba", - "joven", - "joya", - "juerga", - "jueves", - "juez", - "jugador", - "jugo", - "juguete", - "juicio", - "junco", - "jungla", - "junio", - "juntar", - "júpiter", - "jurar", - "justo", - "juvenil", - "juzgar", - "kilo", - "koala", - "labio", - "lacio", - "lacra", - "lado", - "ladrón", - "lagarto", - "lágrima", - "laguna", - "laico", - "lamer", - "lámina", - "lámpara", - "lana", - "lancha", - "langosta", - "lanza", - "lápiz", - "largo", - "larva", - "lástima", - "lata", - "látex", - "latir", - "laurel", - "lavar", - "lazo", - "leal", - "lección", - "leche", - "lector", - "leer", - "legión", - "legumbre", - "lejano", - "lengua", - "lento", - "leña", - "león", - "leopardo", - "lesión", - "letal", - "letra", - "leve", - "leyenda", - "libertad", - "libro", - "licor", - "líder", - "lidiar", - "lienzo", - "liga", - "ligero", - "lima", - "límite", - "limón", - "limpio", - "lince", - "lindo", - "línea", - "lingote", - "lino", - "linterna", - "líquido", - "liso", - "lista", - "litera", - "litio", - "litro", - "llaga", - "llama", - "llanto", - "llave", - "llegar", - "llenar", - "llevar", - "llorar", - "llover", - "lluvia", - "lobo", - "loción", - "loco", - "locura", - "lógica", - "logro", - "lombriz", - "lomo", - "lonja", - "lote", - "lucha", - "lucir", - "lugar", - "lujo", - "luna", - "lunes", - "lupa", - "lustro", - "luto", - "luz", - "maceta", - "macho", - "madera", - "madre", - "maduro", - "maestro", - "mafia", - "magia", - "mago", - "maíz", - "maldad", - "maleta", - "malla", - "malo", - "mamá", - "mambo", - "mamut", - "manco", - "mando", - "manejar", - "manga", - "maniquí", - "manjar", - "mano", - "manso", - "manta", - "mañana", - "mapa", - "máquina", - "mar", - "marco", - "marea", - "marfil", - "margen", - "marido", - "mármol", - "marrón", - "martes", - "marzo", - "masa", - "máscara", - "masivo", - "matar", - "materia", - "matiz", - "matriz", - "máximo", - "mayor", - "mazorca", - "mecha", - "medalla", - "medio", - "médula", - "mejilla", - "mejor", - "melena", - "melón", - "memoria", - "menor", - "mensaje", - "mente", - "menú", - "mercado", - "merengue", - "mérito", - "mes", - "mesón", - "meta", - "meter", - "método", - "metro", - "mezcla", - "miedo", - "miel", - "miembro", - "miga", - "mil", - "milagro", - "militar", - "millón", - "mimo", - "mina", - "minero", - "mínimo", - "minuto", - "miope", - "mirar", - "misa", - "miseria", - "misil", - "mismo", - "mitad", - "mito", - "mochila", - "moción", - "moda", - "modelo", - "moho", - "mojar", - "molde", - "moler", - "molino", - "momento", - "momia", - "monarca", - "moneda", - "monja", - "monto", - "moño", - "morada", - "morder", - "moreno", - "morir", - "morro", - "morsa", - "mortal", - "mosca", - "mostrar", - "motivo", - "mover", - "móvil", - "mozo", - "mucho", - "mudar", - "mueble", - "muela", - "muerte", - "muestra", - "mugre", - "mujer", - "mula", - "muleta", - "multa", - "mundo", - "muñeca", - "mural", - "muro", - "músculo", - "museo", - "musgo", - "música", - "muslo", - "nácar", - "nación", - "nadar", - "naipe", - "naranja", - "nariz", - "narrar", - "nasal", - "natal", - "nativo", - "natural", - "náusea", - "naval", - "nave", - "navidad", - "necio", - "néctar", - "negar", - "negocio", - "negro", - "neón", - "nervio", - "neto", - "neutro", - "nevar", - "nevera", - "nicho", - "nido", - "niebla", - "nieto", - "niñez", - "niño", - "nítido", - "nivel", - "nobleza", - "noche", - "nómina", - "noria", - "norma", - "norte", - "nota", - "noticia", - "novato", - "novela", - "novio", - "nube", - "nuca", - "núcleo", - "nudillo", - "nudo", - "nuera", - "nueve", - "nuez", - "nulo", - "número", - "nutria", - "oasis", - "obeso", - "obispo", - "objeto", - "obra", - "obrero", - "observar", - "obtener", - "obvio", - "oca", - "ocaso", - "océano", - "ochenta", - "ocho", - "ocio", - "ocre", - "octavo", - "octubre", - "oculto", - "ocupar", - "ocurrir", - "odiar", - "odio", - "odisea", - "oeste", - "ofensa", - "oferta", - "oficio", - "ofrecer", - "ogro", - "oído", - "oír", - "ojo", - "ola", - "oleada", - "olfato", - "olivo", - "olla", - "olmo", - "olor", - "olvido", - "ombligo", - "onda", - "onza", - "opaco", - "opción", - "ópera", - "opinar", - "oponer", - "optar", - "óptica", - "opuesto", - "oración", - "orador", - "oral", - "órbita", - "orca", - "orden", - "oreja", - "órgano", - "orgía", - "orgullo", - "oriente", - "origen", - "orilla", - "oro", - "orquesta", - "oruga", - "osadía", - "oscuro", - "osezno", - "oso", - "ostra", - "otoño", - "otro", - "oveja", - "óvulo", - "óxido", - "oxígeno", - "oyente", - "ozono", - "pacto", - "padre", - "paella", - "página", - "pago", - "país", - "pájaro", - "palabra", - "palco", - "paleta", - "pálido", - "palma", - "paloma", - "palpar", - "pan", - "panal", - "pánico", - "pantera", - "pañuelo", - "papá", - "papel", - "papilla", - "paquete", - "parar", - "parcela", - "pared", - "parir", - "paro", - "párpado", - "parque", - "párrafo", - "parte", - "pasar", - "paseo", - "pasión", - "paso", - "pasta", - "pata", - "patio", - "patria", - "pausa", - "pauta", - "pavo", - "payaso", - "peatón", - "pecado", - "pecera", - "pecho", - "pedal", - "pedir", - "pegar", - "peine", - "pelar", - "peldaño", - "pelea", - "peligro", - "pellejo", - "pelo", - "peluca", - "pena", - "pensar", - "peñón", - "peón", - "peor", - "pepino", - "pequeño", - "pera", - "percha", - "perder", - "pereza", - "perfil", - "perico", - "perla", - "permiso", - "perro", - "persona", - "pesa", - "pesca", - "pésimo", - "pestaña", - "pétalo", - "petróleo", - "pez", - "pezuña", - "picar", - "pichón", - "pie", - "piedra", - "pierna", - "pieza", - "pijama", - "pilar", - "piloto", - "pimienta", - "pino", - "pintor", - "pinza", - "piña", - "piojo", - "pipa", - "pirata", - "pisar", - "piscina", - "piso", - "pista", - "pitón", - "pizca", - "placa", - "plan", - "plata", - "playa", - "plaza", - "pleito", - "pleno", - "plomo", - "pluma", - "plural", - "pobre", - "poco", - "poder", - "podio", - "poema", - "poesía", - "poeta", - "polen", - "policía", - "pollo", - "polvo", - "pomada", - "pomelo", - "pomo", - "pompa", - "poner", - "porción", - "portal", - "posada", - "poseer", - "posible", - "poste", - "potencia", - "potro", - "pozo", - "prado", - "precoz", - "pregunta", - "premio", - "prensa", - "preso", - "previo", - "primo", - "príncipe", - "prisión", - "privar", - "proa", - "probar", - "proceso", - "producto", - "proeza", - "profesor", - "programa", - "prole", - "promesa", - "pronto", - "propio", - "próximo", - "prueba", - "público", - "puchero", - "pudor", - "pueblo", - "puerta", - "puesto", - "pulga", - "pulir", - "pulmón", - "pulpo", - "pulso", - "puma", - "punto", - "puñal", - "puño", - "pupa", - "pupila", - "puré", - "quedar", - "queja", - "quemar", - "querer", - "queso", - "quieto", - "química", - "quince", - "quitar", - "rábano", - "rabia", - "rabo", - "ración", - "radical", - "raíz", - "rama", - "rampa", - "rancho", - "rango", - "rapaz", - "rápido", - "rapto", - "rasgo", - "raspa", - "rato", - "rayo", - "raza", - "razón", - "reacción", - "realidad", - "rebaño", - "rebote", - "recaer", - "receta", - "rechazo", - "recoger", - "recreo", - "recto", - "recurso", - "red", - "redondo", - "reducir", - "reflejo", - "reforma", - "refrán", - "refugio", - "regalo", - "regir", - "regla", - "regreso", - "rehén", - "reino", - "reír", - "reja", - "relato", - "relevo", - "relieve", - "relleno", - "reloj", - "remar", - "remedio", - "remo", - "rencor", - "rendir", - "renta", - "reparto", - "repetir", - "reposo", - "reptil", - "res", - "rescate", - "resina", - "respeto", - "resto", - "resumen", - "retiro", - "retorno", - "retrato", - "reunir", - "revés", - "revista", - "rey", - "rezar", - "rico", - "riego", - "rienda", - "riesgo", - "rifa", - "rígido", - "rigor", - "rincón", - "riñón", - "río", - "riqueza", - "risa", - "ritmo", - "rito" - ]; -} \ No newline at end of file diff --git a/cw_haven/lib/pending_haven_transaction.dart b/cw_haven/lib/pending_haven_transaction.dart deleted file mode 100644 index dbf075044..000000000 --- a/cw_haven/lib/pending_haven_transaction.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:cw_haven/api/structs/pending_transaction.dart'; -import 'package:cw_haven/api/transaction_history.dart' - as haven_transaction_history; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/amount_converter.dart'; -import 'package:cw_core/pending_transaction.dart'; - -class DoubleSpendException implements Exception { - DoubleSpendException(); - - @override - String toString() => - 'This transaction cannot be committed. This can be due to many reasons including the wallet not being synced, there is not enough XMR in your available balance, or previous transactions are not yet fully processed.'; -} - -class PendingHavenTransaction with PendingTransaction { - PendingHavenTransaction(this.pendingTransactionDescription, this.cryptoCurrency); - - final PendingTransactionDescription pendingTransactionDescription; - final CryptoCurrency cryptoCurrency; - - @override - String get id => pendingTransactionDescription.hash; - - @override - String get hex => ''; - - @override - String get amountFormatted => AmountConverter.amountIntToString( - cryptoCurrency, pendingTransactionDescription.amount); - - @override - String get feeFormatted => AmountConverter.amountIntToString( - cryptoCurrency, pendingTransactionDescription.fee); - - @override - Future commit() async { - try { - haven_transaction_history.commitTransactionFromPointerAddress( - address: pendingTransactionDescription.pointerAddress); - } catch (e) { - final message = e.toString(); - - if (message.contains('Reason: double spend')) { - throw DoubleSpendException(); - } - - rethrow; - } - } - - @override - Future commitUR() { - throw UnimplementedError(); - } -} diff --git a/cw_haven/lib/update_haven_rate.dart b/cw_haven/lib/update_haven_rate.dart deleted file mode 100644 index a7d5b2c77..000000000 --- a/cw_haven/lib/update_haven_rate.dart +++ /dev/null @@ -1,15 +0,0 @@ -//import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/monero_amount_format.dart'; -import 'package:cw_haven/api/balance_list.dart'; - -//Future updateHavenRate(FiatConversionStore fiatConversionStore) async { -// final rate = getRate(); -// final base = rate.firstWhere((row) => row.getAssetType() == 'XUSD', orElse: () => null); -// rate.forEach((row) { -// final cur = CryptoCurrency.fromString(row.getAssetType()); -// final baseRate = moneroAmountToDouble(amount: base.getRate()); -// final rowRate = moneroAmountToDouble(amount: row.getRate()); -// fiatConversionStore.prices[cur] = baseRate * rowRate; -// }); -//} \ No newline at end of file diff --git a/cw_haven/pubspec.lock b/cw_haven/pubspec.lock deleted file mode 100644 index da5a11b89..000000000 --- a/cw_haven/pubspec.lock +++ /dev/null @@ -1,789 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8" - url: "https://pub.dev" - source: hosted - version: "47.0.0" - analyzer: - dependency: transitive - description: - name: analyzer - sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80" - url: "https://pub.dev" - source: hosted - version: "4.7.0" - args: - dependency: transitive - description: - name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 - url: "https://pub.dev" - source: hosted - version: "2.6.0" - asn1lib: - dependency: transitive - description: - name: asn1lib - sha256: "6b151826fcc95ff246cd219a0bf4c753ea14f4081ad71c61939becf3aba27f70" - url: "https://pub.dev" - source: hosted - version: "1.5.5" - async: - dependency: transitive - description: - name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" - url: "https://pub.dev" - source: hosted - version: "2.11.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - build: - dependency: transitive - description: - name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - build_config: - dependency: transitive - description: - name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 - url: "https://pub.dev" - source: hosted - version: "1.1.1" - build_daemon: - dependency: transitive - description: - name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" - url: "https://pub.dev" - source: hosted - version: "4.0.2" - build_resolvers: - dependency: "direct dev" - description: - name: build_resolvers - sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6" - url: "https://pub.dev" - source: hosted - version: "2.0.10" - build_runner: - dependency: "direct dev" - description: - name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" - url: "https://pub.dev" - source: hosted - version: "2.4.13" - build_runner_core: - dependency: transitive - description: - name: build_runner_core - sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" - url: "https://pub.dev" - source: hosted - version: "7.2.10" - built_collection: - dependency: transitive - description: - name: built_collection - sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" - url: "https://pub.dev" - source: hosted - version: "5.1.1" - built_value: - dependency: transitive - description: - name: built_value - sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb - url: "https://pub.dev" - source: hosted - version: "8.9.2" - cake_backup: - dependency: transitive - description: - path: "." - ref: main - resolved-ref: "3aba867dcab6737f6707782f5db15d71f303db38" - url: "https://github.com/cake-tech/cake_backup.git" - source: git - version: "1.0.0+1" - characters: - dependency: transitive - description: - name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" - url: "https://pub.dev" - source: hosted - version: "1.3.0" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff - url: "https://pub.dev" - source: hosted - version: "2.0.3" - clock: - dependency: transitive - description: - name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.dev" - source: hosted - version: "1.1.1" - code_builder: - dependency: transitive - description: - name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 - url: "https://pub.dev" - source: hosted - version: "4.10.0" - collection: - dependency: transitive - description: - name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a - url: "https://pub.dev" - source: hosted - version: "1.18.0" - convert: - dependency: transitive - description: - name: convert - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - crypto: - dependency: transitive - description: - name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" - url: "https://pub.dev" - source: hosted - version: "3.0.6" - cryptography: - dependency: transitive - description: - name: cryptography - sha256: d146b76d33d94548cf035233fbc2f4338c1242fa119013bead807d033fc4ae05 - url: "https://pub.dev" - source: hosted - version: "2.7.0" - cupertino_icons: - dependency: transitive - description: - name: cupertino_icons - sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 - url: "https://pub.dev" - source: hosted - version: "1.0.8" - cw_core: - dependency: "direct main" - description: - path: "../cw_core" - relative: true - source: path - version: "0.0.1" - dart_style: - dependency: transitive - description: - name: dart_style - sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" - url: "https://pub.dev" - source: hosted - version: "2.2.4" - decimal: - dependency: transitive - description: - name: decimal - sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21" - url: "https://pub.dev" - source: hosted - version: "2.3.3" - encrypt: - dependency: transitive - description: - name: encrypt - sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" - url: "https://pub.dev" - source: hosted - version: "5.0.3" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.dev" - source: hosted - version: "1.3.1" - ffi: - dependency: "direct main" - description: - name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" - url: "https://pub.dev" - source: hosted - version: "2.1.3" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_mobx: - dependency: "direct main" - description: - name: flutter_mobx - sha256: "859fbf452fa9c2519d2700b125dd7fb14c508bbdd7fb65e26ca8ff6c92280e2e" - url: "https://pub.dev" - source: hosted - version: "2.2.1+1" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - frontend_server_client: - dependency: transitive - description: - name: frontend_server_client - sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 - url: "https://pub.dev" - source: hosted - version: "4.0.0" - glob: - dependency: transitive - description: - name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - graphs: - dependency: transitive - description: - name: graphs - sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - hive: - dependency: transitive - description: - name: hive - sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" - url: "https://pub.dev" - source: hosted - version: "2.2.3" - hive_generator: - dependency: "direct dev" - description: - name: hive_generator - sha256: "81fd20125cb2ce8fd23623d7744ffbaf653aae93706c9bd3bf7019ea0ace3938" - url: "https://pub.dev" - source: hosted - version: "1.1.3" - http: - dependency: "direct main" - description: - name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 - url: "https://pub.dev" - source: hosted - version: "1.2.2" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" - url: "https://pub.dev" - source: hosted - version: "4.0.2" - intl: - dependency: "direct main" - description: - name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf - url: "https://pub.dev" - source: hosted - version: "0.19.0" - io: - dependency: transitive - description: - name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" - url: "https://pub.dev" - source: hosted - version: "1.0.4" - js: - dependency: transitive - description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" - source: hosted - version: "0.6.7" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" - url: "https://pub.dev" - source: hosted - version: "4.9.0" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" - url: "https://pub.dev" - source: hosted - version: "10.0.5" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" - url: "https://pub.dev" - source: hosted - version: "3.0.5" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" - url: "https://pub.dev" - source: hosted - version: "3.0.1" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb - url: "https://pub.dev" - source: hosted - version: "0.12.16+1" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" - source: hosted - version: "0.11.1" - meta: - dependency: transitive - description: - name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 - url: "https://pub.dev" - source: hosted - version: "1.15.0" - mime: - dependency: transitive - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - mobx: - dependency: "direct main" - description: - name: mobx - sha256: "63920b27b32ad1910adfe767ab1750e4c212e8923232a1f891597b362074ea5e" - url: "https://pub.dev" - source: hosted - version: "2.3.3+2" - mobx_codegen: - dependency: "direct dev" - description: - name: mobx_codegen - sha256: d4beb9cea4b7b014321235f8fdc7c2193ee0fe1d1198e9da7403f8bc85c4407c - url: "https://pub.dev" - source: hosted - version: "2.3.0" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - package_config: - dependency: transitive - description: - name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - path: - dependency: transitive - description: - name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" - url: "https://pub.dev" - source: hosted - version: "1.9.0" - path_provider: - dependency: "direct main" - description: - name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 - url: "https://pub.dev" - source: hosted - version: "2.1.4" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a - url: "https://pub.dev" - source: hosted - version: "2.2.12" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 - url: "https://pub.dev" - source: hosted - version: "2.4.0" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" - url: "https://pub.dev" - source: hosted - version: "3.9.1" - pool: - dependency: transitive - description: - name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" - url: "https://pub.dev" - source: hosted - version: "1.5.1" - provider: - dependency: transitive - description: - name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c - url: "https://pub.dev" - source: hosted - version: "6.1.2" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - pubspec_parse: - dependency: transitive - description: - name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - rational: - dependency: transitive - description: - name: rational - sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336 - url: "https://pub.dev" - source: hosted - version: "2.2.3" - shelf: - dependency: transitive - description: - name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 - url: "https://pub.dev" - source: hosted - version: "1.4.1" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - socks5_proxy: - dependency: transitive - description: - name: socks5_proxy - sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" - url: "https://pub.dev" - source: hosted - version: "1.0.6" - source_gen: - dependency: transitive - description: - name: source_gen - sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" - url: "https://pub.dev" - source: hosted - version: "1.2.6" - source_helper: - dependency: transitive - description: - name: source_helper - sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - source_span: - dependency: transitive - description: - name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" - url: "https://pub.dev" - source: hosted - version: "1.10.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" - url: "https://pub.dev" - source: hosted - version: "1.11.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 - url: "https://pub.dev" - source: hosted - version: "2.1.2" - stream_transform: - dependency: transitive - description: - name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.dev" - source: hosted - version: "1.2.1" - test_api: - dependency: transitive - description: - name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" - url: "https://pub.dev" - source: hosted - version: "0.7.2" - timing: - dependency: transitive - description: - name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - tuple: - dependency: transitive - description: - name: tuple - sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 - url: "https://pub.dev" - source: hosted - version: "2.0.2" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - unorm_dart: - dependency: transitive - description: - name: unorm_dart - sha256: "23d8bf65605401a6a32cff99435fed66ef3dab3ddcad3454059165df46496a3b" - url: "https://pub.dev" - source: hosted - version: "0.3.0" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc - url: "https://pub.dev" - source: hosted - version: "14.2.4" - watcher: - dependency: "direct overridden" - description: - name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - web: - dependency: transitive - description: - name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb - url: "https://pub.dev" - source: hosted - version: "1.1.0" - web_socket: - dependency: transitive - description: - name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" - url: "https://pub.dev" - source: hosted - version: "0.1.6" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" - url: "https://pub.dev" - source: hosted - version: "3.0.1" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - yaml: - dependency: transitive - description: - name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" - url: "https://pub.dev" - source: hosted - version: "3.1.2" -sdks: - dart: ">=3.5.0 <4.0.0" - flutter: ">=3.24.0" diff --git a/cw_haven/pubspec.yaml b/cw_haven/pubspec.yaml deleted file mode 100644 index 452fed93a..000000000 --- a/cw_haven/pubspec.yaml +++ /dev/null @@ -1,81 +0,0 @@ -name: cw_haven -description: A new flutter plugin project. -version: 0.0.1 -publish_to: none -author: Cake Wallet -homepage: https://cakewallet.com - -environment: - sdk: ">=2.17.5 <3.0.0" - flutter: ">=1.20.0" - -dependencies: - flutter: - sdk: flutter - ffi: ^2.0.1 - http: ^1.1.0 - path_provider: ^2.0.11 - mobx: ^2.0.7+4 - flutter_mobx: ^2.0.6+1 - intl: ^0.19.0 - cw_core: - path: ../cw_core - -dev_dependencies: - flutter_test: - sdk: flutter - build_runner: ^2.4.7 - mobx_codegen: ^2.0.7 - build_resolvers: ^2.0.9 - hive_generator: ^1.1.3 - -dependency_overrides: - watcher: ^1.1.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. -flutter: - # This section identifies this Flutter project as a plugin project. - # The 'pluginClass' and Android 'package' identifiers should not ordinarily - # be modified. They are used by the tooling to maintain consistency when - # adding or updating assets for this project. - plugin: - platforms: - android: - package: com.cakewallet.cw_haven - pluginClass: CwHavenPlugin - ios: - pluginClass: CwHavenPlugin - - # To add assets to your plugin package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # To add custom fonts to your plugin package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/custom-fonts/#from-packages diff --git a/integration_test/helpers/test_helpers.dart b/integration_test/helpers/test_helpers.dart index 979ff60ff..caa4ebd27 100644 --- a/integration_test/helpers/test_helpers.dart +++ b/integration_test/helpers/test_helpers.dart @@ -78,7 +78,6 @@ class TestHelpers { static MockHiveInterface getAndRegisterHiveInterface() { _removeRegistrationIfExists(); final service = MockHiveInterface(); - final box = MockHiveBox(); getIt.registerSingleton(service); return service; } diff --git a/integration_test/robots/create_pin_welcome_page_robot.dart b/integration_test/robots/create_pin_welcome_page_robot.dart index ca136cb38..d11750d7a 100644 --- a/integration_test/robots/create_pin_welcome_page_robot.dart +++ b/integration_test/robots/create_pin_welcome_page_robot.dart @@ -21,10 +21,6 @@ class CreatePinWelcomePageRobot { title = S.current.monero_com; } - if (isHaven) { - title = S.current.haven_app; - } - title = S.current.cake_wallet; commonTestCases.hasText(title); @@ -36,10 +32,6 @@ class CreatePinWelcomePageRobot { description = S.current.monero_com_wallet_text; } - if (isHaven) { - description = S.current.haven_app_wallet_text; - } - description = S.current.new_first_wallet_text; commonTestCases.hasText(description); diff --git a/integration_test/robots/restore_from_seed_or_key_robot.dart b/integration_test/robots/restore_from_seed_or_key_robot.dart index 015a9e46f..db23e50cb 100644 --- a/integration_test/robots/restore_from_seed_or_key_robot.dart +++ b/integration_test/robots/restore_from_seed_or_key_robot.dart @@ -1,9 +1,7 @@ import 'package:cake_wallet/entities/seed_type.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/restore/wallet_restore_page.dart'; -import 'package:cake_wallet/src/widgets/seed_widget.dart'; import 'package:cake_wallet/src/widgets/validable_annotated_editable_text.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import '../components/common_test_cases.dart'; diff --git a/integration_test/test_suites/confirm_seeds_flow_test.dart b/integration_test/test_suites/confirm_seeds_flow_test.dart index a62ce3f60..6716c8055 100644 --- a/integration_test/test_suites/confirm_seeds_flow_test.dart +++ b/integration_test/test_suites/confirm_seeds_flow_test.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'package:cake_wallet/wallet_types.g.dart'; import 'package:cw_core/wallet_type.dart'; @@ -7,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../components/common_test_cases.dart'; import '../components/common_test_constants.dart'; import '../components/common_test_flows.dart'; import '../robots/auth_page_robot.dart'; diff --git a/integration_test/test_suites/restore_wallet_through_seeds_flow_test.dart b/integration_test/test_suites/restore_wallet_through_seeds_flow_test.dart index 0589d16ba..d8d873361 100644 --- a/integration_test/test_suites/restore_wallet_through_seeds_flow_test.dart +++ b/integration_test/test_suites/restore_wallet_through_seeds_flow_test.dart @@ -1,7 +1,5 @@ -import 'dart:io'; import 'package:cake_wallet/wallet_types.g.dart'; -import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/lib/anypay/any_pay_payment.dart b/lib/anypay/any_pay_payment.dart index 0657b0526..b37e17f3e 100644 --- a/lib/anypay/any_pay_payment.dart +++ b/lib/anypay/any_pay_payment.dart @@ -1,5 +1,4 @@ import 'package:cake_wallet/anypay/any_pay_chain.dart'; -import 'package:flutter/foundation.dart'; import 'package:cake_wallet/anypay/any_pay_payment_instruction.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/monero/monero.dart'; diff --git a/lib/anypay/any_pay_payment_committed_info.dart b/lib/anypay/any_pay_payment_committed_info.dart index 12adea003..5d3db4cf1 100644 --- a/lib/anypay/any_pay_payment_committed_info.dart +++ b/lib/anypay/any_pay_payment_committed_info.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:cake_wallet/anypay/any_pay_trasnaction.dart'; class AnyPayPaymentCommittedInfo { diff --git a/lib/anypay/any_pay_payment_instruction.dart b/lib/anypay/any_pay_payment_instruction.dart index 5181cad75..178f9be93 100644 --- a/lib/anypay/any_pay_payment_instruction.dart +++ b/lib/anypay/any_pay_payment_instruction.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:cake_wallet/anypay/any_pay_payment_instruction_output.dart'; class AnyPayPaymentInstruction { diff --git a/lib/buy/buy_amount.dart b/lib/buy/buy_amount.dart index e41bb1148..fdb440a70 100644 --- a/lib/buy/buy_amount.dart +++ b/lib/buy/buy_amount.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; class BuyAmount { BuyAmount({ diff --git a/lib/buy/buy_exception.dart b/lib/buy/buy_exception.dart index c201b3b2d..b312af330 100644 --- a/lib/buy/buy_exception.dart +++ b/lib/buy/buy_exception.dart @@ -1,5 +1,3 @@ -import 'package:flutter/foundation.dart'; -import 'package:cake_wallet/buy/buy_provider_description.dart'; class BuyException implements Exception { BuyException({required this.title, required this.content}); diff --git a/lib/buy/get_buy_provider_icon.dart b/lib/buy/get_buy_provider_icon.dart index c755d9615..e477b7e00 100644 --- a/lib/buy/get_buy_provider_icon.dart +++ b/lib/buy/get_buy_provider_icon.dart @@ -10,16 +10,12 @@ Image? getBuyProviderIcon(BuyProviderDescription providerDescription, Image.asset('assets/images/moonpay-icon.png', color: iconColor, width: 36, height: 34); - if (providerDescription != null) { - switch (providerDescription) { - case BuyProviderDescription.wyre: - return _wyreIcon; - case BuyProviderDescription.moonPay: - return _moonPayIcon; - default: - return null; - } - } else { - return null; + switch (providerDescription) { + case BuyProviderDescription.wyre: + return _wyreIcon; + case BuyProviderDescription.moonPay: + return _moonPayIcon; + default: + return null; } } \ No newline at end of file diff --git a/lib/core/monero_account_label_validator.dart b/lib/core/monero_account_label_validator.dart index ef4cf0f9a..fe98ed15c 100644 --- a/lib/core/monero_account_label_validator.dart +++ b/lib/core/monero_account_label_validator.dart @@ -1,7 +1,5 @@ -import 'package:flutter/foundation.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/core/validator.dart'; -import 'package:cw_core/crypto_currency.dart'; class MoneroLabelValidator extends TextValidator { MoneroLabelValidator() diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index 5356ed875..344b5391d 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -2,7 +2,6 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/validator.dart'; import 'package:cake_wallet/entities/mnemonic_item.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; -import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; @@ -32,8 +31,6 @@ class SeedValidator extends Validator { return getBitcoinWordList(language); case WalletType.monero: return monero!.getMoneroWordList(language); - case WalletType.haven: - return haven!.getMoneroWordList(language); case WalletType.ethereum: return ethereum!.getEthereumWordList(language); case WalletType.bitcoinCash: @@ -54,6 +51,7 @@ class SeedValidator extends Validator { case WalletType.decred: return decred!.getDecredWordList(); case WalletType.none: + case WalletType.haven: return []; } } diff --git a/lib/di.dart b/lib/di.dart index 5c6d11e4b..8d9b045df 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -50,13 +50,9 @@ import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart'; import 'package:cake_wallet/src/screens/settings/mweb_logs_page.dart'; import 'package:cake_wallet/src/screens/settings/mweb_node_page.dart'; import 'package:cake_wallet/src/screens/welcome/welcome_page.dart'; -import 'package:cake_wallet/view_model/link_view_model.dart'; -import 'package:cake_wallet/tron/tron.dart'; -import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:cake_wallet/view_model/send/fees_view_model.dart'; -import 'package:cw_core/receive_page_option.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/entities/template.dart'; @@ -65,7 +61,6 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/cake_pay/cake_pay_card.dart'; import 'package:cake_wallet/exchange/exchange_template.dart'; import 'package:cake_wallet/exchange/trade.dart'; -import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; @@ -863,7 +858,6 @@ Future setup({ (AccountListItem? account, _) => MoneroAccountEditOrCreateViewModel( monero!.getAccountList(getIt.get().wallet!), wownero?.getAccountList(getIt.get().wallet!), - haven?.getAccountList(getIt.get().wallet!), wallet: getIt.get().wallet!, accountListItem: account)); @@ -1078,8 +1072,6 @@ Future setup({ getIt.registerFactoryParam((WalletType param1, __) { switch (param1) { - case WalletType.haven: - return haven!.createHavenWalletService(_walletInfoSource); case WalletType.monero: return monero!.createMoneroWalletService(_walletInfoSource, _unspentCoinsInfoSource); case WalletType.bitcoin: @@ -1120,6 +1112,7 @@ Future setup({ case WalletType.decred: return decred!.createDecredWalletService(_walletInfoSource, _unspentCoinsInfoSource); case WalletType.none: + case WalletType.haven: throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService'); } }); diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 5ed35b5a1..45234d5ec 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -5,7 +5,6 @@ import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/haven_seed_store.dart'; -import 'package:cake_wallet/haven/haven.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cake_wallet/entities/secret_store_key.dart'; import 'package:cw_core/root_dir.dart'; @@ -423,7 +422,7 @@ Future defaultSettingsMigration( ); break; case 45: - await _backupHavenSeeds(havenSeedStore); + // await _backupHavenSeeds(havenSeedStore); addWalletNodeList(nodes: nodes, type: WalletType.polygon); addWalletNodeList(nodes: nodes, type: WalletType.ethereum); @@ -546,14 +545,6 @@ Future _updateNode({ } } -Future _backupHavenSeeds(Box havenSeedStore) async { - final future = haven?.backupHavenSeeds(havenSeedStore); - if (future != null) { - await future; - } - return; -} - /// generic function for changing any wallet default node /// instead of making a new function for each change Future _changeDefaultNode({ diff --git a/lib/entities/priority_for_wallet_type.dart b/lib/entities/priority_for_wallet_type.dart index f7165500f..5307250d5 100644 --- a/lib/entities/priority_for_wallet_type.dart +++ b/lib/entities/priority_for_wallet_type.dart @@ -1,7 +1,6 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; -import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/wownero/wownero.dart'; @@ -20,8 +19,6 @@ List priorityForWalletType(WalletType type) { return bitcoin!.getTransactionPriorities(); case WalletType.litecoin: return bitcoin!.getLitecoinTransactionPriorities(); - case WalletType.haven: - return haven!.getTransactionPriorities(); case WalletType.ethereum: return ethereum!.getTransactionPriorities(); case WalletType.bitcoinCash: @@ -38,7 +35,8 @@ List priorityForWalletType(WalletType type) { return zano!.getTransactionPriorities(); case WalletType.decred: return decred!.getTransactionPriorities(); - default: + case WalletType.none: + case WalletType.haven: return []; } } diff --git a/lib/entities/qr_scanner.dart b/lib/entities/qr_scanner.dart index 91c63a574..c4d37ad6c 100644 --- a/lib/entities/qr_scanner.dart +++ b/lib/entities/qr_scanner.dart @@ -1,9 +1,7 @@ import 'dart:math'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/main.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:fast_scanner/fast_scanner.dart'; diff --git a/lib/entities/seed_phrase_length.dart b/lib/entities/seed_phrase_length.dart index 65e9fac40..e2c93c025 100644 --- a/lib/entities/seed_phrase_length.dart +++ b/lib/entities/seed_phrase_length.dart @@ -1,4 +1,3 @@ -import 'package:cake_wallet/generated/i18n.dart'; enum SeedPhraseLength { twelveWords(12), diff --git a/lib/entities/update_haven_rate.dart b/lib/entities/update_haven_rate.dart deleted file mode 100644 index 7c04154b1..000000000 --- a/lib/entities/update_haven_rate.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/monero_amount_format.dart'; -import 'package:cake_wallet/haven/haven.dart'; - -Future updateHavenRate(FiatConversionStore fiatConversionStore) async { - try { - final rate = haven!.getAssetRate(); - final base = rate.firstWhere((row) => row.asset == 'XUSD'); - - rate.forEach((row) { - final cur = CryptoCurrency.fromString(row.asset); - final baseRate = moneroAmountToDouble(amount: base.rate); - final rowRate = moneroAmountToDouble(amount: row.rate); - - if (cur == CryptoCurrency.xusd) { - fiatConversionStore.prices[cur] = 1.0; - return; - } - - fiatConversionStore.prices[cur] = baseRate / rowRate; - }); - } catch(_) { - // FIX-ME: handle exception - } -} \ No newline at end of file diff --git a/lib/reactions/fiat_rate_update.dart b/lib/reactions/fiat_rate_update.dart index 2ddb1c6f2..dbc5f9ae4 100644 --- a/lib/reactions/fiat_rate_update.dart +++ b/lib/reactions/fiat_rate_update.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:cake_wallet/core/fiat_conversion_service.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; -import 'package:cake_wallet/entities/update_haven_rate.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; @@ -28,15 +27,11 @@ Future startFiatRateUpdate( return; } - if (appStore.wallet!.type == WalletType.haven) { - await updateHavenRate(fiatConversionStore); - } else { - fiatConversionStore.prices[appStore.wallet!.currency] = - await FiatConversionService.fetchPrice( - crypto: appStore.wallet!.currency, - fiat: settingsStore.fiatCurrency, - torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly); - } + fiatConversionStore.prices[appStore.wallet!.currency] = + await FiatConversionService.fetchPrice( + crypto: appStore.wallet!.currency, + fiat: settingsStore.fiatCurrency, + torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly); Iterable? currencies; if (appStore.wallet!.type == WalletType.ethereum) { diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 513c97c4e..b1786b743 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -1,7 +1,6 @@ import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; -import 'package:cake_wallet/entities/update_haven_rate.dart'; import 'package:cake_wallet/entities/wallet_manager.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/polygon/polygon.dart'; @@ -85,10 +84,6 @@ void startCurrentWalletChangeReaction( await wallet.connectToPowNode(node: powNode); } - if (wallet.type == WalletType.haven) { - await updateHavenRate(fiatConversionStore); - } - if (wallet.walletInfo.address.isEmpty) { wallet.walletInfo.address = wallet.walletAddresses.address; diff --git a/lib/reactions/on_wallet_sync_status_change.dart b/lib/reactions/on_wallet_sync_status_change.dart index fc618dabd..bf2e8ed0d 100644 --- a/lib/reactions/on_wallet_sync_status_change.dart +++ b/lib/reactions/on_wallet_sync_status_change.dart @@ -1,7 +1,5 @@ -import 'package:cake_wallet/entities/update_haven_rate.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/wallet_base.dart'; @@ -20,10 +18,6 @@ void startWalletSyncStatusChangeReaction( try { if (status is ConnectedSyncStatus) { await wallet.startSync(); - - if (wallet.type == WalletType.haven) { - await updateHavenRate(fiatConversionStore); - } } if (status is SyncingSyncStatus || status is ProcessingSyncStatus) { await WakelockPlus.enable(); diff --git a/lib/src/screens/buy/widgets/buy_list_item.dart b/lib/src/screens/buy/widgets/buy_list_item.dart index d8c457ac0..d41325ce8 100644 --- a/lib/src/screens/buy/widgets/buy_list_item.dart +++ b/lib/src/screens/buy/widgets/buy_list_item.dart @@ -1,5 +1,4 @@ import 'package:cake_wallet/buy/buy_provider.dart'; -import 'package:cake_wallet/buy/get_buy_provider_icon.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/palette.dart'; diff --git a/lib/src/screens/welcome/create_pin_welcome_page.dart b/lib/src/screens/welcome/create_pin_welcome_page.dart index ddf3b8d5b..d4230668a 100644 --- a/lib/src/screens/welcome/create_pin_welcome_page.dart +++ b/lib/src/screens/welcome/create_pin_welcome_page.dart @@ -26,10 +26,6 @@ class CreatePinWelcomePage extends BasePage { return S.of(context).monero_com; } - if (isHaven) { - return S.of(context).haven_app; - } - return S.of(context).cake_wallet; } @@ -38,10 +34,6 @@ class CreatePinWelcomePage extends BasePage { return S.of(context).monero_com_wallet_text; } - if (isHaven) { - return S.of(context).haven_app_wallet_text; - } - return S.of(context).new_first_wallet_text; } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 49c0dbc59..a4408a935 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -32,7 +32,6 @@ import 'package:cake_wallet/view_model/dashboard/order_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart'; import 'package:cake_wallet/view_model/settings/sync_mode.dart'; -import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cryptography/cryptography.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/cake_hive.dart'; @@ -70,8 +69,8 @@ abstract class DashboardViewModelBase with Store { required this.anonpayTransactionsStore, required this.sharedPreferences, required this.keyService}) - : hasTradeAction = false, - hasSwapAction = false, + : hasTradeAction = true, + hasSwapAction = true, isShowFirstYatIntroduction = false, isShowSecondYatIntroduction = false, isShowThirdYatIntroduction = false, @@ -176,7 +175,6 @@ abstract class DashboardViewModelBase with Store { isShowFirstYatIntroduction = false; isShowSecondYatIntroduction = false; isShowThirdYatIntroduction = false; - updateActions(); final _wallet = wallet; @@ -664,7 +662,6 @@ abstract class DashboardViewModelBase with Store { this.wallet = wallet; type = wallet.type; name = wallet.name; - updateActions(); if (wallet.type == WalletType.monero) { subname = monero!.getCurrentAccount(wallet).label; @@ -800,11 +797,6 @@ abstract class DashboardViewModelBase with Store { } } - void updateActions() { - hasSwapAction = !isHaven; - hasTradeAction = !isHaven; - } - @computed SyncMode get syncMode => settingsStore.currentSyncMode; diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 83953f9f1..808ccabea 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -16,7 +16,6 @@ import 'package:cw_core/transaction_info.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; @@ -172,13 +171,6 @@ class TransactionListItem extends ActionListItem with Keyable { cryptoAmount: bitcoin!.formatterBitcoinAmountToDouble(amount: transaction.amount), price: price); break; - case WalletType.haven: - final asset = haven!.assetOfTransaction(transaction); - final price = balanceViewModel.fiatConvertationStore.prices[asset]; - amount = calculateFiatAmountRaw( - cryptoAmount: haven!.formatterMoneroAmountToDouble(amount: transaction.amount), - price: price); - break; case WalletType.ethereum: final asset = ethereum!.assetOfTransaction(balanceViewModel.wallet, transaction); final price = balanceViewModel.fiatConvertationStore.prices[asset]; @@ -234,6 +226,7 @@ class TransactionListItem extends ActionListItem with Keyable { break; case WalletType.none: case WalletType.banano: + case WalletType.haven: break; } diff --git a/lib/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart b/lib/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart index 8d626e258..8eb034029 100644 --- a/lib/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart +++ b/lib/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart @@ -1,11 +1,9 @@ import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; part 'monero_account_edit_or_create_view_model.g.dart'; @@ -14,7 +12,7 @@ class MoneroAccountEditOrCreateViewModel = MoneroAccountEditOrCreateViewModelBas with _$MoneroAccountEditOrCreateViewModel; abstract class MoneroAccountEditOrCreateViewModelBase with Store { - MoneroAccountEditOrCreateViewModelBase(this._moneroAccountList, this._wowneroAccountList, this._havenAccountList, + MoneroAccountEditOrCreateViewModelBase(this._moneroAccountList, this._wowneroAccountList, {required WalletBase wallet, AccountListItem? accountListItem}) : state = InitialExecutionState(), isEdit = accountListItem != null, @@ -32,7 +30,6 @@ abstract class MoneroAccountEditOrCreateViewModelBase with Store { final MoneroAccountList _moneroAccountList; final WowneroAccountList? _wowneroAccountList; - final HavenAccountList? _havenAccountList; final AccountListItem? _accountListItem; final WalletBase _wallet; @@ -41,10 +38,6 @@ abstract class MoneroAccountEditOrCreateViewModelBase with Store { await saveMonero(); } - if (_wallet.type == WalletType.haven) { - await saveHaven(); - } - if (_wallet.type == WalletType.wownero) { await saveWownero(); } @@ -57,7 +50,7 @@ abstract class MoneroAccountEditOrCreateViewModelBase with Store { if (_accountListItem != null) { await _moneroAccountList.setLabelAccount( _wallet, - accountIndex: _accountListItem!.id, + accountIndex: _accountListItem.id, label: label); } else { await _moneroAccountList.addAccount( @@ -72,32 +65,6 @@ abstract class MoneroAccountEditOrCreateViewModelBase with Store { } } - Future saveHaven() async { - if (!(_wallet.type == WalletType.haven)) { - return; - } - - try { - state = IsExecutingState(); - - if (_accountListItem != null) { - await _havenAccountList!.setLabelAccount( - _wallet, - accountIndex: _accountListItem!.id, - label: label); - } else { - await _havenAccountList!.addAccount( - _wallet, - label: label); - } - - await _wallet.save(); - state = ExecutedSuccessfullyState(); - } catch (e) { - state = FailureState(e.toString()); - } - } - Future saveWownero() async { try { state = IsExecutingState(); @@ -105,7 +72,7 @@ abstract class MoneroAccountEditOrCreateViewModelBase with Store { if (_accountListItem != null) { await _wowneroAccountList?.setLabelAccount( _wallet, - accountIndex: _accountListItem!.id, + accountIndex: _accountListItem.id, label: label); } else { await _wowneroAccountList?.addAccount( diff --git a/lib/view_model/monero_account_list/monero_account_list_view_model.dart b/lib/view_model/monero_account_list/monero_account_list_view_model.dart index 448106779..4ecffcfb3 100644 --- a/lib/view_model/monero_account_list/monero_account_list_view_model.dart +++ b/lib/view_model/monero_account_list/monero_account_list_view_model.dart @@ -5,7 +5,6 @@ import 'package:mobx/mobx.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/haven/haven.dart'; part 'monero_account_list_view_model.g.dart'; @@ -27,16 +26,6 @@ abstract class MoneroAccountListViewModelBase with Store { @computed List get accounts { - if (_wallet.type == WalletType.haven) { - return haven - !.getAccountList(_wallet) - .accounts.map((acc) => AccountListItem( - label: acc.label, - id: acc.id, - isSelected: acc.id == haven!.getCurrentAccount(_wallet).id)) - .toList(); - } - if (_wallet.type == WalletType.monero) { return monero !.getAccountList(_wallet) @@ -82,12 +71,5 @@ abstract class MoneroAccountListViewModelBase with Store { item.balance, ); } - - if (_wallet.type == WalletType.haven) { - haven!.setCurrentAccount( - _wallet, - item.id, - item.label); - } } } diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index fdaaeba0d..a59bf5852 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -4,7 +4,6 @@ import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; -import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; @@ -105,9 +104,6 @@ abstract class OutputBase with Store { case WalletType.decred: _amount = decred!.formatterStringDoubleToDecredAmount(_cryptoAmount); break; - case WalletType.haven: - _amount = haven!.formatterMoneroParseAmount(amount: _cryptoAmount); - break; case WalletType.ethereum: _amount = ethereum!.formatterEthereumParseAmount(_cryptoAmount); break; @@ -120,7 +116,12 @@ abstract class OutputBase with Store { case WalletType.zano: _amount = zano!.formatterParseAmount(amount: _cryptoAmount, currency: cryptoCurrencyHandler()); break; - default: + case WalletType.none: + case WalletType.haven: + case WalletType.nano: + case WalletType.banano: + case WalletType.solana: + case WalletType.tron: break; } @@ -177,10 +178,6 @@ abstract class OutputBase with Store { return wownero!.formatterWowneroAmountToDouble(amount: fee); } - if (_wallet.type == WalletType.haven) { - return haven!.formatterMoneroAmountToDouble(amount: fee); - } - if (_wallet.type == WalletType.ethereum) { return ethereum!.formatterEthereumAmountToDouble(amount: BigInt.from(fee)); } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 3bf85ef2f..52a8b06db 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -1,6 +1,5 @@ import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/evm_transaction_error_fees_handler.dart'; -import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; @@ -25,7 +24,6 @@ import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; import 'package:cw_core/exceptions.dart'; import 'package:cw_core/transaction_info.dart'; -import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cake_wallet/view_model/send/send_template_view_model.dart'; @@ -49,9 +47,7 @@ import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:collection/collection.dart'; part 'send_view_model.g.dart'; @@ -521,10 +517,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor return wownero! .createWowneroTransactionCreationCredentials(outputs: outputs, priority: priority!); - case WalletType.haven: - return haven!.createHavenTransactionCreationCredentials( - outputs: outputs, priority: priority!, assetType: selectedCryptoCurrency.title); - case WalletType.ethereum: return ethereum!.createEthereumTransactionCredentials(outputs, priority: priority!, currency: selectedCryptoCurrency); diff --git a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart index 3a678ff0f..c417f6f4e 100644 --- a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart @@ -4,7 +4,6 @@ import 'package:mobx/mobx.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/decred/decred.dart'; import 'package:cw_core/wallet_type.dart'; @@ -105,13 +104,6 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { wallet.walletAddresses.manualAddresses.add(addr); await wallet.save(); } - - if (wallet.type == WalletType.haven) { - await haven! - .getSubaddressList(wallet) - .addSubaddress(wallet, accountIndex: haven!.getCurrentAccount(wallet).id, label: label); - await wallet.save(); - } } Future _update() async { @@ -137,11 +129,6 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { accountIndex: wownero!.getCurrentAccount(wallet).id, addressIndex: index, label: label); await wallet.save(); } - if (wallet.type == WalletType.haven) { - await haven!.getSubaddressList(wallet).setLabelSubaddress(wallet, - accountIndex: haven!.getCurrentAccount(wallet).id, addressIndex: index, label: label); - await wallet.save(); - } } } } diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 0b0873a60..5b9be9109 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -8,7 +8,6 @@ import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; @@ -27,7 +26,6 @@ import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_i import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/amount_converter.dart'; import 'package:cw_core/currency.dart'; -import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:intl/intl.dart'; import 'package:mobx/mobx.dart'; @@ -377,20 +375,6 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo addressList.addAll(addressItems); } - if (wallet.type == WalletType.haven) { - final primaryAddress = haven!.getSubaddressList(wallet).subaddresses.first; - final addressItems = haven!.getSubaddressList(wallet).subaddresses.map((subaddress) { - final isPrimary = subaddress == primaryAddress; - - return WalletAddressListItem( - id: subaddress.id, - isPrimary: isPrimary, - name: subaddress.label, - address: subaddress.address); - }); - addressList.addAll(addressItems); - } - if (isElectrumWallet) { if (bitcoin!.hasSelectedSilentPayments(wallet)) { final addressItems = bitcoin!.getSilentPaymentAddresses(wallet).map((address) { @@ -556,10 +540,6 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo wownero! .getSubaddressList(wallet) .update(wallet, accountIndex: wownero!.getCurrentAccount(wallet).id); - } else if (wallet.type == WalletType.haven) { - haven! - .getSubaddressList(wallet) - .update(wallet, accountIndex: haven!.getCurrentAccount(wallet).id); } } @@ -573,8 +553,6 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo return monero!.getCurrentAccount(wallet).label; case WalletType.wownero: wownero!.getCurrentAccount(wallet).label; - case WalletType.haven: - return haven!.getCurrentAccount(wallet).label; default: return ''; } diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index 467b7c3a5..246227a80 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -1,5 +1,4 @@ import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; import 'package:cake_wallet/store/app_store.dart'; @@ -116,9 +115,6 @@ abstract class WalletKeysViewModelBase with Store { case WalletType.monero: keys = monero!.getKeys(_wallet); break; - case WalletType.haven: - keys = haven!.getKeys(_wallet); - break; case WalletType.wownero: keys = wownero!.getKeys(_wallet); break; @@ -166,6 +162,7 @@ abstract class WalletKeysViewModelBase with Store { case WalletType.litecoin: case WalletType.bitcoinCash: case WalletType.none: + case WalletType.haven: // final keys = bitcoin!.getWalletKeys(_appStore.wallet!); // // items.addAll([ @@ -215,9 +212,6 @@ abstract class WalletKeysViewModelBase with Store { } Future _currentHeight() async { - if (_wallet.type == WalletType.haven) { - return await haven!.getCurrentHeight(); - } if (_wallet.type == WalletType.monero) { return await monero!.getCurrentHeight(); } @@ -307,8 +301,6 @@ abstract class WalletKeysViewModelBase with Store { List _getWalletTransactions(WalletBase wallet) { if (wallet.type == WalletType.monero) { return monero!.getTransactionHistory(wallet).transactions.values.toList(); - } else if (wallet.type == WalletType.haven) { - return haven!.getTransactionHistory(wallet).transactions.values.toList(); } else if (wallet.type == WalletType.wownero) { return wownero!.getTransactionHistory(wallet).transactions.values.toList(); } @@ -318,8 +310,6 @@ abstract class WalletKeysViewModelBase with Store { int _getRestoreHeightByTransactions(WalletType type, DateTime date) { if (type == WalletType.monero) { return monero!.getHeightByDate(date: date); - } else if (type == WalletType.haven) { - return haven!.getHeightByDate(date: date); } else if (type == WalletType.wownero) { return wownero!.getHeightByDate(date: date); } diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index e82ae8773..f4117f8ab 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -5,29 +5,21 @@ import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/wownero/wownero.dart'; -import 'package:cake_wallet/zano/zano.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cake_wallet/entities/seed_type.dart'; -import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; -import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/store/app_store.dart'; -import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; -import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/decred/decred.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:hive/hive.dart'; -import 'package:mobx/mobx.dart'; import '../polygon/polygon.dart'; import 'advanced_privacy_settings_view_model.dart'; @@ -113,9 +105,6 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { passphrase: passphrase, mnemonic: newWalletArguments!.mnemonic, ); - case WalletType.haven: - return haven!.createHavenNewWalletCredentials( - name: name, language: options!.first as String, password: walletPassword); case WalletType.ethereum: return ethereum!.createEthereumNewWalletCredentials( name: name, @@ -176,6 +165,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { case WalletType.decred: return decred!.createDecredNewWalletCredentials(name: name); 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 3e005e9bc..dec0be6b1 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -4,7 +4,6 @@ import 'package:cake_wallet/core/generate_wallet_password.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; -import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; @@ -117,9 +116,6 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { derivationType: derivationInfo!.derivationType!, derivationPath: derivationInfo.derivationPath!, ); - case WalletType.haven: - return haven!.createHavenRestoreWalletFromSeedCredentials( - name: name, height: height, mnemonic: seed, password: password); case WalletType.ethereum: return ethereum!.createEthereumRestoreWalletFromSeedCredentials( name: name, @@ -187,6 +183,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { password: password, ); case WalletType.none: + case WalletType.haven: break; } } @@ -208,17 +205,6 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { language: 'English', ); - case WalletType.haven: - return haven!.createHavenRestoreWalletFromKeysCredentials( - name: name, - height: height, - spendKey: spendKey!, - viewKey: viewKey!, - address: address!, - password: password, - language: 'English', - ); - case WalletType.ethereum: return ethereum!.createEthereumRestoreWalletFromPrivateKey( name: name, diff --git a/lib/wallet_type_utils.dart b/lib/wallet_type_utils.dart index 459ca992b..093aebc8e 100644 --- a/lib/wallet_type_utils.dart +++ b/lib/wallet_type_utils.dart @@ -6,11 +6,6 @@ bool get isMoneroOnly { && availableWalletTypes.first == WalletType.monero; } -bool get isHaven { - return availableWalletTypes.length == 1 - && availableWalletTypes.first == WalletType.haven; -} - bool get isSingleCoin { return availableWalletTypes.length == 1; @@ -24,10 +19,6 @@ String get approximatedAppName { if (isMoneroOnly) { return 'Monero.com'; } - - if (isHaven) { - return 'Haven'; - } return 'Cake Wallet'; } diff --git a/model_generator.sh b/model_generator.sh index 56b891903..7a5e3bfd7 100755 --- a/model_generator.sh +++ b/model_generator.sh @@ -1,7 +1,7 @@ #!/bin/bash set -x -e -for cwcoin in cw_{core,evm,monero,bitcoin,haven,nano,bitcoin_cash,solana,tron,wownero,zano,decred} +for cwcoin in cw_{core,evm,monero,bitcoin,nano,bitcoin_cash,solana,tron,wownero,zano,decred} do if [[ "x$1" == "xasync" ]]; then diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 41095ef26..597c0aa9a 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -9,9 +9,8 @@ APP_ANDROID_SCHEME="" MONERO_COM="monero.com" CAKEWALLET="cakewallet" -HAVEN="haven" -TYPES=($MONERO_COM $CAKEWALLET $HAVEN) +TYPES=($MONERO_COM $CAKEWALLET) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" @@ -28,11 +27,6 @@ CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" -HAVEN_NAME="Haven" -HAVEN_VERSION="1.0.0" -HAVEN_BUILD_NUMBER=1 -HAVEN_BUNDLE_ID="com.cakewallet.haven" -HAVEN_PACKAGE="com.cakewallet.haven" if ! [[ " ${TYPES[*]} " =~ " ${APP_ANDROID_TYPE} " ]]; then echo "Wrong app type." diff --git a/scripts/android/app_icon.sh b/scripts/android/app_icon.sh index 9db01f227..9d7be355a 100755 --- a/scripts/android/app_icon.sh +++ b/scripts/android/app_icon.sh @@ -23,11 +23,6 @@ case $APP_ANDROID_TYPE in ANDROID_ICON=$CAKEWALLET_PATH ANDROID_ICON_SET=$CAKEWALLET_ICON_SET_PATH ;; - "haven") - APP_LOGO=$ASSETS_DIR/images/cakewallet_logo.png - ANDROID_ICON=$CAKEWALLET_PATH - ANDROID_ICON_SET=$CAKEWALLET_ICON_SET_PATH - ;; esac rm $APP_LOGO_DEST_PATH diff --git a/scripts/android/build_all.sh b/scripts/android/build_all.sh index dc730af55..25a395898 100755 --- a/scripts/android/build_all.sh +++ b/scripts/android/build_all.sh @@ -10,8 +10,6 @@ DIR=$(dirname "$0") case $APP_ANDROID_TYPE in "monero.com") $DIR/build_monero_all.sh ;; "cakewallet") $DIR/build_monero_all.sh - $DIR/build_haven_all.sh $DIR/build_mwebd.sh $DIR/build_decred.sh ;; - "haven") $DIR/build_haven_all.sh ;; esac diff --git a/scripts/android/build_haven.sh b/scripts/android/build_haven.sh deleted file mode 100755 index 16cf05039..000000000 --- a/scripts/android/build_haven.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/bin/sh - -. ./config.sh -HAVEN_VERSION=tags/v3.0.7 -HAVEN_SRC_DIR=${WORKDIR}/haven - -git clone https://github.com/haven-protocol-org/haven-main.git ${HAVEN_SRC_DIR} -cd $HAVEN_SRC_DIR -git checkout ${HAVEN_VERSION} -git submodule init -git submodule update - -for arch in "aarch" "aarch64" "i686" "x86_64" -do -FLAGS="" -PREFIX=${WORKDIR}/prefix_${arch} -DEST_LIB_DIR=${PREFIX}/lib/haven -DEST_INCLUDE_DIR=${PREFIX}/include/haven -export CMAKE_INCLUDE_PATH="${PREFIX}/include" -export CMAKE_LIBRARY_PATH="${PREFIX}/lib" -ANDROID_STANDALONE_TOOLCHAIN_PATH="${TOOLCHAIN_BASE_DIR}_${arch}" -PATH="${ANDROID_STANDALONE_TOOLCHAIN_PATH}/bin:${ORIGINAL_PATH}" - -mkdir -p $DEST_LIB_DIR -mkdir -p $DEST_INCLUDE_DIR - -case $arch in - "aarch" ) - CLANG=arm-linux-androideabi-clang - CXXLANG=arm-linux-androideabi-clang++ - BUILD_64=OFF - TAG="android-armv7" - ARCH="armv7-a" - ARCH_ABI="armeabi-v7a" - FLAGS="-D CMAKE_ANDROID_ARM_MODE=ON -D NO_AES=true";; - "aarch64" ) - CLANG=aarch64-linux-androideabi-clang - CXXLANG=aarch64-linux-androideabi-clang++ - BUILD_64=ON - TAG="android-armv8" - ARCH="armv8-a" - ARCH_ABI="arm64-v8a";; - "i686" ) - CLANG=i686-linux-androideabi-clang - CXXLANG=i686-linux-androideabi-clang++ - BUILD_64=OFF - TAG="android-x86" - ARCH="i686" - ARCH_ABI="x86";; - "x86_64" ) - CLANG=x86_64-linux-androideabi-clang - CXXLANG=x86_64-linux-androideabi-clang++ - BUILD_64=ON - TAG="android-x86_64" - ARCH="x86-64" - ARCH_ABI="x86_64";; -esac - -cd $HAVEN_SRC_DIR -rm -rf ./build/release -mkdir -p ./build/release -cd ./build/release -CC=${CLANG} CXX=${CXXLANG} cmake -D USE_DEVICE_TREZOR=OFF -D BUILD_GUI_DEPS=1 -D BUILD_TESTS=OFF -D ARCH=${ARCH} -D STATIC=ON -D BUILD_64=${BUILD_64} -D CMAKE_BUILD_TYPE=release -D ANDROID=true -D INSTALL_VENDORED_LIBUNBOUND=ON -D BUILD_TAG=${TAG} -D CMAKE_SYSTEM_NAME="Android" -D CMAKE_ANDROID_STANDALONE_TOOLCHAIN="${ANDROID_STANDALONE_TOOLCHAIN_PATH}" -D CMAKE_ANDROID_ARCH_ABI=${ARCH_ABI} $FLAGS ../.. - -make wallet_api -j$THREADS -find . -path ./lib -prune -o -name '*.a' -exec cp '{}' lib \; - -cp -r ./lib/* $DEST_LIB_DIR -cp ../../src/wallet/api/wallet2_api.h $DEST_INCLUDE_DIR -done diff --git a/scripts/android/build_haven_all.sh b/scripts/android/build_haven_all.sh deleted file mode 100755 index daadadc6b..000000000 --- a/scripts/android/build_haven_all.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -./build_iconv.sh -./build_boost.sh -./build_openssl.sh -./build_sodium.sh -./build_zmq.sh -./build_haven.sh diff --git a/scripts/android/pubspec_gen.sh b/scripts/android/pubspec_gen.sh index d3341f63d..b980f877d 100755 --- a/scripts/android/pubspec_gen.sh +++ b/scripts/android/pubspec_gen.sh @@ -11,12 +11,6 @@ case $APP_ANDROID_TYPE in ;; $CAKEWALLET) CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred" - if [ "$CW_WITH_HAVEN" = true ];then - CONFIG_ARGS="$CONFIG_ARGS --haven" - fi - ;; - $HAVEN) - CONFIG_ARGS="--haven" ;; esac diff --git a/scripts/docker/Dockerfile b/scripts/docker/Dockerfile index a352cdc71..a41e6b3a6 100755 --- a/scripts/docker/Dockerfile +++ b/scripts/docker/Dockerfile @@ -12,8 +12,6 @@ WORKDIR /opt/android/cakewallet/ # build_all.sh # build_boost.sh -# build_haven.sh -# build_haven_all.sh # build_iconv.sh # build_monero.sh # build_openssl.sh @@ -56,7 +54,5 @@ RUN ./build_zmq.sh COPY entrypoint.sh /opt/android/cakewallet/ COPY build_monero.sh /opt/android/cakewallet/ COPY copy_monero_deps.sh /opt/android/cakewallet/ -COPY build_haven.sh /opt/android/cakewallet/ -COPY copy_haven_deps.sh /opt/android/cakewallet/ ENTRYPOINT ["./entrypoint.sh"] diff --git a/scripts/docker/build_all.sh b/scripts/docker/build_all.sh index a4163c3f4..c53c3ac69 100755 --- a/scripts/docker/build_all.sh +++ b/scripts/docker/build_all.sh @@ -1,17 +1,15 @@ -#!/bin/sh - -set -x -e - -if [ -z "$APP_ANDROID_TYPE" ]; then - echo "Please set APP_ANDROID_TYPE" - exit 1 -fi - -DIR=$(dirname "$0") - -case $APP_ANDROID_TYPE in - "monero.com") $DIR/build_monero_all.sh ;; - "cakewallet") $DIR/build_monero_all.sh - $DIR/build_haven.sh ;; - "haven") $DIR/build_haven_all.sh ;; -esac +#!/bin/sh + +set -x -e + +if [ -z "$APP_ANDROID_TYPE" ]; then + echo "Please set APP_ANDROID_TYPE" + exit 1 +fi + +DIR=$(dirname "$0") + +case $APP_ANDROID_TYPE in + "monero.com") $DIR/build_monero_all.sh ;; + "cakewallet") $DIR/build_monero_all.sh +esac diff --git a/scripts/docker/build_haven.sh b/scripts/docker/build_haven.sh deleted file mode 100755 index 1cfb16265..000000000 --- a/scripts/docker/build_haven.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/sh -set -x -e - -. ./config.sh -HAVEN_VERSION=tags/v3.0.7 -HAVEN_SRC_DIR=${WORKDIR}/haven - -git clone https://github.com/haven-protocol-org/haven-main.git ${HAVEN_SRC_DIR} -cd $HAVEN_SRC_DIR -git checkout ${HAVEN_VERSION} -git submodule init -git submodule update - -for arch in "aarch" "aarch64" "i686" "x86_64" -do -FLAGS="" -PREFIX=${WORKDIR}/prefix_${arch} -DEST_LIB_DIR=${PREFIX}/lib/haven -DEST_INCLUDE_DIR=${PREFIX}/include/haven -export CMAKE_INCLUDE_PATH="${PREFIX}/include" -export CMAKE_LIBRARY_PATH="${PREFIX}/lib" -ANDROID_STANDALONE_TOOLCHAIN_PATH="${TOOLCHAIN_BASE_DIR}_${arch}" -PATH="${ANDROID_STANDALONE_TOOLCHAIN_PATH}/bin:${ORIGINAL_PATH}" - -mkdir -p $DEST_LIB_DIR -mkdir -p $DEST_INCLUDE_DIR - -case $arch in - "aarch" ) - CLANG=arm-linux-androideabi-clang - CXXLANG=arm-linux-androideabi-clang++ - BUILD_64=OFF - TAG="android-armv7" - ARCH="armv7-a" - ARCH_ABI="armeabi-v7a" - FLAGS="-D CMAKE_ANDROID_ARM_MODE=ON -D NO_AES=true";; - "aarch64" ) - CLANG=aarch64-linux-androideabi-clang - CXXLANG=aarch64-linux-androideabi-clang++ - BUILD_64=ON - TAG="android-armv8" - ARCH="armv8-a" - ARCH_ABI="arm64-v8a";; - "i686" ) - CLANG=i686-linux-androideabi-clang - CXXLANG=i686-linux-androideabi-clang++ - BUILD_64=OFF - TAG="android-x86" - ARCH="i686" - ARCH_ABI="x86";; - "x86_64" ) - CLANG=x86_64-linux-androideabi-clang - CXXLANG=x86_64-linux-androideabi-clang++ - BUILD_64=ON - TAG="android-x86_64" - ARCH="x86-64" - ARCH_ABI="x86_64";; -esac - -cd $HAVEN_SRC_DIR -rm -rf ./build/release -mkdir -p ./build/release -cd ./build/release -CC=${CLANG} CXX=${CXXLANG} cmake -D USE_DEVICE_TREZOR=OFF -D BUILD_GUI_DEPS=1 -D BUILD_TESTS=OFF -D ARCH=${ARCH} -D STATIC=ON -D BUILD_64=${BUILD_64} -D CMAKE_BUILD_TYPE=release -D ANDROID=true -D INSTALL_VENDORED_LIBUNBOUND=ON -D BUILD_TAG=${TAG} -D CMAKE_SYSTEM_NAME="Android" -D CMAKE_ANDROID_STANDALONE_TOOLCHAIN="${ANDROID_STANDALONE_TOOLCHAIN_PATH}" -D CMAKE_ANDROID_ARCH_ABI=${ARCH_ABI} $FLAGS ../.. - -make wallet_api -j$THREADS -find . -path ./lib -prune -o -name '*.a' -exec cp '{}' lib \; - -cp -r ./lib/* $DEST_LIB_DIR -cp ../../src/wallet/api/wallet2_api.h $DEST_INCLUDE_DIR -done diff --git a/scripts/docker/build_haven_all.sh b/scripts/docker/build_haven_all.sh deleted file mode 100755 index ce8eb3f0e..000000000 --- a/scripts/docker/build_haven_all.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -x -e - -./build_iconv.sh -./build_boost.sh -./build_openssl.sh -./build_sodium.sh -./build_zmq.sh -./build_haven.sh diff --git a/scripts/docker/copy_haven_deps.sh b/scripts/docker/copy_haven_deps.sh deleted file mode 100755 index cef644701..000000000 --- a/scripts/docker/copy_haven_deps.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash -set -x - -WORKDIR=/opt/android -CW_DIR=${WORKDIR}/cake_wallet -CW_EXRTERNAL_DIR=${CW_DIR}/cw_shared_external/ios/External/android -CW_HAVEN_EXTERNAL_DIR=${CW_DIR}/cw_haven/ios/External/android -CW_MONERO_EXTERNAL_DIR=${CW_DIR}/cw_monero/ios/External/android -for arch in "aarch" "aarch64" "i686" "x86_64" -do - -PREFIX=${WORKDIR}/prefix_${arch} -ABI="" - -case $arch in - "aarch" ) - ABI="armeabi-v7a";; - "aarch64" ) - ABI="arm64-v8a";; - "i686" ) - ABI="x86";; - "x86_64" ) - ABI="x86_64";; -esac - -LIB_DIR=${CW_EXRTERNAL_DIR}/${ABI}/lib -INCLUDE_DIR=${CW_EXRTERNAL_DIR}/${ABI}/include -LIBANBOUND_PATH=${PREFIX}/lib/libunbound.a - -mkdir -p $LIB_DIR -mkdir -p $INCLUDE_DIR - -cp -r ${PREFIX}/lib/* $LIB_DIR -cp -r ${PREFIX}/include/* $INCLUDE_DIR - -if [ -f "$LIBANBOUND_PATH" ]; then - cp $LIBANBOUND_PATH ${LIB_DIR}/monero -fi - -done - -mkdir -p ${CW_HAVEN_EXTERNAL_DIR}/include -mkdir -p ${CW_MONERO_EXTERNAL_DIR}/include - -cp $CW_EXRTERNAL_DIR/x86/include/monero/wallet2_api.h ${CW_MONERO_EXTERNAL_DIR}/include -cp $CW_EXRTERNAL_DIR/x86/include/haven/wallet2_api.h ${CW_HAVEN_EXTERNAL_DIR}/include diff --git a/scripts/docker/entrypoint.sh b/scripts/docker/entrypoint.sh index 14f02a1f8..07257bb57 100755 --- a/scripts/docker/entrypoint.sh +++ b/scripts/docker/entrypoint.sh @@ -6,6 +6,4 @@ ls /opt/android rm -rf monero haven ./build_monero.sh -./build_haven.sh ./copy_monero_deps.sh -./copy_haven_deps.sh diff --git a/scripts/ios/app_config.sh b/scripts/ios/app_config.sh index 32d6d5edd..b642d67e4 100755 --- a/scripts/ios/app_config.sh +++ b/scripts/ios/app_config.sh @@ -3,7 +3,6 @@ source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/functions.sh" set -x -e MONERO_COM="monero.com" CAKEWALLET="cakewallet" -HAVEN="haven" DIR=`pwd` if [ -z "$APP_IOS_TYPE" ]; then @@ -33,13 +32,6 @@ case $APP_IOS_TYPE in $CAKEWALLET) CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred" - if [ "$CW_WITH_HAVEN" = true ];then - CONFIG_ARGS="$CONFIG_ARGS --haven" - fi - ;; - - $HAVEN) - CONFIG_ARGS="--haven" ;; esac diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 7d5bfff38..1c1579830 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -7,9 +7,8 @@ APP_IOS_BUNDLE_ID="" MONERO_COM="monero.com" CAKEWALLET="cakewallet" -HAVEN="haven" -TYPES=($MONERO_COM $CAKEWALLET $HAVEN) +TYPES=($MONERO_COM $CAKEWALLET) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" @@ -22,10 +21,6 @@ CAKEWALLET_VERSION="4.23.4" CAKEWALLET_BUILD_NUMBER=302 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" -HAVEN_NAME="Haven" -HAVEN_VERSION="1.0.0" -HAVEN_BUILD_NUMBER=3 -HAVEN_BUNDLE_ID="com.cakewallet.haven" if ! [[ " ${TYPES[*]} " =~ " ${APP_IOS_TYPE} " ]]; then echo "Wrong app type." diff --git a/scripts/ios/build_all.sh b/scripts/ios/build_all.sh index f5b58d6fa..64b04a1be 100755 --- a/scripts/ios/build_all.sh +++ b/scripts/ios/build_all.sh @@ -9,6 +9,5 @@ DIR=$(dirname "$0") case $APP_IOS_TYPE in "monero.com") $DIR/build_monero_all.sh ;; - "cakewallet") $DIR/build_monero_all.sh && $DIR/build_haven.sh && $DIR/build_mwebd.sh && $DIR/build_decred.sh ;; - "haven") $DIR/build_haven_all.sh ;; + "cakewallet") $DIR/build_monero_all.sh && $DIR/build_mwebd.sh && $DIR/build_decred.sh ;; esac diff --git a/scripts/ios/build_haven.sh b/scripts/ios/build_haven.sh deleted file mode 100755 index 8de8b4c83..000000000 --- a/scripts/ios/build_haven.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/bin/sh - -. ./config.sh - -HAVEN_URL="https://github.com/haven-protocol-org/haven-main.git" -HAVEN_DIR_PATH="${EXTERNAL_IOS_SOURCE_DIR}/haven" -HAVEN_VERSION=tags/v3.0.7 -BUILD_TYPE=release -PREFIX=${EXTERNAL_IOS_DIR} -DEST_LIB_DIR=${EXTERNAL_IOS_LIB_DIR}/haven -DEST_INCLUDE_DIR=${EXTERNAL_IOS_INCLUDE_DIR}/haven - -echo "Cloning haven from - $HAVEN_URL to - $HAVEN_DIR_PATH" -git clone $HAVEN_URL $HAVEN_DIR_PATH -cd $HAVEN_DIR_PATH -git checkout $HAVEN_VERSION -git submodule update --init --force -mkdir -p build -cd .. - -ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -if [ -z $INSTALL_PREFIX ]; then - INSTALL_PREFIX=${ROOT_DIR}/haven -fi - -for arch in "arm64" #"armv7" "arm64" -do - -echo "Building IOS ${arch}" -export CMAKE_INCLUDE_PATH="${PREFIX}/include" -export CMAKE_LIBRARY_PATH="${PREFIX}/lib" - -case $arch in - "armv7" ) - DEST_LIB=../../lib-armv7;; - "arm64" ) - DEST_LIB=../../lib-armv8-a;; -esac - -rm -rf haven/build > /dev/null - -mkdir -p haven/build/${BUILD_TYPE} -pushd haven/build/${BUILD_TYPE} -cmake -D IOS=ON \ - -DARCH=${arch} \ - -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ - -DSTATIC=ON \ - -DBUILD_GUI_DEPS=ON \ - -DINSTALL_VENDORED_LIBUNBOUND=ON \ - -DCMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} \ - -DUSE_DEVICE_TREZOR=OFF \ - ../.. -make -j4 && make install -cp src/cryptonote_basic/libcryptonote_basic.a ${DEST_LIB} -cp src/offshore/liboffshore.a ${DEST_LIB} -popd - -done - -#only for arm64 -mkdir -p $DEST_LIB_DIR -mkdir -p $DEST_INCLUDE_DIR -cp ${HAVEN_DIR_PATH}/lib-armv8-a/* $DEST_LIB_DIR -cp ${HAVEN_DIR_PATH}/include/wallet/api/* $DEST_INCLUDE_DIR \ No newline at end of file diff --git a/scripts/ios/build_haven_all.sh b/scripts/ios/build_haven_all.sh deleted file mode 100755 index 116a30d25..000000000 --- a/scripts/ios/build_haven_all.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh - -. ./config.sh -./install_missing_headers.sh -./build_openssl.sh -./build_boost.sh -./build_sodium.sh -./build_zmq.sh -./build_haven.sh \ No newline at end of file diff --git a/scripts/macos/app_config.sh b/scripts/macos/app_config.sh index ad1f04b3e..c10116136 100755 --- a/scripts/macos/app_config.sh +++ b/scripts/macos/app_config.sh @@ -36,7 +36,7 @@ case $APP_MACOS_TYPE in $MONERO_COM) CONFIG_ARGS="--monero";; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --decred";; #--haven + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --decred";; esac cp -rf pubspec_description.yaml pubspec.yaml diff --git a/scripts/macos/build_haven.sh b/scripts/macos/build_haven.sh deleted file mode 100755 index fb67da442..000000000 --- a/scripts/macos/build_haven.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/sh - -. ./config.sh - -HAVEN_URL="https://github.com/haven-protocol-org/haven-main.git" -HAVEN_DIR_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/haven" -HAVEN_VERSION=tags/v3.0.0 -BUILD_TYPE=release -PREFIX=${EXTERNAL_MACOS_DIR} -DEST_LIB_DIR=${EXTERNAL_MACOS_LIB_DIR}/haven -DEST_INCLUDE_DIR=${EXTERNAL_MACOS_INCLUDE_DIR}/haven -ARCH=`uname -m` - -echo "Cloning haven from - $HAVEN_URL to - $HAVEN_DIR_PATH" -git clone $HAVEN_URL $HAVEN_DIR_PATH -cd $HAVEN_DIR_PATH -git checkout $HAVEN_VERSION -git submodule update --init --force -mkdir -p build -cd .. - -ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -if [ -z $INSTALL_PREFIX ]; then - INSTALL_PREFIX=${ROOT_DIR}/haven -fi - -mkdir -p $DEST_LIB_DIR -mkdir -p $DEST_INCLUDE_DIR - -echo "Building MACOS ${ARCH}" -export CMAKE_INCLUDE_PATH="${PREFIX}/include" -export CMAKE_LIBRARY_PATH="${PREFIX}/lib" -rm -rf haven/build > /dev/null - -mkdir -p haven/build/${BUILD_TYPE} -pushd haven/build/${BUILD_TYPE} -cmake -DARCH=${ARCH} \ - -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ - -DSTATIC=ON \ - -DBUILD_GUI_DEPS=ON \ - -DINSTALL_VENDORED_LIBUNBOUND=ON \ - -DCMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} \ - -DUSE_DEVICE_TREZOR=OFF \ - ../.. -make -j4 && make install -find . -path ./lib -prune -o -name '*.a' -exec cp '{}' lib \; -cp -r ./lib/* $DEST_LIB_DIR -cp ../../src/wallet/api/wallet2_api.h $DEST_INCLUDE_DIR -popd - diff --git a/tool/configure.dart b/tool/configure.dart index d1c9326c8..63ed2de84 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -2,7 +2,6 @@ import 'dart:io'; const bitcoinOutputPath = 'lib/bitcoin/bitcoin.dart'; const moneroOutputPath = 'lib/monero/monero.dart'; -const havenOutputPath = 'lib/haven/haven.dart'; const ethereumOutputPath = 'lib/ethereum/ethereum.dart'; const bitcoinCashOutputPath = 'lib/bitcoin_cash/bitcoin_cash.dart'; const nanoOutputPath = 'lib/nano/nano.dart'; @@ -21,7 +20,6 @@ Future main(List args) async { const prefix = '--'; final hasBitcoin = args.contains('${prefix}bitcoin'); final hasMonero = args.contains('${prefix}monero'); - final hasHaven = args.contains('${prefix}haven'); final hasEthereum = args.contains('${prefix}ethereum'); final hasBitcoinCash = args.contains('${prefix}bitcoinCash'); final hasNano = args.contains('${prefix}nano'); @@ -36,7 +34,6 @@ Future main(List args) async { await generateBitcoin(hasBitcoin); await generateMonero(hasMonero); - await generateHaven(hasHaven); await generateEthereum(hasEthereum); await generateBitcoinCash(hasBitcoinCash); await generateNano(hasNano); @@ -51,7 +48,6 @@ Future main(List args) async { await generatePubspec( hasMonero: hasMonero, hasBitcoin: hasBitcoin, - hasHaven: hasHaven, hasEthereum: hasEthereum, hasNano: hasNano, hasBanano: hasBanano, @@ -67,7 +63,6 @@ Future main(List args) async { await generateWalletTypes( hasMonero: hasMonero, hasBitcoin: hasBitcoin, - hasHaven: hasHaven, hasEthereum: hasEthereum, hasNano: hasNano, hasBanano: hasBanano, @@ -654,194 +649,6 @@ abstract class WowneroAccountList { await outputFile.writeAsString(output); } -Future generateHaven(bool hasImplementation) async { - final outputFile = File(havenOutputPath); - const havenCommonHeaders = """ -import 'package:mobx/mobx.dart'; -import 'package:flutter/foundation.dart'; -import 'package:cw_core/wallet_credentials.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/transaction_priority.dart'; -import 'package:cw_core/transaction_history.dart'; -import 'package:cw_core/transaction_info.dart'; -import 'package:cw_core/balance.dart'; -import 'package:cw_core/output_info.dart'; -import 'package:cake_wallet/view_model/send/output.dart'; -import 'package:cw_core/wallet_service.dart'; -import 'package:hive/hive.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cake_wallet/core/key_service.dart'; -import 'package:cake_wallet/core/secure_storage.dart'; -import 'package:cake_wallet/entities/haven_seed_store.dart'; -import 'package:cw_core/cake_hive.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/wallet_type.dart'; -"""; - const havenCWHeaders = """ -import 'package:cw_core/get_height_by_date.dart'; -import 'package:cw_core/monero_amount_format.dart'; -import 'package:cw_core/monero_transaction_priority.dart'; -import 'package:cw_haven/haven_wallet_service.dart'; -import 'package:cw_haven/haven_wallet.dart'; -import 'package:cw_haven/haven_transaction_info.dart'; -import 'package:cw_haven/haven_transaction_history.dart'; -import 'package:cw_core/account.dart' as monero_account; -import 'package:cw_haven/api/wallet.dart' as monero_wallet_api; -import 'package:cw_haven/mnemonics/english.dart'; -import 'package:cw_haven/mnemonics/chinese_simplified.dart'; -import 'package:cw_haven/mnemonics/dutch.dart'; -import 'package:cw_haven/mnemonics/german.dart'; -import 'package:cw_haven/mnemonics/japanese.dart'; -import 'package:cw_haven/mnemonics/russian.dart'; -import 'package:cw_haven/mnemonics/spanish.dart'; -import 'package:cw_haven/mnemonics/portuguese.dart'; -import 'package:cw_haven/mnemonics/french.dart'; -import 'package:cw_haven/mnemonics/italian.dart'; -import 'package:cw_haven/haven_transaction_creation_credentials.dart'; -import 'package:cw_haven/api/balance_list.dart'; -import 'package:cw_haven/haven_wallet_service.dart'; -"""; - const havenCwPart = "part 'cw_haven.dart';"; - const havenContent = """ -class Account { - Account({required this.id, required this.label}); - final int id; - final String label; -} - -class Subaddress { - Subaddress({ - required this.id, - required this.label, - required this.address}); - final int id; - final String label; - final String address; -} - -class HavenBalance extends Balance { - HavenBalance({required this.fullBalance, required this.unlockedBalance}) - : formattedFullBalance = haven!.formatterMoneroAmountToString(amount: fullBalance), - formattedUnlockedBalance = - haven!.formatterMoneroAmountToString(amount: unlockedBalance), - super(unlockedBalance, fullBalance); - - HavenBalance.fromString( - {required this.formattedFullBalance, - required this.formattedUnlockedBalance}) - : fullBalance = haven!.formatterMoneroParseAmount(amount: formattedFullBalance), - unlockedBalance = haven!.formatterMoneroParseAmount(amount: formattedUnlockedBalance), - super(haven!.formatterMoneroParseAmount(amount: formattedUnlockedBalance), - haven!.formatterMoneroParseAmount(amount: formattedFullBalance)); - - final int fullBalance; - final int unlockedBalance; - final String formattedFullBalance; - final String formattedUnlockedBalance; - - @override - String get formattedAvailableBalance => formattedUnlockedBalance; - - @override - String get formattedAdditionalBalance => formattedFullBalance; -} - -class AssetRate { - AssetRate(this.asset, this.rate); - - final String asset; - final int rate; -} - -abstract class HavenWalletDetails { - // FIX-ME: it's abstract class - @observable - late Account account; - // FIX-ME: it's abstract class - @observable - late HavenBalance balance; -} - -abstract class Haven { - HavenAccountList getAccountList(Object wallet); - - MoneroSubaddressList getSubaddressList(Object wallet); - - TransactionHistoryBase getTransactionHistory(Object wallet); - - HavenWalletDetails getMoneroWalletDetails(Object wallet); - - String getTransactionAddress(Object wallet, int accountIndex, int addressIndex); - - int getHeightByDate({required DateTime date}); - Future getCurrentHeight(); - TransactionPriority getDefaultTransactionPriority(); - TransactionPriority deserializeMoneroTransactionPriority({required int raw}); - List getTransactionPriorities(); - List getMoneroWordList(String language); - - WalletCredentials createHavenRestoreWalletFromKeysCredentials({ - required String name, - required String spendKey, - required String viewKey, - required String address, - required String password, - required String language, - required int height}); - WalletCredentials createHavenRestoreWalletFromSeedCredentials({required String name, required String password, required int height, required String mnemonic}); - WalletCredentials createHavenNewWalletCredentials({required String name, required String language, String? password}); - Map getKeys(Object wallet); - Object createHavenTransactionCreationCredentials({required List outputs, required TransactionPriority priority, required String assetType}); - String formatterMoneroAmountToString({required int amount}); - double formatterMoneroAmountToDouble({required int amount}); - int formatterMoneroParseAmount({required String amount}); - Account getCurrentAccount(Object wallet); - void setCurrentAccount(Object wallet, int id, String label); - void onStartup(); - int getTransactionInfoAccountId(TransactionInfo tx); - WalletService createHavenWalletService(Box walletInfoSource); - Future backupHavenSeeds(Box havenSeedStore); - CryptoCurrency assetOfTransaction(TransactionInfo tx); - List getAssetRate(); -} - -abstract class MoneroSubaddressList { - ObservableList get subaddresses; - void update(Object wallet, {required int accountIndex}); - void refresh(Object wallet, {required int accountIndex}); - List getAll(Object wallet); - Future addSubaddress(Object wallet, {required int accountIndex, required String label}); - Future setLabelSubaddress(Object wallet, - {required int accountIndex, required int addressIndex, required String label}); -} - -abstract class HavenAccountList { - ObservableList get accounts; - void update(Object wallet); - void refresh(Object wallet); - List getAll(Object wallet); - Future addAccount(Object wallet, {required String label}); - Future setLabelAccount(Object wallet, {required int accountIndex, required String label}); -} - """; - - const havenEmptyDefinition = 'Haven? haven;\n'; - const havenCWDefinition = 'Haven? haven = CWHaven();\n'; - - final output = '$havenCommonHeaders\n' + - (hasImplementation ? '$havenCWHeaders\n' : '\n') + - (hasImplementation ? '$havenCwPart\n\n' : '\n') + - (hasImplementation ? havenCWDefinition : havenEmptyDefinition) + - '\n' + - havenContent; - - if (outputFile.existsSync()) { - await outputFile.delete(); - } - - await outputFile.writeAsString(output); -} - Future generateEthereum(bool hasImplementation) async { final outputFile = File(ethereumOutputPath); const ethereumCommonHeaders = """ @@ -1573,7 +1380,6 @@ abstract class Decred { Future generatePubspec({ required bool hasMonero, required bool hasBitcoin, - required bool hasHaven, required bool hasEthereum, required bool hasNano, required bool hasBanano, @@ -1598,14 +1404,6 @@ Future generatePubspec({ cw_bitcoin: path: ./cw_bitcoin """; - const cwHaven = """ - cw_haven: - path: ./cw_haven - """; - const cwSharedExternal = """ - cw_shared_external: - path: ./cw_shared_external - """; const flutterSecureStorage = """ flutter_secure_storage: git: @@ -1702,10 +1500,6 @@ Future generatePubspec({ output += '\n$cwTron'; } - if (hasHaven) { - output += '\n$cwSharedExternal\n$cwHaven'; - } - if (hasDecred) { output += '\n$cwDecred'; } @@ -1741,7 +1535,6 @@ Future generatePubspec({ Future generateWalletTypes({ required bool hasMonero, required bool hasBitcoin, - required bool hasHaven, required bool hasEthereum, required bool hasNano, required bool hasBanano, @@ -1815,10 +1608,6 @@ Future generateWalletTypes({ outputContent += '\tWalletType.wownero,\n'; } - if (hasHaven) { - outputContent += '\tWalletType.haven,\n'; - } - outputContent += '];\n'; await walletTypesFile.writeAsString(outputContent); } From 5a09eecb011a6697c549f792654a25962978c479 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Fri, 21 Mar 2025 04:58:34 +0200 Subject: [PATCH 049/243] update pubspec lock [skip ci] --- cw_bitcoin/pubspec.lock | 9 +++++++++ cw_monero/pubspec.lock | 18 ++++++++++++++++++ cw_nano/pubspec.lock | 18 ++++++++++++++++++ cw_wownero/pubspec.lock | 18 ++++++++++++++++++ cw_zano/pubspec.lock | 18 ++++++++++++++++++ 5 files changed, 81 insertions(+) diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 6a8e7d5c4..949d40f7e 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -661,6 +661,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + on_chain: + dependency: transitive + description: + path: "." + ref: cake-update-v2 + resolved-ref: "93440dc5126369b873ca1fccc13c3c1240b1c5c2" + url: "https://github.com/cake-tech/on_chain.git" + source: git + version: "3.7.0" package_config: dependency: transitive description: diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 56451735a..5a3d0fb4d 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -41,6 +41,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + blockchain_utils: + dependency: transitive + description: + path: "." + ref: cake-update-v2 + resolved-ref: "59fdf29d72068e0522a96a8953ed7272833a9f57" + url: "https://github.com/cake-tech/blockchain_utils" + source: git + version: "3.3.0" bluez: dependency: transitive description: @@ -532,6 +541,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + on_chain: + dependency: transitive + description: + path: "." + ref: cake-update-v2 + resolved-ref: "93440dc5126369b873ca1fccc13c3c1240b1c5c2" + url: "https://github.com/cake-tech/on_chain.git" + source: git + version: "3.7.0" package_config: dependency: transitive description: diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock index f426d96dc..6f5999e59 100644 --- a/cw_nano/pubspec.lock +++ b/cw_nano/pubspec.lock @@ -57,6 +57,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" + blockchain_utils: + dependency: transitive + description: + path: "." + ref: cake-update-v2 + resolved-ref: "59fdf29d72068e0522a96a8953ed7272833a9f57" + url: "https://github.com/cake-tech/blockchain_utils" + source: git + version: "3.3.0" boolean_selector: dependency: transitive description: @@ -537,6 +546,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + on_chain: + dependency: transitive + description: + path: "." + ref: cake-update-v2 + resolved-ref: "93440dc5126369b873ca1fccc13c3c1240b1c5c2" + url: "https://github.com/cake-tech/on_chain.git" + source: git + version: "3.7.0" package_config: dependency: transitive description: diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index 645beb141..7d91b618e 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -41,6 +41,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + blockchain_utils: + dependency: transitive + description: + path: "." + ref: cake-update-v2 + resolved-ref: "59fdf29d72068e0522a96a8953ed7272833a9f57" + url: "https://github.com/cake-tech/blockchain_utils" + source: git + version: "3.3.0" boolean_selector: dependency: transitive description: @@ -492,6 +501,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + on_chain: + dependency: transitive + description: + path: "." + ref: cake-update-v2 + resolved-ref: "93440dc5126369b873ca1fccc13c3c1240b1c5c2" + url: "https://github.com/cake-tech/on_chain.git" + source: git + version: "3.7.0" package_config: dependency: transitive description: diff --git a/cw_zano/pubspec.lock b/cw_zano/pubspec.lock index 55246ad3a..824ad5ead 100644 --- a/cw_zano/pubspec.lock +++ b/cw_zano/pubspec.lock @@ -41,6 +41,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + blockchain_utils: + dependency: transitive + description: + path: "." + ref: cake-update-v2 + resolved-ref: "59fdf29d72068e0522a96a8953ed7272833a9f57" + url: "https://github.com/cake-tech/blockchain_utils" + source: git + version: "3.3.0" boolean_selector: dependency: transitive description: @@ -489,6 +498,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + on_chain: + dependency: transitive + description: + path: "." + ref: cake-update-v2 + resolved-ref: "93440dc5126369b873ca1fccc13c3c1240b1c5c2" + url: "https://github.com/cake-tech/on_chain.git" + source: git + version: "3.7.0" package_config: dependency: transitive description: From 00642e6027954c060efab0addd0d7b523368fbf3 Mon Sep 17 00:00:00 2001 From: cyan Date: Fri, 21 Mar 2025 04:03:17 +0100 Subject: [PATCH 050/243] add missing android-35 dependency (#2101) --- .github/workflows/pr_test_build_android.yml | 4 ++-- .github/workflows/pr_test_build_linux.yml | 2 +- Dockerfile | 13 ++++++++----- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 61fb67ee2..0f107e7f6 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -9,7 +9,7 @@ jobs: PR_test_build: runs-on: linux-amd64 container: - image: ghcr.io/cake-tech/cake_wallet:main-linux + image: ghcr.io/cake-tech/cake_wallet:3.24.4-linux env: STORE_PASS: test@cake_wallet KEY_PASS: test@cake_wallet @@ -307,4 +307,4 @@ jobs: uses: actions/upload-artifact@v4 with: path: ${{ github.workspace }}/build/app/outputs/flutter-apk - name: "android apk" \ No newline at end of file + name: "android apk" diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 53904149f..d729972c7 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -9,7 +9,7 @@ jobs: PR_test_build: runs-on: linux-amd64 container: - image: ghcr.io/cake-tech/cake_wallet:main-linux + image: ghcr.io/cake-tech/cake_wallet:3.24.4-linux env: STORE_PASS: test@cake_wallet KEY_PASS: test@cake_wallet diff --git a/Dockerfile b/Dockerfile index dae174a61..21f1ee6f1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # Usage: -# docker build . -f Dockerfile -t ghcr.io/cake-tech/cake_wallet:main-linux -# docker push ghcr.io/cake-tech/cake_wallet:main-linux +# docker build . -f Dockerfile -t ghcr.io/cake-tech/cake_wallet:3.24.4-linux +# docker push ghcr.io/cake-tech/cake_wallet:3.24.4-linux # Heavily inspired by cirrusci images # https://github.com/cirruslabs/docker-images-android/blob/master/sdk/tools/Dockerfile @@ -23,7 +23,7 @@ ENV FLUTTER_VERSION=3.24.4 # Comes from https://developer.android.com/studio/#command-tools ENV ANDROID_SDK_TOOLS_VERSION=11076708 # Comes from https://developer.android.com/studio/releases/build-tools -ENV ANDROID_PLATFORM_VERSION=34 +ENV ANDROID_PLATFORM_VERSION=35 ENV ANDROID_BUILD_TOOLS_VERSION=34.0.0 # If we ever need to migrate the home directory... @@ -106,6 +106,8 @@ RUN yes | sdkmanager \ "platforms;android-$ANDROID_PLATFORM_VERSION" \ "build-tools;$ANDROID_BUILD_TOOLS_VERSION" \ "platforms;android-33" \ + "platforms;android-34" \ + "platforms;android-35" \ "build-tools;33.0.2" \ "build-tools;33.0.1" \ "build-tools;33.0.0" \ @@ -118,10 +120,11 @@ RUN yes | sdkmanager "ndk;$ANDROID_NDK_VERSION" \ # Install dependencies for tests # Comes from https://github.com/ReactiveCircus/android-emulator-runner -RUN yes | sdkmanager "system-images;android-29;default;x86" \ +RUN yes | sdkmanager \ "system-images;android-29;default;x86_64" \ "system-images;android-31;default;x86_64" \ - "platforms;android-29" + "platforms;android-29" \ + "platforms;android-31" # Fake the KVM status so the Android emulator doesn't complain (that much) RUN (addgroup kvm || true) && \ From 626e532fce21403fab1f0f98bfbc3763f23e8a6f Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 21 Mar 2025 05:24:22 +0200 Subject: [PATCH 051/243] Cw 921 create new bottom sheet sending confirmation UI (#2099) * init commit * add slide button * address book popup * fix colors for themes * update exchange page * update cake pay purchase card page * Update rbf_details_page.dart * refactored code * add generating tx bottom sheet * add show scrollbar * minor fixes --- assets/images/birthday_cake.svg | 31 ++ assets/images/contact_icon.svg | 3 + lib/core/execution_state.dart | 2 + .../cake_pay_confirm_purchase_card_page.dart | 80 ++-- .../exchange_trade/exchange_trade_page.dart | 228 ++++----- lib/src/screens/send/send_page.dart | 175 ++++--- .../transaction_details/rbf_details_page.dart | 85 ++-- .../base_bottom_sheet_widget.dart | 83 ++++ .../confirm_sending_bottom_sheet_widget.dart | 450 ++++++++++++++++++ .../info_bottom_sheet_widget.dart | 231 +++++++++ .../widgets/standard_slide_button_widget.dart | 90 ++++ lib/view_model/send/send_view_model.dart | 5 + 12 files changed, 1192 insertions(+), 271 deletions(-) create mode 100644 assets/images/birthday_cake.svg create mode 100644 assets/images/contact_icon.svg create mode 100644 lib/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart create mode 100644 lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart create mode 100644 lib/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart create mode 100644 lib/src/widgets/standard_slide_button_widget.dart diff --git a/assets/images/birthday_cake.svg b/assets/images/birthday_cake.svg new file mode 100644 index 000000000..b5e31dddb --- /dev/null +++ b/assets/images/birthday_cake.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/contact_icon.svg b/assets/images/contact_icon.svg new file mode 100644 index 000000000..6dbfcd5f4 --- /dev/null +++ b/assets/images/contact_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/core/execution_state.dart b/lib/core/execution_state.dart index 6bc906010..860dfee10 100644 --- a/lib/core/execution_state.dart +++ b/lib/core/execution_state.dart @@ -2,6 +2,8 @@ abstract class ExecutionState {} class InitialExecutionState extends ExecutionState {} +class LoadingTemplateExecutingState extends ExecutionState {} + class IsExecutingState extends ExecutionState {} class ExecutedSuccessfullyState extends ExecutionState { diff --git a/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart b/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart index fc3679104..4ac0cf12d 100644 --- a/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart +++ b/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart @@ -7,10 +7,11 @@ import 'package:cake_wallet/src/screens/cake_pay/widgets/cake_pay_alert_modal.da import 'package:cake_wallet/src/screens/cake_pay/widgets/image_placeholder.dart'; import 'package:cake_wallet/src/screens/cake_pay/widgets/link_extractor.dart'; import 'package:cake_wallet/src/screens/cake_pay/widgets/text_icon_button.dart'; -import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/base_alert_dialog.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/standard_checkbox.dart'; @@ -371,39 +372,39 @@ class CakePayBuyCardDetailPage extends BasePage { }); final order = cakePayPurchaseViewModel.order; - final pendingTransaction = cakePayPurchaseViewModel.sendViewModel.pendingTransaction!; - await showPopUp( + showModalBottomSheet( context: context, - builder: (popupContext) { - return Observer( - builder: (_) => ConfirmSendingAlert( - alertTitle: S.of(popupContext).confirm_sending, - paymentId: S.of(popupContext).payment_id, - paymentIdValue: order?.orderId, - expirationTime: cakePayPurchaseViewModel.formattedRemainingTime, - onDispose: () => _handleDispose(disposer), - amount: S.of(popupContext).send_amount, - amountValue: pendingTransaction.amountFormatted, - fiatAmountValue: - cakePayPurchaseViewModel.sendViewModel.pendingTransactionFiatAmountFormatted, - fee: S.of(popupContext).send_fee, - feeValue: pendingTransaction.feeFormatted, - feeFiatAmount: - cakePayPurchaseViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted, - feeRate: pendingTransaction.feeRate, - outputs: cakePayPurchaseViewModel.sendViewModel.outputs, - rightButtonText: S.of(popupContext).send, - leftButtonText: S.of(popupContext).cancel, - actionRightButton: () async { - Navigator.of(context).pop(); - await cakePayPurchaseViewModel.sendViewModel.commitTransaction(context); - }, - actionLeftButton: () => Navigator.of(popupContext).pop())); + isDismissible: false, + isScrollControlled: true, + builder: (BuildContext popupContext) { + return ConfirmSendingBottomSheet( + key: ValueKey('send_page_confirm_sending_dialog_key'), + currentTheme: currentTheme, + paymentId: S.of(popupContext).payment_id, + paymentIdValue: order?.orderId, + expirationTime: cakePayPurchaseViewModel.formattedRemainingTime, + titleText: 'Confirm Transaction', + titleIconPath: cakePayPurchaseViewModel.sendViewModel.selectedCryptoCurrency.iconPath, + currency: cakePayPurchaseViewModel.sendViewModel.selectedCryptoCurrency, + amount: S.of(popupContext).send_amount, + amountValue: cakePayPurchaseViewModel.sendViewModel.pendingTransaction!.amountFormatted, + fiatAmountValue: cakePayPurchaseViewModel.sendViewModel.pendingTransactionFiatAmountFormatted, + fee: S.of(popupContext).send_fee, + feeValue: cakePayPurchaseViewModel.sendViewModel.pendingTransaction!.feeFormatted, + feeFiatAmount: cakePayPurchaseViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted, + outputs: cakePayPurchaseViewModel.sendViewModel.outputs, + onSlideComplete: () async { + Navigator.of(popupContext).pop(); + cakePayPurchaseViewModel.sendViewModel.commitTransaction(context); + }, + ); }, ); } + BuildContext? loadingBottomSheetContext; + void _setEffects(BuildContext context) { if (_effectsInstalled) { return; @@ -416,6 +417,29 @@ class CakePayBuyCardDetailPage extends BasePage { }); } + if (state is! IsExecutingState && + loadingBottomSheetContext != null && + loadingBottomSheetContext!.mounted) { + Navigator.of(loadingBottomSheetContext!).pop(); + } + + if (state is IsExecutingState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted) { + showModalBottomSheet( + context: context, + isDismissible: false, + builder: (BuildContext context) { + loadingBottomSheetContext = context; + return LoadingBottomSheet( + titleText: 'Generating transaction', + ); + }, + ); + } + }); + } + if (state is ExecutedSuccessfullyState) { WidgetsBinding.instance.addPostFrameCallback((_) async { await _showConfirmSendingAlert(context); diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index 6adb08de8..243ab63c8 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -1,7 +1,10 @@ +import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart'; import 'package:cake_wallet/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'dart:ui'; @@ -13,7 +16,6 @@ import 'package:flutter/material.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart'; -import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; @@ -209,6 +211,8 @@ class ExchangeTradeState extends State { ); } + BuildContext? loadingBottomSheetContext; + void _setEffects() { if (_effectsInstalled) { return; @@ -216,6 +220,13 @@ class ExchangeTradeState extends State { _exchangeStateReaction = reaction((_) => this.widget.exchangeTradeViewModel.sendViewModel.state, (ExecutionState state) { + + if (state is! IsExecutingState && + loadingBottomSheetContext != null && + loadingBottomSheetContext!.mounted) { + Navigator.of(loadingBottomSheetContext!).pop(); + } + if (state is FailureState) { WidgetsBinding.instance.addPostFrameCallback((_) { showPopUp( @@ -232,163 +243,92 @@ class ExchangeTradeState extends State { }); } + if (state is IsExecutingState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted) { + showModalBottomSheet( + context: context, + isDismissible: false, + builder: (BuildContext context) { + loadingBottomSheetContext = context; + return LoadingBottomSheet( + titleText: 'Generating transaction', + ); + }, + ); + } + }); + } + if (state is ExecutedSuccessfullyState) { WidgetsBinding.instance.addPostFrameCallback((_) { - showPopUp( + if (context.mounted) { + showModalBottomSheet( context: context, - builder: (BuildContext popupContext) { - return ConfirmSendingAlert( - key: ValueKey('exchange_trade_page_confirm_sending_dialog_key'), - alertLeftActionButtonKey: ValueKey('exchange_trade_page_confirm_sending_dialog_cancel_button_key'), - alertRightActionButtonKey: - ValueKey('exchange_trade_page_confirm_sending_dialog_send_button_key'), - alertTitle: S.of(popupContext).confirm_sending, - amount: S.of(popupContext).send_amount, - amountValue: widget.exchangeTradeViewModel.sendViewModel - .pendingTransaction!.amountFormatted, - fee: S.of(popupContext).send_fee, - feeValue: widget.exchangeTradeViewModel.sendViewModel - .pendingTransaction!.feeFormatted, - feeRate: widget.exchangeTradeViewModel.sendViewModel.pendingTransaction!.feeRate, - rightButtonText: S.of(popupContext).send, - leftButtonText: S.of(popupContext).cancel, - actionRightButton: () async { - Navigator.of(popupContext).pop(); - await widget.exchangeTradeViewModel.sendViewModel - .commitTransaction(context); - transactionStatePopup(); - }, - actionLeftButton: () => Navigator.of(popupContext).pop(), - feeFiatAmount: widget.exchangeTradeViewModel - .pendingTransactionFeeFiatAmountFormatted, - fiatAmountValue: widget.exchangeTradeViewModel - .pendingTransactionFiatAmountValueFormatted, - outputs: widget.exchangeTradeViewModel.sendViewModel - .outputs); - }); + isDismissible: false, + isScrollControlled: true, + builder: (BuildContext bottomSheetContext) { + return ConfirmSendingBottomSheet( + key: ValueKey('exchange_trade_page_confirm_sending_bottom_sheet_key'), + currentTheme: widget.currentTheme, + titleText: 'Confirm Transaction', + titleIconPath: widget.exchangeTradeViewModel.sendViewModel.selectedCryptoCurrency.iconPath, + currency: widget.exchangeTradeViewModel.sendViewModel.selectedCryptoCurrency, + amount: S.of(bottomSheetContext).send_amount, + amountValue: widget.exchangeTradeViewModel.sendViewModel.pendingTransaction!.amountFormatted, + fiatAmountValue: widget.exchangeTradeViewModel.sendViewModel.pendingTransactionFiatAmountFormatted, + fee: isEVMCompatibleChain(widget.exchangeTradeViewModel.sendViewModel.walletType) + ? S.of(bottomSheetContext).send_estimated_fee + : S.of(bottomSheetContext).send_fee, + feeValue: widget.exchangeTradeViewModel.sendViewModel.pendingTransaction!.feeFormatted, + feeFiatAmount: widget.exchangeTradeViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted, + outputs: widget.exchangeTradeViewModel.sendViewModel.outputs, + onSlideComplete: () async { + Navigator.of(bottomSheetContext).pop(); + widget.exchangeTradeViewModel.sendViewModel.commitTransaction(context); + }, + ); + }, + ); + } }); } if (state is TransactionCommitted) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - showPopUp( - context: context, - builder: (BuildContext popupContext) { - return AlertWithOneAction( - alertTitle: S.of(popupContext).sending, - alertContent: S.of(popupContext).transaction_sent, - buttonText: S.of(popupContext).ok, - buttonAction: () => Navigator.of(popupContext).pop()); - }); + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (!context.mounted) { + return; } + + + await showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (BuildContext bottomSheetContext) { + return InfoBottomSheet( + currentTheme: widget.currentTheme, + titleText: 'Transaction Sent', + contentImage: 'assets/images/birthday_cake.svg', + actionButtonText: S.of(bottomSheetContext).close, + actionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'), + actionButton: () { + Navigator.of(bottomSheetContext).pop(); + Navigator.of(context).pushNamedAndRemoveUntil( + Routes.dashboard, + (route) => false, + ); + RequestReviewHandler.requestReview(); + }); + }, + ); + }); } + }); _effectsInstalled = true; } - - void transactionStatePopup() { - if (this.mounted) { - showPopUp( - context: context, - builder: (BuildContext popupContext) { - return Observer(builder: (_) { - final state = widget - .exchangeTradeViewModel.sendViewModel.state; - - if (state is TransactionCommitted) { - return Stack( - children: [ - Container( - color: Theme.of(popupContext).colorScheme.background, - child: Center( - child: Image.asset( - 'assets/images/birthday_cake.png'), - ), - ), - Center( - child: Padding( - padding: EdgeInsets.only( - top: 220, left: 24, right: 24), - child: Text( - S.of(popupContext).send_success(widget - .exchangeTradeViewModel - .wallet - .currency - .toString()), - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - color: Theme.of(popupContext).extension()!.titleColor, - decoration: TextDecoration.none, - ), - ), - ), - ), - Positioned( - left: 24, - right: 24, - bottom: 24, - child: PrimaryButton( - onPressed: () { - Navigator.pushNamedAndRemoveUntil( - popupContext, - Routes.dashboard, - (route) => false, - ); - RequestReviewHandler.requestReview(); - }, - text: S.of(popupContext).got_it, - color: Theme.of(popupContext).primaryColor, - textColor: Colors.white)) - ], - ); - } - - return Stack( - children: [ - Container( - color: Theme.of(popupContext).colorScheme.background, - child: Center( - child: Image.asset( - 'assets/images/birthday_cake.png'), - ), - ), - BackdropFilter( - filter: ImageFilter.blur( - sigmaX: 3.0, sigmaY: 3.0), - child: Container( - decoration: BoxDecoration( - color: Theme.of(popupContext) - .colorScheme - .background - .withOpacity(0.25)), - child: Center( - child: Padding( - padding: EdgeInsets.only(top: 220), - child: Text( - S.of(popupContext).send_sending, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - color: Theme.of(popupContext).extension()!.titleColor, - decoration: TextDecoration.none, - ), - ), - ), - ), - ), - ) - ], - ); - }); - }); - } - } } class _ExchangeTradeItemsCardSection extends StatelessWidget { diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 7e7080d0f..fbbb6c0e1 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -12,18 +12,20 @@ import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart'; -import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart'; import 'package:cake_wallet/src/screens/send/widgets/send_card.dart'; import 'package:cake_wallet/src/widgets/adaptable_page_view.dart'; import 'package:cake_wallet/src/widgets/add_template_button.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/template_tile.dart'; import 'package:cake_wallet/src/widgets/trail_button.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; import 'package:cake_wallet/themes/extensions/seed_widget_theme.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; @@ -162,6 +164,8 @@ class SendPage extends BasePage { }); }); + bool _bottomSheetOpened = false; + @override Widget body(BuildContext context) { _setEffects(context); @@ -290,7 +294,7 @@ class SendPage extends BasePage { ? template.cryptoCurrency : template.fiatCurrency, onTap: () async { - sendViewModel.state = IsExecutingState(); + sendViewModel.state = LoadingTemplateExecutingState(); if (template.additionalRecipients?.isNotEmpty ?? false) { sendViewModel.clearOutputs(); @@ -464,7 +468,8 @@ class SendPage extends BasePage { textColor: Colors.white, isLoading: sendViewModel.state is IsExecutingState || sendViewModel.state is TransactionCommitting || - sendViewModel.state is IsAwaitingDeviceResponseState, + sendViewModel.state is IsAwaitingDeviceResponseState || + sendViewModel.state is LoadingTemplateExecutingState, isDisabled: !sendViewModel.isReadyForSend, ); }, @@ -479,6 +484,7 @@ class SendPage extends BasePage { } BuildContext? dialogContext; + BuildContext? loadingBottomSheetContext; void _setEffects(BuildContext context) { if (_effectsInstalled) { @@ -494,6 +500,13 @@ class SendPage extends BasePage { Navigator.of(dialogContext!).pop(); } + if (state is! IsExecutingState && + loadingBottomSheetContext != null && + loadingBottomSheetContext!.mounted) { + Navigator.of(loadingBottomSheetContext!).pop(); + } + + if (state is FailureState) { WidgetsBinding.instance.addPostFrameCallback((_) { showPopUp( @@ -510,92 +523,110 @@ class SendPage extends BasePage { }); } - if (state is ExecutedSuccessfullyState) { + if (state is IsExecutingState) { WidgetsBinding.instance.addPostFrameCallback((_) { if (context.mounted) { - showPopUp( - context: context, - builder: (BuildContext _dialogContext) { - return ConfirmSendingAlert( - key: ValueKey('send_page_confirm_sending_dialog_key'), - alertTitle: S.of(_dialogContext).confirm_sending, - amount: S.of(_dialogContext).send_amount, - amountValue: sendViewModel.pendingTransaction!.amountFormatted, - fiatAmountValue: sendViewModel.pendingTransactionFiatAmountFormatted, - fee: isEVMCompatibleChain(sendViewModel.walletType) - ? S.of(_dialogContext).send_estimated_fee - : S.of(_dialogContext).send_fee, - feeRate: sendViewModel.pendingTransaction!.feeRate, - feeValue: sendViewModel.pendingTransaction!.feeFormatted, - feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmountFormatted, - outputs: sendViewModel.outputs, - change: sendViewModel.pendingTransaction!.change, - rightButtonText: S.of(_dialogContext).send, - leftButtonText: S.of(_dialogContext).cancel, - alertRightActionButtonKey: - ValueKey('send_page_confirm_sending_dialog_send_button_key'), - alertLeftActionButtonKey: - ValueKey('send_page_confirm_sending_dialog_cancel_button_key'), - actionRightButton: () async { - Navigator.of(_dialogContext).pop(); - sendViewModel.commitTransaction(context); - }, - actionLeftButton: () => Navigator.of(_dialogContext).pop()); - }); + showModalBottomSheet( + context: context, + isDismissible: false, + builder: (BuildContext context) { + loadingBottomSheetContext = context; + return LoadingBottomSheet( + titleText: 'Generating transaction', + ); + }, + ); } }); } + if (state is ExecutedSuccessfullyState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted) { + showModalBottomSheet( + context: context, + isDismissible: false, + isScrollControlled: true, + builder: (BuildContext bottomSheetContext) { + return ConfirmSendingBottomSheet( + key: ValueKey('send_page_confirm_sending_dialog_key'), + titleText: 'Confirm Transaction', + currentTheme: currentTheme, + titleIconPath: sendViewModel.selectedCryptoCurrency.iconPath, + currency: sendViewModel.selectedCryptoCurrency, + amount: S.of(bottomSheetContext).send_amount, + amountValue: sendViewModel.pendingTransaction!.amountFormatted, + fiatAmountValue: sendViewModel.pendingTransactionFiatAmountFormatted, + fee: isEVMCompatibleChain(sendViewModel.walletType) + ? S.of(bottomSheetContext).send_estimated_fee + : S.of(bottomSheetContext).send_fee, + feeValue: sendViewModel.pendingTransaction!.feeFormatted, + feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmountFormatted, + outputs: sendViewModel.outputs, + onSlideComplete: () async { + Navigator.of(bottomSheetContext).pop(); + sendViewModel.commitTransaction(context); + }, + change: sendViewModel.pendingTransaction!.change, + ); + }, + ); + } + }); + } + + + if (state is TransactionCommitted) { WidgetsBinding.instance.addPostFrameCallback((_) async { if (!context.mounted) { return; } - final successMessage = - S.of(context).send_success(sendViewModel.selectedCryptoCurrency.toString()); - - final waitMessage = sendViewModel.walletType == WalletType.solana - ? '. ${S.of(context).waitFewSecondForTxUpdate}' - : ''; - - String alertContent = "$successMessage$waitMessage"; - - await Navigator.of(context) - .pushNamed(Routes.transactionSuccessPage, arguments: alertContent); - newContactAddress = newContactAddress ?? sendViewModel.newContactAddress(); + if (newContactAddress?.address != null && isRegularElectrumAddress(newContactAddress!.address)) { newContactAddress = null; } - if (sendViewModel.coinTypeToSpendFrom != UnspentCoinType.any) newContactAddress = null; - - if (newContactAddress != null && sendViewModel.showAddressBookPopup) { - await showPopUp( - context: context, - builder: (BuildContext _dialogContext) => AlertWithTwoActions( - alertDialogKey: ValueKey('send_page_sent_dialog_key'), - alertTitle: '', - alertContent: S.of(_dialogContext).add_contact_to_address_book, - rightButtonText: S.of(_dialogContext).add_contact, - leftButtonText: S.of(_dialogContext).ignor, - alertLeftActionButtonKey: ValueKey('send_page_sent_dialog_ignore_button_key'), - alertRightActionButtonKey: - ValueKey('send_page_sent_dialog_add_contact_button_key'), - actionRightButton: () { - Navigator.of(_dialogContext).pop(); - RequestReviewHandler.requestReview(); - Navigator.of(context) - .pushNamed(Routes.addressBookAddContact, arguments: newContactAddress); - newContactAddress = null; - }, - actionLeftButton: () { - Navigator.of(_dialogContext).pop(); - RequestReviewHandler.requestReview(); - newContactAddress = null; - })); - } + await showModalBottomSheet( + context: context, + isDismissible: false, + builder: (BuildContext bottomSheetContext) { + return newContactAddress != null && sendViewModel.showAddressBookPopup + ? InfoBottomSheet( + currentTheme: currentTheme, + showDontAskMeCheckbox: true, + onCheckboxChanged: (value) => sendViewModel.setShowAddressBookPopup(!value), + titleText: 'Transaction Sent', + contentImage: 'assets/images/contact_icon.svg', + contentImageColor: Theme.of(context).extension()!.titleColor, + content: S.of(bottomSheetContext).add_contact_to_address_book, + isTwoAction: true, + leftButtonText: 'No', + rightButtonText: 'Yes', + actionLeftButton: () { + Navigator.of(bottomSheetContext).pop(); + RequestReviewHandler.requestReview(); + newContactAddress = null; + }, + actionRightButton: () { + Navigator.of(bottomSheetContext).pop(); + RequestReviewHandler.requestReview(); + Navigator.of(context) + .pushNamed(Routes.addressBookAddContact, arguments: newContactAddress); + newContactAddress = null; + }, + ) + : InfoBottomSheet( + currentTheme: currentTheme, + titleText: 'Transaction Sent', + contentImage: 'assets/images/birthday_cake.svg', + actionButtonText: S.of(bottomSheetContext).close, + actionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'), + actionButton: () => Navigator.of(bottomSheetContext).pop()); + }, + ); if (initialPaymentRequest?.callbackUrl?.isNotEmpty ?? false) { // wait a second so it's not as jarring: diff --git a/lib/src/screens/transaction_details/rbf_details_page.dart b/lib/src/screens/transaction_details/rbf_details_page.dart index 2c5edd8b4..3b6513a03 100644 --- a/lib/src/screens/transaction_details/rbf_details_page.dart +++ b/lib/src/screens/transaction_details/rbf_details_page.dart @@ -1,7 +1,6 @@ import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart'; import 'package:cake_wallet/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart'; import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; import 'package:cake_wallet/src/screens/transaction_details/textfield_list_item.dart'; @@ -9,6 +8,8 @@ import 'package:cake_wallet/src/screens/transaction_details/transaction_expandab import 'package:cake_wallet/src/screens/transaction_details/widgets/textfield_list_row.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/list_row.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/standard_expandable_list.dart'; @@ -110,12 +111,21 @@ class RBFDetailsPage extends BasePage { ); } + BuildContext? loadingBottomSheetContext; + void _setEffects(BuildContext context) { if (_effectsInstalled) { return; } reaction((_) => transactionDetailsViewModel.sendViewModel.state, (ExecutionState state) { + + if (state is! IsExecutingState && + loadingBottomSheetContext != null && + loadingBottomSheetContext!.mounted) { + Navigator.of(loadingBottomSheetContext!).pop(); + } + if (state is FailureState) { WidgetsBinding.instance.addPostFrameCallback((_) { showPopUp( @@ -151,35 +161,56 @@ class RBFDetailsPage extends BasePage { }); } + if (state is IsExecutingState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted) { + showModalBottomSheet( + context: context, + isDismissible: false, + builder: (BuildContext context) { + loadingBottomSheetContext = context; + return LoadingBottomSheet( + titleText: 'Generating transaction', + ); + }, + ); + } + }); + } + if (state is ExecutedSuccessfullyState) { WidgetsBinding.instance.addPostFrameCallback((_) { - showPopUp( + if (context.mounted) { + showModalBottomSheet( context: context, - builder: (BuildContext popupContext) { - return ConfirmSendingAlert( - alertTitle: S.of(popupContext).confirm_sending, - amount: S.of(popupContext).send_amount, - amountValue: transactionDetailsViewModel - .sendViewModel.pendingTransaction!.amountFormatted, - fee: S.of(popupContext).send_fee, - feeValue: - transactionDetailsViewModel.sendViewModel.pendingTransaction!.feeFormatted, - rightButtonText: S.of(popupContext).send, - leftButtonText: S.of(popupContext).cancel, - actionRightButton: () async { - Navigator.of(popupContext).pop(); - await transactionDetailsViewModel.sendViewModel.commitTransaction(context); - try { - Navigator.of(popupContext).pop(); - } catch (_) {} - }, - actionLeftButton: () => Navigator.of(popupContext).pop(), - feeFiatAmount: - transactionDetailsViewModel.pendingTransactionFeeFiatAmountFormatted, - fiatAmountValue: - transactionDetailsViewModel.pendingTransactionFiatAmountValueFormatted, - outputs: transactionDetailsViewModel.sendViewModel.outputs); - }); + isDismissible: false, + isScrollControlled: true, + builder: (BuildContext bottomSheetContext) { + return ConfirmSendingBottomSheet( + key: ValueKey('rbf_confirm_sending_bottom_sheet'), + titleText: 'Confirm Transaction', + currentTheme: currentTheme, + titleIconPath: transactionDetailsViewModel.sendViewModel.selectedCryptoCurrency.iconPath, + currency: transactionDetailsViewModel.sendViewModel.selectedCryptoCurrency, + amount: S.of(bottomSheetContext).send_amount, + amountValue: transactionDetailsViewModel.sendViewModel.pendingTransaction!.amountFormatted, + fiatAmountValue: transactionDetailsViewModel.sendViewModel.pendingTransactionFiatAmountFormatted, + fee: S.of(bottomSheetContext).send_fee, + feeValue: transactionDetailsViewModel.sendViewModel.pendingTransaction!.feeFormatted, + feeFiatAmount: transactionDetailsViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted, + outputs: transactionDetailsViewModel.sendViewModel.outputs, + onSlideComplete: () async { + Navigator.of(bottomSheetContext).pop(); + await transactionDetailsViewModel.sendViewModel.commitTransaction(context); + try { + Navigator.of(bottomSheetContext).pop(); + } catch (_) {} + }, + change: transactionDetailsViewModel.sendViewModel.pendingTransaction!.change, + ); + }, + ); + } }); } diff --git a/lib/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart b/lib/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart new file mode 100644 index 000000000..206780eae --- /dev/null +++ b/lib/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; + +abstract class BaseBottomSheet extends StatelessWidget { + final String titleText; + final String? titleIconPath; + + const BaseBottomSheet({required this.titleText, this.titleIconPath}); + + Widget headerWidget(BuildContext context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Row( + children: [ + const Spacer(flex: 4), + Expanded( + flex: 2, + child: Container( + height: 6, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Theme.of(context).extension()!.titleColor, + ), + ), + ), + const Spacer(flex: 4), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (titleIconPath != null) + Image.asset(titleIconPath!, height: 24, width: 24) + else + Container(), + const SizedBox(width: 6), + Text( + titleText, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + ), + ), + ], + ), + const SizedBox(height: 13), + ], + ); + } + + Widget contentWidget(BuildContext context); + + Widget footerWidget(BuildContext context); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: BoxConstraints(maxHeight: 600), + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(30.0), topRight: Radius.circular(30.0)), + child: Container( + color: Theme.of(context).dialogBackgroundColor, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + headerWidget(context), + contentWidget(context), + footerWidget(context), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart new file mode 100644 index 000000000..bf16695e9 --- /dev/null +++ b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart @@ -0,0 +1,450 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/standard_slide_button_widget.dart'; +import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/extensions/filter_theme.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:flutter/material.dart'; +import 'dart:math' as math; + +import 'base_bottom_sheet_widget.dart'; + +class ConfirmSendingBottomSheet extends BaseBottomSheet { + final CryptoCurrency currency; + final ThemeBase currentTheme; + final String? paymentId; + final String? paymentIdValue; + final String? expirationTime; + final String amount; + final String amountValue; + final String fiatAmountValue; + final String fee; + final String feeValue; + final String feeFiatAmount; + final List outputs; + final VoidCallback onSlideComplete; + final PendingChange? change; + + ConfirmSendingBottomSheet({ + required String titleText, + String? titleIconPath, + required this.currency, + required this.currentTheme, + this.paymentId, + this.paymentIdValue, + this.expirationTime, + required this.amount, + required this.amountValue, + required this.fiatAmountValue, + required this.fee, + required this.feeValue, + required this.feeFiatAmount, + required this.outputs, + required this.onSlideComplete, + this.change, + Key? key, + }) : showScrollbar = outputs.length > 3, + super(titleText: titleText, titleIconPath: titleIconPath); + + final bool showScrollbar; + final ScrollController scrollController = ScrollController(); + + @override + Widget contentWidget(BuildContext context) { + final itemTitleTextStyle = TextStyle( + fontSize: 16, + fontFamily: 'Lato', + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + ); + final itemSubTitleTextStyle = TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: currentTheme.type == ThemeType.bright + ? Theme.of(context).extension()!.titleColor + : Theme.of(context).extension()!.labelTextColor, + decoration: TextDecoration.none, + ); + + Widget content = Padding( + padding: EdgeInsets.fromLTRB(8, 0, showScrollbar ? 16 : 8, 8), + child: Column( + children: [ + if (paymentId != null && paymentIdValue != null) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: AddressTile( + itemTitle: paymentId!, + currentTheme: currentTheme, + itemTitleTextStyle: itemTitleTextStyle, + isBatchSending: false, + amount: '', + address: paymentIdValue!, + itemSubTitleTextStyle: itemSubTitleTextStyle, + ), + ), + StandardTile( + itemTitle: amount, + itemValue: amountValue + ' ${currency.title}', + itemTitleTextStyle: itemTitleTextStyle, + itemSubTitle: fiatAmountValue, + itemSubTitleTextStyle: itemSubTitleTextStyle, + ), + const SizedBox(height: 8), + StandardTile( + itemTitle: fee, + itemValue: feeValue, + itemTitleTextStyle: itemTitleTextStyle, + itemSubTitle: feeFiatAmount, + itemSubTitleTextStyle: itemSubTitleTextStyle, + ), + const SizedBox(height: 8), + Column( + children: [ + ListView.separated( + padding: const EdgeInsets.only(top: 0), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: outputs.length, + separatorBuilder: (context, index) => const SizedBox(height: 8), + itemBuilder: (context, index) { + final bool isBatchSending = outputs.length > 1; + final item = outputs[index]; + final contactName = item.parsedAddress.name; + final batchContactTitle = + '${index + 1}/${outputs.length} - ${contactName.isEmpty ? 'Address' : contactName}'; + final _address = item.isParsedAddress ? item.extractedAddress : item.address; + final _amount = item.cryptoAmount.replaceAll(',', '.') + ' ${currency.title}'; + return isBatchSending || contactName.isNotEmpty + ? AddressExpansionTile( + contactType: 'Contact', + currentTheme: currentTheme, + name: isBatchSending ? batchContactTitle : contactName, + address: _address, + amount: _amount, + isBatchSending: isBatchSending, + itemTitleTextStyle: itemTitleTextStyle, + itemSubTitleTextStyle: itemSubTitleTextStyle, + ) + : AddressTile( + itemTitle: 'Address', + currentTheme: currentTheme, + itemTitleTextStyle: itemTitleTextStyle, + isBatchSending: isBatchSending, + amount: _amount, + address: _address, + itemSubTitleTextStyle: itemSubTitleTextStyle, + ); + }, + ), + if (change != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: AddressExpansionTile( + contactType: 'Change', + currentTheme: currentTheme, + name: S.of(context).send_change_to_you, + address: change!.address, + amount: change!.amount + ' ${currency.title}', + isBatchSending: true, + itemTitleTextStyle: itemTitleTextStyle, + itemSubTitleTextStyle: itemSubTitleTextStyle, + ), + ), + ], + ), + const SizedBox(height: 8), + ], + ), + ); + + if (showScrollbar) { + return SizedBox( + height: 380, + child: Scrollbar( + controller: scrollController, + thumbVisibility: true, + child: SingleChildScrollView( + controller: scrollController, + physics: const BouncingScrollPhysics(), + child: content, + ), + ), + ); + } else { + return content; + } + } + + @override + Widget footerWidget(BuildContext context) { + return Container( + padding: const EdgeInsets.fromLTRB(40, 12, 40, 34), + decoration: BoxDecoration( + color: Theme.of(context).dialogBackgroundColor, + boxShadow: [ + if (showScrollbar) + BoxShadow( + color: Colors.black.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 10, + offset: const Offset(0, 0), + ), + ], + ), + child: StandardSlideButton( + onSlideComplete: onSlideComplete, + buttonText: 'Swipe to send', + currentTheme: currentTheme, + ), + ); + } +} + +class StandardTile extends StatelessWidget { + const StandardTile({ + super.key, + required this.itemTitle, + required this.itemValue, + required this.itemTitleTextStyle, + this.itemSubTitle, + required this.itemSubTitleTextStyle, + }); + + final String itemTitle; + final String itemValue; + final TextStyle itemTitleTextStyle; + final String? itemSubTitle; + final TextStyle itemSubTitleTextStyle; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Theme.of(context).extension()!.buttonColor), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(itemTitle, style: itemTitleTextStyle), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(itemValue, style: itemTitleTextStyle), + itemSubTitle == null + ? Container() + : Text(itemSubTitle!, style: itemSubTitleTextStyle), + ], + ), + ], + ), + ); + } +} + +class AddressTile extends StatelessWidget { + const AddressTile({ + super.key, + required this.itemTitle, + required this.currentTheme, + required this.itemTitleTextStyle, + required this.isBatchSending, + required this.amount, + required this.address, + required this.itemSubTitleTextStyle, + }); + + final String itemTitle; + final ThemeBase currentTheme; + final TextStyle itemTitleTextStyle; + final bool isBatchSending; + final String amount; + final String address; + final TextStyle itemSubTitleTextStyle; + + @override + Widget build(BuildContext context) { + final addressTextStyle = TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: currentTheme.type == ThemeType.bright + ? Theme.of(context).extension()!.labelTextColor + : Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + ); + return Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Theme.of(context).extension()!.buttonColor, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(itemTitle, style: itemTitleTextStyle), + if (isBatchSending) Text(amount, style: itemTitleTextStyle), + ], + ), + buildSegmentedAddress( + address: address, + evenTextStyle: currentTheme.type == ThemeType.bright + ? itemSubTitleTextStyle + : addressTextStyle, + oddTextStyle: itemSubTitleTextStyle, + ), + ], + ), + ); + } + + Widget buildSegmentedAddress({ + required String address, + int chunkSize = 6, + required TextStyle evenTextStyle, + required TextStyle oddTextStyle, + }) { + final spans = []; + + int index = 0; + for (int i = 0; i < address.length; i += chunkSize) { + final chunk = address.substring(i, math.min(i + chunkSize, address.length)); + final style = (index % 2 == 0) ? evenTextStyle : oddTextStyle; + + spans.add( + TextSpan(text: '$chunk ', style: style), + ); + + index++; + } + + return RichText( + text: TextSpan(children: spans, style: evenTextStyle), + overflow: TextOverflow.visible, + ); + } +} + +class AddressExpansionTile extends StatelessWidget { + const AddressExpansionTile({ + super.key, + required this.contactType, + required this.currentTheme, + required this.name, + required this.address, + required this.amount, + required this.isBatchSending, + required this.itemTitleTextStyle, + required this.itemSubTitleTextStyle, + }); + + final String contactType; + final ThemeBase currentTheme; + final String name; + final String address; + final String amount; + final bool isBatchSending; + final TextStyle itemTitleTextStyle; + final TextStyle itemSubTitleTextStyle; + + @override + Widget build(BuildContext context) { + final addressTextStyle = TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: currentTheme.type == ThemeType.bright + ? Theme.of(context).extension()!.labelTextColor + : Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + ); + + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(10)), + color: Theme.of(context).extension()!.buttonColor, + ), + child: Theme( + data: Theme.of(context).copyWith(dividerColor: Colors.transparent), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 14, vertical: isBatchSending ? 0 : 8), + child: ExpansionTile( + childrenPadding: EdgeInsets.zero, + tilePadding: EdgeInsets.zero, + dense: true, + visualDensity: VisualDensity.compact, + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(isBatchSending ? name : contactType, style: itemTitleTextStyle), + Text(isBatchSending ? amount : name, + style: TextStyle( + fontSize: 14, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + )), + ], + ), + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildSegmentedAddress( + address: address, + evenTextStyle: currentTheme.type == ThemeType.bright + ? itemSubTitleTextStyle + : addressTextStyle, + oddTextStyle: itemSubTitleTextStyle, + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + Widget buildSegmentedAddress({ + required String address, + int chunkSize = 6, + required TextStyle evenTextStyle, + required TextStyle oddTextStyle, + }) { + final spans = []; + + int index = 0; + for (int i = 0; i < address.length; i += chunkSize) { + final chunk = address.substring(i, math.min(i + chunkSize, address.length)); + final style = (index % 2 == 0) ? evenTextStyle : oddTextStyle; + + spans.add( + TextSpan(text: '$chunk ', style: style), + ); + + index++; + } + + return RichText( + text: TextSpan(children: spans, style: evenTextStyle), + overflow: TextOverflow.visible, + ); + } +} diff --git a/lib/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart b/lib/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart new file mode 100644 index 000000000..26d25b50d --- /dev/null +++ b/lib/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart @@ -0,0 +1,231 @@ +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/menu_theme.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +import 'base_bottom_sheet_widget.dart'; + +class LoadingBottomSheet extends BaseBottomSheet { + LoadingBottomSheet({required String titleText, String? titleIconPath}) + : super(titleText: titleText, titleIconPath: titleIconPath); + + @override + Widget contentWidget(BuildContext context) { + return SizedBox( + height: 200, + child: Center(child: CircularProgressIndicator()), + ); + } + + @override + Widget footerWidget(BuildContext context) => const SizedBox(height: 94); +} + +class InfoBottomSheet extends BaseBottomSheet { + final ThemeBase currentTheme; + final String? contentImage; + final Color? contentImageColor; + final String? content; + final bool isTwoAction; + final bool showDontAskMeCheckbox; + final Function(bool)? onCheckboxChanged; + final String? actionButtonText; + final VoidCallback? actionButton; + final Key? actionButtonKey; + final String? leftButtonText; + final String? rightButtonText; + final VoidCallback? actionLeftButton; + final VoidCallback? actionRightButton; + final Key? rightActionButtonKey; + final Key? leftActionButtonKey; + + InfoBottomSheet({ + required String titleText, + String? titleIconPath, + required this.currentTheme, + this.contentImage, + this.contentImageColor, + this.content, + this.isTwoAction = false, + this.showDontAskMeCheckbox = false, + this.onCheckboxChanged, + this.actionButtonText, + this.actionButton, + this.actionButtonKey, + this.leftButtonText, + this.rightButtonText, + this.actionLeftButton, + this.actionRightButton, + this.rightActionButtonKey, + this.leftActionButtonKey, + }) : super(titleText: titleText, titleIconPath: titleIconPath); + + @override + Widget contentWidget(BuildContext context) { + return SizedBox( + height: 200, + child: Column( + children: [ + if (contentImage != null) + Expanded( + flex: 4, + child: getImage(contentImage!, imageColor: contentImageColor), + ) + else + Container(), + if (content != null) + Expanded( + flex: 2, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacer(flex: 2), + Expanded( + flex: 6, + child: Text( + content!, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontFamily: 'Lato', + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + ), + ), + ), + const Spacer(flex: 2), + ], + ), + ), + if (showDontAskMeCheckbox) + Padding( + padding: const EdgeInsets.only(left: 34.0), + child: Row( + children: [ + SimpleCheckbox(onChanged: onCheckboxChanged), + const SizedBox(width: 8), + Text( + 'Don’t ask me next time', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontFamily: 'Lato', + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + ), + ), + ], + ), + ), + ], + ), + ); + } + + @override + Widget footerWidget(BuildContext context) { + if (isTwoAction) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 34), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Flexible( + child: Container( + padding: const EdgeInsets.only(right: 8.0, top: 8.0), + child: PrimaryButton( + key: leftActionButtonKey, + onPressed: actionLeftButton, + text: leftButtonText ?? '', + color: currentTheme.type == ThemeType.dark + ? Theme.of(context).extension()!.backgroundColor + : Theme.of(context).cardColor, + textColor: currentTheme.type == ThemeType.dark + ? Theme.of(context).extension()!.textColor + : Theme.of(context).extension()!.buttonTextColor, + ), + ), + ), + Flexible( + child: Container( + padding: const EdgeInsets.only(left: 8.0, top: 8.0), + child: PrimaryButton( + key: rightActionButtonKey, + onPressed: actionRightButton, + text: rightButtonText ?? '', + color: Theme.of(context).primaryColor, + textColor: currentTheme.type == ThemeType.dark + ? Theme.of(context).extension()!.textColor + : Theme.of(context).extension()!.buttonTextColor, + ), + ), + ), + ], + ), + ); + } else { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 34), + child: LoadingPrimaryButton( + key: actionButtonKey, + onPressed: actionButton ?? () {}, + text: actionButtonText ?? '', + color: Theme.of(context).primaryColor, + textColor: Colors.white, + isLoading: false, + isDisabled: false, + ), + ); + } + } + + Widget getImage(String imagePath, {Color? imageColor}) { + final bool isSvg = imagePath.endsWith('.svg'); + if (isSvg) { + return SvgPicture.asset( + imagePath, + colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null, + ); + } else { + return Image.asset(imagePath); + } + } +} + +class SimpleCheckbox extends StatefulWidget { + SimpleCheckbox({this.onChanged}); + + final Function(bool)? onChanged; + + @override + State createState() => _SimpleCheckboxState(); +} + +class _SimpleCheckboxState extends State { + bool initialValue = false; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 24.0, + width: 24.0, + child: Checkbox( + value: initialValue, + onChanged: (value) => setState(() { + initialValue = value!; + widget.onChanged?.call(value); + }), + checkColor: Theme.of(context).extension()!.titleColor, + activeColor: Colors.transparent, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + side: WidgetStateBorderSide.resolveWith((states) => BorderSide( + color: Theme.of(context).extension()!.titleColor, width: 1.0)), + ), + ); + } +} diff --git a/lib/src/widgets/standard_slide_button_widget.dart b/lib/src/widgets/standard_slide_button_widget.dart new file mode 100644 index 000000000..e777155aa --- /dev/null +++ b/lib/src/widgets/standard_slide_button_widget.dart @@ -0,0 +1,90 @@ +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/extensions/filter_theme.dart'; +import 'package:cake_wallet/themes/extensions/menu_theme.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:flutter/material.dart'; + +class StandardSlideButton extends StatefulWidget { + const StandardSlideButton({ + Key? key, + required this.onSlideComplete, + this.buttonText = '', + this.height = 48.0, + required this.currentTheme, + }) : super(key: key); + + final VoidCallback onSlideComplete; + final String buttonText; + final double height; + final ThemeBase currentTheme; + + @override + _StandardSlideButtonState createState() => _StandardSlideButtonState(); +} + +class _StandardSlideButtonState extends State { + double _dragPosition = 0.0; + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + final double maxWidth = constraints.maxWidth; + const double sideMargin = 4.0; + final double effectiveMaxWidth = maxWidth - 2 * sideMargin; + const double sliderWidth = 42.0; + + return Container( + height: widget.height, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: widget.currentTheme.type == ThemeType.light || widget.currentTheme.type == ThemeType.bright + ? Theme.of(context).disabledColor + : widget.currentTheme.type == ThemeType.oled ? Colors.black : Theme.of(context).extension()!.backgroundColor), + child: Stack( + alignment: Alignment.centerLeft, + children: [ + Center( + child: Text(widget.buttonText, + style: TextStyle( + fontSize: 16, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).extension()!.titleColor))), + Positioned( + left: sideMargin + _dragPosition, + child: GestureDetector( + onHorizontalDragUpdate: (details) { + setState(() { + _dragPosition += details.delta.dx; + if (_dragPosition < 0) _dragPosition = 0; + if (_dragPosition > effectiveMaxWidth - sliderWidth) { + _dragPosition = effectiveMaxWidth - sliderWidth; + } + }); + }, + onHorizontalDragEnd: (details) { + if (_dragPosition >= effectiveMaxWidth - sliderWidth - 10) { + widget.onSlideComplete(); + } else { + setState(() => _dragPosition = 0); + } + }, + child: Container( + width: sliderWidth, + height: widget.height - 8, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: widget.currentTheme.type == ThemeType.bright ? Theme.of(context).extension()!.backgroundColor : Theme.of(context).extension()!.buttonColor, + ), + alignment: Alignment.center, + child: Icon(Icons.arrow_forward, + color: Theme.of(context).extension()!.titleColor), + ), + ), + ) + ], + ), + ); + }); + } +} diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 52a8b06db..27bef933b 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -106,6 +106,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor bool get showAddressBookPopup => _settingsStore.showAddressBookPopupEnabled; + @action + void setShowAddressBookPopup(bool value) { + _settingsStore.showAddressBookPopupEnabled = value; + } + @action void addOutput() { outputs From 597a716f9d4abd647887ee4d36ce054abd4a50ac Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Fri, 21 Mar 2025 04:32:40 +0100 Subject: [PATCH 052/243] CW-686-polyseed-language-issues (#2098) * feat: Fix space for japanese seed * feat: Fix space for japanese seed * feat: Upgrade Polyseed version with improved jp support * feat: Fix monero legacy seed language falling back to english --- cw_monero/lib/api/wallet.dart | 23 +++++++++++++++---- cw_monero/lib/api/wallet_manager.dart | 6 ++++- cw_monero/lib/monero_wallet_service.dart | 18 ++++++--------- cw_monero/pubspec.yaml | 2 +- cw_wownero/pubspec.yaml | 2 +- .../validable_annotated_editable_text.dart | 3 ++- lib/view_model/wallet_creation_vm.dart | 2 +- pubspec_base.yaml | 2 +- 8 files changed, 37 insertions(+), 21 deletions(-) diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index 588ae9750..b9f3d21ec 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -61,9 +61,7 @@ String getSeed() { return legacy; } -String getSeedLegacy(String? language) { - final cakepassphrase = getPassphrase(); - var legacy = monero.Wallet_seed(wptr!, seedOffset: cakepassphrase); +String? getSeedLanguage(String? language) { switch (language) { case "Chinese (Traditional)": language = "Chinese (simplified)"; break; case "Chinese (Simplified)": language = "Chinese (simplified)"; break; @@ -71,8 +69,25 @@ String getSeedLegacy(String? language) { case "Czech": language = "English"; break; case "Japanese": language = "English"; break; } + return language; +} + +String getSeedLegacy(String? language) { + final cakepassphrase = getPassphrase(); + var legacy = monero.Wallet_seed(wptr!, seedOffset: cakepassphrase); + language = getSeedLanguage(language); if (monero.Wallet_status(wptr!) != 0) { - monero.Wallet_setSeedLanguage(wptr!, language: language ?? "English"); + if (language != null) { + monero.Wallet_setSeedLanguage(wptr!, language: language); + final status = monero.Wallet_status(wptr!); + if (status != 0) { + final err = monero.Wallet_errorString(wptr!); + if (legacy.isNotEmpty) { + return "$err\n\n$legacy"; + } + return err; + } + } legacy = monero.Wallet_seed(wptr!, seedOffset: cakepassphrase); } if (monero.Wallet_status(wptr!) != 0) { diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart index 621f79577..7cadadbb9 100644 --- a/cw_monero/lib/api/wallet_manager.dart +++ b/cw_monero/lib/api/wallet_manager.dart @@ -73,6 +73,7 @@ void createWalletSync( required String passphrase, int nettype = 0}) { txhistory = null; + language = getSeedLanguage(language)!; final newWptr = monero.WalletManager_createWallet(wmPtr, path: path, password: password, language: language, networkType: 0); @@ -366,6 +367,9 @@ Future loadWallet( } if (deviceType == 1) { + if (gLedger == null) { + throw Exception("Tried to open a ledger wallet with no ledger connected"); + } final dummyWPtr = wptr ?? monero.WalletManager_openWallet(wmPtr, path: '', password: ''); enableLedgerExchange(dummyWPtr, gLedger!); @@ -533,4 +537,4 @@ Future restoreFromSpendKey( bool isWalletExist({required String path}) => _isWalletExist(path); -bool isViewOnlyBySpendKey() => int.tryParse(monero.Wallet_secretSpendKey(wptr!)) == 0; \ No newline at end of file +bool isViewOnlyBySpendKey() => int.tryParse(monero.Wallet_secretSpendKey(wptr!)) == 0; diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index fbb1c5331..598d400e5 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -11,8 +11,6 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart'; -import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager; import 'package:cw_monero/api/wallet_manager.dart'; @@ -292,16 +290,14 @@ class MoneroWalletService extends WalletService< Future restoreFromSeed( MoneroRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { - if (credentials.mnemonic.split(" ").length == 16) { - // Restore from Polyseed - try { - if (Polyseed.isValidSeed(credentials.mnemonic)) { - return restoreFromPolyseed(credentials); - } - } catch (e) { - printV("Polyseed restore failed: $e"); - rethrow; + // Restore from Polyseed + try { + if (Polyseed.isValidSeed(credentials.mnemonic)) { + return restoreFromPolyseed(credentials); } + } catch (e) { + printV("Polyseed restore failed: $e"); + rethrow; } try { diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index 58c5fc968..862109e94 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: flutter_mobx: ^2.0.6+1 intl: ^0.19.0 encrypt: ^5.0.1 - polyseed: ^0.0.6 + polyseed: ^0.0.7 cw_core: path: ../cw_core monero: diff --git a/cw_wownero/pubspec.yaml b/cw_wownero/pubspec.yaml index d81cda250..b35f36071 100644 --- a/cw_wownero/pubspec.yaml +++ b/cw_wownero/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: flutter_mobx: ^2.0.6+1 intl: ^0.19.0 encrypt: ^5.0.1 - polyseed: ^0.0.6 + polyseed: ^0.0.7 cw_core: path: ../cw_core monero: diff --git a/lib/src/widgets/validable_annotated_editable_text.dart b/lib/src/widgets/validable_annotated_editable_text.dart index a7777961d..77f19fee0 100644 --- a/lib/src/widgets/validable_annotated_editable_text.dart +++ b/lib/src/widgets/validable_annotated_editable_text.dart @@ -92,7 +92,8 @@ class ValidatableAnnotatedEditableTextState extends EditableTextState { List getRanges() { final result = []; - final text = textEditingValue.text; + // Replace Ideographic Space (U+3000) with a normal space + final text = textEditingValue.text.replaceAll("\u3000", " "); final source = text .split(' ') .map((word) { diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 4ee8069b3..f423622f6 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -60,7 +60,7 @@ abstract class WalletCreationVMBase with Store { final SeedSettingsViewModel seedSettingsViewModel; bool isPolyseed(String seed) => - (type == WalletType.monero || type == WalletType.wownero) && + [WalletType.monero, WalletType.wownero].contains(type) && (Polyseed.isValidSeed(seed) || (seed.split(" ").length == 14)); bool nameExists(String name) => walletCreationService.exists(name); diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 7e700e588..ca49f2842 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -104,7 +104,7 @@ dependencies: # ref: main socks5_proxy: ^1.0.4 flutter_svg: ^2.0.9 - polyseed: ^0.0.6 + polyseed: ^0.0.7 nostr_tools: ^1.0.9 ledger_flutter_plus: git: From d44621e6c71999e0c3ddb4af84cac81f4e7f60fb Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 21 Mar 2025 19:11:26 +0200 Subject: [PATCH 053/243] fix new send UI (#2103) * minor ui fixes * localization * Update confirm_sending_bottom_sheet_widget.dart --- .../cake_pay_confirm_purchase_card_page.dart | 4 +-- .../exchange_trade/exchange_trade_page.dart | 6 ++--- lib/src/screens/send/send_page.dart | 18 +++++++++---- .../transaction_details/rbf_details_page.dart | 4 +-- .../confirm_sending_bottom_sheet_widget.dart | 27 +++++++------------ res/values/strings_ar.arb | 2 ++ res/values/strings_bg.arb | 2 ++ res/values/strings_cs.arb | 2 ++ res/values/strings_de.arb | 4 ++- res/values/strings_en.arb | 2 ++ res/values/strings_es.arb | 2 ++ res/values/strings_fr.arb | 2 ++ res/values/strings_ha.arb | 2 ++ res/values/strings_hi.arb | 4 ++- res/values/strings_hr.arb | 2 ++ res/values/strings_hy.arb | 2 ++ res/values/strings_id.arb | 2 ++ res/values/strings_it.arb | 2 ++ res/values/strings_ja.arb | 2 ++ res/values/strings_ko.arb | 2 ++ res/values/strings_my.arb | 2 ++ res/values/strings_nl.arb | 2 ++ res/values/strings_pl.arb | 2 ++ res/values/strings_pt.arb | 2 ++ res/values/strings_ru.arb | 2 ++ res/values/strings_th.arb | 2 ++ res/values/strings_tl.arb | 2 ++ res/values/strings_tr.arb | 2 ++ res/values/strings_uk.arb | 2 ++ res/values/strings_ur.arb | 2 ++ res/values/strings_vi.arb | 2 ++ res/values/strings_yo.arb | 2 ++ res/values/strings_zh.arb | 2 ++ 33 files changed, 87 insertions(+), 32 deletions(-) diff --git a/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart b/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart index 4ac0cf12d..9ae2acf8e 100644 --- a/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart +++ b/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart @@ -384,7 +384,7 @@ class CakePayBuyCardDetailPage extends BasePage { paymentId: S.of(popupContext).payment_id, paymentIdValue: order?.orderId, expirationTime: cakePayPurchaseViewModel.formattedRemainingTime, - titleText: 'Confirm Transaction', + titleText: S.of(popupContext).confirm_transaction, titleIconPath: cakePayPurchaseViewModel.sendViewModel.selectedCryptoCurrency.iconPath, currency: cakePayPurchaseViewModel.sendViewModel.selectedCryptoCurrency, amount: S.of(popupContext).send_amount, @@ -432,7 +432,7 @@ class CakePayBuyCardDetailPage extends BasePage { builder: (BuildContext context) { loadingBottomSheetContext = context; return LoadingBottomSheet( - titleText: 'Generating transaction', + titleText: S.of(context).generating_transaction, ); }, ); diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index 243ab63c8..97d9a34bb 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -252,7 +252,7 @@ class ExchangeTradeState extends State { builder: (BuildContext context) { loadingBottomSheetContext = context; return LoadingBottomSheet( - titleText: 'Generating transaction', + titleText: S.of(context).generating_transaction, ); }, ); @@ -271,7 +271,7 @@ class ExchangeTradeState extends State { return ConfirmSendingBottomSheet( key: ValueKey('exchange_trade_page_confirm_sending_bottom_sheet_key'), currentTheme: widget.currentTheme, - titleText: 'Confirm Transaction', + titleText: S.of(bottomSheetContext).confirm_transaction, titleIconPath: widget.exchangeTradeViewModel.sendViewModel.selectedCryptoCurrency.iconPath, currency: widget.exchangeTradeViewModel.sendViewModel.selectedCryptoCurrency, amount: S.of(bottomSheetContext).send_amount, @@ -307,7 +307,7 @@ class ExchangeTradeState extends State { builder: (BuildContext bottomSheetContext) { return InfoBottomSheet( currentTheme: widget.currentTheme, - titleText: 'Transaction Sent', + titleText: S.of(bottomSheetContext).transaction_sent, contentImage: 'assets/images/birthday_cake.svg', actionButtonText: S.of(bottomSheetContext).close, actionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'), diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index fbbb6c0e1..69e4bd02a 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -532,7 +532,7 @@ class SendPage extends BasePage { builder: (BuildContext context) { loadingBottomSheetContext = context; return LoadingBottomSheet( - titleText: 'Generating transaction', + titleText: S.of(context).generating_transaction, ); }, ); @@ -550,7 +550,7 @@ class SendPage extends BasePage { builder: (BuildContext bottomSheetContext) { return ConfirmSendingBottomSheet( key: ValueKey('send_page_confirm_sending_dialog_key'), - titleText: 'Confirm Transaction', + titleText: S.of(bottomSheetContext).confirm_transaction, currentTheme: currentTheme, titleIconPath: sendViewModel.selectedCryptoCurrency.iconPath, currency: sendViewModel.selectedCryptoCurrency, @@ -598,7 +598,7 @@ class SendPage extends BasePage { currentTheme: currentTheme, showDontAskMeCheckbox: true, onCheckboxChanged: (value) => sendViewModel.setShowAddressBookPopup(!value), - titleText: 'Transaction Sent', + titleText: S.of(bottomSheetContext).transaction_sent, contentImage: 'assets/images/contact_icon.svg', contentImageColor: Theme.of(context).extension()!.titleColor, content: S.of(bottomSheetContext).add_contact_to_address_book, @@ -607,6 +607,8 @@ class SendPage extends BasePage { rightButtonText: 'Yes', actionLeftButton: () { Navigator.of(bottomSheetContext).pop(); + Navigator.of(context) + .pushNamedAndRemoveUntil(Routes.dashboard, (route) => false); RequestReviewHandler.requestReview(); newContactAddress = null; }, @@ -620,11 +622,17 @@ class SendPage extends BasePage { ) : InfoBottomSheet( currentTheme: currentTheme, - titleText: 'Transaction Sent', + titleText: S.of(bottomSheetContext).transaction_sent, contentImage: 'assets/images/birthday_cake.svg', actionButtonText: S.of(bottomSheetContext).close, actionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'), - actionButton: () => Navigator.of(bottomSheetContext).pop()); + actionButton: () { + Navigator.of(bottomSheetContext).pop(); + Navigator.of(context) + .pushNamedAndRemoveUntil(Routes.dashboard, (route) => false); + RequestReviewHandler.requestReview(); + newContactAddress = null; + }); }, ); diff --git a/lib/src/screens/transaction_details/rbf_details_page.dart b/lib/src/screens/transaction_details/rbf_details_page.dart index 3b6513a03..10cd40940 100644 --- a/lib/src/screens/transaction_details/rbf_details_page.dart +++ b/lib/src/screens/transaction_details/rbf_details_page.dart @@ -170,7 +170,7 @@ class RBFDetailsPage extends BasePage { builder: (BuildContext context) { loadingBottomSheetContext = context; return LoadingBottomSheet( - titleText: 'Generating transaction', + titleText: S.of(context).generating_transaction, ); }, ); @@ -188,7 +188,7 @@ class RBFDetailsPage extends BasePage { builder: (BuildContext bottomSheetContext) { return ConfirmSendingBottomSheet( key: ValueKey('rbf_confirm_sending_bottom_sheet'), - titleText: 'Confirm Transaction', + titleText: S.of(bottomSheetContext).confirm_transaction, currentTheme: currentTheme, titleIconPath: transactionDetailsViewModel.sendViewModel.selectedCryptoCurrency.iconPath, currency: transactionDetailsViewModel.sendViewModel.selectedCryptoCurrency, diff --git a/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart index bf16695e9..84997b34e 100644 --- a/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart +++ b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart @@ -275,7 +275,7 @@ class AddressTile extends StatelessWidget { fontFamily: 'Lato', fontWeight: FontWeight.w600, color: currentTheme.type == ThemeType.bright - ? Theme.of(context).extension()!.labelTextColor + ? Theme.of(context).extension()!.titleColor.withOpacity(0.5) : Theme.of(context).extension()!.titleColor, decoration: TextDecoration.none, ); @@ -297,9 +297,7 @@ class AddressTile extends StatelessWidget { ), buildSegmentedAddress( address: address, - evenTextStyle: currentTheme.type == ThemeType.bright - ? itemSubTitleTextStyle - : addressTextStyle, + evenTextStyle: addressTextStyle, oddTextStyle: itemSubTitleTextStyle, ), ], @@ -363,7 +361,7 @@ class AddressExpansionTile extends StatelessWidget { fontFamily: 'Lato', fontWeight: FontWeight.w600, color: currentTheme.type == ThemeType.bright - ? Theme.of(context).extension()!.labelTextColor + ? Theme.of(context).extension()!.titleColor.withOpacity(0.5) : Theme.of(context).extension()!.titleColor, decoration: TextDecoration.none, ); @@ -378,14 +376,14 @@ class AddressExpansionTile extends StatelessWidget { child: Padding( padding: EdgeInsets.symmetric(horizontal: 14, vertical: isBatchSending ? 0 : 8), child: ExpansionTile( - childrenPadding: EdgeInsets.zero, + childrenPadding: isBatchSending ? const EdgeInsets.only(bottom: 8) : EdgeInsets.zero, tilePadding: EdgeInsets.zero, dense: true, visualDensity: VisualDensity.compact, title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(isBatchSending ? name : contactType, style: itemTitleTextStyle), + Expanded(child: Text(isBatchSending ? name : contactType, style: itemTitleTextStyle, softWrap: true)), Text(isBatchSending ? amount : name, style: TextStyle( fontSize: 14, @@ -400,17 +398,10 @@ class AddressExpansionTile extends StatelessWidget { Row( children: [ Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - buildSegmentedAddress( - address: address, - evenTextStyle: currentTheme.type == ThemeType.bright - ? itemSubTitleTextStyle - : addressTextStyle, - oddTextStyle: itemSubTitleTextStyle, - ), - ], + child: buildSegmentedAddress( + address: address, + evenTextStyle: addressTextStyle, + oddTextStyle: itemSubTitleTextStyle, ), ), ], diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 6072b046c..60a0c2b78 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "تأكيد عبارة المرور", "confirm_sending": "تأكيد الإرسال", "confirm_silent_payments_switch_node": "العقدة الحالية لا تدعم المدفوعات الصامتة \\ ncake wallet سوف تتحول إلى عقدة متوافقة ، فقط للمسح الضوئي", + "confirm_transaction": "تأكيد المعاملة", "confirmations": "التأكيدات", "confirmed": "رصيد مؤكد", "confirmed_tx": "مؤكد", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "الغاز المطلوب بالمعاملة يتجاوز البدل.", "generate_name": "توليد الاسم", "generating_gift_card": "يتم توليد بطاقة هدية", + "generating_transaction": "توليد معاملة", "get_a": "احصل على", "get_card_note": " يمكنك إعادة تحميلها بالعملات الرقمية. لا توجد معلومات إضافية مطلوبة!", "get_your_yat": "احصل على Yat", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 62f1bbf18..146fa8efd 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "Потвърдете парола", "confirm_sending": "Потвърждаване на изпращането", "confirm_silent_payments_switch_node": "Текущият ви възел не поддържа Silent Payments \\ Ncake Wallet ще премине към съвместим възел, само за сканиране", + "confirm_transaction": "Потвърдете транзакцията", "confirmations": "потвърждения", "confirmed": "Потвърден баланс", "confirmed_tx": "Потвърдено", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "Газът, изискван от транзакцията, надвишава надбавката.", "generate_name": "Генериране на име", "generating_gift_card": "Създаване на Gift Card", + "generating_transaction": "Генериране на транзакция", "get_a": "Вземете ", "get_card_note": ", която можете да заредите с дигитална валута. Без нужда от допълнителна информация!", "get_your_yat": "Получете своя Yat", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 92eed7a3d..2c770069b 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "Potvrďte přístupovou frázi", "confirm_sending": "Potvrdit odeslání", "confirm_silent_payments_switch_node": "Váš aktuální uzel nepodporuje tiché platby \\ Ncake peněženka se přepne na kompatibilní uzel, pouze pro skenování", + "confirm_transaction": "Potvrďte transakci", "confirmations": "Potvrzení", "confirmed": "Potvrzený zůstatek", "confirmed_tx": "Potvrzeno", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "Plyn vyžadovaný transakcí přesahuje příspěvek.", "generate_name": "Generovat jméno", "generating_gift_card": "Generuji dárkovou kartu", + "generating_transaction": "Generování transakce", "get_a": "Získejte ", "get_card_note": " které můžete nabít digitální měnou. Žádné další informace nejsou vyžadovány!", "get_your_yat": "Získat Yat", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 43db11f18..d7071061b 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "Passphrase bestätigen", "confirm_sending": "Senden bestätigen", "confirm_silent_payments_switch_node": "Ihr aktueller Knoten unterstützt keine Silent Payments.\\n\\nCake Wallet wechselt zu einem kompatiblen Knoten, nur zum Scannen", + "confirm_transaction": "Transaktion bestätigen", "confirmations": "Bestätigungen", "confirmed": "Bestätigter Saldo", "confirmed_tx": "Bestätigt", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "Die durch Transaktion erforderliche Gas übertrifft die Zulage.", "generate_name": "Namen generieren", "generating_gift_card": "Geschenkkarte wird erstellt", + "generating_transaction": "Transaktion erzeugen", "get_a": "Hole ein", "get_card_note": " die Sie mit digitaler Währung aufladen können. Keine zusätzlichen Informationen erforderlich!", "get_your_yat": "Holen Sie sich Ihre Yat", @@ -528,8 +530,8 @@ "please_choose_one": "Bitte wählen Sie einen", "please_fill_totp": "Bitte geben Sie den 8-stelligen Code ein, der auf Ihrem anderen Gerät vorhanden ist", "please_make_selection": "Bitte treffen Sie unten eine Auswahl zum Erstellen oder Wiederherstellen Ihrer Wallet.", - "please_reference_document": "Bitte verweisen Sie auf die folgenden Dokumente, um weitere Informationen zu erhalten.", "Please_reference_document": "Weitere Informationen finden Sie in den Dokumenten unten.", + "please_reference_document": "Bitte verweisen Sie auf die folgenden Dokumente, um weitere Informationen zu erhalten.", "please_select": "Bitte auswählen:", "please_select_backup_file": "Bitte wählen Sie die Sicherungsdatei und geben Sie das Sicherungskennwort ein.", "please_try_to_connect_to_another_node": "Bitte versuchen Sie, sich mit einem anderen Knoten zu verbinden", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index b6023d86e..68e730b27 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "Confirm passphrase", "confirm_sending": "Confirm sending", "confirm_silent_payments_switch_node": "Your current node does not support Silent Payments.\\n\\nCake Wallet will switch to a compatible node while scanning.", + "confirm_transaction": "Confirm transaction", "confirmations": "Confirmations", "confirmed": "Confirmed Balance", "confirmed_tx": "Confirmed", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "Gas required by transaction exceeds allowance.", "generate_name": "Generate Name", "generating_gift_card": "Generating Gift Card", + "generating_transaction": "Generating transaction", "get_a": "Get a ", "get_card_note": " that you can reload with digital currencies. No additional information needed!", "get_your_yat": "Get your Yat", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index c0474d672..3d45ab3a0 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "Confirmar la contraseña", "confirm_sending": "Confirmar envío", "confirm_silent_payments_switch_node": "Tu nodo actual no admite pagos silenciosos \\ nCake cambiará a un nodo compatible, solo para escanear", + "confirm_transaction": "Confirmar transacción", "confirmations": "Confirmaciones", "confirmed": "Saldo confirmado", "confirmed_tx": "Confirmado", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "El gas requerido por la transacción excede la asignación.", "generate_name": "Generar nombre", "generating_gift_card": "Generando tarjeta de regalo", + "generating_transaction": "Generación de transacciones", "get_a": "Obtener un", "get_card_note": " que puedes recargar con monedas digitales. ¡No se necesita información adicional!", "get_your_yat": "Obtén tu Yat", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index d779a22f5..a6433da9d 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "Confirmer la phrase passante", "confirm_sending": "Confirmer l'envoi", "confirm_silent_payments_switch_node": "Votre nœud actuel ne prend pas en charge les paiements silencieux.\n\nCake Wallet passera à un nœud compatible pendant l'analyse.", + "confirm_transaction": "Confirmer la transaction", "confirmations": "Confirmations", "confirmed": "Solde confirmé", "confirmed_tx": "Confirmé", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "Le gaz requis par la transaction dépasse l'allocation.", "generate_name": "Générer un nom", "generating_gift_card": "Génération d'une carte-cadeau", + "generating_transaction": "Transaction de génération", "get_a": "Obtenir un ", "get_card_note": " que vous pouvez recharger avec des devises numériques. Aucune information supplémentaire n'est nécessaire !", "get_your_yat": "Obtenir votre Yat", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 13cbe1828..deec1e470 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "Tabbatar da kalmar wucewa", "confirm_sending": "Tabbatar da aikawa", "confirm_silent_payments_switch_node": "Kumburinku na yanzu ba ya goyan bayan biyan shiru da shiru \\ NCADA Wallet zai canza zuwa kumburi mai dacewa, don bincika", + "confirm_transaction": "Tabbatar da ma'amala", "confirmations": "Tabbatar", "confirmed": "An tabbatar", "confirmed_tx": "Tabbatar", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "Gas da ake buƙata ta hanyar ma'amala ya wuce izini.", "generate_name": "Ƙirƙirar Suna", "generating_gift_card": "Samar da Katin Kyauta", + "generating_transaction": "Ma'amala samar da ma'amala", "get_a": "Samu a", "get_card_note": "cewa zaku iya sake lodawa tare da kudaden dijital. Babu ƙarin bayani da ake buƙata!", "get_your_yat": "Samun Yat ka", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 409e0fd07..91bd94dac 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "पासफ़्रेज़ की पुष्टि करें", "confirm_sending": "भेजने की पुष्टि करें", "confirm_silent_payments_switch_node": "आपका वर्तमान नोड मूक भुगतान का समर्थन नहीं करता है \\ ncake वॉलेट एक संगत नोड पर स्विच करेगा, बस स्कैनिंग के लिए", + "confirm_transaction": "लेनदेन की पुष्टि करें", "confirmations": "पुष्टिकरण", "confirmed": "पुष्टि की गई शेष राशिी", "confirmed_tx": "की पुष्टि", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "लेनदेन द्वारा आवश्यक गैस भत्ता से अधिक है।", "generate_name": "नाम जनरेट करें", "generating_gift_card": "गिफ्ट कार्ड जनरेट कर रहा है", + "generating_transaction": "सृजन लेन -देन", "get_a": "एक प्राप्त करें", "get_card_note": " कि आप डिजिटल मुद्राओं के साथ पुनः लोड कर सकते हैं। कोई अतिरिक्त जानकारी की आवश्यकता नहीं है!", "get_your_yat": "अपना प्राप्त करें Yat", @@ -517,8 +519,8 @@ "paste": "पेस्ट करें", "pause_wallet_creation": "हेवन वॉलेट बनाने की क्षमता फिलहाल रुकी हुई है।", "payment_id": "भुगतान ID: ", - "Payment_was_received": "आपका भुगतान प्राप्त हो गया था।", "payment_was_received": "आपका भुगतान प्राप्त हुआ था।", + "Payment_was_received": "आपका भुगतान प्राप्त हो गया था।", "pending": " (अपूर्ण)", "percentageOf": "${amount} का", "pin_at_top": "शीर्ष पर ${token} पिन करें", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index f93f86d1d..c5e4cd54b 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "Potvrdite prolaznu frazu", "confirm_sending": "Potvrdi slanje", "confirm_silent_payments_switch_node": "Vaš trenutni čvor ne podržava tiha plaćanja \\ ncake novčanik prebacit će se na kompatibilni čvor, samo za skeniranje", + "confirm_transaction": "Potvrdite transakciju", "confirmations": "Potvrde", "confirmed": "Potvrđeno stanje", "confirmed_tx": "Potvrđen", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "Plin potreban transakcijom premašuje dodatak.", "generate_name": "Generiraj ime", "generating_gift_card": "Generiranje darovne kartice", + "generating_transaction": "Generiranje transakcije", "get_a": "Nabavite ", "get_card_note": " koju možete ponovno napuniti digitalnim valutama. Nisu potrebne dodatne informacije!", "get_your_yat": "Uzmi svoj Yat", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 3378f1a0d..d2e307164 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "Հաստատեք գաղտնաբառը", "confirm_sending": "Հաստատեք ուղարկումը", "confirm_silent_payments_switch_node": "Ձեր ընթացիկ հանգույցը չի աջակցում Լուռ վճարումներին\nCake Wallet-ը կանցնի համատեղելի հանգույց, միայն սկանավորման համար", + "confirm_transaction": "Հաստատեք գործարքը", "confirmations": "Հաստատումներ", "confirmed": "Հաստատված մնացորդ", "confirmed_tx": "Հաստատված", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "Գործարքով պահանջվող գազը գերազանցում է նպաստը:", "generate_name": "Գեներացնել անուն", "generating_gift_card": "Գեներացնում է նվեր քարտ", + "generating_transaction": "Ստեղծող գործարք", "get_a": "Ստանալ ", "get_card_note": " որը կարող եք լրացնել թվային արժույթներով: Հավելյալ տեղեկատվություն պետք չէ!", "get_your_yat": "Ստանալ ձեր Yat", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 22558854f..19ffba89e 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "Konfirmasi frasa sandi", "confirm_sending": "Konfirmasi pengiriman", "confirm_silent_payments_switch_node": "Node Anda saat ini tidak mendukung pembayaran diam \\ ncake Wallet akan beralih ke simpul yang kompatibel, hanya untuk pemindaian", + "confirm_transaction": "Konfirmasi transaksi", "confirmations": "Konfirmasi", "confirmed": "Saldo Terkonfirmasi", "confirmed_tx": "Dikonfirmasi", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "Gas yang dibutuhkan oleh transaksi melebihi tunjangan.", "generate_name": "Hasilkan Nama", "generating_gift_card": "Membuat Kartu Hadiah", + "generating_transaction": "Menghasilkan transaksi", "get_a": "Dapatkan ", "get_card_note": " yang dapat Anda muat ulang dengan mata uang digital. Tidak perlu informasi tambahan!", "get_your_yat": "Dapatkan Yat Anda", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index a30944901..721da624a 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "Conferma passphrase", "confirm_sending": "Conferma l'invio", "confirm_silent_payments_switch_node": "Il tuo nodo corrente non supporta Silent Payments\\n\\nCake Wallet passerà a un nodo compatibile durante la scansione.", + "confirm_transaction": "Conferma la transazione", "confirmations": "Conferme", "confirmed": "Saldo Confermato", "confirmed_tx": "Confermato", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "Il Gas richiesto dalla transazione supera il limite consentito.", "generate_name": "Genera nome", "generating_gift_card": "Generazione carta regalo", + "generating_transaction": "Generazione di transazione", "get_a": "Prendi un ", "get_card_note": "che puoi ricaricare con le valute digitali. Non sono necessarie informazioni aggiuntive!", "get_your_yat": "Ottieni il tuo Yat", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 25b16a92a..92130db7b 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "パスフレーズを確認します", "confirm_sending": "送信を確認", "confirm_silent_payments_switch_node": "現在のノードはサイレントペイメントをサポートしていません\\ ncakeウォレットは、スキャン用に互換性のあるノードに切り替えます", + "confirm_transaction": "トランザクションを確認します", "confirmations": "確認", "confirmed": "確認済み残高", "confirmed_tx": "確認済み", @@ -345,6 +346,7 @@ "generate_name": "名前の生成", "generated_gift_card": "ギフトカードの生成", "generating_gift_card": "ギフトカードの生成", + "generating_transaction": "トランザクションの生成", "get_a": "Get a", "get_card_note": "デジタル通貨でリロードできます。追加情報は必要ありません!", "get_your_yat": "あなたのYatを入手してください", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 9849ccd09..3892448a6 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "암호를 확인하십시오", "confirm_sending": "전송 확인", "confirm_silent_payments_switch_node": "현재 노드는 무음 지불을 지원하지 않습니다 \\ ncake 지갑은 스캔을 위해 호환 가능한 노드로 전환됩니다.", + "confirm_transaction": "거래 확인", "confirmations": "확인", "confirmed": "확인된 잔액", "confirmed_tx": "확인", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "거래에 필요한 가스는 수당을 초과합니다.", "generate_name": "이름 생성", "generating_gift_card": "기프트 카드 생성 중", + "generating_transaction": "거래 생성", "get_a": "가져오기", "get_card_note": " 디지털 통화로 충전할 수 있습니다. 추가 정보가 필요하지 않습니다!", "get_your_yat": "당신의 Yat를 얻으십시오", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index a8f57a824..2f7131aa9 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "passphrase အတည်ပြုပါ", "confirm_sending": "ပေးပို့အတည်ပြုပါ။", "confirm_silent_payments_switch_node": "သင်၏လက်ရှိ node သည်အသံတိတ်ငွေပေးချေမှုကိုမပံ့ပိုးပါဟု \\ t", + "confirm_transaction": "ငွေပေးငွေယူအတည်ပြုပါ", "confirmations": "အတည်ပြုချက်များ", "confirmed": "အတည်ပြုထားသော လက်ကျန်ငွေ", "confirmed_tx": "အတည်ပြုသည်", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "ငွေပေးငွေယူမှလိုအပ်သောဓာတ်ငွေ့ထောက်ပံ့ကြေးကျော်လွန်။", "generate_name": "အမည်ဖန်တီးပါ။", "generating_gift_card": "လက်ဆောင်ကတ်ထုတ်ပေးခြင်း။", + "generating_transaction": "ငွေပေးငွေယူကိုထုတ်လုပ်", "get_a": "တစ်ခုရယူပါ။", "get_card_note": " ဒစ်ဂျစ်တယ်ငွေကြေးများဖြင့် ပြန်လည်စတင်နိုင်သည်။ နောက်ထပ် အချက်အလက် မလိုအပ်ပါ။", "get_your_yat": "မင်းရဲ့ Yat ကိုယူလိုက်ပါ။", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index a4f2bb9a1..9d998fcc5 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "Bevestig Passaspherase", "confirm_sending": "Bevestig verzending", "confirm_silent_payments_switch_node": "Uw huidige knooppunt ondersteunt geen stille betalingen \\ ncake -portemonnee schakelt over naar een compatibele knoop", + "confirm_transaction": "Bevestig transactie", "confirmations": "Bevestigingen", "confirmed": "Bevestigd saldo", "confirmed_tx": "Bevestigd", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "Gas vereist door transactie overschrijdt de vergoeding.", "generate_name": "Naam genereren", "generating_gift_card": "Cadeaubon genereren", + "generating_transaction": "Transactie genereren", "get_a": "Krijg een ", "get_card_note": " die u kunt herladen met digitale valuta. Geen aanvullende informatie nodig!", "get_your_yat": "Haal je Yato", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 12e867518..52ee64a5b 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "Potwierdź hasło", "confirm_sending": "Potwierdź wysłanie", "confirm_silent_payments_switch_node": "Twój obecny węzeł nie obsługuje cichych płatności \\ NCAKE Portfel przełączy się na kompatybilny węzeł, tylko do skanowania", + "confirm_transaction": "Potwierdź transakcję", "confirmations": "Potwierdzenia", "confirmed": "Potwierdzone saldo", "confirmed_tx": "Potwierdzony", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "Gaz wymagany przez transakcję przekracza dodatek.", "generate_name": "Wygeneruj nazwę", "generating_gift_card": "Generowanie karty podarunkowej", + "generating_transaction": "Generowanie transakcji", "get_a": "Zdobądź ", "get_card_note": " które możesz doładować walutami cyfrowymi. Nie są potrzebne żadne dodatkowe informacje!", "get_your_yat": "Zdobądź swój Yat", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 7d60a3d79..453dae4db 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "Confirme a senha", "confirm_sending": "Confirmar o envio", "confirm_silent_payments_switch_node": "Seu nó atual não suporta pagamentos silenciosos \n A Cake Wallet mudará para um nó compatível, apenas para escanear", + "confirm_transaction": "Confirme a transação", "confirmations": "Confirmações", "confirmed": "Saldo Confirmado", "confirmed_tx": "Confirmado", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "O gás exigido pela transação excede o subsídio.", "generate_name": "Gerar nome", "generating_gift_card": "Gerando Cartão Presente", + "generating_transaction": "Gerando transação", "get_a": "Obter um ", "get_card_note": " que você pode recarregar com moedas digitais. Nenhuma informação adicional é necessária!", "get_your_yat": "Pegue seu Yat", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 9c80e483f..4decec064 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "Подтвердите Passfrase", "confirm_sending": "Подтвердить отправку", "confirm_silent_payments_switch_node": "Ваш текущий узел не поддерживает Silent Payments \\ ncake Wallet переключится на совместимый узел, только для сканирования", + "confirm_transaction": "Подтвердите транзакцию", "confirmations": "Подтверждения", "confirmed": "Подтвержденный баланс", "confirmed_tx": "Подтвержденный", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "Газ, требуемый в результате транзакции, превышает пособие.", "generate_name": "Создать имя", "generating_gift_card": "Создание подарочной карты", + "generating_transaction": "Генерирующая транзакция", "get_a": "Получить ", "get_card_note": " которую вы можете пополнить цифровой валютой. Дополнительная информация не требуется!", "get_your_yat": "Получить свой Yat", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 874e9f390..f718a9c76 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "ยืนยันวลีรหัสผ่าน", "confirm_sending": "ยืนยันการส่ง", "confirm_silent_payments_switch_node": "โหนดปัจจุบันของคุณไม่รองรับการชำระเงินแบบเงียบ \\ ncake กระเป๋าเงินจะเปลี่ยนเป็นโหนดที่เข้ากันได้เพียงเพื่อการสแกน", + "confirm_transaction": "ยืนยันการทำธุรกรรม", "confirmations": "การยืนยัน", "confirmed": "ยอดคงเหลือที่ยืนยันแล้ว", "confirmed_tx": "ซึ่งยืนยันแล้ว", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "ก๊าซที่ต้องการโดยการทำธุรกรรมเกินค่าเผื่อ", "generate_name": "สร้างชื่อ", "generating_gift_card": "กำลังสร้างบัตรของขวัญ", + "generating_transaction": "การสร้างธุรกรรม", "get_a": "รับ ", "get_card_note": "ที่คุณสามารถเติมเงินด้วยสกุลเงินดิจิตอล ไม่จำเป็นต้องใส่ข้อมูลเพิ่มเติม!", "get_your_yat": "รับ Yat ของคุณ", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 31683c4fc..90ae8f9a8 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "Kumpirma ang passphrase", "confirm_sending": "Kumpirmahin ang pagpapadala", "confirm_silent_payments_switch_node": "Ang iyong kasalukuyang node ay hindi sumusuporta sa tahimik na pagbabayad \\ nCake Wallet ay lilipat sa isang katugmang node, para lamang sa pag-scan", + "confirm_transaction": "Kumpirmahin ang transaksyon", "confirmations": "Mga kumpirmasyon", "confirmed": "Nakumpirma na Balanse", "confirmed_tx": "Nakumpirma", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "Ang gas na kinakailangan ng transaksyon ay lumampas sa allowance.", "generate_name": "Bumuo ng pangalan", "generating_gift_card": "Bumubuo ng Gift Card", + "generating_transaction": "Bumubuo ng transaksyon", "get_a": "Kumuha ng ", "get_card_note": " na maaari mong i-load gamit ang mga digital na pera. Walang karagdagang impormasyon na kailangan!", "get_your_yat": "Kunin ang iyong Yat", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index f7553d5c1..bbcd5e575 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "Parola onaylayın", "confirm_sending": "Göndermeyi onayla", "confirm_silent_payments_switch_node": "Mevcut düğümünüz sessiz ödemeleri desteklemiyor \\ nCake cüzdanı, sadece tarama için uyumlu bir düğüme geçecektir", + "confirm_transaction": "İşlemi onaylayın", "confirmations": "Onay", "confirmed": "Onaylanmış Bakiye", "confirmed_tx": "Onaylanmış", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "İşlemin gerektirdiği gaz ödeneği aşar.", "generate_name": "İsim Oluştur", "generating_gift_card": "Hediye Kartı Oluşturuluyor", + "generating_transaction": "İşlem Oluşturma", "get_a": "Bir ", "get_card_note": " dijital ve fiziksel ön ödemeli banka kartı edinin!", "get_your_yat": "Yat'ını al", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index e3a150bca..77ae06a22 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "Підтвердьте пасфрази", "confirm_sending": "Підтвердити відправлення", "confirm_silent_payments_switch_node": "Ваш поточний вузол не підтримує мовчазні платежі \\ ncake Wallet перейде на сумісний вузол, лише для сканування", + "confirm_transaction": "Підтвердити транзакцію", "confirmations": "Підтвердження", "confirmed": "Підтверджений баланс", "confirmed_tx": "Підтверджений", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "Газ, необхідний транзакціям, перевищує надбавку.", "generate_name": "Згенерувати назву", "generating_gift_card": "Створення подарункової картки", + "generating_transaction": "Генерування транзакції", "get_a": "Отримати ", "get_card_note": " яку можна перезавантажувати цифровими валютами. Додаткова інформація не потрібна!", "get_your_yat": "Одержати свій Yat", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index d95bb9237..fea91580b 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "پاسفریز کی تصدیق کریں", "confirm_sending": "بھیجنے کی تصدیق کریں۔", "confirm_silent_payments_switch_node": "آپ کا موجودہ نوڈ خاموش ادائیگیوں کی حمایت نہیں کرتا ہے۔", + "confirm_transaction": "لین دین کی تصدیق کریں", "confirmations": "تصدیقات", "confirmed": "تصدیق شدہ بیلنس", "confirmed_tx": "تصدیق", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "لین دین کے ذریعہ درکار گیس الاؤنس سے زیادہ ہے۔", "generate_name": "نام پیدا کریں۔", "generating_gift_card": "گفٹ کارڈ تیار کرنا", + "generating_transaction": "ٹرانزیکشن پیدا کرنا", "get_a": "حاصل", "get_card_note": " جسے آپ ڈیجیٹل کرنسیوں کے ساتھ دوبارہ لوڈ کر سکتے ہیں۔ کوئی اضافی معلومات کی ضرورت نہیں!", "get_your_yat": "اپنی Yat حاصل کریں۔", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 3ae00e89f..776131f5f 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -160,6 +160,7 @@ "confirm_passphrase": "Xác nhận cụm mật khẩu", "confirm_sending": "Xác nhận gửi", "confirm_silent_payments_switch_node": "Nút hiện tại của bạn không hỗ trợ thanh toán im lặng\\nCake Wallet sẽ chuyển sang một nút tương thích chỉ để quét", + "confirm_transaction": "Xác nhận giao dịch", "confirmations": "Xác nhận", "confirmed": "Số dư đã xác nhận", "confirmed_tx": "Đã xác nhận", @@ -343,6 +344,7 @@ "gas_exceeds_allowance": "Gas theo yêu cầu của giao dịch vượt quá trợ cấp.", "generate_name": "Tạo tên", "generating_gift_card": "Đang tạo thẻ quà tặng", + "generating_transaction": "Tạo giao dịch", "get_a": "Nhận một ", "get_card_note": " mà bạn có thể nạp lại bằng tiền điện tử. Không cần thêm thông tin!", "get_your_yat": "Nhận Yat của bạn", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 3502629fc..cbe7c8ffd 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "Jẹrisi kọwe", "confirm_sending": "Jẹ́rìí sí ránṣẹ́", "confirm_silent_payments_switch_node": "Ilode rẹ ti lọwọlọwọ ko ṣe atilẹyin awọn sisanwo ti o dakẹ \\ owet apamọwọ yoo yipada si oju-ọrọ ibaramu, o kan fun Scning", + "confirm_transaction": "Jẹrisi iṣowo naa", "confirmations": "Àwọn ẹ̀rí", "confirmed": "A ti jẹ́rìí ẹ̀", "confirmed_tx": "Jẹrisi", @@ -345,6 +346,7 @@ "gas_exceeds_allowance": "Gaasi ti a beere nipasẹ idunadura ju lọ.", "generate_name": "Ṣẹda Orukọ", "generating_gift_card": "À ń dá káàdì ìrajà t'á lò nínú irú kan ìtajà", + "generating_transaction": "Ifọwọsi Iṣowo", "get_a": "Gba ", "get_card_note": " t'ẹ lè fikún owó ayélujára. Ẹ kò nílò ìṣofúnni àfikún!", "get_your_yat": "Gba Yat yín", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 741266f90..598f90a00 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -161,6 +161,7 @@ "confirm_passphrase": "确认密码", "confirm_sending": "确认发送", "confirm_silent_payments_switch_node": "您当前的节点不支持无声付款\\ ncake钱包将切换到兼容节点,仅用于扫描", + "confirm_transaction": "确认交易", "confirmations": "确认", "confirmed": "确认余额", "confirmed_tx": "确认的", @@ -344,6 +345,7 @@ "gas_exceeds_allowance": "交易要求的气体超出了津贴。", "generate_name": "生成名称", "generating_gift_card": "生成礼品卡", + "generating_transaction": "生成交易", "get_a": "得到一个", "get_card_note": "你可以用数字货币重新加载。不需要额外的信息!", "get_your_yat": "得到你的 Yat", From 686580ff7872745e8319e9b853fc6a065f8995d8 Mon Sep 17 00:00:00 2001 From: cyan Date: Fri, 21 Mar 2025 18:22:00 +0100 Subject: [PATCH 054/243] Implement background sync for xmr using flutter_daemon (#2094) * Implement background sync for xmr using flutter_daemon * - initialize app config in background thread - initializeAppConfigs without loading the wallet. * - properly do awaited calls in methodChannel - prevent locking main thread during background sync * add back background sync debug page fix issues caused by xmr wallet being view only (and read only) * changes from review improve starting of bgsync task * update stopBackgroundSync, await listener functions, ensure that listener always start (call _start in constructor) * DO-NOT-MERGE: extre verbose monero logs * stop background service when app is being opened * improve monitoring of background sync * update flutter_daemon to ensure network constraint prevent throwing errors on isBackgroundSyncEnabled check network before syncing * Update lib/main.dart * revert Update main.dart [skip ci] * continously run network check * disable charging requirement, fix status reporting of background sync in UI * Refactor background sync logic, and add UI notifications for battery optimization. Updated flutter_daemon version modified build.gradle for signing config to allow testing in both release and debug modes. * verbose monero only when requested in code. Do not start background sync when battery optimization is on * fix background sync mode not properly reflecting state changes * drop unnecessary dependency --------- Co-authored-by: Omar Hatem --- android/app/build.gradle | 5 +- cw_core/lib/set_app_secure_native.dart | 4 +- cw_core/lib/wallet_base.dart | 6 + cw_monero/lib/api/account_list.dart | 3 +- cw_monero/lib/api/wallet.dart | 25 ++- cw_monero/lib/api/wallet_manager.dart | 66 ++++++- cw_monero/lib/monero_wallet.dart | 55 +++++- cw_wownero/lib/api/wallet_manager.dart | 1 + ios/Podfile.lock | 103 ++++------- ios/Runner/AppDelegate.swift | 10 -- lib/core/background_sync.dart | 107 +++++++++++ lib/di.dart | 14 +- lib/entities/background_tasks.dart | 166 ------------------ lib/entities/load_current_wallet.dart | 3 - lib/main.dart | 44 ++++- lib/reactions/bootstrap.dart | 6 +- lib/router.dart | 10 ++ lib/routes.dart | 2 + .../screens/dev/monero_background_sync.dart | 112 ++++++++++++ .../settings/background_sync_page.dart | 91 ++++++++++ .../settings/connection_sync_page.dart | 51 +----- .../screens/settings/other_settings_page.dart | 7 + lib/store/settings_store.dart | 20 +-- lib/utils/feature_flag.dart | 2 +- .../dashboard/dashboard_view_model.dart | 81 ++++++++- .../dev/monero_background_sync.dart | 106 +++++++++++ lib/view_model/settings/sync_mode.dart | 10 +- lib/view_model/wallet_creation_vm.dart | 2 - .../wallet_groups_display_view_model.dart | 1 + .../wallet_list/wallet_list_item.dart | 2 + .../wallet_list/wallet_list_view_model.dart | 1 + pubspec_base.yaml | 5 +- res/values/strings_ar.arb | 3 + res/values/strings_bg.arb | 3 + res/values/strings_cs.arb | 3 + res/values/strings_de.arb | 3 + res/values/strings_en.arb | 3 + res/values/strings_es.arb | 3 + res/values/strings_fr.arb | 3 + res/values/strings_ha.arb | 3 + res/values/strings_hi.arb | 3 + res/values/strings_hr.arb | 3 + res/values/strings_hy.arb | 3 + res/values/strings_id.arb | 3 + res/values/strings_it.arb | 3 + res/values/strings_ja.arb | 3 + res/values/strings_ko.arb | 3 + res/values/strings_my.arb | 3 + res/values/strings_nl.arb | 3 + res/values/strings_pl.arb | 3 + res/values/strings_pt.arb | 3 + res/values/strings_ru.arb | 3 + res/values/strings_th.arb | 3 + res/values/strings_tl.arb | 3 + res/values/strings_tr.arb | 3 + res/values/strings_uk.arb | 3 + res/values/strings_ur.arb | 3 + res/values/strings_vi.arb | 3 + res/values/strings_yo.arb | 3 + res/values/strings_zh.arb | 3 + 60 files changed, 853 insertions(+), 352 deletions(-) create mode 100644 lib/core/background_sync.dart delete mode 100644 lib/entities/background_tasks.dart create mode 100644 lib/src/screens/dev/monero_background_sync.dart create mode 100644 lib/src/screens/settings/background_sync_page.dart create mode 100644 lib/view_model/dev/monero_background_sync.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index b65c54108..67f34cc67 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -37,7 +37,7 @@ if (appPropertiesFile.exists()) { } android { - compileSdkVersion 34 + compileSdkVersion 35 buildToolsVersion "34.0.0" lintOptions { @@ -81,6 +81,9 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } + debug { + signingConfig signingConfigs.release + } } ndkVersion "27.0.12077973" diff --git a/cw_core/lib/set_app_secure_native.dart b/cw_core/lib/set_app_secure_native.dart index 84096e2d6..16faa56c1 100644 --- a/cw_core/lib/set_app_secure_native.dart +++ b/cw_core/lib/set_app_secure_native.dart @@ -1,9 +1,9 @@ import 'package:flutter/services.dart'; -void setIsAppSecureNative(bool isAppSecure) { +Future setIsAppSecureNative(bool isAppSecure) async { try { final utils = const MethodChannel('com.cake_wallet/native_utils'); - utils.invokeMethod('setIsAppSecure', {'isAppSecure': isAppSecure}); + await utils.invokeMethod('setIsAppSecure', {'isAppSecure': isAppSecure}); } catch (_) {} } diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 42bd66da0..e15dca89b 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -67,6 +67,12 @@ abstract class WalletBase connectToPowNode({required Node node}) async {} + // startBackgroundSync is used to start sync in the background, without doing any + // extra things in the background. + // startSync is used as a fallback. + Future startBackgroundSync() => startSync(); + Future stopBackgroundSync(String password) => stopSync(); + Future startSync(); Future stopSync() async {} diff --git a/cw_monero/lib/api/account_list.dart b/cw_monero/lib/api/account_list.dart index e3bb25c97..0e55ce15c 100644 --- a/cw_monero/lib/api/account_list.dart +++ b/cw_monero/lib/api/account_list.dart @@ -37,7 +37,8 @@ List getAllAccount() { int size = monero.SubaddressAccount_getAll_size(subaddressAccount!); if (size == 0) { monero.Wallet_addSubaddressAccount(wptr!); - return getAllAccount(); + monero.Wallet_status(wptr!); + return []; } return List.generate(size, (index) { return monero.SubaddressAccount_getAll_byIndex(subaddressAccount!, index: index); diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index b9f3d21ec..755887652 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:ffi'; import 'dart:isolate'; +import 'package:cw_core/root_dir.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/exceptions/setup_wallet_exception.dart'; @@ -108,9 +109,13 @@ Map>> addressCache = {}; String getAddress({int accountIndex = 0, int addressIndex = 0}) { // printV("getaddress: ${accountIndex}/${addressIndex}: ${monero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)}: ${monero.Wallet_address(wptr!, accountIndex: accountIndex, addressIndex: addressIndex)}"); - while (monero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)-1 < addressIndex) { - printV("adding subaddress"); - monero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex); + // this could be a while loop, but I'm in favor of making it if to not cause freezes + if (monero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)-1 < addressIndex) { + if (monero.Wallet_numSubaddressAccounts(wptr!) < accountIndex) { + monero.Wallet_addSubaddressAccount(wptr!); + } else { + monero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex); + } } addressCache[wptr!.address] ??= {}; addressCache[wptr!.address]![accountIndex] ??= {}; @@ -149,6 +154,7 @@ Future setupNodeSync( } '''); final addr = wptr!.address; + printV("init: start"); await Isolate.run(() { monero.Wallet_init(Pointer.fromAddress(addr), daemonAddress: address, @@ -157,6 +163,7 @@ Future setupNodeSync( daemonUsername: login ?? '', daemonPassword: password ?? ''); }); + printV("init: end"); final status = monero.Wallet_status(wptr!); @@ -168,7 +175,7 @@ Future setupNodeSync( } } - if (kDebugMode && debugMonero) { + if (true) { monero.Wallet_init3( wptr!, argv0: '', defaultLogBaseName: 'moneroc', @@ -243,7 +250,9 @@ class SyncListener { SyncListener(this.onNewBlock, this.onNewTransaction) : _cachedBlockchainHeight = 0, _lastKnownBlockHeight = 0, - _initialSyncHeight = 0; + _initialSyncHeight = 0 { + _start(); + } void Function(int, int, double) onNewBlock; void Function() onNewTransaction; @@ -261,7 +270,7 @@ class SyncListener { return _cachedBlockchainHeight; } - void start() { + void _start() { _cachedBlockchainHeight = 0; _lastKnownBlockHeight = 0; _initialSyncHeight = 0; @@ -282,7 +291,7 @@ class SyncListener { } final bchHeight = await getNodeHeightOrUpdate(syncHeight); - + // printV("syncHeight: $syncHeight, _lastKnownBlockHeight: $_lastKnownBlockHeight, bchHeight: $bchHeight"); if (_lastKnownBlockHeight == syncHeight) { return; } @@ -379,4 +388,4 @@ String signMessage(String message, {String address = ""}) { bool verifyMessage(String message, String address, String signature) { return monero.Wallet_verifySignedMessage(wptr!, message: message, address: address, signature: signature); -} +} \ No newline at end of file diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart index 7cadadbb9..bfebe4247 100644 --- a/cw_monero/lib/api/wallet_manager.dart +++ b/cw_monero/lib/api/wallet_manager.dart @@ -11,6 +11,7 @@ import 'package:cw_monero/api/exceptions/wallet_restore_from_seed_exception.dart import 'package:cw_monero/api/transaction_history.dart'; import 'package:cw_monero/api/wallet.dart'; import 'package:cw_monero/ledger.dart'; +import 'package:flutter/foundation.dart'; import 'package:monero/monero.dart' as monero; class MoneroCException implements Exception { @@ -50,7 +51,13 @@ final monero.WalletManager wmPtr = Pointer.fromAddress((() { // codebase, so it will be easier to debug what happens. At least easier // than plugging gdb in. Especially on windows/android. monero.printStarts = false; + if (kDebugMode && debugMonero) { + monero.WalletManagerFactory_setLogLevel(4); + } _wmPtr ??= monero.WalletManagerFactory_getWalletManager(); + if (kDebugMode && debugMonero) { + monero.WalletManagerFactory_setLogLevel(4); + } printV("ptr: $_wmPtr"); } catch (e) { printV(e); @@ -77,10 +84,17 @@ void createWalletSync( final newWptr = monero.WalletManager_createWallet(wmPtr, path: path, password: password, language: language, networkType: 0); - final status = monero.Wallet_status(newWptr); + int status = monero.Wallet_status(newWptr); if (status != 0) { throw WalletCreationException(message: monero.Wallet_errorString(newWptr)); } + + monero.Wallet_setupBackgroundSync(newWptr, backgroundSyncType: 2, walletPassword: password, backgroundCachePassword: ''); + status = monero.Wallet_status(newWptr); + if (status != 0) { + throw WalletCreationException(message: monero.Wallet_errorString(newWptr)); + } + wptr = newWptr; monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase); monero.Wallet_store(wptr!, path: path); @@ -166,12 +180,19 @@ void restoreWalletFromKeysSync( nettype: 0, ); - final status = monero.Wallet_status(newWptr); + int status = monero.Wallet_status(newWptr); if (status != 0) { throw WalletRestoreFromKeysException( message: monero.Wallet_errorString(newWptr)); } + + monero.Wallet_setupBackgroundSync(newWptr, backgroundSyncType: 2, walletPassword: password, backgroundCachePassword: ''); + status = monero.Wallet_status(newWptr); + if (status != 0) { + throw WalletCreationException(message: monero.Wallet_errorString(newWptr)); + } + // CW-712 - Try to restore deterministic wallet first, if the view key doesn't // match the view key provided if (spendKey != "") { @@ -190,11 +211,17 @@ void restoreWalletFromKeysSync( spendKeyString: spendKey, nettype: 0, ); - final status = monero.Wallet_status(newWptr); + int status = monero.Wallet_status(newWptr); if (status != 0) { throw WalletRestoreFromKeysException( message: monero.Wallet_errorString(newWptr)); } + + monero.Wallet_setupBackgroundSync(newWptr, backgroundSyncType: 2, walletPassword: password, backgroundCachePassword: ''); + status = monero.Wallet_status(newWptr); + if (status != 0) { + throw WalletCreationException(message: monero.Wallet_errorString(newWptr)); + } } } @@ -227,7 +254,7 @@ void restoreWalletFromPolyseedWithOffset( kdfRounds: 1, ); - final status = monero.Wallet_status(newWptr); + int status = monero.Wallet_status(newWptr); if (status != 0) { final err = monero.Wallet_errorString(newWptr); @@ -240,6 +267,12 @@ void restoreWalletFromPolyseedWithOffset( monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: seedOffset); monero.Wallet_store(wptr!); + + monero.Wallet_setupBackgroundSync(newWptr, backgroundSyncType: 2, walletPassword: password, backgroundCachePassword: ''); + status = monero.Wallet_status(newWptr); + if (status != 0) { + throw WalletCreationException(message: monero.Wallet_errorString(newWptr)); + } storeSync(); openedWalletsByPath[path] = wptr!; @@ -277,7 +310,7 @@ void restoreWalletFromSpendKeySync( restoreHeight: restoreHeight, ); - final status = monero.Wallet_status(newWptr); + int status = monero.Wallet_status(newWptr); if (status != 0) { final err = monero.Wallet_errorString(newWptr); @@ -290,6 +323,12 @@ void restoreWalletFromSpendKeySync( monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); storeSync(); + + monero.Wallet_setupBackgroundSync(newWptr, backgroundSyncType: 2, walletPassword: password, backgroundCachePassword: ''); + status = monero.Wallet_status(newWptr); + if (status != 0) { + throw WalletCreationException(message: monero.Wallet_errorString(newWptr)); + } openedWalletsByPath[path] = wptr!; _lastOpenedWallet = path; @@ -321,6 +360,14 @@ Future restoreWalletFromHardwareWallet( final error = monero.Wallet_errorString(newWptr); throw WalletRestoreFromSeedException(message: error); } + + // TODO: Check with upstream if we can use background sync here + // monero.Wallet_setupBackgroundSync(newWptr, backgroundSyncType: 2, walletPassword: password, backgroundCachePassword: ''); + // status = monero.Wallet_status(newWptr); + // if (status != 0) { + // throw WalletCreationException(message: monero.Wallet_errorString(newWptr)); + // } + wptr = newWptr; _lastOpenedWallet = path; openedWalletsByPath[path] = wptr!; @@ -384,7 +431,14 @@ Future loadWallet( final newWptr = Pointer.fromAddress(newWptrAddr); - final status = monero.Wallet_status(newWptr); + int status = monero.Wallet_status(newWptr); + if (status != 0) { + final err = monero.Wallet_errorString(newWptr); + printV("loadWallet:"+err); + throw WalletOpeningException(message: err); + } + monero.Wallet_setupBackgroundSync(newWptr, backgroundSyncType: 2, walletPassword: password, backgroundCachePassword: ''); + status = monero.Wallet_status(newWptr); if (status != 0) { final err = monero.Wallet_errorString(newWptr); printV("loadWallet:"+err); diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index ebad95f8e..db4d30ee8 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -218,7 +218,7 @@ abstract class MoneroWalletBase extends WalletBase startBackgroundSync() async { + if (isBackgroundSyncRunning) { + printV("Background sync already running"); + return; + } + isBackgroundSyncRunning = true; + int status = monero.Wallet_status(wptr!); + if (status != 0) { + final err = monero.Wallet_errorString(wptr!); + throw Exception("unable to setup background sync: $err"); + } + await save(); + + monero.Wallet_startBackgroundSync(wptr!); + status = monero.Wallet_status(wptr!); + if (status != 0) { + final err = monero.Wallet_errorString(wptr!); + throw Exception("unable to start background sync: $err"); + } + await save(); + await init(); + await startSync(); + } + + bool isBackgroundSyncRunning = false; + + @action + @override + Future stopSync() async { + if (isBackgroundSyncRunning) { + printV("Stopping background sync"); + await save(); + monero.Wallet_stopBackgroundSync(wptr!, ''); + await save(); + isBackgroundSyncRunning = false; + } + } + + @action + @override + Future stopBackgroundSync(String password) async { + if (isBackgroundSyncRunning) { + printV("Stopping background sync"); + await save(); + monero.Wallet_stopBackgroundSync(wptr!, password); + await save(); + isBackgroundSyncRunning = false; + } + } + @override Future startSync() async { try { @@ -250,7 +301,6 @@ abstract class MoneroWalletBase extends WalletBase sync() async { + printV("Background sync started"); + await _syncMonero(); + printV("Background sync completed"); + } + + Future _syncMonero() async { + final walletLoadingService = getIt.get(); + final walletListViewModel = getIt.get(); + final settingsStore = getIt.get(); + + + final List moneroWallets = walletListViewModel.wallets + .where((element) => !element.isHardware) + .where((element) => [WalletType.monero].contains(element.type)) + .toList(); + for (int i = 0; i < moneroWallets.length; i++) { + final wallet = await walletLoadingService.load(moneroWallets[i].type, moneroWallets[i].name); + int syncedTicks = 0; + final keyService = getIt.get(); + + int stuckTicks = 0; + + inner: + while (true) { + await Future.delayed(const Duration(seconds: 1)); + final syncStatus = wallet.syncStatus; + final progress = syncStatus.progress(); + if (syncStatus is ConnectedSyncStatus || syncStatus is AttemptingSyncStatus || syncStatus is NotConnectedSyncStatus) { + stuckTicks++; + if (stuckTicks > 30) { + printV("${wallet.name} STUCK SYNCING"); + break inner; + } + } else { + stuckTicks = 0; + } + if (syncStatus is NotConnectedSyncStatus) { + printV("${wallet.name} NOT CONNECTED"); + final node = settingsStore.getCurrentNode(wallet.type); + await wallet.connectToNode(node: node); + await wallet.startBackgroundSync(); + printV("STARTED SYNC"); + continue inner; + } + + if (progress > 0.999 || syncStatus is SyncedSyncStatus) { + syncedTicks++; + if (syncedTicks > 5) { + syncedTicks = 0; + printV("WALLET $i SYNCED"); + try { + await wallet.stopBackgroundSync((await keyService.getWalletPassword(walletName: wallet.name))); + } catch (e) { + printV("error stopping sync: $e"); + } + break inner; + } + } else { + syncedTicks = 0; + } + if (kDebugMode) { + if (syncStatus is SyncingSyncStatus) { + final blocksLeft = syncStatus.blocksLeft; + printV("$blocksLeft Blocks Left"); + } else if (syncStatus is SyncedSyncStatus) { + printV("Synced"); + } else if (syncStatus is SyncedTipSyncStatus) { + printV("Scanned Tip: ${syncStatus.tip}"); + } else if (syncStatus is NotConnectedSyncStatus) { + printV("Still Not Connected"); + } else if (syncStatus is AttemptingSyncStatus) { + printV("Attempting Sync"); + } else if (syncStatus is StartingScanSyncStatus) { + printV("Starting Scan"); + } else if (syncStatus is SyncronizingSyncStatus) { + printV("Syncronizing"); + } else if (syncStatus is FailedSyncStatus) { + printV("Failed Sync"); + } else if (syncStatus is ConnectingSyncStatus) { + printV("Connecting"); + } else { + printV("Unknown Sync Status ${syncStatus.runtimeType}"); + } + } + } + await wallet.stopBackgroundSync(await keyService.getWalletPassword(walletName: wallet.name)); + await wallet.close(shouldCleanup: true); + } + } +} \ No newline at end of file diff --git a/lib/di.dart b/lib/di.dart index 8d9b045df..8fb60fa29 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -26,7 +26,6 @@ import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; import 'package:cake_wallet/core/yat_service.dart'; -import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/entities/biometric_auth.dart'; import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/contact_record.dart'; @@ -34,6 +33,9 @@ import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/hardware_wallet/require_hardware_wallet_connection.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; +import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart'; +import 'package:cake_wallet/src/screens/settings/background_sync_page.dart'; +import 'package:cake_wallet/view_model/dev/monero_background_sync.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; @@ -309,9 +311,6 @@ Future setup({ getIt.registerSingletonAsync(() => SharedPreferences.getInstance()); getIt.registerSingleton(secureStorage); } - if (!_isSetupFinished) { - getIt.registerFactory(() => BackgroundTasks()); - } final isBitcoinBuyEnabled = (secrets.wyreSecretKey.isNotEmpty) && (secrets.wyreApiKey.isNotEmpty) && @@ -909,6 +908,8 @@ Future setup({ getIt.registerFactory(() => SeedSettingsViewModel(getIt.get(), getIt.get())); + getIt.registerFactory(() => DevMoneroBackgroundSync(getIt.get().wallet!)); + getIt.registerFactoryParam((bool isWalletCreated, _) => WalletSeedPage(getIt.get(), isNewWalletCreated: isWalletCreated)); @@ -1068,6 +1069,8 @@ Future setup({ getIt.registerFactory( () => ExchangeTradeExternalSendPage(exchangeTradeViewModel: getIt.get())); + getIt.registerFactory(() => BackgroundSyncPage(getIt.get())); + getIt.registerFactory(() => ExchangeTemplatePage(getIt.get())); getIt.registerFactoryParam((WalletType param1, __) { @@ -1443,7 +1446,8 @@ Future setup({ getIt.registerFactory(() => SignViewModel(getIt.get().wallet!)); - getIt.registerFactory(() => SeedVerificationPage(getIt.get())); + getIt.registerFactory(() => SeedVerificationPage(getIt.get())); + getIt.registerFactory(() => DevMoneroBackgroundSyncPage(getIt.get())); _isSetupFinished = true; } diff --git a/lib/entities/background_tasks.dart b/lib/entities/background_tasks.dart deleted file mode 100644 index 60e4c86cd..000000000 --- a/lib/entities/background_tasks.dart +++ /dev/null @@ -1,166 +0,0 @@ -import 'dart:io'; - -import 'package:cake_wallet/core/wallet_loading_service.dart'; -import 'package:cake_wallet/entities/preferences_key.dart'; -import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cake_wallet/utils/device_info.dart'; -import 'package:cake_wallet/utils/feature_flag.dart'; -import 'package:cake_wallet/view_model/settings/sync_mode.dart'; -import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; -import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:flutter/foundation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:workmanager/workmanager.dart'; -import 'package:cake_wallet/main.dart'; -import 'package:cake_wallet/di.dart'; - -const moneroSyncTaskKey = "com.fotolockr.cakewallet.monero_sync_task"; - -@pragma('vm:entry-point') -void callbackDispatcher() { - Workmanager().executeTask((task, inputData) async { - try { - switch (task) { - case moneroSyncTaskKey: - - /// The work manager runs on a separate isolate from the main flutter isolate. - /// thus we initialize app configs first; hive, getIt, etc... - await initializeAppConfigs(); - - final walletLoadingService = getIt.get(); - - final typeRaw = getIt.get().getInt(PreferencesKey.currentWalletType); - - WalletBase? wallet; - - if (inputData!['sync_all'] as bool) { - /// get all Monero wallets of the user and sync them - final List moneroWallets = getIt - .get() - .wallets - .where((element) => [WalletType.monero, WalletType.wownero].contains(element.type)) - .toList(); - - for (int i = 0; i < moneroWallets.length; i++) { - wallet = - await walletLoadingService.load(moneroWallets[i].type, moneroWallets[i].name); - final node = getIt.get().getCurrentNode(moneroWallets[i].type); - await wallet.connectToNode(node: node); - await wallet.startSync(); - } - } else { - /// if the user chose to sync only active wallet - /// if the current wallet is monero; sync it only - if (typeRaw == WalletType.monero.index || typeRaw == WalletType.wownero.index) { - final name = - getIt.get().getString(PreferencesKey.currentWalletName); - - wallet = await walletLoadingService.load(WalletType.values[typeRaw!], name!); - final node = getIt.get().getCurrentNode(WalletType.values[typeRaw]); - - await wallet.connectToNode(node: node); - await wallet.startSync(); - } - } - - if (wallet?.syncStatus.progress() == null) { - return Future.error("No Monero/Wownero wallet found"); - } - - for (int i = 0;; i++) { - await Future.delayed(const Duration(seconds: 1)); - if (wallet?.syncStatus.progress() == 1.0) { - break; - } - if (i > 600) { - return Future.error("Synchronization Timed out"); - } - } - break; - } - - return Future.value(true); - } catch (error, stackTrace) { - printV(error); - printV(stackTrace); - return Future.error(error); - } - }); -} - -class BackgroundTasks { - void registerSyncTask({bool changeExisting = false}) async { - try { - bool hasMonero = getIt - .get() - .wallets - .any((element) => element.type == WalletType.monero); - - /// if its not android nor ios, or the user has no monero wallets; exit - if (!DeviceInfo.instance.isMobile || !hasMonero) { - return; - } - - final settingsStore = getIt.get(); - - final SyncMode syncMode = settingsStore.currentSyncMode; - final bool syncAll = settingsStore.currentSyncAll; - - if (syncMode.type == SyncType.disabled || !FeatureFlag.isBackgroundSyncEnabled) { - cancelSyncTask(); - return; - } - - await Workmanager().initialize( - callbackDispatcher, - isInDebugMode: kDebugMode, - ); - - final inputData = {"sync_all": syncAll}; - final constraints = Constraints( - networkType: - syncMode.type == SyncType.unobtrusive ? NetworkType.unmetered : NetworkType.connected, - requiresBatteryNotLow: syncMode.type == SyncType.unobtrusive, - requiresCharging: syncMode.type == SyncType.unobtrusive, - requiresDeviceIdle: syncMode.type == SyncType.unobtrusive, - ); - - if (Platform.isIOS) { - await Workmanager().registerOneOffTask( - moneroSyncTaskKey, - moneroSyncTaskKey, - initialDelay: syncMode.frequency, - existingWorkPolicy: ExistingWorkPolicy.replace, - inputData: inputData, - constraints: constraints, - ); - return; - } - - await Workmanager().registerPeriodicTask( - moneroSyncTaskKey, - moneroSyncTaskKey, - initialDelay: syncMode.frequency, - frequency: syncMode.frequency, - existingWorkPolicy: changeExisting ? ExistingWorkPolicy.replace : ExistingWorkPolicy.keep, - inputData: inputData, - constraints: constraints, - ); - } catch (error, stackTrace) { - printV(error); - printV(stackTrace); - } - } - - void cancelSyncTask() { - try { - Workmanager().cancelByUniqueName(moneroSyncTaskKey); - } catch (error, stackTrace) { - printV(error); - printV(stackTrace); - } - } -} diff --git a/lib/entities/load_current_wallet.dart b/lib/entities/load_current_wallet.dart index e67b59997..a421168d0 100644 --- a/lib/entities/load_current_wallet.dart +++ b/lib/entities/load_current_wallet.dart @@ -1,7 +1,6 @@ import 'package:cake_wallet/di.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cake_wallet/store/app_store.dart'; -import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; @@ -26,6 +25,4 @@ Future loadCurrentWallet({String? password}) async { name, password: password); await appStore.changeCurrentWallet(wallet); - - getIt.get().registerSyncTask(); } diff --git a/lib/main.dart b/lib/main.dart index 2bf0f269d..7e5f24b60 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,11 @@ import 'dart:async'; import 'dart:io'; +import 'dart:ui'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/app_scroll_behavior.dart'; import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/core/background_sync.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/default_settings_migration.dart'; @@ -34,11 +36,13 @@ import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/mweb_utxo.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_daemon/flutter_daemon.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:hive/hive.dart'; import 'package:cw_core/root_dir.dart'; @@ -68,6 +72,7 @@ Future runAppWithZone({Key? topLevelKey}) async { return true; }; + await FlutterDaemon().unmarkBackgroundSync(); await initializeAppAtRoot(); if (kDebugMode) { @@ -100,7 +105,7 @@ Future initializeAppAtRoot({bool reInitializing = false}) async { await initializeAppConfigs(); } -Future initializeAppConfigs() async { +Future initializeAppConfigs({bool loadWallet = true}) async { setRootDirFromEnv(); final appDir = await getAppDir(); CakeHive.init(appDir.path); @@ -200,6 +205,7 @@ Future initializeAppConfigs() async { encryptionKey: havenSeedStoreBoxKey); await initialSetup( + loadWallet: loadWallet, sharedPreferences: await SharedPreferences.getInstance(), nodes: nodes, powNodes: powNodes, @@ -220,7 +226,8 @@ Future initializeAppConfigs() async { } Future initialSetup( - {required SharedPreferences sharedPreferences, + {required bool loadWallet, + required SharedPreferences sharedPreferences, required Box nodes, required Box powNodes, required Box walletInfoSource, @@ -262,7 +269,7 @@ Future initialSetup( navigatorKey: navigatorKey, secureStorage: secureStorage, ); - await bootstrap(navigatorKey); + await bootstrap(navigatorKey, loadWallet: loadWallet); } class App extends StatefulWidget { @@ -390,3 +397,34 @@ class TopLevelErrorWidget extends StatelessWidget { ); } } + +@pragma('vm:entry-point') +Future backgroundSync() async { + bool shouldUnmark = false; + try { + printV("Background sync triggered"); + printV("- WidgetsFlutterBinding.ensureInitialized()"); + WidgetsFlutterBinding.ensureInitialized(); + printV("- DartPluginRegistrant.ensureInitialized()"); + DartPluginRegistrant.ensureInitialized(); + printV("- FlutterDaemon.markBackgroundSync()"); + final val = await FlutterDaemon().markBackgroundSync(); + if (val) { + printV("Background sync already in progress"); + return; + } + shouldUnmark = true; + printV("Starting background sync"); + final backgroundSync = BackgroundSync(); + await initializeAppConfigs(loadWallet: false); + await backgroundSync.sync(); + printV("Background sync completed"); + } finally { + if (shouldUnmark) { + printV("Unmarking background sync"); + await FlutterDaemon().unmarkBackgroundSync(); + } else { + printV("Not unmarking background sync"); + } + } +} diff --git a/lib/reactions/bootstrap.dart b/lib/reactions/bootstrap.dart index bf045c0dd..e767433aa 100644 --- a/lib/reactions/bootstrap.dart +++ b/lib/reactions/bootstrap.dart @@ -15,7 +15,7 @@ import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/authentication_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; -Future bootstrap(GlobalKey navigatorKey) async { +Future bootstrap(GlobalKey navigatorKey, {required bool loadWallet}) async { final appStore = getIt.get(); final authenticationStore = getIt.get(); final settingsStore = getIt.get(); @@ -27,7 +27,9 @@ Future bootstrap(GlobalKey navigatorKey) async { authenticationStore.installed(); } - startAuthenticationStateChange(authenticationStore, navigatorKey); + if (loadWallet) { + startAuthenticationStateChange(authenticationStore, navigatorKey); + } startCurrentWalletChangeReaction(appStore, settingsStore, fiatConversionStore); startCurrentFiatChangeReaction(appStore, settingsStore, fiatConversionStore); startCurrentFiatApiModeChangeReaction(appStore, settingsStore, fiatConversionStore); diff --git a/lib/router.dart b/lib/router.dart index 54cbb1531..35177c0eb 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -36,6 +36,7 @@ import 'package:cake_wallet/src/screens/dashboard/pages/address_page.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/nft_details_page.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart'; import 'package:cake_wallet/src/screens/dashboard/sign_page.dart'; +import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart'; import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart'; import 'package:cake_wallet/src/screens/exchange/exchange_page.dart'; import 'package:cake_wallet/src/screens/exchange/exchange_template_page.dart'; @@ -73,6 +74,7 @@ import 'package:cake_wallet/src/screens/seed/wallet_seed_page.dart'; import 'package:cake_wallet/src/screens/send/send_page.dart'; import 'package:cake_wallet/src/screens/send/send_template_page.dart'; import 'package:cake_wallet/src/screens/send/transaction_success_info_page.dart'; +import 'package:cake_wallet/src/screens/settings/background_sync_page.dart'; import 'package:cake_wallet/src/screens/settings/connection_sync_page.dart'; import 'package:cake_wallet/src/screens/settings/desktop_settings/desktop_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/display_settings_page.dart'; @@ -824,6 +826,14 @@ Route createRoute(RouteSettings settings) { case Routes.exchangeTradeExternalSendPage: return MaterialPageRoute(builder: (_) => getIt.get(),); + case Routes.backgroundSync: + return CupertinoPageRoute( + fullscreenDialog: true, builder: (_) => getIt.get()); + case Routes.devMoneroBackgroundSync: + return MaterialPageRoute( + builder: (_) => getIt.get(), + ); + default: return MaterialPageRoute( builder: (_) => Scaffold( diff --git a/lib/routes.dart b/lib/routes.dart index e2b4fdb34..be5e9f05d 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -110,6 +110,8 @@ class Routes { static const nftDetailsPage = '/nft_details_page'; static const importNFTPage = '/import_nft_page'; static const torPage = '/tor_page'; + static const backgroundSync = '/background_sync'; + static const devMoneroBackgroundSync = '/dev/monero_background_sync'; static const signPage = '/sign_page'; static const connectDevices = '/device/connect'; diff --git a/lib/src/screens/dev/monero_background_sync.dart b/lib/src/screens/dev/monero_background_sync.dart new file mode 100644 index 000000000..8927b33cf --- /dev/null +++ b/lib/src/screens/dev/monero_background_sync.dart @@ -0,0 +1,112 @@ +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/view_model/dev/monero_background_sync.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class DevMoneroBackgroundSyncPage extends BasePage { + final DevMoneroBackgroundSync viewModel; + + DevMoneroBackgroundSyncPage(this.viewModel); + + @override + String? get title => "[dev] xmr background sync"; + + Widget _buildSingleCell(String title, String value) { + return Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(title, style: TextStyle(fontWeight: FontWeight.bold)), + Text(value, maxLines: 1, overflow: TextOverflow.ellipsis), + ], + ), + ); + } + + @override + Widget body(BuildContext context) { + return Observer( + builder: (_) { + return GridView.count( + padding: const EdgeInsets.all(16), + crossAxisCount: 2, + childAspectRatio: 25/9, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + children: [ + _buildSingleCell('Height (local)', viewModel.localBlockHeight ?? ''), + _buildSingleCell('Height (node)', viewModel.nodeBlockHeight ?? ''), + _buildSingleCell('Time', viewModel.tick.toString()), + _buildSingleCell('Background Sync', viewModel.isBackgroundSyncing ? 'Enabled' : 'Disabled'), + _buildSingleCell('Public View Key', viewModel.publicViewKey ?? ''), + _buildSingleCell('Private View Key', viewModel.privateViewKey ?? ''), + _buildSingleCell('Public Spend Key', viewModel.publicSpendKey ?? ''), + _buildSingleCell('Private Spend Key', viewModel.privateSpendKey ?? ''), + _buildSingleCell('Primary Address', viewModel.primaryAddress ?? ''), + _buildSingleCell('Passphrase', viewModel.passphrase ?? ''), + _buildSingleCell('Seed', viewModel.seed ?? ''), + _buildSingleCell('Seed Legacy', viewModel.seedLegacy ?? ''), + _enableBackgroundSyncButton(), + _disableBackgroundSyncButton(), + _refreshButton(), + _manualRescanButton(), + ], + ); + }, + ); + } + + PrimaryButton _enableBackgroundSyncButton() { + return PrimaryButton( + text: "Enable background sync", + color: Colors.purple, + textColor: Colors.white, + onPressed: () { + viewModel.startBackgroundSync(); + }, + ); + } + + PrimaryButton _disableBackgroundSyncButton() { + return PrimaryButton( + text: "Disable background sync", + color: Colors.purple, + textColor: Colors.white, + onPressed: () { + viewModel.stopBackgroundSync(); + }, + ); + } + + PrimaryButton _refreshButton() { + return PrimaryButton( + text: viewModel.refreshTimer == null ? "Enable refresh" : "Disable refresh", + color: Colors.purple, + textColor: Colors.white, + onPressed: () { + if (viewModel.refreshTimer == null) { + viewModel.startRefreshTimer(); + } else { + viewModel.stopRefreshTimer(); + } + }, + ); + } + + PrimaryButton _manualRescanButton() { + return PrimaryButton( + text: "Manual rescan", + color: Colors.purple, + textColor: Colors.white, + onPressed: () { + viewModel.manualRescan(); + }, + ); + } +} diff --git a/lib/src/screens/settings/background_sync_page.dart b/lib/src/screens/settings/background_sync_page.dart new file mode 100644 index 000000000..f9589297d --- /dev/null +++ b/lib/src/screens/settings/background_sync_page.dart @@ -0,0 +1,91 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; +import 'package:cake_wallet/src/widgets/alert_with_no_action.dart.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cake_wallet/view_model/settings/sync_mode.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class BackgroundSyncPage extends BasePage { + BackgroundSyncPage(this.dashboardViewModel); + + @override + String get title => S.current.background_sync; + + final DashboardViewModel dashboardViewModel; + + @override + Widget body(BuildContext context) { + return Container( + padding: EdgeInsets.only(top: 10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (dashboardViewModel.hasBatteryOptimization) + Observer(builder: (context) { + return SettingsSwitcherCell( + title: S.current.unrestricted_background_service, + value: !dashboardViewModel.batteryOptimizationEnabled, + onValueChange: (_, bool value) { + dashboardViewModel.disableBatteryOptimization(); + }, + ); + }), + Observer(builder: (context) { + return SettingsSwitcherCell( + title: S.current.background_sync, + value: dashboardViewModel.backgroundSyncEnabled, + onValueChange: (dashboardViewModel.batteryOptimizationEnabled && dashboardViewModel.hasBatteryOptimization) ? (_, bool value) { + unawaited(showPopUp(context: context, builder: (context) => AlertWithOneAction( + alertTitle: S.current.background_sync, + alertContent: S.current.unrestricted_background_service_notice, + buttonText: S.current.ok, + buttonAction: () => Navigator.of(context).pop(), + ))); + } : (_, bool value) { + if (value) { + dashboardViewModel.enableBackgroundSync(); + } else { + dashboardViewModel.disableBackgroundSync(); + } + }, + ); + }), + Observer(builder: (context) { + return SettingsPickerCell( + title: S.current.background_sync_mode, + items: SyncMode.all, + displayItem: (SyncMode syncMode) => syncMode.name, + selectedItem: dashboardViewModel.settingsStore.currentSyncMode, + onItemSelected: (dashboardViewModel.batteryOptimizationEnabled && dashboardViewModel.hasBatteryOptimization) ? null : (syncMode) async { + dashboardViewModel.setSyncMode(syncMode); + }); + }), + + // Observer(builder: (context) { + // return SettingsSwitcherCell( + // title: S.current.background_sync_on_battery, + // value: dashboardViewModel.backgroundSyncOnBattery, + // onValueChange: (_, bool value) => + // dashboardViewModel.setBackgroundSyncOnBattery(value), + // ); + // }), + // Observer(builder: (context) { + // return SettingsSwitcherCell( + // title: S.current.background_sync_on_data, + // value: dashboardViewModel.backgroundSyncOnData, + // onValueChange: (_, bool value) => dashboardViewModel.setBackgroundSyncOnData(value), + // ); + // }), + ], + ), + ); + } +} diff --git a/lib/src/screens/settings/connection_sync_page.dart b/lib/src/screens/settings/connection_sync_page.dart index c4d85a3a5..739e01c55 100644 --- a/lib/src/screens/settings/connection_sync_page.dart +++ b/lib/src/screens/settings/connection_sync_page.dart @@ -44,56 +44,17 @@ class ConnectionSyncPage extends BasePage { : S.current.rescan, handler: (context) => Navigator.of(context).pushNamed(Routes.rescan), ), - if (DeviceInfo.instance.isMobile && FeatureFlag.isBackgroundSyncEnabled) ...[ - Observer(builder: (context) { - return SettingsPickerCell( - title: S.current.background_sync_mode, - items: SyncMode.all, - displayItem: (SyncMode syncMode) => syncMode.name, - selectedItem: dashboardViewModel.syncMode, - onItemSelected: (syncMode) async { - dashboardViewModel.setSyncMode(syncMode); - - if (Platform.isIOS) return; - - if (syncMode.type != SyncType.disabled) { - final isDisabled = await isBatteryOptimizationDisabled(); - - if (isDisabled) return; - - await showPopUp( - context: context, - builder: (BuildContext dialogContext) { - return AlertWithTwoActions( - alertTitle: S.current.disableBatteryOptimization, - alertContent: S.current.disableBatteryOptimizationDescription, - leftButtonText: S.of(context).cancel, - rightButtonText: S.of(context).ok, - actionLeftButton: () => Navigator.of(dialogContext).pop(), - actionRightButton: () async { - await requestDisableBatteryOptimization(); - - Navigator.of(dialogContext).pop(); - }, - ); - }, - ); - } - }); - }), - Observer(builder: (context) { - return SettingsSwitcherCell( - title: S.current.sync_all_wallets, - value: dashboardViewModel.syncAll, - onValueChange: (_, bool value) => dashboardViewModel.setSyncAll(value), - ); - }), - ], ], SettingsCellWithArrow( title: S.current.manage_nodes, handler: (context) => Navigator.of(context).pushNamed(Routes.manageNodes), ), + if (dashboardViewModel.hasBackgroundSync && Platform.isAndroid && FeatureFlag.isBackgroundSyncEnabled) ...[ + SettingsCellWithArrow( + title: S.current.background_sync, + handler: (context) => Navigator.of(context).pushNamed(Routes.backgroundSync), + ), + ], Observer( builder: (context) { if (!dashboardViewModel.hasPowNodes) return const SizedBox(); diff --git a/lib/src/screens/settings/other_settings_page.dart b/lib/src/screens/settings/other_settings_page.dart index 8e0a25958..ca1c1b2cb 100644 --- a/lib/src/screens/settings/other_settings_page.dart +++ b/lib/src/screens/settings/other_settings_page.dart @@ -10,6 +10,7 @@ import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell. import 'package:cake_wallet/src/screens/settings/widgets/settings_version_cell.dart'; import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -63,6 +64,12 @@ class OtherSettingsPage extends BasePage { handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.readDisclaimer), ), + if (kDebugMode && _otherSettingsViewModel.walletType == WalletType.monero) + SettingsCellWithArrow( + title: '[dev] monero background sync', + handler: (BuildContext context) => + Navigator.of(context).pushNamed(Routes.devMoneroBackgroundSync), + ), Spacer(), SettingsVersionCell( title: S.of(context).version(_otherSettingsViewModel.currentVersion)), diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index d43550806..5f6b415b9 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -8,7 +9,6 @@ import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/action_list_display_mode.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; -import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; import 'package:cake_wallet/entities/cake_2fa_preset_options.dart'; import 'package:cake_wallet/entities/country.dart'; @@ -46,6 +46,7 @@ import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_daemon/flutter_daemon.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -57,7 +58,6 @@ class SettingsStore = SettingsStoreBase with _$SettingsStore; abstract class SettingsStoreBase with Store { SettingsStoreBase( {required SecureStorage secureStorage, - required BackgroundTasks backgroundTasks, required SharedPreferences sharedPreferences, required bool initialShouldShowMarketPlaceInDashboard, required bool initialShowAddressBookPopupEnabled, @@ -146,7 +146,6 @@ abstract class SettingsStoreBase with Store { powNodes = ObservableMap.of(powNodes), _secureStorage = secureStorage, _sharedPreferences = sharedPreferences, - _backgroundTasks = backgroundTasks, fiatCurrency = initialFiatCurrency, balanceDisplayMode = initialBalanceDisplayMode, shouldSaveRecipientAddress = initialSaveRecipientAddress, @@ -303,11 +302,11 @@ abstract class SettingsStoreBase with Store { PreferencesKey.shouldSaveRecipientAddressKey, shouldSaveRecipientAddress)); if (DeviceInfo.instance.isMobile) { - setIsAppSecureNative(isAppSecure); + unawaited(setIsAppSecureNative(isAppSecure)); reaction((_) => isAppSecure, (bool isAppSecure) { sharedPreferences.setBool(PreferencesKey.isAppSecureKey, isAppSecure); - setIsAppSecureNative(isAppSecure); + unawaited(setIsAppSecureNative(isAppSecure)); }); } @@ -402,14 +401,11 @@ abstract class SettingsStoreBase with Store { reaction((_) => currentSyncMode, (SyncMode syncMode) { sharedPreferences.setInt(PreferencesKey.syncModeKey, syncMode.type.index); - - _backgroundTasks.registerSyncTask(changeExisting: true); + FlutterDaemon().startBackgroundSync(syncMode.frequency.inMinutes); }); reaction((_) => currentSyncAll, (bool syncAll) { sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll); - - _backgroundTasks.registerSyncTask(changeExisting: true); }); reaction( @@ -807,6 +803,7 @@ abstract class SettingsStoreBase with Store { @observable bool lookupsWellKnown; + @observable SyncMode currentSyncMode; @@ -843,7 +840,6 @@ abstract class SettingsStoreBase with Store { final SecureStorage _secureStorage; final SharedPreferences _sharedPreferences; - final BackgroundTasks _backgroundTasks; ObservableMap nodes; ObservableMap powNodes; @@ -885,7 +881,6 @@ abstract class SettingsStoreBase with Store { ThemeBase? initialTheme}) async { final sharedPreferences = await getIt.getAsync(); final secureStorage = await getIt.get(); - final backgroundTasks = getIt.get(); final currentFiatCurrency = FiatCurrency.deserialize( raw: sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey)!); final savedCakePayCountryRaw = sharedPreferences.getString(PreferencesKey.currentCakePayCountry); @@ -1157,7 +1152,7 @@ abstract class SettingsStoreBase with Store { } final savedSyncMode = SyncMode.all.firstWhere((element) { - return element.type.index == (sharedPreferences.getInt(PreferencesKey.syncModeKey) ?? 0); + return element.type.index == (sharedPreferences.getInt(PreferencesKey.syncModeKey) ?? 2); // default to 2 - daily sync }); final savedSyncAll = sharedPreferences.getBool(PreferencesKey.syncAllKey) ?? true; @@ -1339,7 +1334,6 @@ abstract class SettingsStoreBase with Store { shouldRequireTOTP2FAForAllSecurityAndBackupSettings, initialEthereumTransactionPriority: ethereumTransactionPriority, initialPolygonTransactionPriority: polygonTransactionPriority, - backgroundTasks: backgroundTasks, initialSyncMode: savedSyncMode, initialSyncAll: savedSyncAll, shouldShowYatPopup: shouldShowYatPopup, diff --git a/lib/utils/feature_flag.dart b/lib/utils/feature_flag.dart index 593e0f216..6e829d474 100644 --- a/lib/utils/feature_flag.dart +++ b/lib/utils/feature_flag.dart @@ -4,6 +4,6 @@ class FeatureFlag { static const bool isCakePayEnabled = false; static const bool isExolixEnabled = true; static const bool isInAppTorEnabled = false; - static const bool isBackgroundSyncEnabled = false; + static const bool isBackgroundSyncEnabled = true; static const int verificationWordsCount = kDebugMode ? 0 : 2; } \ No newline at end of file diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index a4408a935..29ccedef2 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -4,6 +4,7 @@ import 'dart:io' show Platform; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/core/background_sync.dart'; import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; @@ -40,12 +41,14 @@ import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/utils/file.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:eth_sig_util/util/utils.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_daemon/flutter_daemon.dart'; import 'package:http/http.dart' as http; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -175,6 +178,8 @@ abstract class DashboardViewModelBase with Store { isShowFirstYatIntroduction = false; isShowSecondYatIntroduction = false; isShowThirdYatIntroduction = false; + unawaited(isBackgroundSyncEnabled()); + unawaited(isBatteryOptimizationEnabled()); final _wallet = wallet; @@ -406,6 +411,11 @@ abstract class DashboardViewModelBase with Store { @computed bool get hasRescan => wallet.hasRescan; + @computed + bool get hasBackgroundSync => [ + WalletType.monero, + ].contains(wallet.type); + @computed bool get isMoneroViewOnly { if (wallet.type != WalletType.monero) return false; @@ -492,6 +502,69 @@ abstract class DashboardViewModelBase with Store { @observable late bool showDecredInfoCard; + @observable + bool backgroundSyncEnabled = false; + + @action + Future isBackgroundSyncEnabled() async { + if (!Platform.isAndroid) { + return false; + } + final resp = await FlutterDaemon().getBackgroundSyncStatus(); + backgroundSyncEnabled = resp; + return resp; + } + + bool get hasBatteryOptimization => Platform.isAndroid; + + @observable + bool batteryOptimizationEnabled = false; + + @action + Future isBatteryOptimizationEnabled() async { + if (!hasBatteryOptimization) { + return false; + } + final resp = await FlutterDaemon().isBatteryOptimizationDisabled(); + batteryOptimizationEnabled = !resp; + if (batteryOptimizationEnabled && await isBackgroundSyncEnabled()) { + // If the battery optimization is enabled, we need to disable the background sync + await disableBackgroundSync(); + } + return resp; + } + + @action + Future disableBatteryOptimization() async { + final resp = await FlutterDaemon().requestDisableBatteryOptimization(); + unawaited((() async { + // android doesn't return if the permission was granted, so we need to poll it, + // minute should be enough for the fallback method (opening settings and changing the permission) + for (var i = 0; i < 4 * 60; i++) { + await Future.delayed(Duration(milliseconds: 250)); + await isBatteryOptimizationEnabled(); + } + })()); + } + + @action + Future enableBackgroundSync() async { + if (hasBatteryOptimization && batteryOptimizationEnabled) { + disableBackgroundSync(); + return; + } + final resp = await FlutterDaemon().startBackgroundSync(settingsStore.currentSyncMode.frequency.inMinutes); + printV("Background sync enabled: $resp"); + backgroundSyncEnabled = true; + } + + @action + Future disableBackgroundSync() async { + final resp = await FlutterDaemon().stopBackgroundSync(); + printV("Background sync disabled: $resp"); + backgroundSyncEnabled = false; + } + @computed bool get hasEnabledMwebBefore => settingsStore.hasEnabledMwebBefore; @@ -797,11 +870,11 @@ abstract class DashboardViewModelBase with Store { } } - @computed - SyncMode get syncMode => settingsStore.currentSyncMode; - @action - void setSyncMode(SyncMode syncMode) => settingsStore.currentSyncMode = syncMode; + Future setSyncMode(SyncMode syncMode) async { + settingsStore.currentSyncMode = syncMode; + await enableBackgroundSync(); + } @computed bool get syncAll => settingsStore.currentSyncAll; diff --git a/lib/view_model/dev/monero_background_sync.dart b/lib/view_model/dev/monero_background_sync.dart new file mode 100644 index 000000000..3eb4292b1 --- /dev/null +++ b/lib/view_model/dev/monero_background_sync.dart @@ -0,0 +1,106 @@ +import 'dart:async'; + +import 'package:cake_wallet/core/key_service.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/monero/monero.dart'; +import 'package:cw_monero/monero_wallet.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cw_core/wallet_base.dart'; + +part 'monero_background_sync.g.dart'; + +class DevMoneroBackgroundSync = DevMoneroBackgroundSyncBase with _$DevMoneroBackgroundSync; + +abstract class DevMoneroBackgroundSyncBase with Store { + DevMoneroBackgroundSyncBase(WalletBase wallet) : wallet = wallet; + + final WalletBase wallet; + + @observable + Timer? refreshTimer; + + @observable + String? localBlockHeight; + + @observable + String? nodeBlockHeight; + + @observable + String? primaryAddress; + + @observable + String? publicViewKey; + + @observable + String? privateViewKey; + + @observable + String? publicSpendKey; + + @observable + String? privateSpendKey; + + @observable + String? passphrase; + + @observable + String? seed; + + @observable + String? seedLegacy; + + @observable + int tick = -1; + + @observable + bool isBackgroundSyncing = false; + + Future _setValues() async { + final w = (wallet as MoneroWallet); + localBlockHeight = (await monero!.getCurrentHeight()).toString(); + nodeBlockHeight = (await w.getNodeHeight()).toString(); + final keys = w.keys; + primaryAddress = keys.primaryAddress; + publicViewKey = keys.publicViewKey; + privateViewKey = keys.privateViewKey; + publicSpendKey = keys.publicSpendKey; + privateSpendKey = keys.privateSpendKey; + passphrase = keys.passphrase; + seed = w.seed; + seedLegacy = w.seedLegacy("English"); + tick = refreshTimer?.tick ?? -1; + isBackgroundSyncing = w.isBackgroundSyncRunning; + } + + @action + Future manualRescan() async { + final w = (wallet as MoneroWallet); + await wallet.rescan(height: await w.getNodeHeight() - 10000); + } + + @action + void startRefreshTimer() { + refreshTimer = Timer.periodic(Duration(seconds: 1), (timer) async { + await _setValues(); + }); + } + + @action + void stopRefreshTimer() { + refreshTimer?.cancel(); + refreshTimer = null; + } + + @action + void startBackgroundSync() { + final w = (wallet as MoneroWallet); + w.startBackgroundSync(); + } + + @action + Future stopBackgroundSync() async { + final w = (wallet as MoneroWallet); + final keyService = getIt.get(); + await w.stopBackgroundSync(await keyService.getWalletPassword(walletName: wallet.name)); + } +} diff --git a/lib/view_model/settings/sync_mode.dart b/lib/view_model/settings/sync_mode.dart index ac3ac8717..86046a839 100644 --- a/lib/view_model/settings/sync_mode.dart +++ b/lib/view_model/settings/sync_mode.dart @@ -1,4 +1,4 @@ -enum SyncType { disabled, unobtrusive, aggressive } +enum SyncType { aggresive, hourly, daily } class SyncMode { SyncMode(this.name, this.type, this.frequency); @@ -8,8 +8,10 @@ class SyncMode { final Duration frequency; static final all = [ - SyncMode("Disabled", SyncType.disabled, Duration.zero), - SyncMode("Unobtrusive", SyncType.unobtrusive, Duration(hours: 12)), - SyncMode("Aggressive", SyncType.aggressive, Duration(hours: 3)), + // **Technically** we could call aggressive option "15 minutes" but OS may "not feel like it", + // so instead we will call it aggressive so user knows that it will be as frequent as possible. + SyncMode("Aggressive", SyncType.aggresive, Duration(minutes: 15)), + SyncMode("Hourly", SyncType.hourly, Duration(hours: 1)), + SyncMode("Daily", SyncType.daily, Duration(hours: 18)), // yes this is straight up lie. ]; } diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index f423622f6..07a9aefd0 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -2,7 +2,6 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cake_wallet/di.dart'; -import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/entities/generate_name.dart'; import 'package:cake_wallet/entities/hash_wallet_identifier.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -113,7 +112,6 @@ abstract class WalletCreationVMBase with Store { walletInfo.address = wallet.walletAddresses.address; await _walletInfoSource.add(walletInfo); await _appStore.changeCurrentWallet(wallet); - getIt.get().registerSyncTask(); _appStore.authenticationStore.allowedCreate(); state = ExecutedSuccessfullyState(); } catch (e, s) { diff --git a/lib/view_model/wallet_groups_display_view_model.dart b/lib/view_model/wallet_groups_display_view_model.dart index 09d6d656c..056d713aa 100644 --- a/lib/view_model/wallet_groups_display_view_model.dart +++ b/lib/view_model/wallet_groups_display_view_model.dart @@ -158,6 +158,7 @@ abstract class WalletGroupsDisplayViewModelBase with Store { isCurrent: info.name == _appStore.wallet?.name && info.type == _appStore.wallet?.type, isEnabled: availableWalletTypes.contains(info.type), isTestnet: info.network?.toLowerCase().contains('testnet') ?? false, + isHardware: info.isHardwareWallet, ); } } diff --git a/lib/view_model/wallet_list/wallet_list_item.dart b/lib/view_model/wallet_list/wallet_list_item.dart index 24b1a3bd0..8f8c58ea9 100644 --- a/lib/view_model/wallet_list/wallet_list_item.dart +++ b/lib/view_model/wallet_list/wallet_list_item.dart @@ -5,6 +5,7 @@ class WalletListItem { required this.name, required this.type, required this.key, + required this.isHardware, this.isCurrent = false, this.isEnabled = true, this.isTestnet = false, @@ -16,4 +17,5 @@ class WalletListItem { final dynamic key; final bool isEnabled; final bool isTestnet; + final bool isHardware; } diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index 407dce55a..8c36f8412 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -265,6 +265,7 @@ abstract class WalletListViewModelBase with Store { info.type == _appStore.wallet?.type, isEnabled: availableWalletTypes.contains(info.type), isTestnet: info.network?.toLowerCase().contains('testnet') ?? false, + isHardware: info.isHardwareWallet, ); } } diff --git a/pubspec_base.yaml b/pubspec_base.yaml index ca49f2842..8bf2de274 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -68,7 +68,6 @@ dependencies: git: url: https://github.com/MrCyjaneK/device_display_brightness.git ref: 4cac18c446ce686f3d75b1565badbd7da439bbd9 - workmanager: ^0.5.2 wakelock_plus: ^1.2.5 flutter_mailer: git: @@ -119,6 +118,10 @@ dependencies: git: url: https://github.com/cake-tech/blockchain_utils ref: cake-update-v2 + flutter_daemon: + git: + url: https://github.com/MrCyjaneK/flutter_daemon + ref: 5c369e0e69e6f459357b9802bc694a221397298a dev_dependencies: flutter_test: diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 60a0c2b78..64089065d 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -69,6 +69,7 @@ "avg_savings": "متوسط مدخرات", "awaitDAppProcessing": ".ﺔﺠﻟﺎﻌﻤﻟﺍ ﻦﻣ dApp ﻲﻬﺘﻨﻳ ﻰﺘﺣ ﺭﺎﻈﺘﻧﻻﺍ ﻰﺟﺮﻳ", "awaiting_payment_confirmation": "في انتظار تأكيد الدفع", + "background_sync": "مزامنة الخلفية", "background_sync_mode": "وضع مزامنة الخلفية", "backup": "نسخ الاحتياطي", "backup_file": "ملف النسخ الاحتياطي", @@ -922,6 +923,8 @@ "understand": "لقد فهمت", "unlock": "الغاء القفل", "unmatched_currencies": "عملة محفظتك الحالية لا تتطابق مع عملة QR الممسوحة ضوئيًا", + "unrestricted_background_service": "خدمة خلفية غير مقيدة", + "unrestricted_background_service_notice": "من أجل تمكين مزامنة الخلفية ، تحتاج إلى تمكين خدمة الخلفية غير المقيدة", "unspent_change": "يتغير", "unspent_coins_details_title": "تفاصيل العملات الغير المنفقة", "unspent_coins_title": "العملات الغير المنفقة", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 146fa8efd..24b6e2640 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -69,6 +69,7 @@ "avg_savings": "Средни спестявания", "awaitDAppProcessing": "Моля, изчакайте dApp да завърши обработката.", "awaiting_payment_confirmation": "Чака се потвърждение на плащането", + "background_sync": "Фон Синхх", "background_sync_mode": "Режим на синхронизиране на фона", "backup": "Резервно копие", "backup_file": "Резервно копие", @@ -922,6 +923,8 @@ "understand": "Разбирам", "unlock": "Отключване", "unmatched_currencies": "Валутата на този портфейл не съвпада с тази от сканирания QR код", + "unrestricted_background_service": "Неограничена фонова услуга", + "unrestricted_background_service_notice": "За да активирате синхронизирането на фона, трябва да активирате неограничена фонова услуга", "unspent_change": "Промяна", "unspent_coins_details_title": "Подробности за неизползваните монети", "unspent_coins_title": "Неизползвани монети", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 2c770069b..9dbc99d5c 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -69,6 +69,7 @@ "avg_savings": "Prům. ušetřeno", "awaitDAppProcessing": "Počkejte, až dApp dokončí zpracování.", "awaiting_payment_confirmation": "Čeká se na potvrzení platby", + "background_sync": "Synchronizace pozadí", "background_sync_mode": "Režim synchronizace pozadí", "backup": "Záloha", "backup_file": "Soubor se zálohou", @@ -922,6 +923,8 @@ "understand": "Rozumím", "unlock": "Odemknout", "unmatched_currencies": "Měna vaší současné peněženky neodpovídá té v naskenovaném QR kódu", + "unrestricted_background_service": "Neomezená služba na pozadí", + "unrestricted_background_service_notice": "Chcete -li povolit synchronizaci pozadí, musíte povolit neomezenou službu na pozadí", "unspent_change": "Změna", "unspent_coins_details_title": "Podrobnosti o neutracených mincích", "unspent_coins_title": "Neutracené mince", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index d7071061b..54303caf3 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -69,6 +69,7 @@ "avg_savings": "Durchschn. Einsparungen", "awaitDAppProcessing": "Bitte warten Sie, bis die dApp die Verarbeitung abgeschlossen hat.", "awaiting_payment_confirmation": "Warten auf Zahlungsbestätigung", + "background_sync": "Hintergrundsynchronisation", "background_sync_mode": "Hintergrundsynchronisierungsmodus", "backup": "Sicherung", "backup_file": "Sicherungsdatei", @@ -924,6 +925,8 @@ "understand": "Ich verstehe", "unlock": "Freischalten", "unmatched_currencies": "Die Währung Ihres aktuellen Wallets stimmt nicht mit der des gescannten QR überein", + "unrestricted_background_service": "Uneingeschränkter Hintergrunddienst", + "unrestricted_background_service_notice": "Um die Hintergrundsynchronisierung zu ermöglichen, müssen Sie einen uneingeschränkten Hintergrundservice aktivieren", "unspent_change": "Wechselgeld", "unspent_coins_details_title": "Details zu nicht ausgegebenen Coins", "unspent_coins_title": "Nicht ausgegebene Coins", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 68e730b27..85103162d 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -69,6 +69,7 @@ "avg_savings": "Avg. Savings", "awaitDAppProcessing": "Kindly wait for the dApp to finish processing.", "awaiting_payment_confirmation": "Awaiting Payment Confirmation", + "background_sync": "Background sync", "background_sync_mode": "Background sync mode", "backup": "Backup", "backup_file": "Backup file", @@ -923,6 +924,8 @@ "understand": "I understand", "unlock": "Unlock", "unmatched_currencies": "Your current wallet's currency does not match that of the scanned QR", + "unrestricted_background_service": "Unrestricted background service", + "unrestricted_background_service_notice": "In order to enable background sync you need to enable unrestricted background service", "unspent_change": "Change", "unspent_coins_details_title": "Unspent coins details", "unspent_coins_title": "Unspent coins", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 3d45ab3a0..cd63e5a3d 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -69,6 +69,7 @@ "avg_savings": "Ahorro promedio", "awaitDAppProcessing": "Espere a que la dApp termine de procesarse.", "awaiting_payment_confirmation": "Esperando confirmación de pago", + "background_sync": "Sincronización de fondo", "background_sync_mode": "Modo de sincronización en segundo plano", "backup": "Apoyo", "backup_file": "Archivo de respaldo", @@ -923,6 +924,8 @@ "understand": "Entiendo", "unlock": "desbloquear", "unmatched_currencies": "La moneda de tu billetera actual no coincide con la del QR escaneado", + "unrestricted_background_service": "Servicio de antecedentes sin restricciones", + "unrestricted_background_service_notice": "Para habilitar la sincronización de antecedentes, debe habilitar el servicio de fondo sin restricciones", "unspent_change": "Cambiar", "unspent_coins_details_title": "Detalles de monedas no gastadas", "unspent_coins_title": "Monedas no gastadas", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index a6433da9d..309319277 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -69,6 +69,7 @@ "avg_savings": "Économies moy.", "awaitDAppProcessing": "Veuillez attendre que l'application décentralisée (dApp) termine le traitement.", "awaiting_payment_confirmation": "En attente de confirmation de paiement", + "background_sync": "Synchronisation de fond", "background_sync_mode": "Mode de synchronisation en arrière-plan", "backup": "Sauvegarde", "backup_file": "Fichier de sauvegarde", @@ -922,6 +923,8 @@ "understand": "J'ai compris", "unlock": "Ouvrir", "unmatched_currencies": "La devise de votre portefeuille (wallet) actuel ne correspond pas à celle du QR code scanné", + "unrestricted_background_service": "Service de fond sans restriction", + "unrestricted_background_service_notice": "Afin d'activer la synchronisation des antécédents, vous devez activer le service de fond sans restriction", "unspent_change": "Monnaie", "unspent_coins_details_title": "Détails des pièces (coins) non dépensées", "unspent_coins_title": "Pièces (coins) non dépensées", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index deec1e470..4ae70f8dd 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -69,6 +69,7 @@ "avg_savings": "Matsakaici Adana", "awaitDAppProcessing": "Da fatan za a jira dApp ya gama aiki.", "awaiting_payment_confirmation": "Ana jiran Tabbacin Biyan Kuɗi", + "background_sync": "Tunawa da Setc", "background_sync_mode": "Yanayin Sync", "backup": "Ajiyayyen", "backup_file": "Ajiyayyen fayil", @@ -924,6 +925,8 @@ "understand": "na gane", "unlock": "Buɗe", "unmatched_currencies": "Nau'in walat ɗin ku na yanzu bai dace da na lambar QR da aka bincika ba", + "unrestricted_background_service": "Sabis na baya", + "unrestricted_background_service_notice": "Don ba da damar Sync na asali kuna buƙatar kunna sabis na baya da ba a santa ba", "unspent_change": "Canza", "unspent_coins_details_title": "Bayanan tsabar kudi da ba a kashe ba", "unspent_coins_title": "Tsabar da ba a kashe ba", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 91bd94dac..de15f45b6 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -69,6 +69,7 @@ "avg_savings": "औसत बचत", "awaitDAppProcessing": "कृपया डीएपी की प्रोसेसिंग पूरी होने तक प्रतीक्षा करें।", "awaiting_payment_confirmation": "भुगतान की पुष्टि की प्रतीक्षा में", + "background_sync": "पृष्ठभूमि सिंक", "background_sync_mode": "बैकग्राउंड सिंक मोड", "backup": "बैकअप", "backup_file": "बैकअपफ़ाइल", @@ -924,6 +925,8 @@ "understand": "मुझे समझ", "unlock": "अनलॉक", "unmatched_currencies": "आपके वर्तमान वॉलेट की मुद्रा स्कैन किए गए क्यूआर से मेल नहीं खाती", + "unrestricted_background_service": "अप्रतिबंधित पृष्ठभूमि सेवा", + "unrestricted_background_service_notice": "पृष्ठभूमि सिंक को सक्षम करने के लिए आपको अप्रतिबंधित पृष्ठभूमि सेवा को सक्षम करने की आवश्यकता है", "unspent_change": "परिवर्तन", "unspent_coins_details_title": "अव्ययित सिक्कों का विवरण", "unspent_coins_title": "खर्च न किए गए सिक्के", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index c5e4cd54b..48da3833a 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -69,6 +69,7 @@ "avg_savings": "Prosj. ušteda", "awaitDAppProcessing": "Molimo pričekajte da dApp završi obradu.", "awaiting_payment_confirmation": "Čeka se potvrda plaćanja", + "background_sync": "Sinkronizacija pozadine", "background_sync_mode": "Sinkronizacija u pozadini", "backup": "Sigurnosna kopija", "backup_file": "Sigurnosna kopija datoteke", @@ -922,6 +923,8 @@ "understand": "Razumijem", "unlock": "Otključati", "unmatched_currencies": "Valuta vašeg trenutnog novčanika ne odgovara onoj na skeniranom QR-u", + "unrestricted_background_service": "Neograničena pozadinska usluga", + "unrestricted_background_service_notice": "Da biste omogućili sinkronizaciju pozadine, morate omogućiti neograničenu pozadinsku uslugu", "unspent_change": "Promijeniti", "unspent_coins_details_title": "Nepotrošeni detalji o novčićima", "unspent_coins_title": "Nepotrošeni novčići", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index d2e307164..ee1c997b0 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -69,6 +69,7 @@ "avg_savings": "Միջին խնայողություն", "awaitDAppProcessing": "Խնդրեմ սպասեք, մինչև դիմումը կավարտի մշակումը։", "awaiting_payment_confirmation": "Վճարման հաստատման սպասում", + "background_sync": "Ֆոնային համաժամեցում", "background_sync_mode": "Հետին պլանի համաժամացման ռեժիմ", "backup": "Կրկնօրինակ", "backup_file": "Կրկնօրինակի ֆայլ", @@ -920,6 +921,8 @@ "understand": "Ես հասկանում եմ", "unlock": "Բացել", "unmatched_currencies": "Ձեր ընթացիկ դրամապանակի արժույթը չի համապատասխանում սկանավորված QR կոդի արժույթին", + "unrestricted_background_service": "Անսահմանափակ ֆոնային ծառայություն", + "unrestricted_background_service_notice": "Ֆոնային համաժամացման համար անհրաժեշտ է միացնել անսահմանափակ ֆոնային ծառայություն", "unspent_change": "Մնացորդ", "unspent_coins_details_title": "Չծախսված արժույթների մանրամասները", "unspent_coins_title": "Չծախսված արժույթներ", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 19ffba89e..f9a231112 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -69,6 +69,7 @@ "avg_savings": "Rata-rata Pembayaran", "awaitDAppProcessing": "Mohon tunggu hingga dApp menyelesaikan pemrosesan.", "awaiting_payment_confirmation": "Menunggu Konfirmasi Pembayaran", + "background_sync": "Sinkronisasi Latar Belakang", "background_sync_mode": "Mode Sinkronisasi Latar Belakang", "backup": "Cadangan", "backup_file": "File cadangan", @@ -925,6 +926,8 @@ "understand": "Saya mengerti", "unlock": "Membuka kunci", "unmatched_currencies": "Mata uang dompet Anda saat ini tidak cocok dengan yang ditandai QR", + "unrestricted_background_service": "Layanan latar belakang tidak terbatas", + "unrestricted_background_service_notice": "Untuk mengaktifkan sinkronisasi latar belakang, Anda perlu mengaktifkan layanan latar belakang yang tidak dibatasi", "unspent_change": "Mengubah", "unspent_coins_details_title": "Rincian koin yang tidak terpakai", "unspent_coins_title": "Koin yang tidak terpakai", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 721da624a..25d02d9d0 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -69,6 +69,7 @@ "avg_savings": "Risparmio medio", "awaitDAppProcessing": "Attendi gentilmente che la dApp termini l'elaborazione.", "awaiting_payment_confirmation": "In attesa di conferma del pagamento", + "background_sync": "Sincronizzazione in background", "background_sync_mode": "Modalità di sincronizzazione in background", "backup": "Backup", "backup_file": "Backup file", @@ -923,6 +924,8 @@ "understand": "Capisco", "unlock": "Sblocca", "unmatched_currencies": "La valuta del tuo portafoglio attuale non corrisponde a quella del QR scansionato", + "unrestricted_background_service": "Servizio di background senza restrizioni", + "unrestricted_background_service_notice": "Per abilitare la sincronizzazione in background è necessario abilitare il servizio di background senza restrizioni", "unspent_change": "Resto", "unspent_coins_details_title": "Dettagli sulle monete non spese", "unspent_coins_title": "Monete non spese", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 92130db7b..6a3a5da72 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -69,6 +69,7 @@ "avg_savings": "平均節約額", "awaitDAppProcessing": "dAppの処理が完了するまでお待ちください。", "awaiting_payment_confirmation": "支払い確認を待っています", + "background_sync": "背景同期", "background_sync_mode": "バックグラウンド同期モード", "backup": "バックアップ", "backup_file": "バックアップファイル", @@ -923,6 +924,8 @@ "understand": "わかります", "unlock": "ロックを解除します", "unmatched_currencies": "現在のウォレットの通貨がスキャンされたQRの通貨と一致しません", + "unrestricted_background_service": "無制限のバックグラウンドサービス", + "unrestricted_background_service_notice": "バックグラウンドの同期を有​​効にするには、無制限のバックグラウンドサービスを有効にする必要があります", "unspent_change": "変化", "unspent_coins_details_title": "未使用のコインの詳細", "unspent_coins_title": "未使用のコイン", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 3892448a6..d16e18a53 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -69,6 +69,7 @@ "avg_savings": "평균 절감액", "awaitDAppProcessing": "dApp이 처리를 마칠 때까지 기다려주세요.", "awaiting_payment_confirmation": "결제 확인 대기 중", + "background_sync": "배경 동기화", "background_sync_mode": "백그라운드 동기화 모드", "backup": "지원", "backup_file": "백업 파일", @@ -922,6 +923,8 @@ "understand": "이해 했어요", "unlock": "터놓다", "unmatched_currencies": "현재 지갑의 통화가 스캔한 QR의 통화와 일치하지 않습니다.", + "unrestricted_background_service": "무제한 배경 서비스", + "unrestricted_background_service_notice": "배경 동기화를 활성화하려면 무제한 배경 서비스를 활성화해야합니다.", "unspent_change": "변화", "unspent_coins_details_title": "사용하지 않은 동전 세부 정보", "unspent_coins_title": "사용하지 않은 동전", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 2f7131aa9..d7ba8d7a6 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -69,6 +69,7 @@ "avg_savings": "ပျမ်းမျှ စုဆောင်းငွေ", "awaitDAppProcessing": "ကျေးဇူးပြု၍ dApp ကို စီမံလုပ်ဆောင်ခြင်း အပြီးသတ်ရန် စောင့်ပါ။", "awaiting_payment_confirmation": "ငွေပေးချေမှု အတည်ပြုချက်ကို စောင့်မျှော်နေပါသည်။", + "background_sync": "နောက်ခံထပ်တူပြုခြင်း", "background_sync_mode": "နောက်ခံထပ်တူပြုခြင်း mode ကို", "backup": "မိတ္တူ", "backup_file": "အရန်ဖိုင်", @@ -922,6 +923,8 @@ "understand": "ကျွန်တော်နားလည်ပါတယ်", "unlock": "သော့ဖွင့်", "unmatched_currencies": "သင့်လက်ရှိပိုက်ဆံအိတ်၏ငွေကြေးသည် စကင်ဖတ်ထားသော QR နှင့် မကိုက်ညီပါ။", + "unrestricted_background_service": "အကန့်အသတ်မရှိနောက်ခံဝန်ဆောင်မှု", + "unrestricted_background_service_notice": "နောက်ခံထပ်တူပြုခြင်းကို Enable လုပ်ရန်သင်ကန့်သတ်ထားသောနောက်ခံဝန်ဆောင်မှုကိုဖွင့်ရန်လိုအပ်သည်", "unspent_change": "ပေြာင်းလဲခြင်း", "unspent_coins_details_title": "အသုံးမဝင်သော အကြွေစေ့အသေးစိတ်များ", "unspent_coins_title": "အသုံးမဝင်သော အကြွေစေ့များ", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 9d998fcc5..55e588f62 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -69,6 +69,7 @@ "avg_savings": "Gem. besparingen", "awaitDAppProcessing": "Wacht tot de dApp klaar is met verwerken.", "awaiting_payment_confirmation": "In afwachting van betalingsbevestiging", + "background_sync": "Achtergrondsynchronisatie", "background_sync_mode": "Achtergrondsynchronisatiemodus", "backup": "Back-up", "backup_file": "Backup bestand", @@ -922,6 +923,8 @@ "understand": "Ik begrijp het", "unlock": "Ontgrendelen", "unmatched_currencies": "De valuta van uw huidige portemonnee komt niet overeen met die van de gescande QR", + "unrestricted_background_service": "Onbeperkte achtergrondservice", + "unrestricted_background_service_notice": "Om achtergrondsynchronisatie in te schakelen, moet u onbeperkte achtergrondservice inschakelen", "unspent_change": "Wijziging", "unspent_coins_details_title": "Details van niet-uitgegeven munten", "unspent_coins_title": "Ongebruikte munten", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 52ee64a5b..cb4f09982 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -69,6 +69,7 @@ "avg_savings": "Śr. oszczędności", "awaitDAppProcessing": "Poczekaj, aż dApp zakończy przetwarzanie.", "awaiting_payment_confirmation": "Oczekiwanie na potwierdzenie płatności", + "background_sync": "Synchronizacja tła", "background_sync_mode": "Tryb synchronizacji w tle", "backup": "Kopia zapasowa", "backup_file": "Plik kopii zapasowej", @@ -922,6 +923,8 @@ "understand": "Rozumiem", "unlock": "Odblokować", "unmatched_currencies": "Waluta Twojego obecnego portfela nie zgadza się z waluctą zeskanowanego kodu QR", + "unrestricted_background_service": "Nieograniczona usługa w tle", + "unrestricted_background_service_notice": "Aby włączyć synchronizację tła, musisz włączyć nieograniczoną usługę w tle", "unspent_change": "Zmiana", "unspent_coins_details_title": "Szczegóły niewydanych monet", "unspent_coins_title": "Niewydane monety", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 453dae4db..2fb3bac5f 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -69,6 +69,7 @@ "avg_savings": "Poupança média", "awaitDAppProcessing": "Aguarde até que o dApp termine o processamento.", "awaiting_payment_confirmation": "Aguardando confirmação de pagamento", + "background_sync": "Sincronização de fundo", "background_sync_mode": "Modo de sincronização em segundo plano", "backup": "Cópia de segurança", "backup_file": "Arquivo de backup", @@ -924,6 +925,8 @@ "understand": "Entendo", "unlock": "Desbloquear", "unmatched_currencies": "A moeda da sua carteira atual não corresponde à do QR digitalizado", + "unrestricted_background_service": "Serviço de fundo irrestrito", + "unrestricted_background_service_notice": "Para ativar a sincronização de fundo, você precisa ativar o serviço de fundo irrestrito", "unspent_change": "Troco", "unspent_coins_details_title": "Detalhes de moedas não gastas", "unspent_coins_title": "Moedas não gastas", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 4decec064..4173ad6d5 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -69,6 +69,7 @@ "avg_savings": "Средняя экономия", "awaitDAppProcessing": "Пожалуйста, подождите, пока dApp завершит обработку.", "awaiting_payment_confirmation": "Ожидается подтверждения платежа", + "background_sync": "Фоновая синхронизация", "background_sync_mode": "Режим фоновой синхронизации", "backup": "Резервная копия", "backup_file": "Файл резервной копии", @@ -923,6 +924,8 @@ "understand": "Понятно", "unlock": "Разблокировать", "unmatched_currencies": "Валюта вашего текущего кошелька не соответствует валюте отсканированного QR-кода.", + "unrestricted_background_service": "Неограниченная фоновая служба", + "unrestricted_background_service_notice": "Чтобы включить фона синхронизации, необходимо включить неограниченную фоновую службу", "unspent_change": "Изменять", "unspent_coins_details_title": "Сведения о неизрасходованных монетах", "unspent_coins_title": "Неизрасходованные монеты", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index f718a9c76..acd70c554 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -69,6 +69,7 @@ "avg_savings": "ประหยัดเฉลี่ย", "awaitDAppProcessing": "โปรดรอให้ dApp ประมวลผลเสร็จสิ้น", "awaiting_payment_confirmation": "รอการยืนยันการชำระเงิน", + "background_sync": "การซิงค์พื้นหลัง", "background_sync_mode": "โหมดซิงค์พื้นหลัง", "backup": "สำรองข้อมูล", "backup_file": "ไฟล์สำรองข้อมูล", @@ -922,6 +923,8 @@ "understand": "ฉันเข้าใจ", "unlock": "ปลดล็อค", "unmatched_currencies": "สกุลเงินของกระเป๋าปัจจุบันของคุณไม่ตรงกับของ QR ที่สแกน", + "unrestricted_background_service": "บริการพื้นหลังที่ไม่ จำกัด", + "unrestricted_background_service_notice": "ในการเปิดใช้งานการซิงค์พื้นหลังคุณต้องเปิดใช้งานบริการพื้นหลังที่ไม่ จำกัด", "unspent_change": "เปลี่ยน", "unspent_coins_details_title": "รายละเอียดเหรียญที่ไม่ได้ใช้", "unspent_coins_title": "เหรียญที่ไม่ได้ใช้", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 90ae8f9a8..a378ff3ea 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -69,6 +69,7 @@ "avg_savings": "Avg. Matitipid", "awaitDAppProcessing": "Pakihintay na matapos ang pagproseso ng dApp.", "awaiting_payment_confirmation": "Nanghihintay ng Kumpirmasyon sa Pagbabayad", + "background_sync": "Pag -sync ng background", "background_sync_mode": "Background sync mode", "backup": "Backup", "backup_file": "Backup na file", @@ -922,6 +923,8 @@ "understand": "Naiitindihan ko", "unlock": "I-unlock", "unmatched_currencies": "Hindi tumutugma ang pera ng iyong kasalukuyang wallet sa na-scan na QR", + "unrestricted_background_service": "Hindi pinigilan na serbisyo sa background", + "unrestricted_background_service_notice": "Upang paganahin ang pag -sync ng background kailangan mong paganahin ang hindi pinigilan na serbisyo sa background", "unspent_change": "Sukli", "unspent_coins_details_title": "Mga detalye ng mga hindi nagastos na barya", "unspent_coins_title": "Mga hindi nagamit na barya", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index bbcd5e575..8011401dc 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -69,6 +69,7 @@ "avg_savings": "Ortalama Tasarruf", "awaitDAppProcessing": "Lütfen dApp'in işlemeyi bitirmesini bekleyin.", "awaiting_payment_confirmation": "Ödemenin onaylanması bekleniyor", + "background_sync": "Arka plan senkronizasyonu", "background_sync_mode": "Arka Plan Senkronizasyon Modu", "backup": "Yedek", "backup_file": "Yedek dosyası", @@ -922,6 +923,8 @@ "understand": "Anladım", "unlock": "Kilidini aç", "unmatched_currencies": "Mevcut cüzdanınızın para birimi taranan QR ile eşleşmiyor", + "unrestricted_background_service": "Sınırsız arka plan hizmeti", + "unrestricted_background_service_notice": "Arka plan senkronizasyonunu etkinleştirmek için sınırsız arka plan hizmetini etkinleştirmeniz gerekir", "unspent_change": "Değiştirmek", "unspent_coins_details_title": "Harcanmamış koin detayları", "unspent_coins_title": "Harcanmamış koinler", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 77ae06a22..8227d581f 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -69,6 +69,7 @@ "avg_savings": "Середня економія", "awaitDAppProcessing": "Зачекайте, доки dApp завершить обробку.", "awaiting_payment_confirmation": "Очікується підтвердження платежу", + "background_sync": "Фонове синхронізація", "background_sync_mode": "Фоновий режим синхронізації", "backup": "Резервна копія", "backup_file": "Файл резервної копії", @@ -923,6 +924,8 @@ "understand": "Зрозуміло", "unlock": "Розблокувати", "unmatched_currencies": "Валюта вашого гаманця не збігається з валютою сканованого QR-коду", + "unrestricted_background_service": "Необмежена фонова послуга", + "unrestricted_background_service_notice": "Для того, щоб увімкнути фонову синхронізацію, вам потрібно ввімкнути необмежену фонову послугу", "unspent_change": "Зміна", "unspent_coins_details_title": "Відомості про невитрачені монети", "unspent_coins_title": "Невитрачені монети", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index fea91580b..307340a21 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -69,6 +69,7 @@ "avg_savings": "اوسط بچت", "awaitDAppProcessing": "۔ﮟﯾﺮﮐ ﺭﺎﻈﺘﻧﺍ ﺎﮐ ﮯﻧﻮﮨ ﻞﻤﮑﻣ ﮓﻨﺴﯿﺳﻭﺮﭘ ﮯﮐ dApp ﻡﺮﮐ ﮦﺍﺮﺑ", "awaiting_payment_confirmation": "ادائیگی کی تصدیق کے منتظر", + "background_sync": "پس منظر کی ہم آہنگی", "background_sync_mode": "پس منظر کی مطابقت پذیری کا موڈ", "backup": "بیک اپ", "backup_file": "بیک اپ فائل", @@ -924,6 +925,8 @@ "understand": "میں سمجھتا ہوں۔", "unlock": "غیر مقفل", "unmatched_currencies": "آپ کے پرس کی موجودہ کرنسی اسکین شدہ QR سے مماثل نہیں ہے۔", + "unrestricted_background_service": "غیر محدود پس منظر کی خدمت", + "unrestricted_background_service_notice": "پس منظر کی مطابقت پذیری کو قابل بنانے کے ل you آپ کو غیر محدود پس منظر کی خدمت کو فعال کرنے کی ضرورت ہے", "unspent_change": "تبدیل کریں", "unspent_coins_details_title": "غیر خرچ شدہ سککوں کی تفصیلات", "unspent_coins_title": "غیر خرچ شدہ سکے ۔", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 776131f5f..c2935e9a0 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -69,6 +69,7 @@ "avg_savings": "Tiết kiệm trung bình", "awaitDAppProcessing": "Vui lòng đợi ứng dụng phi tập trung hoàn thành xử lý.", "awaiting_payment_confirmation": "Đang chờ xác nhận thanh toán", + "background_sync": "Đồng bộ nền", "background_sync_mode": "Chế độ đồng bộ nền", "backup": "Sao lưu", "backup_file": "Tập tin sao lưu", @@ -919,6 +920,8 @@ "understand": "Tôi hiểu", "unlock": "Mở khóa", "unmatched_currencies": "Tiền tệ của ví hiện tại của bạn không khớp với QR đã quét", + "unrestricted_background_service": "Dịch vụ nền không giới hạn", + "unrestricted_background_service_notice": "Để cho phép đồng bộ hóa nền, bạn cần bật dịch vụ nền không giới hạn", "unspent_change": "Tiền thối", "unspent_coins_details_title": "Chi tiết các đồng tiền chưa chi tiêu", "unspent_coins_title": "Các đồng tiền chưa chi tiêu", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index cbe7c8ffd..b97fbb757 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -69,6 +69,7 @@ "avg_savings": "Ìpamọ́ l’óòrèkóòrè", "awaitDAppProcessing": "Fi inurere duro fun dApp lati pari sisẹ.", "awaiting_payment_confirmation": "À ń dúró de ìjẹ́rìísí àránṣẹ́", + "background_sync": "Imuṣiṣẹ Labẹ", "background_sync_mode": "Ipo amuṣiṣẹpọ abẹlẹ", "backup": "Ṣẹ̀dà", "backup_file": "Ṣẹ̀dà akọsílẹ̀", @@ -923,6 +924,8 @@ "understand": "Ó ye mi", "unlock": "Sisalẹ", "unmatched_currencies": "Irú owó ti àpamọ́wọ́ yín kì í ṣe irú ti yíya àmì ìlujá", + "unrestricted_background_service": "Iṣẹ ipilẹṣẹ ti ko nilẹ", + "unrestricted_background_service_notice": "Ni ibere lati mu ṣiṣẹpọ lẹhin ti o nilo lati ṣiṣẹ iṣẹ iṣẹ ti ko ni ibatan", "unspent_change": "Yipada", "unspent_coins_details_title": "Àwọn owó ẹyọ t'á kò tí ì san", "unspent_coins_title": "Àwọn owó ẹyọ t'á kò tí ì san", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 598f90a00..e1767a28f 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -69,6 +69,7 @@ "avg_savings": "平均储蓄", "awaitDAppProcessing": "请等待 dApp 处理完成。", "awaiting_payment_confirmation": "等待付款确认", + "background_sync": "背景同步", "background_sync_mode": "后台同步模式", "backup": "备份", "backup_file": "备份文件", @@ -922,6 +923,8 @@ "understand": "我已知晓", "unlock": "开锁", "unmatched_currencies": "您当前钱包的货币与扫描的 QR 的货币不匹配", + "unrestricted_background_service": "不受限制的背景服务", + "unrestricted_background_service_notice": "为了启用背景同步,您需要启用无限制的背景服务", "unspent_change": "改变", "unspent_coins_details_title": "未使用代幣詳情", "unspent_coins_title": "未使用的硬幣", From a085eff984c222ed3532fe82f068b9151ab1aa4d Mon Sep 17 00:00:00 2001 From: cyan Date: Fri, 21 Mar 2025 19:04:50 +0100 Subject: [PATCH 055/243] add decred to docs (#2105) --- docs/builds/ANDROID.md | 1 + docs/builds/IOS.md | 1 + docs/builds/MACOS.md | 1 + 3 files changed, 3 insertions(+) diff --git a/docs/builds/ANDROID.md b/docs/builds/ANDROID.md index 61d04185e..9e4e75685 100644 --- a/docs/builds/ANDROID.md +++ b/docs/builds/ANDROID.md @@ -28,6 +28,7 @@ pushd scripts/android # source ./app_env.sh monero.com # Uncomment this line to build monero.com ./app_config.sh ./build_monero_all.sh + ./build_decred.sh ./build_mwebd.sh --dont-install popd pushd android/app diff --git a/docs/builds/IOS.md b/docs/builds/IOS.md index dd75bfed7..00c28a178 100644 --- a/docs/builds/IOS.md +++ b/docs/builds/IOS.md @@ -101,6 +101,7 @@ Build the necessary libraries and their dependencies: ```zsh ./build_monero_all.sh ./build_mwebd.sh +./build_decred.sh ``` NOTE: This step will take quite a while, so be sure you grab a cup of coffee or a good book! diff --git a/docs/builds/MACOS.md b/docs/builds/MACOS.md index af3d8c1df..187b4739b 100644 --- a/docs/builds/MACOS.md +++ b/docs/builds/MACOS.md @@ -93,6 +93,7 @@ Build the necessary libraries and their dependencies: ```zsh ./build_monero_all.sh +./build_decred.sh ``` NOTE: This step will take quite a while, so be sure you grab a cup of coffee or a good book! From 102ab8dbe6e2d38dbdfab29ff3f924f1356dcb76 Mon Sep 17 00:00:00 2001 From: cyan Date: Sat, 22 Mar 2025 02:16:54 +0100 Subject: [PATCH 056/243] CW-875 BackupServiceV3 (#2064) * implement v3 backup system - Add new BackupServiceV3 with chunked backup support and checksums - Implement json-based metadata in backup system - Instead of binary file export a .zip file that user can open and see that it is a cake backup (should also prevent 3rd party software from corrupting binary data inside of the file, in case it doesn't checksum will fail, and user will know for sure that backup got corrupted) - Update flutter to 3.27.4 to use archive ^4.x.x (it offers in memory archive operations) * fix wallets not getting restored properly * prevent out of memory errors on files that are way too big during restore * Update lib/view_model/backup_view_model.dart [skip ci] * Update lib/core/backup_service.dart [skip ci] * Update lib/core/backup_service.dart --------- Co-authored-by: Omar Hatem --- .../workflows/automated_integration_test.yml | 2 +- .github/workflows/pr_test_build_android.yml | 2 +- .github/workflows/pr_test_build_linux.yml | 2 +- Dockerfile | 7 +- android/app/build.gradle | 16 +- android/build.gradle | 14 - android/settings.gradle | 31 +- cw_bitcoin/pubspec.lock | 206 ++++---- cw_bitcoin/pubspec.yaml | 2 +- cw_core/lib/utils/print_verbose.dart | 10 +- cw_core/pubspec.lock | 178 +++---- cw_decred/pubspec.lock | 62 +-- .../.plugin_symlinks/path_provider_linux | 2 +- cw_monero/pubspec.lock | 164 +++--- cw_mweb/pubspec.yaml | 2 +- cw_nano/pubspec.lock | 152 +++--- cw_wownero/pubspec.lock | 144 +++--- cw_zano/pubspec.lock | 104 ++-- cw_zano/pubspec.yaml | 2 +- docs/builds/IOS.md | 8 +- docs/builds/MACOS.md | 8 +- docs/builds/WINDOWS.md | 6 +- ios/Podfile.lock | 16 +- .../xcshareddata/xcschemes/Runner.xcscheme | 1 + lib/core/backup_service.dart | 311 +++++------- lib/core/backup_service_v3.dart | 470 ++++++++++++++++++ lib/di.dart | 7 +- lib/locales/hausa_intl.dart | 3 +- lib/locales/yoruba_intl.dart | 2 + lib/src/screens/backup/backup_page.dart | 5 +- lib/view_model/backup_view_model.dart | 36 +- .../restore_from_backup_view_model.dart | 6 +- pubspec_base.yaml | 13 +- scripts/linux/.fvmrc | 2 +- scripts/windows/Dockerfile.windows | 2 +- tool/download_moneroc_prebuilds.dart | 2 +- 36 files changed, 1193 insertions(+), 807 deletions(-) create mode 100644 lib/core/backup_service_v3.dart diff --git a/.github/workflows/automated_integration_test.yml b/.github/workflows/automated_integration_test.yml index 539513111..1ed5baf9f 100644 --- a/.github/workflows/automated_integration_test.yml +++ b/.github/workflows/automated_integration_test.yml @@ -55,7 +55,7 @@ jobs: - name: Flutter action uses: subosito/flutter-action@v1 with: - flutter-version: "3.24.0" + flutter-version: "3.27.4" channel: stable - name: Install package dependencies diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 0f107e7f6..6c72b587d 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -9,7 +9,7 @@ jobs: PR_test_build: runs-on: linux-amd64 container: - image: ghcr.io/cake-tech/cake_wallet:3.24.4-linux + image: ghcr.io/cake-tech/cake_wallet:3.27.4-linux env: STORE_PASS: test@cake_wallet KEY_PASS: test@cake_wallet diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index d729972c7..9317aab34 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -9,7 +9,7 @@ jobs: PR_test_build: runs-on: linux-amd64 container: - image: ghcr.io/cake-tech/cake_wallet:3.24.4-linux + image: ghcr.io/cake-tech/cake_wallet:3.27.4-linux env: STORE_PASS: test@cake_wallet KEY_PASS: test@cake_wallet diff --git a/Dockerfile b/Dockerfile index 21f1ee6f1..d9c99da0b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,5 @@ -# Usage: -# docker build . -f Dockerfile -t ghcr.io/cake-tech/cake_wallet:3.24.4-linux -# docker push ghcr.io/cake-tech/cake_wallet:3.24.4-linux +# docker build . -f Dockerfile -t ghcr.io/cake-tech/cake_wallet:3.27.4-linux +# docker push ghcr.io/cake-tech/cake_wallet:3.27.4-linux # Heavily inspired by cirrusci images # https://github.com/cirruslabs/docker-images-android/blob/master/sdk/tools/Dockerfile @@ -17,7 +16,7 @@ LABEL org.opencontainers.image.source=https://github.com/cake-tech/cake_wallet ENV GOLANG_VERSION=1.23.4 # Pin Flutter version to latest known-working version -ENV FLUTTER_VERSION=3.24.4 +ENV FLUTTER_VERSION=3.27.4 # Pin Android Studio, platform, and build tools versions to latest known-working version # Comes from https://developer.android.com/studio/#command-tools diff --git a/android/app/build.gradle b/android/app/build.gradle index 67f34cc67..c45ed9368 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,3 +1,9 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -21,9 +22,6 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { @@ -38,7 +36,7 @@ if (appPropertiesFile.exists()) { android { compileSdkVersion 35 - buildToolsVersion "34.0.0" + buildToolsVersion "35.0.0" lintOptions { disable 'InvalidPackage' diff --git a/android/build.gradle b/android/build.gradle index 66de0bdca..bc157bd1a 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,17 +1,3 @@ -buildscript { - ext.kotlin_version = '2.0.21' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:8.7.1' - classpath 'com.google.gms:google-services:4.3.8' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() diff --git a/android/settings.gradle b/android/settings.gradle index 5a2f14fb1..468f9b55f 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,15 +1,26 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } } -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.7.1" apply false + id "org.jetbrains.kotlin.android" version "2.0.21" apply false + id "com.google.gms.google-services" version "4.3.8" apply false } + +include ":app" \ No newline at end of file diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 949d40f7e..1ad7b0c60 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -17,14 +17,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.7.0" - archive: - dependency: transitive - description: - name: archive - sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d - url: "https://pub.dev" - source: hosted - version: "3.6.1" args: dependency: transitive description: @@ -37,10 +29,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: "6b151826fcc95ff246cd219a0bf4c753ea14f4081ad71c61939becf3aba27f70" + sha256: "1c296cd268f486cabcc3930e9b93a8133169305f18d722916e675959a88f6d2c" url: "https://pub.dev" source: hosted - version: "1.5.5" + version: "1.5.9" async: dependency: transitive description: @@ -105,10 +97,10 @@ packages: dependency: transitive description: name: bluez - sha256: "203a1924e818a9dd74af2b2c7a8f375ab8e5edf0e486bba8f90a0d8a17ed9fce" + sha256: "61a7204381925896a374301498f2f5399e59827c6498ae1e924aaa598751b545" url: "https://pub.dev" source: hosted - version: "0.8.2" + version: "0.8.3" boolean_selector: dependency: transitive description: @@ -137,18 +129,18 @@ packages: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.4" build_resolvers: dependency: "direct dev" description: @@ -185,10 +177,10 @@ packages: dependency: transitive description: name: built_value - sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + sha256: "8b158ab94ec6913e480dc3f752418348b5ae099eb75868b5f4775f0572999c61" url: "https://pub.dev" source: hosted - version: "8.9.2" + version: "8.9.4" cake_backup: dependency: transitive description: @@ -218,10 +210,10 @@ packages: dependency: transitive description: name: cli_util - sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c url: "https://pub.dev" source: hosted - version: "0.4.1" + version: "0.4.2" clock: dependency: transitive description: @@ -234,18 +226,18 @@ packages: dependency: transitive description: name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.10.1" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" convert: dependency: transitive description: @@ -312,10 +304,10 @@ packages: dependency: transitive description: name: dbus - sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" decimal: dependency: transitive description: @@ -381,10 +373,10 @@ packages: dependency: "direct main" description: name: flutter_mobx - sha256: "859fbf452fa9c2519d2700b125dd7fb14c508bbdd7fb65e26ca8ff6c92280e2e" + sha256: ba5e93467866a2991259dc51cffd41ef45f695c667c2b8e7b087bf24118b50fe url: "https://pub.dev" source: hosted - version: "2.2.1+1" + version: "2.3.0" flutter_test: dependency: "direct dev" description: flutter @@ -394,10 +386,10 @@ packages: dependency: transitive description: name: flutter_web_bluetooth - sha256: fcd03e2e5f82edcedcbc940f1b6a0635a50757374183254f447640886c53208e + sha256: "1363831def5eed1e1064d1eca04e8ccb35446e8f758579c3c519e156b77926da" url: "https://pub.dev" source: hosted - version: "0.2.4" + version: "1.0.0" flutter_web_plugins: dependency: transitive description: flutter @@ -415,18 +407,18 @@ packages: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" google_identity_services_web: dependency: transitive description: name: google_identity_services_web - sha256: "5be191523702ba8d7a01ca97c17fca096822ccf246b0a9f11923a6ded06199b6" + sha256: "55580f436822d64c8ff9a77e37d61f5fb1e6c7ec9d632a43ee324e2a05c3c6c9" url: "https://pub.dev" source: hosted - version: "0.3.1+4" + version: "0.3.3" googleapis_auth: dependency: transitive description: @@ -447,10 +439,10 @@ packages: dependency: "direct main" description: name: grpc - sha256: e93ee3bce45c134bf44e9728119102358c7cd69de7832d9a874e2e74eb8cab40 + sha256: "5b99b7a420937d4361ece68b798c9af8e04b5bc128a7859f2a4be87427694813" url: "https://pub.dev" source: hosted - version: "3.2.4" + version: "4.0.1" hex: dependency: transitive description: @@ -479,34 +471,34 @@ packages: dependency: "direct main" description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.3.0" http2: dependency: transitive description: name: http2 - sha256: "9ced024a160b77aba8fb8674e38f70875e321d319e6f303ec18e87bd5a4b0c1d" + sha256: "382d3aefc5bd6dc68c6b892d7664f29b5beb3251611ae946a98d35158a82bbfa" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" intl: dependency: "direct main" description: @@ -519,10 +511,10 @@ packages: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" js: dependency: transitive description: @@ -543,18 +535,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -568,7 +560,7 @@ packages: description: path: "packages/ledger-bitcoin" ref: HEAD - resolved-ref: "07cd61ef76a2a017b6d5ef233396740163265457" + resolved-ref: e93254f3ff3f996fb91f65a1e7ceffb9f510b4c8 url: "https://github.com/cake-tech/ledger-flutter-plus-plugins" source: git version: "0.0.3" @@ -576,16 +568,16 @@ packages: dependency: "direct main" description: name: ledger_flutter_plus - sha256: c7b04008553193dbca7e17b430768eecc372a72b0ff3625b5e7fc5e5c8d3231b + sha256: "1c03f3c4a9754b5f0170a9eb9552ec54fa86e985f8ee71a255ee2c5629b53d31" url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.5.1" ledger_litecoin: dependency: "direct main" description: path: "packages/ledger-litecoin" ref: HEAD - resolved-ref: "3dee36713e6ebec9dceb59b9ccae7f243a53ea9e" + resolved-ref: e93254f3ff3f996fb91f65a1e7ceffb9f510b4c8 url: "https://github.com/cake-tech/ledger-flutter-plus-plugins" source: git version: "0.0.2" @@ -641,10 +633,10 @@ packages: dependency: "direct main" description: name: mobx - sha256: "63920b27b32ad1910adfe767ab1750e4c212e8923232a1f891597b362074ea5e" + sha256: bf1a90e5bcfd2851fc6984e20eef69557c65d9e4d0a88f5be4cf72c9819ce6b0 url: "https://pub.dev" source: hosted - version: "2.3.3+2" + version: "2.5.0" mobx_codegen: dependency: "direct dev" description: @@ -674,10 +666,10 @@ packages: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" path: dependency: transitive description: @@ -690,26 +682,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted - version: "2.2.12" + version: "2.2.15" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -794,18 +786,18 @@ packages: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" quiver: dependency: transitive description: @@ -834,26 +826,26 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.5.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" + sha256: a768fc8ede5f0c8e6150476e14f38e2417c0864ca36bb4582be8e21925a03c22 url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.6" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4" shared_preferences_linux: dependency: transitive description: @@ -874,10 +866,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" shared_preferences_windows: dependency: transitive description: @@ -890,23 +882,23 @@ packages: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" socks5_proxy: dependency: transitive description: @@ -944,7 +936,7 @@ packages: description: path: "." ref: "sp_v4.0.0" - resolved-ref: ca1add293bd1e06920aa049b655832da50d0dab2 + resolved-ref: "2554cb8bd3ee1d026bc63e76a30d1226960c7cb4" url: "https://github.com/cake-tech/sp_scanner" source: git version: "0.0.1" @@ -952,10 +944,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -968,18 +960,18 @@ packages: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" term_glyph: dependency: transitive description: @@ -992,18 +984,18 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" timing: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" tuple: dependency: transitive description: @@ -1024,10 +1016,10 @@ packages: dependency: transitive description: name: universal_ble - sha256: "0dfbd6b64bff3ad61ed7a895c232530d9614e9b01ab261a74433a43267edb7f3" + sha256: "1fad089150a29db82b3b7d60327e18c5ad6b3a5bb509defc1c690b0a76b9c098" url: "https://pub.dev" source: hosted - version: "0.12.0" + version: "0.15.0" universal_platform: dependency: transitive description: @@ -1056,26 +1048,26 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.3.0" watcher: dependency: "direct overridden" description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web_socket: dependency: transitive description: @@ -1088,10 +1080,10 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" xdg_directories: dependency: transitive description: @@ -1112,18 +1104,18 @@ packages: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" yaml_edit: dependency: transitive description: name: yaml_edit - sha256: e9c1a3543d2da0db3e90270dbb1e4eebc985ee5e3ffe468d83224472b2194a5f + sha256: fb38626579fb345ad00e674e2af3a5c9b0cc4b9bfb8fd7f7ff322c7c9e62aef5 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.6.0 <4.0.0" flutter: ">=3.24.0" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 522199c82..253abce47 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -32,7 +32,7 @@ dependencies: ref: cake-update-v2 cw_mweb: path: ../cw_mweb - grpc: ^3.2.4 + grpc: ^4.0.1 sp_scanner: git: url: https://github.com/cake-tech/sp_scanner diff --git a/cw_core/lib/utils/print_verbose.dart b/cw_core/lib/utils/print_verbose.dart index a5c3337e5..de7cc8557 100644 --- a/cw_core/lib/utils/print_verbose.dart +++ b/cw_core/lib/utils/print_verbose.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + void printV(dynamic content) { CustomTrace programInfo = CustomTrace(StackTrace.current); print("${programInfo.fileName}#${programInfo.lineNumber}:${programInfo.columnNumber} ${programInfo.callerFunctionName}: $content"); @@ -29,7 +31,7 @@ class CustomTrace { var indexOfWhiteSpace = currentTrace.indexOf(' '); /* Create a substring from the first whitespace index till the end of the string */ - var subStr = currentTrace.substring(indexOfWhiteSpace); + var subStr = currentTrace.substring(max(0, indexOfWhiteSpace)); /* Grab the function name using reg expr */ var indexOfFunction = subStr.indexOf(RegExp(r'[A-Za-z0-9_]')); @@ -40,7 +42,7 @@ class CustomTrace { indexOfWhiteSpace = subStr.indexOf(RegExp(r'[ .]')); /* Create a new substring from start to the first index of a whitespace. This substring gives us the function name */ - subStr = subStr.substring(0, indexOfWhiteSpace); + subStr = subStr.substring(0, max(0, indexOfWhiteSpace)); return subStr; } @@ -61,7 +63,7 @@ class CustomTrace { /* Search through the string and find the index of the file name by looking for the '.dart' regex */ var indexOfFileName = traceString.indexOf(RegExp(r'[/A-Za-z_]+.dart'), 1); // 1 to offest and not print the printV function name - var fileInfo = traceString.substring(indexOfFileName); + var fileInfo = traceString.substring(max(0, indexOfFileName)); var listOfInfos = fileInfo.split(":"); @@ -78,7 +80,7 @@ class CustomTrace { columnStr = columnStr.replaceFirst(")", ""); this.columnNumber = int.tryParse(columnStr); } catch (e) { - + print("Unable to parse trace (printV): $e"); } } } diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index da7768ee0..4ba8df5c5 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -5,23 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "72.0.0" + version: "76.0.0" _macros: dependency: transitive description: dart source: sdk - version: "0.3.2" + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "6.7.0" + version: "6.11.0" args: dependency: transitive description: @@ -34,10 +34,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: "6b151826fcc95ff246cd219a0bf4c753ea14f4081ad71c61939becf3aba27f70" + sha256: "1c296cd268f486cabcc3930e9b93a8133169305f18d722916e675959a88f6d2c" url: "https://pub.dev" source: hosted - version: "1.5.5" + version: "1.5.9" async: dependency: transitive description: @@ -67,50 +67,50 @@ packages: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" build_config: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.4" build_resolvers: dependency: "direct dev" description: name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.15" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.3.2" + version: "8.0.0" built_collection: dependency: transitive description: @@ -123,10 +123,10 @@ packages: dependency: transitive description: name: built_value - sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + sha256: "8b158ab94ec6913e480dc3f752418348b5ae099eb75868b5f4775f0572999c61" url: "https://pub.dev" source: hosted - version: "8.9.2" + version: "8.9.4" cake_backup: dependency: "direct main" description: @@ -164,18 +164,18 @@ packages: dependency: transitive description: name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.10.1" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" convert: dependency: transitive description: @@ -212,10 +212,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.3.8" decimal: dependency: "direct main" description: @@ -273,10 +273,10 @@ packages: dependency: "direct main" description: name: flutter_mobx - sha256: "859fbf452fa9c2519d2700b125dd7fb14c508bbdd7fb65e26ca8ff6c92280e2e" + sha256: ba5e93467866a2991259dc51cffd41ef45f695c667c2b8e7b087bf24118b50fe url: "https://pub.dev" source: hosted - version: "2.2.1+1" + version: "2.3.0" flutter_test: dependency: "direct dev" description: flutter @@ -294,10 +294,10 @@ packages: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" graphs: dependency: transitive description: @@ -326,26 +326,26 @@ packages: dependency: "direct main" description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.3.0" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" intl: dependency: "direct main" description: @@ -358,10 +358,10 @@ packages: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" js: dependency: transitive description: @@ -382,18 +382,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -414,10 +414,10 @@ packages: dependency: transitive description: name: macros - sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" url: "https://pub.dev" source: hosted - version: "0.1.2-main.4" + version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -454,18 +454,18 @@ packages: dependency: "direct main" description: name: mobx - sha256: "63920b27b32ad1910adfe767ab1750e4c212e8923232a1f891597b362074ea5e" + sha256: bf1a90e5bcfd2851fc6984e20eef69557c65d9e4d0a88f5be4cf72c9819ce6b0 url: "https://pub.dev" source: hosted - version: "2.3.3+2" + version: "2.5.0" mobx_codegen: dependency: "direct dev" description: name: mobx_codegen - sha256: "8e0d8653a0c720ad933cd8358f6f89f740ce89203657c13f25bea772ef1fff7c" + sha256: "990da80722f7d7c0017dec92040b31545d625b15d40204c36a1e63d167c73cdc" url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.7.0" nested: dependency: transitive description: @@ -487,10 +487,10 @@ packages: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" path: dependency: transitive description: @@ -503,26 +503,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted - version: "2.2.12" + version: "2.2.15" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -591,18 +591,18 @@ packages: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" rational: dependency: transitive description: @@ -615,23 +615,23 @@ packages: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "3.0.0" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" socks5_proxy: dependency: "direct main" description: @@ -652,10 +652,10 @@ packages: dependency: transitive description: name: source_helper - sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" url: "https://pub.dev" source: hosted - version: "1.3.4" + version: "1.3.5" source_span: dependency: transitive description: @@ -668,10 +668,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -684,18 +684,18 @@ packages: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" term_glyph: dependency: transitive description: @@ -708,18 +708,18 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" timing: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" tuple: dependency: transitive description: @@ -756,26 +756,26 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.3.0" watcher: dependency: "direct overridden" description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web_socket: dependency: transitive description: @@ -788,10 +788,10 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" xdg_directories: dependency: transitive description: @@ -804,10 +804,10 @@ packages: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.6.0 <4.0.0" flutter: ">=3.24.0" diff --git a/cw_decred/pubspec.lock b/cw_decred/pubspec.lock index e3d39372b..2324e7bd2 100644 --- a/cw_decred/pubspec.lock +++ b/cw_decred/pubspec.lock @@ -5,23 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "72.0.0" + version: "76.0.0" _macros: dependency: transitive description: dart source: sdk - version: "0.3.2" + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "6.7.0" + version: "6.11.0" args: dependency: transitive description: @@ -75,18 +75,18 @@ packages: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.4" build_resolvers: dependency: "direct dev" description: @@ -180,10 +180,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" convert: dependency: transitive description: @@ -365,10 +365,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" intl: dependency: transitive description: @@ -405,18 +405,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -437,10 +437,10 @@ packages: dependency: transitive description: name: macros - sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" url: "https://pub.dev" source: hosted - version: "0.1.2-main.4" + version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -646,10 +646,10 @@ packages: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_web_socket: dependency: transitive description: @@ -662,7 +662,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" socks5_proxy: dependency: transitive description: @@ -699,10 +699,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -723,10 +723,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" term_glyph: dependency: transitive description: @@ -739,10 +739,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" timing: dependency: transitive description: @@ -787,10 +787,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.3.0" watcher: dependency: transitive description: @@ -848,5 +848,5 @@ packages: source: hosted version: "2.2.2" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.6.0 <4.0.0" flutter: ">=3.24.0" diff --git a/cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux b/cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux index 1306eaecd..a2b4915e7 120000 --- a/cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux +++ b/cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux @@ -1 +1 @@ -/home/parallels/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ \ No newline at end of file +/Users/user/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ \ No newline at end of file diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 5a3d0fb4d..019c9f437 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: "6b151826fcc95ff246cd219a0bf4c753ea14f4081ad71c61939becf3aba27f70" + sha256: "1c296cd268f486cabcc3930e9b93a8133169305f18d722916e675959a88f6d2c" url: "https://pub.dev" source: hosted - version: "1.5.5" + version: "1.5.9" async: dependency: transitive description: @@ -54,10 +54,10 @@ packages: dependency: transitive description: name: bluez - sha256: "203a1924e818a9dd74af2b2c7a8f375ab8e5edf0e486bba8f90a0d8a17ed9fce" + sha256: "61a7204381925896a374301498f2f5399e59827c6498ae1e924aaa598751b545" url: "https://pub.dev" source: hosted - version: "0.8.2" + version: "0.8.3" boolean_selector: dependency: transitive description: @@ -78,18 +78,18 @@ packages: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.4" build_resolvers: dependency: "direct dev" description: @@ -126,10 +126,10 @@ packages: dependency: transitive description: name: built_value - sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + sha256: "8b158ab94ec6913e480dc3f752418348b5ae099eb75868b5f4775f0572999c61" url: "https://pub.dev" source: hosted - version: "8.9.2" + version: "8.9.4" cake_backup: dependency: transitive description: @@ -167,18 +167,18 @@ packages: dependency: transitive description: name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.10.1" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" convert: dependency: transitive description: @@ -230,10 +230,10 @@ packages: dependency: transitive description: name: dbus - sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" decimal: dependency: transitive description: @@ -291,10 +291,10 @@ packages: dependency: "direct main" description: name: flutter_mobx - sha256: "859fbf452fa9c2519d2700b125dd7fb14c508bbdd7fb65e26ca8ff6c92280e2e" + sha256: ba5e93467866a2991259dc51cffd41ef45f695c667c2b8e7b087bf24118b50fe url: "https://pub.dev" source: hosted - version: "2.2.1+1" + version: "2.3.0" flutter_test: dependency: "direct dev" description: flutter @@ -304,10 +304,10 @@ packages: dependency: transitive description: name: flutter_web_bluetooth - sha256: "52ce64f65d7321c4bf6abfe9dac02fb888731339a5e0ad6de59fb916c20c9f02" + sha256: "1363831def5eed1e1064d1eca04e8ccb35446e8f758579c3c519e156b77926da" url: "https://pub.dev" source: hosted - version: "0.2.3" + version: "1.0.0" frontend_server_client: dependency: transitive description: @@ -320,10 +320,10 @@ packages: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" graphs: dependency: transitive description: @@ -336,10 +336,10 @@ packages: dependency: transitive description: name: hashlib - sha256: f572f2abce09fc7aee53f15927052b9732ea1053e540af8cae211111ee0b99b1 + sha256: e13e8237d93fb275cd1c55fc339bb90638994d1a4f140c7ee270173b51f3d169 url: "https://pub.dev" source: hosted - version: "1.21.0" + version: "1.21.1" hashlib_codecs: dependency: transitive description: @@ -368,26 +368,26 @@ packages: dependency: "direct main" description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.3.0" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" intl: dependency: "direct main" description: @@ -400,10 +400,10 @@ packages: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" js: dependency: transitive description: @@ -424,18 +424,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -448,10 +448,10 @@ packages: dependency: "direct main" description: name: ledger_flutter_plus - sha256: c7b04008553193dbca7e17b430768eecc372a72b0ff3625b5e7fc5e5c8d3231b + sha256: "1c03f3c4a9754b5f0170a9eb9552ec54fa86e985f8ee71a255ee2c5629b53d31" url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.5.1" ledger_usb_plus: dependency: transitive description: @@ -504,10 +504,10 @@ packages: dependency: "direct main" description: name: mobx - sha256: "63920b27b32ad1910adfe767ab1750e4c212e8923232a1f891597b362074ea5e" + sha256: bf1a90e5bcfd2851fc6984e20eef69557c65d9e4d0a88f5be4cf72c9819ce6b0 url: "https://pub.dev" source: hosted - version: "2.3.3+2" + version: "2.5.0" mobx_codegen: dependency: "direct dev" description: @@ -554,10 +554,10 @@ packages: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" path: dependency: transitive description: @@ -570,26 +570,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted - version: "2.2.12" + version: "2.2.15" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -650,10 +650,10 @@ packages: dependency: "direct main" description: name: polyseed - sha256: "11d4dbee409db053c5e9cd77382b2f5115f43fc2529158a826a96f3ba505d770" + sha256: ccc6ba0ed3976837e770a324a6295fbf3d7c0d05fc0fa1906e36479b07c2675b url: "https://pub.dev" source: hosted - version: "0.0.6" + version: "0.0.7" pool: dependency: transitive description: @@ -674,18 +674,18 @@ packages: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" rational: dependency: transitive description: @@ -706,23 +706,23 @@ packages: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" socks5_proxy: dependency: transitive description: @@ -759,10 +759,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -775,18 +775,18 @@ packages: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" term_glyph: dependency: transitive description: @@ -799,18 +799,18 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" timing: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" tuple: dependency: transitive description: @@ -831,10 +831,10 @@ packages: dependency: transitive description: name: universal_ble - sha256: "0dfbd6b64bff3ad61ed7a895c232530d9614e9b01ab261a74433a43267edb7f3" + sha256: "1fad089150a29db82b3b7d60327e18c5ad6b3a5bb509defc1c690b0a76b9c098" url: "https://pub.dev" source: hosted - version: "0.12.0" + version: "0.15.0" universal_platform: dependency: transitive description: @@ -863,26 +863,26 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.3.0" watcher: dependency: "direct overridden" description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web_socket: dependency: transitive description: @@ -895,10 +895,10 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" xdg_directories: dependency: transitive description: @@ -919,10 +919,10 @@ packages: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.6.0 <4.0.0" flutter: ">=3.24.0" diff --git a/cw_mweb/pubspec.yaml b/cw_mweb/pubspec.yaml index 2b71264ee..2a8f6b59d 100644 --- a/cw_mweb/pubspec.yaml +++ b/cw_mweb/pubspec.yaml @@ -10,7 +10,7 @@ environment: dependencies: flutter: sdk: flutter - grpc: ^3.2.4 + grpc: ^4.0.1 path_provider: ^2.1.2 plugin_platform_interface: ^2.0.2 cw_core: diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock index 6f5999e59..0891588f2 100644 --- a/cw_nano/pubspec.lock +++ b/cw_nano/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: "6b151826fcc95ff246cd219a0bf4c753ea14f4081ad71c61939becf3aba27f70" + sha256: "1c296cd268f486cabcc3930e9b93a8133169305f18d722916e675959a88f6d2c" url: "https://pub.dev" source: hosted - version: "1.5.5" + version: "1.5.9" async: dependency: transitive description: @@ -94,18 +94,18 @@ packages: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.4" build_resolvers: dependency: transitive description: @@ -142,10 +142,10 @@ packages: dependency: transitive description: name: built_value - sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + sha256: "8b158ab94ec6913e480dc3f752418348b5ae099eb75868b5f4775f0572999c61" url: "https://pub.dev" source: hosted - version: "8.9.2" + version: "8.9.4" cake_backup: dependency: transitive description: @@ -183,18 +183,18 @@ packages: dependency: transitive description: name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.10.1" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" convert: dependency: transitive description: @@ -315,10 +315,10 @@ packages: dependency: transitive description: name: flutter_mobx - sha256: "859fbf452fa9c2519d2700b125dd7fb14c508bbdd7fb65e26ca8ff6c92280e2e" + sha256: ba5e93467866a2991259dc51cffd41ef45f695c667c2b8e7b087bf24118b50fe url: "https://pub.dev" source: hosted - version: "2.2.1+1" + version: "2.3.0" flutter_test: dependency: "direct dev" description: flutter @@ -341,10 +341,10 @@ packages: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" graphs: dependency: transitive description: @@ -381,26 +381,26 @@ packages: dependency: "direct main" description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.3.0" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" intl: dependency: transitive description: @@ -413,10 +413,10 @@ packages: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" js: dependency: transitive description: @@ -437,18 +437,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -509,10 +509,10 @@ packages: dependency: "direct main" description: name: mobx - sha256: "63920b27b32ad1910adfe767ab1750e4c212e8923232a1f891597b362074ea5e" + sha256: bf1a90e5bcfd2851fc6984e20eef69557c65d9e4d0a88f5be4cf72c9819ce6b0 url: "https://pub.dev" source: hosted - version: "2.3.3+2" + version: "2.5.0" mobx_codegen: dependency: "direct dev" description: @@ -559,10 +559,10 @@ packages: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" path: dependency: transitive description: @@ -575,26 +575,26 @@ packages: dependency: transitive description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted - version: "2.2.12" + version: "2.2.15" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -671,18 +671,18 @@ packages: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" rational: dependency: transitive description: @@ -695,26 +695,26 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.5.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" + sha256: a768fc8ede5f0c8e6150476e14f38e2417c0864ca36bb4582be8e21925a03c22 url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.6" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4" shared_preferences_linux: dependency: transitive description: @@ -735,10 +735,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" shared_preferences_windows: dependency: transitive description: @@ -751,23 +751,23 @@ packages: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" socks5_proxy: dependency: transitive description: @@ -804,10 +804,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -820,18 +820,18 @@ packages: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" term_glyph: dependency: transitive description: @@ -844,18 +844,18 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" timing: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" tuple: dependency: transitive description: @@ -892,26 +892,26 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.3.0" watcher: dependency: "direct overridden" description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web_socket: dependency: transitive description: @@ -924,10 +924,10 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" xdg_directories: dependency: transitive description: @@ -940,10 +940,10 @@ packages: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.6.0 <4.0.0" flutter: ">=3.24.0" diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index 7d91b618e..6d4493874 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: "6b151826fcc95ff246cd219a0bf4c753ea14f4081ad71c61939becf3aba27f70" + sha256: "1c296cd268f486cabcc3930e9b93a8133169305f18d722916e675959a88f6d2c" url: "https://pub.dev" source: hosted - version: "1.5.5" + version: "1.5.9" async: dependency: transitive description: @@ -70,18 +70,18 @@ packages: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.4" build_resolvers: dependency: "direct dev" description: @@ -118,10 +118,10 @@ packages: dependency: transitive description: name: built_value - sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + sha256: "8b158ab94ec6913e480dc3f752418348b5ae099eb75868b5f4775f0572999c61" url: "https://pub.dev" source: hosted - version: "8.9.2" + version: "8.9.4" cake_backup: dependency: transitive description: @@ -159,18 +159,18 @@ packages: dependency: transitive description: name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.10.1" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" convert: dependency: transitive description: @@ -275,10 +275,10 @@ packages: dependency: "direct main" description: name: flutter_mobx - sha256: "859fbf452fa9c2519d2700b125dd7fb14c508bbdd7fb65e26ca8ff6c92280e2e" + sha256: ba5e93467866a2991259dc51cffd41ef45f695c667c2b8e7b087bf24118b50fe url: "https://pub.dev" source: hosted - version: "2.2.1+1" + version: "2.3.0" flutter_test: dependency: "direct dev" description: flutter @@ -296,10 +296,10 @@ packages: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" graphs: dependency: transitive description: @@ -312,10 +312,10 @@ packages: dependency: transitive description: name: hashlib - sha256: f572f2abce09fc7aee53f15927052b9732ea1053e540af8cae211111ee0b99b1 + sha256: e13e8237d93fb275cd1c55fc339bb90638994d1a4f140c7ee270173b51f3d169 url: "https://pub.dev" source: hosted - version: "1.21.0" + version: "1.21.1" hashlib_codecs: dependency: transitive description: @@ -344,26 +344,26 @@ packages: dependency: "direct main" description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.3.0" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" intl: dependency: "direct main" description: @@ -376,10 +376,10 @@ packages: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" js: dependency: transitive description: @@ -400,18 +400,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -464,10 +464,10 @@ packages: dependency: "direct main" description: name: mobx - sha256: "63920b27b32ad1910adfe767ab1750e4c212e8923232a1f891597b362074ea5e" + sha256: bf1a90e5bcfd2851fc6984e20eef69557c65d9e4d0a88f5be4cf72c9819ce6b0 url: "https://pub.dev" source: hosted - version: "2.3.3+2" + version: "2.5.0" mobx_codegen: dependency: "direct dev" description: @@ -514,10 +514,10 @@ packages: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" path: dependency: transitive description: @@ -530,26 +530,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted - version: "2.2.12" + version: "2.2.15" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -602,10 +602,10 @@ packages: dependency: "direct main" description: name: polyseed - sha256: "11d4dbee409db053c5e9cd77382b2f5115f43fc2529158a826a96f3ba505d770" + sha256: ccc6ba0ed3976837e770a324a6295fbf3d7c0d05fc0fa1906e36479b07c2675b url: "https://pub.dev" source: hosted - version: "0.0.6" + version: "0.0.7" pool: dependency: transitive description: @@ -626,18 +626,18 @@ packages: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" rational: dependency: transitive description: @@ -650,23 +650,23 @@ packages: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" socks5_proxy: dependency: transitive description: @@ -703,10 +703,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -719,18 +719,18 @@ packages: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" term_glyph: dependency: transitive description: @@ -743,18 +743,18 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" timing: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" tuple: dependency: transitive description: @@ -791,26 +791,26 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.3.0" watcher: dependency: "direct overridden" description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web_socket: dependency: transitive description: @@ -823,10 +823,10 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" xdg_directories: dependency: transitive description: @@ -839,10 +839,10 @@ packages: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.6.0 <4.0.0" flutter: ">=3.24.0" diff --git a/cw_zano/pubspec.lock b/cw_zano/pubspec.lock index 824ad5ead..74ec7206f 100644 --- a/cw_zano/pubspec.lock +++ b/cw_zano/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" + sha256: "1c296cd268f486cabcc3930e9b93a8133169305f18d722916e675959a88f6d2c" url: "https://pub.dev" source: hosted - version: "1.5.8" + version: "1.5.9" async: dependency: transitive description: @@ -70,18 +70,18 @@ packages: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.4" build_resolvers: dependency: "direct dev" description: @@ -118,10 +118,10 @@ packages: dependency: transitive description: name: built_value - sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + sha256: "8b158ab94ec6913e480dc3f752418348b5ae099eb75868b5f4775f0572999c61" url: "https://pub.dev" source: hosted - version: "8.9.2" + version: "8.9.4" cake_backup: dependency: transitive description: @@ -167,10 +167,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" convert: dependency: transitive description: @@ -275,10 +275,10 @@ packages: dependency: "direct main" description: name: flutter_mobx - sha256: "859fbf452fa9c2519d2700b125dd7fb14c508bbdd7fb65e26ca8ff6c92280e2e" + sha256: ba5e93467866a2991259dc51cffd41ef45f695c667c2b8e7b087bf24118b50fe url: "https://pub.dev" source: hosted - version: "2.2.1+1" + version: "2.3.0" flutter_test: dependency: "direct dev" description: flutter @@ -293,10 +293,10 @@ packages: dependency: "direct main" description: name: fluttertoast - sha256: "95f349437aeebe524ef7d6c9bde3e6b4772717cf46a0eb6a3ceaddc740b297cc" + sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1" url: "https://pub.dev" source: hosted - version: "8.2.8" + version: "8.2.12" frontend_server_client: dependency: transitive description: @@ -309,10 +309,10 @@ packages: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" graphs: dependency: transitive description: @@ -341,26 +341,26 @@ packages: dependency: "direct main" description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.3.0" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" intl: dependency: "direct main" description: @@ -405,18 +405,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -469,10 +469,10 @@ packages: dependency: "direct main" description: name: mobx - sha256: "1f01a429529ac55e5e80c0fcad62c60112fb91df3dec11a9113d71cf0c2e2c4c" + sha256: bf1a90e5bcfd2851fc6984e20eef69557c65d9e4d0a88f5be4cf72c9819ce6b0 url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.5.0" mobx_codegen: dependency: "direct dev" description: @@ -631,10 +631,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" rational: dependency: transitive description: @@ -647,10 +647,10 @@ packages: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_web_socket: dependency: transitive description: @@ -663,7 +663,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" socks5_proxy: dependency: transitive description: @@ -700,10 +700,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -716,18 +716,18 @@ packages: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" term_glyph: dependency: transitive description: @@ -740,10 +740,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" timing: dependency: transitive description: @@ -788,26 +788,26 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.3.0" watcher: dependency: "direct overridden" description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web_socket: dependency: transitive description: @@ -820,10 +820,10 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" xdg_directories: dependency: transitive description: @@ -836,10 +836,10 @@ packages: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.6.0 <4.0.0" flutter: ">=3.24.0" diff --git a/cw_zano/pubspec.yaml b/cw_zano/pubspec.yaml index b036a874f..9bf9c66eb 100644 --- a/cw_zano/pubspec.yaml +++ b/cw_zano/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: cw_core: path: ../cw_core json_bigint: ^3.0.0 - fluttertoast: ^8.2.8 + fluttertoast: ^8.2.12 monero: git: url: https://github.com/mrcyjanek/monero_c diff --git a/docs/builds/IOS.md b/docs/builds/IOS.md index 00c28a178..44abaa805 100644 --- a/docs/builds/IOS.md +++ b/docs/builds/IOS.md @@ -7,7 +7,7 @@ The following are the system requirements to build Cake Wallet for your iOS devi ```txt macOS 15.3.1 Xcode 16.2 -Flutter 3.24.4 +Flutter 3.27.4 ``` NOTE: Newer versions of macOS and Xcode may also work, but have not been confirmed to work by the Cake team. @@ -43,9 +43,9 @@ To enable iOS build support for Xcode, perform the following: ### 3. Installing Flutter -Install Flutter, specifically version `3.24.4` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). +Install Flutter, specifically version `3.27.4` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). -NOTE: as `3.24.4` is not the latest version, you'll need to download it from instead of the link in the docs above. +NOTE: as `3.27.4` is not the latest version, you'll need to download it from instead of the link in the docs above. ### 4. Installing Rust @@ -65,7 +65,7 @@ The output of this command should appear like this, indicating successful instal ```zsh Doctor summary (to see all details, run flutter doctor -v): -[✓] Flutter (Channel stable, 3.24.4, on macOS 15.x.x) +[✓] Flutter (Channel stable, 3.27.4, on macOS 15.x.x) [✓] Xcode - develop for iOS and macOS (Xcode 16.2) ``` diff --git a/docs/builds/MACOS.md b/docs/builds/MACOS.md index 187b4739b..46a9842a4 100644 --- a/docs/builds/MACOS.md +++ b/docs/builds/MACOS.md @@ -7,7 +7,7 @@ The following are the system requirements to build Cake Wallet for your macOS de ```txt macOS 15.3.1 Xcode 16.2 -Flutter 3.24.4 +Flutter 3.27.4 ``` ### 1. Installing dependencies @@ -34,9 +34,9 @@ sudo xcodebuild -runFirstLaunch ### 3. Installing Flutter -Install Flutter, specifically version `3.24.4` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). +Install Flutter, specifically version `3.27.4` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). -NOTE: as `3.24.4` is not the latest version, you'll need to download it from instead of the link in the docs above. +NOTE: as `3.27.4` is not the latest version, you'll need to download it from instead of the link in the docs above. ### 4. Installing Rust @@ -56,7 +56,7 @@ The output of this command should appear like this, indicating successful instal ```zsh Doctor summary (to see all details, run flutter doctor -v): -[✓] Flutter (Channel stable, 3.24.4, on macOS 15.x.x) +[✓] Flutter (Channel stable, 3.27.4, on macOS 15.x.x) ... [✓] Xcode - develop for iOS and macOS (Xcode 16.2) ... diff --git a/docs/builds/WINDOWS.md b/docs/builds/WINDOWS.md index 1b5d7a0e8..4fec78dc0 100644 --- a/docs/builds/WINDOWS.md +++ b/docs/builds/WINDOWS.md @@ -6,18 +6,18 @@ The following are the system requirements to build Cake Wallet for your Windows ```txt Windows 10 or later (64-bit), x86-64 based -Flutter 3.24.4 +Flutter 3.27.4 ``` ### 1. Installing Flutter -Install Flutter, specifically version `3.24.4` by following the [official docs](https://docs.flutter.dev/get-started/install/windows). +Install Flutter, specifically version `3.27.4` by following the [official docs](https://docs.flutter.dev/get-started/install/windows). In order for Flutter to function, you'll also need to enable Developer Mode: Start Menu > search for "Run" > type `ms-settings:developers`, and turn on Developer Mode. -NOTE: as `3.24.4` is not the latest version, you'll need to download it from instead of the link in the docs above. +NOTE: as `3.27.4` is not the latest version, you'll need to download it from instead of the link in the docs above. ### 2. Install Development Tools diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 8e9852f30..fc70b4fb6 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3,6 +3,8 @@ PODS: - Flutter - ReachabilitySwift - CryptoSwift (1.8.3) + - cw_mweb (0.0.1): + - Flutter - cw_decred (0.0.1): - Flutter - cw_mweb (0.0.1): @@ -65,7 +67,6 @@ PODS: - Flutter - fluttertoast (0.0.2): - Flutter - - Toast - in_app_review (2.0.0): - Flutter - integration_test (0.0.1): @@ -76,7 +77,7 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - permission_handler_apple (9.1.1): + - permission_handler_apple (9.3.0): - Flutter - ReachabilitySwift (5.2.4) - SDWebImage (5.19.7): @@ -92,7 +93,6 @@ PODS: - sp_scanner (0.0.1): - Flutter - SwiftyGif (5.4.5) - - Toast (4.1.1) - uni_links (0.0.1): - Flutter - universal_ble (0.0.1): @@ -106,6 +106,7 @@ PODS: DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - CryptoSwift + - cw_mweb (from `.symlinks/plugins/cw_mweb/ios`) - cw_decred (from `.symlinks/plugins/cw_decred/ios`) - cw_mweb (from `.symlinks/plugins/cw_mweb/ios`) - device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`) @@ -142,11 +143,12 @@ SPEC REPOS: - ReachabilitySwift - SDWebImage - SwiftyGif - - Toast EXTERNAL SOURCES: connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" + cw_mweb: + :path: ".symlinks/plugins/cw_mweb/ios" cw_decred: :path: ".symlinks/plugins/cw_decred/ios" cw_mweb: @@ -211,19 +213,19 @@ SPEC CHECKSUMS: DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 fast_scanner: 2cb1ad3e69e645e9980fb4961396ce5804caa3e3 - file_picker: 07c75322ede1d47ec9bb4ac82b27c94d3598251a + file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 flutter_local_authentication: 989278c681612f1ee0e36019e149137f114b9d7f flutter_mailer: 3a8cd4f36c960fb04528d5471097270c19fec1c4 flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418 - fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038 + fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 in_app_review: 5596fe56fab799e8edb3561c03d053363ab13457 integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 - permission_handler_apple: 3787117e48f80715ff04a3830ca039283d6a4f29 + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3 sensitive_clipboard: 161e9abc3d56b3131309d8a321eb4690a803c16b diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5e31d3d34..c53e2b314 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -48,6 +48,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 76f54aa14..2af037d44 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -24,118 +24,27 @@ import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/wallet_types.g.dart'; import 'package:cake_backup/backup.dart' as cake_backup; -class BackupService { - BackupService(this._secureStorage, this._walletInfoSource, this._transactionDescriptionBox, - this._keyService, this._sharedPreferences) - : _cipher = Cryptography.instance.chacha20Poly1305Aead(), - _correctWallets = []; +class $BackupService { + $BackupService(this._secureStorage, this.walletInfoSource, this.transactionDescriptionBox, + this.keyService, this.sharedPreferences) + : cipher = Cryptography.instance.chacha20Poly1305Aead(), + correctWallets = []; - static const currentVersion = _v2; + static const currentVersion = _v3; - static const _v1 = 1; static const _v2 = 2; + static const _v3 = 3; - final Cipher _cipher; + final Cipher cipher; final SecureStorage _secureStorage; - final SharedPreferences _sharedPreferences; - final Box _walletInfoSource; - final Box _transactionDescriptionBox; - final KeyService _keyService; - List _correctWallets; + final SharedPreferences sharedPreferences; + final Box walletInfoSource; + final Box transactionDescriptionBox; + final KeyService keyService; + List correctWallets; - Future importBackup(Uint8List data, String password, - {String nonce = secrets.backupSalt}) async { - final version = getVersion(data); - switch (version) { - case _v1: - final backupBytes = data.toList()..removeAt(0); - final backupData = Uint8List.fromList(backupBytes); - await _importBackupV1(backupData, password, nonce: nonce); - break; - case _v2: - await _importBackupV2(data, password); - break; - default: - break; - } - } - - Future exportBackup(String password, - {String nonce = secrets.backupSalt, int version = currentVersion}) async { - switch (version) { - case _v1: - return await _exportBackupV1(password, nonce: nonce); - case _v2: - return await _exportBackupV2(password); - default: - throw Exception('Incorrect version: $version for exportBackup'); - } - } - - @Deprecated('Use v2 instead') - Future _exportBackupV1(String password, {String nonce = secrets.backupSalt}) async => - throw Exception('Deprecated. Export for backups v1 is deprecated. Please use export v2.'); - - Future _exportBackupV2(String password) async { - final zipEncoder = ZipFileEncoder(); - final appDir = await getAppDir(); - final now = DateTime.now(); - final tmpDir = Directory('${appDir.path}/~_BACKUP_TMP'); - final archivePath = '${tmpDir.path}/backup_${now.toString()}.zip'; - final fileEntities = appDir.listSync(recursive: false); - final keychainDump = await _exportKeychainDumpV2(password); - final preferencesDump = await _exportPreferencesJSON(); - final preferencesDumpFile = File('${tmpDir.path}/~_preferences_dump_TMP'); - final keychainDumpFile = File('${tmpDir.path}/~_keychain_dump_TMP'); - final transactionDescriptionDumpFile = - File('${tmpDir.path}/~_transaction_descriptions_dump_TMP'); - - final transactionDescriptionData = _transactionDescriptionBox - .toMap() - .map((key, value) => MapEntry(key.toString(), value.toJson())); - final transactionDescriptionDump = jsonEncode(transactionDescriptionData); - - if (tmpDir.existsSync()) { - tmpDir.deleteSync(recursive: true); - } - - tmpDir.createSync(); - zipEncoder.create(archivePath); - - fileEntities.forEach((entity) { - if (entity.path == archivePath || entity.path == tmpDir.path) { - return; - } - final filename = entity.absolute; - for (var ignore in ignoreFiles) { - final filename = entity.absolute.path; - if (filename.endsWith(ignore) && !filename.contains("wallets/")) { - printV("ignoring backup file: $filename"); - return; - } - } - printV("restoring: $filename"); - if (entity.statSync().type == FileSystemEntityType.directory) { - zipEncoder.addDirectory(Directory(entity.path)); - } else { - zipEncoder.addFile(File(entity.path)); - } - }); - await keychainDumpFile.writeAsBytes(keychainDump.toList()); - await preferencesDumpFile.writeAsString(preferencesDump); - await transactionDescriptionDumpFile.writeAsString(transactionDescriptionDump); - await zipEncoder.addFile(preferencesDumpFile, '~_preferences_dump'); - await zipEncoder.addFile(keychainDumpFile, '~_keychain_dump'); - await zipEncoder.addFile(transactionDescriptionDumpFile, '~_transaction_descriptions_dump'); - zipEncoder.close(); - - final content = File(archivePath).readAsBytesSync(); - tmpDir.deleteSync(recursive: true); - return await _encryptV2(content, password); - } - - Future _importBackupV1(Uint8List data, String password, {required String nonce}) async { + Future importBackupV1(Uint8List data, String password, {required String nonce}) async { final appDir = await getAppDir(); final decryptedData = await _decryptV1(data, password, nonce); final zip = ZipDecoder().decodeBytes(decryptedData); @@ -153,9 +62,9 @@ class BackupService { } }; - await _verifyWallets(); + await verifyWallets(); await _importKeychainDumpV1(password, nonce: nonce); - await _importPreferencesDump(); + await importPreferencesDump(); } // checked with .endsWith - so this should be the last part of the filename @@ -163,12 +72,13 @@ class BackupService { "flutter_assets/kernel_blob.bin", "flutter_assets/vm_snapshot_data", "flutter_assets/isolate_snapshot_data", + "README.txt", ".lock", ]; - Future _importBackupV2(Uint8List data, String password) async { + Future importBackupV2(Uint8List data, String password) async { final appDir = await getAppDir(); - final decryptedData = await _decryptV2(data, password); + final decryptedData = await decryptV2(data, password); final zip = ZipDecoder().decodeBytes(decryptedData); outer: @@ -187,22 +97,25 @@ class BackupService { ..createSync(recursive: true) ..writeAsBytesSync(content, flush: true); } else { - Directory('${appDir.path}/' + filename)..create(recursive: true); + final dir = Directory('${appDir.path}/' + filename); + if (!dir.existsSync()) { + dir.createSync(recursive: true); + } } }; - await _verifyWallets(); - await _importKeychainDumpV2(password); - await _importPreferencesDump(); - await _importTransactionDescriptionDump(); // HiveError: Box has already been closed + await verifyWallets(); + await importKeychainDumpV2(password); + await importPreferencesDump(); + await importTransactionDescriptionDump(); // HiveError: Box has already been closed } - Future _verifyWallets() async { + Future verifyWallets() async { final walletInfoSource = await _reloadHiveWalletInfoBox(); - _correctWallets = + correctWallets = walletInfoSource.values.where((info) => availableWalletTypes.contains(info.type)).toList(); - if (_correctWallets.isEmpty) { + if (correctWallets.isEmpty) { throw Exception('Correct wallets not detected'); } } @@ -219,7 +132,7 @@ class BackupService { return await CakeHive.openBox(WalletInfo.boxName); } - Future _importTransactionDescriptionDump() async { + Future importTransactionDescriptionDump() async { final appDir = await getAppDir(); final transactionDescriptionFile = File('${appDir.path}/~_transaction_descriptions_dump'); @@ -231,7 +144,7 @@ class BackupService { json.decode(transactionDescriptionFile.readAsStringSync()) as Map; final descriptionsMap = jsonData.map((key, value) => MapEntry(key, TransactionDescription.fromJson(value as Map))); - var box = _transactionDescriptionBox; + var box = transactionDescriptionBox; if (!box.isOpen) { final transactionDescriptionsBoxKey = await getEncryptionKey(secureStorage: _secureStorage, forKey: TransactionDescription.boxKey); @@ -242,7 +155,7 @@ class BackupService { await box.putAll(descriptionsMap); } - Future _importPreferencesDump() async { + Future importPreferencesDump() async { final appDir = await getAppDir(); final preferencesFile = File('${appDir.path}/~_preferences_dump'); @@ -254,12 +167,12 @@ class BackupService { String currentWalletName = data[PreferencesKey.currentWalletName] as String; int currentWalletType = data[PreferencesKey.currentWalletType] as int; - final isCorrentCurrentWallet = _correctWallets + final isCorrentCurrentWallet = correctWallets .any((info) => info.name == currentWalletName && info.type.index == currentWalletType); if (!isCorrentCurrentWallet) { - currentWalletName = _correctWallets.first.name; - currentWalletType = serializeToInt(_correctWallets.first.type); + currentWalletName = correctWallets.first.name; + currentWalletType = serializeToInt(correctWallets.first.type); } final currentNodeId = data[PreferencesKey.currentNodeIdKey] as int?; @@ -298,114 +211,114 @@ class BackupService { final autoGenerateSubaddressStatus = data[PreferencesKey.autoGenerateSubaddressStatusKey] as int?; - await _sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName); + await sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName); if (currentNodeId != null) - await _sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, currentNodeId); + await sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, currentNodeId); if (currentBalanceDisplayMode != null) - await _sharedPreferences.setInt( + await sharedPreferences.setInt( PreferencesKey.currentBalanceDisplayModeKey, currentBalanceDisplayMode); - await _sharedPreferences.setInt(PreferencesKey.currentWalletType, currentWalletType); + await sharedPreferences.setInt(PreferencesKey.currentWalletType, currentWalletType); if (currentFiatCurrency != null) - await _sharedPreferences.setString( + await sharedPreferences.setString( PreferencesKey.currentFiatCurrencyKey, currentFiatCurrency); if (shouldSaveRecipientAddress != null) - await _sharedPreferences.setBool( + await sharedPreferences.setBool( PreferencesKey.shouldSaveRecipientAddressKey, shouldSaveRecipientAddress); if (isAppSecure != null) - await _sharedPreferences.setBool(PreferencesKey.isAppSecureKey, isAppSecure); + await sharedPreferences.setBool(PreferencesKey.isAppSecureKey, isAppSecure); if (disableTradeOption != null) - await _sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption); + await sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption); if (currentTransactionPriorityKeyLegacy != null) - await _sharedPreferences.setInt( + await sharedPreferences.setInt( PreferencesKey.currentTransactionPriorityKeyLegacy, currentTransactionPriorityKeyLegacy); if (currentBitcoinElectrumSererId != null) - await _sharedPreferences.setInt( + await sharedPreferences.setInt( PreferencesKey.currentBitcoinElectrumSererIdKey, currentBitcoinElectrumSererId); if (currentLanguageCode != null) - await _sharedPreferences.setString(PreferencesKey.currentLanguageCode, currentLanguageCode); + await sharedPreferences.setString(PreferencesKey.currentLanguageCode, currentLanguageCode); if (displayActionListMode != null) - await _sharedPreferences.setInt( + await sharedPreferences.setInt( PreferencesKey.displayActionListModeKey, displayActionListMode); if (fiatApiMode != null) - await _sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey, fiatApiMode); + await sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey, fiatApiMode); if (autoGenerateSubaddressStatus != null) - await _sharedPreferences.setInt( + await sharedPreferences.setInt( PreferencesKey.autoGenerateSubaddressStatusKey, autoGenerateSubaddressStatus); if (currentTheme != null && DeviceInfo.instance.isMobile) { - await _sharedPreferences.setInt(PreferencesKey.currentTheme, currentTheme); + await sharedPreferences.setInt(PreferencesKey.currentTheme, currentTheme); // enforce dark theme on desktop platforms until the design is ready: } else if (DeviceInfo.instance.isDesktop) { - await _sharedPreferences.setInt(PreferencesKey.currentTheme, ThemeList.darkTheme.raw); + await sharedPreferences.setInt(PreferencesKey.currentTheme, ThemeList.darkTheme.raw); } if (exchangeStatus != null) - await _sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, exchangeStatus); + await sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, exchangeStatus); if (currentDefaultSettingsMigrationVersion != null) - await _sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, + await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, currentDefaultSettingsMigrationVersion); if (moneroTransactionPriority != null) - await _sharedPreferences.setInt( + await sharedPreferences.setInt( PreferencesKey.moneroTransactionPriority, moneroTransactionPriority); if (bitcoinTransactionPriority != null) - await _sharedPreferences.setInt( + await sharedPreferences.setInt( PreferencesKey.bitcoinTransactionPriority, bitcoinTransactionPriority); if (sortBalanceTokensBy != null) - await _sharedPreferences.setInt(PreferencesKey.sortBalanceBy, sortBalanceTokensBy); + await sharedPreferences.setInt(PreferencesKey.sortBalanceBy, sortBalanceTokensBy); if (pinNativeTokenAtTop != null) - await _sharedPreferences.setBool(PreferencesKey.pinNativeTokenAtTop, pinNativeTokenAtTop); + await sharedPreferences.setBool(PreferencesKey.pinNativeTokenAtTop, pinNativeTokenAtTop); if (useEtherscan != null) - await _sharedPreferences.setBool(PreferencesKey.useEtherscan, useEtherscan); + await sharedPreferences.setBool(PreferencesKey.useEtherscan, useEtherscan); if (defaultNanoRep != null) - await _sharedPreferences.setString(PreferencesKey.defaultNanoRep, defaultNanoRep); + await sharedPreferences.setString(PreferencesKey.defaultNanoRep, defaultNanoRep); if (defaultBananoRep != null) - await _sharedPreferences.setString(PreferencesKey.defaultBananoRep, defaultBananoRep); + await sharedPreferences.setString(PreferencesKey.defaultBananoRep, defaultBananoRep); - if (syncAll != null) await _sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll); + if (syncAll != null) await sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll); if (lookupsTwitter != null) - await _sharedPreferences.setBool(PreferencesKey.lookupsTwitter, lookupsTwitter); + await sharedPreferences.setBool(PreferencesKey.lookupsTwitter, lookupsTwitter); if (lookupsMastodon != null) - await _sharedPreferences.setBool(PreferencesKey.lookupsMastodon, lookupsMastodon); + await sharedPreferences.setBool(PreferencesKey.lookupsMastodon, lookupsMastodon); if (lookupsYatService != null) - await _sharedPreferences.setBool(PreferencesKey.lookupsYatService, lookupsYatService); + await sharedPreferences.setBool(PreferencesKey.lookupsYatService, lookupsYatService); if (lookupsUnstoppableDomains != null) - await _sharedPreferences.setBool( + await sharedPreferences.setBool( PreferencesKey.lookupsUnstoppableDomains, lookupsUnstoppableDomains); if (lookupsOpenAlias != null) - await _sharedPreferences.setBool(PreferencesKey.lookupsOpenAlias, lookupsOpenAlias); + await sharedPreferences.setBool(PreferencesKey.lookupsOpenAlias, lookupsOpenAlias); - if (lookupsENS != null) await _sharedPreferences.setBool(PreferencesKey.lookupsENS, lookupsENS); + if (lookupsENS != null) await sharedPreferences.setBool(PreferencesKey.lookupsENS, lookupsENS); if (lookupsWellKnown != null) - await _sharedPreferences.setBool(PreferencesKey.lookupsWellKnown, lookupsWellKnown); + await sharedPreferences.setBool(PreferencesKey.lookupsWellKnown, lookupsWellKnown); - if (syncAll != null) await _sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll); + if (syncAll != null) await sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll); - if (syncMode != null) await _sharedPreferences.setInt(PreferencesKey.syncModeKey, syncMode); + if (syncMode != null) await sharedPreferences.setInt(PreferencesKey.syncModeKey, syncMode); await preferencesFile.delete(); } @@ -436,12 +349,12 @@ class BackupService { keychainDumpFile.deleteSync(); } - Future _importKeychainDumpV2(String password, + Future importKeychainDumpV2(String password, {String keychainSalt = secrets.backupKeychainSalt}) async { final appDir = await getAppDir(); final keychainDumpFile = File('${appDir.path}/~_keychain_dump'); final decryptedKeychainDumpFileData = - await _decryptV2(keychainDumpFile.readAsBytesSync(), '$keychainSalt$password'); + await decryptV2(keychainDumpFile.readAsBytesSync(), '$keychainSalt$password'); final keychainJSON = json.decode(utf8.decode(decryptedKeychainDumpFileData)) as Map; final keychainWalletsInfo = keychainJSON['wallets'] as List; @@ -462,7 +375,7 @@ class BackupService { final name = info['name'] as String; final password = info['password'] as String; - await _keyService.saveWalletPassword(walletName: name, password: password); + await keyService.saveWalletPassword(walletName: name, password: password); } @Deprecated('Use v2 instead') @@ -470,14 +383,14 @@ class BackupService { {required String nonce, String keychainSalt = secrets.backupKeychainSalt}) async => throw Exception('Deprecated'); - Future _exportKeychainDumpV2(String password, + Future exportKeychainDumpV2(String password, {String keychainSalt = secrets.backupKeychainSalt}) async { final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); - final wallets = await Future.wait(_walletInfoSource.values.map((walletInfo) async { + final wallets = await Future.wait(walletInfoSource.values.map((walletInfo) async { return { 'name': walletInfo.name, 'type': walletInfo.type.toString(), - 'password': await _keyService.getWalletPassword(walletName: walletInfo.name) + 'password': await keyService.getWalletPassword(walletName: walletInfo.name) }; })); final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword); @@ -489,58 +402,58 @@ class BackupService { return encrypted; } - Future _exportPreferencesJSON() async { + Future exportPreferencesJSON() async { final preferences = { PreferencesKey.currentWalletName: - _sharedPreferences.getString(PreferencesKey.currentWalletName), - PreferencesKey.currentNodeIdKey: _sharedPreferences.getInt(PreferencesKey.currentNodeIdKey), + sharedPreferences.getString(PreferencesKey.currentWalletName), + PreferencesKey.currentNodeIdKey: sharedPreferences.getInt(PreferencesKey.currentNodeIdKey), PreferencesKey.currentBalanceDisplayModeKey: - _sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey), - PreferencesKey.currentWalletType: _sharedPreferences.getInt(PreferencesKey.currentWalletType), + sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey), + PreferencesKey.currentWalletType: sharedPreferences.getInt(PreferencesKey.currentWalletType), PreferencesKey.currentFiatCurrencyKey: - _sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey), + sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey), PreferencesKey.shouldSaveRecipientAddressKey: - _sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey), - PreferencesKey.disableTradeOption: _sharedPreferences.getBool(PreferencesKey.disableTradeOption), + sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey), + PreferencesKey.disableTradeOption: sharedPreferences.getBool(PreferencesKey.disableTradeOption), PreferencesKey.currentTransactionPriorityKeyLegacy: - _sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy), + sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy), PreferencesKey.currentBitcoinElectrumSererIdKey: - _sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey), + sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey), PreferencesKey.currentLanguageCode: - _sharedPreferences.getString(PreferencesKey.currentLanguageCode), + sharedPreferences.getString(PreferencesKey.currentLanguageCode), PreferencesKey.displayActionListModeKey: - _sharedPreferences.getInt(PreferencesKey.displayActionListModeKey), - PreferencesKey.currentTheme: _sharedPreferences.getInt(PreferencesKey.currentTheme), - PreferencesKey.exchangeStatusKey: _sharedPreferences.getInt(PreferencesKey.exchangeStatusKey), + sharedPreferences.getInt(PreferencesKey.displayActionListModeKey), + PreferencesKey.currentTheme: sharedPreferences.getInt(PreferencesKey.currentTheme), + PreferencesKey.exchangeStatusKey: sharedPreferences.getInt(PreferencesKey.exchangeStatusKey), PreferencesKey.currentDefaultSettingsMigrationVersion: - _sharedPreferences.getInt(PreferencesKey.currentDefaultSettingsMigrationVersion), + sharedPreferences.getInt(PreferencesKey.currentDefaultSettingsMigrationVersion), PreferencesKey.bitcoinTransactionPriority: - _sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority), + sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority), PreferencesKey.moneroTransactionPriority: - _sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority), + sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority), PreferencesKey.currentFiatApiModeKey: - _sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey), - PreferencesKey.sortBalanceBy: _sharedPreferences.getInt(PreferencesKey.sortBalanceBy), + sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey), + PreferencesKey.sortBalanceBy: sharedPreferences.getInt(PreferencesKey.sortBalanceBy), PreferencesKey.pinNativeTokenAtTop: - _sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop), - PreferencesKey.useEtherscan: _sharedPreferences.getBool(PreferencesKey.useEtherscan), - PreferencesKey.defaultNanoRep: _sharedPreferences.getString(PreferencesKey.defaultNanoRep), + sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop), + PreferencesKey.useEtherscan: sharedPreferences.getBool(PreferencesKey.useEtherscan), + PreferencesKey.defaultNanoRep: sharedPreferences.getString(PreferencesKey.defaultNanoRep), PreferencesKey.defaultBananoRep: - _sharedPreferences.getString(PreferencesKey.defaultBananoRep), - PreferencesKey.lookupsTwitter: _sharedPreferences.getBool(PreferencesKey.lookupsTwitter), - PreferencesKey.lookupsMastodon: _sharedPreferences.getBool(PreferencesKey.lookupsMastodon), + sharedPreferences.getString(PreferencesKey.defaultBananoRep), + PreferencesKey.lookupsTwitter: sharedPreferences.getBool(PreferencesKey.lookupsTwitter), + PreferencesKey.lookupsMastodon: sharedPreferences.getBool(PreferencesKey.lookupsMastodon), PreferencesKey.lookupsYatService: - _sharedPreferences.getBool(PreferencesKey.lookupsYatService), + sharedPreferences.getBool(PreferencesKey.lookupsYatService), PreferencesKey.lookupsUnstoppableDomains: - _sharedPreferences.getBool(PreferencesKey.lookupsUnstoppableDomains), - PreferencesKey.lookupsOpenAlias: _sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias), - PreferencesKey.lookupsENS: _sharedPreferences.getBool(PreferencesKey.lookupsENS), + sharedPreferences.getBool(PreferencesKey.lookupsUnstoppableDomains), + PreferencesKey.lookupsOpenAlias: sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias), + PreferencesKey.lookupsENS: sharedPreferences.getBool(PreferencesKey.lookupsENS), PreferencesKey.lookupsWellKnown: - _sharedPreferences.getBool(PreferencesKey.lookupsWellKnown), - PreferencesKey.syncModeKey: _sharedPreferences.getInt(PreferencesKey.syncModeKey), - PreferencesKey.syncAllKey: _sharedPreferences.getBool(PreferencesKey.syncAllKey), + sharedPreferences.getBool(PreferencesKey.lookupsWellKnown), + PreferencesKey.syncModeKey: sharedPreferences.getInt(PreferencesKey.syncModeKey), + PreferencesKey.syncAllKey: sharedPreferences.getBool(PreferencesKey.syncAllKey), PreferencesKey.autoGenerateSubaddressStatusKey: - _sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey), + sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey), }; return json.encode(preferences); @@ -564,13 +477,13 @@ class BackupService { final nonce = base64.decode(nonceBase64).toList(); final box = SecretBox(Uint8List.sublistView(data, 0, data.lengthInBytes - macLength).toList(), nonce: nonce, mac: Mac(Uint8List.sublistView(data, data.lengthInBytes - macLength))); - final plainData = await _cipher.decrypt(box, secretKey: secretKey); + final plainData = await cipher.decrypt(box, secretKey: secretKey); return Uint8List.fromList(plainData); } Future _encryptV2(Uint8List data, String passphrase) async => cake_backup.encrypt(passphrase, data, version: _v2); - Future _decryptV2(Uint8List data, String passphrase) async => + Future decryptV2(Uint8List data, String passphrase) async => cake_backup.decrypt(passphrase, data); } diff --git a/lib/core/backup_service_v3.dart b/lib/core/backup_service_v3.dart new file mode 100644 index 000000000..5f5e77b00 --- /dev/null +++ b/lib/core/backup_service_v3.dart @@ -0,0 +1,470 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:archive/archive_io.dart'; +import 'package:cake_wallet/core/backup_service.dart'; +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_backup/backup.dart' as cake_backup; +import 'package:cake_wallet/utils/package_info.dart'; +import 'package:crypto/crypto.dart'; +import 'package:cw_core/root_dir.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:flutter/foundation.dart'; + +enum BackupVersion { + unknown, // index 0 + v1, + v2, + v3, +} + +class ChunkChecksum { + ChunkChecksum({ + required this.encrypted, + required this.plain, + }); + + final String encrypted; + final String plain; + + factory ChunkChecksum.fromJson(Map json) { + return ChunkChecksum( + encrypted: json['encrypted'] as String, + plain: json['plain'] as String, + ); + } + + Map toJson() { + return { + 'encrypted': encrypted, + 'plain': plain, + }; + } + + @override + String toString() { + return 'ChunkChecksum(encrypted: $encrypted, plain: $plain)'; + } +} + +class ChunkLength { + ChunkLength({ + required this.encrypted, + required this.plain, + }); + + final int encrypted; + final int plain; + + factory ChunkLength.fromJson(Map json) { + return ChunkLength( + encrypted: json['encrypted'] as int, + plain: json['plain'] as int, + ); + } + + Map toJson() { + return { + 'encrypted': encrypted, + 'plain': plain, + }; + } + @override + String toString() { + return 'ChunkLength(encrypted: $encrypted, plain: $plain)'; + } +} + +class ChunkDetails { + ChunkDetails({ + required this.sha512sum, + required this.length, + }); + + final ChunkChecksum sha512sum; + final ChunkLength length; + + factory ChunkDetails.fromJson(Map json) { + return ChunkDetails( + sha512sum: ChunkChecksum.fromJson(json['sha512sum'] as Map), + length: ChunkLength.fromJson(json['length'] as Map), + ); + } + + Map toJson() { + return { + 'sha512sum': sha512sum, + 'length': length, + }; + } + + @override + String toString() { + return 'ChunkDetails(sha512sum: $sha512sum, length: $length)'; + } +} + +class BackupMetadata { + BackupMetadata({ + required this.version, + required this.sha512sum, + required this.chunks, + required this.cakeVersion, + }); + + final BackupVersion version; + String sha512sum; + final List chunks; + String cakeVersion; + factory BackupMetadata.fromJson(Map json) { + return BackupMetadata( + version: BackupVersion.values[json['version'] as int], + sha512sum: json['sha512sum'] as String, + chunks: (json['chunks'] as List).map((chunk) => ChunkDetails.fromJson(chunk as Map)).toList(), + cakeVersion: json['cakeVersion'] as String, + ); + } + + Map toJson() { + return { + 'version': version.index, + 'sha512sum': sha512sum, + 'chunks': chunks.map((chunk) => chunk.toJson()).toList(), + 'cakeVersion': cakeVersion, + }; + } + + @override + String toString() { + return 'BackupMetadata(version: $version, sha512sum: $sha512sum, chunks: $chunks)'; + } +} + +class BackupServiceV3 extends $BackupService { + BackupServiceV3(super.secureStorage, super.walletInfoSource, super.transactionDescriptionBox, super.keyService, super.sharedPreferences); + + static BackupVersion get currentVersion => BackupVersion.v3; + + Future exportBackupFile(String password, {String nonce = secrets.backupSalt}) { + return exportBackupFileV3(password, nonce: nonce); + } + + BackupVersion getVersionFile(File data) { + final raf = data.openSync(mode: FileMode.read); + + try { + // Read first 4 bytes to check both version and zip signature + final buffer = Uint8List(1); + final bytesRead = raf.readIntoSync(buffer); + + if (bytesRead == 0) { + throw Exception('Invalid backup file: empty file'); + } + + // Check if first byte is version 1 or 2 + if (buffer[0] == 1) { + return BackupVersion.v1; + } else if (buffer[0] == 2) { + return BackupVersion.v2; + } else if (buffer[0] == 0x50) { + // $ head -c 64 test-archive.zip | hexdump -C + // 00000000 50 4b 03 04 .... + // Here we just check if the first byte is the zip signature + // Inside of v3 backup we have multiple files. + // Check metadata.json for version in v3 backup + final inputStream = InputFileStream(data.path); + final archive = ZipDecoder().decodeStream(inputStream); + final metadataFile = archive.findFile('metadata.json'); + if (metadataFile == null) { + throw Exception('Invalid v3 backup: missing metadata.json'); + } + final metadataBytes = metadataFile.rawContent!.readBytes(); + final metadataString = utf8.decode(metadataBytes); + final metadataJsonRaw = json.decode(metadataString) as Map; + final metadata = BackupMetadata.fromJson(metadataJsonRaw); + if (metadata.version == BackupVersion.v3) { + return BackupVersion.v3; + } + } + + throw Exception('Invalid backup file: unknown version'); + } finally { + raf.closeSync(); + } + } + + Future importBackupFile(File file, String password, {String nonce = secrets.backupSalt}) { + final version = getVersionFile(file); + switch (version) { + case BackupVersion.unknown: + throw Exception('Invalid backup file: unknown version'); + case BackupVersion.v1: + final data = file.readAsBytesSync(); + final backupBytes = data.toList()..removeAt(0); + final backupData = Uint8List.fromList(backupBytes); + return super.importBackupV1(backupData, password, nonce: nonce); + case BackupVersion.v2: + return super.importBackupV2(file.readAsBytesSync(), password); + case BackupVersion.v3: + return importBackupFileV3(file, password, nonce: nonce); + } + } + + Future importBackupFileV3(File file, String password, {String nonce = secrets.backupSalt}) async{ + // Overall design of v3 backup is the following: + // 1. backup.zip - plaintext zip file that user can open with any archive manager + // 2. backup.zip/README.txt - text file to let user know what is inside of this file + // 3. backup.zip/metadata.json - json file with metadata about backup. + // 4. backup.zip/data.bin - v2 backup file + + final inputStream = InputFileStream(file.path); + final archive = ZipDecoder().decodeStream(inputStream); + final metadataFile = archive.findFile('metadata.json'); + if (metadataFile == null) { + throw Exception('Invalid v3 backup: missing metadata.json'); + } + final metadataBytes = metadataFile.rawContent!.readBytes(); + final metadataString = utf8.decode(metadataBytes); + final metadataJsonRaw = json.decode(metadataString) as Map; + final metadata = BackupMetadata.fromJson(metadataJsonRaw); + + final dataFile = archive.findFile('data.bin'); + if (dataFile == null) { + throw Exception('Invalid v3 backup: missing data.bin'); + } + final dataStream = dataFile.rawContent!.getStream(); + + final decryptedData = File('${file.path}_decrypted'); // decrypted zip file + if (decryptedData.existsSync()) { + decryptedData.deleteSync(); + } + decryptedData.createSync(recursive: true); + decryptedData.writeAsBytesSync(Uint8List(0), mode: FileMode.write, flush: true); + + int chunkIndex = 0; + for (var chunk in metadata.chunks) { + chunkIndex++; + final chunkBytes = dataStream.readBytes(chunk.length.encrypted).toUint8List(); + final chunkChecksum = (await sha512.bind(Stream.fromIterable([chunkBytes])).first).toString(); + + // readBytes stores position internally, so we don't need to think about it. + if (chunk.sha512sum.encrypted != chunkChecksum) { + throw Exception('Invalid v3 backup: chunk (${chunk.length.encrypted} bytes) checksum mismatch at index $chunkIndex\n' + 'expected: ${chunk.sha512sum.encrypted}\n' + 'got: $chunkChecksum'); + } + final decryptedChunk = await cake_backup.decrypt(password, chunkBytes); + decryptedData.writeAsBytesSync(decryptedChunk, mode: FileMode.append, flush: true); + } + + + final sha512sum = (await sha512.bind(decryptedData.openRead()).first).toString(); + if (sha512sum.toString() != metadata.sha512sum) { + throw Exception('Invalid v3 backup: SHA512 checksum mismatch\n' + 'expected: ${metadata.sha512sum}\n' + 'got: $sha512sum'); + } + + // Decryption done, now we can import the backup (that is, unzip app data) + + // archive is **NOT** backup, it is just a zip file that contains data.bin inside. + // We need to unzip it to get the backup. + // data.bin after decryption is available in decryptedData. + + final zip = ZipDecoder(); + final decryptedDataStream = InputFileStream(decryptedData.path); + final backupArchive = zip.decodeStream(decryptedDataStream); + + + final appDir = await getAppDir(); + + outer: + for (var file in backupArchive.files) { + final filename = file.name; + for (var ignore in $BackupService.ignoreFiles) { + if (filename.endsWith(ignore) && !filename.contains("wallets/")) { + printV("ignoring backup file: $filename"); + continue outer; + } + } + printV("restoring: $filename"); + if (file.isFile) { + final output = File('${appDir.path}/' + filename) + ..createSync(recursive: true); + final outputStream = OutputFileStream(output.path); + file.writeContent(outputStream); + outputStream.flush(); + } else { + final dir = Directory('${appDir.path}/' + filename); + if (!dir.existsSync()) { + dir.createSync(recursive: true); + } + } + }; + + // Continue importing the backup the old way + await super.verifyWallets(); + await super.importKeychainDumpV2(password); + await super.importPreferencesDump(); + await super.importTransactionDescriptionDump(); + + // Delete decrypted data file + decryptedData.deleteSync(); + } + + Future exportBackupFileV3(String password, {String nonce = secrets.backupSalt}) async { + final metadata = BackupMetadata( + version: BackupVersion.v3, + sha512sum: 'tbd', + chunks: [], + cakeVersion: 'tbd', + ); + final zipEncoder = ZipFileEncoder(); + final appDir = await getAppDir(); + final now = DateTime.now().toIso8601String().replaceAll(':', '-'); + final tmpDir = Directory('${appDir.path}/~_BACKUP_TMP'); + final archivePath = '${tmpDir.path}/backup_${now}.tmp.zip'; + final archivePathExport = '${tmpDir.path}/backup_${now}.zip'; + final fileEntities = appDir.listSync(recursive: false); + final keychainDump = await super.exportKeychainDumpV2(password); + final preferencesDump = await super.exportPreferencesJSON(); + final preferencesDumpFile = File('${tmpDir.path}/~_preferences_dump_TMP'); + final keychainDumpFile = File('${tmpDir.path}/~_keychain_dump_TMP'); + final transactionDescriptionDumpFile = + File('${tmpDir.path}/~_transaction_descriptions_dump_TMP'); + + final transactionDescriptionData = super.transactionDescriptionBox + .toMap() + .map((key, value) => MapEntry(key.toString(), value.toJson())); + final transactionDescriptionDump = jsonEncode(transactionDescriptionData); + + if (tmpDir.existsSync()) { + tmpDir.deleteSync(recursive: true); + } + + tmpDir.createSync(); + zipEncoder.create(archivePath); + outer: + for (var entity in fileEntities) { + if (entity.path == archivePath || entity.path == tmpDir.path) { + continue; + } + for (var ignore in $BackupService.ignoreFiles) { + final filename = entity.absolute.path; + if (filename.endsWith(ignore) && !filename.contains("wallets/")) { + printV("ignoring backup file: $filename"); + continue outer; + } + } + + if (entity.statSync().type == FileSystemEntityType.directory) { + await zipEncoder.addDirectory(Directory(entity.path)); + } else { + await zipEncoder.addFile(File(entity.path)); + } + } + await keychainDumpFile.writeAsBytes(keychainDump.toList()); + await preferencesDumpFile.writeAsString(preferencesDump); + await transactionDescriptionDumpFile.writeAsString(transactionDescriptionDump); + await zipEncoder.addFile(preferencesDumpFile, '~_preferences_dump'); + await zipEncoder.addFile(keychainDumpFile, '~_keychain_dump'); + await zipEncoder.addFile(transactionDescriptionDumpFile, '~_transaction_descriptions_dump'); + await zipEncoder.close(); + + final dataBinUnencrypted = File(archivePath); + + final dataBin = File('${tmpDir.path}/data.bin'); + dataBin.writeAsBytesSync(Uint8List(0), mode: FileMode.write, flush: true); + final dataBinWriter = dataBin.openWrite(); + + printV("------ Backup stats ------"); + printV("Backup version: ${metadata.version}"); + printV("Backup size: ${await dataBinUnencrypted.length()}"); + printV("Backup chunks: ${(await dataBinUnencrypted.length()) / chunkSize}"); + printV("------ Backup stats ------"); + + int chunkIndex = 0; + final stopwatch = Stopwatch()..start(); + printV("Starting backup encryption..."); + + metadata.sha512sum = (await sha512.bind(dataBinUnencrypted.openRead()).first).toString(); + + final raf = await dataBinUnencrypted.open(); + + + while (true) { + printV("Reading chunk ${chunkIndex++}"); + + stopwatch.reset(); + final chunk = await raf.read(chunkSize); + printV("Chunk read completed in ${stopwatch.elapsed}"); + printV("Chunk length: ${chunk.length} expected: $chunkSize"); + if (chunk.length == 0) { + break; + } + + stopwatch.reset(); + final encryptedChunk = await cake_backup.encrypt(password, chunk); + printV("Encryption completed in ${stopwatch.elapsed}"); + + stopwatch.reset(); + final sha512sumEncryptedChunk = await sha512.bind(Stream.fromIterable([encryptedChunk])).first; + final sha512sumUnencryptedChunk = await sha512.bind(Stream.fromIterable([chunk])).first; + printV("Hashing completed in ${stopwatch.elapsed}"); + + stopwatch.reset(); + dataBinWriter.add(encryptedChunk); + metadata.chunks.add(ChunkDetails( + sha512sum: ChunkChecksum( + encrypted: sha512sumEncryptedChunk.toString(), + plain: sha512sumUnencryptedChunk.toString(), + ), + length: ChunkLength( + encrypted: encryptedChunk.length, + plain: chunk.length, + ), + )); + + await dataBinWriter.flush(); + printV("Writing completed in ${stopwatch.elapsed}"); + } + await raf.close(); + + // Give the file to the user + + final metadataFile = File('${tmpDir.path}/metadata.json'); + final packageInfo = await PackageInfo.fromPlatform(); + metadata.cakeVersion = packageInfo.version; + + metadataFile.writeAsStringSync(JsonEncoder.withIndent(' ').convert(metadata.toJson())); + final readmeFile = File('${tmpDir.path}/README.txt'); + readmeFile.writeAsStringSync('''This is a ${packageInfo.appName} backup. Do not modify this archive. + +App version: ${packageInfo.version} + +If you have any issues with this backup, please contact our in-app support. +This backup was created on ${DateTime.now().toIso8601String()} +'''); + final zip = ZipFileEncoder(); + zip.create(archivePathExport, level: 9); + await zip.addFile(dataBin, 'data.bin'); + await zip.addFile(metadataFile, 'metadata.json'); + await zip.addFile(readmeFile, 'README.txt'); + await zip.close(); + // tmpDir.deleteSync(recursive: true); + final file = File(archivePathExport); + return file; + } + + static const chunkSize = 24 * 1024 * 1024; // 24MiB + + File setVersionFile(File file, BackupVersion version) { + if (version == BackupVersion.v3) return file; // v3 uses + // helper function to call super.setVersion(); + final data = file.readAsBytesSync(); + super.setVersion(data, version.index); + file.writeAsBytesSync(data); + return file; + } +} \ No newline at end of file diff --git a/lib/di.dart b/lib/di.dart index 8fb60fa29..b20b4062f 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -11,6 +11,7 @@ import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart'; import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; import 'package:cake_wallet/buy/order.dart'; +import 'package:cake_wallet/core/backup_service_v3.dart'; import 'package:cake_wallet/core/new_wallet_arguments.dart'; import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart'; import 'package:cake_wallet/core/auth_service.dart'; @@ -1215,12 +1216,12 @@ Future setup({ getIt.registerFactory(() => CakeFeaturesViewModel(getIt.get())); - getIt.registerFactory(() => BackupService(getIt.get(), _walletInfoSource, + getIt.registerFactory(() => BackupServiceV3(getIt.get(), _walletInfoSource, _transactionDescriptionBox, getIt.get(), getIt.get())); getIt.registerFactory(() => BackupViewModel( - getIt.get(), getIt.get(), getIt.get())); + getIt.get(), getIt.get(), getIt.get())); getIt.registerFactory(() => BackupPage(getIt.get())); @@ -1232,7 +1233,7 @@ Future setup({ getIt.registerFactoryParam( (bool isNewInstall, _) => RestoreOptionsPage(isNewInstall: isNewInstall)); - getIt.registerFactory(() => RestoreFromBackupViewModel(getIt.get())); + getIt.registerFactory(() => RestoreFromBackupViewModel(getIt.get())); getIt.registerFactory(() => RestoreFromBackupPage(getIt.get())); diff --git a/lib/locales/hausa_intl.dart b/lib/locales/hausa_intl.dart index 99e47d7da..ea302541f 100644 --- a/lib/locales/hausa_intl.dart +++ b/lib/locales/hausa_intl.dart @@ -825,7 +825,6 @@ class _HaCupertinoLocalizationsDelegate extends LocalizationsDelegate( HaCupertinoLocalizations( localeName: localeName, @@ -847,6 +846,7 @@ class _HaCupertinoLocalizationsDelegate extends LocalizationsDelegate content; + final File file; } class BackupViewModel = BackupViewModelBase with _$BackupViewModel; @@ -38,7 +39,7 @@ abstract class BackupViewModelBase with Store { final SecureStorage secureStorage; final SecretStore secretStore; - final BackupService backupService; + final BackupServiceV3 backupService; @observable ExecutionState state; @@ -59,14 +60,14 @@ abstract class BackupViewModelBase with Store { Future exportBackup() async { try { state = IsExecutingState(); - final backupContent = await backupService.exportBackup(backupPassword); + final backupFile = await backupService.exportBackupFile(backupPassword); state = ExecutedSuccessfullyState(); final now = DateTime.now(); final formatter = DateFormat('yyyy-MM-dd_Hm'); final snakeAppName = approximatedAppName.replaceAll(' ', '_').toLowerCase(); - final fileName = '${snakeAppName}_backup_${formatter.format(now)}'; + final fileName = '${snakeAppName}_backup_${formatter.format(now)}.zip'; - return BackupExportFile(backupContent.toList(), name: fileName); + return BackupExportFile(backupFile, name: fileName); } catch (e) { printV(e.toString()); state = FailureState(e.toString()); @@ -77,26 +78,35 @@ abstract class BackupViewModelBase with Store { Future saveBackupFileLocally(BackupExportFile backup) async { final appDir = await getAppDir(); final path = '${appDir.path}/${backup.name}'; - final backupFile = File(path); - await backupFile.writeAsBytes(backup.content); + if (File(path).existsSync()) { + File(path).deleteSync(); + } + await backup.file.copy(path); return path; } Future removeBackupFileLocally(BackupExportFile backup) async { final appDir = await getAppDir(); final path = '${appDir.path}/${backup.name}'; - final backupFile = File(path); - await backupFile.delete(); + if (File(path).existsSync()) { + File(path).deleteSync(); + } } @action void showMasterPassword() => isBackupPasswordVisible = true; @action - Future saveToDownload(String name, List content) async { + Future saveToDownload(String name, File file) async { + if (!Platform.isAndroid) { + return; + } const downloadDirPath = '/storage/emulated/0/Download'; // For Android final filePath = '$downloadDirPath/${name}'; - final file = File(filePath); - await file.writeAsBytes(content); + final downloadFile = File(filePath); + if (downloadFile.existsSync()) { + downloadFile.deleteSync(); + } + await file.copy(filePath); } } diff --git a/lib/view_model/restore_from_backup_view_model.dart b/lib/view_model/restore_from_backup_view_model.dart index f9894b592..1c3dc5048 100644 --- a/lib/view_model/restore_from_backup_view_model.dart +++ b/lib/view_model/restore_from_backup_view_model.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:cake_wallet/core/backup_service_v3.dart'; import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; import 'package:flutter/material.dart'; @@ -20,7 +21,7 @@ abstract class RestoreFromBackupViewModelBase with Store { : state = InitialExecutionState(), filePath = ''; - final BackupService backupService; + final BackupServiceV3 backupService; @observable String filePath; @@ -42,10 +43,9 @@ abstract class RestoreFromBackupViewModelBase with Store { } final file = File(filePath); - final data = await file.readAsBytes(); - await backupService.importBackup(data, password); + await backupService.importBackupFile(file, password); try { await initializeAppAtRoot(reInitializing: true); diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 8bf2de274..a4a23ed36 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -45,7 +45,7 @@ dependencies: url: https://github.com/MrCyjaneK/uni_links ref: 8e9efa4d9beb19e4ac44009576337f1ce51c22e2 path: uni_links - lottie: ^1.3.0 + lottie: ^3.3.1 animate_do: ^2.1.0 cupertino_icons: ^1.0.5 encrypt: 5.0.2 @@ -57,13 +57,13 @@ dependencies: connectivity_plus: ^5.0.2 keyboard_actions: ^4.0.1 another_flushbar: ^1.12.29 - archive: ^3.3.0 + archive: ^4.0.3 cryptography: ^2.0.5 file_picker: git: url: https://github.com/cake-tech/flutter_file_picker.git - ref: master - permission_handler: ^10.0.0 + ref: c414574bc5ac349450f601e7f72c7b9f31b4d087 + permission_handler: 11.4.0 device_display_brightness: git: url: https://github.com/MrCyjaneK/device_display_brightness.git @@ -72,7 +72,7 @@ dependencies: flutter_mailer: git: url: https://github.com/taljacobson/flutter_mailer - ref: 2a7d04d61f56e1ca166ab42e91e0daf1bfddfaf2 + ref: 9c4ed111a9151a2bbfb9afe2c18a37599c6f84f3 device_info_plus: ^9.1.0 base32: 2.1.3 in_app_review: ^2.0.6 @@ -96,7 +96,7 @@ dependencies: git: url: https://github.com/cake-tech/ens_dart.git ref: main - fluttertoast: 8.2.8 + fluttertoast: 8.2.12 # tor: # git: # url: https://github.com/cake-tech/tor.git @@ -142,7 +142,6 @@ dev_dependencies: git: url: https://github.com/cake-tech/google-translator.git version: 1.0.0 - archive: ^3.6.1 dependency_overrides: bech32: diff --git a/scripts/linux/.fvmrc b/scripts/linux/.fvmrc index 262e5e837..c2783c697 100644 --- a/scripts/linux/.fvmrc +++ b/scripts/linux/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.24.0" + "flutter": "3.27.4" } \ No newline at end of file diff --git a/scripts/windows/Dockerfile.windows b/scripts/windows/Dockerfile.windows index f2a08b41c..893009b8f 100644 --- a/scripts/windows/Dockerfile.windows +++ b/scripts/windows/Dockerfile.windows @@ -4,7 +4,7 @@ FROM mcr.microsoft.com/windows/servercore:ltsc2022 -ENV FLUTTER_VERSION=3.24.0 +ENV FLUTTER_VERSION=3.27.4 ENV GIT_VERSION=2.47.1 ENV VS_INSTALLED_DIR="C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools" ENV PATH="C:\Users\ContainerAdministrator\.cargo\bin;C:\ProgramData\chocolatey\bin;C:\flutter\flutter\bin;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Users\ContainerAdministrator\AppData\Local\Microsoft\WindowsApps" diff --git a/tool/download_moneroc_prebuilds.dart b/tool/download_moneroc_prebuilds.dart index 8909ca8a8..378d9293b 100644 --- a/tool/download_moneroc_prebuilds.dart +++ b/tool/download_moneroc_prebuilds.dart @@ -47,7 +47,7 @@ Future main() async { if (localFilename.endsWith(".xz")) { printV(" extracting $localFilename"); final inputStream = InputFileStream(localFilename); - final archive = XZDecoder().decodeBuffer(inputStream); + final archive = XZDecoder().decodeBytes(inputStream.toUint8List()); final outputStream = OutputFileStream(localFilename.replaceAll(".xz", "")); outputStream.writeBytes(archive); } From 13dc6de8dd697a2dd2f458917326f7a3885edca1 Mon Sep 17 00:00:00 2001 From: Serhii Date: Mon, 24 Mar 2025 22:11:20 +0200 Subject: [PATCH 057/243] fix-colors-for-address-tile-send-UI (#2113) * Update confirm_sending_bottom_sheet_widget.dart * Update standard_slide_button_widget.dart --- .../confirm_sending_bottom_sheet_widget.dart | 32 +++++++++++++++---- .../widgets/standard_slide_button_widget.dart | 15 ++++++--- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart index 84997b34e..a5bdafc7c 100644 --- a/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart +++ b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart @@ -3,6 +3,7 @@ import 'package:cake_wallet/src/widgets/standard_slide_button_widget.dart'; import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/filter_theme.dart'; +import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -71,6 +72,12 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { decoration: TextDecoration.none, ); + final tileBackgroundColor = currentTheme.type == ThemeType.light + ? Theme.of(context).extension()!.syncedBackgroundColor + : currentTheme.type == ThemeType.oled + ? Colors.black.withOpacity(0.5) + : Theme.of(context).extension()!.buttonColor; + Widget content = Padding( padding: EdgeInsets.fromLTRB(8, 0, showScrollbar ? 16 : 8, 8), child: Column( @@ -86,6 +93,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { amount: '', address: paymentIdValue!, itemSubTitleTextStyle: itemSubTitleTextStyle, + tileBackgroundColor: tileBackgroundColor, ), ), StandardTile( @@ -94,6 +102,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { itemTitleTextStyle: itemTitleTextStyle, itemSubTitle: fiatAmountValue, itemSubTitleTextStyle: itemSubTitleTextStyle, + tileBackgroundColor: tileBackgroundColor, ), const SizedBox(height: 8), StandardTile( @@ -102,6 +111,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { itemTitleTextStyle: itemTitleTextStyle, itemSubTitle: feeFiatAmount, itemSubTitleTextStyle: itemSubTitleTextStyle, + tileBackgroundColor: tileBackgroundColor, ), const SizedBox(height: 8), Column( @@ -130,6 +140,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { isBatchSending: isBatchSending, itemTitleTextStyle: itemTitleTextStyle, itemSubTitleTextStyle: itemSubTitleTextStyle, + tileBackgroundColor: tileBackgroundColor, ) : AddressTile( itemTitle: 'Address', @@ -139,6 +150,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { amount: _amount, address: _address, itemSubTitleTextStyle: itemSubTitleTextStyle, + tileBackgroundColor: tileBackgroundColor, ); }, ), @@ -154,6 +166,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { isBatchSending: true, itemTitleTextStyle: itemTitleTextStyle, itemSubTitleTextStyle: itemSubTitleTextStyle, + tileBackgroundColor: tileBackgroundColor, ), ), ], @@ -214,6 +227,7 @@ class StandardTile extends StatelessWidget { required this.itemTitleTextStyle, this.itemSubTitle, required this.itemSubTitleTextStyle, + required this.tileBackgroundColor, }); final String itemTitle; @@ -221,14 +235,14 @@ class StandardTile extends StatelessWidget { final TextStyle itemTitleTextStyle; final String? itemSubTitle; final TextStyle itemSubTitleTextStyle; + final Color tileBackgroundColor; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Theme.of(context).extension()!.buttonColor), + decoration: + BoxDecoration(borderRadius: BorderRadius.circular(10), color: tileBackgroundColor), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -258,6 +272,7 @@ class AddressTile extends StatelessWidget { required this.amount, required this.address, required this.itemSubTitleTextStyle, + required this.tileBackgroundColor, }); final String itemTitle; @@ -267,6 +282,7 @@ class AddressTile extends StatelessWidget { final String amount; final String address; final TextStyle itemSubTitleTextStyle; + final Color tileBackgroundColor; @override Widget build(BuildContext context) { @@ -283,7 +299,7 @@ class AddressTile extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), - color: Theme.of(context).extension()!.buttonColor, + color: tileBackgroundColor, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -343,6 +359,7 @@ class AddressExpansionTile extends StatelessWidget { required this.isBatchSending, required this.itemTitleTextStyle, required this.itemSubTitleTextStyle, + required this.tileBackgroundColor, }); final String contactType; @@ -353,6 +370,7 @@ class AddressExpansionTile extends StatelessWidget { final bool isBatchSending; final TextStyle itemTitleTextStyle; final TextStyle itemSubTitleTextStyle; + final Color tileBackgroundColor; @override Widget build(BuildContext context) { @@ -369,7 +387,7 @@ class AddressExpansionTile extends StatelessWidget { return Container( decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(10)), - color: Theme.of(context).extension()!.buttonColor, + color: tileBackgroundColor, ), child: Theme( data: Theme.of(context).copyWith(dividerColor: Colors.transparent), @@ -383,7 +401,9 @@ class AddressExpansionTile extends StatelessWidget { title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded(child: Text(isBatchSending ? name : contactType, style: itemTitleTextStyle, softWrap: true)), + Expanded( + child: Text(isBatchSending ? name : contactType, + style: itemTitleTextStyle, softWrap: true)), Text(isBatchSending ? amount : name, style: TextStyle( fontSize: 14, diff --git a/lib/src/widgets/standard_slide_button_widget.dart b/lib/src/widgets/standard_slide_button_widget.dart index e777155aa..57271b6b5 100644 --- a/lib/src/widgets/standard_slide_button_widget.dart +++ b/lib/src/widgets/standard_slide_button_widget.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/filter_theme.dart'; import 'package:cake_wallet/themes/extensions/menu_theme.dart'; +import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:flutter/material.dart'; @@ -33,13 +34,17 @@ class _StandardSlideButtonState extends State { final double effectiveMaxWidth = maxWidth - 2 * sideMargin; const double sliderWidth = 42.0; + final tileBackgroundColor = widget.currentTheme.type == ThemeType.light + ? Theme.of(context).extension()!.syncedBackgroundColor + : widget.currentTheme.type == ThemeType.oled + ? Colors.black.withOpacity(0.5) + : Theme.of(context).extension()!.buttonColor; + return Container( height: widget.height, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), - color: widget.currentTheme.type == ThemeType.light || widget.currentTheme.type == ThemeType.bright - ? Theme.of(context).disabledColor - : widget.currentTheme.type == ThemeType.oled ? Colors.black : Theme.of(context).extension()!.backgroundColor), + color: tileBackgroundColor), child: Stack( alignment: Alignment.centerLeft, children: [ @@ -74,11 +79,11 @@ class _StandardSlideButtonState extends State { height: widget.height - 8, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), - color: widget.currentTheme.type == ThemeType.bright ? Theme.of(context).extension()!.backgroundColor : Theme.of(context).extension()!.buttonColor, + color: Theme.of(context).extension()!.titleColor, ), alignment: Alignment.center, child: Icon(Icons.arrow_forward, - color: Theme.of(context).extension()!.titleColor), + color: widget.currentTheme.type == ThemeType.bright ? Theme.of(context).extension()!.backgroundColor : Theme.of(context).extension()!.buttonColor), ), ), ) From 0aa563ead734f9ad7182e841a11ffa4bee1436d2 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Tue, 25 Mar 2025 03:49:54 +0200 Subject: [PATCH 058/243] v4.24.0 (#2102) * - Add Decred Wallet - Remove Haven Wallet - Fix and Improve Solana Wallet - Improve app usability and user experience - User interface enhancements - Bug fixes * Release candidate feedback fixes * Release candidate feedback fixes * update release notes [skip ci] * fix iOS stupidity [skip ci] * minor fix * decred: Do not create log file. (#2106) (#2107) Co-authored-by: JoeGruffins <34998433+JoeGruffins@users.noreply.github.com> * fix Monero HWW * fix Monero view only wallet * fix background sync for hardware and viewonly wallets * check for status on correct wptr * minor fixes [skip ci] --------- Co-authored-by: JoeGruffins <34998433+JoeGruffins@users.noreply.github.com> Co-authored-by: Czarek Nakamoto --- assets/text/Monerocom_Release_Notes.txt | 6 +- assets/text/Release_Notes.txt | 9 +- cw_bitcoin/pubspec.lock | 26 ++--- cw_core/pubspec.lock | 50 +++++----- cw_decred/lib/api/libdcrwallet.dart | 20 ++-- cw_decred/lib/wallet.dart | 95 ++++++++++--------- cw_decred/lib/wallet_addresses.dart | 20 ++-- cw_decred/lib/wallet_service.dart | 9 +- cw_decred/pubspec.lock | 18 ++-- cw_monero/lib/api/wallet_manager.dart | 65 ++++--------- cw_monero/lib/monero_wallet.dart | 12 +-- cw_monero/pubspec.lock | 26 ++--- cw_nano/pubspec.lock | 26 ++--- cw_wownero/pubspec.lock | 26 ++--- cw_zano/pubspec.lock | 26 ++--- ios/Podfile.lock | 70 +++++++------- lib/monero/cw_monero.dart | 2 +- .../pages/balance/crypto_balance_widget.dart | 2 +- lib/src/screens/send/send_page.dart | 4 +- lib/src/screens/settings/privacy_page.dart | 2 +- .../dashboard/dashboard_view_model.dart | 5 +- .../node_create_or_edit_view_model.dart | 1 - macos/Podfile.lock | 8 -- 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_fr.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_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 + scripts/android/app_env.sh | 8 +- scripts/android/build_decred.sh | 2 +- scripts/ios/app_env.sh | 8 +- scripts/ios/build_decred.sh | 2 +- scripts/linux/app_env.sh | 4 +- scripts/macos/app_config.sh | 2 +- scripts/macos/app_env.sh | 8 +- scripts/macos/build_decred.sh | 2 +- scripts/windows/build_exe_installer.iss | 2 +- 60 files changed, 304 insertions(+), 290 deletions(-) diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index d1f91139b..5b2a9f873 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,3 +1,5 @@ -UI/UX enhancements -Stability improvements +Add background sync to Monero +Enhance Backup files +Improve app usability and user experience +User interface enhancements Bug fixes \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index 0f8118b4e..1a5986bd4 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1 +1,8 @@ -Update for Zano's Hard fork \ No newline at end of file +Add background sync to Monero +Add Decred Wallet +Remove Haven Wallet +Fix and Improve Solana Wallet +Enhance Backup files +Improve app usability and user experience +User interface enhancements +Bug fixes \ No newline at end of file diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 1ad7b0c60..09870224c 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: "1c296cd268f486cabcc3930e9b93a8133169305f18d722916e675959a88f6d2c" + sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" url: "https://pub.dev" source: hosted - version: "1.5.9" + version: "1.5.8" async: dependency: transitive description: @@ -129,18 +129,18 @@ packages: dependency: transitive description: name: build_config - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon - sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.4" + version: "4.0.2" build_resolvers: dependency: "direct dev" description: @@ -495,10 +495,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "4.1.2" + version: "4.0.2" intl: dependency: "direct main" description: @@ -794,10 +794,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.4.0" quiver: dependency: transitive description: @@ -882,10 +882,10 @@ packages: dependency: transitive description: name: shelf - sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.4.2" + version: "1.4.1" shelf_web_socket: dependency: transitive description: @@ -1117,5 +1117,5 @@ packages: source: hosted version: "2.2.2" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.5.0 <4.0.0" flutter: ">=3.24.0" diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index 4ba8df5c5..36bba72b6 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -34,10 +34,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: "1c296cd268f486cabcc3930e9b93a8133169305f18d722916e675959a88f6d2c" + sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" url: "https://pub.dev" source: hosted - version: "1.5.9" + version: "1.5.8" async: dependency: transitive description: @@ -67,50 +67,50 @@ packages: dependency: transitive description: name: build - sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.1" build_config: dependency: transitive description: name: build_config - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon - sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.4" + version: "4.0.2" build_resolvers: dependency: "direct dev" description: name: build_resolvers - sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.4.15" + version: "2.4.13" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "7.3.2" built_collection: dependency: transitive description: @@ -212,10 +212,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" url: "https://pub.dev" source: hosted - version: "2.3.8" + version: "2.3.7" decimal: dependency: "direct main" description: @@ -342,10 +342,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "4.1.2" + version: "4.0.2" intl: dependency: "direct main" description: @@ -599,10 +599,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.4.0" rational: dependency: transitive description: @@ -615,18 +615,18 @@ packages: dependency: transitive description: name: shelf - sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.4.2" + version: "1.4.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "2.0.1" sky_engine: dependency: transitive description: flutter @@ -809,5 +809,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.5.0 <4.0.0" flutter: ">=3.24.0" diff --git a/cw_decred/lib/api/libdcrwallet.dart b/cw_decred/lib/api/libdcrwallet.dart index 6a26e64c6..b5e5bbe73 100644 --- a/cw_decred/lib/api/libdcrwallet.dart +++ b/cw_decred/lib/api/libdcrwallet.dart @@ -3,7 +3,6 @@ import 'dart:ffi'; import 'dart:io'; import 'dart:async'; import 'dart:isolate'; -import 'package:flutter/foundation.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_decred/api/libdcrwallet_bindings.dart'; import 'package:cw_decred/api/util.dart'; @@ -79,10 +78,12 @@ class Libwallet { switch (method) { case "initlibdcrwallet": final logDir = args["logdir"] ?? ""; + final level = args["level"] ?? ""; final cLogDir = logDir.toCString(); + final cLevel = level.toCString(); executePayloadFn( - fn: () => dcrwalletApi.initialize(cLogDir), - ptrsToFree: [cLogDir], + fn: () => dcrwalletApi.initialize(cLogDir, cLevel), + ptrsToFree: [cLogDir, cLevel], ); break; case "createwallet": @@ -300,7 +301,7 @@ class Libwallet { break; case "shutdown": final name = args["name"] ?? ""; - final cName = name.toCString(); + // final cName = name.toCString(); executePayloadFn( fn: () => dcrwalletApi.shutdown(), ptrsToFree: [], @@ -326,8 +327,8 @@ class Libwallet { // initLibdcrwallet initializes libdcrwallet using the provided logDir and gets // it ready for use. This must be done before attempting to create, load or use - // a wallet. - Future initLibdcrwallet(String logDir) async { + // a wallet. An empty string can be used to log to stdout and create no log files. + Future initLibdcrwallet(String logDir, String level) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; @@ -335,6 +336,7 @@ class Libwallet { final req = { "method": "initlibdcrwallet", "logdir": logDir, + "level": level, }; _commands.send((id, req)); await completer.future; @@ -463,7 +465,11 @@ class Libwallet { }; _commands.send((id, req)); final res = await completer.future as PayloadResult; - return jsonDecode(res.payload); + try { + return jsonDecode(res.payload); + } catch (_) { + return {}; + } } Future estimateFee(String walletName, int numBlocks) async { diff --git a/cw_decred/lib/wallet.dart b/cw_decred/lib/wallet.dart index 028ba6cda..db30ab373 100644 --- a/cw_decred/lib/wallet.dart +++ b/cw_decred/lib/wallet.dart @@ -380,7 +380,7 @@ abstract class DecredWalletBase totalAmt = totalIn; } else if (out.cryptoAmount != null) { final coins = double.parse(out.cryptoAmount!); - amt = (coins * 1e8).toInt(); + amt = (coins * 1e8).round(); } totalAmt += amt; final o = { @@ -415,7 +415,7 @@ abstract class DecredWalletBase }; final fee = decoded["fee"] ?? 0; if (sendAll) { - totalAmt = (totalAmt - fee).toInt(); + totalAmt = (totalAmt - fee).round(); } return DecredPendingTransaction( txid: decoded["txid"] ?? "", amount: totalAmt, fee: fee, rawHex: signedHex, send: send); @@ -475,36 +475,41 @@ abstract class DecredWalletBase } Future> fetchFiveTransactions(int from) async { - final res = await _libwallet.listTransactions(walletInfo.name, from.toString(), "5"); - final decoded = json.decode(res); - var txs = {}; - for (final d in decoded) { - final txid = uniqueTxID(d["txid"] ?? "", d["vout"] ?? 0); - var direction = TransactionDirection.outgoing; - if (d["category"] == "receive") { - direction = TransactionDirection.incoming; + try { + final res = await _libwallet.listTransactions(walletInfo.name, from.toString(), "5"); + final decoded = json.decode(res); + var txs = {}; + for (final d in decoded) { + final txid = uniqueTxID(d["txid"] ?? "", d["vout"] ?? 0); + var direction = TransactionDirection.outgoing; + if (d["category"] == "receive") { + direction = TransactionDirection.incoming; + } + final amountDouble = d["amount"] ?? 0.0; + final amount = (amountDouble * 1e8).round().abs(); + final feeDouble = d["fee"] ?? 0.0; + final fee = (feeDouble * 1e8).round().abs(); + final confs = d["confirmations"] ?? 0; + final sendTime = d["time"] ?? 0; + final height = d["height"] ?? 0; + final txInfo = DecredTransactionInfo( + id: txid, + amount: amount, + fee: fee, + direction: direction, + isPending: confs == 0, + date: DateTime.fromMillisecondsSinceEpoch(sendTime * 1000, isUtc: false), + height: height, + confirmations: confs, + to: d["address"] ?? "", + ); + txs[txid] = txInfo; } - final amountDouble = d["amount"] ?? 0.0; - final amount = (amountDouble * 1e8).toInt().abs(); - final feeDouble = d["fee"] ?? 0.0; - final fee = (feeDouble * 1e8).toInt().abs(); - final confs = d["confirmations"] ?? 0; - final sendTime = d["time"] ?? 0; - final height = d["height"] ?? 0; - final txInfo = DecredTransactionInfo( - id: txid, - amount: amount, - fee: fee, - direction: direction, - isPending: confs == 0, - date: DateTime.fromMillisecondsSinceEpoch(sendTime * 1000, isUtc: false), - height: height, - confirmations: confs, - to: d["address"] ?? "", - ); - txs[txid] = txInfo; + return txs; + } catch (e) { + printV(e); + return {}; } - return txs; } // uniqueTxID combines the tx id and vout to create a unique id. @@ -612,21 +617,25 @@ abstract class DecredWalletBase } Future fetchUnspents() async { - final res = await _libwallet.listUnspents(walletInfo.name); - final decoded = json.decode(res); - var unspents = []; - for (final d in decoded) { - final spendable = d["spendable"] ?? false; - if (!spendable) { - continue; + try { + final res = await _libwallet.listUnspents(walletInfo.name); + final decoded = json.decode(res); + var unspents = []; + for (final d in decoded) { + final spendable = d["spendable"] ?? false; + if (!spendable) { + continue; + } + final amountDouble = d["amount"] ?? 0.0; + final amount = (amountDouble * 1e8).round().abs(); + final utxo = Unspent(d["address"] ?? "", d["txid"] ?? "", amount, d["vout"] ?? 0, null); + utxo.isChange = d["ischange"] ?? false; + unspents.add(utxo); } - final amountDouble = d["amount"] ?? 0.0; - final amount = (amountDouble * 1e8).toInt().abs(); - final utxo = Unspent(d["address"] ?? "", d["txid"] ?? "", amount, d["vout"] ?? 0, null); - utxo.isChange = d["ischange"] ?? false; - unspents.add(utxo); + _unspents = unspents; + } catch (e) { + printV(e); } - _unspents = unspents; } List unspents() { diff --git a/cw_decred/lib/wallet_addresses.dart b/cw_decred/lib/wallet_addresses.dart index 10970b2d6..860a576d9 100644 --- a/cw_decred/lib/wallet_addresses.dart +++ b/cw_decred/lib/wallet_addresses.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/address_info.dart'; @@ -103,13 +104,18 @@ abstract class DecredWalletAddressesBase extends WalletAddresses with Store { if (this.isEnabledAutoGenerateSubaddress) { nUnused = "3"; } - final res = await _libwallet.addresses(walletInfo.name, nUsed, nUnused); - final decoded = json.decode(res); - final usedAddrs = List.from(decoded["used"] ?? []); - final unusedAddrs = List.from(decoded["unused"] ?? []); - // index is the index of the first unused address. - final index = decoded["index"] ?? 0; - return new LibAddresses(usedAddrs, unusedAddrs, index); + try { + final res = await _libwallet.addresses(walletInfo.name, nUsed, nUnused); + final decoded = json.decode(res); + final usedAddrs = List.from(decoded["used"] ?? []); + final unusedAddrs = List.from(decoded["unused"] ?? []); + // index is the index of the first unused address. + final index = decoded["index"] ?? 0; + return new LibAddresses(usedAddrs, unusedAddrs, index); + } catch (e) { + printV(e); + return LibAddresses([], [], 0); + } } Future generateNewAddress(String label) async { diff --git a/cw_decred/lib/wallet_service.dart b/cw_decred/lib/wallet_service.dart index a54833321..161184b0a 100644 --- a/cw_decred/lib/wallet_service.dart +++ b/cw_decred/lib/wallet_service.dart @@ -27,17 +27,16 @@ class DecredWalletService extends WalletService< static final pubkeyRestorePathTestnet = "m/44'/1'/0'"; final mainnet = "mainnet"; final testnet = "testnet"; - Libwallet? libwallet; + static Libwallet? libwallet; Future init() async { if (libwallet != null) { return; } libwallet = await Libwallet.spawn(); - // Use the general path for all dcr wallets as the general log directory. - // Individual wallet paths may be removed if the wallet is deleted. - final dcrLogDir = await pathForWalletDir(name: '', type: WalletType.decred); - libwallet!.initLibdcrwallet(dcrLogDir); + // Init logging with no directory to force printing to stdout and only + // print ERROR level logs. + libwallet!.initLibdcrwallet("", "err"); } void closeLibwallet() { diff --git a/cw_decred/pubspec.lock b/cw_decred/pubspec.lock index 2324e7bd2..f9954fe53 100644 --- a/cw_decred/pubspec.lock +++ b/cw_decred/pubspec.lock @@ -75,18 +75,18 @@ packages: dependency: transitive description: name: build_config - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon - sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.4" + version: "4.0.2" build_resolvers: dependency: "direct dev" description: @@ -365,10 +365,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "4.1.2" + version: "4.0.2" intl: dependency: transitive description: @@ -646,10 +646,10 @@ packages: dependency: transitive description: name: shelf - sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.4.2" + version: "1.4.1" shelf_web_socket: dependency: transitive description: @@ -848,5 +848,5 @@ packages: source: hosted version: "2.2.2" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.5.0 <4.0.0" flutter: ">=3.24.0" diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart index bfebe4247..0dcb3c851 100644 --- a/cw_monero/lib/api/wallet_manager.dart +++ b/cw_monero/lib/api/wallet_manager.dart @@ -89,11 +89,7 @@ void createWalletSync( throw WalletCreationException(message: monero.Wallet_errorString(newWptr)); } - monero.Wallet_setupBackgroundSync(newWptr, backgroundSyncType: 2, walletPassword: password, backgroundCachePassword: ''); - status = monero.Wallet_status(newWptr); - if (status != 0) { - throw WalletCreationException(message: monero.Wallet_errorString(newWptr)); - } + setupBackgroundSync(password, newWptr); wptr = newWptr; monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase); @@ -186,13 +182,6 @@ void restoreWalletFromKeysSync( message: monero.Wallet_errorString(newWptr)); } - - monero.Wallet_setupBackgroundSync(newWptr, backgroundSyncType: 2, walletPassword: password, backgroundCachePassword: ''); - status = monero.Wallet_status(newWptr); - if (status != 0) { - throw WalletCreationException(message: monero.Wallet_errorString(newWptr)); - } - // CW-712 - Try to restore deterministic wallet first, if the view key doesn't // match the view key provided if (spendKey != "") { @@ -216,12 +205,8 @@ void restoreWalletFromKeysSync( throw WalletRestoreFromKeysException( message: monero.Wallet_errorString(newWptr)); } - - monero.Wallet_setupBackgroundSync(newWptr, backgroundSyncType: 2, walletPassword: password, backgroundCachePassword: ''); - status = monero.Wallet_status(newWptr); - if (status != 0) { - throw WalletCreationException(message: monero.Wallet_errorString(newWptr)); - } + + setupBackgroundSync(password, newWptr); } } @@ -267,12 +252,8 @@ void restoreWalletFromPolyseedWithOffset( monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: seedOffset); monero.Wallet_store(wptr!); - - monero.Wallet_setupBackgroundSync(newWptr, backgroundSyncType: 2, walletPassword: password, backgroundCachePassword: ''); - status = monero.Wallet_status(newWptr); - if (status != 0) { - throw WalletCreationException(message: monero.Wallet_errorString(newWptr)); - } + + setupBackgroundSync(password, newWptr); storeSync(); openedWalletsByPath[path] = wptr!; @@ -323,12 +304,8 @@ void restoreWalletFromSpendKeySync( monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); storeSync(); - - monero.Wallet_setupBackgroundSync(newWptr, backgroundSyncType: 2, walletPassword: password, backgroundCachePassword: ''); - status = monero.Wallet_status(newWptr); - if (status != 0) { - throw WalletCreationException(message: monero.Wallet_errorString(newWptr)); - } + + setupBackgroundSync(password, newWptr); openedWalletsByPath[path] = wptr!; _lastOpenedWallet = path; @@ -361,13 +338,6 @@ Future restoreWalletFromHardwareWallet( throw WalletRestoreFromSeedException(message: error); } - // TODO: Check with upstream if we can use background sync here - // monero.Wallet_setupBackgroundSync(newWptr, backgroundSyncType: 2, walletPassword: password, backgroundCachePassword: ''); - // status = monero.Wallet_status(newWptr); - // if (status != 0) { - // throw WalletCreationException(message: monero.Wallet_errorString(newWptr)); - // } - wptr = newWptr; _lastOpenedWallet = path; openedWalletsByPath[path] = wptr!; @@ -437,12 +407,8 @@ Future loadWallet( printV("loadWallet:"+err); throw WalletOpeningException(message: err); } - monero.Wallet_setupBackgroundSync(newWptr, backgroundSyncType: 2, walletPassword: password, backgroundCachePassword: ''); - status = monero.Wallet_status(newWptr); - if (status != 0) { - final err = monero.Wallet_errorString(newWptr); - printV("loadWallet:"+err); - throw WalletOpeningException(message: err); + if (deviceType == 0) { + setupBackgroundSync(password, newWptr); } wptr = newWptr; @@ -451,6 +417,17 @@ Future loadWallet( } } +void setupBackgroundSync(String password, Pointer? wptrOverride) { + if (isViewOnlyBySpendKey(wptrOverride)) { + return; + } + monero.Wallet_setupBackgroundSync(wptrOverride ?? wptr!, backgroundSyncType: 2, walletPassword: password, backgroundCachePassword: ''); + if (monero.Wallet_status(wptrOverride ?? wptr!) != 0) { + // We simply ignore the error. + printV("setupBackgroundSync: ${monero.Wallet_errorString(wptrOverride ?? wptr!)}"); + } +} + void _createWallet(Map args) { final path = args['path'] as String; final password = args['password'] as String; @@ -591,4 +568,4 @@ Future restoreFromSpendKey( bool isWalletExist({required String path}) => _isWalletExist(path); -bool isViewOnlyBySpendKey() => int.tryParse(monero.Wallet_secretSpendKey(wptr!)) == 0; +bool isViewOnlyBySpendKey(Pointer? wptrOverride) => int.tryParse(monero.Wallet_secretSpendKey(wptrOverride ?? wptr!)) == 0; diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index db4d30ee8..5637f82e5 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -233,18 +233,14 @@ abstract class MoneroWalletBase extends WalletBase=3.6.0 <4.0.0" + dart: ">=3.5.0 <4.0.0" flutter: ">=3.24.0" diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock index 0891588f2..ca8b61974 100644 --- a/cw_nano/pubspec.lock +++ b/cw_nano/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: "1c296cd268f486cabcc3930e9b93a8133169305f18d722916e675959a88f6d2c" + sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" url: "https://pub.dev" source: hosted - version: "1.5.9" + version: "1.5.8" async: dependency: transitive description: @@ -94,18 +94,18 @@ packages: dependency: transitive description: name: build_config - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon - sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.4" + version: "4.0.2" build_resolvers: dependency: transitive description: @@ -397,10 +397,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "4.1.2" + version: "4.0.2" intl: dependency: transitive description: @@ -679,10 +679,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.4.0" rational: dependency: transitive description: @@ -751,10 +751,10 @@ packages: dependency: transitive description: name: shelf - sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.4.2" + version: "1.4.1" shelf_web_socket: dependency: transitive description: @@ -945,5 +945,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.5.0 <4.0.0" flutter: ">=3.24.0" diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index 6d4493874..404060e43 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: "1c296cd268f486cabcc3930e9b93a8133169305f18d722916e675959a88f6d2c" + sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" url: "https://pub.dev" source: hosted - version: "1.5.9" + version: "1.5.8" async: dependency: transitive description: @@ -70,18 +70,18 @@ packages: dependency: transitive description: name: build_config - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon - sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.4" + version: "4.0.2" build_resolvers: dependency: "direct dev" description: @@ -360,10 +360,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "4.1.2" + version: "4.0.2" intl: dependency: "direct main" description: @@ -634,10 +634,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.4.0" rational: dependency: transitive description: @@ -650,10 +650,10 @@ packages: dependency: transitive description: name: shelf - sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.4.2" + version: "1.4.1" shelf_web_socket: dependency: transitive description: @@ -844,5 +844,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.5.0 <4.0.0" flutter: ">=3.24.0" diff --git a/cw_zano/pubspec.lock b/cw_zano/pubspec.lock index 74ec7206f..50835ed3f 100644 --- a/cw_zano/pubspec.lock +++ b/cw_zano/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: "1c296cd268f486cabcc3930e9b93a8133169305f18d722916e675959a88f6d2c" + sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" url: "https://pub.dev" source: hosted - version: "1.5.9" + version: "1.5.8" async: dependency: transitive description: @@ -70,18 +70,18 @@ packages: dependency: transitive description: name: build_config - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon - sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.4" + version: "4.0.2" build_resolvers: dependency: "direct dev" description: @@ -357,10 +357,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "4.1.2" + version: "4.0.2" intl: dependency: "direct main" description: @@ -631,10 +631,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.4.0" rational: dependency: transitive description: @@ -647,10 +647,10 @@ packages: dependency: transitive description: name: shelf - sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.4.2" + version: "1.4.1" shelf_web_socket: dependency: transitive description: @@ -841,5 +841,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.5.0 <4.0.0" flutter: ">=3.24.0" diff --git a/ios/Podfile.lock b/ios/Podfile.lock index fc70b4fb6..d3ba59827 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2,9 +2,7 @@ PODS: - connectivity_plus (0.0.1): - Flutter - ReachabilitySwift - - CryptoSwift (1.8.3) - - cw_mweb (0.0.1): - - Flutter + - CryptoSwift (1.8.4) - cw_decred (0.0.1): - Flutter - cw_mweb (0.0.1): @@ -80,9 +78,9 @@ PODS: - permission_handler_apple (9.3.0): - Flutter - ReachabilitySwift (5.2.4) - - SDWebImage (5.19.7): - - SDWebImage/Core (= 5.19.7) - - SDWebImage/Core (5.19.7) + - SDWebImage (5.20.0): + - SDWebImage/Core (= 5.20.0) + - SDWebImage/Core (5.20.0) - sensitive_clipboard (0.0.1): - Flutter - share_plus (0.0.1): @@ -106,7 +104,6 @@ PODS: DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - CryptoSwift - - cw_mweb (from `.symlinks/plugins/cw_mweb/ios`) - cw_decred (from `.symlinks/plugins/cw_decred/ios`) - cw_mweb (from `.symlinks/plugins/cw_mweb/ios`) - device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`) @@ -147,8 +144,6 @@ SPEC REPOS: EXTERNAL SOURCES: connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" - cw_mweb: - :path: ".symlinks/plugins/cw_mweb/ios" cw_decred: :path: ".symlinks/plugins/cw_decred/ios" cw_mweb: @@ -203,41 +198,40 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/wakelock_plus/ios" SPEC CHECKSUMS: - connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf - CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483 - cw_decred: a02cf30175a46971c1e2fa22c48407534541edc6 - cw_mweb: 3aea2fb35b2bd04d8b2d21b83216f3b8fb768d85 - device_display_brightness: 04374ebd653619292c1d996f00f42877ea19f17f - device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89 - devicelocale: bd64aa714485a8afdaded0892c1e7d5b7f680cf8 + connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d + CryptoSwift: e64e11850ede528a02a0f3e768cec8e9d92ecb90 + cw_decred: 9c0e1df74745b51a1289ec5e91fb9e24b68fa14a + cw_mweb: 22cd01dfb8ad2d39b15332006f22046aaa8352a3 + device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 + device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 + devicelocale: 35ba84dc7f45f527c3001535d8c8d104edd5d926 DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 - fast_scanner: 2cb1ad3e69e645e9980fb4961396ce5804caa3e3 - file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 + fast_scanner: 44c00940355a51258cd6c2085734193cd23d95bc + file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 - flutter_local_authentication: 989278c681612f1ee0e36019e149137f114b9d7f - flutter_mailer: 3a8cd4f36c960fb04528d5471097270c19fec1c4 - flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418 - fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 - in_app_review: 5596fe56fab799e8edb3561c03d053363ab13457 - integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e + flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 + flutter_local_authentication: 1172a4dd88f6306dadce067454e2c4caf07977bb + flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83 + flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be + fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f + in_app_review: a31b5257259646ea78e0e35fc914979b0031d011 + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 - package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 - permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3 - sensitive_clipboard: 161e9abc3d56b3131309d8a321eb4690a803c16b - share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - sp_scanner: b1bc9321690980bdb44bba7ec85d5543e716d1b5 + SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8 + sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 + share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sp_scanner: eaa617fa827396b967116b7f1f43549ca62e9a12 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 - Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e - uni_links: ed8c961e47ed9ce42b6d91e1de8049e38a4b3152 - universal_ble: ff19787898040d721109c6324472e5dd4bc86adc - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d - wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49 + uni_links: d97da20c7701486ba192624d99bffaaffcfc298a + universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56 PODFILE CHECKSUM: e448f662d4c41f0c0b1ccbb78afd57dbf895a597 diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index aa6fafc6b..a955e1d98 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -422,6 +422,6 @@ class CWMonero extends Monero { } bool isViewOnly() { - return isViewOnlyBySpendKey(); + return isViewOnlyBySpendKey(null); } } diff --git a/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart b/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart index cef313780..d453a698f 100644 --- a/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart +++ b/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart @@ -315,7 +315,7 @@ class CryptoBalanceWidget extends StatelessWidget { Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: InfoCard( - title: S.of(context).decred_info_title, + title: S.of(context).synchronizing, description: S.of(context).decred_info_card_details, image: 'assets/images/dcr_icon.png', leftButtonTitle: S.of(context).litecoin_mweb_dismiss, diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 69e4bd02a..d02d246fa 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -36,7 +36,6 @@ import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; @@ -164,8 +163,6 @@ class SendPage extends BasePage { }); }); - bool _bottomSheetOpened = false; - @override Widget body(BuildContext context) { _setEffects(context); @@ -400,6 +397,7 @@ class SendPage extends BasePage { return LoadingPrimaryButton( key: ValueKey('send_page_send_button_key'), onPressed: () async { + FocusManager.instance.primaryFocus?.unfocus(); if (sendViewModel.state is IsExecutingState) return; if (_formKey.currentState != null && !_formKey.currentState!.validate()) { diff --git a/lib/src/screens/settings/privacy_page.dart b/lib/src/screens/settings/privacy_page.dart index 238e58eab..5a22f622a 100644 --- a/lib/src/screens/settings/privacy_page.dart +++ b/lib/src/screens/settings/privacy_page.dart @@ -42,7 +42,7 @@ class PrivacyPage extends BasePage { ), SettingsChoicesCell( ChoicesListItem( - title: S.current.exchange, + title: S.current.swap, items: ExchangeApiMode.all, selectedItem: _privacySettingsViewModel.exchangeStatus, onItemSelected: (ExchangeApiMode mode) => diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 29ccedef2..a07ba26ef 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -266,7 +266,8 @@ abstract class DashboardViewModelBase with Store { reaction((_) => appStore.wallet, (wallet) { _onWalletChange(wallet); _checkMweb(); - showDecredInfoCard = wallet?.type == WalletType.decred; + showDecredInfoCard = wallet?.type == WalletType.decred && + sharedPreferences.getBool(PreferencesKey.showDecredInfoCard) != false; }); _transactionDisposer?.reaction.dispose(); @@ -792,7 +793,7 @@ abstract class DashboardViewModelBase with Store { (List txs) { transactions.clear(); - + transactions.addAll( txs.where((tx) { if (wallet.type == WalletType.monero) { diff --git a/lib/view_model/node_list/node_create_or_edit_view_model.dart b/lib/view_model/node_list/node_create_or_edit_view_model.dart index 8fbe174e3..7e4e73915 100644 --- a/lib/view_model/node_list/node_create_or_edit_view_model.dart +++ b/lib/view_model/node_list/node_create_or_edit_view_model.dart @@ -1,7 +1,6 @@ import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/entities/qr_scanner.dart'; import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cw_core/utils/print_verbose.dart'; import 'package:flutter/cupertino.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; diff --git a/macos/Podfile.lock b/macos/Podfile.lock index cc6ae6e3b..689f0ea03 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -3,7 +3,6 @@ PODS: - FlutterMacOS - ReachabilitySwift - cw_mweb (0.0.1): - - cw_decred (0.0.1): - FlutterMacOS - device_info_plus (0.0.1): - FlutterMacOS @@ -46,7 +45,6 @@ PODS: DEPENDENCIES: - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) - cw_mweb (from `Flutter/ephemeral/.symlinks/plugins/cw_mweb/macos`) - - cw_decred (from `Flutter/ephemeral/.symlinks/plugins/cw_decred/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - devicelocale (from `Flutter/ephemeral/.symlinks/plugins/devicelocale/macos`) - fast_scanner (from `Flutter/ephemeral/.symlinks/plugins/fast_scanner/macos`) @@ -74,10 +72,6 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos cw_mweb: :path: Flutter/ephemeral/.symlinks/plugins/cw_mweb/macos - cw_decred: - :path: Flutter/ephemeral/.symlinks/plugins/cw_decred/macos - cw_monero: - :path: Flutter/ephemeral/.symlinks/plugins/cw_monero/macos device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos devicelocale: @@ -114,8 +108,6 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 cw_mweb: 7440b12ead811dda972a9918442ea2a458e8742c - cw_monero: ec03de55a19c4a2b174ea687e0f4202edc716fa4 - cw_decred: 0c93fbeb31bd97a6ad4ec5680960af0943bfca78 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f devicelocale: 9f0f36ac651cabae2c33f32dcff4f32b61c38225 fast_scanner: d31bae07e2653403a69dac99fb710c1722b16a97 diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 64089065d..686f8c8ce 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -822,6 +822,7 @@ "sync_status_syncronizing": "يتم المزامنة", "sync_status_timed_out": "نفد وقته", "sync_status_unsupported": "عقدة غير مدعومة", + "synchronizing": "المزامنة", "syncing_wallet_alert_content": "قد لا يكتمل رصيدك وقائمة المعاملات الخاصة بك حتى تظهر عبارة “SYNCHRONIZED“ في الأعلى. انقر / اضغط لمعرفة المزيد.", "syncing_wallet_alert_title": "محفظتك تتم مزامنتها", "template": "قالب", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 24b6e2640..a7b9a27f1 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -822,6 +822,7 @@ "sync_status_syncronizing": "СИНХРОНИЗИРАНЕ", "sync_status_timed_out": "ВРЕМЕТО ИЗТЕЧЕ", "sync_status_unsupported": "Неподдържан възел", + "synchronizing": "Синхронизиране", "syncing_wallet_alert_content": "Списъкът ви с баланс и транзакции може да не е пълен, докато в горната част не пише „СИНХРОНИЗИРАН“. Кликнете/докоснете, за да научите повече.", "syncing_wallet_alert_title": "Вашият портфейл се синхронизира", "template": "Шаблон", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 9dbc99d5c..7458b219a 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -822,6 +822,7 @@ "sync_status_syncronizing": "SYNCHRONIZUJI", "sync_status_timed_out": "ČAS VYPRŠEL", "sync_status_unsupported": "Nepodporovaný uzel", + "synchronizing": "Synchronizace", "syncing_wallet_alert_content": "Váš seznam zůstatků a transakcí nemusí být úplný, dokud nebude nahoře uvedeno „SYNCHRONIZOVANÉ“. Kliknutím/klepnutím se dozvíte více.", "syncing_wallet_alert_title": "Vaše peněženka se synchronizuje", "template": "Šablona", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 54303caf3..d25e02494 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -823,6 +823,7 @@ "sync_status_syncronizing": "SYNCHRONISIERE", "sync_status_timed_out": "Zeitlich abgestimmt", "sync_status_unsupported": "Nicht unterstützter Knoten", + "synchronizing": "Synchronisierung", "syncing_wallet_alert_content": "Ihr Kontostand und Ihre Transaktionsliste sind möglicherweise erst vollständig, wenn oben „SYNCHRONISIERT“ steht. Klicken/tippen Sie, um mehr zu erfahren.", "syncing_wallet_alert_title": "Ihr Wallet wird synchronisiert", "template": "Vorlage", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 85103162d..238f36fda 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -823,6 +823,7 @@ "sync_status_syncronizing": "SYNCHRONIZING", "sync_status_timed_out": "TIMED OUT", "sync_status_unsupported": "UNSUPPORTED NODE", + "synchronizing": "Synchronizing", "syncing_wallet_alert_content": "Your balance and transaction list may not be complete until it says “SYNCHRONIZED” at the top. Click/tap to learn more.", "syncing_wallet_alert_title": "Your wallet is syncing", "template": "Template", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index cd63e5a3d..1252324ee 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -823,6 +823,7 @@ "sync_status_syncronizing": "SINCRONIZANDO", "sync_status_timed_out": "CADUCADO", "sync_status_unsupported": "Nodo no compatible", + "synchronizing": "Sincronización", "syncing_wallet_alert_content": "Es posible que su lista de saldo y transacciones no esté completa hasta que diga \"SINCRONIZADO\" en la parte superior. Haga clic/toque para obtener más información.", "syncing_wallet_alert_title": "Tu billetera se está sincronizando", "template": "Plantilla", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 309319277..d180df3b9 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -822,6 +822,7 @@ "sync_status_syncronizing": "SYNCHRONISATION EN COURS", "sync_status_timed_out": "FIN DU TEMPS", "sync_status_unsupported": "Nœud non pris en charge", + "synchronizing": "Synchronisation", "syncing_wallet_alert_content": "Votre solde et votre liste de transactions peuvent ne pas être à jour tant que la mention « SYNCHRONISÉ » n'apparaît en haut de l'écran. Cliquez/appuyez pour en savoir plus.", "syncing_wallet_alert_title": "Votre portefeuille (wallet) est en cours de synchronisation", "template": "Modèle", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 4ae70f8dd..1f8962b52 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -824,6 +824,7 @@ "sync_status_syncronizing": "KWAFI", "sync_status_timed_out": "ATED Out", "sync_status_unsupported": "Ba a Taimako ba", + "synchronizing": "Aikiɓaɓaƙe", "syncing_wallet_alert_content": "Ma'aunin ku da lissafin ma'amala bazai cika ba har sai an ce \"SYNCHRONIZED\" a saman. Danna/matsa don ƙarin koyo.", "syncing_wallet_alert_title": "Walat ɗin ku yana aiki tare", "template": "Samfura", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index de15f45b6..3c30f82fd 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -824,6 +824,7 @@ "sync_status_syncronizing": "सिंक्रनाइज़ करने", "sync_status_timed_out": "समय समााप्त", "sync_status_unsupported": "असमर्थित नोड", + "synchronizing": "सिंक्रनाइज़ करना", "syncing_wallet_alert_content": "आपकी शेष राशि और लेनदेन सूची तब तक पूरी नहीं हो सकती जब तक कि शीर्ष पर \"सिंक्रनाइज़्ड\" न लिखा हो। अधिक जानने के लिए क्लिक/टैप करें।", "syncing_wallet_alert_title": "आपका वॉलेट सिंक हो रहा है", "template": "खाका", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 48da3833a..cbb9f0f89 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -822,6 +822,7 @@ "sync_status_syncronizing": "SINKRONIZIRANJE", "sync_status_timed_out": "ISTEKLO", "sync_status_unsupported": "Nepodržani čvor", + "synchronizing": "Sinkronizirajući", "syncing_wallet_alert_content": "Vaš saldo i popis transakcija možda neće biti potpuni sve dok na vrhu ne piše \"SINKRONIZIRANO\". Kliknite/dodirnite da biste saznali više.", "syncing_wallet_alert_title": "Vaš novčanik se sinkronizira", "template": "Predložak", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index ee1c997b0..e39390939 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -820,6 +820,7 @@ "sync_status_syncronizing": "ՀԱՄԱԺԱՄԵՑՎՈՒՄ Է", "sync_status_timed_out": "Ժամանակը սպառվեց", "sync_status_unsupported": "ՉԱՋԱԿՑՎՈՂ ՀԱՆԳՈՒՅՑ,", + "synchronizing": "Համաժամանակող", "syncing_wallet_alert_content": "Ձեր հաշիվը և գործարքների ցանկը կարող են լինել անավարտ մինչև վերին մասում գրված լինի “ՀԱՂՈՒՄ”։ Սեղմեք/դիպեք ուսումնասիրելու համար։", "syncing_wallet_alert_title": "Ձեր դրամապանակը համաժամացվում է", "template": "Տարբերակ", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index f9a231112..f6041fcb9 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -825,6 +825,7 @@ "sync_status_syncronizing": "SEDANG SINKRONISASI", "sync_status_timed_out": "WAKTU HABIS", "sync_status_unsupported": "Node yang tidak didukung", + "synchronizing": "Sinkronisasi", "syncing_wallet_alert_content": "Saldo dan daftar transaksi Anda mungkin belum lengkap sampai tertulis “SYNCHRONIZED” di bagian atas. Klik/ketuk untuk mempelajari lebih lanjut.", "syncing_wallet_alert_title": "Dompet Anda sedang disinkronkan", "template": "Template", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 25d02d9d0..8209f532a 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -823,6 +823,7 @@ "sync_status_syncronizing": "SINCRONIZZAZIONE", "sync_status_timed_out": "TIMED OUT", "sync_status_unsupported": "NODO NON SUPPORTATO", + "synchronizing": "Sincronizzazione", "syncing_wallet_alert_content": "Il saldo e l'elenco delle transazioni potrebbero non essere completi fin quando non viene visualizzato \"SINCRONIZZATO\" in alto. Clicca/tocca per saperne di più.", "syncing_wallet_alert_title": "Il tuo portafoglio si sta sincronizzando", "template": "Modello", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 6a3a5da72..af7ab6502 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -823,6 +823,7 @@ "sync_status_syncronizing": "同期", "sync_status_timed_out": "タイムアウトしました", "sync_status_unsupported": "サポートされていないノード", + "synchronizing": "同期", "syncing_wallet_alert_content": "上部に「同期済み」と表示されるまで、残高と取引リストが完了していない可能性があります。詳細については、クリック/タップしてください。", "syncing_wallet_alert_title": "ウォレットは同期中です", "template": "テンプレート", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index d16e18a53..6386cf7c1 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -822,6 +822,7 @@ "sync_status_syncronizing": "동기화", "sync_status_timed_out": "시간 초과", "sync_status_unsupported": "지원되지 않은 노드", + "synchronizing": "동기화", "syncing_wallet_alert_content": "상단에 \"동기화됨\"이라고 표시될 때까지 잔액 및 거래 목록이 완전하지 않을 수 있습니다. 자세히 알아보려면 클릭/탭하세요.", "syncing_wallet_alert_title": "지갑 동기화 중", "template": "주형", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index d7ba8d7a6..31e5335b5 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -822,6 +822,7 @@ "sync_status_syncronizing": "ထပ်တူပြုခြင်း။", "sync_status_timed_out": "ထွက်အချိန်ကုန်", "sync_status_unsupported": "node မထောက်ပံ့ node ကို", + "synchronizing": "ထပ်တူပြုခြင်း", "syncing_wallet_alert_content": "သင်၏လက်ကျန်နှင့် ငွေပေးငွေယူစာရင်းသည် ထိပ်တွင် \"Synchronizeed\" ဟုပြောသည်အထိ မပြီးမြောက်နိုင်ပါ။ ပိုမိုလေ့လာရန် နှိပ်/နှိပ်ပါ။", "syncing_wallet_alert_title": "သင့်ပိုက်ဆံအိတ်ကို စင့်ခ်လုပ်နေပါသည်။", "template": "ပုံစံခွက်", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 55e588f62..a15d55c09 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -822,6 +822,7 @@ "sync_status_syncronizing": "SYNCHRONISEREN", "sync_status_timed_out": "Uitgeput", "sync_status_unsupported": "Niet ondersteund knooppunt", + "synchronizing": "Synchronisatie", "syncing_wallet_alert_content": "Uw saldo- en transactielijst is mogelijk pas compleet als er bovenaan 'GESYNCHRONISEERD' staat. Klik/tik voor meer informatie.", "syncing_wallet_alert_title": "Uw portemonnee wordt gesynchroniseerd", "template": "Sjabloon", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index cb4f09982..51aad7be2 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -822,6 +822,7 @@ "sync_status_syncronizing": "SYNCHRONIZACJA", "sync_status_timed_out": "PRZEKROCZONO LIMIT CZASU", "sync_status_unsupported": "Nieobsługiwany węzeł", + "synchronizing": "Synchronizacja", "syncing_wallet_alert_content": "Twoje saldo i lista transakcji mogą nie być kompletne, dopóki u góry nie pojawi się napis „SYNCHRONIZOWANY”. Kliknij/stuknij, aby dowiedzieć się więcej.", "syncing_wallet_alert_title": "Twój portfel się synchronizuje", "template": "Szablon", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 2fb3bac5f..4774e917e 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -824,6 +824,7 @@ "sync_status_syncronizing": "SINCRONIZANDO", "sync_status_timed_out": "TEMPO ESGOTADO", "sync_status_unsupported": "Nó não suportado", + "synchronizing": "Sincronizando", "syncing_wallet_alert_content": "Seu saldo e lista de transações podem não estar completos até que diga “SYNCHRONIZED” no topo. Clique/toque para saber mais.", "syncing_wallet_alert_title": "Sua carteira está sincronizando", "template": "Modelo", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 4173ad6d5..d06a001d2 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -823,6 +823,7 @@ "sync_status_syncronizing": "СИНХРОНИЗАЦИЯ", "sync_status_timed_out": "ВРЕМЯ ВЫШЛО", "sync_status_unsupported": "Неподдерживаемый узел", + "synchronizing": "Синхронизация", "syncing_wallet_alert_content": "Ваш баланс и список транзакций могут быть неполными, пока вверху не будет написано «СИНХРОНИЗИРОВАНО». Щелкните/коснитесь, чтобы узнать больше.", "syncing_wallet_alert_title": "Ваш кошелек синхронизируется", "template": "Шаблон", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index acd70c554..aa92d9171 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -822,6 +822,7 @@ "sync_status_syncronizing": "กำลังซิงโครไนซ์", "sync_status_timed_out": "หมดเวลา", "sync_status_unsupported": "โหนดที่ไม่ได้รับการสนับสนุน", + "synchronizing": "การซิงโครไนซ์", "syncing_wallet_alert_content": "รายการยอดเงินและธุรกรรมของคุณอาจไม่สมบูรณ์จนกว่าจะมีข้อความว่า “ซิงโครไนซ์” ที่ด้านบน คลิก/แตะเพื่อเรียนรู้เพิ่มเติม่", "syncing_wallet_alert_title": "กระเป๋าสตางค์ของคุณกำลังซิงค์", "template": "แบบฟอร์ม", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index a378ff3ea..f2087c573 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -822,6 +822,7 @@ "sync_status_syncronizing": "PAG-SYNCHRONIZE", "sync_status_timed_out": "NAG-TIME OUT", "sync_status_unsupported": "HINDI SUPORTADONG NODE", + "synchronizing": "Pag -synchronize", "syncing_wallet_alert_content": "Ang iyong balanse at listahan ng transaksyon ay maaaring hindi kumpleto hanggang sa sabihin nito na \"NAKA-SYNCHRONIZE\" sa tuktok. Mag-click/tap upang malaman ang higit pa.", "syncing_wallet_alert_title": "Ang iyong wallet ay nag-sync", "template": "Template", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 8011401dc..44cb64b69 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -822,6 +822,7 @@ "sync_status_syncronizing": "SENKRONİZE EDİLİYOR", "sync_status_timed_out": "ZAMAN AŞIMINA UĞRADI", "sync_status_unsupported": "Desteklenmeyen düğüm", + "synchronizing": "Senkronize etme", "syncing_wallet_alert_content": "Bakiyeniz ve işlem listeniz, en üstte \"SENKRONİZE EDİLDİ\" yazana kadar tamamlanmamış olabilir. Daha fazla bilgi edinmek için tıklayın/dokunun.", "syncing_wallet_alert_title": "Cüzdanınız senkronize ediliyor", "template": "Şablon", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 8227d581f..2e250dd05 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -823,6 +823,7 @@ "sync_status_syncronizing": "СИНХРОНІЗАЦІЯ", "sync_status_timed_out": "ТАЙМ-АУТ", "sync_status_unsupported": "Непідтримуваний вузол", + "synchronizing": "Синхронізація", "syncing_wallet_alert_content": "Ваш баланс та список транзакцій може бути неповним, доки вгорі не буде написано «СИНХРОНІЗОВАНО». Натисніть/торкніться, щоб дізнатися більше.", "syncing_wallet_alert_title": "Ваш гаманець синхронізується", "template": "Шаблон", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 307340a21..6684fb2f3 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -824,6 +824,7 @@ "sync_status_syncronizing": "مطابقت پذیری", "sync_status_timed_out": "وقت ختم", "sync_status_unsupported": "غیر تعاون یافتہ نوڈ", + "synchronizing": "ہم آہنگی", "syncing_wallet_alert_content": "آپ کے بیلنس اور لین دین کی فہرست اس وقت تک مکمل نہیں ہو سکتی جب تک کہ یہ سب سے اوپر \"SYNCRONIZED\" نہ کہے۔ مزید جاننے کے لیے کلک/تھپتھپائیں۔", "syncing_wallet_alert_title": "آپ کا بٹوہ مطابقت پذیر ہو رہا ہے۔", "template": "سانچے", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index c2935e9a0..45ffac20e 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -819,6 +819,7 @@ "sync_status_syncronizing": "ĐANG ĐỒNG BỘ", "sync_status_timed_out": "HẾT THỜI GIAN", "sync_status_unsupported": "NÓT KHÔNG ĐƯỢC HỖ TRỢ", + "synchronizing": "Đồng bộ hóa", "syncing_wallet_alert_content": "Số dư và danh sách giao dịch của bạn có thể không đầy đủ cho đến khi nó hiển thị “ĐÃ ĐỒNG BỘ” ở trên cùng. Nhấn vào đây để tìm hiểu thêm.", "syncing_wallet_alert_title": "Ví của bạn đang đồng bộ", "template": "Mẫu", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index b97fbb757..1d754b366 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -823,6 +823,7 @@ "sync_status_syncronizing": "Ń MÚDỌ́GBA", "sync_status_timed_out": "Ti akoko jade", "sync_status_unsupported": "Ile-igbimọ ti ko ni atilẹyin", + "synchronizing": "Mimuuṣiṣẹpọ", "syncing_wallet_alert_content": "Iwontunws.funfun rẹ ati atokọ idunadura le ma pari titi ti yoo fi sọ “SYNCHRONIZED” ni oke. Tẹ/tẹ ni kia kia lati ni imọ siwaju sii.", "syncing_wallet_alert_title": "Apamọwọ rẹ n muṣiṣẹpọ", "template": "Àwòṣe", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index e1767a28f..7325cb5f0 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -822,6 +822,7 @@ "sync_status_syncronizing": "正在同步", "sync_status_timed_out": "时间到", "sync_status_unsupported": "不支持的节点", + "synchronizing": "同步", "syncing_wallet_alert_content": "您的余额和交易列表可能不完整,直到顶部显示“已同步”。单击/点击以了解更多信息。", "syncing_wallet_alert_title": "您的钱包正在同步", "template": "模板", diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 597c0aa9a..13cae2000 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -14,15 +14,15 @@ TYPES=($MONERO_COM $CAKEWALLET) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.20.3" -MONERO_COM_BUILD_NUMBER=115 +MONERO_COM_VERSION="1.21.0" +MONERO_COM_BUILD_NUMBER=116 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.23.4" -CAKEWALLET_BUILD_NUMBER=249 +CAKEWALLET_VERSION="4.24.0" +CAKEWALLET_BUILD_NUMBER=252 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/android/build_decred.sh b/scripts/android/build_decred.sh index d7b2fdb6b..fee8e9af8 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="87b2769538db3065b334d247b25774593fc6443d" +LIBWALLET_VERSION="dba5327d35cb5d5d1ff113b780869deee154511f" if [ -e $LIBWALLET_PATH ]; then rm -fr $LIBWALLET_PATH/{*,.*} || true diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 1c1579830..985a48971 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -12,13 +12,13 @@ TYPES=($MONERO_COM $CAKEWALLET) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.20.3" -MONERO_COM_BUILD_NUMBER=113 +MONERO_COM_VERSION="1.21.0" +MONERO_COM_BUILD_NUMBER=114 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.23.4" -CAKEWALLET_BUILD_NUMBER=302 +CAKEWALLET_VERSION="4.24.0" +CAKEWALLET_BUILD_NUMBER=305 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" diff --git a/scripts/ios/build_decred.sh b/scripts/ios/build_decred.sh index e6b13d0da..78c36690b 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="87b2769538db3065b334d247b25774593fc6443d" +LIBWALLET_VERSION="dba5327d35cb5d5d1ff113b780869deee154511f" if [ -e $LIBWALLET_PATH ]; then rm -fr $LIBWALLET_PATH diff --git a/scripts/linux/app_env.sh b/scripts/linux/app_env.sh index ef1747ba6..9d97b63cb 100755 --- a/scripts/linux/app_env.sh +++ b/scripts/linux/app_env.sh @@ -14,8 +14,8 @@ if [ -n "$1" ]; then fi CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.13.4" -CAKEWALLET_BUILD_NUMBER=49 +CAKEWALLET_VERSION="1.14.0" +CAKEWALLET_BUILD_NUMBER=50 if ! [[ " ${TYPES[*]} " =~ " ${APP_LINUX_TYPE} " ]]; then echo "Wrong app type." diff --git a/scripts/macos/app_config.sh b/scripts/macos/app_config.sh index c10116136..641a7b46b 100755 --- a/scripts/macos/app_config.sh +++ b/scripts/macos/app_config.sh @@ -36,7 +36,7 @@ case $APP_MACOS_TYPE in $MONERO_COM) CONFIG_ARGS="--monero";; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --decred";; + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero";; esac cp -rf pubspec_description.yaml pubspec.yaml diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index 7410ee3a3..4ac64ba42 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -16,13 +16,13 @@ if [ -n "$1" ]; then fi MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.10.3" -MONERO_COM_BUILD_NUMBER=45 +MONERO_COM_VERSION="1.11.0" +MONERO_COM_BUILD_NUMBER=46 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.16.4" -CAKEWALLET_BUILD_NUMBER=107 +CAKEWALLET_VERSION="1.17.0" +CAKEWALLET_BUILD_NUMBER=108 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then diff --git a/scripts/macos/build_decred.sh b/scripts/macos/build_decred.sh index d4b7d4d65..e7e5d492f 100755 --- a/scripts/macos/build_decred.sh +++ b/scripts/macos/build_decred.sh @@ -4,7 +4,7 @@ LIBWALLET_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/libwallet" LIBWALLET_URL="https://github.com/decred/libwallet.git" -LIBWALLET_VERSION="87b2769538db3065b334d247b25774593fc6443d" +LIBWALLET_VERSION="dba5327d35cb5d5d1ff113b780869deee154511f" echo "======================= DECRED LIBWALLET =========================" diff --git a/scripts/windows/build_exe_installer.iss b/scripts/windows/build_exe_installer.iss index 211017eff..3f433e1ae 100644 --- a/scripts/windows/build_exe_installer.iss +++ b/scripts/windows/build_exe_installer.iss @@ -1,5 +1,5 @@ #define MyAppName "Cake Wallet" -#define MyAppVersion "0.4.4" +#define MyAppVersion "0.5.0" #define MyAppPublisher "Cake Labs LLC" #define MyAppURL "https://cakewallet.com/" #define MyAppExeName "CakeWallet.exe" From 36600dd82b5097166bde59851b1930e2c1aec817 Mon Sep 17 00:00:00 2001 From: Slixe Date: Wed, 26 Mar 2025 01:46:38 +0100 Subject: [PATCH 059/243] fix french translation (#2117) --- res/values/strings_fr.arb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index d180df3b9..f726aab7a 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -303,7 +303,7 @@ "etherscan_history": "Historique Etherscan", "event": "Événement", "events": "Événements", - "exchange": "Échange", + "exchange": "Acheter / Vendre", "exchange_incorrect_current_wallet_for_xmr": "Si vous souhaitez échanger des XMR depuis le solde Monero de votre Cake Wallet, veuillez d'abord passer à votre portefeuille Monero.", "exchange_new_template": "Nouveau modèle d'échange", "exchange_provider_unsupported": "${providerName} n'est plus pris en charge !", @@ -756,8 +756,8 @@ "shared_seed_wallet_groups": "Groupes de portefeuilles partagés", "show": "Montrer", "show_address_book_popup": "Afficher la fenêtre contextuelle du carnet d'adresses", - "show_balance": "Longue presse pour montrer l'équilibre", - "show_balance_toast": "Longue appuyez sur pour masquer ou afficher l'équilibre", + "show_balance": "Appuyez longtemps pour afficher votre solde", + "show_balance_toast": "Appuyez longtemps pour masquer ou afficher votre solde", "show_details": "Afficher les détails", "show_keys": "Visualiser la phrase secrète (seed) et les clefs", "show_market_place": "Afficher la place de marché", @@ -782,7 +782,7 @@ "silent_payments_scanned_tip": "SCANNEZ POUR DONNER ! (${tip})", "silent_payments_scanning": "Scan des paiements silencieux", "silent_payments_settings": "Paramètres de paiement silencieux", - "single_seed_wallets_group": "Portefeuilles de semences simples", + "single_seed_wallets_group": "Portefeuilles de mots-clés (seed) simples", "slidable": "Glissable", "solana_create_associated_token_account_exception": "Création d'erreur Création de jetons associés pour l'adresse détenue.", "solana_no_associated_token_account_exception": "Il n'y a pas de compte de jeton associé pour cette adresse.", @@ -944,7 +944,7 @@ "variable_pair_not_supported": "Cette paire variable n'est pas prise en charge avec les échanges sélectionnés", "verification": "Vérification", "verify_message": "Vérifier le message", - "verify_seed": "Vérifiez les semences", + "verify_seed": "Vérifiez les mots-clés (seed)", "verify_with_2fa": "Vérifier avec Cake 2FA", "version": "Version ${currentVersion}", "view_all": "Voir tout", From 72e011ead0fafd58e701a40d99e92ca6f96dfd54 Mon Sep 17 00:00:00 2001 From: cyan Date: Wed, 26 Mar 2025 02:34:39 +0100 Subject: [PATCH 060/243] send logs about trace only in debug mode (#2115) --- .github/workflows/no_print_in_dart.yaml | 2 +- cw_core/lib/utils/print_verbose.dart | 5 +++-- tool/append_translation.dart | 1 - tool/download_moneroc_prebuilds.dart | 2 +- tool/generate_localization.dart | 2 +- tool/print_verbose_dummy.dart | 1 + tool/translation_add_lang.dart | 2 +- tool/translation_consistence.dart | 2 +- tool/utils/translation/arb_file_utils.dart | 2 +- 9 files changed, 10 insertions(+), 9 deletions(-) create mode 100644 tool/print_verbose_dummy.dart diff --git a/.github/workflows/no_print_in_dart.yaml b/.github/workflows/no_print_in_dart.yaml index b321a9cc9..b1c356c31 100644 --- a/.github/workflows/no_print_in_dart.yaml +++ b/.github/workflows/no_print_in_dart.yaml @@ -11,7 +11,7 @@ jobs: - name: Check for print() statements in dart code (use printV() instead) if: github.event_name == 'pull_request' run: | - GIT_GREP_OUT="$(git grep ' print(' | (grep .dart: || test $? = 1) | (grep -v print_verbose.dart || test $? = 1) || true)" + GIT_GREP_OUT="$(git grep ' print(' | (grep .dart: || test $? = 1) | (grep -v print_verbose.dart || test $? = 1) | (grep -v print_verbose_dummy.dart || test $? = 1) || true)" [[ "x$GIT_GREP_OUT" == "x" ]] && exit 0 echo "$GIT_GREP_OUT" echo "There are .dart files which use print() statements" diff --git a/cw_core/lib/utils/print_verbose.dart b/cw_core/lib/utils/print_verbose.dart index de7cc8557..42014d609 100644 --- a/cw_core/lib/utils/print_verbose.dart +++ b/cw_core/lib/utils/print_verbose.dart @@ -1,4 +1,5 @@ import 'dart:math'; +import 'package:flutter/foundation.dart'; void printV(dynamic content) { CustomTrace programInfo = CustomTrace(StackTrace.current); @@ -20,7 +21,7 @@ class CustomTrace { try { _parseTrace(); } catch (e) { - print("Unable to parse trace (printV): $e"); + if (kDebugMode) print("Unable to parse trace (printV): $e"); } } @@ -80,7 +81,7 @@ class CustomTrace { columnStr = columnStr.replaceFirst(")", ""); this.columnNumber = int.tryParse(columnStr); } catch (e) { - print("Unable to parse trace (printV): $e"); + if (kDebugMode) print("Unable to parse trace (printV): $e"); } } } diff --git a/tool/append_translation.dart b/tool/append_translation.dart index 8ef646502..b4c975599 100644 --- a/tool/append_translation.dart +++ b/tool/append_translation.dart @@ -1,4 +1,3 @@ -import 'package:cw_core/utils/print_verbose.dart'; import 'utils/translation/arb_file_utils.dart'; import 'utils/translation/translation_constants.dart'; diff --git a/tool/download_moneroc_prebuilds.dart b/tool/download_moneroc_prebuilds.dart index 378d9293b..8889a1bc1 100644 --- a/tool/download_moneroc_prebuilds.dart +++ b/tool/download_moneroc_prebuilds.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:cw_core/utils/print_verbose.dart'; +import './print_verbose_dummy.dart'; import 'package:dio/dio.dart'; import 'package:archive/archive_io.dart'; diff --git a/tool/generate_localization.dart b/tool/generate_localization.dart index 0f9af8366..0e07dc2c3 100644 --- a/tool/generate_localization.dart +++ b/tool/generate_localization.dart @@ -1,6 +1,6 @@ import 'dart:io'; import 'dart:convert'; -import 'package:cw_core/utils/print_verbose.dart'; +import './print_verbose_dummy.dart'; import 'localization/localization_constants.dart'; import 'utils/utils.dart'; diff --git a/tool/print_verbose_dummy.dart b/tool/print_verbose_dummy.dart new file mode 100644 index 000000000..f7286e1f9 --- /dev/null +++ b/tool/print_verbose_dummy.dart @@ -0,0 +1 @@ +void printV(dynamic content) => print(content); \ No newline at end of file diff --git a/tool/translation_add_lang.dart b/tool/translation_add_lang.dart index 96f22158d..e3c372a8c 100644 --- a/tool/translation_add_lang.dart +++ b/tool/translation_add_lang.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:cw_core/utils/print_verbose.dart'; +import './print_verbose_dummy.dart'; import 'utils/translation/arb_file_utils.dart'; import 'utils/translation/translation_constants.dart'; diff --git a/tool/translation_consistence.dart b/tool/translation_consistence.dart index df24cce15..2cdd358c8 100644 --- a/tool/translation_consistence.dart +++ b/tool/translation_consistence.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:cw_core/utils/print_verbose.dart'; +import './print_verbose_dummy.dart'; import 'utils/translation/arb_file_utils.dart'; import 'utils/translation/translation_constants.dart'; diff --git a/tool/utils/translation/arb_file_utils.dart b/tool/utils/translation/arb_file_utils.dart index 0fe31d85d..92a8f6e2e 100644 --- a/tool/utils/translation/arb_file_utils.dart +++ b/tool/utils/translation/arb_file_utils.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'dart:io'; -import 'package:cw_core/utils/print_verbose.dart'; +import '../../print_verbose_dummy.dart'; void appendStringToArbFile(String fileName, String name, String text, {bool force = false}) { final file = File(fileName); From 18049308df20175e69f5950c1eea53d4ccbb1440 Mon Sep 17 00:00:00 2001 From: cyan Date: Wed, 26 Mar 2025 02:49:48 +0100 Subject: [PATCH 061/243] fix syntax errors in decred script (#2109) * fix syntax errors in decred script * [skip ci] fix typo in error message --- scripts/android/build_decred.sh | 10 +++++----- scripts/ios/build_decred.sh | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/scripts/android/build_decred.sh b/scripts/android/build_decred.sh index fee8e9af8..75ed45aca 100755 --- a/scripts/android/build_decred.sh +++ b/scripts/android/build_decred.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash set -e cd "$(dirname "$0")" @@ -9,8 +9,8 @@ LIBWALLET_PATH="${PWD}/decred/libwallet" LIBWALLET_URL="https://github.com/decred/libwallet.git" LIBWALLET_VERSION="dba5327d35cb5d5d1ff113b780869deee154511f" -if [ -e $LIBWALLET_PATH ]; then - rm -fr $LIBWALLET_PATH/{*,.*} || true +if [[ -e $LIBWALLET_PATH ]]; then + rm -fr $LIBWALLET_PATH || true fi mkdir -p $LIBWALLET_PATH || true @@ -32,7 +32,7 @@ then echo "You have these versions installed on your system currently:" ls ${ANDROID_HOME}/ndk/ | cat | awk '{ print "- " $1 }' echo "echo > ~/.zprofile" - echo "echo 'export ANDROID_NDK_CERSION=..... > ~/.zprofile" + echo "echo 'export ANDROID_NDK_VERSION=..... > ~/.zprofile" exit 1 fi @@ -65,7 +65,7 @@ do esac # PATH="${TOOLCHAIN_BASE_DIR}_${arch}/bin:${ORIGINAL_PATH}" - if [ -e ./build ]; then + if [[ -e ./build ]]; then rm -fr ./build fi diff --git a/scripts/ios/build_decred.sh b/scripts/ios/build_decred.sh index 78c36690b..6860c7776 100755 --- a/scripts/ios/build_decred.sh +++ b/scripts/ios/build_decred.sh @@ -1,12 +1,12 @@ -#!/bin/sh +#!/bin/bash set -e . ./config.sh LIBWALLET_PATH="${EXTERNAL_IOS_SOURCE_DIR}/libwallet" LIBWALLET_URL="https://github.com/decred/libwallet.git" LIBWALLET_VERSION="dba5327d35cb5d5d1ff113b780869deee154511f" -if [ -e $LIBWALLET_PATH ]; then - rm -fr $LIBWALLET_PATH +if [[ -e $LIBWALLET_PATH ]]; then + rm -fr $LIBWALLET_PATH fi mkdir -p $LIBWALLET_PATH git clone $LIBWALLET_URL $LIBWALLET_PATH @@ -17,8 +17,8 @@ SYSROOT=`xcrun --sdk iphoneos --show-sdk-path` CLANG="clang -target arm64-apple-ios -isysroot ${SYSROOT}" CLANGXX="clang++ -target arm64-apple-ios -isysroot ${SYSROOT}" -if [ -e ./build ]; then - rm -fr ./build +if [[ -e ./build ]]; then + rm -fr ./build fi CGO_ENABLED=1 GOOS=ios GOARCH=arm64 CC=$CLANG CXX=$CLANGXX \ go build -v -buildmode=c-archive -o ./build/libdcrwallet.a ./cgo || exit 1 From 2f0cb28fb204229a847c5fdcac98a0c34edbd02d Mon Sep 17 00:00:00 2001 From: cyan Date: Wed, 26 Mar 2025 02:56:24 +0100 Subject: [PATCH 062/243] properly await multDest transactino so it won't freeze the UI (#2116) --- cw_monero/lib/api/transaction_history.dart | 64 +++++++--------------- cw_monero/lib/monero_wallet.dart | 1 + 2 files changed, 22 insertions(+), 43 deletions(-) diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index 854ee01c3..162b9ac1a 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -161,31 +161,39 @@ Future createTransactionSync( ); } -PendingTransactionDescription createTransactionMultDestSync( +Future createTransactionMultDest( {required List outputs, required String paymentId, required int priorityRaw, int accountIndex = 0, - List preferredInputs = const []}) { + List preferredInputs = const []}) async { final dstAddrs = outputs.map((e) => e.address).toList(); final amounts = outputs.map((e) => monero.Wallet_amountFromString(e.amount)).toList(); - // printV("multDest: dstAddrs: $dstAddrs"); - // printV("multDest: amounts: $amounts"); + final waddr = wptr!.address; + + // force reconnection in case the os killed the connection + Isolate.run(() async { + monero.Wallet_synchronized(Pointer.fromAddress(waddr)); + }); + + final txptr = Pointer.fromAddress(await Isolate.run(() { + return monero.Wallet_createTransactionMultDest( + Pointer.fromAddress(waddr), + dstAddr: dstAddrs, + isSweepAll: false, + amounts: amounts, + mixinCount: 0, + pendingTransactionPriority: priorityRaw, + subaddr_account: accountIndex, + ).address; + })); - final txptr = monero.Wallet_createTransactionMultDest( - wptr!, - dstAddr: dstAddrs, - isSweepAll: false, - amounts: amounts, - mixinCount: 0, - pendingTransactionPriority: priorityRaw, - subaddr_account: accountIndex, - ); if (monero.PendingTransaction_status(txptr) != 0) { throw CreationTransactionException(message: monero.PendingTransaction_errorString(txptr)); } + return PendingTransactionDescription( amount: monero.PendingTransaction_amount(txptr), fee: monero.PendingTransaction_fee(txptr), @@ -255,21 +263,6 @@ Future _createTransactionSync(Map args) async { preferredInputs: preferredInputs); } -PendingTransactionDescription _createTransactionMultDestSync(Map args) { - final outputs = args['outputs'] as List; - final paymentId = args['paymentId'] as String; - final priorityRaw = args['priorityRaw'] as int; - final accountIndex = args['accountIndex'] as int; - final preferredInputs = args['preferredInputs'] as List; - - return createTransactionMultDestSync( - outputs: outputs, - paymentId: paymentId, - priorityRaw: priorityRaw, - accountIndex: accountIndex, - preferredInputs: preferredInputs); -} - Future createTransaction( {required String address, required int priorityRaw, @@ -286,21 +279,6 @@ Future createTransaction( 'preferredInputs': preferredInputs }); -Future createTransactionMultDest( - {required List outputs, - required int priorityRaw, - String paymentId = '', - int accountIndex = 0, - List preferredInputs = const []}) async => - _createTransactionMultDestSync({ - 'outputs': outputs, - 'paymentId': paymentId, - 'priorityRaw': priorityRaw, - 'accountIndex': accountIndex, - 'preferredInputs': preferredInputs - }); - - class Transaction { final String displayLabel; late final String subaddressLabel = monero.Wallet_getSubaddressLabel( diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 5637f82e5..0a56c5cbc 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -402,6 +402,7 @@ abstract class MoneroWalletBase extends WalletBase Date: Wed, 26 Mar 2025 15:03:02 +0200 Subject: [PATCH 063/243] fix Monero view only wallet (#2121) --- lib/buy/onramper/onramper_buy_provider.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/buy/onramper/onramper_buy_provider.dart b/lib/buy/onramper/onramper_buy_provider.dart index 5480ab2cd..1fd72cccb 100644 --- a/lib/buy/onramper/onramper_buy_provider.dart +++ b/lib/buy/onramper/onramper_buy_provider.dart @@ -222,8 +222,7 @@ class OnRamperBuyProvider extends BuyProvider { '${prefix}defaultAmount': amount.toString(), if (paymentMethod != null) '${prefix}defaultPaymentMethod': paymentMethod, 'onlyOnramps': quote.rampId, - 'networkWallets': '$defaultCrypto:$cryptoCurrencyAddress', - 'walletAddress': cryptoCurrencyAddress, + 'networkWallets': '${quote.cryptoCurrency.fullName?.toUpperCase()}:$cryptoCurrencyAddress', 'supportSwap': "false", 'primaryColor': primaryColor, 'secondaryColor': secondaryColor, From 2cedd53d139796673400f84e3c3336396e90fea1 Mon Sep 17 00:00:00 2001 From: cyan Date: Wed, 26 Mar 2025 14:08:57 +0100 Subject: [PATCH 064/243] remove autogenerated files and add them to gitignore (#2108) --- .gitignore | 23 ++++++++++ .../flutter/generated_plugin_registrant.cc | 15 ------- .../flutter/generated_plugin_registrant.h | 15 ------- .../linux/flutter/generated_plugins.cmake | 24 ---------- .../.plugin_symlinks/path_provider_linux | 1 - .../flutter/generated_plugin_registrant.cc | 11 ----- .../flutter/generated_plugin_registrant.h | 15 ------- .../linux/flutter/generated_plugins.cmake | 23 ---------- linux/flutter/generated_plugin_registrant.cc | 27 ------------ linux/flutter/generated_plugin_registrant.h | 15 ------- linux/flutter/generated_plugins.cmake | 28 ------------ macos/Flutter/GeneratedPluginRegistrant.swift | 44 ------------------- .../flutter/generated_plugin_registrant.cc | 35 --------------- windows/flutter/generated_plugin_registrant.h | 15 ------- windows/flutter/generated_plugins.cmake | 32 -------------- 15 files changed, 23 insertions(+), 300 deletions(-) delete mode 100644 cw_monero/example/linux/flutter/generated_plugin_registrant.cc delete mode 100644 cw_monero/example/linux/flutter/generated_plugin_registrant.h delete mode 100644 cw_monero/example/linux/flutter/generated_plugins.cmake delete mode 120000 cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux delete mode 100644 cw_monero/linux/flutter/generated_plugin_registrant.cc delete mode 100644 cw_monero/linux/flutter/generated_plugin_registrant.h delete mode 100644 cw_monero/linux/flutter/generated_plugins.cmake delete mode 100644 linux/flutter/generated_plugin_registrant.cc delete mode 100644 linux/flutter/generated_plugin_registrant.h delete mode 100644 linux/flutter/generated_plugins.cmake delete mode 100644 macos/Flutter/GeneratedPluginRegistrant.swift delete mode 100644 windows/flutter/generated_plugin_registrant.cc delete mode 100644 windows/flutter/generated_plugin_registrant.h delete mode 100644 windows/flutter/generated_plugins.cmake diff --git a/.gitignore b/.gitignore index e78b1c4e6..f57b19d82 100644 --- a/.gitignore +++ b/.gitignore @@ -185,3 +185,26 @@ ios/MoneroWallet.framework/MoneroWallet ios/WowneroWallet.framework/WowneroWallet ios/ZanoWallet.framework/ZanoWallet *_libwallet2_api_c.dylib + +# macOS +**/Flutter/ephemeral/ +**/Pods/ +**/macos/Flutter/GeneratedPluginRegistrant.swift +**/macos/Flutter/ephemeral +**/xcuserdata/ + +# Windows +**/windows/flutter/ephemeral/ +**/windows/flutter/generated_plugin_registrant.cc +**/windows/flutter/generated_plugin_registrant.h +**/windows/flutter/generated_plugins.cmake + +# Linux +**/linux/flutter/ephemeral/ +**/linux/flutter/generated_plugin_registrant.cc +**/linux/flutter/generated_plugin_registrant.h +**/linux/flutter/generated_plugins.cmake + +cake_wallet.flatpak +flatpak-build/ +export/ \ No newline at end of file diff --git a/cw_monero/example/linux/flutter/generated_plugin_registrant.cc b/cw_monero/example/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 1936c88a6..000000000 --- a/cw_monero/example/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include - -void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) cw_monero_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "CwMoneroPlugin"); - cw_monero_plugin_register_with_registrar(cw_monero_registrar); -} diff --git a/cw_monero/example/linux/flutter/generated_plugin_registrant.h b/cw_monero/example/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47bc..000000000 --- a/cw_monero/example/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/cw_monero/example/linux/flutter/generated_plugins.cmake b/cw_monero/example/linux/flutter/generated_plugins.cmake deleted file mode 100644 index efcc9a8f9..000000000 --- a/cw_monero/example/linux/flutter/generated_plugins.cmake +++ /dev/null @@ -1,24 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST - cw_monero -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux b/cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux deleted file mode 120000 index a2b4915e7..000000000 --- a/cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux +++ /dev/null @@ -1 +0,0 @@ -/Users/user/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ \ No newline at end of file diff --git a/cw_monero/linux/flutter/generated_plugin_registrant.cc b/cw_monero/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index e71a16d23..000000000 --- a/cw_monero/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void fl_register_plugins(FlPluginRegistry* registry) { -} diff --git a/cw_monero/linux/flutter/generated_plugin_registrant.h b/cw_monero/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47bc..000000000 --- a/cw_monero/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/cw_monero/linux/flutter/generated_plugins.cmake b/cw_monero/linux/flutter/generated_plugins.cmake deleted file mode 100644 index 2e1de87a7..000000000 --- a/cw_monero/linux/flutter/generated_plugins.cmake +++ /dev/null @@ -1,23 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 01b922894..000000000 --- a/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,27 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include -#include -#include -#include - -void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) devicelocale_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "DevicelocalePlugin"); - devicelocale_plugin_register_with_registrar(devicelocale_registrar); - g_autoptr(FlPluginRegistrar) flutter_local_authentication_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterLocalAuthenticationPlugin"); - flutter_local_authentication_plugin_register_with_registrar(flutter_local_authentication_registrar); - g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); - flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); - g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); - url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); -} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47bc..000000000 --- a/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake deleted file mode 100644 index f52be7481..000000000 --- a/linux/flutter/generated_plugins.cmake +++ /dev/null @@ -1,28 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST - devicelocale - flutter_local_authentication - flutter_secure_storage_linux - url_launcher_linux -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST - sp_scanner -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index 528de8c42..000000000 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import connectivity_plus -import cw_decred -import cw_mweb -import device_info_plus -import devicelocale -import fast_scanner -import flutter_inappwebview_macos -import flutter_local_authentication -import flutter_secure_storage_macos -import in_app_review -import package_info_plus -import path_provider_foundation -import share_plus -import shared_preferences_foundation -import universal_ble -import url_launcher_macos -import wakelock_plus - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) - CwDecredPlugin.register(with: registry.registrar(forPlugin: "CwDecredPlugin")) - CwMwebPlugin.register(with: registry.registrar(forPlugin: "CwMwebPlugin")) - DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) - DevicelocalePlugin.register(with: registry.registrar(forPlugin: "DevicelocalePlugin")) - MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) - InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) - FlutterLocalAuthenticationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalAuthenticationPlugin")) - FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) - InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) - FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) - SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) - SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) - UniversalBlePlugin.register(with: registry.registrar(forPlugin: "UniversalBlePlugin")) - UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) - WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) -} diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 4deae3420..000000000 --- a/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,35 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -void RegisterPlugins(flutter::PluginRegistry* registry) { - ConnectivityPlusWindowsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); - FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); - FlutterLocalAuthenticationPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FlutterLocalAuthenticationPluginCApi")); - FlutterSecureStorageWindowsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); - PermissionHandlerWindowsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); - SharePlusWindowsPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); - UniversalBlePluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("UniversalBlePluginCApi")); - UrlLauncherWindowsRegisterWithRegistrar( - registry->GetRegistrarForPlugin("UrlLauncherWindows")); -} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d85a..000000000 --- a/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake deleted file mode 100644 index e0f2c11c0..000000000 --- a/windows/flutter/generated_plugins.cmake +++ /dev/null @@ -1,32 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST - connectivity_plus - flutter_inappwebview_windows - flutter_local_authentication - flutter_secure_storage_windows - permission_handler_windows - share_plus - universal_ble - url_launcher_windows -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST - sp_scanner -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) From 78f0df1748db625cd8c9c9d8d3bd3e8d8405bd6c Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Wed, 26 Mar 2025 17:36:13 +0200 Subject: [PATCH 065/243] Update mweb (#2076) * update mweb * update mweb (synchronization should be 2x faster) * update mweb version --- Dockerfile | 2 +- scripts/android/build_mwebd.sh | 8 ++++---- scripts/ios/build_mwebd.sh | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index d9c99da0b..7fa47192e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ LABEL org.opencontainers.image.source=https://github.com/cake-tech/cake_wallet # Set necessary environment variables # Set Go version to latest known-working version -ENV GOLANG_VERSION=1.23.4 +ENV GOLANG_VERSION=1.24.1 # Pin Flutter version to latest known-working version ENV FLUTTER_VERSION=3.27.4 diff --git a/scripts/android/build_mwebd.sh b/scripts/android/build_mwebd.sh index cd4e2c1f4..3e9c5199d 100755 --- a/scripts/android/build_mwebd.sh +++ b/scripts/android/build_mwebd.sh @@ -1,9 +1,9 @@ if [[ "$1" == "--dont-install" ]]; then echo "Skipping Go installation as per --dont-install flag" else - # install go > 1.23: - wget https://go.dev/dl/go1.23.1.linux-amd64.tar.gz - sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.1.linux-amd64.tar.gz + # install go > 1.24: + wget https://go.dev/dl/go1.24.1.linux-amd64.tar.gz + sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.24.1.linux-amd64.tar.gz export PATH=$PATH:/usr/local/go/bin export PATH=$PATH:~/go/bin go install golang.org/x/mobile/cmd/gomobile@latest @@ -13,7 +13,7 @@ fi # build mwebd: git clone https://github.com/ltcmweb/mwebd cd mwebd -git reset --hard 555349415f76a42ec5c76152b64c4ab9aabc448f +git reset --hard f75cb9edef07d03360ee2d33e6edae4243a890b1 gomobile bind -target=android -androidapi 21 . mkdir -p ../../../cw_mweb/android/libs/ cp ./mwebd.aar $_ \ No newline at end of file diff --git a/scripts/ios/build_mwebd.sh b/scripts/ios/build_mwebd.sh index 5bdd32e15..456d43bb8 100755 --- a/scripts/ios/build_mwebd.sh +++ b/scripts/ios/build_mwebd.sh @@ -2,7 +2,7 @@ if [[ "$1" == "--dont-install" ]]; then echo "Skipping Go installation as per --dont-install flag" else - # install go > 1.23: + # install go > 1.24: brew install go export PATH=$PATH:~/go/bin go install golang.org/x/mobile/cmd/gomobile@latest @@ -12,7 +12,7 @@ fi # build mwebd: git clone https://github.com/ltcmweb/mwebd cd mwebd -git reset --hard 555349415f76a42ec5c76152b64c4ab9aabc448f +git reset --hard f75cb9edef07d03360ee2d33e6edae4243a890b1 gomobile bind -target=ios . mv -fn ./Mwebd.xcframework ../../../cw_mweb/ios/ # cleanup: From 604432a66c3f36598e561cb5a4e2bb802825c21a Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Thu, 27 Mar 2025 03:47:49 +0200 Subject: [PATCH 066/243] Fixate mobx as it's causing an issue --- pubspec_base.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec_base.yaml b/pubspec_base.yaml index a4a23ed36..2af036db1 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -21,7 +21,7 @@ dependencies: http: ^1.1.0 path_provider: ^2.0.11 mobx: ^2.1.4 - flutter_mobx: ^2.0.6+5 + flutter_mobx: 2.0.6+5 flutter_slidable: ^3.0.1 share_plus: ^10.0.0 # date_range_picker: ^1.0.6 From 72184f5837a8af2b76aa2e7f06e0d9431fb15939 Mon Sep 17 00:00:00 2001 From: cyan Date: Thu, 27 Mar 2025 03:47:45 +0100 Subject: [PATCH 067/243] open wallet even when cache file backup is corrupted (#1720) Co-authored-by: Omar Hatem --- cw_monero/lib/monero_wallet_service.dart | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index 598d400e5..d1bfcd936 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -75,6 +75,12 @@ class MoneroRestoreWalletFromKeysCredentials extends WalletCredentials { final String spendKey; } +enum OpenWalletTry { + initial, + cacheRestored, + cacheRemoved, +} + class MoneroWalletService extends WalletService< MoneroNewWalletCredentials, MoneroRestoreWalletFromSeedCredentials, @@ -139,7 +145,7 @@ class MoneroWalletService extends WalletService< } @override - Future openWallet(String name, String password, {bool? retryOnFailure}) async { + Future openWallet(String name, String password, {OpenWalletTry openWalletTry = OpenWalletTry.initial}) async { try { final path = await pathForWallet(name: name, type: getType()); @@ -172,12 +178,16 @@ class MoneroWalletService extends WalletService< } catch (e) { // TODO: Implement Exception for wallet list service. - if (retryOnFailure == false) { - rethrow; + switch (openWalletTry) { + case OpenWalletTry.initial: + await restoreOrResetWalletFiles(name); + return await openWallet(name, password, openWalletTry: OpenWalletTry.cacheRestored); + case OpenWalletTry.cacheRestored: + await removeCache(name); + return await openWallet(name, password, openWalletTry: OpenWalletTry.cacheRemoved); + case OpenWalletTry.cacheRemoved: + rethrow; } - - await restoreOrResetWalletFiles(name); - return await openWallet(name, password, retryOnFailure: false); } } From 3ec7d01c30134e71d57c447df4ef5e66c96886d6 Mon Sep 17 00:00:00 2001 From: cyan Date: Thu, 27 Mar 2025 13:39:15 +0100 Subject: [PATCH 068/243] add missing import (#2125) --- tool/append_translation.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/tool/append_translation.dart b/tool/append_translation.dart index b4c975599..f626ac494 100644 --- a/tool/append_translation.dart +++ b/tool/append_translation.dart @@ -1,4 +1,5 @@ +import 'print_verbose_dummy.dart'; import 'utils/translation/arb_file_utils.dart'; import 'utils/translation/translation_constants.dart'; import 'utils/translation/translation_utils.dart'; From be81042a0610e5ff7d00a9dc9e6948cd5bf982a2 Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 28 Mar 2025 00:18:50 +0200 Subject: [PATCH 069/243] Cw 1018 minor UI enhancements (#2118) * fix ui for monero batch sending * minor ui fixes * Revert "fix ui for monero batch sending" This reverts commit 4827b17bfeefe897dc7b6580d0520ff02aa9683f. * fix: focus node issue --- .../wallet_group_description_page.dart | 60 ++++++++++--------- ..._group_existing_seed_description_page.dart | 42 ++++++------- lib/src/screens/send/send_page.dart | 5 +- .../info_bottom_sheet_widget.dart | 3 +- 4 files changed, 60 insertions(+), 50 deletions(-) diff --git a/lib/src/screens/new_wallet/wallet_group_description_page.dart b/lib/src/screens/new_wallet/wallet_group_description_page.dart index b566d0422..e10c64bf9 100644 --- a/lib/src/screens/new_wallet/wallet_group_description_page.dart +++ b/lib/src/screens/new_wallet/wallet_group_description_page.dart @@ -29,37 +29,41 @@ class WalletGroupDescriptionPage extends BasePage { Image.asset(currentTheme.type.walletGroupImage, height: 200), SizedBox(height: 32), Expanded( - child: Text.rich( - TextSpan( - children: [ - TextSpan(text: '${S.of(context).wallet_group_description_one} '), + child: Scrollbar( + child: SingleChildScrollView( + child: Text.rich( TextSpan( - text: '${S.of(context).wallet_group.toLowerCase()} ', - style: TextStyle(fontWeight: FontWeight.w700), + children: [ + TextSpan(text: '${S.of(context).wallet_group_description_one} '), + TextSpan( + text: '${S.of(context).wallet_group.toLowerCase()} ', + style: TextStyle(fontWeight: FontWeight.w700), + ), + TextSpan( + text: '${S.of(context).wallet_group_description_two} ', + ), + TextSpan( + text: '${S.of(context).choose_wallet_group} ', + style: TextStyle(fontWeight: FontWeight.w700), + ), + TextSpan( + text: '${S.of(context).wallet_group_description_three} ', + ), + TextSpan( + text: '${S.of(context).create_new_seed} ', + style: TextStyle(fontWeight: FontWeight.w700), + ), + TextSpan(text: S.of(context).wallet_group_description_four), + ], ), - TextSpan( - text: '${S.of(context).wallet_group_description_two} ', + textAlign: TextAlign.center, + style: TextStyle( + height: 1.5, + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.secondaryTextColor, ), - TextSpan( - text: '${S.of(context).choose_wallet_group} ', - style: TextStyle(fontWeight: FontWeight.w700), - ), - TextSpan( - text: '${S.of(context).wallet_group_description_three} ', - ), - TextSpan( - text: '${S.of(context).create_new_seed} ', - style: TextStyle(fontWeight: FontWeight.w700), - ), - TextSpan(text: S.of(context).wallet_group_description_four), - ], - ), - textAlign: TextAlign.center, - style: TextStyle( - height: 1.5, - fontSize: 16, - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.secondaryTextColor, + ), ), ), ), diff --git a/lib/src/screens/new_wallet/wallet_group_existing_seed_description_page.dart b/lib/src/screens/new_wallet/wallet_group_existing_seed_description_page.dart index 4eab8fcec..34e07bbaf 100644 --- a/lib/src/screens/new_wallet/wallet_group_existing_seed_description_page.dart +++ b/lib/src/screens/new_wallet/wallet_group_existing_seed_description_page.dart @@ -32,28 +32,30 @@ class WalletGroupExistingSeedDescriptionPage extends BasePage { Image.asset(currentTheme.type.walletGroupImage, height: 200), SizedBox(height: 32), Expanded( - child: RichText( - text: TextSpan( - children: [ - TextSpan( - text: S.current.wallet_group_description_existing_seed + '\n\n', - style: textStyle), - TextSpan( - text: S.current.wallet_group_description_open_wallet + '\n\n', - style: textStyle), - TextSpan( - text: S.current.wallet_group_description_view_seed + '\n', style: textStyle), - TextSpan( - text: S.current.seed_display_path, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w800, - color: Theme.of(context).extension()!.secondaryTextColor, + child: SingleChildScrollView( + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: S.current.wallet_group_description_existing_seed + '\n\n', + style: textStyle), + TextSpan( + text: S.current.wallet_group_description_open_wallet + '\n\n', + style: textStyle), + TextSpan( + text: S.current.wallet_group_description_view_seed + '\n', style: textStyle), + TextSpan( + text: S.current.seed_display_path, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: Theme.of(context).extension()!.secondaryTextColor, + ), ), - ), - ], + ], + ), + textAlign: TextAlign.center, ), - textAlign: TextAlign.center, ), ), Column( diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index d02d246fa..277986c3f 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -397,7 +397,10 @@ class SendPage extends BasePage { return LoadingPrimaryButton( key: ValueKey('send_page_send_button_key'), onPressed: () async { - FocusManager.instance.primaryFocus?.unfocus(); + + //Request dummy node to get the focus out of the text fields + FocusScope.of(context).requestFocus(FocusNode()); + if (sendViewModel.state is IsExecutingState) return; if (_formKey.currentState != null && !_formKey.currentState!.validate()) { diff --git a/lib/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart b/lib/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart index 26d25b50d..c3ccd7987 100644 --- a/lib/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart +++ b/lib/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart @@ -1,3 +1,4 @@ +import 'package:auto_size_text/auto_size_text.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; @@ -85,7 +86,7 @@ class InfoBottomSheet extends BaseBottomSheet { const Spacer(flex: 2), Expanded( flex: 6, - child: Text( + child: AutoSizeText( content!, textAlign: TextAlign.center, style: TextStyle( From 3d0f66c0bea92aa677a17443fd103eda196d4e0b Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Thu, 27 Mar 2025 15:22:14 -0700 Subject: [PATCH 070/243] CW-1013 Add deuro token (#2124) * add deuro token * move to be before DAI * add asset and update currencies list --------- Co-authored-by: Omar Hatem --- assets/images/deuro_icon.png | Bin 0 -> 135253 bytes cw_core/lib/crypto_currency.dart | 1 + .../lib/default_ethereum_erc20_tokens.dart | 7 +++++++ 3 files changed, 8 insertions(+) create mode 100644 assets/images/deuro_icon.png diff --git a/assets/images/deuro_icon.png b/assets/images/deuro_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4dc068ff8b335d6d467ba92da5ed42a9a93064fd GIT binary patch literal 135253 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 literal 0 HcmV?d00001 diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 00d49c288..7f44d1713 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -231,6 +231,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const ton = CryptoCurrency(title: 'TON', fullName: 'Toncoin', raw: 95, name: 'ton', iconPath: 'assets/images/ton_icon.png', decimals: 8); static const zano = CryptoCurrency(title: 'ZANO', tag: 'ZANO', fullName: 'Zano', raw: 96, name: 'zano', iconPath: 'assets/images/zano_icon.png', decimals: 12); static const flip = CryptoCurrency(title: 'FLIP', tag: 'ETH', fullName: 'Chainflip', raw: 97, name: 'flip', iconPath: 'assets/images/flip_icon.png', decimals: 18); + static const deuro = CryptoCurrency(title: 'DEURO', tag: 'ETH', fullName: 'dEuro', raw: 98, name: 'deuro', iconPath: 'assets/images/deuro_icon.png', decimals: 18); static final Map _rawCurrencyMap = [...all, ...havenCurrencies].fold>({}, (acc, item) { diff --git a/cw_ethereum/lib/default_ethereum_erc20_tokens.dart b/cw_ethereum/lib/default_ethereum_erc20_tokens.dart index ee60a3d6c..209acb751 100644 --- a/cw_ethereum/lib/default_ethereum_erc20_tokens.dart +++ b/cw_ethereum/lib/default_ethereum_erc20_tokens.dart @@ -17,6 +17,13 @@ class DefaultEthereumErc20Tokens { decimal: 6, enabled: true, ), + Erc20Token( + name: "dEuro", + symbol: "dEUR", + contractAddress: "0xbA3f535bbCcCcA2A154b573Ca6c5A49BAAE0a3ea", + decimal: 18, + enabled: true, + ), Erc20Token( name: "Dai", symbol: "DAI", From 66d6fd478b91c6268970c6b7339adbaae51f91df Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Thu, 27 Mar 2025 16:06:20 -0700 Subject: [PATCH 071/243] Cw 1013 deuro minor fix (#2130) * add deuro token * move to be before DAI * add asset and update currencies list * fix icon * remove print --------- Co-authored-by: Omar Hatem --- cw_core/lib/crypto_currency.dart | 5 +++-- cw_ethereum/lib/default_ethereum_erc20_tokens.dart | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 7f44d1713..cb8485ec5 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -110,7 +110,8 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen CryptoCurrency.wow, CryptoCurrency.zano, CryptoCurrency.ton, - CryptoCurrency.flip + CryptoCurrency.flip, + CryptoCurrency.deuro ]; static const havenCurrencies = [ @@ -231,7 +232,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const ton = CryptoCurrency(title: 'TON', fullName: 'Toncoin', raw: 95, name: 'ton', iconPath: 'assets/images/ton_icon.png', decimals: 8); static const zano = CryptoCurrency(title: 'ZANO', tag: 'ZANO', fullName: 'Zano', raw: 96, name: 'zano', iconPath: 'assets/images/zano_icon.png', decimals: 12); static const flip = CryptoCurrency(title: 'FLIP', tag: 'ETH', fullName: 'Chainflip', raw: 97, name: 'flip', iconPath: 'assets/images/flip_icon.png', decimals: 18); - static const deuro = CryptoCurrency(title: 'DEURO', tag: 'ETH', fullName: 'dEuro', raw: 98, name: 'deuro', iconPath: 'assets/images/deuro_icon.png', decimals: 18); + static const deuro = CryptoCurrency(title: 'DEURO', tag: 'ETH', fullName: 'Digital Euro', raw: 98, name: 'deuro', iconPath: 'assets/images/deuro_icon.png', decimals: 18); static final Map _rawCurrencyMap = [...all, ...havenCurrencies].fold>({}, (acc, item) { diff --git a/cw_ethereum/lib/default_ethereum_erc20_tokens.dart b/cw_ethereum/lib/default_ethereum_erc20_tokens.dart index 209acb751..8381744d6 100644 --- a/cw_ethereum/lib/default_ethereum_erc20_tokens.dart +++ b/cw_ethereum/lib/default_ethereum_erc20_tokens.dart @@ -18,8 +18,8 @@ class DefaultEthereumErc20Tokens { enabled: true, ), Erc20Token( - name: "dEuro", - symbol: "dEUR", + name: "Digital Euro", + symbol: "DEURO", contractAddress: "0xbA3f535bbCcCcA2A154b573Ca6c5A49BAAE0a3ea", decimal: 18, enabled: true, From 16b180add04301d039d34b3042040cae34f7eacb Mon Sep 17 00:00:00 2001 From: cyan Date: Fri, 28 Mar 2025 00:08:43 +0100 Subject: [PATCH 072/243] rename background sync cache file (#2129) --- cw_monero/lib/monero_wallet.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 0a56c5cbc..eff5ae909 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -506,6 +506,7 @@ abstract class MoneroWalletBase extends WalletBase Date: Fri, 28 Mar 2025 13:22:24 +0200 Subject: [PATCH 073/243] New app icon (#2123) * Change Cake Wallet Logo * minor [skip ci] * Change Cake Wallet Logo * Change Cake Wallet Logo * Update app icon * Update app icon * default rpc.nano node * update android version [skip ci] * update android version [skip ci] --- assets/images/2.0x/Telegram.png | Bin 2913 -> 0 bytes assets/images/2.0x/Twitter.png | Bin 1374 -> 0 bytes .../images/2.0x/arrow_bottom_cake_green.png | Bin 410 -> 0 bytes assets/images/2.0x/back_arrow.png | Bin 540 -> 0 bytes assets/images/2.0x/back_arrow_dark_theme.png | Bin 496 -> 0 bytes assets/images/2.0x/back_vector.png | Bin 337 -> 0 bytes assets/images/2.0x/backup.png | Bin 5275 -> 0 bytes assets/images/2.0x/bitcoin.png | Bin 2418 -> 0 bytes assets/images/2.0x/bitcoin_menu.png | Bin 2395 -> 0 bytes assets/images/2.0x/bitmap.png | Bin 185925 -> 0 bytes assets/images/2.0x/cake_arrow.png | Bin 434 -> 0 bytes assets/images/2.0x/cake_logo.png | Bin 6397 -> 0 bytes assets/images/2.0x/change_now.png | Bin 2367 -> 0 bytes assets/images/2.0x/china.png | Bin 1184 -> 0 bytes assets/images/2.0x/close.png | Bin 378 -> 0 bytes assets/images/2.0x/close_button.png | Bin 1373 -> 0 bytes .../images/2.0x/close_button_dark_theme.png | Bin 1524 -> 0 bytes assets/images/2.0x/coins.png | Bin 971 -> 0 bytes assets/images/2.0x/copy_address.png | Bin 802 -> 0 bytes assets/images/2.0x/copy_content.png | Bin 517 -> 0 bytes assets/images/2.0x/crypto_lock.png | Bin 187826 -> 0 bytes assets/images/2.0x/crypto_lock_light.png | Bin 95852 -> 0 bytes assets/images/2.0x/decred.png | Bin 2112 -> 0 bytes assets/images/2.0x/decred_menu.png | Bin 1871 -> 0 bytes assets/images/2.0x/delete_icon.png | Bin 762 -> 0 bytes assets/images/2.0x/down_arrow.png | Bin 428 -> 0 bytes assets/images/2.0x/exchange.png | Bin 1079 -> 0 bytes assets/images/2.0x/eye.png | Bin 873 -> 0 bytes assets/images/2.0x/eye_action.png | Bin 831 -> 0 bytes assets/images/2.0x/eye_menu.png | Bin 939 -> 0 bytes assets/images/2.0x/face.png | Bin 836 -> 0 bytes assets/images/2.0x/filter_button.png | Bin 1344 -> 0 bytes assets/images/2.0x/filter_light_button.png | Bin 1363 -> 0 bytes assets/images/2.0x/germany.png | Bin 1017 -> 0 bytes assets/images/2.0x/github.png | Bin 981 -> 0 bytes assets/images/2.0x/header.png | Bin 186 -> 0 bytes assets/images/2.0x/holland.png | Bin 1001 -> 0 bytes assets/images/2.0x/india.png | Bin 1413 -> 0 bytes assets/images/2.0x/japan.png | Bin 1118 -> 0 bytes assets/images/2.0x/key.png | Bin 667 -> 0 bytes assets/images/2.0x/key_menu.png | Bin 923 -> 0 bytes assets/images/2.0x/keysIco.png | Bin 105407 -> 0 bytes assets/images/2.0x/litecoin.png | Bin 1980 -> 0 bytes assets/images/2.0x/load.png | Bin 1722 -> 0 bytes assets/images/2.0x/menu_button.png | Bin 226 -> 0 bytes assets/images/2.0x/monero.png | Bin 2230 -> 0 bytes assets/images/2.0x/monero_menu.png | Bin 2804 -> 0 bytes assets/images/2.0x/morph_icon.png | Bin 1552 -> 0 bytes assets/images/2.0x/new_wallet.png | Bin 270 -> 0 bytes assets/images/2.0x/nodes.png | Bin 1142 -> 0 bytes assets/images/2.0x/nodes_menu.png | Bin 1085 -> 0 bytes assets/images/2.0x/open_book.png | Bin 685 -> 0 bytes assets/images/2.0x/open_book_menu.png | Bin 735 -> 0 bytes assets/images/2.0x/paste_ios.png | Bin 903 -> 0 bytes assets/images/2.0x/poland.png | Bin 959 -> 0 bytes assets/images/2.0x/portugal.png | Bin 1621 -> 0 bytes assets/images/2.0x/pre_seed_dark.png | Bin 186268 -> 0 bytes assets/images/2.0x/pre_seed_light.png | Bin 98188 -> 0 bytes assets/images/2.0x/privacy_menu.png | Bin 1324 -> 0 bytes assets/images/2.0x/qr_code_icon.png | Bin 1142 -> 0 bytes assets/images/2.0x/qr_icon.png | Bin 715 -> 0 bytes assets/images/2.0x/question_mark.png | Bin 1240 -> 0 bytes assets/images/2.0x/reconnect.png | Bin 793 -> 0 bytes assets/images/2.0x/reconnect_menu.png | Bin 875 -> 0 bytes assets/images/2.0x/refresh_icon.png | Bin 2137 -> 0 bytes assets/images/2.0x/restoreSeed.png | Bin 170439 -> 0 bytes assets/images/2.0x/restore_keys.png | Bin 5508 -> 0 bytes assets/images/2.0x/restore_seed.png | Bin 5180 -> 0 bytes assets/images/2.0x/restore_wallet.png | Bin 451 -> 0 bytes assets/images/2.0x/restore_wallet_image.png | Bin 4392 -> 0 bytes assets/images/2.0x/right_arrow.png | Bin 244 -> 0 bytes assets/images/2.0x/russia.png | Bin 954 -> 0 bytes assets/images/2.0x/scanner.png | Bin 1374 -> 0 bytes assets/images/2.0x/seedIco.png | Bin 109183 -> 0 bytes assets/images/2.0x/seedKeys.png | Bin 195198 -> 0 bytes assets/images/2.0x/seed_image.png | Bin 63428 -> 0 bytes assets/images/2.0x/select_arrow.png | Bin 385 -> 0 bytes assets/images/2.0x/send.png | Bin 582 -> 0 bytes assets/images/2.0x/settings.png | Bin 830 -> 0 bytes assets/images/2.0x/settings_menu.png | Bin 1051 -> 0 bytes assets/images/2.0x/share.png | Bin 798 -> 0 bytes assets/images/2.0x/south_korea.png | Bin 1354 -> 0 bytes assets/images/2.0x/spain.png | Bin 944 -> 0 bytes assets/images/2.0x/to_icon.png | Bin 235 -> 0 bytes assets/images/2.0x/transfer.png | Bin 1081 -> 0 bytes assets/images/2.0x/trash.png | Bin 913 -> 0 bytes assets/images/2.0x/triangle.png | Bin 202 -> 0 bytes assets/images/2.0x/up_arrow.png | Bin 429 -> 0 bytes assets/images/2.0x/upload.png | Bin 760 -> 0 bytes assets/images/2.0x/usa.png | Bin 1678 -> 0 bytes assets/images/2.0x/wallet.png | Bin 551 -> 0 bytes assets/images/2.0x/wallet_menu.png | Bin 659 -> 0 bytes assets/images/2.0x/wallet_name.png | Bin 149723 -> 0 bytes assets/images/2.0x/wallet_name_light.png | Bin 154924 -> 0 bytes assets/images/2.0x/wallet_type.png | Bin 148854 -> 0 bytes assets/images/2.0x/wallet_type_light.png | Bin 153456 -> 0 bytes assets/images/2.0x/welcome.png | Bin 396916 -> 0 bytes assets/images/2.0x/welcomeImg.png | Bin 458672 -> 0 bytes assets/images/2.0x/welcome_light.png | Bin 322350 -> 0 bytes assets/images/2.0x/xmr_btc.png | Bin 1988 -> 0 bytes assets/images/3.0x/Telegram.png | Bin 5285 -> 0 bytes assets/images/3.0x/Twitter.png | Bin 2018 -> 0 bytes .../images/3.0x/arrow_bottom_cake_green.png | Bin 573 -> 0 bytes assets/images/3.0x/back_arrow.png | Bin 703 -> 0 bytes assets/images/3.0x/back_arrow_dark_theme.png | Bin 673 -> 0 bytes assets/images/3.0x/back_vector.png | Bin 459 -> 0 bytes assets/images/3.0x/backup.png | Bin 8976 -> 0 bytes assets/images/3.0x/bitcoin.png | Bin 3975 -> 0 bytes assets/images/3.0x/bitcoin_menu.png | Bin 3469 -> 0 bytes assets/images/3.0x/bitmap.png | Bin 403790 -> 0 bytes assets/images/3.0x/cake_arrow.png | Bin 598 -> 0 bytes assets/images/3.0x/cake_logo.png | Bin 9349 -> 0 bytes assets/images/3.0x/change_now.png | Bin 3967 -> 0 bytes assets/images/3.0x/china.png | Bin 1735 -> 0 bytes assets/images/3.0x/close.png | Bin 461 -> 0 bytes assets/images/3.0x/close_button.png | Bin 1938 -> 0 bytes .../images/3.0x/close_button_dark_theme.png | Bin 2039 -> 0 bytes assets/images/3.0x/coins.png | Bin 1409 -> 0 bytes assets/images/3.0x/copy_address.png | Bin 1165 -> 0 bytes assets/images/3.0x/copy_content.png | Bin 599 -> 0 bytes assets/images/3.0x/crypto_lock.png | Bin 385472 -> 0 bytes assets/images/3.0x/crypto_lock_light.png | Bin 195355 -> 0 bytes assets/images/3.0x/decred.png | Bin 2918 -> 0 bytes assets/images/3.0x/decred_menu.png | Bin 1423 -> 0 bytes assets/images/3.0x/delete_icon.png | Bin 1205 -> 0 bytes assets/images/3.0x/down_arrow.png | Bin 559 -> 0 bytes assets/images/3.0x/exchange.png | Bin 1528 -> 0 bytes assets/images/3.0x/eye.png | Bin 1188 -> 0 bytes assets/images/3.0x/eye_action.png | Bin 1307 -> 0 bytes assets/images/3.0x/eye_menu.png | Bin 1398 -> 0 bytes assets/images/3.0x/face.png | Bin 1251 -> 0 bytes assets/images/3.0x/filter_button.png | Bin 1923 -> 0 bytes assets/images/3.0x/filter_light_button.png | Bin 1909 -> 0 bytes assets/images/3.0x/germany.png | Bin 1409 -> 0 bytes assets/images/3.0x/github.png | Bin 1539 -> 0 bytes assets/images/3.0x/header.png | Bin 231 -> 0 bytes assets/images/3.0x/holland.png | Bin 1404 -> 0 bytes assets/images/3.0x/india.png | Bin 2090 -> 0 bytes assets/images/3.0x/japan.png | Bin 1539 -> 0 bytes assets/images/3.0x/key.png | Bin 906 -> 0 bytes assets/images/3.0x/key_menu.png | Bin 1353 -> 0 bytes assets/images/3.0x/keysIco.png | Bin 199368 -> 0 bytes assets/images/3.0x/litecoin.png | Bin 3368 -> 0 bytes assets/images/3.0x/load.png | Bin 2760 -> 0 bytes assets/images/3.0x/menu_button.png | Bin 293 -> 0 bytes assets/images/3.0x/monero.png | Bin 3522 -> 0 bytes assets/images/3.0x/monero_menu.png | Bin 4077 -> 0 bytes assets/images/3.0x/morph_icon.png | Bin 2358 -> 0 bytes assets/images/3.0x/new_wallet.png | Bin 474 -> 0 bytes assets/images/3.0x/nodes.png | Bin 1472 -> 0 bytes assets/images/3.0x/nodes_menu.png | Bin 1476 -> 0 bytes assets/images/3.0x/open_book.png | Bin 1005 -> 0 bytes assets/images/3.0x/open_book_menu.png | Bin 994 -> 0 bytes assets/images/3.0x/paste_ios.png | Bin 903 -> 0 bytes assets/images/3.0x/poland.png | Bin 1365 -> 0 bytes assets/images/3.0x/portugal.png | Bin 2320 -> 0 bytes assets/images/3.0x/pre_seed_dark.png | Bin 383270 -> 0 bytes assets/images/3.0x/pre_seed_light.png | Bin 200270 -> 0 bytes assets/images/3.0x/privacy_menu.png | Bin 1954 -> 0 bytes assets/images/3.0x/qr_code_icon.png | Bin 1256 -> 0 bytes assets/images/3.0x/qr_icon.png | Bin 755 -> 0 bytes assets/images/3.0x/question_mark.png | Bin 1662 -> 0 bytes assets/images/3.0x/reconnect.png | Bin 1122 -> 0 bytes assets/images/3.0x/reconnect_menu.png | Bin 1281 -> 0 bytes assets/images/3.0x/refresh_icon.png | Bin 3457 -> 0 bytes assets/images/3.0x/restoreSeed.png | Bin 357747 -> 0 bytes assets/images/3.0x/restore_keys.png | Bin 9161 -> 0 bytes assets/images/3.0x/restore_seed.png | Bin 9188 -> 0 bytes assets/images/3.0x/restore_wallet.png | Bin 586 -> 0 bytes assets/images/3.0x/restore_wallet_image.png | Bin 7359 -> 0 bytes assets/images/3.0x/right_arrow.png | Bin 268 -> 0 bytes assets/images/3.0x/russia.png | Bin 1362 -> 0 bytes assets/images/3.0x/scanner.png | Bin 1950 -> 0 bytes assets/images/3.0x/seedIco.png | Bin 214832 -> 0 bytes assets/images/3.0x/seedKeys.png | Bin 421309 -> 0 bytes assets/images/3.0x/seed_image.png | Bin 135525 -> 0 bytes assets/images/3.0x/select_arrow.png | Bin 497 -> 0 bytes assets/images/3.0x/send.png | Bin 836 -> 0 bytes assets/images/3.0x/settings.png | Bin 1249 -> 0 bytes assets/images/3.0x/settings_menu.png | Bin 1444 -> 0 bytes assets/images/3.0x/share.png | Bin 1170 -> 0 bytes assets/images/3.0x/south_korea.png | Bin 1895 -> 0 bytes assets/images/3.0x/spain.png | Bin 1343 -> 0 bytes assets/images/3.0x/to_icon.png | Bin 267 -> 0 bytes assets/images/3.0x/transfer.png | Bin 1609 -> 0 bytes assets/images/3.0x/trash.png | Bin 1278 -> 0 bytes assets/images/3.0x/triangle.png | Bin 242 -> 0 bytes assets/images/3.0x/up_arrow.png | Bin 479 -> 0 bytes assets/images/3.0x/upload.png | Bin 1082 -> 0 bytes assets/images/3.0x/usa.png | Bin 2328 -> 0 bytes assets/images/3.0x/wallet.png | Bin 733 -> 0 bytes assets/images/3.0x/wallet_menu.png | Bin 920 -> 0 bytes assets/images/3.0x/wallet_name.png | Bin 292447 -> 0 bytes assets/images/3.0x/wallet_name_light.png | Bin 302113 -> 0 bytes assets/images/3.0x/wallet_type.png | Bin 288654 -> 0 bytes assets/images/3.0x/wallet_type_light.png | Bin 300592 -> 0 bytes assets/images/3.0x/welcome.png | Bin 809962 -> 0 bytes assets/images/3.0x/welcomeImg.png | Bin 996855 -> 0 bytes assets/images/3.0x/welcome_light.png | Bin 654903 -> 0 bytes assets/images/3.0x/xmr_btc.png | Bin 3154 -> 0 bytes assets/images/cake_logo.png | Bin 3120 -> 1673 bytes assets/images/cakewallet_android_icon.png | Bin 31276 -> 53524 bytes .../mipmap-anydpi-v26/ic_launcher.xml | 6 +++--- .../mipmap-hdpi/ic_launcher.png | Bin 2552 -> 5110 bytes .../mipmap-hdpi/ic_launcher_adaptive_back.png | Bin 509 -> 0 bytes .../mipmap-hdpi/ic_launcher_adaptive_fore.png | Bin 3709 -> 0 bytes .../mipmap-hdpi/ic_launcher_adaptive_mono.png | Bin 2359 -> 0 bytes .../mipmap-hdpi/ic_launcher_background.png | Bin 0 -> 852 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 6614 bytes .../mipmap-hdpi/ic_launcher_monochrome.png | Bin 0 -> 6614 bytes .../mipmap-mdpi/ic_launcher.png | Bin 1697 -> 2922 bytes .../mipmap-mdpi/ic_launcher_adaptive_back.png | Bin 327 -> 0 bytes .../mipmap-mdpi/ic_launcher_adaptive_fore.png | Bin 2215 -> 0 bytes .../mipmap-mdpi/ic_launcher_adaptive_mono.png | Bin 1544 -> 0 bytes .../mipmap-mdpi/ic_launcher_background.png | Bin 0 -> 459 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 3337 bytes .../mipmap-mdpi/ic_launcher_monochrome.png | Bin 0 -> 3337 bytes .../mipmap-xhdpi/ic_launcher.png | Bin 3184 -> 6884 bytes .../ic_launcher_adaptive_back.png | Bin 677 -> 0 bytes .../ic_launcher_adaptive_fore.png | Bin 5481 -> 0 bytes .../ic_launcher_adaptive_mono.png | Bin 3285 -> 0 bytes .../mipmap-xhdpi/ic_launcher_background.png | Bin 0 -> 1317 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 8738 bytes .../mipmap-xhdpi/ic_launcher_monochrome.png | Bin 0 -> 8738 bytes .../mipmap-xxhdpi/ic_launcher.png | Bin 5104 -> 12165 bytes .../ic_launcher_adaptive_back.png | Bin 1017 -> 0 bytes .../ic_launcher_adaptive_fore.png | Bin 9464 -> 0 bytes .../ic_launcher_adaptive_mono.png | Bin 5258 -> 0 bytes .../mipmap-xxhdpi/ic_launcher_background.png | Bin 0 -> 2949 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 17014 bytes .../mipmap-xxhdpi/ic_launcher_monochrome.png | Bin 0 -> 17014 bytes .../mipmap-xxxhdpi/ic_launcher.png | Bin 7289 -> 16871 bytes .../ic_launcher_adaptive_back.png | Bin 1703 -> 0 bytes .../ic_launcher_adaptive_fore.png | Bin 13870 -> 0 bytes .../ic_launcher_adaptive_mono.png | Bin 7640 -> 0 bytes .../mipmap-xxxhdpi/ic_launcher_background.png | Bin 0 -> 4232 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 24638 bytes .../mipmap-xxxhdpi/ic_launcher_monochrome.png | Bin 0 -> 24638 bytes assets/images/cakewallet_app_logo.png | Bin 31276 -> 0 bytes assets/images/cakewallet_icon_1024.png | Bin 98397 -> 212222 bytes assets/images/cakewallet_icon_120.png | Bin 11763 -> 6323 bytes assets/images/cakewallet_icon_180.png | Bin 16773 -> 9918 bytes assets/images/cakewallet_logo.png | Bin 131092 -> 211467 bytes assets/nano_node_list.yml | 2 +- scripts/android/app_env.sh | 4 ++-- scripts/ios/app_env.sh | 4 ++-- scripts/ios/app_icon.sh | 4 ---- tool/append_translation.dart | 1 - 248 files changed, 8 insertions(+), 13 deletions(-) delete mode 100644 assets/images/2.0x/Telegram.png delete mode 100644 assets/images/2.0x/Twitter.png delete mode 100644 assets/images/2.0x/arrow_bottom_cake_green.png delete mode 100644 assets/images/2.0x/back_arrow.png delete mode 100644 assets/images/2.0x/back_arrow_dark_theme.png delete mode 100644 assets/images/2.0x/back_vector.png delete mode 100644 assets/images/2.0x/backup.png delete mode 100644 assets/images/2.0x/bitcoin.png delete mode 100644 assets/images/2.0x/bitcoin_menu.png delete mode 100644 assets/images/2.0x/bitmap.png delete mode 100644 assets/images/2.0x/cake_arrow.png delete mode 100644 assets/images/2.0x/cake_logo.png delete mode 100644 assets/images/2.0x/change_now.png delete mode 100644 assets/images/2.0x/china.png delete mode 100644 assets/images/2.0x/close.png delete mode 100644 assets/images/2.0x/close_button.png delete mode 100644 assets/images/2.0x/close_button_dark_theme.png delete mode 100644 assets/images/2.0x/coins.png delete mode 100644 assets/images/2.0x/copy_address.png delete mode 100644 assets/images/2.0x/copy_content.png delete mode 100644 assets/images/2.0x/crypto_lock.png delete mode 100644 assets/images/2.0x/crypto_lock_light.png delete mode 100644 assets/images/2.0x/decred.png delete mode 100644 assets/images/2.0x/decred_menu.png delete mode 100644 assets/images/2.0x/delete_icon.png delete mode 100644 assets/images/2.0x/down_arrow.png delete mode 100644 assets/images/2.0x/exchange.png delete mode 100644 assets/images/2.0x/eye.png delete mode 100644 assets/images/2.0x/eye_action.png delete mode 100644 assets/images/2.0x/eye_menu.png delete mode 100644 assets/images/2.0x/face.png delete mode 100644 assets/images/2.0x/filter_button.png delete mode 100644 assets/images/2.0x/filter_light_button.png delete mode 100644 assets/images/2.0x/germany.png delete mode 100644 assets/images/2.0x/github.png delete mode 100644 assets/images/2.0x/header.png delete mode 100644 assets/images/2.0x/holland.png delete mode 100644 assets/images/2.0x/india.png delete mode 100644 assets/images/2.0x/japan.png delete mode 100644 assets/images/2.0x/key.png delete mode 100644 assets/images/2.0x/key_menu.png delete mode 100644 assets/images/2.0x/keysIco.png delete mode 100644 assets/images/2.0x/litecoin.png delete mode 100644 assets/images/2.0x/load.png delete mode 100644 assets/images/2.0x/menu_button.png delete mode 100644 assets/images/2.0x/monero.png delete mode 100644 assets/images/2.0x/monero_menu.png delete mode 100644 assets/images/2.0x/morph_icon.png delete mode 100644 assets/images/2.0x/new_wallet.png delete mode 100644 assets/images/2.0x/nodes.png delete mode 100644 assets/images/2.0x/nodes_menu.png delete mode 100644 assets/images/2.0x/open_book.png delete mode 100644 assets/images/2.0x/open_book_menu.png delete mode 100644 assets/images/2.0x/paste_ios.png delete mode 100644 assets/images/2.0x/poland.png delete mode 100644 assets/images/2.0x/portugal.png delete mode 100755 assets/images/2.0x/pre_seed_dark.png delete mode 100755 assets/images/2.0x/pre_seed_light.png delete mode 100644 assets/images/2.0x/privacy_menu.png delete mode 100644 assets/images/2.0x/qr_code_icon.png delete mode 100644 assets/images/2.0x/qr_icon.png delete mode 100644 assets/images/2.0x/question_mark.png delete mode 100644 assets/images/2.0x/reconnect.png delete mode 100644 assets/images/2.0x/reconnect_menu.png delete mode 100644 assets/images/2.0x/refresh_icon.png delete mode 100644 assets/images/2.0x/restoreSeed.png delete mode 100644 assets/images/2.0x/restore_keys.png delete mode 100644 assets/images/2.0x/restore_seed.png delete mode 100644 assets/images/2.0x/restore_wallet.png delete mode 100644 assets/images/2.0x/restore_wallet_image.png delete mode 100644 assets/images/2.0x/right_arrow.png delete mode 100644 assets/images/2.0x/russia.png delete mode 100644 assets/images/2.0x/scanner.png delete mode 100644 assets/images/2.0x/seedIco.png delete mode 100644 assets/images/2.0x/seedKeys.png delete mode 100644 assets/images/2.0x/seed_image.png delete mode 100644 assets/images/2.0x/select_arrow.png delete mode 100644 assets/images/2.0x/send.png delete mode 100644 assets/images/2.0x/settings.png delete mode 100644 assets/images/2.0x/settings_menu.png delete mode 100644 assets/images/2.0x/share.png delete mode 100644 assets/images/2.0x/south_korea.png delete mode 100644 assets/images/2.0x/spain.png delete mode 100644 assets/images/2.0x/to_icon.png delete mode 100644 assets/images/2.0x/transfer.png delete mode 100644 assets/images/2.0x/trash.png delete mode 100644 assets/images/2.0x/triangle.png delete mode 100644 assets/images/2.0x/up_arrow.png delete mode 100644 assets/images/2.0x/upload.png delete mode 100644 assets/images/2.0x/usa.png delete mode 100644 assets/images/2.0x/wallet.png delete mode 100644 assets/images/2.0x/wallet_menu.png delete mode 100644 assets/images/2.0x/wallet_name.png delete mode 100644 assets/images/2.0x/wallet_name_light.png delete mode 100644 assets/images/2.0x/wallet_type.png delete mode 100644 assets/images/2.0x/wallet_type_light.png delete mode 100644 assets/images/2.0x/welcome.png delete mode 100644 assets/images/2.0x/welcomeImg.png delete mode 100644 assets/images/2.0x/welcome_light.png delete mode 100644 assets/images/2.0x/xmr_btc.png delete mode 100644 assets/images/3.0x/Telegram.png delete mode 100644 assets/images/3.0x/Twitter.png delete mode 100644 assets/images/3.0x/arrow_bottom_cake_green.png delete mode 100644 assets/images/3.0x/back_arrow.png delete mode 100644 assets/images/3.0x/back_arrow_dark_theme.png delete mode 100644 assets/images/3.0x/back_vector.png delete mode 100644 assets/images/3.0x/backup.png delete mode 100644 assets/images/3.0x/bitcoin.png delete mode 100644 assets/images/3.0x/bitcoin_menu.png delete mode 100644 assets/images/3.0x/bitmap.png delete mode 100644 assets/images/3.0x/cake_arrow.png delete mode 100644 assets/images/3.0x/cake_logo.png delete mode 100644 assets/images/3.0x/change_now.png delete mode 100644 assets/images/3.0x/china.png delete mode 100644 assets/images/3.0x/close.png delete mode 100644 assets/images/3.0x/close_button.png delete mode 100644 assets/images/3.0x/close_button_dark_theme.png delete mode 100644 assets/images/3.0x/coins.png delete mode 100644 assets/images/3.0x/copy_address.png delete mode 100644 assets/images/3.0x/copy_content.png delete mode 100644 assets/images/3.0x/crypto_lock.png delete mode 100644 assets/images/3.0x/crypto_lock_light.png delete mode 100644 assets/images/3.0x/decred.png delete mode 100644 assets/images/3.0x/decred_menu.png delete mode 100644 assets/images/3.0x/delete_icon.png delete mode 100644 assets/images/3.0x/down_arrow.png delete mode 100644 assets/images/3.0x/exchange.png delete mode 100644 assets/images/3.0x/eye.png delete mode 100644 assets/images/3.0x/eye_action.png delete mode 100644 assets/images/3.0x/eye_menu.png delete mode 100644 assets/images/3.0x/face.png delete mode 100644 assets/images/3.0x/filter_button.png delete mode 100644 assets/images/3.0x/filter_light_button.png delete mode 100644 assets/images/3.0x/germany.png delete mode 100644 assets/images/3.0x/github.png delete mode 100644 assets/images/3.0x/header.png delete mode 100644 assets/images/3.0x/holland.png delete mode 100644 assets/images/3.0x/india.png delete mode 100644 assets/images/3.0x/japan.png delete mode 100644 assets/images/3.0x/key.png delete mode 100644 assets/images/3.0x/key_menu.png delete mode 100644 assets/images/3.0x/keysIco.png delete mode 100644 assets/images/3.0x/litecoin.png delete mode 100644 assets/images/3.0x/load.png delete mode 100644 assets/images/3.0x/menu_button.png delete mode 100644 assets/images/3.0x/monero.png delete mode 100644 assets/images/3.0x/monero_menu.png delete mode 100644 assets/images/3.0x/morph_icon.png delete mode 100644 assets/images/3.0x/new_wallet.png delete mode 100644 assets/images/3.0x/nodes.png delete mode 100644 assets/images/3.0x/nodes_menu.png delete mode 100644 assets/images/3.0x/open_book.png delete mode 100644 assets/images/3.0x/open_book_menu.png delete mode 100644 assets/images/3.0x/paste_ios.png delete mode 100644 assets/images/3.0x/poland.png delete mode 100644 assets/images/3.0x/portugal.png delete mode 100755 assets/images/3.0x/pre_seed_dark.png delete mode 100755 assets/images/3.0x/pre_seed_light.png delete mode 100644 assets/images/3.0x/privacy_menu.png delete mode 100644 assets/images/3.0x/qr_code_icon.png delete mode 100644 assets/images/3.0x/qr_icon.png delete mode 100644 assets/images/3.0x/question_mark.png delete mode 100644 assets/images/3.0x/reconnect.png delete mode 100644 assets/images/3.0x/reconnect_menu.png delete mode 100644 assets/images/3.0x/refresh_icon.png delete mode 100644 assets/images/3.0x/restoreSeed.png delete mode 100644 assets/images/3.0x/restore_keys.png delete mode 100644 assets/images/3.0x/restore_seed.png delete mode 100644 assets/images/3.0x/restore_wallet.png delete mode 100644 assets/images/3.0x/restore_wallet_image.png delete mode 100644 assets/images/3.0x/right_arrow.png delete mode 100644 assets/images/3.0x/russia.png delete mode 100644 assets/images/3.0x/scanner.png delete mode 100644 assets/images/3.0x/seedIco.png delete mode 100644 assets/images/3.0x/seedKeys.png delete mode 100644 assets/images/3.0x/seed_image.png delete mode 100644 assets/images/3.0x/select_arrow.png delete mode 100644 assets/images/3.0x/send.png delete mode 100644 assets/images/3.0x/settings.png delete mode 100644 assets/images/3.0x/settings_menu.png delete mode 100644 assets/images/3.0x/share.png delete mode 100644 assets/images/3.0x/south_korea.png delete mode 100644 assets/images/3.0x/spain.png delete mode 100644 assets/images/3.0x/to_icon.png delete mode 100644 assets/images/3.0x/transfer.png delete mode 100644 assets/images/3.0x/trash.png delete mode 100644 assets/images/3.0x/triangle.png delete mode 100644 assets/images/3.0x/up_arrow.png delete mode 100644 assets/images/3.0x/upload.png delete mode 100644 assets/images/3.0x/usa.png delete mode 100644 assets/images/3.0x/wallet.png delete mode 100644 assets/images/3.0x/wallet_menu.png delete mode 100644 assets/images/3.0x/wallet_name.png delete mode 100644 assets/images/3.0x/wallet_name_light.png delete mode 100644 assets/images/3.0x/wallet_type.png delete mode 100644 assets/images/3.0x/wallet_type_light.png delete mode 100644 assets/images/3.0x/welcome.png delete mode 100644 assets/images/3.0x/welcomeImg.png delete mode 100644 assets/images/3.0x/welcome_light.png delete mode 100644 assets/images/3.0x/xmr_btc.png mode change 100755 => 100644 assets/images/cakewallet_android_icon.png delete mode 100644 assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_back.png delete mode 100644 assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_fore.png delete mode 100644 assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_mono.png create mode 100644 assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_background.png create mode 100644 assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_monochrome.png delete mode 100644 assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_back.png delete mode 100644 assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_fore.png delete mode 100644 assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_mono.png create mode 100644 assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_background.png create mode 100644 assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_monochrome.png delete mode 100644 assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_back.png delete mode 100644 assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_fore.png delete mode 100644 assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_mono.png create mode 100644 assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_background.png create mode 100644 assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_monochrome.png delete mode 100644 assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_back.png delete mode 100644 assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_fore.png delete mode 100644 assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_mono.png create mode 100644 assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_background.png create mode 100644 assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_monochrome.png delete mode 100644 assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_back.png delete mode 100644 assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png delete mode 100644 assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_mono.png create mode 100644 assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_background.png create mode 100644 assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_monochrome.png delete mode 100644 assets/images/cakewallet_app_logo.png diff --git a/assets/images/2.0x/Telegram.png b/assets/images/2.0x/Telegram.png deleted file mode 100644 index b8f8cce3de3f8a7a94b45960ed78ca5909cb9b4d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2913 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 LNkvXXu0mjfkSKmw diff --git a/assets/images/2.0x/Twitter.png b/assets/images/2.0x/Twitter.png deleted file mode 100644 index 642e8eb729c5b5b2d06b6c5ae7d31076e1659022..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1374 zcmV-k1)=(hP)~y&few{pyB|~SwH}-fog<9}IyghJ#-92i z3sQ#y-7I%0kfw-IAnDKoW|kDNm70w!#S%qc-#d$nzAW)bMOJ=*Ad))L@7?|1yLU%3 z=mD|xK}feCCN!7<0bvd>E0_r3ama1MfEfb75Daqku^H@S<|hmY2m_yx(Zdzzs$hH$ zKt#b0g5?|y)`K{s>_E+M&erOCI53QDSR4|njwm_sQSx!T=Y}`Igh+h9G#U5Y-b(E*YS2!88% zN~QxLuJN?Ap#vvMNa~%jYcK$Gii@@mENtat9C#WA2#2%f)Rl?N=IbpE$bcoypN?!g zVOkoy)Ew9?G2l32djO3mLV88CsG`9Ese8--FsLw?T}&NZYrIsYn(KFA@G-4J7Rjtk zm~>Jb0o#Fr$T%Jd4UNN^dm=}mAHDzf1sz_S8iF%AgZudk+~2Rje$lD@K0V4C&bv?> zg&{h#9Xsk_QmzqwWf-y#%bwa!k8$|&oih*)X|-!};Sty`IFS6~Y4v#{u>G(EJ0712 z&qg6}Y_;HPUhf>k9{l{xG5PUA{N1w+#=DSX?sIm{3jF%Wf>GUjZHbdxl)gF12NdcY z_m9vheD&VA9Qu=>Hl2QVxj3!EDmqfM1Sk-V-8u;IWBK?af;~$;Ooq1Otqa3m9s^|d zjXMQUMLP>0at7Lr%1sn#hH2N%T`Amn{~Ubq>PUlJ#ym>m>tFs;?R^OoFgt`3mvWiB z@q1DBd_CmIXEUSnn2a${*X^|54u$N~2oqr(0TEWYhm4B8Jkl@<%}|`Yj=J^|l)D|C z(+zw&jsU2Y4wU|)VQ<>@^t_^f2Y}WLphyAbL6cCgp!694bq9!>zZGE5uWp-o5U?{i z3dvNwN6IMi^C8a0B?!XpM`iy3T!mvQQg(odoYUk4~ z3#IA5o*(}_3f%j&Rmm|34Dg9x8STkJ?g#>dTa#Sa_C4?kWu%*Tim>|Azn-(nUNwG# zNY8~fiMgd~83@3|=^(iBo{Qeft7XZ>#m0URf0LJK4}rs&3KYn)+kANJ;?WN@*^<$ ztkbu&K)#@Ga7n4Sok9g%cwCe95jjKECmHQlti3k69?L34GM~O=)Vh$iD{vIz@IoLq zl8ciobW|%PDjcr+nUzli%PJ?_A19A4{~p#nfX=tt<|w2pPY%Fqt^%_pWt-d|Sg#IJSe*gdg07*qoM6N<$g7=Sms{jB1 diff --git a/assets/images/2.0x/arrow_bottom_cake_green.png b/assets/images/2.0x/arrow_bottom_cake_green.png deleted file mode 100644 index 67c523951a395432df08e0844621ecdfbb2032fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 410 zcmV;L0cHM)P)NaqEn-L9Q@HpZ-_o8dpX+Cl`rU>J7AI2DBHk7=8LQ>SDjBT+5;&ayU<;RTztf+WmWM zDY(P#k{NEGK@X1^@s60h=H400009a7bBm000&x z000&x0ZCFM@Bjb+0drDELIAGL9O(c600d`2O+f$vv5yP1 z!ax)N@b?OXTWdt);7}2@!KI@kY$yyJ-Na8o`~<=`@EeGu6OyQeN!Z+EtU|PI7%|j^ zV30mvffx!`P;Y21_wJXz`|}RqpACOAN!{8r0+N&=A_Y(?KABh9xiF%oVh+&l_?QEb z4$S;h)X;faiMtD)01*Uzk!Glp(<^XS5yjCLLC;N36~!HJV$$CcWi7K*^*~iM6r@3V zGtf=W?R(Z3Sm+f9K%a*^a9vC08O4N%oKx$elp7x#q#D*z?tvbP5FuSMI-VR4hPC}P zhfjovs7Bq+ktiksVn&F7Ec)!kEvsUSp?m+t()L3`2#^;A5K#|n$umC~Xt(NBWqDiY z&CvvLU0&W&>{h)z1>KLb7A-^HNibfW`ZK`pVrU!Nt;fm~w3}vES=+h5ul@l~6|Ffo ziGRw<#d2t)h=7JKUW$CQ-y3(MOuS9i5`4)ezT~J1@Sun-^ZMMUtQceP-tVX!z_{=Q zuv-mVUWt|wh}S$({i8H}pAP%YY|5d?>u}B9#>HUId~^8xYI11vWfC*Y&V*ywO-~)} eHsznWF24Z?8LP$r8CBl^0000?d#)(b!KQpou3Sp`7sxEMEX#!K85VfTVEpSQBYH$|vv%9!yN2 z+i@BM3CtpKE=^|oy4&t_1RM|@Kg{rS2-bN}DV{Z#t8PSh zFzFqFk}!Tz=u)xF8|c+G&`)lS?%0(8Uz}3j-p(4GVOXJa;t|5s_*@jYk`yS)yn$I1 z%?s74{lP*Mz!pHzTVyhz3kfP3vyRTuh+^OK0+WUH>x&+Bt;;@EKsGu~`TU9$PVux! zTWbTaFCU&lkA+K|yQQ}8bCf<*r8}IoSeS%iEE;r~0;@%%J9u^1``E5~en3s0rhWy! z^)PN~CxG`DSSpncXp0tC7fKbhqhlYY+(#W;XguSSSYlB=uS{slDqhtbA@s7f|%k7 z;>rAppC@=N3U>J<)TcFV>W3J&BB93D5pGRR@~SFL(iiJ&cN^G0yZe2S^+Io_9Q6{9 eDZb5$LGxAy_GukjXCMmnF@vY8pUXO@geCx7wta#C diff --git a/assets/images/2.0x/backup.png b/assets/images/2.0x/backup.png deleted file mode 100644 index 5cf40bba74cd6bd9cbcf4bfcbde47586b02f9d70..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5275 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 diff --git a/assets/images/2.0x/cake_arrow.png b/assets/images/2.0x/cake_arrow.png deleted file mode 100644 index cb72693ceb8b9e2a98276074709afcbf41115e2a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 434 zcmV;j0ZsmiP)T&^1|3dzPi#W~xCg{oN;&y{xSoNb*oE9Z=Vo0*j>T9wm?t9>fVUqT zYI-3EhClvgRO^5M>n5Osz;E6+ZVgb#3pAIlp#e{v5^%=SSJfplK`65lgnDuL%1%X> ztB-LTF*QB1n78zLF&5h3ZRMcVtlqcWq=~gP}@* zo(4$_iHyJ$56yanIv$NyqS?Z~3i1v*fbC|8lC2o)$BtflV#k$gGRGwMK^*k+2(i2Q zuZjk_EoN`|*pCZ|0paLW^Z${mg+Qvk2o@D{-XSBX-7b;Q_{;A%22%Z}{Y1@uTI@_{ cJ+)oYKZ}`^1TTe{!vFvP07*qoM6N<$g0~m3dH?_b diff --git a/assets/images/2.0x/cake_logo.png b/assets/images/2.0x/cake_logo.png deleted file mode 100644 index 2778168137655ba1550464e6c3f2a35ecc6b0233..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6397 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 Wn3vF4_x(*KzvN*>>sToqjF%)nv#9b z+VFv(sO-OZ@$|kC%a25;uyA9xqL{~Ml$K$9pmVMijU*b%Z~}#eua!}9UL8l{1+p14 zudZIbVe4>$YP^erG8ClW)2W7W2cu^yZ)axau=@Jyida%H9q8u!2bg&H;yE#-19s?? zKVtI0=k@034Gq=T23{DH#3Eq@=kXjX&Dg+$oU6+RCib8tCgaay(itznJfac~?CnP( zJ83a&=1&}022lw_rE}4qo|A6s)MCt>BA_%2DR9LHji^Bi>u8xko7wb zp(h#znZ%iyxd-Wx5ks1;d$D!vCM?QZfTpHqF?B>i;RvbW?THrV<=E{C5P~JCK)`8@+Bsj2_8uUUM|vYr^tnOK|D( z6%-V_1eePxuh-VA=j}(qqFlHSu(e8e*>#lEk~_h zkB*K`NtZi!YN4vCq=JZyX`0wUa!ZyjMtxlau3x_?9aa<-jg3uk67RJs8g#u5RaO7P z%WDgW?qy_g69z23kLU-UPjPEIygUc$9)3%YGJ_BWCJ0sO1xT=t7h%}Ro6*L)z+VVj!f zg8QdAaPM9K?y^l{)0}Yrcs`UF@fU+aI*1X#>WI*TG`L7V=v=-8Q=441&Q?Jd0@70;Bw1AI~|$M{2lR1e9)?xZ8P6(RI1s={K*S9HO$Tp$4A(n(fLj6UlRZo$RWxF} z;n9%yN<%zul2gGu3|MzMpu8{*jA*1T^1vdejPp%UiI&X479OD%l|)dE4E0En(4ZvcVM}^s1|C9LI1P>>dehQK7*`*{ zq#q+ERf>!#+VlT~jMLaS7O+@~p6^ANP*B|~1-bsD3--cNgMCko1 zl(0|W{>36B{ALAQKgof0tBtDBC2%UMQHY3LctE=7J#piq&}y^P-y-#jV!(LZozH;Z zU!hBh!orddsEF)~DN41O(a7?U*{_==IOK!*4&XZiDm)(x`iS%BRASgDOe#e7_nbvHwsc(&(Wa3` zx!}Vj2Itm3zyI@3dfJ21oOb7~##WbI|sB zLxUdldgtG#*YqUYBQIBLuXmo9Yxpn;VDtXpe;(>u%r7+L#JS1f zgl&BJ{#e(C{YptoPC+HHb2r}K42bjQUK;sB@q|YBnbk*I?=JD-BOBuY+BT#w_>pZr zFRQq+7zGLoi)qfJ?8EZ|jk227dqm002ovPDHLkV1gkKifRA= diff --git a/assets/images/2.0x/china.png b/assets/images/2.0x/china.png deleted file mode 100644 index d199532eeb47e0811233318d89d4086b2ce25aab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1184 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 diff --git a/assets/images/2.0x/copy_content.png b/assets/images/2.0x/copy_content.png deleted file mode 100644 index cf639846a21ce37619fd6cd6ee47aa583d2f6e41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 517 zcmV+g0{Z=lP);s)4p1ib+!CcsJzxNs$`!Qc*Ap*PSI$PQTP5$X*vCL~tc@eOfm zOG45*16DI%Lg=KE$(Nbe2mKM&=nkh126)N=Y}f{wtlc^}c!@jR3JPDn?KhBdCfIRK*CYVgyzBJA&u+(pUsL^7EF-Ez= diff --git a/assets/images/2.0x/crypto_lock.png b/assets/images/2.0x/crypto_lock.png deleted file mode 100644 index c93941b77513d13532c3687d0ab1f42fc85c1ad5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 187826 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

GFk`W*QlbG}xFSadBmdcn8*pVGSw``?Yq=|-dK zM@<6R^;u30n<}kjwsB`yZIC5yYF~I1>%aY@na*uVA`cQNy+T&D82Hj;fTisAr4eW4 zv<3UO?s@yfX>M}HQ2G054Zy?8dg#60o7oO;x|HfknR~PUD zxb(=*e7j=^J596wV zUrzy!XP%Dd$(lN=%CRxBb!Dr%nMb@zP<}IF3mcmW%5zWTW3sv^*CmdQ2;rH0^Z1M( zkFdWYAy8|U!NRo0J?kgL12}RL$fVU?4HzIr=Y%7yu2fP0^re@Bh0tJG5Sg@gkey)C zbE?9XxTNo@T-Fk%^@9w*Tq7QSJSv5t{XWscp?)JCYK}v{JcQ31Ze+_7ybR zQ(L6C$`AF_WWVr4t4hfvl1LaG65kHIL@GjRU`YHvY(}vr-HJ=SwKAKf8w8Q5ss)qf z=UFb0^d$^FvJ7N;{gEAxJmv-7vLRqY#liG%{X=eFA<#E}zcn!&o2eYa$Ph&vd4ty| zJGU8j@M`Bqeq*}c4Y)F7&j}<_6;u94d+2kk1?|Z!#C4<@$aLv6=|jpO)lH6PJI^!y%MX`hm3)q)`*dT)P7@y^3?_HEw$?Y z5wrA%&78zp2YQ{jy6}n1gBj=PL&?A_y>;q(DT072@5dSABnB3fpW!2`O~*}Q=Z4m} hzT0lEPTry6`3a^?Q6J$o3E=<$002ovPDHLkV1lWYQqTYZ diff --git a/assets/images/2.0x/bitcoin.png b/assets/images/2.0x/bitcoin.png deleted file mode 100644 index 169c734269c56e35db6183b4e97f8e09af0b6fac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2418 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 diff --git a/assets/images/2.0x/bitcoin_menu.png b/assets/images/2.0x/bitcoin_menu.png deleted file mode 100644 index 7afe772aa4307ec935bc4469f798d5bcc0a64074..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2395 zcmV-h38eOkP)YLXAiWYjqK+&tE*qA*m9W*a-nmh~V}zJi37z zQ44{m{(X;0d(009d~1VSh&A32;qRB>cVlo2X9SA1X;4Ezd~D0ob_YcoA^6qr{|=5| zoxnM9?4y1H_tsVt6anG$i|}X!mSL5^Q~!FP&^vF=;)-D3BCQa}R@aHK^^#Ap2OkG+ z-ckJQx9~WEJ)vMvJoT?nV)Wj8MC1tu2Yk*yhPRJlPgJ(R)Yk>6ultChM;F218B|0i z0%gc*RM!O9g--w+yifimeOzTo?@VAqp7-j*ohYh;qIaYZcO`Kr0zX~y=GdLY(gc3` zR zA8xcj&TWOfM${AuY?q(QkGbrFML=Eu4EszsXmi^4a?_i+1R8$>GywY;#WM5=p77%v zOe`piW&dXoi|oGy7H4x*XQ#P%Pn4#-H*$wCBKQRM!52P$5gd#4$n8pA`@z5Ndf;{0 zghb}=>O4|2{_kI{-!4BF#oKsJhd@;4JQBv)`pX)DY<)vlA&b)bb@sIv;VPkd*x>(` z4|oL5ft?w)WI_#>UtJr*Y<*5`;Y0zz-W)$%0bBJSG@Klp!i*}t8i`ga4)20CRw$5wr%*h*ZH$fOPClZhcv zP7oT%I&^M8{2mH*pYMk3->x|u`135de{&k%|5S!dp7A3l@f$}Zwr3o(nsbzY_yqgV z04Ng>XOZ<~913-jXe4B^WxgyTQo<%p@e^O2)GMvQRs4D|tzJR`@qM+Yb}K#~93VTSG%D#x1s z0te7aJyJY~A3_7z)&K1WbW#q`pgJn)46Pe``@faHRhVNtG7@6$l=u;-9XQ!G?8^DS z1k!C)bc`z_;ZedC*iihp&1y_%DViZX+EVYB64J0ttD39SP6%vK+SFYqiu|7-RizjD z;g7%k@j%6KM;>d>i+=t2+foyQDU3N38L&<%D8Z_l;SxzJwmm%>YIFiO7Cuv*)6br1+U)aolg~VjS zDL4+$nH93HVGG21!V0_jZvqxIi)+|Kg+Y5&AamC;QjgX3N@l2@D6=$`g<7J4q&!4= zYoA?}_6#Z&m#fYo{rxtxlp@> z7pxQjS4sFeq$&P!TuE7JtBOjVfHlcm;xwoNq(Isxs9gPG%Ej$c3uBO6DKW|1VKNau znOk;6aNeV6rVrC! z!lgp6l6d3TSP{YNI#!HtgTrR2DaV*!$%DoGD|J@BI|1uxj!=hb>J<}c>dlK(j)!rf zvRe(*ljd(}T;d<;ZyB0U10+*%mgD35X3*N(+xVAT`A#7em3kPnU$e0QT42aa)_6F; zsvxo=XKBDI7fS^eU!60D@C)zs-3K4i#vM zvPG;F0_EgSgZq&yE0iw%<>#hmvC>wOL$GP=W@601Cpbv6rkHOBi$}?Ou0gmikFDu} zVuh~y>ay!UX0p6t2^!ZM$f1$Gojg!<>G#(%pEUy0maxiu2yFcN9RHVXtBW(sAX0&~ z_PTW*HWl!573s_HSfcDPSOi=3G^;qTw$)LzHVZe@x08jWx|<648;eRF#ZN5PR0r$> z2OJAdMcQY7azj+$|LB8gJD$$AjL3~O5owWS>Z=I@+tD_`HBr_fx7TYT&HF$0%foH) zX`z%VP2PNGLue=qW1&d;ZS#B56z?fhSe4_U9A=lFI)<^(rpc3QF0&y|A%R9TBz^31 z-NlCoiew|6GDm*i_GAqXwmq5S^Ts>t*ts6}dF!WD?A+Rst^hi$d17%2xt1#AC7;~` z4TLxjphq52SXDyXyQH4h9U9x=V-HA=b&w|z>;fx23F#J~=Z<#zunrM6I6Zq}Ljkwi z)FIYb-^%eSph#LwQnADeEO*5ukmg1Oc=7=eIc3#*)FS=d_O|g(2%MvNizL~5|H=h% zb2ue%4%y2<3P(`WR`8njJH@5PtOD)=wGg+F}B>zZPsNAHvQGt$j>yxpM{{STpbo*RQ9A*Fj N002ovPDHLkV1fZ1e=-07 diff --git a/assets/images/2.0x/bitmap.png b/assets/images/2.0x/bitmap.png deleted file mode 100644 index b08091847435b74de640049e123d5dc46ae73e47..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 185925 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 z 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