From 651792a0fd7eda40fc504ba84d7a85e10f2ef2f4 Mon Sep 17 00:00:00 2001 From: Seth For Privacy <40500387+sethforprivacy@users.noreply.github.com> Date: Fri, 4 Oct 2024 11:55:51 +0000 Subject: [PATCH 01/73] Clean up Silent Payments copy (#1724) --- res/values/strings_en.arb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 324db7548..19f939d19 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -145,7 +145,7 @@ "confirm_fee_deduction_content": "Do you agree to deduct the fee from the output?", "confirm_passphrase": "Confirm passphrase", "confirm_sending": "Confirm sending", - "confirm_silent_payments_switch_node": "Your current node does not support silent payments\\nCake Wallet will switch to a compatible node, just for scanning", + "confirm_silent_payments_switch_node": "Your current node does not support Silent Payments.\\n\\nCake Wallet will switch to a compatible node while scanning.", "confirmations": "Confirmations", "confirmed": "Confirmed Balance", "confirmed_tx": "Confirmed", @@ -236,7 +236,7 @@ "email_address": "Email Address", "enable_mempool_api": "Mempool API for accurate fees and dates", "enable_replace_by_fee": "Enable Replace-By-Fee", - "enable_silent_payments_scanning": "Start scanning silent payments, until the tip is reached", + "enable_silent_payments_scanning": "Start scanning for transactions sent to your Silent Payment address.", "enabled": "Enabled", "enter_amount": "Enter Amount", "enter_backup_password": "Enter backup password here", @@ -706,7 +706,7 @@ "silent_payments_disclaimer": "New addresses are not new identities. It is a re-use of an existing identity with a different label.", "silent_payments_display_card": "Show Silent Payments card", "silent_payments_scan_from_date": "Scan from date", - "silent_payments_scan_from_date_or_blockheight": "Please enter the block height you want to start scanning for incoming silent payments, or, use the date instead. You can choose if the wallet continues scanning every block, or checks only the specified height.", + "silent_payments_scan_from_date_or_blockheight": "Please enter the block height you want to start scanning for incoming Silent Payments or use the date instead. You can choose if the wallet continues scanning every block, or checks only the specified height.", "silent_payments_scan_from_height": "Scan from block height", "silent_payments_scanned_tip": "SCANNED TO TIP! (${tip})", "silent_payments_scanning": "Silent Payments Scanning", @@ -905,7 +905,7 @@ "warning": "Warning", "welcome": "Welcome to", "welcome_to_cakepay": "Welcome to Cake Pay!", - "what_is_silent_payments": "What is silent payments?", + "what_is_silent_payments": "What are Silent Payments?", "widgets_address": "Address", "widgets_or": "or", "widgets_restore_from_blockheight": "Restore from blockheight", @@ -934,4 +934,4 @@ "you_will_get": "Convert to", "you_will_send": "Convert from", "yy": "YY" -} \ No newline at end of file +} From 3348213e39ced300078b48c60cbbdee6796004c9 Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 4 Oct 2024 19:38:07 +0300 Subject: [PATCH 02/73] fix min fee not met exception (#1726) --- cw_bitcoin/lib/exceptions.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cw_bitcoin/lib/exceptions.dart b/cw_bitcoin/lib/exceptions.dart index f7c593135..9bdb66eef 100644 --- a/cw_bitcoin/lib/exceptions.dart +++ b/cw_bitcoin/lib/exceptions.dart @@ -45,6 +45,6 @@ class BitcoinTransactionCommitFailedVoutNegative extends TransactionCommitFailed class BitcoinTransactionCommitFailedBIP68Final extends TransactionCommitFailedBIP68Final {} -class BitcoinTransactionCommitFailedLessThanMin extends TransactionCommitFailedBIP68Final {} +class BitcoinTransactionCommitFailedLessThanMin extends TransactionCommitFailedLessThanMin {} class BitcoinTransactionSilentPaymentsNotSupported extends TransactionInputNotSupported {} From fc14bf4e2b9f1f0906328481d7810e5e0a642b5a Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Fri, 4 Oct 2024 17:39:29 +0100 Subject: [PATCH 03/73] fix: Issues with imaging (#1723) --- .../wallet_group_description_page.dart | 7 ---- .../new_wallet/wallet_group_display_page.dart | 41 ++++++++++++++----- 2 files changed, 30 insertions(+), 18 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 5becea3a5..d0936b640 100644 --- a/lib/src/screens/new_wallet/wallet_group_description_page.dart +++ b/lib/src/screens/new_wallet/wallet_group_description_page.dart @@ -7,7 +7,6 @@ import 'package:cake_wallet/themes/theme_base.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:flutter_svg/svg.dart'; class WalletGroupDescriptionPage extends BasePage { WalletGroupDescriptionPage({required this.selectedWalletType}); @@ -21,12 +20,6 @@ class WalletGroupDescriptionPage extends BasePage { @override Widget body(BuildContext context) { - final lightImage = 'assets/images/wallet_group_light.png'; - final darkImage = 'assets/images/wallet_group_dark.png'; - final brightImage = 'assets/images/wallet_group_bright.png'; - - final image = currentTheme.type == ThemeType.light ? lightImage : darkImage; - return Container( alignment: Alignment.center, padding: EdgeInsets.all(24), 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 d3ce27446..a99d2bac7 100644 --- a/lib/src/screens/new_wallet/wallet_group_display_page.dart +++ b/lib/src/screens/new_wallet/wallet_group_display_page.dart @@ -4,11 +4,11 @@ import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/view_model/wallet_groups_display_view_model.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; - import '../../../themes/extensions/cake_text_theme.dart'; class WalletGroupsDisplayPage extends BasePage { @@ -16,22 +16,24 @@ class WalletGroupsDisplayPage extends BasePage { final WalletGroupsDisplayViewModel walletGroupsDisplayViewModel; - final walletTypeImage = Image.asset('assets/images/wallet_type.png'); - final walletTypeLightImage = Image.asset('assets/images/wallet_type_light.png'); - @override String get title => S.current.wallet_group; @override Widget body(BuildContext context) => WalletGroupsDisplayBody( walletGroupsDisplayViewModel: walletGroupsDisplayViewModel, + currentTheme: currentTheme, ); } class WalletGroupsDisplayBody extends StatelessWidget { - WalletGroupsDisplayBody({required this.walletGroupsDisplayViewModel}); + WalletGroupsDisplayBody({ + required this.walletGroupsDisplayViewModel, + required this.currentTheme, + }); final WalletGroupsDisplayViewModel walletGroupsDisplayViewModel; + final ThemeBase currentTheme; @override Widget build(BuildContext context) { @@ -47,7 +49,9 @@ class WalletGroupsDisplayBody extends StatelessWidget { return Column( children: [ if (walletGroupsDisplayViewModel.hasNoFilteredWallet) ...{ - WalletGroupEmptyStateWidget(), + WalletGroupEmptyStateWidget( + currentTheme: currentTheme, + ), }, ...walletGroupsDisplayViewModel.multiWalletGroups.map( (walletGroup) { @@ -153,17 +157,17 @@ class WalletGroupsDisplayBody extends StatelessWidget { } class WalletGroupEmptyStateWidget extends StatelessWidget { - const WalletGroupEmptyStateWidget({ - super.key, - }); + const WalletGroupEmptyStateWidget({required this.currentTheme, super.key}); + + final ThemeBase currentTheme; @override Widget build(BuildContext context) { return Column( children: [ Image.asset( - 'assets/images/wallet_group.png', - scale: 0.8, + _getThemedWalletGroupImage(currentTheme.type), + scale: 1.8, ), SizedBox(height: 32), Text.rich( @@ -190,4 +194,19 @@ class WalletGroupEmptyStateWidget extends StatelessWidget { ], ); } + + String _getThemedWalletGroupImage(ThemeType theme) { + final lightImage = 'assets/images/wallet_group_light.png'; + final darkImage = 'assets/images/wallet_group_dark.png'; + final brightImage = 'assets/images/wallet_group_bright.png'; + + switch (theme) { + case ThemeType.bright: + return brightImage; + case ThemeType.light: + return lightImage; + default: + return darkImage; + } + } } From 4b4d8a4840309ec746accbca82ea097d00a5bf6a Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Fri, 4 Oct 2024 20:01:46 +0300 Subject: [PATCH 04/73] Add passphrase support for Eth, Polygon, and Tron (#1719) * Add passphrase support for Eth, Polygon, and Tron * move passphrase to advanced settings even for restore --- .../lib/src/bitcoin_cash_wallet.dart | 2 +- .../lib/src/bitcoin_cash_wallet_service.dart | 2 +- cw_ethereum/lib/ethereum_wallet.dart | 5 +- cw_ethereum/lib/ethereum_wallet_service.dart | 2 + cw_evm/lib/evm_chain_wallet.dart | 16 +++- ...evm_chain_wallet_creation_credentials.dart | 25 +++--- cw_nano/lib/nano_wallet.dart | 4 + .../lib/nano_wallet_creation_credentials.dart | 6 +- cw_polygon/lib/polygon_wallet.dart | 5 +- cw_polygon/lib/polygon_wallet_service.dart | 2 + cw_solana/lib/solana_wallet.dart | 5 +- .../solana_wallet_creation_credentials.dart | 20 +++-- cw_tron/lib/tron_wallet.dart | 13 ++- .../lib/tron_wallet_creation_credentials.dart | 20 +++-- cw_tron/lib/tron_wallet_service.dart | 7 +- lib/entities/unstoppable_domain_address.dart | 3 - lib/ethereum/cw_ethereum.dart | 10 ++- lib/nano/cw_nano.dart | 4 + lib/polygon/cw_polygon.dart | 23 +++-- lib/solana/cw_solana.dart | 10 ++- .../advanced_privacy_settings_page.dart | 90 +++++++++---------- .../wallet_restore_from_seed_form.dart | 29 ------ .../screens/restore/wallet_restore_page.dart | 5 +- lib/tron/cw_tron.dart | 10 ++- .../advanced_privacy_settings_view_model.dart | 3 + .../restore/restore_from_qr_vm.dart | 39 ++++++-- lib/view_model/wallet_new_vm.dart | 5 ++ lib/view_model/wallet_restore_view_model.dart | 19 ++-- tool/configure.dart | 19 ++-- 29 files changed, 249 insertions(+), 154 deletions(-) diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index b1e5e7bf6..825c80d4a 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -83,7 +83,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { unspentCoinsInfo: unspentCoinsInfo, initialAddresses: initialAddresses, initialBalance: initialBalance, - seedBytes: await MnemonicBip39.toSeed(mnemonic, passphrase: passphrase), + seedBytes: MnemonicBip39.toSeed(mnemonic, passphrase: passphrase), encryptionFileUtils: encryptionFileUtils, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart index ab5117aa8..d14dc582d 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart @@ -36,7 +36,7 @@ class BitcoinCashWalletService extends WalletService< final strength = credentials.seedPhraseLength == 24 ? 256 : 128; final wallet = await BitcoinCashWalletBase.create( - mnemonic: credentials.mnemonic ?? await MnemonicBip39.generate(strength: strength), + mnemonic: credentials.mnemonic ?? MnemonicBip39.generate(strength: strength), password: credentials.password!, walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, diff --git a/cw_ethereum/lib/ethereum_wallet.dart b/cw_ethereum/lib/ethereum_wallet.dart index e8a9e85fa..765ace052 100644 --- a/cw_ethereum/lib/ethereum_wallet.dart +++ b/cw_ethereum/lib/ethereum_wallet.dart @@ -27,6 +27,7 @@ class EthereumWallet extends EVMChainWallet { super.initialBalance, super.privateKey, required super.encryptionFileUtils, + super.passphrase, }) : super(nativeCurrency: CryptoCurrency.eth); @override @@ -150,8 +151,9 @@ class EthereumWallet extends EVMChainWallet { 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, @@ -166,6 +168,7 @@ class EthereumWallet extends EVMChainWallet { password: password, mnemonic: keysData.mnemonic, privateKey: keysData.privateKey, + passphrase: keysData.passphrase, initialBalance: balance, client: EthereumClient(), encryptionFileUtils: encryptionFileUtils, diff --git a/cw_ethereum/lib/ethereum_wallet_service.dart b/cw_ethereum/lib/ethereum_wallet_service.dart index 51d6ecf42..858416055 100644 --- a/cw_ethereum/lib/ethereum_wallet_service.dart +++ b/cw_ethereum/lib/ethereum_wallet_service.dart @@ -27,6 +27,7 @@ class EthereumWalletService extends EVMChainWalletService { walletInfo: credentials.walletInfo!, mnemonic: mnemonic, password: credentials.password!, + passphrase: credentials.passphrase, client: client, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); @@ -144,6 +145,7 @@ class EthereumWalletService extends EVMChainWalletService { password: credentials.password!, mnemonic: credentials.mnemonic, walletInfo: credentials.walletInfo!, + passphrase: credentials.passphrase, client: client, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index 0ade2215e..9bc18d14b 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -70,6 +70,7 @@ abstract class EVMChainWalletBase required String password, EVMChainERC20Balance? initialBalance, required this.encryptionFileUtils, + this.passphrase, }) : syncStatus = const NotConnectedSyncStatus(), _password = password, _mnemonic = mnemonic, @@ -178,6 +179,7 @@ abstract class EVMChainWalletBase mnemonic: _mnemonic, privateKey: _hexPrivateKey, password: _password, + passphrase: passphrase, ); walletAddresses.address = _evmChainPrivateKey.address.hexEip55; } @@ -545,6 +547,7 @@ abstract class EVMChainWalletBase 'mnemonic': _mnemonic, 'private_key': privateKey, 'balance': balance[currency]!.toJSON(), + 'passphrase': passphrase, }); Future _updateBalance() async { @@ -574,15 +577,19 @@ abstract class EVMChainWalletBase } } - Future getPrivateKey( - {String? mnemonic, String? privateKey, required String password}) async { + Future getPrivateKey({ + String? mnemonic, + String? privateKey, + required String password, + String? passphrase, + }) async { assert(mnemonic != null || privateKey != null); if (privateKey != null) { return EthPrivateKey.fromHex(privateKey); } - final seed = bip39.mnemonicToSeed(mnemonic!); + final seed = bip39.mnemonicToSeed(mnemonic!, passphrase: passphrase ?? ''); final root = bip32.BIP32.fromSeed(seed); @@ -716,4 +723,7 @@ abstract class EVMChainWalletBase @override String get password => _password; + + @override + final String? passphrase; } diff --git a/cw_evm/lib/evm_chain_wallet_creation_credentials.dart b/cw_evm/lib/evm_chain_wallet_creation_credentials.dart index d7a33f6b3..5075e6289 100644 --- a/cw_evm/lib/evm_chain_wallet_creation_credentials.dart +++ b/cw_evm/lib/evm_chain_wallet_creation_credentials.dart @@ -4,28 +4,25 @@ import 'package:cw_core/wallet_info.dart'; class EVMChainNewWalletCredentials extends WalletCredentials { EVMChainNewWalletCredentials({ - required String name, - WalletInfo? walletInfo, - String? password, - String? parentAddress, + required super.name, + super.walletInfo, + super.password, + super.parentAddress, this.mnemonic, - }) : super( - name: name, - walletInfo: walletInfo, - password: password, - parentAddress: parentAddress, - ); + super.passphrase, + }); final String? mnemonic; } class EVMChainRestoreWalletFromSeedCredentials extends WalletCredentials { EVMChainRestoreWalletFromSeedCredentials({ - required String name, - required String password, + required super.name, + required super.password, required this.mnemonic, - WalletInfo? walletInfo, - }) : super(name: name, password: password, walletInfo: walletInfo); + super.walletInfo, + super.passphrase, + }); final String mnemonic; } diff --git a/cw_nano/lib/nano_wallet.dart b/cw_nano/lib/nano_wallet.dart index 700710c2e..040fc9522 100644 --- a/cw_nano/lib/nano_wallet.dart +++ b/cw_nano/lib/nano_wallet.dart @@ -42,6 +42,7 @@ abstract class NanoWalletBase required String password, NanoBalance? initialBalance, required EncryptionFileUtils encryptionFileUtils, + this.passphrase, }) : syncStatus = NotConnectedSyncStatus(), _password = password, _mnemonic = mnemonic, @@ -548,4 +549,7 @@ abstract class NanoWalletBase } return await NanoSignatures.verifyMessage(message, signature, address); } + + @override + final String? passphrase; } diff --git a/cw_nano/lib/nano_wallet_creation_credentials.dart b/cw_nano/lib/nano_wallet_creation_credentials.dart index 9958fad7c..59789aec7 100644 --- a/cw_nano/lib/nano_wallet_creation_credentials.dart +++ b/cw_nano/lib/nano_wallet_creation_credentials.dart @@ -9,13 +9,15 @@ class NanoNewWalletCredentials extends WalletCredentials { DerivationType? derivationType, this.mnemonic, String? parentAddress, + String? passphrase, }) : super( name: name, password: password, walletInfo: walletInfo, parentAddress: parentAddress, + passphrase: passphrase, ); - + final String? mnemonic; } @@ -25,10 +27,12 @@ class NanoRestoreWalletFromSeedCredentials extends WalletCredentials { required this.mnemonic, String? password, required DerivationType derivationType, + String? passphrase, }) : super( name: name, password: password, derivationInfo: DerivationInfo(derivationType: derivationType), + passphrase: passphrase, ); final String mnemonic; diff --git a/cw_polygon/lib/polygon_wallet.dart b/cw_polygon/lib/polygon_wallet.dart index eb59a746e..f9aff16c3 100644 --- a/cw_polygon/lib/polygon_wallet.dart +++ b/cw_polygon/lib/polygon_wallet.dart @@ -27,6 +27,7 @@ class PolygonWallet extends EVMChainWallet { super.privateKey, required super.client, required super.encryptionFileUtils, + super.passphrase, }) : super(nativeCurrency: CryptoCurrency.maticpoly); @override @@ -128,8 +129,9 @@ class PolygonWallet extends EVMChainWallet { 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, @@ -144,6 +146,7 @@ class PolygonWallet extends EVMChainWallet { password: password, mnemonic: keysData.mnemonic, privateKey: keysData.privateKey, + passphrase: keysData.passphrase, initialBalance: balance, client: PolygonClient(), encryptionFileUtils: encryptionFileUtils, diff --git a/cw_polygon/lib/polygon_wallet_service.dart b/cw_polygon/lib/polygon_wallet_service.dart index 514e73314..994912e8d 100644 --- a/cw_polygon/lib/polygon_wallet_service.dart +++ b/cw_polygon/lib/polygon_wallet_service.dart @@ -30,6 +30,7 @@ class PolygonWalletService extends EVMChainWalletService { walletInfo: credentials.walletInfo!, mnemonic: mnemonic, password: credentials.password!, + passphrase: credentials.passphrase, client: client, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); @@ -125,6 +126,7 @@ class PolygonWalletService extends EVMChainWalletService { password: credentials.password!, mnemonic: credentials.mnemonic, walletInfo: credentials.walletInfo!, + passphrase: credentials.passphrase, client: client, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index 4e69db3b8..3c3078f2e 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -33,7 +33,6 @@ import 'package:solana/base58.dart'; import 'package:solana/metaplex.dart' as metaplex; import 'package:solana/solana.dart'; import 'package:solana/src/crypto/ed25519_hd_keypair.dart'; -import 'package:cryptography/cryptography.dart'; part 'solana_wallet.g.dart'; @@ -49,6 +48,7 @@ abstract class SolanaWalletBase required String password, SolanaBalance? initialBalance, required this.encryptionFileUtils, + this.passphrase, }) : syncStatus = const NotConnectedSyncStatus(), _password = password, _mnemonic = mnemonic, @@ -632,4 +632,7 @@ abstract class SolanaWalletBase @override String get password => _password; + + @override + final String? passphrase; } diff --git a/cw_solana/lib/solana_wallet_creation_credentials.dart b/cw_solana/lib/solana_wallet_creation_credentials.dart index f0901df79..121ef2b44 100644 --- a/cw_solana/lib/solana_wallet_creation_credentials.dart +++ b/cw_solana/lib/solana_wallet_creation_credentials.dart @@ -8,22 +8,30 @@ class SolanaNewWalletCredentials extends WalletCredentials { String? password, String? parentAddress, this.mnemonic, + String? passphrase, }) : super( name: name, walletInfo: walletInfo, password: password, parentAddress: parentAddress, + passphrase: passphrase, ); final String? mnemonic; } class SolanaRestoreWalletFromSeedCredentials extends WalletCredentials { - SolanaRestoreWalletFromSeedCredentials( - {required String name, - required String password, - required this.mnemonic, - WalletInfo? walletInfo}) - : super(name: name, password: password, walletInfo: walletInfo); + SolanaRestoreWalletFromSeedCredentials({ + required String name, + required String password, + required this.mnemonic, + WalletInfo? walletInfo, + String? passphrase, + }) : super( + name: name, + password: password, + walletInfo: walletInfo, + passphrase: passphrase, + ); final String mnemonic; } diff --git a/cw_tron/lib/tron_wallet.dart b/cw_tron/lib/tron_wallet.dart index f5841d894..9012ef528 100644 --- a/cw_tron/lib/tron_wallet.dart +++ b/cw_tron/lib/tron_wallet.dart @@ -47,6 +47,7 @@ abstract class TronWalletBase required String password, TronBalance? initialBalance, required this.encryptionFileUtils, + this.passphrase, }) : syncStatus = const NotConnectedSyncStatus(), _password = password, _mnemonic = mnemonic, @@ -113,6 +114,7 @@ abstract class TronWalletBase mnemonic: _mnemonic, privateKey: _hexPrivateKey, password: _password, + passphrase: passphrase, ); _tronPublicKey = _tronPrivateKey.publicKey(); @@ -149,8 +151,9 @@ abstract class TronWalletBase 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, @@ -165,6 +168,7 @@ abstract class TronWalletBase password: password, mnemonic: keysData.mnemonic, privateKey: keysData.privateKey, + passphrase: keysData.passphrase, initialBalance: balance, encryptionFileUtils: encryptionFileUtils, ); @@ -190,12 +194,13 @@ abstract class TronWalletBase String? mnemonic, String? privateKey, required String password, + String? passphrase, }) async { assert(mnemonic != null || privateKey != null); if (privateKey != null) return TronPrivateKey(privateKey); - final seed = bip39.mnemonicToSeed(mnemonic!); + final seed = bip39.mnemonicToSeed(mnemonic!, passphrase: passphrase ?? ''); // Derive a TRON private key from the seed final bip44 = Bip44.fromSeed(seed, Bip44Coins.tron); @@ -463,6 +468,7 @@ abstract class TronWalletBase 'mnemonic': _mnemonic, 'private_key': privateKey, 'balance': balance[currency]!.toJSON(), + 'passphrase': passphrase, }); Future _updateBalance() async { @@ -607,4 +613,7 @@ abstract class TronWalletBase @override String get password => _password; + + @override + final String? passphrase; } diff --git a/cw_tron/lib/tron_wallet_creation_credentials.dart b/cw_tron/lib/tron_wallet_creation_credentials.dart index 5055c8a8d..fd9066acd 100644 --- a/cw_tron/lib/tron_wallet_creation_credentials.dart +++ b/cw_tron/lib/tron_wallet_creation_credentials.dart @@ -8,23 +8,31 @@ class TronNewWalletCredentials extends WalletCredentials { String? password, this.mnemonic, String? parentAddress, + String? passphrase, }) : super( name: name, walletInfo: walletInfo, password: password, parentAddress: parentAddress, + passphrase: passphrase, ); final String? mnemonic; } class TronRestoreWalletFromSeedCredentials extends WalletCredentials { - TronRestoreWalletFromSeedCredentials( - {required String name, - required String password, - required this.mnemonic, - WalletInfo? walletInfo}) - : super(name: name, password: password, walletInfo: walletInfo); + TronRestoreWalletFromSeedCredentials({ + required String name, + required String password, + required this.mnemonic, + WalletInfo? walletInfo, + String? passphrase, + }) : super( + name: name, + password: password, + walletInfo: walletInfo, + passphrase: passphrase, + ); final String mnemonic; } diff --git a/cw_tron/lib/tron_wallet_service.dart b/cw_tron/lib/tron_wallet_service.dart index 200653eb9..c732a87c4 100644 --- a/cw_tron/lib/tron_wallet_service.dart +++ b/cw_tron/lib/tron_wallet_service.dart @@ -33,10 +33,7 @@ class TronWalletService extends WalletService< WalletType getType() => WalletType.tron; @override - Future create( - TronNewWalletCredentials credentials, { - bool? isTestnet, - }) async { + Future create(TronNewWalletCredentials credentials, {bool? isTestnet}) async { final strength = credentials.seedPhraseLength == 24 ? 256 : 128; final mnemonic = credentials.mnemonic ?? bip39.generateMnemonic(strength: strength); @@ -45,6 +42,7 @@ class TronWalletService extends WalletService< walletInfo: credentials.walletInfo!, mnemonic: mnemonic, password: credentials.password!, + passphrase: credentials.passphrase, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); @@ -120,6 +118,7 @@ class TronWalletService extends WalletService< password: credentials.password!, mnemonic: credentials.mnemonic, walletInfo: credentials.walletInfo!, + passphrase: credentials.passphrase, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); diff --git a/lib/entities/unstoppable_domain_address.dart b/lib/entities/unstoppable_domain_address.dart index 0f56517b8..6966fdd75 100644 --- a/lib/entities/unstoppable_domain_address.dart +++ b/lib/entities/unstoppable_domain_address.dart @@ -1,10 +1,7 @@ import 'dart:convert'; -import 'package:flutter/services.dart'; import 'package:http/http.dart' as http; -const channel = MethodChannel('com.cake_wallet/native_utils'); - Future fetchUnstoppableDomainAddress(String domain, String ticker) async { var address = ''; diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart index 7796e1c60..e2c3d1358 100644 --- a/lib/ethereum/cw_ethereum.dart +++ b/lib/ethereum/cw_ethereum.dart @@ -14,6 +14,7 @@ class CWEthereum extends Ethereum { String? parentAddress, WalletInfo? walletInfo, String? password, + String? passphrase, }) => EVMChainNewWalletCredentials( name: name, @@ -21,6 +22,7 @@ class CWEthereum extends Ethereum { password: password, parentAddress: parentAddress, mnemonic: mnemonic, + passphrase: passphrase, ); @override @@ -28,8 +30,14 @@ class CWEthereum extends Ethereum { required String name, required String mnemonic, required String password, + String? passphrase, }) => - EVMChainRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic); + EVMChainRestoreWalletFromSeedCredentials( + name: name, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + ); @override WalletCredentials createEthereumRestoreWalletFromPrivateKey({ diff --git a/lib/nano/cw_nano.dart b/lib/nano/cw_nano.dart index 83702a8ce..9e47edc04 100644 --- a/lib/nano/cw_nano.dart +++ b/lib/nano/cw_nano.dart @@ -95,6 +95,7 @@ class CWNano extends Nano { String? password, String? mnemonic, String? parentAddress, + String? passphrase, }) => NanoNewWalletCredentials( name: name, @@ -102,6 +103,7 @@ class CWNano extends Nano { mnemonic: mnemonic, parentAddress: parentAddress, walletInfo: walletInfo, + passphrase: passphrase, ); @override @@ -110,6 +112,7 @@ class CWNano extends Nano { required String password, required String mnemonic, required DerivationType derivationType, + String? passphrase, }) { if (mnemonic.split(" ").length == 12 && derivationType != DerivationType.bip39) { throw Exception("Invalid mnemonic for derivation type!"); @@ -120,6 +123,7 @@ class CWNano extends Nano { password: password, mnemonic: mnemonic, derivationType: derivationType, + passphrase: passphrase, ); } diff --git a/lib/polygon/cw_polygon.dart b/lib/polygon/cw_polygon.dart index 307107dd4..299c8278a 100644 --- a/lib/polygon/cw_polygon.dart +++ b/lib/polygon/cw_polygon.dart @@ -8,18 +8,21 @@ class CWPolygon extends Polygon { PolygonWalletService(walletInfoSource, isDirect, client: PolygonClient()); @override - WalletCredentials createPolygonNewWalletCredentials( - {required String name, - String? mnemonic, - String? parentAddress, - WalletInfo? walletInfo, - String? password}) => + WalletCredentials createPolygonNewWalletCredentials({ + required String name, + String? mnemonic, + String? parentAddress, + WalletInfo? walletInfo, + String? password, + String? passphrase, + }) => EVMChainNewWalletCredentials( name: name, walletInfo: walletInfo, password: password, mnemonic: mnemonic, parentAddress: parentAddress, + passphrase: passphrase, ); @override @@ -27,8 +30,14 @@ class CWPolygon extends Polygon { required String name, required String mnemonic, required String password, + String? passphrase, }) => - EVMChainRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic); + EVMChainRestoreWalletFromSeedCredentials( + name: name, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + ); @override WalletCredentials createPolygonRestoreWalletFromPrivateKey({ diff --git a/lib/solana/cw_solana.dart b/lib/solana/cw_solana.dart index 22cc478b6..7894f77ed 100644 --- a/lib/solana/cw_solana.dart +++ b/lib/solana/cw_solana.dart @@ -14,6 +14,7 @@ class CWSolana extends Solana { String? parentAddress, WalletInfo? walletInfo, String? password, + String? passphrase, }) => SolanaNewWalletCredentials( name: name, @@ -21,6 +22,7 @@ class CWSolana extends Solana { password: password, mnemonic: mnemonic, parentAddress: parentAddress, + passphrase: passphrase, ); @override @@ -28,8 +30,14 @@ class CWSolana extends Solana { required String name, required String mnemonic, required String password, + String? passphrase, }) => - SolanaRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic); + SolanaRestoreWalletFromSeedCredentials( + name: name, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + ); @override WalletCredentials createSolanaRestoreWalletFromPrivateKey({ 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 8b7be18a1..26c96fb74 100644 --- a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart +++ b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart @@ -189,7 +189,7 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo ), ); }), - if (!widget.isFromRestore) ...[ + if (!widget.isFromRestore) Observer(builder: (_) { if (widget.privacySettingsViewModel.hasSeedPhraseLengthOption) return SettingsPickerCell( @@ -202,54 +202,53 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo ); return Container(); }), - if (widget.privacySettingsViewModel.hasPassphraseOption) - Padding( - padding: EdgeInsets.all(24), - child: Form( - key: _passphraseFormKey, - child: Column( - children: [ - BaseTextFormField( - hintText: S.of(context).passphrase, - controller: passphraseController, - obscureText: obscurePassphrase, - suffixIcon: GestureDetector( - onTap: () => setState(() { - obscurePassphrase = !obscurePassphrase; - }), - child: Icon( - Icons.remove_red_eye, - color: obscurePassphrase ? Colors.black54 : Colors.black26, - ), + if (widget.privacySettingsViewModel.hasPassphraseOption) + Padding( + padding: EdgeInsets.all(24), + child: Form( + key: _passphraseFormKey, + child: Column( + children: [ + BaseTextFormField( + hintText: S.of(context).passphrase, + controller: passphraseController, + obscureText: obscurePassphrase, + suffixIcon: GestureDetector( + onTap: () => setState(() { + obscurePassphrase = !obscurePassphrase; + }), + child: Icon( + Icons.remove_red_eye, + color: obscurePassphrase ? Colors.black54 : Colors.black26, ), ), - const SizedBox(height: 10), - BaseTextFormField( - hintText: S.of(context).confirm_passphrase, - controller: confirmPassphraseController, - obscureText: obscurePassphrase, - validator: (text) { - if (text == passphraseController.text) { - return null; - } + ), + const SizedBox(height: 10), + BaseTextFormField( + hintText: S.of(context).confirm_passphrase, + controller: confirmPassphraseController, + obscureText: obscurePassphrase, + validator: (text) { + if (text == passphraseController.text) { + return null; + } - return S.of(context).passphrases_doesnt_match; - }, - suffixIcon: GestureDetector( - onTap: () => setState(() { - obscurePassphrase = !obscurePassphrase; - }), - child: Icon( - Icons.remove_red_eye, - color: obscurePassphrase ? Colors.black54 : Colors.black26, - ), + return S.of(context).passphrases_doesnt_match; + }, + suffixIcon: GestureDetector( + onTap: () => setState(() { + obscurePassphrase = !obscurePassphrase; + }), + child: Icon( + Icons.remove_red_eye, + color: obscurePassphrase ? Colors.black54 : Colors.black26, ), ), - ], - ), + ), + ], ), ), - ], + ), Observer(builder: (_) { return Column( children: [ @@ -311,13 +310,14 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo widget.nodeViewModel.save(); } if (passphraseController.text.isNotEmpty) { - if (_passphraseFormKey.currentState != null && !_passphraseFormKey.currentState!.validate()) { + if (_passphraseFormKey.currentState != null && + !_passphraseFormKey.currentState!.validate()) { return; } - - widget.seedTypeViewModel.setPassphrase(passphraseController.text); } + widget.seedTypeViewModel.setPassphrase(passphraseController.text); + Navigator.pop(context); }, text: S.of(context).continue_text, 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 897a6bed0..c8a75cf40 100644 --- a/lib/src/screens/restore/wallet_restore_from_seed_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_seed_form.dart @@ -19,7 +19,6 @@ class WalletRestoreFromSeedForm extends StatefulWidget { WalletRestoreFromSeedForm({Key? key, required this.displayLanguageSelector, required this.displayBlockHeightSelector, - required this.displayPassphrase, required this.type, required this.displayWalletPassword, required this.seedSettingsViewModel, @@ -35,7 +34,6 @@ class WalletRestoreFromSeedForm extends StatefulWidget { final bool displayLanguageSelector; final bool displayBlockHeightSelector; final bool displayWalletPassword; - final bool displayPassphrase; final SeedSettingsViewModel seedSettingsViewModel; final FocusNode? blockHeightFocusNode; final Function(bool)? onHeightOrDateEntered; @@ -60,7 +58,6 @@ class WalletRestoreFromSeedFormState extends State { repeatedPasswordTextEditingController = displayWalletPassword ? TextEditingController() : null, - passphraseController = TextEditingController(), seedTypeController = TextEditingController(); final GlobalKey seedWidgetStateKey; @@ -70,15 +67,11 @@ class WalletRestoreFromSeedFormState extends State { final TextEditingController? passwordTextEditingController; final TextEditingController? repeatedPasswordTextEditingController; final TextEditingController seedTypeController; - final TextEditingController passphraseController; final GlobalKey formKey; late ReactionDisposer moneroSeedTypeReaction; String language; void Function()? passwordListener; void Function()? repeatedPasswordListener; - void Function()? passphraseListener; - - bool obscurePassphrase = true; @override void initState() { @@ -96,9 +89,6 @@ class WalletRestoreFromSeedFormState extends State { repeatedPasswordTextEditingController?.addListener(repeatedPasswordListener!); } - passphraseListener = () => widget.seedSettingsViewModel.setPassphrase(passphraseController.text); - passphraseController.addListener(passphraseListener!); - moneroSeedTypeReaction = reaction((_) => widget.seedSettingsViewModel.moneroSeedType, (MoneroSeedType item) { _setSeedType(item); @@ -120,8 +110,6 @@ class WalletRestoreFromSeedFormState extends State { repeatedPasswordTextEditingController?.removeListener(repeatedPasswordListener!); } - passphraseController.removeListener(passphraseListener!); - super.dispose(); } @@ -280,23 +268,6 @@ class WalletRestoreFromSeedFormState extends State { hasDatePicker: widget.type == WalletType.monero || widget.type == WalletType.wownero, walletType: widget.type, ), - if (widget.displayPassphrase) ...[ - const SizedBox(height: 10), - BaseTextFormField( - hintText: S.current.passphrase, - controller: passphraseController, - obscureText: obscurePassphrase, - suffixIcon: GestureDetector( - onTap: () => setState(() { - obscurePassphrase = !obscurePassphrase; - }), - child: Icon( - Icons.remove_red_eye, - color: obscurePassphrase ? Colors.black54 : Colors.black26, - ), - ), - ), - ] ])); } diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 4a4e27b3d..6215e26c3 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -37,7 +37,6 @@ class WalletRestorePage extends BasePage { displayBlockHeightSelector: walletRestoreViewModel.hasBlockchainHeightLanguageSelector, displayLanguageSelector: walletRestoreViewModel.hasSeedLanguageSelector, - displayPassphrase: walletRestoreViewModel.hasPassphrase, type: walletRestoreViewModel.type, key: walletRestoreFromSeedFormKey, blockHeightFocusNode: _blockHeightFocusNode, @@ -320,9 +319,7 @@ class WalletRestorePage extends BasePage { -1; } - if (walletRestoreViewModel.hasPassphrase) { - credentials['passphrase'] = seedSettingsViewModel.passphrase; - } + credentials['passphrase'] = seedSettingsViewModel.passphrase; credentials['name'] = walletRestoreFromSeedFormKey.currentState!.nameTextEditingController.text; diff --git a/lib/tron/cw_tron.dart b/lib/tron/cw_tron.dart index b4aed4235..8bceafe01 100644 --- a/lib/tron/cw_tron.dart +++ b/lib/tron/cw_tron.dart @@ -15,12 +15,14 @@ class CWTron extends Tron { String? password, String? mnemonic, String? parentAddress, + String? passphrase, }) => TronNewWalletCredentials( name: name, walletInfo: walletInfo, password: password, mnemonic: mnemonic, + passphrase: passphrase, parentAddress: parentAddress); @override @@ -28,8 +30,14 @@ class CWTron extends Tron { required String name, required String mnemonic, required String password, + String? passphrase, }) => - TronRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic); + TronRestoreWalletFromSeedCredentials( + name: name, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + ); @override WalletCredentials createTronRestoreWalletFromPrivateKey({ diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index b97c796f7..85b9dbead 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -75,6 +75,9 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash, + WalletType.ethereum, + WalletType.polygon, + WalletType.tron, ].contains(type); @computed diff --git a/lib/view_model/restore/restore_from_qr_vm.dart b/lib/view_model/restore/restore_from_qr_vm.dart index 6701b639d..798105b9d 100644 --- a/lib/view_model/restore/restore_from_qr_vm.dart +++ b/lib/view_model/restore/restore_from_qr_vm.dart @@ -13,7 +13,7 @@ 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: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'; @@ -26,14 +26,19 @@ 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) + 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); + super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, + type: type, isRecovery: true); @observable int height; @@ -124,26 +129,44 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password, + passphrase: restoreWallet.passphrase, ); case WalletType.ethereum: return ethereum!.createEthereumRestoreWalletFromSeedCredentials( - name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); + name: name, + mnemonic: restoreWallet.mnemonicSeed ?? '', + password: password, + passphrase: restoreWallet.passphrase, + ); case WalletType.nano: 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); + name: name, + mnemonic: restoreWallet.mnemonicSeed ?? '', + password: password, + passphrase: restoreWallet.passphrase, + ); case WalletType.solana: return solana!.createSolanaRestoreWalletFromSeedCredentials( - name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); + name: name, + mnemonic: restoreWallet.mnemonicSeed ?? '', + password: password, + passphrase: restoreWallet.passphrase, + ); case WalletType.tron: return tron!.createTronRestoreWalletFromSeedCredentials( - name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); + name: name, + mnemonic: restoreWallet.mnemonicSeed ?? '', + password: password, + passphrase: restoreWallet.passphrase, + ); case WalletType.wownero: return wownero!.createWowneroRestoreWalletFromSeedCredentials( name: name, diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index 2919afcac..be30811d9 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -106,6 +106,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { password: walletPassword, mnemonic: newWalletArguments!.mnemonic, parentAddress: newWalletArguments!.parentAddress, + passphrase: passphrase, ); case WalletType.bitcoinCash: return bitcoinCash!.createBitcoinCashNewWalletCredentials( @@ -122,6 +123,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { password: walletPassword, mnemonic: newWalletArguments!.mnemonic, parentAddress: newWalletArguments!.parentAddress, + passphrase: passphrase, ); case WalletType.polygon: return polygon!.createPolygonNewWalletCredentials( @@ -129,6 +131,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { password: walletPassword, mnemonic: newWalletArguments!.mnemonic, parentAddress: newWalletArguments!.parentAddress, + passphrase: passphrase, ); case WalletType.solana: return solana!.createSolanaNewWalletCredentials( @@ -136,6 +139,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { password: walletPassword, mnemonic: newWalletArguments!.mnemonic, parentAddress: newWalletArguments!.parentAddress, + passphrase: passphrase, ); case WalletType.tron: return tron!.createTronNewWalletCredentials( @@ -143,6 +147,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { password: walletPassword, mnemonic: newWalletArguments!.mnemonic, parentAddress: newWalletArguments!.parentAddress, + passphrase: passphrase, ); case WalletType.wownero: return wownero!.createWowneroNewWalletCredentials( diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 5462ce4c8..d37b69f74 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -78,9 +78,6 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { final bool hasBlockchainHeightLanguageSelector; final bool hasRestoreFromPrivateKey; - bool get hasPassphrase => - [WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(type); - @observable WalletRestoreMode mode; @@ -116,10 +113,18 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { name: name, height: height, mnemonic: seed, password: password); case WalletType.ethereum: return ethereum!.createEthereumRestoreWalletFromSeedCredentials( - name: name, mnemonic: seed, password: password); + name: name, + mnemonic: seed, + password: password, + passphrase: passphrase, + ); case WalletType.bitcoinCash: return bitcoinCash!.createBitcoinCashRestoreWalletFromSeedCredentials( - name: name, mnemonic: seed, password: password); + name: name, + mnemonic: seed, + password: password, + passphrase: passphrase, + ); case WalletType.nano: case WalletType.banano: return nano!.createNanoRestoreWalletFromSeedCredentials( @@ -127,24 +132,28 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { mnemonic: seed, password: password, derivationType: derivationInfo!.derivationType!, + passphrase: passphrase, ); case WalletType.polygon: return polygon!.createPolygonRestoreWalletFromSeedCredentials( name: name, mnemonic: seed, password: password, + passphrase: passphrase, ); case WalletType.solana: return solana!.createSolanaRestoreWalletFromSeedCredentials( name: name, mnemonic: seed, password: password, + passphrase: passphrase, ); case WalletType.tron: return tron!.createTronRestoreWalletFromSeedCredentials( name: name, mnemonic: seed, password: password, + passphrase: passphrase, ); case WalletType.wownero: return wownero!.createWowneroRestoreWalletFromSeedCredentials( diff --git a/tool/configure.dart b/tool/configure.dart index d54ec153d..c3d84c4f6 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -843,8 +843,8 @@ 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}); - WalletCredentials createEthereumRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password}); + WalletCredentials createEthereumNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? parentAddress, 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}); String getAddress(WalletBase wallet); @@ -947,8 +947,8 @@ 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}); - WalletCredentials createPolygonRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password}); + WalletCredentials createPolygonNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? parentAddress, 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}); String getAddress(WalletBase wallet); @@ -1119,6 +1119,7 @@ abstract class Nano { String? mnemonic, String? parentAddress, WalletInfo? walletInfo, + String? passphrase, }); WalletCredentials createNanoRestoreWalletFromSeedCredentials({ @@ -1126,6 +1127,7 @@ abstract class Nano { required String password, required String mnemonic, required DerivationType derivationType, + String? passphrase, }); WalletCredentials createNanoRestoreWalletFromKeysCredentials({ @@ -1234,9 +1236,9 @@ 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,}); + {required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? parentAddress, String? passphrase}); WalletCredentials createSolanaRestoreWalletFromSeedCredentials( - {required String name, required String mnemonic, required String password}); + {required String name, required String mnemonic, required String password, String? passphrase}); WalletCredentials createSolanaRestoreWalletFromPrivateKey( {required String name, required String privateKey, required String password}); @@ -1320,9 +1322,8 @@ import 'package:cw_tron/tron_wallet_service.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}); - WalletCredentials createTronRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password}); + WalletCredentials createTronNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? parentAddress, 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); From d933743a72be1e5c1031c82447cb2d104ba498f2 Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 4 Oct 2024 22:04:23 +0300 Subject: [PATCH 05/73] Cw 772 restore from qr generates different wallet (#1721) * fix derivation info for QR restoring * allow all available seed languages for Monero --------- Co-authored-by: Omar Hatem --- .../restore/restore_from_qr_vm.dart | 21 +++--- .../restore/wallet_restore_from_qr_code.dart | 4 ++ lib/view_model/wallet_creation_vm.dart | 64 ++++++++----------- 3 files changed, 40 insertions(+), 49 deletions(-) diff --git a/lib/view_model/restore/restore_from_qr_vm.dart b/lib/view_model/restore/restore_from_qr_vm.dart index 798105b9d..042a048d9 100644 --- a/lib/view_model/restore/restore_from_qr_vm.dart +++ b/lib/view_model/restore/restore_from_qr_vm.dart @@ -37,8 +37,7 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store spendKey = '', wif = '', address = '', - super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, - type: type, isRecovery: true); + super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, type: type, isRecovery: true); @observable int height; @@ -58,13 +57,10 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store bool get hasRestorationHeight => type == WalletType.monero || type == WalletType.wownero; @override - WalletCredentials getCredentialsFromRestoredWallet( - dynamic options, RestoredWallet restoreWallet) { + Future getWalletCredentialsFromQRCredentials( + RestoredWallet restoreWallet) async { final password = generateWalletPassword(); - DerivationInfo? derivationInfo; - derivationInfo ??= getDefaultCreateDerivation(); - switch (restoreWallet.restoreMode) { case WalletRestoreMode.keys: switch (restoreWallet.type) { @@ -116,12 +112,13 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store ); case WalletType.bitcoin: case WalletType.litecoin: + final derivationInfo = (await getDerivationInfoFromQRCredentials(restoreWallet)).first; return bitcoin!.createBitcoinRestoreWalletFromSeedCredentials( name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password, passphrase: restoreWallet.passphrase, - derivationType: derivationInfo!.derivationType!, + derivationType: derivationInfo.derivationType!, derivationPath: derivationInfo.derivationPath!, ); case WalletType.bitcoinCash: @@ -139,11 +136,13 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store 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!, + derivationType: derivationInfo.derivationType!, passphrase: restoreWallet.passphrase, ); case WalletType.polygon: @@ -183,8 +182,8 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store } @override - Future processFromRestoredWallet( - WalletCredentials credentials, RestoredWallet restoreWallet) async { + Future processFromRestoredWallet(WalletCredentials credentials, + RestoredWallet restoreWallet) async { try { switch (restoreWallet.restoreMode) { case WalletRestoreMode.keys: 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 335b1a006..23850befa 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -142,6 +142,10 @@ class WalletRestoreFromQRCode { return WalletRestoreMode.seed; } + if ((type == WalletType.monero || type == WalletType.wownero)) { + return WalletRestoreMode.seed; + } + seedValue.split(' ').forEach((element) { if (!words.contains(element)) { throw Exception( diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 9fce86712..13c1a5661 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -85,21 +85,9 @@ abstract class WalletCreationVMBase with Store { final dirPath = await pathForWalletDir(name: name, type: type); final path = await pathForWallet(name: name, type: type); - WalletCredentials credentials; - if (restoreWallet != null) { - if (restoreWallet.restoreMode == WalletRestoreMode.seed && - options == null && - (type == WalletType.nano || - type == WalletType.bitcoin || - type == WalletType.litecoin)) { - final derivationInfo = await getDerivationInfo(restoreWallet); - options ??= {}; - options["derivationInfo"] = derivationInfo.first; - } - credentials = getCredentialsFromRestoredWallet(options, restoreWallet); - } else { - credentials = getCredentials(options); - } + final credentials = restoreWallet != null + ? await getWalletCredentialsFromQRCredentials(restoreWallet) + : getCredentials(options); final walletInfo = WalletInfo.external( id: WalletBase.idFor(name, type), @@ -200,36 +188,36 @@ abstract class WalletCreationVMBase with Store { } } - Future> getDerivationInfo(RestoredWallet restoreWallet) async { - var list = []; - final walletType = restoreWallet.type; - var appStore = getIt.get(); - var node = appStore.settingsStore.getCurrentNode(walletType); + Future> getDerivationInfoFromQRCredentials(RestoredWallet restoreWallet) async { + var list = []; + final walletType = restoreWallet.type; + var appStore = getIt.get(); + var node = appStore.settingsStore.getCurrentNode(walletType); - switch (walletType) { - case WalletType.bitcoin: - case WalletType.litecoin: - return bitcoin!.getDerivationsFromMnemonic( - mnemonic: restoreWallet.mnemonicSeed!, - node: node, - ); - case WalletType.nano: - return nanoUtil!.getDerivationsFromMnemonic( - mnemonic: restoreWallet.mnemonicSeed!, - node: node, - ); - default: - break; + switch (walletType) { + case WalletType.bitcoin: + case WalletType.litecoin: + return bitcoin!.getDerivationsFromMnemonic( + mnemonic: restoreWallet.mnemonicSeed!, + node: node, + passphrase: restoreWallet.passphrase, + ); + case WalletType.nano: + return nanoUtil!.getDerivationsFromMnemonic( + mnemonic: restoreWallet.mnemonicSeed!, + node: node, + ); + default: + break; + } + return list; } - return list; - } WalletCredentials getCredentials(dynamic options) => throw UnimplementedError(); Future process(WalletCredentials credentials) => throw UnimplementedError(); - WalletCredentials getCredentialsFromRestoredWallet( - dynamic options, RestoredWallet restoreWallet) => + Future getWalletCredentialsFromQRCredentials(RestoredWallet restoreWallet) async => throw UnimplementedError(); Future processFromRestoredWallet( From 6dba73a1d5b627a8bb4566f273aec63111a90cb3 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Fri, 4 Oct 2024 23:00:36 +0300 Subject: [PATCH 06/73] Generic fixes (#1716) * reset brightness after using * set default qr to auto detect * minor safety enhancement * close socket after checking connectivity * fix incorrect parent exception class * pump medium and fast priorities even if they are equal to slow * Add Ton to exchange * fix silent payments add Seth bitcoin node * deselect quantex add seth node for existing users * fix node uri --- assets/bitcoin_electrum_server_list.yml | 3 + assets/images/ton_icon.png | Bin 0 -> 14318 bytes cw_bitcoin/lib/electrum_wallet.dart | 48 +++++++++------- cw_bitcoin/pubspec.lock | 39 +++++++++---- cw_core/lib/crypto_currency.dart | 2 + cw_core/lib/node.dart | 7 ++- lib/entities/default_settings_migration.dart | 52 ++++++++++++++++-- lib/main.dart | 2 +- lib/reactions/check_connection.dart | 2 + lib/src/screens/receive/widgets/qr_image.dart | 2 +- lib/utils/brightness_util.dart | 5 +- 11 files changed, 119 insertions(+), 43 deletions(-) create mode 100644 assets/images/ton_icon.png diff --git a/assets/bitcoin_electrum_server_list.yml b/assets/bitcoin_electrum_server_list.yml index 8b734a7bb..305db38f5 100644 --- a/assets/bitcoin_electrum_server_list.yml +++ b/assets/bitcoin_electrum_server_list.yml @@ -6,3 +6,6 @@ isDefault: true - uri: electrs.cakewallet.com:50001 +- + uri: fulcrum.sethforprivacy.com:50002 + useSSL: true diff --git a/assets/images/ton_icon.png b/assets/images/ton_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..90f9968cccff796368b266a6f497fdd7403091b3 GIT binary patch literal 14318 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 YCEguol^6iv2LH6xZ{B!+9TWcl0QGAZ7ytkO literal 0 HcmV?d00001 diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 4f8f3eab5..877b108b7 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -479,8 +479,14 @@ abstract class ElectrumWalletBase final result = json.decode(response.body) as Map; final slowFee = result['economyFee']?.toInt() ?? 0; - final mediumFee = result['hourFee']?.toInt() ?? 0; - final fastFee = result['fastestFee']?.toInt() ?? 0; + int mediumFee = result['hourFee']?.toInt() ?? 0; + int fastFee = result['fastestFee']?.toInt() ?? 0; + if (slowFee == mediumFee) { + mediumFee++; + } + while (fastFee <= mediumFee) { + fastFee++; + } _feeRates = [slowFee, mediumFee, fastFee]; return; } catch (_) {} @@ -813,6 +819,8 @@ abstract class ElectrumWalletBase network: network, memo: memo, feeRate: feeRate, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + vinOutpoints: utxoDetails.vinOutpoints, ); if (fee == 0) { @@ -1639,27 +1647,29 @@ abstract class ElectrumWalletBase if (verboseTransaction.isEmpty) { transactionHex = await electrumClient.getTransactionHex(hash: hash); - if (height != null && await checkIfMempoolAPIIsEnabled()) { - final blockHash = await http.get( - Uri.parse( - "http://mempool.cakewallet.com:8999/api/v1/block-height/$height", - ), - ); - - if (blockHash.statusCode == 200 && - blockHash.body.isNotEmpty && - jsonDecode(blockHash.body) != null) { - final blockResponse = await http.get( + if (height != null && height > 0 && await checkIfMempoolAPIIsEnabled()) { + try { + final blockHash = await http.get( Uri.parse( - "http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}", + "http://mempool.cakewallet.com:8999/api/v1/block-height/$height", ), ); - if (blockResponse.statusCode == 200 && - blockResponse.body.isNotEmpty && - jsonDecode(blockResponse.body)['timestamp'] != null) { - time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString()); + + if (blockHash.statusCode == 200 && + blockHash.body.isNotEmpty && + jsonDecode(blockHash.body) != null) { + final blockResponse = await http.get( + Uri.parse( + "http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}", + ), + ); + if (blockResponse.statusCode == 200 && + blockResponse.body.isNotEmpty && + jsonDecode(blockResponse.body)['timestamp'] != null) { + time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString()); + } } - } + } catch (_) {} } } else { transactionHex = verboseTransaction['hex'] as String; diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index f741230a5..4c7c49a9e 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -49,6 +49,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bech32: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "05755063b593aa6cca0a4820a318e0ce17de6192" + url: "https://github.com/cake-tech/bech32.git" + source: git + version: "0.2.2" bip32: dependency: transitive description: @@ -78,8 +87,8 @@ packages: dependency: "direct overridden" description: path: "." - ref: cake-update-v7 - resolved-ref: f577e83fe78766b2655ea0602baa9299b953a31b + ref: cake-update-v8 + resolved-ref: fc045a11db3d85d806ca67f75e8b916c706745a2 url: "https://github.com/cake-tech/bitcoin_base" source: git version: "4.7.0" @@ -308,13 +317,13 @@ packages: source: hosted version: "1.3.1" ffi: - dependency: transitive + dependency: "direct overridden" description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.0" ffigen: dependency: transitive description: @@ -450,6 +459,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" + http2: + dependency: transitive + description: + name: http2 + sha256: "9ced024a160b77aba8fb8674e38f70875e321d319e6f303ec18e87bd5a4b0c1d" + url: "https://pub.dev" + source: hosted + version: "2.3.0" http_multi_server: dependency: transitive description: @@ -693,13 +710,13 @@ packages: source: hosted version: "2.1.8" pointycastle: - dependency: transitive + dependency: "direct overridden" description: name: pointycastle - sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" url: "https://pub.dev" source: hosted - version: "3.9.1" + version: "3.7.4" pool: dependency: transitive description: @@ -709,13 +726,13 @@ packages: source: hosted version: "1.5.1" protobuf: - dependency: transitive + dependency: "direct overridden" description: name: protobuf - sha256: "01dd9bd0fa02548bf2ceee13545d4a0ec6046459d847b6b061d8a27237108a08" + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "3.1.0" provider: dependency: transitive description: diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 4f9b5d835..0280bb45a 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -106,6 +106,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen CryptoCurrency.usdcTrc20, CryptoCurrency.tbtc, CryptoCurrency.wow, + CryptoCurrency.ton, ]; static const havenCurrencies = [ @@ -223,6 +224,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const usdcTrc20 = CryptoCurrency(title: 'USDC', tag: 'TRX', fullName: 'USDC Coin', raw: 92, name: 'usdctrc20', iconPath: 'assets/images/usdc_icon.png', decimals: 6); static const tbtc = CryptoCurrency(title: 'tBTC', fullName: 'Testnet Bitcoin', raw: 93, name: 'tbtc', iconPath: 'assets/images/tbtc.png', decimals: 8); static const wow = CryptoCurrency(title: 'WOW', fullName: 'Wownero', raw: 94, name: 'wow', iconPath: 'assets/images/wownero_icon.png', decimals: 11); + static const ton = CryptoCurrency(title: 'TON', fullName: 'Toncoin', raw: 95, name: 'ton', iconPath: 'assets/images/ton_icon.png', decimals: 8); static final Map _rawCurrencyMap = diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 85c61de15..e19d2a54b 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -239,12 +239,15 @@ class Node extends HiveObject with Keyable { // you try to communicate with it Future requestElectrumServer() async { try { + final Socket socket; if (useSSL == true) { - await SecureSocket.connect(uri.host, uri.port, + socket = await SecureSocket.connect(uri.host, uri.port, timeout: Duration(seconds: 5), onBadCertificate: (_) => true); } else { - await Socket.connect(uri.host, uri.port, timeout: Duration(seconds: 5)); + socket = await Socket.connect(uri.host, uri.port, timeout: Duration(seconds: 5)); } + + socket.destroy(); return true; } catch (_) { return false; diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 802ad0184..1c46cd605 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io' show Directory, File, Platform; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/secure_storage.dart'; @@ -234,7 +235,8 @@ Future defaultSettingsMigration( break; case 36: await addWowneroNodeList(nodes: nodes); - await changeWowneroCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + await changeWowneroCurrentNodeToDefault( + sharedPreferences: sharedPreferences, nodes: nodes); break; case 37: await replaceTronDefaultNode(sharedPreferences: sharedPreferences, nodes: nodes); @@ -249,6 +251,10 @@ Future defaultSettingsMigration( case 40: await removeMoneroWorld(sharedPreferences: sharedPreferences, nodes: nodes); break; + case 41: + _deselectQuantex(sharedPreferences); + await _addSethNode(nodes, sharedPreferences); + break; default: break; } @@ -263,6 +269,19 @@ Future defaultSettingsMigration( await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, version); } +void _deselectQuantex(SharedPreferences sharedPreferences) { + final Map exchangeProvidersSelection = + json.decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") + as Map; + + exchangeProvidersSelection['Quantex'] = false; + + sharedPreferences.setString( + PreferencesKey.exchangeProvidersSelection, + json.encode(exchangeProvidersSelection), + ); +} + void _fixNodesUseSSLFlag(Box nodes) { for (Node node in nodes.values) { switch (node.uriRaw) { @@ -887,7 +906,30 @@ Future changeDefaultBitcoinNode( } } +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 { const cakeWalletBitcoinNodeUriPattern = '.cakewallet.com'; final currentBitcoinNodeId = sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); @@ -896,12 +938,11 @@ Future _addElectRsNode(Box nodeSource, SharedPreferences sharedPrefe final needToReplaceCurrentBitcoinNode = currentBitcoinNode.uri.toString().contains(cakeWalletBitcoinNodeUriPattern); - final newElectRsBitcoinNode = - Node(uri: cakeWalletSilentPaymentsElectrsUri, type: WalletType.bitcoin, useSSL: false); + final newElectRsBitcoinNode = Node(uri: nodeUri, type: WalletType.bitcoin, useSSL: useSSL); await nodeSource.add(newElectRsBitcoinNode); - if (needToReplaceCurrentBitcoinNode) { + if (needToReplaceCurrentBitcoinNode && replaceExisting) { await sharedPreferences.setInt( PreferencesKey.currentBitcoinElectrumSererIdKey, newElectRsBitcoinNode.key as int); } @@ -1262,7 +1303,8 @@ Future removeMoneroWorld( const cakeWalletMoneroNodeUriPattern = '.moneroworld.com'; final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); final currentMoneroNode = nodes.values.firstWhere((node) => node.key == currentMoneroNodeId); - final needToReplaceCurrentMoneroNode = currentMoneroNode.uri.toString().contains(cakeWalletMoneroNodeUriPattern); + final needToReplaceCurrentMoneroNode = + currentMoneroNode.uri.toString().contains(cakeWalletMoneroNodeUriPattern); nodes.values.forEach((node) async { if (node.type == WalletType.monero && diff --git a/lib/main.dart b/lib/main.dart index dcfd8d0da..7237de8b9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -192,7 +192,7 @@ Future initializeAppConfigs() async { transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 40, + initialMigrationVersion: 41, ); } diff --git a/lib/reactions/check_connection.dart b/lib/reactions/check_connection.dart index 812fa9fcd..570b96864 100644 --- a/lib/reactions/check_connection.dart +++ b/lib/reactions/check_connection.dart @@ -11,6 +11,8 @@ Timer? _checkConnectionTimer; void startCheckConnectionReaction(WalletBase wallet, SettingsStore settingsStore, {int timeInterval = 5}) { _checkConnectionTimer?.cancel(); + // TODO: check the validity of this code, and if it's working fine, then no need for + // having the connect function in electrum.dart when the syncstatus is lost or failed and add the not connected state _checkConnectionTimer = Timer.periodic(Duration(seconds: timeInterval), (_) async { if (wallet.type == WalletType.bitcoin && wallet.syncStatus is SyncingSyncStatus) { return; diff --git a/lib/src/screens/receive/widgets/qr_image.dart b/lib/src/screens/receive/widgets/qr_image.dart index f388fdd0b..043958ef4 100644 --- a/lib/src/screens/receive/widgets/qr_image.dart +++ b/lib/src/screens/receive/widgets/qr_image.dart @@ -23,7 +23,7 @@ class QrImage extends StatelessWidget { return qr.QrImageView( data: data, errorCorrectionLevel: errorCorrectionLevel, - version: version ?? 9, // Previous value: 7 something happened after flutter upgrade monero wallets addresses are longer than ver. 7 ??? + version: version ?? qr.QrVersions.auto, size: size, foregroundColor: foregroundColor, backgroundColor: backgroundColor, diff --git a/lib/utils/brightness_util.dart b/lib/utils/brightness_util.dart index 5afe065e5..1b34bceac 100644 --- a/lib/utils/brightness_util.dart +++ b/lib/utils/brightness_util.dart @@ -9,15 +9,12 @@ class BrightnessUtil { return; } - // Get the current brightness: - final brightness = await DeviceDisplayBrightness.getBrightness(); - // ignore: unawaited_futures DeviceDisplayBrightness.setBrightness(1.0); await func(); // ignore: unawaited_futures - DeviceDisplayBrightness.setBrightness(brightness); + DeviceDisplayBrightness.resetBrightness(); } } \ No newline at end of file From dfccedddb21b9581fe45b9a35ca5d1b4bacdec11 Mon Sep 17 00:00:00 2001 From: Serhii Date: Sat, 5 Oct 2024 00:50:36 +0300 Subject: [PATCH 07/73] Cw 769 fix transaction notes not showing (#1718) * use focusNode instead of onTapOutside for TextFieldListRow * add a transaction description box to the backup * fix --------- Co-authored-by: OmarHatem --- lib/core/backup_service.dart | 33 ++++- lib/di.dart | 1 + lib/entities/transaction_description.dart | 14 +++ .../widgets/textfield_list_row.dart | 117 +++++++++++------- .../unspent_coins_details_page.dart | 1 - 5 files changed, 117 insertions(+), 49 deletions(-) diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 42e24d3c7..26e2136dc 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -2,14 +2,13 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:cake_wallet/core/secure_storage.dart'; +import 'package:cake_wallet/entities/transaction_description.dart'; import 'package:cake_wallet/themes/theme_list.dart'; import 'package:cw_core/root_dir.dart'; import 'package:cake_wallet/utils/device_info.dart'; -import 'package:cw_core/root_dir.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:cryptography/cryptography.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:archive/archive_io.dart'; @@ -24,8 +23,8 @@ 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._keyService, this._sharedPreferences) + BackupService(this._secureStorage, this._walletInfoSource, this._transactionDescriptionBox, + this._keyService, this._sharedPreferences) : _cipher = Cryptography.instance.chacha20Poly1305Aead(), _correctWallets = []; @@ -38,6 +37,7 @@ class BackupService { final SecureStorage _secureStorage; final SharedPreferences _sharedPreferences; final Box _walletInfoSource; + final Box _transactionDescriptionBox; final KeyService _keyService; List _correctWallets; @@ -86,6 +86,13 @@ class BackupService { 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); @@ -107,8 +114,10 @@ class BackupService { }); 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(); @@ -160,6 +169,7 @@ class BackupService { await _verifyWallets(); await _importKeychainDumpV2(password); await _importPreferencesDump(); + await _importTransactionDescriptionDump(); } Future _verifyWallets() async { @@ -184,6 +194,21 @@ class BackupService { return await CakeHive.openBox(WalletInfo.boxName); } + Future _importTransactionDescriptionDump() async { + final appDir = await getAppDir(); + final transactionDescriptionFile = File('${appDir.path}/~_transaction_descriptions_dump'); + + if (!transactionDescriptionFile.existsSync()) { + return; + } + + final jsonData = + json.decode(transactionDescriptionFile.readAsStringSync()) as Map; + final descriptionsMap = jsonData.map((key, value) => + MapEntry(key, TransactionDescription.fromJson(value as Map))); + await _transactionDescriptionBox.putAll(descriptionsMap); + } + Future _importPreferencesDump() async { final appDir = await getAppDir(); final preferencesFile = File('${appDir.path}/~_preferences_dump'); diff --git a/lib/di.dart b/lib/di.dart index 4a1952729..5e5417b34 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1149,6 +1149,7 @@ Future setup({ getIt.registerFactory(() => CakeFeaturesViewModel(getIt.get())); getIt.registerFactory(() => BackupService(getIt.get(), _walletInfoSource, + _transactionDescriptionBox, getIt.get(), getIt.get())); getIt.registerFactory(() => BackupViewModel( diff --git a/lib/entities/transaction_description.dart b/lib/entities/transaction_description.dart index 088f9c480..2ac573652 100644 --- a/lib/entities/transaction_description.dart +++ b/lib/entities/transaction_description.dart @@ -21,4 +21,18 @@ class TransactionDescription extends HiveObject { String? transactionNote; String get note => transactionNote ?? ''; + + Map toJson() => { + 'id': id, + 'recipientAddress': recipientAddress, + 'transactionNote': transactionNote, + }; + + factory TransactionDescription.fromJson(Map json) { + return TransactionDescription( + id: json['id'] as String, + recipientAddress: json['recipientAddress'] as String?, + transactionNote: json['transactionNote'] as String?, + ); + } } diff --git a/lib/src/screens/transaction_details/widgets/textfield_list_row.dart b/lib/src/screens/transaction_details/widgets/textfield_list_row.dart index ff5513502..a86645ecb 100644 --- a/lib/src/screens/transaction_details/widgets/textfield_list_row.dart +++ b/lib/src/screens/transaction_details/widgets/textfield_list_row.dart @@ -1,27 +1,49 @@ -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:flutter/material.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; +import 'package:flutter/material.dart'; -class TextFieldListRow extends StatelessWidget { +class TextFieldListRow extends StatefulWidget { TextFieldListRow( {required this.title, required this.value, this.titleFontSize = 14, this.valueFontSize = 16, - this.onSubmitted, - this.onTapOutside}) - : _textController = TextEditingController() { - _textController.text = value; - } + this.onSubmitted}); final String title; final String value; final double titleFontSize; final double valueFontSize; final Function(String value)? onSubmitted; - final Function(String value)? onTapOutside; - final TextEditingController _textController; + + @override + _TextFieldListRowState createState() => _TextFieldListRowState(); +} + +class _TextFieldListRowState extends State { + late TextEditingController _textController; + late FocusNode _focusNode; + + @override + void initState() { + super.initState(); + _textController = TextEditingController(text: widget.value); + _focusNode = FocusNode(); + + _focusNode.addListener(() { + if (!_focusNode.hasFocus) { + widget.onSubmitted?.call(_textController.text); + } + }); + } + + @override + void dispose() { + _textController.dispose(); + _focusNode.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -29,41 +51,48 @@ class TextFieldListRow extends StatelessWidget { width: double.infinity, color: Theme.of(context).colorScheme.background, child: Padding( - padding: - const EdgeInsets.only(left: 24, top: 16, bottom: 16, right: 24), + padding: const EdgeInsets.only(left: 24, top: 16, bottom: 16, right: 24), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, - style: TextStyle( - fontSize: titleFontSize, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.detailsTitlesColor), - textAlign: TextAlign.left), - TextField( - controller: _textController, - keyboardType: TextInputType.multiline, - textInputAction: TextInputAction.done, - maxLines: null, - textAlign: TextAlign.start, - style: TextStyle( - fontSize: valueFontSize, - fontWeight: FontWeight.w500, - color: - Theme.of(context).extension()!.titleColor), - decoration: InputDecoration( - isDense: true, - contentPadding: EdgeInsets.only(top: 12, bottom: 0), - hintText: S.of(context).enter_your_note, - hintStyle: TextStyle( - fontSize: valueFontSize, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.detailsTitlesColor), - border: InputBorder.none), - onTapOutside: (_) => onTapOutside?.call(_textController.text), - onSubmitted: (value) => onSubmitted?.call(value), - ) - ]), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.title, + style: TextStyle( + fontSize: widget.titleFontSize, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.detailsTitlesColor, + ), + textAlign: TextAlign.left, + ), + TextField( + controller: _textController, + focusNode: _focusNode, + keyboardType: TextInputType.multiline, + textInputAction: TextInputAction.done, + maxLines: null, + textAlign: TextAlign.start, + style: TextStyle( + fontSize: widget.valueFontSize, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.titleColor, + ), + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.only(top: 12, bottom: 0), + hintText: S.of(context).enter_your_note, + hintStyle: TextStyle( + fontSize: widget.valueFontSize, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.detailsTitlesColor, + ), + border: InputBorder.none, + ), + onSubmitted: (value) { + widget.onSubmitted?.call(value); + }, + ), + ], + ), ), ); } diff --git a/lib/src/screens/unspent_coins/unspent_coins_details_page.dart b/lib/src/screens/unspent_coins/unspent_coins_details_page.dart index 61689b52a..165995ba0 100644 --- a/lib/src/screens/unspent_coins/unspent_coins_details_page.dart +++ b/lib/src/screens/unspent_coins/unspent_coins_details_page.dart @@ -44,7 +44,6 @@ class UnspentCoinsDetailsPage extends BasePage { return TextFieldListRow( title: item.title, value: item.value, - onTapOutside: item.onSubmitted, onSubmitted: item.onSubmitted, ); } From cc61a25cd17f8662f56672e4d19f0a43579057f3 Mon Sep 17 00:00:00 2001 From: cyan Date: Fri, 4 Oct 2024 23:53:55 +0200 Subject: [PATCH 08/73] Set flush: true in backup service (#1717) This should ensure that files are flushed to disk before this function returns. Also, replaced foreach loop with for loop. --- lib/core/backup_service.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 26e2136dc..ba90a0449 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -130,18 +130,18 @@ class BackupService { final decryptedData = await _decryptV1(data, password, nonce); final zip = ZipDecoder().decodeBytes(decryptedData); - zip.files.forEach((file) { + for (var file in zip.files) { final filename = file.name; if (file.isFile) { final content = file.content as List; File('${appDir.path}/' + filename) ..createSync(recursive: true) - ..writeAsBytesSync(content); + ..writeAsBytesSync(content, flush: true); } else { Directory('${appDir.path}/' + filename)..create(recursive: true); } - }); + }; await _verifyWallets(); await _importKeychainDumpV1(password, nonce: nonce); @@ -153,18 +153,18 @@ class BackupService { final decryptedData = await _decryptV2(data, password); final zip = ZipDecoder().decodeBytes(decryptedData); - zip.files.forEach((file) { + for (var file in zip.files) { final filename = file.name; if (file.isFile) { final content = file.content as List; File('${appDir.path}/' + filename) ..createSync(recursive: true) - ..writeAsBytesSync(content); + ..writeAsBytesSync(content, flush: true); } else { Directory('${appDir.path}/' + filename)..create(recursive: true); } - }); + }; await _verifyWallets(); await _importKeychainDumpV2(password); From 382a0ff35ddb21b70aa1cde33cb32f258e85d02e Mon Sep 17 00:00:00 2001 From: cyan Date: Sat, 5 Oct 2024 02:30:16 +0200 Subject: [PATCH 09/73] Various monero fixes (#1722) * enable autogenerate subaddress on xmr in different place fix monero_com build fix autogenerate accounts in address book * Show account number for all wallets * generate addressbook with latest addresses regardless of isAutoGenerateEnabled --- lib/reactions/on_current_wallet_change.dart | 5 --- .../contact_list/contact_list_view_model.dart | 36 +++++++++---------- tool/configure.dart | 1 + 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 6f1ba1d8c..6630f1dfc 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -152,11 +152,6 @@ void _setAutoGenerateSubaddressStatus( WalletBase, TransactionInfo> wallet, SettingsStore settingsStore, ) async { - final walletHasAddresses = await wallet.walletAddresses.addressesMap.length > 1; - if (settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.initialized && - walletHasAddresses) { - settingsStore.autoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.disabled; - } wallet.isEnabledAutoGenerateSubaddress = settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.enabled || settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.initialized; 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 df6cbdb9f..4089d988b 100644 --- a/lib/view_model/contact_list/contact_list_view_model.dart +++ b/lib/view_model/contact_list/contact_list_view_model.dart @@ -26,22 +26,23 @@ abstract class ContactListViewModelBase with Store { isAutoGenerateEnabled = settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.enabled { walletInfoSource.values.forEach((info) { - if (isAutoGenerateEnabled && info.type == WalletType.monero && info.addressInfos != null) { - final key = info.addressInfos!.keys.first; - final value = info.addressInfos![key]; - final address = value?.first; - if (address != null) { - final name = _createName(info.name, address.label); - walletContacts.add(WalletContact( - address.address, - name, - walletTypeToCryptoCurrency(info.type), - )); + if ([WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type) && info.addressInfos != null) { + for (var key in info.addressInfos!.keys) { + final value = info.addressInfos![key]; + final address = value?.first; + if (address != null) { + final name = _createName(info.name, address.label, key: key); + walletContacts.add(WalletContact( + address.address, + name, + walletTypeToCryptoCurrency(info.type), + )); + } } } else if (info.addresses?.isNotEmpty == true && info.addresses!.length > 1) { if ([WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type)) { final address = info.address; - final name = _createName(info.name, ""); + final name = _createName(info.name, "", key: 0); walletContacts.add(WalletContact( address, name, @@ -52,7 +53,7 @@ abstract class ContactListViewModelBase with Store { if (label.isEmpty) { return; } - final name = _createName(info.name, label); + final name = _createName(info.name, label, key: null); walletContacts.add(WalletContact( address, name, @@ -65,7 +66,7 @@ abstract class ContactListViewModelBase with Store { } else { walletContacts.add(WalletContact( info.address, - info.name, + _createName(info.name, "", key: [WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type) ? 0 : null), walletTypeToCryptoCurrency(info.type), )); } @@ -76,10 +77,9 @@ abstract class ContactListViewModelBase with Store { initialFire: true); } - String _createName(String walletName, String label) { - return label.isNotEmpty - ? '$walletName (${label.replaceAll(RegExp(r'active', caseSensitive: false), S.current.active).replaceAll(RegExp(r'silent payments', caseSensitive: false), S.current.silent_payments)})' - : walletName; + String _createName(String walletName, String label, {int? key = null}) { + final actualLabel = label.replaceAll(RegExp(r'active', caseSensitive: false), S.current.active).replaceAll(RegExp(r'silent payments', caseSensitive: false), S.current.silent_payments); + return '$walletName${key == null ? "" : " [#${key}]"} ${actualLabel.isNotEmpty ? "($actualLabel)" : ""}'.trim(); } final bool isAutoGenerateEnabled; diff --git a/tool/configure.dart b/tool/configure.dart index c3d84c4f6..0c0f491a5 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -84,6 +84,7 @@ import 'package:cw_core/node.dart'; import 'package:cw_core/output_info.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/receive_page_option.dart'; +import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_transaction_output.dart'; From 37b822b7f522736779839fa00e36b12004f3d502 Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Fri, 4 Oct 2024 17:30:52 -0700 Subject: [PATCH 10/73] Mweb enhancements (#1715) * node peer enhancement, delay mweb address generation, increase logging * prevent unnecessary sync status changes if we can't connect to the ltc node * handle potential errors * set nodeUri to null for testing * [skip ci] redo good changes * [skip ci] draft * [skip ci] minor * [skip ci] cleanup * [skip ci] minor * [skip ci] minor * [skip ci] localization * [skip ci] save * [skip ci] wip * use proxy layer * ui * minor changes Add ToDos for later * fixes * [skip ci] minor * [skip ci] minor * [skip ci] ui * handle case where there are no addresses with txcount > 0 * comment out pegin button --------- Co-authored-by: OmarHatem --- cw_bitcoin/lib/electrum_wallet_addresses.dart | 4 +- cw_bitcoin/lib/litecoin_wallet.dart | 94 +- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 30 +- cw_core/lib/sync_status.dart | 10 +- .../com/cakewallet/mweb/CwMwebPlugin.kt | 3 +- cw_mweb/ios/Classes/CwMwebPlugin.swift | 5 +- cw_mweb/lib/cw_mweb.dart | 3 +- cw_mweb/lib/cw_mweb_method_channel.dart | 5 +- cw_mweb/lib/cw_mweb_platform_interface.dart | 2 +- lib/bitcoin/cw_bitcoin.dart | 11 + lib/core/sync_status_title.dart | 11 +- .../screens/dashboard/pages/balance_page.dart | 839 +++++++++++------- .../dashboard/widgets/menu_widget.dart | 2 +- lib/src/widgets/setting_actions.dart | 2 +- .../dashboard/balance_view_model.dart | 5 +- .../dashboard/dashboard_view_model.dart | 23 +- .../wallet_address_list_view_model.dart | 51 +- res/values/strings_ar.arb | 6 + res/values/strings_bg.arb | 6 + res/values/strings_cs.arb | 6 + res/values/strings_de.arb | 6 + res/values/strings_en.arb | 10 +- res/values/strings_es.arb | 6 + res/values/strings_fr.arb | 6 + res/values/strings_ha.arb | 6 + res/values/strings_hi.arb | 6 + res/values/strings_hr.arb | 6 + res/values/strings_hy.arb | 6 + res/values/strings_id.arb | 6 + res/values/strings_it.arb | 6 + res/values/strings_ja.arb | 6 + res/values/strings_ko.arb | 6 + res/values/strings_my.arb | 6 + res/values/strings_nl.arb | 6 + res/values/strings_pl.arb | 6 + res/values/strings_pt.arb | 6 + res/values/strings_ru.arb | 6 + res/values/strings_th.arb | 6 + res/values/strings_tl.arb | 6 + res/values/strings_tr.arb | 6 + res/values/strings_uk.arb | 6 + res/values/strings_ur.arb | 6 + res/values/strings_vi.arb | 6 + res/values/strings_yo.arb | 6 + res/values/strings_zh.arb | 6 + tool/configure.dart | 2 + 46 files changed, 853 insertions(+), 421 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 61d4fdbc3..91345d726 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -100,10 +100,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { static const gap = 20; final ObservableList _addresses; - late ObservableList addressesByReceiveType; + final ObservableList addressesByReceiveType; final ObservableList receiveAddresses; final ObservableList changeAddresses; + // TODO: add this variable in `bitcoin_wallet_addresses` and just add a cast in cw_bitcoin to use it final ObservableList silentAddresses; + // TODO: add this variable in `litecoin_wallet_addresses` and just add a cast in cw_bitcoin to use it final ObservableList mwebAddresses; final BasedUtxoNetwork network; final Bip32Slip10Secp256k1 mainHd; diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index d7e6fef61..8fe02ecd3 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -236,16 +236,18 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { Future waitForMwebAddresses() async { // ensure that we have the full 1000 mweb addresses generated before continuing: // should no longer be needed, but leaving here just in case - final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; - while (mwebAddrs.length < 1000) { - print("waiting for mweb addresses to finish generating..."); - await Future.delayed(const Duration(milliseconds: 1000)); - } + // final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; + // while (mwebAddrs.length < 1000) { + // print("waiting for mweb addresses to finish generating..."); + // await Future.delayed(const Duration(milliseconds: 1000)); + // } + await (walletAddresses as LitecoinWalletAddresses).ensureMwebAddressUpToIndexExists(1020); } @action @override Future startSync() async { + print("startSync() called!"); if (syncStatus is SyncronizingSyncStatus) { return; } @@ -289,46 +291,59 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { _syncTimer = Timer.periodic(const Duration(milliseconds: 1500), (timer) async { if (syncStatus is FailedSyncStatus) return; + print("SYNCING...."); + final nodeHeight = await electrumClient.getCurrentBlockChainTip() ?? 0; // current block height of our node - final resp = await CwMweb.status(StatusRequest()); - print("resp.mwebUtxosHeight: ${resp.mwebUtxosHeight}"); - print("resp.mwebHeaderHeight: ${resp.mwebHeaderHeight}"); - print("resp.blockHeaderHeight: ${resp.blockHeaderHeight}"); - if (resp.blockHeaderHeight < nodeHeight) { - int h = resp.blockHeaderHeight; - syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); - } else if (resp.mwebHeaderHeight < nodeHeight) { - int h = resp.mwebHeaderHeight; - syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); - } else if (resp.mwebUtxosHeight < nodeHeight) { - syncStatus = SyncingSyncStatus(1, 0.999); - } else { - if (resp.mwebUtxosHeight > walletInfo.restoreHeight) { - await walletInfo.updateRestoreHeight(resp.mwebUtxosHeight); - await checkMwebUtxosSpent(); - // update the confirmations for each transaction: - for (final transaction in transactionHistory.transactions.values) { - if (transaction.isPending) continue; - int txHeight = transaction.height ?? resp.mwebUtxosHeight; - final confirmations = (resp.mwebUtxosHeight - txHeight) + 1; - if (transaction.confirmations == confirmations) continue; - transaction.confirmations = confirmations; - transactionHistory.addOne(transaction); - } - await transactionHistory.save(); - } - - // prevent unnecessary reaction triggers: - if (syncStatus is! SyncedSyncStatus) { - // mwebd is synced, but we could still be processing incoming utxos: - if (!processingUtxos) { - syncStatus = SyncedSyncStatus(); - } + if (nodeHeight == 0) { + // we aren't connected to the ltc node yet + if (syncStatus is! NotConnectedSyncStatus) { + syncStatus = FailedSyncStatus(error: "Failed to connect to Litecoin node"); } return; } + + final resp = await CwMweb.status(StatusRequest()); + + try { + if (resp.blockHeaderHeight < nodeHeight) { + int h = resp.blockHeaderHeight; + syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); + } else if (resp.mwebHeaderHeight < nodeHeight) { + int h = resp.mwebHeaderHeight; + syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); + } else if (resp.mwebUtxosHeight < nodeHeight) { + syncStatus = SyncingSyncStatus(1, 0.999); + } else { + if (resp.mwebUtxosHeight > walletInfo.restoreHeight) { + await walletInfo.updateRestoreHeight(resp.mwebUtxosHeight); + await checkMwebUtxosSpent(); + // update the confirmations for each transaction: + for (final transaction in transactionHistory.transactions.values) { + if (transaction.isPending) continue; + int txHeight = transaction.height ?? resp.mwebUtxosHeight; + final confirmations = (resp.mwebUtxosHeight - txHeight) + 1; + if (transaction.confirmations == confirmations) continue; + transaction.confirmations = confirmations; + transactionHistory.addOne(transaction); + } + await transactionHistory.save(); + } + + // prevent unnecessary reaction triggers: + if (syncStatus is! SyncedSyncStatus) { + // mwebd is synced, but we could still be processing incoming utxos: + if (!processingUtxos) { + syncStatus = SyncedSyncStatus(); + } + } + return; + } + } catch (e) { + print("error syncing: $e"); + syncStatus = FailedSyncStatus(error: e.toString()); + } }); } @@ -411,6 +426,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } Future handleIncoming(MwebUtxo utxo, RpcClient stub) async { + print("handleIncoming() called!"); final status = await stub.status(StatusRequest()); var date = DateTime.now(); var confirmations = 0; diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 3a7856516..88f640d85 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -70,13 +70,9 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with await Future.delayed(Duration(milliseconds: 100)); } } - } - Future initMwebAddresses() async { - if (mwebAddrs.length < 1000) { - print("Generating MWEB addresses..."); - await ensureMwebAddressUpToIndexExists(1020); - print("done generating MWEB addresses"); + // ensure mweb addresses are up to date: + if (mwebAddresses.length < mwebAddrs.length) { List addressRecords = mwebAddrs .asMap() .entries @@ -88,7 +84,27 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with )) .toList(); addMwebAddresses(addressRecords); - print("added ${addressRecords.length} mweb addresses"); + print("set ${addressRecords.length} mweb addresses"); + } + } + + Future initMwebAddresses() async { + if (mwebAddrs.length < 1000) { + print("Generating MWEB addresses..."); + await ensureMwebAddressUpToIndexExists(20); + print("done generating MWEB addresses"); + // List addressRecords = mwebAddrs + // .asMap() + // .entries + // .map((e) => BitcoinAddressRecord( + // e.value, + // index: e.key, + // type: SegwitAddresType.mweb, + // network: network, + // )) + // .toList(); + // addMwebAddresses(addressRecords); + // print("added ${addressRecords.length} mweb addresses"); return; } } diff --git a/cw_core/lib/sync_status.dart b/cw_core/lib/sync_status.dart index 627b513b2..7d6b0a285 100644 --- a/cw_core/lib/sync_status.dart +++ b/cw_core/lib/sync_status.dart @@ -67,7 +67,13 @@ class AttemptingScanSyncStatus extends SyncStatus { double progress() => 0.0; } -class FailedSyncStatus extends NotConnectedSyncStatus {} +class FailedSyncStatus extends NotConnectedSyncStatus { + String? error; + FailedSyncStatus({this.error}); + + @override + String toString() => error ?? super.toString(); +} class ConnectingSyncStatus extends SyncStatus { @override @@ -89,4 +95,4 @@ class TimedOutSyncStatus extends NotConnectedSyncStatus { class LostConnectionSyncStatus extends NotConnectedSyncStatus { @override String toString() => 'Reconnecting'; -} \ No newline at end of file +} diff --git a/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt b/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt index 57ae3d4c3..0cc4cc537 100644 --- a/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt +++ b/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt @@ -30,7 +30,8 @@ class CwMwebPlugin: FlutterPlugin, MethodCallHandler { if (call.method == "start") { server?.stop() val dataDir = call.argument("dataDir") ?: "" - server = server ?: Mwebd.newServer("", dataDir, "") + val nodeUri = call.argument("nodeUri") ?: "" + server = server ?: Mwebd.newServer("", dataDir, nodeUri) port = server?.start(0) result.success(port) } else if (call.method == "stop") { diff --git a/cw_mweb/ios/Classes/CwMwebPlugin.swift b/cw_mweb/ios/Classes/CwMwebPlugin.swift index f1fd78cd8..92061ded8 100644 --- a/cw_mweb/ios/Classes/CwMwebPlugin.swift +++ b/cw_mweb/ios/Classes/CwMwebPlugin.swift @@ -12,6 +12,7 @@ public static func register(with registrar: FlutterPluginRegistrar) { private static var server: MwebdServer? private static var port: Int = 0 private static var dataDir: String? + private static var nodeUri: String? public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { @@ -22,7 +23,9 @@ public static func register(with registrar: FlutterPluginRegistrar) { stopServer() let args = call.arguments as? [String: String] let dataDir = args?["dataDir"] + let nodeUri = args?["nodeUri"] CwMwebPlugin.dataDir = dataDir + CwMwebPlugin.nodeUri = nodeUri startServer(result: result) break case "stop": @@ -48,7 +51,7 @@ public static func register(with registrar: FlutterPluginRegistrar) { private func startServer(result: @escaping FlutterResult) { if CwMwebPlugin.server == nil { var error: NSError? - CwMwebPlugin.server = MwebdNewServer("", CwMwebPlugin.dataDir, "", &error) + CwMwebPlugin.server = MwebdNewServer("", CwMwebPlugin.dataDir, CwMwebPlugin.nodeUri, &error) if let server = CwMwebPlugin.server { do { diff --git a/cw_mweb/lib/cw_mweb.dart b/cw_mweb/lib/cw_mweb.dart index 63ff1bf97..3be8e9bf5 100644 --- a/cw_mweb/lib/cw_mweb.dart +++ b/cw_mweb/lib/cw_mweb.dart @@ -17,7 +17,8 @@ class CwMweb { await Future.delayed(const Duration(seconds: 5)); final appDir = await getApplicationSupportDirectory(); - _port = await CwMwebPlatform.instance.start(appDir.path); + const ltcNodeUri = "45.79.13.180:9333"; + _port = await CwMwebPlatform.instance.start(appDir.path, ltcNodeUri); if (_port == null || _port == 0) { throw Exception("Failed to start server"); } diff --git a/cw_mweb/lib/cw_mweb_method_channel.dart b/cw_mweb/lib/cw_mweb_method_channel.dart index 70e4a1789..4956dcb8d 100644 --- a/cw_mweb/lib/cw_mweb_method_channel.dart +++ b/cw_mweb/lib/cw_mweb_method_channel.dart @@ -10,8 +10,9 @@ class MethodChannelCwMweb extends CwMwebPlatform { final methodChannel = const MethodChannel('cw_mweb'); @override - Future start(String dataDir) async { - final result = await methodChannel.invokeMethod('start', {'dataDir': dataDir}); + Future start(String dataDir, String nodeUri) async { + final result = + await methodChannel.invokeMethod('start', {'dataDir': dataDir, 'nodeUri': nodeUri}); return result; } diff --git a/cw_mweb/lib/cw_mweb_platform_interface.dart b/cw_mweb/lib/cw_mweb_platform_interface.dart index 8cc80f3e9..d5c03a56c 100644 --- a/cw_mweb/lib/cw_mweb_platform_interface.dart +++ b/cw_mweb/lib/cw_mweb_platform_interface.dart @@ -25,7 +25,7 @@ abstract class CwMwebPlatform extends PlatformInterface { _instance = instance; } - Future start(String dataDir) { + Future start(String dataDir, String nodeUri) { throw UnimplementedError('start() has not been implemented.'); } diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 50c22d113..2bed8a451 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -654,4 +654,15 @@ class CWBitcoin extends Bitcoin { // TODO: this could be improved: return inputAddressesContainMweb || outputAddressesContainMweb; } + + String? getUnusedMwebAddress(Object wallet) { + try { + final electrumWallet = wallet as ElectrumWallet; + final walletAddresses = electrumWallet.walletAddresses as ElectrumWalletAddresses; + final mwebAddress = walletAddresses.mwebAddresses.firstWhere((element) => !element.isUsed); + return mwebAddress.address; + } catch (_) { + return null; + } + } } diff --git a/lib/core/sync_status_title.dart b/lib/core/sync_status_title.dart index 4582f7b1f..46dd62c3a 100644 --- a/lib/core/sync_status_title.dart +++ b/lib/core/sync_status_title.dart @@ -16,6 +16,13 @@ String syncStatusTitle(SyncStatus syncStatus) { return S.current.sync_status_syncronized; } + if (syncStatus is FailedSyncStatus) { + if (syncStatus.error != null) { + return syncStatus.error!; + } + return S.current.sync_status_failed_connect; + } + if (syncStatus is NotConnectedSyncStatus) { return S.current.sync_status_not_connected; } @@ -24,10 +31,6 @@ String syncStatusTitle(SyncStatus syncStatus) { return S.current.sync_status_attempting_sync; } - if (syncStatus is FailedSyncStatus) { - return S.current.sync_status_failed_connect; - } - if (syncStatus is ConnectingSyncStatus) { return S.current.sync_status_connecting; } diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index a7b2a99fa..08e045a0d 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/nft_listing_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/action_button.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/home_screen_account_widget.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; @@ -19,14 +20,14 @@ 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/utils/feature_flag.dart'; +import 'package:cake_wallet/utils/payment_request.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/dashboard/nft_view_model.dart'; +import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:mobx/mobx.dart'; import 'package:url_launcher/url_launcher.dart'; class BalancePage extends StatelessWidget { @@ -238,8 +239,8 @@ class CryptoBalanceWidget extends StatelessWidget { return Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: DashBoardRoundedCardWidget( - title: S.current.rep_warning, - subTitle: S.current.rep_warning_sub, + title: S.of(context).rep_warning, + subTitle: S.of(context).rep_warning_sub, onTap: () => Navigator.of(context).pushNamed(Routes.changeRep), onClose: () { dashboardViewModel.settingsStore.shouldShowRepWarning = false; @@ -259,6 +260,7 @@ class CryptoBalanceWidget extends StatelessWidget { dashboardViewModel.balanceViewModel.formattedBalances.elementAt(index); return Observer(builder: (_) { return BalanceRowWidget( + dashboardViewModel: dashboardViewModel, availableBalanceLabel: '${dashboardViewModel.balanceViewModel.availableBalanceLabel}', availableBalance: balance.availableBalance, @@ -379,57 +381,68 @@ class CryptoBalanceWidget extends StatelessWidget { padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: DashBoardRoundedCardWidget( customBorder: 30, - title: S.current.litecoin_mweb, - subTitle: S.current.litecoin_enable_mweb_sync, + title: S.of(context).litecoin_mweb, + subTitle: '', hint: Column( children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => launchUrl( - Uri.parse( - "https://guides.cakewallet.com/docs/cryptos/litecoin/#mweb"), - mode: LaunchMode.externalApplication, + Text( + S.of(context).litecoin_mweb_description, + style: TextStyle( + color: Colors.white, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 8), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => launchUrl( + Uri.parse( + "https://guides.cakewallet.com/docs/cryptos/litecoin/#mweb"), + mode: LaunchMode.externalApplication, + ), + child: Center( + child: Text( + S.of(context).learn_more, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .labelTextColor, + height: 1, ), - child: Row( - children: [ - Text( - S.current.litecoin_what_is_mweb, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .labelTextColor, - height: 1, - ), - softWrap: true, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Icon(Icons.help_outline, - size: 16, - color: Theme.of(context) - .extension()! - .labelTextColor), - ) - ], + softWrap: true, + ), + ), + ), + SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: () => _dismissMweb(context), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + ), + child: Text( + S.of(context).litecoin_mweb_dismiss, + style: TextStyle(color: Colors.white), ), ), - Observer( - builder: (_) => StandardSwitch( - value: dashboardViewModel.mwebScanningActive, - onTaped: () => _toggleMweb(context), - ), - ) + ElevatedButton( + onPressed: () => _enableMweb(context), + child: Text(S.of(context).litecoin_enable_mweb_sync), + ), ], ), ], ), - onTap: () => _toggleMweb(context), + onTap: () => {}, icon: ImageIcon( AssetImage('assets/images/mweb_logo.png'), color: @@ -479,20 +492,34 @@ class CryptoBalanceWidget extends StatelessWidget { return dashboardViewModel.setSilentPaymentsScanning(newValue); } - Future _toggleMweb(BuildContext context) async { + Future _enableMweb(BuildContext context) async { if (!dashboardViewModel.hasEnabledMwebBefore) { await showPopUp( context: context, builder: (BuildContext context) => AlertWithOneAction( - alertTitle: S.of(context).warning, - alertContent: S.current.litecoin_mweb_warning, - buttonText: S.of(context).ok, + alertTitle: S.of(context).alert_notice, + alertContent: S.of(context).litecoin_mweb_warning, + buttonText: S.of(context).understand, buttonAction: () { Navigator.of(context).pop(); }, )); } - dashboardViewModel.setMwebScanningActive(!dashboardViewModel.mwebScanningActive); + dashboardViewModel.setMwebScanningActive(); + } + + Future _dismissMweb(BuildContext context) async { + await showPopUp( + context: context, + builder: (BuildContext context) => AlertWithOneAction( + alertTitle: S.of(context).alert_notice, + alertContent: S.of(context).litecoin_mweb_enable_later, + buttonText: S.of(context).understand, + buttonAction: () { + Navigator.of(context).pop(); + }, + )); + dashboardViewModel.dismissMweb(); } } @@ -517,6 +544,7 @@ class BalanceRowWidget extends StatelessWidget { required this.hasSecondAvailableBalance, required this.hasSecondAdditionalBalance, required this.isTestnet, + required this.dashboardViewModel, super.key, }); @@ -539,187 +567,238 @@ class BalanceRowWidget extends StatelessWidget { final bool hasSecondAvailableBalance; final bool hasSecondAdditionalBalance; final bool isTestnet; + final DashboardViewModel dashboardViewModel; // void _showBalanceDescription(BuildContext context) { // showPopUp( // context: context, // builder: (_) => - // InformationPage(information: S.current.available_balance_description), + // InformationPage(information: S.of(context).available_balance_description), // ); // } @override Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.only(left: 16, right: 16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30.0), - border: Border.all( - color: Theme.of(context).extension()!.cardBorderColor, - width: 1, + return Column(children: [ + Container( + margin: const EdgeInsets.only(left: 16, right: 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, ), - color: Theme.of(context).extension()!.syncedBackgroundColor, - ), - child: Container( - margin: const EdgeInsets.only(top: 16, left: 24, right: 8, bottom: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ + child: Container( + margin: const EdgeInsets.only(top: 16, left: 24, right: 8, bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: hasAdditionalBalance + ? () => _showBalanceDescription( + context, S.of(context).available_balance_description) + : null, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Semantics( + hint: 'Double tap to see more information', + container: true, + child: Text('${availableBalanceLabel}', + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .labelTextColor, + height: 1)), + ), + if (hasAdditionalBalance) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Icon(Icons.help_outline, + size: 16, + color: Theme.of(context) + .extension()! + .labelTextColor), + ), + ], + ), + SizedBox(height: 6), + AutoSizeText(availableBalance, + style: TextStyle( + fontSize: 24, + fontFamily: 'Lato', + fontWeight: FontWeight.w900, + color: Theme.of(context) + .extension()! + .balanceAmountColor, + height: 1), + maxLines: 1, + textAlign: TextAlign.start), + SizedBox(height: 6), + if (isTestnet) + Text(S.of(context).testnet_coins_no_value, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.textColor, + height: 1)), + if (!isTestnet) + Text('${availableFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontFamily: 'Lato', + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.textColor, + height: 1)), + ], + ), + ), + SizedBox( + width: min(MediaQuery.of(context).size.width * 0.2, 100), + child: Center( + child: Column( + children: [ + CakeImageWidget( + imageUrl: currency.iconPath, + height: 40, + width: 40, + displayOnError: Container( + height: 30.0, + width: 30.0, + child: Center( + child: Text( + currency.title.substring(0, min(currency.title.length, 2)), + style: TextStyle(fontSize: 11), + ), + ), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey.shade400, + ), + ), + ), + const SizedBox(height: 10), + Text( + currency.title, + style: TextStyle( + fontSize: 15, + fontFamily: 'Lato', + fontWeight: FontWeight.w800, + color: + Theme.of(context).extension()!.assetTitleColor, + height: 1, + ), + ), + ], + ), + ), + ), + ], + ), + if (frozenBalance.isNotEmpty) GestureDetector( behavior: HitTestBehavior.opaque, onTap: hasAdditionalBalance - ? () => - _showBalanceDescription(context, S.current.available_balance_description) + ? () => _showBalanceDescription( + context, S.of(context).unavailable_balance_description) : null, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox(height: 26), Row( children: [ - Semantics( - hint: 'Double tap to see more information', - container: true, - child: Text('${availableBalanceLabel}', - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .labelTextColor, - height: 1)), - ), - if (hasAdditionalBalance) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Icon(Icons.help_outline, - size: 16, - color: Theme.of(context) - .extension()! - .labelTextColor), + Text( + S.of(context).unavailable_balance, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: + Theme.of(context).extension()!.labelTextColor, + height: 1, ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Icon(Icons.help_outline, + size: 16, + color: Theme.of(context) + .extension()! + .labelTextColor), + ), ], ), - SizedBox(height: 6), - AutoSizeText(availableBalance, - style: TextStyle( - fontSize: 24, - fontFamily: 'Lato', - fontWeight: FontWeight.w900, - color: Theme.of(context) - .extension()! - .balanceAmountColor, - height: 1), - maxLines: 1, - textAlign: TextAlign.start), - SizedBox(height: 6), - if (isTestnet) - Text(S.current.testnet_coins_no_value, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.textColor, - height: 1)), + SizedBox(height: 8), + AutoSizeText( + frozenBalance, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: + Theme.of(context).extension()!.balanceAmountColor, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, + ), + SizedBox(height: 4), if (!isTestnet) - Text('${availableFiatBalance}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - fontFamily: 'Lato', - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.textColor, - height: 1)), - ], - ), - ), - SizedBox( - width: min(MediaQuery.of(context).size.width * 0.2, 100), - child: Center( - child: Column( - children: [ - CakeImageWidget( - imageUrl: currency.iconPath, - height: 40, - width: 40, - displayOnError: Container( - height: 30.0, - width: 30.0, - child: Center( - child: Text( - currency.title.substring(0, min(currency.title.length, 2)), - style: TextStyle(fontSize: 11), - ), - ), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.grey.shade400, - ), - ), - ), - const SizedBox(height: 10), Text( - currency.title, - style: TextStyle( - fontSize: 15, - fontFamily: 'Lato', - fontWeight: FontWeight.w800, - color: Theme.of(context).extension()!.assetTitleColor, - height: 1, - ), - ), - ], - ), - ), - ), - ], - ), - if (frozenBalance.isNotEmpty) - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: hasAdditionalBalance - ? () => - _showBalanceDescription(context, S.current.unavailable_balance_description) - : null, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 26), - Row( - children: [ - Text( - S.current.unavailable_balance, + frozenFiatBalance, textAlign: TextAlign.center, style: TextStyle( fontSize: 12, fontFamily: 'Lato', fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.labelTextColor, + color: Theme.of(context).extension()!.textColor, height: 1, ), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Icon(Icons.help_outline, - size: 16, - color: - Theme.of(context).extension()!.labelTextColor), - ), - ], + ], + ), + ), + if (hasAdditionalBalance) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24), + Text( + '${additionalBalanceLabel}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.labelTextColor, + height: 1, + ), ), SizedBox(height: 8), AutoSizeText( - frozenBalance, + additionalBalance, style: TextStyle( fontSize: 20, fontFamily: 'Lato', fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.balanceAmountColor, + color: Theme.of(context).extension()!.assetTitleColor, height: 1, ), maxLines: 1, @@ -728,7 +807,7 @@ class BalanceRowWidget extends StatelessWidget { SizedBox(height: 4), if (!isTestnet) Text( - frozenFiatBalance, + '${additionalFiatBalance}', textAlign: TextAlign.center, style: TextStyle( fontSize: 12, @@ -740,143 +819,247 @@ class BalanceRowWidget extends StatelessWidget { ), ], ), - ), - if (hasAdditionalBalance) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 24), - Text( - '${additionalBalanceLabel}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.labelTextColor, - height: 1, - ), - ), - SizedBox(height: 8), - AutoSizeText( - additionalBalance, - style: TextStyle( - fontSize: 20, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.assetTitleColor, - height: 1, - ), - maxLines: 1, - textAlign: TextAlign.center, - ), - SizedBox(height: 4), - if (!isTestnet) - Text( - '${additionalFiatBalance}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.textColor, - height: 1, - ), - ), - ], - ), - if (hasSecondAvailableBalance) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 24), - Text( - '${secondAvailableBalanceLabel}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.labelTextColor, - height: 1, - ), - ), - SizedBox(height: 8), - AutoSizeText( - secondAvailableBalance, - style: TextStyle( - fontSize: 20, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.assetTitleColor, - height: 1, - ), - maxLines: 1, - textAlign: TextAlign.center, - ), - SizedBox(height: 4), - if (!isTestnet) - Text( - '${secondAvailableFiatBalance}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.textColor, - height: 1, - ), - ), - ], - ), - if (hasSecondAdditionalBalance) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 24), - Text( - '${secondAdditionalBalanceLabel}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.labelTextColor, - height: 1, - ), - ), - SizedBox(height: 8), - AutoSizeText( - secondAdditionalBalance, - style: TextStyle( - fontSize: 20, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.assetTitleColor, - height: 1, - ), - maxLines: 1, - textAlign: TextAlign.center, - ), - SizedBox(height: 4), - if (!isTestnet) - Text( - '${secondAdditionalFiatBalance}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.textColor, - height: 1, - ), - ), - ], - ), - ], + ], + ), ), ), - ); + if (hasSecondAdditionalBalance || hasSecondAvailableBalance) ...[ + SizedBox(height: 16), + Container( + margin: const EdgeInsets.only(left: 16, right: 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: Container( + margin: const EdgeInsets.only(top: 0, left: 24, right: 8, bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + if (currency == CryptoCurrency.ltc) + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + padding: EdgeInsets.only(right: 16, top: 16), + child: Column( + children: [ + CakeImageWidget( + imageUrl: 'assets/images/mweb_logo.png', + height: 40, + width: 40, + displayOnError: Container( + height: 30.0, + width: 30.0, + child: Center( + child: Text( + currency.title.substring(0, min(currency.title.length, 2)), + style: TextStyle(fontSize: 11), + ), + ), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey.shade400, + ), + ), + ), + const SizedBox(height: 10), + Text( + 'MWEB', + style: TextStyle( + fontSize: 15, + fontFamily: 'Lato', + fontWeight: FontWeight.w800, + color: Theme.of(context) + .extension()! + .assetTitleColor, + height: 1, + ), + ), + ], + ), + ), + ], + ), + if (hasSecondAvailableBalance) + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24), + Text( + '${secondAvailableBalanceLabel}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .labelTextColor, + height: 1, + ), + ), + SizedBox(height: 8), + AutoSizeText( + secondAvailableBalance, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .assetTitleColor, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, + ), + SizedBox(height: 4), + if (!isTestnet) + Text( + '${secondAvailableFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: + Theme.of(context).extension()!.textColor, + height: 1, + ), + ), + ], + ), + ], + ), + ], + ), + Stack( + children: [ + if (hasSecondAdditionalBalance) + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24), + Text( + '${secondAdditionalBalanceLabel}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .labelTextColor, + height: 1, + ), + ), + SizedBox(height: 8), + AutoSizeText( + secondAdditionalBalance, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .assetTitleColor, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, + ), + SizedBox(height: 4), + if (!isTestnet) + Text( + '${secondAdditionalFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: + Theme.of(context).extension()!.textColor, + height: 1, + ), + ), + ], + ), + ], + ), + // TODO: smarter peg in / out buttons + // if (currency == CryptoCurrency.ltc) + // Row( + // mainAxisAlignment: MainAxisAlignment.end, + // children: [ + // Container( + // margin: EdgeInsets.only(top: 24, right: 8), + // child: ElevatedButton( + // style: ElevatedButton.styleFrom( + // backgroundColor: Theme.of(context).highlightColor, + // ), + // onPressed: () { + // final mwebAddress = + // bitcoin!.getUnusedMwebAddress(dashboardViewModel.wallet); + // if (mwebAddress == null) return; + // final paymentRequest = + // PaymentRequest.fromUri(Uri.parse("litecoin:${mwebAddress}")); + // Navigator.of(context) + // .pushNamed(Routes.send, arguments: paymentRequest); + // }, + // child: Container( + // color: Colors.transparent, + // margin: EdgeInsets.all(4), + // child: Column( + // mainAxisSize: MainAxisSize.max, + // crossAxisAlignment: CrossAxisAlignment.center, + // children: [ + // Container( + // alignment: Alignment.center, + // decoration: BoxDecoration(shape: BoxShape.circle), + // child: Image.asset( + // 'assets/images/received.png', + // color: Theme.of(context) + // .extension()! + // .balanceAmountColor, + // width: 64, + // height: 32, + // ), + // ), + // SizedBox(height: 4), + // Text( + // S.of(context).litecoin_mweb_pegin, + // style: TextStyle( + // fontSize: 10, + // color: Theme.of(context) + // .extension()! + // .cardTextColor), + // ) + // ], + // ), + // ), + // ), + // ), + // ], + // ), + ], + ), + ], + ), + ), + ), + ], + ]); } void _showBalanceDescription(BuildContext context, String content) { diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index 30c7b7f78..a59bfd2fa 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -192,7 +192,7 @@ class MenuWidgetState extends State { final item = items[index]; if (!widget.dashboardViewModel.hasMweb && - item.name(context) == S.current.litecoin_mweb_settings) { + item.name(context) == S.of(context).litecoin_mweb_settings) { return const SizedBox(); } diff --git a/lib/src/widgets/setting_actions.dart b/lib/src/widgets/setting_actions.dart index a8a9558d5..80d07962f 100644 --- a/lib/src/widgets/setting_actions.dart +++ b/lib/src/widgets/setting_actions.dart @@ -49,7 +49,7 @@ class SettingActions { ); static SettingActions litecoinMwebSettingAction = SettingActions._( - name: (context) => S.current.litecoin_mweb_settings, + name: (context) => S.of(context).litecoin_mweb_settings, image: 'assets/images/bitcoin_menu.png', onTap: (BuildContext context) { Navigator.pop(context); diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index c3fb5718a..083c31302 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -358,14 +358,15 @@ abstract class BalanceViewModelBase with Store { } bool _hasSecondAdditionalBalanceForWalletType(WalletType type) { - if (wallet.type == WalletType.litecoin && settingsStore.mwebEnabled) { + if (wallet.type == WalletType.litecoin && settingsStore.mwebAlwaysScan) { + // if ((wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) > 0) return true; } return false; } bool _hasSecondAvailableBalanceForWalletType(WalletType type) { - if (wallet.type == WalletType.litecoin && settingsStore.mwebEnabled) { + if (wallet.type == WalletType.litecoin && settingsStore.mwebAlwaysScan) { return true; } return false; diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 21a167e2a..77c60bd63 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -262,6 +262,8 @@ abstract class DashboardViewModelBase with Store { reaction((_) => settingsStore.mwebAlwaysScan, (bool alwaysScan) { if (alwaysScan) { mwebScanningActive = true; + } else { + mwebScanningActive = false; } }); } @@ -431,7 +433,7 @@ abstract class DashboardViewModelBase with Store { bool get hasMweb => wallet.type == WalletType.litecoin; @computed - bool get showMwebCard => hasMweb && settingsStore.mwebCardDisplay; + bool get showMwebCard => hasMweb && settingsStore.mwebCardDisplay && !mwebScanningActive; @observable bool mwebScanningActive = false; @@ -440,18 +442,23 @@ abstract class DashboardViewModelBase with Store { bool get hasEnabledMwebBefore => settingsStore.hasEnabledMwebBefore; @action - void setMwebScanningActive(bool active) { + void setMwebScanningActive() { if (!hasMweb) { return; } - if (active) { - settingsStore.hasEnabledMwebBefore = true; - } + settingsStore.hasEnabledMwebBefore = true; + mwebScanningActive = true; + bitcoin!.setMwebEnabled(wallet, true); + settingsStore.mwebAlwaysScan = true; + } - settingsStore.mwebEnabled = active; - mwebScanningActive = active; - bitcoin!.setMwebEnabled(wallet, active); + @action + void dismissMweb() { + settingsStore.mwebCardDisplay = false; + settingsStore.mwebAlwaysScan = false; + mwebScanningActive = false; + bitcoin!.setMwebEnabled(wallet, false); } BalanceViewModel balanceViewModel; 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 9fb7509eb..0260dc6b4 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 @@ -326,13 +326,13 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo final isPrimary = subaddress == primaryAddress; return WalletAddressListItem( - id: subaddress.id, - isPrimary: isPrimary, - name: subaddress.label, - address: subaddress.address, - balance: subaddress.received, - txCount: subaddress.txCount, - ); + id: subaddress.id, + isPrimary: isPrimary, + name: subaddress.label, + address: subaddress.address, + balance: subaddress.received, + txCount: subaddress.txCount, + ); }); addressList.addAll(addressItems); } @@ -418,8 +418,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo if (wallet.type == WalletType.litecoin && addressItems.length >= 1000) { // find the index of the last item with a txCount > 0 final addressItemsList = addressItems.toList(); - final lastItemWithTxCount = addressItemsList.lastWhere((item) => (item.txCount ?? 0) > 0); - final index = addressItemsList.indexOf(lastItemWithTxCount); + int index = addressItemsList.lastIndexWhere((item) => (item.txCount ?? 0) > 0); + if (index == -1) { + index = 0; + } // show only up to that index + 20: addressItems = addressItemsList.sublist(0, index + 20); } @@ -461,14 +463,16 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo for (var i = 0; i < addressList.length; i++) { if (!(addressList[i] is WalletAddressListItem)) continue; - (addressList[i] as WalletAddressListItem).isHidden = wallet.walletAddresses.hiddenAddresses.contains((addressList[i] as WalletAddressListItem).address); + (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.contains((addressList[i] as WalletAddressListItem).address); + (addressList[i] as WalletAddressListItem).isManual = wallet.walletAddresses.manualAddresses + .contains((addressList[i] as WalletAddressListItem).address); } - + if (searchText.isNotEmpty) { return ObservableList.of(addressList.where((item) { if (item is WalletAddressListItem) { @@ -480,6 +484,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo return addressList; } + Future toggleHideAddress(WalletAddressListItem item) async { if (item.isHidden) { wallet.walletAddresses.hiddenAddresses.removeWhere((element) => element == item.address); @@ -488,13 +493,20 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo } await wallet.walletAddresses.saveAddressesInBox(); if (wallet.type == WalletType.monero) { - monero!.getSubaddressList(wallet).update(wallet, accountIndex: monero!.getCurrentAccount(wallet).id); + monero! + .getSubaddressList(wallet) + .update(wallet, accountIndex: monero!.getCurrentAccount(wallet).id); } else if (wallet.type == WalletType.wownero) { - wownero!.getSubaddressList(wallet).update(wallet, accountIndex: wownero!.getCurrentAccount(wallet).id); + 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); + haven! + .getSubaddressList(wallet) + .update(wallet, accountIndex: haven!.getCurrentAccount(wallet).id); } } + @observable bool hasAccounts; @@ -535,8 +547,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @computed bool get isReceivedAvailable => - wallet.type == WalletType.monero || - wallet.type == WalletType.wownero; + wallet.type == WalletType.monero || wallet.type == WalletType.wownero; @computed bool get isSilentPayments => @@ -549,9 +560,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @computed bool get showAddManualAddresses => - !isAutoGenerateSubaddressEnabled || - wallet.type == WalletType.monero || - wallet.type == WalletType.wownero; + !isAutoGenerateSubaddressEnabled || + wallet.type == WalletType.monero || + wallet.type == WalletType.wownero; List _baseItems; diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index e81696b2a..d55faad76 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -36,6 +36,7 @@ "agree": "موافق", "agree_and_continue": "الموافقة ومتابعة", "agree_to": "من خلال إنشاء حساب فإنك توافق على", + "alert_notice": "يلاحظ", "all": "الكل", "all_trades": "جميع عمليات التداول", "all_transactions": "كل التحركات المالية", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "تمكين MWEB المسح الضوئي", "litecoin_mweb": "mweb", "litecoin_mweb_always_scan": "اضبط MWEB دائمًا على المسح الضوئي", + "litecoin_mweb_description": "MWEB هو بروتوكول جديد يجلب معاملات أسرع وأرخص وأكثر خصوصية إلى Litecoin", + "litecoin_mweb_dismiss": "رفض", "litecoin_mweb_display_card": "عرض بطاقة mweb", + "litecoin_mweb_enable_later": "يمكنك اختيار تمكين MWEB مرة أخرى ضمن إعدادات العرض.", + "litecoin_mweb_pegin": "ربط في", + "litecoin_mweb_pegout": "ربط", "litecoin_mweb_scanning": "MWEB المسح الضوئي", "litecoin_mweb_settings": "إعدادات MWEB", "litecoin_mweb_warning": "سيقوم استخدام MWEB في البداية بتنزيل ~ 600 ميجابايت من البيانات ، وقد يستغرق ما يصل إلى 30 دقيقة حسب سرعة الشبكة. سيتم تنزيل هذه البيانات الأولية مرة واحدة فقط وستكون متاحة لجميع محافظ Litecoin", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 1ddcf99f9..dd5c9a260 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -36,6 +36,7 @@ "agree": "Съгласен/а съм", "agree_and_continue": "Съгласяване и продължаване", "agree_to": "Чрез създаването на акаунт вие се съгласявате с ", + "alert_notice": "Забележете", "all": "ALL", "all_trades": "Всички сделкки", "all_transactions": "Всички транзакции", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Активирайте сканирането на MWeb", "litecoin_mweb": "Mweb", "litecoin_mweb_always_scan": "Задайте MWeb винаги сканиране", + "litecoin_mweb_description": "MWeb е нов протокол, който носи по -бърз, по -евтин и повече частни транзакции на Litecoin", + "litecoin_mweb_dismiss": "Уволнение", "litecoin_mweb_display_card": "Показване на MWEB карта", + "litecoin_mweb_enable_later": "Можете да изберете да активирате MWEB отново под настройките на дисплея.", + "litecoin_mweb_pegin": "PEG в", + "litecoin_mweb_pegout": "PEG OUT", "litecoin_mweb_scanning": "Сканиране на MWEB", "litecoin_mweb_settings": "Настройки на MWEB", "litecoin_mweb_warning": "Използването на MWEB първоначално ще изтегли ~ 600MB данни и може да отнеме до 30 минути в зависимост от скоростта на мрежата. Тези първоначални данни ще изтеглят само веднъж и ще бъдат достъпни за всички портфейли Litecoin", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index f295692b1..65ef50a71 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -36,6 +36,7 @@ "agree": "Souhlasím", "agree_and_continue": "Souhlasím & pokračovat", "agree_to": "Vytvořením účtu souhlasíte s ", + "alert_notice": "Oznámení", "all": "VŠE", "all_trades": "Všechny obchody", "all_transactions": "Všechny transakce", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Povolit skenování MWeb", "litecoin_mweb": "MWeb", "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", "litecoin_mweb_display_card": "Zobrazit kartu MWeb", + "litecoin_mweb_enable_later": "V nastavení zobrazení můžete vybrat znovu povolit MWeb.", + "litecoin_mweb_pegin": "Peg in", + "litecoin_mweb_pegout": "Zkrachovat", "litecoin_mweb_scanning": "Skenování mWeb", "litecoin_mweb_settings": "Nastavení mWeb", "litecoin_mweb_warning": "Pomocí MWeb zpočátku stahuje ~ 600 MB dat a může trvat až 30 minut v závislosti na rychlosti sítě. Tato počáteční data si stáhnou pouze jednou a budou k dispozici pro všechny litecoinové peněženky", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index acb5aa919..194b1d05b 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -36,6 +36,7 @@ "agree": "stimme zu", "agree_and_continue": "Zustimmen & fortfahren", "agree_to": "Indem Sie ein Konto erstellen, stimmen Sie den ", + "alert_notice": "Beachten", "all": "ALLES", "all_trades": "Alle Trades", "all_transactions": "Alle Transaktionen", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Aktivieren Sie das MWEB -Scannen", "litecoin_mweb": "MWeb", "litecoin_mweb_always_scan": "Setzen Sie MWeb immer scannen", + "litecoin_mweb_description": "MWEB ist ein neues Protokoll, das schnellere, billigere und privatere Transaktionen zu Litecoin bringt", + "litecoin_mweb_dismiss": "Zurückweisen", "litecoin_mweb_display_card": "MWEB -Karte anzeigen", + "litecoin_mweb_enable_later": "Sie können MWEB unter Anzeigeeinstellungen erneut aktivieren.", + "litecoin_mweb_pegin": "Peg in", + "litecoin_mweb_pegout": "Abstecken", "litecoin_mweb_scanning": "MWEB Scanning", "litecoin_mweb_settings": "MWEB -Einstellungen", "litecoin_mweb_warning": "Durch die Verwendung von MWEB wird zunächst ~ 600 MB Daten heruntergeladen und kann je nach Netzwerkgeschwindigkeit bis zu 30 Minuten dauern. Diese ersten Daten werden nur einmal heruntergeladen und für alle Litecoin -Brieftaschen verfügbar", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 19f939d19..c79719641 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -36,6 +36,7 @@ "agree": "Agree", "agree_and_continue": "Agree & Continue", "agree_to": "By creating account you agree to the ", + "alert_notice": "Notice", "all": "ALL", "all_trades": "All trades", "all_transactions": "All transactions", @@ -333,9 +334,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "help", - "hide": "Hide", "hidden_addresses": "Hidden Addresses", "hidden_balance": "Hidden Balance", + "hide": "Hide", "hide_details": "Hide Details", "high_contrast_theme": "High Contrast Theme", "home_screen_settings": "Home screen settings", @@ -368,7 +369,12 @@ "litecoin_enable_mweb_sync": "Enable MWEB scanning", "litecoin_mweb": "MWEB", "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", "litecoin_mweb_display_card": "Show MWEB card", + "litecoin_mweb_enable_later": "You can choose to enable MWEB again under Display Settings.", + "litecoin_mweb_pegin": "Peg In", + "litecoin_mweb_pegout": "Peg Out", "litecoin_mweb_scanning": "MWEB Scanning", "litecoin_mweb_settings": "MWEB settings", "litecoin_mweb_warning": "Using MWEB will initially download ~600MB of data, and may take up to 30 minutes depending on network speed. This initial data will only download once and be available for all Litecoin wallets", @@ -685,8 +691,8 @@ "setup_your_debit_card": "Set up your debit card", "share": "Share", "share_address": "Share address", - "show": "Show", "shared_seed_wallet_groups": "Shared Seed Wallet Groups", + "show": "Show", "show_details": "Show Details", "show_keys": "Show seed/keys", "show_market_place": "Show Marketplace", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index d9b2db88a..ced8d9a13 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -36,6 +36,7 @@ "agree": "De acuerdo", "agree_and_continue": "Aceptar y continuar", "agree_to": "Al crear una cuenta, aceptas ", + "alert_notice": "Aviso", "all": "TODOS", "all_trades": "Todos los oficios", "all_transactions": "Todas las transacciones", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Habilitar el escaneo mweb", "litecoin_mweb": "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", "litecoin_mweb_display_card": "Mostrar tarjeta MWEB", + "litecoin_mweb_enable_later": "Puede elegir habilitar MWEB nuevamente en la configuración de visualización.", + "litecoin_mweb_pegin": "Meter", + "litecoin_mweb_pegout": "Estirar la pata", "litecoin_mweb_scanning": "Escaneo mweb", "litecoin_mweb_settings": "Configuración de MWEB", "litecoin_mweb_warning": "El uso de MWEB inicialmente descargará ~ 600 MB de datos, y puede tomar hasta 30 minutos según la velocidad de la red. Estos datos iniciales solo se descargarán una vez y estarán disponibles para todas las billeteras de Litecoin", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 907058095..1f8567b31 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -36,6 +36,7 @@ "agree": "d'accord", "agree_and_continue": "Accepter et continuer", "agree_to": "En créant un compte, vous acceptez les ", + "alert_notice": "Avis", "all": "TOUT", "all_trades": "Tous échanges", "all_transactions": "Toutes transactions", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Activer la numérisation MWEB", "litecoin_mweb": "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", "litecoin_mweb_display_card": "Afficher la carte MWeb", + "litecoin_mweb_enable_later": "Vous pouvez choisir d'activer à nouveau MWEB sous Paramètres d'affichage.", + "litecoin_mweb_pegin": "Entraver", + "litecoin_mweb_pegout": "Crever", "litecoin_mweb_scanning": "Scann mweb", "litecoin_mweb_settings": "Paramètres MWEB", "litecoin_mweb_warning": "L'utilisation de MWEB téléchargera initialement ~ 600 Mo de données et peut prendre jusqu'à 30 minutes en fonction de la vitesse du réseau. Ces données initiales ne téléchargeront qu'une seule fois et seront disponibles pour tous les portefeuilles litecoin", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 28b698c30..1e5b5d8e3 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -36,6 +36,7 @@ "agree": "Yarda", "agree_and_continue": "Amincewa & Ci gaba", "agree_to": "Ta hanyar ƙirƙirar asusu kun yarda da", + "alert_notice": "Sanarwa", "all": "DUK", "all_trades": "Duk ciniki", "all_transactions": "Dukan Ma'amaloli", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Kunna binciken Mweb", "litecoin_mweb": "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", "litecoin_mweb_display_card": "Nuna katin Mweb", + "litecoin_mweb_enable_later": "Kuna iya zaɓar kunna Mweb kuma a ƙarƙashin saitunan nuni.", + "litecoin_mweb_pegin": "Peg in", + "litecoin_mweb_pegout": "Peg fita", "litecoin_mweb_scanning": "Mweb scanning", "litecoin_mweb_settings": "Saitunan Mweb", "litecoin_mweb_warning": "Amfani da Mweb zai fara saukewa ~ 600MB na bayanai, kuma yana iya ɗaukar minti 30 dangane da saurin cibiyar sadarwa. Wannan bayanan farko zai saika saukarwa sau ɗaya kawai kuma a samu don duk wuraren shakatawa", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 8b46685a8..07d8311ef 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -36,6 +36,7 @@ "agree": "सहमत", "agree_and_continue": "सहमत और जारी रखें", "agree_to": "खाता बनाकर आप इससे सहमत होते हैं ", + "alert_notice": "सूचना", "all": "सब", "all_trades": "सभी व्यापार", "all_transactions": "सभी लेन - देन", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "MWEB स्कैनिंग सक्षम करें", "litecoin_mweb": "मावली", "litecoin_mweb_always_scan": "MWEB हमेशा स्कैनिंग सेट करें", + "litecoin_mweb_description": "MWEB एक नया प्रोटोकॉल है जो लिटकोइन के लिए तेजी से, सस्ता और अधिक निजी लेनदेन लाता है", + "litecoin_mweb_dismiss": "नकार देना", "litecoin_mweb_display_card": "MWEB कार्ड दिखाएं", + "litecoin_mweb_enable_later": "आप प्रदर्शन सेटिंग्स के तहत फिर से MWEB को सक्षम करने के लिए चुन सकते हैं।", + "litecoin_mweb_pegin": "खूंटी", + "litecoin_mweb_pegout": "मरना", "litecoin_mweb_scanning": "MWEB स्कैनिंग", "litecoin_mweb_settings": "MWEB सेटिंग्स", "litecoin_mweb_warning": "MWEB का उपयोग शुरू में ~ 600MB डेटा डाउनलोड करेगा, और नेटवर्क की गति के आधार पर 30 मिनट तक का समय लग सकता है। यह प्रारंभिक डेटा केवल एक बार डाउनलोड करेगा और सभी लिटकोइन वॉलेट के लिए उपलब्ध होगा", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index ab3f536c7..7969716de 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -36,6 +36,7 @@ "agree": "Slažem se", "agree_and_continue": "Slažem se i nastavi", "agree_to": "Stvaranjem računa pristajete na ", + "alert_notice": "Obavijest", "all": "SVE", "all_trades": "Svi obrti", "all_transactions": "Sve transakcije", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Omogućite MWEB skeniranje", "litecoin_mweb": "MWeb", "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", "litecoin_mweb_display_card": "Prikaži MWeb karticu", + "litecoin_mweb_enable_later": "Možete odabrati da MWEB ponovo omogućite pod postavkama zaslona.", + "litecoin_mweb_pegin": "Uvući se", + "litecoin_mweb_pegout": "Odapeti", "litecoin_mweb_scanning": "MWEB skeniranje", "litecoin_mweb_settings": "Postavke MWEB -a", "litecoin_mweb_warning": "Korištenje MWEB -a u početku će preuzeti ~ 600MB podataka, a može potrajati do 30 minuta, ovisno o brzini mreže. Ovi početni podaci preuzet će samo jednom i biti dostupni za sve Litecoin novčanike", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 3d37a0a85..aa58edd64 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -36,6 +36,7 @@ "agree": "Համաձայն եմ", "agree_and_continue": "Համաձայն եմ և շարունակեմ", "agree_to": "Ստեղծելով հաշիվ դուք համաձայնում եք ", + "alert_notice": "Ծանուցում", "all": "Բոլորը", "all_trades": "Բոլոր գործարքները", "all_transactions": "Բոլոր գործառնությունները", @@ -363,6 +364,11 @@ "ledger_error_wrong_app": "Խնդրում ենք համոզվել, որ դուք բացել եք ճիշտ ծրագիրը ձեր Ledger-ում", "ledger_please_enable_bluetooth": "Խնդրում ենք միացնել Bluetooth-ը ձեր Ledger-ը հայտնաբերելու համար", "light_theme": "Լուսավոր", + "litecoin_mweb_description": "Mweb- ը նոր արձանագրություն է, որը բերում է ավելի արագ, ավելի էժան եւ ավելի մասնավոր գործարքներ դեպի LITECOIN", + "litecoin_mweb_dismiss": "Հեռացնել", + "litecoin_mweb_enable_later": "Կարող եք ընտրել Mweb- ը կրկին միացնել ցուցադրման պարամետրերը:", + "litecoin_mweb_pegin": "Peg in", + "litecoin_mweb_pegout": "Հափշտակել", "live_fee_rates": "Ապակի վարձավճարներ API- ի միջոցով", "load_more": "Բեռնել ավելին", "loading_your_wallet": "Ձեր հաշվեհամարը բեռնում է", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 504c67d50..867040df5 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -36,6 +36,7 @@ "agree": "Setuju", "agree_and_continue": "Setuju & Lanjutkan", "agree_to": "Dengan membuat akun Anda setuju dengan ", + "alert_notice": "Melihat", "all": "SEMUA", "all_trades": "Semua perdagangan", "all_transactions": "Semua transaksi", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Aktifkan pemindaian MWEB", "litecoin_mweb": "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", "litecoin_mweb_display_card": "Tunjukkan kartu mWeb", + "litecoin_mweb_enable_later": "Anda dapat memilih untuk mengaktifkan MWEB lagi di bawah pengaturan tampilan.", + "litecoin_mweb_pegin": "Pasak masuk", + "litecoin_mweb_pegout": "Mati", "litecoin_mweb_scanning": "Pemindaian MWEB", "litecoin_mweb_settings": "Pengaturan MWEB", "litecoin_mweb_warning": "Menggunakan MWEB pada awalnya akan mengunduh ~ 600MB data, dan dapat memakan waktu hingga 30 menit tergantung pada kecepatan jaringan. Data awal ini hanya akan mengunduh sekali dan tersedia untuk semua dompet litecoin", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 668346dfa..3b20d0410 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -36,6 +36,7 @@ "agree": "d'accordo", "agree_and_continue": "Accetta e continua", "agree_to": "Creando un account accetti il ​​", + "alert_notice": "Avviso", "all": "TUTTO", "all_trades": "Svi obrti", "all_transactions": "Sve transakcije", @@ -367,7 +368,12 @@ "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_display_card": "Mostra la scheda MWeb", + "litecoin_mweb_enable_later": "È possibile scegliere di abilitare nuovamente MWeb nelle impostazioni di visualizzazione.", + "litecoin_mweb_pegin": "Piolo 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", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 301b0f6cc..0449a8749 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -36,6 +36,7 @@ "agree": "同意する", "agree_and_continue": "同意して続行", "agree_to": "アカウントを作成することにより、", + "alert_notice": "知らせ", "all": "すべて", "all_trades": "すべての取引", "all_transactions": "全取引", @@ -367,7 +368,12 @@ "litecoin_enable_mweb_sync": "MWEBスキャンを有効にします", "litecoin_mweb": "mweb", "litecoin_mweb_always_scan": "MWEBを常にスキャンします", + "litecoin_mweb_description": "MWEBは、Litecoinにより速く、より安価で、よりプライベートなトランザクションをもたらす新しいプロトコルです", + "litecoin_mweb_dismiss": "却下する", "litecoin_mweb_display_card": "MWEBカードを表示します", + "litecoin_mweb_enable_later": "表示設定の下で、MWEBを再度有効にすることを選択できます。", + "litecoin_mweb_pegin": "ペグイン", + "litecoin_mweb_pegout": "ペグアウト", "litecoin_mweb_scanning": "MWEBスキャン", "litecoin_mweb_settings": "MWEB設定", "litecoin_mweb_warning": "MWEBを使用すると、最初は〜600MBのデータをダウンロードし、ネットワーク速度に応じて最大30分かかる場合があります。この最初のデータは一度だけダウンロードされ、すべてのLitecoinウォレットで利用可能になります", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 5ae26757c..ccda2dbbc 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -36,6 +36,7 @@ "agree": "동의하다", "agree_and_continue": "동의 및 계속", "agree_to": "계정을 생성하면 ", + "alert_notice": "알아채다", "all": "모든", "all_trades": "A모든 거래", "all_transactions": "모든 거래 창구", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "mweb 스캔을 활성화합니다", "litecoin_mweb": "mweb", "litecoin_mweb_always_scan": "mweb는 항상 스캔을 설정합니다", + "litecoin_mweb_description": "MWEB는 Litecoin에 더 빠르고 저렴하며 개인 거래를 제공하는 새로운 프로토콜입니다.", + "litecoin_mweb_dismiss": "해고하다", "litecoin_mweb_display_card": "mweb 카드를 보여주십시오", + "litecoin_mweb_enable_later": "디스플레이 설정에서 MWEB를 다시 활성화하도록 선택할 수 있습니다.", + "litecoin_mweb_pegin": "페그를 입력하십시오", + "litecoin_mweb_pegout": "죽다", "litecoin_mweb_scanning": "mweb 스캔", "litecoin_mweb_settings": "mweb 설정", "litecoin_mweb_warning": "MWEB를 사용하면 처음에는 ~ 600MB의 데이터를 다운로드하며 네트워크 속도에 따라 최대 30 분이 소요될 수 있습니다. 이 초기 데이터는 한 번만 다운로드하여 모든 조명 지갑에 사용할 수 있습니다.", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index b9c6441a7..b2c6386f0 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -36,6 +36,7 @@ "agree": "သဘောတူသည်။", "agree_and_continue": "သဘောတူပြီး ရှေ့ဆက်ပါ။", "agree_to": "အကောင့်ဖန်တီးခြင်းဖြင့် သင်သည် ဤအရာကို သဘောတူပါသည်။", + "alert_notice": "မှတ်သား", "all": "အားလုံး", "all_trades": "ကုန်သွယ်မှုအားလုံး", "all_transactions": "အရောင်းအဝယ်အားလုံး", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "mweb scanning ဖွင့်ပါ", "litecoin_mweb": "မင်္ဂလာပါ", "litecoin_mweb_always_scan": "Mweb အမြဲစကင်ဖတ်စစ်ဆေးပါ", + "litecoin_mweb_description": "Mweb သည် Protocol အသစ်ဖြစ်ပြီး LitCoin သို့ပိုမိုဈေးချိုသာသော, စျေးသက်သက်သာသာသုံးခြင်းနှင့်ပိုမိုများပြားသောပုဂ္ဂလိကငွေပို့ဆောင်မှုများကိုဖြစ်ပေါ်စေသည်", + "litecoin_mweb_dismiss": "ထုတ်ပစ်", "litecoin_mweb_display_card": "MweB ကဒ်ကိုပြပါ", + "litecoin_mweb_enable_later": "သင် MweB ကို display settings အောက်ရှိ ထပ်မံ. ခွင့်ပြုရန်ရွေးချယ်နိုင်သည်။", + "litecoin_mweb_pegin": "တံစို့", + "litecoin_mweb_pegout": "တံစို့", "litecoin_mweb_scanning": "mweb scanning", "litecoin_mweb_settings": "Mweb ဆက်တင်များ", "litecoin_mweb_warning": "MweB ကိုအသုံးပြုခြင်းသည်အစပိုင်းတွင် ~ 600MB ဒေတာများကို download လုပ်ပြီးကွန်ယက်အမြန်နှုန်းပေါ် မူတည်. မိနစ် 30 အထိကြာနိုင်သည်။ ဤကန ဦး ဒေတာကိုတစ်ကြိမ်သာ download လုပ်ပြီး litecoin Walkets အားလုံးအတွက်ရနိုင်သည်", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index f447b9a3a..12315cd3c 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -36,6 +36,7 @@ "agree": "mee eens", "agree_and_continue": "Akkoord & doorgaan", "agree_to": "Door een account aan te maken gaat u akkoord met de ", + "alert_notice": "Kennisgeving", "all": "ALLE", "all_trades": "Alle transacties", "all_transactions": "Alle transacties", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "MWEB -scanning inschakelen", "litecoin_mweb": "Mweb", "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", "litecoin_mweb_display_card": "Toon MWEB -kaart", + "litecoin_mweb_enable_later": "U kunt ervoor kiezen om MWeb opnieuw in te schakelen onder weergave -instellingen.", + "litecoin_mweb_pegin": "Vastmaken", + "litecoin_mweb_pegout": "Uithakken", "litecoin_mweb_scanning": "MWEB -scanning", "litecoin_mweb_settings": "MWEB -instellingen", "litecoin_mweb_warning": "Het gebruik van MWeb downloadt in eerste instantie ~ 600 MB aan gegevens en kan tot 30 minuten duren, afhankelijk van de netwerksnelheid. Deze eerste gegevens worden slechts eenmaal gedownload en zijn beschikbaar voor alle Litecoin -portefeuilles", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index bcd420679..accb31d46 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -36,6 +36,7 @@ "agree": "Zgadzam się", "agree_and_continue": "Zgadzam się i kontynuuj", "agree_to": "Tworząc konto wyrażasz zgodę na ", + "alert_notice": "Ogłoszenie", "all": "WSZYSTKO", "all_trades": "Wszystkie operacje", "all_transactions": "Wszystkie transakcje", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Włącz skanowanie MWEB", "litecoin_mweb": "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ć", "litecoin_mweb_display_card": "Pokaż kartę MWEB", + "litecoin_mweb_enable_later": "Możesz ponownie włączyć MWEB w ustawieniach wyświetlania.", + "litecoin_mweb_pegin": "Kołek", + "litecoin_mweb_pegout": "Palikować", "litecoin_mweb_scanning": "Skanowanie MWEB", "litecoin_mweb_settings": "Ustawienia MWEB", "litecoin_mweb_warning": "Korzystanie z MWEB początkowo pobiera ~ 600 MB danych i może potrwać do 30 minut w zależności od prędkości sieci. Te początkowe dane pobierają tylko raz i będą dostępne dla wszystkich portfeli Litecoin", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 51d11f9b0..294a6bf1b 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -36,6 +36,7 @@ "agree": "Concordo", "agree_and_continue": "Concordar e continuar", "agree_to": "Ao criar conta você concorda com ", + "alert_notice": "Perceber", "all": "TUDO", "all_trades": "Todas as negociações", "all_transactions": "Todas as transacções", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Ativar digitalização do MWEB", "litecoin_mweb": "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", "litecoin_mweb_display_card": "Mostre o cartão MWEB", + "litecoin_mweb_enable_later": "Você pode optar por ativar o MWEB novamente em Configurações de exibição.", + "litecoin_mweb_pegin": "Peg in", + "litecoin_mweb_pegout": "Peg fora", "litecoin_mweb_scanning": "MWEB Scanning", "litecoin_mweb_settings": "Configurações do MWEB", "litecoin_mweb_warning": "O uso do MWEB baixará inicialmente ~ 600 MB de dados e pode levar até 30 minutos, dependendo da velocidade da rede. Esses dados iniciais serão baixados apenas uma vez e estarão disponíveis para todas as carteiras Litecoin", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 94c8a2de3..1c3a23d67 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -36,6 +36,7 @@ "agree": "согласен", "agree_and_continue": "Согласиться и продолжить", "agree_to": "Создавая аккаунт, вы соглашаетесь с ", + "alert_notice": "Уведомление", "all": "ВСЕ", "all_trades": "Все сделки", "all_transactions": "Все транзакции", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Включить MWEB сканирование", "litecoin_mweb": "Мвеб", "litecoin_mweb_always_scan": "Установить MWEB всегда сканирование", + "litecoin_mweb_description": "MWEB - это новый протокол, который приносит быстрее, дешевле и более частные транзакции в Litecoin", + "litecoin_mweb_dismiss": "Увольнять", "litecoin_mweb_display_card": "Показать карту MWEB", + "litecoin_mweb_enable_later": "Вы можете снова включить MWEB в настройках отображения.", + "litecoin_mweb_pegin": "Внедрять", + "litecoin_mweb_pegout": "Выкрикивать", "litecoin_mweb_scanning": "MWEB сканирование", "litecoin_mweb_settings": "Настройки MWEB", "litecoin_mweb_warning": "Использование MWEB изначально загрузит ~ 600 МБ данных и может занять до 30 минут в зависимости от скорости сети. Эти начальные данные будут загружаться только один раз и будут доступны для всех кошельков Litecoin", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 8fb6b12cc..d1328257d 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -36,6 +36,7 @@ "agree": "ยอมรับ", "agree_and_continue": "ยอมรับและดำเนินการต่อ", "agree_to": "การสร้างบัญชีของคุณยอมรับเงื่อนไขของ", + "alert_notice": "สังเกต", "all": "ทั้งหมด", "all_trades": "การซื้อขายทั้งหมด", "all_transactions": "การทำธุรกรรมทั้งหมด", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "เปิดใช้งานการสแกน MWEB", "litecoin_mweb": "mweb", "litecoin_mweb_always_scan": "ตั้งค่าการสแกน MWEB เสมอ", + "litecoin_mweb_description": "MWEB เป็นโปรโตคอลใหม่ที่นำการทำธุรกรรมที่เร็วกว่าราคาถูกกว่าและเป็นส่วนตัวมากขึ้นไปยัง Litecoin", + "litecoin_mweb_dismiss": "อนุญาตให้ออกไป", "litecoin_mweb_display_card": "แสดงการ์ด mweb", + "litecoin_mweb_enable_later": "คุณสามารถเลือกเปิดใช้งาน MWEB อีกครั้งภายใต้การตั้งค่าการแสดงผล", + "litecoin_mweb_pegin": "หมุด", + "litecoin_mweb_pegout": "ตรึง", "litecoin_mweb_scanning": "การสแกน MWEB", "litecoin_mweb_settings": "การตั้งค่า MWEB", "litecoin_mweb_warning": "การใช้ MWEB จะดาวน์โหลดข้อมูล ~ 600MB ในขั้นต้นและอาจใช้เวลาสูงสุด 30 นาทีขึ้นอยู่กับความเร็วเครือข่าย ข้อมูลเริ่มต้นนี้จะดาวน์โหลดได้เพียงครั้งเดียวและพร้อมใช้งานสำหรับกระเป๋าเงินทั้งหมดของ Litecoin", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 1b699ba06..ca3a48478 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -36,6 +36,7 @@ "agree": "Sumang-ayon", "agree_and_continue": "Sumang-ayon & Magpatuloy", "agree_to": "Sa pamamagitan ng paggawa ng account sumasang-ayon ka sa ", + "alert_notice": "PAUNAWA", "all": "LAHAT", "all_trades": "Lahat ng mga trade", "all_transactions": "Lahat ng mga transaksyon", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Paganahin ang pag -scan ng MWeb", "litecoin_mweb": "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", "litecoin_mweb_display_card": "Ipakita ang MWEB Card", + "litecoin_mweb_enable_later": "Maaari kang pumili upang paganahin muli ang MWeb sa ilalim ng mga setting ng pagpapakita.", + "litecoin_mweb_pegin": "Peg in", + "litecoin_mweb_pegout": "Peg out", "litecoin_mweb_scanning": "Pag -scan ng Mweb", "litecoin_mweb_settings": "Mga Setting ng Mweb", "litecoin_mweb_warning": "Ang paggamit ng MWEB ay unang i -download ang ~ 600MB ng data, at maaaring tumagal ng hanggang sa 30 minuto depende sa bilis ng network. Ang paunang data na ito ay mag -download lamang ng isang beses at magagamit para sa lahat ng mga wallets ng Litecoin", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index c352192a8..dac8c1c81 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -36,6 +36,7 @@ "agree": "Kabul Et", "agree_and_continue": "Kabul Et & Devam Et", "agree_to": "Hesap oluşturarak bunları kabul etmiş olursunuz ", + "alert_notice": "Fark etme", "all": "HEPSİ", "all_trades": "Tüm takaslar", "all_transactions": "Tüm transferler", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "MWEB taramasını etkinleştir", "litecoin_mweb": "Mweb", "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", "litecoin_mweb_display_card": "MWEB kartını göster", + "litecoin_mweb_enable_later": "Ekran ayarlarının altında MWEB'yi tekrar etkinleştirmeyi seçebilirsiniz.", + "litecoin_mweb_pegin": "Takılmak", + "litecoin_mweb_pegout": "Çiğnemek", "litecoin_mweb_scanning": "MWEB taraması", "litecoin_mweb_settings": "MWEB ayarları", "litecoin_mweb_warning": "MWEB kullanmak başlangıçta ~ 600MB veri indirir ve ağ hızına bağlı olarak 30 dakikaya kadar sürebilir. Bu ilk veriler yalnızca bir kez indirilecek ve tüm Litecoin cüzdanları için kullanılabilir olacak", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index bb8dce457..ba6c6516d 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -36,6 +36,7 @@ "agree": "Згоден", "agree_and_continue": "Погодитися та продовжити", "agree_to": "Створюючи обліковий запис, ви погоджуєтеся з ", + "alert_notice": "Ув'язнення", "all": "ВСЕ", "all_trades": "Всі операції", "all_transactions": "Всі транзакції", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "Увімкнути сканування MWEB", "litecoin_mweb": "Мвеб", "litecoin_mweb_always_scan": "Встановити mweb завжди сканувати", + "litecoin_mweb_description": "MWEB - це новий протокол, який приносить швидкі, дешевші та більш приватні транзакції Litecoin", + "litecoin_mweb_dismiss": "Звільнити", "litecoin_mweb_display_card": "Показати карту MWeb", + "litecoin_mweb_enable_later": "Ви можете знову ввімкнути MWEB в налаштуваннях дисплея.", + "litecoin_mweb_pegin": "Подякувати", + "litecoin_mweb_pegout": "Подякувати", "litecoin_mweb_scanning": "Сканування Mweb", "litecoin_mweb_settings": "Налаштування MWEB", "litecoin_mweb_warning": "Використання MWEB спочатку завантажить ~ 600 Мб даних і може зайняти до 30 хвилин залежно від швидкості мережі. Ці початкові дані завантажуються лише один раз і будуть доступні для всіх гаманців Litecoin", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index da3d04a04..3e68a5fb4 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -36,6 +36,7 @@ "agree": "متفق", "agree_and_continue": "اتفاق کریں اور جاری رکھیں", "agree_to": "اکاؤنٹ بنا کر آپ اس سے اتفاق کرتے ہیں۔", + "alert_notice": "نوٹس", "all": "تمام", "all_trades": "تمام تجارت", "all_transactions": "تمام لین دین", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "MWEB اسکیننگ کو فعال کریں", "litecoin_mweb": "MWEB", "litecoin_mweb_always_scan": "MWEB ہمیشہ اسکیننگ سیٹ کریں", + "litecoin_mweb_description": "MWEB ایک نیا پروٹوکول ہے جو لیٹیکوئن میں تیز ، سستا اور زیادہ نجی لین دین لاتا ہے", + "litecoin_mweb_dismiss": "خارج", "litecoin_mweb_display_card": "MWEB کارڈ دکھائیں", + "litecoin_mweb_enable_later": "آپ ڈسپلے کی ترتیبات کے تحت MWEB کو دوبارہ فعال کرنے کا انتخاب کرسکتے ہیں۔", + "litecoin_mweb_pegin": "پیگ میں", + "litecoin_mweb_pegout": "پیگ آؤٹ", "litecoin_mweb_scanning": "MWEB اسکیننگ", "litecoin_mweb_settings": "MWEB کی ترتیبات", "litecoin_mweb_warning": "MWEB کا استعمال ابتدائی طور پر m 600mb ڈیٹا ڈاؤن لوڈ کرے گا ، اور نیٹ ورک کی رفتار کے لحاظ سے 30 منٹ تک کا وقت لگ سکتا ہے۔ یہ ابتدائی اعداد و شمار صرف ایک بار ڈاؤن لوڈ کریں گے اور تمام لیٹیکوئن بٹوے کے لئے دستیاب ہوں گے", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index f4ed7aebb..159a127d0 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -36,6 +36,7 @@ "agree": "Đồng ý", "agree_and_continue": "Đồng ý & Tiếp tục", "agree_to": "Bằng cách tạo tài khoản, bạn đồng ý với ", + "alert_notice": "Để ý", "all": "TẤT CẢ", "all_trades": "Tất cả giao dịch", "all_transactions": "Tất cả giao dịch", @@ -364,6 +365,11 @@ "ledger_error_wrong_app": "Vui lòng đảm bảo bạn đã mở đúng ứng dụng trên Ledger của mình", "ledger_please_enable_bluetooth": "Vui lòng bật Bluetooth để phát hiện Ledger của bạn", "light_theme": "Chủ đề sáng", + "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", + "litecoin_mweb_enable_later": "Bạn có thể chọn bật lại MWEB trong cài đặt hiển thị.", + "litecoin_mweb_pegin": "Chốt vào", + "litecoin_mweb_pegout": "Chốt ra", "live_fee_rates": "Tỷ lệ phí hiện tại qua API", "load_more": "Tải thêm", "loading_your_wallet": "Đang tải ví của bạn", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 8114d5ab1..a12774d5d 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -36,6 +36,7 @@ "agree": "Jọ rò", "agree_and_continue": "Jọ Rò àti Tẹ̀síwájú", "agree_to": "Tẹ́ ẹ bá dá àkáǹtì ẹ jọ rò ", + "alert_notice": "Akiyesi", "all": "Gbogbo", "all_trades": "Gbogbo àwọn pàṣípààrọ̀", "all_transactions": "Gbogbo àwọn àránṣẹ́", @@ -367,7 +368,12 @@ "litecoin_enable_mweb_sync": "Mu mweb ọlọjẹ", "litecoin_mweb": "Mweb", "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", "litecoin_mweb_display_card": "Fihan kaadi Mweb", + "litecoin_mweb_enable_later": "O le yan lati ṣiṣẹ Mweb lẹẹkansi labẹ awọn eto ifihan.", + "litecoin_mweb_pegin": "Peg in", + "litecoin_mweb_pegout": "Peg jade", "litecoin_mweb_scanning": "Mweb scanning", "litecoin_mweb_settings": "Awọn eto Mweb", "litecoin_mweb_warning": "Lilo Mweb yoo wa lakoko igbasilẹ ~ 600MB ti data, o le gba to iṣẹju 30 da lori iyara nẹtiwọọki. Awọn data akọkọ yii yoo ṣe igbasilẹ lẹẹkan si ki o wa fun gbogbo awọn Wolinkun LiveCooin", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index de1bb4df4..8f8e8cd13 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -36,6 +36,7 @@ "agree": "同意", "agree_and_continue": "同意并继续", "agree_to": "创建账户即表示您同意 ", + "alert_notice": "注意", "all": "全部", "all_trades": "所有的变化", "all_transactions": "所有交易", @@ -366,7 +367,12 @@ "litecoin_enable_mweb_sync": "启用MWEB扫描", "litecoin_mweb": "MWEB", "litecoin_mweb_always_scan": "设置MWEB总是扫描", + "litecoin_mweb_description": "MWEB是一项新协议,它将更快,更便宜和更多的私人交易带给Litecoin", + "litecoin_mweb_dismiss": "解雇", "litecoin_mweb_display_card": "显示MWEB卡", + "litecoin_mweb_enable_later": "您可以选择在显示设置下再次启用MWEB。", + "litecoin_mweb_pegin": "钉进", + "litecoin_mweb_pegout": "昏倒", "litecoin_mweb_scanning": "MWEB扫描", "litecoin_mweb_settings": "MWEB设置", "litecoin_mweb_warning": "使用MWEB最初将下载约600MB的数据,并且最多可能需要30分钟的时间,具体取决于网络速度。此初始数据只能下载一次,并适用于所有莱特币钱包", diff --git a/tool/configure.dart b/tool/configure.dart index 0c0f491a5..1e2741721 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -107,6 +107,7 @@ import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_bitcoin/bitcoin_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; +import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; @@ -230,6 +231,7 @@ abstract class Bitcoin { bool txIsMweb(TransactionInfo txInfo); Future setMwebEnabled(Object wallet, bool enabled); bool getMwebEnabled(Object wallet); + String? getUnusedMwebAddress(Object wallet); } """; From 62f55ae8f51d6697f658c38e541dd055b392ec41 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Tue, 8 Oct 2024 01:07:27 +0100 Subject: [PATCH 11/73] Allow Wallet Group Names to be the same as Wallet Names (#1730) * fix: Issues with imaging * fix: Allow group names to be the same as wallet names * fix: Bug with wallet grouping when a wallet is minimized * fix: Bug with wallet grouping when a wallet is minimized --- lib/di.dart | 6 +++++- lib/src/screens/wallet/wallet_edit_page.dart | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/di.dart b/lib/di.dart index 5e5417b34..0008ad8fd 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -378,7 +378,11 @@ Future setup({ getIt.registerFactory(() => NewWalletTypeViewModel(_walletInfoSource)); getIt.registerFactory( - () => WalletManager(_walletInfoSource, getIt.get()), + () { + final instance = WalletManager(_walletInfoSource, getIt.get()); + instance.updateWalletGroups(); + return instance; + }, ); getIt.registerFactoryParam( diff --git a/lib/src/screens/wallet/wallet_edit_page.dart b/lib/src/screens/wallet/wallet_edit_page.dart index 9515e58c3..340091a1e 100644 --- a/lib/src/screens/wallet/wallet_edit_page.dart +++ b/lib/src/screens/wallet/wallet_edit_page.dart @@ -18,7 +18,6 @@ import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; - class WalletEditPage extends BasePage { WalletEditPage({ required this.pageArguments, @@ -86,8 +85,9 @@ class WalletEditPage extends BasePage { child: LoadingPrimaryButton( onPressed: () async { if (_formKey.currentState?.validate() ?? false) { - if (pageArguments.walletNewVM! - .nameExists(walletEditViewModel.newName)) { + if (!pageArguments.isWalletGroup && + pageArguments.walletNewVM! + .nameExists(walletEditViewModel.newName)) { showPopUp( context: context, builder: (_) { From 8acf8bdfb21cabad9cd16c296465d01199b54a51 Mon Sep 17 00:00:00 2001 From: Rafael Date: Thu, 10 Oct 2024 11:10:12 -0300 Subject: [PATCH 12/73] feat: add onError exception (#1743) --- cw_bitcoin/lib/electrum_wallet.dart | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 877b108b7..0be83c624 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1315,7 +1315,7 @@ abstract class ElectrumWalletBase // Set the balance of all non-silent payment addresses to 0 before updating walletAddresses.allAddresses.forEach((addr) { - if(addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0; + if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0; }); await Future.wait(walletAddresses.allAddresses.map((address) async { @@ -1825,6 +1825,8 @@ abstract class ElectrumWalletBase Future> _fetchAddressHistory( BitcoinAddressRecord addressRecord, int? currentHeight) async { + String txid = ""; + try { final Map historiesWithDetails = {}; @@ -1834,7 +1836,7 @@ abstract class ElectrumWalletBase addressRecord.setAsUsed(); await Future.wait(history.map((transaction) async { - final txid = transaction['tx_hash'] as String; + txid = transaction['tx_hash'] as String; final height = transaction['height'] as int; final storedTx = transactionHistory.transactions[txid]; @@ -1865,8 +1867,12 @@ abstract class ElectrumWalletBase } return historiesWithDetails; - } catch (e) { - print(e.toString()); + } catch (e, stacktrace) { + _onError?.call(FlutterErrorDetails( + exception: "$txid - $e", + stack: stacktrace, + library: this.runtimeType.toString(), + )); return {}; } } From 380f7653b2e39f468d722424b8e4d7f0f0eb910f Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Sun, 13 Oct 2024 02:13:52 +0300 Subject: [PATCH 13/73] V4.20.0 rc2 (#1727) * version 4.20.0 * update build numbers * UI updates and script fix for ios bundle identifier * disable mweb for desktop * change hardcoded ltc server ip address electrum connection enhancement * MWEB enhancements 2.0 (#1735) * additional logging and minor fixes * additional logging and minor fixes * addresses pt.1 * Allow Wallet Group Names to be the same as Wallet Names (#1730) * fix: Issues with imaging * fix: Allow group names to be the same as wallet names * fix: Bug with wallet grouping when a wallet is minimized * fix: Bug with wallet grouping when a wallet is minimized * logs of fixes and experimental changes, close wallet before opening next * save * fix icon * fixes * [skip ci] updates * [skip ci] updates * updates * minor optimizations * fix for when switching between wallets * [skip ci] updates * [skip ci] updates * Update cw_bitcoin/lib/litecoin_wallet.dart Co-authored-by: Omar Hatem * Update cw_bitcoin/lib/litecoin_wallet.dart Co-authored-by: Omar Hatem * mobx * mostly logging * stream fix pt.1 [skip ci] * updates * some fixes and enhancements * [skip ci] minor * potential partial fix for streamsink closed * fix stream sink closed errors * fix mweb logo colors * save * minor enhancements [skip ci] * save * experimental * minor * minor [skip ci] --------- Co-authored-by: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Co-authored-by: Omar Hatem * fix menu list removing from original list --------- Co-authored-by: Matthew Fosse Co-authored-by: David Adegoke <64401859+Blazebrain@users.noreply.github.com> --- assets/text/Monerocom_Release_Notes.txt | 6 +- assets/text/Release_Notes.txt | 13 +- .../lib/bitcoin_receive_page_option.dart | 2 +- cw_bitcoin/lib/electrum.dart | 13 +- cw_bitcoin/lib/electrum_wallet.dart | 30 ++-- cw_bitcoin/lib/electrum_wallet_addresses.dart | 6 +- cw_bitcoin/lib/electrum_wallet_snapshot.dart | 11 +- cw_bitcoin/lib/litecoin_wallet.dart | 60 ++++---- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 55 +++---- .../lib/pending_bitcoin_transaction.dart | 2 + cw_core/lib/wallet_base.dart | 2 +- cw_evm/lib/evm_chain_wallet.dart | 2 +- cw_haven/lib/haven_wallet.dart | 2 +- cw_haven/lib/haven_wallet_service.dart | 2 +- cw_monero/lib/monero_wallet.dart | 2 +- cw_monero/lib/monero_wallet_service.dart | 2 +- .../com/cakewallet/mweb/CwMwebPlugin.kt | 11 +- cw_mweb/ios/Classes/CwMwebPlugin.swift | 17 ++- cw_mweb/lib/cw_mweb.dart | 135 +++++++++++++----- cw_mweb/lib/cw_mweb_method_channel.dart | 25 ++++ cw_mweb/lib/cw_mweb_platform_interface.dart | 4 + cw_nano/lib/nano_wallet.dart | 2 +- cw_nano/pubspec.lock | 4 +- cw_solana/lib/solana_wallet.dart | 2 +- cw_tron/lib/tron_wallet.dart | 2 +- cw_wownero/lib/wownero_wallet.dart | 2 +- cw_wownero/lib/wownero_wallet_service.dart | 2 +- ios/Podfile.lock | 46 +++++- ios/Runner.xcodeproj/project.pbxproj | 54 +++---- lib/bitcoin/cw_bitcoin.dart | 15 +- .../screens/dashboard/pages/balance_page.dart | 121 ++++++++-------- .../dashboard/widgets/menu_widget.dart | 14 +- lib/src/screens/root/root.dart | 7 - lib/src/screens/settings/mweb_settings.dart | 5 +- lib/src/widgets/setting_actions.dart | 3 +- lib/store/app_store.dart | 3 +- .../dashboard/balance_view_model.dart | 37 ++++- .../dashboard/dashboard_view_model.dart | 39 ++--- .../settings/mweb_settings_view_model.dart | 14 +- lib/view_model/wallet_creation_vm.dart | 48 ++++--- .../wallet_list/wallet_list_view_model.dart | 2 + 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 | 3 +- 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_mwebd.sh | 2 +- scripts/ios/app_config.sh | 2 +- scripts/ios/app_env.sh | 8 +- scripts/ios/build_mwebd.sh | 2 +- scripts/linux/app_env.sh | 4 +- scripts/macos/app_config.sh | 2 +- scripts/macos/app_env.sh | 8 +- scripts/windows/build_exe_installer.iss | 2 +- tool/configure.dart | 1 + 80 files changed, 572 insertions(+), 329 deletions(-) diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index fec1485ac..2f79dd5c6 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,2 +1,4 @@ -Enhance auto-address generation for Monero -Bug fixes and enhancements \ No newline at end of file +Monero enhancements for sending and address generation +StealthEx +LetsExchange +Visual enhancements and bug fixes \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index 4e8a79fc1..868ae7954 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,4 +1,9 @@ -Enable BIP39 by default for wallet creation also on Bitcoin/Litecoin (Electrum seed type is still accessible through advanced settings page) -Improve fee calculation for Bitcoin to protect against overpaying or underpaying -Enhance auto-address generation for Monero -Bug fixes and enhancements \ No newline at end of file +Add Litecoin MWEB +Wallet groups (same seed, multiple wallets) +Silent Payments enhancements +Monero enhancements for sending and address generation +StealthEx +LetsExchange +Replace-By-Fee improvements +ERC20 tokens potential scam detection +Visual enhancements and bug fixes \ No newline at end of file diff --git a/cw_bitcoin/lib/bitcoin_receive_page_option.dart b/cw_bitcoin/lib/bitcoin_receive_page_option.dart index 8e72bc7b0..07083e111 100644 --- a/cw_bitcoin/lib/bitcoin_receive_page_option.dart +++ b/cw_bitcoin/lib/bitcoin_receive_page_option.dart @@ -30,7 +30,7 @@ class BitcoinReceivePageOption implements ReceivePageOption { static const allLitecoin = [ BitcoinReceivePageOption.p2wpkh, - BitcoinReceivePageOption.mweb + BitcoinReceivePageOption.mweb, ]; BitcoinAddressType toType() { diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index cfcc71d09..54ccaaef5 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -116,7 +116,7 @@ class ElectrumClient { _parseResponse(message); } } catch (e) { - print(e.toString()); + print("socket.listen: $e"); } }, onError: (Object error) { @@ -125,14 +125,15 @@ class ElectrumClient { unterminatedString = ''; }, onDone: () { + print("SOCKET CLOSED!!!!!"); unterminatedString = ''; try { if (host == socket?.address.host) { - socket?.destroy(); _setConnectionStatus(ConnectionStatus.disconnected); + socket?.destroy(); } } catch (e) { - print(e.toString()); + print("onDone: $e"); } }, cancelOnError: true, @@ -421,7 +422,7 @@ class ElectrumClient { BehaviorSubject? subscribe( {required String id, required String method, List params = const []}) { try { - if (socket == null) { + if (socket == null || !isConnected) { return null; } final subscription = BehaviorSubject(); @@ -437,7 +438,7 @@ class ElectrumClient { Future call( {required String method, List params = const [], Function(int)? idCallback}) async { - if (socket == null) { + if (socket == null || !isConnected) { return null; } final completer = Completer(); @@ -453,7 +454,7 @@ class ElectrumClient { Future callWithTimeout( {required String method, List params = const [], int timeout = 5000}) async { try { - if (socket == null) { + if (socket == null || !isConnected) { return null; } final completer = Completer(); diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 0be83c624..97897046c 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:cw_bitcoin/litecoin_wallet_addresses.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; @@ -23,7 +22,6 @@ import 'package:cw_bitcoin/electrum_transaction_history.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/exceptions.dart'; -import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -168,7 +166,10 @@ abstract class ElectrumWalletBase @observable SyncStatus syncStatus; - Set get addressesSet => walletAddresses.allAddresses.map((addr) => addr.address).toSet(); + Set get addressesSet => walletAddresses.allAddresses + .where((element) => element.type != SegwitAddresType.mweb) + .map((addr) => addr.address) + .toSet(); List get scriptHashes => walletAddresses.addressesByReceiveType .where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress) @@ -1174,6 +1175,7 @@ abstract class ElectrumWalletBase 'silent_addresses': walletAddresses.silentAddresses.map((addr) => addr.toJSON()).toList(), 'silent_address_index': walletAddresses.currentSilentAddressIndex.toString(), 'mweb_addresses': walletAddresses.mwebAddresses.map((addr) => addr.toJSON()).toList(), + 'alwaysScan': alwaysScan, }); int feeRate(TransactionPriority priority) { @@ -1291,7 +1293,7 @@ abstract class ElectrumWalletBase } @override - Future close() async { + Future close({required bool shouldCleanup}) async { try { await _receiveStream?.cancel(); await electrumClient.close(); @@ -1314,11 +1316,15 @@ abstract class ElectrumWalletBase } // Set the balance of all non-silent payment addresses to 0 before updating - walletAddresses.allAddresses.forEach((addr) { + walletAddresses.allAddresses + .where((element) => element.type != SegwitAddresType.mweb) + .forEach((addr) { if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0; }); - await Future.wait(walletAddresses.allAddresses.map((address) async { + await Future.wait(walletAddresses.allAddresses + .where((element) => element.type != SegwitAddresType.mweb) + .map((address) async { updatedUnspentCoins.addAll(await fetchUnspent(address)); })); @@ -1878,6 +1884,7 @@ abstract class ElectrumWalletBase } Future updateTransactions() async { + print("updateTransactions() called!"); try { if (_isTransactionUpdating) { return; @@ -1903,12 +1910,16 @@ abstract class ElectrumWalletBase Future subscribeForUpdates() async { final unsubscribedScriptHashes = walletAddresses.allAddresses.where( - (address) => !_scripthashesUpdateSubject.containsKey(address.getScriptHash(network)), + (address) => + !_scripthashesUpdateSubject.containsKey(address.getScriptHash(network)) && + address.type != SegwitAddresType.mweb, ); await Future.wait(unsubscribedScriptHashes.map((address) async { final sh = address.getScriptHash(network); - await _scripthashesUpdateSubject[sh]?.close(); + if (!(_scripthashesUpdateSubject[sh]?.isClosed ?? true)) { + await _scripthashesUpdateSubject[sh]?.close(); + } _scripthashesUpdateSubject[sh] = await electrumClient.scripthashUpdate(sh); _scripthashesUpdateSubject[sh]?.listen((event) async { try { @@ -1918,7 +1929,7 @@ abstract class ElectrumWalletBase await _fetchAddressHistory(address, await getCurrentChainTip()); } catch (e, s) { - print(e.toString()); + print("sub error: $e"); _onError?.call(FlutterErrorDetails( exception: e, stack: s, @@ -1996,6 +2007,7 @@ abstract class ElectrumWalletBase } Future updateBalance() async { + print("updateBalance() called!"); balance[currency] = await fetchBalances(); await save(); } diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 91345d726..ce1ff9713 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -1,3 +1,5 @@ +import 'dart:io' show Platform; + import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; @@ -238,7 +240,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); } else if (walletInfo.type == WalletType.litecoin) { await _generateInitialAddresses(type: SegwitAddresType.p2wpkh); - await _generateInitialAddresses(type: SegwitAddresType.mweb); + if (Platform.isAndroid || Platform.isIOS) { + await _generateInitialAddresses(type: SegwitAddresType.mweb); + } } else if (walletInfo.type == WalletType.bitcoin) { await _generateInitialAddresses(); await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 25cc5637e..990719089 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -24,6 +24,7 @@ class ElectrumWalletSnapshot { required this.silentAddresses, required this.silentAddressIndex, required this.mwebAddresses, + required this.alwaysScan, this.passphrase, this.derivationType, this.derivationPath, @@ -46,6 +47,7 @@ class ElectrumWalletSnapshot { List addresses; List silentAddresses; List mwebAddresses; + bool alwaysScan; ElectrumBalance balance; Map regularAddressIndex; @@ -54,15 +56,15 @@ class ElectrumWalletSnapshot { DerivationType? derivationType; String? derivationPath; - static Future load( - EncryptionFileUtils encryptionFileUtils, String name, WalletType type, String password, BasedUtxoNetwork network) async { + static Future load(EncryptionFileUtils encryptionFileUtils, String name, + WalletType type, String password, BasedUtxoNetwork network) async { final path = await pathForWallet(name: name, type: type); final jsonSource = await encryptionFileUtils.read(path: path, password: password); final data = json.decode(jsonSource) as Map; final mnemonic = data['mnemonic'] as String?; final xpub = data['xpub'] as String?; final passphrase = data['passphrase'] as String? ?? ''; - + final addressesTmp = data['addresses'] as List? ?? []; final addresses = addressesTmp .whereType() @@ -81,6 +83,8 @@ class ElectrumWalletSnapshot { .map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network)) .toList(); + final alwaysScan = data['alwaysScan'] as bool? ?? false; + final balance = ElectrumBalance.fromJSON(data['balance'] as String?) ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; @@ -124,6 +128,7 @@ class ElectrumWalletSnapshot { silentAddresses: silentAddresses, silentAddressIndex: silentAddressIndex, mwebAddresses: mwebAddresses, + alwaysScan: alwaysScan, ); } } diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 8fe02ecd3..f4d99d807 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -76,6 +76,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { seedBytes: seedBytes, encryptionFileUtils: encryptionFileUtils, currency: CryptoCurrency.ltc, + alwaysScan: alwaysScan, ) { mwebHd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/1000'") as Bip32Slip10Secp256k1; mwebEnabled = alwaysScan ?? false; @@ -101,7 +102,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { Timer? _feeRatesTimer; Timer? _processingTimer; StreamSubscription? _utxoStream; - late RpcClient _stub; late bool mwebEnabled; bool processingUtxos = false; @@ -229,18 +229,14 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialRegularAddressIndex: snp?.regularAddressIndex, initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: snp?.addressPageType, - alwaysScan: alwaysScan, + alwaysScan: snp?.alwaysScan, ); } Future waitForMwebAddresses() async { + print("waitForMwebAddresses() called!"); // ensure that we have the full 1000 mweb addresses generated before continuing: // should no longer be needed, but leaving here just in case - // final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; - // while (mwebAddrs.length < 1000) { - // print("waiting for mweb addresses to finish generating..."); - // await Future.delayed(const Duration(milliseconds: 1000)); - // } await (walletAddresses as LitecoinWalletAddresses).ensureMwebAddressUpToIndexExists(1020); } @@ -252,16 +248,20 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return; } print("STARTING SYNC - MWEB ENABLED: $mwebEnabled"); + _syncTimer?.cancel(); try { syncStatus = SyncronizingSyncStatus(); await subscribeForUpdates(); updateFeeRates(); - _feeRatesTimer?.cancel(); _feeRatesTimer = Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates()); if (!mwebEnabled) { + try { + // in case we're switching from a litecoin wallet that had mweb enabled + CwMweb.stop(); + } catch (_) {} try { await updateAllUnspents(); await updateTransactions(); @@ -276,23 +276,19 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } await waitForMwebAddresses(); - await getStub(); await processMwebUtxos(); await updateTransactions(); await updateUnspent(); await updateBalance(); } catch (e) { print("failed to start mweb sync: $e"); - syncStatus = FailedSyncStatus(); + syncStatus = FailedSyncStatus(error: "failed to start"); return; } - _syncTimer?.cancel(); - _syncTimer = Timer.periodic(const Duration(milliseconds: 1500), (timer) async { + _syncTimer = Timer.periodic(const Duration(milliseconds: 3000), (timer) async { if (syncStatus is FailedSyncStatus) return; - print("SYNCING...."); - final nodeHeight = await electrumClient.getCurrentBlockChainTip() ?? 0; // current block height of our node @@ -350,10 +346,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @action @override Future stopSync() async { + print("stopSync() called!"); _syncTimer?.cancel(); _utxoStream?.cancel(); _feeRatesTimer?.cancel(); await CwMweb.stop(); + print("stopped syncing!"); } Future initMwebUtxosBox() async { @@ -425,9 +423,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { await initMwebUtxosBox(); } - Future handleIncoming(MwebUtxo utxo, RpcClient stub) async { + Future handleIncoming(MwebUtxo utxo) async { print("handleIncoming() called!"); - final status = await stub.status(StatusRequest()); + final status = await CwMweb.status(StatusRequest()); var date = DateTime.now(); var confirmations = 0; if (utxo.height > 0) { @@ -493,6 +491,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } Future processMwebUtxos() async { + print("processMwebUtxos() called!"); if (!mwebEnabled) { return; } @@ -502,7 +501,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final req = UtxosRequest(scanSecret: scanSecret, fromHeight: restoreHeight); // process new utxos as they come in: - _utxoStream?.cancel(); + await _utxoStream?.cancel(); ResponseStream? responseStream = await CwMweb.utxos(req); if (responseStream == null) { throw Exception("failed to get utxos stream!"); @@ -544,7 +543,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { await mwebUtxosBox.put(utxo.outputId, utxo); - await handleIncoming(utxo, _stub); + await handleIncoming(utxo); }); } @@ -662,6 +661,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } Future updateUnspent() async { + print("updateUnspent() called!"); await checkMwebUtxosSpent(); await updateAllUnspents(); } @@ -675,7 +675,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (!mwebEnabled) { return; } - await getStub(); // add the mweb unspents to the list: List mwebUnspentCoins = []; @@ -718,7 +717,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (!mwebEnabled) { return balance; } - await getStub(); // update unspent balances: await updateUnspent(); @@ -891,7 +889,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return tx; } await waitForMwebAddresses(); - await getStub(); final resp = await CwMweb.create(CreateRequest( rawTx: hex.decode(tx.hex), @@ -1000,13 +997,17 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } @override - Future close() async { + Future close({required bool shouldCleanup}) async { _utxoStream?.cancel(); _feeRatesTimer?.cancel(); _syncTimer?.cancel(); _processingTimer?.cancel(); - await stopSync(); - await super.close(); + if (shouldCleanup) { + try { + await stopSync(); + } catch (_) {} + } + await super.close(shouldCleanup: shouldCleanup); } Future setMwebEnabled(bool enabled) async { @@ -1014,17 +1015,16 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return; } + alwaysScan = enabled; mwebEnabled = enabled; (walletAddresses as LitecoinWalletAddresses).mwebEnabled = enabled; - await stopSync(); + await save(); + try { + await stopSync(); + } catch (_) {} await startSync(); } - Future getStub() async { - _stub = await CwMweb.stub(); - return _stub; - } - Future getStatusRequest() async { final resp = await CwMweb.status(StatusRequest()); return resp; diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 88f640d85..0a20665bf 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io' show Platform; import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; @@ -39,8 +40,10 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with bool mwebEnabled; int mwebTopUpIndex = 1000; List mwebAddrs = []; + bool generating = false; List get scanSecret => mwebHd.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; + List get spendPubkey => mwebHd.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed; @@ -57,20 +60,38 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with } Future ensureMwebAddressUpToIndexExists(int index) async { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + return null; + } + Uint8List scan = Uint8List.fromList(scanSecret); Uint8List spend = Uint8List.fromList(spendPubkey); - int count = 0; - while (mwebAddrs.length <= (index + 1)) { - final address = await CwMweb.address(scan, spend, mwebAddrs.length); - mwebAddrs.add(address!); - count++; - // sleep for a bit to avoid making the main thread unresponsive: - if (count > 50) { - count = 0; - await Future.delayed(Duration(milliseconds: 100)); - } + + if (index < mwebAddresses.length && index < mwebAddrs.length) { + return; } + while (generating) { + print("generating....."); + // this function was called multiple times in multiple places: + await Future.delayed(const Duration(milliseconds: 100)); + } + + print("Generating MWEB addresses up to index $index"); + generating = true; + try { + while (mwebAddrs.length <= (index + 1)) { + final addresses = + await CwMweb.addresses(scan, spend, mwebAddrs.length, mwebAddrs.length + 50); + print("generated up to index ${mwebAddrs.length}"); + // sleep for a bit to avoid making the main thread unresponsive: + await Future.delayed(Duration(milliseconds: 200)); + mwebAddrs.addAll(addresses!); + } + } catch (_) {} + generating = false; + print("Done generating MWEB addresses len: ${mwebAddrs.length}"); + // ensure mweb addresses are up to date: if (mwebAddresses.length < mwebAddrs.length) { List addressRecords = mwebAddrs @@ -90,21 +111,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Future initMwebAddresses() async { if (mwebAddrs.length < 1000) { - print("Generating MWEB addresses..."); await ensureMwebAddressUpToIndexExists(20); - print("done generating MWEB addresses"); - // List addressRecords = mwebAddrs - // .asMap() - // .entries - // .map((e) => BitcoinAddressRecord( - // e.value, - // index: e.key, - // type: SegwitAddresType.mweb, - // network: network, - // )) - // .toList(); - // addMwebAddresses(addressRecords); - // print("added ${addressRecords.length} mweb addresses"); return; } } diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 30cc29ec9..c722dc14f 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -117,6 +117,8 @@ class PendingBitcoinTransaction with PendingTransaction { idOverride = resp.txid; } on GrpcError catch (e) { throw BitcoinTransactionCommitFailed(errorMessage: e.message); + } catch (e) { + throw BitcoinTransactionCommitFailed(errorMessage: "Unknown error: ${e.toString()}"); } } diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 4df1aa1f9..112a20852 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -83,7 +83,7 @@ abstract class WalletBase rescan({required int height}); - void close(); + Future close({required bool shouldCleanup}); Future changePassword(String password); diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index 9bc18d14b..cfaf39d98 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -264,7 +264,7 @@ abstract class EVMChainWalletBase } @override - void close() { + Future close({required bool shouldCleanup}) async { _client.stop(); _transactionsUpdateTimer?.cancel(); _updateFeesTimer?.cancel(); diff --git a/cw_haven/lib/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart index 317d9dc65..06a838100 100644 --- a/cw_haven/lib/haven_wallet.dart +++ b/cw_haven/lib/haven_wallet.dart @@ -106,7 +106,7 @@ abstract class HavenWalletBase Future? updateBalance() => null; @override - void close() { + Future close({required bool shouldCleanup}) async { _listener?.stop(); _onAccountChangeReaction?.reaction.dispose(); _autoSaveTimer?.cancel(); diff --git a/cw_haven/lib/haven_wallet_service.dart b/cw_haven/lib/haven_wallet_service.dart index 14bc520da..274ddc4f8 100644 --- a/cw_haven/lib/haven_wallet_service.dart +++ b/cw_haven/lib/haven_wallet_service.dart @@ -116,7 +116,7 @@ class HavenWalletService extends WalletService< if (!isValid) { await restoreOrResetWalletFiles(name); - wallet.close(); + wallet.close(shouldCleanup: false); return openWallet(name, password); } diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index c6d5d2e5f..0ae2202ba 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -170,7 +170,7 @@ abstract class MoneroWalletBase extends WalletBase? updateBalance() => null; @override - void close() async { + Future close({required bool shouldCleanup}) async { _listener?.stop(); _onAccountChangeReaction?.reaction.dispose(); _onTxHistoryChangeReaction?.reaction.dispose(); diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index 7f4672d43..63ad17583 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -137,7 +137,7 @@ class MoneroWalletService extends WalletService< if (!isValid) { await restoreOrResetWalletFiles(name); - wallet.close(); + wallet.close(shouldCleanup: false); return openWallet(name, password); } diff --git a/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt b/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt index 0cc4cc537..3ab24ee12 100644 --- a/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt +++ b/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt @@ -40,10 +40,17 @@ class CwMwebPlugin: FlutterPlugin, MethodCallHandler { port = null result.success(null) } else if (call.method == "address") { + // val scanSecret: ByteArray = call.argument("scanSecret") ?: ByteArray(0) + // val spendPub: ByteArray = call.argument("spendPub") ?: ByteArray(0) + // val index: Int = call.argument("index") ?: 0 + // val res = Mwebd.address(scanSecret, spendPub, index) + // result.success(res) + } else if (call.method == "addresses") { val scanSecret: ByteArray = call.argument("scanSecret") ?: ByteArray(0) val spendPub: ByteArray = call.argument("spendPub") ?: ByteArray(0) - val index: Int = call.argument("index") ?: 0 - val res = Mwebd.address(scanSecret, spendPub, index) + val fromIndex: Int = call.argument("fromIndex") ?: 0 + val toIndex: Int = call.argument("toIndex") ?: 0 + val res = Mwebd.addresses(scanSecret, spendPub, fromIndex, toIndex) result.success(res) } else { result.notImplemented() diff --git a/cw_mweb/ios/Classes/CwMwebPlugin.swift b/cw_mweb/ios/Classes/CwMwebPlugin.swift index 92061ded8..55b380829 100644 --- a/cw_mweb/ios/Classes/CwMwebPlugin.swift +++ b/cw_mweb/ios/Classes/CwMwebPlugin.swift @@ -32,15 +32,26 @@ public static func register(with registrar: FlutterPluginRegistrar) { stopServer() result(nil) break - case "address": + // case "address": + // let args = call.arguments as! [String: Any] + // let scanSecret = args["scanSecret"] as! FlutterStandardTypedData + // let spendPub = args["spendPub"] as! FlutterStandardTypedData + // let index = args["index"] as! Int32 + + // let scanSecretData = scanSecret.data + // let spendPubData = spendPub.data + // result(MwebdAddress(scanSecretData, spendPubData, index)) + // break + case "addresses": let args = call.arguments as! [String: Any] let scanSecret = args["scanSecret"] as! FlutterStandardTypedData let spendPub = args["spendPub"] as! FlutterStandardTypedData - let index = args["index"] as! Int32 + let fromIndex = args["fromIndex"] as! Int32 + let toIndex = args["toIndex"] as! Int32 let scanSecretData = scanSecret.data let spendPubData = spendPub.data - result(MwebdAddress(scanSecretData, spendPubData, index)) + result(MwebdAddresses(scanSecretData, spendPubData, fromIndex, toIndex)) break default: result(FlutterMethodNotImplemented) diff --git a/cw_mweb/lib/cw_mweb.dart b/cw_mweb/lib/cw_mweb.dart index 3be8e9bf5..39aa433cd 100644 --- a/cw_mweb/lib/cw_mweb.dart +++ b/cw_mweb/lib/cw_mweb.dart @@ -1,3 +1,7 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; import 'dart:typed_data'; import 'package:grpc/grpc.dart'; @@ -10,25 +14,51 @@ class CwMweb { static ClientChannel? _clientChannel; static int? _port; static const TIMEOUT_DURATION = Duration(seconds: 5); + static Timer? logTimer; + + static void readFileWithTimer(String filePath) { + final file = File(filePath); + int lastLength = 0; + + logTimer?.cancel(); + logTimer = Timer.periodic(const Duration(seconds: 1), (timer) async { + try { + final currentLength = await file.length(); + + if (currentLength != lastLength) { + final fileStream = file.openRead(lastLength, currentLength); + final newLines = await fileStream.transform(utf8.decoder).join(); + lastLength = currentLength; + log(newLines); + } + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); + } catch (e) { + log('The mwebd debug log probably is not initialized yet.'); + } + }); + } static Future _initializeClient() async { - await stop(); - // wait a few seconds to make sure the server is stopped - await Future.delayed(const Duration(seconds: 5)); - + print("initialize client called!"); final appDir = await getApplicationSupportDirectory(); - const ltcNodeUri = "45.79.13.180:9333"; + const ltcNodeUri = "ltc-electrum.cakewallet.com:9333"; + + String debugLogPath = "${appDir.path}/logs/debug.log"; + readFileWithTimer(debugLogPath); + _port = await CwMwebPlatform.instance.start(appDir.path, ltcNodeUri); if (_port == null || _port == 0) { throw Exception("Failed to start server"); } - print("Attempting to connect to server on port: $_port"); + log("Attempting to connect to server on port: $_port"); // wait for the server to finish starting up before we try to connect to it: await Future.delayed(const Duration(seconds: 5)); _clientChannel = ClientChannel('127.0.0.1', port: _port!, channelShutdownHandler: () { - print("Channel is shutting down!"); + _rpcClient = null; + log("Channel is shutting down!"); }, options: const ChannelOptions( credentials: ChannelCredentials.insecure(), @@ -49,9 +79,15 @@ class CwMweb { throw Exception("blockTime shouldn't be 0! (this connection is likely broken)"); } return _rpcClient!; - } catch (e) { - print("Attempt $i failed: $e"); + } on GrpcError catch (e) { + log("Attempt $i failed: $e"); + log('Caught grpc error: ${e.message}'); _rpcClient = null; + await Future.delayed(const Duration(seconds: 3)); + } catch (e) { + log("Attempt $i failed: $e"); + _rpcClient = null; + await Future.delayed(const Duration(seconds: 3)); } } throw Exception("Failed to connect after $maxRetries attempts"); @@ -61,22 +97,43 @@ class CwMweb { try { await CwMwebPlatform.instance.stop(); await cleanup(); + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); } catch (e) { - print("Error stopping server: $e"); + log("Error stopping server: $e"); } } static Future address(Uint8List scanSecret, Uint8List spendPub, int index) async { try { - return CwMwebPlatform.instance.address(scanSecret, spendPub, index); + return (await CwMwebPlatform.instance.addresses(scanSecret, spendPub, index, index + 1)) + ?.split(',') + .first; + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); } catch (e) { - print("Error getting address: $e"); - return null; + log("Error getting address: $e"); } + return null; + } + + static Future?> addresses( + Uint8List scanSecret, Uint8List spendPub, int fromIndex, int toIndex) async { + try { + return (await CwMwebPlatform.instance.addresses(scanSecret, spendPub, fromIndex, toIndex)) + ?.split(','); + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); + } catch (e) { + log("Error getting addresses: $e"); + } + return null; } static Future cleanup() async { - await _clientChannel?.terminate(); + try { + await _clientChannel?.terminate(); + } catch (_) {} _rpcClient = null; _clientChannel = null; _port = null; @@ -84,51 +141,57 @@ class CwMweb { // wrappers that handle the connection issues: static Future spent(SpentRequest request) async { + log("mweb.spent() called"); try { - if (_rpcClient == null) { - await _initializeClient(); - } + _rpcClient = await stub(); return await _rpcClient!.spent(request, options: CallOptions(timeout: TIMEOUT_DURATION)); + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); } catch (e) { - print("Error getting spent: $e"); - return SpentResponse(); + log("Error getting spent: $e"); } + return SpentResponse(); } static Future status(StatusRequest request) async { + log("mweb.status() called"); try { - if (_rpcClient == null) { - await _initializeClient(); - } + _rpcClient = await stub(); return await _rpcClient!.status(request, options: CallOptions(timeout: TIMEOUT_DURATION)); + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); } catch (e) { - print("Error getting status: $e"); - return StatusResponse(); + log("Error getting status: $e"); } + return StatusResponse(); } static Future create(CreateRequest request) async { + log("mweb.create() called"); try { - if (_rpcClient == null) { - await _initializeClient(); - } + _rpcClient = await stub(); return await _rpcClient!.create(request, options: CallOptions(timeout: TIMEOUT_DURATION)); + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); } catch (e) { - print("Error getting create: $e"); - return CreateResponse(); + log("Error getting create: $e"); } + return CreateResponse(); } static Future?> utxos(UtxosRequest request) async { + log("mweb.utxos() called"); try { - if (_rpcClient == null) { - await _initializeClient(); - } - // this is a stream, so we should have an effectively infinite timeout: - return _rpcClient!.utxos(request, options: CallOptions(timeout: const Duration(days: 1000 * 365))); + _rpcClient = await stub(); + final resp = _rpcClient! + .utxos(request, options: CallOptions(timeout: const Duration(days: 1000 * 365))); + log("got utxo stream"); + return resp; + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); } catch (e) { - print("Error getting utxos: $e"); - return null; + log("Error getting utxos: $e"); } + return null; } } diff --git a/cw_mweb/lib/cw_mweb_method_channel.dart b/cw_mweb/lib/cw_mweb_method_channel.dart index 4956dcb8d..1aeb1939e 100644 --- a/cw_mweb/lib/cw_mweb_method_channel.dart +++ b/cw_mweb/lib/cw_mweb_method_channel.dart @@ -1,3 +1,5 @@ +import 'dart:io' show Platform; + import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -11,6 +13,9 @@ class MethodChannelCwMweb extends CwMwebPlatform { @override Future start(String dataDir, String nodeUri) async { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + return null; + } final result = await methodChannel.invokeMethod('start', {'dataDir': dataDir, 'nodeUri': nodeUri}); return result; @@ -18,11 +23,17 @@ class MethodChannelCwMweb extends CwMwebPlatform { @override Future stop() async { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + return; + } await methodChannel.invokeMethod('stop'); } @override Future address(Uint8List scanSecret, Uint8List spendPub, int index) async { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + return null; + } final result = await methodChannel.invokeMethod('address', { 'scanSecret': scanSecret, 'spendPub': spendPub, @@ -30,4 +41,18 @@ class MethodChannelCwMweb extends CwMwebPlatform { }); return result; } + + @override + Future addresses(Uint8List scanSecret, Uint8List spendPub, int fromIndex, int toIndex) async { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + return null; + } + final result = await methodChannel.invokeMethod('addresses', { + 'scanSecret': scanSecret, + 'spendPub': spendPub, + 'fromIndex': fromIndex, + 'toIndex': toIndex, + }); + return result; + } } diff --git a/cw_mweb/lib/cw_mweb_platform_interface.dart b/cw_mweb/lib/cw_mweb_platform_interface.dart index d5c03a56c..6ec22bf56 100644 --- a/cw_mweb/lib/cw_mweb_platform_interface.dart +++ b/cw_mweb/lib/cw_mweb_platform_interface.dart @@ -36,4 +36,8 @@ abstract class CwMwebPlatform extends PlatformInterface { Future address(Uint8List scanSecret, Uint8List spendPub, int index) { throw UnimplementedError('address(int) has not been implemented.'); } + + Future addresses(Uint8List scanSecret, Uint8List spendPub, int fromIndex, int toIndex) { + throw UnimplementedError('addresses has not been implemented.'); + } } diff --git a/cw_nano/lib/nano_wallet.dart b/cw_nano/lib/nano_wallet.dart index 040fc9522..f89c595c9 100644 --- a/cw_nano/lib/nano_wallet.dart +++ b/cw_nano/lib/nano_wallet.dart @@ -149,7 +149,7 @@ abstract class NanoWalletBase Future changePassword(String password) => throw UnimplementedError("changePassword"); @override - void close() { + Future close({required bool shouldCleanup}) async { _client.stop(); _receiveTimer?.cancel(); } diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock index ad1885d8b..dd955e344 100644 --- a/cw_nano/pubspec.lock +++ b/cw_nano/pubspec.lock @@ -117,10 +117,10 @@ packages: dependency: "direct overridden" description: name: build_runner_core - sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + sha256: "0671ad4162ed510b70d0eb4ad6354c249f8429cab4ae7a4cec86bbc2886eb76e" url: "https://pub.dev" source: hosted - version: "7.2.7" + version: "7.2.7+1" built_collection: dependency: transitive description: diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index 3c3078f2e..da082e833 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -179,7 +179,7 @@ abstract class SolanaWalletBase Future changePassword(String password) => throw UnimplementedError("changePassword"); @override - void close() { + Future close({required bool shouldCleanup}) async { _client.stop(); _transactionsUpdateTimer?.cancel(); } diff --git a/cw_tron/lib/tron_wallet.dart b/cw_tron/lib/tron_wallet.dart index 9012ef528..3cd8bfc99 100644 --- a/cw_tron/lib/tron_wallet.dart +++ b/cw_tron/lib/tron_wallet.dart @@ -217,7 +217,7 @@ abstract class TronWalletBase Future changePassword(String password) => throw UnimplementedError("changePassword"); @override - void close() => _transactionsUpdateTimer?.cancel(); + Future close({required bool shouldCleanup}) async => _transactionsUpdateTimer?.cancel(); @action @override diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index 02d968eb3..331957d67 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -160,7 +160,7 @@ abstract class WowneroWalletBase Future? updateBalance() => null; @override - void close() async { + Future close({required bool shouldCleanup}) async { _listener?.stop(); _onAccountChangeReaction?.reaction.dispose(); _onTxHistoryChangeReaction?.reaction.dispose(); diff --git a/cw_wownero/lib/wownero_wallet_service.dart b/cw_wownero/lib/wownero_wallet_service.dart index 286bfccd0..6f0fafc88 100644 --- a/cw_wownero/lib/wownero_wallet_service.dart +++ b/cw_wownero/lib/wownero_wallet_service.dart @@ -134,7 +134,7 @@ class WowneroWalletService extends WalletService< if (!isValid) { await restoreOrResetWalletFiles(name); - wallet.close(); + wallet.close(shouldCleanup: false); return openWallet(name, password); } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index e574aafc1..322ef6f86 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -7,7 +7,38 @@ PODS: - Flutter - ReachabilitySwift - CryptoSwift (1.8.2) + - 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): @@ -96,7 +127,7 @@ PODS: - FlutterMacOS - sp_scanner (0.0.1): - Flutter - - SwiftProtobuf (1.26.0) + - SwiftProtobuf (1.27.1) - SwiftyGif (5.4.5) - Toast (4.1.1) - uni_links (0.0.1): @@ -112,7 +143,9 @@ DEPENDENCIES: - barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`) - 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`) @@ -125,7 +158,6 @@ DEPENDENCIES: - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - in_app_review (from `.symlinks/plugins/in_app_review/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - - package_info (from `.symlinks/plugins/package_info/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) @@ -158,8 +190,12 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/barcode_scan2/ios" 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: @@ -184,8 +220,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/in_app_review/ios" integration_test: :path: ".symlinks/plugins/integration_test/ios" - package_info: - :path: ".symlinks/plugins/package_info/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: @@ -215,7 +249,9 @@ SPEC CHECKSUMS: barcode_scan2: 0af2bb63c81b4565aab6cd78278e4c0fa136dbb0 connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d CryptoSwift: c63a805d8bb5e5538e88af4e44bb537776af11ea + cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a cw_mweb: 87af74f9659fed0c1a2cbfb44413f1070e79e3ae + cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 devicelocale: b22617f40038496deffba44747101255cee005b0 @@ -243,7 +279,7 @@ SPEC CHECKSUMS: share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sp_scanner: eaa617fa827396b967116b7f1f43549ca62e9a12 - SwiftProtobuf: 5e8349171e7c2f88f5b9e683cb3cb79d1dc780b3 + SwiftProtobuf: b109bd17979d7993a84da14b1e1fdd8b0ded934a SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e uni_links: d97da20c7701486ba192624d99bffaaffcfc298a diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 10cc6434d..09c75feee 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -14,12 +14,12 @@ 2193F104374FA2746CE8945B /* ResourceHelper.swift in Resources */ = {isa = PBXBuildFile; fileRef = 78D25C60B94E9D9E48D52E5E /* ResourceHelper.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 495FEFF9B395392FED3425DE /* TaskProtocol.swift in Resources */ = {isa = PBXBuildFile; fileRef = 0F42D8065219E0653321EE2B /* TaskProtocol.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; - 4DFD1BB54A3A50573E19A583 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C663361C56EBB242598F609 /* Pods_Runner.framework */; }; 525A2200C6C2A43EDC5C8FC5 /* BreezSDKConnector.swift in Resources */ = {isa = PBXBuildFile; fileRef = 1FB06A93B13D606F06B3924D /* BreezSDKConnector.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; 6909E1D79C9986ADF2DE41E9 /* LnurlPayInvoice.swift in Resources */ = {isa = PBXBuildFile; fileRef = DCEA540E3586164FB47AD13E /* LnurlPayInvoice.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; 724FDA327BF191BC29DCAA2E /* Constants.swift in Resources */ = {isa = PBXBuildFile; fileRef = 0CCA7ADAD6FF9185EBBB2BCA /* Constants.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; 73138617307FA4F838D21D62 /* ServiceLogger.swift in Resources */ = {isa = PBXBuildFile; fileRef = F42258C3697CFE3C8C8D1933 /* ServiceLogger.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 8B1F4FCAA5EB9F3A83D32D5F /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D7CD6B6020744E8FA471915D /* Pods_Runner.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -50,6 +50,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 014D7E4DBCFD76DDE652A4D9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 0C400E0F25B21ABB0025E469 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; 0C44A7192518EF8000B570ED /* decrypt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = decrypt.swift; sourceTree = ""; }; 0C50DFB82BF3CB56002B0EB3 /* MoneroWallet.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = MoneroWallet.framework; sourceTree = ""; }; @@ -57,13 +58,11 @@ 0C9D68C8264854B60011B691 /* secRandom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = secRandom.swift; sourceTree = ""; }; 0CCA7ADAD6FF9185EBBB2BCA /* Constants.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Constants.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/Constants.swift"; sourceTree = ""; }; 0F42D8065219E0653321EE2B /* TaskProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TaskProtocol.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/TaskProtocol.swift"; sourceTree = ""; }; - 11F9FC13F9EE2A705B213FA9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 1F083F2041D1F553F2AF8B62 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 1FB06A93B13D606F06B3924D /* BreezSDKConnector.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BreezSDKConnector.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/BreezSDKConnector.swift"; sourceTree = ""; }; + 28F61114229803070973270D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3C663361C56EBB242598F609 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 41102141140E57B1DC27FBA1 /* SDKNotificationService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SDKNotificationService.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/SDKNotificationService.swift"; sourceTree = ""; }; 58C22CBD8C22B9D6023D59F8 /* LnurlPayInfo.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LnurlPayInfo.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/Task/LnurlPayInfo.swift"; sourceTree = ""; }; 5AFFEBFC279AD49C00F906A4 /* wakeLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = wakeLock.swift; sourceTree = ""; }; @@ -83,11 +82,12 @@ 9D2F2C9F2555316C95EE7EA3 /* RedeemSwap.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RedeemSwap.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/Task/RedeemSwap.swift"; sourceTree = ""; }; 9F46EE5D2BC11178009318F5 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; ABD6FCBB0F4244B090459128 /* BreezSDK.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BreezSDK.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/BreezSDK.swift"; sourceTree = ""; }; - AD0937B0140D5A4C24E73BEA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; B3D5E78267F5F18D882FDC3B /* ServiceConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ServiceConfig.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/ServiceConfig.swift"; sourceTree = ""; }; C58D93382C00FAC6004BCF69 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; }; CE291CFD2C15DB9A00B9F709 /* WowneroWallet.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = WowneroWallet.framework; sourceTree = ""; }; CEAFE49D2C539250009FF3AD /* Mwebd.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Mwebd.xcframework; sourceTree = ""; }; + D139E30AEB36740C21C00A9E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + D7CD6B6020744E8FA471915D /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DCEA540E3586164FB47AD13E /* LnurlPayInvoice.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LnurlPayInvoice.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/Task/LnurlPayInvoice.swift"; sourceTree = ""; }; F42258C3697CFE3C8C8D1933 /* ServiceLogger.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ServiceLogger.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/ServiceLogger.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -97,8 +97,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 4DFD1BB54A3A50573E19A583 /* Pods_Runner.framework in Frameworks */, CEAFE4A02C53926F009FF3AD /* libresolv.tbd in Frameworks */, + 8B1F4FCAA5EB9F3A83D32D5F /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -111,7 +111,7 @@ CEAFE49D2C539250009FF3AD /* Mwebd.xcframework */, C58D93382C00FAC6004BCF69 /* libresolv.tbd */, 0C9986A3251A932F00D566FD /* CryptoSwift.framework */, - 3C663361C56EBB242598F609 /* Pods_Runner.framework */, + D7CD6B6020744E8FA471915D /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -137,10 +137,10 @@ 84389F1A05D5860790D82820 /* Pods */ = { isa = PBXGroup; children = ( - 11F9FC13F9EE2A705B213FA9 /* Pods-Runner.debug.xcconfig */, - 1F083F2041D1F553F2AF8B62 /* Pods-Runner.release.xcconfig */, - AD0937B0140D5A4C24E73BEA /* Pods-Runner.profile.xcconfig */, 0B80439B9064C9708DDB0ADA /* breez_sdk-OnDemandResources */, + 014D7E4DBCFD76DDE652A4D9 /* Pods-Runner.debug.xcconfig */, + 28F61114229803070973270D /* Pods-Runner.release.xcconfig */, + D139E30AEB36740C21C00A9E /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -222,14 +222,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - B91154210ADCED81FBF06A85 /* [CP] Check Pods Manifest.lock */, + 11278EDF4D5DB437B3FDB787 /* [CP] Check Pods Manifest.lock */, CE5E8A222BEE19C700608EA1 /* CopyFiles */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 32D0076A9969C0C38D68AF62 /* [CP] Embed Pods Frameworks */, + F6F67323547956BC4F7B67F1 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -305,21 +305,26 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 32D0076A9969C0C38D68AF62 /* [CP] Embed Pods Frameworks */ = { + 11278EDF4D5DB437B3FDB787 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { @@ -353,26 +358,21 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; - B91154210ADCED81FBF06A85 /* [CP] Check Pods Manifest.lock */ = { + F6F67323547956BC4F7B67F1 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 2bed8a451..e9e83597a 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -262,7 +262,14 @@ class CWBitcoin extends Bitcoin { List getBitcoinReceivePageOptions() => BitcoinReceivePageOption.all; @override - List getLitecoinReceivePageOptions() => BitcoinReceivePageOption.allLitecoin; + List getLitecoinReceivePageOptions() { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + return BitcoinReceivePageOption.allLitecoin + .where((element) => element != BitcoinReceivePageOption.mweb) + .toList(); + } + return BitcoinReceivePageOption.allLitecoin; + } @override BitcoinAddressType getBitcoinAddressType(ReceivePageOption option) { @@ -608,7 +615,7 @@ class CWBitcoin extends Bitcoin { final updatedOutputs = outputs.map((output) { try { - final pendingOut = pendingTx!.outputs[outputs.indexOf(output)]; + final pendingOut = pendingTx.outputs[outputs.indexOf(output)]; final updatedOutput = output; updatedOutput.stealthAddress = P2trAddress.fromScriptPubkey(script: pendingOut.scriptPubKey) @@ -658,8 +665,8 @@ class CWBitcoin extends Bitcoin { String? getUnusedMwebAddress(Object wallet) { try { final electrumWallet = wallet as ElectrumWallet; - final walletAddresses = electrumWallet.walletAddresses as ElectrumWalletAddresses; - final mwebAddress = walletAddresses.mwebAddresses.firstWhere((element) => !element.isUsed); + final mwebAddress = + electrumWallet.walletAddresses.mwebAddresses.firstWhere((element) => !element.isUsed); return mwebAddress.address; } catch (_) { return null; diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index 08e045a0d..3b88c685a 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -6,7 +6,6 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/nft_listing_page.dart'; -import 'package:cake_wallet/src/screens/dashboard/widgets/action_button.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/home_screen_account_widget.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; @@ -20,12 +19,11 @@ 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/utils/feature_flag.dart'; -import 'package:cake_wallet/utils/payment_request.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/dashboard/nft_view_model.dart'; -import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -382,18 +380,10 @@ class CryptoBalanceWidget extends StatelessWidget { child: DashBoardRoundedCardWidget( customBorder: 30, title: S.of(context).litecoin_mweb, - subTitle: '', + subTitle: S.of(context).litecoin_mweb_description, hint: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - S.of(context).litecoin_mweb_description, - style: TextStyle( - color: Colors.white, - fontSize: 14, - ), - textAlign: TextAlign.center, - ), - SizedBox(height: 8), GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => launchUrl( @@ -401,53 +391,64 @@ class CryptoBalanceWidget extends StatelessWidget { "https://guides.cakewallet.com/docs/cryptos/litecoin/#mweb"), mode: LaunchMode.externalApplication, ), - child: Center( - 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, + 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: 24), + SizedBox(height: 8), Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - ElevatedButton( - onPressed: () => _dismissMweb(context), - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).primaryColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), + 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), ), ), - child: Text( - S.of(context).litecoin_mweb_dismiss, - style: TextStyle(color: Colors.white), - ), ), - ElevatedButton( - onPressed: () => _enableMweb(context), - child: Text(S.of(context).litecoin_enable_mweb_sync), + 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, + ), + ), ), ], ), ], ), onTap: () => {}, - icon: ImageIcon( - AssetImage('assets/images/mweb_logo.png'), - color: - Theme.of(context).extension()!.pageTitleTextColor, - size: 50, + icon: Container( + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: ImageIcon( + AssetImage('assets/images/mweb_logo.png'), + color: Color.fromARGB(255, 11, 70, 129), + size: 40, + ), ), ), ), @@ -505,7 +506,7 @@ class CryptoBalanceWidget extends StatelessWidget { }, )); } - dashboardViewModel.setMwebScanningActive(); + dashboardViewModel.setMwebEnabled(); } Future _dismissMweb(BuildContext context) async { @@ -850,23 +851,15 @@ class BalanceRowWidget extends StatelessWidget { padding: EdgeInsets.only(right: 16, top: 16), child: Column( children: [ - CakeImageWidget( - imageUrl: 'assets/images/mweb_logo.png', - height: 40, - width: 40, - displayOnError: Container( - height: 30.0, - width: 30.0, - child: Center( - child: Text( - currency.title.substring(0, min(currency.title.length, 2)), - style: TextStyle(fontSize: 11), - ), - ), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.grey.shade400, - ), + Container( + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: ImageIcon( + AssetImage('assets/images/mweb_logo.png'), + color: Color.fromARGB(255, 11, 70, 129), + size: 40, ), ), const SizedBox(height: 10), diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index a59bfd2fa..6d8379b29 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -97,14 +97,13 @@ class MenuWidgetState extends State { @override Widget build(BuildContext context) { - List items = SettingActions.all; + List items = List.of(SettingActions.all); if (!widget.dashboardViewModel.hasSilentPayments) { items.removeWhere((element) => element.name(context) == S.of(context).silent_payments_settings); } - // if (!widget.dashboardViewModel.hasMweb) { - // itemCount--; - // items.removeWhere((element) => element.name(context) == S.of(context).litecoin_mweb_settings); - // } + if (!widget.dashboardViewModel.hasMweb) { + items.removeWhere((element) => element.name(context) == S.of(context).litecoin_mweb_settings); + } int itemCount = items.length; moneroIcon = Image.asset('assets/images/monero_menu.png', @@ -191,11 +190,6 @@ class MenuWidgetState extends State { final item = items[index]; - if (!widget.dashboardViewModel.hasMweb && - item.name(context) == S.of(context).litecoin_mweb_settings) { - return const SizedBox(); - } - final isLastTile = index == itemCount - 1; return SettingActionButton( diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index 2acf6138e..6b62435d0 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -135,10 +135,6 @@ class RootState extends State with WidgetsBindingObserver { setState(() => _setInactive(true)); } - if (widget.appStore.wallet?.type == WalletType.litecoin) { - widget.appStore.wallet?.stopSync(); - } - break; case AppLifecycleState.resumed: widget.authService.requireAuth().then((value) { @@ -148,9 +144,6 @@ class RootState extends State with WidgetsBindingObserver { }); } }); - if (widget.appStore.wallet?.type == WalletType.litecoin) { - widget.appStore.wallet?.startSync(); - } break; default: break; diff --git a/lib/src/screens/settings/mweb_settings.dart b/lib/src/screens/settings/mweb_settings.dart index 88dc00f7c..722ffa9aa 100644 --- a/lib/src/screens/settings/mweb_settings.dart +++ b/lib/src/screens/settings/mweb_settings.dart @@ -4,7 +4,6 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; import 'package:cake_wallet/view_model/settings/mweb_settings_view_model.dart'; -import 'package:cake_wallet/view_model/settings/silent_payments_settings_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -33,9 +32,9 @@ class MwebSettingsPage extends BasePage { ), SettingsSwitcherCell( title: S.current.litecoin_mweb_always_scan, - value: _mwebSettingsViewModel.mwebAlwaysScan, + value: _mwebSettingsViewModel.mwebEnabled, onValueChange: (_, bool value) { - _mwebSettingsViewModel.setMwebAlwaysScan(value); + _mwebSettingsViewModel.setMwebEnabled(value); }, ), SettingsCellWithArrow( diff --git a/lib/src/widgets/setting_actions.dart b/lib/src/widgets/setting_actions.dart index 80d07962f..b9af97f32 100644 --- a/lib/src/widgets/setting_actions.dart +++ b/lib/src/widgets/setting_actions.dart @@ -31,7 +31,6 @@ class SettingActions { walletSettingAction, addressBookSettingAction, silentPaymentsSettingAction, - litecoinMwebSettingAction, securityBackupSettingAction, privacySettingAction, displaySettingAction, @@ -50,7 +49,7 @@ class SettingActions { static SettingActions litecoinMwebSettingAction = SettingActions._( name: (context) => S.of(context).litecoin_mweb_settings, - image: 'assets/images/bitcoin_menu.png', + image: 'assets/images/litecoin_menu.png', onTap: (BuildContext context) { Navigator.pop(context); Navigator.of(context).pushNamed(Routes.mwebSettings); diff --git a/lib/store/app_store.dart b/lib/store/app_store.dart index 7d61abfc5..cd8881633 100644 --- a/lib/store/app_store.dart +++ b/lib/store/app_store.dart @@ -37,7 +37,8 @@ abstract class AppStoreBase with Store { @action Future changeCurrentWallet( WalletBase, TransactionInfo> wallet) async { - this.wallet?.close(); + bool changingToSameWalletType = this.wallet?.type == wallet.type; + this.wallet?.close(shouldCleanup: !changingToSameWalletType); this.wallet = wallet; this.wallet!.setExceptionHandler(ExceptionHandler.onError); diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 083c31302..56a5935c9 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/sort_balance_types.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; @@ -31,6 +32,7 @@ class BalanceRecord { required this.fiatSecondAdditionalBalance, required this.asset, required this.formattedAssetTitle}); + final String fiatAdditionalBalance; final String fiatAvailableBalance; final String fiatFrozenBalance; @@ -53,7 +55,22 @@ abstract class BalanceViewModelBase with Store { : isReversing = false, isShowCard = appStore.wallet!.walletInfo.isShowIntroCakePayCard, wallet = appStore.wallet! { - reaction((_) => appStore.wallet, _onWalletChange); + reaction((_) => appStore.wallet, (wallet) { + _onWalletChange(wallet); + _checkMweb(); + }); + + _checkMweb(); + + reaction((_) => settingsStore.mwebAlwaysScan, (bool value) { + _checkMweb(); + }); + } + + void _checkMweb() { + if (wallet.type == WalletType.litecoin) { + mwebEnabled = bitcoin!.getMwebEnabled(wallet); + } } final AppStore appStore; @@ -336,14 +353,19 @@ abstract class BalanceViewModelBase with Store { }); } + @observable + bool mwebEnabled = false; + @computed bool get hasAdditionalBalance => _hasAdditionalBalanceForWalletType(wallet.type); @computed - bool get hasSecondAdditionalBalance => _hasSecondAdditionalBalanceForWalletType(wallet.type); + bool get hasSecondAdditionalBalance => + mwebEnabled && _hasSecondAdditionalBalanceForWalletType(wallet.type); @computed - bool get hasSecondAvailableBalance => _hasSecondAvailableBalanceForWalletType(wallet.type); + bool get hasSecondAvailableBalance => + mwebEnabled && _hasSecondAvailableBalanceForWalletType(wallet.type); bool _hasAdditionalBalanceForWalletType(WalletType type) { switch (type) { @@ -358,15 +380,16 @@ abstract class BalanceViewModelBase with Store { } bool _hasSecondAdditionalBalanceForWalletType(WalletType type) { - if (wallet.type == WalletType.litecoin && settingsStore.mwebAlwaysScan) { - // if ((wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) > 0) - return true; + if (wallet.type == WalletType.litecoin) { + if ((wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) > 0) { + return true; + } } return false; } bool _hasSecondAvailableBalanceForWalletType(WalletType type) { - if (wallet.type == WalletType.litecoin && settingsStore.mwebAlwaysScan) { + if (wallet.type == WalletType.litecoin) { return true; } return false; diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 77c60bd63..26bc5ceb9 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -1,4 +1,6 @@ +import 'dart:async'; import 'dart:convert'; +import 'dart:io' show Platform; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/bitcoin/bitcoin.dart'; @@ -222,7 +224,10 @@ abstract class DashboardViewModelBase with Store { // subname = nano!.getCurrentAccount(_wallet).label; // } - reaction((_) => appStore.wallet, _onWalletChange); + reaction((_) => appStore.wallet, (wallet) { + _onWalletChange(wallet); + _checkMweb(); + }); connectMapToListWithTransform( appStore.wallet!.transactionHistory.transactions, @@ -256,16 +261,16 @@ abstract class DashboardViewModelBase with Store { }); } + _checkMweb(); + reaction((_) => settingsStore.mwebAlwaysScan, (bool value) { + _checkMweb(); + }); + } + + void _checkMweb() { if (hasMweb) { - mwebScanningActive = bitcoin!.getMwebEnabled(wallet); - settingsStore.mwebEnabled = mwebScanningActive; - reaction((_) => settingsStore.mwebAlwaysScan, (bool alwaysScan) { - if (alwaysScan) { - mwebScanningActive = true; - } else { - mwebScanningActive = false; - } - }); + mwebEnabled = bitcoin!.getMwebEnabled(wallet); + balanceViewModel.mwebEnabled = mwebEnabled; } } @@ -430,34 +435,36 @@ abstract class DashboardViewModelBase with Store { } @computed - bool get hasMweb => wallet.type == WalletType.litecoin; + bool get hasMweb => wallet.type == WalletType.litecoin && (Platform.isIOS || Platform.isAndroid); @computed - bool get showMwebCard => hasMweb && settingsStore.mwebCardDisplay && !mwebScanningActive; + bool get showMwebCard => hasMweb && settingsStore.mwebCardDisplay && !mwebEnabled; @observable - bool mwebScanningActive = false; + bool mwebEnabled = false; @computed bool get hasEnabledMwebBefore => settingsStore.hasEnabledMwebBefore; @action - void setMwebScanningActive() { + void setMwebEnabled() { if (!hasMweb) { return; } settingsStore.hasEnabledMwebBefore = true; - mwebScanningActive = true; + mwebEnabled = true; bitcoin!.setMwebEnabled(wallet, true); + balanceViewModel.mwebEnabled = true; settingsStore.mwebAlwaysScan = true; } @action void dismissMweb() { settingsStore.mwebCardDisplay = false; + balanceViewModel.mwebEnabled = false; settingsStore.mwebAlwaysScan = false; - mwebScanningActive = false; + mwebEnabled = false; bitcoin!.setMwebEnabled(wallet, false); } diff --git a/lib/view_model/settings/mweb_settings_view_model.dart b/lib/view_model/settings/mweb_settings_view_model.dart index 343947d00..c6370e23f 100644 --- a/lib/view_model/settings/mweb_settings_view_model.dart +++ b/lib/view_model/settings/mweb_settings_view_model.dart @@ -8,7 +8,10 @@ part 'mweb_settings_view_model.g.dart'; class MwebSettingsViewModel = MwebSettingsViewModelBase with _$MwebSettingsViewModel; abstract class MwebSettingsViewModelBase with Store { - MwebSettingsViewModelBase(this._settingsStore, this._wallet); + MwebSettingsViewModelBase(this._settingsStore, this._wallet) { + mwebEnabled = bitcoin!.getMwebEnabled(_wallet); + _settingsStore.mwebAlwaysScan = mwebEnabled; + } final SettingsStore _settingsStore; final WalletBase _wallet; @@ -16,8 +19,8 @@ abstract class MwebSettingsViewModelBase with Store { @computed bool get mwebCardDisplay => _settingsStore.mwebCardDisplay; - @computed - bool get mwebAlwaysScan => _settingsStore.mwebAlwaysScan; + @observable + late bool mwebEnabled; @action void setMwebCardDisplay(bool value) { @@ -25,8 +28,9 @@ abstract class MwebSettingsViewModelBase with Store { } @action - void setMwebAlwaysScan(bool value) { - _settingsStore.mwebAlwaysScan = value; + void setMwebEnabled(bool value) { + mwebEnabled = value; bitcoin!.setMwebEnabled(_wallet, value); + _settingsStore.mwebAlwaysScan = value; } } diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 13c1a5661..f31f879a1 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -188,36 +188,38 @@ abstract class WalletCreationVMBase with Store { } } - Future> getDerivationInfoFromQRCredentials(RestoredWallet restoreWallet) async { - var list = []; - final walletType = restoreWallet.type; - var appStore = getIt.get(); - var node = appStore.settingsStore.getCurrentNode(walletType); + Future> getDerivationInfoFromQRCredentials( + RestoredWallet restoreWallet) async { + var list = []; + final walletType = restoreWallet.type; + var appStore = getIt.get(); + var node = appStore.settingsStore.getCurrentNode(walletType); - switch (walletType) { - case WalletType.bitcoin: - case WalletType.litecoin: - return bitcoin!.getDerivationsFromMnemonic( - mnemonic: restoreWallet.mnemonicSeed!, - node: node, - passphrase: restoreWallet.passphrase, - ); - case WalletType.nano: - return nanoUtil!.getDerivationsFromMnemonic( - mnemonic: restoreWallet.mnemonicSeed!, - node: node, - ); - default: - break; - } - return list; + switch (walletType) { + case WalletType.bitcoin: + case WalletType.litecoin: + return bitcoin!.getDerivationsFromMnemonic( + mnemonic: restoreWallet.mnemonicSeed!, + node: node, + passphrase: restoreWallet.passphrase, + ); + case WalletType.nano: + return nanoUtil!.getDerivationsFromMnemonic( + mnemonic: restoreWallet.mnemonicSeed!, + node: node, + ); + default: + break; } + return list; + } WalletCredentials getCredentials(dynamic options) => throw UnimplementedError(); Future process(WalletCredentials credentials) => throw UnimplementedError(); - Future getWalletCredentialsFromQRCredentials(RestoredWallet restoreWallet) async => + Future getWalletCredentialsFromQRCredentials( + RestoredWallet restoreWallet) async => throw UnimplementedError(); Future processFromRestoredWallet( 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 4a4fdcb19..0ae6b75a5 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -70,6 +70,8 @@ abstract class WalletListViewModelBase with Store { @action Future loadWallet(WalletListItem walletItem) async { + // bool switchingToSameWalletType = walletItem.type == _appStore.wallet?.type; + // await _appStore.wallet?.close(shouldCleanup: !switchingToSameWalletType); final wallet = await _walletLoadingService.load(walletItem.type, walletItem.name); await _appStore.changeCurrentWallet(wallet); } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 8951a2dd1..d6199d028 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -2,6 +2,8 @@ PODS: - connectivity_plus (0.0.1): - FlutterMacOS - ReachabilitySwift + - cw_mweb (0.0.1): + - FlutterMacOS - device_info_plus (0.0.1): - FlutterMacOS - devicelocale (0.0.1): @@ -37,6 +39,7 @@ PODS: DEPENDENCIES: - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) + - cw_mweb (from `Flutter/ephemeral/.symlinks/plugins/cw_mweb/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - devicelocale (from `Flutter/ephemeral/.symlinks/plugins/devicelocale/macos`) - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`) @@ -60,6 +63,8 @@ SPEC REPOS: EXTERNAL SOURCES: connectivity_plus: :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos + cw_mweb: + :path: Flutter/ephemeral/.symlinks/plugins/cw_mweb/macos device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos devicelocale: @@ -91,6 +96,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 + cw_mweb: 7440b12ead811dda972a9918442ea2a458e8742c device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f devicelocale: 9f0f36ac651cabae2c33f32dcff4f32b61c38225 flutter_inappwebview_macos: 9600c9df9fdb346aaa8933812009f8d94304203d @@ -105,7 +111,7 @@ SPEC CHECKSUMS: share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sp_scanner: 269d96e0ec3173e69156be7239b95182be3b8303 - url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 PODFILE CHECKSUM: 65ec1541137fb5b35d00490dec1bb48d4d9586bb diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index d55faad76..3797334e9 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -235,6 +235,7 @@ "edit_token": "تحرير الرمز المميز", "electrum_address_disclaimer": "نقوم بإنشاء عناوين جديدة في كل مرة تستخدم فيها عنوانًا ، لكن العناوين السابقة تستمر في العمل", "email_address": "عنوان البريد الالكترونى", + "enable": "يُمكَِن", "enable_mempool_api": "MEMPOOL API للحصول على رسوم وتواريخ دقيقة", "enable_replace_by_fee": "تمكين الاستبدال", "enable_silent_payments_scanning": "ابدأ في مسح المدفوعات الصامتة ، حتى يتم الوصول إلى الطرف", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index dd5c9a260..1d674530b 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -235,6 +235,7 @@ "edit_token": "Редактиране на токена", "electrum_address_disclaimer": "Нови адреси се генерират всеки път, когато използвате този, но и предишните продължават да работят", "email_address": "Имейл адрес", + "enable": "Активиране", "enable_mempool_api": "Mempool API за точни такси и дати", "enable_replace_by_fee": "Активиране на замяна по забрана", "enable_silent_payments_scanning": "Започнете да сканирате безшумните плащания, докато се достигне съветът", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 65ef50a71..b97c1e9f8 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -235,6 +235,7 @@ "edit_token": "Upravit token", "electrum_address_disclaimer": "Po každém použití je generována nová adresa, ale předchozí adresy také stále fungují", "email_address": "E-mailová adresa", + "enable": "Umožnit", "enable_mempool_api": "Mempool API pro přesné poplatky a data", "enable_replace_by_fee": "Povolit výměnu podle poplatku", "enable_silent_payments_scanning": "Začněte skenovat tiché platby, dokud není dosaženo špičky", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 194b1d05b..80c33a3b7 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -235,6 +235,7 @@ "edit_token": "Token bearbeiten", "electrum_address_disclaimer": "Wir generieren jedes Mal neue Adressen, wenn Sie eine verwenden, aber vorherige Adressen funktionieren weiterhin", "email_address": "E-Mail-Adresse", + "enable": "Aktivieren", "enable_mempool_api": "Mempool -API für genaue Gebühren und Daten", "enable_replace_by_fee": "Aktivieren Sie Ersatz für Fee", "enable_silent_payments_scanning": "Scannen Sie stille Zahlungen, bis die Spitze erreicht ist", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index c79719641..b1547be88 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -235,6 +235,7 @@ "edit_token": "Edit token", "electrum_address_disclaimer": "We generate new addresses each time you use one, but previous addresses continue to work", "email_address": "Email Address", + "enable": "Enable", "enable_mempool_api": "Mempool API for accurate fees and dates", "enable_replace_by_fee": "Enable Replace-By-Fee", "enable_silent_payments_scanning": "Start scanning for transactions sent to your Silent Payment address.", @@ -940,4 +941,4 @@ "you_will_get": "Convert to", "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 ced8d9a13..c4e6bb21f 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -235,6 +235,7 @@ "edit_token": "Editar token", "electrum_address_disclaimer": "Generamos nuevas direcciones cada vez que usa una, pero las direcciones anteriores siguen funcionando", "email_address": "Dirección de correo electrónico", + "enable": "Permitir", "enable_mempool_api": "API de Mempool para tarifas y fechas precisas", "enable_replace_by_fee": "Habilitar reemplazar por tarea", "enable_silent_payments_scanning": "Comience a escanear pagos silenciosos, hasta que se alcance la punta", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 1f8567b31..8508dda99 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -235,6 +235,7 @@ "edit_token": "Modifier le token", "electrum_address_disclaimer": "Nous générons de nouvelles adresses à chaque fois que vous en utilisez une, mais les adresses précédentes continuent à fonctionner", "email_address": "Adresse e-mail", + "enable": "Activer", "enable_mempool_api": "API Mempool pour les frais et dates précis", "enable_replace_by_fee": "Activer Remplace-by-Fee", "enable_silent_payments_scanning": "Commencez à scanner les paiements silencieux, jusqu'à ce que la pointe soit atteinte", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 1e5b5d8e3..b25cc14f5 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -235,6 +235,7 @@ "edit_token": "Gyara alamar", "electrum_address_disclaimer": "Muna samar da sababbin adireshi duk lokacin da kuka yi amfani da ɗaya, amma adiresoshin da suka gabata suna ci gaba da aiki", "email_address": "Adireshin i-mel", + "enable": "Ba dama", "enable_mempool_api": "Mampool API don ingantattun kudade da kwanakin", "enable_replace_by_fee": "Ba da damar maye gurbin-by-kudin", "enable_silent_payments_scanning": "Fara bincika biya na shiru, har sai tip ɗin ya kai", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 07d8311ef..c00184218 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -235,6 +235,7 @@ "edit_token": "टोकन संपादित करें", "electrum_address_disclaimer": "हर बार जब आप एक का उपयोग करते हैं तो हम नए पते उत्पन्न करते हैं, लेकिन पिछले पते काम करना जारी रखते हैं", "email_address": "ईमेल पता", + "enable": "सक्षम", "enable_mempool_api": "सटीक शुल्क और तिथियों के लिए मेमपूल एपीआई", "enable_replace_by_fee": "प्रतिस्थापित-दर-शुल्क सक्षम करें", "enable_silent_payments_scanning": "मूक भुगतान स्कैनिंग सक्षम करें", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 7969716de..030d6f49b 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -235,6 +235,7 @@ "edit_token": "Uredi token", "electrum_address_disclaimer": "Minden egyes alkalommal új címeket generálunk, de a korábbi címek továbbra is működnek", "email_address": "Adresa e-pošte", + "enable": "Omogućiti", "enable_mempool_api": "Mempool API za točne naknade i datume", "enable_replace_by_fee": "Omogući zamjenu", "enable_silent_payments_scanning": "Započnite skeniranje tihih plaćanja, dok se ne postigne savjet", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index aa58edd64..1869001a7 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -235,6 +235,7 @@ "edit_token": "Փոփոխել տոկեն", "electrum_address_disclaimer": "Մենք ստեղծում ենք նոր հասցե ամեն անգամ, երբ դուք օգտագործում եք այն, բայց նախորդ հասցեները շարունակում են աշխատել", "email_address": "Էլ. փոստի հասցե", + "enable": "Միացնել", "enable_mempool_api": "Mempool API ճշգրիտ վճարների եւ ամսաթվերի համար", "enable_replace_by_fee": "Միացնել փոխարինումը միջնորդավճարով", "enable_silent_payments_scanning": "Միացնել Լուռ Վճարումների սկանավորումը", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 867040df5..b215fa148 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -235,6 +235,7 @@ "edit_token": "Mengedit token", "electrum_address_disclaimer": "Kami menghasilkan alamat baru setiap kali Anda menggunakan satu, tetapi alamat sebelumnya tetap berfungsi", "email_address": "Alamat Email", + "enable": "Memungkinkan", "enable_mempool_api": "API Mempool untuk biaya dan tanggal yang akurat", "enable_replace_by_fee": "Aktifkan ganti-by-fee", "enable_silent_payments_scanning": "Mulailah memindai pembayaran diam, sampai ujung tercapai", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 3b20d0410..14963f58b 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -236,6 +236,7 @@ "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", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 0449a8749..af61ab1f9 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -235,6 +235,7 @@ "edit_token": "トークンの編集", "electrum_address_disclaimer": "使用するたびに新しいアドレスが生成されますが、以前のアドレスは引き続き機能します", "email_address": "メールアドレス", + "enable": "有効にする", "enable_mempool_api": "正確な料金と日付のMempool API", "enable_replace_by_fee": "交換ごとに有効にします", "enable_silent_payments_scanning": "先端に達するまで、サイレント決済のスキャンを開始します", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index ccda2dbbc..dd19af792 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -235,6 +235,7 @@ "edit_token": "토큰 편집", "electrum_address_disclaimer": "사용할 때마다 새 주소가 생성되지만 이전 주소는 계속 작동합니다.", "email_address": "이메일 주소", + "enable": "할 수 있게 하다", "enable_mempool_api": "정확한 수수료 및 날짜에 대한 Mempool API", "enable_replace_by_fee": "대체별로 활성화하십시오", "enable_silent_payments_scanning": "팁에 도달 할 때까지 사일런트 지불을 스캔하기 시작합니다.", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index b2c6386f0..8ce050339 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -235,6 +235,7 @@ "edit_token": "တိုကင်ကို တည်းဖြတ်ပါ။", "electrum_address_disclaimer": "သင်အသုံးပြုသည့်အချိန်တိုင်းတွင် ကျွန်ုပ်တို့သည် လိပ်စာအသစ်များကို ထုတ်ပေးသော်လည်း ယခင်လိပ်စာများသည် ဆက်လက်အလုပ်လုပ်နေပါသည်။", "email_address": "အီးမေးလ်လိပ်စာ", + "enable": "စွမ်းဆောင်နိုင်စေ", "enable_mempool_api": "Mempool API တိကျသောအခကြေးငွေနှင့်ရက်စွဲများအတွက်", "enable_replace_by_fee": "အစားထိုး - by- အခကြေးငွေ enable", "enable_silent_payments_scanning": "အစွန်အဖျားသို့ရောက်ရှိသည်အထိအသံတိတ်ငွေပေးချေမှုကိုစကင်ဖတ်စစ်ဆေးပါ", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 12315cd3c..6b1dd29c5 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -235,6 +235,7 @@ "edit_token": "Token bewerken", "electrum_address_disclaimer": "We genereren nieuwe adressen elke keer dat u er een gebruikt, maar eerdere adressen blijven werken", "email_address": "E-mailadres", + "enable": "Inschakelen", "enable_mempool_api": "Mempool API voor nauwkeurige kosten en datums", "enable_replace_by_fee": "Schakel vervangen door een fee", "enable_silent_payments_scanning": "Begin met het scannen van stille betalingen, totdat de tip is bereikt", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index accb31d46..4bf323c1c 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -235,6 +235,7 @@ "edit_token": "Edytuj token", "electrum_address_disclaimer": "Za każdym razem, gdy wykorzystasz adres, dla wiekszej prywatności generujemy nowy, ale poprzednie adresy nadal działają, i moga odbierać środki", "email_address": "Adres e-mail", + "enable": "Włączać", "enable_mempool_api": "Mempool API dla dokładnych opłat i dat", "enable_replace_by_fee": "Włącz wymianę po lewej", "enable_silent_payments_scanning": "Zacznij skanować ciche płatności, aż do osiągnięcia wskazówki", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 294a6bf1b..603dd75b7 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -235,6 +235,7 @@ "edit_token": "Editar símbolo", "electrum_address_disclaimer": "Geramos novos endereços cada vez que você usa um, mas os endereços anteriores continuam funcionando", "email_address": "Endereço de e-mail", + "enable": "Habilitar", "enable_mempool_api": "Mempool API para taxas e datas precisas", "enable_replace_by_fee": "Habilite substituir por taxa", "enable_silent_payments_scanning": "Comece a escanear pagamentos silenciosos, até que o topo seja alcançada", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 1c3a23d67..43d912ffc 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -235,6 +235,7 @@ "edit_token": "Изменить токен", "electrum_address_disclaimer": "Мы генерируем новые адреса каждый раз, когда вы их используете, но предыдущие адреса продолжают работать.", "email_address": "Адрес электронной почты", + "enable": "Давать возможность", "enable_mempool_api": "Mempool API за точные сборы и даты", "enable_replace_by_fee": "Включить замену за пикой", "enable_silent_payments_scanning": "Начните сканировать безмолвные платежи, пока не будет достигнут наконечник", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index d1328257d..c02c7cc33 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -235,6 +235,7 @@ "edit_token": "แก้ไขโทเค็น", "electrum_address_disclaimer": "เราสร้างที่อยู่ใหม่ทุกครั้งที่คุณใช้หนึ่งอย่าง แต่ที่อยู่เก่ายังสามารถใช้ได้ต่อไป", "email_address": "ที่อยู่อีเมล", + "enable": "เปิดใช้งาน", "enable_mempool_api": "Mempool API สำหรับค่าธรรมเนียมและวันที่ที่ถูกต้อง", "enable_replace_by_fee": "เปิดใช้งานการเปลี่ยนโดยค่าธรรมเนียม", "enable_silent_payments_scanning": "เริ่มสแกนการชำระเงินแบบเงียบจนกว่าจะถึงปลาย", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index ca3a48478..99d22bb1e 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -235,6 +235,7 @@ "edit_token": "I-edit ang token", "electrum_address_disclaimer": "Bumubuo kami ng mga bagong address sa tuwing gagamit ka ng isa, ngunit ang mga nakaraang address ay patuloy na gumagana", "email_address": "Email Address", + "enable": "Paganahin", "enable_mempool_api": "Mempool API para sa tumpak na bayad at mga petsa", "enable_replace_by_fee": "Paganahin ang Replace-By-Fee", "enable_silent_payments_scanning": "Simulan ang pag -scan ng tahimik na pagbabayad, hanggang sa maabot ang tip", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index dac8c1c81..86239cd77 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -235,6 +235,7 @@ "edit_token": "Belirteci düzenle", "electrum_address_disclaimer": "Adresini her kullandığında yeni adres oluşturuyoruz, ancak önceki adresler de çalışmaya devam eder", "email_address": "E-posta Adresi", + "enable": "Olanak vermek", "enable_mempool_api": "Doğru ücretler ve tarihler için Mempool API'si", "enable_replace_by_fee": "Farklı Değiştir'i Etkinleştir", "enable_silent_payments_scanning": "Bahşiş ulaşılıncaya kadar sessiz ödemeleri taramaya başlayın", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index ba6c6516d..2d049e12e 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -235,6 +235,7 @@ "edit_token": "Редагувати маркер", "electrum_address_disclaimer": "Ми створюємо нові адреси щоразу, коли ви використовуєте їх, але попередні адреси продовжують працювати", "email_address": "Адреса електронної пошти", + "enable": "Ввімкнути", "enable_mempool_api": "API Mempool для точних зборів та дат", "enable_replace_by_fee": "Увімкнути заміну з комісією", "enable_silent_payments_scanning": "Почніть сканувати мовчазні платежі, поки не буде досягнуто наконечника", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 3e68a5fb4..f080c23a5 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -235,6 +235,7 @@ "edit_token": "ٹوکن میں ترمیم کریں۔", "electrum_address_disclaimer": "جب بھی آپ ایک کا استعمال کرتے ہیں تو ہم نئے پتے تیار کرتے ہیں، لیکن پچھلے پتے کام کرتے رہتے ہیں۔", "email_address": "ای میل اڈریس", + "enable": "قابل بنائیں", "enable_mempool_api": "درست فیسوں اور تاریخوں کے لئے میمپول API", "enable_replace_by_fee": "فی فیس کو تبدیل کریں", "enable_silent_payments_scanning": "خاموش ادائیگیوں کو اسکین کرنا شروع کریں ، جب تک کہ نوک نہ پہنچ جائے", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 159a127d0..63db7ce88 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -236,6 +236,7 @@ "edit_token": "Chỉnh sửa token", "electrum_address_disclaimer": "Chúng tôi tạo địa chỉ mới mỗi khi bạn sử dụng, nhưng các địa chỉ cũ vẫn tiếp tục hoạt động", "email_address": "Địa chỉ Email", + "enable": "Cho phép", "enable_mempool_api": "API Mempool cho các khoản phí và ngày chính xác", "enable_replace_by_fee": "Bật Thay thế Bằng Phí", "enable_silent_payments_scanning": "Bật quét thanh toán im lặng", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index a12774d5d..16a887a39 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -236,6 +236,7 @@ "edit_token": "Ṣatunkọ àmi", "electrum_address_disclaimer": "A dá àwọn àdírẹ́sì títun ní gbogbo àwọn ìgbà t'ẹ́ lo ó kan ṣùgbọ́n ẹ lè tẹ̀síwájú lo àwọn àdírẹ́sì tẹ́lẹ̀tẹ́lẹ̀.", "email_address": "Àdírẹ́sì ímeèlì", + "enable": "Mu ṣiṣẹ", "enable_mempool_api": "Mempool API fun awọn owo deede ati awọn ọjọ", "enable_replace_by_fee": "Mu ki o rọpo", "enable_silent_payments_scanning": "Bẹrẹ awọn sisanwo ipalọlọ, titi ti o fi de opin", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 8f8e8cd13..9398e2408 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -235,6 +235,7 @@ "edit_token": "编辑令牌", "electrum_address_disclaimer": "每次您使用一个地址时,我们都会生成新地址,但之前的地址仍然有效", "email_address": "电子邮件地址", + "enable": "使能够", "enable_mempool_api": "Mempool API获得准确的费用和日期", "enable_replace_by_fee": "启用by-Fee替换", "enable_silent_payments_scanning": "开始扫描无声付款,直到达到提示", diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index a60077e82..428891b2c 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.16.5" -MONERO_COM_BUILD_NUMBER=100 +MONERO_COM_VERSION="1.17.0" +MONERO_COM_BUILD_NUMBER=102 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.19.5" -CAKEWALLET_BUILD_NUMBER=227 +CAKEWALLET_VERSION="4.20.0" +CAKEWALLET_BUILD_NUMBER=230 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/android/build_mwebd.sh b/scripts/android/build_mwebd.sh index 90dbc4c20..4434e30f1 100755 --- a/scripts/android/build_mwebd.sh +++ b/scripts/android/build_mwebd.sh @@ -13,7 +13,7 @@ fi # build mwebd: git clone https://github.com/ltcmweb/mwebd cd mwebd -git reset --hard f6ea8a9e3d348b01bb44f03a1cc4ad65b0abe935 +git reset --hard 555349415f76a42ec5c76152b64c4ab9aabc448f gomobile bind -target=android -androidapi 21 . mkdir -p ../../../cw_mweb/android/libs/ mv ./mwebd.aar $_ diff --git a/scripts/ios/app_config.sh b/scripts/ios/app_config.sh index 67375c914..2232491a6 100755 --- a/scripts/ios/app_config.sh +++ b/scripts/ios/app_config.sh @@ -22,7 +22,7 @@ cp -rf ./ios/Runner/InfoBase.plist ./ios/Runner/Info.plist /usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:1:CFBundleURLName string ${APP_IOS_TYPE}" ./ios/Runner/Info.plist /usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:1:CFBundleURLSchemes array" ./ios/Runner/Info.plist /usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:1:CFBundleURLSchemes: string ${APP_IOS_TYPE}" ./ios/Runner/Info.plist -sed -i '' "s/\${PRODUCT_BUNDLE_IDENTIFIER}/${APP_IOS_BUNDLE_ID}/g" ./ios/Runner.xcodeproj/project.pbxproj +sed -i '' "s/PRODUCT_BUNDLE_IDENTIFIER = .*;/PRODUCT_BUNDLE_IDENTIFIER = $APP_IOS_BUNDLE_ID;/g" ./ios/Runner.xcodeproj/project.pbxproj CONFIG_ARGS="" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 9df4b685e..d976f07fd 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.16.5" -MONERO_COM_BUILD_NUMBER=98 +MONERO_COM_VERSION="1.17.0" +MONERO_COM_BUILD_NUMBER=100 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.19.5" -CAKEWALLET_BUILD_NUMBER=266 +CAKEWALLET_VERSION="4.20.0" +CAKEWALLET_BUILD_NUMBER=269 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/ios/build_mwebd.sh b/scripts/ios/build_mwebd.sh index f0fa64605..e13c4931c 100755 --- a/scripts/ios/build_mwebd.sh +++ b/scripts/ios/build_mwebd.sh @@ -12,7 +12,7 @@ fi # build mwebd: git clone https://github.com/ltcmweb/mwebd cd mwebd -git reset --hard f6ea8a9e3d348b01bb44f03a1cc4ad65b0abe935 +git reset --hard 555349415f76a42ec5c76152b64c4ab9aabc448f gomobile bind -target=ios . mv -fn ./Mwebd.xcframework ../../../ios/ # cleanup: diff --git a/scripts/linux/app_env.sh b/scripts/linux/app_env.sh index 1c89c6e8a..aaeb2bc2a 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.9.5" -CAKEWALLET_BUILD_NUMBER=33 +CAKEWALLET_VERSION="1.10.0" +CAKEWALLET_BUILD_NUMBER=35 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 b8785a9be..92a8636bd 100755 --- a/scripts/macos/app_config.sh +++ b/scripts/macos/app_config.sh @@ -28,7 +28,7 @@ sed -i '' "s/\${BUNDLE_ID}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/DebugProfile sed -i '' "s/\${BUNDLE_ID}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/Release.entitlements sed -i '' "s/\${BUNDLE_ID}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/Runner.entitlements sed -i '' "s/\${PRODUCT_NAME}/${APP_MACOS_NAME}/g" ./macos/Runner/Configs/AppInfo.xcconfig -sed -i '' "s/\${PRODUCT_BUNDLE_IDENTIFIER}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/Configs/AppInfo.xcconfig +sed -i '' "s/PRODUCT_BUNDLE_IDENTIFIER = .*;/PRODUCT_BUNDLE_IDENTIFIER = $APP_MACOS_BUNDLE_ID;/g" ./macos/Runner/Configs/AppInfo.xcconfig CONFIG_ARGS="" case $APP_MACOS_TYPE in diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index a6afac2cb..af2dc1322 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.6.5" -MONERO_COM_BUILD_NUMBER=31 +MONERO_COM_VERSION="1.7.0" +MONERO_COM_BUILD_NUMBER=33 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.12.5" -CAKEWALLET_BUILD_NUMBER=88 +CAKEWALLET_VERSION="1.13.0" +CAKEWALLET_BUILD_NUMBER=90 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 bb13f49ef..33cd11292 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.0.6" +#define MyAppVersion "0.1.0" #define MyAppPublisher "Cake Labs LLC" #define MyAppURL "https://cakewallet.com/" #define MyAppExeName "CakeWallet.exe" diff --git a/tool/configure.dart b/tool/configure.dart index 1e2741721..eb41c5341 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -75,6 +75,7 @@ Future main(List args) async { Future generateBitcoin(bool hasImplementation) async { final outputFile = File(bitcoinOutputPath); const bitcoinCommonHeaders = """ +import 'dart:io' show Platform; import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; From ebe8c65407774c49547c2ba86f6ea83c260a8919 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Tue, 15 Oct 2024 00:28:38 +0300 Subject: [PATCH 14/73] Peg in and peg out flow (#1745) * version 4.20.0 * update build numbers * UI updates and script fix for ios bundle identifier * disable mweb for desktop * change hardcoded ltc server ip address electrum connection enhancement * additional logging and minor fixes * additional logging and minor fixes * addresses pt.1 * logs of fixes and experimental changes, close wallet before opening next * save * fix icon * fixes * [skip ci] updates * [skip ci] updates * updates * minor optimizations * fix for when switching between wallets * [skip ci] updates * [skip ci] updates * Update cw_bitcoin/lib/litecoin_wallet.dart Co-authored-by: Omar Hatem * Update cw_bitcoin/lib/litecoin_wallet.dart Co-authored-by: Omar Hatem * mobx * mostly logging * stream fix pt.1 [skip ci] * updates * some fixes and enhancements * [skip ci] minor * potential partial fix for streamsink closed * fix stream sink closed errors * fix mweb logo colors * add initial whitelisting for coin types on send screen * MWEB enhancements 2.0 (#1735) * additional logging and minor fixes * additional logging and minor fixes * addresses pt.1 * Allow Wallet Group Names to be the same as Wallet Names (#1730) * fix: Issues with imaging * fix: Allow group names to be the same as wallet names * fix: Bug with wallet grouping when a wallet is minimized * fix: Bug with wallet grouping when a wallet is minimized * logs of fixes and experimental changes, close wallet before opening next * save * fix icon * fixes * [skip ci] updates * [skip ci] updates * updates * minor optimizations * fix for when switching between wallets * [skip ci] updates * [skip ci] updates * Update cw_bitcoin/lib/litecoin_wallet.dart Co-authored-by: Omar Hatem * Update cw_bitcoin/lib/litecoin_wallet.dart Co-authored-by: Omar Hatem * mobx * mostly logging * stream fix pt.1 [skip ci] * updates * some fixes and enhancements * [skip ci] minor * potential partial fix for streamsink closed * fix stream sink closed errors * fix mweb logo colors * save * minor enhancements [skip ci] * save * experimental * minor * minor [skip ci] --------- Co-authored-by: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Co-authored-by: Omar Hatem * fix menu list removing from original list * handle pegin and pegout * fix text color * fix import * pegin/out button ui updates * update spacing + tx creation fix * add correct args for link view model [skip ci] --------- Co-authored-by: Matthew Fosse Co-authored-by: fossephate Co-authored-by: David Adegoke <64401859+Blazebrain@users.noreply.github.com> --- .../lib/bitcoin_transaction_credentials.dart | 4 +- cw_bitcoin/lib/electrum_wallet.dart | 28 +- cw_bitcoin/lib/litecoin_wallet.dart | 7 +- cw_bitcoin/pubspec.lock | 28 +- cw_core/lib/unspent_coin_type.dart | 1 + lib/bitcoin/cw_bitcoin.dart | 60 ++- lib/di.dart | 27 +- lib/router.dart | 24 +- .../screens/dashboard/pages/balance_page.dart | 444 +++++++++++------- lib/src/screens/send/widgets/send_card.dart | 6 +- lib/view_model/link_view_model.dart | 5 +- lib/view_model/send/send_view_model.dart | 25 +- .../unspent_coins_list_view_model.dart | 12 +- tool/configure.dart | 10 +- 14 files changed, 428 insertions(+), 253 deletions(-) create mode 100644 cw_core/lib/unspent_coin_type.dart diff --git a/cw_bitcoin/lib/bitcoin_transaction_credentials.dart b/cw_bitcoin/lib/bitcoin_transaction_credentials.dart index bda7c39ae..01e905fb0 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_credentials.dart @@ -1,11 +1,13 @@ import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_core/output_info.dart'; +import 'package:cw_core/unspent_coin_type.dart'; class BitcoinTransactionCredentials { BitcoinTransactionCredentials(this.outputs, - {required this.priority, this.feeRate}); + {required this.priority, this.feeRate, this.coinTypeToSpendFrom = UnspentCoinType.any}); final List outputs; final BitcoinTransactionPriority? priority; final int? feeRate; + final UnspentCoinType coinTypeToSpendFrom; } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 97897046c..ceaa2f088 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -37,6 +37,7 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/get_height_by_date.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; @@ -584,6 +585,7 @@ abstract class ElectrumWalletBase required int credentialsAmount, required bool paysToSilentPayment, int? inputsCount, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) { List utxos = []; List vinOutpoints = []; @@ -594,7 +596,20 @@ abstract class ElectrumWalletBase bool spendsUnconfirmedTX = false; int leftAmount = credentialsAmount; - final availableInputs = unspentCoins.where((utx) => utx.isSending && !utx.isFrozen).toList(); + final availableInputs = unspentCoins.where((utx) { + if (!utx.isSending || utx.isFrozen) { + return false; + } + + switch (coinTypeToSpendFrom) { + case UnspentCoinType.mweb: + return utx.bitcoinAddressRecord.type == SegwitAddresType.mweb; + case UnspentCoinType.nonMweb: + return utx.bitcoinAddressRecord.type != SegwitAddresType.mweb; + case UnspentCoinType.any: + return true; + } + }).toList(); final unconfirmedCoins = availableInputs.where((utx) => utx.confirmations == 0).toList(); for (int i = 0; i < availableInputs.length; i++) { @@ -701,11 +716,13 @@ abstract class ElectrumWalletBase String? memo, int credentialsAmount = 0, bool hasSilentPayment = false, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) async { final utxoDetails = _createUTXOS( sendAll: true, credentialsAmount: credentialsAmount, paysToSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); int fee = await calcFee( @@ -772,12 +789,14 @@ abstract class ElectrumWalletBase String? memo, bool? useUnconfirmed, bool hasSilentPayment = false, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) async { final utxoDetails = _createUTXOS( sendAll: false, credentialsAmount: credentialsAmount, inputsCount: inputsCount, paysToSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); final spendingAllCoins = utxoDetails.availableInputs.length == utxoDetails.utxos.length; @@ -797,6 +816,7 @@ abstract class ElectrumWalletBase inputsCount: utxoDetails.utxos.length + 1, memo: memo, hasSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); } @@ -855,6 +875,7 @@ abstract class ElectrumWalletBase inputsCount: utxoDetails.utxos.length + 1, memo: memo, useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); } @@ -862,6 +883,7 @@ abstract class ElectrumWalletBase outputs, feeRate, memo: memo, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); if (estimatedSendAll.amount == credentialsAmount) { @@ -900,6 +922,7 @@ abstract class ElectrumWalletBase memo: memo, useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, hasSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); } } @@ -957,6 +980,7 @@ abstract class ElectrumWalletBase final hasMultiDestination = transactionCredentials.outputs.length > 1; final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll; final memo = transactionCredentials.outputs.first.memo; + final coinTypeToSpendFrom = transactionCredentials.coinTypeToSpendFrom; int credentialsAmount = 0; bool hasSilentPayment = false; @@ -1012,6 +1036,7 @@ abstract class ElectrumWalletBase memo: memo, credentialsAmount: credentialsAmount, hasSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); } else { estimatedTx = await estimateTxForAmount( @@ -1020,6 +1045,7 @@ abstract class ElectrumWalletBase feeRateInt, memo: memo, hasSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); } diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index f4d99d807..29a2df48a 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -251,7 +251,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { _syncTimer?.cancel(); try { syncStatus = SyncronizingSyncStatus(); - await subscribeForUpdates(); + try { + await subscribeForUpdates(); + } catch (e) { + print("failed to subcribe for updates: $e"); + } updateFeeRates(); _feeRatesTimer?.cancel(); _feeRatesTimer = @@ -916,6 +920,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } if (!hasMwebInput && !hasMwebOutput) { + tx.isMweb = false; return tx; } diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 4c7c49a9e..36d762ea1 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: archive - sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "3.4.10" + version: "3.6.1" args: dependency: transitive description: @@ -336,10 +336,10 @@ packages: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" fixnum: dependency: transitive description: @@ -403,14 +403,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "5be191523702ba8d7a01ca97c17fca096822ccf246b0a9f11923a6ded06199b6" + url: "https://pub.dev" + source: hosted + version: "0.3.1+4" googleapis_auth: dependency: transitive description: name: googleapis_auth - sha256: af7c3a3edf9d0de2e1e0a77e994fae0a581c525fa7012af4fa0d4a52ed9484da + sha256: befd71383a955535060acde8792e7efc11d2fccd03dd1d3ec434e85b68775938 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.6.0" graphs: dependency: transitive description: @@ -809,10 +817,10 @@ packages: dependency: transitive description: name: shared_preferences_foundation - sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.3" shared_preferences_linux: dependency: transitive description: @@ -1031,10 +1039,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" yaml: dependency: transitive description: diff --git a/cw_core/lib/unspent_coin_type.dart b/cw_core/lib/unspent_coin_type.dart new file mode 100644 index 000000000..a042610fc --- /dev/null +++ b/cw_core/lib/unspent_coin_type.dart @@ -0,0 +1 @@ +enum UnspentCoinType { mweb, nonMweb, any } \ No newline at end of file diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index e9e83597a..91232792a 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -106,34 +106,33 @@ class CWBitcoin extends Bitcoin { } @override - Object createBitcoinTransactionCredentials(List outputs, - {required TransactionPriority priority, int? feeRate}) { + Object createBitcoinTransactionCredentials( + List outputs, { + required TransactionPriority priority, + int? feeRate, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, + }) { final bitcoinFeeRate = priority == BitcoinTransactionPriority.custom && feeRate != null ? feeRate : null; return BitcoinTransactionCredentials( - 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, - memo: out.memo)) - .toList(), - priority: priority as BitcoinTransactionPriority, - feeRate: bitcoinFeeRate); + 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, + memo: out.memo)) + .toList(), + priority: priority as BitcoinTransactionPriority, + feeRate: bitcoinFeeRate, + coinTypeToSpendFrom: coinTypeToSpendFrom, + ); } - @override - Object createBitcoinTransactionCredentialsRaw(List outputs, - {TransactionPriority? priority, required int feeRate}) => - BitcoinTransactionCredentials(outputs, - priority: priority != null ? priority as BitcoinTransactionPriority : null, - feeRate: feeRate); - @override @computed List getSubAddresses(Object wallet) { @@ -205,9 +204,20 @@ class CWBitcoin extends Bitcoin { (priority as BitcoinTransactionPriority).labelWithRate(rate, customRate); @override - List getUnspents(Object wallet) { + List getUnspents(Object wallet, + {UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) { final bitcoinWallet = wallet as ElectrumWallet; - return bitcoinWallet.unspentCoins; + return bitcoinWallet.unspentCoins.where((element) { + switch(coinTypeToSpendFrom) { + case UnspentCoinType.mweb: + return element.bitcoinAddressRecord.type == SegwitAddresType.mweb; + case UnspentCoinType.nonMweb: + return element.bitcoinAddressRecord.type != SegwitAddresType.mweb; + case UnspentCoinType.any: + return true; + } + + }).toList(); } Future updateUnspents(Object wallet) async { diff --git a/lib/di.dart b/lib/di.dart index 0008ad8fd..13ffd839e 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -167,6 +167,7 @@ import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_i import 'package:cake_wallet/view_model/wallet_list/wallet_edit_view_model.dart'; import 'package:cake_wallet/view_model/wallet_restore_choose_derivation_view_model.dart'; import 'package:cw_core/nano_account.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/transaction_info.dart'; @@ -725,8 +726,8 @@ Future setup({ getIt.get(), getIt.get())); - getIt.registerFactory( - () => SendViewModel( + getIt.registerFactoryParam( + (coinTypeToSpendFrom, _) => SendViewModel( getIt.get(), getIt.get(), getIt.get(), @@ -734,12 +735,13 @@ Future setup({ getIt.get(), _transactionDescriptionBox, getIt.get().wallet!.isHardwareWallet ? getIt.get() : null, + coinTypeToSpendFrom: coinTypeToSpendFrom ?? UnspentCoinType.any, ), ); - getIt.registerFactoryParam( - (PaymentRequest? initialPaymentRequest, _) => SendPage( - sendViewModel: getIt.get(), + getIt.registerFactoryParam( + (PaymentRequest? initialPaymentRequest, coinTypeToSpendFrom) => SendPage( + sendViewModel: getIt.get(param1: coinTypeToSpendFrom), authService: getIt.get(), initialPaymentRequest: initialPaymentRequest, )); @@ -1215,14 +1217,21 @@ Future setup({ getIt.registerFactory(() => SupportOtherLinksPage(getIt.get())); - getIt.registerFactory(() { + getIt.registerFactoryParam( + (coinTypeToSpendFrom, _) { final wallet = getIt.get().wallet; - return UnspentCoinsListViewModel(wallet: wallet!, unspentCoinsInfo: _unspentCoinsInfoSource); + return UnspentCoinsListViewModel( + wallet: wallet!, + unspentCoinsInfo: _unspentCoinsInfoSource, + coinTypeToSpendFrom: coinTypeToSpendFrom ?? UnspentCoinType.any, + ); }); - getIt.registerFactory(() => - UnspentCoinsListPage(unspentCoinsListViewModel: getIt.get())); + getIt.registerFactoryParam( + (coinTypeToSpendFrom, _) => UnspentCoinsListPage( + unspentCoinsListViewModel: + getIt.get(param1: coinTypeToSpendFrom))); getIt.registerFactoryParam( diff --git a/lib/router.dart b/lib/router.dart index 7beace174..3b4c38546 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -120,6 +120,7 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/nano_account.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/cupertino.dart'; @@ -184,7 +185,8 @@ Route createRoute(RouteSettings settings) { final type = settings.arguments as WalletType; final walletGroupsDisplayVM = getIt.get(param1: type); - return CupertinoPageRoute(builder: (_) => WalletGroupsDisplayPage(walletGroupsDisplayVM)); + return CupertinoPageRoute( + builder: (_) => WalletGroupsDisplayPage(walletGroupsDisplayVM)); case Routes.newWallet: final args = settings.arguments as NewWalletArguments; @@ -348,13 +350,17 @@ Route createRoute(RouteSettings settings) { settings: settings, builder: (_) => getIt.get()); case Routes.send: - final initialPaymentRequest = settings.arguments as PaymentRequest?; + final args = settings.arguments as Map?; + final initialPaymentRequest = args?['paymentRequest'] as PaymentRequest?; + final coinTypeToSpendFrom = args?['coinTypeToSpendFrom'] as UnspentCoinType?; return CupertinoPageRoute( - fullscreenDialog: true, - builder: (_) => getIt.get( - param1: initialPaymentRequest, - )); + fullscreenDialog: true, + builder: (_) => getIt.get( + param1: initialPaymentRequest, + param2: coinTypeToSpendFrom, + ), + ); case Routes.sendTemplate: return CupertinoPageRoute( @@ -604,7 +610,9 @@ Route createRoute(RouteSettings settings) { fullscreenDialog: true, builder: (_) => getIt.get()); case Routes.unspentCoinsList: - return MaterialPageRoute(builder: (_) => getIt.get()); + final coinTypeToSpendFrom = settings.arguments as UnspentCoinType?; + return MaterialPageRoute( + builder: (_) => getIt.get(param1: coinTypeToSpendFrom)); case Routes.unspentCoinsDetails: final args = settings.arguments as List; @@ -778,7 +786,7 @@ Route createRoute(RouteSettings settings) { case Routes.walletGroupDescription: final walletType = settings.arguments as WalletType; - + return MaterialPageRoute( builder: (_) => WalletGroupDescriptionPage( selectedWalletType: walletType, diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index 3b88c685a..708941023 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -17,13 +17,15 @@ import 'package:cake_wallet/src/widgets/standard_switch.dart'; import 'package:cake_wallet/store/settings_store.dart'; 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/send_page_theme.dart'; import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; import 'package:cake_wallet/utils/feature_flag.dart'; +import 'package:cake_wallet/utils/payment_request.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/dashboard/nft_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -837,216 +839,300 @@ class BalanceRowWidget extends StatelessWidget { color: Theme.of(context).extension()!.syncedBackgroundColor, ), child: Container( - margin: const EdgeInsets.only(top: 0, left: 24, right: 8, bottom: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Stack( - children: [ - if (currency == CryptoCurrency.ltc) - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Container( - padding: EdgeInsets.only(right: 16, top: 16), - child: Column( - children: [ - Container( - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, + Container( + margin: const EdgeInsets.only(top: 0, left: 24, right: 8, bottom: 16), + child: Stack( + children: [ + if (currency == CryptoCurrency.ltc) + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + padding: EdgeInsets.only(right: 16, top: 16), + child: Column( + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: ImageIcon( + AssetImage('assets/images/mweb_logo.png'), + color: Color.fromARGB(255, 11, 70, 129), + size: 40, + ), ), - child: ImageIcon( - AssetImage('assets/images/mweb_logo.png'), - color: Color.fromARGB(255, 11, 70, 129), - size: 40, + const SizedBox(height: 10), + Text( + 'MWEB', + style: TextStyle( + fontSize: 15, + fontFamily: 'Lato', + fontWeight: FontWeight.w800, + color: Theme.of(context) + .extension()! + .assetTitleColor, + height: 1, + ), + ), + ], + ), + ), + ], + ), + if (hasSecondAvailableBalance) + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24), + Text( + '${secondAvailableBalanceLabel}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .labelTextColor, + height: 1, ), ), - const SizedBox(height: 10), - Text( - 'MWEB', + SizedBox(height: 8), + AutoSizeText( + secondAvailableBalance, style: TextStyle( - fontSize: 15, + fontSize: 20, fontFamily: 'Lato', - fontWeight: FontWeight.w800, + fontWeight: FontWeight.w400, color: Theme.of(context) .extension()! .assetTitleColor, height: 1, ), + maxLines: 1, + textAlign: TextAlign.center, ), + SizedBox(height: 4), + if (!isTestnet) + Text( + '${secondAvailableFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .textColor, + height: 1, + ), + ), ], ), - ), - ], - ), - if (hasSecondAvailableBalance) - Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 24), - Text( - '${secondAvailableBalanceLabel}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .labelTextColor, - height: 1, - ), - ), - SizedBox(height: 8), - AutoSizeText( - secondAvailableBalance, - style: TextStyle( - fontSize: 20, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .assetTitleColor, - height: 1, - ), - maxLines: 1, - textAlign: TextAlign.center, - ), - SizedBox(height: 4), - if (!isTestnet) + ], + ), + ], + ), + ), + Container( + margin: const EdgeInsets.only(top: 0, left: 24, right: 8, bottom: 16), + child: Stack( + children: [ + if (hasSecondAdditionalBalance) + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24), Text( - '${secondAvailableFiatBalance}', + '${secondAdditionalBalanceLabel}', textAlign: TextAlign.center, style: TextStyle( fontSize: 12, fontFamily: 'Lato', fontWeight: FontWeight.w400, - color: - Theme.of(context).extension()!.textColor, + color: Theme.of(context) + .extension()! + .labelTextColor, height: 1, ), ), - ], - ), - ], - ), - ], - ), - Stack( - children: [ - if (hasSecondAdditionalBalance) - Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 24), - Text( - '${secondAdditionalBalanceLabel}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .labelTextColor, - height: 1, - ), - ), - SizedBox(height: 8), - AutoSizeText( - secondAdditionalBalance, - style: TextStyle( - fontSize: 20, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .assetTitleColor, - height: 1, - ), - maxLines: 1, - textAlign: TextAlign.center, - ), - SizedBox(height: 4), - if (!isTestnet) - Text( - '${secondAdditionalFiatBalance}', - textAlign: TextAlign.center, + SizedBox(height: 8), + AutoSizeText( + secondAdditionalBalance, style: TextStyle( - fontSize: 12, + fontSize: 20, fontFamily: 'Lato', fontWeight: FontWeight.w400, - color: - Theme.of(context).extension()!.textColor, + color: Theme.of(context) + .extension()! + .assetTitleColor, height: 1, ), + maxLines: 1, + textAlign: TextAlign.center, ), - ], - ), - ], - ), - // TODO: smarter peg in / out buttons - // if (currency == CryptoCurrency.ltc) - // Row( - // mainAxisAlignment: MainAxisAlignment.end, - // children: [ - // Container( - // margin: EdgeInsets.only(top: 24, right: 8), - // child: ElevatedButton( - // style: ElevatedButton.styleFrom( - // backgroundColor: Theme.of(context).highlightColor, - // ), - // onPressed: () { - // final mwebAddress = - // bitcoin!.getUnusedMwebAddress(dashboardViewModel.wallet); - // if (mwebAddress == null) return; - // final paymentRequest = - // PaymentRequest.fromUri(Uri.parse("litecoin:${mwebAddress}")); - // Navigator.of(context) - // .pushNamed(Routes.send, arguments: paymentRequest); - // }, - // child: Container( - // color: Colors.transparent, - // margin: EdgeInsets.all(4), - // child: Column( - // mainAxisSize: MainAxisSize.max, - // crossAxisAlignment: CrossAxisAlignment.center, - // children: [ - // Container( - // alignment: Alignment.center, - // decoration: BoxDecoration(shape: BoxShape.circle), - // child: Image.asset( - // 'assets/images/received.png', - // color: Theme.of(context) - // .extension()! - // .balanceAmountColor, - // width: 64, - // height: 32, - // ), - // ), - // SizedBox(height: 4), - // Text( - // S.of(context).litecoin_mweb_pegin, - // style: TextStyle( - // fontSize: 10, - // color: Theme.of(context) - // .extension()! - // .cardTextColor), - // ) - // ], - // ), - // ), - // ), - // ), - // ], - // ), - ], + SizedBox(height: 4), + if (!isTestnet) + Text( + '${secondAdditionalFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .textColor, + height: 1, + ), + ), + ], + ), + ], + ), + ], + ), ), + IntrinsicHeight( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Semantics( + label: S.of(context).litecoin_mweb_pegin, + child: OutlinedButton( + onPressed: () { + final mwebAddress = + bitcoin!.getUnusedMwebAddress(dashboardViewModel.wallet); + PaymentRequest? paymentRequest = null; + if ((mwebAddress?.isNotEmpty ?? false)) { + paymentRequest = + PaymentRequest.fromUri(Uri.parse("litecoin:${mwebAddress}")); + } + + Navigator.pushNamed( + context, + Routes.send, + arguments: { + 'paymentRequest': paymentRequest, + 'coinTypeToSpendFrom': UnspentCoinType.nonMweb, + }, + ); + }, + style: OutlinedButton.styleFrom( + backgroundColor: Theme.of(context) + .extension()! + .textFieldButtonIconColor + .withAlpha(50), + side: BorderSide(color: Colors.grey.shade400, width: 0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: Container( + padding: EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + height: 30, + width: 30, + 'assets/images/received.png', + color: Theme.of(context) + .extension()! + .balanceAmountColor, + ), + const SizedBox(width: 8), + Text( + S.of(context).litecoin_mweb_pegin, + style: TextStyle( + color: Theme.of(context) + .extension()! + .assetTitleColor, + ), + ), + ], + ), + ), + ), + ), + ), + SizedBox(width: 32), + Expanded( + child: Semantics( + label: S.of(context).litecoin_mweb_pegout, + child: OutlinedButton( + onPressed: () { + final litecoinAddress = + bitcoin!.getAddress(dashboardViewModel.wallet); + PaymentRequest? paymentRequest = null; + if (litecoinAddress.isNotEmpty) { + paymentRequest = PaymentRequest.fromUri( + Uri.parse("litecoin:${litecoinAddress}")); + } + + Navigator.pushNamed( + context, + Routes.send, + arguments: { + 'paymentRequest': paymentRequest, + 'coinTypeToSpendFrom': UnspentCoinType.mweb, + }, + ); + }, + style: OutlinedButton.styleFrom( + backgroundColor: Theme.of(context) + .extension()! + .textFieldButtonIconColor + .withAlpha(50), + side: BorderSide(color: Colors.grey.shade400, width: 0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: Container( + padding: EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + height: 30, + width: 30, + 'assets/images/upload.png', + color: Theme.of(context) + .extension()! + .balanceAmountColor, + ), + const SizedBox(width: 8), + Text( + S.of(context).litecoin_mweb_pegout, + style: TextStyle( + color: Theme.of(context) + .extension()! + .assetTitleColor, + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + SizedBox(height: 16), ], ), ), diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 2a14da305..0713fb8c4 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -14,7 +14,6 @@ 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:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; @@ -373,7 +372,10 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin Navigator.of(context).pushNamed(Routes.unspentCoinsList), + onTap: () => Navigator.of(context).pushNamed( + Routes.unspentCoinsList, + arguments: widget.sendViewModel.coinTypeToSpendFrom, + ), child: Container( color: Colors.transparent, child: Row( diff --git a/lib/view_model/link_view_model.dart b/lib/view_model/link_view_model.dart index 99aed486e..27f0c0560 100644 --- a/lib/view_model/link_view_model.dart +++ b/lib/view_model/link_view_model.dart @@ -65,15 +65,16 @@ class LinkViewModel { if (isNanoGptLink) { switch (currentLink?.authority ?? '') { case "exchange": - case "send": return PaymentRequest.fromUri(currentLink); + case "send": + return {"paymentRequest": PaymentRequest.fromUri(currentLink)}; case "buy": return true; } } if (_isValidPaymentUri) { - return PaymentRequest.fromUri(currentLink); + return {"paymentRequest": PaymentRequest.fromUri(currentLink)}; } return null; diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 79f473eb3..0ad8ba376 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -20,6 +20,7 @@ import 'package:cake_wallet/wownero/wownero.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'; import 'package:hive/hive.dart'; @@ -67,8 +68,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor this.balanceViewModel, this.contactListViewModel, this.transactionDescriptionBox, - this.ledgerViewModel, - ) : state = InitialExecutionState(), + this.ledgerViewModel, { + this.coinTypeToSpendFrom = UnspentCoinType.any, + }) : state = InitialExecutionState(), currencies = appStore.wallet!.balance.keys.toList(), selectedCryptoCurrency = appStore.wallet!.currency, hasMultipleTokens = isEVMCompatibleChain(appStore.wallet!.type) || @@ -97,6 +99,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor ObservableList outputs; + final UnspentCoinType coinTypeToSpendFrom; + @action void addOutput() { outputs @@ -217,7 +221,14 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor PendingTransaction? pendingTransaction; @computed - String get balance => wallet.balance[selectedCryptoCurrency]!.formattedFullAvailableBalance; + String get balance { + if (coinTypeToSpendFrom == UnspentCoinType.mweb) { + return balanceViewModel.balances.values.first.secondAvailableBalance; + } else if (coinTypeToSpendFrom == UnspentCoinType.nonMweb) { + return balanceViewModel.balances.values.first.availableBalance; + } + return wallet.balance[selectedCryptoCurrency]!.formattedFullAvailableBalance; + } @computed bool get isFiatDisabled => balanceViewModel.isFiatDisabled; @@ -494,8 +505,12 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor case WalletType.bitcoin: case WalletType.litecoin: case WalletType.bitcoinCash: - return bitcoin!.createBitcoinTransactionCredentials(outputs, - priority: priority!, feeRate: customBitcoinFeeRate); + return bitcoin!.createBitcoinTransactionCredentials( + outputs, + priority: priority!, + feeRate: customBitcoinFeeRate, + coinTypeToSpendFrom: coinTypeToSpendFrom, + ); case WalletType.monero: return monero! 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 72dcdb27b..f16b8390f 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 @@ -3,6 +3,7 @@ import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/utils/exception_handler.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'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_core/wallet_base.dart'; @@ -16,9 +17,11 @@ part 'unspent_coins_list_view_model.g.dart'; class UnspentCoinsListViewModel = UnspentCoinsListViewModelBase with _$UnspentCoinsListViewModel; abstract class UnspentCoinsListViewModelBase with Store { - UnspentCoinsListViewModelBase( - {required this.wallet, required Box unspentCoinsInfo}) - : _unspentCoinsInfo = unspentCoinsInfo, + UnspentCoinsListViewModelBase({ + required this.wallet, + required Box unspentCoinsInfo, + this.coinTypeToSpendFrom = UnspentCoinType.any, + }) : _unspentCoinsInfo = unspentCoinsInfo, _items = ObservableList() { _updateUnspentCoinsInfo(); _updateUnspents(); @@ -26,6 +29,7 @@ abstract class UnspentCoinsListViewModelBase with Store { WalletBase wallet; final Box _unspentCoinsInfo; + final UnspentCoinType coinTypeToSpendFrom; @observable ObservableList _items; @@ -103,7 +107,7 @@ abstract class UnspentCoinsListViewModelBase with Store { case WalletType.bitcoin: case WalletType.litecoin: case WalletType.bitcoinCash: - return bitcoin!.getUnspents(wallet); + return bitcoin!.getUnspents(wallet, coinTypeToSpendFrom: coinTypeToSpendFrom); default: return List.empty(); } diff --git a/tool/configure.dart b/tool/configure.dart index eb41c5341..ce079dd29 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -87,6 +87,7 @@ import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/receive_page_option.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:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_core/wallet_base.dart'; @@ -94,6 +95,7 @@ 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_core/get_height_by_date.dart'; import 'package:hive/hive.dart'; import 'package:ledger_flutter/ledger_flutter.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; @@ -108,7 +110,6 @@ import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_bitcoin/bitcoin_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; -import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; @@ -119,8 +120,6 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/litecoin_wallet_service.dart'; import 'package:cw_bitcoin/litecoin_wallet.dart'; -import 'package:cw_core/get_height_by_date.dart'; -import 'package:cw_core/transaction_info.dart'; import 'package:cw_bitcoin/bitcoin_hardware_wallet_service.dart'; import 'package:mobx/mobx.dart'; """; @@ -166,8 +165,7 @@ abstract class Bitcoin { int getFeeRate(Object wallet, TransactionPriority priority); Future generateNewAddress(Object wallet, String label); Future updateAddress(Object wallet,String address, String label); - Object createBitcoinTransactionCredentials(List outputs, {required TransactionPriority priority, int? feeRate}); - Object createBitcoinTransactionCredentialsRaw(List outputs, {TransactionPriority? priority, required int feeRate}); + Object createBitcoinTransactionCredentials(List outputs, {required TransactionPriority priority, int? feeRate, UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}); String getAddress(Object wallet); List getSilentPaymentAddresses(Object wallet); @@ -181,7 +179,7 @@ abstract class Bitcoin { int formatterStringDoubleToBitcoinAmount(String amount); String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate, {int? customRate}); - List getUnspents(Object wallet); + List getUnspents(Object wallet, {UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}); Future updateUnspents(Object wallet); WalletService createBitcoinWalletService( Box walletInfoSource, Box unspentCoinSource, bool alwaysScan, bool isDirect); From a211eab435b26fd28e555ce6095d74a6d3934fa8 Mon Sep 17 00:00:00 2001 From: tuxsudo Date: Mon, 14 Oct 2024 17:31:27 -0400 Subject: [PATCH 15/73] Change exchange to swap (#1731) * Change exchange strings to swap * Remove bad value --- res/values/strings_ar.arb | 10 +++++----- res/values/strings_bg.arb | 10 +++++----- res/values/strings_cs.arb | 10 +++++----- res/values/strings_de.arb | 10 +++++----- res/values/strings_en.arb | 10 +++++----- res/values/strings_es.arb | 8 ++++---- res/values/strings_fr.arb | 8 ++++---- res/values/strings_ha.arb | 10 +++++----- res/values/strings_hi.arb | 10 +++++----- res/values/strings_hr.arb | 10 +++++----- res/values/strings_hy.arb | 10 +++++----- res/values/strings_id.arb | 10 +++++----- res/values/strings_it.arb | 10 +++++----- res/values/strings_ja.arb | 10 +++++----- res/values/strings_ko.arb | 8 ++++---- res/values/strings_my.arb | 10 +++++----- res/values/strings_nl.arb | 10 +++++----- res/values/strings_pl.arb | 8 ++++---- res/values/strings_pt.arb | 8 ++++---- res/values/strings_ru.arb | 10 +++++----- res/values/strings_th.arb | 6 +++--- res/values/strings_tl.arb | 8 ++++---- res/values/strings_tr.arb | 8 ++++---- res/values/strings_uk.arb | 10 +++++----- res/values/strings_ur.arb | 8 ++++---- res/values/strings_vi.arb | 10 +++++----- res/values/strings_yo.arb | 10 +++++----- res/values/strings_zh.arb | 10 +++++----- 28 files changed, 130 insertions(+), 130 deletions(-) diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 3797334e9..81fe3cc2c 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -114,7 +114,7 @@ "change_currency": "تغيير العملة", "change_current_node": "هل أنت متأكد من تغيير العقدة الحالية إلى ${node}؟", "change_current_node_title": "تغيير العقدة الحالية", - "change_exchange_provider": "تغيير مزود الصرف", + "change_exchange_provider": "تغيير مزود المبادلة", "change_language": "تغيير اللغة", "change_language_to": "هل تريد تغيير اللغة إلى ${language}؟", "change_password": "تغيير كلمة المرور", @@ -281,8 +281,8 @@ "etherscan_history": "Etherscan تاريخ", "event": "ﺙﺪﺣ", "events": "ﺙﺍﺪﺣﻷﺍ", - "exchange": "تبادل", - "exchange_incorrect_current_wallet_for_xmr": "إذا كنت ترغب في استبدال XMR من رصيد Cake Wallet Monero ، فيرجى التبديل إلى محفظة Monero أولاً.", + "exchange": "تبديل", + "exchange_incorrect_current_wallet_for_xmr": "إذا كنت ترغب في تبديل XMR من رصيد محفظة الكعكة ، فيرجى التبديل إلى محفظة Monero أولاً.", "exchange_new_template": "قالب جديد", "exchange_provider_unsupported": "${providerName} لم يعد مدعومًا!", "exchange_result_confirm": "بالضغط على تأكيد ، سترسل ${fetchingLabel} ${from} من محفظتك المسماة ${walletName} إلى العنوان الموضح أدناه. أو يمكنك الإرسال من محفظتك الخارجية إلى العنوان أدناه / QR.\n\nيرجى الضغط على تأكيد للمتابعة أو الرجوع لتغيير المبالغ.", @@ -309,7 +309,7 @@ "fill_code": "يرجى ملء رمز التحقق المرسل إلى بريدك الإلكتروني", "filter_by": "تصفية حسب", "first_wallet_text": "محفظة رائعة ل Monero, Bitcoin, Ethereum, Litecoin و Haven", - "fixed_pair_not_supported": "هذا الزوج الثابت غير مدعوم في التبادلات المحددة", + "fixed_pair_not_supported": "لا يتم دعم هذا الزوج الثابت مع خدمات المبادلة المحددة", "fixed_rate": "السعر الثابت", "fixed_rate_alert": "ستتمكن من إدخال مبلغ الاستلام عند تشغيل وضع السعر الثابت. هل تريد التبديل إلى وضع السعر الثابت؟", "forgot_password": "هل نسيت كلمة السر", @@ -439,7 +439,7 @@ "node_test": "تجربة", "nodes": "العقد", "nodes_list_reset_to_default_message": "هل أنت متأكد أنك تريد إعادة تعيين الإعدادات إلى الافتراضي؟", - "none_of_selected_providers_can_exchange": "لا يمكن لأي من مقدمي الخدمة المختارين إجراء هذا التبادل", + "none_of_selected_providers_can_exchange": "لا يمكن لأي من مقدمي الخدمات المختارين إجراء هذا المبادلة", "noNFTYet": "ﻥﻵﺍ ﻰﺘﺣ NFTs ﺪﺟﻮﻳ ﻻ", "normal": "طبيعي", "note_optional": "ملاحظة (اختياري)", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 1d674530b..50db1610a 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -114,7 +114,7 @@ "change_currency": "Смени валута", "change_current_node": "Сигурни ли сте, че искате да промените сегашния node на ${node}?", "change_current_node_title": "Промени сегашния node", - "change_exchange_provider": "Промяна на Exchange Provider", + "change_exchange_provider": "Промяна на доставчика на суап", "change_language": "Смяна на езика", "change_language_to": "Смяна на езика на ${language}?", "change_password": "Смяна на парола", @@ -281,8 +281,8 @@ "etherscan_history": "История на Etherscan", "event": "Събитие", "events": "събития", - "exchange": "Exchange", - "exchange_incorrect_current_wallet_for_xmr": "Ако искате да обмените XMR от своя Cake Wallet Monero баланс, първо изберете своя Monero портфейл.", + "exchange": "Разметка", + "exchange_incorrect_current_wallet_for_xmr": "Ако искате да смените XMR от вашия баланс на портфейла на тортата Monero, моля, преминете първо към вашия портфейл Monero.", "exchange_new_template": "Нов шаблон", "exchange_provider_unsupported": "${providerName} вече не се поддържа!", "exchange_result_confirm": "By pressing confirm, 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 below address/QR code.\n\nPlease press confirm to continue or go back to change the amounts.", @@ -309,7 +309,7 @@ "fill_code": "Моля, въведето кода за потвърждаване, изпратен на Вашия имейл", "filter_by": "Филтрирай по", "first_wallet_text": "Невероятен портфейл за Monero, Bitcoin, Ethereum, Litecoin и Haven", - "fixed_pair_not_supported": "Този fixed pair не се поддържа от избраната борса", + "fixed_pair_not_supported": "Тази фиксирана двойка не се поддържа с избраните услуги за суап", "fixed_rate": "Постоянен обменен курс", "fixed_rate_alert": "Ще можете да въведете сумата за получаване, когато е избранен постоянен обменен курс. Искате ли да изберете постоянен обменен курс?", "forgot_password": "Забравена парола", @@ -439,7 +439,7 @@ "node_test": "Тест", "nodes": "Nodes", "nodes_list_reset_to_default_message": "Сигурни ли сте, че искате да възстановите фабричните настройки?", - "none_of_selected_providers_can_exchange": "Нито един от избраните provider-ъри не може да направи този превод", + "none_of_selected_providers_can_exchange": "Нито един от избраните доставчици не може да направи този размяна", "noNFTYet": "Все още няма NFT", "normal": "нормално", "note_optional": "Бележка (не е задължително)", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index b97c1e9f8..ddc91340b 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -114,7 +114,7 @@ "change_currency": "Změnit měnu", "change_current_node": "Opravdu chcete změnit současný uzel na ${node}?", "change_current_node_title": "Změnit současný uzel", - "change_exchange_provider": "Změnit směnárnu", + "change_exchange_provider": "Změnit poskytovatele swapu", "change_language": "Změnit jazyk", "change_language_to": "Změnit jazyk na ${language}?", "change_password": "Změnit heslo", @@ -281,8 +281,8 @@ "etherscan_history": "Historie Etherscanu", "event": "událost", "events": "Události", - "exchange": "Směnit", - "exchange_incorrect_current_wallet_for_xmr": "Pokud chcete směnit XMR z Monero částky v Cake Wallet, prosím přepněte se nejprve do své Monero peněženky.", + "exchange": "Swap", + "exchange_incorrect_current_wallet_for_xmr": "Pokud chcete vyměnit XMR z vaší dortové peněženky Monero Balance, nejprve přepněte na peněženku Monero.", "exchange_new_template": "Nová šablona", "exchange_provider_unsupported": "${providerName} již není podporováno!", "exchange_result_confirm": "Po stisknutí Potvrdit odešlete ${fetchingLabel} ${from} ze své peněženky s názvem ${walletName} na adresu uvedenou níže. Nebo můžete prostředky poslat ze své externí peněženky na níže uvedenou adresu/QR kód.\n\nProsím stiskněte Potvrdit pro pokračování, nebo se vraťte zpět pro změnu částky.", @@ -309,7 +309,7 @@ "fill_code": "Prosím vyplňte ověřovací kód zaslaný na Váš e-mail", "filter_by": "Filtrovat podle", "first_wallet_text": "Úžasná peněženka pro Monero, Bitcoin, Ethereum, Litecoin a Haven", - "fixed_pair_not_supported": "Tento pár s pevným kurzem není ve zvolené směnárně podporován", + "fixed_pair_not_supported": "Tento pevný pár není podporován vybranými službami swapu", "fixed_rate": "Pevný kurz", "fixed_rate_alert": "Když je zvolený pevný kurz, můžete zadat konkrétní částku, kterou chcete dostat. Chcete se přepnout do režimu s pevným kurzem?", "forgot_password": "Zapomenuté heslo", @@ -439,7 +439,7 @@ "node_test": "Otestovat", "nodes": "Uzly", "nodes_list_reset_to_default_message": "Opravdu chcete zrušit nastavení a vrátit výchozí hodnotu?", - "none_of_selected_providers_can_exchange": "Žádný ze zvolených poskytovatelů nemůže provést tuto směnu", + "none_of_selected_providers_can_exchange": "Žádný z vybraných poskytovatelů nemůže tuto swap provést", "noNFTYet": "Zatím žádné NFT", "normal": "Normální", "note_optional": "Poznámka (nepovinné)", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 80c33a3b7..80433ae0e 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -114,7 +114,7 @@ "change_currency": "Währung ändern", "change_current_node": "Möchten Sie den aktuellen Knoten wirklich zu ${node}? ändern?", "change_current_node_title": "Aktuellen Knoten ändern", - "change_exchange_provider": "Exchange-Anbieter ändern", + "change_exchange_provider": "Swap -Anbieter ändern", "change_language": "Sprache ändern", "change_language_to": "Sprache zu ${language} ändern?", "change_password": "Passwort ändern", @@ -281,8 +281,8 @@ "etherscan_history": "Etherscan-Geschichte", "event": "Ereignis", "events": "Veranstaltungen", - "exchange": "Umwechseln", - "exchange_incorrect_current_wallet_for_xmr": "Wenn Sie XMR von Ihrem Cake Wallet Monero-Guthaben umtauschen möchten, wechseln Sie bitte zuerst zu Ihrer Monero-Wallet.", + "exchange": "Tauschen", + "exchange_incorrect_current_wallet_for_xmr": "Wenn Sie XMR aus Ihrer Cake -Wallet Monero -Balance austauschen möchten, wechseln Sie zuerst zu Ihrer Monero -Brieftasche.", "exchange_new_template": "Neue Vorlage", "exchange_provider_unsupported": "${providerName} wird nicht mehr unterstützt!", "exchange_result_confirm": "Durch Drücken von \"Bestätigen\" wird ${fetchingLabel} ${from} von Ihrer Wallet namens ${walletName} an die unten angegebene Adresse gesendet. Alternativ können Sie von einer externen Wallet an die unten angegebene Adresse / QR-Code senden.\n\nBitte bestätigen Sie, um fortzufahren, oder gehen Sie zurück, um die Beträge zu ändern.", @@ -309,7 +309,7 @@ "fill_code": "Geben Sie den Bestätigungscode ein, den Sie per E-Mail erhalten haben", "filter_by": "Filtern nach", "first_wallet_text": "Eine großartige Wallet für Monero, Bitcoin, Ethereum, Litecoin, und Haven", - "fixed_pair_not_supported": "Dieses feste Paar wird von den ausgewählten Vermittlungsstellen nicht unterstützt", + "fixed_pair_not_supported": "Dieses feste Paar wird nicht von den ausgewählten Swap -Diensten unterstützt", "fixed_rate": "Feste Rate", "fixed_rate_alert": "Sie können den Empfangsbetrag eingeben, wenn der Festratenmodus aktiviert ist. Möchten Sie in den Festratenmodus wechseln?", "forgot_password": "Passwort vergessen", @@ -439,7 +439,7 @@ "node_test": "Test", "nodes": "Knoten", "nodes_list_reset_to_default_message": "Möchten Sie wirklich die Standardeinstellungen wiederherstellen?", - "none_of_selected_providers_can_exchange": "Keiner der ausgewählten Anbieter kann diesen Austausch vornehmen", + "none_of_selected_providers_can_exchange": "Keiner der ausgewählten Anbieter kann diesen Tausch machen", "noNFTYet": "Noch keine NFTs", "normal": "Normal", "note_optional": "Bemerkung (optional)", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index b1547be88..d6a0ee9af 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -114,7 +114,7 @@ "change_currency": "Change Currency", "change_current_node": "Are you sure to change current node to ${node}?", "change_current_node_title": "Change current node", - "change_exchange_provider": "Change Exchange Provider", + "change_exchange_provider": "Change Swap Provider", "change_language": "Change language", "change_language_to": "Change language to ${language}?", "change_password": "Change password", @@ -281,8 +281,8 @@ "etherscan_history": "Etherscan history", "event": "Event", "events": "Events", - "exchange": "Exchange", - "exchange_incorrect_current_wallet_for_xmr": "If you want to exchange XMR from your Cake Wallet Monero balance, please switch to your Monero wallet first.", + "exchange": "Swap", + "exchange_incorrect_current_wallet_for_xmr": "If you want to swap XMR from your Cake Wallet Monero balance, please switch to your Monero wallet first.", "exchange_new_template": "New template", "exchange_provider_unsupported": "${providerName} is no longer supported!", "exchange_result_confirm": "By pressing confirm, 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 below address/QR code.\n\nPlease press confirm to continue or go back to change the amounts.", @@ -309,7 +309,7 @@ "fill_code": "Please fill in the verification code provided to your email", "filter_by": "Filter by", "first_wallet_text": "Awesome wallet for Monero, Bitcoin, Ethereum, Litecoin, and Haven", - "fixed_pair_not_supported": "This fixed pair is not supported with the selected exchanges", + "fixed_pair_not_supported": "This fixed pair is not supported with the selected swap services", "fixed_rate": "Fixed rate", "fixed_rate_alert": "You will be able to enter receive amount when fixed rate mode is checked. Do you want to switch to fixed rate mode?", "forgot_password": "Forgot Password", @@ -441,7 +441,7 @@ "node_test": "Test", "nodes": "Nodes", "nodes_list_reset_to_default_message": "Are you sure that you want to reset settings to default?", - "none_of_selected_providers_can_exchange": "None of the selected providers can make this exchange", + "none_of_selected_providers_can_exchange": "None of the selected providers can make this swap", "noNFTYet": "No NFTs yet", "normal": "Normal", "note_optional": "Note (optional)", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index c4e6bb21f..a8ad7549a 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -281,8 +281,8 @@ "etherscan_history": "historia de etherscan", "event": "Evento", "events": "Eventos", - "exchange": "Intercambiar", - "exchange_incorrect_current_wallet_for_xmr": "Si desea intercambiar XMR de su saldo de Cake Wallet Monero, primero cambie a su billetera Monero.", + "exchange": "Intercambio", + "exchange_incorrect_current_wallet_for_xmr": "Si desea intercambiar XMR desde su billetera de pastel Monero Balance, primero cambie a su billetera Monero.", "exchange_new_template": "Nueva plantilla", "exchange_provider_unsupported": "¡${providerName} ya no es compatible!", "exchange_result_confirm": "Al presionar confirmar, 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 siguiente dirección / código QR anterior.\n\nPresione confirmar para continuar o regrese para cambiar los montos.", @@ -309,7 +309,7 @@ "fill_code": "Por favor complete el código de verificación proporcionado a su correo electrónico", "filter_by": "Filtrado por", "first_wallet_text": "Impresionante billetera para Monero, Bitcoin, Ethereum, Litecoin, y Haven", - "fixed_pair_not_supported": "Este par fijo no es compatible con los intercambios seleccionados", + "fixed_pair_not_supported": "Este par fijo no es compatible con los servicios de intercambio seleccionados", "fixed_rate": "Tipo de interés fijo", "fixed_rate_alert": "Podrá ingresar la cantidad recibida cuando el modo de tarifa fija esté marcado. ¿Quieres cambiar al modo de tarifa fija?", "forgot_password": "Olvidé mi contraseña", @@ -439,7 +439,7 @@ "node_test": "Prueba", "nodes": "Nodos", "nodes_list_reset_to_default_message": "¿Está seguro de que desea restablecer la configuración predeterminada?", - "none_of_selected_providers_can_exchange": "Ninguno de los proveedores seleccionados puede realizar este intercambio", + "none_of_selected_providers_can_exchange": "Ninguno de los proveedores seleccionados puede hacer este intercambio", "noNFTYet": "Aún no hay NFT", "normal": "Normal", "note_optional": "Nota (opcional)", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 8508dda99..be5b48dd8 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -114,7 +114,7 @@ "change_currency": "Changer de Devise", "change_current_node": "Êtes vous certain de vouloir changer le nœud actuel pour ${node} ?", "change_current_node_title": "Changer le nœud actuel", - "change_exchange_provider": "Changer de Plateforme d'Échange", + "change_exchange_provider": "Changer le fournisseur d'échange", "change_language": "Changer de langue", "change_language_to": "Changer la langue vers ${language} ?", "change_password": "Changer le mot de passe", @@ -282,7 +282,7 @@ "event": "Événement", "events": "Événements", "exchange": "Échanger", - "exchange_incorrect_current_wallet_for_xmr": "Si vous souhaitez échanger des XMR du solde Monero de votre Cake Wallet, merci de sélectionner votre portefeuille (wallet) Monero au préalable.", + "exchange_incorrect_current_wallet_for_xmr": "Si vous souhaitez échanger XMR à partir de votre balance monero portefeuille de gâteau, 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 !", "exchange_result_confirm": "En pressant confirmer, vous enverrez ${fetchingLabel} ${from} depuis votre portefeuille (wallet) nommé ${walletName} vers l'adresse ci-dessous. Vous pouvez aussi envoyer depuis votre portefeuille externe vers l'adresse/QR code ci-dessous.\n\nMerci d'appuyer sur confirmer pour continuer ou retournez en arrière pour modifier les montants.", @@ -309,7 +309,7 @@ "fill_code": "Veuillez remplir le code de vérification fourni sur votre e-mail", "filter_by": "Filtrer par", "first_wallet_text": "Super portefeuille (wallet) pour Monero, Bitcoin, Ethereum, Litecoin et Haven", - "fixed_pair_not_supported": "Cette paire fixe n'est pas prise en charge avec les échanges sélectionnés", + "fixed_pair_not_supported": "Cette paire fixe n'est pas prise en charge avec les services d'échange sélectionnés", "fixed_rate": "Taux fixe", "fixed_rate_alert": "Vous aurez la possibilité de rentrer le montant reçu lorsque le mode taux fixe est sélectionné. Souhaitez vous basculer en mode taux fixe ?", "forgot_password": "Mot de passe oublié", @@ -439,7 +439,7 @@ "node_test": "Tester", "nodes": "Nœuds", "nodes_list_reset_to_default_message": "Êtes vous certain de vouloir revenir aux réglages par défaut ?", - "none_of_selected_providers_can_exchange": "Aucun des fournisseurs sélectionnés ne peut effectuer cet échange", + "none_of_selected_providers_can_exchange": "Aucun des fournisseurs sélectionnés ne peut faire cet échange", "noNFTYet": "Pas encore de NFT", "normal": "Normal", "note_optional": "Note (optionnelle)", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index b25cc14f5..4deb0df1d 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -114,7 +114,7 @@ "change_currency": "Canja Kuɗi", "change_current_node": "Kuna tabbatar kuna so ku canja node yanzu zuwa ${node}?", "change_current_node_title": "Canja node yanzu", - "change_exchange_provider": "Canza Mai Bayar da Musanya", + "change_exchange_provider": "Canza mai canzawa", "change_language": "canja harshen", "change_language_to": "canja harshen zuwa ${language}?", "change_password": "Canza kalmar shiga", @@ -281,8 +281,8 @@ "etherscan_history": "Etherscan tarihin kowane zamani", "event": "Lamarin", "events": "Abubuwan da suka faru", - "exchange": "Exchange", - "exchange_incorrect_current_wallet_for_xmr": "Idan kana son musanya XMR daga ma'aunin Cake Wallet Monero, da fatan za a fara canza wallet ɗin Monero ɗin ku.", + "exchange": "Musya", + "exchange_incorrect_current_wallet_for_xmr": "Idan kana son canza XMR daga walat ɗin Bed Wallet ɗinka, da fatan za a canza zuwa walat ɗinku na Monero.", "exchange_new_template": "Sabon template", "exchange_provider_unsupported": "${providerName}", "exchange_result_confirm": "Ta danna tabbatarwa, zaku aika ${fetchingLabel} ${from} daga walat ɗin ku mai suna ${walletName} zuwa address dake kasa. Ko zaka iya aika daga kwalinku na external zuwa address/QR code dake kasa.\n\nDon Allah shigar da confirm don ci gaba ko dawo ka canja adadinku.", @@ -309,7 +309,7 @@ "fill_code": "Da fatan za a cika lambar tabbatarwa da aka bayar zuwa imel ɗin ku", "filter_by": "Tace ta", "first_wallet_text": "Aikace-aikacen e-wallet ga Monero, Bitcoin, Ethereum, Litecoin, da kuma Haven", - "fixed_pair_not_supported": "Wannan kafaffen guda biyu ba shi da tallafi tare da zaɓaɓɓun musayar", + "fixed_pair_not_supported": "Ba a tallafa wa wannan adireshin da aka zaɓi tare da zaɓin siye ba", "fixed_rate": "Kafaffen ƙima", "fixed_rate_alert": "Za ku iya shigar da adadin karɓa lokacin da aka duba ƙayyadadden zaɓin ƙimar kuɗi. Kuna so ku canza zuwa ƙayyadadden yanayin ƙimar kuɗi?", "forgot_password": "Manta Kalmar wucewa", @@ -439,7 +439,7 @@ "node_test": "Gwaji", "nodes": "Nodes", "nodes_list_reset_to_default_message": "Kuna tabbatar kuna so ku sake saitunan zuwa default?", - "none_of_selected_providers_can_exchange": "Babu ɗaya daga cikin zaɓaɓɓun masu samarwa da zai iya yin wannan musayar", + "none_of_selected_providers_can_exchange": "Babu wani daga cikin masu siye da aka zaɓa na iya yin wannan musan", "noNFTYet": "Babu NFTs tukuna", "normal": "Na al'ada", "note_optional": "Bayani (optional)", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index c00184218..5161250fc 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -114,7 +114,7 @@ "change_currency": "मुद्रा परिवर्तन करें", "change_current_node": "क्या आप वर्तमान नोड को बदलना सुनिश्चित करते हैं ${node}?", "change_current_node_title": "वर्तमान नोड बदलें", - "change_exchange_provider": "एक्सचेंज प्रदाता बदलें", + "change_exchange_provider": "स्वैप प्रदाता बदलें", "change_language": "भाषा बदलो", "change_language_to": "को भाषा बदलें ${language}?", "change_password": "पासवर्ड बदलें", @@ -281,8 +281,8 @@ "etherscan_history": "इथरस्कैन इतिहास", "event": "आयोजन", "events": "आयोजन", - "exchange": "अदला बदली", - "exchange_incorrect_current_wallet_for_xmr": "यदि आप अपने केक वॉलेट मोनेरो बैलेंस से एक्सएमआर का आदान-प्रदान करना चाहते हैं, तो कृपया अपने मोनेरो वॉलेट में जाएं।", + "exchange": "बदलना", + "exchange_incorrect_current_wallet_for_xmr": "यदि आप अपने केक वॉलेट मोनेरो बैलेंस से XMR को स्वैप करना चाहते हैं, तो कृपया पहले अपने मोनेरो वॉलेट पर स्विच करें।", "exchange_new_template": "नया टेम्पलेट", "exchange_provider_unsupported": "${providerName} अब समर्थित नहीं है!", "exchange_result_confirm": "पुष्टि दबाकर, आप भेज रहे होंगे ${fetchingLabel} ${from} अपने बटुए से ${walletName} नीचे दिखाए गए पते पर। या आप अपने बाहरी वॉलेट से नीचे के पते पर भेज सकते हैं / क्यूआर कोड पर भेज सकते हैं।\n\nकृपया जारी रखने या राशि बदलने के लिए वापस जाने के लिए पुष्टि करें दबाएं.", @@ -309,7 +309,7 @@ "fill_code": "कृपया अपने ईमेल पर प्रदान किया गया सत्यापन कोड भरें", "filter_by": "के द्वारा छनित", "first_wallet_text": "Monero, Bitcoin, Ethereum, Litecoin, और Haven के लिए बहुत बढ़िया बटुआ", - "fixed_pair_not_supported": "यह निश्चित जोड़ी चयनित एक्सचेंजों के साथ समर्थित नहीं है", + "fixed_pair_not_supported": "यह निश्चित जोड़ी चयनित स्वैप सेवाओं के साथ समर्थित नहीं है", "fixed_rate": "निर्धारित दर", "fixed_rate_alert": "फिक्स्ड रेट मोड की जांच करने पर आप प्राप्त राशि दर्ज कर पाएंगे। क्या आप निश्चित दर मोड पर स्विच करना चाहते हैं?", "forgot_password": "पासवर्ड भूल गए", @@ -439,7 +439,7 @@ "node_test": "परीक्षा", "nodes": "नोड्स", "nodes_list_reset_to_default_message": "क्या आप वाकई सेटिंग को डिफ़ॉल्ट पर रीसेट करना चाहते हैं?", - "none_of_selected_providers_can_exchange": "चयनित प्रदाताओं में से कोई भी इस एक्सचेंज को नहीं बना सकता", + "none_of_selected_providers_can_exchange": "चयनित प्रदाता में से कोई भी यह स्वैप नहीं कर सकता है", "noNFTYet": "अभी तक कोई एनएफटी नहीं", "normal": "सामान्य", "note_optional": "नोट (वैकल्पिक)", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 030d6f49b..8ef92aaf0 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -114,7 +114,7 @@ "change_currency": "Promijenite valutu", "change_current_node": "Jeste li sigurni da želite promijeniti trenutni node na ${node}?", "change_current_node_title": "Promijeni trenutni node", - "change_exchange_provider": "Promjena davatelja usluge razmjene", + "change_exchange_provider": "Promijenite davatelja zamjene", "change_language": "Promijeni jezik", "change_language_to": "Promijeni jezik u ${language}?", "change_password": "Promijeni lozinku", @@ -281,8 +281,8 @@ "etherscan_history": "Etherscan povijest", "event": "Događaj", "events": "Događaji", - "exchange": "Razmijeni", - "exchange_incorrect_current_wallet_for_xmr": "Ako želite razmijeniti XMR s vlastitog Monero računa na Cake Wallet novčaniku, molimo prvo se prebacite na svoj Monero novčanik.", + "exchange": "Zamjena", + "exchange_incorrect_current_wallet_for_xmr": "Ako želite zamijeniti XMR iz vašeg novčanika za kolač Monero, prvo se prebacite na svoj novčanik Monero.", "exchange_new_template": "Novi predložak", "exchange_provider_unsupported": "${providerName} više nije podržan!", "exchange_result_confirm": "Pritiskom na potvrdi, poslat ćete ${fetchingLabel} ${from} sa svog novčanika pod nazivom ${walletName} na adresu prikazanu ispod ili iznos možete poslati s vanjskog novčanika na niže navedenu adresu. /QR code.\n\nMolimo potvrdite za nastavak ili se vratite natrag za promjenu iznosa.", @@ -309,7 +309,7 @@ "fill_code": "Molimo vas da ispunite kontrolni kod koji ste dobili na svojoj e-pošti", "filter_by": "Filtrirati po", "first_wallet_text": "Odličan novčanik za Monero, Bitcoin, Ethereum, Litecoin, i Haven", - "fixed_pair_not_supported": "Ovaj fiksni par nije podržan s odabranim burzama", + "fixed_pair_not_supported": "Ovaj fiksni par nije podržan s odabranim SWAP uslugama", "fixed_rate": "Fiksna stopa", "fixed_rate_alert": "Moći ćete unijeti iznos koji želite primiti nakon što označite način rada fiksne stope. Želite li se prebaciti na način rada fiksne stope?", "forgot_password": "Zaboravljena lozinka", @@ -439,7 +439,7 @@ "node_test": "Provjeri", "nodes": "Nodes", "nodes_list_reset_to_default_message": "Jeste li sigurni da se želite vratiti na početne postavke?", - "none_of_selected_providers_can_exchange": "Niti jedan od odabranih pružatelja usluga ne može izvršiti ovu razmjenu", + "none_of_selected_providers_can_exchange": "Nijedan od odabranih pružatelja usluga ne može napraviti ovu zamjenu", "noNFTYet": "Još nema NFT-ova", "normal": "Normalno", "note_optional": "Poruka (nije obvezno)", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 1869001a7..40ed1e116 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -114,7 +114,7 @@ "change_currency": "Փոխել արժույթը", "change_current_node": "Վստահ եք, որ ցանկանում եք փոխել ընթացիկ հանգույցը ${node}?", "change_current_node_title": "Փոխել ընթացիկ հանգույցը", - "change_exchange_provider": "Փոխել փոխանակման մատակարարին", + "change_exchange_provider": "Փոխեք փոխանակման մատակարարը", "change_language": "Փոխել լեզուն", "change_language_to": "Փոխել լեզուն ${language}?", "change_password": "Փոխել գաղտնաբառը", @@ -281,8 +281,8 @@ "etherscan_history": "Etherscan պատմություն", "event": "Իրադարձություն", "events": "Իրադարձություններ", - "exchange": "Փոխել", - "exchange_incorrect_current_wallet_for_xmr": "Եթե դուք ցանկանում եք փոխանակել XMR ձեր Cake Wallet Monero հաշվեհամարից, խնդրում ենք անցնել ձեր Monero հաշվեհամարին", + "exchange": "Փոխանակել", + "exchange_incorrect_current_wallet_for_xmr": "Եթե ​​ցանկանում եք փոխանակել XMR ձեր տորթի դրամապանակից Monero Relandal- ից, խնդրում ենք նախ անցնել ձեր Monero դրամապանակին:", "exchange_new_template": "Նոր տեսակ", "exchange_provider_unsupported": "${providerName} այլևս չի ապահովվում", "exchange_result_confirm": "Սեղմելով հաստատել, դուք կուղարկեք ${fetchingLabel} ${from} ձեր հաշվեհամարից ${walletName} հետևյալ հասցեին: Կամ կարող եք ուղարկել ձեր արտաքին հաշվեհամարից հետևյալ հասցեին/QR կոդին:\n\nԽնդրում ենք սեղմել հաստատել կամ վերադառնալ գումարը փոխելու համար", @@ -309,7 +309,7 @@ "fill_code": "Խնդրում ենք լրացնել հաստատման կոդը ձեր էլեկտրոնային փոստում", "filter_by": "Ֆիլտրել ըստ", "first_wallet_text": "Հիանալի հաշվեհամար Monero, Bitcoin, Ethereum, Litecoin և Haven արժույթների համար", - "fixed_pair_not_supported": "Այս ֆիքսված զույգը չի ապահովվում ընտրված փոխանակման կետերում", + "fixed_pair_not_supported": "Այս ֆիքսված զույգը չի ապահովվում ընտրված փոխանակման ծառայություններ", "fixed_rate": "Ֆիքսված փոխարժեք", "fixed_rate_alert": "Դուք կկարողանաք մուտքագրել ստացվող գումարը, երբ ֆիքսված փոխարժեքի ռեժիմը միացված է: Դուք ցանկանում եք անցնել ֆիքսված փոխարժեքի ռեժիմին?", "forgot_password": "Մոռացել եմ գաղտնաբառը", @@ -431,7 +431,7 @@ "node_test": "Փորձարկում", "nodes": "Հանգույցներ", "nodes_list_reset_to_default_message": "Վերակայվում եք կարգավորումները լռությամբ?", - "none_of_selected_providers_can_exchange": "Ոչ մի ընտրված մատակարար չի կարող այս փոխանակումը կատարել", + "none_of_selected_providers_can_exchange": "Ընտրված մատակարարներից ոչ մեկը չի կարող կատարել այս փոխանակումը", "noNFTYet": "Դեռ ոչ մի NFT", "normal": "Նորմալ", "note_optional": "Նշում (ոչ պարտադիր)", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index b215fa148..5f93082ec 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -114,7 +114,7 @@ "change_currency": "Ganti Mata Uang", "change_current_node": "Apakah Anda yakin ingin mengubah node saat ini menjadi ${node}?", "change_current_node_title": "Ubah node saat ini", - "change_exchange_provider": "Ganti Penyedia Tukar", + "change_exchange_provider": "Ubah penyedia swap", "change_language": "Ganti bahasa", "change_language_to": "Ganti bahasa ke ${language}?", "change_password": "Ubah kata sandi", @@ -281,8 +281,8 @@ "etherscan_history": "Sejarah Etherscan", "event": "Peristiwa", "events": "Acara", - "exchange": "Tukar", - "exchange_incorrect_current_wallet_for_xmr": "Jika Anda ingin menukar XMR dari saldo Monero Cake Wallet Anda, silakan beralih ke dompet Monero Anda terlebih dahulu.", + "exchange": "Menukar", + "exchange_incorrect_current_wallet_for_xmr": "Jika Anda ingin bertukar XMR dari Saldo Monero Dompet Kue Anda, silakan beralih ke Monero Wallet Anda terlebih dahulu.", "exchange_new_template": "Template baru", "exchange_provider_unsupported": "${providerName} tidak lagi didukung!", "exchange_result_confirm": "Dengan menekan tombol konfirmasi, Anda akan mengirimkan ${fetchingLabel} ${from} dari dompet Anda yang disebut ${walletName} ke alamat yang ditampilkan di bawah. Anda juga dapat mengirim dari dompet eksternal Anda ke alamat/QR code di bawah.\n\nSilakan tekan konfirmasi untuk melanjutkan atau kembali untuk mengubah jumlah.", @@ -309,7 +309,7 @@ "fill_code": "Silakan isi kode verifikasi yang diterima di email Anda", "filter_by": "Filter berdasarkan", "first_wallet_text": "Dompet luar biasa untuk Monero, Bitcoin, Ethereum, Litecoin, dan Haven", - "fixed_pair_not_supported": "Pasangan tetap ini tidak didukung dengan bursa yang dipilih", + "fixed_pair_not_supported": "Pasangan tetap ini tidak didukung dengan layanan swap yang dipilih", "fixed_rate": "Rate tetap", "fixed_rate_alert": "Anda akan dapat memasukkan jumlah penerimaan saat mode rate tetap dicentang. Apakah Anda ingin beralih ke mode rate tetap?", "forgot_password": "Lupa Kata Sandi", @@ -439,7 +439,7 @@ "node_test": "Uji", "nodes": "Node", "nodes_list_reset_to_default_message": "Apakah Anda yakin ingin mengatur ulang pengaturan ke default?", - "none_of_selected_providers_can_exchange": "Tidak ada dari penyedia yang dipilih yang dapat melakukan pertukaran ini", + "none_of_selected_providers_can_exchange": "Tak satu pun dari penyedia yang dipilih dapat melakukan pertukaran ini", "noNFTYet": "Belum ada NFT", "normal": "Normal", "note_optional": "Catatan (opsional)", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 14963f58b..08ae928af 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -114,7 +114,7 @@ "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": "Cambia Exchange", + "change_exchange_provider": "Provider di swap di cambiamento", "change_language": "Cambia lingua", "change_language_to": "Cambiare lingua in ${language}?", "change_password": "Cambia password", @@ -282,8 +282,8 @@ "etherscan_history": "Storia Etherscan", "event": "Evento", "events": "Eventi", - "exchange": "Scambia", - "exchange_incorrect_current_wallet_for_xmr": "Se vuoi scambiare XMR dal tuo saldo Cake Wallet Monero, gentilmente passa al tuo portafoglio Monero.", + "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_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.", @@ -310,7 +310,7 @@ "fill_code": "Compila il codice di verifica fornito alla tua email", "filter_by": "Filtrirati po", "first_wallet_text": "Portafoglio fantastico per Monero, Bitcoin, Ethereum, Litecoin, e Haven", - "fixed_pair_not_supported": "Questa coppia fissa non è supportata con gli scambi selezionati", + "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?", "forgot_password": "Password dimenticata", @@ -440,7 +440,7 @@ "node_test": "Test", "nodes": "Nodi", "nodes_list_reset_to_default_message": "Sei sicuro di voler ripristinare le impostazioni predefinite?", - "none_of_selected_providers_can_exchange": "Nessuno dei fornitori selezionati può effettuare questo scambio", + "none_of_selected_providers_can_exchange": "Nessuno dei provider selezionati può fare questo scambio", "noNFTYet": "Nessun NFT ancora", "normal": "Normale", "note_optional": "Nota (opzionale)", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index af61ab1f9..d70eca31b 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -114,7 +114,7 @@ "change_currency": "通貨を変更する", "change_current_node": "現在のノードを変更してよろしいですか ${node}?", "change_current_node_title": "現在のノードを変更する", - "change_exchange_provider": "Exchangeプロバイダーの変更", + "change_exchange_provider": "スワッププロバイダーを変更します", "change_language": "言語を変えてください", "change_language_to": "言語を変更 ${language}?", "change_password": "パスワードを変更する", @@ -281,8 +281,8 @@ "etherscan_history": "イーサスキャンの歴史", "event": "イベント", "events": "イベント", - "exchange": "交換する", - "exchange_incorrect_current_wallet_for_xmr": "Cake Wallet Moneroの残高からXMRを交換する場合は、最初にMoneroウォレットに切り替えてください。", + "exchange": "スワップ", + "exchange_incorrect_current_wallet_for_xmr": "XMRをケーキウォレットモネロバランスから交換したい場合は、最初にMoneroウォレットに切り替えてください。", "exchange_new_template": "新しいテンプレート", "exchange_provider_unsupported": "${providerName}はサポートされなくなりました!", "exchange_result_confirm": "確認を押すと、送信されます ${fetchingLabel} ${from} と呼ばれるあなたの財布から ${walletName} 下記の住所へ。 または、外部ウォレットから以下のアドレスに送信することもできます/ QRコードに送信できます.\n\n確認を押して続行するか、戻って金額を変更してください.", @@ -309,7 +309,7 @@ "fill_code": "メールアドレスに記載されている確認コードを入力してください", "filter_by": "でフィルタリング", "first_wallet_text": "Monero、Bitcoin、Ethereum、Litecoin、Haven用の素晴らしいウォレット", - "fixed_pair_not_supported": "この固定ペアは、選択したエクスチェンジではサポートされていません", + "fixed_pair_not_supported": "この固定ペアは、選択したスワップサービスではサポートされていません", "fixed_rate": "固定金利", "fixed_rate_alert": "固定金利モードにチェックを入れると、受取額を入力できるようになります。 固定金利モードに切り替えますか?", "forgot_password": "パスワードを忘れた", @@ -440,7 +440,7 @@ "node_test": "テスト", "nodes": "ノード", "nodes_list_reset_to_default_message": "設定をデフォルトにリセットしてもよろしいですか?", - "none_of_selected_providers_can_exchange": "選択したプロバイダーはいずれもこの交換を行うことができません", + "none_of_selected_providers_can_exchange": "選択したプロバイダーはいずれもこのスワップを作成できません", "noNFTYet": "NFTはまだありません", "normal": "普通", "note_optional": "注(オプション)", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index dd19af792..133ca1838 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -114,7 +114,7 @@ "change_currency": "통화 변경", "change_current_node": "현재 노드를 다음으로 변경 하시겠습니까 ${node}?", "change_current_node_title": "현재 노드 변경", - "change_exchange_provider": "교환 공급자 변경", + "change_exchange_provider": "스왑 제공 업체를 변경하십시오", "change_language": "언어 변경", "change_language_to": "언어를로 변경 ${language}?", "change_password": "비밀번호 변경", @@ -282,7 +282,7 @@ "event": "이벤트", "events": "이벤트", "exchange": "교환", - "exchange_incorrect_current_wallet_for_xmr": "Cake Wallet Monero 잔액에서 XMR을 교환하려면 먼저 Monero 지갑으로 전환하십시오.", + "exchange_incorrect_current_wallet_for_xmr": "케이크 지갑 Monero Balance에서 XMR을 교체하려면 먼저 Monero 지갑으로 전환하십시오.", "exchange_new_template": "새 템플릿", "exchange_provider_unsupported": "${providerName}은 더 이상 지원되지 않습니다!", "exchange_result_confirm": "확인을 누르면 전송됩니다 ${fetchingLabel} ${from} 지갑에서 ${walletName} 아래 주소로. 또는 외부 지갑에서 아래 주소로 보낼 수 있습니다 / QR 코드로 보낼 수 있습니다.\n\n확인을 눌러 계속하거나 금액을 변경하려면 돌아가십시오.", @@ -309,7 +309,7 @@ "fill_code": "이메일에 제공된 인증 코드를 입력하세요.", "filter_by": "필터링 기준", "first_wallet_text": "Monero, Bitcoin, Ethereum, Litecoin 및 Haven을 위한 멋진 지갑", - "fixed_pair_not_supported": "이 고정 쌍은 선택한 교환에서 지원되지 않습니다.", + "fixed_pair_not_supported": "이 고정 쌍은 선택한 스왑 서비스에서 지원되지 않습니다.", "fixed_rate": "고정 비율", "fixed_rate_alert": "고정 금리 모드 체크시 수취 금액 입력이 가능합니다. 고정 속도 모드로 전환 하시겠습니까?", "forgot_password": "비밀번호 찾기", @@ -439,7 +439,7 @@ "node_test": "테스트", "nodes": "노드", "nodes_list_reset_to_default_message": "설정을 기본값으로 재설정 하시겠습니까?", - "none_of_selected_providers_can_exchange": "선택한 공급자 중 누구도 이 교환을 할 수 없습니다.", + "none_of_selected_providers_can_exchange": "선택한 공급자 중 어느 것도이 교환을 할 수 없습니다", "noNFTYet": "아직 NFT가 없습니다", "normal": "정상", "note_optional": "참고 (선택 사항)", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 8ce050339..1727f0d71 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -114,7 +114,7 @@ "change_currency": "ငွေကြေးကိုပြောင်းပါ။", "change_current_node": "လက်ရှိ နှာခေါင်း ကို ${node} သို့ ပြောင်းရန် သေချာပါသလား။", "change_current_node_title": "လက်ရှိ နှာခေါင်း ကိုပြောင်းပါ။", - "change_exchange_provider": "အပြန်အလှန် လဲလှယ်ရေး ထောက်ပံ့ပေးသူကို ပြောင်းလဲပါ", + "change_exchange_provider": "SWAP ပံ့ပိုးသူပြောင်းလဲပါ", "change_language": "ဘာသာစကားပြောင်းပါ။", "change_language_to": "ဘာသာစကားကို ${language} သို့ ပြောင်းမလား။", "change_password": "စကားဝှက်ကိုပြောင်းရန်", @@ -281,8 +281,8 @@ "etherscan_history": "Etherscan သမိုင်း", "event": "ပွဲ", "events": "အဲ့ဒါနဲ့", - "exchange": "ချိန်းတယ်။", - "exchange_incorrect_current_wallet_for_xmr": "သင်၏ Cake Wallet Monero လက်ကျန်မှ XMR ကိုလဲလှယ်လိုပါက၊ သင်၏ Monero ပိုက်ဆံအိတ်သို့ ဦးစွာပြောင်းပါ။", + "exchange": "လဲလှယ်", + "exchange_incorrect_current_wallet_for_xmr": "အကယ်. သင်သည် XMR ကိုသင်၏ကိတ်မုန့် Monero Balance မှ Swap ကိုလဲလှယ်လိုပါကသင်၏ Monero Wallet ကိုပထမဆုံးအကြိမ်ပြောင်းပါ။", "exchange_new_template": "ပုံစံအသစ်", "exchange_provider_unsupported": "${providerName} မရှိတော့ပါ!", "exchange_result_confirm": "အတည်ပြုချက်ကို နှိပ်ခြင်းဖြင့်၊ သင်သည် ${fetchingLabel} ${from} ဟုခေါ်သော သင့်ပိုက်ဆံအိတ်မှ ${walletName} ကို အောက်ဖော်ပြပါလိပ်စာသို့ ပေးပို့မည်ဖြစ်ပါသည်။ သို့မဟုတ် သင့်ပြင်ပပိုက်ဆံအိတ်မှ အောက်ပါလိပ်စာ/QR ကုဒ်သို့ ပေးပို့နိုင်ပါသည်။\n\nပမာဏများကို ပြောင်းလဲရန် ဆက်လက်လုပ်ဆောင်ရန် သို့မဟုတ် ပြန်သွားရန် အတည်ပြုချက်ကို နှိပ်ပါ။", @@ -309,7 +309,7 @@ "fill_code": "သင့်အီးမေးလ်သို့ ပေးထားသည့် အတည်ပြုကုဒ်ကို ဖြည့်ပါ။", "filter_by": "အလိုက် စစ်ထုတ်ပါ။", "first_wallet_text": "Monero၊ Bitcoin၊ Ethereum၊ Litecoin နှင့် Haven အတွက် အလွန်ကောင်းမွန်သော ပိုက်ဆံအိတ်", - "fixed_pair_not_supported": "ရွေးချယ်ထားသော ဖလှယ်မှုများဖြင့် ဤပုံသေအတွဲကို ပံ့ပိုးမထားပါ။", + "fixed_pair_not_supported": "ဤပုံသေစုံတွဲသည်ရွေးချယ်ထားသောလဲလှယ်ရေးအစီအစဉ်များဖြင့်မထောက်ပံ့ပါ", "fixed_rate": "ပုံသေနှုန်း", "fixed_rate_alert": "ပုံသေနှုန်းထားမုဒ်ကို စစ်ဆေးသည့်အခါ လက်ခံပမာဏကို ထည့်သွင်းနိုင်မည်ဖြစ်သည်။ ပုံသေနှုန်းမုဒ်သို့ ပြောင်းလိုပါသလား။", "forgot_password": "စကားဝှက်မေ့နေပါသလား", @@ -439,7 +439,7 @@ "node_test": "စမ်း", "nodes": "ဆုံမှတ်များ", "nodes_list_reset_to_default_message": "ဆက်တင်များကို မူရင်းအတိုင်း ပြန်လည်သတ်မှတ်လိုသည်မှာ သေချာပါသလား။", - "none_of_selected_providers_can_exchange": "ရွေးချယ်ထားသော ဝန်ဆောင်မှုပေးသူများမှ ဤလဲလှယ်မှုကို ပြုလုပ်၍မရပါ။", + "none_of_selected_providers_can_exchange": "ရွေးချယ်ထားသောပံ့ပိုးပေးသူတစ် ဦး တစ်ယောက်မှဤအစီအစဉ်ကိုလုပ်နိုင်သည်", "noNFTYet": "NFTs မရှိသေးပါ။", "normal": "ပုံမှန်", "note_optional": "မှတ်ချက် (ချန်လှပ်ထားနိုင်သည်)", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 6b1dd29c5..3f2df531b 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -114,7 +114,7 @@ "change_currency": "Verander valuta", "change_current_node": "Weet u zeker dat u het huidige knooppunt wilt wijzigen in ${node}?", "change_current_node_title": "Wijzig het huidige knooppunt", - "change_exchange_provider": "Wijzig Exchange Provider", + "change_exchange_provider": "Verander Swap Provider", "change_language": "Verander de taal", "change_language_to": "Verander de taal in ${language}?", "change_password": "Wachtwoord wijzigen", @@ -281,8 +281,8 @@ "etherscan_history": "Etherscan-geschiedenis", "event": "Evenement", "events": "Evenementen", - "exchange": "Uitwisseling", - "exchange_incorrect_current_wallet_for_xmr": "Als u XMR wilt omwisselen van uw Cake Wallet Monero-saldo, moet u eerst overschakelen naar uw Monero-portemonnee.", + "exchange": "Ruil", + "exchange_incorrect_current_wallet_for_xmr": "Als je XMR uit je cake -portemonnee Monero -balans wilt ruilen, schakel dan eerst over naar je Monero -portemonnee.", "exchange_new_template": "Nieuwe sjabloon", "exchange_provider_unsupported": "${providerName} wordt niet langer ondersteund!", "exchange_result_confirm": "Door op bevestigen te drukken, wordt u verzonden ${fetchingLabel} ${from} uit je portemonnee genoemd ${walletName} naar het onderstaande adres. Of u kunt vanuit uw externe portemonnee naar het onderstaande adres verzenden / QR-code sturen.\n\nDruk op bevestigen om door te gaan of terug te gaan om de bedragen te wijzigen.", @@ -309,7 +309,7 @@ "fill_code": "Vul de verificatiecode in die u in uw e-mail hebt ontvangen", "filter_by": "Filteren op", "first_wallet_text": "Geweldige portemonnee voor Monero, Bitcoin, Ethereum, Litecoin, en Haven", - "fixed_pair_not_supported": "Dit vaste paar wordt niet ondersteund bij de geselecteerde exchanges", + "fixed_pair_not_supported": "Dit vaste paar wordt niet ondersteund met de geselecteerde swap -services", "fixed_rate": "Vast tarief", "fixed_rate_alert": "U kunt het ontvangen bedrag invoeren wanneer de modus voor vaste tarieven is aangevinkt. Wilt u overschakelen naar de vaste-tariefmodus?", "forgot_password": "Wachtwoord vergeten", @@ -439,7 +439,7 @@ "node_test": "Test", "nodes": "Knooppunten", "nodes_list_reset_to_default_message": "Weet u zeker dat u de standaardinstellingen wilt herstellen?", - "none_of_selected_providers_can_exchange": "Geen van de geselecteerde providers kan deze uitwisseling maken", + "none_of_selected_providers_can_exchange": "Geen van de geselecteerde providers kan deze swap maken", "noNFTYet": "Nog geen NFT's", "normal": "Normaal", "note_optional": "Opmerking (optioneel)", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 4bf323c1c..91b265144 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -114,7 +114,7 @@ "change_currency": "Zmień walutę", "change_current_node": "Czy na pewno chcesz wybrać ten węzeł? ${node}?", "change_current_node_title": "Zmień bieżący węzeł", - "change_exchange_provider": "Zmień dostawcę wymiany", + "change_exchange_provider": "Zmień dostawca zamiany", "change_language": "Zmień język", "change_language_to": "Zmień język na ${language}?", "change_password": "Zmień hasło", @@ -281,8 +281,8 @@ "etherscan_history": "Historia Etherscanu", "event": "Wydarzenie", "events": "Wydarzenia", - "exchange": "Wymień", - "exchange_incorrect_current_wallet_for_xmr": "Jeśli chcesz wymienić XMR z salda Cake Wallet Monero, najpierw przełącz się na portfel Monero.", + "exchange": "Zamieniać", + "exchange_incorrect_current_wallet_for_xmr": "Jeśli chcesz zamienić XMR z salda Monero Portfer, najpierw przejdź na portfel Monero.", "exchange_new_template": "Nowy szablon wymiany", "exchange_provider_unsupported": "${providerName} nie jest już obsługiwany!", "exchange_result_confirm": "Naciskając Potwierdź, wyślesz ${fetchingLabel} ${from} z twojego portfela ${walletName} na adres podany poniżej. Lub możesz wysłać z zewnętrznego portfela na poniższy adres / kod QR.\n\nNaciśnij Potwierdź, aby kontynuować lub wróć, aby zmienić kwoty.", @@ -309,7 +309,7 @@ "fill_code": "Proszę wpisać kod weryfikacyjny który otrzymałeś w wiadomości e-mail", "filter_by": "Filtruj według", "first_wallet_text": "Świetny portfel na Monero, Bitcoin, Ethereum, Litecoin, i Haven", - "fixed_pair_not_supported": "Ta stała para nie jest obsługiwana na wybranych giełdach", + "fixed_pair_not_supported": "Ta stała para nie jest obsługiwana z wybranymi usługami swap", "fixed_rate": "Stała stawka", "fixed_rate_alert": "Będziesz mógł wprowadzić kwotę do otrzymania, gdy wybrany bedzie tryb stałego przeliczenia. Czy chcesz przejść do trybu stałej stawki?", "forgot_password": "Zapomniałem hasła", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 603dd75b7..524dbcace 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -114,7 +114,7 @@ "change_currency": "Alterar moeda", "change_current_node": "Você realmente deseja alterar o nó atual para ${node}?", "change_current_node_title": "Mudar o nó atual", - "change_exchange_provider": "Alterar o provedor de troca", + "change_exchange_provider": "Provedor de troca de alteração", "change_language": "Mudar idioma", "change_language_to": "Alterar idioma para ${language}?", "change_password": "Mudar senha", @@ -282,7 +282,7 @@ "event": "Evento", "events": "Eventos", "exchange": "Trocar", - "exchange_incorrect_current_wallet_for_xmr": "Se você deseja trocar o XMR de seu saldo da Carteira Monero Cake, troque primeiro para sua carteira Monero.", + "exchange_incorrect_current_wallet_for_xmr": "Se você deseja trocar o XMR do balanço da carteira de bolo, mude para a sua carteira Monero primeiro.", "exchange_new_template": "Novo modelo", "exchange_provider_unsupported": "${providerName} não é mais suportado!", "exchange_result_confirm": "Ao confirmar, você enviará ${fetchingLabel} ${from} da sua carteira ${walletName} para o endereço mostrado abaixo. Ou você pode enviar de sua carteira externa para o endereço abaixo/código QR acima.\n\nPressione Confirmar para continuar ou volte para alterar os valores.", @@ -309,7 +309,7 @@ "fill_code": "Por favor, preencha o código de verificação fornecido ao seu e-mail", "filter_by": "Filtrar por", "first_wallet_text": "Carteira incrível para Monero, Bitcoin, Ethereum, Litecoin, e Haven", - "fixed_pair_not_supported": "Este par fixo não é compatível com as exchanges selecionadas", + "fixed_pair_not_supported": "Este par fixo não é suportado com os serviços de troca selecionados", "fixed_rate": "Taxa fixa", "fixed_rate_alert": "Você poderá inserir a quantia recebida quando o modo de taxa fixa estiver marcado. Quer mudar para o modo de taxa fixa?", "forgot_password": "Esqueci a senha", @@ -440,7 +440,7 @@ "node_test": "Teste", "nodes": "Nós", "nodes_list_reset_to_default_message": "Você realmente deseja redefinir as configurações para o padrão?", - "none_of_selected_providers_can_exchange": "Nenhum dos provedores selecionados pode fazer esta troca", + "none_of_selected_providers_can_exchange": "Nenhum dos fornecedores selecionados pode fazer essa troca", "noNFTYet": "Ainda não há NFT", "normal": "Normal", "note_optional": "Nota (opcional)", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 43d912ffc..1a8c2447f 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -114,7 +114,7 @@ "change_currency": "Изменить валюту", "change_current_node": "Вы уверены, что хотите изменить текущую ноду на ${node}?", "change_current_node_title": "Изменить текущую ноду", - "change_exchange_provider": "Изменить провайдера обмена", + "change_exchange_provider": "Изменить поставщика свопа", "change_language": "Изменить язык", "change_language_to": "Изменить язык на ${language}?", "change_password": "Изменить пароль", @@ -281,8 +281,8 @@ "etherscan_history": "История Эфириума", "event": "Событие", "events": "События", - "exchange": "Обмен", - "exchange_incorrect_current_wallet_for_xmr": "Если вы хотите обменять XMR со своего баланса Monero в Cake Wallet, сначала переключитесь на свой кошелек Monero.", + "exchange": "Менять", + "exchange_incorrect_current_wallet_for_xmr": "Если вы хотите поменять XMR с баланса с кошельком для торта Monero, сначала переключитесь на свой кошелек Monero.", "exchange_new_template": "Новый шаблон", "exchange_provider_unsupported": "${providerName} больше не поддерживается!", "exchange_result_confirm": "Нажимая подтвердить, вы отправите ${fetchingLabel} ${from} с вашего кошелька ${walletName} на адрес указанный ниже. Или вы можете отправить со своего внешнего кошелька на нижеуказанный адрес/QR-код.\n\nПожалуйста, нажмите подтвердить для продолжения, или вернитесь назад для изменения суммы.", @@ -309,7 +309,7 @@ "fill_code": "Пожалуйста, введите код подтверждения, отправленный на вашу электронную почту", "filter_by": "Фильтровать по", "first_wallet_text": "В самом удобном кошельке для Monero, Bitcoin, Ethereum, Litecoin, и Haven", - "fixed_pair_not_supported": "Эта фиксированная пара не поддерживается выбранными биржами.", + "fixed_pair_not_supported": "Эта фиксированная пара не поддерживается выбранными службами свопа", "fixed_rate": "Фиксированная ставка", "fixed_rate_alert": "Вы сможете ввести сумму получения тогда, когда будет установлен режим фиксированной ставки. Вы хотите перейти в режим фиксированной ставки?", "forgot_password": "Забыли пароль", @@ -439,7 +439,7 @@ "node_test": "Тест", "nodes": "Ноды", "nodes_list_reset_to_default_message": "Вы уверены, что хотите сбросить настройки до значений по умолчанию?", - "none_of_selected_providers_can_exchange": "Ни один из выбранных провайдеров не может совершить этот обмен", + "none_of_selected_providers_can_exchange": "Ни один из выбранных провайдеров не может сделать это обмен", "noNFTYet": "NFT пока нет", "normal": "Нормальный", "note_optional": "Примечание (необязательно)", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index c02c7cc33..213f74530 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -282,7 +282,7 @@ "event": "เหตุการณ์", "events": "กิจกรรม", "exchange": "แลกเปลี่ยน", - "exchange_incorrect_current_wallet_for_xmr": "หากคุณต้องการแลกเปลี่ยน XMR จากยอดคงเหลือ Monero ใน Cake Wallet ของคุณ กรุณาเปลี่ยนเป็นกระเป๋า Monero ก่อน", + "exchange_incorrect_current_wallet_for_xmr": "หากคุณต้องการสลับ XMR จาก Cake Wallet Monero Balance โปรดเปลี่ยนไปใช้กระเป๋าเงิน Monero ก่อน", "exchange_new_template": "เทมเพลทใหม่", "exchange_provider_unsupported": "${providerName} ไม่ได้รับการสนับสนุนอีกต่อไป!", "exchange_result_confirm": "โดยกดปุ่มยืนยัน, คุณจะส่ง ${fetchingLabel} ${from} จากกระเป๋าของคุณที่เรียกว่า ${walletName} ไปยังที่อยู่ที่แสดงข้างล่าง หรือคุณสามารถส่งจากกระเป๋าภายนอกไปยังที่อยู่/รหัส QR ด้านล่าง\n\nโปรดกดปุ่มยืนยันเพื่อดำเนินการต่อหรือกลับไปเปลี่ยนจำนวน", @@ -309,7 +309,7 @@ "fill_code": "โปรดกรอกรหัสยืนยันที่ส่งไปยังอีเมลของคุณ", "filter_by": "กรองตาม", "first_wallet_text": "กระเป๋าสตางค์ที่สวยงามสำหรับ Monero, Bitcoin, Ethereum, Litecoin และ Haven", - "fixed_pair_not_supported": "คู่ความสัมพันธ์ที่ถูกกำหนดไว้นี้ไม่สนับสนุนกับหุ้นที่เลือก", + "fixed_pair_not_supported": "คู่คงที่นี้ไม่ได้รับการสนับสนุนด้วยบริการแลกเปลี่ยนที่เลือก", "fixed_rate": "อัตราคงที่", "fixed_rate_alert": "คุณจะสามารถป้อนจำนวนที่ได้รับเมื่อเลือกโหมดอัตราคงที่ คุณต้องการสลับไปที่โหมดอัตราคงที่?", "forgot_password": "ลืมรหัสผ่าน", @@ -439,7 +439,7 @@ "node_test": "ทดสอบ", "nodes": "โหนด", "nodes_list_reset_to_default_message": "คุณแน่ใจหรือว่าต้องการรีเซ็ตการตั้งค่าเป็นค่าเริ่มต้น?", - "none_of_selected_providers_can_exchange": "ไม่มีผู้ให้บริการที่เลือกที่สามารถแลกเปลี่ยนนี้ได้", + "none_of_selected_providers_can_exchange": "ผู้ให้บริการที่เลือกไม่สามารถทำการแลกเปลี่ยนนี้ได้", "noNFTYet": "ยังไม่มี NFT", "normal": "ปกติ", "note_optional": "บันทึก (ไม่จำเป็น)", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 99d22bb1e..0ca8ee665 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -114,7 +114,7 @@ "change_currency": "Baguhin ang pera", "change_current_node": "Sigurado ka bang baguhin ang kasalukuyang node sa ${node}?", "change_current_node_title": "Baguhin ang kasalukuyang node", - "change_exchange_provider": "Baguhin ang exchange provider", + "change_exchange_provider": "Baguhin ang Swap Provider", "change_language": "Baguhin ang wika", "change_language_to": "Baguhin ang wika sa ${language}?", "change_password": "Baguhin ang password", @@ -282,7 +282,7 @@ "event": "Kaganapan", "events": "Mga kaganapan", "exchange": "Palitan", - "exchange_incorrect_current_wallet_for_xmr": "Kung gusto mong palitan ang XMR mula sa iyong balanse ng Monero ng Cake Wallet, mangyaring lumipat muna sa iyong Monero wallet.", + "exchange_incorrect_current_wallet_for_xmr": "Kung nais mong magpalit ng XMR mula sa iyong balanse ng Wallet Monero, mangyaring lumipat sa iyong Monero Wallet muna.", "exchange_new_template": "Bagong template", "exchange_provider_unsupported": "Ang ${providerName} ay hindi na suportado!", "exchange_result_confirm": "Sa pamamagitan ng pagpindot sa kumpirmahin, ikaw ay magpapadala ${fetchingLabel} ${from} mula sa inyong wallet na tinatawag ${walletName} sa wallet na ipinapakita sa ibaba. O pwede kang magpadala sa inyong external wallet sa ibabang address/QR code.\n\nPara magpatuloy, mangyaring pindutin upang kumpirmahin o bumalik para baguhin ang halaga.", @@ -309,7 +309,7 @@ "fill_code": "Mangyaring ilagay ang verfification code na ibinigay sa iyong email", "filter_by": "Filter ni", "first_wallet_text": "Kahanga-hangang wallet para sa Monero, Bitcoin, Litecoin, Ethereum, at Haven", - "fixed_pair_not_supported": "Ang nakapirming pares na ito ay hindi suportado sa mga napiling palitan", + "fixed_pair_not_supported": "Ang nakapirming pares na ito ay hindi suportado sa mga napiling serbisyo ng pagpapalit", "fixed_rate": "Fixed rate", "fixed_rate_alert": "Makakapagpasok ka ng halaga ng pagtanggap kapag nasuri ang fixed rate mode. Gusto mo bang lumipat sa fixed rate mode?", "forgot_password": "Nakalimutan ang Password", @@ -439,7 +439,7 @@ "node_test": "Test", "nodes": "Mga node", "nodes_list_reset_to_default_message": "Sigurado ka bang gusto mo bang i-reset ang mga settings sa default?", - "none_of_selected_providers_can_exchange": "Wala sa mga napiling provider ang makakagawa ng palitan na ito", + "none_of_selected_providers_can_exchange": "Wala sa mga napiling tagapagbigay ng serbisyo ang maaaring gumawa ng pagpapalit na ito", "noNFTYet": "Wala pang NFT", "normal": "Normal", "note_optional": "Tala (opsyonal)", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 86239cd77..b23f64d60 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -114,7 +114,7 @@ "change_currency": "Para Birimini Değiştir", "change_current_node": "Şimdiki düğümü ${node} düğümüne değiştirmek istediğinizden emin misin?", "change_current_node_title": "Şimdiki düğümü değiştir", - "change_exchange_provider": "Takas sağlayıcısını değiştir", + "change_exchange_provider": "Takas Sağlayıcısı Değiştir", "change_language": "Dili değiştir", "change_language_to": "Dili şuna değiştir: ${language}?", "change_password": "Parolayı değiştir", @@ -282,7 +282,7 @@ "event": "Etkinlik", "events": "Olaylar", "exchange": "Takas", - "exchange_incorrect_current_wallet_for_xmr": "Cake Wallet'daki Monero bakiyenizi kullanarak takas yapmak istiyorsan, lütfen önce Monero cüzdanına geç.", + "exchange_incorrect_current_wallet_for_xmr": "XMR'yi kek cüzdanı Monero bakiyenizden değiştirmek istiyorsanız, lütfen önce Monero cüzdanınıza geçin.", "exchange_new_template": "Yeni şablon", "exchange_provider_unsupported": "${providerName} artık desteklenmiyor!", "exchange_result_confirm": "Onaylaya basarak, ${fetchingLabel} ${from} miktarında ${walletName} olarak adlandırılan cüzdanından aşağıda gösterilen adrese gönderilecek. Veya harici cüzdanından aşağıdaki adrese / QR koduna gönderebilirsin.\n\nLütfen devam etmek için onayla'ya bas veya tutarı değiştirmek için geri dön.", @@ -309,7 +309,7 @@ "fill_code": "Lütfen e-postanıza gelen doğrulama kodunu girin", "filter_by": "Şuna göre filtrele", "first_wallet_text": "Monero, Bitcoin, Ethereum, Litecoin ve Haven için harika cüzdan", - "fixed_pair_not_supported": "Bu sabit paritesi seçilen borsalarda desteklenmemekte", + "fixed_pair_not_supported": "Bu sabit çift seçilen takas hizmetleri ile desteklenmez", "fixed_rate": "Sabit oran", "fixed_rate_alert": "Sabit oran modunu işaretlersen alım tutarını girebilirsin. Sabit oran moduna geçmek ister misin?", "forgot_password": "Parolamı unuttum", @@ -439,7 +439,7 @@ "node_test": "Test Et", "nodes": "Düğümler", "nodes_list_reset_to_default_message": "Ayarları varsayılana sıfırlamak istediğinizden emin misin?", - "none_of_selected_providers_can_exchange": "Seçilen sağlayıcılardan hiçbiri bu takası yapamaz", + "none_of_selected_providers_can_exchange": "Seçilen sağlayıcıların hiçbiri bu takas yapamaz", "noNFTYet": "Henüz NFT yok", "normal": "Normal", "note_optional": "Not (isteğe bağlı)", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 2d049e12e..79dc0543f 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -114,7 +114,7 @@ "change_currency": "Змінити валюту", "change_current_node": "Ви впевнені, що хочете змінити поточний вузол на ${node}?", "change_current_node_title": "Змінити поточний вузол", - "change_exchange_provider": "Змінити провайдера обміну", + "change_exchange_provider": "Змінити постачальник свопів", "change_language": "Змінити мову", "change_language_to": "Змінити мову на ${language}?", "change_password": "Змінити пароль", @@ -281,8 +281,8 @@ "etherscan_history": "Історія Etherscan", "event": "Подія", "events": "Події", - "exchange": "Обмін", - "exchange_incorrect_current_wallet_for_xmr": "Якщо ви хочете обміняти XMR із вашого балансу Cake Wallet Monero, спочатку перейдіть на свій гаманець Monero.", + "exchange": "Обміняти", + "exchange_incorrect_current_wallet_for_xmr": "Якщо ви хочете поміняти XMR зі свого балансу для тортів Monero Balance, спочатку перейдіть на свій гаманець Monero.", "exchange_new_template": "Новий шаблон", "exchange_provider_unsupported": "${providerName} більше не підтримується!", "exchange_result_confirm": "Натиснувши підтвердити, ви відправите ${fetchingLabel} ${from} з вашого гаманця ${walletName} на адресу вказану нижче. Або ви можете відправити зі свого зовнішнього гаманця на нижчевказану адресу/QR-код.\n\nБудь ласка, натисніть підтвердити для продовження або поверніться назад щоб змінити суму.", @@ -309,7 +309,7 @@ "fill_code": "Будь ласка, введіть код підтвердження, надісланий на вашу електронну адресу", "filter_by": "Фільтрувати по", "first_wallet_text": "В самому зручному гаманці для Monero, Bitcoin, Ethereum, Litecoin, та Haven", - "fixed_pair_not_supported": "Ця фіксована пара не підтримується вибраними біржами", + "fixed_pair_not_supported": "Ця фіксована пара не підтримується вибраними послугами Swap", "fixed_rate": "Фіксована ставка", "fixed_rate_alert": "Ви зможете ввести суму отримання тоді, коли буде встановлений режим фіксованої ставки. Ви хочете перейти в режим фіксованої ставки?", "forgot_password": "Забули пароль", @@ -439,7 +439,7 @@ "node_test": "Тест", "nodes": "Вузли", "nodes_list_reset_to_default_message": "Ви впевнені, що хочете скинути до налаштувань за замовченням?", - "none_of_selected_providers_can_exchange": "Жоден із вибраних провайдерів не може здійснити цей обмін", + "none_of_selected_providers_can_exchange": "Жоден із вибраних постачальників не може зробити цей своп", "noNFTYet": "NFT ще немає", "normal": "нормальний", "note_optional": "Примітка (необов’язково)", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index f080c23a5..0a136d140 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -114,7 +114,7 @@ "change_currency": "کرنسی تبدیل کریں", "change_current_node": "کیا آپ یقینی طور پر موجودہ نوڈ کو ${node} میں تبدیل کرنا چاہتے ہیں؟", "change_current_node_title": "موجودہ نوڈ کو تبدیل کریں۔", - "change_exchange_provider": "ایکسچینج فراہم کنندہ کو تبدیل کریں۔", + "change_exchange_provider": "تبادلہ فراہم کرنے والے کو تبدیل کریں", "change_language": "زبان تبدیل کریں", "change_language_to": "زبان کو ${language} میں تبدیل کریں؟", "change_password": "پاس ورڈ تبدیل کریں", @@ -282,7 +282,7 @@ "event": "ﺐﯾﺮﻘﺗ", "events": "ﺕﺎﺒﯾﺮﻘﺗ", "exchange": "تبادلہ", - "exchange_incorrect_current_wallet_for_xmr": "اگر آپ اپنے Cake والیٹ Monero بیلنس سے XMR کا تبادلہ کرنا چاہتے ہیں، تو براہ کرم پہلے اپنے Monero والیٹ پر جائیں۔", + "exchange_incorrect_current_wallet_for_xmr": "اگر آپ اپنے کیک پرس مونیرو بیلنس سے XMR تبدیل کرنا چاہتے ہیں تو ، براہ کرم پہلے اپنے مونیرو پرس میں جائیں۔", "exchange_new_template": "نیا سانچہ", "exchange_provider_unsupported": "${providerName} اب تعاون نہیں کیا جاتا ہے!", "exchange_result_confirm": "تصدیق کو دبانے سے، آپ اپنے بٹوے سے ${fetchingLabel} ${from} بھیجیں گے جسے ${walletName} کہتے ہیں نیچے دکھائے گئے پتے پر۔ یا آپ اپنے بیرونی والیٹ سے نیچے دیئے گئے پتے/QR کوڈ پر بھیج سکتے ہیں۔\\n\\nجاری رکھنے کے لیے براہ کرم تصدیق کو دبائیں یا رقم تبدیل کرنے کے لیے واپس جائیں۔", @@ -309,7 +309,7 @@ "fill_code": "براہ کرم اپنے ای میل پر فراہم کردہ تصدیقی کوڈ کو پُر کریں۔", "filter_by": "کی طرف سے فلٹر", "first_wallet_text": "Monero، Bitcoin، Ethereum، Litecoin، اور Haven کے لیے زبردست پرس", - "fixed_pair_not_supported": "یہ مقررہ جوڑا منتخب کردہ تبادلے کے ساتھ تعاون یافتہ نہیں ہے۔", + "fixed_pair_not_supported": "یہ فکسڈ جوڑی منتخب شدہ تبادلہ خدمات کے ساتھ تعاون یافتہ نہیں ہے", "fixed_rate": "مقررہ شرح", "fixed_rate_alert": "فکسڈ ریٹ موڈ چیک ہونے پر آپ وصولی رقم درج کر سکیں گے۔ کیا آپ فکسڈ ریٹ موڈ پر سوئچ کرنا چاہتے ہیں؟", "forgot_password": "پاسورڈ بھول گے", @@ -439,7 +439,7 @@ "node_test": "پرکھ", "nodes": "نوڈس", "nodes_list_reset_to_default_message": "کیا آپ واقعی ترتیبات کو ڈیفالٹ پر دوبارہ ترتیب دینا چاہتے ہیں؟", - "none_of_selected_providers_can_exchange": "منتخب فراہم کنندگان میں سے کوئی بھی یہ تبادلہ نہیں کر سکتا", + "none_of_selected_providers_can_exchange": "منتخب کردہ کوئی بھی فراہم کنندہ یہ تبادلہ نہیں کرسکتا", "noNFTYet": "۔ﮟﯿﮨ ﮟﯿﮩﻧ NFTs ﯽﺋﻮﮐ ﮏﺗ ﯽﮭﺑﺍ", "normal": "نارمل", "note_optional": "نوٹ (اختیاری)", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 63db7ce88..8d28d48a2 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -114,7 +114,7 @@ "change_currency": "Thay đổi Tiền tệ", "change_current_node": "Bạn có chắc chắn muốn thay đổi nút hiện tại sang ${node} không?", "change_current_node_title": "Thay đổi nút hiện tại", - "change_exchange_provider": "Thay đổi Nhà cung cấp Trao đổi", + "change_exchange_provider": "Thay đổi nhà cung cấp hoán đổi", "change_language": "Thay đổi ngôn ngữ", "change_language_to": "Thay đổi ngôn ngữ sang ${language}?", "change_password": "Thay đổi mật khẩu", @@ -282,8 +282,8 @@ "etherscan_history": "Lịch sử Etherscan", "event": "Sự kiện", "events": "Các sự kiện", - "exchange": "Trao đổi", - "exchange_incorrect_current_wallet_for_xmr": "Nếu bạn muốn trao đổi XMR từ số dư Monero của Ví Cake, vui lòng chuyển sang ví Monero của bạn trước.", + "exchange": "Tráo đổi", + "exchange_incorrect_current_wallet_for_xmr": "Nếu bạn muốn trao đổi XMR từ CAPE CAME MONERO BALANCE, vui lòng chuyển sang ví Monero của bạn trước.", "exchange_new_template": "Mẫu mới", "exchange_provider_unsupported": "${providerName} không còn được hỗ trợ nữa!", "exchange_result_confirm": "Bằng cách nhấn xác nhận, bạn sẽ gửi ${fetchingLabel} ${from} từ ví có tên ${walletName} của mình đến địa chỉ dưới đây. Hoặc bạn có thể gửi từ ví bên ngoài của mình đến địa chỉ/mã QR bên dưới.\n\nVui lòng nhấn xác nhận để tiếp tục hoặc quay lại để thay đổi số tiền.", @@ -310,7 +310,7 @@ "fill_code": "Vui lòng điền mã xác minh được gửi đến email của bạn", "filter_by": "Lọc theo", "first_wallet_text": "Ví tuyệt vời cho Monero, Bitcoin, Ethereum, Litecoin, và Haven", - "fixed_pair_not_supported": "Cặp tỷ giá cố định này không được hỗ trợ với các sàn giao dịch đã chọn", + "fixed_pair_not_supported": "Cặp cố định này không được hỗ trợ với các dịch vụ hoán đổi đã chọn", "fixed_rate": "Tỷ giá cố định", "fixed_rate_alert": "Bạn sẽ có thể nhập số lượng nhận được khi chế độ tỷ giá cố định được chọn. Bạn có muốn chuyển sang chế độ tỷ giá cố định không?", "forgot_password": "Quên mật khẩu", @@ -432,7 +432,7 @@ "node_test": "Kiểm tra", "nodes": "Các nút", "nodes_list_reset_to_default_message": "Bạn có chắc chắn muốn đặt lại cài đặt về mặc định không?", - "none_of_selected_providers_can_exchange": "Không có nhà cung cấp nào đã chọn có thể thực hiện giao dịch này", + "none_of_selected_providers_can_exchange": "Không có nhà cung cấp nào được chọn có thể thực hiện hoán đổi này", "noNFTYet": "Chưa có NFT", "normal": "Bình thường", "note_optional": "Ghi chú (tùy chọn)", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 16a887a39..14270120c 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -114,7 +114,7 @@ "change_currency": "Pààrọ̀ irú owó", "change_current_node": "Ṣé ó dá yín lójú pé ẹ fẹ́ pààrọ̀ apẹka lọ́wọ́ sí ${node}?", "change_current_node_title": "Pààrọ̀ apẹka lọwọ́", - "change_exchange_provider": "Pààrọ̀ Ilé Ìfowóṣòwò", + "change_exchange_provider": "Yipada olupese Swap", "change_language": "Pààrọ̀ èdè", "change_language_to": "Pààrọ̀ èdè sí ${language}?", "change_password": "Pààrọ̀ ọ̀rọ̀ aṣínà", @@ -282,8 +282,8 @@ "etherscan_history": "Etherscan itan", "event": "Iṣẹlẹ", "events": "Awọn iṣẹlẹ", - "exchange": "Pàṣípààrọ̀", - "exchange_incorrect_current_wallet_for_xmr": "T'ẹ́ bá fẹ́ pàṣípààrọ̀ XMR láti ìyókù owó Cake Wallet yín, ẹ jọ̀wọ́ kọ́kọ́ sún àpamọ́wọ́ Monero mọ́.", + "exchange": "Eepo", + "exchange_incorrect_current_wallet_for_xmr": "Ti o ba fẹ lati yi XMR lati dọgba oyinbo oyinbo kekere rẹ ti a fi omi ṣan rẹ, jọwọ yipada si apamọwọ Monrou akọkọ.", "exchange_new_template": "Àwòṣe títun", "exchange_provider_unsupported": "${providerName} ko ni atilẹyin mọ!", "exchange_result_confirm": "T'ẹ́ bá tẹ̀ jẹ́rìí, ẹ máa fi ${fetchingLabel} ${from} ránṣẹ́ láti àpamọ́wọ́ yín t'á pe ${walletName} sí àdírẹ́sì t'ó ṣàfihàn òun lísàlẹ̀. Tàbí ẹ lè fi àpamọ́wọ́ mìíràn yín ránṣẹ́ sí àdírẹ́sì / àmì ìlujá lísàlẹ̀.\n\nẸ jọ̀wọ́ tẹ̀ jẹ́rìí́ tẹ̀síwájú tàbí padà sọ́dọ̀ pààrọ̀ iye náà.", @@ -310,7 +310,7 @@ "fill_code": "Ẹ jọ̀wọ́ tẹ̀ ọ̀rọ̀ ìjẹ́rìísí t'á ti ránṣẹ́ sí ímeèlì yín.", "filter_by": "Ṣẹ́ láti", "first_wallet_text": "Àpamọ́wọ́ t'á fi Monero, Bitcoin, Ethereum, Litecoin, àti Haven pamọ́ wà pa", - "fixed_pair_not_supported": "A kì í ṣe k'á fi àwọn ilé pàṣípààrọ̀ yìí ṣe pàṣípààrọ̀ irú owó méji yìí", + "fixed_pair_not_supported": "Bata ti o wa titi ko ṣe atilẹyin pẹlu awọn iṣẹ SWAP ti o yan", "fixed_rate": "Iye t'á ṣẹ́ owó sí ò ní pààrọ̀", "fixed_rate_alert": "Ẹ lè tẹ̀ iye owó tó ń bọ̀ tí iye t'a ṣẹ́ owó sí bá is checked. Ṣé ẹ fẹ́ sún ipò ti iye t'á ṣẹ́ owó sí ò ní pààrọ̀ mọ́?", "forgot_password": "Ẹ ti gbàgbé ọ̀rọ̀ aṣínà", @@ -440,7 +440,7 @@ "node_test": "Dánwò", "nodes": "Àwọn apẹka", "nodes_list_reset_to_default_message": "Ṣé ó dá yín lójú pé ẹ fẹ́ yí àwọn ààtò padà?", - "none_of_selected_providers_can_exchange": "Àwọn ilé pàṣípààrọ̀ yíyàn kò lè ṣe pàṣípààrọ̀ yìí", + "none_of_selected_providers_can_exchange": "Ko si ọkan ninu awọn olupese ti a yan le ṣe ina yii", "noNFTYet": "Ko si awọn NFT sibẹsibẹ", "normal": "Deede", "note_optional": "Àkọsílẹ̀ (ìyàn nìyí)", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 9398e2408..65047b4fe 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -114,7 +114,7 @@ "change_currency": "更改币种", "change_current_node": "您确定将当前节点更改为 ${node}?", "change_current_node_title": "更改当前节点", - "change_exchange_provider": "更改交易所", + "change_exchange_provider": "更改交换提供商", "change_language": "修改语言", "change_language_to": "修改语言为 ${language}?", "change_password": "更改密码", @@ -281,8 +281,8 @@ "etherscan_history": "以太扫描历史", "event": "事件", "events": "活动", - "exchange": "兑换", - "exchange_incorrect_current_wallet_for_xmr": "如果要从Cake Wallet Monero余额中兑换XMR,请先切换到Monero钱包。", + "exchange": "交换", + "exchange_incorrect_current_wallet_for_xmr": "如果您想从蛋糕钱包Monero余额中交换XMR,请先切换到Monero Wallet。", "exchange_new_template": "新模板", "exchange_provider_unsupported": "${providerName}不再支持!", "exchange_result_confirm": "点击确认 您将发送 ${fetchingLabel} ${from} 从你的钱包里 ${walletName} 到下面显示的地址。 或者您可以从外部钱包发送到以下地址/ QR码。\n\n请按确认继续或返回以更改金额", @@ -309,7 +309,7 @@ "fill_code": "请填写提供给您邮箱的验证码", "filter_by": "过滤", "first_wallet_text": "适用于门罗币、比特币、以太坊、莱特币和避风港的超棒钱包", - "fixed_pair_not_supported": "所选交易所不支持此固定货币对", + "fixed_pair_not_supported": "所选的交换服务不支持这对固定对", "fixed_rate": "固定汇率", "fixed_rate_alert": "选中固定汇率模式后,您将可以输入接收金额。 您要切换到固定汇率模式吗?", "forgot_password": "忘记密码", @@ -439,7 +439,7 @@ "node_test": "测试", "nodes": "节点", "nodes_list_reset_to_default_message": "您确定要将设置重设为默认值吗?", - "none_of_selected_providers_can_exchange": "选定的供应商都不能进行此交换", + "none_of_selected_providers_can_exchange": "选定的提供商都无法进行此交换", "noNFTYet": "还没有 NFT", "normal": "普通的", "note_optional": "注释(可选)", From c3019b91cd7c73c3ed7f96da4ddfe68158cf0500 Mon Sep 17 00:00:00 2001 From: tuxsudo Date: Mon, 14 Oct 2024 19:10:48 -0400 Subject: [PATCH 16/73] Harden the Android manifest (#1741) * Test with Android manifest modifications * Commit correct changes * Remove permission that was already present --- android/app/src/main/AndroidManifestBase.xml | 37 +++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index b03c8a925..1d0e2c93c 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -1,35 +1,30 @@ - - - - - + + - - + - - - + + + - - - + + + + - + + + + - + + Date: Wed, 16 Oct 2024 02:58:41 +0100 Subject: [PATCH 17/73] fix: Issue with nownodes on older wallets (#1750) --- lib/entities/default_settings_migration.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 1c46cd605..2b8c49631 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -254,6 +254,7 @@ Future defaultSettingsMigration( case 41: _deselectQuantex(sharedPreferences); await _addSethNode(nodes, sharedPreferences); + await updateTronNodesWithNowNodes(sharedPreferences: sharedPreferences, nodes: nodes); break; default: break; @@ -1317,3 +1318,16 @@ Future removeMoneroWorld( await changeMoneroCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); } } + +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); +} From cb75f08b35617452ed4a6013172494104c6bfabc Mon Sep 17 00:00:00 2001 From: Serhii Date: Wed, 16 Oct 2024 16:28:24 +0300 Subject: [PATCH 18/73] fix P2wpkhAddress btc ltc validation (#1749) * fix P2wpkhAddress btc ltc validation * Update address_validator.dart --- lib/core/address_validator.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 0aabfa032..21726fab8 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -36,9 +36,9 @@ class AddressValidator extends TextValidator { '|[0-9a-zA-Z]{105}|addr1[0-9a-zA-Z]{98}'; case CryptoCurrency.btc: pattern = - '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}'; + '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${RegExp(r'(bc|tb)1q[ac-hj-np-z02-9]{25,39}}').pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}'; case CryptoCurrency.ltc: - pattern = '^${P2wpkhAddress.regex.pattern}\$|^${MwebAddress.regex.pattern}\$'; + pattern = '^${RegExp(r'ltc1q[ac-hj-np-z02-9]{25,39}').pattern}\$|^${MwebAddress.regex.pattern}\$'; case CryptoCurrency.nano: pattern = '[0-9a-zA-Z_]+'; case CryptoCurrency.banano: From 605d164998c0f964267861edfff1972c88e15ab1 Mon Sep 17 00:00:00 2001 From: Serhii Date: Wed, 16 Oct 2024 20:25:22 +0300 Subject: [PATCH 19/73] Cw 772 restore from qr generates different wallet (#1742) * fix derivation info for QR restoring * allow all available seed languages for Monero * set default derivation info for an empty wallet * fix electrum case --- .../restore/restore_from_qr_vm.dart | 12 +++++- lib/view_model/wallet_creation_vm.dart | 41 +++++++++++-------- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/lib/view_model/restore/restore_from_qr_vm.dart b/lib/view_model/restore/restore_from_qr_vm.dart index 042a048d9..cbdad85b8 100644 --- a/lib/view_model/restore/restore_from_qr_vm.dart +++ b/lib/view_model/restore/restore_from_qr_vm.dart @@ -37,7 +37,8 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store spendKey = '', wif = '', address = '', - super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, type: type, isRecovery: true); + super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, + type: type, isRecovery: true); @observable int height; @@ -112,7 +113,14 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store ); case WalletType.bitcoin: case WalletType.litecoin: - final derivationInfo = (await getDerivationInfoFromQRCredentials(restoreWallet)).first; + + final derivationInfoList = await getDerivationInfoFromQRCredentials(restoreWallet); + DerivationInfo derivationInfo; + if (derivationInfoList.isEmpty) { + derivationInfo = getDefaultCreateDerivation()!; + } else { + derivationInfo = derivationInfoList.first; + } return bitcoin!.createBitcoinRestoreWalletFromSeedCredentials( name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index f31f879a1..90b4c5c3a 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -194,24 +194,31 @@ abstract class WalletCreationVMBase with Store { final walletType = restoreWallet.type; var appStore = getIt.get(); var node = appStore.settingsStore.getCurrentNode(walletType); + + switch (walletType) { + case WalletType.bitcoin: + case WalletType.litecoin: - switch (walletType) { - case WalletType.bitcoin: - case WalletType.litecoin: - return bitcoin!.getDerivationsFromMnemonic( - mnemonic: restoreWallet.mnemonicSeed!, - node: node, - passphrase: restoreWallet.passphrase, - ); - case WalletType.nano: - return nanoUtil!.getDerivationsFromMnemonic( - mnemonic: restoreWallet.mnemonicSeed!, - node: node, - ); - default: - break; - } - return list; + final derivationList = await bitcoin!.getDerivationsFromMnemonic( + mnemonic: restoreWallet.mnemonicSeed!, + node: node, + passphrase: restoreWallet.passphrase, + ); + + + if (derivationList.first.transactionsCount == 0 && derivationList.length > 1) return []; + + return derivationList; + + case WalletType.nano: + return nanoUtil!.getDerivationsFromMnemonic( + mnemonic: restoreWallet.mnemonicSeed!, + node: node, + ); + default: + break; + } + return list; } WalletCredentials getCredentials(dynamic options) => throw UnimplementedError(); From 564f8e2c70c3e377db985097680c377a034e72a9 Mon Sep 17 00:00:00 2001 From: cyan Date: Wed, 16 Oct 2024 19:27:04 +0200 Subject: [PATCH 20/73] fix backup on debug builds (#1728) open transactionDecscriptionBox if it's not open --- lib/core/backup_service.dart | 38 ++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index ba90a0449..d65530eb5 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:cake_wallet/core/secure_storage.dart'; +import 'package:cake_wallet/entities/get_encryption_key.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; import 'package:cake_wallet/themes/theme_list.dart'; import 'package:cw_core/root_dir.dart'; @@ -105,7 +106,15 @@ class BackupService { 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/")) { + print("ignoring backup file: $filename"); + return; + } + } + print("restoring: $filename"); if (entity.statSync().type == FileSystemEntityType.directory) { zipEncoder.addDirectory(Directory(entity.path)); } else { @@ -148,14 +157,29 @@ class BackupService { await _importPreferencesDump(); } + // checked with .endsWith - so this should be the last part of the filename + static const ignoreFiles = [ + "flutter_assets/kernel_blob.bin", + "flutter_assets/vm_snapshot_data", + "flutter_assets/isolate_snapshot_data", + ".lock", + ]; + Future _importBackupV2(Uint8List data, String password) async { final appDir = await getAppDir(); final decryptedData = await _decryptV2(data, password); final zip = ZipDecoder().decodeBytes(decryptedData); + outer: for (var file in zip.files) { final filename = file.name; - + for (var ignore in ignoreFiles) { + if (filename.endsWith(ignore) && !filename.contains("wallets/")) { + print("ignoring backup file: $filename"); + continue outer; + } + } + print("restoring: $filename"); if (file.isFile) { final content = file.content as List; File('${appDir.path}/' + filename) @@ -206,6 +230,16 @@ class BackupService { json.decode(transactionDescriptionFile.readAsStringSync()) as Map; final descriptionsMap = jsonData.map((key, value) => MapEntry(key, TransactionDescription.fromJson(value as Map))); + + if (!_transactionDescriptionBox.isOpen) { + final transactionDescriptionsBoxKey = await getEncryptionKey(secureStorage: secureStorageShared, forKey: TransactionDescription.boxKey); + final transactionDescriptionBox = await CakeHive.openBox( + TransactionDescription.boxName, + encryptionKey: transactionDescriptionsBoxKey, + ); + await transactionDescriptionBox.putAll(descriptionsMap); + return; + } await _transactionDescriptionBox.putAll(descriptionsMap); } From cd5a1277a7759e11d3f5d0714abb3b8df8e463db Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Thu, 17 Oct 2024 02:17:29 +0100 Subject: [PATCH 21/73] Disable sendAll on native EVM Currencies (#1753) * fix: Issue with nownodes on older wallets * fix: Disable sendAll on native evm currencies --- lib/view_model/send/send_view_model.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 0ad8ba376..334394a18 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -123,7 +123,17 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor @computed bool get isBatchSending => outputs.length > 1; - bool get shouldDisplaySendALL => walletType != WalletType.solana; + bool get shouldDisplaySendALL { + if (walletType == WalletType.solana) return false; + + if (walletType == WalletType.ethereum && selectedCryptoCurrency == CryptoCurrency.eth) + return false; + + if (walletType == WalletType.polygon && selectedCryptoCurrency == CryptoCurrency.matic) + return false; + + return true; + } @computed String get pendingTransactionFiatAmount { From 6c9de67599fb0612ee95a87ab644e8b83ffb858e Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Thu, 17 Oct 2024 14:52:55 +0200 Subject: [PATCH 22/73] Fix fiat rate on receive screen. (#1754) * Fix fiat rate on receive screen. * Code cleanup * Update Receive QR on currency change --- .../wallet_address_list_view_model.dart | 231 ++++++++++-------- 1 file changed, 133 insertions(+), 98 deletions(-) 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 0260dc6b4..3e399266a 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 @@ -1,14 +1,15 @@ -import 'dart:math'; +import 'dart:developer' as dev; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/core/fiat_conversion_service.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.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/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/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/store/app_store.dart'; @@ -24,16 +25,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/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cw_monero/api/wallet.dart'; -import 'package:flutter/foundation.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}); @@ -43,12 +42,11 @@ abstract class PaymentURI { } class MoneroURI extends PaymentURI { - MoneroURI({required String amount, required String address}) - : super(amount: amount, address: address); + MoneroURI({required super.amount, required super.address}); @override String toString() { - var base = 'monero:' + address; + var base = 'monero:$address'; if (amount.isNotEmpty) { base += '?tx_amount=${amount.replaceAll(',', '.')}'; @@ -59,12 +57,11 @@ class MoneroURI extends PaymentURI { } class HavenURI extends PaymentURI { - HavenURI({required String amount, required String address}) - : super(amount: amount, address: address); + HavenURI({required super.amount, required super.address}); @override String toString() { - var base = 'haven:' + address; + var base = 'haven:$address'; if (amount.isNotEmpty) { base += '?tx_amount=${amount.replaceAll(',', '.')}'; @@ -75,12 +72,11 @@ class HavenURI extends PaymentURI { } class BitcoinURI extends PaymentURI { - BitcoinURI({required String amount, required String address}) - : super(amount: amount, address: address); + BitcoinURI({required super.amount, required super.address}); @override String toString() { - var base = 'bitcoin:' + address; + var base = 'bitcoin:$address'; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; @@ -91,12 +87,11 @@ class BitcoinURI extends PaymentURI { } class LitecoinURI extends PaymentURI { - LitecoinURI({required String amount, required String address}) - : super(amount: amount, address: address); + LitecoinURI({required super.amount, required super.address}); @override String toString() { - var base = 'litecoin:' + address; + var base = 'litecoin:$address'; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; @@ -107,12 +102,11 @@ class LitecoinURI extends PaymentURI { } class EthereumURI extends PaymentURI { - EthereumURI({required String amount, required String address}) - : super(amount: amount, address: address); + EthereumURI({required super.amount, required super.address}); @override String toString() { - var base = 'ethereum:' + address; + var base = 'ethereum:$address'; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; @@ -123,8 +117,7 @@ class EthereumURI extends PaymentURI { } class BitcoinCashURI extends PaymentURI { - BitcoinCashURI({required String amount, required String address}) - : super(amount: amount, address: address); + BitcoinCashURI({required super.amount, required super.address}); @override String toString() { @@ -139,12 +132,11 @@ class BitcoinCashURI extends PaymentURI { } class NanoURI extends PaymentURI { - NanoURI({required String amount, required String address}) - : super(amount: amount, address: address); + NanoURI({required super.amount, required super.address}); @override String toString() { - var base = 'nano:' + address; + var base = 'nano:$address'; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; } @@ -154,12 +146,11 @@ class NanoURI extends PaymentURI { } class PolygonURI extends PaymentURI { - PolygonURI({required String amount, required String address}) - : super(amount: amount, address: address); + PolygonURI({required super.amount, required super.address}); @override String toString() { - var base = 'polygon:' + address; + var base = 'polygon:$address'; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; @@ -170,12 +161,12 @@ class PolygonURI extends PaymentURI { } class SolanaURI extends PaymentURI { - SolanaURI({required String amount, required String address}) - : super(amount: amount, address: address); + SolanaURI({required super.amount, required super.address}); @override String toString() { - var base = 'solana:' + address; + var base = 'solana:$address'; + if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; } @@ -185,12 +176,12 @@ class SolanaURI extends PaymentURI { } class TronURI extends PaymentURI { - TronURI({required String amount, required String address}) - : super(amount: amount, address: address); + TronURI({required super.amount, required super.address}); @override String toString() { - var base = 'tron:' + address; + var base = 'tron:$address'; + if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; } @@ -200,12 +191,11 @@ class TronURI extends PaymentURI { } class WowneroURI extends PaymentURI { - WowneroURI({required String amount, required String address}) - : super(amount: amount, address: address); + WowneroURI({required super.amount, required super.address}); @override String toString() { - var base = 'wownero:' + address; + var base = 'wownero:$address'; if (amount.isNotEmpty) { base += '?tx_amount=${amount.replaceAll(',', '.')}'; @@ -215,7 +205,8 @@ class WowneroURI extends PaymentURI { } } -abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store { +abstract class WalletAddressListViewModelBase + extends WalletChangeListenerViewModel with Store { WalletAddressListViewModelBase({ required AppStore appStore, required this.yatStore, @@ -223,9 +214,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo }) : _baseItems = [], selectedCurrency = walletTypeToCryptoCurrency(appStore.wallet!.type), _cryptoNumberFormat = NumberFormat(_cryptoNumberPattern), - hasAccounts = appStore.wallet!.type == WalletType.monero || - appStore.wallet!.type == WalletType.wownero || - appStore.wallet!.type == WalletType.haven, + hasAccounts = [WalletType.monero, WalletType.wownero, WalletType.haven] + .contains(appStore.wallet!.type), amount = '', _settingsStore = appStore.settingsStore, super(appStore: appStore) { @@ -237,9 +227,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo _init(); selectedCurrency = walletTypeToCryptoCurrency(wallet.type); - hasAccounts = wallet.type == WalletType.monero || - wallet.type == WalletType.wownero || - wallet.type == WalletType.haven; + hasAccounts = [WalletType.monero, WalletType.wownero, WalletType.haven] + .contains(wallet.type); } static const String _cryptoNumberPattern = '0.00000000'; @@ -249,7 +238,11 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo final FiatConversionStore fiatConversionStore; final SettingsStore _settingsStore; - List get currencies => [walletTypeToCryptoCurrency(wallet.type), ...FiatCurrency.all]; + double? _fiatRate; + String _rawAmount = ''; + + List get currencies => + [walletTypeToCryptoCurrency(wallet.type), ...FiatCurrency.all]; String get buttonTitle { if (isElectrumWallet) { @@ -275,9 +268,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo WalletType get type => wallet.type; @computed - WalletAddressListItem get address { - return WalletAddressListItem(address: wallet.walletAddresses.address, isPrimary: false); - } + WalletAddressListItem get address => WalletAddressListItem( + address: wallet.walletAddresses.address, isPrimary: false); @computed PaymentURI get uri { @@ -321,8 +313,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo 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( @@ -338,8 +332,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo } 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( @@ -352,8 +348,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo } 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( @@ -367,7 +365,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo 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( @@ -418,7 +417,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo 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; } @@ -432,19 +432,22 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo 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) { @@ -458,18 +461,21 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo 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); } @@ -487,7 +493,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo Future toggleHideAddress(WalletAddressListItem item) async { if (item.isHidden) { - wallet.walletAddresses.hiddenAddresses.removeWhere((element) => element == item.address); + wallet.walletAddresses.hiddenAddresses + .removeWhere((element) => element == item.address); } else { wallet.walletAddresses.hiddenAddresses.add(item.address); } @@ -512,57 +519,58 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @computed String get accountLabel { - if (wallet.type == WalletType.monero) { - return monero!.getCurrentAccount(wallet).label; + switch (wallet.type) { + case WalletType.monero: + return monero!.getCurrentAccount(wallet).label; + case WalletType.wownero: + wownero!.getCurrentAccount(wallet).label; + case WalletType.haven: + return haven!.getCurrentAccount(wallet).label; + default: + return ''; } - - if (wallet.type == WalletType.wownero) { - return wownero!.getCurrentAccount(wallet).label; - } - - if (wallet.type == WalletType.haven) { - return haven!.getCurrentAccount(wallet).label; - } - return ''; } @computed - bool get hasAddressList => - wallet.type == WalletType.monero || - wallet.type == WalletType.wownero || - wallet.type == WalletType.haven || - wallet.type == WalletType.bitcoinCash || - wallet.type == WalletType.bitcoin || - wallet.type == WalletType.litecoin; + bool get hasAddressList => [ + WalletType.monero, + WalletType.wownero, + WalletType.haven, + WalletType.bitcoinCash, + WalletType.bitcoin, + WalletType.litecoin + ].contains(wallet.type); @computed - bool get isElectrumWallet => - wallet.type == WalletType.bitcoin || - wallet.type == WalletType.litecoin || - wallet.type == WalletType.bitcoinCash; + bool get isElectrumWallet => [ + WalletType.bitcoin, + WalletType.litecoin, + WalletType.bitcoinCash + ].contains(wallet.type); @computed bool get isBalanceAvailable => isElectrumWallet; @computed bool get isReceivedAvailable => - wallet.type == WalletType.monero || wallet.type == WalletType.wownero; + [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 bool get showAddManualAddresses => !isAutoGenerateSubaddressEnabled || - wallet.type == WalletType.monero || - wallet.type == WalletType.wownero; + [WalletType.monero, WalletType.wownero].contains(wallet.type); List _baseItems; @@ -574,7 +582,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @action Future setAddressType(dynamic option) async { - if (wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin) { + if ([WalletType.bitcoin, WalletType.litecoin].contains(wallet.type)) { await bitcoin!.setAddressType(wallet, option); } } @@ -586,13 +594,15 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo _baseItems.add(WalletAddressHiddenListHeader()); } - if (wallet.type == WalletType.monero || - wallet.type == WalletType.wownero || - wallet.type == WalletType.haven) { + if ([ + WalletType.monero, + WalletType.wownero, + WalletType.haven, + ].contains(wallet.type)) { _baseItems.add(WalletAccountListHeader()); } - if (wallet.type != WalletType.nano && wallet.type != WalletType.banano) { + if (![WalletType.nano, WalletType.banano].contains(wallet.type)) { _baseItems.add(WalletAddressListHeader()); } if (wallet.isEnabledAutoGenerateSubaddress) { @@ -603,11 +613,27 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @action void selectCurrency(Currency currency) { selectedCurrency = currency; + + if (currency is FiatCurrency && _settingsStore.fiatCurrency != currency) { + final cryptoCurrency = walletTypeToCryptoCurrency(wallet.type); + + dev.log("Requesting Fiat rate for $cryptoCurrency-$currency"); + FiatConversionService.fetchPrice( + crypto: cryptoCurrency, + fiat: currency, + torOnly: _settingsStore.fiatApiMode == FiatApiMode.torOnly, + ).then((value) { + dev.log("Received Fiat rate 1 $cryptoCurrency = $value $currency"); + _fiatRate = value; + _convertAmountToCrypto(); + }); + } } @action void changeAmount(String amount) { this.amount = amount; + this._rawAmount = amount; if (selectedCurrency is FiatCurrency) { _convertAmountToCrypto(); } @@ -618,11 +644,20 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo searchText = text; } + @action void _convertAmountToCrypto() { final cryptoCurrency = walletTypeToCryptoCurrency(wallet.type); + final fiatRate = + _fiatRate ?? (fiatConversionStore.prices[cryptoCurrency] ?? 0.0); + + if (fiatRate <= 0.0) { + dev.log("Invalid Fiat Rate $fiatRate"); + amount = ''; + return; + } + try { - final crypto = - double.parse(amount.replaceAll(',', '.')) / fiatConversionStore.prices[cryptoCurrency]!; + final crypto = double.parse(_rawAmount.replaceAll(',', '.')) / fiatRate; final cryptoAmountTmp = _cryptoNumberFormat.format(crypto); if (amount != cryptoAmountTmp) { amount = cryptoAmountTmp; From 5c2735be42db03bf7f4311389804fc7025dea7c1 Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 18 Oct 2024 05:58:57 +0300 Subject: [PATCH 23/73] transaction description key id address (#1755) * transaction description key id+address * minor fix[skip ci] * fix backward compatibility * add monero primary address --- cw_core/lib/wallet_addresses.dart | 2 +- cw_evm/lib/evm_chain_wallet_addresses.dart | 3 +++ cw_monero/lib/monero_wallet_addresses.dart | 3 +++ cw_nano/lib/nano_wallet_addresses.dart | 3 +++ cw_solana/lib/solana_wallet_addresses.dart | 3 +++ cw_tron/lib/tron_wallet_addresses.dart | 3 +++ cw_wownero/lib/wownero_wallet_addresses.dart | 3 +++ lib/view_model/send/send_view_model.dart | 12 +++++++++--- lib/view_model/transaction_details_view_model.dart | 6 ++++-- 9 files changed, 32 insertions(+), 6 deletions(-) diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index ca488cfed..4324823c5 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -23,7 +23,7 @@ abstract class WalletAddresses { return _localAddress ?? address; } - String? get primaryAddress => null; + String get primaryAddress; String? _localAddress; diff --git a/cw_evm/lib/evm_chain_wallet_addresses.dart b/cw_evm/lib/evm_chain_wallet_addresses.dart index 4615d79ed..7dd501cc5 100644 --- a/cw_evm/lib/evm_chain_wallet_addresses.dart +++ b/cw_evm/lib/evm_chain_wallet_addresses.dart @@ -17,6 +17,9 @@ abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store { @observable String address; + @override + String get primaryAddress => address; + @override Future init() async { address = walletInfo.address; diff --git a/cw_monero/lib/monero_wallet_addresses.dart b/cw_monero/lib/monero_wallet_addresses.dart index c8a4448a4..ae78132d3 100644 --- a/cw_monero/lib/monero_wallet_addresses.dart +++ b/cw_monero/lib/monero_wallet_addresses.dart @@ -29,6 +29,9 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { @observable String address; + @override + String get primaryAddress => getAddress(accountIndex: account?.id ?? 0, addressIndex: 0); + @override String get latestAddress { var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1; diff --git a/cw_nano/lib/nano_wallet_addresses.dart b/cw_nano/lib/nano_wallet_addresses.dart index cc532d2c7..e8eae7737 100644 --- a/cw_nano/lib/nano_wallet_addresses.dart +++ b/cw_nano/lib/nano_wallet_addresses.dart @@ -18,6 +18,9 @@ abstract class NanoWalletAddressesBase extends WalletAddresses with Store { @observable String address; + @override + String get primaryAddress => address; + @observable NanoAccount? account; diff --git a/cw_solana/lib/solana_wallet_addresses.dart b/cw_solana/lib/solana_wallet_addresses.dart index 97a76fb99..19eb91fa1 100644 --- a/cw_solana/lib/solana_wallet_addresses.dart +++ b/cw_solana/lib/solana_wallet_addresses.dart @@ -14,6 +14,9 @@ abstract class SolanaWalletAddressesBase extends WalletAddresses with Store { @override String address; + @override + String get primaryAddress => address; + @override Future init() async { address = walletInfo.address; diff --git a/cw_tron/lib/tron_wallet_addresses.dart b/cw_tron/lib/tron_wallet_addresses.dart index 35939de26..095f97fa9 100644 --- a/cw_tron/lib/tron_wallet_addresses.dart +++ b/cw_tron/lib/tron_wallet_addresses.dart @@ -17,6 +17,9 @@ abstract class TronWalletAddressesBase extends WalletAddresses with Store { @observable String address; + @override + String get primaryAddress => address; + @override Future init() async { address = walletInfo.address; diff --git a/cw_wownero/lib/wownero_wallet_addresses.dart b/cw_wownero/lib/wownero_wallet_addresses.dart index b2f9ec67a..eed81eb45 100644 --- a/cw_wownero/lib/wownero_wallet_addresses.dart +++ b/cw_wownero/lib/wownero_wallet_addresses.dart @@ -29,6 +29,9 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { @observable String address; + @override + String get primaryAddress => getAddress(accountIndex: account?.id ?? 0, addressIndex: 0); + @override String get latestAddress { var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1; diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 334394a18..4064a1258 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -482,12 +482,18 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor nano!.updateTransactions(wallet); } + if (pendingTransaction!.id.isNotEmpty) { + + final descriptionKey = '${pendingTransaction!.id}_${wallet.walletAddresses.primaryAddress}'; _settingsStore.shouldSaveRecipientAddress ? await transactionDescriptionBox.add(TransactionDescription( - id: pendingTransaction!.id, recipientAddress: address, transactionNote: note)) - : await transactionDescriptionBox - .add(TransactionDescription(id: pendingTransaction!.id, transactionNote: note)); + id: descriptionKey, + recipientAddress: address, + transactionNote: note)) + : await transactionDescriptionBox.add(TransactionDescription( + id: descriptionKey, + transactionNote: note)); } state = TransactionCommitted(); diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index a96d70a90..1833a0f51 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -110,9 +110,11 @@ abstract class TransactionDetailsViewModelBase with Store { } catch (e) {} })); + final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}'; + final description = transactionDescriptionBox.values.firstWhere( - (val) => val.id == transactionInfo.txHash, - orElse: () => TransactionDescription(id: transactionInfo.txHash)); + (val) => val.id == descriptionKey || val.id == transactionInfo.txHash, + orElse: () => TransactionDescription(id: descriptionKey)); items.add(TextFieldListItem( title: S.current.note_tap_to_change, From 7faca38cfa52bcb3914c8b20cda291e4eefe594a Mon Sep 17 00:00:00 2001 From: Rafael Date: Fri, 18 Oct 2024 20:24:15 -0300 Subject: [PATCH 24/73] fix: sp input calc (#1756) --- cw_bitcoin/lib/electrum_wallet.dart | 45 ++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index ceaa2f088..527bef3ea 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -784,6 +784,7 @@ abstract class ElectrumWalletBase Future estimateTxForAmount( int credentialsAmount, List outputs, + List updatedOutputs, int feeRate, { int? inputsCount, String? memo, @@ -812,6 +813,7 @@ abstract class ElectrumWalletBase return estimateTxForAmount( credentialsAmount, outputs, + updatedOutputs, feeRate, inputsCount: utxoDetails.utxos.length + 1, memo: memo, @@ -824,19 +826,30 @@ abstract class ElectrumWalletBase } final changeAddress = await walletAddresses.getChangeAddress( - outputs: outputs, + outputs: updatedOutputs, utxoDetails: utxoDetails, ); final address = RegexUtils.addressTypeFromStr(changeAddress, network); + updatedOutputs.add(BitcoinOutput( + address: address, + value: BigInt.from(amountLeftForChangeAndFee), + isChange: true, + )); outputs.add(BitcoinOutput( address: address, value: BigInt.from(amountLeftForChangeAndFee), isChange: true, )); + // calcFee updates the silent payment outputs to calculate the tx size accounting + // for taproot addresses, but if more inputs are needed to make up for fees, + // the silent payment outputs need to be recalculated for the new inputs + var temp = outputs.map((output) => output).toList(); int fee = await calcFee( utxos: utxoDetails.utxos, - outputs: outputs, + // Always take only not updated bitcoin outputs here so for every estimation + // the SP outputs are re-generated to the proper taproot addresses + outputs: temp, network: network, memo: memo, feeRate: feeRate, @@ -844,18 +857,25 @@ abstract class ElectrumWalletBase vinOutpoints: utxoDetails.vinOutpoints, ); + updatedOutputs.clear(); + updatedOutputs.addAll(temp); + if (fee == 0) { throw BitcoinTransactionNoFeeException(); } int amount = credentialsAmount; - final lastOutput = outputs.last; + final lastOutput = updatedOutputs.last; final amountLeftForChange = amountLeftForChangeAndFee - fee; - print(amountLeftForChangeAndFee); - if (!_isBelowDust(amountLeftForChange)) { // Here, lastOutput already is change, return the amount left without the fee to the user's address. + updatedOutputs[updatedOutputs.length - 1] = BitcoinOutput( + address: lastOutput.address, + value: BigInt.from(amountLeftForChange), + isSilentPayment: lastOutput.isSilentPayment, + isChange: true, + ); outputs[outputs.length - 1] = BitcoinOutput( address: lastOutput.address, value: BigInt.from(amountLeftForChange), @@ -864,6 +884,7 @@ abstract class ElectrumWalletBase ); } else { // If has change that is lower than dust, will end up with tx rejected by network rules, so estimate again without the added change + updatedOutputs.removeLast(); outputs.removeLast(); // Still has inputs to spend before failing @@ -871,16 +892,18 @@ abstract class ElectrumWalletBase return estimateTxForAmount( credentialsAmount, outputs, + updatedOutputs, feeRate, inputsCount: utxoDetails.utxos.length + 1, memo: memo, + hasSilentPayment: hasSilentPayment, useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, coinTypeToSpendFrom: coinTypeToSpendFrom, ); } final estimatedSendAll = await estimateSendAllTx( - outputs, + updatedOutputs, feeRate, memo: memo, coinTypeToSpendFrom: coinTypeToSpendFrom, @@ -913,10 +936,12 @@ abstract class ElectrumWalletBase if (spendingAllCoins) { throw BitcoinTransactionWrongBalanceException(); } else { + updatedOutputs.removeLast(); outputs.removeLast(); return estimateTxForAmount( credentialsAmount, outputs, + updatedOutputs, feeRate, inputsCount: utxoDetails.utxos.length + 1, memo: memo, @@ -1029,6 +1054,9 @@ abstract class ElectrumWalletBase : feeRate(transactionCredentials.priority!); EstimatedTxResult estimatedTx; + final updatedOutputs = + outputs.map((e) => BitcoinOutput(address: e.address, value: e.value)).toList(); + if (sendAll) { estimatedTx = await estimateSendAllTx( outputs, @@ -1042,6 +1070,7 @@ abstract class ElectrumWalletBase estimatedTx = await estimateTxForAmount( credentialsAmount, outputs, + updatedOutputs, feeRateInt, memo: memo, hasSilentPayment: hasSilentPayment, @@ -1052,7 +1081,7 @@ abstract class ElectrumWalletBase if (walletInfo.isHardwareWallet) { final transaction = await buildHardwareWalletTransaction( utxos: estimatedTx.utxos, - outputs: outputs, + outputs: updatedOutputs, publicKeys: estimatedTx.publicKeys, fee: BigInt.from(estimatedTx.fee), network: network, @@ -1092,7 +1121,7 @@ abstract class ElectrumWalletBase } else { txb = BitcoinTransactionBuilder( utxos: estimatedTx.utxos, - outputs: outputs, + outputs: updatedOutputs, fee: BigInt.from(estimatedTx.fee), network: network, memo: estimatedTx.memo, From 50825a62c194cdd97bf5414487d6d40b246c96a9 Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Fri, 18 Oct 2024 17:05:48 -0700 Subject: [PATCH 25/73] Mweb enhancements 3 (#1744) * version 4.20.0 * update build numbers * UI updates and script fix for ios bundle identifier * disable mweb for desktop * change hardcoded ltc server ip address electrum connection enhancement * MWEB enhancements 2.0 (#1735) * additional logging and minor fixes * additional logging and minor fixes * addresses pt.1 * Allow Wallet Group Names to be the same as Wallet Names (#1730) * fix: Issues with imaging * fix: Allow group names to be the same as wallet names * fix: Bug with wallet grouping when a wallet is minimized * fix: Bug with wallet grouping when a wallet is minimized * logs of fixes and experimental changes, close wallet before opening next * save * fix icon * fixes * [skip ci] updates * [skip ci] updates * updates * minor optimizations * fix for when switching between wallets * [skip ci] updates * [skip ci] updates * Update cw_bitcoin/lib/litecoin_wallet.dart Co-authored-by: Omar Hatem * Update cw_bitcoin/lib/litecoin_wallet.dart Co-authored-by: Omar Hatem * mobx * mostly logging * stream fix pt.1 [skip ci] * updates * some fixes and enhancements * [skip ci] minor * potential partial fix for streamsink closed * fix stream sink closed errors * fix mweb logo colors * save * minor enhancements [skip ci] * save * experimental * minor * minor [skip ci] --------- Co-authored-by: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Co-authored-by: Omar Hatem * fix menu list removing from original list * detach sync status from mwebsyncstatus * minor * keep sync status in sync where necessary * minor * wip * appears to work? * updates * prevent mwebd from submitting non mweb transactions * fix unspent coins info not persisting for mweb coins + other minor fixes * [skip ci] minor * Polish MWEB card UI * make sure current chain tip is updated correctly [skip ci] * [skip ci] review fixes * [skip ci] detect mweb outputs more thoroughly (fix peg-in commit error) * fix change address on send ui * fix qr code scan issue * get segwit address for pegout even if mweb is selected on the receive screen [skip ci] * - Fix adding nodes twice - Fix mempool API parsing error * (potentially) fix duplicate tx history bug * [skip ci] fix bc1 address * don't show contacts prompt on pegin/out + potential unconfirmed balance fixes * [skip ci] minor cleanup * fix mweb input detection * fix showing mweb address for non-mweb transactions --------- Co-authored-by: OmarHatem Co-authored-by: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Co-authored-by: tuxpizza --- cw_bitcoin/lib/electrum.dart | 22 ++- cw_bitcoin/lib/electrum_wallet.dart | 69 +++++--- cw_bitcoin/lib/electrum_wallet_addresses.dart | 6 +- cw_bitcoin/lib/litecoin_wallet.dart | 150 ++++++++++++------ cw_bitcoin/lib/litecoin_wallet_addresses.dart | 16 +- .../lib/pending_bitcoin_transaction.dart | 6 + cw_mweb/lib/cw_mweb.dart | 7 +- lib/bitcoin/cw_bitcoin.dart | 22 ++- lib/entities/default_settings_migration.dart | 8 +- .../screens/dashboard/pages/balance_page.dart | 48 +++--- lib/src/screens/send/send_page.dart | 5 + lib/view_model/wallet_creation_vm.dart | 46 +++--- tool/configure.dart | 1 + 13 files changed, 266 insertions(+), 140 deletions(-) diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 54ccaaef5..a8cb7ee9f 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -68,8 +68,8 @@ class ElectrumClient { try { await socket?.close(); - socket = null; } catch (_) {} + socket = null; try { if (useSSL == false || (useSSL == null && uri.toString().contains("btc-electrum"))) { @@ -102,7 +102,8 @@ class ElectrumClient { return; } - _setConnectionStatus(ConnectionStatus.connected); + // use ping to determine actual connection status since we could've just not timed out yet: + // _setConnectionStatus(ConnectionStatus.connected); socket!.listen( (Uint8List event) { @@ -128,7 +129,7 @@ class ElectrumClient { print("SOCKET CLOSED!!!!!"); unterminatedString = ''; try { - if (host == socket?.address.host) { + if (host == socket?.address.host || socket == null) { _setConnectionStatus(ConnectionStatus.disconnected); socket?.destroy(); } @@ -178,7 +179,7 @@ class ElectrumClient { unterminatedString = ''; } } catch (e) { - print(e.toString()); + print("parse $e"); } } @@ -191,7 +192,7 @@ class ElectrumClient { try { await callWithTimeout(method: 'server.ping'); _setConnectionStatus(ConnectionStatus.connected); - } on RequestFailedTimeoutException catch (_) { + } catch (_) { _setConnectionStatus(ConnectionStatus.disconnected); } } @@ -431,7 +432,7 @@ class ElectrumClient { return subscription; } catch (e) { - print(e.toString()); + print("subscribe $e"); return null; } } @@ -470,7 +471,8 @@ class ElectrumClient { return completer.future; } catch (e) { - print(e.toString()); + print("callWithTimeout $e"); + rethrow; } } @@ -537,6 +539,12 @@ class ElectrumClient { onConnectionStatusChange?.call(status); _connectionStatus = status; _isConnected = status == ConnectionStatus.connected; + if (!_isConnected) { + try { + socket?.destroy(); + } catch (_) {} + socket = null; + } } void _handleResponse(Map response) { diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 527bef3ea..893cff6f2 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/bitcoin_wallet.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; @@ -249,7 +250,7 @@ abstract class ElectrumWalletBase int? _currentChainTip; Future getCurrentChainTip() async { - if (_currentChainTip != null) { + if ((_currentChainTip ?? 0) > 0) { return _currentChainTip!; } _currentChainTip = await electrumClient.getCurrentBlockChainTip() ?? 0; @@ -301,6 +302,7 @@ abstract class ElectrumWalletBase @action Future _setListeners(int height, {int? chainTipParam, bool? doSingleScan}) async { + if (this is! BitcoinWallet) return; final chainTip = chainTipParam ?? await getUpdatedChainTip(); if (chainTip == height) { @@ -467,7 +469,7 @@ abstract class ElectrumWalletBase } } catch (e, stacktrace) { print(stacktrace); - print(e.toString()); + print("startSync $e"); syncStatus = FailedSyncStatus(); } } @@ -479,10 +481,10 @@ abstract class ElectrumWalletBase final response = await http.get(Uri.parse("http://mempool.cakewallet.com:8999/api/v1/fees/recommended")); - final result = json.decode(response.body) as Map; - final slowFee = result['economyFee']?.toInt() ?? 0; - int mediumFee = result['hourFee']?.toInt() ?? 0; - int fastFee = result['fastestFee']?.toInt() ?? 0; + final result = json.decode(response.body) as Map; + final slowFee = (result['economyFee'] as num?)?.toInt() ?? 0; + int mediumFee = (result['hourFee'] as num?)?.toInt() ?? 0; + int fastFee = (result['fastestFee'] as num?)?.toInt() ?? 0; if (slowFee == mediumFee) { mediumFee++; } @@ -491,7 +493,9 @@ abstract class ElectrumWalletBase } _feeRates = [slowFee, mediumFee, fastFee]; return; - } catch (_) {} + } catch (e) { + print(e); + } } final feeRates = await electrumClient.feeRates(network: network); @@ -571,7 +575,7 @@ abstract class ElectrumWalletBase await electrumClient.connectToUri(node.uri, useSSL: node.useSSL); } catch (e, stacktrace) { print(stacktrace); - print(e.toString()); + print("connectToNode $e"); syncStatus = FailedSyncStatus(); } } @@ -826,8 +830,8 @@ abstract class ElectrumWalletBase } final changeAddress = await walletAddresses.getChangeAddress( + inputs: utxoDetails.availableInputs, outputs: updatedOutputs, - utxoDetails: utxoDetails, ); final address = RegexUtils.addressTypeFromStr(changeAddress, network); updatedOutputs.add(BitcoinOutput( @@ -1181,6 +1185,7 @@ abstract class ElectrumWalletBase hasChange: estimatedTx.hasChange, isSendAll: estimatedTx.isSendAll, hasTaprootInputs: hasTaprootInputs, + utxos: estimatedTx.utxos, )..addListener((transaction) async { transactionHistory.addOne(transaction); if (estimatedTx.spendsSilentPayment) { @@ -1370,7 +1375,7 @@ abstract class ElectrumWalletBase }); } - // Set the balance of all non-silent payment addresses to 0 before updating + // Set the balance of all non-silent payment and non-mweb addresses to 0 before updating walletAddresses.allAddresses .where((element) => element.type != SegwitAddresType.mweb) .forEach((addr) { @@ -1487,7 +1492,7 @@ abstract class ElectrumWalletBase await unspentCoinsInfo.deleteAll(keys); } } catch (e) { - print(e.toString()); + print("refreshUnspentCoinsInfo $e"); } } @@ -1831,7 +1836,7 @@ abstract class ElectrumWalletBase return historiesWithDetails; } catch (e) { - print(e.toString()); + print("fetchTransactions $e"); return {}; } } @@ -1905,7 +1910,9 @@ abstract class ElectrumWalletBase if (height > 0) { storedTx.height = height; // the tx's block itself is the first confirmation so add 1 - if (currentHeight != null) storedTx.confirmations = currentHeight - height + 1; + if ((currentHeight ?? 0) > 0) { + storedTx.confirmations = currentHeight! - height + 1; + } storedTx.isPending = storedTx.confirmations == 0; } @@ -1946,9 +1953,13 @@ abstract class ElectrumWalletBase } await getCurrentChainTip(); - transactionHistory.transactions.values.forEach((tx) async { - if (tx.unspents != null && tx.unspents!.isNotEmpty && tx.height != null && tx.height! > 0) { - tx.confirmations = await getCurrentChainTip() - tx.height! + 1; + transactionHistory.transactions.values.forEach((tx) { + if (tx.unspents != null && + tx.unspents!.isNotEmpty && + tx.height != null && + tx.height! > 0 && + (_currentChainTip ?? 0) > 0) { + tx.confirmations = _currentChainTip! - tx.height! + 1; } }); @@ -1973,9 +1984,17 @@ abstract class ElectrumWalletBase await Future.wait(unsubscribedScriptHashes.map((address) async { final sh = address.getScriptHash(network); if (!(_scripthashesUpdateSubject[sh]?.isClosed ?? true)) { - await _scripthashesUpdateSubject[sh]?.close(); + try { + await _scripthashesUpdateSubject[sh]?.close(); + } catch (e) { + print("failed to close: $e"); + } + } + try { + _scripthashesUpdateSubject[sh] = await electrumClient.scripthashUpdate(sh); + } catch (e) { + print("failed scripthashUpdate: $e"); } - _scripthashesUpdateSubject[sh] = await electrumClient.scripthashUpdate(sh); _scripthashesUpdateSubject[sh]?.listen((event) async { try { await updateUnspentsForAddress(address); @@ -2171,6 +2190,7 @@ abstract class ElectrumWalletBase @action void _onConnectionStatusChange(ConnectionStatus status) { + switch (status) { case ConnectionStatus.connected: if (syncStatus is NotConnectedSyncStatus || @@ -2182,19 +2202,26 @@ abstract class ElectrumWalletBase break; case ConnectionStatus.disconnected: - syncStatus = NotConnectedSyncStatus(); + if (syncStatus is! NotConnectedSyncStatus) { + syncStatus = NotConnectedSyncStatus(); + } break; case ConnectionStatus.failed: - syncStatus = LostConnectionSyncStatus(); + if (syncStatus is! LostConnectionSyncStatus) { + syncStatus = LostConnectionSyncStatus(); + } break; case ConnectionStatus.connecting: - syncStatus = ConnectingSyncStatus(); + if (syncStatus is! ConnectingSyncStatus) { + syncStatus = ConnectingSyncStatus(); + } break; default: } } void _syncStatusReaction(SyncStatus syncStatus) async { + print("SYNC_STATUS_CHANGE: ${syncStatus}"); if (syncStatus is SyncingSyncStatus) { return; } diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index ce1ff9713..6c10dc615 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -3,7 +3,7 @@ import 'dart:io' show Platform; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; -import 'package:cw_bitcoin/electrum_wallet.dart'; +import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -267,7 +267,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @action - Future getChangeAddress({List? outputs, UtxoDetails? utxoDetails}) async { + Future getChangeAddress({List? inputs, List? outputs, bool isPegIn = false}) async { updateChangeAddresses(); if (changeAddresses.isEmpty) { @@ -478,7 +478,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { await saveAddressesInBox(); } catch (e) { - print(e.toString()); + print("updateAddresses $e"); } } diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 29a2df48a..a09850f59 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -7,6 +7,7 @@ import 'package:crypto/crypto.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/mweb_utxo.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_mweb/mwebd.pbgrpc.dart'; import 'package:fixnum/fixnum.dart'; import 'package:bip39/bip39.dart' as bip39; @@ -95,6 +96,36 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; }); + reaction((_) => mwebSyncStatus, (status) async { + if (mwebSyncStatus is FailedSyncStatus) { + // we failed to connect to mweb, check if we are connected to the litecoin node: + late int nodeHeight; + try { + nodeHeight = await electrumClient.getCurrentBlockChainTip() ?? 0; + } catch (_) { + nodeHeight = 0; + } + + if (nodeHeight == 0) { + // we aren't connected to the litecoin node, so the current electrum_wallet reactions will take care of this case for us + } else { + // we're connected to the litecoin node, but we failed to connect to mweb, try again after a few seconds: + await CwMweb.stop(); + await Future.delayed(const Duration(seconds: 5)); + startSync(); + } + } else if (mwebSyncStatus is SyncingSyncStatus) { + syncStatus = mwebSyncStatus; + } else if (mwebSyncStatus is SyncronizingSyncStatus) { + if (syncStatus is! SyncronizingSyncStatus) { + syncStatus = mwebSyncStatus; + } + } else if (mwebSyncStatus is SyncedSyncStatus) { + if (syncStatus is! SyncedSyncStatus) { + syncStatus = mwebSyncStatus; + } + } + }); } late final Bip32Slip10Secp256k1 mwebHd; late final Box mwebUtxosBox; @@ -105,6 +136,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { late bool mwebEnabled; bool processingUtxos = false; + @observable + SyncStatus mwebSyncStatus = NotConnectedSyncStatus(); + List get scanSecret => mwebHd.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; List get spendSecret => mwebHd.childKey(Bip32KeyIndex(0x80000001)).privateKey.privKey.raw; @@ -244,13 +278,24 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @override Future startSync() async { print("startSync() called!"); - if (syncStatus is SyncronizingSyncStatus) { + print("STARTING SYNC - MWEB ENABLED: $mwebEnabled"); + if (!mwebEnabled) { + try { + // in case we're switching from a litecoin wallet that had mweb enabled + CwMweb.stop(); + } catch (_) {} + super.startSync(); return; } + + if (mwebSyncStatus is SyncronizingSyncStatus) { + return; + } + print("STARTING SYNC - MWEB ENABLED: $mwebEnabled"); _syncTimer?.cancel(); try { - syncStatus = SyncronizingSyncStatus(); + mwebSyncStatus = SyncronizingSyncStatus(); try { await subscribeForUpdates(); } catch (e) { @@ -261,45 +306,32 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { _feeRatesTimer = Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates()); - if (!mwebEnabled) { - try { - // in case we're switching from a litecoin wallet that had mweb enabled - CwMweb.stop(); - } catch (_) {} - try { - await updateAllUnspents(); - await updateTransactions(); - await updateBalance(); - syncStatus = SyncedSyncStatus(); - } catch (e, s) { - print(e); - print(s); - syncStatus = FailedSyncStatus(); - } - return; - } - + print("START SYNC FUNCS"); await waitForMwebAddresses(); await processMwebUtxos(); await updateTransactions(); await updateUnspent(); await updateBalance(); - } catch (e) { - print("failed to start mweb sync: $e"); - syncStatus = FailedSyncStatus(error: "failed to start"); + print("DONE SYNC FUNCS"); + } catch (e, s) { + print("mweb sync failed: $e $s"); + mwebSyncStatus = FailedSyncStatus(error: "mweb sync failed: $e"); return; } _syncTimer = Timer.periodic(const Duration(milliseconds: 3000), (timer) async { - if (syncStatus is FailedSyncStatus) return; + if (mwebSyncStatus is FailedSyncStatus) { + _syncTimer?.cancel(); + return; + } final nodeHeight = await electrumClient.getCurrentBlockChainTip() ?? 0; // current block height of our node if (nodeHeight == 0) { // we aren't connected to the ltc node yet - if (syncStatus is! NotConnectedSyncStatus) { - syncStatus = FailedSyncStatus(error: "Failed to connect to Litecoin node"); + if (mwebSyncStatus is! NotConnectedSyncStatus) { + mwebSyncStatus = FailedSyncStatus(error: "litecoin node isn't connected"); } return; } @@ -309,12 +341,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { try { if (resp.blockHeaderHeight < nodeHeight) { int h = resp.blockHeaderHeight; - syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); + mwebSyncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); } else if (resp.mwebHeaderHeight < nodeHeight) { int h = resp.mwebHeaderHeight; - syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); + mwebSyncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); } else if (resp.mwebUtxosHeight < nodeHeight) { - syncStatus = SyncingSyncStatus(1, 0.999); + mwebSyncStatus = SyncingSyncStatus(1, 0.999); } else { if (resp.mwebUtxosHeight > walletInfo.restoreHeight) { await walletInfo.updateRestoreHeight(resp.mwebUtxosHeight); @@ -325,6 +357,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { int txHeight = transaction.height ?? resp.mwebUtxosHeight; final confirmations = (resp.mwebUtxosHeight - txHeight) + 1; if (transaction.confirmations == confirmations) continue; + if (transaction.confirmations == 0) { + updateBalance(); + } transaction.confirmations = confirmations; transactionHistory.addOne(transaction); } @@ -332,17 +367,17 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } // prevent unnecessary reaction triggers: - if (syncStatus is! SyncedSyncStatus) { + if (mwebSyncStatus is! SyncedSyncStatus) { // mwebd is synced, but we could still be processing incoming utxos: if (!processingUtxos) { - syncStatus = SyncedSyncStatus(); + mwebSyncStatus = SyncedSyncStatus(); } } return; } } catch (e) { print("error syncing: $e"); - syncStatus = FailedSyncStatus(error: e.toString()); + mwebSyncStatus = FailedSyncStatus(error: e.toString()); } }); } @@ -512,8 +547,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } _utxoStream = responseStream.listen((Utxo sUtxo) async { // we're processing utxos, so our balance could still be innacurate: - if (syncStatus is! SyncronizingSyncStatus && syncStatus is! SyncingSyncStatus) { - syncStatus = SyncronizingSyncStatus(); + if (mwebSyncStatus is! SyncronizingSyncStatus && mwebSyncStatus is! SyncingSyncStatus) { + mwebSyncStatus = SyncronizingSyncStatus(); processingUtxos = true; _processingTimer?.cancel(); _processingTimer = Timer.periodic(const Duration(seconds: 2), (timer) async { @@ -530,10 +565,18 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { value: sUtxo.value.toInt(), ); - // if (mwebUtxosBox.containsKey(utxo.outputId)) { - // // we've already stored this utxo, skip it: - // return; - // } + if (mwebUtxosBox.containsKey(utxo.outputId)) { + // we've already stored this utxo, skip it: + // but do update the utxo height if it's somehow different: + final existingUtxo = mwebUtxosBox.get(utxo.outputId); + if (existingUtxo!.height != utxo.height) { + print( + "updating utxo height for $utxo.outputId: ${existingUtxo.height} -> ${utxo.height}"); + existingUtxo.height = utxo.height; + await mwebUtxosBox.put(utxo.outputId, existingUtxo); + } + return; + } await updateUnspent(); await updateBalance(); @@ -579,7 +622,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final height = await electrumClient.getCurrentBlockChainTip(); if (height == null || status.blockHeaderHeight != height) return; if (status.mwebUtxosHeight != height) return; // we aren't synced - int amount = 0; Set inputAddresses = {}; var output = convert.AccumulatorSink(); @@ -673,10 +715,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @override @action Future updateAllUnspents() async { - // get ltc unspents: - await super.updateAllUnspents(); - if (!mwebEnabled) { + await super.updateAllUnspents(); return; } @@ -712,6 +752,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } mwebUnspentCoins.add(unspent); }); + + // copy coin control attributes to mwebCoins: + await updateCoins(mwebUnspentCoins); + // get regular ltc unspents (this resets unspentCoins): + await super.updateAllUnspents(); + // add the mwebCoins: unspentCoins.addAll(mwebUnspentCoins); } @@ -890,6 +936,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { tx.isMweb = mwebEnabled; if (!mwebEnabled) { + tx.changeAddressOverride = + await (walletAddresses as LitecoinWalletAddresses).getChangeAddress(isPegIn: false); return tx; } await waitForMwebAddresses(); @@ -913,12 +961,23 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { hasMwebOutput = true; break; } + if (output.address.toLowerCase().contains("mweb")) { + hasMwebOutput = true; + break; + } } - if (tx2.mwebBytes != null && tx2.mwebBytes!.isNotEmpty) { - hasMwebInput = true; + // check if mweb inputs are used: + for (final utxo in tx.utxos) { + if (utxo.utxo.scriptType == SegwitAddresType.mweb) { + hasMwebInput = true; + } } + bool isPegIn = !hasMwebInput && hasMwebOutput; + bool isRegular = !hasMwebInput && !hasMwebOutput; + tx.changeAddressOverride = await (walletAddresses as LitecoinWalletAddresses) + .getChangeAddress(isPegIn: isPegIn || isRegular); if (!hasMwebInput && !hasMwebOutput) { tx.isMweb = false; return tx; @@ -971,7 +1030,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final addresses = {}; transaction.inputAddresses?.forEach((id) async { final utxo = mwebUtxosBox.get(id); - // await mwebUtxosBox.delete(id);// gets deleted in checkMwebUtxosSpent + await mwebUtxosBox.delete(id); // gets deleted in checkMwebUtxosSpent if (utxo == null) return; final addressRecord = walletAddresses.allAddresses .firstWhere((addressRecord) => addressRecord.address == utxo.address); @@ -990,6 +1049,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { print(e); print(s); if (e.toString().contains("commit failed")) { + print(e); throw Exception("Transaction commit failed (no peers responded), please try again."); } rethrow; diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 0a20665bf..6154a0ead 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -5,6 +5,7 @@ import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; @@ -142,14 +143,15 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with @action @override - Future getChangeAddress({List? outputs, UtxoDetails? utxoDetails}) async { + Future getChangeAddress( + {List? inputs, List? outputs, bool isPegIn = false}) async { // use regular change address on peg in, otherwise use mweb for change address: - if (!mwebEnabled) { + if (!mwebEnabled || isPegIn) { return super.getChangeAddress(); } - if (outputs != null && utxoDetails != null) { + if (inputs != null && outputs != null) { // check if this is a PEGIN: bool outputsToMweb = false; bool comesFromMweb = false; @@ -161,14 +163,18 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with outputsToMweb = true; } } - // TODO: this doesn't respect coin control because it doesn't know which available inputs are selected - utxoDetails.availableInputs.forEach((element) { + + inputs.forEach((element) { + if (!element.isSending || element.isFrozen) { + return; + } if (element.address.contains("mweb")) { comesFromMweb = true; } }); bool isPegIn = !comesFromMweb && outputsToMweb; + if (isPegIn && mwebEnabled) { return super.getChangeAddress(); } diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index c722dc14f..5ed84dbf4 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -24,6 +24,7 @@ class PendingBitcoinTransaction with PendingTransaction { this.isSendAll = false, this.hasTaprootInputs = false, this.isMweb = false, + this.utxos = const [], }) : _listeners = []; final WalletType type; @@ -36,7 +37,9 @@ class PendingBitcoinTransaction with PendingTransaction { final bool isSendAll; final bool hasChange; final bool hasTaprootInputs; + List utxos; bool isMweb; + String? changeAddressOverride; String? idOverride; String? hexOverride; List? outputAddresses; @@ -63,6 +66,9 @@ class PendingBitcoinTransaction with PendingTransaction { PendingChange? get change { try { final change = _tx.outputs.firstWhere((out) => out.isChange); + if (changeAddressOverride != null) { + return PendingChange(changeAddressOverride!, BtcUtils.fromSatoshi(change.amount)); + } return PendingChange(change.scriptPubKey.toAddress(), BtcUtils.fromSatoshi(change.amount)); } catch (_) { return null; diff --git a/cw_mweb/lib/cw_mweb.dart b/cw_mweb/lib/cw_mweb.dart index 39aa433cd..a1a592fb8 100644 --- a/cw_mweb/lib/cw_mweb.dart +++ b/cw_mweb/lib/cw_mweb.dart @@ -40,7 +40,7 @@ class CwMweb { } static Future _initializeClient() async { - print("initialize client called!"); + print("_initializeClient() called!"); final appDir = await getApplicationSupportDirectory(); const ltcNodeUri = "ltc-electrum.cakewallet.com:9333"; @@ -54,7 +54,7 @@ class CwMweb { log("Attempting to connect to server on port: $_port"); // wait for the server to finish starting up before we try to connect to it: - await Future.delayed(const Duration(seconds: 5)); + await Future.delayed(const Duration(seconds: 8)); _clientChannel = ClientChannel('127.0.0.1', port: _port!, channelShutdownHandler: () { _rpcClient = null; @@ -83,10 +83,13 @@ class CwMweb { log("Attempt $i failed: $e"); log('Caught grpc error: ${e.message}'); _rpcClient = null; + // necessary if the database isn't open: + await stop(); await Future.delayed(const Duration(seconds: 3)); } catch (e) { log("Attempt $i failed: $e"); _rpcClient = null; + await stop(); await Future.delayed(const Duration(seconds: 3)); } } diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 91232792a..365d86be5 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -208,7 +208,7 @@ class CWBitcoin extends Bitcoin { {UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) { final bitcoinWallet = wallet as ElectrumWallet; return bitcoinWallet.unspentCoins.where((element) { - switch(coinTypeToSpendFrom) { + switch (coinTypeToSpendFrom) { case UnspentCoinType.mweb: return element.bitcoinAddressRecord.type == SegwitAddresType.mweb; case UnspentCoinType.nonMweb: @@ -216,7 +216,6 @@ class CWBitcoin extends Bitcoin { case UnspentCoinType.any: return true; } - }).toList(); } @@ -399,19 +398,21 @@ class CWBitcoin extends Bitcoin { final history = await electrumClient.getHistory(sh); final balance = await electrumClient.getBalance(sh); - dInfoCopy.balance = balance.entries.first.value.toString(); + dInfoCopy.balance = balance.entries.firstOrNull?.value.toString() ?? "0"; dInfoCopy.address = address; dInfoCopy.transactionsCount = history.length; list.add(dInfoCopy); - } catch (e) { - print(e); + } catch (e, s) { + print("derivationInfoError: $e"); + print("derivationInfoStack: $s"); } } } // sort the list such that derivations with the most transactions are first: list.sort((a, b) => b.transactionsCount.compareTo(a.transactionsCount)); + return list; } @@ -682,4 +683,15 @@ class CWBitcoin extends Bitcoin { return null; } } + + String? getUnusedSegwitAddress(Object wallet) { + try { + final electrumWallet = wallet as ElectrumWallet; + final segwitAddress = electrumWallet.walletAddresses.allAddresses + .firstWhere((element) => !element.isUsed && element.type == SegwitAddresType.p2wpkh); + return segwitAddress.address; + } catch (_) { + return null; + } + } } diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 2b8c49631..198781cea 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -899,7 +899,9 @@ Future changeDefaultBitcoinNode( final newCakeWalletBitcoinNode = Node(uri: newCakeWalletBitcoinUri, type: WalletType.bitcoin, useSSL: false); - await nodeSource.add(newCakeWalletBitcoinNode); + if (!nodeSource.values.any((element) => element.uriRaw == newCakeWalletBitcoinUri)) { + await nodeSource.add(newCakeWalletBitcoinNode); + } if (needToReplaceCurrentBitcoinNode) { await sharedPreferences.setInt( @@ -931,6 +933,10 @@ Future _addBitcoinNode({ 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); diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index 708941023..84bdfd58b 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -17,7 +17,6 @@ import 'package:cake_wallet/src/widgets/standard_switch.dart'; import 'package:cake_wallet/store/settings_store.dart'; 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/send_page_theme.dart'; import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cake_wallet/utils/payment_request.dart'; @@ -843,7 +842,7 @@ class BalanceRowWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - margin: const EdgeInsets.only(top: 0, left: 24, right: 8, bottom: 16), + margin: const EdgeInsets.only(top: 16, left: 24, right: 8, bottom: 16), child: Stack( children: [ if (currency == CryptoCurrency.ltc) @@ -851,17 +850,15 @@ class BalanceRowWidget extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ Container( - padding: EdgeInsets.only(right: 16, top: 16), + padding: EdgeInsets.only(right: 16, top: 0), child: Column( children: [ Container( - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), child: ImageIcon( AssetImage('assets/images/mweb_logo.png'), - color: Color.fromARGB(255, 11, 70, 129), + color: Theme.of(context) + .extension()! + .assetTitleColor, size: 40, ), ), @@ -889,7 +886,6 @@ class BalanceRowWidget extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 24), Text( '${secondAvailableBalanceLabel}', textAlign: TextAlign.center, @@ -907,9 +903,9 @@ class BalanceRowWidget extends StatelessWidget { AutoSizeText( secondAvailableBalance, style: TextStyle( - fontSize: 20, + fontSize: 24, fontFamily: 'Lato', - fontWeight: FontWeight.w400, + fontWeight: FontWeight.w900, color: Theme.of(context) .extension()! .assetTitleColor, @@ -918,15 +914,15 @@ class BalanceRowWidget extends StatelessWidget { maxLines: 1, textAlign: TextAlign.center, ), - SizedBox(height: 4), + SizedBox(height: 6), if (!isTestnet) Text( '${secondAvailableFiatBalance}', textAlign: TextAlign.center, style: TextStyle( - fontSize: 12, + fontSize: 16, fontFamily: 'Lato', - fontWeight: FontWeight.w400, + fontWeight: FontWeight.w500, color: Theme.of(context) .extension()! .textColor, @@ -1019,7 +1015,6 @@ class BalanceRowWidget extends StatelessWidget { paymentRequest = PaymentRequest.fromUri(Uri.parse("litecoin:${mwebAddress}")); } - Navigator.pushNamed( context, Routes.send, @@ -1030,11 +1025,10 @@ class BalanceRowWidget extends StatelessWidget { ); }, style: OutlinedButton.styleFrom( - backgroundColor: Theme.of(context) - .extension()! - .textFieldButtonIconColor + backgroundColor: Colors.grey.shade400 .withAlpha(50), - side: BorderSide(color: Colors.grey.shade400, width: 0), + side: BorderSide(color: Colors.grey.shade400 + .withAlpha(50), width: 0), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), @@ -1058,7 +1052,7 @@ class BalanceRowWidget extends StatelessWidget { style: TextStyle( color: Theme.of(context) .extension()! - .assetTitleColor, + .textColor, ), ), ], @@ -1074,13 +1068,12 @@ class BalanceRowWidget extends StatelessWidget { child: OutlinedButton( onPressed: () { final litecoinAddress = - bitcoin!.getAddress(dashboardViewModel.wallet); + bitcoin!.getUnusedSegwitAddress(dashboardViewModel.wallet); PaymentRequest? paymentRequest = null; - if (litecoinAddress.isNotEmpty) { + if ((litecoinAddress?.isNotEmpty ?? false)) { paymentRequest = PaymentRequest.fromUri( Uri.parse("litecoin:${litecoinAddress}")); } - Navigator.pushNamed( context, Routes.send, @@ -1091,11 +1084,10 @@ class BalanceRowWidget extends StatelessWidget { ); }, style: OutlinedButton.styleFrom( - backgroundColor: Theme.of(context) - .extension()! - .textFieldButtonIconColor + backgroundColor: Colors.grey.shade400 .withAlpha(50), - side: BorderSide(color: Colors.grey.shade400, width: 0), + side: BorderSide(color: Colors.grey.shade400 + .withAlpha(50), width: 0), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), @@ -1119,7 +1111,7 @@ class BalanceRowWidget extends StatelessWidget { style: TextStyle( color: Theme.of(context) .extension()! - .assetTitleColor, + .textColor, ), ), ], diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index ccf4a1dc4..d881d0341 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -28,6 +28,7 @@ import 'package:cake_wallet/utils/request_review_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/send/output.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'; @@ -508,6 +509,10 @@ class SendPage extends BasePage { if (state is TransactionCommitted) { newContactAddress = newContactAddress ?? sendViewModel.newContactAddress(); + + if (sendViewModel.coinTypeToSpendFrom != UnspentCoinType.any) { + newContactAddress = null; + } final successMessage = S.of(_dialogContext).send_success( sendViewModel.selectedCryptoCurrency.toString()); diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 90b4c5c3a..17a8d6d28 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -115,7 +115,9 @@ abstract class WalletCreationVMBase with Store { getIt.get().registerSyncTask(); _appStore.authenticationStore.allowed(); state = ExecutedSuccessfullyState(); - } catch (e, _) { + } catch (e, s) { + print("error: $e"); + print("stack: $s"); state = FailureState(e.toString()); } } @@ -194,31 +196,29 @@ abstract class WalletCreationVMBase with Store { final walletType = restoreWallet.type; var appStore = getIt.get(); var node = appStore.settingsStore.getCurrentNode(walletType); - - switch (walletType) { - case WalletType.bitcoin: - case WalletType.litecoin: - final derivationList = await bitcoin!.getDerivationsFromMnemonic( - mnemonic: restoreWallet.mnemonicSeed!, - node: node, - passphrase: restoreWallet.passphrase, - ); + switch (walletType) { + case WalletType.bitcoin: + case WalletType.litecoin: + final derivationList = await bitcoin!.getDerivationsFromMnemonic( + mnemonic: restoreWallet.mnemonicSeed!, + node: node, + passphrase: restoreWallet.passphrase, + ); + if (derivationList.firstOrNull?.transactionsCount == 0 && derivationList.length > 1) + return []; + return derivationList; - if (derivationList.first.transactionsCount == 0 && derivationList.length > 1) return []; - - return derivationList; - - case WalletType.nano: - return nanoUtil!.getDerivationsFromMnemonic( - mnemonic: restoreWallet.mnemonicSeed!, - node: node, - ); - default: - break; - } - return list; + case WalletType.nano: + return nanoUtil!.getDerivationsFromMnemonic( + mnemonic: restoreWallet.mnemonicSeed!, + node: node, + ); + default: + break; + } + return list; } WalletCredentials getCredentials(dynamic options) => throw UnimplementedError(); diff --git a/tool/configure.dart b/tool/configure.dart index ce079dd29..704b47526 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -231,6 +231,7 @@ abstract class Bitcoin { Future setMwebEnabled(Object wallet, bool enabled); bool getMwebEnabled(Object wallet); String? getUnusedMwebAddress(Object wallet); + String? getUnusedSegwitAddress(Object wallet); } """; From 34166c3a11af78c876f983131e2ea12809330692 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Mon, 21 Oct 2024 13:03:53 +0300 Subject: [PATCH 26/73] v4.20.0 (#1759) * new version * fix android manifest [skip ci] * fix sp scanning [skip ci] --- android/app/src/main/AndroidManifestBase.xml | 2 +- assets/text/Monerocom_Release_Notes.txt | 7 +++---- assets/text/Release_Notes.txt | 15 ++++++--------- cw_bitcoin/lib/electrum.dart | 6 +++--- cw_bitcoin/lib/litecoin_wallet.dart | 2 -- cw_core/lib/wallet_addresses.dart | 2 +- lib/src/screens/dashboard/pages/balance_page.dart | 2 +- scripts/android/app_env.sh | 4 ++-- scripts/ios/app_env.sh | 4 ++-- scripts/linux/app_env.sh | 2 +- scripts/macos/app_env.sh | 4 ++-- 11 files changed, 22 insertions(+), 28 deletions(-) diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index 1d0e2c93c..98bbb1236 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -14,7 +14,7 @@ - + diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index 2f79dd5c6..613ea4281 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,4 +1,3 @@ -Monero enhancements for sending and address generation -StealthEx -LetsExchange -Visual enhancements and bug fixes \ No newline at end of file +Monero enhancements +Introducing StealthEx and LetxExchange +Bug fixes \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index 868ae7954..d359d6e8a 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,9 +1,6 @@ -Add Litecoin MWEB -Wallet groups (same seed, multiple wallets) -Silent Payments enhancements -Monero enhancements for sending and address generation -StealthEx -LetsExchange -Replace-By-Fee improvements -ERC20 tokens potential scam detection -Visual enhancements and bug fixes \ No newline at end of file +Added Litecoin MWEB +Added wallet groups +Silent Payment enhancements for speed & reliability +Monero enhancements +Introducing StealthEx and LetxExchange +Additional ERC20 tokens scam detection \ No newline at end of file diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index a8cb7ee9f..a18c038fa 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -423,7 +423,7 @@ class ElectrumClient { BehaviorSubject? subscribe( {required String id, required String method, List params = const []}) { try { - if (socket == null || !isConnected) { + if (socket == null) { return null; } final subscription = BehaviorSubject(); @@ -439,7 +439,7 @@ class ElectrumClient { Future call( {required String method, List params = const [], Function(int)? idCallback}) async { - if (socket == null || !isConnected) { + if (socket == null) { return null; } final completer = Completer(); @@ -455,7 +455,7 @@ class ElectrumClient { Future callWithTimeout( {required String method, List params = const [], int timeout = 5000}) async { try { - if (socket == null || !isConnected) { + if (socket == null) { return null; } final completer = Completer(); diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index a09850f59..afa8cc388 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -7,7 +7,6 @@ import 'package:crypto/crypto.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/mweb_utxo.dart'; -import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_mweb/mwebd.pbgrpc.dart'; import 'package:fixnum/fixnum.dart'; import 'package:bip39/bip39.dart' as bip39; @@ -425,7 +424,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { bool? usingElectrs, }) async { _syncTimer?.cancel(); - int oldHeight = walletInfo.restoreHeight; await walletInfo.updateRestoreHeight(height); // go through mwebUtxos and clear any that are above the new restore height: diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index 4324823c5..714d229d9 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -23,7 +23,7 @@ abstract class WalletAddresses { return _localAddress ?? address; } - String get primaryAddress; + String get primaryAddress => address; String? _localAddress; diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index 84bdfd58b..478f1a3c3 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -1061,7 +1061,7 @@ class BalanceRowWidget extends StatelessWidget { ), ), ), - SizedBox(width: 32), + SizedBox(width: 24), Expanded( child: Semantics( label: S.of(context).litecoin_mweb_pegout, diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 428891b2c..22fa7ef20 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -16,14 +16,14 @@ APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" MONERO_COM_VERSION="1.17.0" -MONERO_COM_BUILD_NUMBER=102 +MONERO_COM_BUILD_NUMBER=103 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.20.0" -CAKEWALLET_BUILD_NUMBER=230 +CAKEWALLET_BUILD_NUMBER=232 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 d976f07fd..066b752cc 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -14,12 +14,12 @@ APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" MONERO_COM_VERSION="1.17.0" -MONERO_COM_BUILD_NUMBER=100 +MONERO_COM_BUILD_NUMBER=101 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.20.0" -CAKEWALLET_BUILD_NUMBER=269 +CAKEWALLET_BUILD_NUMBER=276 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/linux/app_env.sh b/scripts/linux/app_env.sh index aaeb2bc2a..f1616d7c5 100755 --- a/scripts/linux/app_env.sh +++ b/scripts/linux/app_env.sh @@ -15,7 +15,7 @@ fi CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="1.10.0" -CAKEWALLET_BUILD_NUMBER=35 +CAKEWALLET_BUILD_NUMBER=36 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 af2dc1322..9c5fef1ff 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -17,12 +17,12 @@ fi MONERO_COM_NAME="Monero.com" MONERO_COM_VERSION="1.7.0" -MONERO_COM_BUILD_NUMBER=33 +MONERO_COM_BUILD_NUMBER=34 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="1.13.0" -CAKEWALLET_BUILD_NUMBER=90 +CAKEWALLET_BUILD_NUMBER=92 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then From 0a1e32c363fbf1a654b459665cfc7cbc6643080e Mon Sep 17 00:00:00 2001 From: omahs <73983677+omahs@users.noreply.github.com> Date: Tue, 22 Oct 2024 02:29:00 +0200 Subject: [PATCH 27/73] Fix typos (#1760) * fix typos * fix typos * fix typos * fix typos --- build-guide-linux.md | 4 ++-- how_to_add_new_wallet_type.md | 6 +++--- howto-build-ios.md | 4 ++-- howto-build-windows.md | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/build-guide-linux.md b/build-guide-linux.md index e0158945b..50ecc76fe 100644 --- a/build-guide-linux.md +++ b/build-guide-linux.md @@ -15,7 +15,7 @@ These steps will help you configure and execute a build of CakeWallet from its s ### 1. Installing Package Dependencies -CakeWallet requires some packages to be install on your build system. You may easily install them on your build system with the following command: +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` @@ -145,7 +145,7 @@ Path to executable file will be: # Flatpak -For package the built application into flatpak you need fistly to install `flatpak` and `flatpak-builder`: +For package the built application into flatpak you need firstly to install `flatpak` and `flatpak-builder`: `$ sudo apt install flatpak flatpak-builder` diff --git a/how_to_add_new_wallet_type.md b/how_to_add_new_wallet_type.md index 917e87cf4..d71e181e0 100644 --- a/how_to_add_new_wallet_type.md +++ b/how_to_add_new_wallet_type.md @@ -5,7 +5,7 @@ **N:B** Throughout this guide, `walletx` refers to the specific wallet type you want to add. If you're adding `BNB` to CakeWallet, then `walletx` for you here is `bnb`. **Core Folder/Files Setup** -- Idenitify your core component/package (major project component), which would power the integration e.g web3dart, solana, onchain etc +- Identify your core component/package (major project component), which would power the integration e.g web3dart, solana, onchain etc - Add a new entry to `WalletType` class in `cw_core/wallet_type.dart`. - Fill out the necessary information in the various functions in the files, concerning the wallet name, the native currency type, symbol etc. - Go to `cw_core/lib/currency_for_wallet_type.dart`, in the `currencyForWalletType` function, add a case for `walletx`, returning the native cryptocurrency for `walletx`. @@ -144,7 +144,7 @@ You can add as many node entries as desired. } } -- Next, we’ll write the function to change walletX current node to default. An handy function we would make use of later on. Add a new preference key in `lib/entities/preference_key.dart` with the format `PreferencesKey.currentWalletXNodeIdKey`, we’ll use it to identify the current node id. +- Next, we’ll write the function to change walletX current node to default. A handy function we would make use of later on. Add a new preference key in `lib/entities/preference_key.dart` with the format `PreferencesKey.currentWalletXNodeIdKey`, we’ll use it to identify the current node id. Future changeWalletXCurrentNodeToDefault( {required SharedPreferences sharedPreferences, required Box nodes}) async { @@ -228,7 +228,7 @@ Now you can run the codebase and successfully create a wallet for type walletX s **Balance Screen** - Go to `lib/view_model/dashboard/balance_view_model.dart` -- Modify the function to adjust the way the balance is being display on the app: `isHomeScreenSettingsEnabled` +- Modify the function to adjust the way the balance is being displayed on the app: `isHomeScreenSettingsEnabled` - Add a case to the `availableBalanceLabel` getter to modify the text being displayed (Available or confirmed) - Same for `additionalBalanceLabel` - Next, go to `lib/reactions/fiat_rate_update.dart` diff --git a/howto-build-ios.md b/howto-build-ios.md index 418fbc96b..544d4359e 100644 --- a/howto-build-ios.md +++ b/howto-build-ios.md @@ -57,7 +57,7 @@ Proceed into the source code before proceeding with the next steps: ### 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. +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. @@ -92,7 +92,7 @@ Then we need to generate localization files and mobx models. `$ flutter build ios --release` -Then you can open `ios/Runner.xcworkspace` with Xcode and you can to archive the application. +Then you can open `ios/Runner.xcworkspace` with Xcode and you can archive the application. Or if you want to run to connected device: diff --git a/howto-build-windows.md b/howto-build-windows.md index 796cb3cc8..504f8f785 100644 --- a/howto-build-windows.md +++ b/howto-build-windows.md @@ -22,7 +22,7 @@ Then install `Desktop development with C++` packages via Visual Studio 2022, or - `C++ 2022 Redistributable Update` - `C++ core desktop features` - `MVC v143 - VS 2022 C++ x64/x86 build tools` -- `C++ CMake tools for Windwos` +- `C++ CMake tools for Windows` - `Testing tools core features - Build Tools` - `C++ AddressSanitizer`. @@ -38,7 +38,7 @@ For building monero dependencies, it is required to install Windows WSL (https:/ ### 5. Pull Cake Wallet source code -You can downlaod CakeWallet source code from our [GitHub repository](github.com/cake-tech/cake_wallet) via git: +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) @@ -52,6 +52,6 @@ For that you need to run the shell (bash - typically same named utility should b 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 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. +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. From 20efba43cb2e335bd75f88d05c6c00649a3239f0 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Tue, 22 Oct 2024 15:23:42 +0300 Subject: [PATCH 28/73] enable ssl for btc-electrum (#1765) --- assets/bitcoin_electrum_server_list.yml | 1 + lib/entities/default_settings_migration.dart | 12 ++++++++++++ lib/main.dart | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/assets/bitcoin_electrum_server_list.yml b/assets/bitcoin_electrum_server_list.yml index 305db38f5..20a28cd24 100644 --- a/assets/bitcoin_electrum_server_list.yml +++ b/assets/bitcoin_electrum_server_list.yml @@ -3,6 +3,7 @@ useSSL: true - uri: btc-electrum.cakewallet.com:50002 + useSSL: true isDefault: true - uri: electrs.cakewallet.com:50001 diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 198781cea..af6f1ce78 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -256,6 +256,9 @@ Future defaultSettingsMigration( await _addSethNode(nodes, sharedPreferences); await updateTronNodesWithNowNodes(sharedPreferences: sharedPreferences, nodes: nodes); break; + case 42: + updateBtcElectrumNodeToUseSSL(nodes, sharedPreferences); + break; default: break; } @@ -270,6 +273,15 @@ Future defaultSettingsMigration( await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, version); } +void updateBtcElectrumNodeToUseSSL(Box nodes, SharedPreferences sharedPreferences) { + final btcElectrumNode = nodes.values.firstWhereOrNull((element) => element.uriRaw == newCakeWalletBitcoinUri); + + if (btcElectrumNode != null) { + btcElectrumNode.useSSL = true; + btcElectrumNode.save(); + } +} + void _deselectQuantex(SharedPreferences sharedPreferences) { final Map exchangeProvidersSelection = json.decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") diff --git a/lib/main.dart b/lib/main.dart index 7237de8b9..2b5bef06c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -192,7 +192,7 @@ Future initializeAppConfigs() async { transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 41, + initialMigrationVersion: 42, ); } From 3898208a6aad9f41b38eaca210c66178a38156f4 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Tue, 22 Oct 2024 16:46:25 +0300 Subject: [PATCH 29/73] fix send all (#1766) * shouldn't matter, but still investigating why this fixes it * got it, but this change is just for consistency [skip ci] --- cw_bitcoin/lib/electrum_wallet.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 893cff6f2..4d127c248 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1063,7 +1063,7 @@ abstract class ElectrumWalletBase if (sendAll) { estimatedTx = await estimateSendAllTx( - outputs, + updatedOutputs, feeRateInt, memo: memo, credentialsAmount: credentialsAmount, @@ -1115,7 +1115,7 @@ abstract class ElectrumWalletBase if (network is BitcoinCashNetwork) { txb = ForkedTransactionBuilder( utxos: estimatedTx.utxos, - outputs: outputs, + outputs: updatedOutputs, fee: BigInt.from(estimatedTx.fee), network: network, memo: estimatedTx.memo, From 68b18f66bd6e604d09602c5ee26901837f9c70ba Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Tue, 22 Oct 2024 17:40:49 +0300 Subject: [PATCH 30/73] new versions (#1767) --- assets/text/Release_Notes.txt | 3 ++- 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/windows/build_exe_installer.iss | 2 +- 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index d359d6e8a..61aafb6e4 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -3,4 +3,5 @@ Added wallet groups Silent Payment enhancements for speed & reliability Monero enhancements Introducing StealthEx and LetxExchange -Additional ERC20 tokens scam detection \ No newline at end of file +Additional ERC20 tokens scam detection +Bug fixes \ No newline at end of file diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 22fa7ef20..8beffbcc2 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.20.0" -CAKEWALLET_BUILD_NUMBER=232 +CAKEWALLET_VERSION="4.20.1" +CAKEWALLET_BUILD_NUMBER=233 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 066b752cc..bc3b39747 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -18,8 +18,8 @@ MONERO_COM_BUILD_NUMBER=101 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.20.0" -CAKEWALLET_BUILD_NUMBER=276 +CAKEWALLET_VERSION="4.20.1" +CAKEWALLET_BUILD_NUMBER=277 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/linux/app_env.sh b/scripts/linux/app_env.sh index f1616d7c5..2dabf083b 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.10.0" -CAKEWALLET_BUILD_NUMBER=36 +CAKEWALLET_VERSION="1.10.1" +CAKEWALLET_BUILD_NUMBER=37 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 9c5fef1ff..026ea034b 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -21,8 +21,8 @@ MONERO_COM_BUILD_NUMBER=34 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.13.0" -CAKEWALLET_BUILD_NUMBER=92 +CAKEWALLET_VERSION="1.13.1" +CAKEWALLET_BUILD_NUMBER=93 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 33cd11292..f65900cca 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.1.0" +#define MyAppVersion "0.1.1" #define MyAppPublisher "Cake Labs LLC" #define MyAppURL "https://cakewallet.com/" #define MyAppExeName "CakeWallet.exe" From e04185a7c1c96f2bd155a5a491d21b80a0f9b24f Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Tue, 22 Oct 2024 21:06:08 +0300 Subject: [PATCH 31/73] upload x86 arch to artifacts [skip ci] --- .github/workflows/pr_test_build_android.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 925f4e00d..c9021fac0 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -224,6 +224,7 @@ jobs: cd /opt/android/cake_wallet/build/app/outputs/flutter-apk mkdir test-apk cp app-arm64-v8a-release.apk test-apk/${{env.BRANCH_NAME}}.apk + cp app-x86_64-release.apk test-apk/${{env.BRANCH_NAME}}_x86.apk - name: Upload Artifact uses: kittaakos/upload-artifact-as-is@v0 From 68926c0a337700165b807e76f34a31c52a9d0b79 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Wed, 23 Oct 2024 17:38:31 +0200 Subject: [PATCH 32/73] Cw 679 add ledger litecoin support (#1565) * Add Litecoin Hardware Wallet Creation * Add Litecoin Hardware Wallet Creation * Fix Bitcoin not sending on Ledger * Fixes to sending LTC using Ledger * CW-679 Fix merge conflicts * CW-679 Fix merge conflicts * CW-679 Minor fixes * CW-679 Add derivation Path of change address * ledger flutter plus refactoring * ledger flutter plus refactoring * ledger flutter plus refactoring * Ups :| * Ups :| I forgot USB * Handle BT Off * Fix Issue with A14 and USB * Small Ledger Quality of life improvements * Small Ledger Quality of life improvements * Small Ledger Quality of life improvements * Small Ledger Quality of life improvements * Small Ledger Quality of life improvements * Small Ledger Quality of life improvements * Small Ledger Quality of life improvements * Pls work * Pls work * Pls work * Pls work * Fix overpopulation * Fix ble device detection and support for Stax and Flex * clean up pubspec * clean up * MWeb merge fix * MWeb merge fix * Fix Merge conflicts * Fix Requested changes --- assets/images/hardware_wallet/ledger_flex.png | Bin 0 -> 1777 bytes .../images/hardware_wallet/ledger_nano_s.png | Bin 0 -> 2639 bytes .../images/hardware_wallet/ledger_nano_x.png | Bin 0 -> 2634 bytes assets/images/hardware_wallet/ledger_stax.png | Bin 0 -> 1612 bytes assets/images/ledger_nano.png | Bin 1463 -> 0 bytes .../lib/bitcoin_hardware_wallet_service.dart | 21 +-- cw_bitcoin/lib/bitcoin_wallet.dart | 57 ++++---- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 1 + cw_bitcoin/lib/electrum_wallet.dart | 69 ++++++--- cw_bitcoin/lib/electrum_wallet_addresses.dart | 21 ++- .../lib/litecoin_hardware_wallet_service.dart | 46 ++++++ cw_bitcoin/lib/litecoin_wallet.dart | 102 +++++++++++-- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 29 ++-- cw_bitcoin/lib/litecoin_wallet_service.dart | 23 ++- cw_bitcoin/lib/psbt_transaction_builder.dart | 4 - cw_bitcoin/pubspec.lock | 134 +++++++++++------- cw_bitcoin/pubspec.yaml | 19 +-- .../lib/src/bitcoin_cash_wallet.dart | 1 + .../src/bitcoin_cash_wallet_addresses.dart | 1 + .../lib/hardware/device_connection_type.dart | 1 + .../evm_chain_hardware_wallet_service.dart | 18 +-- .../evm_chain_transaction_credentials.dart | 1 - cw_evm/lib/evm_ledger_credentials.dart | 57 ++++---- cw_evm/pubspec.yaml | 9 +- lib/bitcoin/cw_bitcoin.dart | 24 +++- .../hardware_wallet_device.dart | 65 +++++++++ lib/ethereum/cw_ethereum.dart | 16 +-- lib/main.dart | 14 +- lib/polygon/cw_polygon.dart | 15 +- .../connect_device/connect_device_page.dart | 120 ++++++++++------ .../connect_device/debug_device_page.dart | 112 +++++++++------ .../connect_device/widgets/device_tile.dart | 2 +- .../screens/dashboard/pages/balance_page.dart | 2 +- .../screens/restore/restore_options_page.dart | 4 +- lib/src/screens/send/send_page.dart | 11 +- .../dashboard/dashboard_view_model.dart | 2 +- .../hardware_wallet/ledger_view_model.dart | 109 ++++++++++---- lib/view_model/send/send_view_model.dart | 19 ++- .../wallet_hardware_restore_view_model.dart | 13 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec_base.yaml | 9 +- res/values/strings_de.arb | 74 +++++----- tool/configure.dart | 16 ++- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 45 files changed, 842 insertions(+), 405 deletions(-) create mode 100644 assets/images/hardware_wallet/ledger_flex.png create mode 100644 assets/images/hardware_wallet/ledger_nano_s.png create mode 100644 assets/images/hardware_wallet/ledger_nano_x.png create mode 100644 assets/images/hardware_wallet/ledger_stax.png delete mode 100644 assets/images/ledger_nano.png create mode 100644 cw_bitcoin/lib/litecoin_hardware_wallet_service.dart create mode 100644 lib/entities/hardware_wallet/hardware_wallet_device.dart diff --git a/assets/images/hardware_wallet/ledger_flex.png b/assets/images/hardware_wallet/ledger_flex.png new file mode 100644 index 0000000000000000000000000000000000000000..fa39f241fccf109565a28f66c090bf57ca274fd0 GIT binary patch literal 1777 zcmVPx*t4TybRCr$Poo$FzRT#(r&s~*jgAA8~ie9AXgwQf<` z8}8g$1C7X?NwLTX9r_R^>D=*~Nl*lS%A7l!(W{1qRG&;1F<6t<`^2uhIxqKS?mhR+ znVmiJbj;={ z0FtS#w>fa`137&Ls1M+))cSxX9Pnwdae6_I^ZhsbawprVr!5iC|J1fV_td!$ z4;|V(DNCs|0LfK_0cI|Nu~e3LS>x742H}|FAU#$#e~@8M3V>uLzm6I2%Mg5N%%&ic z8dc|r@m!^Bej4|jVh1pgDZE0=@5U{(6UHOLno7ys9jBI90W43A_B%w?V1ZIsYq0s~ zE&esw=|55)`8oO!^*v()a8GLU)xC7ZF92@x6*ud9##b=#BQc+8YVQCr;+o>jI38m% z(PB8b7laipT0CxB=3One4Qv)0fJC~m4dB-UES@C7rVA6l?;kr*oCs9c#s>GV-rBq9 z{A7keZvnW$w|r$Du9kCK6FTnTqFfMhzqlR-mn?|OCvfQ$3PQ2mWA zj~$GS2kr8(=hH#AUgA}d-hwXq)gj~dvC{C=bL5tP;sCn53=NTl&)Sw*^9Fcp3^ahC zTM>;5%%d9Rh%al21JGL#nE35#DfdQeYrejf1`reh0gogd#zv8E2gCu)=Nu5jy)F$P zC;}ueFbzNhh?2V6*T=UE)mxB!5E_7dANyL+05kx956E62iv17S^V3z3NEKvI(cUNa z=AQ;20-*D&AfsrPhP(IO(9Q?Y<*%h^0P<6`PQvnitgizU`&a{*Qvm8dLJ`k#x(w~^ zGE{tpsE&@yN!aCKu&#pS`v1tk}mIfCeDo0oi+`fV5b;%ATJtLviehu5E4L%bzH@wAa@GtvOMo)f|Zc8gxC3oAd*jAow~7>m(esM%4b@eG8hoFQjvT zd^mF?2h1GU)aZ{GR)~BT`&;OI00LjgJd`QiJ?UVDK~A3vM>}ks2JmIIoIUPk-|w)F zIKBdvnuH0?u8K9{dT8JDzcwLyM}^}XZ8FokYB~FXo0i%LXgsc_GXO-0fS}WMLqiEZ z#=-3>s_y^TTwuQA+j8ziW5YQe0r1EH1L?vh0&foOXsI1&#I~|c2hetY4)FPe$16k= z>HJiFjsfi&zehxF2i*3XEezu$IA+vs3J0l0I z$`|3x+=^)NunwsJ!XumG03syesl>+2Yn#I_;_{8?^xAHo+hv6Lg^?p=3%XRUc_iU5 z2jPX8j8I!It=;&Us!9N!M^9=1LhDz&^Be~77=im;JwQFxN@@5rm(uQH9~u8IEGmj1 zt#0Fb^a&$DI0u9=rrS1qKm$;`Dl`BMK(PlDo7WMb0Vwu>V)HrzGyugOP;CBx3ot01 ToY=F<00000NkvXXu0mjfR%KGE literal 0 HcmV?d00001 diff --git a/assets/images/hardware_wallet/ledger_nano_s.png b/assets/images/hardware_wallet/ledger_nano_s.png new file mode 100644 index 0000000000000000000000000000000000000000..02777aeb69b2e436d2b9bd96c3b5f39a773b7def GIT binary patch literal 2639 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 literal 0 HcmV?d00001 diff --git a/assets/images/hardware_wallet/ledger_stax.png b/assets/images/hardware_wallet/ledger_stax.png new file mode 100644 index 0000000000000000000000000000000000000000..06c9c848e8336bacbe4378b5e5999604393821e6 GIT binary patch literal 1612 zcmV-S2DABzP)Px*21!IgRCr$Pon43I}_78*`QruT<7~(O#p^{uMC_A;~WhcKbIC#6~BG-ECNOo+54F2Fo0X_2W7dy z!sG7R=Dz?=7#C^Z;?dMOVW(P}^8`R_JohLuj`=YFsXnYJuqH+o-ZC?#D#g^hQuf=~ z5eG0jHob#489xEo;mmnwvvpk{XI6V|Ww~YItBWunKV6*oqi1_<42T1WC1xH2aZ%c^ zC;(^7tCBJEnN8So*Vo0Dt@I@Rk=6&rug|k80B!l8!R7v6hMw7Ys7BcPw!Izk_t4~@;ilU1SkLxr*jvkH3GU2P4bfP0~nu~ z1M}WiqV#(eWceJo@gKlv2Ak={;>7n-(B;>F@!2`B8C3vP-5Hpz6w)aP2=V|zdMCW0 z40W`Z#o%Wuh0F)NLXZa#?G&_8@88RX^fd~gR|Oi&Q2RBYuJw??H-58_`K1>ZJdc2l z4^kqq2-j)N2H##;$eiwL02`fC0L`d8i2!Ea+sd#3%q{luW?XX_YX3B?KO-QSn`dB8 zOS0kEjB`{4S+ES|*kUoW)L$uRK+4c6fD0U3Jf;A8nSf2#+HOTOwAKg^m43}w_yG*g zjTriJ5w?3PA`E4yo&QY%$Hr~t;=ylw2}Joav|$mhd!tROpe9!G00trjt=%82GT(fv zm^s_m5m4t`v?5@+Tu5*2MSSa8`3M-0`v78krBqD4(SHC2AO#UHT`Hs}dllcC2&e;4 zzn2n*D#-qkBp+uoCoB2M?CJq6%WFzMzBKw43k_Syp=Yu`ZpD^^+s4Nev0%?xrm!81LE2+#=dc1F9k9*IS`?GLwV8LDMyr-+D71n6d5C)U;f z>muxU5hfUfFh7t9&s!0Gu3;JhO%aei4$vdb`J*b;;c_9h-%)OH*hn5g*uSEb%vnEO zd~Iv}cz?N&vChR2`^W>RJ(LgOBHZcl31+&#QcQg*roJx1Yf|3()YQn|%OkS{ykMir z%3tR%-*)!oODp{iz$OLti5?1$w-`e{k=Jt;$7{nkQPqp|-LtFBBs zxX4^JZ&~_u;ztKfT{RJ314fhCDF%uD#=ff&dC-`MkCgJ6Cp|DNe4t70)Lkbkv^!(gnzhR2L!mmz%@Qx3$UX?7 zlb3{S8n+W62n^nMyr2kA$3_Sr51X5x^=jtV{19IQYKH$$MehT+%JHCGH*^BhrBTL7 z8lu;{I5Vu3C!PVYI@>k&=G7O5&D{q0dJxwgjI(~ytaaY3^;fVTlZ`(K^hvpR@Ham_ z@f3ic0|{$i3Ls!|3P1q_tU$osYXm5OfE5V1dyN1E5U>IPcmE$44V#X~Kndvp0000< KMNUMnLSTZg0^5E7 literal 0 HcmV?d00001 diff --git a/assets/images/ledger_nano.png b/assets/images/ledger_nano.png deleted file mode 100644 index bb61ba1750728b5584680110bd90727c3e195eca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1463 zcmV;o1xWgdP)@~0drDELIAGL9O(c600d`2O+f$vv5yPXg70CCgwzCt2)>6E5>gQmA^0Admtb5#jNp4@T7ppl2?XCG zvl5I6&<#GdX&}HZO+ObEO*1LMh=4G`-``rVKYsMEJvYB3Y?P3=fUv>$ez#RZq5`4@ z-@`@;i3x}sd=J|sBqAWG;Ct94A+~^|gYRLBgjfPh1>eI436TYu4!%d$B}5WX{=8Yr zA+%KO;s)O%%Mv09c=gh?Iz{7ZP@ANJ?~zpr4n%;;XqSUOPd4}-hqO(kZL8W*oBBJ{ zMZ!5Ck^qg(A{ZiT5+Zd$ugrgVMbQNr?FO>c9Yq@h-ds8oLevz^Sb_$T1U&xZVRsr1 zJ_`~euLGv#;Lp2+SSw+c5TDq&AeIo1M6#hQAv%fG16V>d60L}_gzzL@6J`nFFmhJ} zOVG!N00~Rb!~R z=8tYgi|P$ufI;;41FB$<)CEZN4j!Mt6zm5I=z&2dB0!=#cvm=Wn1cNP7usNuRL6i( z{Q6>fSHthapnDIPE+Dk~eQYH0ZX+!L z2|n?jEf6;2ickuW4`UQu@fR>Yh@K;$@%xXrBlQl-R74&fqsgfA56-%Xccb14uM{BA zEd!$@>hlN_OU7jpDdFAgcUHrECl4{80i;VglQv`=1SnB`qAa@`Ra+Nfqg-vabO+{G z0%Y(Hw%2ZeB~+H1H4?EQh4{C^*+M})pda1`4Y&&mflN)GoE%-Dv;2NwLjj92!wVY3 zx&pGs*_Hla^-IN=(UotfRhJdA)yhhfx+DLx=`g+CR{c-D0*1)NW}$fDKS5JUI)iIe zoY@eL*r_akvqOD)X|#R+Z1%P6uPu)qOD0bx4arV#hN?UaZ{&lp?Hykk0rm)b+g1Q zASCk4FU(xHOA)RGweuS@MqvS(3<6)6sSdgwR4lNlSihG>JwWD3C|UIYvQG&$3o@rO zCRgk1m&CmSN-}c@79c?HHFz|LH}*o@roz+E1Q|Tn5c2jEr2rI;axTk6e^sYaz|-UZ zH-QO2^k{sz74=!!QiPFbu=Q;yG(reAP{& RfT{oh002ovPDHLkV1g$1iR}OY diff --git a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart index de339175d..a02c51c69 100644 --- a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart @@ -5,30 +5,31 @@ import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:ledger_bitcoin/ledger_bitcoin.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; class BitcoinHardwareWalletService { - BitcoinHardwareWalletService(this.ledger, this.device); + BitcoinHardwareWalletService(this.ledgerConnection); - final Ledger ledger; - final LedgerDevice device; + final LedgerConnection ledgerConnection; - Future> getAvailableAccounts({int index = 0, int limit = 5}) async { - final bitcoinLedgerApp = BitcoinLedgerApp(ledger); + Future> getAvailableAccounts( + {int index = 0, int limit = 5}) async { + final bitcoinLedgerApp = BitcoinLedgerApp(ledgerConnection); - final masterFp = await bitcoinLedgerApp.getMasterFingerprint(device); - print(masterFp); + final masterFp = await bitcoinLedgerApp.getMasterFingerprint(); final accounts = []; final indexRange = List.generate(limit, (i) => i + index); for (final i in indexRange) { final derivationPath = "m/84'/0'/$i'"; - final xpub = await bitcoinLedgerApp.getXPubKey(device, derivationPath: derivationPath); + final xpub = + await bitcoinLedgerApp.getXPubKey(derivationPath: derivationPath); Bip32Slip10Secp256k1 hd = Bip32Slip10Secp256k1.fromExtendedKey(xpub).childKey(Bip32KeyIndex(0)); - final address = generateP2WPKHAddress(hd: hd, index: 0, network: BitcoinNetwork.mainnet); + final address = generateP2WPKHAddress( + hd: hd, index: 0, network: BitcoinNetwork.mainnet); accounts.add(HardwareAccountData( address: address, diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 30f04667a..908897845 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -5,13 +5,13 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; +import 'package:cw_bitcoin/psbt_transaction_builder.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; -import 'package:cw_bitcoin/psbt_transaction_builder.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_info.dart'; @@ -19,7 +19,7 @@ import 'package:cw_core/wallet_keys_file.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:ledger_bitcoin/ledger_bitcoin.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:mobx/mobx.dart'; part 'bitcoin_wallet.g.dart'; @@ -61,8 +61,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialBalance: initialBalance, seedBytes: seedBytes, encryptionFileUtils: encryptionFileUtils, - currency: - networkParam == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc, + currency: networkParam == BitcoinNetwork.testnet + ? CryptoCurrency.tbtc + : CryptoCurrency.btc, alwaysScan: alwaysScan, ) { // in a standard BIP44 wallet, mainHd derivation path = m/84'/0'/0'/0 (account 0, index unspecified here) @@ -80,11 +81,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { mainHd: hd, sideHd: accountHD.childKey(Bip32KeyIndex(1)), network: networkParam ?? network, - masterHd: seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null, + masterHd: + seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null, + isHardwareWallet: walletInfo.isHardwareWallet, ); autorun((_) { - this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; + this.walletAddresses.isEnabledAutoGenerateSubaddress = + this.isEnabledAutoGenerateSubaddress; }); } @@ -185,8 +189,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { walletInfo.derivationInfo ??= DerivationInfo(); // set the default if not present: - walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? electrum_path; - walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum; + walletInfo.derivationInfo!.derivationPath ??= + snp?.derivationPath ?? electrum_path; + walletInfo.derivationInfo!.derivationType ??= + snp?.derivationType ?? DerivationType.electrum; Uint8List? seedBytes = null; final mnemonic = keysData.mnemonic; @@ -228,15 +234,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { ); } - Ledger? _ledger; - LedgerDevice? _ledgerDevice; + LedgerConnection? _ledgerConnection; BitcoinLedgerApp? _bitcoinLedgerApp; - void setLedger(Ledger setLedger, LedgerDevice setLedgerDevice) { - _ledger = setLedger; - _ledgerDevice = setLedgerDevice; - _bitcoinLedgerApp = - BitcoinLedgerApp(_ledger!, derivationPath: walletInfo.derivationInfo!.derivationPath!); + @override + void setLedgerConnection(LedgerConnection connection) { + _ledgerConnection = connection; + _bitcoinLedgerApp = BitcoinLedgerApp(_ledgerConnection!, + derivationPath: walletInfo.derivationInfo!.derivationPath!); } @override @@ -251,12 +256,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { BitcoinOrdering inputOrdering = BitcoinOrdering.bip69, BitcoinOrdering outputOrdering = BitcoinOrdering.bip69, }) async { - final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint(_ledgerDevice!); + final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint(); final psbtReadyInputs = []; for (final utxo in utxos) { - final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); - final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; + final rawTx = + await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); + final publicKeyAndDerivationPath = + publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; psbtReadyInputs.add(PSBTReadyUtxoWithAddress( utxo: utxo.utxo, @@ -268,10 +275,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { )); } - final psbt = - PSBTTransactionBuild(inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF); + final psbt = PSBTTransactionBuild( + inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF); - final rawHex = await _bitcoinLedgerApp!.signPsbt(_ledgerDevice!, psbt: psbt.psbt); + final rawHex = await _bitcoinLedgerApp!.signPsbt(psbt: psbt.psbt); return BtcTransaction.fromRaw(BytesUtils.toHexString(rawHex)); } @@ -279,14 +286,16 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Future signMessage(String message, {String? address = null}) async { if (walletInfo.isHardwareWallet) { final addressEntry = address != null - ? walletAddresses.allAddresses.firstWhere((element) => element.address == address) + ? walletAddresses.allAddresses + .firstWhere((element) => element.address == address) : null; final index = addressEntry?.index ?? 0; final isChange = addressEntry?.isHidden == true ? 1 : 0; final accountPath = walletInfo.derivationInfo?.derivationPath; - final derivationPath = accountPath != null ? "$accountPath/$isChange/$index" : null; + final derivationPath = + accountPath != null ? "$accountPath/$isChange/$index" : null; - final signature = await _bitcoinLedgerApp!.signMessage(_ledgerDevice!, + final signature = await _bitcoinLedgerApp!.signMessage( message: ascii.encode(message), signDerivationPath: derivationPath); return base64Encode(signature); } diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 697719894..04a3cae36 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -15,6 +15,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S required super.mainHd, required super.sideHd, required super.network, + required super.isHardwareWallet, super.initialAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 4d127c248..4b80cb201 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -6,7 +6,6 @@ import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_wallet.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:cw_core/encryption_file_utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:collection/collection.dart'; import 'package:cw_bitcoin/address_from_output.dart'; @@ -26,6 +25,8 @@ import 'package:cw_bitcoin/exceptions.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/encryption_file_utils.dart'; +import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pending_transaction.dart'; @@ -37,10 +38,10 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; import 'package:mobx/mobx.dart'; import 'package:rxdart/subjects.dart'; import 'package:sp_scanner/sp_scanner.dart'; @@ -51,9 +52,10 @@ part 'electrum_wallet.g.dart'; class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet; -abstract class ElectrumWalletBase - extends WalletBase - with Store, WalletKeysFile { +abstract class ElectrumWalletBase extends WalletBase< + ElectrumBalance, + ElectrumTransactionHistory, + ElectrumTransactionInfo> with Store, WalletKeysFile { ElectrumWalletBase({ required String password, required WalletInfo walletInfo, @@ -69,8 +71,8 @@ abstract class ElectrumWalletBase ElectrumBalance? initialBalance, CryptoCurrency? currency, this.alwaysScan, - }) : accountHD = - getAccountHDWallet(currency, network, seedBytes, xpub, walletInfo.derivationInfo), + }) : accountHD = getAccountHDWallet( + currency, network, seedBytes, xpub, walletInfo.derivationInfo), syncStatus = NotConnectedSyncStatus(), _password = password, _feeRates = [], @@ -105,8 +107,12 @@ abstract class ElectrumWalletBase sharedPrefs.complete(SharedPreferences.getInstance()); } - static Bip32Slip10Secp256k1 getAccountHDWallet(CryptoCurrency? currency, BasedUtxoNetwork network, - Uint8List? seedBytes, String? xpub, DerivationInfo? derivationInfo) { + static Bip32Slip10Secp256k1 getAccountHDWallet( + CryptoCurrency? currency, + BasedUtxoNetwork network, + Uint8List? seedBytes, + String? xpub, + DerivationInfo? derivationInfo) { if (seedBytes == null && xpub == null) { throw Exception( "To create a Wallet you need either a seed or an xpub. This should not happen"); @@ -117,8 +123,9 @@ abstract class ElectrumWalletBase case CryptoCurrency.btc: case CryptoCurrency.ltc: case CryptoCurrency.tbtc: - return Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath( - _hardenedDerivationPath(derivationInfo?.derivationPath ?? electrum_path)) + return Bip32Slip10Secp256k1.fromSeed(seedBytes, getKeyNetVersion(network)) + .derivePath(_hardenedDerivationPath( + derivationInfo?.derivationPath ?? electrum_path)) as Bip32Slip10Secp256k1; case CryptoCurrency.bch: return bitcoinCashHDWallet(seedBytes); @@ -127,15 +134,26 @@ abstract class ElectrumWalletBase } } - return Bip32Slip10Secp256k1.fromExtendedKey(xpub!); + return Bip32Slip10Secp256k1.fromExtendedKey( + xpub!, getKeyNetVersion(network)); } static Bip32Slip10Secp256k1 bitcoinCashHDWallet(Uint8List seedBytes) => - Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") as Bip32Slip10Secp256k1; + Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") + as Bip32Slip10Secp256k1; static int estimatedTransactionSize(int inputsCount, int outputsCounts) => inputsCount * 68 + outputsCounts * 34 + 10; + static Bip32KeyNetVersions? getKeyNetVersion(BasedUtxoNetwork network) { + switch (network) { + case LitecoinNetwork.mainnet: + return Bip44Conf.litecoinMainNet.altKeyNetVer; + default: + return null; + } + } + bool? alwaysScan; final Bip32Slip10Secp256k1 accountHD; @@ -634,8 +652,9 @@ abstract class ElectrumWalletBase ECPrivate? privkey; bool? isSilentPayment = false; - final hd = - utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd; + final hd = utx.bitcoinAddressRecord.isHidden + ? walletAddresses.sideHd + : walletAddresses.mainHd; if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) { final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord; @@ -833,7 +852,7 @@ abstract class ElectrumWalletBase inputs: utxoDetails.availableInputs, outputs: updatedOutputs, ); - final address = RegexUtils.addressTypeFromStr(changeAddress, network); + final address = RegexUtils.addressTypeFromStr(changeAddress.address, network); updatedOutputs.add(BitcoinOutput( address: address, value: BigInt.from(amountLeftForChangeAndFee), @@ -845,6 +864,14 @@ abstract class ElectrumWalletBase isChange: true, )); + // Get Derivation path for change Address since it is needed in Litecoin and BitcoinCash hardware Wallets + final changeDerivationPath = + "${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? "m/0'")}" + "/${changeAddress.isHidden ? "1" : "0"}" + "/${changeAddress.index}"; + utxoDetails.publicKeys[address.pubKeyHash()] = + PublicKeyWithDerivationPath('', changeDerivationPath); + // calcFee updates the silent payment outputs to calculate the tx size accounting // for taproot addresses, but if more inputs are needed to make up for fees, // the silent payment outputs need to be recalculated for the new inputs @@ -1206,6 +1233,9 @@ abstract class ElectrumWalletBase } } + void setLedgerConnection(ledger.LedgerConnection connection) => + throw UnimplementedError(); + Future buildHardwareWalletTransaction({ required List outputs, required BigInt fee, @@ -1563,7 +1593,9 @@ abstract class ElectrumWalletBase final btcAddress = RegexUtils.addressTypeFromStr(addressRecord.address, network); final privkey = generateECPrivate( - hd: addressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, + hd: addressRecord.isHidden + ? walletAddresses.sideHd + : walletAddresses.mainHd, index: addressRecord.index, network: network); @@ -1745,7 +1777,8 @@ abstract class ElectrumWalletBase if (height != null) { if (time == null && height > 0) { - time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round(); + time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000) + .round(); } if (confirmations == null) { diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 6c10dc615..2686d12cc 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -36,6 +36,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { required this.mainHd, required this.sideHd, required this.network, + required this.isHardwareWallet, List? initialAddresses, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, @@ -44,6 +45,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { List? initialMwebAddresses, Bip32Slip10Secp256k1? masterHd, BitcoinAddressType? initialAddressPageType, + }) : _addresses = ObservableList.of((initialAddresses ?? []).toSet()), addressesByReceiveType = ObservableList.of(([]).toSet()), @@ -112,6 +114,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final BasedUtxoNetwork network; final Bip32Slip10Secp256k1 mainHd; final Bip32Slip10Secp256k1 sideHd; + final bool isHardwareWallet; @observable SilentPaymentOwner? silentAddress; @@ -240,15 +243,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); } else if (walletInfo.type == WalletType.litecoin) { await _generateInitialAddresses(type: SegwitAddresType.p2wpkh); - if (Platform.isAndroid || Platform.isIOS) { + if ((Platform.isAndroid || Platform.isIOS) && !isHardwareWallet) { await _generateInitialAddresses(type: SegwitAddresType.mweb); } } else if (walletInfo.type == WalletType.bitcoin) { await _generateInitialAddresses(); - await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); - await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh); - await _generateInitialAddresses(type: SegwitAddresType.p2tr); - await _generateInitialAddresses(type: SegwitAddresType.p2wsh); + if (!isHardwareWallet) { + await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); + await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh); + await _generateInitialAddresses(type: SegwitAddresType.p2tr); + await _generateInitialAddresses(type: SegwitAddresType.p2wsh); + } } updateAddressesByMatch(); @@ -267,7 +272,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @action - Future getChangeAddress({List? inputs, List? outputs, bool isPegIn = false}) async { + Future getChangeAddress({List? inputs, List? outputs, bool isPegIn = false}) async { updateChangeAddresses(); if (changeAddresses.isEmpty) { @@ -282,7 +287,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } updateChangeAddresses(); - final address = changeAddresses[currentChangeAddressIndex].address; + final address = changeAddresses[currentChangeAddressIndex]; currentChangeAddressIndex += 1; return address; } @@ -670,7 +675,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } Bip32Slip10Secp256k1 _getHd(bool isHidden) => isHidden ? sideHd : mainHd; + bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type; + bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => !addr.isHidden && !addr.isUsed && addr.type == type; diff --git a/cw_bitcoin/lib/litecoin_hardware_wallet_service.dart b/cw_bitcoin/lib/litecoin_hardware_wallet_service.dart new file mode 100644 index 000000000..62840933c --- /dev/null +++ b/cw_bitcoin/lib/litecoin_hardware_wallet_service.dart @@ -0,0 +1,46 @@ +import 'dart:async'; + +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/hardware/hardware_account_data.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; +import 'package:ledger_litecoin/ledger_litecoin.dart'; + +class LitecoinHardwareWalletService { + LitecoinHardwareWalletService(this.ledgerConnection); + + final LedgerConnection ledgerConnection; + + Future> getAvailableAccounts( + {int index = 0, int limit = 5}) async { + final litecoinLedgerApp = LitecoinLedgerApp(ledgerConnection); + + await litecoinLedgerApp.getVersion(); + + final accounts = []; + final indexRange = List.generate(limit, (i) => i + index); + final xpubVersion = Bip44Conf.litecoinMainNet.altKeyNetVer; + + for (final i in indexRange) { + final derivationPath = "m/84'/2'/$i'"; + final xpub = await litecoinLedgerApp.getXPubKey( + accountsDerivationPath: derivationPath, + xPubVersion: int.parse(hex.encode(xpubVersion.public), radix: 16)); + final hd = Bip32Slip10Secp256k1.fromExtendedKey(xpub, xpubVersion) + .childKey(Bip32KeyIndex(0)); + + final address = generateP2WPKHAddress( + hd: hd, index: 0, network: LitecoinNetwork.mainnet); + + accounts.add(HardwareAccountData( + address: address, + accountIndex: i, + derivationPath: derivationPath, + xpub: xpub, + )); + } + + return accounts; + } +} diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index afa8cc388..1fb39c878 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1,5 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:typed_data'; + import 'package:convert/convert.dart' as convert; import 'dart:math'; import 'package:collection/collection.dart'; @@ -37,6 +39,8 @@ import 'package:cw_core/wallet_keys_file.dart'; import 'package:flutter/foundation.dart'; import 'package:grpc/grpc.dart'; import 'package:hive/hive.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; +import 'package:ledger_litecoin/ledger_litecoin.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_mweb/cw_mweb.dart'; @@ -50,12 +54,13 @@ class LitecoinWallet = LitecoinWalletBase with _$LitecoinWallet; abstract class LitecoinWalletBase extends ElectrumWallet with Store { LitecoinWalletBase({ - required String mnemonic, required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, - required Uint8List seedBytes, required EncryptionFileUtils encryptionFileUtils, + Uint8List? seedBytes, + String? mnemonic, + String? xpub, String? passphrase, String? addressPageType, List? initialAddresses, @@ -68,6 +73,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { }) : super( mnemonic: mnemonic, password: password, + xpub: xpub, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, network: LitecoinNetwork.mainnet, @@ -78,8 +84,14 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { currency: CryptoCurrency.ltc, alwaysScan: alwaysScan, ) { - mwebHd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/1000'") as Bip32Slip10Secp256k1; - mwebEnabled = alwaysScan ?? false; + if (seedBytes != null) { + mwebHd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath( + "m/1000'") as Bip32Slip10Secp256k1; + mwebEnabled = alwaysScan ?? false; + } else { + mwebHd = null; + mwebEnabled = false; + } walletAddresses = LitecoinWalletAddresses( walletInfo, initialAddresses: initialAddresses, @@ -91,6 +103,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { network: network, mwebHd: mwebHd, mwebEnabled: mwebEnabled, + isHardwareWallet: walletInfo.isHardwareWallet, ); autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; @@ -126,7 +139,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } }); } - late final Bip32Slip10Secp256k1 mwebHd; + late final Bip32Slip10Secp256k1? mwebHd; late final Box mwebUtxosBox; Timer? _syncTimer; Timer? _feeRatesTimer; @@ -138,8 +151,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @observable SyncStatus mwebSyncStatus = NotConnectedSyncStatus(); - List get scanSecret => mwebHd.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; - List get spendSecret => mwebHd.childKey(Bip32KeyIndex(0x80000001)).privateKey.privKey.raw; + List get scanSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; + List get spendSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000001)).privateKey.privKey.raw; static Future create( {required String mnemonic, @@ -249,14 +262,15 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } return LitecoinWallet( - mnemonic: keysData.mnemonic!, + mnemonic: keysData.mnemonic, + xpub: keysData.xPub, password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, initialAddresses: snp?.addresses, initialMwebAddresses: snp?.mwebAddresses, initialBalance: snp?.balance, - seedBytes: seedBytes!, + seedBytes: seedBytes, passphrase: passphrase, encryptionFileUtils: encryptionFileUtils, initialRegularAddressIndex: snp?.regularAddressIndex, @@ -935,7 +949,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (!mwebEnabled) { tx.changeAddressOverride = - await (walletAddresses as LitecoinWalletAddresses).getChangeAddress(isPegIn: false); + (await (walletAddresses as LitecoinWalletAddresses) + .getChangeAddress(isPegIn: false)) + .address; return tx; } await waitForMwebAddresses(); @@ -974,8 +990,10 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { bool isPegIn = !hasMwebInput && hasMwebOutput; bool isRegular = !hasMwebInput && !hasMwebOutput; - tx.changeAddressOverride = await (walletAddresses as LitecoinWalletAddresses) - .getChangeAddress(isPegIn: isPegIn || isRegular); + tx.changeAddressOverride = + (await (walletAddresses as LitecoinWalletAddresses) + .getChangeAddress(isPegIn: isPegIn || isRegular)) + .address; if (!hasMwebInput && !hasMwebOutput) { tx.isMweb = false; return tx; @@ -1215,4 +1233,64 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return false; } + + LedgerConnection? _ledgerConnection; + LitecoinLedgerApp? _litecoinLedgerApp; + + @override + void setLedgerConnection(LedgerConnection connection) { + _ledgerConnection = connection; + _litecoinLedgerApp = + LitecoinLedgerApp(_ledgerConnection!, derivationPath: walletInfo.derivationInfo!.derivationPath!); + } + + @override + Future buildHardwareWalletTransaction({ + required List outputs, + required BigInt fee, + required BasedUtxoNetwork network, + required List utxos, + required Map publicKeys, + String? memo, + bool enableRBF = false, + BitcoinOrdering inputOrdering = BitcoinOrdering.bip69, + BitcoinOrdering outputOrdering = BitcoinOrdering.bip69, + }) async { + final readyInputs = []; + for (final utxo in utxos) { + final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); + final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; + + readyInputs.add(LedgerTransaction( + rawTx: rawTx, + outputIndex: utxo.utxo.vout, + ownerPublicKey: Uint8List.fromList(hex.decode(publicKeyAndDerivationPath.publicKey)), + ownerDerivationPath: publicKeyAndDerivationPath.derivationPath, + // sequence: enableRBF ? 0x1 : 0xffffffff, + sequence: 0xffffffff, + )); + } + + String? changePath; + for (final output in outputs) { + final maybeChangePath = publicKeys[(output as BitcoinOutput).address.pubKeyHash()]; + if (maybeChangePath != null) changePath ??= maybeChangePath.derivationPath; + } + + + final rawHex = await _litecoinLedgerApp!.createTransaction( + inputs: readyInputs, + outputs: outputs + .map((e) => TransactionOutput.fromBigInt( + (e as BitcoinOutput).value, Uint8List.fromList(e.address.toScriptPubKey().toBytes()))) + .toList(), + changePath: changePath, + sigHashType: 0x01, + additionals: ["bech32"], + isSegWit: true, + useTrustedInputForSegwit: true + ); + + return BtcTransaction.fromRaw(rawHex); + } } diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 6154a0ead..c55f5fc76 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -16,14 +16,17 @@ import 'package:mobx/mobx.dart'; part 'litecoin_wallet_addresses.g.dart'; -class LitecoinWalletAddresses = LitecoinWalletAddressesBase with _$LitecoinWalletAddresses; +class LitecoinWalletAddresses = LitecoinWalletAddressesBase + with _$LitecoinWalletAddresses; -abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Store { +abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses + with Store { LitecoinWalletAddressesBase( WalletInfo walletInfo, { required super.mainHd, required super.sideHd, required super.network, + required super.isHardwareWallet, required this.mwebHd, required this.mwebEnabled, super.initialAddresses, @@ -37,20 +40,20 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with print("initialized with ${mwebAddrs.length} mweb addresses"); } - final Bip32Slip10Secp256k1 mwebHd; + final Bip32Slip10Secp256k1? mwebHd; bool mwebEnabled; int mwebTopUpIndex = 1000; List mwebAddrs = []; bool generating = false; - List get scanSecret => mwebHd.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; - + List get scanSecret => + mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; List get spendPubkey => - mwebHd.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed; + mwebHd!.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed; @override Future init() async { - await initMwebAddresses(); + if (!isHardwareWallet) await initMwebAddresses(); await super.init(); } @@ -94,6 +97,9 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with print("Done generating MWEB addresses len: ${mwebAddrs.length}"); // ensure mweb addresses are up to date: + // This is the Case if the Litecoin Wallet is a hardware Wallet + if (mwebHd == null) return; + if (mwebAddresses.length < mwebAddrs.length) { List addressRecords = mwebAddrs .asMap() @@ -143,7 +149,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with @action @override - Future getChangeAddress( + Future getChangeAddress( {List? inputs, List? outputs, bool isPegIn = false}) async { // use regular change address on peg in, otherwise use mweb for change address: @@ -187,7 +193,12 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with if (mwebEnabled) { await ensureMwebAddressUpToIndexExists(1); - return mwebAddrs[0]; + return BitcoinAddressRecord( + mwebAddrs[0], + index: 0, + type: SegwitAddresType.mweb, + network: network, + ); } return super.getChangeAddress(); diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index c659dd658..7cc266f5b 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart'; import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart'; import 'package:cw_core/encryption_file_utils.dart'; @@ -20,7 +21,7 @@ class LitecoinWalletService extends WalletService< BitcoinNewWalletCredentials, BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromWIFCredentials, - BitcoinNewWalletCredentials> { + BitcoinRestoreWalletFromHardware> { LitecoinWalletService( this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect); @@ -147,9 +148,23 @@ class LitecoinWalletService extends WalletService< } @override - Future restoreFromHardwareWallet(BitcoinNewWalletCredentials credentials) { - throw UnimplementedError( - "Restoring a Litecoin wallet from a hardware wallet is not yet supported!"); + Future restoreFromHardwareWallet(BitcoinRestoreWalletFromHardware credentials, + {bool? isTestnet}) async { + final network = isTestnet == true ? LitecoinNetwork.testnet : LitecoinNetwork.mainnet; + credentials.walletInfo?.network = network.value; + credentials.walletInfo?.derivationInfo?.derivationPath = + credentials.hwAccountData.derivationPath; + + final wallet = await LitecoinWallet( + password: credentials.password!, + xpub: credentials.hwAccountData.xpub, + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + await wallet.save(); + await wallet.init(); + return wallet; } @override diff --git a/cw_bitcoin/lib/psbt_transaction_builder.dart b/cw_bitcoin/lib/psbt_transaction_builder.dart index d8d2c9fac..81efb792e 100644 --- a/cw_bitcoin/lib/psbt_transaction_builder.dart +++ b/cw_bitcoin/lib/psbt_transaction_builder.dart @@ -16,10 +16,6 @@ class PSBTTransactionBuild { for (var i = 0; i < inputs.length; i++) { final input = inputs[i]; - print(input.utxo.isP2tr()); - print(input.utxo.isSegwit()); - print(input.utxo.isP2shSegwit()); - psbt.setInputPreviousTxId(i, Uint8List.fromList(hex.decode(input.utxo.txHash).reversed.toList())); psbt.setInputOutputIndex(i, input.utxo.vout); psbt.setInputSequence(i, enableRBF ? 0x1 : 0xffffffff); diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 36d762ea1..5cba9b734 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -101,6 +101,14 @@ packages: url: "https://github.com/cake-tech/blockchain_utils" source: git version: "3.3.0" + bluez: + dependency: transitive + description: + name: bluez + sha256: "203a1924e818a9dd74af2b2c7a8f375ab8e5edf0e486bba8f90a0d8a17ed9fce" + url: "https://pub.dev" + source: hosted + version: "0.8.2" boolean_selector: dependency: transitive description: @@ -300,6 +308,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" + dbus: + dependency: transitive + description: + name: dbus + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + url: "https://pub.dev" + source: hosted + version: "0.7.10" encrypt: dependency: transitive description: @@ -361,19 +377,19 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.1+1" - flutter_reactive_ble: - dependency: transitive - description: - name: flutter_reactive_ble - sha256: "247e2efa76de203d1ba11335c13754b5b9d0504b5423e5b0c93a600f016b24e0" - url: "https://pub.dev" - source: hosted - version: "5.3.1" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_bluetooth: + dependency: transitive + description: + name: flutter_web_bluetooth + sha256: "52ce64f65d7321c4bf6abfe9dac02fb888731339a5e0ad6de59fb916c20c9f02" + url: "https://pub.dev" + source: hosted + version: "0.2.3" flutter_web_plugins: dependency: transitive description: flutter @@ -387,14 +403,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" - functional_data: - dependency: transitive - description: - name: functional_data - sha256: "76d17dc707c40e552014f5a49c0afcc3f1e3f05e800cd6b7872940bfe41a5039" - url: "https://pub.dev" - source: hosted - version: "1.2.0" glob: dependency: transitive description: @@ -550,29 +558,37 @@ packages: ledger_bitcoin: dependency: "direct main" description: - path: "." + path: "packages/ledger-bitcoin" ref: HEAD - resolved-ref: f819d37e235e239c315e93856abbf5e5d3b71dab - url: "https://github.com/cake-tech/ledger-bitcoin" + resolved-ref: dbb5c4956949dc734af3fc8febdbabed89da72aa + url: "https://github.com/cake-tech/ledger-flutter-plus-plugins" source: git - version: "0.0.2" - ledger_flutter: + version: "0.0.3" + ledger_flutter_plus: dependency: "direct main" description: - path: "." - ref: cake-v3 - resolved-ref: "66469ff9dffe2417c70ae7287c9d76d2fe7157a4" - url: "https://github.com/cake-tech/ledger-flutter.git" - source: git - version: "1.0.2" - ledger_usb: - dependency: transitive - description: - name: ledger_usb - sha256: "52c92d03a4cffe06c82921c8e2f79f3cdad6e1cf78e1e9ca35444196ff8f14c2" + name: ledger_flutter_plus + sha256: ea3ed586e1697776dacf42ac979095f1ca3bd143bf007cbe5c78e09cb6943f42 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.2.5" + ledger_litecoin: + dependency: "direct main" + description: + path: "packages/ledger-litecoin" + ref: HEAD + resolved-ref: dbb5c4956949dc734af3fc8febdbabed89da72aa + url: "https://github.com/cake-tech/ledger-flutter-plus-plugins" + source: git + version: "0.0.2" + ledger_usb_plus: + dependency: transitive + description: + name: ledger_usb_plus + sha256: "21cc5d976cf7edb3518bd2a0c4164139cbb0817d2e4f2054707fc4edfdf9ce87" + url: "https://pub.dev" + source: hosted + version: "1.0.4" logging: dependency: transitive description: @@ -701,6 +717,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" platform: dependency: transitive description: @@ -773,30 +797,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.2" - reactive_ble_mobile: - dependency: transitive - description: - name: reactive_ble_mobile - sha256: "9ec2b4c9c725e439950838d551579750060258fbccd5536d0543b4d07d225798" - url: "https://pub.dev" - source: hosted - version: "5.3.1" - reactive_ble_platform_interface: - dependency: transitive - description: - name: reactive_ble_platform_interface - sha256: "632c92401a2d69c9b94bd48f8fd47488a7013f3d1f9b291884350291a4a81813" - url: "https://pub.dev" - source: hosted - version: "5.3.1" rxdart: dependency: "direct main" description: name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" url: "https://pub.dev" source: hosted - version: "0.27.7" + version: "0.28.0" shared_preferences: dependency: "direct main" description: @@ -987,6 +995,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + universal_ble: + dependency: transitive + description: + name: universal_ble + sha256: "0dfbd6b64bff3ad61ed7a895c232530d9614e9b01ab261a74433a43267edb7f3" + url: "https://pub.dev" + source: hosted + version: "0.12.0" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" unorm_dart: dependency: transitive description: @@ -1043,6 +1067,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" yaml: dependency: transitive description: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 7e33d8260..9f1cee67d 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -24,16 +24,12 @@ dependencies: git: url: https://github.com/cake-tech/bitbox-flutter.git ref: Add-Support-For-OP-Return-data - rxdart: ^0.27.5 + rxdart: ^0.28.0 cryptography: ^2.0.5 blockchain_utils: git: url: https://github.com/cake-tech/blockchain_utils ref: cake-update-v2 - ledger_flutter: ^1.0.1 - ledger_bitcoin: - git: - url: https://github.com/cake-tech/ledger-bitcoin cw_mweb: path: ../cw_mweb grpc: ^3.2.4 @@ -44,6 +40,15 @@ dependencies: bech32: git: url: https://github.com/cake-tech/bech32.git + ledger_flutter_plus: ^1.4.1 + ledger_bitcoin: + git: + url: https://github.com/cake-tech/ledger-flutter-plus-plugins + path: packages/ledger-bitcoin + ledger_litecoin: + git: + url: https://github.com/cake-tech/ledger-flutter-plus-plugins + path: packages/ledger-litecoin dev_dependencies: flutter_test: @@ -54,10 +59,6 @@ dev_dependencies: hive_generator: ^1.1.3 dependency_overrides: - ledger_flutter: - git: - url: https://github.com/cake-tech/ledger-flutter.git - ref: cake-v3 watcher: ^1.1.0 protobuf: ^3.1.0 bitcoin_base: diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index 825c80d4a..d55914dcd 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -58,6 +58,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { sideHd: accountHD.childKey(Bip32KeyIndex(1)), network: network, initialAddressPageType: addressPageType, + isHardwareWallet: walletInfo.isHardwareWallet, ); autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart index 7342dc7f5..fe0ebc828 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -15,6 +15,7 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi required super.mainHd, required super.sideHd, required super.network, + required super.isHardwareWallet, super.initialAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, diff --git a/cw_core/lib/hardware/device_connection_type.dart b/cw_core/lib/hardware/device_connection_type.dart index 99fd5b1f0..9a3069552 100644 --- a/cw_core/lib/hardware/device_connection_type.dart +++ b/cw_core/lib/hardware/device_connection_type.dart @@ -8,6 +8,7 @@ enum DeviceConnectionType { [bool isIOS = false]) { switch (walletType) { case WalletType.bitcoin: + case WalletType.litecoin: case WalletType.ethereum: case WalletType.polygon: if (isIOS) return [DeviceConnectionType.ble]; diff --git a/cw_evm/lib/evm_chain_hardware_wallet_service.dart b/cw_evm/lib/evm_chain_hardware_wallet_service.dart index 6f0d11f2e..d8f67c641 100644 --- a/cw_evm/lib/evm_chain_hardware_wallet_service.dart +++ b/cw_evm/lib/evm_chain_hardware_wallet_service.dart @@ -2,26 +2,26 @@ import 'dart:async'; import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:ledger_ethereum/ledger_ethereum.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; class EVMChainHardwareWalletService { - EVMChainHardwareWalletService(this.ledger, this.device); + EVMChainHardwareWalletService(this.ledgerConnection); - final Ledger ledger; - final LedgerDevice device; + final LedgerConnection ledgerConnection; - Future> getAvailableAccounts({int index = 0, int limit = 5}) async { - final ethereumLedgerApp = EthereumLedgerApp(ledger); + Future> getAvailableAccounts( + {int index = 0, int limit = 5}) async { + final ethereumLedgerApp = EthereumLedgerApp(ledgerConnection); - final version = await ethereumLedgerApp.getVersion(device); + await ethereumLedgerApp.getVersion(); final accounts = []; final indexRange = List.generate(limit, (i) => i + index); for (final i in indexRange) { final derivationPath = "m/44'/60'/$i'/0/0"; - final address = - await ethereumLedgerApp.getAccounts(device, accountsDerivationPath: derivationPath); + final address = await ethereumLedgerApp.getAccounts( + accountsDerivationPath: derivationPath); accounts.add(HardwareAccountData( address: address.first, diff --git a/cw_evm/lib/evm_chain_transaction_credentials.dart b/cw_evm/lib/evm_chain_transaction_credentials.dart index 02927cb4d..5b5bdf170 100644 --- a/cw_evm/lib/evm_chain_transaction_credentials.dart +++ b/cw_evm/lib/evm_chain_transaction_credentials.dart @@ -1,7 +1,6 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/output_info.dart'; import 'package:cw_evm/evm_chain_transaction_priority.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; class EVMChainTransactionCredentials { EVMChainTransactionCredentials( diff --git a/cw_evm/lib/evm_ledger_credentials.dart b/cw_evm/lib/evm_ledger_credentials.dart index 0d8de1736..a0b7788dc 100644 --- a/cw_evm/lib/evm_ledger_credentials.dart +++ b/cw_evm/lib/evm_ledger_credentials.dart @@ -1,17 +1,16 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:cw_core/hardware/device_not_connected_exception.dart'; +import 'package:cw_core/hardware/device_not_connected_exception.dart' + as exception; import 'package:ledger_ethereum/ledger_ethereum.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:web3dart/crypto.dart'; import 'package:web3dart/web3dart.dart'; class EvmLedgerCredentials extends CredentialsWithKnownAddress { final String _address; - Ledger? ledger; - LedgerDevice? ledgerDevice; EthereumLedgerApp? ethereumLedgerApp; EvmLedgerCredentials(this._address); @@ -19,25 +18,25 @@ class EvmLedgerCredentials extends CredentialsWithKnownAddress { @override EthereumAddress get address => EthereumAddress.fromHex(_address); - void setLedger(Ledger setLedger, [LedgerDevice? setLedgerDevice, String? derivationPath]) { - ledger = setLedger; - ledgerDevice = setLedgerDevice; - ethereumLedgerApp = - EthereumLedgerApp(ledger!, derivationPath: derivationPath ?? "m/44'/60'/0'/0/0"); + void setLedgerConnection(LedgerConnection connection, + [String? derivationPath]) { + ethereumLedgerApp = EthereumLedgerApp(connection, + derivationPath: derivationPath ?? "m/44'/60'/0'/0/0"); } @override - MsgSignature signToEcSignature(Uint8List payload, {int? chainId, bool isEIP1559 = false}) => - throw UnimplementedError("EvmLedgerCredentials.signToEcSignature"); + MsgSignature signToEcSignature(Uint8List payload, + {int? chainId, bool isEIP1559 = false}) => + throw UnimplementedError("EvmLedgerCredentials.signToEcSignature"); @override Future signToSignature(Uint8List payload, {int? chainId, bool isEIP1559 = false}) async { - if (ledgerDevice == null && ledger?.devices.isNotEmpty != true) { - throw DeviceNotConnectedException(); + if (ethereumLedgerApp == null) { + throw exception.DeviceNotConnectedException(); } - final sig = await ethereumLedgerApp!.signTransaction(device, payload); + final sig = await ethereumLedgerApp!.signTransaction(payload); final v = sig[0].toInt(); final r = bytesToHex(sig.sublist(1, 1 + 32)); @@ -65,14 +64,16 @@ class EvmLedgerCredentials extends CredentialsWithKnownAddress { chainIdV = chainId != null ? (parity + (chainId * 2 + 35)) : parity; } - return MsgSignature(BigInt.parse(r, radix: 16), BigInt.parse(s, radix: 16), chainIdV); + return MsgSignature( + BigInt.parse(r, radix: 16), BigInt.parse(s, radix: 16), chainIdV); } @override - Future signPersonalMessage(Uint8List payload, {int? chainId}) async { - if (isNotConnected) throw DeviceNotConnectedException(); + Future signPersonalMessage(Uint8List payload, + {int? chainId}) async { + if (isNotConnected) throw exception.DeviceNotConnectedException(); - final sig = await ethereumLedgerApp!.signMessage(device, payload); + final sig = await ethereumLedgerApp!.signMessage(payload); final r = sig.sublist(1, 1 + 32); final s = sig.sublist(1 + 32, 1 + 32 + 32); @@ -84,20 +85,22 @@ class EvmLedgerCredentials extends CredentialsWithKnownAddress { @override Uint8List signPersonalMessageToUint8List(Uint8List payload, {int? chainId}) => - throw UnimplementedError("EvmLedgerCredentials.signPersonalMessageToUint8List"); + throw UnimplementedError( + "EvmLedgerCredentials.signPersonalMessageToUint8List"); - Future provideERC20Info(String erc20ContractAddress, int chainId) async { - if (isNotConnected) throw DeviceNotConnectedException(); + Future provideERC20Info( + String erc20ContractAddress, int chainId) async { + if (isNotConnected) throw exception.DeviceNotConnectedException(); try { - await ethereumLedgerApp!.getAndProvideERC20TokenInformation(device, + await ethereumLedgerApp!.getAndProvideERC20TokenInformation( erc20ContractAddress: erc20ContractAddress, chainId: chainId); - } on LedgerException catch (e) { - if (e.errorCode != -28672) rethrow; + } catch (e) { + print(e); + rethrow; + // if (e.errorCode != -28672) rethrow; } } - bool get isNotConnected => (ledgerDevice ?? ledger?.devices.firstOrNull) == null; - - LedgerDevice get device => ledgerDevice ?? ledger!.devices.first; + bool get isNotConnected => ethereumLedgerApp == null || ethereumLedgerApp!.connection.isDisconnected; } diff --git a/cw_evm/pubspec.yaml b/cw_evm/pubspec.yaml index 3e12834b1..326ff4dc9 100644 --- a/cw_evm/pubspec.yaml +++ b/cw_evm/pubspec.yaml @@ -25,20 +25,17 @@ dependencies: mobx: ^2.0.7+4 cw_core: path: ../cw_core - ledger_flutter: ^1.0.1 + ledger_flutter_plus: ^1.4.1 ledger_ethereum: git: - url: https://github.com/cake-tech/ledger-ethereum.git + url: https://github.com/cake-tech/ledger-flutter-plus-plugins + path: packages/ledger-ethereum dependency_overrides: web3dart: git: url: https://github.com/cake-tech/web3dart.git ref: cake - ledger_flutter: - git: - url: https://github.com/cake-tech/ledger-flutter.git - ref: cake-v3 watcher: ^1.1.0 dev_dependencies: diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 365d86be5..60364c289 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -487,18 +487,30 @@ class CWBitcoin extends Bitcoin { } @override - void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device) { - (wallet as BitcoinWallet).setLedger(ledger, device); + void setLedgerConnection(WalletBase wallet, ledger.LedgerConnection connection) { + (wallet as ElectrumWallet).setLedgerConnection(connection); } @override - Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, + Future> getHardwareWalletBitcoinAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}) async { - final hardwareWalletService = BitcoinHardwareWalletService(ledgerVM.ledger, ledgerVM.device); + final hardwareWalletService = BitcoinHardwareWalletService(ledgerVM.connection); try { return hardwareWalletService.getAvailableAccounts(index: index, limit: limit); - } on LedgerException catch (err) { - print(err.message); + } catch (err) { + print(err); + throw err; + } + } + + @override + Future> getHardwareWalletLitecoinAccounts(LedgerViewModel ledgerVM, + {int index = 0, int limit = 5}) async { + final hardwareWalletService = LitecoinHardwareWalletService(ledgerVM.connection); + try { + return hardwareWalletService.getAvailableAccounts(index: index, limit: limit); + } catch (err) { + print(err); throw err; } } diff --git a/lib/entities/hardware_wallet/hardware_wallet_device.dart b/lib/entities/hardware_wallet/hardware_wallet_device.dart new file mode 100644 index 000000000..d3acc5d32 --- /dev/null +++ b/lib/entities/hardware_wallet/hardware_wallet_device.dart @@ -0,0 +1,65 @@ +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; + +class HardwareWalletDevice { + final String name; + final HardwareWalletDeviceType type; + final HardwareWalletConnectionType connectionType; + + const HardwareWalletDevice({ + required this.name, + required this.type, + required this.connectionType, + }); + + factory HardwareWalletDevice.fromLedgerDevice(ledger.LedgerDevice device) => + HardwareWalletDevice( + name: device.name, + type: device.deviceInfo.toGeneric(), + connectionType: device.connectionType.toGeneric(), + ); +} + +enum HardwareWalletDeviceType { + ledgerBlue, + ledgerNanoS, + ledgerNanoX, + ledgerNanoSPlus, + ledgerStax, + ledgerFlex; +} + +enum HardwareWalletConnectionType { + usb, + ble, + nfc; +} + +extension ToGenericHardwareWalletDeviceType on ledger.LedgerDeviceType { + HardwareWalletDeviceType toGeneric() { + switch (this) { + case ledger.LedgerDeviceType.blue: + return HardwareWalletDeviceType.ledgerBlue; + case ledger.LedgerDeviceType.nanoS: + return HardwareWalletDeviceType.ledgerNanoS; + case ledger.LedgerDeviceType.nanoSP: + return HardwareWalletDeviceType.ledgerNanoSPlus; + case ledger.LedgerDeviceType.nanoX: + return HardwareWalletDeviceType.ledgerNanoX; + case ledger.LedgerDeviceType.stax: + return HardwareWalletDeviceType.ledgerStax; + case ledger.LedgerDeviceType.flex: + return HardwareWalletDeviceType.ledgerFlex; + } + } +} + +extension ToGenericHardwareWalletConnectionType on ledger.ConnectionType { + HardwareWalletConnectionType toGeneric() { + switch (this) { + case ledger.ConnectionType.usb: + return HardwareWalletConnectionType.usb; + case ledger.ConnectionType.ble: + return HardwareWalletConnectionType.ble; + } + } +} diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart index e2c3d1358..7a06a1679 100644 --- a/lib/ethereum/cw_ethereum.dart +++ b/lib/ethereum/cw_ethereum.dart @@ -191,21 +191,21 @@ class CWEthereum extends Ethereum { String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress; @override - void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device) { - ((wallet as EVMChainWallet).evmChainPrivateKey as EvmLedgerCredentials).setLedger( - ledger, - device.connectionType == ConnectionType.usb ? device : null, - wallet.walletInfo.derivationInfo?.derivationPath); + void setLedgerConnection( + WalletBase wallet, ledger.LedgerConnection connection) { + ((wallet as EVMChainWallet).evmChainPrivateKey as EvmLedgerCredentials) + .setLedgerConnection( + connection, wallet.walletInfo.derivationInfo?.derivationPath); } @override Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}) async { - final hardwareWalletService = EVMChainHardwareWalletService(ledgerVM.ledger, ledgerVM.device); + final hardwareWalletService = EVMChainHardwareWalletService(ledgerVM.connection); try { return await hardwareWalletService.getAvailableAccounts(index: index, limit: limit); - } on LedgerException catch (err) { - print(err.message); + } catch (err) { + print(err); throw err; } } diff --git a/lib/main.dart b/lib/main.dart index 2b5bef06c..29b216b22 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/app_scroll_behavior.dart'; import 'package:cake_wallet/buy/order.dart'; @@ -43,6 +44,7 @@ import 'package:hive/hive.dart'; import 'package:cw_core/root_dir.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cw_core/window_size.dart'; +import 'package:logging/logging.dart'; final navigatorKey = GlobalKey(); final rootKey = GlobalKey(); @@ -68,8 +70,18 @@ Future runAppWithZone({Key? topLevelKey}) async { }; await initializeAppAtRoot(); - runApp(App(key: topLevelKey)); + if (kDebugMode) { + final appDocDir = await getAppDir(); + final ledgerFile = File('${appDocDir.path}/ledger_log.txt'); + if (!ledgerFile.existsSync()) ledgerFile.createSync(); + Logger.root.onRecord.listen((event) async { + final content = ledgerFile.readAsStringSync(); + ledgerFile.writeAsStringSync("$content\n${event.message}"); + }); + } + + runApp(App(key: topLevelKey)); isAppRunning = true; }, (error, stackTrace) async { if (!isAppRunning) { diff --git a/lib/polygon/cw_polygon.dart b/lib/polygon/cw_polygon.dart index 299c8278a..74b4026eb 100644 --- a/lib/polygon/cw_polygon.dart +++ b/lib/polygon/cw_polygon.dart @@ -190,20 +190,21 @@ class CWPolygon extends Polygon { String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress; @override - void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device) { - ((wallet as EVMChainWallet).evmChainPrivateKey as EvmLedgerCredentials).setLedger( - ledger, - device.connectionType == ConnectionType.usb ? device : null, - wallet.walletInfo.derivationInfo?.derivationPath); + void setLedgerConnection( + WalletBase wallet, ledger.LedgerConnection connection) { + ((wallet as EVMChainWallet).evmChainPrivateKey as EvmLedgerCredentials) + .setLedgerConnection( + connection, wallet.walletInfo.derivationInfo?.derivationPath); } @override Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}) async { - final hardwareWalletService = EVMChainHardwareWalletService(ledgerVM.ledger, ledgerVM.device); + final hardwareWalletService = EVMChainHardwareWalletService(ledgerVM.connection); try { return await hardwareWalletService.getAvailableAccounts(index: index, limit: limit); - } on LedgerException catch (err) { + } catch (err) { + print(err); throw err; } } diff --git a/lib/src/screens/connect_device/connect_device_page.dart b/lib/src/screens/connect_device/connect_device_page.dart index a482b1c41..9e331e818 100644 --- a/lib/src/screens/connect_device/connect_device_page.dart +++ b/lib/src/screens/connect_device/connect_device_page.dart @@ -3,15 +3,14 @@ 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/connect_device/debug_device_page.dart'; import 'package:cake_wallet/src/screens/connect_device/widgets/device_tile.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; -import 'package:permission_handler/permission_handler.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; typedef OnConnectDevice = void Function(BuildContext, LedgerViewModel); @@ -19,7 +18,8 @@ class ConnectDevicePageParams { final WalletType walletType; final OnConnectDevice onConnectDevice; - ConnectDevicePageParams({required this.walletType, required this.onConnectDevice}); + ConnectDevicePageParams( + {required this.walletType, required this.onConnectDevice}); } class ConnectDevicePage extends BasePage { @@ -35,7 +35,8 @@ class ConnectDevicePage extends BasePage { String get title => S.current.restore_title_from_hardware_wallet; @override - Widget body(BuildContext context) => ConnectDevicePageBody(walletType, onConnectDevice, ledgerVM); + Widget body(BuildContext context) => + ConnectDevicePageBody(walletType, onConnectDevice, ledgerVM); } class ConnectDevicePageBody extends StatefulWidget { @@ -43,47 +44,35 @@ class ConnectDevicePageBody extends StatefulWidget { final OnConnectDevice onConnectDevice; final LedgerViewModel ledgerVM; - const ConnectDevicePageBody(this.walletType, this.onConnectDevice, this.ledgerVM); + const ConnectDevicePageBody( + this.walletType, this.onConnectDevice, this.ledgerVM); @override ConnectDevicePageBodyState createState() => ConnectDevicePageBodyState(); } class ConnectDevicePageBodyState extends State { - final imageLedger = 'assets/images/ledger_nano.png'; - - final ledger = Ledger( - options: LedgerOptions( - scanMode: ScanMode.balanced, - maxScanDuration: const Duration(minutes: 5), - ), - onPermissionRequest: (_) async { - Map statuses = await [ - Permission.bluetoothScan, - Permission.bluetoothConnect, - Permission.bluetoothAdvertise, - ].request(); - - return statuses.values.where((status) => status.isDenied).isEmpty; - }, - ); - - var bleIsEnabled = true; var bleDevices = []; var usbDevices = []; late Timer? _usbRefreshTimer = null; late Timer? _bleRefreshTimer = null; + late Timer? _bleStateTimer = null; late StreamSubscription? _bleRefresh = null; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - _bleRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshBleDevices()); + _bleStateTimer = Timer.periodic( + Duration(seconds: 1), (_) => widget.ledgerVM.updateBleState()); + + _bleRefreshTimer = + Timer.periodic(Duration(seconds: 1), (_) => _refreshBleDevices()); if (Platform.isAndroid) { - _usbRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshUsbDevices()); + _usbRefreshTimer = + Timer.periodic(Duration(seconds: 1), (_) => _refreshUsbDevices()); } }); } @@ -91,35 +80,59 @@ class ConnectDevicePageBodyState extends State { @override void dispose() { _bleRefreshTimer?.cancel(); + _bleStateTimer?.cancel(); _usbRefreshTimer?.cancel(); _bleRefresh?.cancel(); super.dispose(); } Future _refreshUsbDevices() async { - final dev = await ledger.listUsbDevices(); + final dev = await widget.ledgerVM.ledgerPlusUSB.devices; if (usbDevices.length != dev.length) setState(() => usbDevices = dev); + // _usbRefresh = widget.ledgerVM + // .scanForUsbDevices() + // .listen((device) => setState(() => usbDevices.add(device))) + // ..onError((e) { + // throw e.toString(); + // }); + // Keep polling until the lfp lib gets updated + // _usbRefreshTimer?.cancel(); + // _usbRefreshTimer = null; } Future _refreshBleDevices() async { try { - _bleRefresh = ledger.scan().listen((device) => setState(() => bleDevices.add(device))) + _bleRefresh = widget.ledgerVM + .scanForBleDevices() + .listen((device) => setState(() => bleDevices.add(device))) ..onError((e) { throw e.toString(); }); - setState(() => bleIsEnabled = true); _bleRefreshTimer?.cancel(); _bleRefreshTimer = null; } catch (e) { - setState(() => bleIsEnabled = false); + print(e); } } Future _connectToDevice(LedgerDevice device) async { - await widget.ledgerVM.connectLedger(device); + await widget.ledgerVM.connectLedger(device, widget.walletType); widget.onConnectDevice(context, widget.ledgerVM); } + String _getDeviceTileLeading(LedgerDeviceType deviceInfo) { + switch (deviceInfo) { + case LedgerDeviceType.nanoX: + return 'assets/images/hardware_wallet/ledger_nano_x.png'; + case LedgerDeviceType.stax: + return 'assets/images/hardware_wallet/ledger_stax.png'; + case LedgerDeviceType.flex: + return 'assets/images/hardware_wallet/ledger_flex.png'; + default: + return 'assets/images/hardware_wallet/ledger_nano_x.png'; + } + } + @override Widget build(BuildContext context) { return Center( @@ -139,7 +152,9 @@ class ConnectDevicePageBodyState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.titleColor), + color: Theme.of(context) + .extension()! + .titleColor), textAlign: TextAlign.center, ), ), @@ -152,18 +167,25 @@ class ConnectDevicePageBodyState extends State { // title: "Debug Ledger", // leading: imageLedger, // ), - if (!bleIsEnabled) - Padding( - padding: EdgeInsets.only(left: 20, right: 20, bottom: 20), - child: Text( - S.of(context).ledger_please_enable_bluetooth, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.titleColor), - textAlign: TextAlign.center, + Observer( + builder: (_) => Offstage( + offstage: widget.ledgerVM.bleIsEnabled, + child: Padding( + padding: EdgeInsets.only(left: 20, right: 20, bottom: 20), + child: Text( + S.of(context).ledger_please_enable_bluetooth, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .extension()! + .titleColor), + textAlign: TextAlign.center, + ), ), ), + ), + if (bleDevices.length > 0) ...[ Padding( padding: EdgeInsets.only(left: 20, right: 20, bottom: 20), @@ -174,7 +196,9 @@ class ConnectDevicePageBodyState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.titleColor, + color: Theme.of(context) + .extension()! + .titleColor, ), ), ), @@ -186,7 +210,7 @@ class ConnectDevicePageBodyState extends State { child: DeviceTile( onPressed: () => _connectToDevice(device), title: device.name, - leading: imageLedger, + leading: _getDeviceTileLeading(device.deviceInfo), connectionType: device.connectionType, ), ), @@ -203,7 +227,9 @@ class ConnectDevicePageBodyState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.titleColor, + color: Theme.of(context) + .extension()! + .titleColor, ), ), ), @@ -215,7 +241,7 @@ class ConnectDevicePageBodyState extends State { child: DeviceTile( onPressed: () => _connectToDevice(device), title: device.name, - leading: imageLedger, + leading: _getDeviceTileLeading(device.deviceInfo), connectionType: device.connectionType, ), ), diff --git a/lib/src/screens/connect_device/debug_device_page.dart b/lib/src/screens/connect_device/debug_device_page.dart index f5a9ef2a4..bed9d59a7 100644 --- a/lib/src/screens/connect_device/debug_device_page.dart +++ b/lib/src/screens/connect_device/debug_device_page.dart @@ -1,15 +1,15 @@ -// import 'dart:convert'; +// import 'dart:typed_data'; // +// import 'package:basic_utils/basic_utils.dart'; +// import 'package:bitcoin_base/bitcoin_base.dart'; // import 'package:cake_wallet/src/screens/base_page.dart'; // import 'package:cake_wallet/src/screens/connect_device/widgets/device_tile.dart'; // import 'package:cake_wallet/src/widgets/primary_button.dart'; // import 'package:cake_wallet/utils/responsive_layout_util.dart'; -// import 'package:convert/convert.dart'; // import 'package:flutter/material.dart'; -// import 'package:ledger_bitcoin/ledger_bitcoin.dart'; -// import 'package:ledger_flutter/ledger_flutter.dart'; +// import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; +// import 'package:ledger_litecoin/ledger_litecoin.dart'; // import 'package:permission_handler/permission_handler.dart'; -// import 'package:polyseed/polyseed.dart'; // // class DebugDevicePage extends BasePage { // @override @@ -50,7 +50,9 @@ // }, // ); // -// late BitcoinLedgerApp btc; +// // late BitcoinLedgerApp btc; +// late LitecoinLedgerApp ltc; +// // var devices = []; // var status = ""; // var counter = 0; @@ -59,7 +61,8 @@ // @override // void initState() { // super.initState(); -// btc = BitcoinLedgerApp(ledger); +// // btc = BitcoinLedgerApp(ledger); +// ltc = LitecoinLedgerApp(ledger); // } // // @override @@ -81,7 +84,7 @@ // // @override // Widget build(BuildContext context) { -// final imageLedger = 'assets/images/ledger_nano.png'; +// final imageLedger = 'assets/images/hardware_wallet/ledger_nano_x.png'; // // return Center( // child: Container( @@ -99,40 +102,25 @@ // DebugButton( // title: "Get Version", // method: "Version", -// func: () async => await btc.getVersion(selectedDevice!), -// ), -// DebugButton( -// title: "Get Master Fingerprint", -// method: "Master Fingerprint", -// func: () async => hex.encode(await btc.getMasterFingerprint(selectedDevice!)), -// ), -// DebugButton( -// title: "Get XPub", -// method: "XPub", -// func: () async => await btc.getXPubKey(selectedDevice!, derivationPath: "m/84'/0'/$counter'"), +// // func: () async => await btc.getVersion(selectedDevice!), +// func: () async => await ltc.getVersion(selectedDevice!), // ), // DebugButton( // title: "Get Wallet Address", // method: "Wallet Address", // func: () async { // setState(() => counter++); -// final derivationPath = "m/84'/0'/$counter'/0/0"; -// return await btc.getAccounts(selectedDevice!, accountsDerivationPath: derivationPath); +// final derivationPath = "m/84'/2'/0'/0/0"; +// return await ltc.getAccounts(selectedDevice!, +// accountsDerivationPath: derivationPath); +// // return await btc.getAccounts(selectedDevice!, accountsDerivationPath: derivationPath); // // return await ethereum!.getHardwareWalletAccounts(selectedDevice!); -// }, +// }, // ), // DebugButton( // title: "Send Money", -// method: "Sig", -// func: () async { -// final psbt = PsbtV2(); -// final psbtBuf = base64.decode( -// "cHNidP8BAgQCAAAAAQQBAQEFAQIAAQ4gTW6k/cwKKu1u7m9oKr5ob7VcAC0IPkfaDitRi/FkD7sBDwQAAAAAARAE/////wEA/ekBAQAAAAABA9AYVQLI722H0osKMa/4dvMucrnKV1Myxtlp0l0BoOBDAQAAAAD/////ku6r2ABaHt9N26f/P4eMljX8t1f4lBcFfEwuNm/uXYoBAAAAAP////+YeAl8arEGKOcyrWJAYwSboyCstkhHN8zn7/vy7pkYTAEAAAAA/////wHlHgAAAAAAABYAFKdq0umSucBGVkl2MpT6Hgo/0a/xAkcwRAIgMkiJmNFbEi2I3CQYOwyV/JepCnFQRvj4xghkySpFcJMCIGAypkkWltfj+ucvqUIu27tusDAIAAB+rBhX/GV7hPlEASEDyLmWyTLjLfC9kn8pnW42jW5N6EJo5fObjWWEyfLDu9UCSDBFAiEAg9crVtwBPF+sWk+Th6pLwzDjJGItwsUCvoBPtmMTEb4CIDGuM7WOguV0TP21oidF3bSUZlEAjUHWfWzxLKw+3LofASEDfN16xKb70UZSeQyX5Tlh8iRq7np5Nlz9GYdcSU50sKwCSDBFAiEAvotOblaEiBptRWkvb6bj2MGyRjTphKLBLiHYmrRMTCgCIEKJH+z65uPSSz1NIb0d/u3bU9l0xcWk0idEsXjB+BIiASEDrAEiEtrSNKxbh6F/KPaCTafF2LVjCzb75WB+x4xSuoQAAAAAAQEf5R4AAAAAAAAWABSnatLpkrnARlZJdjKU+h4KP9Gv8SIGA3xMuxmPsBAm9aMEUBs3N46DB+Kdts3bZR/Wxt+uM0H4GKtN6bpUAACAAAAAgAAAAIAAAAAAAAAAAAABBBTk7bEOxYcdXDi1eeWraYDufm6eJgEDCOgDAAAAAAAAAAEEFDX3g/pnDXIfsRw8shK42NZn+SdpAQMIiBMAAAAAAAAiAgN8TLsZj7AQJvWjBFAbNzeOgwfinbbN22Uf1sbfrjNB+BirTem6VAAAgAAAAIAAAACAAAAAAAAAAAAA" -// ); -// psbt.deserialize(psbtBuf); -// final result = await btc.signPsbt(selectedDevice!, psbt: psbt); -// return result.toHexString(); -// }, +// method: "Raw Tx", +// func: sendMoney // ), // Padding( // padding: EdgeInsets.only(top: 20), @@ -147,18 +135,18 @@ // ...devices // .map( // (device) => Padding( -// padding: EdgeInsets.only(bottom: 20), -// child: DeviceTile( -// onPressed: () { -// setState(() => selectedDevice = device); -// ledger.connect(device); -// }, -// title: device.name, -// leading: imageLedger, -// connectionType: device.connectionType, -// ), -// ), -// ) +// padding: EdgeInsets.only(bottom: 20), +// child: DeviceTile( +// onPressed: () { +// setState(() => selectedDevice = device); +// ledger.connect(device); +// }, +// title: device.name, +// leading: imageLedger, +// connectionType: device.connectionType, +// ), +// ), +// ) // .toList(), // PrimaryButton( // text: "Refresh BLE", @@ -188,6 +176,42 @@ // ); // } // +// Future sendMoney() async { +// final readyInputs = [ +// LedgerTransaction( +// rawTx: "010000000001018c055c85c3724c98842d27712771dd0de139711f5940bba2df4615c5522184740000000017160014faf7f6dfb4e70798b92c93f33b4c51024491829df0ffffff022b05c70000000000160014f489f947fd13a1fb44ac168427081d3f30b6ce0cde9dd82e0000000017a914d5eca376cb49d65031220ff9093b7d407073ed0d8702483045022100f648c9f6a9b8f35b6ec29bbfae312c95ed3d56ce6a3f177d994efe90562ec4bd02205b82ce2c94bc0c9d152c3afc668b200bd82f48d6a14e83c66ba0f154cd5f69190121038f1dca119420d4aa7ad04af1c0d65304723789cccc56d335b18692390437f35900000000", +// outputIndex: 0, +// ownerPublicKey: +// HexUtils.decode("03b2e67958ed3356e329e05cf94c3bee6b20c17175ac3b2a1278e073bf44f5d6ec"), +// ownerDerivationPath: "m/84'/2'/0'/0/0", +// sequence: 0xffffffff, +// ) +// ]; +// +// final outputs = [ +// BitcoinOutput( +// address: P2wpkhAddress.fromAddress( +// address: "ltc1qn0g5e36xaj07lqj6w9xn52ng07hud42g3jf5ps", +// network: LitecoinNetwork.mainnet), +// value: BigInt.from(1000000)), +// BitcoinOutput( +// address: P2wpkhAddress.fromAddress( +// address: "ltc1qrx29qz4ghu4j0xk37ptgk7034cwpmjyxhrcnk9", +// network: LitecoinNetwork.mainnet), +// value: BigInt.from(12042705)), +// ]; +// return await ltc.createTransaction(selectedDevice!, +// inputs: readyInputs, +// outputs: outputs +// .map((e) => TransactionOutput.fromBigInt( +// e.value, Uint8List.fromList(e.address.toScriptPubKey().toBytes()))) +// .toList(), +// sigHashType: 0x01, +// additionals: ["bech32"], +// isSegWit: true, +// useTrustedInputForSegwit: true); +// } +// // Widget DebugButton( // {required String title, required String method, required Future Function() func}) { // return Padding( diff --git a/lib/src/screens/connect_device/widgets/device_tile.dart b/lib/src/screens/connect_device/widgets/device_tile.dart index 8367d1606..58f65c5de 100644 --- a/lib/src/screens/connect_device/widgets/device_tile.dart +++ b/lib/src/screens/connect_device/widgets/device_tile.dart @@ -1,6 +1,6 @@ import 'package:cake_wallet/themes/extensions/option_tile_theme.dart'; import 'package:flutter/material.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; class DeviceTile extends StatelessWidget { const DeviceTile({ diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index 478f1a3c3..028b3cfcf 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -188,7 +188,7 @@ class CryptoBalanceWidget extends StatelessWidget { Padding( padding: const EdgeInsets.all(8.0), child: Image.asset( - 'assets/images/ledger_nano.png', + 'assets/images/hardware_wallet/ledger_nano_x.png', width: 24, color: Theme.of(context) .extension()! diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index d671230c4..57f5ec727 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -67,7 +67,7 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { final mainImageColor = Theme.of(context).extension()!.pageTitleTextColor; final brightImageColor = Theme.of(context).extension()!.textColor; final imageColor = widget.themeType == ThemeType.bright ? brightImageColor : mainImageColor; - final imageLedger = Image.asset('assets/images/ledger_nano.png', width: 40, color: imageColor); + final imageLedger = Image.asset('assets/images/hardware_wallet/ledger_nano_x.png', width: 40, color: imageColor); final imageSeedKeys = Image.asset('assets/images/restore_wallet_image.png', color: imageColor); final imageBackup = Image.asset('assets/images/backup.png', color: imageColor); @@ -186,4 +186,4 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { } } } -} \ No newline at end of file +} diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index d881d0341..bce82312d 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -395,16 +395,19 @@ class SendPage extends BasePage { if (sendViewModel.wallet.isHardwareWallet) { if (!sendViewModel.ledgerViewModel!.isConnected) { - await Navigator.of(context).pushNamed(Routes.connectDevices, + await Navigator.of(context).pushNamed( + Routes.connectDevices, arguments: ConnectDevicePageParams( walletType: sendViewModel.walletType, onConnectDevice: (BuildContext context, _) { - sendViewModel.ledgerViewModel!.setLedger(sendViewModel.wallet); + sendViewModel.ledgerViewModel! + .setLedger(sendViewModel.wallet); Navigator.of(context).pop(); }, )); } else { - sendViewModel.ledgerViewModel!.setLedger(sendViewModel.wallet); + sendViewModel.ledgerViewModel! + .setLedger(sendViewModel.wallet); } } @@ -509,7 +512,7 @@ class SendPage extends BasePage { if (state is TransactionCommitted) { newContactAddress = newContactAddress ?? sendViewModel.newContactAddress(); - + if (sendViewModel.coinTypeToSpendFrom != UnspentCoinType.any) { newContactAddress = null; } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 26bc5ceb9..53a8277ab 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -435,7 +435,7 @@ abstract class DashboardViewModelBase with Store { } @computed - bool get hasMweb => wallet.type == WalletType.litecoin && (Platform.isIOS || Platform.isAndroid); + bool get hasMweb => wallet.type == WalletType.litecoin && (Platform.isIOS || Platform.isAndroid) && !wallet.isHardwareWallet; @computed bool get showMwebCard => hasMweb && settingsStore.mwebCardDisplay && !mwebEnabled; diff --git a/lib/view_model/hardware_wallet/ledger_view_model.dart b/lib/view_model/hardware_wallet/ledger_view_model.dart index f05b1c805..19b190fe3 100644 --- a/lib/view_model/hardware_wallet/ledger_view_model.dart +++ b/lib/view_model/hardware_wallet/ledger_view_model.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; @@ -9,11 +10,19 @@ import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cw_core/hardware/device_connection_type.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; + +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as sdk; +import 'package:mobx/mobx.dart'; import 'package:permission_handler/permission_handler.dart'; -class LedgerViewModel { - late final Ledger ledger; +part 'ledger_view_model.g.dart'; + +class LedgerViewModel = LedgerViewModelBase with _$LedgerViewModel; + +abstract class LedgerViewModelBase with Store { + // late final Ledger ledger; + late final sdk.LedgerInterface ledgerPlusBLE; + late final sdk.LedgerInterface ledgerPlusUSB; bool get _doesSupportHardwareWallets { if (!DeviceInfo.instance.isMobile) { @@ -21,53 +30,97 @@ class LedgerViewModel { } if (isMoneroOnly) { - return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS) + return DeviceConnectionType.supportedConnectionTypes( + WalletType.monero, Platform.isIOS) .isNotEmpty; } return true; } - LedgerViewModel() { + LedgerViewModelBase() { if (_doesSupportHardwareWallets) { - ledger = Ledger( - options: LedgerOptions( - scanMode: ScanMode.balanced, - maxScanDuration: const Duration(minutes: 5), - ), - onPermissionRequest: (_) async { - Map statuses = await [ - Permission.bluetoothScan, - Permission.bluetoothConnect, - Permission.bluetoothAdvertise, - ].request(); + reaction((_) => bleIsEnabled, (_) { + if (bleIsEnabled) _initBLE(); + }); + updateBleState(); - return statuses.values.where((status) => status.isDenied).isEmpty; - }, - ); + if (!Platform.isIOS) { + ledgerPlusUSB = sdk.LedgerInterface.usb(); + } } } - Future connectLedger(LedgerDevice device) async { - await ledger.connect(device); + @observable + bool bleIsEnabled = false; - if (device.connectionType == ConnectionType.usb) _device = device; + bool _bleIsInitialized = false; + Future _initBLE() async { + if (bleIsEnabled && !_bleIsInitialized) { + ledgerPlusBLE = sdk.LedgerInterface.ble(onPermissionRequest: (_) async { + Map statuses = await [ + Permission.bluetoothScan, + Permission.bluetoothConnect, + Permission.bluetoothAdvertise, + ].request(); + + return statuses.values.where((status) => status.isDenied).isEmpty; + }); + _bleIsInitialized = true; + } } - LedgerDevice? _device; + Future updateBleState() async { + final bleState = await sdk.UniversalBle.getBluetoothAvailabilityState(); - bool get isConnected => ledger.devices.isNotEmpty || _device != null; + final newState = bleState == sdk.AvailabilityState.poweredOn; - LedgerDevice get device => _device ?? ledger.devices.first; + if (newState != bleIsEnabled) bleIsEnabled = newState; + } + + Stream scanForBleDevices() => ledgerPlusBLE.scan(); + + Stream scanForUsbDevices() => ledgerPlusUSB.scan(); + + Future connectLedger(sdk.LedgerDevice device, WalletType type) async { + if (isConnected) { + try { + await _connection!.disconnect(); + } catch (_) {} + } + final ledger = device.connectionType == sdk.ConnectionType.ble + ? ledgerPlusBLE + : ledgerPlusUSB; + + if (_connectionChangeListener == null) { + _connectionChangeListener = ledger.deviceStateChanges.listen((event) { + print('Ledger Device State Changed: $event'); + if (event == sdk.BleConnectionState.disconnected) { + _connection = null; + _connectionChangeListener?.cancel(); + } + }); + } + + _connection = await ledger.connect(device); + } + + StreamSubscription? _connectionChangeListener; + sdk.LedgerConnection? _connection; + + bool get isConnected => _connection != null && !(_connection!.isDisconnected); + + sdk.LedgerConnection get connection => _connection!; void setLedger(WalletBase wallet) { switch (wallet.type) { case WalletType.bitcoin: - return bitcoin!.setLedger(wallet, ledger, device); + case WalletType.litecoin: + return bitcoin!.setLedgerConnection(wallet, connection); case WalletType.ethereum: - return ethereum!.setLedger(wallet, ledger, device); + return ethereum!.setLedgerConnection(wallet, connection); case WalletType.polygon: - return polygon!.setLedger(wallet, ledger, device); + return polygon!.setLedgerConnection(wallet, connection); default: throw Exception('Unexpected wallet type: ${wallet.type}'); } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 4064a1258..24e83f364 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -24,7 +24,6 @@ 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'; import 'package:hive/hive.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/entities/template.dart'; import 'package:cake_wallet/core/address_validator.dart'; @@ -408,16 +407,16 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor state = ExecutedSuccessfullyState(); return pendingTransaction; } catch (e) { - if (e is LedgerException) { - final errorCode = e.errorCode.toRadixString(16); - final fallbackMsg = - e.message.isNotEmpty ? e.message : "Unexpected Ledger Error Code: $errorCode"; - final errorMsg = ledgerViewModel!.interpretErrorCode(errorCode) ?? fallbackMsg; - - state = FailureState(errorMsg); - } else { + // if (e is LedgerException) { + // final errorCode = e.errorCode.toRadixString(16); + // final fallbackMsg = + // e.message.isNotEmpty ? e.message : "Unexpected Ledger Error Code: $errorCode"; + // final errorMsg = ledgerViewModel!.interpretErrorCode(errorCode) ?? fallbackMsg; + // + // state = FailureState(errorMsg); + // } else { state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency)); - } + // } } return null; } diff --git a/lib/view_model/wallet_hardware_restore_view_model.dart b/lib/view_model/wallet_hardware_restore_view_model.dart index 68bc95a00..91e0de685 100644 --- a/lib/view_model/wallet_hardware_restore_view_model.dart +++ b/lib/view_model/wallet_hardware_restore_view_model.dart @@ -13,7 +13,6 @@ 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:ledger_flutter/ledger_flutter.dart'; import 'package:mobx/mobx.dart'; part 'wallet_hardware_restore_view_model.g.dart'; @@ -58,7 +57,11 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with switch (type) { case WalletType.bitcoin: accounts = await bitcoin! - .getHardwareWalletAccounts(ledgerViewModel, index: _nextIndex, limit: limit); + .getHardwareWalletBitcoinAccounts(ledgerViewModel, index: _nextIndex, limit: limit); + break; + case WalletType.litecoin: + accounts = await bitcoin! + .getHardwareWalletLitecoinAccounts(ledgerViewModel, index: _nextIndex, limit: limit); break; case WalletType.ethereum: accounts = await ethereum! @@ -74,9 +77,10 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with availableAccounts.addAll(accounts); _nextIndex += limit; - } on LedgerException catch (e) { - error = ledgerViewModel.interpretErrorCode(e.errorCode.toRadixString(16)); + // } on LedgerException catch (e) { + // error = ledgerViewModel.interpretErrorCode(e.errorCode.toRadixString(16)); } catch (e) { + print(e); error = S.current.ledger_connection_error; } @@ -89,6 +93,7 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with WalletCredentials credentials; switch (type) { case WalletType.bitcoin: + case WalletType.litecoin: credentials = bitcoin!.createBitcoinHardwareWalletCredentials(name: name, accountData: selectedAccount!); break; diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 86b3462ac..9895e6305 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -17,6 +17,7 @@ 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 @@ -33,6 +34,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 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/pubspec_base.yaml b/pubspec_base.yaml index a2f19e596..d5fce76e9 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -12,7 +12,7 @@ dependencies: version: 4.0.2 shared_preferences: ^2.0.15 # provider: ^6.0.3 - rxdart: ^0.27.4 + rxdart: ^0.28.0 yaml: ^3.1.1 #barcode_scan: any barcode_scan2: ^4.2.1 @@ -97,7 +97,7 @@ dependencies: polyseed: ^0.0.6 nostr_tools: ^1.0.9 solana: ^0.30.1 - ledger_flutter: ^1.0.1 + ledger_flutter_plus: ^1.4.1 hashlib: ^1.19.2 dev_dependencies: @@ -125,10 +125,6 @@ dependency_overrides: bech32: git: url: https://github.com/cake-tech/bech32.git - ledger_flutter: - git: - url: https://github.com/cake-tech/ledger-flutter.git - ref: cake-v3 web3dart: git: url: https://github.com/cake-tech/web3dart.git @@ -155,6 +151,7 @@ flutter: assets: - assets/images/ - assets/images/flags/ + - assets/images/hardware_wallet/ - assets/node_list.yml - assets/haven_node_list.yml - assets/bitcoin_electrum_server_list.yml diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 80433ae0e..2ec59f349 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -97,7 +97,7 @@ "cake_pay_account_note": "Melden Sie sich nur mit einer E-Mail-Adresse an, um Karten anzuzeigen und zu kaufen. Einige sind sogar mit Rabatt erhältlich!", "cake_pay_learn_more": "Kaufen und lösen Sie Geschenkkarten sofort in der App ein!\nWischen Sie von links nach rechts, um mehr zu erfahren.", "cake_pay_save_order": "Die Karte sollte innerhalb von 1 Werktag an Ihre E-Mail gesendet werden, \n Ihre Bestell-ID zu speichern:", - "cake_pay_subtitle": "Kaufen Sie weltweite Prepaid -Karten und Geschenkkarten", + "cake_pay_subtitle": "Kaufen Sie weltweite Prepaid-Karten und Geschenkkarten", "cake_pay_web_cards_subtitle": "Kaufen Sie weltweit Prepaid-Karten und Geschenkkarten", "cake_pay_web_cards_title": "Cake Pay-Webkarten", "cake_wallet": "Cake Wallet", @@ -114,7 +114,7 @@ "change_currency": "Währung ändern", "change_current_node": "Möchten Sie den aktuellen Knoten wirklich zu ${node}? ändern?", "change_current_node_title": "Aktuellen Knoten ändern", - "change_exchange_provider": "Swap -Anbieter ändern", + "change_exchange_provider": "Swap-Anbieter ändern", "change_language": "Sprache ändern", "change_language_to": "Sprache zu ${language} ändern?", "change_password": "Passwort ändern", @@ -131,7 +131,7 @@ "choose_one": "Wähle ein", "choose_relay": "Bitte wählen Sie ein zu verwendendes Relais aus", "choose_wallet_currency": "Bitte wählen Sie die Währung der Wallet:", - "choose_wallet_group": "Wählen Sie Brieftaschengruppe", + "choose_wallet_group": "Wählen Sie Walletgruppe", "clear": "Zurücksetzen", "clearnet_link": "Clearnet-Link", "close": "Schließen", @@ -179,7 +179,7 @@ "create_invoice": "Rechnung erstellen", "create_new": "Neue Wallet erstellen", "create_new_account": "Neues Konto erstellen", - "create_new_seed": "Neue Samen erstellen", + "create_new_seed": "Neue Seed erstellen", "creating_new_wallet": "Neue Wallet erstellen", "creating_new_wallet_error": "Fehler: ${description}", "creation_date": "Erstellungsdatum", @@ -189,9 +189,9 @@ "custom_value": "Benutzerdefinierten Wert", "dark_theme": "Dunkel", "debit_card": "Debitkarte", - "debit_card_terms": "Die Speicherung und Nutzung Ihrer Zahlungskartennummer (und Ihrer Zahlungskartennummer entsprechenden Anmeldeinformationen) in dieser digitalen Geldbörse unterliegt den Allgemeinen Geschäftsbedingungen des geltenden Karteninhabervertrags mit dem Zahlungskartenaussteller, gültig ab von Zeit zu Zeit.", + "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.", + "decimals_cannot_be_zero": "Token-Dezimalzahl kann nicht Null sein.", "default_buy_provider": "Standard-Kaufanbieter", "default_sell_provider": "Standard-Verkaufsanbieter", "delete": "Löschen", @@ -236,7 +236,7 @@ "electrum_address_disclaimer": "Wir generieren jedes Mal neue Adressen, wenn Sie eine verwenden, aber vorherige Adressen funktionieren weiterhin", "email_address": "E-Mail-Adresse", "enable": "Aktivieren", - "enable_mempool_api": "Mempool -API für genaue Gebühren und Daten", + "enable_mempool_api": "Mempool-API für genaue Gebühren und Daten", "enable_replace_by_fee": "Aktivieren Sie Ersatz für Fee", "enable_silent_payments_scanning": "Scannen Sie stille Zahlungen, bis die Spitze erreicht ist", "enabled": "Ermöglicht", @@ -245,7 +245,7 @@ "enter_code": "Code eingeben", "enter_seed_phrase": "Geben Sie Ihre Seed-Phrase ein", "enter_totp_code": "Bitte geben Sie den TOTP-Code ein.", - "enter_wallet_password": "Geben Sie das Brieftaschenkennwort ein", + "enter_wallet_password": "Geben Sie das Walletkennwort ein", "enter_your_note": "Geben Sie Ihre Bemerkung ein…", "enter_your_pin": "PIN eingeben", "enter_your_pin_again": "Geben Sie Ihre PIN erneut ein", @@ -282,7 +282,7 @@ "event": "Ereignis", "events": "Veranstaltungen", "exchange": "Tauschen", - "exchange_incorrect_current_wallet_for_xmr": "Wenn Sie XMR aus Ihrer Cake -Wallet Monero -Balance austauschen möchten, wechseln Sie zuerst zu Ihrer Monero -Brieftasche.", + "exchange_incorrect_current_wallet_for_xmr": "Wenn Sie XMR aus Ihrer CakeWallet Monero-Balance tauschen möchten, wechseln Sie zuerst zu Ihrer Monero-Wallet.", "exchange_new_template": "Neue Vorlage", "exchange_provider_unsupported": "${providerName} wird nicht mehr unterstützt!", "exchange_result_confirm": "Durch Drücken von \"Bestätigen\" wird ${fetchingLabel} ${from} von Ihrer Wallet namens ${walletName} an die unten angegebene Adresse gesendet. Alternativ können Sie von einer externen Wallet an die unten angegebene Adresse / QR-Code senden.\n\nBitte bestätigen Sie, um fortzufahren, oder gehen Sie zurück, um die Beträge zu ändern.", @@ -309,7 +309,7 @@ "fill_code": "Geben Sie den Bestätigungscode ein, den Sie per E-Mail erhalten haben", "filter_by": "Filtern nach", "first_wallet_text": "Eine großartige Wallet für Monero, Bitcoin, Ethereum, Litecoin, und Haven", - "fixed_pair_not_supported": "Dieses feste Paar wird nicht von den ausgewählten Swap -Diensten unterstützt", + "fixed_pair_not_supported": "Dieses feste Paar wird nicht von den ausgewählten Swap-Diensten unterstützt", "fixed_rate": "Feste Rate", "fixed_rate_alert": "Sie können den Empfangsbetrag eingeben, wenn der Festratenmodus aktiviert ist. Möchten Sie in den Festratenmodus wechseln?", "forgot_password": "Passwort vergessen", @@ -349,9 +349,9 @@ "incoming": "Eingehend", "incorrect_seed": "Der eingegebene Text ist ungültig.", "inputs": "Eingänge", - "insufficient_lamport_for_tx": "Sie haben nicht genug SOL, um die Transaktion und ihre Transaktionsgebühr abzudecken. Bitte fügen Sie Ihrer Brieftasche mehr Sol hinzu oder reduzieren Sie die SO -Menge, die Sie senden.", - "insufficient_lamports": "Sie haben nicht genug SOL, um die Transaktion und ihre Transaktionsgebühr abzudecken. Sie brauchen mindestens ${solValueNeeded} Sol. Bitte fügen Sie mehr Sol zu Ihrer Wallet hinzu oder reduzieren Sie den von Ihnen gesendeten Sol -Betrag", - "insufficientFundsForRentError": "Sie haben nicht genug SOL, um die Transaktionsgebühr und die Miete für das Konto zu decken. Bitte fügen Sie mehr Sol zu Ihrer Brieftasche hinzu oder reduzieren Sie den von Ihnen gesendeten Sol -Betrag", + "insufficient_lamport_for_tx": "Sie haben nicht genug SOL, um die Transaktion und ihre Transaktionsgebühr abzudecken. Bitte fügen Sie Ihrer Wallet mehr Sol hinzu oder reduzieren Sie die SOL-Menge, die Sie senden.", + "insufficient_lamports": "Sie haben nicht genug SOL, um die Transaktion und ihre Transaktionsgebühr abzudecken. Sie brauchen mindestens ${solValueNeeded} Sol. Bitte fügen Sie mehr Sol zu Ihrer Wallet hinzu oder reduzieren Sie den von Ihnen gesendeten Sol-Betrag", + "insufficientFundsForRentError": "Sie haben nicht genug SOL, um die Transaktionsgebühr und die Miete für das Konto zu decken. Bitte fügen Sie mehr Sol zu Ihrer Wallet hinzu oder reduzieren Sie den von Ihnen gesendeten Sol-Betrag", "introducing_cake_pay": "Einführung von Cake Pay!", "invalid_input": "Ungültige Eingabe", "invalid_password": "Ungültiges Passwort", @@ -365,20 +365,20 @@ "ledger_error_wrong_app": "Bitte stellen Sie sicher, dass Sie die richtige App auf Ihrem Ledger geöffnet haben", "ledger_please_enable_bluetooth": "Bitte aktivieren Sie Bluetooth um sich mit Ihren Ledger zu verbinden.", "light_theme": "Hell", - "litecoin_enable_mweb_sync": "Aktivieren Sie das MWEB -Scannen", + "litecoin_enable_mweb_sync": "Aktivieren Sie das MWEB-Scannen", "litecoin_mweb": "MWeb", "litecoin_mweb_always_scan": "Setzen Sie MWeb immer scannen", - "litecoin_mweb_description": "MWEB ist ein neues Protokoll, das schnellere, billigere und privatere Transaktionen zu Litecoin bringt", + "litecoin_mweb_description": "MWWB ist ein neues Protokoll, das schnellere, billigere und privatere Transaktionen zu Litecoin bringt", "litecoin_mweb_dismiss": "Zurückweisen", - "litecoin_mweb_display_card": "MWEB -Karte anzeigen", + "litecoin_mweb_display_card": "MWEB-Karte anzeigen", "litecoin_mweb_enable_later": "Sie können MWEB unter Anzeigeeinstellungen erneut aktivieren.", "litecoin_mweb_pegin": "Peg in", "litecoin_mweb_pegout": "Abstecken", "litecoin_mweb_scanning": "MWEB Scanning", - "litecoin_mweb_settings": "MWEB -Einstellungen", - "litecoin_mweb_warning": "Durch die Verwendung von MWEB wird zunächst ~ 600 MB Daten heruntergeladen und kann je nach Netzwerkgeschwindigkeit bis zu 30 Minuten dauern. Diese ersten Daten werden nur einmal heruntergeladen und für alle Litecoin -Brieftaschen verfügbar", + "litecoin_mweb_settings": "MWEB-Einstellungen", + "litecoin_mweb_warning": "Durch die Verwendung von MWEB wird zunächst ~ 600 MB Daten heruntergeladen und kann je nach Netzwerkgeschwindigkeit bis zu 30 Minuten dauern. Diese ersten Daten werden nur einmal heruntergeladen und für alle Litecoin-Wallets verfügbar", "litecoin_what_is_mweb": "Was ist MWeb?", - "live_fee_rates": "Live -Gebührenpreise über API", + "live_fee_rates": "Live-Gebührenpreise über API", "load_more": "Mehr laden", "loading_your_wallet": "Wallet wird geladen", "login": "Einloggen", @@ -533,8 +533,8 @@ "rename": "Umbenennen", "rep_warning": "Repräsentative Warnung", "rep_warning_sub": "Ihr Vertreter scheint nicht gut zu sein. Tippen Sie hier, um eine neue auszuwählen", - "repeat_wallet_password": "Wiederholen Sie das Brieftaschenkennwort", - "repeated_password_is_incorrect": "Wiederholtes Passwort ist falsch. Bitte wiederholen Sie das Brieftaschenkennwort erneut.", + "repeat_wallet_password": "Wiederholen Sie das Walletkennwort", + "repeated_password_is_incorrect": "Wiederholtes Passwort ist falsch. Bitte wiederholen Sie das Walletkennwort erneut.", "require_for_adding_contacts": "Erforderlich zum Hinzufügen von Kontakten", "require_for_all_security_and_backup_settings": "Für alle Sicherheits- und Sicherungseinstellungen erforderlich", "require_for_assessing_wallet": "Für den Zugriff auf die Wallet erforderlich", @@ -576,7 +576,7 @@ "restore_wallet": "Wallet wiederherstellen", "restore_wallet_name": "Walletname", "restore_wallet_restore_description": "Beschreibung zur Wallet-Wiederherstellung", - "robinhood_option_description": "Kaufen und übertragen Sie sofort mit Ihrem Debitkarten-, Bankkonto- oder Robinhood -Guthaben. Nur USA.", + "robinhood_option_description": "Kaufen und übertragen Sie sofort mit Ihrem Debitkarten-, Bankkonto- oder Robinhood-Guthaben. Nur USA.", "router_no_route": "Keine Route definiert für ${name}", "save": "Speichern", "save_backup_password": "Bitte stellen Sie sicher, dass Sie Ihr Sicherungskennwort gespeichert haben. Ohne dieses können Sie Ihre Sicherungsdateien nicht importieren.", @@ -622,8 +622,8 @@ "seed_share": "Seed teilen", "seed_title": "Seed", "seedtype": "Seedtyp", - "seedtype_alert_content": "Das Teilen von Samen mit anderen Brieftaschen ist nur mit bip39 Seedype möglich.", - "seedtype_alert_title": "Seedype -Alarm", + "seedtype_alert_content": "Das Teilen von Seeds mit anderen Wallet ist nur mit bip39 Seedype möglich.", + "seedtype_alert_title": "Seedype-Alarm", "seedtype_legacy": "Veraltet (25 Wörter)", "seedtype_polyseed": "Polyseed (16 Wörter)", "seedtype_wownero": "WOWNO (14 Wörter)", @@ -691,7 +691,7 @@ "setup_your_debit_card": "Richten Sie Ihre Debitkarte ein", "share": "Teilen", "share_address": "Adresse teilen ", - "shared_seed_wallet_groups": "Gemeinsame Samenbrieftaschengruppen", + "shared_seed_wallet_groups": "Gemeinsame Walletsseed Gruppen", "show_details": "Details anzeigen", "show_keys": "Seed/Schlüssel anzeigen", "show_market_place": "Marktplatz anzeigen", @@ -711,12 +711,12 @@ "silent_payments_disclaimer": "Neue Adressen sind keine neuen Identitäten. Es ist eine Wiederverwendung einer bestehenden Identität mit einem anderen Etikett.", "silent_payments_display_card": "Zeigen Sie stille Zahlungskarte", "silent_payments_scan_from_date": "Scan ab Datum", - "silent_payments_scan_from_date_or_blockheight": "Bitte geben Sie die Blockhöhe ein, die Sie für eingehende stille Zahlungen scannen möchten, oder verwenden Sie stattdessen das Datum. Sie können wählen, ob die Brieftasche jeden Block scannt oder nur die angegebene Höhe überprüft.", + "silent_payments_scan_from_date_or_blockheight": "Bitte geben Sie die Blockhöhe ein, die Sie für eingehende stille Zahlungen scannen möchten, oder verwenden Sie stattdessen das Datum. Sie können wählen, ob die Wallet jeden Block scannt oder nur die angegebene Höhe überprüft.", "silent_payments_scan_from_height": "Scan aus der Blockhöhe scannen", "silent_payments_scanned_tip": "Gescannt zum Trinkgeld! (${tip})", "silent_payments_scanning": "Stille Zahlungen scannen", "silent_payments_settings": "Einstellungen für stille Zahlungen", - "single_seed_wallets_group": "Einzelne Samenbriefen", + "single_seed_wallets_group": "Einzelne Wallets", "slidable": "Verschiebbar", "sort_by": "Sortiere nach", "spend_key_private": "Spend Key (geheim)", @@ -755,11 +755,11 @@ "syncing_wallet_alert_title": "Ihr Wallet wird synchronisiert", "template": "Vorlage", "template_name": "Vorlagenname", - "testnet_coins_no_value": "Testnet -Münzen haben keinen Wert", + "testnet_coins_no_value": "Testnet-Münzen haben keinen Wert", "third_intro_content": "Yats leben auch außerhalb von Cake Wallet. Jede Wallet-Adresse auf der Welt kann durch ein Yat ersetzt werden!", "third_intro_title": "Yat spielt gut mit anderen", "thorchain_contract_address_not_supported": "Thorchain unterstützt das Senden an eine Vertragsadresse nicht", - "thorchain_taproot_address_not_supported": "Der Thorchain -Anbieter unterstützt keine Taproot -Adressen. Bitte ändern Sie die Adresse oder wählen Sie einen anderen Anbieter aus.", + "thorchain_taproot_address_not_supported": "Der Thorchain-Anbieter unterstützt keine Taproot-Adressen. Bitte ändern Sie die Adresse oder wählen Sie einen anderen Anbieter aus.", "time": "${minutes}m ${seconds}s", "tip": "Hinweis:", "today": "Heute", @@ -879,12 +879,12 @@ "voting_weight": "Stimmgewicht", "waitFewSecondForTxUpdate": "Bitte warten Sie einige Sekunden, bis die Transaktion im Transaktionsverlauf angezeigt wird", "waiting_payment_confirmation": "Warte auf Zahlungsbestätigung", - "wallet_group": "Brieftaschengruppe", - "wallet_group_description_four": "eine Brieftasche mit einem völlig neuen Samen schaffen.", - "wallet_group_description_one": "In Kuchenbrieftasche können Sie eine erstellen", - "wallet_group_description_three": "Sehen Sie den Bildschirm zur verfügbaren Brieftaschen und/oder Brieftaschengruppen. Oder wählen", - "wallet_group_description_two": "Durch die Auswahl einer vorhandenen Brieftasche, mit der ein Samen geteilt werden kann. Jede Brieftaschengruppe kann eine einzelne Brieftasche jedes Währungstyps enthalten. \n\n Sie können auswählen", - "wallet_group_empty_state_text_one": "Sieht so aus, als hätten Sie keine kompatiblen Brieftaschengruppen !\n\n TAP", + "wallet_group": "Walletgruppe", + "wallet_group_description_four": "eine Wallet mit einem völlig neuen Seed schaffen.", + "wallet_group_description_one": "In CakeWallet können Sie eine erstellen", + "wallet_group_description_three": "Sehen Sie den Bildschirm zur verfügbaren Wallet und/oder Walletgruppen. Oder wählen", + "wallet_group_description_two": "Durch die Auswahl einer vorhandenen Wallet, mit der ein Seed geteilt werden kann. Jede Walletgruppe kann eine einzelne Wallet jedes Währungstyps enthalten. \n\n Sie können auswählen", + "wallet_group_empty_state_text_one": "Sieht so aus, als hätten Sie keine kompatiblen Walletgruppen !\n\n TAP", "wallet_group_empty_state_text_two": "unten, um einen neuen zu machen.", "wallet_keys": "Wallet-Seed/-Schlüssel", "wallet_list_create_new_wallet": "Neue Wallet erstellen", @@ -901,7 +901,7 @@ "wallet_menu": "Wallet-Menü", "wallet_name": "Walletname", "wallet_name_exists": "Wallet mit diesem Namen existiert bereits", - "wallet_password_is_empty": "Brieftaschenkennwort ist leer. Brieftaschenkennwort sollte nicht leer sein", + "wallet_password_is_empty": "Walletkennwort ist leer. Walletkennwort sollte nicht leer sein", "wallet_recovery_height": "Erstellungshöhe", "wallet_restoration_store_incorrect_seed_length": "Falsche Seed-Länge", "wallet_seed": "Wallet-Seed", @@ -941,4 +941,4 @@ "you_will_get": "Konvertieren zu", "you_will_send": "Konvertieren von", "yy": "YY" -} \ No newline at end of file +} diff --git a/tool/configure.dart b/tool/configure.dart index 704b47526..97541c2fa 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -97,7 +97,7 @@ import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/get_height_by_date.dart'; import 'package:hive/hive.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:bip39/bip39.dart' as bip39; """; @@ -121,6 +121,7 @@ import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/litecoin_wallet_service.dart'; import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:cw_bitcoin/bitcoin_hardware_wallet_service.dart'; +import 'package:cw_bitcoin/litecoin_hardware_wallet_service.dart'; import 'package:mobx/mobx.dart'; """; const bitcoinCwPart = "part 'cw_bitcoin.dart';"; @@ -223,8 +224,9 @@ abstract class Bitcoin { void deleteSilentPaymentAddress(Object wallet, String address); Future updateFeeRates(Object wallet); int getMaxCustomFeeRate(Object wallet); - void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device); - Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); + void setLedgerConnection(WalletBase wallet, ledger.LedgerConnection connection); + Future> getHardwareWalletBitcoinAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); + Future> getHardwareWalletLitecoinAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); List updateOutputs(PendingTransaction pendingTransaction, List outputs); bool txIsReceivedSilentPayment(TransactionInfo txInfo); bool txIsMweb(TransactionInfo txInfo); @@ -819,7 +821,7 @@ 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:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; import 'package:web3dart/web3dart.dart'; """; @@ -885,7 +887,7 @@ abstract class Ethereum { Web3Client? getWeb3Client(WalletBase wallet); String getTokenAddress(CryptoCurrency asset); - void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device); + void setLedgerConnection(WalletBase wallet, ledger.LedgerConnection connection); Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); } """; @@ -923,7 +925,7 @@ 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:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; import 'package:web3dart/web3dart.dart'; """; @@ -989,7 +991,7 @@ abstract class Polygon { Web3Client? getWeb3Client(WalletBase wallet); String getTokenAddress(CryptoCurrency asset); - void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device); + void setLedgerConnection(WalletBase wallet, ledger.LedgerConnection connection); Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); } """; diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index c6444e09c..ad540a359 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -11,6 +11,7 @@ #include #include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { @@ -24,6 +25,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + UniversalBlePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UniversalBlePluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 0a0b2f9eb..92431a6fb 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_secure_storage_windows permission_handler_windows share_plus + universal_ble url_launcher_windows ) From 4372165b261bcb52818e56966e1295a24e1cf2a2 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Thu, 24 Oct 2024 04:20:25 +0100 Subject: [PATCH 33/73] fix: Issue with prevent screenshots implementation on iOS (#1729) --- ios/Runner/AppDelegate.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 402f6556b..477d47f7b 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -110,12 +110,12 @@ import workmanager private func makeSecure() { if (!self.window.subviews.contains(textField)) { + let view = UIView(frame: CGRect(x: 0, y: 0, width: textField.frame.self.width, height: textField.frame.self.height)) self.window.addSubview(textField) - textField.centerYAnchor.constraint(equalTo: self.window.centerYAnchor).isActive = true - textField.centerXAnchor.constraint(equalTo: self.window.centerXAnchor).isActive = true self.window.layer.superlayer?.addSublayer(textField.layer) - textField.layer.sublayers?.first?.addSublayer(self.window.layer) + textField.layer.sublayers?.last!.addSublayer(self.window.layer) + textField.leftView = view + textField.leftViewMode = .always } } - -} +} \ No newline at end of file From 858e5cac523583ee37255b7eb033d13946686593 Mon Sep 17 00:00:00 2001 From: rottenwheel <92872541+rottenwheel@users.noreply.github.com> Date: Fri, 25 Oct 2024 21:56:02 +0000 Subject: [PATCH 34/73] spanish: miscellaneous rewording (#1777) --- res/values/strings_es.arb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index a8ad7549a..520c8ea78 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -78,7 +78,7 @@ "billing_address_info": "Si se le solicita una dirección de facturación, proporcione su dirección de envío", "biometric_auth_reason": "Escanee su huella digital para autenticar", "bitcoin_dark_theme": "Tema oscuro de Bitcoin", - "bitcoin_light_theme": "Tema de la luz de Bitcoin", + "bitcoin_light_theme": "Tema claro de Bitcoin", "bitcoin_payments_require_1_confirmation": "Los pagos de Bitcoin requieren 1 confirmación, que puede demorar 20 minutos o más. ¡Gracias por su paciencia! Se le enviará un correo electrónico cuando se confirme el pago.", "block_remaining": "1 bloqueo restante", "Blocks_remaining": "${status} Bloques restantes", @@ -93,7 +93,7 @@ "buy_with": "Compra con", "by_cake_pay": "por Cake Pay", "cake_2fa_preset": "Pastel 2FA preestablecido", - "cake_dark_theme": "Tema oscuro del pastel", + "cake_dark_theme": "Tema oscuro", "cake_pay_account_note": "Regístrese con solo una dirección de correo electrónico para ver y comprar tarjetas. ¡Algunas incluso están disponibles con descuento!", "cake_pay_learn_more": "¡Compre y canjee tarjetas de regalo al instante en la aplicación!\nDeslice el dedo de izquierda a derecha para obtener más información.", "cake_pay_save_order": "La tarjeta debe enviarse a su correo electrónico dentro de 1 día hábil \n Guardar su ID de pedido:", @@ -144,9 +144,9 @@ "confirm_delete_wallet": "Esta acción eliminará esta billetera. ¿Desea continuar?", "confirm_fee_deduction": "Confirmar la deducción de la tarifa", "confirm_fee_deduction_content": "¿Acepta deducir la tarifa de la producción?", - "confirm_passphrase": "Confirmar la frase de pases", + "confirm_passphrase": "Confirmar la contraseña", "confirm_sending": "Confirmar envío", - "confirm_silent_payments_switch_node": "Su nodo actual no admite pagos silenciosos \\ ncake billet cambiará a un nodo compatible, solo para escanear", + "confirm_silent_payments_switch_node": "Su nodo actual no admite pagos silenciosos \\ nCake cambiará a un nodo compatible, solo para escanear", "confirmations": "Confirmaciones", "confirmed": "Saldo confirmado", "confirmed_tx": "Confirmado", @@ -281,7 +281,7 @@ "etherscan_history": "historia de etherscan", "event": "Evento", "events": "Eventos", - "exchange": "Intercambio", + "exchange": "Intercambiar", "exchange_incorrect_current_wallet_for_xmr": "Si desea intercambiar XMR desde su billetera de pastel Monero Balance, primero cambie a su billetera Monero.", "exchange_new_template": "Nueva plantilla", "exchange_provider_unsupported": "¡${providerName} ya no es compatible!", @@ -826,7 +826,7 @@ "transaction_priority_slow": "Lento", "transaction_sent": "Transacción enviada!", "transaction_sent_notice": "Si la pantalla no continúa después de 1 minuto, revisa un explorador de bloques y tu correo electrónico.", - "transactions": "Actas", + "transactions": "Transacciones", "transactions_by_date": "Transacciones por fecha", "trongrid_history": "Historia trongrid", "trusted": "de confianza", @@ -939,4 +939,4 @@ "you_will_get": "Convertir a", "you_will_send": "Convertir de", "yy": "YY" -} \ No newline at end of file +} From 54c41d41cd1cd3dec59fafad9fd56eeb0bda671b Mon Sep 17 00:00:00 2001 From: rottenwheel <92872541+rottenwheel@users.noreply.github.com> Date: Fri, 25 Oct 2024 21:56:21 +0000 Subject: [PATCH 35/73] spanish (#1776) --- res/values/strings_es.arb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 520c8ea78..449557671 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -372,8 +372,8 @@ "litecoin_mweb_dismiss": "Despedir", "litecoin_mweb_display_card": "Mostrar tarjeta MWEB", "litecoin_mweb_enable_later": "Puede elegir habilitar MWEB nuevamente en la configuración de visualización.", - "litecoin_mweb_pegin": "Meter", - "litecoin_mweb_pegout": "Estirar la pata", + "litecoin_mweb_pegin": "Convertir", + "litecoin_mweb_pegout": "Recuperar", "litecoin_mweb_scanning": "Escaneo mweb", "litecoin_mweb_settings": "Configuración de MWEB", "litecoin_mweb_warning": "El uso de MWEB inicialmente descargará ~ 600 MB de datos, y puede tomar hasta 30 minutos según la velocidad de la red. Estos datos iniciales solo se descargarán una vez y estarán disponibles para todas las billeteras de Litecoin", From 5c206a8b7cf2a9279910a7c97bf2689099a26f0a Mon Sep 17 00:00:00 2001 From: Serhii Date: Mon, 28 Oct 2024 02:37:38 +0200 Subject: [PATCH 36/73] fix MobX Exception: computed values issue (#1763) --- cw_bitcoin/lib/electrum_wallet_addresses.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 2686d12cc..c29579436 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -331,7 +331,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { ); silentAddresses.add(address); - updateAddressesByMatch(); + Future.delayed(Duration.zero, () => updateAddressesByMatch()); return address; } @@ -348,7 +348,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { network: network, ); _addresses.add(address); - updateAddressesByMatch(); + Future.delayed(Duration.zero, () => updateAddressesByMatch()); return address; } From 198510533e854f437037573fe0575d73a9b21971 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Wed, 30 Oct 2024 17:57:26 +0100 Subject: [PATCH 37/73] Fix disappearing recipientAddress for XMR Transactions (#1784) --- .../transaction_details_view_model.dart | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 1833a0f51..da6699ae6 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -83,18 +83,19 @@ abstract class TransactionDetailsViewModelBase with Store { break; } - if (showRecipientAddress && !isRecipientAddressShown) { - try { - final recipientAddress = transactionDescriptionBox.values - .firstWhere((val) => val.id == transactionInfo.txHash) - .recipientAddress; + final descriptionKey = + '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}'; + final description = transactionDescriptionBox.values.firstWhere( + (val) => val.id == descriptionKey || val.id == transactionInfo.txHash, + orElse: () => TransactionDescription(id: descriptionKey)); - if (recipientAddress?.isNotEmpty ?? false) { - items.add(StandartListItem( - title: S.current.transaction_details_recipient_address, value: recipientAddress!)); - } - } catch (_) { - // FIX-ME: Unhandled exception + if (showRecipientAddress && !isRecipientAddressShown) { + final recipientAddress = description.recipientAddress; + + if (recipientAddress?.isNotEmpty ?? false) { + items.add(StandartListItem( + title: S.current.transaction_details_recipient_address, + value: recipientAddress!)); } } @@ -110,12 +111,6 @@ abstract class TransactionDetailsViewModelBase with Store { } catch (e) {} })); - final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}'; - - final description = transactionDescriptionBox.values.firstWhere( - (val) => val.id == descriptionKey || val.id == transactionInfo.txHash, - orElse: () => TransactionDescription(id: descriptionKey)); - items.add(TextFieldListItem( title: S.current.note_tap_to_change, value: description.note, From e501a4131444ea79e6db79348081e5945ee21490 Mon Sep 17 00:00:00 2001 From: tuxsudo Date: Wed, 30 Oct 2024 14:44:49 -0400 Subject: [PATCH 38/73] Fix the manifest once and for all (#1773) --- android/app/src/main/AndroidManifestBase.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index 98bbb1236..5b080e3ec 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -14,7 +14,8 @@ - + + From 5fc649023cda6a802802466559642c5707d16f73 Mon Sep 17 00:00:00 2001 From: oksanaphmn Date: Wed, 30 Oct 2024 20:50:04 +0200 Subject: [PATCH 39/73] Update README.md (#1770) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1c28f92a2..ca3061e61 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ # Cake Wallet -Cake Wallet is an open source, non-custodial, and private multi-currency crypto wallet for Android, iOS, macOS, and Linux. +[Cake Wallet](https://cakewallet.com) is an open source, non-custodial, and private multi-currency crypto wallet for Android, iOS, macOS, and Linux. Cake Wallet includes support for several cryptocurrencies, including: * Monero (XMR) @@ -26,7 +26,7 @@ Cake Wallet includes support for several cryptocurrencies, including: * Ethereum (ETH) * Litecoin (LTC) * Bitcoin Cash (BCH) -* Polygon (MATIC) +* Polygon (Pol) * Solana (SOL) * Nano (XNO) * Haven (XHV) From 752b6bbebf7e975840f11ff569f13bff7600d1ea Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Thu, 31 Oct 2024 02:10:40 +0100 Subject: [PATCH 40/73] fix: Generic fixes of bugs from Support (#1774) --- cw_bitcoin/lib/electrum_wallet.dart | 2 ++ cw_haven/lib/haven_wallet_addresses.dart | 2 ++ lib/core/wallet_connect/web3wallet_service.dart | 8 ++++++++ lib/entities/fiat_currency.dart | 2 +- .../desktop_wallet_selection_dropdown.dart | 8 ++++++-- .../widgets/modals/bottom_sheet_listener.dart | 4 +++- lib/src/widgets/cake_image_widget.dart | 2 ++ lib/src/widgets/services_updates_widget.dart | 2 ++ lib/utils/exception_handler.dart | 8 ++++++++ lib/utils/image_utill.dart | 14 ++++++++++++-- 10 files changed, 46 insertions(+), 6 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 4b80cb201..c05095cf1 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -2270,6 +2270,8 @@ abstract class ElectrumWalletBase extends WalletBase< Timer(Duration(seconds: 5), () { if (this.syncStatus is NotConnectedSyncStatus || this.syncStatus is LostConnectionSyncStatus) { + if (node == null) return; + this.electrumClient.connectToUri( node!.uri, useSSL: node!.useSSL ?? false, diff --git a/cw_haven/lib/haven_wallet_addresses.dart b/cw_haven/lib/haven_wallet_addresses.dart index 06de44dff..c3d1ef46c 100644 --- a/cw_haven/lib/haven_wallet_addresses.dart +++ b/cw_haven/lib/haven_wallet_addresses.dart @@ -23,6 +23,8 @@ abstract class HavenWalletAddressesBase extends WalletAddressesWithAccount address; + // @override @observable Account? account; diff --git a/lib/core/wallet_connect/web3wallet_service.dart b/lib/core/wallet_connect/web3wallet_service.dart index adb516817..ba4785643 100644 --- a/lib/core/wallet_connect/web3wallet_service.dart +++ b/lib/core/wallet_connect/web3wallet_service.dart @@ -267,6 +267,8 @@ abstract class Web3WalletServiceBase with Store { final keyForWallet = getKeyForStoringTopicsForWallet(); + if (keyForWallet.isEmpty) return; + final currentTopicsForWallet = getPairingTopicsForWallet(keyForWallet); final filteredPairings = @@ -360,6 +362,10 @@ abstract class Web3WalletServiceBase with Store { String getKeyForStoringTopicsForWallet() { List chainKeys = walletKeyService.getKeysForChain(appStore.wallet!); + if (chainKeys.isEmpty) { + return ''; + } + final keyForPairingTopic = PreferencesKey.walletConnectPairingTopicsListForWallet(chainKeys.first.publicKey); @@ -386,6 +392,8 @@ abstract class Web3WalletServiceBase with Store { // Get key specific to the current wallet final key = getKeyForStoringTopicsForWallet(); + if (key.isEmpty) return; + // Get all pairing topics attached to this key final pairingTopicsForWallet = getPairingTopicsForWallet(key); diff --git a/lib/entities/fiat_currency.dart b/lib/entities/fiat_currency.dart index a8e2829d8..1a031ee82 100644 --- a/lib/entities/fiat_currency.dart +++ b/lib/entities/fiat_currency.dart @@ -114,7 +114,7 @@ class FiatCurrency extends EnumerableItem with Serializable impl FiatCurrency.tur.raw: FiatCurrency.tur, }; - static FiatCurrency deserialize({required String raw}) => _all[raw]!; + static FiatCurrency deserialize({required String raw}) => _all[raw] ?? FiatCurrency.usd; @override bool operator ==(Object other) => other is FiatCurrency && other.raw == raw; 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 f8cbe9120..0fb629685 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 @@ -10,7 +10,6 @@ import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/dropdown_item_ import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/menu_theme.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; @@ -100,6 +99,11 @@ class _DesktopWalletSelectionDropDownState extends State element.isSelected, + orElse: () => dropDownItems.first, + ); + return DropdownButton( items: dropDownItems .map( @@ -115,7 +119,7 @@ class _DesktopWalletSelectionDropDownState extends State()!.backgroundColor, style: TextStyle(color: themeData.extension()!.titleColor), selectedItemBuilder: (context) => dropDownItems.map((item) => item.child).toList(), - value: dropDownItems.firstWhere((element) => element.isSelected), + value: selectedItem, underline: const SizedBox(), focusColor: Colors.transparent, borderRadius: BorderRadius.circular(15.0), diff --git a/lib/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart b/lib/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart index 30b6af7e0..42bef5135 100644 --- a/lib/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart +++ b/lib/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart @@ -52,7 +52,9 @@ class BottomSheetListenerState extends State { ); }, ); - item.completer.complete(value); + if (!item.completer.isCompleted) { + item.completer.complete(value); + } widget.bottomSheetService.resetCurrentSheet(); } } diff --git a/lib/src/widgets/cake_image_widget.dart b/lib/src/widgets/cake_image_widget.dart index ad02c48dd..2038030c0 100644 --- a/lib/src/widgets/cake_image_widget.dart +++ b/lib/src/widgets/cake_image_widget.dart @@ -25,6 +25,7 @@ class CakeImageWidget extends StatelessWidget { imageUrl!, height: height, width: width, + errorBuilder: (_, __, ___) => Icon(Icons.error), ); } @@ -33,6 +34,7 @@ class CakeImageWidget extends StatelessWidget { imageUrl!, height: height, width: width, + placeholderBuilder: (_) => Icon(Icons.error), ); } diff --git a/lib/src/widgets/services_updates_widget.dart b/lib/src/widgets/services_updates_widget.dart index 1babd23d0..1787b7118 100644 --- a/lib/src/widgets/services_updates_widget.dart +++ b/lib/src/widgets/services_updates_widget.dart @@ -46,6 +46,7 @@ class _ServicesUpdatesWidgetState extends State { "assets/images/notification_icon.svg", color: Theme.of(context).extension()!.pageTitleTextColor, width: 30, + placeholderBuilder: (_) => Icon(Icons.error), ), ); } @@ -136,6 +137,7 @@ class _ServicesUpdatesWidgetState extends State { "assets/images/notification_icon.svg", color: Theme.of(context).extension()!.pageTitleTextColor, width: 30, + placeholderBuilder: (_) => Icon(Icons.error), ), if (state.hasData && state.data!.hasUpdates && !wasOpened) Container( diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index 91797c45e..41ae91d41 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -62,6 +62,14 @@ class ExceptionHandler { await _addDeviceInfo(_file!); + // Check if a mail client is available + final bool canSend = await FlutterMailer.canSendMail(); + + if (Platform.isIOS && !canSend) { + debugPrint('Mail app is not available'); + return; + } + final MailOptions mailOptions = MailOptions( subject: 'Mobile App Issue', recipients: ['support@cakewallet.com'], diff --git a/lib/utils/image_utill.dart b/lib/utils/image_utill.dart index a138df23a..ef3775e4c 100644 --- a/lib/utils/image_utill.dart +++ b/lib/utils/image_utill.dart @@ -53,8 +53,18 @@ class ImageUtil { ); } else { return isSvg - ? SvgPicture.asset(imagePath, height: _height, width: _width) - : Image.asset(imagePath, height: _height, width: _width); + ? SvgPicture.asset( + imagePath, + height: _height, + width: _width, + placeholderBuilder: (_) => Icon(Icons.error), + ) + : Image.asset( + imagePath, + height: _height, + width: _width, + errorBuilder: (_, __, ___) => Icon(Icons.error), + ); } } } From 740f466e77464090f1e4c9457f3c4abeb7c2181e Mon Sep 17 00:00:00 2001 From: Rafael Date: Fri, 1 Nov 2024 14:34:17 -0300 Subject: [PATCH 41/73] fix: sp filter option (#1780) * fix: sp filter option * fix: nullable --- .../dashboard/transaction_filter_store.dart | 5 +++-- .../dashboard/dashboard_view_model.dart | 16 ++++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/store/dashboard/transaction_filter_store.dart b/lib/store/dashboard/transaction_filter_store.dart index fb9ee14cd..f28f7e915 100644 --- a/lib/store/dashboard/transaction_filter_store.dart +++ b/lib/store/dashboard/transaction_filter_store.dart @@ -91,8 +91,9 @@ abstract class TransactionFilterStoreBase with Store { (displayOutgoing && item.transaction.direction == TransactionDirection.outgoing) || (displayIncoming && item.transaction.direction == TransactionDirection.incoming && - !bitcoin!.txIsReceivedSilentPayment(item.transaction)) || - (displaySilentPayments && bitcoin!.txIsReceivedSilentPayment(item.transaction)); + !(bitcoin?.txIsReceivedSilentPayment(item.transaction) ?? false)) || + (displaySilentPayments && + (bitcoin?.txIsReceivedSilentPayment(item.transaction) ?? false)); } else if (item is AnonpayTransactionListItem) { allowed = displayIncoming; } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 53a8277ab..423babefc 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -90,11 +90,12 @@ abstract class DashboardViewModelBase with Store { value: () => transactionFilterStore.displayOutgoing, caption: S.current.outgoing, onChanged: transactionFilterStore.toggleOutgoing), - FilterItem( - value: () => transactionFilterStore.displaySilentPayments, - caption: S.current.silent_payments, - onChanged: transactionFilterStore.toggleSilentPayments, - ), + if (appStore.wallet!.type == WalletType.bitcoin) + FilterItem( + value: () => transactionFilterStore.displaySilentPayments, + caption: S.current.silent_payments, + onChanged: transactionFilterStore.toggleSilentPayments, + ), // FilterItem( // value: () => false, // caption: S.current.transactions_by_date, @@ -435,7 +436,10 @@ abstract class DashboardViewModelBase with Store { } @computed - bool get hasMweb => wallet.type == WalletType.litecoin && (Platform.isIOS || Platform.isAndroid) && !wallet.isHardwareWallet; + bool get hasMweb => + wallet.type == WalletType.litecoin && + (Platform.isIOS || Platform.isAndroid) && + !wallet.isHardwareWallet; @computed bool get showMwebCard => hasMweb && settingsStore.mwebCardDisplay && !mwebEnabled; From 4916d1f4576e72542a57fcc8052e6a5038d25da2 Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 1 Nov 2024 19:54:17 +0200 Subject: [PATCH 42/73] fix the issue with updating sending outputs (#1785) --- lib/view_model/send/send_view_model.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 24e83f364..1403eb109 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -400,7 +400,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor final updatedOutputs = bitcoin!.updateOutputs(pendingTransaction!, outputs); if (outputs.length == updatedOutputs.length) { - outputs = ObservableList.of(updatedOutputs); + outputs.clear(); + outputs.addAll(updatedOutputs); } } From 72241d49114afeb61a8551b53b9853fd695884af Mon Sep 17 00:00:00 2001 From: rottenwheel <92872541+rottenwheel@users.noreply.github.com> Date: Fri, 1 Nov 2024 21:57:17 +0000 Subject: [PATCH 43/73] spanish: tidy up and reword a lot of ES translations; catch missed unstralated strings (#1788) --- res/values/strings_es.arb | 312 +++++++++++++++++++------------------- 1 file changed, 156 insertions(+), 156 deletions(-) diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 449557671..25c9f95c1 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -1,5 +1,5 @@ { - "about_cake_pay": "Cake Pay le permite comprar fácilmente tarjetas de regalo con activos virtuales, gastables instantáneamente en más de 150 000 comerciantes en los Estados Unidos.", + "about_cake_pay": "Cake Pay te permite comprar fácilmente tarjetas de regalo con activos virtuales, gastables instantáneamente en más de 150,000 comerciantes en los Estados Unidos.", "account": "Cuenta", "accounts": "Cuentas", "accounts_subaddresses": "Cuentas y subdirecciones", @@ -21,13 +21,13 @@ "add_token_disclaimer_check": "He confirmado la dirección del contrato del token y la información utilizando una fuente confiable. Agregar información maliciosa o incorrecta puede resultar en una pérdida de fondos.", "add_token_warning": "No edite ni agregue tokens según las instrucciones de los estafadores.\n¡Confirme siempre las direcciones de los tokens con fuentes acreditadas!", "add_value": "Añadir valor", - "address": "DIRECCIÓN", + "address": "Dirección", "address_book": "Libreta de direcciones", "address_book_menu": "Libreta de direcciones", "address_detected": "Dirección detectada", "address_from_domain": "Esta dirección es de ${domain} en Unstoppable Domains", "address_from_yat": "Esta dirección es de ${emoji} en Yat", - "address_label": "Address label", + "address_label": "Etiqueta de dirección", "address_remove_contact": "Remover contacto", "address_remove_content": "¿Estás seguro de que quieres eliminar el contacto seleccionado?", "addresses": "Direcciones", @@ -37,12 +37,12 @@ "agree_and_continue": "Aceptar y continuar", "agree_to": "Al crear una cuenta, aceptas ", "alert_notice": "Aviso", - "all": "TODOS", + "all": "Todos", "all_trades": "Todos los oficios", "all_transactions": "Todas las transacciones", "alphabetical": "Alfabético", "already_have_account": "¿Ya tienes una cuenta?", - "always": "siempre", + "always": "Siempre", "amount": "Cantidad: ", "amount_is_below_minimum_limit": "Su saldo después de las tarifas sería menor que la cantidad mínima necesaria para el intercambio (${min})", "amount_is_estimate": "El monto recibido es un estimado", @@ -54,17 +54,17 @@ "arrive_in_this_address": "${currency} ${tag}llegará a esta dirección", "ascending": "Ascendente", "ask_each_time": "Pregunta cada vez", - "auth_store_ban_timeout": "prohibición de tiempo de espera", + "auth_store_ban_timeout": "Prohibición de tiempo de espera", "auth_store_banned_for": "Prohibido para ", "auth_store_banned_minutes": " minutos", "auth_store_incorrect_password": "Contraseña PIN", "authenticated": "Autenticados", "authentication": "Autenticación", - "auto_generate_addresses": "Auto Generar direcciones", + "auto_generate_addresses": "Auto-generar nuevas direcciones", "auto_generate_subaddresses": "Generar subdirecciones automáticamente", "automatic": "Automático", "available_balance": "Balance disponible", - "available_balance_description": "Su saldo disponible es la cantidad de fondos que puede gastar. Los fondos que se muestran aquí se pueden gastar inmediatamente.", + "available_balance_description": "Tu saldo disponible es la cantidad de fondos que puedes gastar. Los fondos que se muestran aquí, se pueden gastar inmediatamente.", "avg_savings": "Ahorro promedio", "awaitDAppProcessing": "Espere a que la dApp termine de procesarse.", "awaiting_payment_confirmation": "Esperando confirmación de pago", @@ -79,7 +79,7 @@ "biometric_auth_reason": "Escanee su huella digital para autenticar", "bitcoin_dark_theme": "Tema oscuro de Bitcoin", "bitcoin_light_theme": "Tema claro de Bitcoin", - "bitcoin_payments_require_1_confirmation": "Los pagos de Bitcoin requieren 1 confirmación, que puede demorar 20 minutos o más. ¡Gracias por su paciencia! Se le enviará un correo electrónico cuando se confirme el pago.", + "bitcoin_payments_require_1_confirmation": "Los pagos de Bitcoin requieren 1 confirmación, que puede demorar 20 minutos o más. ¡Gracias por tu paciencia! Se te enviará un correo electrónico cuando se confirme el pago.", "block_remaining": "1 bloqueo restante", "Blocks_remaining": "${status} Bloques restantes", "bluetooth": "Bluetooth", @@ -94,25 +94,25 @@ "by_cake_pay": "por Cake Pay", "cake_2fa_preset": "Pastel 2FA preestablecido", "cake_dark_theme": "Tema oscuro", - "cake_pay_account_note": "Regístrese con solo una dirección de correo electrónico para ver y comprar tarjetas. ¡Algunas incluso están disponibles con descuento!", - "cake_pay_learn_more": "¡Compre y canjee tarjetas de regalo al instante en la aplicación!\nDeslice el dedo de izquierda a derecha para obtener más información.", - "cake_pay_save_order": "La tarjeta debe enviarse a su correo electrónico dentro de 1 día hábil \n Guardar su ID de pedido:", - "cake_pay_subtitle": "Compre tarjetas prepagas y tarjetas de regalo en todo el mundo", - "cake_pay_web_cards_subtitle": "Compre tarjetas de prepago y tarjetas de regalo en todo el mundo", + "cake_pay_account_note": "Regístrate con solo una dirección de correo electrónico para ver y comprar tarjetas. ¡Algunas incluso están disponibles con descuento!", + "cake_pay_learn_more": "¡Compra y canjea tarjetas de regalo al instante en la aplicación!\nDesliza el dedo de izquierda a derecha para obtener más información.", + "cake_pay_save_order": "La tarjeta debe enviarse a tu correo electrónico dentro de 1 día hábil \n Guardar su ID de pedido:", + "cake_pay_subtitle": "Compra tarjetas prepagadas y tarjetas de regalo en todo el mundo", + "cake_pay_web_cards_subtitle": "Compra tarjetas de prepago y tarjetas de regalo en todo el mundo", "cake_pay_web_cards_title": "Tarjetas Web Cake Pay", "cake_wallet": "Cake Wallet", "cakepay_prepaid_card": "Tarjeta de Débito Prepago CakePay", - "camera_consent": "Su cámara será utilizada para capturar una imagen con fines de identificación por ${provider}. Consulte su Política de privacidad para obtener más detalles.", - "camera_permission_is_required": "Se requiere permiso de la cámara.\nHabilítelo desde la configuración de la aplicación.", + "camera_consent": "Su cámara será utilizada para capturar una imagen con fines de identificación por ${provider}. Consulta tu Política de privacidad para obtener más detalles.", + "camera_permission_is_required": "Se requiere permiso de la cámara.\nHabilítalo desde la configuración de la aplicación.", "cancel": "Cancelar", "card_address": "Dirección:", "cardholder_agreement": "Acuerdo del titular de la tarjeta", "cards": "Cartas", "chains": "Cadenas", "change": "Cambio", - "change_backup_password_alert": "Sus archivos de respaldo anteriores no estarán disponibles para importar con la nueva contraseña de respaldo. La nueva contraseña de respaldo se utilizará solo para los nuevos archivos de respaldo. ¿Está seguro de que desea cambiar la contraseña de respaldo?", + "change_backup_password_alert": "Tus archivos de respaldo anteriores no estarán disponibles para importar con la nueva contraseña de respaldo. La nueva contraseña de respaldo se utilizará solo para los nuevos archivos de respaldo. ¿Está seguro de que desea cambiar la contraseña de respaldo?", "change_currency": "Cambiar moneda", - "change_current_node": "¿Está seguro de cambiar el nodo actual a ${node}?", + "change_current_node": "¿Estás seguro de cambiar el nodo actual a ${node}?", "change_current_node_title": "Cambiar el nodo actual", "change_exchange_provider": "Cambiar proveedor de intercambio", "change_language": "Cambiar idioma", @@ -125,38 +125,38 @@ "change_wallet_alert_title": "Cambiar billetera actual", "choose_account": "Elegir cuenta", "choose_address": "\n\nPor favor elija la dirección:", - "choose_card_value": "Elija un valor de tarjeta", - "choose_derivation": "Elija la derivación de la billetera", - "choose_from_available_options": "Elija entre las opciones disponibles:", + "choose_card_value": "Elige un valor de tarjeta", + "choose_derivation": "Elige la derivación de la billetera", + "choose_from_available_options": "Elige entre las opciones disponibles:", "choose_one": "Elige uno", - "choose_relay": "Por favor elija un relé para usar", - "choose_wallet_currency": "Por favor, elija la moneda de la billetera:", - "choose_wallet_group": "Elija el grupo de billetera", + "choose_relay": "Por favor elige un relay para usar", + "choose_wallet_currency": "Por favor, elige la moneda de la billetera:", + "choose_wallet_group": "Elige el grupo de billetera", "clear": "Claro", "clearnet_link": "enlace Clearnet", "close": "Cerca", "coin_control": "Control de monedas (opcional)", - "cold_or_recover_wallet": "Agregue una billetera fría o recupere una billetera de papel", + "cold_or_recover_wallet": "Agrega una billetera fría o recupera una billetera de papel", "color_theme": "Tema de color", "commit_transaction_amount_fee": "Confirmar transacción\nCantidad: ${amount}\nCuota: ${fee}", "confirm": "Confirmar", - "confirm_delete_template": "Esta acción eliminará esta plantilla. ¿Desea continuar?", - "confirm_delete_wallet": "Esta acción eliminará esta billetera. ¿Desea continuar?", + "confirm_delete_template": "Esta acción eliminará esta plantilla. ¿Deseas continuar?", + "confirm_delete_wallet": "Esta acción eliminará esta billetera. ¿Deseas continuar?", "confirm_fee_deduction": "Confirmar la deducción de la tarifa", - "confirm_fee_deduction_content": "¿Acepta deducir la tarifa de la producción?", + "confirm_fee_deduction_content": "¿Aceptas deducir la tarifa de la producción?", "confirm_passphrase": "Confirmar la contraseña", "confirm_sending": "Confirmar envío", - "confirm_silent_payments_switch_node": "Su nodo actual no admite pagos silenciosos \\ nCake cambiará a un nodo compatible, solo para escanear", + "confirm_silent_payments_switch_node": "Tu nodo actual no admite pagos silenciosos \\ nCake cambiará a un nodo compatible, solo para escanear", "confirmations": "Confirmaciones", "confirmed": "Saldo confirmado", "confirmed_tx": "Confirmado", "congratulations": "Felicidades!", "connect_an_existing_yat": "Conectar un Yat existente", "connect_yats": "Conectar Yats", - "connect_your_hardware_wallet": "Conecte su billetera de hardware con Bluetooth o USB", - "connect_your_hardware_wallet_ios": "Conecte su billetera de hardware con Bluetooth", + "connect_your_hardware_wallet": "Conecta tu billetera de hardware con Bluetooth o USB", + "connect_your_hardware_wallet_ios": "Conecta tu billetera de hardware con Bluetooth", "connection_sync": "Conexión y sincronización", - "connectWalletPrompt": "Conecte su billetera con WalletConnect para realizar transacciones", + "connectWalletPrompt": "Conecte tu billetera con WalletConnect para realizar transacciones", "contact": "Contacto", "contact_list_contacts": "Contactos", "contact_list_wallets": "Mis billeteras", @@ -187,7 +187,7 @@ "custom_drag": "Custom (mantenía y arrastre)", "custom_redeem_amount": "Cantidad de canje personalizada", "custom_value": "Valor personalizado", - "dark_theme": "Oscura", + "dark_theme": "Oscuro", "debit_card": "Tarjeta de Débito", "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", @@ -197,21 +197,21 @@ "delete": "Borrar", "delete_account": "Eliminar cuenta", "delete_wallet": "Eliminar billetera", - "delete_wallet_confirm_message": "¿Está seguro de que desea eliminar la billetera ${wallet_name}?", - "deleteConnectionConfirmationPrompt": "¿Está seguro de que desea eliminar la conexión a", + "delete_wallet_confirm_message": "¿Estás seguro de que deseas eliminar la billetera ${wallet_name}?", + "deleteConnectionConfirmationPrompt": "¿Estás seguro de que deseas eliminar la conexión a", "denominations": "Denominaciones", "derivationpath": "Ruta de derivación", "descending": "Descendente", "description": "Descripción", "destination_tag": "Etiqueta de destino:", - "dfx_option_description": "Compre criptografía con EUR y CHF. Para clientes minoristas y corporativos en Europa", + "dfx_option_description": "Compre cripto con EUR y CHF. Para clientes minoristas y corporativos en Europa", "didnt_get_code": "¿No recibiste el código?", "digit_pin": "-dígito PIN", "digital_and_physical_card": " tarjeta de débito prepago digital y física", "disable": "Desactivar", "disable_bulletin": "Desactivar el boletín de estado del servicio", "disable_buy": "Desactivar acción de compra", - "disable_cake_2fa": "Desactivar pastel 2FA", + "disable_cake_2fa": "Desactivar 2FA", "disable_exchange": "Deshabilitar 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", @@ -224,7 +224,7 @@ "displayable": "Visualizable", "do_not_have_enough_gas_asset": "No tienes suficiente ${currency} para realizar una transacción con las condiciones actuales de la red blockchain. Necesita más ${currency} para pagar las tarifas de la red blockchain, incluso si envía un activo diferente.", "do_not_send": "no enviar", - "do_not_share_warning_text": "No comparta estos con nadie más, incluido el soporte.\n\n¡Sus fondos pueden ser y serán robados!", + "do_not_share_warning_text": "No compartas estos con nadie más, incluido el soporte.\n\n¡Tus fondos pueden ser y serán robados!", "do_not_show_me": "no me muestres esto otra vez", "domain_looks_up": "Búsquedas de dominio", "donation_link_details": "Detalles del enlace de donación", @@ -238,21 +238,21 @@ "enable": "Permitir", "enable_mempool_api": "API de Mempool para tarifas y fechas precisas", "enable_replace_by_fee": "Habilitar reemplazar por tarea", - "enable_silent_payments_scanning": "Comience a escanear pagos silenciosos, hasta que se alcance la punta", + "enable_silent_payments_scanning": "Comienza a escanear pagos silenciosos, hasta que se alcance la altura actual", "enabled": "Activado", - "enter_amount": "Ingrese la cantidad", - "enter_backup_password": "Ingrese la contraseña de respaldo aquí", + "enter_amount": "Ingresa la cantidad", + "enter_backup_password": "Ingresa la contraseña de respaldo aquí", "enter_code": "Ingresar código", - "enter_seed_phrase": "Ingrese su frase de semillas", - "enter_totp_code": "Ingrese el código TOTP.", - "enter_wallet_password": "Ingrese la contraseña de la billetera", + "enter_seed_phrase": "Ingresa su frase de semillas", + "enter_totp_code": "Ingresa el código TOTP.", + "enter_wallet_password": "Ingresa la contraseña de la billetera", "enter_your_note": "Ingresa tu nota…", "enter_your_pin": "Introduce tu PIN", - "enter_your_pin_again": "Ingrese su PIN nuevamente", - "enterTokenID": "Ingrese el ID del token", - "enterWalletConnectURI": "Ingrese el URI de WalletConnect", + "enter_your_pin_again": "Ingresa su PIN nuevamente", + "enterTokenID": "Ingresa el ID del token", + "enterWalletConnectURI": "Ingresa el URI de WalletConnect", "error": "Error", - "error_dialog_content": "Vaya, tenemos un error.\n\nEnvíe el informe de bloqueo a nuestro equipo de soporte para mejorar la aplicación.", + "error_dialog_content": "Vaya, tenemos un error.\n\nEnvía el informe de error a nuestro equipo de soporte para mejorar la aplicación.", "error_text_account_name": "El nombre de la cuenta solo puede contener letras, números \ny debe tener entre 1 y 15 caracteres de longitud", "error_text_address": "La dirección de la billetera debe corresponder al tipo \nde criptomoneda", "error_text_amount": "La cantidad solo puede contener números", @@ -304,14 +304,14 @@ "fee_rate": "Tarifa", "fetching": "Cargando", "fiat_api": "Fiat API", - "fiat_balance": "Equilibrio Fiat", + "fiat_balance": "Balance fiat", "field_required": "Este campo es obligatorio", - "fill_code": "Por favor complete el código de verificación proporcionado a su correo electrónico", + "fill_code": "Por favor completa el código de verificación proporcionado en tu correo electrónico", "filter_by": "Filtrado por", "first_wallet_text": "Impresionante billetera para Monero, Bitcoin, Ethereum, Litecoin, y Haven", "fixed_pair_not_supported": "Este par fijo no es compatible con los servicios de intercambio seleccionados", "fixed_rate": "Tipo de interés fijo", - "fixed_rate_alert": "Podrá ingresar la cantidad recibida cuando el modo de tarifa fija esté marcado. ¿Quieres cambiar al modo de tarifa fija?", + "fixed_rate_alert": "Podrás ingresar la cantidad recibida cuando el modo de tarifa fija esté marcado. ¿Quieres cambiar al modo de tarifa fija?", "forgot_password": "Olvidé mi contraseña", "freeze": "Congelar", "frequently_asked_questions": "Preguntas frecuentes", @@ -333,7 +333,7 @@ "gross_balance": "Saldo bruto", "group_by_type": "Grupo por tipo", "haven_app": "Haven by Cake Wallet", - "haven_app_wallet_text": "Awesome wallet for Haven", + "haven_app_wallet_text": "Increíble billetera para Haven", "help": "ayuda", "hidden_balance": "Balance oculto", "hide_details": "Ocultar detalles", @@ -349,9 +349,9 @@ "incoming": "Entrante", "incorrect_seed": "El texto ingresado no es válido.", "inputs": "Entradas", - "insufficient_lamport_for_tx": "No tiene suficiente SOL para cubrir la transacción y su tarifa de transacción. Por favor, agregue más SOL a su billetera o reduzca la cantidad de sol que está enviando.", - "insufficient_lamports": "No tiene suficiente SOL para cubrir la transacción y su tarifa de transacción. Necesita al menos ${solValueNeeded} sol. Por favor, agregue más sol a su billetera o reduzca la cantidad de sol que está enviando", - "insufficientFundsForRentError": "No tiene suficiente SOL para cubrir la tarifa de transacción y alquilar para la cuenta. Por favor, agregue más sol a su billetera o reduzca la cantidad de sol que está enviando", + "insufficient_lamport_for_tx": "No tienes suficiente SOL para cubrir la transacción y su tarifa de transacción. Por favor, agrega más SOL a su billetera o reduce la cantidad de sol que está enviando.", + "insufficient_lamports": "No tienes suficiente SOL para cubrir la transacción y su tarifa de transacción. Necesita al menos ${solValueNeeded} sol. Por favor, agrega más sol a su billetera o reduzca la cantidad de sol que está enviando", + "insufficientFundsForRentError": "No tienes suficiente SOL para cubrir la tarifa de transacción y alquilar para la cuenta. Por favor, agrega más sol a su billetera o reduce la cantidad de sol que está enviando", "introducing_cake_pay": "¡Presentamos Cake Pay!", "invalid_input": "Entrada inválida", "invalid_password": "Contraseña invalida", @@ -359,12 +359,12 @@ "is_percentage": "es", "last_30_days": "Últimos 30 días", "learn_more": "Aprende más", - "ledger_connection_error": "No se pudo conectar con su libro mayor. Inténtalo de nuevo.", - "ledger_error_device_locked": "El libro mayor está bloqueado", + "ledger_connection_error": "No se pudo conectar con ledger. Inténtalo de nuevo.", + "ledger_error_device_locked": "Ledger está bloqueado", "ledger_error_tx_rejected_by_user": "Transacción rechazada en el dispositivo", - "ledger_error_wrong_app": "Por favor, asegúrese de abrir la aplicación correcta en su libro mayor.", - "ledger_please_enable_bluetooth": "Habilite Bluetooth para detectar su libro mayor", - "light_theme": "Ligera", + "ledger_error_wrong_app": "Por favor, asegúrate de abrir la aplicación correcta en su libro mayor.", + "ledger_please_enable_bluetooth": "Habilita tu Bluetooth para detectar tu ledger", + "light_theme": "Ligero", "litecoin_enable_mweb_sync": "Habilitar el escaneo mweb", "litecoin_mweb": "Mweb", "litecoin_mweb_always_scan": "Establecer mweb siempre escaneo", @@ -401,9 +401,9 @@ "min_value": "Min: ${value} ${currency}", "minutes_to_pin_code": "${minute} minutos", "mm": "mm", - "modify_2fa": "Modificar torta 2FA", - "monero_com": "Monero.com by Cake Wallet", - "monero_com_wallet_text": "Awesome wallet for Monero", + "modify_2fa": "Modificar 2FA", + "monero_com": "Monero.com por Cake Wallet", + "monero_com_wallet_text": "Increíble billetera para Monero", "monero_dark_theme": "Tema oscuro de Monero", "monero_light_theme": "Tema ligero de Monero", "moonpay_alert_text": "El valor de la cantidad debe ser mayor o igual a ${minAmount} ${fiatCurrency}", @@ -412,12 +412,12 @@ "mweb_unconfirmed": "Mweb no confirmado", "name": "Nombre", "nano_current_rep": "Representante actual", - "nano_gpt_thanks_message": "¡Gracias por usar nanogpt! ¡Recuerde regresar al navegador después de que se complete su transacción!", + "nano_gpt_thanks_message": "¡Gracias por usar nanogpt! ¡Recuerda regresar al navegador después de que se complete su transacción!", "nano_pick_new_rep": "Elija un nuevo representante", "nanogpt_subtitle": "Todos los modelos más nuevos (GPT-4, Claude). \\nSin suscripción, pague con cripto.", "narrow": "Angosto", - "new_first_wallet_text": "Mantenga fácilmente su criptomoneda segura", - "new_node_testing": "Prueba de nuevos nodos", + "new_first_wallet_text": "Mantén fácilmente tu criptomoneda segura", + "new_node_testing": "Prueba nuevos nodos", "new_subaddress_create": "Crear", "new_subaddress_label_name": "Nombre de etiqueta", "new_subaddress_title": "Nueva direccion", @@ -426,10 +426,10 @@ "newConnection": "Nueva conexión", "no_cards_found": "No se encuentran cartas", "no_id_needed": "¡No se necesita identificación!", - "no_id_required": "No se requiere identificación. Recargue y gaste en cualquier lugar", - "no_relay_on_domain": "No hay una retransmisión para el dominio del usuario o la retransmisión no está disponible. Elija un relé para usar.", - "no_relays": "Sin relevos", - "no_relays_message": "Encontramos un registro Nostr NIP-05 para este usuario, pero no contiene ningún relé. Indique al destinatario que agregue retransmisiones a su registro Nostr.", + "no_id_required": "No se requiere identificación. Recarga y gaste en cualquier lugar", + "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.", "node_address": "Dirección de nodo", "node_connection_failed": "La conexión falló", "node_connection_successful": "La conexión fue exitosa", @@ -449,15 +449,15 @@ "offline": "fuera de línea", "ok": "OK", "old_fee": "Tarifa antigua", - "onion_link": "Enlace de cebolla", + "onion_link": "Enlace de cebolla (Tor)", "online": "En línea", - "onramper_option_description": "Compre rápidamente cripto con muchos métodos de pago. Disponible en la mayoría de los países. Los diferenciales y las tarifas varían.", + "onramper_option_description": "Compra rápidamente cripto con muchos métodos de pago. Disponible en la mayoría de los países. Los diferenciales y las tarifas varían.", "open_gift_card": "Abrir tarjeta de regalo", "optional_description": "Descripción opcional", "optional_email_hint": "Correo electrónico de notificación del beneficiario opcional", "optional_name": "Nombre del destinatario opcional", "optionally_order_card": "Opcionalmente pide una tarjeta física.", - "orbot_running_alert": "Asegúrese de que Orbot se esté ejecutando antes de conectarse a este nodo.", + "orbot_running_alert": "Asegúrate de que Orbot se esté ejecutando antes de conectarte a este nodo.", "order_by": "Ordenar", "order_id": "Identificación del pedido", "order_physical_card": "Pedir tarjeta física", @@ -466,7 +466,7 @@ "outdated_electrum_wallet_receive_warning": "Si esta billetera tiene una semilla de 12 palabras y se creó en Cake, NO deposite Bitcoin en esta billetera. Cualquier BTC transferido a esta billetera se puede perder. Cree una nueva billetera de 24 palabras (toque el menú en la parte superior derecha, seleccione Monederos, elija Crear nueva billetera, luego seleccione Bitcoin) e INMEDIATAMENTE mueva su BTC allí. Las nuevas carteras BTC (24 palabras) de Cake son seguras", "outgoing": "Saliente", "outputs": "Salidas", - "overwrite_amount": "Overwrite amount", + "overwrite_amount": "Sobreescribir monto", "pairingInvalidEvent": "Evento de emparejamiento no válido", "passphrase": "Passfrase (opcional)", "passphrases_doesnt_match": "Las frases de contrato no coinciden, intente nuevamente", @@ -482,18 +482,18 @@ "pin_is_incorrect": "PIN es incorrecto", "pin_number": "Número PIN", "placeholder_contacts": "Tus contactos se mostrarán aquí", - "placeholder_transactions": "Sus transacciones se mostrarán aquí", - "please_fill_totp": "Complete el código de 8 dígitos presente en su otro dispositivo", - "please_make_selection": "Seleccione a continuación para crear o recuperar su billetera.", - "please_reference_document": "Consulte los documentos a continuación para obtener más información.", - "please_select": "Por favor seleccione:", - "please_select_backup_file": "Seleccione el archivo de respaldo e ingrese la contraseña de respaldo.", + "placeholder_transactions": "Tus transacciones se mostrarán aquí", + "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.", + "please_select": "Por favor selecciona:", + "please_select_backup_file": "Selecciona el archivo de respaldo e ingrese la contraseña de respaldo.", "please_try_to_connect_to_another_node": "Intenta conectarte a otro nodo", - "please_wait": "Espere por favor", + "please_wait": "Espera por favor", "polygonscan_history": "Historial de PolygonScan", - "powered_by": "Energizado por ${title}", + "powered_by": "Posible gracias a ${title}", "pre_seed_button_text": "Entiendo. Muéstrame mi semilla", - "pre_seed_description": "En la página siguiente verá una serie de ${words} palabras. Esta es su semilla única y privada y es la ÚNICA forma de recuperar su billetera en caso de pérdida o mal funcionamiento. Es SU responsabilidad escribirlo y guardarlo en un lugar seguro fuera de la aplicación Cake Wallet.", + "pre_seed_description": "En la página siguiente verás una serie de ${words} palabras. Esta es su semilla única y privada y es la ÚNICA forma de recuperar tu billetera en caso de pérdida o mal funcionamiento. Es TU responsabilidad escribirla y guardarla en un lugar seguro fuera de la aplicación Cake Wallet.", "pre_seed_title": "IMPORTANTE", "prepaid_cards": "Tajetas prepagadas", "prevent_screenshots": "Evitar capturas de pantalla y grabación de pantalla", @@ -502,17 +502,17 @@ "privacy_settings": "Configuración de privacidad", "private_key": "Clave privada", "proceed_after_one_minute": "Si la pantalla no continúa después de 1 minuto, revisa tu correo electrónico.", - "proceed_on_device": "Continúe con su dispositivo", - "proceed_on_device_description": "Siga las instrucciones solicitadas en su billetera de hardware", + "proceed_on_device": "Continúa con tu dispositivo", + "proceed_on_device_description": "Sigue las instrucciones solicitadas en su billetera de hardware", "profile": "Perfil", "provider_error": "${provider} error", "public_key": "Clave pública", "purchase_gift_card": "Comprar tarjeta de regalo", "purple_dark_theme": "Tema morado oscuro", "qr_fullscreen": "Toque para abrir el código QR en pantalla completa", - "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", + "qr_payment_amount": "Este código QR contiene un monto de pago. ¿Quieres sobreescribirlo?", "quantity": "Cantidad", - "question_to_disable_2fa": "¿Está seguro de que desea deshabilitar Cake 2FA? Ya no se necesitará un código 2FA para acceder a la billetera y a ciertas funciones.", + "question_to_disable_2fa": "¿Estás seguro de que desea deshabilitar Cake 2FA? Ya no se necesitará un código 2FA para acceder a la billetera y a ciertas funciones.", "receivable_balance": "Saldo de cuentas por cobrar", "receive": "Recibir", "receive_amount": "Cantidad", @@ -529,12 +529,12 @@ "remaining": "restante", "remove": "Retirar", "remove_node": "Eliminar nodo", - "remove_node_message": "¿Está seguro de que desea eliminar el nodo seleccionado?", - "rename": "Rebautizar", + "remove_node_message": "¿Estás seguro de que desea eliminar el nodo seleccionado?", + "rename": "Renombrar", "rep_warning": "Advertencia representativa", - "rep_warning_sub": "Su representante no parece estar en buena posición. Toque aquí para seleccionar uno nuevo", - "repeat_wallet_password": "Repita la contraseña de billetera", - "repeated_password_is_incorrect": "La contraseña repetida es incorrecta. Repita la contraseña de la billetera nuevamente.", + "rep_warning_sub": "Tu representante no parece estar en buena posición. Toca aquí para seleccionar uno nuevo", + "repeat_wallet_password": "Repite la contraseña de billetera", + "repeated_password_is_incorrect": "La contraseña repetida es incorrecta. Repite la contraseña de la billetera nuevamente.", "require_for_adding_contacts": "Requerido para agregar contactos", "require_for_all_security_and_backup_settings": "Requerido para todas las configuraciones de seguridad y copia de seguridad", "require_for_assessing_wallet": "Requerido para acceder a la billetera", @@ -566,17 +566,17 @@ "restore_recover": "Recuperar", "restore_restore_wallet": "Recuperar Cartera", "restore_seed_keys_restore": "Restauración de semillas / llaves", - "restore_spend_key_private": "Spend clave (privado)", + "restore_spend_key_private": "Llave de gasto (privada)", "restore_title_from_backup": "Restaurar desde un archivo de respaldo", - "restore_title_from_hardware_wallet": "Restaurar desde la billetera de hardware", - "restore_title_from_keys": "De las claves", - "restore_title_from_seed": "De la semilla", - "restore_title_from_seed_keys": "Restaurar desde semilla/claves", - "restore_view_key_private": "View clave (privado)", + "restore_title_from_hardware_wallet": "Restaurar desde una cartera fría", + "restore_title_from_keys": "Usando las claves", + "restore_title_from_seed": "Usando la semilla", + "restore_title_from_seed_keys": "Restaurar usando semilla/claves", + "restore_view_key_private": "Llave de vista (privado)", "restore_wallet": "Restaurar billetera", "restore_wallet_name": "Nombre de la billetera", "restore_wallet_restore_description": "Restaurar billetera", - "robinhood_option_description": "Compre y transfiera instantáneamente utilizando su tarjeta de débito, cuenta bancaria o saldo de Robinhood. Solo EE. UU.", + "robinhood_option_description": "Compra y transfiere instantáneamente utilizando su tarjeta de débito, cuenta bancaria o saldo de Robinhood. Solo EE. UU.", "router_no_route": "No hay ruta definida para ${name}", "save": "Salvar", "save_backup_password": "Asegúrese de haber guardado su contraseña de respaldo. No podrá importar sus archivos de respaldo sin él.", @@ -585,7 +585,7 @@ "saved_the_trade_id": "He salvado comercial ID", "scan_one_block": "Escanear un bloque", "scan_qr_code": "Escanear código QR", - "scan_qr_code_to_get_address": "Escanee el código QR para obtener la dirección", + "scan_qr_code_to_get_address": "Escanea el código QR para obtener la dirección", "scan_qr_on_device": "Escanea este código QR en otro dispositivo", "search": "Búsqueda", "search_add_token": "Buscar/Agregar token", @@ -612,25 +612,25 @@ "seed_language_german": "Alemán", "seed_language_italian": "Italiana/Italiano", "seed_language_japanese": "Japonés", - "seed_language_korean": "coreano", + "seed_language_korean": "Coreano", "seed_language_next": "Próximo", "seed_language_portuguese": "Portugués", "seed_language_russian": "Ruso", "seed_language_spanish": "Español", "seed_phrase_length": "Longitud de la frase inicial", - "seed_reminder": "Anótelos en caso de que pierda o borre su teléfono", + "seed_reminder": "Anótalos en caso de que pierdas o borres tu aplicación", "seed_share": "Compartir semillas", "seed_title": "Semilla", - "seedtype": "Type de semillas", - "seedtype_alert_content": "Compartir semillas con otras billeteras solo es posible con Bip39 Seed Type.", - "seedtype_alert_title": "Alerta de type de semillas", - "seedtype_legacy": "Legado (25 palabras)", - "seedtype_polyseed": "Polieta (16 palabras)", + "seedtype": "Tipos de semillas", + "seedtype_alert_content": "Compartir semillas con otras billeteras solo es posible con semillas bip39 - un tipo específico de semilla.", + "seedtype_alert_title": "Alerta de tipo de semillas", + "seedtype_legacy": "Semilla clásica-legacy (25 palabras)", + "seedtype_polyseed": "Poli-semilla (16 palabras)", "seedtype_wownero": "Wownero (14 palabras)", "select_backup_file": "Seleccionar archivo de respaldo", - "select_buy_provider_notice": "Seleccione un proveedor de compra arriba. Puede omitir esta pantalla configurando su proveedor de compra predeterminado en la configuración de la aplicación.", - "select_destination": "Seleccione el destino del archivo de copia de seguridad.", - "select_sell_provider_notice": "Seleccione un proveedor de venta arriba. Puede omitir esta pantalla configurando su proveedor de venta predeterminado en la configuración de la aplicación.", + "select_buy_provider_notice": "Selecciona un proveedor de compra arriba. Puede omitir esta pantalla configurando su proveedor de compra predeterminado en la configuración de la aplicación.", + "select_destination": "Selecciona el destino del archivo de copia de seguridad.", + "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.", "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", @@ -676,16 +676,16 @@ "settings_only_transactions": "Solo transacciones", "settings_personal": "Personal", "settings_save_recipient_address": "Guardar dirección del destinatario", - "settings_support": "Apoyo", + "settings_support": "Ayuda", "settings_terms_and_conditions": "Términos y Condiciones", "settings_title": "Configuraciones", "settings_trades": "Comercia", "settings_transactions": "Transacciones", "settings_wallets": "Carteras", - "setup_2fa": "Configurar pastel 2FA", - "setup_2fa_text": "Cake 2FA funciona utilizando TOTP como segundo factor de autenticación.\n\nEl TOTP de Cake 2FA requiere SHA-512 y soporte de 8 dígitos; esto proporciona una mayor seguridad. Puede encontrar más información y aplicaciones compatibles en la guía.", + "setup_2fa": "Configurar 2FA", + "setup_2fa_text": "Cake 2FA funciona utilizando TOTP como segundo factor de autenticación.\n\nEl TOTP de Cake 2FA requiere SHA-512 y soporte de 8 dígitos; esto proporciona una mayor seguridad. Puedes encontrar más información y aplicaciones compatibles en la guía.", "setup_pin": "PIN de configuración", - "setup_successful": "Su PIN se ha configurado correctamente!", + "setup_successful": "Tu PIN se ha configurado correctamente!", "setup_totp_recommended": "Configurar TOTP", "setup_warning_2fa_text": "Deberá restaurar su billetera a partir de la semilla mnemotécnica.\n\nEl soporte de Cake no podrá ayudarlo si pierde el acceso a su 2FA o a sus semillas mnemotécnicas.\nCake 2FA es una segunda autenticación para ciertas acciones en la billetera. Antes de usar Cake 2FA, recomendamos leer la guía.NO es tan seguro como el almacenamiento en frío.\n\nSi pierde el acceso a su aplicación 2FA o a sus claves TOTP, perderá el acceso a esta billetera. ", "setup_your_debit_card": "Configura tu tarjeta de débito", @@ -704,23 +704,23 @@ "signature": "Firma", "signature_invalid_error": "La firma no es válida para el mensaje dado", "signTransaction": "Firmar transacción", - "signup_for_card_accept_terms": "Regístrese para obtener la tarjeta y acepte los términos.", + "signup_for_card_accept_terms": "Regístrate para obtener la tarjeta y acepte los términos.", "silent_payment": "Pago silencioso", "silent_payments": "Pagos silenciosos", "silent_payments_always_scan": "Establecer pagos silenciosos siempre escaneando", "silent_payments_disclaimer": "Las nuevas direcciones no son nuevas identidades. Es una reutilización de una identidad existente con una etiqueta diferente.", "silent_payments_display_card": "Mostrar tarjeta de pagos silenciosos", "silent_payments_scan_from_date": "Escanear desde la fecha", - "silent_payments_scan_from_date_or_blockheight": "Ingrese la altura del bloque que desea comenzar a escanear para pagos silenciosos entrantes, o use la fecha en su lugar. Puede elegir si la billetera continúa escaneando cada bloque, o verifica solo la altura especificada.", - "silent_payments_scan_from_height": "Escanear desde la altura del bloque", - "silent_payments_scanned_tip": "Escaneado hasta la punta! (${tip})", + "silent_payments_scan_from_date_or_blockheight": "Ingresa la altura de bloque que desea comenzar a escanear para pagos silenciosos entrantes, o usa la fecha en su lugar. Puedes elegir si la billetera continúa escaneando cada bloque, o verifica solo la altura especificada.", + "silent_payments_scan_from_height": "Escanear desde la altura de bloque específico", + "silent_payments_scanned_tip": "Escaneado hasta la altura actual! (${tip})", "silent_payments_scanning": "Escaneo de pagos silenciosos", "silent_payments_settings": "Configuración de pagos silenciosos", "single_seed_wallets_group": "Billeteras de semillas individuales", "slidable": "deslizable", "sort_by": "Ordenar por", - "spend_key_private": "Spend clave (privado)", - "spend_key_public": "Spend clave (público)", + "spend_key_private": "Llave de gasto (privada)", + "spend_key_public": "Llave de gasto (pública)", "status": "Estado: ", "string_default": "Por defecto", "subaddress_title": "Lista de subdirecciones", @@ -729,14 +729,14 @@ "successful": "Exitoso", "support_description_guides": "Documentación y apoyo para problemas comunes", "support_description_live_chat": "¡GRATIS y RÁPIDO! Los representantes de apoyo capacitado están disponibles para ayudar", - "support_description_other_links": "Únase a nuestras comunidades o comuníquese con nosotros nuestros socios a través de otros métodos", + "support_description_other_links": "Únete a nuestras comunidades o comunícate con nosotros nuestros socios a través de otros métodos", "support_title_guides": "Guías de billetera para pastel", - "support_title_live_chat": "Soporte vital", + "support_title_live_chat": "Soporte en tiempo real", "support_title_other_links": "Otros enlaces de soporte", - "sweeping_wallet": "Billetera de barrido", + "sweeping_wallet": "Barrer billetera (gastar todos los fondos disponibles)", "sweeping_wallet_alert": "Esto no debería llevar mucho tiempo. NO DEJES ESTA PANTALLA O SE PUEDEN PERDER LOS FONDOS BARRIDOS", - "switchToETHWallet": "Cambie a una billetera Ethereum e inténtelo nuevamente.", - "switchToEVMCompatibleWallet": "Cambie a una billetera compatible con EVM e inténtelo nuevamente (Ethereum, Polygon)", + "switchToETHWallet": "Cambia a una billetera Ethereum e inténtelo nuevamente.", + "switchToEVMCompatibleWallet": "Cambia a una billetera compatible con EVM e inténtelo nuevamente (Ethereum, Polygon)", "symbol": "Símbolo", "sync_all_wallets": "Sincronizar todas las billeteras", "sync_status_attempting_scan": "Intento de escaneo", @@ -759,7 +759,7 @@ "third_intro_content": "Los Yats también viven fuera de Cake Wallet. Cualquier dirección de billetera en la tierra se puede reemplazar con un Yat!", "third_intro_title": "Yat juega muy bien con otras", "thorchain_contract_address_not_supported": "Thorchain no admite enviar a una dirección de contrato", - "thorchain_taproot_address_not_supported": "El proveedor de Thorchain no admite las direcciones de Taproot. Cambie la dirección o seleccione un proveedor diferente.", + "thorchain_taproot_address_not_supported": "El proveedor de Thorchain no admite las direcciones de Taproot. Cambia la dirección o selecciona un proveedor diferente.", "time": "${minutes}m ${seconds}s", "tip": "Consejo:", "today": "Hoy", @@ -772,8 +772,8 @@ "tor_only": "solo Tor", "total": "Total", "total_saving": "Ahorro Total", - "totp_2fa_failure": "Código incorrecto. Intente con un código diferente o genere una nueva clave secreta. Use una aplicación 2FA compatible que admita códigos de 8 dígitos y SHA512.", - "totp_2fa_success": "¡Éxito! Cake 2FA habilitado para esta billetera. Recuerde guardar su semilla mnemotécnica en caso de que pierda el acceso a la billetera.", + "totp_2fa_failure": "Código incorrecto. Intente con un código diferente o genere una nueva clave secreta. Usa una aplicación 2FA compatible que admita códigos de 8 dígitos y SHA512.", + "totp_2fa_success": "¡Éxito! Cake 2FA habilitado para esta billetera. Recuerda guardar tu semilla mnemotécnica en caso de que pierdas el acceso a la billetera.", "totp_auth_url": "URL de autenticación TOTP", "totp_code": "Código TOTP", "totp_secret_code": "Código secreto TOTP", @@ -831,16 +831,16 @@ "trongrid_history": "Historia trongrid", "trusted": "de confianza", "tx_commit_exception_no_dust_on_change": "La transacción se rechaza con esta cantidad. Con estas monedas puede enviar ${min} sin cambios o ${max} que devuelve el cambio.", - "tx_commit_failed": "La confirmación de transacción falló. Póngase en contacto con el soporte.", - "tx_commit_failed_no_peers": "La transacción no se transmitió, intente nuevamente en un segundo más o menos", + "tx_commit_failed": "La confirmación de transacción falló. Ponte en contacto con el soporte.", + "tx_commit_failed_no_peers": "La transacción no se transmitió, intenta nuevamente en un segundo más o menos", "tx_invalid_input": "Está utilizando el tipo de entrada incorrecto para este tipo de pago", "tx_no_dust_exception": "La transacción se rechaza enviando una cantidad demasiado pequeña. Intente aumentar la cantidad.", - "tx_not_enough_inputs_exception": "No hay suficientes entradas disponibles. Seleccione más bajo control de monedas", + "tx_not_enough_inputs_exception": "No hay suficientes entradas disponibles. Selecciona más bajo control de monedas", "tx_rejected_bip68_final": "La transacción tiene entradas no confirmadas y no ha podido reemplazar por tarifa.", - "tx_rejected_dust_change": "Transacción rechazada por reglas de red, bajo cambio de cambio (polvo). Intente enviar todo o reducir la cantidad.", - "tx_rejected_dust_output": "Transacción rechazada por reglas de red, baja cantidad de salida (polvo). Aumente la cantidad.", + "tx_rejected_dust_change": "Transacción rechazada por reglas de red, bajo cambio de cambio (polvo). Intenta enviar todo o reducir la cantidad.", + "tx_rejected_dust_output": "Transacción rechazada por reglas de red, baja cantidad de salida (polvo). Aumenta la cantidad.", "tx_rejected_dust_output_send_all": "Transacción rechazada por reglas de red, baja cantidad de salida (polvo). Verifique el saldo de monedas seleccionadas bajo control de monedas.", - "tx_rejected_vout_negative": "No es suficiente saldo para pagar las tarifas de esta transacción. Verifique el saldo de monedas bajo control de monedas.", + "tx_rejected_vout_negative": "No es suficiente saldo para pagar las tarifas de esta transacción. Verifica el saldo de monedas bajo control de monedas.", "tx_wrong_balance_exception": "No tiene suficiente ${currency} para enviar esta cantidad.", "tx_wrong_balance_with_amount_exception": "No tiene suficiente ${currency} para enviar la cantidad total de ${amount}", "tx_zero_fee_exception": "No se puede enviar transacciones con 0 tarifa. Intente aumentar la tasa o verificar su conexión para las últimas estimaciones.", @@ -849,7 +849,7 @@ "unconfirmed": "Saldo no confirmado", "understand": "Entiendo", "unlock": "desbloquear", - "unmatched_currencies": "La moneda de su billetera actual no coincide con la del QR escaneado", + "unmatched_currencies": "La moneda de tu billetera actual no coincide con la del QR escaneado", "unspent_change": "Cambiar", "unspent_coins_details_title": "Detalles de monedas no gastadas", "unspent_coins_title": "Monedas no gastadas", @@ -858,11 +858,11 @@ "upto": "hasta ${value}", "usb": "USB", "use": "Utilizar a ", - "use_card_info_three": "Utilice la tarjeta digital en línea o con métodos de pago sin contacto.", + "use_card_info_three": "Utiliza la tarjeta digital en línea o con métodos de pago sin contacto.", "use_card_info_two": "Los fondos se convierten a USD cuando se mantienen en la cuenta prepaga, no en monedas digitales.", - "use_ssl": "Utilice SSL", + "use_ssl": "Utiliza SSL", "use_suggested": "Usar sugerido", - "use_testnet": "Use TestNet", + "use_testnet": "Usar TestNet", "value": "Valor", "value_type": "Tipo de valor", "variable_pair_not_supported": "Este par de variables no es compatible con los intercambios seleccionados", @@ -871,17 +871,17 @@ "verify_with_2fa": "Verificar con Cake 2FA", "version": "Versión ${currentVersion}", "view_all": "Ver todo", - "view_in_block_explorer": "View in Block Explorer", - "view_key_private": "View clave (privado)", - "view_key_public": "View clave (público)", - "view_transaction_on": "View Transaction on ", + "view_in_block_explorer": "Ver en explorador de bloques", + "view_key_private": "Llave de vista (privada)", + "view_key_public": "Llave de vista (pública)", + "view_transaction_on": "Ver transacción en ", "voting_weight": "Peso de votación", - "waitFewSecondForTxUpdate": "Espere unos segundos para que la transacción se refleje en el historial de transacciones.", + "waitFewSecondForTxUpdate": "Espera unos segundos para que la transacción se refleje en el historial de transacciones.", "wallet_group": "Grupo de billetera", - "wallet_group_description_four": "para crear una billetera con una semilla completamente nueva.", + "wallet_group_description_four": "Para crear una billetera con una semilla completamente nueva.", "wallet_group_description_one": "En la billetera de pastel, puedes crear un", "wallet_group_description_three": "Para ver las billeteras disponibles y/o la pantalla de grupos de billeteras. O elegir", - "wallet_group_description_two": "seleccionando una billetera existente para compartir una semilla con. Cada grupo de billetera puede contener una sola billetera de cada tipo de moneda. \n\n puede seleccionar", + "wallet_group_description_two": "Seleccionando una billetera existente para compartir una semilla con. Cada grupo de billetera puede contener una sola billetera de cada tipo de moneda. \n\n puedes seleccionar", "wallet_group_empty_state_text_one": "Parece que no tienes ningún grupo de billetera compatible !\n\n toque", "wallet_group_empty_state_text_two": "a continuación para hacer uno nuevo.", "wallet_keys": "Billetera semilla/claves", @@ -889,7 +889,7 @@ "wallet_list_edit_group_name": "Editar nombre de grupo", "wallet_list_edit_wallet": "Editar billetera", "wallet_list_failed_to_load": "No se pudo cargar ${wallet_name} la billetera. ${error}", - "wallet_list_failed_to_remove": "Error al elimina ${wallet_name} billetera. ${error}", + "wallet_list_failed_to_remove": "Error al eliminar ${wallet_name} billetera. ${error}", "wallet_list_load_wallet": "Billetera de carga", "wallet_list_loading_wallet": "Billetera ${wallet_name} de carga", "wallet_list_removing_wallet": "Retirar ${wallet_name} billetera", @@ -898,8 +898,8 @@ "wallet_list_wallet_name": "Nombre de la billetera", "wallet_menu": "Menú de billetera", "wallet_name": "Nombre de la billetera", - "wallet_name_exists": "Wallet con ese nombre ya ha existido", - "wallet_password_is_empty": "La contraseña de billetera está vacía. La contraseña de la billetera no debe estar vacía", + "wallet_name_exists": "Cartera con ese nombre ya existe, escoge otro", + "wallet_password_is_empty": "La contraseña de billetera está vacía. La contraseña de la billetera no puede estar vacía", "wallet_recovery_height": "Altura de recuperación", "wallet_restoration_store_incorrect_seed_length": "Longitud de semilla incorrecta", "wallet_seed": "Semilla de billetera", @@ -913,28 +913,28 @@ "what_is_silent_payments": "¿Qué son los pagos silenciosos?", "widgets_address": "Dirección", "widgets_or": "o", - "widgets_restore_from_blockheight": "Restaurar desde blockheight", + "widgets_restore_from_blockheight": "Restaurar desde altura de bloque", "widgets_restore_from_date": "Restaurar desde fecha", "widgets_seed": "Semilla", "wouoldLikeToConnect": "quisiera conectar", - "write_down_backup_password": "Escriba su contraseña de respaldo, que se utiliza para la importación de sus archivos de respaldo.", - "xlm_extra_info": "No olvide especificar el ID de nota al enviar la transacción XLM para el intercambio", + "write_down_backup_password": "Escribe su contraseña de respaldo, que se utiliza para la importación de sus archivos de respaldo.", + "xlm_extra_info": "No olvides especificar el ID de nota al enviar la transacción XLM para el intercambio", "xmr_available_balance": "Saldo disponible", "xmr_full_balance": "Balance total", "xmr_hidden": "Oculto", "xmr_to_error": "Error de XMR.TO", "xmr_to_error_description": "Monto invalido. Límite máximo de 8 dígitos después del punto decimal", - "xrp_extra_info": "No olvide especificar la etiqueta de destino al enviar la transacción XRP para el intercambio", + "xrp_extra_info": "No olvides especificar la etiqueta de destino al enviar la transacción XRP para el intercambio", "yat": "Yat", "yat_address": "Dirección de Yat", "yat_alert_content": "Los usuarios de Cake Wallet ahora pueden enviar y recibir todas sus monedas favoritas con un nombre de usuario único basado en emoji.", - "yat_alert_title": "Envíe y reciba criptomonedas más fácilmente con Yat", + "yat_alert_title": "Envía y recibe criptomonedas más fácilmente con Yat", "yat_error": "Error de Yat", "yat_error_content": "No hay direcciones vinculadas con este Yat. Prueba con otro Yat", - "yat_popup_content": "Ahora puede enviar y recibir criptografía en Cake Wallet con su Yat, un nombre de usuario corto basado en emoji. Administre Yats en cualquier momento en la pantalla de configuración", - "yat_popup_title": "La dirección de su billetera se puede emojificar.", + "yat_popup_content": "Ahora puede enviar y recibir criptografía en Cake Wallet con su Yat, un nombre de usuario corto basado en emoji. Administra Yats en cualquier momento en la pantalla de configuración", + "yat_popup_title": "La dirección de tu billetera se puede emojificar.", "yesterday": "Ayer", - "you_now_have_debit_card": "Ahora tiene una tarjeta de débito", + "you_now_have_debit_card": "Ahora tienes una tarjeta de débito", "you_pay": "Tú pagas", "you_will_get": "Convertir a", "you_will_send": "Convertir de", From b8ffd47f0624a35b987038e6217c3ea140a9d694 Mon Sep 17 00:00:00 2001 From: Elias Rad <146735585+nnsW3@users.noreply.github.com> Date: Mon, 4 Nov 2024 04:51:19 +0200 Subject: [PATCH 44/73] Docs spelling issues (#1789) * Update PRIVACY.md * Update README.md * Update SECURITY.md * Update build-guide-linux.md --- PRIVACY.md | 4 ++-- README.md | 6 +++--- SECURITY.md | 2 +- build-guide-linux.md | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/PRIVACY.md b/PRIVACY.md index 76cfcc4d3..a5c8eddfb 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -5,7 +5,7 @@ Last modified: January 24, 2024 Introduction ============ - Cake Labs LLC ("Cake Labs", "Company", or "We") respect your privacy and are committed to protecting it through our compliance with this policy. + Cake Labs LLC ("Cake Labs", "Company", or "We") respects your privacy and are committed to protecting it through our compliance with this policy. This policy describes the types of information we may collect from you or that you may provide when you use the App (our "App") and our practices for collecting, using, maintaining, protecting, and disclosing that information. @@ -13,7 +13,7 @@ Introduction - On this App. - In email, text, and other electronic messages between you and this App. It does not apply to information collected by: - - Us offline or through any other means, including on any other App operated by Company or any third party (including our affiliates and subsidiaries); or + - Us offline or through any other means, including on any other App operated by the Company or any third party (including our affiliates and subsidiaries); or - Any third party (including our affiliates and subsidiaries), including through any application or content (including advertising) that may link to or be accessible from or on the App. Please read this policy carefully to understand our policies and practices regarding your information and how we will treat it. If you do not agree with our policies and practices, you have the choice to not use the App. By accessing or using this App, you agree to this privacy policy. This policy may change from time to time. Your continued use of this App after we make changes is deemed to be acceptance of those changes, so please check the policy periodically for updates. diff --git a/README.md b/README.md index ca3061e61..078c4437e 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ # Cake Wallet -[Cake Wallet](https://cakewallet.com) is an open source, non-custodial, and private multi-currency crypto wallet for Android, iOS, macOS, and Linux. +[Cake Wallet](https://cakewallet.com) is an open-source, non-custodial, and private multi-currency crypto wallet for Android, iOS, macOS, and Linux. Cake Wallet includes support for several cryptocurrencies, including: * Monero (XMR) @@ -44,7 +44,7 @@ Cake Wallet includes support for several cryptocurrencies, including: * Create several wallets * Select your own custom nodes/servers * Address book -* Backup to external location or iCloud +* Backup to an external location or iCloud * Send to OpenAlias, Unstoppable Domains, Yats, and FIO Crypto Handles * Set desired network fee level * Store local transaction notes @@ -161,7 +161,7 @@ The only parts to be translated, if needed, are the values m and s after the var 4. Add the language to `lib/entities/language_service.dart` under both `supportedLocales` and `localeCountryCode`. Use the name of the language in the local language and in English in parentheses after for `supportedLocales`. Use the [ISO 3166-1 alpha-3 code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3) for `localeCountryCode`. You must choose one country, so choose the country with the most native speakers of this language or is otherwise best associated with this language. -5. Add a relevant flag to `assets/images/flags/XXXX.png`, replacing XXXX with the 3 letters localeCountryCode. The image must be 42x26 pixels with a 3 pixels of transparent margin on all 4 sides. You can resize the flag with [paint.net](https://www.getpaint.net/) to 36x20 pixels, expand the canvas to 42x26 pixels with the flag anchored in the middle, and then manually delete the 3 pixels on each side to make transparent. Or you can use another program like Photoshop. +5. Add a relevant flag to `assets/images/flags/XXXX.png`, replacing XXXX with the 3 letters localeCountryCode. The image must be 42x26 pixels with 3 pixels of transparent margin on all 4 sides. You can resize the flag with [paint.net](https://www.getpaint.net/) to 36x20 pixels, expand the canvas to 42x26 pixels with the flag anchored in the middle, and then manually delete the 3 pixels on each side to make it transparent. Or you can use another program like Photoshop. 6. Add the new language code to `tool/utils/translation/translation_constants.dart` diff --git a/SECURITY.md b/SECURITY.md index a1b489b76..e7c6baa02 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -9,4 +9,4 @@ If you need to report a vulnerability, please either: ## Supported Versions -As we don't maintain prevoius versions of the app, only the latest release for each platform is supported and any updates will bump the version number. +As we don't maintain previous versions of the app, only the latest release for each platform is supported and any updates will bump the version number. diff --git a/build-guide-linux.md b/build-guide-linux.md index 50ecc76fe..55264fbfa 100644 --- a/build-guide-linux.md +++ b/build-guide-linux.md @@ -55,7 +55,7 @@ Need to install flutter. For this please check section [How to install flutter o ### 3. Verify Installations -Verify that the Flutter have been correctly installed on your system with the following command: +Verify that the Flutter has been correctly installed on your system with the following command: `$ flutter doctor` @@ -163,7 +163,7 @@ And then export bundle: `$ flatpak build-bundle export cake_wallet.flatpak com.cakewallet.CakeWallet` -Result file: `cake_wallet.flatpak` should be generated in current directory. +Result file: `cake_wallet.flatpak` should be generated in the current directory. For install generated flatpak file use: From 459f0d352d92150a3b6b96cf94106d060ebd92dd Mon Sep 17 00:00:00 2001 From: Serhii Date: Tue, 5 Nov 2024 11:33:24 +0200 Subject: [PATCH 45/73] Cw 805 fix bch refund address (#1782) * fix bch address format for exolix, stealth, thorchain, simpleswap * exclude SP and MWEB from the exchange address book * address pr review comments --- lib/core/address_validator.dart | 3 +-- .../provider/exolix_exchange_provider.dart | 7 +++++-- .../provider/simpleswap_exchange_provider.dart | 7 +++++-- .../provider/stealth_ex_exchange_provider.dart | 7 +++++-- .../provider/thorchain_exchange.provider.dart | 11 ++++++----- .../contact_list/contact_list_view_model.dart | 15 ++++++++++----- 6 files changed, 32 insertions(+), 18 deletions(-) diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 21726fab8..9ca8a41ad 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -106,8 +106,7 @@ class AddressValidator extends TextValidator { case CryptoCurrency.wow: pattern = '[0-9a-zA-Z]+'; case CryptoCurrency.bch: - pattern = - '(?!bitcoincash:)[0-9a-zA-Z]*|(?!bitcoincash:)q|p[0-9a-zA-Z]{41}|(?!bitcoincash:)q|p[0-9a-zA-Z]{42}|bitcoincash:q|p[0-9a-zA-Z]{41}|bitcoincash:q|p[0-9a-zA-Z]{42}'; + pattern = '^(bitcoincash:)?(q|p)[0-9a-zA-Z]{41,42}'; case CryptoCurrency.bnb: pattern = '[0-9a-zA-Z]+'; case CryptoCurrency.hbar: diff --git a/lib/exchange/provider/exolix_exchange_provider.dart b/lib/exchange/provider/exolix_exchange_provider.dart index 8f2d5c241..5eeb6f9cf 100644 --- a/lib/exchange/provider/exolix_exchange_provider.dart +++ b/lib/exchange/provider/exolix_exchange_provider.dart @@ -141,8 +141,8 @@ class ExolixExchangeProvider extends ExchangeProvider { 'coinTo': _normalizeCurrency(request.toCurrency), 'networkFrom': _networkFor(request.fromCurrency), 'networkTo': _networkFor(request.toCurrency), - 'withdrawalAddress': request.toAddress, - 'refundAddress': request.refundAddress, + 'withdrawalAddress': _normalizeAddress(request.toAddress), + 'refundAddress': _normalizeAddress(request.refundAddress), 'rateType': _getRateType(isFixedRateMode), 'apiToken': apiKey, }; @@ -275,4 +275,7 @@ class ExolixExchangeProvider extends ExchangeProvider { return tag; } } + + String _normalizeAddress(String address) => + address.startsWith('bitcoincash:') ? address.replaceFirst('bitcoincash:', '') : address; } diff --git a/lib/exchange/provider/simpleswap_exchange_provider.dart b/lib/exchange/provider/simpleswap_exchange_provider.dart index be52b73fe..2a72ac793 100644 --- a/lib/exchange/provider/simpleswap_exchange_provider.dart +++ b/lib/exchange/provider/simpleswap_exchange_provider.dart @@ -129,8 +129,8 @@ class SimpleSwapExchangeProvider extends ExchangeProvider { "currency_to": _normalizeCurrency(request.toCurrency), "amount": request.fromAmount, "fixed": isFixedRateMode, - "user_refund_address": request.refundAddress, - "address_to": request.toAddress + "user_refund_address": _normalizeAddress(request.refundAddress), + "address_to": _normalizeAddress(request.toAddress) }; final uri = Uri.https(apiAuthority, createExchangePath, params); @@ -243,4 +243,7 @@ class SimpleSwapExchangeProvider extends ExchangeProvider { return currency.title.toLowerCase(); } } + + String _normalizeAddress(String address) => + address.startsWith('bitcoincash:') ? address.replaceFirst('bitcoincash:', '') : address; } diff --git a/lib/exchange/provider/stealth_ex_exchange_provider.dart b/lib/exchange/provider/stealth_ex_exchange_provider.dart index 601735595..123f759ef 100644 --- a/lib/exchange/provider/stealth_ex_exchange_provider.dart +++ b/lib/exchange/provider/stealth_ex_exchange_provider.dart @@ -129,8 +129,8 @@ class StealthExExchangeProvider extends ExchangeProvider { if (isFixedRateMode) 'rate_id': rateId, 'amount': isFixedRateMode ? double.parse(request.toAmount) : double.parse(request.fromAmount), - 'address': request.toAddress, - 'refund_address': request.refundAddress, + 'address': _normalizeAddress(request.toAddress), + 'refund_address': _normalizeAddress(request.refundAddress), 'additional_fee_percent': _additionalFeePercent, }; @@ -296,4 +296,7 @@ class StealthExExchangeProvider extends ExchangeProvider { return currency.tag!.toLowerCase(); } + + String _normalizeAddress(String address) => + address.startsWith('bitcoincash:') ? address.replaceFirst('bitcoincash:', '') : address; } diff --git a/lib/exchange/provider/thorchain_exchange.provider.dart b/lib/exchange/provider/thorchain_exchange.provider.dart index 897c2fdb9..99b9dcf9f 100644 --- a/lib/exchange/provider/thorchain_exchange.provider.dart +++ b/lib/exchange/provider/thorchain_exchange.provider.dart @@ -116,9 +116,7 @@ class ThorChainExchangeProvider extends ExchangeProvider { required bool isFixedRateMode, required bool isSendAll, }) async { - String formattedToAddress = request.toAddress.startsWith('bitcoincash:') - ? request.toAddress.replaceFirst('bitcoincash:', '') - : request.toAddress; + final formattedFromAmount = double.parse(request.fromAmount); @@ -126,11 +124,11 @@ class ThorChainExchangeProvider extends ExchangeProvider { 'from_asset': _normalizeCurrency(request.fromCurrency), 'to_asset': _normalizeCurrency(request.toCurrency), 'amount': _doubleToThorChainString(formattedFromAmount), - 'destination': formattedToAddress, + 'destination': _normalizeAddress(request.toAddress), 'affiliate': _affiliateName, 'affiliate_bps': _affiliateBps, 'refund_address': - isRefundAddressSupported.contains(request.fromCurrency) ? request.refundAddress : '', + isRefundAddressSupported.contains(request.fromCurrency) ? _normalizeAddress(request.refundAddress) : '', }; final responseJSON = await _getSwapQuote(params); @@ -288,4 +286,7 @@ class ThorChainExchangeProvider extends ExchangeProvider { return currentState; } + + String _normalizeAddress(String address) => + address.startsWith('bitcoincash:') ? address.replaceFirst('bitcoincash:', '') : address; } 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 4089d988b..89192583e 100644 --- a/lib/view_model/contact_list/contact_list_view_model.dart +++ b/lib/view_model/contact_list/contact_list_view_model.dart @@ -99,18 +99,23 @@ abstract class ContactListViewModelBase with Store { Future delete(ContactRecord contact) async => contact.original.delete(); - @computed - List get contactsToShow => - contacts.where((element) => _isValidForCurrency(element)).toList(); + ObservableList get contactsToShow => + ObservableList.of(contacts.where((element) => _isValidForCurrency(element))); @computed List get walletContactsToShow => walletContacts.where((element) => _isValidForCurrency(element)).toList(); bool _isValidForCurrency(ContactBase element) { + if (element.name.contains('Silent Payments')) return false; + if (element.name.contains('MWEB')) return false; + return _currency == null || element.type == _currency || - element.type.title == _currency!.tag || - element.type.tag == _currency!.tag; + (element.type.tag != null && + _currency?.tag != null && + element.type.tag == _currency?.tag) || + _currency?.toString() == element.type.tag || + _currency?.tag == element.type.toString(); } } From 109d9b458eac30534dd93492173145a3fc48b948 Mon Sep 17 00:00:00 2001 From: Serhii Date: Thu, 7 Nov 2024 03:26:14 +0200 Subject: [PATCH 46/73] Cw 514 add sort functionality for addressbook mywallets and contacts (#1309) * add sort function to contact list * fix UI * prevent duplicate contact names * dispose contact source subscription * fix custom order issue * update the address book UI * fix saving custom order * fix merge conflict issue * review fixes [skip ci] * revert to single scroll for entire page * tabBarView address book --------- Co-authored-by: Omar Hatem --- lib/entities/contact.dart | 9 +- lib/entities/contact_record.dart | 23 +- lib/entities/preferences_key.dart | 2 + lib/entities/wallet_list_order_types.dart | 10 +- .../screens/contact/contact_list_page.dart | 388 ++++++++++++++---- .../dashboard/widgets/filter_list_widget.dart | 26 +- .../screens/wallet_list/filtered_list.dart | 46 ++- .../screens/wallet_list/wallet_list_page.dart | 2 +- .../widgets/collapsible_standart_list.dart | 39 -- lib/store/settings_store.dart | 40 +- .../contact_list/contact_list_view_model.dart | 84 +++- .../contact_list/contact_view_model.dart | 24 +- .../wallet_list/wallet_list_view_model.dart | 14 +- res/values/strings_ar.arb | 5 +- res/values/strings_bg.arb | 5 +- res/values/strings_cs.arb | 5 +- res/values/strings_en.arb | 5 +- res/values/strings_es.arb | 3 +- res/values/strings_fr.arb | 5 +- res/values/strings_ha.arb | 5 +- res/values/strings_hi.arb | 5 +- res/values/strings_hr.arb | 5 +- res/values/strings_id.arb | 5 +- res/values/strings_it.arb | 5 +- res/values/strings_ja.arb | 5 +- res/values/strings_ko.arb | 5 +- res/values/strings_my.arb | 5 +- res/values/strings_nl.arb | 5 +- res/values/strings_pl.arb | 5 +- res/values/strings_pt.arb | 2 +- res/values/strings_ru.arb | 5 +- res/values/strings_th.arb | 5 +- res/values/strings_tl.arb | 2 +- res/values/strings_tr.arb | 5 +- res/values/strings_uk.arb | 5 +- res/values/strings_ur.arb | 5 +- res/values/strings_yo.arb | 5 +- res/values/strings_zh.arb | 5 +- 38 files changed, 588 insertions(+), 236 deletions(-) delete mode 100644 lib/src/widgets/collapsible_standart_list.dart diff --git a/lib/entities/contact.dart b/lib/entities/contact.dart index cd4fa55a2..901993f43 100644 --- a/lib/entities/contact.dart +++ b/lib/entities/contact.dart @@ -7,7 +7,8 @@ part 'contact.g.dart'; @HiveType(typeId: Contact.typeId) class Contact extends HiveObject with Keyable { - Contact({required this.name, required this.address, CryptoCurrency? type}) { + Contact({required this.name, required this.address, CryptoCurrency? type, DateTime? lastChange}) + : lastChange = lastChange ?? DateTime.now() { if (type != null) { raw = type.raw; } @@ -25,6 +26,9 @@ class Contact extends HiveObject with Keyable { @HiveField(2, defaultValue: 0) late int raw; + @HiveField(3) + DateTime lastChange; + CryptoCurrency get type => CryptoCurrency.deserialize(raw: raw); @override @@ -36,6 +40,5 @@ class Contact extends HiveObject with Keyable { @override int get hashCode => key.hashCode; - void updateCryptoCurrency({required CryptoCurrency currency}) => - raw = currency.raw; + void updateCryptoCurrency({required CryptoCurrency currency}) => raw = currency.raw; } diff --git a/lib/entities/contact_record.dart b/lib/entities/contact_record.dart index 50a432727..4b0e120ba 100644 --- a/lib/entities/contact_record.dart +++ b/lib/entities/contact_record.dart @@ -1,22 +1,21 @@ +import 'package:cake_wallet/entities/contact.dart'; +import 'package:cake_wallet/entities/contact_base.dart'; +import 'package:cake_wallet/entities/record.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/entities/contact.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cake_wallet/entities/record.dart'; -import 'package:cake_wallet/entities/contact_base.dart'; part 'contact_record.g.dart'; class ContactRecord = ContactRecordBase with _$ContactRecord; -abstract class ContactRecordBase extends Record - with Store - implements ContactBase { +abstract class ContactRecordBase extends Record with Store implements ContactBase { ContactRecordBase(Box source, Contact original) : name = original.name, address = original.address, type = original.type, - super(source, original); + lastChange = original.lastChange, + super(source, original); @override @observable @@ -30,14 +29,14 @@ abstract class ContactRecordBase extends Record @observable CryptoCurrency type; + DateTime? lastChange; + @override void toBind(Contact original) { reaction((_) => name, (String name) => original.name = name); reaction((_) => address, (String address) => original.address = address); - reaction( - (_) => type, - (CryptoCurrency currency) => - original.updateCryptoCurrency(currency: currency)); + reaction((_) => type, + (CryptoCurrency currency) => original.updateCryptoCurrency(currency: currency)); } @override diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 4fbe358e5..73556452c 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -25,7 +25,9 @@ class PreferencesKey { static const disableBulletinKey = 'disable_bulletin'; static const defaultBuyProvider = 'default_buy_provider'; static const walletListOrder = 'wallet_list_order'; + static const contactListOrder = 'contact_list_order'; static const walletListAscending = 'wallet_list_ascending'; + static const contactListAscending = 'contact_list_ascending'; static const currentFiatApiModeKey = 'current_fiat_api_mode'; static const failedTotpTokenTrials = 'failed_token_trials'; static const disableExchangeKey = 'disable_exchange'; diff --git a/lib/entities/wallet_list_order_types.dart b/lib/entities/wallet_list_order_types.dart index f848170f4..751569e9e 100644 --- a/lib/entities/wallet_list_order_types.dart +++ b/lib/entities/wallet_list_order_types.dart @@ -1,6 +1,6 @@ import 'package:cake_wallet/generated/i18n.dart'; -enum WalletListOrderType { +enum FilterListOrderType { CreationDate, Alphabetical, GroupByType, @@ -9,13 +9,13 @@ enum WalletListOrderType { @override String toString() { switch (this) { - case WalletListOrderType.CreationDate: + case FilterListOrderType.CreationDate: return S.current.creation_date; - case WalletListOrderType.Alphabetical: + case FilterListOrderType.Alphabetical: return S.current.alphabetical; - case WalletListOrderType.GroupByType: + case FilterListOrderType.GroupByType: return S.current.group_by_type; - case WalletListOrderType.Custom: + case FilterListOrderType.Custom: return S.current.custom_drag; } } diff --git a/lib/src/screens/contact/contact_list_page.dart b/lib/src/screens/contact/contact_list_page.dart index 99f2aa251..130380b09 100644 --- a/lib/src/screens/contact/contact_list_page.dart +++ b/lib/src/screens/contact/contact_list_page.dart @@ -1,21 +1,24 @@ import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/entities/contact_base.dart'; import 'package:cake_wallet/entities/contact_record.dart'; +import 'package:cake_wallet/entities/wallet_list_order_types.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/dashboard/widgets/filter_list_widget.dart'; +import 'package:cake_wallet/src/screens/wallet_list/filtered_list.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/src/widgets/standard_list.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; -import 'package:cake_wallet/src/widgets/collapsible_standart_list.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; class ContactListPage extends BasePage { ContactListPage(this.contactListViewModel, this.authService); @@ -74,45 +77,101 @@ class ContactListPage extends BasePage { } @override - Widget body(BuildContext context) { - return Container( - padding: EdgeInsets.all(20.0), - child: Observer(builder: (_) { - final contacts = contactListViewModel.contactsToShow; - final walletContacts = contactListViewModel.walletContactsToShow; - return CollapsibleSectionList( - sectionCount: 2, - sectionTitleBuilder: (int sectionIndex) { - var title = S.current.contact_list_contacts; + Widget body(BuildContext context) => ContactPageBody(contactListViewModel: contactListViewModel); +} - if (sectionIndex == 0) { - title = S.current.contact_list_wallets; - } +class ContactPageBody extends StatefulWidget { + const ContactPageBody({required this.contactListViewModel}); - return Container( - padding: EdgeInsets.only(bottom: 10), - child: Text(title, style: TextStyle(fontSize: 36))); - }, - itemCounter: (int sectionIndex) => - sectionIndex == 0 ? walletContacts.length : contacts.length, - itemBuilder: (int sectionIndex, index) { - if (sectionIndex == 0) { - final walletInfo = walletContacts[index]; - return generateRaw(context, walletInfo); - } + final ContactListViewModel contactListViewModel; - final contact = contacts[index]; - final content = generateRaw(context, contact); - return contactListViewModel.isEditable - ? Slidable( - key: Key('${contact.key}'), - endActionPane: _actionPane(context, contact), - child: content, - ) - : content; - }, - ); - })); + @override + State createState() => _ContactPageBodyState(); +} + +class _ContactPageBodyState extends State with SingleTickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 24), + child: Column( + children: [ + Align( + alignment: Alignment.centerLeft, + child: TabBar( + controller: _tabController, + splashFactory: NoSplash.splashFactory, + indicatorSize: TabBarIndicatorSize.label, + isScrollable: true, + labelStyle: TextStyle( + fontSize: 18, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).appBarTheme.titleTextStyle!.color, + ), + unselectedLabelStyle: TextStyle( + fontSize: 18, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).appBarTheme.titleTextStyle!.color?.withOpacity(0.5)), + labelColor: Theme.of(context).appBarTheme.titleTextStyle!.color, + indicatorColor: Theme.of(context).appBarTheme.titleTextStyle!.color, + indicatorPadding: EdgeInsets.zero, + labelPadding: EdgeInsets.only(right: 24), + tabAlignment: TabAlignment.center, + dividerColor: Colors.transparent, + padding: EdgeInsets.zero, + tabs: [ + Tab(text: S.of(context).wallets), + Tab(text: S.of(context).contact_list_contacts), + ], + ), + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildWalletContacts(context), + ContactListBody( + contactListViewModel: widget.contactListViewModel, + tabController: _tabController), + ], + ), + ), + ], + ), + ); + } + + Widget _buildWalletContacts(BuildContext context) { + final walletContacts = widget.contactListViewModel.walletContactsToShow; + + return ListView.builder( + shrinkWrap: true, + itemCount: walletContacts.length * 2, + itemBuilder: (context, index) { + if (index.isOdd) { + return StandardListSeparator(); + } else { + final walletInfo = walletContacts[index ~/ 2]; + return generateRaw(context, walletInfo); + } + }, + ); } Widget generateRaw(BuildContext context, ContactBase contact) { @@ -123,7 +182,7 @@ class ContactListPage extends BasePage { return GestureDetector( onTap: () async { - if (!contactListViewModel.isEditable) { + if (!widget.contactListViewModel.isEditable) { Navigator.of(context).pop(contact); return; } @@ -143,8 +202,7 @@ class ContactListPage extends BasePage { mainAxisAlignment: MainAxisAlignment.start, children: [ currencyIcon, - Expanded( - child: Padding( + Padding( padding: EdgeInsets.only(left: 12), child: Text( contact.name, @@ -154,13 +212,215 @@ class ContactListPage extends BasePage { color: Theme.of(context).extension()!.titleColor, ), ), - )) + ), ], ), ), ); } + Future showNameAndAddressDialog(BuildContext context, String name, String address) async { + return await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithTwoActions( + alertTitle: name, + alertContent: address, + rightButtonText: S.of(context).copy, + leftButtonText: S.of(context).cancel, + actionRightButton: () => Navigator.of(context).pop(true), + actionLeftButton: () => Navigator.of(context).pop(false)); + }) ?? + false; + } +} + +class ContactListBody extends StatefulWidget { + ContactListBody({required this.contactListViewModel, required this.tabController}); + + final ContactListViewModel contactListViewModel; + final TabController tabController; + + @override + State createState() => _ContactListBodyState(); +} + +class _ContactListBodyState extends State { + bool _isContactsTabActive = false; + + @override + void initState() { + super.initState(); + widget.tabController.addListener(_handleTabChange); + } + + void _handleTabChange() { + setState(() { + _isContactsTabActive = widget.tabController.index == 1; + }); + } + + @override + void dispose() { + widget.tabController.removeListener(_handleTabChange); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final contacts = widget.contactListViewModel.contacts; + return Scaffold( + body: Container( + child: FilteredList( + list: contacts, + updateFunction: widget.contactListViewModel.reorderAccordingToContactList, + canReorder: widget.contactListViewModel.isEditable, + shrinkWrap: true, + itemBuilder: (context, index) { + final contact = contacts[index]; + final contactContent = + generateContactRaw(context, contact, contacts.length == index + 1); + return GestureDetector( + key: Key('${contact.name}'), + onTap: () async { + if (!widget.contactListViewModel.isEditable) { + Navigator.of(context).pop(contact); + return; + } + + final isCopied = + await showNameAndAddressDialog(context, contact.name, contact.address); + + if (isCopied) { + await Clipboard.setData(ClipboardData(text: contact.address)); + await showBar(context, S.of(context).copied_to_clipboard); + } + }, + behavior: HitTestBehavior.opaque, + child: widget.contactListViewModel.isEditable + ? Slidable( + key: Key('${contact.key}'), + endActionPane: _actionPane(context, contact), + child: contactContent) + : contactContent, + ); + }, + ), + ), + floatingActionButton: + _isContactsTabActive ? filterButtonWidget(context, widget.contactListViewModel) : null, + ); + } + + Widget generateContactRaw(BuildContext context, ContactRecord contact, bool isLast) { + final image = contact.type.iconPath; + final currencyIcon = image != null + ? Image.asset(image, height: 24, width: 24) + : const SizedBox(height: 24, width: 24); + return Column( + children: [ + Container( + key: Key('${contact.name}'), + padding: const EdgeInsets.only(top: 16, bottom: 16, right: 24), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + currencyIcon, + Expanded( + child: Padding( + padding: EdgeInsets.only(left: 12), + child: Text( + contact.name, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Theme.of(context).extension()!.titleColor, + ), + ), + )) + ], + ), + ), + StandardListSeparator() + ], + ); + } + + ActionPane _actionPane(BuildContext context, ContactRecord contact) => ActionPane( + motion: const ScrollMotion(), + extentRatio: 0.4, + children: [ + SlidableAction( + onPressed: (_) async => await Navigator.of(context) + .pushNamed(Routes.addressBookAddContact, arguments: contact), + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + icon: Icons.edit, + label: S.of(context).edit, + ), + SlidableAction( + onPressed: (_) async { + final isDelete = await showAlertDialog(context); + + if (isDelete) { + await widget.contactListViewModel.delete(contact); + } + }, + backgroundColor: Colors.red, + foregroundColor: Colors.white, + icon: CupertinoIcons.delete, + label: S.of(context).delete, + ), + ], + ); + + Widget filterButtonWidget(BuildContext context, ContactListViewModel contactListViewModel) { + final filterIcon = Image.asset('assets/images/filter_icon.png', + color: Theme.of(context).appBarTheme.titleTextStyle!.color); + return MergeSemantics( + child: SizedBox( + height: 58, + width: 58, + child: ButtonTheme( + minWidth: double.minPositive, + child: Semantics( + container: true, + child: GestureDetector( + onTap: () async { + await showPopUp( + context: context, + builder: (context) => FilterListWidget( + initalType: contactListViewModel.orderType, + initalAscending: contactListViewModel.ascending, + onClose: (bool ascending, FilterListOrderType type) async { + contactListViewModel.setAscending(ascending); + await contactListViewModel.setOrderType(type); + }, + ), + ); + }, + child: Semantics( + label: 'Transaction Filter', + button: true, + enabled: true, + child: Container( + height: 36, + width: 36, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).extension()!.buttonBackgroundColor, + ), + child: filterIcon, + ), + ), + ), + ), + ), + ), + ); + } + Future showAlertDialog(BuildContext context) async { return await showPopUp( context: context, @@ -190,32 +450,4 @@ class ContactListPage extends BasePage { }) ?? false; } - - ActionPane _actionPane(BuildContext context, ContactRecord contact) => ActionPane( - motion: const ScrollMotion(), - extentRatio: 0.4, - children: [ - SlidableAction( - onPressed: (_) async => await Navigator.of(context) - .pushNamed(Routes.addressBookAddContact, arguments: contact), - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - icon: Icons.edit, - label: S.of(context).edit, - ), - SlidableAction( - onPressed: (_) async { - final isDelete = await showAlertDialog(context); - - if (isDelete) { - await contactListViewModel.delete(contact); - } - }, - backgroundColor: Colors.red, - foregroundColor: Colors.white, - icon: CupertinoIcons.delete, - label: S.of(context).delete, - ), - ], - ); } diff --git a/lib/src/screens/dashboard/widgets/filter_list_widget.dart b/lib/src/screens/dashboard/widgets/filter_list_widget.dart index cda4f5f10..8e95de2af 100644 --- a/lib/src/screens/dashboard/widgets/filter_list_widget.dart +++ b/lib/src/screens/dashboard/widgets/filter_list_widget.dart @@ -18,9 +18,9 @@ class FilterListWidget extends StatefulWidget { required this.onClose, }); - final WalletListOrderType? initalType; + final FilterListOrderType? initalType; final bool initalAscending; - final Function(bool, WalletListOrderType) onClose; + final Function(bool, FilterListOrderType) onClose; @override FilterListWidgetState createState() => FilterListWidgetState(); @@ -28,7 +28,7 @@ class FilterListWidget extends StatefulWidget { class FilterListWidgetState extends State { late bool ascending; - late WalletListOrderType? type; + late FilterListOrderType? type; @override void initState() { @@ -37,7 +37,7 @@ class FilterListWidgetState extends State { type = widget.initalType; } - void setSelectedOrderType(WalletListOrderType? orderType) { + void setSelectedOrderType(FilterListOrderType? orderType) { setState(() { type = orderType; }); @@ -72,7 +72,7 @@ class FilterListWidgetState extends State { ), ), ), - if (type != WalletListOrderType.Custom) ...[ + if (type != FilterListOrderType.Custom) ...[ sectionDivider, SettingsChoicesCell( ChoicesListItem( @@ -89,10 +89,10 @@ class FilterListWidgetState extends State { ], sectionDivider, RadioListTile( - value: WalletListOrderType.CreationDate, + value: FilterListOrderType.CreationDate, groupValue: type, title: Text( - WalletListOrderType.CreationDate.toString(), + FilterListOrderType.CreationDate.toString(), style: TextStyle( color: Theme.of(context).extension()!.titleColor, fontSize: 16, @@ -104,10 +104,10 @@ class FilterListWidgetState extends State { activeColor: Theme.of(context).primaryColor, ), RadioListTile( - value: WalletListOrderType.Alphabetical, + value: FilterListOrderType.Alphabetical, groupValue: type, title: Text( - WalletListOrderType.Alphabetical.toString(), + FilterListOrderType.Alphabetical.toString(), style: TextStyle( color: Theme.of(context).extension()!.titleColor, fontSize: 16, @@ -119,10 +119,10 @@ class FilterListWidgetState extends State { activeColor: Theme.of(context).primaryColor, ), RadioListTile( - value: WalletListOrderType.GroupByType, + value: FilterListOrderType.GroupByType, groupValue: type, title: Text( - WalletListOrderType.GroupByType.toString(), + FilterListOrderType.GroupByType.toString(), style: TextStyle( color: Theme.of(context).extension()!.titleColor, fontSize: 16, @@ -134,10 +134,10 @@ class FilterListWidgetState extends State { activeColor: Theme.of(context).primaryColor, ), RadioListTile( - value: WalletListOrderType.Custom, + value: FilterListOrderType.Custom, groupValue: type, title: Text( - WalletListOrderType.Custom.toString(), + FilterListOrderType.Custom.toString(), style: TextStyle( color: Theme.of(context).extension()!.titleColor, fontSize: 16, diff --git a/lib/src/screens/wallet_list/filtered_list.dart b/lib/src/screens/wallet_list/filtered_list.dart index 63a1ae392..5316c8472 100644 --- a/lib/src/screens/wallet_list/filtered_list.dart +++ b/lib/src/screens/wallet_list/filtered_list.dart @@ -7,13 +7,17 @@ class FilteredList extends StatefulWidget { required this.list, required this.itemBuilder, required this.updateFunction, + this.canReorder = true, this.shrinkWrap = false, + this.physics, }); final ObservableList list; final Widget Function(BuildContext, int) itemBuilder; final Function updateFunction; + final bool canReorder; final bool shrinkWrap; + final ScrollPhysics? physics; @override FilteredListState createState() => FilteredListState(); @@ -22,21 +26,31 @@ class FilteredList extends StatefulWidget { class FilteredListState extends State { @override Widget build(BuildContext context) { - return Observer( - builder: (_) => ReorderableListView.builder( - shrinkWrap: widget.shrinkWrap, - physics: const BouncingScrollPhysics(), - itemBuilder: widget.itemBuilder, - itemCount: widget.list.length, - onReorder: (int oldIndex, int newIndex) { - if (oldIndex < newIndex) { - newIndex -= 1; - } - final dynamic item = widget.list.removeAt(oldIndex); - widget.list.insert(newIndex, item); - widget.updateFunction(); - }, - ), - ); + if (widget.canReorder) { + return Observer( + builder: (_) => ReorderableListView.builder( + shrinkWrap: widget.shrinkWrap, + physics: widget.physics ?? const BouncingScrollPhysics(), + itemBuilder: widget.itemBuilder, + itemCount: widget.list.length, + onReorder: (int oldIndex, int newIndex) { + if (oldIndex < newIndex) { + newIndex -= 1; + } + final dynamic item = widget.list.removeAt(oldIndex); + widget.list.insert(newIndex, item); + widget.updateFunction(); + }, + ), + ); + } else { + return Observer( + builder: (_) => ListView.builder( + physics: widget.physics ?? const BouncingScrollPhysics(), + itemBuilder: widget.itemBuilder, + itemCount: widget.list.length, + ), + ); + } } } diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index d17534f6b..afb86cf0a 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -59,7 +59,7 @@ class WalletListPage extends BasePage { builder: (context) => FilterListWidget( initalType: walletListViewModel.orderType, initalAscending: walletListViewModel.ascending, - onClose: (bool ascending, WalletListOrderType type) async { + onClose: (bool ascending, FilterListOrderType type) async { walletListViewModel.setAscending(ascending); await walletListViewModel.setOrderType(type); }, diff --git a/lib/src/widgets/collapsible_standart_list.dart b/lib/src/widgets/collapsible_standart_list.dart deleted file mode 100644 index 83e4daee2..000000000 --- a/lib/src/widgets/collapsible_standart_list.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:cake_wallet/src/widgets/standard_list.dart'; -import 'package:flutter/material.dart'; - -class CollapsibleSectionList extends SectionStandardList { - CollapsibleSectionList( - {required int sectionCount, - required int Function(int sectionIndex) itemCounter, - required Widget Function(int sectionIndex, int itemIndex) itemBuilder, - Widget Function(int sectionIndex)? sectionTitleBuilder, - bool hasTopSeparator = false}) - : super( - hasTopSeparator: hasTopSeparator, - sectionCount: sectionCount, - itemCounter: itemCounter, - itemBuilder: itemBuilder, - sectionTitleBuilder: sectionTitleBuilder); - - @override - Widget buildTitle(List items, int sectionIndex) { - if (sectionTitleBuilder == null) { - throw Exception('Cannot to build title. sectionTitleBuilder is null'); - } - return sectionTitleBuilder!.call(sectionIndex); - } - - @override - List buildSection(int itemCount, List items, int sectionIndex) { - final List section = []; - - for (var itemIndex = 0; itemIndex < itemCount; itemIndex++) { - final item = itemBuilder(sectionIndex, itemIndex); - - section.add(StandardListSeparator()); - - section.add(item); - } - return section; - } -} diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 9f03c95c3..2eadbd8a9 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -62,9 +62,11 @@ abstract class SettingsStoreBase with Store { required bool initialAppSecure, required bool initialDisableBuy, required bool initialDisableSell, + required FilterListOrderType initialWalletListOrder, + required FilterListOrderType initialContactListOrder, required bool initialDisableBulletin, - required WalletListOrderType initialWalletListOrder, required bool initialWalletListAscending, + required bool initialContactListAscending, required FiatApiMode initialFiatMode, required bool initialAllowBiometricalAuthentication, required String initialTotpSecretKey, @@ -149,7 +151,9 @@ abstract class SettingsStoreBase with Store { disableSell = initialDisableSell, disableBulletin = initialDisableBulletin, walletListOrder = initialWalletListOrder, + contactListOrder = initialContactListOrder, walletListAscending = initialWalletListAscending, + contactListAscending = initialContactListAscending, shouldShowMarketPlaceInDashboard = initialShouldShowMarketPlaceInDashboard, exchangeStatus = initialExchangeStatus, currentTheme = initialTheme, @@ -324,14 +328,24 @@ abstract class SettingsStoreBase with Store { reaction( (_) => walletListOrder, - (WalletListOrderType walletListOrder) => + (FilterListOrderType walletListOrder) => sharedPreferences.setInt(PreferencesKey.walletListOrder, walletListOrder.index)); + reaction( + (_) => contactListOrder, + (FilterListOrderType contactListOrder) => + sharedPreferences.setInt(PreferencesKey.contactListOrder, contactListOrder.index)); + reaction( (_) => walletListAscending, (bool walletListAscending) => sharedPreferences.setBool(PreferencesKey.walletListAscending, walletListAscending)); + reaction( + (_) => contactListAscending, + (bool contactListAscending) => + sharedPreferences.setBool(PreferencesKey.contactListAscending, contactListAscending)); + reaction( (_) => autoGenerateSubaddressStatus, (AutoGenerateSubaddressStatus autoGenerateSubaddressStatus) => sharedPreferences.setInt( @@ -645,15 +659,21 @@ abstract class SettingsStoreBase with Store { @observable bool disableSell; + @observable + FilterListOrderType contactListOrder; + @observable bool disableBulletin; @observable - WalletListOrderType walletListOrder; + FilterListOrderType walletListOrder; @observable bool walletListAscending; + @observable + bool contactListAscending; + @observable bool allowBiometricalAuthentication; @@ -907,9 +927,13 @@ abstract class SettingsStoreBase with Store { final disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? false; final disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? false; final walletListOrder = - WalletListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0]; + FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0]; + final contactListOrder = + FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.contactListOrder) ?? 0]; final walletListAscending = sharedPreferences.getBool(PreferencesKey.walletListAscending) ?? true; + final contactListAscending = + sharedPreferences.getBool(PreferencesKey.contactListAscending) ?? true; final currentFiatApiMode = FiatApiMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey) ?? FiatApiMode.enabled.raw); @@ -1200,6 +1224,8 @@ abstract class SettingsStoreBase with Store { initialDisableBulletin: disableBulletin, initialWalletListOrder: walletListOrder, initialWalletListAscending: walletListAscending, + initialContactListOrder: contactListOrder, + initialContactListAscending: contactListAscending, initialFiatMode: currentFiatApiMode, initialAllowBiometricalAuthentication: allowBiometricalAuthentication, initialCake2FAPresetOptions: selectedCake2FAPreset, @@ -1348,9 +1374,11 @@ abstract class SettingsStoreBase with Store { disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? disableBulletin; walletListOrder = - WalletListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0]; + FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0]; + contactListOrder = + FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.contactListOrder) ?? 0]; walletListAscending = sharedPreferences.getBool(PreferencesKey.walletListAscending) ?? true; - + contactListAscending = sharedPreferences.getBool(PreferencesKey.contactListAscending) ?? true; shouldShowMarketPlaceInDashboard = sharedPreferences.getBool(PreferencesKey.shouldShowMarketPlaceInDashboard) ?? shouldShowMarketPlaceInDashboard; 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 89192583e..0015463a5 100644 --- a/lib/view_model/contact_list/contact_list_view_model.dart +++ b/lib/view_model/contact_list/contact_list_view_model.dart @@ -1,18 +1,20 @@ import 'dart:async'; + import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; +import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/contact_base.dart'; +import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/wallet_contact.dart'; +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'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/entities/contact_record.dart'; -import 'package:cake_wallet/entities/contact.dart'; -import 'package:cake_wallet/utils/mobx.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:collection/collection.dart'; part 'contact_list_view_model.g.dart'; @@ -75,6 +77,8 @@ abstract class ContactListViewModelBase with Store { _subscription = contactSource.bindToListWithTransform( contacts, (Contact contact) => ContactRecord(contactSource, contact), initialFire: true); + + setOrderType(settingsStore.contactListOrder); } String _createName(String walletName, String label, {int? key = null}) { @@ -93,6 +97,10 @@ abstract class ContactListViewModelBase with Store { bool get isEditable => _currency == null; + FilterListOrderType? get orderType => settingsStore.contactListOrder; + + bool get ascending => settingsStore.contactListAscending; + @computed bool get shouldRequireTOTP2FAForAddingContacts => settingsStore.shouldRequireTOTP2FAForAddingContacts; @@ -118,4 +126,70 @@ abstract class ContactListViewModelBase with Store { _currency?.toString() == element.type.tag || _currency?.tag == element.type.toString(); } + + void dispose() async { + _subscription?.cancel(); + final List contactsSourceCopy = contacts.map((e) => e.original).toList(); + await reorderContacts(contactsSourceCopy); + } + + void reorderAccordingToContactList() => + settingsStore.contactListOrder = FilterListOrderType.Custom; + + Future reorderContacts(List contactCopy) async { + await contactSource.deleteAll(contactCopy.map((e) => e.key).toList()); + await contactSource.addAll(contactCopy); + } + + Future sortGroupByType() async { + List contactsSourceCopy = contactSource.values.toList(); + + contactsSourceCopy.sort((a, b) => ascending + ? a.type.toString().compareTo(b.type.toString()) + : b.type.toString().compareTo(a.type.toString())); + + await reorderContacts(contactsSourceCopy); + } + + Future sortAlphabetically() async { + List contactsSourceCopy = contactSource.values.toList(); + + contactsSourceCopy + .sort((a, b) => ascending ? a.name.compareTo(b.name) : b.name.compareTo(a.name)); + + await reorderContacts(contactsSourceCopy); + } + + Future sortByCreationDate() async { + List contactsSourceCopy = contactSource.values.toList(); + + contactsSourceCopy.sort((a, b) => + ascending ? a.lastChange.compareTo(b.lastChange) : b.lastChange.compareTo(a.lastChange)); + + await reorderContacts(contactsSourceCopy); + } + + void setAscending(bool ascending) => settingsStore.contactListAscending = ascending; + + Future setOrderType(FilterListOrderType? type) async { + if (type == null) return; + + settingsStore.contactListOrder = type; + + switch (type) { + case FilterListOrderType.CreationDate: + await sortByCreationDate(); + break; + case FilterListOrderType.Alphabetical: + await sortAlphabetically(); + break; + case FilterListOrderType.GroupByType: + await sortGroupByType(); + break; + case FilterListOrderType.Custom: + default: + reorderAccordingToContactList(); + break; + } + } } diff --git a/lib/view_model/contact_list/contact_view_model.dart b/lib/view_model/contact_list/contact_view_model.dart index 053cfe4c5..93abfb11c 100644 --- a/lib/view_model/contact_list/contact_view_model.dart +++ b/lib/view_model/contact_list/contact_view_model.dart @@ -2,7 +2,7 @@ import 'package:cake_wallet/entities/contact_record.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/core/execution_state.dart'; -import 'package:cw_core/wallet_base.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/entities/contact.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -17,7 +17,9 @@ abstract class ContactViewModelBase with Store { _contact = contact, name = contact?.name ?? '', address = contact?.address ?? '', - currency = contact?.type; + currency = contact?.type, + lastChange = contact?.lastChange; + @observable ExecutionState state; @@ -31,6 +33,8 @@ abstract class ContactViewModelBase with Store { @observable CryptoCurrency? currency; + DateTime? lastChange; + @computed bool get isReady => name.isNotEmpty && @@ -51,20 +55,32 @@ abstract class ContactViewModelBase with Store { Future save() async { try { state = IsExecutingState(); + final now = DateTime.now(); + + if (doesContactNameExist(name)) { + state = FailureState(S.current.contact_name_exists); + return; + } 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!)); + .add(Contact(name: name, address: address, type: currency!, lastChange: now)); } + lastChange = now; state = ExecutedSuccessfullyState(); } catch (e) { state = FailureState(e.toString()); } } -} + + bool doesContactNameExist(String name) { + return _contacts.values.any((contact) => contact.name == name); + } +} \ No newline at end of file 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 0ae6b75a5..725af843f 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -76,7 +76,7 @@ abstract class WalletListViewModelBase with Store { await _appStore.changeCurrentWallet(wallet); } - WalletListOrderType? get orderType => _appStore.settingsStore.walletListOrder; + FilterListOrderType? get orderType => _appStore.settingsStore.walletListOrder; bool get ascending => _appStore.settingsStore.walletListAscending; @@ -108,7 +108,7 @@ abstract class WalletListViewModelBase with Store { return; } - _appStore.settingsStore.walletListOrder = WalletListOrderType.Custom; + _appStore.settingsStore.walletListOrder = FilterListOrderType.Custom; // make a copy of the walletInfoSource: List walletInfoSourceCopy = _walletInfoSource.values.toList(); @@ -186,22 +186,22 @@ abstract class WalletListViewModelBase with Store { _appStore.settingsStore.walletListAscending = ascending; } - Future setOrderType(WalletListOrderType? type) async { + Future setOrderType(FilterListOrderType? type) async { if (type == null) return; _appStore.settingsStore.walletListOrder = type; switch (type) { - case WalletListOrderType.CreationDate: + case FilterListOrderType.CreationDate: await sortByCreationDate(); break; - case WalletListOrderType.Alphabetical: + case FilterListOrderType.Alphabetical: await sortAlphabetically(); break; - case WalletListOrderType.GroupByType: + case FilterListOrderType.GroupByType: await sortGroupByType(); break; - case WalletListOrderType.Custom: + case FilterListOrderType.Custom: default: await reorderAccordingToWalletList(); break; diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 81fe3cc2c..c2be1504a 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -937,5 +937,6 @@ "you_pay": "انت تدفع", "you_will_get": "حول الى", "you_will_send": "تحويل من", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": " .ﻒﻠﺘﺨﻣ ﻢﺳﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ .ﻞﻌﻔﻟﺎﺑ ﺓﺩﻮﺟﻮﻣ ﻢﺳﻻﺍ ﺍﺬﻬﺑ ﻝﺎﺼﺗﺍ ﺔﻬﺟ" +} diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 50db1610a..8b68d0d1e 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -937,5 +937,6 @@ "you_pay": "Вие плащате", "you_will_get": "Обръщане в", "you_will_send": "Обръщане от", - "yy": "гг" -} \ No newline at end of file + "yy": "гг", + "contact_name_exists": "Вече съществува контакт с това име. Моля, изберете друго име." +} diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index ddc91340b..9b48ebdc7 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -937,5 +937,6 @@ "you_pay": "Zaplatíte", "you_will_get": "Směnit na", "you_will_send": "Směnit z", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": "Kontakt s tímto jménem již existuje. Vyberte prosím jiný název." +} diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index d6a0ee9af..4f5fd23b3 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -940,5 +940,6 @@ "you_pay": "You Pay", "you_will_get": "Convert to", "you_will_send": "Convert from", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": "A contact with that name already exists. Please choose a different name." +} diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 25c9f95c1..fd20a8b30 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -938,5 +938,6 @@ "you_pay": "Tú pagas", "you_will_get": "Convertir a", "you_will_send": "Convertir de", - "yy": "YY" + "yy": "YY", + "contact_name_exists": "Ya existe un contacto con ese nombre. Elija un nombre diferente." } diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index be5b48dd8..50aceb2f1 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -937,5 +937,6 @@ "you_pay": "Vous payez", "you_will_get": "Convertir vers", "you_will_send": "Convertir depuis", - "yy": "AA" -} \ No newline at end of file + "yy": "AA", + "contact_name_exists": "Un contact portant ce nom existe déjà. Veuillez choisir un autre nom." +} diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 4deb0df1d..e66859531 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -939,5 +939,6 @@ "you_pay": "Ka Bayar", "you_will_get": "Maida zuwa", "you_will_send": "Maida daga", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": "An riga an sami lamba tare da wannan sunan. Da fatan za a zaɓi suna daban." +} diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 5161250fc..bc8b81211 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -939,5 +939,6 @@ "you_pay": "आप भुगतान करते हैं", "you_will_get": "में बदलें", "you_will_send": "से रूपांतरित करें", - "yy": "वाईवाई" -} \ No newline at end of file + "yy": "वाईवाई", + "contact_name_exists": "उस नाम का एक संपर्क पहले से मौजूद है. कृपया कोई भिन्न नाम चुनें." +} diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 8ef92aaf0..9ff57707d 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -937,5 +937,6 @@ "you_pay": "Vi plaćate", "you_will_get": "Razmijeni u", "you_will_send": "Razmijeni iz", - "yy": "GG" -} \ No newline at end of file + "yy": "GG", + "contact_name_exists": "Kontakt s tim imenom već postoji. Odaberite drugo ime." +} diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 5f93082ec..2858fa193 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -940,5 +940,6 @@ "you_pay": "Anda Membayar", "you_will_get": "Konversi ke", "you_will_send": "Konversi dari", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": "Kontak dengan nama tersebut sudah ada. Silakan pilih nama lain." +} diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 08ae928af..034376a6c 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -940,5 +940,6 @@ "you_pay": "Tu paghi", "you_will_get": "Converti a", "you_will_send": "Conveti da", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": "Esiste già un contatto con quel nome. Scegli un nome diverso." +} diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index d70eca31b..7f0792063 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -938,5 +938,6 @@ "you_pay": "あなたが支払う", "you_will_get": "に変換", "you_will_send": "から変換", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": "その名前の連絡先はすでに存在します。別の名前を選択してください。" +} diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 133ca1838..6ba402ce3 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -939,5 +939,6 @@ "you_will_get": "로 변환하다", "you_will_send": "다음에서 변환", "YY": "YY", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": "해당 이름을 가진 연락처가 이미 존재합니다. 다른 이름을 선택하세요." +} diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 1727f0d71..69b888b5a 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -937,5 +937,6 @@ "you_pay": "သင်ပေးချေပါ။", "you_will_get": "သို့ပြောင်းပါ။", "you_will_send": "မှပြောင်းပါ။", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": "ထိုအမည်နှင့် အဆက်အသွယ်တစ်ခု ရှိနှင့်ပြီးဖြစ်သည်။ အခြားအမည်တစ်ခုကို ရွေးပါ။" +} diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 3f2df531b..2c259bf80 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -938,5 +938,6 @@ "you_pay": "U betaalt", "you_will_get": "Converteren naar", "you_will_send": "Converteren van", - "yy": "JJ" -} \ No newline at end of file + "yy": "JJ", + "contact_name_exists": "Er bestaat al een contact met die naam. Kies een andere naam." +} diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 91b265144..936339708 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -937,5 +937,6 @@ "you_pay": "Płacisz", "you_will_get": "Konwertuj na", "you_will_send": "Konwertuj z", - "yy": "RR" -} \ No newline at end of file + "yy": "RR", + "contact_name_exists": "Kontakt o tej nazwie już istnieje. Proszę wybrać inną nazwę." +} diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 524dbcace..0cc0cc00a 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -941,4 +941,4 @@ "you_will_get": "Converter para", "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 1a8c2447f..8aeb56a86 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -938,5 +938,6 @@ "you_pay": "Вы платите", "you_will_get": "Конвертировать в", "you_will_send": "Конвертировать из", - "yy": "ГГ" -} \ No newline at end of file + "yy": "ГГ", + "contact_name_exists": "Контакт с таким именем уже существует. Пожалуйста, выберите другое имя." +} diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 213f74530..736eb8762 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -937,5 +937,6 @@ "you_pay": "คุณจ่าย", "you_will_get": "แปลงเป็น", "you_will_send": "แปลงจาก", - "yy": "ปี" -} \ No newline at end of file + "yy": "ปี", + "contact_name_exists": "มีผู้ติดต่อชื่อนั้นอยู่แล้ว โปรดเลือกชื่ออื่น" +} diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 0ca8ee665..e68bd0b22 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -938,4 +938,4 @@ "you_will_get": "I-convert sa", "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 b23f64d60..83e8f6cbc 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -937,5 +937,6 @@ "you_pay": "Şu kadar ödeyeceksin: ", "you_will_get": "Biçimine dönüştür:", "you_will_send": "Biçiminden dönüştür:", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": "Bu isimde bir kişi zaten mevcut. Lütfen farklı bir ad seçin." +} diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 79dc0543f..909d9c01f 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -938,5 +938,6 @@ "you_pay": "Ви платите", "you_will_get": "Конвертувати в", "you_will_send": "Конвертувати з", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": "Контакт із такою назвою вже існує. Виберіть інше ім'я." +} diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 0a136d140..2dbc97102 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -939,5 +939,6 @@ "you_pay": "تم ادا کرو", "you_will_get": "میں تبدیل کریں۔", "you_will_send": "سے تبدیل کریں۔", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": " ۔ﮟﯾﺮﮐ ﺐﺨﺘﻨﻣ ﻡﺎﻧ ﻒﻠﺘﺨﻣ ﮏﯾﺍ ﻡﺮﮐ ﮦﺍﺮﺑ ۔ﮯﮨ ﺩﻮﺟﻮﻣ ﮯﺳ ﮯﻠﮩﭘ ﮧﻄﺑﺍﺭ ﮏﯾﺍ ﮫﺗﺎﺳ ﮯﮐ ﻡﺎﻧ ﺱﺍ" +} diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 14270120c..03c7c95ef 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -938,5 +938,6 @@ "you_pay": "Ẹ sàn", "you_will_get": "Ṣe pàṣípààrọ̀ sí", "you_will_send": "Ṣe pàṣípààrọ̀ láti", - "yy": "Ọd" -} \ No newline at end of file + "yy": "Ọd", + "contact_name_exists": "Olubasọrọ pẹlu orukọ yẹn ti wa tẹlẹ. Jọwọ yan orukọ ti o yatọ." +} diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 65047b4fe..5c8d708f4 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -937,5 +937,6 @@ "you_pay": "你付钱", "you_will_get": "转换到", "you_will_send": "转换自", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": "已存在具有该名称的联系人。请选择不同的名称。" +} From c8cfc2cff1026c4c433d0f9e683a2c4cbb4023eb Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Wed, 6 Nov 2024 18:57:36 -0800 Subject: [PATCH 47/73] Mweb enhancements 4 (#1768) * [skip-ci] show mweb confirmations, show last mweb balance while syncing * potential send-all fix * [skip-ci] undo fix that didn't work * [skip-ci] undo unnecessary changes * [skip ci] add export mweb logs screen * [skip ci] cleanup * confirmation fixes * catch electrum call errors * [skip ci] undo some changes * potential electrum fixes + mweb logs display only last 10000 characters * Add question mark and link to MWEB card * updates * show negative unconfirmed mweb balanaces + other fixes [skip ci] * error handling * [skip ci] [wip] check if node supports mweb * check fee before building tx * [skip ci] minor * [skip ci] minor * mweb node setting [wip] [skip ci] * prioritize mweb coins when selecting inputs from the pool * potential connection edgecase fix * translations + mweb node fixes * don't use mweb for exchange refund address * add peg in / out labels and make 6 confs only show up for peg in / out * bump bitcoin_base version to v9 * [skip ci] fix logs page * don't fetch txinfo for non-mweb addresses [skip ci] * fix non-mweb confirmations * rename always scan to enable mweb * Update litecoin_wallet_addresses.dart Co-authored-by: Omar Hatem * Update cw_mweb.dart Co-authored-by: Omar Hatem * [skip ci] review updates pt.1 * [skip ci] minor code cleanup * [skip ci] use exception handler * exception handling [skip ci] * [skip ci] exception handling * trigger build * pegout label fixes * fix showing change transactions on peg-out * minor code cleanup and minor peg-out fix * final balance fixes * non-mweb confirmations potential fix * [skip ci] wip * trigger build --------- Co-authored-by: tuxpizza Co-authored-by: Omar Hatem --- cw_bitcoin/lib/electrum.dart | 2 + cw_bitcoin/lib/electrum_balance.dart | 12 +- cw_bitcoin/lib/electrum_transaction_info.dart | 9 +- cw_bitcoin/lib/electrum_wallet.dart | 130 +++++--- cw_bitcoin/lib/litecoin_wallet.dart | 310 ++++++++++++------ cw_bitcoin/lib/litecoin_wallet_addresses.dart | 17 +- cw_bitcoin/lib/litecoin_wallet_service.dart | 4 + .../lib/pending_bitcoin_transaction.dart | 3 +- cw_bitcoin/pubspec.yaml | 2 +- cw_bitcoin_cash/pubspec.yaml | 2 +- cw_core/lib/mweb_utxo.dart | 4 + cw_core/lib/node.dart | 3 + cw_core/lib/transaction_info.dart | 3 +- cw_mweb/lib/cw_mweb.dart | 28 +- lib/di.dart | 6 + lib/entities/preferences_key.dart | 1 + lib/router.dart | 10 + lib/routes.dart | 2 + .../screens/dashboard/pages/balance_page.dart | 42 ++- lib/src/screens/settings/mweb_logs_page.dart | 127 +++++++ lib/src/screens/settings/mweb_node_page.dart | 56 ++++ lib/src/screens/settings/mweb_settings.dart | 10 +- lib/store/settings_store.dart | 25 +- .../dashboard/balance_view_model.dart | 2 +- .../dashboard/transaction_list_item.dart | 52 ++- .../settings/mweb_settings_view_model.dart | 50 +++ pubspec_base.yaml | 2 +- res/values/strings_ar.arb | 4 + res/values/strings_bg.arb | 4 + res/values/strings_cs.arb | 4 + res/values/strings_de.arb | 6 +- 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 + 55 files changed, 821 insertions(+), 207 deletions(-) create mode 100644 lib/src/screens/settings/mweb_logs_page.dart create mode 100644 lib/src/screens/settings/mweb_node_page.dart diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index a18c038fa..df8e14119 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -124,6 +124,7 @@ class ElectrumClient { final errorMsg = error.toString(); print(errorMsg); unterminatedString = ''; + socket = null; }, onDone: () { print("SOCKET CLOSED!!!!!"); @@ -132,6 +133,7 @@ class ElectrumClient { if (host == socket?.address.host || socket == null) { _setConnectionStatus(ConnectionStatus.disconnected); socket?.destroy(); + socket = null; } } catch (e) { print("onDone: $e"); diff --git a/cw_bitcoin/lib/electrum_balance.dart b/cw_bitcoin/lib/electrum_balance.dart index 4e37f40b1..ebd2f06ae 100644 --- a/cw_bitcoin/lib/electrum_balance.dart +++ b/cw_bitcoin/lib/electrum_balance.dart @@ -24,9 +24,12 @@ class ElectrumBalance extends Balance { final decoded = json.decode(jsonSource) as Map; return ElectrumBalance( - confirmed: decoded['confirmed'] as int? ?? 0, - unconfirmed: decoded['unconfirmed'] as int? ?? 0, - frozen: decoded['frozen'] as int? ?? 0); + confirmed: decoded['confirmed'] as int? ?? 0, + unconfirmed: decoded['unconfirmed'] as int? ?? 0, + frozen: decoded['frozen'] as int? ?? 0, + secondConfirmed: decoded['secondConfirmed'] as int? ?? 0, + secondUnconfirmed: decoded['secondUnconfirmed'] as int? ?? 0, + ); } int confirmed; @@ -36,8 +39,7 @@ class ElectrumBalance extends Balance { int secondUnconfirmed = 0; @override - String get formattedAvailableBalance => - bitcoinAmountToString(amount: confirmed - frozen); + String get formattedAvailableBalance => bitcoinAmountToString(amount: confirmed - frozen); @override String get formattedAdditionalBalance => bitcoinAmountToString(amount: unconfirmed); diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index 1ab7799e3..7a8b3b951 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -41,6 +41,7 @@ class ElectrumTransactionInfo extends TransactionInfo { String? to, this.unspents, this.isReceivedSilentPayment = false, + Map? additionalInfo, }) { this.id = id; this.height = height; @@ -54,6 +55,7 @@ class ElectrumTransactionInfo extends TransactionInfo { this.isReplaced = isReplaced; this.confirmations = confirmations; this.to = to; + this.additionalInfo = additionalInfo ?? {}; } factory ElectrumTransactionInfo.fromElectrumVerbose(Map obj, WalletType type, @@ -212,6 +214,7 @@ class ElectrumTransactionInfo extends TransactionInfo { BitcoinSilentPaymentsUnspent.fromJSON(null, unspent as Map)) .toList(), isReceivedSilentPayment: data['isReceivedSilentPayment'] as bool? ?? false, + additionalInfo: data['additionalInfo'] as Map?, ); } @@ -246,7 +249,8 @@ class ElectrumTransactionInfo extends TransactionInfo { isReplaced: isReplaced ?? false, inputAddresses: inputAddresses, outputAddresses: outputAddresses, - confirmations: info.confirmations); + confirmations: info.confirmations, + additionalInfo: additionalInfo); } Map toJson() { @@ -265,10 +269,11 @@ class ElectrumTransactionInfo extends TransactionInfo { m['inputAddresses'] = inputAddresses; m['outputAddresses'] = outputAddresses; m['isReceivedSilentPayment'] = isReceivedSilentPayment; + m['additionalInfo'] = additionalInfo; return m; } String toString() { - return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, isReplaced: $isReplaced, confirmations: $confirmations, to: $to, unspent: $unspents, inputAddresses: $inputAddresses, outputAddresses: $outputAddresses)'; + return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, isReplaced: $isReplaced, confirmations: $confirmations, to: $to, unspent: $unspents, inputAddresses: $inputAddresses, outputAddresses: $outputAddresses, additionalInfo: $additionalInfo)'; } } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index c05095cf1..7988a4479 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -5,6 +5,7 @@ import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_wallet.dart'; +import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:collection/collection.dart'; @@ -52,10 +53,9 @@ part 'electrum_wallet.g.dart'; class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet; -abstract class ElectrumWalletBase extends WalletBase< - ElectrumBalance, - ElectrumTransactionHistory, - ElectrumTransactionInfo> with Store, WalletKeysFile { +abstract class ElectrumWalletBase + extends WalletBase + with Store, WalletKeysFile { ElectrumWalletBase({ required String password, required WalletInfo walletInfo, @@ -71,8 +71,8 @@ abstract class ElectrumWalletBase extends WalletBase< ElectrumBalance? initialBalance, CryptoCurrency? currency, this.alwaysScan, - }) : accountHD = getAccountHDWallet( - currency, network, seedBytes, xpub, walletInfo.derivationInfo), + }) : accountHD = + getAccountHDWallet(currency, network, seedBytes, xpub, walletInfo.derivationInfo), syncStatus = NotConnectedSyncStatus(), _password = password, _feeRates = [], @@ -107,12 +107,8 @@ abstract class ElectrumWalletBase extends WalletBase< sharedPrefs.complete(SharedPreferences.getInstance()); } - static Bip32Slip10Secp256k1 getAccountHDWallet( - CryptoCurrency? currency, - BasedUtxoNetwork network, - Uint8List? seedBytes, - String? xpub, - DerivationInfo? derivationInfo) { + static Bip32Slip10Secp256k1 getAccountHDWallet(CryptoCurrency? currency, BasedUtxoNetwork network, + Uint8List? seedBytes, String? xpub, DerivationInfo? derivationInfo) { if (seedBytes == null && xpub == null) { throw Exception( "To create a Wallet you need either a seed or an xpub. This should not happen"); @@ -123,9 +119,8 @@ abstract class ElectrumWalletBase extends WalletBase< case CryptoCurrency.btc: case CryptoCurrency.ltc: case CryptoCurrency.tbtc: - return Bip32Slip10Secp256k1.fromSeed(seedBytes, getKeyNetVersion(network)) - .derivePath(_hardenedDerivationPath( - derivationInfo?.derivationPath ?? electrum_path)) + return Bip32Slip10Secp256k1.fromSeed(seedBytes, getKeyNetVersion(network)).derivePath( + _hardenedDerivationPath(derivationInfo?.derivationPath ?? electrum_path)) as Bip32Slip10Secp256k1; case CryptoCurrency.bch: return bitcoinCashHDWallet(seedBytes); @@ -134,13 +129,11 @@ abstract class ElectrumWalletBase extends WalletBase< } } - return Bip32Slip10Secp256k1.fromExtendedKey( - xpub!, getKeyNetVersion(network)); + return Bip32Slip10Secp256k1.fromExtendedKey(xpub!, getKeyNetVersion(network)); } static Bip32Slip10Secp256k1 bitcoinCashHDWallet(Uint8List seedBytes) => - Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") - as Bip32Slip10Secp256k1; + Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") as Bip32Slip10Secp256k1; static int estimatedTransactionSize(int inputsCount, int outputsCounts) => inputsCount * 68 + outputsCounts * 34 + 10; @@ -250,7 +243,7 @@ abstract class ElectrumWalletBase extends WalletBase< } if (tip > walletInfo.restoreHeight) { - _setListeners(walletInfo.restoreHeight, chainTipParam: _currentChainTip); + _setListeners(walletInfo.restoreHeight, chainTipParam: currentChainTip); } } else { alwaysScan = false; @@ -265,23 +258,23 @@ abstract class ElectrumWalletBase extends WalletBase< } } - int? _currentChainTip; + int? currentChainTip; Future getCurrentChainTip() async { - if ((_currentChainTip ?? 0) > 0) { - return _currentChainTip!; + if ((currentChainTip ?? 0) > 0) { + return currentChainTip!; } - _currentChainTip = await electrumClient.getCurrentBlockChainTip() ?? 0; + currentChainTip = await electrumClient.getCurrentBlockChainTip() ?? 0; - return _currentChainTip!; + return currentChainTip!; } Future getUpdatedChainTip() async { final newTip = await electrumClient.getCurrentBlockChainTip(); - if (newTip != null && newTip > (_currentChainTip ?? 0)) { - _currentChainTip = newTip; + if (newTip != null && newTip > (currentChainTip ?? 0)) { + currentChainTip = newTip; } - return _currentChainTip ?? 0; + return currentChainTip ?? 0; } @override @@ -357,7 +350,7 @@ abstract class ElectrumWalletBase extends WalletBase< isSingleScan: doSingleScan ?? false, )); - _receiveStream?.cancel(); + await _receiveStream?.cancel(); _receiveStream = receivePort.listen((var message) async { if (message is Map) { for (final map in message.entries) { @@ -618,7 +611,7 @@ abstract class ElectrumWalletBase extends WalletBase< bool spendsUnconfirmedTX = false; int leftAmount = credentialsAmount; - final availableInputs = unspentCoins.where((utx) { + var availableInputs = unspentCoins.where((utx) { if (!utx.isSending || utx.isFrozen) { return false; } @@ -634,6 +627,9 @@ abstract class ElectrumWalletBase extends WalletBase< }).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); + for (int i = 0; i < availableInputs.length; i++) { final utx = availableInputs[i]; if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0; @@ -652,9 +648,8 @@ abstract class ElectrumWalletBase extends WalletBase< ECPrivate? privkey; bool? isSilentPayment = false; - final hd = utx.bitcoinAddressRecord.isHidden - ? walletAddresses.sideHd - : walletAddresses.mainHd; + final hd = + utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd; if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) { final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord; @@ -1233,8 +1228,7 @@ abstract class ElectrumWalletBase extends WalletBase< } } - void setLedgerConnection(ledger.LedgerConnection connection) => - throw UnimplementedError(); + void setLedgerConnection(ledger.LedgerConnection connection) => throw UnimplementedError(); Future buildHardwareWalletTransaction({ required List outputs, @@ -1593,9 +1587,7 @@ abstract class ElectrumWalletBase extends WalletBase< final btcAddress = RegexUtils.addressTypeFromStr(addressRecord.address, network); final privkey = generateECPrivate( - hd: addressRecord.isHidden - ? walletAddresses.sideHd - : walletAddresses.mainHd, + hd: addressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, index: addressRecord.index, network: network); @@ -1777,8 +1769,7 @@ abstract class ElectrumWalletBase extends WalletBase< if (height != null) { if (time == null && height > 0) { - time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000) - .round(); + time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round(); } if (confirmations == null) { @@ -1847,6 +1838,7 @@ abstract class ElectrumWalletBase extends WalletBase< .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); } else if (type == WalletType.litecoin) { await Future.wait(LITECOIN_ADDRESS_TYPES + .where((type) => type != SegwitAddresType.mweb) .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); } @@ -1958,6 +1950,20 @@ abstract class ElectrumWalletBase extends WalletBase< // Got a new transaction fetched, add it to the transaction history // instead of waiting all to finish, and next time it will be faster + + if (this is LitecoinWallet) { + // if we have a peg out transaction with the same value + // that matches this received transaction, mark it as being from a peg out: + for (final tx2 in transactionHistory.transactions.values) { + final heightDiff = ((tx2.height ?? 0) - (tx.height ?? 0)).abs(); + // this isn't a perfect matching algorithm since we don't have the right input/output information from these transaction models (the addresses are in different formats), but this should be more than good enough for now as it's extremely unlikely a user receives the EXACT same amount from 2 different sources and one of them is a peg out and the other isn't WITHIN 5 blocks of each other + if (tx2.additionalInfo["isPegOut"] == true && + tx2.amount == tx.amount && + heightDiff <= 5) { + tx.additionalInfo["fromPegOut"] = true; + } + } + } transactionHistory.addOne(tx); await transactionHistory.save(); } @@ -1984,18 +1990,28 @@ abstract class ElectrumWalletBase extends WalletBase< if (_isTransactionUpdating) { return; } - await getCurrentChainTip(); + currentChainTip = await getUpdatedChainTip(); + bool updated = false; transactionHistory.transactions.values.forEach((tx) { - if (tx.unspents != null && - tx.unspents!.isNotEmpty && - tx.height != null && - tx.height! > 0 && - (_currentChainTip ?? 0) > 0) { - tx.confirmations = _currentChainTip! - tx.height! + 1; + if ((tx.height ?? 0) > 0 && (currentChainTip ?? 0) > 0) { + var confirmations = currentChainTip! - tx.height! + 1; + if (confirmations < 0) { + // if our chain tip is outdated then it could lead to negative confirmations so this is just a failsafe: + confirmations = 0; + } + if (confirmations != tx.confirmations) { + updated = true; + tx.confirmations = confirmations; + transactionHistory.addOne(tx); + } } }); + if (updated) { + await transactionHistory.save(); + } + _isTransactionUpdating = true; await fetchTransactions(); walletAddresses.updateReceiveAddresses(); @@ -2043,6 +2059,8 @@ abstract class ElectrumWalletBase extends WalletBase< library: this.runtimeType.toString(), )); } + }, onError: (e, s) { + print("sub_listen error: $e $s"); }); })); } @@ -2092,6 +2110,13 @@ abstract class ElectrumWalletBase extends WalletBase< final balances = await Future.wait(balanceFutures); + if (balances.isNotEmpty && balances.first['confirmed'] == null) { + // if we got null balance responses from the server, set our connection status to lost and return our last known balance: + print("got null balance responses from the server, setting connection status to lost"); + syncStatus = LostConnectionSyncStatus(); + return balance[currency] ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); + } + for (var i = 0; i < balances.length; i++) { final addressRecord = addresses[i]; final balance = balances[i]; @@ -2197,10 +2222,10 @@ abstract class ElectrumWalletBase extends WalletBase< Future _setInitialHeight() async { if (_chainTipUpdateSubject != null) return; - _currentChainTip = await getUpdatedChainTip(); + currentChainTip = await getUpdatedChainTip(); - if ((_currentChainTip == null || _currentChainTip! == 0) && walletInfo.restoreHeight == 0) { - await walletInfo.updateRestoreHeight(_currentChainTip!); + if ((currentChainTip == null || currentChainTip! == 0) && walletInfo.restoreHeight == 0) { + await walletInfo.updateRestoreHeight(currentChainTip!); } _chainTipUpdateSubject = electrumClient.chainTipSubscribe(); @@ -2209,7 +2234,7 @@ abstract class ElectrumWalletBase extends WalletBase< final height = int.tryParse(event['height'].toString()); if (height != null) { - _currentChainTip = height; + currentChainTip = height; if (alwaysScan == true && syncStatus is SyncedSyncStatus) { _setListeners(walletInfo.restoreHeight); @@ -2223,7 +2248,6 @@ abstract class ElectrumWalletBase extends WalletBase< @action void _onConnectionStatusChange(ConnectionStatus status) { - switch (status) { case ConnectionStatus.connected: if (syncStatus is NotConnectedSyncStatus || @@ -2270,8 +2294,6 @@ abstract class ElectrumWalletBase extends WalletBase< Timer(Duration(seconds: 5), () { if (this.syncStatus is NotConnectedSyncStatus || this.syncStatus is LostConnectionSyncStatus) { - if (node == null) return; - this.electrumClient.connectToUri( node!.uri, useSSL: node!.useSSL ?? false, diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 1fb39c878..86228fc83 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -9,6 +9,7 @@ import 'package:crypto/crypto.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/mweb_utxo.dart'; +import 'package:cw_core/node.dart'; import 'package:cw_mweb/mwebd.pbgrpc.dart'; import 'package:fixnum/fixnum.dart'; import 'package:bip39/bip39.dart' as bip39; @@ -47,6 +48,7 @@ import 'package:cw_mweb/cw_mweb.dart'; import 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart'; import 'package:pointycastle/ecc/api.dart'; import 'package:pointycastle/ecc/curves/secp256k1.dart'; +import 'package:shared_preferences/shared_preferences.dart'; part 'litecoin_wallet.g.dart'; @@ -85,8 +87,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { alwaysScan: alwaysScan, ) { if (seedBytes != null) { - mwebHd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath( - "m/1000'") as Bip32Slip10Secp256k1; + mwebHd = + Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/1000'") as Bip32Slip10Secp256k1; mwebEnabled = alwaysScan ?? false; } else { mwebHd = null; @@ -287,6 +289,16 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { await (walletAddresses as LitecoinWalletAddresses).ensureMwebAddressUpToIndexExists(1020); } + @action + @override + Future connectToNode({required Node node}) async { + await super.connectToNode(node: node); + + final prefs = await SharedPreferences.getInstance(); + final mwebNodeUri = prefs.getString("mwebNodeUri") ?? "ltc-electrum.cakewallet.com:9333"; + await CwMweb.setNodeUriOverride(mwebNodeUri); + } + @action @override Future startSync() async { @@ -349,6 +361,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return; } + // update the current chain tip so that confirmation calculations are accurate: + currentChainTip = nodeHeight; + final resp = await CwMweb.status(StatusRequest()); try { @@ -361,22 +376,46 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } else if (resp.mwebUtxosHeight < nodeHeight) { mwebSyncStatus = SyncingSyncStatus(1, 0.999); } else { + bool confirmationsUpdated = false; if (resp.mwebUtxosHeight > walletInfo.restoreHeight) { await walletInfo.updateRestoreHeight(resp.mwebUtxosHeight); await checkMwebUtxosSpent(); // update the confirmations for each transaction: - for (final transaction in transactionHistory.transactions.values) { - if (transaction.isPending) continue; - int txHeight = transaction.height ?? resp.mwebUtxosHeight; - final confirmations = (resp.mwebUtxosHeight - txHeight) + 1; - if (transaction.confirmations == confirmations) continue; - if (transaction.confirmations == 0) { - updateBalance(); + for (final tx in transactionHistory.transactions.values) { + if (tx.height == null || tx.height == 0) { + // update with first confirmation on next block since it hasn't been confirmed yet: + tx.height = resp.mwebUtxosHeight; + continue; } - transaction.confirmations = confirmations; - transactionHistory.addOne(transaction); + + final confirmations = (resp.mwebUtxosHeight - tx.height!) + 1; + + // if the confirmations haven't changed, skip updating: + if (tx.confirmations == confirmations) continue; + + + // if an outgoing tx is now confirmed, delete the utxo from the box (delete the unspent coin): + if (confirmations >= 2 && + tx.direction == TransactionDirection.outgoing && + tx.unspents != null) { + for (var coin in tx.unspents!) { + final utxo = mwebUtxosBox.get(coin.address); + if (utxo != null) { + print("deleting utxo ${coin.address} @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); + await mwebUtxosBox.delete(coin.address); + } + } + } + + tx.confirmations = confirmations; + tx.isPending = false; + transactionHistory.addOne(tx); + confirmationsUpdated = true; + } + if (confirmationsUpdated) { + await transactionHistory.save(); + await updateTransactions(); } - await transactionHistory.save(); } // prevent unnecessary reaction triggers: @@ -501,13 +540,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { outputAddresses: [utxo.outputId], isReplaced: false, ); - } - - // don't update the confirmations if the tx is updated by electrum: - if (tx.confirmations == 0 || utxo.height != 0) { - tx.height = utxo.height; - tx.isPending = utxo.height == 0; - tx.confirmations = confirmations; + } else { + if (tx.confirmations != confirmations || tx.height != utxo.height) { + tx.height = utxo.height; + tx.confirmations = confirmations; + tx.isPending = utxo.height == 0; + } } bool isNew = transactionHistory.transactions[tx.id] == null; @@ -557,56 +595,88 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (responseStream == null) { throw Exception("failed to get utxos stream!"); } - _utxoStream = responseStream.listen((Utxo sUtxo) async { - // we're processing utxos, so our balance could still be innacurate: - if (mwebSyncStatus is! SyncronizingSyncStatus && mwebSyncStatus is! SyncingSyncStatus) { - mwebSyncStatus = SyncronizingSyncStatus(); - processingUtxos = true; - _processingTimer?.cancel(); - _processingTimer = Timer.periodic(const Duration(seconds: 2), (timer) async { - processingUtxos = false; - timer.cancel(); - }); - } - - final utxo = MwebUtxo( - address: sUtxo.address, - blockTime: sUtxo.blockTime, - height: sUtxo.height, - outputId: sUtxo.outputId, - value: sUtxo.value.toInt(), - ); - - if (mwebUtxosBox.containsKey(utxo.outputId)) { - // we've already stored this utxo, skip it: - // but do update the utxo height if it's somehow different: - final existingUtxo = mwebUtxosBox.get(utxo.outputId); - if (existingUtxo!.height != utxo.height) { - print( - "updating utxo height for $utxo.outputId: ${existingUtxo.height} -> ${utxo.height}"); - existingUtxo.height = utxo.height; - await mwebUtxosBox.put(utxo.outputId, existingUtxo); + _utxoStream = responseStream.listen( + (Utxo sUtxo) async { + // we're processing utxos, so our balance could still be innacurate: + if (mwebSyncStatus is! SyncronizingSyncStatus && mwebSyncStatus is! SyncingSyncStatus) { + mwebSyncStatus = SyncronizingSyncStatus(); + processingUtxos = true; + _processingTimer?.cancel(); + _processingTimer = Timer.periodic(const Duration(seconds: 2), (timer) async { + processingUtxos = false; + timer.cancel(); + }); } - return; - } - await updateUnspent(); - await updateBalance(); + final utxo = MwebUtxo( + address: sUtxo.address, + blockTime: sUtxo.blockTime, + height: sUtxo.height, + outputId: sUtxo.outputId, + value: sUtxo.value.toInt(), + ); - final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; + if (mwebUtxosBox.containsKey(utxo.outputId)) { + // we've already stored this utxo, skip it: + // but do update the utxo height if it's somehow different: + final existingUtxo = mwebUtxosBox.get(utxo.outputId); + if (existingUtxo!.height != utxo.height) { + print( + "updating utxo height for $utxo.outputId: ${existingUtxo.height} -> ${utxo.height}"); + existingUtxo.height = utxo.height; + await mwebUtxosBox.put(utxo.outputId, existingUtxo); + } + return; + } - // don't process utxos with addresses that are not in the mwebAddrs list: - if (utxo.address.isNotEmpty && !mwebAddrs.contains(utxo.address)) { - return; - } + await updateUnspent(); + await updateBalance(); - await mwebUtxosBox.put(utxo.outputId, utxo); + final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; - await handleIncoming(utxo); - }); + // don't process utxos with addresses that are not in the mwebAddrs list: + if (utxo.address.isNotEmpty && !mwebAddrs.contains(utxo.address)) { + return; + } + + await mwebUtxosBox.put(utxo.outputId, utxo); + + await handleIncoming(utxo); + }, + onError: (error) { + print("error in utxo stream: $error"); + mwebSyncStatus = FailedSyncStatus(error: error.toString()); + }, + cancelOnError: true, + ); + } + + Future deleteSpentUtxos() async { + print("deleteSpentUtxos() called!"); + final chainHeight = await electrumClient.getCurrentBlockChainTip(); + final status = await CwMweb.status(StatusRequest()); + if (chainHeight == null || status.blockHeaderHeight != chainHeight) return; + if (status.mwebUtxosHeight != chainHeight) return; // we aren't synced + + // delete any spent utxos with >= 2 confirmations: + final spentOutputIds = mwebUtxosBox.values + .where((utxo) => utxo.spent && (chainHeight - utxo.height) >= 2) + .map((utxo) => utxo.outputId) + .toList(); + + if (spentOutputIds.isEmpty) return; + + final resp = await CwMweb.spent(SpentRequest(outputId: spentOutputIds)); + final spent = resp.outputId; + if (spent.isEmpty) return; + + for (final outputId in spent) { + await mwebUtxosBox.delete(outputId); + } } Future checkMwebUtxosSpent() async { + print("checkMwebUtxosSpent() called!"); if (!mwebEnabled) { return; } @@ -620,15 +690,17 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { updatedAny = await isConfirmed(tx) || updatedAny; } + await deleteSpentUtxos(); + // get output ids of all the mweb utxos that have > 0 height: - final outputIds = - mwebUtxosBox.values.where((utxo) => utxo.height > 0).map((utxo) => utxo.outputId).toList(); + final outputIds = mwebUtxosBox.values + .where((utxo) => utxo.height > 0 && !utxo.spent) + .map((utxo) => utxo.outputId) + .toList(); final resp = await CwMweb.spent(SpentRequest(outputId: outputIds)); final spent = resp.outputId; - if (spent.isEmpty) { - return; - } + if (spent.isEmpty) return; final status = await CwMweb.status(StatusRequest()); final height = await electrumClient.getCurrentBlockChainTip(); @@ -739,7 +811,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { mwebUtxosBox.keys.forEach((dynamic oId) { final String outputId = oId as String; final utxo = mwebUtxosBox.get(outputId); - if (utxo == null) { + if (utxo == null || utxo.spent) { return; } if (utxo.address.isEmpty) { @@ -789,15 +861,23 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { int unconfirmedMweb = 0; try { mwebUtxosBox.values.forEach((utxo) { - if (utxo.height > 0) { + bool isConfirmed = utxo.height > 0; + + print( + "utxo: ${isConfirmed ? "confirmed" : "unconfirmed"} ${utxo.spent ? "spent" : "unspent"} ${utxo.outputId} ${utxo.height} ${utxo.value}"); + + if (isConfirmed) { confirmedMweb += utxo.value.toInt(); - } else { + } + + if (isConfirmed && utxo.spent) { + unconfirmedMweb -= utxo.value.toInt(); + } + + if (!isConfirmed && !utxo.spent) { unconfirmedMweb += utxo.value.toInt(); } }); - if (unconfirmedMweb > 0) { - unconfirmedMweb = -1 * (confirmedMweb - unconfirmedMweb); - } } catch (_) {} for (var addressRecord in walletAddresses.allAddresses) { @@ -829,7 +909,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { // update the txCount for each address using the tx history, since we can't rely on mwebd // to have an accurate count, we should just keep it in sync with what we know from the tx history: for (final tx in transactionHistory.transactions.values) { - // if (tx.isPending) continue; if (tx.inputAddresses == null || tx.outputAddresses == null) { continue; } @@ -908,7 +987,26 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { // https://github.com/ltcmweb/mwebd?tab=readme-ov-file#fee-estimation final preOutputSum = outputs.fold(BigInt.zero, (acc, output) => acc + output.toOutput.amount); - final fee = utxos.sumOfUtxosValue() - preOutputSum; + var fee = utxos.sumOfUtxosValue() - preOutputSum; + + // determines if the fee is correct: + BigInt _sumOutputAmounts(List outputs) { + BigInt sum = BigInt.zero; + for (final e in outputs) { + sum += e.amount; + } + return sum; + } + + final sum1 = _sumOutputAmounts(outputs.map((e) => e.toOutput).toList()) + fee; + final sum2 = utxos.sumOfUtxosValue(); + if (sum1 != sum2) { + print("@@@@@ WE HAD TO ADJUST THE FEE! @@@@@@@@"); + final diff = sum2 - sum1; + // add the difference to the fee (abs value): + fee += diff.abs(); + } + final txb = BitcoinTransactionBuilder(utxos: utxos, outputs: outputs, fee: fee, network: network); final resp = await CwMweb.create(CreateRequest( @@ -949,8 +1047,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (!mwebEnabled) { tx.changeAddressOverride = - (await (walletAddresses as LitecoinWalletAddresses) - .getChangeAddress(isPegIn: false)) + (await (walletAddresses as LitecoinWalletAddresses).getChangeAddress(isPegIn: false)) .address; return tx; } @@ -969,15 +1066,25 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { bool hasMwebInput = false; bool hasMwebOutput = false; + bool hasRegularOutput = false; for (final output in transactionCredentials.outputs) { - if (output.extractedAddress?.toLowerCase().contains("mweb") ?? false) { + final address = output.address.toLowerCase(); + final extractedAddress = output.extractedAddress?.toLowerCase(); + + if (address.contains("mweb")) { hasMwebOutput = true; - break; } - if (output.address.toLowerCase().contains("mweb")) { - hasMwebOutput = true; - break; + if (!address.contains("mweb")) { + hasRegularOutput = true; + } + if (extractedAddress != null && extractedAddress.isNotEmpty) { + if (extractedAddress.contains("mweb")) { + hasMwebOutput = true; + } + if (!extractedAddress.contains("mweb")) { + hasRegularOutput = true; + } } } @@ -989,11 +1096,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } bool isPegIn = !hasMwebInput && hasMwebOutput; + bool isPegOut = hasMwebInput && hasRegularOutput; bool isRegular = !hasMwebInput && !hasMwebOutput; - tx.changeAddressOverride = - (await (walletAddresses as LitecoinWalletAddresses) - .getChangeAddress(isPegIn: isPegIn || isRegular)) - .address; + tx.changeAddressOverride = (await (walletAddresses as LitecoinWalletAddresses) + .getChangeAddress(isPegIn: isPegIn || isRegular)) + .address; if (!hasMwebInput && !hasMwebOutput) { tx.isMweb = false; return tx; @@ -1046,8 +1153,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final addresses = {}; transaction.inputAddresses?.forEach((id) async { final utxo = mwebUtxosBox.get(id); - await mwebUtxosBox.delete(id); // gets deleted in checkMwebUtxosSpent + // await mwebUtxosBox.delete(id); // gets deleted in checkMwebUtxosSpent if (utxo == null) return; + // mark utxo as spent so we add it to the unconfirmed balance (as negative): + utxo.spent = true; + await mwebUtxosBox.put(id, utxo); final addressRecord = walletAddresses.allAddresses .firstWhere((addressRecord) => addressRecord.address == utxo.address); if (!addresses.contains(utxo.address)) { @@ -1056,7 +1166,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { addressRecord.balance -= utxo.value.toInt(); }); transaction.inputAddresses?.addAll(addresses); - + print("isPegIn: $isPegIn, isPegOut: $isPegOut"); + transaction.additionalInfo["isPegIn"] = isPegIn; + transaction.additionalInfo["isPegOut"] = isPegOut; transactionHistory.addOne(transaction); await updateUnspent(); await updateBalance(); @@ -1240,8 +1352,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @override void setLedgerConnection(LedgerConnection connection) { _ledgerConnection = connection; - _litecoinLedgerApp = - LitecoinLedgerApp(_ledgerConnection!, derivationPath: walletInfo.derivationInfo!.derivationPath!); + _litecoinLedgerApp = LitecoinLedgerApp(_ledgerConnection!, + derivationPath: walletInfo.derivationInfo!.derivationPath!); } @override @@ -1277,19 +1389,17 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (maybeChangePath != null) changePath ??= maybeChangePath.derivationPath; } - final rawHex = await _litecoinLedgerApp!.createTransaction( - inputs: readyInputs, - outputs: outputs - .map((e) => TransactionOutput.fromBigInt( - (e as BitcoinOutput).value, Uint8List.fromList(e.address.toScriptPubKey().toBytes()))) - .toList(), - changePath: changePath, - sigHashType: 0x01, - additionals: ["bech32"], - isSegWit: true, - useTrustedInputForSegwit: true - ); + inputs: readyInputs, + outputs: outputs + .map((e) => TransactionOutput.fromBigInt((e as BitcoinOutput).value, + Uint8List.fromList(e.address.toScriptPubKey().toBytes()))) + .toList(), + changePath: changePath, + sigHashType: 0x01, + additionals: ["bech32"], + isSegWit: true, + useTrustedInputForSegwit: true); return BtcTransaction.fromRaw(rawHex); } diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index c55f5fc76..062c590ba 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -16,11 +16,9 @@ import 'package:mobx/mobx.dart'; part 'litecoin_wallet_addresses.g.dart'; -class LitecoinWalletAddresses = LitecoinWalletAddressesBase - with _$LitecoinWalletAddresses; +class LitecoinWalletAddresses = LitecoinWalletAddressesBase with _$LitecoinWalletAddresses; -abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses - with Store { +abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Store { LitecoinWalletAddressesBase( WalletInfo walletInfo, { required super.mainHd, @@ -46,8 +44,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses List mwebAddrs = []; bool generating = false; - List get scanSecret => - mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; + List get scanSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; List get spendPubkey => mwebHd!.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed; @@ -203,4 +200,12 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses return super.getChangeAddress(); } + + @override + String get addressForExchange { + // don't use mweb addresses for exchange refund address: + final addresses = receiveAddresses + .where((element) => element.type == SegwitAddresType.p2wpkh && !element.isUsed); + return addresses.first.address; + } } diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index 7cc266f5b..d519f4d0a 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -112,6 +112,7 @@ class LitecoinWalletService extends WalletService< File neturinoDb = File('$appDirPath/neutrino.db'); File blockHeaders = File('$appDirPath/block_headers.bin'); File regFilterHeaders = File('$appDirPath/reg_filter_headers.bin'); + File mwebdLogs = File('$appDirPath/logs/debug.log'); if (neturinoDb.existsSync()) { neturinoDb.deleteSync(); } @@ -121,6 +122,9 @@ class LitecoinWalletService extends WalletService< if (regFilterHeaders.existsSync()) { regFilterHeaders.deleteSync(); } + if (mwebdLogs.existsSync()) { + mwebdLogs.deleteSync(); + } } } diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 5ed84dbf4..140965655 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -118,8 +118,7 @@ class PendingBitcoinTransaction with PendingTransaction { Future _ltcCommit() async { try { - final stub = await CwMweb.stub(); - final resp = await stub.broadcast(BroadcastRequest(rawTx: BytesUtils.fromHexString(hex))); + final resp = await CwMweb.broadcast(BroadcastRequest(rawTx: BytesUtils.fromHexString(hex))); idOverride = resp.txid; } on GrpcError catch (e) { throw BitcoinTransactionCommitFailed(errorMessage: e.message); diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 9f1cee67d..7a1576a98 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -64,7 +64,7 @@ dependency_overrides: bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v8 + ref: cake-update-v9 pointycastle: 3.7.4 ffi: 2.1.0 diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index cd1e52f51..14ff407e2 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -42,7 +42,7 @@ dependency_overrides: bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v8 + ref: cake-update-v9 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_core/lib/mweb_utxo.dart b/cw_core/lib/mweb_utxo.dart index f8dfab395..5eab11734 100644 --- a/cw_core/lib/mweb_utxo.dart +++ b/cw_core/lib/mweb_utxo.dart @@ -11,6 +11,7 @@ class MwebUtxo extends HiveObject { required this.address, required this.outputId, required this.blockTime, + this.spent = false, }); static const typeId = MWEB_UTXO_TYPE_ID; @@ -30,4 +31,7 @@ class MwebUtxo extends HiveObject { @HiveField(4) int blockTime; + + @HiveField(5, defaultValue: false) + bool spent; } diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index e19d2a54b..1094b6402 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -79,6 +79,9 @@ class Node extends HiveObject with Keyable { @HiveField(9) bool? supportsSilentPayments; + @HiveField(10) + bool? supportsMweb; + bool get isSSL => useSSL ?? false; bool get useSocksProxy => socksProxyAddress == null ? false : socksProxyAddress!.isNotEmpty; diff --git a/cw_core/lib/transaction_info.dart b/cw_core/lib/transaction_info.dart index 9d0c968d8..3e34af75f 100644 --- a/cw_core/lib/transaction_info.dart +++ b/cw_core/lib/transaction_info.dart @@ -25,6 +25,5 @@ abstract class TransactionInfo extends Object with Keyable { @override dynamic get keyIndex => id; - late Map additionalInfo; + Map additionalInfo = {}; } - diff --git a/cw_mweb/lib/cw_mweb.dart b/cw_mweb/lib/cw_mweb.dart index a1a592fb8..75cc0bcca 100644 --- a/cw_mweb/lib/cw_mweb.dart +++ b/cw_mweb/lib/cw_mweb.dart @@ -13,8 +13,18 @@ class CwMweb { static RpcClient? _rpcClient; static ClientChannel? _clientChannel; static int? _port; - static const TIMEOUT_DURATION = Duration(seconds: 5); + static const TIMEOUT_DURATION = Duration(seconds: 15); static Timer? logTimer; + static String? nodeUriOverride; + + + static Future setNodeUriOverride(String uri) async { + nodeUriOverride = uri; + if (_rpcClient != null) { + await stop(); + // will be re-started automatically when the next rpc call is made + } + } static void readFileWithTimer(String filePath) { final file = File(filePath); @@ -47,7 +57,7 @@ class CwMweb { String debugLogPath = "${appDir.path}/logs/debug.log"; readFileWithTimer(debugLogPath); - _port = await CwMwebPlatform.instance.start(appDir.path, ltcNodeUri); + _port = await CwMwebPlatform.instance.start(appDir.path, nodeUriOverride ?? ltcNodeUri); if (_port == null || _port == 0) { throw Exception("Failed to start server"); } @@ -197,4 +207,18 @@ class CwMweb { } return null; } + + static Future broadcast(BroadcastRequest request) async { + log("mweb.broadcast() called"); + try { + _rpcClient = await stub(); + return await _rpcClient!.broadcast(request, options: CallOptions(timeout: TIMEOUT_DURATION)); + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); + throw "error from broadcast mweb: $e"; + } catch (e) { + log("Error getting create: $e"); + rethrow; + } + } } diff --git a/lib/di.dart b/lib/di.dart index 13ffd839e..14fdbd56d 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -35,6 +35,8 @@ import 'package:cake_wallet/entities/parse_address_from_domain.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/receive/address_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/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'; @@ -945,6 +947,10 @@ Future setup({ getIt.registerFactory(() => MwebSettingsPage(getIt.get())); + getIt.registerFactory(() => MwebLogsPage(getIt.get())); + + getIt.registerFactory(() => MwebNodePage(getIt.get())); + getIt.registerFactory(() => OtherSettingsPage(getIt.get())); getIt.registerFactory(() => NanoChangeRepPage( diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 73556452c..8e8bfb6ed 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -54,6 +54,7 @@ class PreferencesKey { static const mwebEnabled = 'mwebEnabled'; static const hasEnabledMwebBefore = 'hasEnabledMwebBefore'; static const mwebAlwaysScan = 'mwebAlwaysScan'; + static const mwebNodeUri = 'mwebNodeUri'; static const shouldShowReceiveWarning = 'should_show_receive_warning'; static const shouldShowYatPopup = 'should_show_yat_popup'; static const shouldShowRepWarning = 'should_show_rep_warning'; diff --git a/lib/router.dart b/lib/router.dart index 3b4c38546..6c234ff80 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -72,6 +72,8 @@ import 'package:cake_wallet/src/screens/settings/desktop_settings/desktop_settin import 'package:cake_wallet/src/screens/settings/display_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/domain_lookups_page.dart'; import 'package:cake_wallet/src/screens/settings/manage_nodes_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/settings/mweb_settings.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; @@ -461,6 +463,14 @@ Route createRoute(RouteSettings settings) { return CupertinoPageRoute( fullscreenDialog: true, builder: (_) => getIt.get()); + case Routes.mwebLogs: + return CupertinoPageRoute( + fullscreenDialog: true, builder: (_) => getIt.get()); + + case Routes.mwebNode: + return CupertinoPageRoute( + fullscreenDialog: true, builder: (_) => getIt.get()); + case Routes.connectionSync: return CupertinoPageRoute( fullscreenDialog: true, builder: (_) => getIt.get()); diff --git a/lib/routes.dart b/lib/routes.dart index 0529d7c6f..5f11b11a3 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -74,6 +74,8 @@ class Routes { static const webViewPage = '/web_view_page'; static const silentPaymentsSettings = '/silent_payments_settings'; static const mwebSettings = '/mweb_settings'; + static const mwebLogs = '/mweb_logs'; + static const mwebNode = '/mweb_node'; static const connectionSync = '/connection_sync_page'; static const securityBackupPage = '/security_and_backup_page'; static const privacyPage = '/privacy_page'; diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index 028b3cfcf..b16a7b090 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -886,17 +886,37 @@ class BalanceRowWidget extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - '${secondAvailableBalanceLabel}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .labelTextColor, - height: 1, + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => launchUrl( + Uri.parse( + "https://guides.cakewallet.com/docs/cryptos/litecoin/#mweb"), + mode: LaunchMode.externalApplication, + ), + child: Row( + children: [ + Text( + '${secondAvailableBalanceLabel}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .labelTextColor, + height: 1, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Icon(Icons.help_outline, + size: 16, + color: Theme.of(context) + .extension()! + .labelTextColor), + ) + ], ), ), SizedBox(height: 8), diff --git a/lib/src/screens/settings/mweb_logs_page.dart b/lib/src/screens/settings/mweb_logs_page.dart new file mode 100644 index 000000000..3c5470214 --- /dev/null +++ b/lib/src/screens/settings/mweb_logs_page.dart @@ -0,0 +1,127 @@ +import 'dart:io'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/utils/exception_handler.dart'; +import 'package:cake_wallet/utils/share_util.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/settings/mweb_settings_view_model.dart'; +import 'package:cw_core/root_dir.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; + +class MwebLogsPage extends BasePage { + MwebLogsPage(this.mwebSettingsViewModelBase); + + final MwebSettingsViewModelBase mwebSettingsViewModelBase; + + @override + String get title => S.current.litecoin_mweb_logs; + + @override + Widget body(BuildContext context) { + return Stack( + fit: StackFit.expand, + children: [ + FutureBuilder( + future: mwebSettingsViewModelBase.getAbbreviatedLogs(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) { + return Center(child: Text('No logs found')); + } else { + return SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Text( + snapshot.data!, + style: TextStyle(fontFamily: 'Monospace'), + ), + ), + ); + } + }, + ), + Positioned( + child: LoadingPrimaryButton( + onPressed: () => onExportLogs(context), + text: S.of(context).export_logs, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + ), + bottom: 24, + left: 24, + right: 24, + ) + ], + ); + } + + void onExportLogs(BuildContext context) { + if (Platform.isAndroid) { + onExportAndroid(context); + } else if (Platform.isIOS) { + share(context); + } else { + _saveFile(); + } + } + + void onExportAndroid(BuildContext context) { + showPopUp( + context: context, + builder: (dialogContext) { + return AlertWithTwoActions( + alertTitle: S.of(context).export_backup, + alertContent: S.of(context).select_destination, + rightButtonText: S.of(context).save_to_downloads, + leftButtonText: S.of(context).share, + actionRightButton: () async { + const downloadDirPath = "/storage/emulated/0/Download"; + final filePath = downloadDirPath + "/debug.log"; + await mwebSettingsViewModelBase.saveLogsLocally(filePath); + Navigator.of(dialogContext).pop(); + }, + actionLeftButton: () async { + Navigator.of(dialogContext).pop(); + try { + await share(context); + } catch (e, s) { + ExceptionHandler.onError(FlutterErrorDetails( + exception: e, + stack: s, + library: "Export Logs", + )); + } + }); + }); + } + + Future share(BuildContext context) async { + final filePath = (await getAppDir()).path + "/debug.log"; + bool success = await mwebSettingsViewModelBase.saveLogsLocally(filePath); + if (!success) return; + await ShareUtil.shareFile(filePath: filePath, fileName: "debug.log", context: context); + await mwebSettingsViewModelBase.removeLogsLocally(filePath); + } + + Future _saveFile() async { + String? outputFile = await FilePicker.platform + .saveFile(dialogTitle: 'Save Your File to desired location', fileName: "debug.log"); + + try { + final filePath = (await getApplicationSupportDirectory()).path + "/debug.log"; + File debugLogFile = File(filePath); + await debugLogFile.copy(outputFile!); + } catch (exception, stackTrace) { + ExceptionHandler.onError(FlutterErrorDetails( + exception: exception, + stack: stackTrace, + library: "Export Logs", + )); + } + } +} diff --git a/lib/src/screens/settings/mweb_node_page.dart b/lib/src/screens/settings/mweb_node_page.dart new file mode 100644 index 000000000..801ab3ac7 --- /dev/null +++ b/lib/src/screens/settings/mweb_node_page.dart @@ -0,0 +1,56 @@ +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/view_model/settings/mweb_settings_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class MwebNodePage extends BasePage { + MwebNodePage(this.mwebSettingsViewModelBase) + : _nodeUriController = TextEditingController(text: mwebSettingsViewModelBase.mwebNodeUri), + super(); + + final MwebSettingsViewModelBase mwebSettingsViewModelBase; + final TextEditingController _nodeUriController; + + @override + String get title => S.current.litecoin_mweb_node; + + @override + Widget body(BuildContext context) { + return Stack( + fit: StackFit.expand, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 24), + child: Row( + children: [ + Expanded( + child: BaseTextFormField(controller: _nodeUriController), + ) + ], + ), + ), + Positioned( + child: Observer( + builder: (_) => LoadingPrimaryButton( + onPressed: () => save(context), + text: S.of(context).save, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + ), + ), + bottom: 24, + left: 24, + right: 24, + ) + ], + ); + } + + void save(BuildContext context) { + mwebSettingsViewModelBase.setMwebNodeUri(_nodeUriController.text); + Navigator.pop(context); + } +} diff --git a/lib/src/screens/settings/mweb_settings.dart b/lib/src/screens/settings/mweb_settings.dart index 722ffa9aa..e78fdf596 100644 --- a/lib/src/screens/settings/mweb_settings.dart +++ b/lib/src/screens/settings/mweb_settings.dart @@ -31,7 +31,7 @@ class MwebSettingsPage extends BasePage { }, ), SettingsSwitcherCell( - title: S.current.litecoin_mweb_always_scan, + title: S.current.litecoin_mweb_enable, value: _mwebSettingsViewModel.mwebEnabled, onValueChange: (_, bool value) { _mwebSettingsViewModel.setMwebEnabled(value); @@ -41,6 +41,14 @@ class MwebSettingsPage extends BasePage { title: S.current.litecoin_mweb_scanning, handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.rescan), ), + SettingsCellWithArrow( + title: S.current.litecoin_mweb_logs, + handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.mwebLogs), + ), + SettingsCellWithArrow( + title: S.current.litecoin_mweb_node, + handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.mwebNode), + ), ], ), ); diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 2eadbd8a9..c966420db 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -120,6 +120,7 @@ abstract class SettingsStoreBase with Store { required this.mwebCardDisplay, required this.mwebEnabled, required this.hasEnabledMwebBefore, + required this.mwebNodeUri, TransactionPriority? initialBitcoinTransactionPriority, TransactionPriority? initialMoneroTransactionPriority, TransactionPriority? initialWowneroTransactionPriority, @@ -358,8 +359,8 @@ abstract class SettingsStoreBase with Store { reaction( (_) => bitcoinSeedType, - (BitcoinSeedType bitcoinSeedType) => sharedPreferences.setInt( - PreferencesKey.bitcoinSeedType, bitcoinSeedType.raw)); + (BitcoinSeedType bitcoinSeedType) => + sharedPreferences.setInt(PreferencesKey.bitcoinSeedType, bitcoinSeedType.raw)); reaction( (_) => nanoSeedType, @@ -442,8 +443,10 @@ abstract class SettingsStoreBase with Store { reaction((_) => useTronGrid, (bool useTronGrid) => _sharedPreferences.setBool(PreferencesKey.useTronGrid, useTronGrid)); - reaction((_) => useMempoolFeeAPI, - (bool useMempoolFeeAPI) => _sharedPreferences.setBool(PreferencesKey.useMempoolFeeAPI, useMempoolFeeAPI)); + reaction( + (_) => useMempoolFeeAPI, + (bool useMempoolFeeAPI) => + _sharedPreferences.setBool(PreferencesKey.useMempoolFeeAPI, useMempoolFeeAPI)); reaction((_) => defaultNanoRep, (String nanoRep) => _sharedPreferences.setString(PreferencesKey.defaultNanoRep, nanoRep)); @@ -591,6 +594,11 @@ abstract class SettingsStoreBase with Store { (bool hasEnabledMwebBefore) => _sharedPreferences.setBool(PreferencesKey.hasEnabledMwebBefore, hasEnabledMwebBefore)); + reaction( + (_) => mwebNodeUri, + (String mwebNodeUri) => + _sharedPreferences.setString(PreferencesKey.mwebNodeUri, mwebNodeUri)); + this.nodes.observe((change) { if (change.newValue != null && change.key != null) { _saveCurrentNode(change.newValue!, change.key!); @@ -822,6 +830,9 @@ abstract class SettingsStoreBase with Store { @observable bool hasEnabledMwebBefore; + @observable + String mwebNodeUri; + final SecureStorage _secureStorage; final SharedPreferences _sharedPreferences; final BackgroundTasks _backgroundTasks; @@ -988,6 +999,8 @@ abstract class SettingsStoreBase with Store { final mwebEnabled = sharedPreferences.getBool(PreferencesKey.mwebEnabled) ?? false; final hasEnabledMwebBefore = sharedPreferences.getBool(PreferencesKey.hasEnabledMwebBefore) ?? false; + final mwebNodeUri = sharedPreferences.getString(PreferencesKey.mwebNodeUri) ?? + "ltc-electrum.cakewallet.com:9333"; // If no value if (pinLength == null || pinLength == 0) { @@ -1259,6 +1272,7 @@ abstract class SettingsStoreBase with Store { mwebAlwaysScan: mwebAlwaysScan, mwebCardDisplay: mwebCardDisplay, mwebEnabled: mwebEnabled, + mwebNodeUri: mwebNodeUri, hasEnabledMwebBefore: hasEnabledMwebBefore, initialMoneroTransactionPriority: moneroTransactionPriority, initialWowneroTransactionPriority: wowneroTransactionPriority, @@ -1686,7 +1700,8 @@ abstract class SettingsStoreBase with Store { deviceName = windowsInfo.productName; } catch (e) { print(e); - print('likely digitalProductId is null wait till https://github.com/fluttercommunity/plus_plugins/pull/3188 is merged'); + print( + 'likely digitalProductId is null wait till https://github.com/fluttercommunity/plus_plugins/pull/3188 is merged'); deviceName = "Windows Device"; } } diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 56a5935c9..20dca292c 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -381,7 +381,7 @@ abstract class BalanceViewModelBase with Store { bool _hasSecondAdditionalBalanceForWalletType(WalletType type) { if (wallet.type == WalletType.litecoin) { - if ((wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) > 0) { + if ((wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) != 0) { return true; } } diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 47fc32ab6..b218c8e9f 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -56,25 +56,53 @@ class TransactionListItem extends ActionListItem with Keyable { } String get formattedPendingStatus { - if (balanceViewModel.wallet.type == WalletType.monero || - balanceViewModel.wallet.type == WalletType.haven) { - if (transaction.confirmations >= 0 && transaction.confirmations < 10) { - return ' (${transaction.confirmations}/10)'; - } - } else if (balanceViewModel.wallet.type == WalletType.wownero) { - if (transaction.confirmations >= 0 && transaction.confirmations < 3) { - return ' (${transaction.confirmations}/3)'; - } + switch (balanceViewModel.wallet.type) { + case WalletType.monero: + case WalletType.haven: + if (transaction.confirmations >= 0 && transaction.confirmations < 10) { + return ' (${transaction.confirmations}/10)'; + } + break; + case WalletType.wownero: + if (transaction.confirmations >= 0 && transaction.confirmations < 3) { + return ' (${transaction.confirmations}/3)'; + } + break; + case WalletType.litecoin: + bool isPegIn = (transaction.additionalInfo["isPegIn"] as bool?) ?? false; + bool isPegOut = (transaction.additionalInfo["isPegOut"] as bool?) ?? false; + bool fromPegOut = (transaction.additionalInfo["fromPegOut"] as bool?) ?? false; + String str = ''; + if (transaction.confirmations <= 0) { + str = S.current.pending; + } + if ((isPegOut || fromPegOut) && transaction.confirmations >= 0 && transaction.confirmations < 6) { + str = " (${transaction.confirmations}/6)"; + } + if (isPegIn) { + str += " (Peg In)"; + } + if (isPegOut) { + str += " (Peg Out)"; + } + return str; + default: + return ''; } + return ''; } String get formattedStatus { - if (balanceViewModel.wallet.type == WalletType.monero || - balanceViewModel.wallet.type == WalletType.wownero || - balanceViewModel.wallet.type == WalletType.haven) { + if ([ + WalletType.monero, + WalletType.haven, + WalletType.wownero, + WalletType.litecoin, + ].contains(balanceViewModel.wallet.type)) { return formattedPendingStatus; } + return transaction.isPending ? S.current.pending : ''; } diff --git a/lib/view_model/settings/mweb_settings_view_model.dart b/lib/view_model/settings/mweb_settings_view_model.dart index c6370e23f..11e4c8177 100644 --- a/lib/view_model/settings/mweb_settings_view_model.dart +++ b/lib/view_model/settings/mweb_settings_view_model.dart @@ -1,7 +1,12 @@ +import 'dart:io'; + import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/utils/exception_handler.dart'; import 'package:cw_core/wallet_base.dart'; +import 'package:flutter/widgets.dart'; import 'package:mobx/mobx.dart'; +import 'package:path_provider/path_provider.dart'; part 'mweb_settings_view_model.g.dart'; @@ -22,15 +27,60 @@ abstract class MwebSettingsViewModelBase with Store { @observable late bool mwebEnabled; + @computed + String get mwebNodeUri => _settingsStore.mwebNodeUri; + @action void setMwebCardDisplay(bool value) { _settingsStore.mwebCardDisplay = value; } + @action + void setMwebNodeUri(String value) { + _settingsStore.mwebNodeUri = value; + } + @action void setMwebEnabled(bool value) { mwebEnabled = value; bitcoin!.setMwebEnabled(_wallet, value); _settingsStore.mwebAlwaysScan = value; } + + Future saveLogsLocally(String filePath) async { + try { + final appSupportPath = (await getApplicationSupportDirectory()).path; + final logsFile = File("$appSupportPath/logs/debug.log"); + if (!logsFile.existsSync()) { + throw Exception('Logs file does not exist'); + } + await logsFile.copy(filePath); + return true; + } catch (e, s) { + ExceptionHandler.onError(FlutterErrorDetails( + exception: e, + stack: s, + library: "Export Logs", + )); + return false; + } + } + + Future getAbbreviatedLogs() async { + final appSupportPath = (await getApplicationSupportDirectory()).path; + final logsFile = File("$appSupportPath/logs/debug.log"); + if (!logsFile.existsSync()) { + return ""; + } + final logs = logsFile.readAsStringSync(); + // return last 10000 characters: + return logs.substring(logs.length > 10000 ? logs.length - 10000 : 0); + } + + Future removeLogsLocally(String filePath) async { + final logsFile = File(filePath); + if (logsFile.existsSync()) { + await logsFile.delete(); + } + } } diff --git a/pubspec_base.yaml b/pubspec_base.yaml index d5fce76e9..2abd8401a 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -134,7 +134,7 @@ dependency_overrides: bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v8 + ref: cake-update-v9 ffi: 2.1.0 flutter_icons: diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index c2be1504a..0db2031dd 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -295,6 +295,7 @@ "expiresOn": "ﻲﻓ ﻪﺘﻴﺣﻼﺻ ﻲﻬﺘﻨﺗ", "expiry_and_validity": "انتهاء الصلاحية والصلاحية", "export_backup": "تصدير نسخة احتياطية", + "export_logs": "سجلات التصدير", "extra_id": "معرف إضافي:", "extracted_address_content": "سوف ترسل الأموال إلى\n${recipient_name}", "failed_authentication": "${state_error} فشل المصادقة.", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB هو بروتوكول جديد يجلب معاملات أسرع وأرخص وأكثر خصوصية إلى Litecoin", "litecoin_mweb_dismiss": "رفض", "litecoin_mweb_display_card": "عرض بطاقة mweb", + "litecoin_mweb_enable": "تمكين MWEB", "litecoin_mweb_enable_later": "يمكنك اختيار تمكين MWEB مرة أخرى ضمن إعدادات العرض.", + "litecoin_mweb_logs": "سجلات MWEB", + "litecoin_mweb_node": "عقدة MWEB", "litecoin_mweb_pegin": "ربط في", "litecoin_mweb_pegout": "ربط", "litecoin_mweb_scanning": "MWEB المسح الضوئي", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 8b68d0d1e..7e78dd69a 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -295,6 +295,7 @@ "expiresOn": "Изтича на", "expiry_and_validity": "Изтичане и валидност", "export_backup": "Експортиране на резервно копие", + "export_logs": "Експортни дневници", "extra_id": "Допълнително ID:", "extracted_address_content": "Ще изпратите средства на \n${recipient_name}", "failed_authentication": "Неуспешно удостоверяване. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWeb е нов протокол, който носи по -бърз, по -евтин и повече частни транзакции на Litecoin", "litecoin_mweb_dismiss": "Уволнение", "litecoin_mweb_display_card": "Показване на MWEB карта", + "litecoin_mweb_enable": "Активирайте MWeb", "litecoin_mweb_enable_later": "Можете да изберете да активирате MWEB отново под настройките на дисплея.", + "litecoin_mweb_logs": "MWeb logs", + "litecoin_mweb_node": "MWEB възел", "litecoin_mweb_pegin": "PEG в", "litecoin_mweb_pegout": "PEG OUT", "litecoin_mweb_scanning": "Сканиране на MWEB", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 9b48ebdc7..f17466a9f 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -295,6 +295,7 @@ "expiresOn": "Vyprší dne", "expiry_and_validity": "Vypršení a platnost", "export_backup": "Exportovat zálohu", + "export_logs": "Vývozní protokoly", "extra_id": "Extra ID:", "extracted_address_content": "Prostředky budete posílat na\n${recipient_name}", "failed_authentication": "Ověřování selhalo. ${state_error}", @@ -371,7 +372,10 @@ "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", "litecoin_mweb_display_card": "Zobrazit kartu MWeb", + "litecoin_mweb_enable": "Povolit mWeb", "litecoin_mweb_enable_later": "V nastavení zobrazení můžete vybrat znovu povolit MWeb.", + "litecoin_mweb_logs": "Protokoly mWeb", + "litecoin_mweb_node": "Uzel mWeb", "litecoin_mweb_pegin": "Peg in", "litecoin_mweb_pegout": "Zkrachovat", "litecoin_mweb_scanning": "Skenování mWeb", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 2ec59f349..cb0a59574 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -295,6 +295,7 @@ "expiresOn": "Läuft aus am", "expiry_and_validity": "Ablauf und Gültigkeit", "export_backup": "Sicherung exportieren", + "export_logs": "Exportprotokolle", "extra_id": "Extra ID:", "extracted_address_content": "Sie senden Geld an\n${recipient_name}", "failed_authentication": "Authentifizierung fehlgeschlagen. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWWB ist ein neues Protokoll, das schnellere, billigere und privatere Transaktionen zu Litecoin bringt", "litecoin_mweb_dismiss": "Zurückweisen", "litecoin_mweb_display_card": "MWEB-Karte anzeigen", + "litecoin_mweb_enable": "Aktivieren Sie MWeb", "litecoin_mweb_enable_later": "Sie können MWEB unter Anzeigeeinstellungen erneut aktivieren.", + "litecoin_mweb_logs": "MWEB -Protokolle", + "litecoin_mweb_node": "MWEB -Knoten", "litecoin_mweb_pegin": "Peg in", "litecoin_mweb_pegout": "Abstecken", "litecoin_mweb_scanning": "MWEB Scanning", @@ -941,4 +945,4 @@ "you_will_get": "Konvertieren zu", "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 4f5fd23b3..40be8fd08 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -295,6 +295,7 @@ "expiresOn": "Expires on", "expiry_and_validity": "Expiry and Validity", "export_backup": "Export backup", + "export_logs": "Export logs", "extra_id": "Extra ID:", "extracted_address_content": "You will be sending funds to\n${recipient_name}", "failed_authentication": "Failed authentication. ${state_error}", @@ -373,7 +374,10 @@ "litecoin_mweb_description": "MWEB is a new protocol that brings faster, cheaper, and more private transactions to Litecoin", "litecoin_mweb_dismiss": "Dismiss", "litecoin_mweb_display_card": "Show MWEB card", + "litecoin_mweb_enable": "Enable MWEB", "litecoin_mweb_enable_later": "You can choose to enable MWEB again under Display Settings.", + "litecoin_mweb_logs": "MWEB Logs", + "litecoin_mweb_node": "MWEB Node", "litecoin_mweb_pegin": "Peg In", "litecoin_mweb_pegout": "Peg Out", "litecoin_mweb_scanning": "MWEB Scanning", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index fd20a8b30..5a693cf32 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -295,6 +295,7 @@ "expiresOn": "Expira el", "expiry_and_validity": "Vencimiento y validez", "export_backup": "Exportar copia de seguridad", + "export_logs": "Registros de exportación", "extra_id": "ID adicional:", "extracted_address_content": "Enviará fondos a\n${recipient_name}", "failed_authentication": "Autenticación fallida. ${state_error}", @@ -371,7 +372,10 @@ "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", "litecoin_mweb_display_card": "Mostrar tarjeta MWEB", + "litecoin_mweb_enable": "Habilitar mweb", "litecoin_mweb_enable_later": "Puede elegir habilitar MWEB nuevamente en la configuración de visualización.", + "litecoin_mweb_logs": "Registros de mweb", + "litecoin_mweb_node": "Nodo mweb", "litecoin_mweb_pegin": "Convertir", "litecoin_mweb_pegout": "Recuperar", "litecoin_mweb_scanning": "Escaneo mweb", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 50aceb2f1..1865e41f2 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -295,6 +295,7 @@ "expiresOn": "Expire le", "expiry_and_validity": "Expiration et validité", "export_backup": "Exporter la sauvegarde", + "export_logs": "Journaux d'exportation", "extra_id": "ID supplémentaire :", "extracted_address_content": "Vous allez envoyer des fonds à\n${recipient_name}", "failed_authentication": "Échec d'authentification. ${state_error}", @@ -371,7 +372,10 @@ "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", "litecoin_mweb_display_card": "Afficher la carte MWeb", + "litecoin_mweb_enable": "Activer Mweb", "litecoin_mweb_enable_later": "Vous pouvez choisir d'activer à nouveau MWEB sous Paramètres d'affichage.", + "litecoin_mweb_logs": "Journaux MWEB", + "litecoin_mweb_node": "Node MWEB", "litecoin_mweb_pegin": "Entraver", "litecoin_mweb_pegout": "Crever", "litecoin_mweb_scanning": "Scann mweb", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index e66859531..52cf14128 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -295,6 +295,7 @@ "expiresOn": "Yana ƙarewa", "expiry_and_validity": "Karewa da inganci", "export_backup": "Ajiyayyen fitarwa", + "export_logs": "Injin fitarwa", "extra_id": "Karin ID:", "extracted_address_content": "Za ku aika da kudade zuwa\n${recipient_name}", "failed_authentication": "Binne wajen shiga. ${state_error}", @@ -371,7 +372,10 @@ "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", "litecoin_mweb_display_card": "Nuna katin Mweb", + "litecoin_mweb_enable": "Kunna Mweb", "litecoin_mweb_enable_later": "Kuna iya zaɓar kunna Mweb kuma a ƙarƙashin saitunan nuni.", + "litecoin_mweb_logs": "Jagoran Mweb", + "litecoin_mweb_node": "Mweb Node", "litecoin_mweb_pegin": "Peg in", "litecoin_mweb_pegout": "Peg fita", "litecoin_mweb_scanning": "Mweb scanning", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index bc8b81211..64f112a42 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -295,6 +295,7 @@ "expiresOn": "पर समय सीमा समाप्त", "expiry_and_validity": "समाप्ति और वैधता", "export_backup": "निर्यात बैकअप", + "export_logs": "निर्यात लॉग", "extra_id": "अतिरिक्त आईडी:", "extracted_address_content": "आपको धनराशि भेजी जाएगी\n${recipient_name}", "failed_authentication": "प्रमाणीकरण विफल. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB एक नया प्रोटोकॉल है जो लिटकोइन के लिए तेजी से, सस्ता और अधिक निजी लेनदेन लाता है", "litecoin_mweb_dismiss": "नकार देना", "litecoin_mweb_display_card": "MWEB कार्ड दिखाएं", + "litecoin_mweb_enable": "MWEB सक्षम करें", "litecoin_mweb_enable_later": "आप प्रदर्शन सेटिंग्स के तहत फिर से MWEB को सक्षम करने के लिए चुन सकते हैं।", + "litecoin_mweb_logs": "MWEB लॉग", + "litecoin_mweb_node": "MWEB नोड", "litecoin_mweb_pegin": "खूंटी", "litecoin_mweb_pegout": "मरना", "litecoin_mweb_scanning": "MWEB स्कैनिंग", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 9ff57707d..4bbf4f141 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -295,6 +295,7 @@ "expiresOn": "Istječe", "expiry_and_validity": "Istek i valjanost", "export_backup": "Izvezi sigurnosnu kopiju", + "export_logs": "Izvozni trupci", "extra_id": "Dodatni ID:", "extracted_address_content": "Poslat ćete sredstva primatelju\n${recipient_name}", "failed_authentication": "Autentifikacija neuspješna. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB je novi protokol koji u Litecoin donosi brže, jeftinije i privatnije transakcije", "litecoin_mweb_dismiss": "Odbaciti", "litecoin_mweb_display_card": "Prikaži MWeb karticu", + "litecoin_mweb_enable": "Omogući MWeb", "litecoin_mweb_enable_later": "Možete odabrati da MWEB ponovo omogućite pod postavkama zaslona.", + "litecoin_mweb_logs": "MWEB trupci", + "litecoin_mweb_node": "MWEB čvor", "litecoin_mweb_pegin": "Uvući se", "litecoin_mweb_pegout": "Odapeti", "litecoin_mweb_scanning": "MWEB skeniranje", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 40ed1e116..cbaf3a548 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -295,6 +295,7 @@ "expiresOn": "Վավերականությունը լրանում է", "expiry_and_validity": "Վավերականություն և լրացում", "export_backup": "Արտահանել կրկնօրինակը", + "export_logs": "Արտահանման տեղեկամատյաններ", "extra_id": "Լրացուցիչ ID", "extracted_address_content": "Դուք կուղարկեք գումար ${recipient_name}", "failed_authentication": "Վավերացումը ձախողվեց. ${state_error}", @@ -367,7 +368,10 @@ "light_theme": "Լուսավոր", "litecoin_mweb_description": "Mweb- ը նոր արձանագրություն է, որը բերում է ավելի արագ, ավելի էժան եւ ավելի մասնավոր գործարքներ դեպի LITECOIN", "litecoin_mweb_dismiss": "Հեռացնել", + "litecoin_mweb_enable": "Միացնել Mweb- ը", "litecoin_mweb_enable_later": "Կարող եք ընտրել Mweb- ը կրկին միացնել ցուցադրման պարամետրերը:", + "litecoin_mweb_logs": "Mweb տեղեկամատյաններ", + "litecoin_mweb_node": "Mweb հանգույց", "litecoin_mweb_pegin": "Peg in", "litecoin_mweb_pegout": "Հափշտակել", "live_fee_rates": "Ապակի վարձավճարներ API- ի միջոցով", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 2858fa193..273087eec 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -295,6 +295,7 @@ "expiresOn": "Kadaluarsa pada", "expiry_and_validity": "Kedaluwarsa dan validitas", "export_backup": "Ekspor cadangan", + "export_logs": "Log ekspor", "extra_id": "ID tambahan:", "extracted_address_content": "Anda akan mengirim dana ke\n${recipient_name}", "failed_authentication": "Otentikasi gagal. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB adalah protokol baru yang membawa transaksi yang lebih cepat, lebih murah, dan lebih pribadi ke Litecoin", "litecoin_mweb_dismiss": "Membubarkan", "litecoin_mweb_display_card": "Tunjukkan kartu mWeb", + "litecoin_mweb_enable": "Aktifkan MWEB", "litecoin_mweb_enable_later": "Anda dapat memilih untuk mengaktifkan MWEB lagi di bawah pengaturan tampilan.", + "litecoin_mweb_logs": "Log MWeb", + "litecoin_mweb_node": "Node MWEB", "litecoin_mweb_pegin": "Pasak masuk", "litecoin_mweb_pegout": "Mati", "litecoin_mweb_scanning": "Pemindaian MWEB", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 034376a6c..917315aa6 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -296,6 +296,7 @@ "expiresOn": "Scade il", "expiry_and_validity": "Scadenza e validità", "export_backup": "Esporta backup", + "export_logs": "Registri di esportazione", "extra_id": "Extra ID:", "extracted_address_content": "Invierai i tuoi fondi a\n${recipient_name}", "failed_authentication": "Autenticazione fallita. ${state_error}", @@ -372,7 +373,10 @@ "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_display_card": "Mostra la scheda 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_node": "Nodo MWeb", "litecoin_mweb_pegin": "Piolo in", "litecoin_mweb_pegout": "PEG OUT", "litecoin_mweb_scanning": "Scansione MWeb", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 7f0792063..ae3e32ea8 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -295,6 +295,7 @@ "expiresOn": "有効期限は次のとおりです", "expiry_and_validity": "有効期限と有効性", "export_backup": "バックアップのエクスポート", + "export_logs": "ログをエクスポートします", "extra_id": "追加ID:", "extracted_address_content": "に送金します\n${recipient_name}", "failed_authentication": "認証失敗. ${state_error}", @@ -372,7 +373,10 @@ "litecoin_mweb_description": "MWEBは、Litecoinにより速く、より安価で、よりプライベートなトランザクションをもたらす新しいプロトコルです", "litecoin_mweb_dismiss": "却下する", "litecoin_mweb_display_card": "MWEBカードを表示します", + "litecoin_mweb_enable": "MWEBを有効にします", "litecoin_mweb_enable_later": "表示設定の下で、MWEBを再度有効にすることを選択できます。", + "litecoin_mweb_logs": "MWEBログ", + "litecoin_mweb_node": "MWEBノード", "litecoin_mweb_pegin": "ペグイン", "litecoin_mweb_pegout": "ペグアウト", "litecoin_mweb_scanning": "MWEBスキャン", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 6ba402ce3..2dc1a2dfc 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -295,6 +295,7 @@ "expiresOn": "만료 날짜", "expiry_and_validity": "만료와 타당성", "export_backup": "백업 내보내기", + "export_logs": "내보내기 로그", "extra_id": "추가 ID:", "extracted_address_content": "당신은에 자금을 보낼 것입니다\n${recipient_name}", "failed_authentication": "인증 실패. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB는 Litecoin에 더 빠르고 저렴하며 개인 거래를 제공하는 새로운 프로토콜입니다.", "litecoin_mweb_dismiss": "해고하다", "litecoin_mweb_display_card": "mweb 카드를 보여주십시오", + "litecoin_mweb_enable": "mweb 활성화", "litecoin_mweb_enable_later": "디스플레이 설정에서 MWEB를 다시 활성화하도록 선택할 수 있습니다.", + "litecoin_mweb_logs": "mweb 로그", + "litecoin_mweb_node": "mweb 노드", "litecoin_mweb_pegin": "페그를 입력하십시오", "litecoin_mweb_pegout": "죽다", "litecoin_mweb_scanning": "mweb 스캔", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 69b888b5a..f42af6863 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -295,6 +295,7 @@ "expiresOn": "သက်တမ်းကုန်သည်။", "expiry_and_validity": "သက်တမ်းကုန်ဆုံးခြင်းနှင့်တရားဝင်မှု", "export_backup": "အရန်ကူးထုတ်ရန်", + "export_logs": "ပို့ကုန်မှတ်တမ်းများ", "extra_id": "အပို ID-", "extracted_address_content": "သင်သည် \n${recipient_name} သို့ ရန်ပုံငွေများ ပေးပို့ပါမည်", "failed_authentication": "အထောက်အထားစိစစ်ခြင်း မအောင်မြင်ပါ။. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "Mweb သည် Protocol အသစ်ဖြစ်ပြီး LitCoin သို့ပိုမိုဈေးချိုသာသော, စျေးသက်သက်သာသာသုံးခြင်းနှင့်ပိုမိုများပြားသောပုဂ္ဂလိကငွေပို့ဆောင်မှုများကိုဖြစ်ပေါ်စေသည်", "litecoin_mweb_dismiss": "ထုတ်ပစ်", "litecoin_mweb_display_card": "MweB ကဒ်ကိုပြပါ", + "litecoin_mweb_enable": "mweb enable", "litecoin_mweb_enable_later": "သင် MweB ကို display settings အောက်ရှိ ထပ်မံ. ခွင့်ပြုရန်ရွေးချယ်နိုင်သည်။", + "litecoin_mweb_logs": "Mweb မှတ်တမ်းများ", + "litecoin_mweb_node": "mweb node ကို", "litecoin_mweb_pegin": "တံစို့", "litecoin_mweb_pegout": "တံစို့", "litecoin_mweb_scanning": "mweb scanning", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 2c259bf80..73e3df6b6 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -295,6 +295,7 @@ "expiresOn": "Verloopt op", "expiry_and_validity": "Vervallen en geldigheid", "export_backup": "Back-up exporteren", + "export_logs": "Exporteer logboeken", "extra_id": "Extra ID:", "extracted_address_content": "U stuurt geld naar\n${recipient_name}", "failed_authentication": "Mislukte authenticatie. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB is een nieuw protocol dat snellere, goedkopere en meer privé -transacties naar Litecoin brengt", "litecoin_mweb_dismiss": "Afwijzen", "litecoin_mweb_display_card": "Toon MWEB -kaart", + "litecoin_mweb_enable": "MWEB inschakelen", "litecoin_mweb_enable_later": "U kunt ervoor kiezen om MWeb opnieuw in te schakelen onder weergave -instellingen.", + "litecoin_mweb_logs": "MWEB -logboeken", + "litecoin_mweb_node": "MWEB -knooppunt", "litecoin_mweb_pegin": "Vastmaken", "litecoin_mweb_pegout": "Uithakken", "litecoin_mweb_scanning": "MWEB -scanning", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 936339708..aaa82a3bf 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -295,6 +295,7 @@ "expiresOn": "Upływa w dniu", "expiry_and_validity": "Wygaśnięcie i ważność", "export_backup": "Eksportuj kopię zapasową", + "export_logs": "Dzienniki eksportu", "extra_id": "Dodatkowy ID:", "extracted_address_content": "Wysyłasz środki na\n${recipient_name}", "failed_authentication": "Nieudane uwierzytelnienie. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB to nowy protokół, który przynosi szybciej, tańsze i bardziej prywatne transakcje do Litecoin", "litecoin_mweb_dismiss": "Odrzucać", "litecoin_mweb_display_card": "Pokaż kartę MWEB", + "litecoin_mweb_enable": "Włącz MWEB", "litecoin_mweb_enable_later": "Możesz ponownie włączyć MWEB w ustawieniach wyświetlania.", + "litecoin_mweb_logs": "Dzienniki MWEB", + "litecoin_mweb_node": "Węzeł MWEB", "litecoin_mweb_pegin": "Kołek", "litecoin_mweb_pegout": "Palikować", "litecoin_mweb_scanning": "Skanowanie MWEB", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 0cc0cc00a..194307a3d 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -295,6 +295,7 @@ "expiresOn": "Expira em", "expiry_and_validity": "Expiração e validade", "export_backup": "Backup de exportação", + "export_logs": "Exportar logs", "extra_id": "ID extra:", "extracted_address_content": "Você enviará fundos para\n${recipient_name}", "failed_authentication": "Falha na autenticação. ${state_error}", @@ -371,7 +372,10 @@ "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", "litecoin_mweb_display_card": "Mostre o cartão MWEB", + "litecoin_mweb_enable": "Ativar Mweb", "litecoin_mweb_enable_later": "Você pode optar por ativar o MWEB novamente em Configurações de exibição.", + "litecoin_mweb_logs": "Logs MWeb", + "litecoin_mweb_node": "Nó MWeb", "litecoin_mweb_pegin": "Peg in", "litecoin_mweb_pegout": "Peg fora", "litecoin_mweb_scanning": "MWEB Scanning", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 8aeb56a86..f42c28c15 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -295,6 +295,7 @@ "expiresOn": "Годен до", "expiry_and_validity": "Истечение и достоверность", "export_backup": "Экспорт резервной копии", + "export_logs": "Экспортные журналы", "extra_id": "Дополнительный ID:", "extracted_address_content": "Вы будете отправлять средства\n${recipient_name}", "failed_authentication": "Ошибка аутентификации. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB - это новый протокол, который приносит быстрее, дешевле и более частные транзакции в Litecoin", "litecoin_mweb_dismiss": "Увольнять", "litecoin_mweb_display_card": "Показать карту MWEB", + "litecoin_mweb_enable": "Включить MWEB", "litecoin_mweb_enable_later": "Вы можете снова включить MWEB в настройках отображения.", + "litecoin_mweb_logs": "MWEB журналы", + "litecoin_mweb_node": "Узел MWEB", "litecoin_mweb_pegin": "Внедрять", "litecoin_mweb_pegout": "Выкрикивать", "litecoin_mweb_scanning": "MWEB сканирование", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 736eb8762..9d8fb0af2 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -295,6 +295,7 @@ "expiresOn": "หมดอายุวันที่", "expiry_and_validity": "หมดอายุและถูกต้อง", "export_backup": "ส่งออกข้อมูลสำรอง", + "export_logs": "บันทึกการส่งออก", "extra_id": "ไอดีเพิ่มเติม:", "extracted_address_content": "คุณกำลังจะส่งเงินไปยัง\n${recipient_name}", "failed_authentication": "การยืนยันสิทธิ์ล้มเหลว ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB เป็นโปรโตคอลใหม่ที่นำการทำธุรกรรมที่เร็วกว่าราคาถูกกว่าและเป็นส่วนตัวมากขึ้นไปยัง Litecoin", "litecoin_mweb_dismiss": "อนุญาตให้ออกไป", "litecoin_mweb_display_card": "แสดงการ์ด mweb", + "litecoin_mweb_enable": "เปิดใช้งาน mweb", "litecoin_mweb_enable_later": "คุณสามารถเลือกเปิดใช้งาน MWEB อีกครั้งภายใต้การตั้งค่าการแสดงผล", + "litecoin_mweb_logs": "บันทึก MWEB", + "litecoin_mweb_node": "โหนด MWEB", "litecoin_mweb_pegin": "หมุด", "litecoin_mweb_pegout": "ตรึง", "litecoin_mweb_scanning": "การสแกน MWEB", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index e68bd0b22..ca35e7997 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -295,6 +295,7 @@ "expiresOn": "Mag-e-expire sa", "expiry_and_validity": "Pag-expire at Bisa", "export_backup": "I-export ang backup", + "export_logs": "Mga log ng pag -export", "extra_id": "Dagdag na ID:", "extracted_address_content": "Magpapadala ka ng pondo sa\n${recipient_name}", "failed_authentication": "Nabigo ang pagpapatunay. ${state_error}", @@ -371,7 +372,10 @@ "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", "litecoin_mweb_display_card": "Ipakita ang MWEB Card", + "litecoin_mweb_enable": "Paganahin ang MWeb", "litecoin_mweb_enable_later": "Maaari kang pumili upang paganahin muli ang MWeb sa ilalim ng mga setting ng pagpapakita.", + "litecoin_mweb_logs": "MWEB log", + "litecoin_mweb_node": "Mweb node", "litecoin_mweb_pegin": "Peg in", "litecoin_mweb_pegout": "Peg out", "litecoin_mweb_scanning": "Pag -scan ng Mweb", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 83e8f6cbc..1fee66369 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -295,6 +295,7 @@ "expiresOn": "Tarihinde sona eriyor", "expiry_and_validity": "Sona erme ve geçerlilik", "export_backup": "Yedeği dışa aktar", + "export_logs": "Dışa aktarma günlükleri", "extra_id": "Ekstra ID:", "extracted_address_content": "Parayı buraya gönderceksin:\n${recipient_name}", "failed_authentication": "Doğrulama başarısız oldu. ${state_error}", @@ -371,7 +372,10 @@ "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", "litecoin_mweb_display_card": "MWEB kartını göster", + "litecoin_mweb_enable": "MWEB'i etkinleştir", "litecoin_mweb_enable_later": "Ekran ayarlarının altında MWEB'yi tekrar etkinleştirmeyi seçebilirsiniz.", + "litecoin_mweb_logs": "MWEB günlükleri", + "litecoin_mweb_node": "MWEB düğümü", "litecoin_mweb_pegin": "Takılmak", "litecoin_mweb_pegout": "Çiğnemek", "litecoin_mweb_scanning": "MWEB taraması", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 909d9c01f..5d9a56c1e 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -295,6 +295,7 @@ "expiresOn": "Термін дії закінчується", "expiry_and_validity": "Закінчення та обгрунтованість", "export_backup": "Експортувати резервну копію", + "export_logs": "Експортні журнали", "extra_id": "Додатковий ID:", "extracted_address_content": "Ви будете відправляти кошти\n${recipient_name}", "failed_authentication": "Помилка аутентифікації. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB - це новий протокол, який приносить швидкі, дешевші та більш приватні транзакції Litecoin", "litecoin_mweb_dismiss": "Звільнити", "litecoin_mweb_display_card": "Показати карту MWeb", + "litecoin_mweb_enable": "Увімкнути mweb", "litecoin_mweb_enable_later": "Ви можете знову ввімкнути MWEB в налаштуваннях дисплея.", + "litecoin_mweb_logs": "Журнали MWeb", + "litecoin_mweb_node": "Вузол MWeb", "litecoin_mweb_pegin": "Подякувати", "litecoin_mweb_pegout": "Подякувати", "litecoin_mweb_scanning": "Сканування Mweb", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 2dbc97102..106f00f96 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -295,6 +295,7 @@ "expiresOn": "ﺩﺎﻌﯿﻣ ﯽﻣﺎﺘﺘﺧﺍ", "expiry_and_validity": "میعاد ختم اور صداقت", "export_backup": "بیک اپ برآمد کریں۔", + "export_logs": "نوشتہ جات برآمد کریں", "extra_id": "اضافی ID:", "extracted_address_content": "آپ فنڈز بھیج رہے ہوں گے\n${recipient_name}", "failed_authentication": "ناکام تصدیق۔ ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB ایک نیا پروٹوکول ہے جو لیٹیکوئن میں تیز ، سستا اور زیادہ نجی لین دین لاتا ہے", "litecoin_mweb_dismiss": "خارج", "litecoin_mweb_display_card": "MWEB کارڈ دکھائیں", + "litecoin_mweb_enable": "MWEB کو فعال کریں", "litecoin_mweb_enable_later": "آپ ڈسپلے کی ترتیبات کے تحت MWEB کو دوبارہ فعال کرنے کا انتخاب کرسکتے ہیں۔", + "litecoin_mweb_logs": "MWEB لاگز", + "litecoin_mweb_node": "MWEB نوڈ", "litecoin_mweb_pegin": "پیگ میں", "litecoin_mweb_pegout": "پیگ آؤٹ", "litecoin_mweb_scanning": "MWEB اسکیننگ", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 8d28d48a2..a95f32aaa 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -296,6 +296,7 @@ "expiresOn": "Hết hạn vào", "expiry_and_validity": "Hạn và hiệu lực", "export_backup": "Xuất sao lưu", + "export_logs": "Nhật ký xuất khẩu", "extra_id": "ID bổ sung:", "extracted_address_content": "Bạn sẽ gửi tiền cho\n${recipient_name}", "failed_authentication": "Xác thực không thành công. ${state_error}", @@ -368,7 +369,10 @@ "light_theme": "Chủ đề sáng", "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", + "litecoin_mweb_enable": "Bật MWEB", "litecoin_mweb_enable_later": "Bạn có thể chọn bật lại MWEB trong cài đặt hiển thị.", + "litecoin_mweb_logs": "Nhật ký MWEB", + "litecoin_mweb_node": "Nút MWEB", "litecoin_mweb_pegin": "Chốt vào", "litecoin_mweb_pegout": "Chốt ra", "live_fee_rates": "Tỷ lệ phí hiện tại qua API", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 03c7c95ef..1a1583da5 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -296,6 +296,7 @@ "expiresOn": "Ipari lori", "expiry_and_validity": "Ipari ati idaniloju", "export_backup": "Sún ẹ̀dà nípamọ́ síta", + "export_logs": "Wọle si okeere", "extra_id": "Àmì ìdánimọ̀ tó fikún:", "extracted_address_content": "Ẹ máa máa fi owó ránṣẹ́ sí\n${recipient_name}", "failed_authentication": "Ìfẹ̀rílàdí pipòfo. ${state_error}", @@ -372,7 +373,10 @@ "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", "litecoin_mweb_display_card": "Fihan kaadi Mweb", + "litecoin_mweb_enable": "Mu mweb", "litecoin_mweb_enable_later": "O le yan lati ṣiṣẹ Mweb lẹẹkansi labẹ awọn eto ifihan.", + "litecoin_mweb_logs": "MTweb logs", + "litecoin_mweb_node": "Alweb joko", "litecoin_mweb_pegin": "Peg in", "litecoin_mweb_pegout": "Peg jade", "litecoin_mweb_scanning": "Mweb scanning", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 5c8d708f4..79fe8faa5 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -295,6 +295,7 @@ "expiresOn": "到期", "expiry_and_validity": "到期和有效性", "export_backup": "导出备份", + "export_logs": "导出日志", "extra_id": "额外ID:", "extracted_address_content": "您将汇款至\n${recipient_name}", "failed_authentication": "身份验证失败. ${state_error}", @@ -371,7 +372,10 @@ "litecoin_mweb_description": "MWEB是一项新协议,它将更快,更便宜和更多的私人交易带给Litecoin", "litecoin_mweb_dismiss": "解雇", "litecoin_mweb_display_card": "显示MWEB卡", + "litecoin_mweb_enable": "启用MWEB", "litecoin_mweb_enable_later": "您可以选择在显示设置下再次启用MWEB。", + "litecoin_mweb_logs": "MWEB日志", + "litecoin_mweb_node": "MWEB节点", "litecoin_mweb_pegin": "钉进", "litecoin_mweb_pegout": "昏倒", "litecoin_mweb_scanning": "MWEB扫描", From 48457fdd6d3b15beacdbe170117dcbc2cbce25dc Mon Sep 17 00:00:00 2001 From: Serhii Date: Thu, 7 Nov 2024 07:50:48 +0200 Subject: [PATCH 48/73] cake pay country picker (#1779) * cake pay country picker * UI changes * hide picker after the country is set * Revert "hide picker after the country is set" This reverts commit cce358b94f55c22e0807b4d3641a13ebd299106c. * show country picker initially * flags * minor ui fixes * Update lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart --------- Co-authored-by: tuxpizza Co-authored-by: Omar Hatem --- assets/images/flags/abw.png | Bin 0 -> 607 bytes assets/images/flags/afg.png | Bin 0 -> 1848 bytes assets/images/flags/ago.png | Bin 0 -> 1159 bytes assets/images/flags/aia.png | Bin 0 -> 1357 bytes assets/images/flags/and.png | Bin 0 -> 1966 bytes assets/images/flags/asm.png | Bin 0 -> 503 bytes assets/images/flags/atf.png | Bin 0 -> 1311 bytes assets/images/flags/atg.png | Bin 0 -> 963 bytes assets/images/flags/aut.png | Bin 0 -> 125 bytes assets/images/flags/aze.png | Bin 0 -> 717 bytes assets/images/flags/bel.png | Bin 0 -> 127 bytes assets/images/flags/bes.png | Bin 0 -> 236 bytes assets/images/flags/bhr.png | Bin 0 -> 567 bytes assets/images/flags/blz.png | Bin 0 -> 1777 bytes assets/images/flags/bmu.png | Bin 0 -> 1518 bytes assets/images/flags/bol.png | Bin 0 -> 668 bytes assets/images/flags/brn.png | Bin 0 -> 1957 bytes assets/images/flags/btn.png | Bin 0 -> 4013 bytes assets/images/flags/bvt.png | Bin 0 -> 147 bytes assets/images/flags/bwa.png | Bin 0 -> 134 bytes assets/images/flags/cck.png | Bin 0 -> 1139 bytes assets/images/flags/cmr.png | Bin 0 -> 402 bytes assets/images/flags/cok.png | Bin 0 -> 1416 bytes assets/images/flags/cpv.png | Bin 0 -> 297 bytes assets/images/flags/cri.png | Bin 0 -> 136 bytes assets/images/flags/cuw.png | Bin 0 -> 481 bytes assets/images/flags/cxr.png | Bin 0 -> 1202 bytes assets/images/flags/cyp.png | Bin 0 -> 1128 bytes assets/images/flags/dji.png | Bin 0 -> 387 bytes assets/images/flags/dma.png | Bin 0 -> 1224 bytes assets/images/flags/dza.png | Bin 0 -> 518 bytes assets/images/flags/ecu.png | Bin 0 -> 1238 bytes assets/images/flags/est.png | Bin 0 -> 130 bytes assets/images/flags/eth.png | Bin 0 -> 1584 bytes assets/images/flags/fin.png | Bin 0 -> 131 bytes assets/images/flags/fji.png | Bin 0 -> 1571 bytes assets/images/flags/flk.png | Bin 0 -> 1591 bytes assets/images/flags/fro.png | Bin 0 -> 147 bytes assets/images/flags/fsm.png | Bin 0 -> 340 bytes assets/images/flags/gab.png | Bin 0 -> 130 bytes assets/images/flags/geo.png | Bin 0 -> 187 bytes assets/images/flags/ggi.png | Bin 0 -> 892 bytes assets/images/flags/ggy.png | Bin 0 -> 192 bytes assets/images/flags/glp.png | Bin 0 -> 1820 bytes assets/images/flags/gmb.png | Bin 0 -> 278 bytes assets/images/flags/grc.png | Bin 0 -> 436 bytes assets/images/flags/grd.png | Bin 0 -> 892 bytes assets/images/flags/grl.png | Bin 0 -> 407 bytes assets/images/flags/guf.png | Bin 0 -> 581 bytes assets/images/flags/gum.png | Bin 0 -> 1259 bytes assets/images/flags/guy.png | Bin 0 -> 635 bytes assets/images/flags/hmd.png | Bin 0 -> 780 bytes assets/images/flags/iot.png | Bin 0 -> 2415 bytes assets/images/flags/irl.png | Bin 0 -> 127 bytes assets/images/flags/jam.png | Bin 0 -> 526 bytes assets/images/flags/jey.png | Bin 0 -> 914 bytes assets/images/flags/jor.png | Bin 0 -> 3131 bytes assets/images/flags/kaz.png | Bin 0 -> 1740 bytes assets/images/flags/ken.png | Bin 0 -> 2074 bytes assets/images/flags/kir.png | Bin 0 -> 2113 bytes assets/images/flags/kwt.png | Bin 0 -> 372 bytes assets/images/flags/lbn.png | Bin 0 -> 960 bytes assets/images/flags/lie.png | Bin 0 -> 844 bytes assets/images/flags/lka.png | Bin 0 -> 1757 bytes assets/images/flags/ltu.png | Bin 0 -> 130 bytes assets/images/flags/lux.png | Bin 0 -> 130 bytes assets/images/flags/lva.png | Bin 0 -> 124 bytes assets/images/flags/mco.png | Bin 0 -> 204 bytes assets/images/flags/mlt.png | Bin 0 -> 485 bytes assets/images/flags/mnp.png | Bin 0 -> 2078 bytes assets/images/flags/mrt.png | Bin 0 -> 509 bytes assets/images/flags/msr.png | Bin 0 -> 1502 bytes assets/images/flags/mtq.png | Bin 0 -> 1512 bytes assets/images/flags/mwi.png | Bin 0 -> 754 bytes assets/images/flags/myt.png | Bin 0 -> 2003 bytes assets/images/flags/ner.png | Bin 0 -> 494 bytes assets/images/flags/nfk.png | Bin 0 -> 1422 bytes assets/images/flags/niu.png | Bin 0 -> 1095 bytes assets/images/flags/omn.png | Bin 0 -> 711 bytes assets/images/flags/per.png | Bin 0 -> 123 bytes assets/images/flags/plw.png | Bin 0 -> 608 bytes assets/images/flags/pri.png | Bin 0 -> 1880 bytes assets/images/flags/pyf.png | Bin 0 -> 1256 bytes assets/images/flags/qat.png | Bin 0 -> 2021 bytes assets/images/flags/slb.png | Bin 0 -> 403 bytes assets/images/flags/slv.png | Bin 0 -> 642 bytes assets/images/flags/svk.png | Bin 0 -> 521 bytes assets/images/flags/svn.png | Bin 0 -> 1963 bytes assets/images/flags/tkm.png | Bin 0 -> 1620 bytes assets/images/flags/ton.png | Bin 0 -> 139 bytes assets/images/flags/tuv.png | Bin 0 -> 1456 bytes assets/images/flags/ury.png | Bin 0 -> 1017 bytes assets/images/flags/vat.png | Bin 0 -> 851 bytes assets/images/flags/vir.png | Bin 0 -> 1987 bytes assets/images/flags/vut.png | Bin 0 -> 1267 bytes lib/cake_pay/cake_pay_api.dart | 10 +- lib/cake_pay/cake_pay_service.dart | 3 +- lib/di.dart | 3 +- lib/entities/country.dart | 386 ++++++++++++++++++ lib/entities/preferences_key.dart | 1 + .../cake_pay/cards/cake_pay_cards_page.dart | 156 +++++-- .../screens/cake_pay/widgets/card_item.dart | 2 +- lib/store/settings_store.dart | 25 +- .../cake_pay_cards_list_view_model.dart | 65 +-- 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 + 132 files changed, 601 insertions(+), 78 deletions(-) create mode 100644 assets/images/flags/abw.png create mode 100644 assets/images/flags/afg.png create mode 100644 assets/images/flags/ago.png create mode 100644 assets/images/flags/aia.png create mode 100644 assets/images/flags/and.png create mode 100644 assets/images/flags/asm.png create mode 100644 assets/images/flags/atf.png create mode 100644 assets/images/flags/atg.png create mode 100644 assets/images/flags/aut.png create mode 100644 assets/images/flags/aze.png create mode 100644 assets/images/flags/bel.png create mode 100644 assets/images/flags/bes.png create mode 100644 assets/images/flags/bhr.png create mode 100644 assets/images/flags/blz.png create mode 100644 assets/images/flags/bmu.png create mode 100644 assets/images/flags/bol.png create mode 100644 assets/images/flags/brn.png create mode 100644 assets/images/flags/btn.png create mode 100644 assets/images/flags/bvt.png create mode 100644 assets/images/flags/bwa.png create mode 100644 assets/images/flags/cck.png create mode 100644 assets/images/flags/cmr.png create mode 100644 assets/images/flags/cok.png create mode 100644 assets/images/flags/cpv.png create mode 100644 assets/images/flags/cri.png create mode 100644 assets/images/flags/cuw.png create mode 100644 assets/images/flags/cxr.png create mode 100644 assets/images/flags/cyp.png create mode 100644 assets/images/flags/dji.png create mode 100644 assets/images/flags/dma.png create mode 100644 assets/images/flags/dza.png create mode 100644 assets/images/flags/ecu.png create mode 100644 assets/images/flags/est.png create mode 100644 assets/images/flags/eth.png create mode 100644 assets/images/flags/fin.png create mode 100644 assets/images/flags/fji.png create mode 100644 assets/images/flags/flk.png create mode 100644 assets/images/flags/fro.png create mode 100644 assets/images/flags/fsm.png create mode 100644 assets/images/flags/gab.png create mode 100644 assets/images/flags/geo.png create mode 100644 assets/images/flags/ggi.png create mode 100644 assets/images/flags/ggy.png create mode 100644 assets/images/flags/glp.png create mode 100644 assets/images/flags/gmb.png create mode 100644 assets/images/flags/grc.png create mode 100644 assets/images/flags/grd.png create mode 100644 assets/images/flags/grl.png create mode 100644 assets/images/flags/guf.png create mode 100644 assets/images/flags/gum.png create mode 100644 assets/images/flags/guy.png create mode 100644 assets/images/flags/hmd.png create mode 100644 assets/images/flags/iot.png create mode 100644 assets/images/flags/irl.png create mode 100644 assets/images/flags/jam.png create mode 100644 assets/images/flags/jey.png create mode 100644 assets/images/flags/jor.png create mode 100644 assets/images/flags/kaz.png create mode 100644 assets/images/flags/ken.png create mode 100644 assets/images/flags/kir.png create mode 100644 assets/images/flags/kwt.png create mode 100644 assets/images/flags/lbn.png create mode 100644 assets/images/flags/lie.png create mode 100644 assets/images/flags/lka.png create mode 100644 assets/images/flags/ltu.png create mode 100644 assets/images/flags/lux.png create mode 100644 assets/images/flags/lva.png create mode 100644 assets/images/flags/mco.png create mode 100644 assets/images/flags/mlt.png create mode 100644 assets/images/flags/mnp.png create mode 100644 assets/images/flags/mrt.png create mode 100644 assets/images/flags/msr.png create mode 100644 assets/images/flags/mtq.png create mode 100644 assets/images/flags/mwi.png create mode 100644 assets/images/flags/myt.png create mode 100644 assets/images/flags/ner.png create mode 100644 assets/images/flags/nfk.png create mode 100644 assets/images/flags/niu.png create mode 100644 assets/images/flags/omn.png create mode 100644 assets/images/flags/per.png create mode 100644 assets/images/flags/plw.png create mode 100644 assets/images/flags/pri.png create mode 100644 assets/images/flags/pyf.png create mode 100644 assets/images/flags/qat.png create mode 100644 assets/images/flags/slb.png create mode 100644 assets/images/flags/slv.png create mode 100644 assets/images/flags/svk.png create mode 100644 assets/images/flags/svn.png create mode 100644 assets/images/flags/tkm.png create mode 100644 assets/images/flags/ton.png create mode 100644 assets/images/flags/tuv.png create mode 100644 assets/images/flags/ury.png create mode 100644 assets/images/flags/vat.png create mode 100644 assets/images/flags/vir.png create mode 100644 assets/images/flags/vut.png create mode 100644 lib/entities/country.dart diff --git a/assets/images/flags/abw.png b/assets/images/flags/abw.png new file mode 100644 index 0000000000000000000000000000000000000000..d049d3a4348416d1826a4236ce7218e6807669b9 GIT binary patch literal 607 zcmV-l0-*hgP)$j``PLRzf4Wb7x>|$0RDQWte!7{p*jKX1y z!eETUQhm8ug1qOjw|tetVvE63e!A+Xt%jGuPJ_X$nzmViy#HBMiJ-}ap2@Ys++&Nu zRerfmg223(uWY5+R)D*=n6PD$#{X1MPJq6;oU~JZxowfd{3IdfdxA`a!svN_{Uab) zjmYwqn2w;w+J%r#e!RED+=Q&xZIs8unyrzTzW+x_fAgsl$B42#r8-@ z@F^?$ARf(;q3|6Z?~3@MEG(l&J43E%0P!`5PIB zRyK|R0004WQchC08VAA(3FhmG%rRM6L~Q{W+swdz@lLj7?mEu=wz&>AjC^nAnI9$#f7*U zs>^V(k{b(-zHSD(s+6Py2Tg57HcAX&la>;uC?#?V3h+>vow+G3511)6fPK_}fir-E t0x(Dn7>$L|SQs^cN(PLAQ7{UC0055fRc;z8VCw(?002ovPDHLkV1l;f6z%{3 literal 0 HcmV?d00001 diff --git a/assets/images/flags/afg.png b/assets/images/flags/afg.png new file mode 100644 index 0000000000000000000000000000000000000000..f2ea25144b2c5bf758a134ae00ca05e99cc9581b GIT binary patch literal 1848 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 literal 0 HcmV?d00001 diff --git a/assets/images/flags/atf.png b/assets/images/flags/atf.png new file mode 100644 index 0000000000000000000000000000000000000000..af77e45d56cffeac7204d977b30278c46b2335de GIT binary patch literal 1311 zcmV+)1>pLLP)Px%Pf$!$MF0Q*03wtCB9#Cllu&20l$4Z|86yA_QvelR z?PzlV6H@>plmH@?03($EBa{FmlmH@>03wqBB9#CYT>v7K02_AzB9#Cll>i)i03nh9 z9DV>Il>j1?03wnAB9s6kl>j4@02*@uAB6xSlmH`?03wwD9D4vBf&e0u03Cmll9QB^ zlQ2JT?P+uXB9xRxOHpXEi+_cbA1IS6GX)x7X>f!9BbDy%?(Hc#|NsBFz}m*n=efb! z?ZU+dD3<{wmHz(zfRf7Y@b^AhuPjNYJXfx~#M=TTmKQgk5i*<h(HRt!#k8=IQj8rqoVmwXeF}9Xz45zTZ7pt{OU^2r8IIV6rw* ztMT*s3@w`NYIW_<(^6@(%h2H%IG@qj?7_A+Q>^3X^Cm>4h?&ieozLj&^>v8G zg_g{Gj>u$rzOlUCKw7VPjK|N`>yDk!&eiLzxZF);wN`GqbB4w9_4<&X(E0lOzQ^P- zO{vAr=fTP4yTaNWJD?djpSi-}Cq$(aG@W;e#<#)YCq<sX{{dkJUc8SJsg2Oykt!I3|MqB#U0000s zbW%=J0Qc1X`y3n`9QzZAiTL#Rnq~A8W*qnFBw;0*n_*$<>KrB1X8s%{VH_MB9R2DX z{U07593FzAMaBRC0xU^HK~z}7V_+BsqW}(sIXF0Uv{<1)Tb~UIjEoIUO*pUuVT5jV zX+1L_U@?aSP763Nw=%;4&7%yJA2qC#fm8(_w# z$-rYqi2-KZ3`~?6z{J2ynF0I^W|SFVHfq4A0mIJ#1B3y(NCvbLZ9odbfFvXX+z1$8 z0|%)H1Cnjw0AWC`H4>NuH^7__fE(cHocAItzt%k7b7X0D-@8EM3Op~SZvNG5^}*s=34 zkYU#2lS#Z6>|N~x&CEPDc|yVH>X#cJQmJy^Iu-tOv<|xYqOCUPvwas!VsRk@uVu=Dm z=aOi^?65dHJG;d|tqveYNZ2fr0}%w~2U%Lq!w%<8 zI2i``7vvY#n3)B8n#G1hG}d~NY=C{9Wn~wH*;MCH4R--A(IymCbO6anU{TWEQWXtv zU@#GFfQOk!pPAXrNoHnV0cNnItc=viA_h!}n%X~Y0!kxGQHUfH{4Im*kehjG3=ERY zq!=)H#>DAJ4NVQ?rY=#y-OMZi&Jh!VG?pa<#LdV63IbAy20Ir!$$)FLlQaqt001|j VkQQ>_vPJ*^002ovPDHLkV1h&LS9<^e literal 0 HcmV?d00001 diff --git a/assets/images/flags/atg.png b/assets/images/flags/atg.png new file mode 100644 index 0000000000000000000000000000000000000000..d9a3d9f9e106261f8cd7e740df225aa703bf0b5d GIT binary patch literal 963 zcmV;!13dhRP)z=0m{{G%#aL*Vl_0rY=H($pPBvS?thYJ`A00PxBMf~#f zuMQw40|$pPT?%Zy0CB~~6f09og(hF8uOB@M2mt)i75mT?J23@CIR+050pYq6o`Vey z3jwhpJ%O&*CMQLJp=33jsVb1riSdIxq!oS_w!z2I|5T5e@;Mg%0b(6l_@upo9&b zf(@L34Si<{NjwJ5s}h-i4bQ6*d}j)qfDLqD3UgozpMwpRdkx>Y6PA1pp@j|)4FREs z4i8Tz9RL6T3UpFVQvjcpm6h+G@1LKF$KMzL00H+&L_t(o!|m2rPXa+0g<*RcKSx(U z?1%``?O0Gj#g4uA-n;()#05cicG?1WhI6qA4D&*sD~gH=E9{@Fq$-bsyvKqf#j4Bx7cwfF*>pGOPvHTA{uFa@9uPz;Z+U&w=3f=f%m z73@L2zzq$c4Nwddk+64h8GmE^iI*3>?~w@_fCx|wQ&IQFDo$?o%^jU$0FVIEBvLAb1mlYUv-wgl&002ovPDHLkV1lw*cfSAt literal 0 HcmV?d00001 diff --git a/assets/images/flags/aut.png b/assets/images/flags/aut.png new file mode 100644 index 0000000000000000000000000000000000000000..1be1ff483baf9d14483c3c5fea30ca5c21c6b406 GIT binary patch literal 125 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0L3?#3!&-4XSoB=)|uI>dsZ#6Cd|NsB@@7-pg zAY)08UoeBivm0qZj*_Q~V~EA+zxp00i_>zopr0P6cAp#T5? literal 0 HcmV?d00001 diff --git a/assets/images/flags/aze.png b/assets/images/flags/aze.png new file mode 100644 index 0000000000000000000000000000000000000000..834b1e696beb647bb499338ce6c9c8a0fa0e7aae GIT binary patch literal 717 zcmV;;0y6!HP)I66G1ULWx{{Wc7-~coLu4U>0HR%8?=>Rb4059nPH0S^=>H|0F050kR zHt7H?>H{_D04w#5rRxqp{`dLm12p~a@#+FJ>>Ehw0W|MFX7h24>=#7q6-DU-Ht#)T z>IFFYw8QEFH0laG?lN2a-sSUZhw2YL@J(*&05b1FY5d&e??h_p0y6lmz4xQG_^!X{ z04nJPH~;?r^^K$G04Mc?o$o_v@nM1ODpmZ~;O;wN^LUf$05R$bIp_c^>l#S$Sa<3Q zI_dy3>?Km`6GQyc-2VCd>IppR2R7;hGU^FC>>W$;VuJa&$LRw%@m_uF6G8s=`055Z z?m1xhp|tm)wD*~<`L)FDC{+8_-v9mm``6$0l&bZEoazTT=>RD0CsOo%m-1?e^L3H> z!_WNU=k<)E@mziFELZxy%I-8-@m6>Gz|HPBT>RbS_^!VC%hl~CQ^!7EaR2}S0d!JM zQvg8b*k%9#0TxL_K~z}7?b1b0LqQP0@$SCu0MCUA6fH%HyL(HK;!xb(t-$AJX-Ieh z60)z?`Jd*n`#Vg==fg1UtAad2009JCK;h5)xz@CV-YXg9epkPq{td9th_baiH`?7; zMc8)&08vj4VRdS(y){Ir7r?myxO5yg@o&gO=iiY9M|#E3Q2)fTU< zEQ@$MWyYeG5v$h?o&5_GaVl00000NkvXXu0mjfDHme4 literal 0 HcmV?d00001 diff --git a/assets/images/flags/bel.png b/assets/images/flags/bel.png new file mode 100644 index 0000000000000000000000000000000000000000..1c06c5fa7ddf179f6268274f97472fd677063e2f GIT binary patch literal 127 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0L3?#3!&-4XSJOMr-uI>dsK<1yjj$bs)bJzm? zfgHw?AirP+hi5m^fE-0n7sn8b)5!@EZVjd)9E-vj5B~b^@F6iIAtb?oi$|G(VfNX+ V2HlN6-GGW2JYD@<);T3K0RT*xB8UJ0 literal 0 HcmV?d00001 diff --git a/assets/images/flags/bes.png b/assets/images/flags/bes.png new file mode 100644 index 0000000000000000000000000000000000000000..b00bfb1f57e3f511ccbc3f1ad8fff2ba78c98185 GIT binary patch literal 236 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0J3?w7mbKU|e&H|6fVg?4jBOuH;Rhv&5C} z^WVRJKXodQP~q;|2o(1Aba4#vIG&s!!TN>ijaP*uBlGFJJqHp)82^eCJPhB^(D+$4 z&Y-dJt6rRd!K#KjE{lJUS$TQRq<>&)4rFciV4STGH^Ct4>IGf~1~dC>r_YBR*#tC} N!PC{xWt~$(698IAS2_Ry literal 0 HcmV?d00001 diff --git a/assets/images/flags/bhr.png b/assets/images/flags/bhr.png new file mode 100644 index 0000000000000000000000000000000000000000..135c254cb496638361bf40c707acac022ac8ab8f GIT binary patch literal 567 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!I14-?iy0XBj({-ZRBb+Kpy2ZW zpAc8~0-tk&YM)P>{dD-m|NsC0{rd+bfg%v{ilOsW6E_I^=hNr!x9on}-1p<|gL5Jp z->%)fWy3ealf9wJTIpG_s5SP_aEH}PrKzGf5pJ*l7{uu#-4LRYL6>hFKgPI z6H@>8>-Y2SsTY;Z?#1W+`}6lsXv!5mhYNB>=Y%zWK7RJFu=ZK! zH|_lM$AG)^9s6se4hy9O$IGvDu%Go&vh|`;FU=tK09Vm;U|v z^ViGQ@78R2w|2`-_wbwU5$8p<&WUQhm^kaj#M#zeY!855<|zsC3ub8R2ZFZ#_wW1r zb(*VPf#UZ(T^vIq4!@mtl#khv$7Q>Z-%KDVJpK3o`^<0G!fZ=SCsv*O-4K{`{zjNc zr;8HD>Yt+B7gH}cHgey1Irn2pY3lw*Ka>sx%6t1XWNf~7UXju2j9SNmvh57}3}og` zRQh&{_n1KAy9L)$5~QQ&TD@Ri^-VkQ47rL(#%M$D zJ|C&x;)|0%G#9FsYku`-{J@1-%e3%~b@QLIsIp{1Z_xe`S&IN^R z;j=rQcC9P_+9xCN=g7{NT91xTOf7n4yGcc;Q(#^LQ(g0PnUW7cpE7v5`njxgN@xNA DZE7OE literal 0 HcmV?d00001 diff --git a/assets/images/flags/blz.png b/assets/images/flags/blz.png new file mode 100644 index 0000000000000000000000000000000000000000..06b23f161a3adf4e224848390dae6b9cb6ced71e GIT binary patch literal 1777 zcmV(w@aFU6^7-06m86?Cj#{@=SNC`swWG?D_BV_hp@2+{eSyzq#4r?^Kjbp{I%2 z(a59Fq@c;1fxLmR)2`&<+)jo&QIJQ`-s-cvro6$bzTmokqGwW@Hw;CKx5(asvU@^- zHNxV-daiQ7%;a8*O2MX)jKqy+69|O0fAHqunrkd%1d(Tr+NvpK(rK%@oAus=+{3V_f@n*QLekdXm&%rCfj;io$Gfqh|N8!?i-J6)084ro%FD~; z+}W0=fi8v&tijaP)#<{*)||Ci~ozh3Rn zKf`}f-sJw7oqed!%vhbhS!$on!jGn~%dM-IX`I}-+pB1F8(*t9C5KIBu-T^3nWM?O zc)C_DYyg|D-tf0e>dUcWtVdu?K(J*D&H#Z3aD@OvrvO8z_SF0^00001bW%=J06^y0 zW&i*KpGibPR9M4fU>F6XU=%ez+(Ult0=SJf<#?`#8N?KQC1dG4Pa!F zv{g{%8lb_eC@-ZJmJ^`F_36{6D5VfCH7`FtPDNe~A`M{e#pmB&Ddaf@c8VTx7Edcs_mt;oLPu4fK`%n#?ux~&S}3l+AZBM z^L}0MnS|y@&W;c-A5KYD!UkxF8cFp<^K(TetotDm6UoU|X3FQn zOUM8Xjbc@=cz#Y!wrX$FirQ?op36O#0=>&QIXPwhOq2vfH3%5M!Xy`A5F^jY$tA(f z-Qe9NbY;TDUOhbtE})SzZc@5MEQAf<3^!-v7_xUc%8R$Z6GP$;ZjV%_B59ZNZ$vth96q9-s^O0(R>V6^+cCGD#M~ z9S4N>i#a`iv3Bj7kFlHA0HdWNs6v{Ph=imeDkoe%S2^nRky+>Ntk2KS|FHh+yIE6B z*f>4h%ygMph{{Y{UO7sq&MuBhN!_w-+m_`iU*6Al6t*-xz*)jdOm=n_kTs4A;N)Dm zs9^Hj?c1d%7Zfbw;#6I#&Z)JFg{T5Wn@w1)Pev;1+8lND?9JJQao@7m@Y@?(@aHry6I(XE&xyi8rb#y9oRwIL zWt_#jATZk6ryH2Z$H;i3CtFu@@w15#U#x>F7HuO=DPjLft5s~~saeT5a0={XWFf6; zbCwhUmcV340Gzvz+C;$Thy0{&V TYF3V$00000NkvXXu0mjfnM18k literal 0 HcmV?d00001 diff --git a/assets/images/flags/bmu.png b/assets/images/flags/bmu.png new file mode 100644 index 0000000000000000000000000000000000000000..6e253a8e64a0ccb0499c4f7a901aa0f1416afb1a GIT binary patch literal 1518 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 literal 0 HcmV?d00001 diff --git a/assets/images/flags/btn.png b/assets/images/flags/btn.png new file mode 100644 index 0000000000000000000000000000000000000000..962e5e5bb46e90ea42ebceaeb754a7c7ad57b26b GIT binary patch literal 4013 zcmai1c|4SD_rGTlW`-n7NXGC|Jma!Hpg^0?| zL_Ep9jF9ckSR$URW9c2$@2Su4{r%qex0CFS%FqkIt3;_7c z0l=IS0H9L=K+uO$bxwonh{W0&yPKK<$C)|@zy{(4SeP1!`2ZkcU@sa|157|7f9n>Y zqdzfV0Eoc>tUoarrraAw%(=(;Az5-jzar*<|3*9Iu>8{ZXc$ffX3YSnkC7b_0H6o= zCJ0EU90CBa9L~zt&(`z|+Lhpqbao@SV38zmpFI|!K|(WCZ>*m)oaF6=C!$H3i0=qA zQ{OYA5b*C1zss5kThp`flY}c+xH3`>DTmO4z~OL>D{k&+3;k0+>CBZT!qd;s2aQ4n z1_mMn6_JE19w>P=H8qr+0!l$amWhxh2I2jjNwRpN#E&5V6GtCQbiIP}@xu}D@V&Us zE(Cu+O$1^u(cj}oomdj?S0+61=eC#|MD2M{@<=(<|3&k|x&J@3Jy9votMb6^-v%{>|~L&fkc(zldtezY~9R{BPn(f*0Y6rH`{KR!jaT z+=+fTd(YOkli_4vmDez!7DPz$1g`ujF$L0ZbErU3wE zW2~=hMFK5Q-X+=_^0#zeTXA&{-Zqj^2TSv?a~>1WuMvQMbUUbQq#MZ*#l~@Vmvz8M z=C*CAYZ#A+fYFe6E?hU96&lMIp(EK6FiBI)qD@J!#Z?zxe7R9)`4T_2IGfc@DySx? zSf?*;h4y_>_n+%~9f)}&Xx?yeiB$viUqoskI4o#qmipDPO|Sn;hiH;Hg;(u3FDq&^ zKHnWFFt?As<{rnf`_CmR;)|o;W-sJ^LbC-#EPWU)nD4UhDb+|4VlY6sMY(Wb$ZKY_ zxyLv;Z-j0?YRJ<7L`r4xk7x*QZokTKDdTi0L*&wm#Xh_;M4B&<`u6?~kB^+1>C%mW z(qMd{(e}e;=(y2me{}oY>FRREc76>IBwU?2R@hHRO|iB1l1esN${5?&X)|EW7o?L$ z$$oD`>Z?&Npl1@VPB#*j?o(?w86!gsce)8VbmQuc;=n_YPZQOf4yRPFJv5YQj|X;^ zx*^G?R%uSS-jS-4oC4i0dfsYwk&{lSMD|Y0Yo(&9&#l2fEmBAFxX7vE4l)la73Jt5a^H zm_aEk84tzzF1=1|UcK+l9aAW*kZzh7lGD}>%?W^chRxpOyj*6zrTjGY^1&C7hgB4H zp|F^`dUMIeF~NzawD$aW6ELCo3975b=R0eF4o0(=N#S2Xvn`1%41wcgx56n`!xhwM z#~lcEg3JEo108t2)F4psawuZ7C&qad%hA=)cS>pGNGi9j>Ue}LoNq_#nHD%5Su&X8$XLrtB{oI!bMd>$(QLiQEgJvpbX{>D&qI^)&QLwd zwX0rrTKhG%wWYzYj(-C6P7Xolc>R$%5iJLgMy90!bMv6gO~z#rmkx&{cy@`{gZ}F3 zPky1SWD#*My_ZqqQ1xkSNP?5dbhn8YJC`r*v9g1GJzm0GUlrv4Ks7E}>g=>W&@^Y% zRm;WBQq(ze#)h{+rIEL?ghfWrdhUVw86UNNA70(%K)W^0iqISj-Weg*{HTcsLf@J&~uV zMB3mI3T9gnN5}07W`6C@aOo&H(>%DoSa`koHke2HRt3JvFLczq``NbM1lp^R(7iDR zX>qL>_qmgjMAd!b{E2&nPp_|U&Oe)TGx)0FaJ|F(N^iAe^6YK~?0;D#{4M;d0so7%mfiy0k%+bol(=tx~Y zZuZgO0CmO0nU%&}pYY~`N78T*Xf{e3$aR0=b)a(%=Su$5LQ-SE3zd z^eTwc>}7N>O-rw&=upABH3#TvJ}<^Hc|%g>n>zVToYW|X{<(R&MxGFtyf_5DuYHP+ ziROInu*DJQ^EzvGX4?CPO(;9p5Lk}8L8}-$6I3{IKu`~$2h+kTpIp?lSWw+(9S9nk z{_5S4<*Q{_P8N}E%$IBSX@#T+`*i7;h&cI~hEuM@LP|FHa6#e_A{bd%wfx896HaZdgBjQIaA(7>od5akZCMsKRs`Cu*7)bF1<9~ygikYfj<@p3Lofl*{NUthVqCEUYBN8a%A8do|~mBR;D%oKf*uzqc*IEP&iDt7=qp8vKq z;aM)&ZsKOa=YSsd0b^&@v&k@}wC7e&aD|s|Psx)C>$p;lgx<|rLrcpj0}`W&&vfuf zRkE{I7Y@AL=V|)H8k_{Nf=B|f-K&qbjB|PUnhY&t{Oi}EjaVk%_?X&f%hggWrpp3a zd2U?j7N!OD#$%&`+!rFDR@f2yu*K+zLS=MYHF-Do)2TabuO7yCJr9fGC6`_7;$YwM z@OuqeJnER-^Fd6Kv7>nWY<piK$mmm-GkDBS;n1a7s-4D362v-nil;tdW{E( zE0+Y!V;@iYx7~F5bpT(^ z5hKSLE^s5o*YmbQOGuSqBuSiR>@a{N5|AGKl`~fi&qAbi8C4^ z+1tNifu&qv@bn@Wej>2&+{mw3KMJux_be+g$j`61-8>Ox5 zVl3Im0|(xOz~1reMPsSi>yXEKi7+h;tl{n8E@F9g@I>w5k0+(ygYH(J`ZAtIQ73^o zqQ~u2zkEDV7k#2}=w&HyBAa%CgIscdph7R%a@`~5mTNiRiA2M8N6gj(tB!5fyj*^L zX!F5w$wQ4;S}@@3=8kPP`j(pdg+S_Zz?x_BqiUb{vU0V63Kl> z?zqiLIv#Ais;-J0sVZJ9Sj`oDxO23J^U$V3{z&llJcssX%gLt$Vi$MHt#-dchf$@y zS)i^5c;D+_SQFh^p{Rajz2G^+KgY#Tq<-rrrV3Lt`A%HlNi2}t=4jwW`_e0|37 zA@Q?iDGAoJ5g>xG`1HB+xhN9X@-70Zv(Yi?RnE9u7l_;xf!OJvRI$`z^@>a&s;WE? zZdlL{A+SSXDoTns4U4jWU!01QpRU-dwAPm5t6- zM8W3anLKK4mEz+fqF%F^_a(Q#WX!$-WTpOqoi5^?GG*s7VBUaHPPz8E{TtdG4*FCt z_sxZ|hE6IQj@ULcEOb07CGuGRu2>$t?;d}^`5Nd&?VdSyRc@rayk@cUi^QBy^UcKS zDy*A25$~01_BsNhsb0kfD-RZ&sPPx-bYG_2Q_{{^pI40aGym+Rc69hL%-2a7geUX5)v z(XvEEZbozl2~NZ{4t`y!Tj3b@mLjZb9=?-3UjcZm3L@hVzc<@moBno_lq7X##ltVH zBzv47*{=Ozv|sKXUYf6t__!Vy7x84X(A3VauDX1)cI^!OUVUxQ1?8oV_$|%DyD0uG zwu@4F@OyvEi`_L`cB<((;e+XEXq9C=mLBIMfwRc}QIu+fzp-$>4OPs#BU--q6KZT= KreCb*6#g%F1Oa{k literal 0 HcmV?d00001 diff --git a/assets/images/flags/bvt.png b/assets/images/flags/bvt.png new file mode 100644 index 0000000000000000000000000000000000000000..4acde7fbf2e00cbbbd9de5ff2716f3428bfd3fe2 GIT binary patch literal 147 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0L3?#3!&-4XSJOMr-uI>ds|NsAIP)q)zVSa4) zIZvQCV@Z%-FoVOh8)-m}si%u$h{fsT1PQkWpu+!&tC#(2Z{j~3-C$~BBBCL{#meZy qQLxsDkvl`#q3y6+*co1B1_s~BH_RBfiG%dsrK@fOdH?_aS1!Ha z4rDQw1o;IsI6S+N2IOdXx;TbdoK8-VaBDCXVP)wtVEp%A;{h}CbuEz%2@6;ZWXujJ cXGt(JaPDB{xf%0S45){})78&qol`;+0JQidng9R* literal 0 HcmV?d00001 diff --git a/assets/images/flags/cck.png b/assets/images/flags/cck.png new file mode 100644 index 0000000000000000000000000000000000000000..d255ab91ab79c85cdfa733e57159c80c0a231976 GIT binary patch literal 1139 zcmYk4c{Cda0LH(F7>UHHT0vP|sbeeReAabX(0FD+>KWFiuA#0{Gu?Yc#1$E#bDu53 zv^qMp9d)1MIa+a4jBV}Owc5>E+q`+R^uPV_{eIv3{{HegKAu#vE*SuoeTwC$u*>VH z5*4hymAR#mTH>jXlL6Z3>kvvUX?lQ?GRK?i?wRPlzX@U{l=wg*py2-sfsbb^KnB#Z zU?|2L0t`<r477pw5E$K448UV%XYs=kA1A#RW1 zk2O4AMpO%A^LV)iBQe}6aOno#DnQRDI)@PCB zzQx`TI9U#-QVdPv^LAKBKoR1DN=#0}@hjXM!S)(PrV!HtmkRKj@JS%5!5O%`41Z2OQRK-ci7yW$vvA@3`mAaph@taBTt|36!XR}s!3Au{5|(^Le0)2n4koxfUFNen15aZseZEWlEG zMpO1YBG!#;$F{d;pLtt1Vm^dJ5@kh{BuCOyY?*Dt+zYZrCbR8v?ia~dqRS7RF7c#h zxk(600sf5C3$~)|p+$Qp(}63nqrB{}G1eQbkyHs{_(sO`SA11!1yjw#!_eF;*n(xE zdCuihPKA%Mh%n=Nv^`GW#m$n^CK@O#UQm^q+B!MYrqpbrx1-E`9kg3--N`QQbfHOG z1;w2Pzx=+!8gB~UOLOJ0RilJG$4|iJB>$hBFRd)Zy!eNjb5qkEC6=M#cH>gd#i0Y9 zpLr0+#A3O(HBCb!L_WJz=R_1{rdy1Z8Sm`m+49?!pD2_3yiC+NG&@&cdPO`bZBnsv z^#dnWgsdW>X9Kl^?L!%JPF!PDAoG*jmio6RY# s^rJ`S0|#S3d6&eG=t{FAdkmTJi8!kslkBA(6{0u@*zP{88*V(of5vX4od5s; literal 0 HcmV?d00001 diff --git a/assets/images/flags/cmr.png b/assets/images/flags/cmr.png new file mode 100644 index 0000000000000000000000000000000000000000..2bc6ad13c4606109b4ab2a5e128aebc378c477c5 GIT binary patch literal 402 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!I14-?iy0XBj({-ZRBb+KpkP;k zPl&5~fzKI1HTy`PHxs4*TogMesCG_Jm7yvQ$Oe)S21Fdlc8K(OJ5~CEwCcO*QhzRr zo)=U7eL?Jwzrr~Y)z`h!=Y&;1ERy_rOzgU)(#sZ^Tds<~&x)OwR6Q@D`m{*)X`$@% zYT5Ims?Td=KQ5E}xK#3SmfYh^xj#S?d=&3^E5^lII{{tAQxfDC%rGAe-oKweb?Vu) z1v7x+(>z@qLn02py=KmT$Uva&;f33hLesyvy?FcozwIVNnaTc19QUm&Hp$(brV}fm zU^3yF_Pq*|z(9m$FlmKhxoYyHYLsrlN@g)y)7 zK3X4q`J#ho=jN~b4!w3fX3Lr%@V(kcmtobjvV5(2h22xUrd~fI-LdF=*Lxm@1M&=y e9EBG;eqr0w+_Y1+?$Q#V$qb&ZelF{r5}E)C5~Z^M literal 0 HcmV?d00001 diff --git a/assets/images/flags/cok.png b/assets/images/flags/cok.png new file mode 100644 index 0000000000000000000000000000000000000000..49386516dc346dd150a2175918135f2fd6621b29 GIT binary patch literal 1416 zcmV;31$X+1P)OH~=4Q&lD@q6e|E9Z2%;F03>}DH;B+2 zG7v0)@YvqdWq2n>k>6%@zQNZMG=>~Ji_zET>YAkg|NrX2%KrWR)ig-iNLtO*hsepKlCPa`n zQkdM~?Q(^;fsDBJ)7R|p_0=^=dW^f(lcYXboQ|HwGEbM1p~nFweb(ISLtUQ~HHWpp z*(*ttNn@g=veB`;*N~vb)7j|m@b*Vxp{%#mTyw1b{r;!3(&_B_Vo8ZTAlOe>TQ9vPG_Zyo5YEl!*7GNPiUoh zi@MI$=G)-xb&0u?qR1FHiK4I19zKj_e6e$fxLtItvc1<>aH@vYNpTD=Iq4G z@3XquMpzv^jDnB5_xSqy>Fd}-RpaOJ?WnQt-sJb&-qnnq>6oJZ`T5N&Lgj&t-(_^* zYI*Ibu;1hE$j#uZ;Pt2g001F$QchC)x0X@SjC(mW5o{T zh6OrE2CymN1rUWR6QdNDwZ&QmD+XJ{#w=2tA74tE0g-j};npz6=*%@)Sl0;h`pk1U`9Ru@vP?ogGv4VaoH1f$l~z0LUvw7y#35uK`U+ zrYR^!1~CG}HUmO* z01*QjAl`NSh&?pXhV=US`u5vEIHoBF_U`BcN1}lph5@!#Q;-4?KPZ~vVv8YYST#L> zNCONaXS6K95Z$=8Aj*nJ7bwPrM`MUZ>uw<03yL-dThRTGWD{aSya5JHo0HIlB37@n zB;Ek6?aGabC?X~6m75k5Z$LuARz*cal)_18O=3Lp1}G|;*lybtRs`kD4vVX{GeIsB zi2(7H%0ZT;`QeKCI@LOP%5gS|BpRStGpklH$3I9hFD^zgs$z~JsRmdED+XxKP&QJW zq+BqyRWX2M1HfS7gzV@rqioE2fmmPyih2Wm@(sXIwNYvS3p)jXWwd!T3P!;wfCB(7 W4pLx#hQQ4L0000Gs5=OO&R*(zj=6)nKxez}Sb@%V z-(P-4_0spsfto#70)A}VUZG}Qff@#Phno2WJ7cT8Ld`5eo55cM_z%i=qS->}jTGCA v{zfvJF{P2rW=w0OV0J(d&Mw)B0Dy}VO50DxlNzXC00000NkvXXu0mjfuugvy literal 0 HcmV?d00001 diff --git a/assets/images/flags/cri.png b/assets/images/flags/cri.png new file mode 100644 index 0000000000000000000000000000000000000000..029bbfc4981c51c45fa2c46419cffc3eff64a0f2 GIT binary patch literal 136 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0L3?#3!&-4XSJOMr-uI>ds|NsAI(5^oxsFtat ze-S9oSQ6wH%;50sMjDW#<>}%WVsSb-LBg#eTa+upL7nkhfQW#DG~>Vj8V?SC5L@6s ggD;>-(v5*3dclmcD_e?Wfw~wxUHx3vIVCg!02gv5{r~^~ literal 0 HcmV?d00001 diff --git a/assets/images/flags/cuw.png b/assets/images/flags/cuw.png new file mode 100644 index 0000000000000000000000000000000000000000..92a36b728ad8bfae0f3b1e5cbcbaa61322b18f3c GIT binary patch literal 481 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!I14-?iy0XBj({-ZRBb+Kpx~|m zpAc8~0-qnxMJ}FH`T0VGLA%~JHI`Ak4#)r^s7PAurBf>Z|Nr;OSW?ur!#rZvzkmM} zT>F3i`m^ukJ!aj8_n*GA=ruM?Kfq<&a{l^rDTki$;#F)0O^iDAs$LUCZ9D6x>^pn) zX?fo+*OY~VmhEN{v(&sM#Z;{2GjF^5=-t_?&o1435nr|L>$hK36ZSCaG?e!26t?b| zy8PtS<)<`#Cttt&YWA8lv)7z`{NiKvq`e;Li@$#VRX=54qEpK(pf9*eg8YIR-oO7p z_5J(#@84@2bFu)6ZufL?42d}W_L@6ivw?_fpn^lgk!9Dv+^f$2eLrlD?uQ8-7xp{z z_x;g5y-6i7#5L%(!zu(hN~^&fgV%{@6hMg6vP9EhV4&4elC==`cvcl^NYti21 VoW(DSeSltI@O1TaS?83{1ORaM)>i-k literal 0 HcmV?d00001 diff --git a/assets/images/flags/cxr.png b/assets/images/flags/cxr.png new file mode 100644 index 0000000000000000000000000000000000000000..e644a49ea96be08fe5e2865520ecd6583398964b GIT binary patch literal 1202 zcmV;j1Wo&iP)>U2gy(tQ?9$034Sbib4P!mmG>h03odiJ#`z2 zLL7=h03oga#yJ#hU>k`-1~--d#W@Zwu@*G6ABsUflR$W)J{^id%D6l7!Z|FCK^}@h zytF%Tpg#Bb`~oAbAU(G~Six$2&rO#=9Ew3BKe*k!I(DHxCX7L$tvptlKME(PP*sR? zp+1IpZ&z7`J~NO(FONaZxH?prKcukXil;qKX2!|4JHxg+ zQfbHg`~4+Bxg0sRc8AhVmq4w!GAXW zCyYTuTf(!kJPawP{KYv)l|U|zLF>6fU0s5hsy)ZIJJ!28Dvd$WxjIjmKZd3~zqLE} z!#S+5JgToe<-a=GygDU}L5Ze4jeKl{r9Hj0JGHVqA8%h4LXKi!fRBA^ji^2S#W~`> zI>oj-!nHf}!#PQnK%1*PnyWnOxkBl;LT{fwTADwXsy&vfJ@~{qIFUedpgym#JfN&R z*t%(S%0ue@Nbn?wK zlbFHRj>Q1AjO={EMra%I@l93dYs6xJQf8VWnn$F?wY0>g(bAF0G(J8)DN#%V3JbDC zP{Tq_iVvh#4$To=Ku7SUdzYx9he1v*YH*qgf%Gq1A!~~64Uo9HPz9D)Kn9B}B|!>T zTCBF5h-!d1P*}*Tln1*334y{8#$vt&^CXwA-OaOgl^%+I(PE(_Uoq_I00=^Z;vkGA zd}d%pd$tR$LDnzk9}tF?7n+0-`V~^;ed58ax$~FK5d)^09ejLy5!yYYB$Lszvs!bx zBEp2&kYEU-7Zd_k+jw^J@onI-Fsi9iQ7LkFcJ)AZ4a9)9mNF%%ZYLzabo78s*t}~C z-@1hYJgU`|dK!6dGN{1`^+LTeRF9*B3z7-O=0F2BZR9gI#+f?N15wz{8aX_5>p*T; zuZuf@qZ?prV~-LeItKds20D0hD&B;I2{4Ty(Ev;%NHzf72vh?iL`XIQ#Q>4;#7I(I zfnq>Rio74G9zpejax}?dK}H~=8bOf(AS1Ye2C%YI0Juh*N26dAi~=|S0A|UcQ9Kn$ Q#Q*>R07*qoM6N<$g2g8g?f?J) literal 0 HcmV?d00001 diff --git a/assets/images/flags/cyp.png b/assets/images/flags/cyp.png new file mode 100644 index 0000000000000000000000000000000000000000..ba5246809c95c86c26bf5a529ca841abdb95e69d GIT binary patch literal 1128 zcmV-u1eg1XP)wLt#r$^EuN{`TqpvN`_u>;1Jt{{H>`;3{^Pj)$8Y_@WB%Qz{^!BV(Zl}k)&10x{`l_w#AyEe_5I0p{l#hi z*P8viQU2(~{QUfnoPT$QWY5*fo2QMx#VlAQ z`TF>Tl5$#YL&VFxgpzZLn0n*p-+GB>hLm;B)W~;-WXaINuD7CvlXUR%?zX{e+$`}_KNiD&!!`}X(sn52r8p@w&d zW&O!>{neEIfnz(I=cv}DE1XcQ3+QVd9n&yr3-6C=R@?g}oh zSSe<54X9U8sU)i!$5>xNupD`dAC<3JV2AZ~;M0Ee|tE z26%CyfD9Sz%5WQCg=#<)Gw}v^*cWn27x$GQCYNFwps@w&6>qdq=S?Ke zfXzF$?F4%z1U38cbvF@b0I!a~R&5M*tCp=%n?$4m`}ZE)wFh0@ytz7a)QJtmeOd?B z&hJ4I;9sq-wQ?!(v9M|Tx<$QxC2;;Mm4ypbmkW>(jr>d6+xS$K%L;+)jw!RJPo23~ zdj&H|B~n*z9$#reQ9d*O6aRXx%jDk@xTmS%+KWr^)MU)Ky0000|3jrLNoB?9;hH{h<8@D}s-&dkmq&eN=L?AQX(u2Ad&CN` zM_7P6!U1?ATtJ9>y%S)J2mpLU1fU~g034A5pb<#`8Ib|#BXR(FL>?fH@PYRW9^nJi z1dnh4Wz0Mqqp%SU(2a7**kL2Q6SR~%e9GIg*^CFY!_Wr*A1)0YP`dB6Lh1wHX+PBZ z@C5Aoe*mR^1EAIC0b+d)AlGLAdVLZ=>Qewz9|K^01fcZ+fY;S+002ovPDHLkV1iqMu^RvY literal 0 HcmV?d00001 diff --git a/assets/images/flags/dma.png b/assets/images/flags/dma.png new file mode 100644 index 0000000000000000000000000000000000000000..7f61af95e6fadfa4470974e4d8458453e325d924 GIT binary patch literal 1224 zcmV;(1ULJMP)%^EE!Pc+jOFx4C{*cvOs7%SNoG13|@!5Az1&=&sv{o5BX z+!!w2MONMyF5O5fvK%KhN;6eUIkg}zt{f~$LNY8)G}{_59#b{g88D+IF3TG(#TF~Y z7c0&jF3=e*)*3JXT`-Uvtov9W1UL zCcqRf%^EMj5+qnYG5FKh>2D;`9WWeYJ;)j@)(|Gw882fwFeYI@dNMF~Gcd#xFZZw- z)e|v6Q#&M5HHs}S+dwN)O*z6GEwvvlSv)b`88H3#_%mBRUP(FZi}N>Do56e=!UJRoC0`pg$dQ99=q zFa7%ZEnPnKts9&tF1H{q-%Vck&(qZuDdu~I-9Rh!rySB1E6y1%=W-*}7ctlrFQp_d z))gwTA}-ArF6V`i))g_)6)NRwC9xbVv>+}mQ8la_CdL{qp(ZZc8Zf3ME~+Fhh%PVJ z8ZN9PF47w<))**AMKX{oEQu~K(;6zeCrj5ED%Be?pHr-zD=*O9y&T-cE}zt(j6!VSuP7!I7CoeSW|D;8#1Lf5W^!Tq$V$!C@#G#Qg}sf0An)U z8Zet6Fq$?CtRgO)Bro7WC2cqvYDX;ADk>3SFTHIOg)=J0t`THh4;frq1YR}YH;W#Kk}6AMsOmt*3Y&b@W}mJK_X@w9Vu zDaR;cQ-{+4XE}d)CLY!u2lj7UZ!XQnlpP-tPLcsG?wUc&OsqEh4((pIREAeXC@Rbc zrvdC(fSZS|BO|Nyl3klESFhq_<&F-O*TAL@rvVDSf%0su((_mBIcB*sk(F(x3Y!9H z2C!b#XJchuu*Tu|1zj~(Hhyk4S&|IU5lPE6WMy4;&f&ySTTx!d5HC?zoCdI90jcQ* zLd?9ZCvDE(w>L26DNfMybi$?%rvVvaIohmT%TFJw~sw&|$0Bag8SD86w;aPsOo7ZkwUD;$bQ!SX(%v34dYp_^=_v$67 zi!&sRgkwq1&RWcU##|!Sl6*X7ZN(|%6ewja6Quah*zt=ssV0+Gq*b&|(l9ld++3J6 mS}%+mFcb_J1*2dT0098Pi?3eDJ`{TZ0000s`{{8*@@bUin`Sr}v{rLFfd4&4u>+Pwr z<9CD9EkNF1ZPO|}AZj`E%FaquHTBKW&lf9lKrr~*-YI1`&lD--dx!0%t?vi9 zE009=`@&?SiWI0&-+QM}!3pwk{^%>lK4T@605)LiPrcT?em0Va zZ6xVdmVZpr&zbK*eGe4NmKkXs!I1{&zMG^!6AAt!AP9nU1$0JHKlNx0jsO4v07*qo IM6N<$g1J%n$N&HU literal 0 HcmV?d00001 diff --git a/assets/images/flags/ecu.png b/assets/images/flags/ecu.png new file mode 100644 index 0000000000000000000000000000000000000000..efb3acb43c117fc506f992468e2cc69f46b666bc GIT binary patch literal 1238 zcmV;{1S$K8P)dsQudj`q=^f+yL&= z3k*`922Y{!&;SuspdwtIo`x0xN~aH0qaItN07a}raC-tyqv*^J|K

P@?tK1Fw)N z_t^q58yxJ>3aYC!07S0;?EuT3708|y@z(~~rx2`H2ASU($Qd}6MzFw4Rlb(ELC z!6q3|kif$vRV#4mG%jE)ZNXwiE5x0?tkVu^w-R9J#wZaZ8zYJN_AWsiF< zx0xzbKP56{m~e?$T0&pR@T_Uc95t(Hkes@3T%@u zc7`kfhCi=u4O=%Zo}^HMXF;k{N*8aSwU#Sghhs--a0FmyxvEu1n6<~mA3vS39dw;R zev{rMDt2W`u2f1PGm#*Dq&k;+I)9X{a!*ZvieRNcVyaNZ!yyuMlZR<(YmlD2m=_^_ zrbCW`NtT&-P$N?^a%EkVMuC&jA1ffN);5o@XsAfDR#_jf+*Gbms*ee4$V9DHTt78> z%NsHed7P7!L-t+&6$=Z92lV_qbYL=akj4rOc4RL^E!?hERsTogV zgkP~xGloHvZfItxiBqG4X@iF%TV_sumR_j5Lz=nk_xt1kqK;>r8vp!ZsNRS75V~?sqStzEzcw`lU5bvqB3>8O)# zfK0yT)XnoI85%Cwy0A$zUWRlR$SO3NOw?=d)0^L_q99AM3z)>DqjOst>T2uTn)70P zrNx*?G=Nc`Wp!)Ibd2qyUT>Flqqx3>XEYU=#oW066Ei|7lqACjbBd07*qoM6N<$g8Y*s AD*ylh literal 0 HcmV?d00001 diff --git a/assets/images/flags/est.png b/assets/images/flags/est.png new file mode 100644 index 0000000000000000000000000000000000000000..c2e417b93016d9acfdc636f69f9065c42e6e8dd2 GIT binary patch literal 130 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0L3?#3!&-4XSJOMr-uI>ds3`OVv|NjpZ&^JC0 z(!f{}Cdiu z!FT|n=>VYaRJWVP(mv7EGn2+=z|cdB#BGYhavQ6~0CZLWY(Wd6*TGCe5~<7pa7_S` zst}&f0Fk8toWTI5+yJNG0H)hvyNw2>+dQ(a52(>hwxCJ1pmB(aPxs_A6nMSpw zp2=NwzkhAMh0xSC2&UGD#BapXK7+z^y3a^4u(VvclrFEgDyzjOuDoHokebL~0H5)C zz@XFMdCgv$I&{+&^;=zxd4!%HL|cn zwx$51*Z^`KU0HWg-smwpKs4K0#cE5c(vaNK!f*z~Hcffrd zs>uPP->Az|U%8ZOy@!Irba=pfF0Zx0(LV^K+Op0|m&ap8w5B7izp2Yrn8#wf&qu7x zQct&?ki}_Iww=?}F^a@(=h-9#rQJKQv`)93L9?q%ww`>!ddSi}hQfB~+9SEnNj9#z zrOH?!tHsUJIr-icS-6;XzkVyNzCE(6alM5gtixx!jsD&c+?Dr800001bW%=J06^y0 zW&i*J-bqA3R9M4fU>F6XU=%QL+9DJkh$SySyujsYzbw+l&aXJz%0 z64K~j=18gmi#>#xS>QlQ$jy~B1LjMrGl6un35GL*XeOa04x||1I?tO$2V{b1)Lv1L zejQ;cmPy1L&?D*1!nXCS7z=BRvY{L|3*+YXTrA8T6+{~_TbqT2d*88>0w+#=N(d5R z(lXfsbdI{4Ds}@n(ZLJ}u=i81U*eIuaPUq>=i=j24x8@u4`AgDBFcaWJ^@+Z{la{FHk;Ib7yW;& zcGi-OpJlgin1Dz#Q3fpMW#JZ+XXQ=(#`UWxBIIT^zhNR5tGoy>AuS>H07pYrIkP&m7dN*Lv$|&8WEBol4dCEVDXwo@>h9iER$8KhOATHFhykMp iP|JW(FbYNi5C8z!Lg2S@fuHpN0000ds3?`NT|NjqhzIPEQ z$XF8O7tG-B>_!@pqv7e|7-DfcIYGj$A^Ym8P;ckOVU7ozCh8rK>SAJI^GYy@3t(*% aWMHs&2+T0Q%Hj{y!{F)a=d#Wzp$PznE+jet literal 0 HcmV?d00001 diff --git a/assets/images/flags/fji.png b/assets/images/flags/fji.png new file mode 100644 index 0000000000000000000000000000000000000000..4700ad579f01011218eca0ade84690d701f28496 GIT binary patch literal 1571 zcmV+;2Hg3HP)gp#_sRK2d%m4t-4-Zhf=25la05h5Y|Nk9IrO*N>QnumI11bIf{?Z*C;)javsHo8h zE!s?QEI<*caJ zJSE#NB;lpA)fg*xlgQacMbQ`-a8`Clu;9N6EJ?fTO}pq;p~7dQy}UaqU8m7avfXT@ z#7?{CPpi~`tGCzK*`B=2Yk!{Y=;pW4*yQEpeP?OXyZr<+9j^=-!Q-(}Rxe_Vw(Qt?7`V=UNf^|Nr{<$G9Cb>)A=}*gE_6 zx7}oT-s!vWsR`dj43NIix+pKw03YUTijcX?k-g7$Rd~M(EZKFc z`^W;lK__-=huBCX@BH!Pa)y=XMzFqOj_&x$Kgq-1+S9 zw!pO5zR#SVy3n+*)wtc8jf=pO4`d8Vkxa_*+ODs915*I#?&bLX^Xj#=*Nc^OQg(NB zir`~Y;(2UfgIDJ0)nSif>FU^*v6$Xi8|I)2*$NcX7c$!^D)Phu^}hq|xC7apsmG6) z-e`epk7|9XhSiUlM}AM`ix6y%YeIQTY?*ZMtGLzGx?!Jl^po)=00001bW%=J06^y0 zW&i*J(Md!>R9M4fU>F6XU=%=sHlv6rGY1riv$AkpA76i7u=m<-QBcKLAtp`dG zX8^(xU<2UR6K??A5g-F{;ntI40LT&f5}ces1M=Y36K4P?3IG~_0-!2Q*ie88vH>0} z_y9zqqN$penwplLo}L;I=&70VAQ^yHzXwF2q}8st)YQ0x`?sw2_wOt(Dv-qK1r}Bm zt~k_yoso9-uToO%f&&5r1G+67B{2h0Nu7^TI1g-qM822?GfE&v9eSMk`sdsDGYJQh z_ihaC!D4{4Jf|SPKrYAtQ4vNN~ z00)x_r-TC35fFu%58q_vyvxc-Pq(+XJ0Y~Gr5M!!i1kt&91sIIm=TVEC=8AH`ziZ# z_7@?W*o&9Xo!%4Ei);YQdJeb&90*516zZov|MTPb|L;cJ;kS~KuAkbhi(~-YdSnB@ zj?e}=0%pL|mtTKdE2 z$*5AVifD_BmhMdEcF6A1z^;G;bXuk7zLvM2mss+ V--yx6DwF^K002ovPDHLkV1k_c9MS*) literal 0 HcmV?d00001 diff --git a/assets/images/flags/flk.png b/assets/images/flags/flk.png new file mode 100644 index 0000000000000000000000000000000000000000..66ff172c28c6efd1a7afd7396d809392bcedb848 GIT binary patch literal 1591 zcmV-72FUq|P)goeEngBDI|NsBY0001L!V`SR(j6V*hl=i~sQmr@-BnfGQBnE& z{d<+l;Bax#6&3HUu4aY85j>nWUa!|VIlrsIAx);(;P1+(xCuI)$<*o8CntoR&VG=? z19Qy)ZNeLS!2oW=)y~O9jlr&eY7ltGO_tC%S*H|1pyH^83pbexd&vM^yVcRn0A|57 zQlUs_v6OUcRh`tGgn9Y-`TYF+?(6DLk-*v9-R9rhq+&j%kB8^#_1D$Yg;_!51z`nf5shOUJ zgp{no|Ns2&_4l8EW7xf}caz8fK^SM2vDrmMh@_0chG#g8&oppL8$T7hp_3_gtEP)d z!O7a|#;sO#e#w$?_1MSc!8hErYHe$Ab!<~?eXd7hrz1$8vV&>9v9e)!$T5}Mij=+?VmSar7P7|HpT5|CtIgJoU_NS&W}nU4o;7ri zb==Ff%b#Djx1QFaRO7f{)x4D4lxJOudu)uDzjaF8n{q&Ilc=DXUW%-#zs$sSR?Mt& zGG2kiZ8*f(=s0JMpv2;ld~F6XU=%=s0IQHN8#@#*vv6`S!Z_l*JbdhU0oVW$Q85k$CQJil zq@@H|`H3)qNlp>r2!sJ3>p5g4i7U4b>JF zYbJ4lfT}82hyjuT5uEq{)PUIw?2K)UV`FSJHEq?@Rdpng48W^D0&0L+(Uv7?Huf6! ziN={_YDp*tpgNh81%=BDHDIoxNo?YBW-De6Yikcp6CFLwK$KVFXBBJ)8z3esqRfUG zh;y{LmTB1KTUq71TBoI&L@{C*ASuhi$HUtIGC){}RX|aW3B`a};asU2u7!opM=G41 z)smvnT>!RTN?L{yWB@yp0)``WqgBJVRCraNaIf}qS5XZ~Ks5kjy*N8N!~k|S3`gij ztGju+H+i~wcAY)wR+5e!jd1JP;Rdi{IAW=x%I?OKzUOb=@@@1zRFaMy3vla^4M2B< z+2%d9EgkKh^+4iU?OqGyFo0W+VgSq$P)-UkN-uwU-1}@o{|g|~3E2g3>(LB=I0EK^ zV}5NPCw+eM@agkO589481!1NG{2=R`k-U&87J-`2W_`KKGuL&dF0EurorTz9^xpv-f9IhTN*- pds|NsAIC_46DTX*}y z6|q2Z#*!evU0d rhromX{Y-Wj+!}b=d)Lg6c4J`ZpBJ#t;AhndpaBe?u6{1-oD!M<#Fj2C literal 0 HcmV?d00001 diff --git a/assets/images/flags/fsm.png b/assets/images/flags/fsm.png new file mode 100644 index 0000000000000000000000000000000000000000..b8aedd34e42d168ffc18f294864d9a2292e97a96 GIT binary patch literal 340 zcmV-a0jvIrP)doWx`TPBiz~j5w?sc-=|NsBU;PL44`HH^cxYzCU`TT;p;Dx*4wAJmO%IBWR z=lcBq=M0Rc00001bW%=J06^y0W&i*H#z{m$R9M69mdOr;AP_`VwgzPX|9>5v%w^1& zbSw`}q>cn$=!I&gX=rHZBLnCW0Hr{H)*!$+05DrCh=jHC*&8g9y5w>`1mF6359AdsR;5S&TohxNmyp}@ zSpX=`SQ6wH%;50sMjDW#;_2cTVsSb-LBg%!zdhFvgEcHW!WK5L9`q$7;KatO6&;`f3E#oP{{H@- z00^ZC63&vAwEzkc0065wKrH|QtpEwM9ww3i2!a3yK>!1l00=q&11SIkt}Qg+#m2P) z49G%BivS0?dVa~CpsNZJ0RRBW5*#%E0?)Csh5!dY00YK^hsYBh%9WU~MogzGGR~^3 z#Day<$H%+?3akJLzyJ!>01I>g1|t9gxg83*p$e%^3AKX?$2JSVF$&WF3(NouyhaMF zSqaD#3$7yxzCQ}q01LMr3bHQ=y)p{X0Sm8C3Df`!y*Uc9O$oO~3eExxd;kWSQV6(A z3XMSrqDLrqKnt?~34|;SQ8fgaJtK@D2bLrVK_>$*1p+Pu0{0lA6aWAK3UpFVQvmPp z|L^O#cyEpJDNm2{ zPe59BVpFM9evL-+c+YOgx-ZGnx=Jz>Ds_OdWweRowDu(P(ZR28Ui6i~hRJQ5QV`|T6pBV05k SBomYX0000mdKI;Vst0C7?}y#N3J literal 0 HcmV?d00001 diff --git a/assets/images/flags/glp.png b/assets/images/flags/glp.png new file mode 100644 index 0000000000000000000000000000000000000000..8bd0a69bf663de4fba86d0c6bbd02078e59f5281 GIT binary patch literal 1820 zcmZ8iSyWSp7Ckrj=H?nA2tfiw1w<4KB!nPDDabqo41<9L34&n^vkIa-RA8ZivG1U}{l7ZK<}6r9Q#8n6$-O%8S46WADAzUi)LOv(8zkBuMD9l4?u^ zfR%o}o*^1_{R{%ta6@PQJB_T!@D1M&pxyr&NHt5F34lUSfXK@yBLK{cfK1n&7orp#yq!sw<_PQnhCU=l zpk=w_`7z>f8rFG@rN=#TuXWSGT>seA(Ax^jw#zox8*P7n$N)Zo1ppbq3}PkdyYC4L zGvvcFV(~{p*@zoE=0KQcKrr)^03nz=9{?$sFcH-)Q!*KL`jIdc))<05?{HW1=H{=t z5eea8G18ioP8{!yz}-<*70y1An2!@w<#nt0bW!(?P)o^W%tMV@q0 zZl1%BAHbR_$agN{kO22CHbw*7KZjW zQ^NdFh6Kh2DCTC!TNUwclxR3XexRUiwm?Kj(a2y_{eZ9&A+NrpT)s}W%|?P#NmC<4 z+Zp`TAf={~?C(xuTOu1>Ga()r432qYz_@GH{OyK6`8 ztRqqAdt)$4ICMATkOtbj;hB=+kc;TXKv6ZOvmGD?to#shB?<@N>Uq1!5VYqi{#a>} zd(@BRu`lPaps(Gc>`T|(a>>O8`;rp(AN!4FMs?D&f`+@y90OAPcF7!;TLLvpPt6Pw z?u40{<(ca0bLVD^&tG5GTu6(br<+L97ZYtrZj7!42W=`aN5CMB?^{0UO=+m&7XFj# z-Wnc6s~!3@VZ3850htR=bRX>}>tz_uR+T0@)K9sv*-rKs0@&<`HF$2H{V!UV5ILV# ztFE1ED9sMLq^5PHw8^A6tS@fnw^_&4 zR}A~|K5EOT+T6Fn)Z*gVj(UEUP&HN(b~nQ;nLPb|YZ+BWUErd@p4MC|7dP70+n)U! zHw*->qPrQ;{e4da7HVA>bPxJgroXG=(>BQt5BiCmnjGDK*-uR%g{H@z!^h8py7kb)kDC{__QPS~n5sIxZO|H>`E& zBOr|DkHmSCEBB~ERc4ijYNDqkDeu%x*2yMryHXx&zJE*mRU{by&lSeC0|&ionV#^deB5Oi^@ zX~X_WviS5BdH3R@o4n`th6iWo_aO+UMkigeqtwie(N(uNNtNUNR4wxGt1mcHeaA5E zluf-_k5y^&AF8j-zuXvDkRG{U;Ny_Tx*@MxXL1BxT&x|p51n9-KN7p-?tSyeYW*eY zx&gbw#^BR|UIKOM#B1^WwdDu9M|y@k#zS=rpWY`~9m(25zxO8n>`Q)W8l)ei{aT1+ z7XG_T&^bGe>#%7RVx`CNRz#&Tp3LV=ItinVO)J`8oH9gz`g@6UZX!Kj?wVgo<8g6t zyYPN+vKF6J4227tkcelTK6SyZ+;~}JkM-+rqfwY~_VQ%*blRLZj9i|p%2)cwUd$@y zn)W`t(?NR)bE8Ga+NujJs_hpeMMQUAU3hw*P_*Pz?ss4m{X0>T>w6OszMPpw($9d`FROF74Fdm{{ycnsXzb# literal 0 HcmV?d00001 diff --git a/assets/images/flags/gmb.png b/assets/images/flags/gmb.png new file mode 100644 index 0000000000000000000000000000000000000000..fa641ca1a08d685d06cf8097e9a342c6912f44f7 GIT binary patch literal 278 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!I14-?iy0XBj({-ZRBb+KprBKL zPl&5~fsa+W#yLT?qMBVml1HZJ-@ktl5g_^W?W2OK-9H{YJ}0COb?F4|xq z;v6W=UZAx3|Ed*9jDdd_n;%#ka>8U0d`TOq=U3psN_fN`m}?8Q#BtKXv~5`9SLZd>{bP@81io zoz4POvE9?fF(l&f+iS=9nhkgw65X7bO85_Dcz>_@&%S19(-me9zgmXKpLvdt>=T+l z*c?xETCx6Sh?nI|ZEYqdtL!(uhP%)7$rUef=&WLPZx#w-{;0updRJj1cmKB!X9B|n zI;=0N|B-0tZ<%sHjH%oCG0*qAw+`=ix%nYt$-4uqogz2?Q+mjp`P*Tgz>e^;OZUnT zJ~(bsqoBz->AvP4=g%B0mW~IKC)Ri!Xm`8Je#$$G;`Yz$5^7VCHuo@uC{sOd-w_CetV+ z?2jAQHzP8OP$Y&_!WSvBw<{-yQ(~M#i>fvce_DvCH?_Db41Qanu`r#mF#vaA&=@6~ zD>Tw0B%UfW&>AK3s2S2BCG@cw@0%OsZXWiy7+;%1T$)4xd0q#6TzPvuKaflUdR`ZU zR|b7t6nxG?yG_5u^BKZ;a1i6r1pp7=Y5VkKYk6Mu z^o66*LLnLr_i_RJ@#nLf`@nLw*|=Tyc{jfOpBhyP&ksyMZ%$RJ^(Ddug=a@95EjX^b}Wm0>~HR z0)SBr(cmo|q-?fykh}&SfOwQ`*6O2dwdwf*W+&+~InA7acv9{rUHcMnDWUF)lzh9dC6m)5S^Tgk=6jn(aFharadr}7s@}pP!uUrB-B4`pq_X(g-_Q&*|NsB;yT;;YfaG(D(-}VV!_D7VbMUjj>zJwBNM-!; z^xjr+(iJ-KwZZ-N_|_{;>5ic0eUka$<@)C8)g?&DmectHCw4a6nZ` zKv_{h3$d00Mj03>K*@ljfL;cA3TR}YIRIW?b@I-3W_JTjGB8oV!MoEGFv!3_0ksU& z6p*CSe40=IVhM=B0bXvN&pi@=3QhB&xZ^tj(Y&9ZBk%yC-9Lo#ya|BXoI`%_MvVZx z`0X~+*uz<1-G_S6Q`b#;s1x7m% zW<8-My{91}-w$*Dy3F~03jgK{s|1I!N6z|J68108q#`LZEjoYr;}%LOz#}CRnxP`+X~Cz-4{3@Bg*w z{{m3Q7fQEbGkv!1|Fh=&WHfxV?Ei8tZ(TNmEJdfh>;Dc+zP^5!u^dKvEo`G8O1bU- zN3d$dF-yE4Kf2)fUAu|Cz1YXE#X_xQ|L{z?@c*&#|F7u$SFvnGK9m_swZ9)ay&gI~ zK$E7h}O!$gPy+6=Dg77r{L(oNM63f+Al+;X1|uh zAURBlIsfTXgVDc*&%AG{eUR4ByeK%!Cpf>{@~xqoV={f0B~$+8fXNGC_D zhbd#mAU8j(VZ|akE|fL6_y50qlUlihytT`Q&$_V2*I2fHfiG#i)a$zH|Deb%{6$&e|JiK0+#Tx#DQwLuey&fz`|_2*y{(7?3UU5((wP2 zyONH&ldZ9^ZL+EblIa(Y-YkW*?ezb<=K686s#9-+0EpvYevciK-$kOoYOJ77lb@bb z1i8w6L8F6XU=%!j6uAbJFs@j$ zY2ET^s-ol>klw4geT()s4L!|F8FCB|R+68%Wc6n4g_AoIorTFTz>ZP9MMGuP#;F}S zA&#bG7{DvflG5C_etLUu1dAmv=>~|HScu7HG)-vBS8)+DH#Q>K03K#fR`KA3th_K; zaUnitLy`@U;1*<+RQ3-D3iOi{66BU3*#Is9HqV%(qT z?10vW`eYAVev%E4@?m40dFbrrUpLhk*Ri=FMh)El-su*#OS2 z*@w^Hc=z`Ea|46@I|?{SHb748$hjxae!u+j`qi7OyVT@JPl+>*-?@GF=Zi02KVLgF zSCNdQ+`aqQgNN5YKK=CQ^t{SQaRAm003O^ Vz<1{)+#3J@002ovPDHLkV1oWGr`iAj literal 0 HcmV?d00001 diff --git a/assets/images/flags/guy.png b/assets/images/flags/guy.png new file mode 100644 index 0000000000000000000000000000000000000000..5845c6db9c8281966eb57e50b531404cb9be61ba GIT binary patch literal 635 zcmV->0)+jEP)(C5hi}lqk+$*|M%Jc^u_(?iT&k!{MR@9)hy@0 z6om^JxDO&h1q=)U0>ls`QU?zk0tJ=~9ITHI6bu4VKnSLa4igLlPdx|f>goak0GSOO z6b}!suC5dg52&c9L%4Z7wRFMf(>1bh`270${rgF}eZJ<II2byhjHoh9fI7lz7y*3*Yyf~^1YkgbafHc$0Y2jKo`A;fsU4sr zUNLT869O7%mq%8h^F)Cg1F*O-$AI&b3k$HePCwz{4opv89a}%4vw`v6E|{LF+kYaf z-)~iDv!uLsfqpw`1_BcRk<(wVHP z!0Z3~(h`u>|Ni)<1;FaRQtxyDWcA%%Kw5g@^wW^E0J^=fbOF4)*T;M2#fuk@-ai;= VSDSEy8L$8V002ovPDHLkV1oHp5p8zwN|NsBY000$4p)*{qCsL^$N~H-poyO7Vftt;4jK#gm z<#mzB*x>Kthl8+)+{a`u%&A%4vtg;BayN{{Qc;u4aY8HeRpSIXSC`7Dgq_ZQki)dY;fkTqDpjh$&F1#_``uSp-BeWG@ZoZ7)GO$ zsniBIn+!dkmaEm7t=5O1&#u1T%+~Ae@%TAmu+I(-cazB3MMcpV7(8RKKV`C4dAwSC zy^y4r%K!iX0d!JMQvg8b*k%9#0p&?VK~z}7?UmV9gD?<=MV3hcTic>m(Q2)B0g6lA zx7PLjA4w7*Y#~kY#tZ(#4IJj1Kbgs7W208BT9Gc~{R0Ui9c?zyR{V0jv%3de5y0VD zAC1f+V0b#{$*KUrJRc_}5&%w*tP=qMNYlhb3<%RtnjiuI2uxgtCL(}P`V=5BK>(ce zioym^6Bw{8O|Nygx4Rk}fK7b#5efkB<*RQa)Gjzc83llhGVw-BRa-_LuxRj45^EU?SVXw3-A!)29K% z#6@f(-@>$-1VGbg0I>;n3unqm)8_!8i9!qCE6P2y54cLT@XgiL&H8{;3(v2w=j#K~ z`uqYOAUASA{47MW?dkv>Pa(&_ovl{_+|67?dVQ4?cXn5837(H;D0qCY>K@qP6tOFB z;+v1-{kPSVgbDDT<3aZ9t(qh(fbsD7V&QP>rTi43loFp9P6rsm(tfruFns0JUn>3iJyVE?uFkcDWJ&0000< KMNUMnLSTY}s&oth literal 0 HcmV?d00001 diff --git a/assets/images/flags/iot.png b/assets/images/flags/iot.png new file mode 100644 index 0000000000000000000000000000000000000000..2863320f58639484d1bce5ae2d1829c8012b5d79 GIT binary patch literal 2415 zcmds3=Q|s07Y|i6S|zm}t7_GzT2!lQlc?Aut@V`CaG3i86&4Tw>v40RRA(K!&>JG^_cqFrB06 zoF>F3%`kfzLSF#@Y`y;qU5X4FKLBtJVEVvP&%o;el}a^ylEiV-4 z;)vs-x5osOuInfKIX)592~Ey#p_S_B#(l>Te`OZ)YoSh0DWp$r;-G}quXD?5J5f1b zDw=%Df4-&- zXXR5qII%i1xi*uK-#xT+RWDxcVYa?=As*rw829;2Y^$ij2mgph32%1QoDSjo1=4jxqP`C1FC11Hg8&G&}M)5 zrn0eT7Ma{RXYJC5U49wfFh29Q(mN%=070MdMvxCm(m096)xpqMnHi@M>d|i7mz2`M z_`-g(bhE8}(!wf<#wQ}SWkN<1kIFinz>V?WP`A&a9R8+JJ*f=h*N#o@N?zYOm`p0} zz;(L7>kI40o))+;c`!=^OA2U&y9T0`p1-k(_K`A0t)~}i#%bkNW04I={}R;em^@wI z5PJvjuhXMe{#o`JFI2*`?cI_3PMOnF*bx|OM@L6@sxa1q3H+B4R+<4nSBcP; z|HP4l2LPZ0_?km303;ZU5C+`SL2I=X%5|hwWfei|T_6V4PRD zr~sl_MQJZ{omiW$v7ZP{!?wxptp8HOtl0?#v^GaG+4+v@0A~g-FtFcXEh;MF5m$7` z&24j+a^a|HvQA-BtiCQQ=Om!Vu%MpH=n5XXKZ=85Fu@F3Ba}-V-eAO;a%EB*jG%KEd zTBak#TK5K9);Tv-b~=sWO1xle*#X*#sw2m6x?W`&Ii_zp(Iq_z#x=)x_%c5sIDaU-;6ugLN1&rcuXM9my zyMV=Vx3BC6=atvBGT89jIrUo2gBeP|o3m|T>MYpW6>m(Z;)cidi~^McB8*c+4VMS1konBjlMNdi6H;yq!h-Zf7&#*H5bd0GbFyFg5Z&`<9X_ z<=;37dY#EETO!7{xH}Z(*xx^+S2+&CWOXAWv$>O?Lrofu-IiH>mN*pu)eZAcf~sGK_Ml|fCboOC^^DX zOUya>a?t>9?qHCWPS@ zRh_cGj3i6--p*DIb}!t9Qfb+g?t(;g3W8$Nrq5+h+AfpHDSTJ z@_nGujUX$S2tBTDUeZ_2r6{J(>bdF#A-R_s^X{bOepxcXI!NNa%gmcsj`GT8$yt&= zW!Q-($$MOe+s3k0EIaIkI%_qN-w*j&GHw?MO*xfx+*wyOtP+|wuCZaXw}kL*u7~gd zx3syN9TGJfHT|bbb4FGu7_2&>^LP&Qwp+JjW$?u^Jidn`&WnM-d2#L8@MBX^GVOu_ Nfb<}`)!L84{s9N8KobA} literal 0 HcmV?d00001 diff --git a/assets/images/flags/irl.png b/assets/images/flags/irl.png new file mode 100644 index 0000000000000000000000000000000000000000..2126054d32f58c1d32626bfecece011150e4f8b3 GIT binary patch literal 127 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0L3?#3!&-4XSJOMr-uI>ds|NsAInC1DulHs(s z(_f%CV@Z%-FoVOh8)-m}qNj^vh{fsT1PQl>Y*7InUe=}`{~bOgh9ra}7;y0@GcY`V W(${cKziSszF@vY8pUXO@geCwr?Y5v>H%eT4Qx}dY6h<5>pg*ijdfg*+m z4jXpHxeJO|0ys78XQ~T2C|+1Txxr1rlGEm0ChwB<+m#nQVQFLt+IN343&)9{s;Ae9 zFfwH6*{O6HFc>fA=vQm#R%+lFGy2}* z?uM1$7&H3b;o~q!?uL~0w!h;qNA7DQAtM1-x@#dH9 z+EMZ4jq~0{=yM?ESR?Z7%<=8a_TS?2-Q4oQ$M)dk)j&7oQ)J~HIP}}z@7?0wAjggMR0bj@~RN_1)m@tr^;J9ottf*hMAfA354yY1Xv6?XDT%Bq`M(G1`(D z$b2B#9WL5lAnu;2+$t>Fa~;k+BI1ab)<7i3WhvzzD&3GB=zfjmh#b*ICEGkC-;^EB zP9NWBAI3^A;;I+Ws3PB&8P{kY<(eAgcZk53Bg{-I;~gsG6)V+m9nexA)JZ18Ml0SM zE!(0P;zA|e9WKpo8qY)_MjZ%~0000LbW%=J0PpXQkB0A$?~jjYS z2><{AtVu*cR9M69mQ{PgFcgM?0i$zE=@|3j&Rm(hySuw@KYyj{S=*442H9QDRpsQ( z_a!G{FzBI&9(f82$CVeE3o{OLSp~;gM1YkS!_kPy;dTQ*oVCezRuNE@eBc|J?W8nc zFz6$Fx7XK6&1ytIrG(=7);0ml>Tr1jkx0PnYR7&EElsI54x~PgE}@{{mD{Meh90_2cu_;t)~2B=--1h(5NT?ryFwHphtS zks3hsar<)i_WE%?iog^lfZ*fc{_ElNdv64RBGrJjl=a=qllT3#VFV5n2Eav5tR8!X%22l6_ z)zbm8587g<0%RYQ?dD$rIaVglu|EobV0F>EMK z`jlyHX|%y+*i`A$t-Y;lsMk7J7}V2_Hb(W};c8@6Px4VM0!-}c(|wpUuQ(b$tf@;@ oNkKN3tm4wtB})%I^vFy60K~=CZtW2sApigX07*qoM6N<$f{GH)!vFvP literal 0 HcmV?d00001 diff --git a/assets/images/flags/jor.png b/assets/images/flags/jor.png new file mode 100644 index 0000000000000000000000000000000000000000..6e5d2bbb897ac0929d76c6582445690f40258eac GIT binary patch literal 3131 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 Ef@dU`QUCw| literal 0 HcmV?d00001 diff --git a/assets/images/flags/kir.png b/assets/images/flags/kir.png new file mode 100644 index 0000000000000000000000000000000000000000..c8b69d702b5f4f1a399441f6f4bcd0be6396e8fe GIT binary patch literal 2113 zcmd^Bi96Jb0{tD+zZi}boD@0f}8zQ4dk69OI-)N$vQq(kE(!0zVY z*n-JFA8`>zat^n6ALk0|xko9C?QPk}YaWD6lJBM7xuHh=@Q3|BcANZojmjb83X zLrNwf)(EuFTk1mbh4UL<=JMy=!EVDPXkmn{=vg?42bis80Z#a@G6eq@^OA|e6;NL$Taayv#9!C8rju5`tzV?SZQ+cuJha&^gkD!d zPaFo^W5~AMGf;>GFKIvyx+A5iu|n)X7G@wGFUUiQYBl;#=#4cYz0z?xo?-_yWO>Hm)bnB)@b zcXmPS;o=l{uJc#2rs7{@tfQRmWg4nT-xt>9vJ}ZUuDRsm>Z+&^NxgCV2rVFarL7Lu zmP#ZZT1@>jJh;&6nay<6>_@;c_&n1fYn_ZOFh#J!C0?af%-`?aRrYI0XFwO;>1>UU1o+aq~F zCkhS@jF7v!}J%*%nhGp6OJH#r08zR;98Z z>w5FNK6ABi2r1D-O`+isLU-kH+N&hyL+4mq0UXILvXl)M3mJ^tPgkGi@!nvJA>dR= zVfcd#yh601a;)Ms*Dj@WJpUEgrFre3p@UNkpqD*a(rhkJL*felK5j~G$P2Q; zoc19Ygmps2Y zui=vYpG8m4I!SB)2z5y5Yh>ofr7$*38Q0F(XUaK8i9)wB@BAkFXzh815(_t+BsAoqYQ)%&?Sr+X?P6V{i;+V3klio?zyGOm?$lSI#0*s{u`kGE`D7d z$e`MpScU}zBYMs%e0?m{S1ORy)mdIgoqd~H$+56XE2diMkLz~K9)S_snauKVQzBmu zO@UpZWvX&0>Ed8V>d6Fqe2$z=I~2U1nRmStH%Ce|!{MBc>@y7uHrvhg`>r5?lQcRu zjk*J2ntB&#wgwkkTF#Jc>FZ<9sYxq=f%fJ&m#alB30H|m5lKZ0$sz-ZY@Q)be4Cii zk{Dj!FPLDfUU}3~dglpV*)7ZU#KYRiO5igqT7I4M2-%kJ+={-Thp7c@Xty-b$qe#R zTwGncB$!P7mu5-fxDu0+{+u$C-X&jD?{hz-A=MU7P~E4BbXwQl)ux;?esdx|y+p^rwFIg5hKM#E0}e+r-g+N#dKVF8p_JQa|h48AALhiFa$fO4^B~%NjK$=!C)wP z>aj@sL7*8P78NCm{oG;RK-A6+gU_&-hKgPHYjEk>$gn^&b`OWsQ>ir-i6#)zyOQK2 zKH=0*w>|pfE-X48#7`$4RwT4@j^D+oH3bJ}4-Hvct_v;gtulGp6E8F16E5-_Lg7Z4 n_@J7;hChoIV5xFS4haB-O`b-I-J>%**8uQv^>wLnCTIK?y8eos literal 0 HcmV?d00001 diff --git a/assets/images/flags/kwt.png b/assets/images/flags/kwt.png new file mode 100644 index 0000000000000000000000000000000000000000..f21563b5f414f41b7fa843432667a3992114654a GIT binary patch literal 372 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!S^|7RT-^(N_6sOWFtOBdi8IVB z=VxTLVCDM%|36UvoS@oLKDpxps%ad;>dfryjEtN46({mY2e9)qI2tjuW-zd_Fj#0a zOe$p1S79iQV(83b5a4By7G-EiW?*My$O&PXQp}L-%b+385aq@YXwOg?#~>!ipdiV> z%f&FgjKRy2frXi&FMqfE)@Y!8j3q&S!3+-1ZlnP@^E_P~Ln02pz39$&C_sQEu$l92 zLQ$XE?1KNl8wI2fi+|d&J(#^>c9+um&vw4cwqDxHILBY9;ddZQ!(S(s3AwHe63dht zu12vK*mW=%RCh2;D;8wP3l}_)s={>RjSEAD*f~aanIFt0$zpmeA<vdp(FYAUo+>Fbr84r}b7x(aBHMswq>CN{~4xQDN2R`o!_y4um`S}M1TTP%# O89ZJ6T-G@yGywompK)OT literal 0 HcmV?d00001 diff --git a/assets/images/flags/lbn.png b/assets/images/flags/lbn.png new file mode 100644 index 0000000000000000000000000000000000000000..2a9ae1dd0e14b5a158493bb2cf3a35fa918274dd GIT binary patch literal 960 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 literal 0 HcmV?d00001 diff --git a/assets/images/flags/ltu.png b/assets/images/flags/ltu.png new file mode 100644 index 0000000000000000000000000000000000000000..dec6babea8788cd571a499454b5ed876cb19487f GIT binary patch literal 130 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0L3?#3!&-4XSJOMr-uI>ds2i0{MvRwY|6gFyl z!v+**ED7=pW^j0RBMrz=@pN$vu{fQaAmP^V-=6D-!5WqwVGA2r4|_G3im|$LBo6=p literal 0 HcmV?d00001 diff --git a/assets/images/flags/lux.png b/assets/images/flags/lux.png new file mode 100644 index 0000000000000000000000000000000000000000..9cc1c65b5fda9060c2b7b60596700a01192a3a7f GIT binary patch literal 130 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0L3?#3!&-4XSJOMr-uI>ds|NsAISn%MBhB^Dk zN$Y^(j3q&S!3+-1ZlnP@DxNNmAr_~T6C~Uk{@Zi?Fj&J9;T-62V1v+us8%L1p8Uo{ a76uv9z6R0XeoKMs89ZJ6T-G@yGywqDC?ds|NsAAY+!L}3tt&f zkg+7lFPOpM*^M+HN72*8F~s6@a)N|gL$)Yagux}&2pk5pe;TP!i-9%&>p|`}dnSzkh#hs)jF6(8kln zF~s9|a)N|pgDk&tj~{egs#gDaX6S2TnQ f=yOOYe$`^g`ZJ%o=$rI6pg9blu6{1-oD!MQ&lD@l$;tE0((b{>-K?_m&C=biv-8Z+-K(+YwY$Z{ z#PZA0->|mdvbX8Ezr?+~->m z#J#=4wzmBB_UXUG>Au9nx47iEzU{!q?!U(2v$^=}?a0H#%%7srqNS&#p~%F<%ATOs zqo=sIx5&lCvaztvo}tQ~p|h^4jDUHrtg6Ju#jI4!@ zNklJ+@VKp7rLdg@sO7GIa+=Mz zLUWJ+XQY0fjymf}+GnK>F*80cdI`=NYqE&3?jR8K_*sC(W;Cz>_A>zh000000Dl7@ bgg(6i&?iv9w}`s+00000NkvXXu0mjf1E?3O literal 0 HcmV?d00001 diff --git a/assets/images/flags/mnp.png b/assets/images/flags/mnp.png new file mode 100644 index 0000000000000000000000000000000000000000..3e6c538e43fd388aefbf9c9ad5e6f170e50813ec GIT binary patch literal 2078 zcmV+(2;ujMP)bWK0CBwlZoU9=z5sE&|NsAtii`kgxd3gs0B5!VXRiQjw*YIr0A#Zxb**HU zxd3p#jD&*s_45E|vjA?retmxhYq0=guZf3;r-`>H$x{Qg6+uPlAp}`bpp)Y!`b#!)wfPs{Xh`o{Eb)2~X zWUE7Mh_uAd9dWA)WTB*`q37o3Kzpk(f3l3Hz8iJ2n2(GLY_2M4lU8qnrcE1eYi$5v zrZs1Ze|UD5t-P{T4BzDEla!KeoV##$ejaV5;MvmXbprL~8v#l@TK zszMH9P7k8F%dxz}>9sEZ>8Z?#8Qav!$*F6XU=%b^OTxrh{iY_gL{N^Rp0X0E;lU z;EvSJ-b9xP6HXho?)&BC;uV*q7W|Q$jVJ?Hxb1Bce*8Re=1{77|3nV0?(XJ}GhRpc zPE0<`FHVF3EL?dB#vi&8SEsg3JoY!a#i!az?qpKO>4^P0-zx=K2^zq{ZL_K7@qyhj z{ju@Y?fQpJvv}J>8c&@}`sURY(-tDYLdXDCmGbb1E_-(+=R^q#WSW71yQP$Gw0Uys z?xWT%zLG5X4PcQp;aKbPXiulNz5t`8p@V~mh=YTln6iLHXuo>ge)X@#JopXZDUDq^ zp}wOnR6(4DO+kWL)yhXYeWn;23oCD!+OGOfpTAhJ;4^?ts$ff8L~Hy)dthj>N&2Nr z=x7;=Ol1POO+YYqUHw*dJCtBRH-JT@MaCs^MU<2;nGeEc6^!5Z{%uVu4{ih4c$UTX-nkvd192sj z!jv2ij!BcnVKMw}d*nuAB}X=#1_-BZjY*6v6BK3<=K=!$Oc7BI4$X^F{6LPfFuSmS zd20zrk11{g`0Txw3#+M#D6)QUMFC-U7Ujwl zUeAw(@!)nrg|TshPB9l zd5Csd6H{<5WZ{kCXnei7f*Yp+%DPJ))XCk1W*AmIS%?8jZcNa0YR<8}b`u^0xJuV8 zZqN~J=ZEk@pdJu~CO#IGX!Y7{Yix1H0vq3=eYZ4Z6qO+kHU~Lh9Y~l!f{jO4Uajb9 z4ITqn_&gUE$%!g~13_2?=!0{rKGr}#@Unn?5XI45+prc-N@N$eT~V}vLr#&K1r+2Q z>XRh2RSnfRL@iiBWnBn|%#&sg8*V(gQCvS@$%1(rnwH|g>^n`yM}nD?Q(MAHUY7@$ z6a1sC^XKY-4a1$C`4n@smdk1e39<@Xc+Qjt>gVKSHn2%80It2L=Zl{z6q;UrDa1QbW$po}YKNHrNH6 zMox@eDoUcVRj;f)Rm2I^3*v6(0jnzKFTJ6dH(M2AfQY-}tY{AP!YUb^G*-ejllZkj zt?WW?4UPa;NEmQ>_|?sjwa#tgkkb<Ydd-LoMLyWw-$b*{cKXgjB?dZ(xXvrOoly zh8bY!dTE+2FBc0bEff}Eu`Fi~ZBAxpP&$~RAjZc=Y8#1#No=Z!k%+31k)g9C6AKxQ zEOs_-C-)RRcPBkQCbC*-AV&x@GBPnS;cn%zk2aD=!6<+M0MnyPZ;HOVtpET307*qo IM6N<$g2i$GIRF3v literal 0 HcmV?d00001 diff --git a/assets/images/flags/mrt.png b/assets/images/flags/mrt.png new file mode 100644 index 0000000000000000000000000000000000000000..0fd8d757ed1598b116b13ff2a14935240c2843df GIT binary patch literal 509 zcmV-rVx0L8+$zql>h($0d!JMQvg8b*k%9#0V+vEK~z}7?UiY7 zgD?z+3rWZu?yK}z*ZcneKXAYZ9Rh8pl_pin`vu#PA1RJ)CKDqgqek)t8Nho0;`l|t zdi^3`w|fHc`QrvaJb3Uc;N$y~?isrP7smE!=io1ZavQ^%qSn-E0j_+K*@e z8)~>~6lhm)z=i<~v?3-zK9rvWv}aU6cI@XLGoYZgatL_6GAIuSiL>CB-NQHC*oK!u zpE}O<3^>6f5I9vtkoUpXu7@a`sil-lH4AIk-qII};^>L1F&n8WSy!GKx6&-p%@@|_ zo#?L~=_~)KrMj4l2#CadVg8K<@DBhZBcoDZ3e`to#GLn%00000NkvXXu0mjfyDigb literal 0 HcmV?d00001 diff --git a/assets/images/flags/msr.png b/assets/images/flags/msr.png new file mode 100644 index 0000000000000000000000000000000000000000..2d2af3aef0785bc9e2d99bb772716267a512bf4f GIT binary patch literal 1502 zcmV<41tI#0P)goeEnguqR0HVDBp1aQv4*)Zn|NsBY001XasU1qCRj9oOHKb6&1>-xCuI)racGA)aleGCxo5OevrdfslEm_l>s!F-B(utqP_sA#0E5%0yLRa zsJ#j~mnTR63fr00Dwx5Dgd0jq(Kk?D4Lo}TuA_+yIh%50HD7Bbcl40kxg+@ zkGik`d=UVo$UItF7=(;fPfQS~#0EMHX;L0yeGLFM2p^uKK{+L#iafQflTj4_O=t*Z ziCQ{ND;|p>QQ&1|++&23SW#J$MrpVI9Eg7LOT->e@n#!ZBb2q+-Q%n9u^yg_XtV z`jZzgpTE7i*NBBx7sCKK6%IjufoPBcq9TkoT54Pv286$U_W0rb_m6IeV;TUqUQtPz z8)N`GmnMglDw-ptSy@8DH-36|@8g|t77PO**2}W9LkwVN!gK`C02Y>OpFh01dwG{3 zngKBD+2IDTV>&_?WI)J`r?;+bTL&~i8p!~-^~eT*9btgv2(SxSHeS87dBZB80rE%& zz^z9y0Okmo0j6LBj_o_Qd$lDCssV88(F}k%0>ywm2acXPxWyXH1?)rtkO7BIpFMHh zHgx@Tatz3@v|7C5;*tFq!h+ZKT3Ke2Y(PO%&%EV351%<17PMk=S9KoA26*`Tb8hj0CKX<%~H=me*gdg07*qoM6N<$ Eg5{WNbN~PV literal 0 HcmV?d00001 diff --git a/assets/images/flags/mtq.png b/assets/images/flags/mtq.png new file mode 100644 index 0000000000000000000000000000000000000000..1897c94e7fba5ea55209a8eef53a28a736083a42 GIT binary patch literal 1512 zcmVP)C3&_i3I`~3YAFpFt>wPty<=<4$wIglnn zlp#HmQfR6-P@TQS-u(Uj^7Q!X?DV9s((?28)7k0${r=wK@1Cm9Hcy;yfw7%8>RWHFB|wyimc(6guP8&6`T6{VlfpGlo8jf~w7uGmo5yyCybmmiJXD}y zbFh`7%WHkMEJl}^rOiZJqs`Oi#LMDJVWuufnC9v7ikQV8I+2&8%z}}@+1~6%U8Hb> zx$Exr7c-28mBXT~(ck0mrm@p!dbIHJ_l1+{^;?fw1z!N}l1SfS6==k@pcfsw(Do5r}o+|}CZkDbU&Vy9trvC!7&yTjer z-0R2AWuy%y9$j;=5mc+is-y=VhevZGLsm|i&@sXa%bcVap zI(#+&000$qQchCVv7lH{{*{yDC?sY! z7u<1pZ+4UY_Bm%}GV{g6WFs3{Co$VX&7K{bNp|m~SC|pNlKC6Qv>>L|o>_@4G_n2n zh1}1PslC0b!g4k6^f45v^Xx4;5Yhv-dLM2Y1|06Qp-NHa+~F$~lp zw47lo6?EV#Vk;RY(3Ju>mGBuRcpgWofKM{1m~e%NG6(8-qm*kBN!*cim=0WvLzodj zien5ND2J zUp#R#1gMa~V({Bo51J{!3}#qNwr9(5LI9hy8ec8oORumd zfDP6V8vdS4h&w^CLW#&XHloUXS zL@3fv2e;ye4&b0^+5E4-l=`$SLcc!Xujy58(ZHL0ax)C64j`^LQW9CyOMdA?Q%OtF z3Y^jh#207%>hF2uHR966?>gtS)4ei;OF{+MzU-{UC1KyW#`v11|Aye;egz0E<%K9K zq%lLSWnubrzz<)&z=l@gGA}~ELJgR1rDprKO(eVa(ko0i{-|$cz2py>q(XXyhR6*7 O0000=yb95e(8n++VB4INkq5J?6O zRtFFu0tRad6kZ4s7y<>83>%3H8K4avRR<3t0|ucD9i9yxatRet2M;j>2_6CkbO{v` z0RvnI5l{vXGXx1D0|qPv2nqoLq7EK`3m9$*6nhF6;}a^03>l9M8Y=?`mkk_M2M}Ng z63`JQOa=}w1PCbv2vP?R+Y%_;5-GnBBU=a&|2oX623&#*7zz`%H0tI9U5{e8NgbNwX z5hf-B2a60EO9l>g3KiE9C{G3tnG76P2M{?03O)r3iVGTh3KlvA3yTXHnhYF#3KvNR z4Lt=50{{VO2^5wL8-EKIfD0IC2osJA8j%bedQ95&00001bW%=J06^y0W&i*I6iGxu zR9M69)5TW9P!xsXMs9A1*WDd2?(XjHP~6@1#bMCJ>WrI);rSon8|`f}K@gM+8|W`U zOQ~u~D-`fOeY|n$H23~;WiJ3&h|2e2|JSGDc?|``fo8pcy0&tD_mD_NV#&nK^(ty_ z)CUYINIAKEq)nWoDFvgdVry$khr@=W4G`a4bpgi(j^DDCUU^Of%Z4OIJ-XP zWLf9V)C_iv%c!-M7>H1NE-yab9%U>%Z?TO`FJnhvCsjuZRC7H`L40;P$LC_$UM#fv zJ!OPIh7yN;hX)M*o2|dwU2m=^B?GMH@|p$-O|W@&v`AHzCB?MfDU*GHHxK{-0FVb1 k83+IX0QgS;f}o$i0h%dmrr`Jm$^ZZW07*qoM6N<$f-M~1c>n+a literal 0 HcmV?d00001 diff --git a/assets/images/flags/myt.png b/assets/images/flags/myt.png new file mode 100644 index 0000000000000000000000000000000000000000..c149a2a7915e8f29470c49d149045f3ec42d7d58 GIT binary patch literal 2003 zcmV;^2Q2uBP)Uev5w^Cr%F4>z+}!>5_WJtz{QUgv?Cih4zuVi}{{8*9xVZQC_qw{e z&d$!7nwrwm(yFSe>!6eo5pvDV&7z{BrlzK!pP#w8xvZ?L+1c6C)6>Vt$FHxiySuxt zuCBtu!knC(!^6Y&_VyMQkroz|z`($yq@;_Bi=m;R%gf7=k&)u!;_B+^%*@Q!KPBE^ zOV~pyzP`S-wY8(8qp7K>yu7>;5puDyv8AP^`sd@9mzTuE#Gam>*+?+f*4Fyy<_$jHd%=H~nR`;Co_?4+3b z>*({y!T$RC@VBk+wXF8n(%oA`?Ca~?RzTlnP}J1a+fh3CG#>x_SMhT zHXGt~W5X^H@Vc?@v8VXm+4j`W<%4pkH4fQFF1Ich;cZ&RE)}~w5AU?9;c#8p#>2&t zh>wr6lasY1ByU?=msM4iCMJ7@g`*xGe06oDLqm%oAbMnEm&wV`dwZBPG=SOJ+Z-Hu zp`oy=tE}#=qvwo${rLDtN09Qrx3X47M@NUXudb+}pF%>9r(_QN^YYCs5yis7h=_=@ zP!F?Z4^dH!vS3r~sh!q5BDQW~vrawjsGYtu8@)0RmUb50(a)}d52$nwm23^fGZ4K* z5W+1Gu!j$}IV7We7n+e)x-AvhK`6$Sjox8P+fzK(KPK|SyZ`?FtYQzlgb<%<4%}Ho z-e5|(P7tqJ52#ZP!!{7Qc5B_%)sSx!vpp%gM-aJH5S)@wzk(3Rnv(e7+_W?v;n&v3 zqMXR2o{|`S{@e%AH36I9LhB7XgraA;{_4G=vc}b?vPv|DdaTFW5RBZ*Ir& z&o3DS6g@OKHelU4EKq{5?p9bqd7Ro4SKvpRDhs3C;p&%n9ivdVjyVRGGg(FWebK2}#Q>I>a>C`E+ zW>1?bKt*^~if`%iglGVcG*i%(Y7LKI1~EOEs2WRD5y{hu87Zj9TIhn5ndJBZ3UNN= zfHdu`2B2;ZK@ek!fozTtfNC#ILkE>FR`3A=>1mus1%*pAa`XvTpmtYHLfPdgU5P$H zu2~|4Dh-#G9^e5e*;J?z=d9&&!0A zU!k=zE)LMkP`m4HS$xBdi;=Ue4sb5Urd9YUm^n|*7X!8iLdxg#uAvn`dsn#Qo7BqN zs*v-55h&<2UZ;q4W` zi!4x=&jTRc5?JHV7m}1mKQ3<_Y8L^7v$Ffg={*!#sCU#riJRmB$k-&U#Ga`4d#I!m z!82Igy2l?zqj+%TO6(q6NfPDp>l5t~Z4xWvn+4pvFf-MuO%xe9ntluW`m?v+eB*_e zUwq{;Y{P1rGl~+m&eY6>UOd`vl=a!c{8F<{xZ+*xo9}=ADz5C4cV2xB+jyT)XD*!& z?0vGKoCg!lf(+ZY*pK^u`Dx>hFF)P)I`-s683OafA)E(vttq_o#ZE!?0TjB!;UFGH zQQWaw;EYt5YUyZzRaSMiIsBd`Tkr$6;SlzFS=<5a(0c6oN3u0xfH|PHQc(bm5iF`l zHr^EUeD~`g$BuXW^=IGlV}0+xL_$+Ms;FNCj7$`ONO#Iw3vfJSBW=1+D71>=I7tx4 z3#_N=bfPE#(h@Net9|f{3jXcct&)hfkWm0~l|=z?t~d|vrpRpL4Uw&jqX%_fF^2<% zMJ10m3jrV7n6`ik1O!6!a+Ti1y4l3cbm60E@w7U?EJ$l)eSuiVdN3$g*R6nDEmcx# zV%NIOjcy28j$({*c`zs&(vM}XExTmy`6)*3f)0hEqn^uVs89--ku~%`lki^_T~K_w l+>!n}VAvPU5sm1K&`I417b_B9>`DLt002ovPDHLkV1neT8R-B3 literal 0 HcmV?d00001 diff --git a/assets/images/flags/ner.png b/assets/images/flags/ner.png new file mode 100644 index 0000000000000000000000000000000000000000..87bb8211f7ac853a62d28d6b0cf4f93cf28eb1f1 GIT binary patch literal 494 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!I14-?iy0XBj({-ZRBb+Kpy0*; zpAc8~0-yh2aKxRJcZ2qWAT}W4Y*B!4feZ-y_uoH%|NVWEqy6vi-+%x9J;~8g+GeY~ z-43V~s2-?MahuJPY^{Gl>2IH(<{LZ+<$79V^7rSr2O*pvHuU}d`tk4A4^J{R-Y>0t zoGkz6)0+>g+y4VqfBW<}Md5L}%DV;Szn)ZL74FnnzFID%?Wof^6sk;6@`{@QD<xp;u(ENVJ;r39^r>c<1P|*1mE9UY_h{6*H|B($W3+knrSQGK#DpXJ zTU&Wp-uyfjE|9SI=>Z|8A2X-Fo5heYnNf$sp-g6jqj!(RgT{sjf9@}2w)SW@*!#bP zWuD8Sb>i1I*{Yupe5bun^7Ar@FDwa0*K@QO*3L`|Wf5?2;3|=twe{n72JMjeFITor Ua`K*e8|WJbPgg&ebxsLQ0Iw|U=Kufz literal 0 HcmV?d00001 diff --git a/assets/images/flags/nfk.png b/assets/images/flags/nfk.png new file mode 100644 index 0000000000000000000000000000000000000000..6e68aee4f51473519b3cd5c1a72425ad9eb9f7d6 GIT binary patch literal 1422 zcmV;91#$X`P)LI8R_|NsB>`u7TdKmd3*|Ns3BfI$FyI}?OOK$>0#eLnd6 z_yKx6-RezV=kn;w)Y$}>(l7hv)8>+q;Tx@?Bej^2!23d zt8|gWpvU3MtkbvX^XmY1IY*vk4uC!2@8bOZ`^DhN@%Zvys&pELM1Z`JCX7x>pJXYI zQCO#NVyk&Ilv!)Ac@=|00C+Pum0c2mKOKlm41Yi$iAR0Aj0k)(IUtBgJC{== zi%4y;fq}e<8-_qZnqXb2Yb%XTERau7qG|d3`CzDdZL)>{a4h5S;{5&jkXEzTya01E zYOsHe!kpFV+Eb%!c(sWsjYwv%f^oBiW375^vVw@fn8Dt~Sfp^U)wJB~-dU)21$#M` z$f1hCo}d4qhxxygBgWL zK9y2xu6t0TVqvLwE00iQt#>DiKLB$sM4VbBi%LP7S0IQ&Xb8?z00001bW%=J06^y0 zW&i*JJV``BR9M4fU>F6XU=%b7wybxdv!tMgGYwcze%* zTmwWCzx>O|NN?6B(*X4kzyAxQt4Mw{CewfiU%waTsebp7VQJc-SZQ~R6yGxg~S zvJ5C+{4BpvA?=4+*4hKJ^~f+l=d7Gc_9rR+gd};zN#wX-{?X{X>{st!^D|kwO=u_G z0PUq-+z}5Y0%c0NrH8XyvKSNhg**F$XL^6PhaDooQN)qsj(Df)s zcxJ{urvA*Tp=);i0~)Hmk#Zx!AUEo=c(uM#_%K!SbiAW&M_gtx4P9 zwPKMFX$D-ncHUjdU+xGS6BDzKQ=-Sh#iSWve?vJaC&DB^kexSBm^H=IaKQ` zcU6&>H7d$Yf}hQriIrb2ZtCP-OX3aS*?0X4TRhvD-MgoB^0o;x2}DXtI7(`h-~y3@ zH!muMgu7U=x3F_FciT*K?QpC$A|(vEb_w_e@QQOa3k%oO8|sR*Hkla|kzzpKl9lr; zY(;eK1?zYys@g;atIH`eK*OX;i!uYaxHKp-prW!=lOh9jN{aO8hj03A)(@>(8DN&o-=07*qoM6N<$f^I;i3;+NC literal 0 HcmV?d00001 diff --git a/assets/images/flags/niu.png b/assets/images/flags/niu.png new file mode 100644 index 0000000000000000000000000000000000000000..acb36780d835e0a31b796d3ee1abde693b0b20d4 GIT binary patch literal 1095 zcmV-N1i1T&P)B;66F76yBRJ96YAkf{r&XF#?>@P z*+^RL@bnHeaLv`>SaO{2w7heLsr=Cv04QkB6f5wxyxB)t|Ns97EoINv;#YB-`|R%i z`}@=|L=iS}8$WmqGH(qtaPq^+2{3I9GjD>Hwea%x+D>2Cke>ej{@aY2c!{goke>0p z!_zB3+fQKg#LDOE@!VEs^vlqCkFd}gDtU>k?C_>6oGsHEU&kq1?+)X6kVOHN@R@~w0_S4teL?zWPC*ECFhnl$EPbA@ABHmOZf|0T1fsN1_D%d+F z;A2+TI40j&Bj4ogDN2AHKzaMk7W(Py>FV&}-Q(CtS=~}3I6QB_6)y!VW>qUrs+ure zUP)anODsBY0x4%qhW7tVQ!!b0!Dgj z&1$-aV78g9mA<8krV3ub0W?6_&j)UVkzU)17O(+O?VfIg4PZ45gBxL_w`B7gpaF30 z#;QaZpsETtqDwDw({kNTgmzUT3{cbof|8?Jq zk~#)_f4NFeEY;L=$O1X=pKsnly8P z%Dl;QLCmVqb~WM*kktT!^6)Ycr7>~g?5Q*QL9CkaRH#&Y=DRg1ps6K$EX3L28RBB003-GJDtd6z_b7W N002ovPDHLkV1h723KReU literal 0 HcmV?d00001 diff --git a/assets/images/flags/omn.png b/assets/images/flags/omn.png new file mode 100644 index 0000000000000000000000000000000000000000..6b6cd8b3b898d8791457cf9979f55756f568e8aa GIT binary patch literal 711 zcmV;&0yzDNP)vwtGBq-h`D(GNi*$WWpW@z3hEazHW^sB7g9U|*^dh(v2+Y=b^mzeH~i}kRv z;4d@ZDlYcCz1a&9-6JRCMM&vwaNQpy@}Q#PJ3ibVBiRZM-6AI4Bq``&WA?tj;W<6p z4HNUFrtzJh*$NQ;>FD4yH`oUa_rbx~0|@3+SM#T+*$ooe2oCR&lJ1F%`_$9yczW6m z65AOb?~sw-C@lHS%;;rk?R|dXIzIf~-Tvq1``Fm)dVJn0FWC(f=1^1G4ioX4o$!{J z?SO*&+uHin)9{j%;4CobSX=wZ$Kf?N)&Kzh<>d3KtK1kJ`o6vSzPThxQy1D$y$okLE<3vaDoSo}-dG@li^|G_`q^0`3 zyz6*-I-G?z00001bW%=J06^y0W&i*I0ZBwbR9M4fU>F6XU=%Nrpia27aRkV^0EpN!aW4Y^8!~e zOIl88MM;#X8y7hSu!zNrWfm4^r=}-+8t{-|fWEVTWOZeJM!86Ur4BP02C$mA`o!j@ zRKNdw{LaC{%*aNL0px;F14a!P tds|NsBLB_#FOEh-l% z$XF8O7tG-B>_!@pqu}Y{7-DfcIYGj$AzM^HhnKZULm>O$iUTVSB#213F)-)|`$*)Z Sd(Q)^W$<+Mb6Mw<&;$T7r69Bb literal 0 HcmV?d00001 diff --git a/assets/images/flags/plw.png b/assets/images/flags/plw.png new file mode 100644 index 0000000000000000000000000000000000000000..6f6ff993a1377710bb411730b1bf47ece87c3571 GIT binary patch literal 608 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 DVNT%K literal 0 HcmV?d00001 diff --git a/assets/images/flags/slb.png b/assets/images/flags/slb.png new file mode 100644 index 0000000000000000000000000000000000000000..d63061a0bdbae2d2f63096e88d2884ac3022baef GIT binary patch literal 403 zcmV;E0c`$>P)o!F8e9JbKE0r`%ME z(JgSsgsa~0_51Vp{C?QMe*gdg0d!JMQvg8b*k%9#0Q^ZrK~z}7?UTt4!ypVqyUL%d z&Pabdo;+ZD8}k~MEuFFcZteO7y)nJt(;4b50d(`+`8Vhc^%U^@Jxk~e^%NlNKh`q< z-2Yae01Evl`t%D>|BOBbfc`ms3dr`)>mvZXg^WG|fLqAw2>{(fUT-D0fb^1Efco++ z6!grmEG=IIDC;pmphp0a9sp$eEI_Ky0mOP9P@!i5HF^e6r%wTu`YoVZzX8r4-UHNn0?_F_prxhdn-4c(NzPq;_HO_H002ovPDHLkV1h~uug(Af literal 0 HcmV?d00001 diff --git a/assets/images/flags/slv.png b/assets/images/flags/slv.png new file mode 100644 index 0000000000000000000000000000000000000000..e597e45b35c16ab3347d0ba96a8c69c1023aa335 GIT binary patch literal 642 zcmV-|0)737P)Puy|M}x;e5?QY;{Wo%`|rB{`s)Ajy#Mvj|M=Yh?yUCfxc~g| z|MSNF`R0$Qhwtjcw#2Rf@U_6ty8r$8|M}tZ>Aknfwg2&mT$mcE>(!ghk0*v_w(9@1_A000VfQchC<@9+QbXlQS5?|*P*1ONa4 z)Ja4^R9M4fU>F6XU=$1%z{WuVU;`S!$xH#@95rCnfI(vbBR4+4!-H3v5oiDl7f!&d z?c!yn&yQOH7Ykkk1T>tvqWpOUCCN2F*VQ{EGuGE!Pl8MX9Nf~PQlnFp$-;4~h@8G*%QL zGZ6WtwQSvO4CF=m$uxjVSW4Z*NLgGEe>z~omyqEE4!|^89*i0=d<S&V0B1l$f=6x+FaQ7m07*qoM6N<$g4exNn*aa+ literal 0 HcmV?d00001 diff --git a/assets/images/flags/svk.png b/assets/images/flags/svk.png new file mode 100644 index 0000000000000000000000000000000000000000..06bed756e409718851e95508f471888f930c72bd GIT binary patch literal 521 zcmV+k0`~ohP)nzjTZO05?HnWuPNDXWlI|QO|Ns9BPNMEAGWe>k?J3Id8PlA~gNuw@7h4NKg z?$_RYHDg0Ug0jcrHARY}yxjV@yYATD?8?wMZ?oRa*i&1T@kdVIA1rM+YUhHCDP*l* zJ8~vSkt$@aScbp%oS*Y!Xzw^bVs7Wy00001bW%=J06^y0W&i*I14%?dR9M69(%Ejp zFc3gd?Ao5#VF_iYrDbiQ3v_|5|Nmcg9Ml99AY)$VDpKafCnGDR5=Bwlf-@q3fLws7 z-Ozv%F2!1Yz_|9oqq@jyms^_s;1*zINc zoa}#DhLhz1P=2V?WTQI<@B{!R)7v7SKMdF{0L}w|00OW8MNzgt4R%dpI>-2;00000 LNkvXXu0mjf!>IX0 literal 0 HcmV?d00001 diff --git a/assets/images/flags/svn.png b/assets/images/flags/svn.png new file mode 100644 index 0000000000000000000000000000000000000000..a791163bdb7a8b51b179b37b375547fd808b8496 GIT binary patch literal 1963 zcmV;c2UPfpP)+Dlv;GEz~G4 z)F&;~D>Bt8G4}TM)F>|0FgDXCEz~6}(=0O6DKOI{E72k;)GjsDJ3-SlI=>zy&mky& zB`6(boXjF9HhQirZlKd5DbzMR&K@Pl93#FPAtr31)GRYCaH7&BEYw9x(JnRFb$P=d zB^+RsB5I%@XrC)@qb6&h#UCcfASNDTmnCVO)IvzqIX}}dH#K;yPmR7(kG}#oUw0A~ z&L%A)Xr0F-D#8Pe#WXr9Y@Na>FU`%(!W<$; zhqpk1v$!iUDQ=_CB`eY>FVHG5J%F;$Ei*ZNu*@qlAZ41(ASXkFwbU~?)l^xEDJ|G@ zcho;bz9J|*ez7EGm-Y4ay)rn`H$A%@BLXs4Xb=-pjJX3nX<3uLOoq4d@$pNDxuV3} z9Ala|e6QDHXM`s!)>~l7Hayp6YRfG%#w|0#A}AYOkjTi$RgJ;HGdRo3%TtWRx-vI! z4->pMJkdHozrVlHGB`VYuXPU-F><3}mcau=b^}6l13F}V5fw~}yq3Asf)WY{Br44}J=0B6*J^LfIzYZAEGTH1(??8Xl(%~#B{X!V#l^+K!oqkRATw*1 zEM}7H@APaB6Gnx#M1QQiySs6Wu|asJNP)5gKyL#-ZPDNAU~-#1dZ~PFkkURwn1`T; zeVx3(&vvBBz0~Gjm&49MMy8&(c&5Cjl&guWon@QHn7O}eoUxR(wS1+dS(3DXrl7b! zLMda9w4AUvZ<)WLv9`9hI&qt7nx@0U!$NYSLw>JsZIU^7r^~3c14emYXp(eun2&;= zTX&*sd7*=uxtpH1j;F-R-RZK^<#u+PugBV;zShCh;^*@9qA%9v0000AbW%=J0RR90 z1SJ0uFbp-3Onv|W1W-vtK~z}7?bcUZQ&$|oaT^c{xk+lZ)eZ+O6%rB%C4^uggfu0P zm@q;#1PCL55KY1gQwUIzy@w#JD1urj%Z1xo>bBO=z4zXG+jH*;^3Z;~*ZAT?z7NU$ zeD44J&bi6UoioSI&JJ>PxCM?N;Nn0fJ2~BP)0~{hR0kIja9%?u*{~iINaQunXkar! zc6G9WEBTfK)Xi966OBr8wE>BWSOXS>OtJwPp=`neM`i=G^9^sk_U1byFP$J1Kw#jg zg+e7mulDr3`*zA(bXQ5G`PloGjYp&MT51*poqgg0hvB0Nw6BZiuZSC;T$au|A z{V5tg4-0Y<3+$+#&dOSZ`SHxy_&9BJH1_93__C&JzQY1LtMvs11&dKXT%GVh{qg5w zdgV#0F5a_#a0e0qH~44gYNP*#?my$ty3a>Llz-y;pbU08-E zJG*S4dM6gx_`H(W)RddM6u9uq@BjRDzTx7)Qari2O}zS0LKNDwWO#j+akG%4F-L>+U8D42Q@fzuAQao;Y4!9vK-Y3keCik03BA zlSP&vwg#S+DDv~;;=;qj?M3;24@1@5``k48xaxl06}1so3D@zu7ZINySpV4 zMI)EXT}TjjA-AznA_;|o)mt(;J3Dg3Y&!j3qUdySPDkg9&tQS=TQV{dw*1fAkV}UK1h&|$u#A~XY|9sQ%~+sO-+nfn@lXX<f6h7$!BEE^FYqwSInKE9C(kA0N?*72e)p$kWs9zdSv`9d8lfBtU?{0NXQ#LN*)xu^4a#el~Cl2D%t5HwVb&E5l|28`f3{1ew7JNkM2q zBr+OVOeQ-f=1~-%&&R%F*lZ?~Wi%Q%9FEzXln@-8nJKs)5TJqJq@W-W0Q<9qbb2A5 z9~~X-3xO+sVIe(+4FX_$a{^c&oGB29x6hmaJVKZ}z|1iiz;dy;u(Hy(v@|Lz>M;mh zmHPTtR&EoEO=|VzBzpcZKfkJ(6MRrn(VLdmwtIJf>47~(MXjyP?d^M$lS@iU9*0Vj zlau$hH#fHy743Ot-@dlCw6wI|-inF?FmUXUzrTMQ`t6&Gu@+Lo4-loWr= xp<`&kW$v9en~T2YIl4I8%*E0EqHcGa_!q@Z;1>BkgQ5Tc002ovPDHLkV1j#on!*48 literal 0 HcmV?d00001 diff --git a/assets/images/flags/tkm.png b/assets/images/flags/tkm.png new file mode 100644 index 0000000000000000000000000000000000000000..0c3ff87556a0be9719ba1a55b8f806c673345076 GIT binary patch literal 1620 zcmV-a2CMmrP)EOS?lS zqZ~DyNiN4%9N5i+$NGPM;m>1PbZS8k~BIoGB#d9?|=$w zK0dQGD~V??YDqk^F(Pw9H_j?3WlbrlEj{>s33E##$3`J?9w>=BHriDgb2crfCpWqt zFw8S0lQ%5HE+l>#CygOBa6(JBB`cm`GR;UJe`Z5JQ%_NMP~1x!WLZ?sa)9E9df9m@ z+o_}3z^dx5a{uSZ?7(^dtu%A3op!IFiMpVmeZ-RtPl<=~{sz~b)fkV`SG zGck=QJx*3WdOI zLfeg#SXxkdPcnxvH_v)5?0`yxD>95eKCF9Th)o@5DkyzTBTzXnP&h2JCq2|jRmVqN z&r&?xmY=^&WPUP6a&kR*T{gsH8jyK8t5+&kOisE-DYa%Lr#~i;V=S9EE2bDWWqwe; zKqE+OUyE*1qjE>HJyqFqYSmy7#Bnm-m^HLcN`+r!%~D9&Um3@FZozduZ(~mFu(5zm zRkVyZL2y{PC@70WOX#1a+-WJmK2*haVU%EN!fjW=VMx44LBfuNy@i6iU@_E;INNv& z-?FZvTTDF6XU=%VhI5bkO&JaNd~ZsbiI$97qEzlu>dJ97Ghl> z(tiI*$D?Ou*VY#bfeheQn8L%wYG{Jj1Y{S8*4};b_(50C!O&c=7g*#i#7vDDqbG3_ z8;F{XPsQ(a9@?v;%my@oi_65^NWp?DCf1yXI4^j3=>!Wa3JPk-D%-NNSewN}Pv+ra zWoIgcfz4objSU}ZHn111D+3#(bgd4a>J^~9Nrx+Ujy*NWJH zQz91+3oD~hoRtZYf#~j|d!(hksZDKNniM!1#Vicvqh?wuFcKM!eEETgTSA%|gk9~} zzy=sana0kJGiM|=C8~;R1p5a02I|Q$LGl7SkF~rQ4-e6qNmWyhU!9kiA83FSRGNjA zm4xifSFK%VuxEFvwid(-Bxh%L|DF0y+qO#Tub3-EjsfgG6+0yNZ9h=It74uFIRy%b zk6*E*Uikj{0zYsUH`C1_t`2+KvJoWR;1UyxM+}lB@lJ*_o^|(ZfrY9|XYJ znH(>OiU$Y_@(K!9$tsf-jqH4`-qLx?^Q5JH6NJc02ht1Nay$z>b37Nh2#^(x4(Zm} zOP4LlUT7OnRyyExPn)0VR_vCUUY1N=H1g%EyLfN(cHwm;FAzc5Sy(VISU3ruok=x7 zQ%(_BJ1c6*Doc@7JM&c+zW+Vp$Bi&8GE-vB)!zP|@a~B>C6dX?&Kx4|`hI==_VZug zEnXpVT#(9l{nNz1-@o+tzFlKamH{3`5g$KK`17GMM2##1n5Bzk6eA-e6}475l4Ssg zhds|Ns9#$iPsxcz!5Q zkg+7lFPOpM*^M+H$H3FYF~s6@a)N|ggQ*BpnBxJaHB5o4SLjb*U9$XAOoN)Ax=2#Y j>VN|)7(`jx1Q{59v1QBn&Na~j>SgeB^>bP0l+XkKsbnVU literal 0 HcmV?d00001 diff --git a/assets/images/flags/tuv.png b/assets/images/flags/tuv.png new file mode 100644 index 0000000000000000000000000000000000000000..15478f1914ef86b45bace3e01667bafe221e5617 GIT binary patch literal 1456 zcmV;h1yA~kP)T#*z4nd&Kb6%~Z8#eJ;McBC`7Dgq_ZQki#}!uXCy2d#uxO zso;65)qJecajD{+wxNEk&v&cXYp3drvAT+|ywk-~bE(~Rs@iO)>2IjykFvITtJZ3# z>vyWy?#(K2spHJUTx_T4=F2ydvazwdg_5(eYp3bi$4HvBrvK9php)o_(+khUS7@g1 zX{YSnS6AIsRE4j^fvw7ovACGDsF1R>|JDGTwWRXTBD}wHgRaNW#a3>p<+r_R|J4J^ z!eF<(d$zrO!og~YufX}y7I>@IskxA-xRP_K-SEyOptqZ(xR&C|JDjzn_s|=+y?m*; zk7uUv@y;fdv#q1Im*B}hqqmsv_4{{|$k|0j(HI!fVQ1i|uBNz@fv(Ez^ZGq!w1Te4 zv%G)*(hH>H66wq^!@+3n%`CdUcI3=5_Rk;M z$U(QgdgseDqPLlIs^9$47j3BL+Q&%x&>C;44ZFkG+u(HEVzqL{R){?ZYkx196P8i=sKth$T#&>h&vNyH!~!W0_M4i0qa4c`C& z00DGTPE!Ct=GbNc00SgRL_t(o!|m1ATiQ?<25@6MIe}CKTntBDfZ!GZaqm5G@BO&< zXsxSi_p-Zo?`3QEu;YJg24E1Op_hGlZ+Ol{e&2hNli=p&iYu;|(!}=<@Wf3y?C!zy zGB1LCeNIA$p@EP{886n`76=au_2Zx60C>m5?UmRA$nk-^h_f63+%sXXgc+d62gOGp z;sW3}S)!RKVFH-(i8yW_K-s`v2^uhu#~lDztAq_$N>IR>dAtLl|H+^s4PygTi47T# zM*{2S@pb`BQ*rCr=6!h*4e+c&0_+?=xPY)@Yhdgl05tI1!$E!>3;^Qky$50evjs=j zd;GMyR)P|QFsnDEzcSARSZ~3@-ToE;gb0Ft|CT->MMQwWteyfM9UsK^Tc-ipMo5C^ z0XMO5Cs~l%xUjqXC2ik9B9j=w3-s|_qksT^esf%mw+*mUGif-gsQsZQf@EK%xkB{3$0%!%Xl_GmuQNMdCz`*a$1Meq5uSdT1gQzU>R=( zm`Yr>J_M-<^NXNF*LV_O0L%D&0A1q5zL&ssAo9iHGL=HZ9A)Ti0Kl|8MHjkD&Y@fZ zAc6#HULZ;d6c$%^PU{HB5kTebIeBdYW6QJ6jE7{J_ zC~{I;G0qLrFLhq;!ssU;T`H@qdp9aTvoREM1g3PuC66+J9D?d$62jfZKk zu(70|li}do>gwvcx2^g4`QhT^>FVsUuA{rPpZ50n`1$*_v!=eeqW1Rns-uUkshW+3 zX_|*Ok%(jH=ik@Qs-BB7@9*%Nn2Ooez@v{g;M~aK-_)3gJF~2g>+9;?*uUlD*p`xh z=H%Yh&$yJ@k z+`7cMhTYe{y{uxup%$KuklE6)%BK;hk~PSv6Q+$GqlXO1rxWSI6u6lY>%$er zx{k)VgXH1Q%D;==*S)!!59Q#@!J{3gjULCR6QhO(Nd%?1Bx|Lh?e{wOPqA zz=EeFJEovCKCvN^M~8F+fd0;it4L~0NvqC@4Rh9zCe;8<*QU&3zJ#ju@*=+IdUrE9 zl3n0b)12EW(9_>5Ak>oTMS3i#+eZj?uMiTRBfNLUge*@ZB@zu_V|J_MUnU|UI!jby z$25L#6J|D&(?OTuqP6S9c8MK6v}ax)ISFaOhV>%i+qP^HKX_p0R2NlJ1JTg8ec|er zYnE@`zi;=}`2m))q#D3v%-*_m)siVQj~v}T-H)uKtk2!Cc=p_hlQvEc7GtFXwhuuDk}Xpu&q0UQ*7Q3FsD(%`7t nMibI#LK?~jjDk@x3V;9rg1U^iIWS=v00000NkvXXu0mjfiLp5y literal 0 HcmV?d00001 diff --git a/assets/images/flags/vat.png b/assets/images/flags/vat.png new file mode 100644 index 0000000000000000000000000000000000000000..d6c99cc1fa8f9c2e59007f7eea3372c248f0fb70 GIT binary patch literal 851 zcmV-Z1FZasP)eBe^-_gE@_~o(q z`T6ta!`P-#^y99?U|_(&!0oy*w0mvnwzSUB(DK^a&8kc8#ZlP2YvRq3%yV<-)3wgF zXt=el-=~-M=eF_Hf#lYJy6w0lxw4(*%#GN+ZvOuM-MypTo}Tpd_3zGY?#5Ql zcqHVqx8KW~*S&GnwtM5sfVjH5(yK${t*yVq#=EGd@bdKKv9$B>FVpp$jRThlK%et;;Kmd^yL5i{_V|f*T0SY{r|_7Ec5W_>&%$@@zcDxtKg%zwG!aUJ?d(G0(-kg-F6XU=&~g4lIBj3m7$E)PSLEfQ$rX25=hcaZ+M{k(-Z$fg-sE2(y{7 zL@_ZrvB(K>lVyOaF>6>%L2R%qtAY?&2AH}>#^o?GCj~j{OOj)NidA?*c2!$NZeobF z5=_f5P46o5*CxvV&-D7vnUg2=mn7TClVyN~7kgv(lD^_J zc1;no43IMS&aCSxO|j7s9FVF(zeEEfO(002ovPDHLkV1io13+?~_ literal 0 HcmV?d00001 diff --git a/assets/images/flags/vir.png b/assets/images/flags/vir.png new file mode 100644 index 0000000000000000000000000000000000000000..a57f924b23e263813f2978b22e903aaaca667425 GIT binary patch literal 1987 zcmV;!2R!(RP)Px&08mU+MF0Q*?d|RD?d^??jl#mh?Ck9A?d^qyg_M+( z?d|RD?d|RD?d|RD!^6Yt>+9|9?d|RD>FMd!)z!_-&F=2*^u|3GCTIYG$L{X!?(XiV z05jd4u=B<}^Ts~y?d{KXkdVXP^UG7bKV|Oi>9;Ui@8iGp%~|uwOYh^n^3P|v*5&Wg z+wj_$^UPQ7?C0+7>;!|y^U6^4;pfZV=K^3ZFq7)IRX>+b5@#MFezB({{Z&0O-% zWAW90@8!ty&}#A1dGXeUX<=IN)`w4})Tz(nX|dR`(&SaB)$rb>9FEIMqto!-s)W7V z!Pvy{*^}Vs;nLyhwAAD&l+HYtNM@{WtIn`fqFqCv(l?buLYhrKnM=-dkMQ5E@8rYt z%vzPj-?FvF@Z6&C+na*C-0$GCFPF~o+Lqtv>wvu6ioo4` z$#b^aC6dh{kj(Gmxs7{Ja<$p<*pKkVJ>2Epr_8Krt#R_fJ&?ebMVwG$s%*B?x?``` zY_E0ixZ7Q?yVm7) zx7tHSUnVhRy}-|AaG60(fFm(*tIy+&ep|fK(E@?Rqs!svuzFdgW4h4J^T$Hmze>SL zQ|!N#fT)bk-{|h?*>7iCN1RfP!QRNxpyTG+^V-@VAQl`|Z2{-uOkoet;*nvUz56 z(#m0L(|udaaEbB5J@y_-ApigX6m(KfQvmn$6C7ol92|-G)aoQ*Vd-HdC8$~st^fc8 zK}keGR9M4fU>F6X01lWrNCwOZ-NI}$`ta-c)T%j3~0V1mXq6navrV2H~!VjcU8RG85SxqtsMY1^} z$Oed~e~6fy1dttvDh-btxB(&- zt-8o6T3P~?gGASDShHRxLBwR|7Gwj|b$moH4FE>EA3{y*CNY=QYd4Ak!H(^__lWJ% zkcG?1AO$Om0U|zKa79V`#85!0C)@zHSP@(Xh$u%Y!j#QGGXU=V1RD`_1B|t_x<%yd zw6si$3(c(X8{nq_)9I_VRSs&v=83sLrlokFNJROZ3#!&&m9N80U4SqQOofG=fg~Oc zp!FsY1A4@_!M%`auvtW8ig?M*>*aIjiR6I8@`JROt(3NsytImulyh+XHLwBpB61=Q z=O6}nh%3MiuoDjv5h-&NiMny?%0f92O_17es0O?T`(NMA*d#9m(*U_?p(3Zm3q&Se zt(fPuK*Xj^T~;*U7m5KZfZFXJ%Y_;*5%?f93~)@;z0p`a!U2ph+K;cf`dri8VlEQX#n1m=FNg zuL-n2!vJVNAJGQDfKI%qsIDf^{!);B@hp-J5Ye~R1y|0SeGNLr4OWtF00Lw*+vQ;s z2RAT?1P)llxF~PpP~6C(xS7WwNQnV_)M)B5Fz~Vokk{B2Mr!V}lMI+gJ4vGe0RTEy VuPKmh{mcLW002ovPDHLkV1je&7L@=1 literal 0 HcmV?d00001 diff --git a/assets/images/flags/vut.png b/assets/images/flags/vut.png new file mode 100644 index 0000000000000000000000000000000000000000..3c4d6e42954adcf6210e991cba9fdba76a392691 GIT binary patch literal 1267 zcmWlZdo)xD9LIn6UNd**y7L|(gci+Y3ibKmPZu_`Z6BAy zzv-(IW}xE}xTh(FQHO9*FBLjvlBz)IdYrpE2SsxWTd&ZIRP0Ofc%LFjzrv zC}8o~2u=rA2c08ICKE^YZD;J?#-IiT4q~0hSJ`(g$~dg5rwZ|B1J$tNZ_2k8Q7fFlDv_vWmys`D^vu01D-Wc?E8c`y$M$kS zI4LOW&c9WGnTaI@qQt9Nn_MCNbarV?yHk9z+r6pK?LXM4c6g^|lpf5J#U;4EZgPp* zqb&~VIy91-ZQfyG8@(BO-V(nixU zxpFdpSDmrm`_6C7{X70>N<{i{`kEj3eUKRj(@@RirIw)P~pqZS+-7+R&Vy#$>*7L+&N#8hNl& zfvq}r9RqF*Hca|fco$XK2;W4@`>SkX#mQ}TiT^rAJ{87M+K@e!FR$>jGSJ=PDz)U; z%vq}xs2%_pnl6cI< zd3`&@bq}fP1cW_oo%#sV)8R=v%H43$)c;P2i>szYesk)hCc^8kZocT9565oIhf@>r bAk!rUX)G!uoFj`v|9>fk+(%aB5uW-V^z7Ux literal 0 HcmV?d00001 diff --git a/lib/cake_pay/cake_pay_api.dart b/lib/cake_pay/cake_pay_api.dart index f403ebc63..1f91b338d 100644 --- a/lib/cake_pay/cake_pay_api.dart +++ b/lib/cake_pay/cake_pay_api.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:cake_wallet/cake_pay/cake_pay_order.dart'; import 'package:cake_wallet/cake_pay/cake_pay_user_credentials.dart'; import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart'; +import 'package:cake_wallet/entities/country.dart'; import 'package:http/http.dart' as http; class CakePayApi { @@ -171,7 +172,7 @@ class CakePayApi { } /// Get Countries - Future> getCountries( + Future> getCountries( {required String CSRFToken, required String authorization}) async { final uri = Uri.https(baseCakePayUri, countriesPath); @@ -188,8 +189,11 @@ class CakePayApi { } final bodyJson = json.decode(response.body) as List; - - return bodyJson.map((country) => country['name'] as String).toList(); + return bodyJson + .map((country) => country['name'] as String) + .map((name) => Country.fromCakePayName(name)) + .whereType() + .toList(); } /// Get Vendors diff --git a/lib/cake_pay/cake_pay_service.dart b/lib/cake_pay/cake_pay_service.dart index be8e1d189..39f2ca77a 100644 --- a/lib/cake_pay/cake_pay_service.dart +++ b/lib/cake_pay/cake_pay_service.dart @@ -3,6 +3,7 @@ import 'package:cake_wallet/cake_pay/cake_pay_api.dart'; import 'package:cake_wallet/cake_pay/cake_pay_order.dart'; import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart'; import 'package:cake_wallet/core/secure_storage.dart'; +import 'package:cake_wallet/entities/country.dart'; class CakePayService { CakePayService(this.secureStorage, this.cakePayApi); @@ -23,7 +24,7 @@ class CakePayService { final CakePayApi cakePayApi; /// Get Available Countries - Future> getCountries() async => + Future> getCountries() async => await cakePayApi.getCountries(CSRFToken: CSRFToken, authorization: authorization); /// Get Vendors diff --git a/lib/di.dart b/lib/di.dart index 14fdbd56d..540a546fd 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1271,7 +1271,8 @@ Future setup({ () => CakePayService(getIt.get(), getIt.get())); getIt.registerFactory( - () => CakePayCardsListViewModel(cakePayService: getIt.get())); + () => CakePayCardsListViewModel(cakePayService: getIt.get(), + settingsStore: getIt.get())); getIt.registerFactory(() => CakePayAuthViewModel(cakePayService: getIt.get())); diff --git a/lib/entities/country.dart b/lib/entities/country.dart new file mode 100644 index 000000000..63eee9a18 --- /dev/null +++ b/lib/entities/country.dart @@ -0,0 +1,386 @@ +import 'package:cw_core/enumerable_item.dart'; +import 'package:collection/collection.dart'; + +class Country extends EnumerableItem with Serializable { + const Country({required String code, required this.fullName, required this.countryCode}) + : super(title: fullName, raw: code); + + final String fullName; + final String countryCode; + + static List get all => _all.values.toList(); + + static const afghanistan = Country(code: 'afg', countryCode: 'AF', fullName: "Afghanistan"); + static const andorra = Country(code: 'and', countryCode: 'AD', fullName: "Andorra"); + static const angola = Country(code: 'ago', countryCode: 'AO', fullName: "Angola"); + static const anguilla = Country(code: 'aia', countryCode: 'AI', fullName: "Anguilla"); + static const antigua_and_barbuda = + Country(code: 'atg', countryCode: 'AG', fullName: "Antigua and Barbuda"); + static const are = Country(code: 'are', countryCode: 'AE', fullName: "United Arab Emirates"); + static const arg = Country(code: 'arg', countryCode: 'AR', fullName: "Argentina"); + static const arm = Country(code: 'arm', countryCode: 'AM', fullName: "Armenia"); + static const aruba = Country(code: 'abw', countryCode: 'AW', fullName: "Aruba"); + static const aus = Country(code: 'aus', countryCode: 'AU', fullName: "Australia"); + static const aut = Country(code: 'aut', countryCode: 'AT', fullName: "Austria"); + static const aze = Country(code: 'aze', countryCode: 'AZ', fullName: "Azerbaijan"); + static const belize = Country(code: 'blz', countryCode: 'BZ', fullName: "Belize"); + static const bfa = Country(code: 'bfa', countryCode: 'BF', fullName: "Burkina Faso"); + static const bel = Country(code: 'bel', countryCode: 'BE', fullName: "Belgium"); + static const bgd = Country(code: 'bgd', countryCode: 'BD', fullName: "Bangladesh"); + static const bhr = Country(code: 'bhr', countryCode: 'BH', fullName: "Bahrain"); + static const bhs = Country(code: 'bhs', countryCode: 'BS', fullName: "Bahamas"); + static const bhutan = Country(code: 'btn', countryCode: 'BT', fullName: "Bhutan"); + static const bol = Country(code: 'bol', countryCode: 'BO', fullName: "Bolivia"); + static const bra = Country(code: 'bra', countryCode: 'BR', fullName: "Brazil"); + static const brn = Country(code: 'brn', countryCode: 'BN', fullName: "Brunei"); + static const bwa = Country(code: 'bwa', countryCode: 'BW', fullName: "Botswana"); + static const cad = Country(code: 'cad', countryCode: 'CA', fullName: "Canada"); + static const che = Country(code: 'che', countryCode: 'CH', fullName: "Switzerland"); + static const chl = Country(code: 'chl', countryCode: 'CL', fullName: "Chile"); + static const chn = Country(code: 'chn', countryCode: 'CN', fullName: "China"); + static const col = Country(code: 'col', countryCode: 'CO', fullName: "Colombia"); + static const cri = Country(code: 'cri', countryCode: 'CR', fullName: "Costa Rica"); + static const cyp = Country(code: 'cyp', countryCode: 'CY', fullName: "Cyprus"); + static const czk = Country(code: 'czk', countryCode: 'CZ', fullName: "Czech Republic"); + static const deu = Country(code: 'deu', countryCode: 'DE', fullName: "Germany"); + static const dji = Country(code: 'dji', countryCode: 'DJ', fullName: "Djibouti"); + static const dnk = Country(code: 'dnk', countryCode: 'DK', fullName: "Denmark"); + static const dza = Country(code: 'dza', countryCode: 'DZ', fullName: "Algeria"); + static const ecu = Country(code: 'ecu', countryCode: 'EC', fullName: "Ecuador"); + static const egy = Country(code: 'egy', countryCode: 'EG', fullName: "Egypt"); + static const esp = Country(code: 'esp', countryCode: 'ES', fullName: "Spain"); + static const est = Country(code: 'est', countryCode: 'EE', fullName: "Estonia"); + static const eur = Country(code: 'eur', countryCode: 'EU', fullName: "European Union"); + static const fin = Country(code: 'fin', countryCode: 'FI', fullName: "Finland"); + static const fji = Country(code: 'fji', countryCode: 'FJ', fullName: "Fiji"); + static const flk = Country(code: 'flk', countryCode: 'FK', fullName: "Falkland Islands"); + static const fra = Country(code: 'fra', countryCode: 'FR', fullName: "France"); + static const fsm = Country(code: 'fsm', countryCode: 'FM', fullName: "Micronesia"); + static const gab = Country(code: 'gab', countryCode: 'GA', fullName: "Gabon"); + static const gbr = Country(code: 'gbr', countryCode: 'GB', fullName: "United Kingdom"); + static const geo = Country(code: 'geo', countryCode: 'GE', fullName: "Georgia"); + static const gha = Country(code: 'gha', countryCode: 'GH', fullName: "Ghana"); + static const grc = Country(code: 'grc', countryCode: 'GR', fullName: "Greece"); + static const grd = Country(code: 'grd', countryCode: 'GD', fullName: "Grenada"); + static const grl = Country(code: 'grl', countryCode: 'GL', fullName: "Greenland"); + static const gtm = Country(code: 'gtm', countryCode: 'GT', fullName: "Guatemala"); + static const guy = Country(code: 'guy', countryCode: 'GY', fullName: "Guyana"); + static const hkg = Country(code: 'hkg', countryCode: 'HK', fullName: "Hong Kong"); + static const hrv = Country(code: 'hrv', countryCode: 'HR', fullName: "Croatia"); + static const hun = Country(code: 'hun', countryCode: 'HU', fullName: "Hungary"); + static const idn = Country(code: 'idn', countryCode: 'ID', fullName: "Indonesia"); + static const ind = Country(code: 'ind', countryCode: 'IN', fullName: "India"); + static const irl = Country(code: 'irl', countryCode: 'IE', fullName: "Ireland"); + static const irn = Country(code: 'irn', countryCode: 'IR', fullName: "Iran"); + static const isl = Country(code: 'isl', countryCode: 'IS', fullName: "Iceland"); + static const isr = Country(code: 'isr', countryCode: 'IL', fullName: "Israel"); + static const ita = Country(code: 'ita', countryCode: 'IT', fullName: "Italy"); + static const jam = Country(code: 'jam', countryCode: 'JM', fullName: "Jamaica"); + static const jor = Country(code: 'jor', countryCode: 'JO', fullName: "Jordan"); + static const jpn = Country(code: 'jpn', countryCode: 'JP', fullName: "Japan"); + static const kaz = Country(code: 'kaz', countryCode: 'KZ', fullName: "Kazakhstan"); + static const ken = Country(code: 'ken', countryCode: 'KE', fullName: "Kenya"); + static const kir = Country(code: 'kir', countryCode: 'KI', fullName: "Kiribati"); + static const kor = Country(code: 'kor', countryCode: 'KR', fullName: "South Korea"); + static const kwt = Country(code: 'kwt', countryCode: 'KW', fullName: "Kuwait"); + static const lie = Country(code: 'lie', countryCode: 'LI', fullName: "Liechtenstein"); + static const lka = Country(code: 'lka', countryCode: 'LK', fullName: "Sri Lanka"); + static const ltu = Country(code: 'ltu', countryCode: 'LT', fullName: "Lithuania"); + static const lux = Country(code: 'lux', countryCode: 'LU', fullName: "Luxembourg"); + static const lva = Country(code: 'lva', countryCode: 'LV', fullName: "Latvia"); + static const mar = Country(code: 'mar', countryCode: 'MA', fullName: "Morocco"); + static const mex = Country(code: 'mex', countryCode: 'MX', fullName: "Mexico"); + static const mlt = Country(code: 'mlt', countryCode: 'MT', fullName: "Malta"); + static const mnp = Country(code: 'mnp', countryCode: 'MP', fullName: "Northern Mariana Islands"); + static const mtq = Country(code: 'mtq', countryCode: 'MQ', fullName: "Martinique"); + static const mys = Country(code: 'mys', countryCode: 'MY', fullName: "Malaysia"); + static const mwi = Country(code: 'mwi', countryCode: 'MW', fullName: "Malawi"); + static const nga = Country(code: 'nga', countryCode: 'NG', fullName: "Nigeria"); + static const niu = Country(code: 'niu', countryCode: 'NU', fullName: "Niue"); + static const nld = Country(code: 'nld', countryCode: 'NL', fullName: "Netherlands"); + static const nor = Country(code: 'nor', countryCode: 'NO', fullName: "Norway"); + static const nzl = Country(code: 'nzl', countryCode: 'NZ', fullName: "New Zealand"); + static const omn = Country(code: 'omn', countryCode: 'OM', fullName: "Oman"); + static const pak = Country(code: 'pak', countryCode: 'PK', fullName: "Pakistan"); + static const per = Country(code: 'per', countryCode: 'PE', fullName: "Peru"); + static const phl = Country(code: 'phl', countryCode: 'PH', fullName: "Philippines"); + static const pol = Country(code: 'pol', countryCode: 'PL', fullName: "Poland"); + static const pri = Country(code: 'pri', countryCode: 'PR', fullName: "Puerto Rico"); + static const prt = Country(code: 'prt', countryCode: 'PT', fullName: "Portugal"); + static const qat = Country(code: 'qat', countryCode: 'QA', fullName: "Qatar"); + static const rou = Country(code: 'rou', countryCode: 'RO', fullName: "Romania"); + static const rus = Country(code: 'rus', countryCode: 'RU', fullName: "Russia"); + static const saf = Country(code: 'saf', countryCode: 'ZA', fullName: "South Africa"); + static const sau = Country(code: 'sau', countryCode: 'SA', fullName: "Saudi Arabia"); + static const sgp = Country(code: 'sgp', countryCode: 'SG', fullName: "Singapore"); + static const slb = Country(code: 'slb', countryCode: 'SB', fullName: "Solomon Islands"); + static const svk = Country(code: 'svk', countryCode: 'SK', fullName: "Slovakia"); + static const svn = Country(code: 'svn', countryCode: 'SI', fullName: "Slovenia"); + static const swe = Country(code: 'swe', countryCode: 'SE', fullName: "Sweden"); + static const tha = Country(code: 'tha', countryCode: 'TH', fullName: "Thailand"); + static const tkm = Country(code: 'tkm', countryCode: 'TM', fullName: "Turkmenistan"); + static const ton = Country(code: 'ton', countryCode: 'TO', fullName: "Tonga"); + static const tur = Country(code: 'tur', countryCode: 'TR', fullName: "Turkey"); + static const tuv = Country(code: 'tuv', countryCode: 'TV', fullName: "Tuvalu"); + static const twn = Country(code: 'twn', countryCode: 'TW', fullName: "Taiwan"); + static const ukr = Country(code: 'ukr', countryCode: 'UA', fullName: "Ukraine"); + static const ury = Country(code: 'ury', countryCode: 'UY', fullName: "Uruguay"); + static const usa = Country(code: 'usa', countryCode: 'US', fullName: "USA"); + static const ven = Country(code: 'ven', countryCode: 'VE', fullName: "Venezuela"); + static const vnm = Country(code: 'vnm', countryCode: 'VN', fullName: "Vietnam"); + static const vut = Country(code: 'vut', countryCode: 'VU', fullName: "Vanuatu"); + static const btn = Country(code: 'btn', countryCode: 'BT', fullName: "Bhutan"); + static const bgr = Country(code: 'bgr', countryCode: 'BG', fullName: "Bulgaria"); + static const guf = Country(code: 'guf', countryCode: 'GF', fullName: "French Guiana"); + static const bes = Country(code: 'bes', countryCode: 'BQ', fullName: "Caribbean Netherlands"); + static const fro = Country(code: 'fro', countryCode: 'FO', fullName: "Faroe Islands"); + static const cuw = Country(code: 'cuw', countryCode: 'CW', fullName: "Curacao"); + static const msr = Country(code: 'msr', countryCode: 'MS', fullName: "Montserrat"); + static const cpv = Country(code: 'cpv', countryCode: 'CV', fullName: "Cabo Verde"); + static const nfk = Country(code: 'nfk', countryCode: 'NF', fullName: "Norfolk Island"); + static const bmu = Country(code: 'bmu', countryCode: 'BM', fullName: "Bermuda"); + static const vat = Country(code: 'vat', countryCode: 'VA', fullName: "Vatican City"); + static const aia = Country(code: 'aia', countryCode: 'AI', fullName: "Anguilla"); + static const gum = Country(code: 'gum', countryCode: 'GU', fullName: "Guam"); + static const myt = Country(code: 'myt', countryCode: 'YT', fullName: "Mayotte"); + static const mrt = Country(code: 'mrt', countryCode: 'MR', fullName: "Mauritania"); + static const ggy = Country(code: 'ggy', countryCode: 'GG', fullName: "Guernsey"); + static const cck = Country(code: 'cck', countryCode: 'CC', fullName: "Cocos (Keeling) Islands"); + static const blz = Country(code: 'blz', countryCode: 'BZ', fullName: "Belize"); + static const cxr = Country(code: 'cxr', countryCode: 'CX', fullName: "Christmas Island"); + static const mco = Country(code: 'mco', countryCode: 'MC', fullName: "Monaco"); + static const ner = Country(code: 'ner', countryCode: 'NE', fullName: "Niger"); + static const jey = Country(code: 'jey', countryCode: 'JE', fullName: "Jersey"); + static const asm = Country(code: 'asm', countryCode: 'AS', fullName: "American Samoa"); + static const gmb = Country(code: 'gmb', countryCode: 'GM', fullName: "Gambia"); + static const dma = Country(code: 'dma', countryCode: 'DM', fullName: "Dominica"); + static const glp = Country(code: 'glp', countryCode: 'GP', fullName: "Guadeloupe"); + static const ggi = Country(code: 'ggi', countryCode: 'GI', fullName: "Gibraltar"); + static const cmr = Country(code: 'cmr', countryCode: 'CM', fullName: "Cameroon"); + static const atg = Country(code: 'atg', countryCode: 'AG', fullName: "Antigua and Barbuda"); + static const slv = Country(code: 'slv', countryCode: 'SV', fullName: "El Salvador"); + static const pyf = Country(code: 'pyf', countryCode: 'PF', fullName: "French Polynesia"); + static const iot = + Country(code: 'iot', countryCode: 'IO', fullName: "British Indian Ocean Territory"); + static const vir = Country(code: 'vir', countryCode: 'VI', fullName: "Virgin Islands (U.S.)"); + static const abw = Country(code: 'abw', countryCode: 'AW', fullName: "Aruba"); + static const ago = Country(code: 'ago', countryCode: 'AO', fullName: "Angola"); + static const afg = Country(code: 'afg', countryCode: 'AF', fullName: "Afghanistan"); + static const lbn = Country(code: 'lbn', countryCode: 'LB', fullName: "Lebanon"); + static const hmd = + Country(code: 'hmd', countryCode: 'HM', fullName: "Heard Island and McDonald Islands"); + static const cok = Country(code: 'cok', countryCode: 'CK', fullName: "Cook Islands"); + static const bvt = Country(code: 'bvt', countryCode: 'BV', fullName: "Bouvet Island"); + static const atf = + Country(code: 'atf', countryCode: 'TF', fullName: "French Southern Territories"); + static const eth = Country(code: 'eth', countryCode: 'ET', fullName: "Ethiopia"); + static const plw = Country(code: 'plw', countryCode: 'PW', fullName: "Palau"); + static const ata = Country(code: 'ata', countryCode: 'AQ', fullName: "Antarctica"); + + static final _all = { + Country.afghanistan.raw: Country.afghanistan, + Country.andorra.raw: Country.andorra, + Country.angola.raw: Country.angola, + Country.anguilla.raw: Country.anguilla, + Country.antigua_and_barbuda.raw: Country.antigua_and_barbuda, + Country.are.raw: Country.are, + Country.arg.raw: Country.arg, + Country.arm.raw: Country.arm, + Country.aruba.raw: Country.aruba, + Country.aus.raw: Country.aus, + Country.aut.raw: Country.aut, + Country.aze.raw: Country.aze, + Country.belize.raw: Country.belize, + Country.bfa.raw: Country.bfa, + Country.bel.raw: Country.bel, + Country.bgd.raw: Country.bgd, + Country.bhr.raw: Country.bhr, + Country.bhs.raw: Country.bhs, + Country.bhutan.raw: Country.bhutan, + Country.bol.raw: Country.bol, + Country.bra.raw: Country.bra, + Country.brn.raw: Country.brn, + Country.bwa.raw: Country.bwa, + Country.cad.raw: Country.cad, + Country.che.raw: Country.che, + Country.chl.raw: Country.chl, + Country.chn.raw: Country.chn, + Country.col.raw: Country.col, + Country.cri.raw: Country.cri, + Country.cyp.raw: Country.cyp, + Country.czk.raw: Country.czk, + Country.deu.raw: Country.deu, + Country.dji.raw: Country.dji, + Country.dnk.raw: Country.dnk, + Country.dza.raw: Country.dza, + Country.ecu.raw: Country.ecu, + Country.egy.raw: Country.egy, + Country.esp.raw: Country.esp, + Country.est.raw: Country.est, + Country.eur.raw: Country.eur, + Country.fin.raw: Country.fin, + Country.fji.raw: Country.fji, + Country.flk.raw: Country.flk, + Country.fra.raw: Country.fra, + Country.fsm.raw: Country.fsm, + Country.gab.raw: Country.gab, + Country.gbr.raw: Country.gbr, + Country.geo.raw: Country.geo, + Country.gha.raw: Country.gha, + Country.grc.raw: Country.grc, + Country.grd.raw: Country.grd, + Country.grl.raw: Country.grl, + Country.gtm.raw: Country.gtm, + Country.guy.raw: Country.guy, + Country.hkg.raw: Country.hkg, + Country.hrv.raw: Country.hrv, + Country.hun.raw: Country.hun, + Country.idn.raw: Country.idn, + Country.ind.raw: Country.ind, + Country.irl.raw: Country.irl, + Country.irn.raw: Country.irn, + Country.isl.raw: Country.isl, + Country.isr.raw: Country.isr, + Country.ita.raw: Country.ita, + Country.jam.raw: Country.jam, + Country.jor.raw: Country.jor, + Country.jpn.raw: Country.jpn, + Country.kaz.raw: Country.kaz, + Country.ken.raw: Country.ken, + Country.kir.raw: Country.kir, + Country.kor.raw: Country.kor, + Country.kwt.raw: Country.kwt, + Country.lie.raw: Country.lie, + Country.lka.raw: Country.lka, + Country.ltu.raw: Country.ltu, + Country.lux.raw: Country.lux, + Country.lva.raw: Country.lva, + Country.mar.raw: Country.mar, + Country.mex.raw: Country.mex, + Country.mlt.raw: Country.mlt, + Country.mnp.raw: Country.mnp, + Country.mtq.raw: Country.mtq, + Country.mys.raw: Country.mys, + Country.mwi.raw: Country.mwi, + Country.nga.raw: Country.nga, + Country.niu.raw: Country.niu, + Country.nld.raw: Country.nld, + Country.nor.raw: Country.nor, + Country.nzl.raw: Country.nzl, + Country.omn.raw: Country.omn, + Country.pak.raw: Country.pak, + Country.per.raw: Country.per, + Country.phl.raw: Country.phl, + Country.pol.raw: Country.pol, + Country.pri.raw: Country.pri, + Country.prt.raw: Country.prt, + Country.qat.raw: Country.qat, + Country.rou.raw: Country.rou, + Country.rus.raw: Country.rus, + Country.saf.raw: Country.saf, + Country.sau.raw: Country.sau, + Country.sgp.raw: Country.sgp, + Country.slb.raw: Country.slb, + Country.svk.raw: Country.svk, + Country.svn.raw: Country.svn, + Country.swe.raw: Country.swe, + Country.tha.raw: Country.tha, + Country.tkm.raw: Country.tkm, + Country.ton.raw: Country.ton, + Country.tur.raw: Country.tur, + Country.tuv.raw: Country.tuv, + Country.twn.raw: Country.twn, + Country.ukr.raw: Country.ukr, + Country.ury.raw: Country.ury, + Country.usa.raw: Country.usa, + Country.ven.raw: Country.ven, + Country.vnm.raw: Country.vnm, + Country.vut.raw: Country.vut, + Country.btn.raw: Country.btn, + Country.bgr.raw: Country.bgr, + Country.guf.raw: Country.guf, + Country.bes.raw: Country.bes, + Country.fro.raw: Country.fro, + Country.cuw.raw: Country.cuw, + Country.msr.raw: Country.msr, + Country.cpv.raw: Country.cpv, + Country.nfk.raw: Country.nfk, + Country.bmu.raw: Country.bmu, + Country.vat.raw: Country.vat, + Country.aia.raw: Country.aia, + Country.gum.raw: Country.gum, + Country.myt.raw: Country.myt, + Country.mrt.raw: Country.mrt, + Country.ggy.raw: Country.ggy, + Country.cck.raw: Country.cck, + Country.blz.raw: Country.blz, + Country.cxr.raw: Country.cxr, + Country.mco.raw: Country.mco, + Country.ner.raw: Country.ner, + Country.jey.raw: Country.jey, + Country.asm.raw: Country.asm, + Country.gmb.raw: Country.gmb, + Country.dma.raw: Country.dma, + Country.glp.raw: Country.glp, + Country.ggi.raw: Country.ggi, + Country.cmr.raw: Country.cmr, + Country.atg.raw: Country.atg, + Country.slv.raw: Country.slv, + Country.pyf.raw: Country.pyf, + Country.iot.raw: Country.iot, + Country.vir.raw: Country.vir, + Country.abw.raw: Country.abw, + Country.ago.raw: Country.ago, + Country.afg.raw: Country.afg, + Country.lbn.raw: Country.lbn, + Country.hmd.raw: Country.hmd, + Country.cok.raw: Country.cok, + Country.bvt.raw: Country.bvt, + Country.atf.raw: Country.atf, + Country.eth.raw: Country.eth, + Country.plw.raw: Country.plw, + Country.ata.raw: Country.ata, + }; + + static final Map _cakePayNames = { + 'Slovak Republic': 'Slovakia', + 'Brunei Darussalam': 'Brunei', + 'Federated States of Micronesia': 'Micronesia', + 'Sri lanka': 'Sri Lanka', + 'UAE': 'United Arab Emirates', + 'UK': 'United Kingdom', + 'Curaçao': "Curacao", + }; + + static Country deserialize({required String raw}) => _all[raw]!; + + static final Map countryByName = { + for (var country in _all.values) country.fullName: country, + }; + + static Country? fromCakePayName(String name) { + final normalizedName = _cakePayNames[name] ?? name; + return countryByName[normalizedName]; + } + + static String getCakePayName(Country country) { + return _cakePayNames.entries + .firstWhere( + (entry) => entry.value == country.fullName, + orElse: () => MapEntry(country.fullName, country.fullName), + ) + .key; + } + + static Country? fromCode(String countryCode) { + return _all.values.firstWhereOrNull((element) => element.raw == countryCode.toLowerCase()); + } + + @override + bool operator ==(Object other) => other is Country && other.raw == raw; + + @override + int get hashCode => raw.hashCode ^ title.hashCode; + + String get iconPath => "assets/images/flags/$raw.png"; +} diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 8e8bfb6ed..0bb526e5d 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -12,6 +12,7 @@ class PreferencesKey { static const currentBananoNodeIdKey = 'current_node_id_banano'; static const currentBananoPowNodeIdKey = 'current_node_id_banano_pow'; static const currentFiatCurrencyKey = 'current_fiat_currency'; + static const currentCakePayCountry = 'current_cake_pay_country'; static const currentBitcoinCashNodeIdKey = 'current_node_id_bch'; static const currentSolanaNodeIdKey = 'current_node_id_sol'; static const currentTronNodeIdKey = 'current_node_id_trx'; diff --git a/lib/src/screens/cake_pay/cards/cake_pay_cards_page.dart b/lib/src/screens/cake_pay/cards/cake_pay_cards_page.dart index 35a58ce0a..31eaa23ff 100644 --- a/lib/src/screens/cake_pay/cards/cake_pay_cards_page.dart +++ b/lib/src/screens/cake_pay/cards/cake_pay_cards_page.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/cake_pay/cake_pay_states.dart'; import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart'; +import 'package:cake_wallet/entities/country.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; @@ -8,6 +9,7 @@ import 'package:cake_wallet/src/screens/cake_pay/widgets/card_menu.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/filter_widget.dart'; import 'package:cake_wallet/src/widgets/cake_scrollbar.dart'; import 'package:cake_wallet/src/widgets/gradient_background.dart'; +import 'package:cake_wallet/src/widgets/picker.dart'; 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/exchange_page_theme.dart'; @@ -20,6 +22,7 @@ import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/cake_pay/cake_pay_cards_list_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; class CakePayCardsPage extends BasePage { CakePayCardsPage(this._cardsListViewModel) : searchFocusNode = FocusNode() { @@ -80,9 +83,25 @@ class CakePayCardsPage extends BasePage { @override Widget body(BuildContext context) { + + if (_cardsListViewModel.settingsStore.selectedCakePayCountry == null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + reaction((_) => _cardsListViewModel.shouldShowCountryPicker, (bool shouldShowCountryPicker) async { + if (shouldShowCountryPicker) { + _cardsListViewModel.storeInitialFilterStates(); + await showCountryPicker(context, _cardsListViewModel); + if (_cardsListViewModel.hasFiltersChanged) { + _cardsListViewModel.resetLoadingNextPageState(); + _cardsListViewModel.getVendors(); + } + } + }); + }); + } + final filterButton = Semantics( label: S.of(context).filter_by, - child: InkWell( + child: GestureDetector( onTap: () async { _cardsListViewModel.storeInitialFilterStates(); await showFilterWidget(context); @@ -92,50 +111,87 @@ class CakePayCardsPage extends BasePage { } }, child: Container( - width: 32, - padding: EdgeInsets.all(8), - decoration: BoxDecoration( - color: Theme.of(context).extension()!.syncedBackgroundColor, - border: Border.all( - color: Colors.white.withOpacity(0.2), + width: 32, + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).extension()!.syncedBackgroundColor, + border: Border.all( + color: Colors.transparent, + ), + borderRadius: BorderRadius.circular(10), ), - borderRadius: BorderRadius.circular(10), + child: Image.asset( + 'assets/images/filter.png', + color: Theme.of(context).extension()!.iconColor, + ))), + ); + final _countryPicker = Semantics( + label: S.of(context).filter_by, + child: GestureDetector( + onTap: () async { + _cardsListViewModel.storeInitialFilterStates(); + await showCountryPicker(context, _cardsListViewModel); + if (_cardsListViewModel.hasFiltersChanged) { + _cardsListViewModel.resetLoadingNextPageState(); + _cardsListViewModel.getVendors(); + } + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 6), + decoration: BoxDecoration( + color: Theme.of(context).extension()!.syncedBackgroundColor, + border: Border.all(color: Colors.transparent), + borderRadius: BorderRadius.circular(10), + ), + child: Container( + margin: EdgeInsets.symmetric(vertical: 2), + child: Row( + children: [ + Image.asset( + _cardsListViewModel.selectedCountry.iconPath, + width: 24, + height: 24, + errorBuilder: (context, error, stackTrace) => Container( + width: 24, + height: 24, + ), + ), + SizedBox(width: 6), + Text( + _cardsListViewModel.selectedCountry.countryCode, + style: TextStyle( + color: Theme.of(context).extension()!.textColor, + fontSize: 16, + fontWeight: FontWeight.w700, + ), + ), + ], ), - child: Image.asset( - 'assets/images/filter.png', - color: Theme.of(context).extension()!.iconColor, - ), - )), + ), + ), + ), ); return Padding( - padding: const EdgeInsets.all(14.0), - child: Column( - children: [ + padding: const EdgeInsets.all(14.0), + child: Column(children: [ Container( - padding: EdgeInsets.only(left: 2, right: 22), - height: 32, - child: Row( - children: [ + padding: EdgeInsets.only(left: 2, right: 22), + height: 32, + child: Row(children: [ Expanded( child: _SearchWidget( controller: _searchController, focusNode: searchFocusNode, )), - SizedBox(width: 10), - filterButton - ], - ), - ), + SizedBox(width: 5), + filterButton, + SizedBox(width: 5), + _countryPicker + ])), SizedBox(height: 8), - Expanded( - child: CakePayCardsPageBody( - cardsListViewModel: _cardsListViewModel, - ), - ), - ], - ), - ); + Expanded(child: CakePayCardsPageBody(cardsListViewModel: _cardsListViewModel)) + ])); } Future showFilterWidget(BuildContext context) async { @@ -148,6 +204,32 @@ class CakePayCardsPage extends BasePage { } } + +Future showCountryPicker(BuildContext context, CakePayCardsListViewModel cardsListViewModel) async { + await showPopUp( + context: context, + builder: (_) => Picker( + title: S.of(context).select_your_country, + items: cardsListViewModel.availableCountries, + images: cardsListViewModel.availableCountries + .map((e) => Image.asset( + e.iconPath, + errorBuilder: (context, error, stackTrace) => Container( + width: 58, + height: 58, + ), + )) + .toList(), + selectedAtIndex: cardsListViewModel.availableCountries + .indexOf(cardsListViewModel.selectedCountry), + onItemSelected: (Country country) => + cardsListViewModel.setSelectedCountry(country), + isSeparated: false, + hintText: S.of(context).search, + matchingCriteria: (Country country, String searchText) => + country.fullName.toLowerCase().contains(searchText.toLowerCase()))); +} + class CakePayCardsPageBody extends StatefulWidget { const CakePayCardsPageBody({ Key? key, @@ -304,15 +386,9 @@ class _SearchWidget extends StatelessWidget { alignLabelWithHint: true, floatingLabelBehavior: FloatingLabelBehavior.never, suffixIcon: searchIcon, - border: OutlineInputBorder( - borderSide: BorderSide( - color: Colors.white.withOpacity(0.2), - ), - borderRadius: BorderRadius.circular(10), - ), enabledBorder: OutlineInputBorder( borderSide: BorderSide( - color: Colors.white.withOpacity(0.2), + color: Colors.transparent, ), borderRadius: BorderRadius.circular(10), ), diff --git a/lib/src/screens/cake_pay/widgets/card_item.dart b/lib/src/screens/cake_pay/widgets/card_item.dart index ce804adc2..1234c0a1f 100644 --- a/lib/src/screens/cake_pay/widgets/card_item.dart +++ b/lib/src/screens/cake_pay/widgets/card_item.dart @@ -9,7 +9,7 @@ class CardItem extends StatelessWidget { required this.backgroundColor, required this.titleColor, required this.subtitleColor, - this.hideBorder = false, + this.hideBorder = true, this.discount = 0.0, this.isAmount = false, this.discountBackground, diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index c966420db..50d51d2ed 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -9,6 +9,7 @@ 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'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; @@ -128,7 +129,8 @@ abstract class SettingsStoreBase with Store { TransactionPriority? initialLitecoinTransactionPriority, TransactionPriority? initialEthereumTransactionPriority, TransactionPriority? initialPolygonTransactionPriority, - TransactionPriority? initialBitcoinCashTransactionPriority}) + TransactionPriority? initialBitcoinCashTransactionPriority, + Country? initialCakePayCountry}) : nodes = ObservableMap.of(nodes), powNodes = ObservableMap.of(powNodes), _secureStorage = secureStorage, @@ -213,6 +215,10 @@ abstract class SettingsStoreBase with Store { priority[WalletType.bitcoinCash] = initialBitcoinCashTransactionPriority; } + if (initialCakePayCountry != null) { + selectedCakePayCountry = initialCakePayCountry; + } + initializeTrocadorProviderStates(); WalletType.values.forEach((walletType) { @@ -244,6 +250,15 @@ abstract class SettingsStoreBase with Store { (FiatCurrency fiatCurrency) => sharedPreferences.setString( PreferencesKey.currentFiatCurrencyKey, fiatCurrency.serialize())); + reaction( + (_) => selectedCakePayCountry, + (Country? country) { + if (country != null) { + sharedPreferences.setString( + PreferencesKey.currentCakePayCountry, country.raw); + } + }); + reaction( (_) => shouldShowYatPopup, (bool shouldShowYatPopup) => @@ -625,6 +640,9 @@ abstract class SettingsStoreBase with Store { @observable FiatCurrency fiatCurrency; + @observable + Country? selectedCakePayCountry; + @observable bool shouldShowYatPopup; @@ -880,6 +898,10 @@ abstract class SettingsStoreBase with Store { final backgroundTasks = getIt.get(); final currentFiatCurrency = FiatCurrency.deserialize( raw: sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey)!); + final savedCakePayCountryRaw = sharedPreferences.getString(PreferencesKey.currentCakePayCountry); + final currentCakePayCountry = savedCakePayCountryRaw != null + ? Country.deserialize(raw: savedCakePayCountryRaw) + : null; TransactionPriority? moneroTransactionPriority = monero?.deserializeMoneroTransactionPriority( raw: sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority)!); @@ -1225,6 +1247,7 @@ abstract class SettingsStoreBase with Store { deviceName: deviceName, isBitcoinBuyEnabled: isBitcoinBuyEnabled, initialFiatCurrency: currentFiatCurrency, + initialCakePayCountry: currentCakePayCountry, initialBalanceDisplayMode: currentBalanceDisplayMode, initialSaveRecipientAddress: shouldSaveRecipientAddress, initialAutoGenerateSubaddressStatus: autoGenerateSubaddressStatus, diff --git a/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart b/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart index d0483596e..8585da9da 100644 --- a/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart +++ b/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart @@ -1,8 +1,10 @@ import 'package:cake_wallet/cake_pay/cake_pay_service.dart'; import 'package:cake_wallet/cake_pay/cake_pay_states.dart'; import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart'; +import 'package:cake_wallet/entities/country.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/view_model/dashboard/dropdown_filter_item.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/view_model/dashboard/filter_item.dart'; import 'package:mobx/mobx.dart'; @@ -13,11 +15,11 @@ class CakePayCardsListViewModel = CakePayCardsListViewModelBase with _$CakePayCa abstract class CakePayCardsListViewModelBase with Store { CakePayCardsListViewModelBase({ required this.cakePayService, + required this.settingsStore, }) : cardState = CakePayCardsStateNoCards(), cakePayVendors = [], availableCountries = [], page = 1, - selectedCountry = 'USA', displayPrepaidCards = true, displayGiftCards = true, displayDenominationsCards = true, @@ -30,13 +32,20 @@ abstract class CakePayCardsListViewModelBase with Store { initialization(); } + static Country _getInitialCountry(FiatCurrency fiatCurrency) { + if (fiatCurrency.countryCode == 'eur') { + return Country.deu; + } + return Country.fromCode(fiatCurrency.countryCode) ?? Country.usa; + } + void initialization() async { await getCountries(); - selectedCountry = availableCountries.first; getVendors(); } final CakePayService cakePayService; + final SettingsStore settingsStore; List CakePayVendorList; @@ -61,28 +70,15 @@ abstract class CakePayCardsListViewModelBase with Store { caption: S.current.custom_value, onChanged: toggleCustomValueCards), ], - S.current.countries: [ - DropdownFilterItem( - items: availableCountries, - caption: '', - selectedItem: selectedCountry, - onItemSelected: (String value) => setSelectedCountry(value), - ), - ] }; String searchString; - int page; - late String _initialSelectedCountry; - + late Country _initialSelectedCountry; late bool _initialDisplayPrepaidCards; - late bool _initialDisplayGiftCards; - late bool _initialDisplayDenominationsCards; - late bool _initialDisplayCustomValueCards; @observable @@ -107,7 +103,7 @@ abstract class CakePayCardsListViewModelBase with Store { List cakePayVendors; @observable - List availableCountries; + List availableCountries; @observable bool displayPrepaidCards; @@ -121,15 +117,22 @@ abstract class CakePayCardsListViewModelBase with Store { @observable bool displayCustomValueCards; - @observable - String selectedCountry; + @computed + Country get selectedCountry => + settingsStore.selectedCakePayCountry ?? _getInitialCountry(settingsStore.fiatCurrency); + + @computed + bool get shouldShowCountryPicker => settingsStore.selectedCakePayCountry == null && availableCountries.isNotEmpty; + + + bool get hasFiltersChanged { + return selectedCountry != _initialSelectedCountry || + displayPrepaidCards != _initialDisplayPrepaidCards || + displayGiftCards != _initialDisplayGiftCards || + displayDenominationsCards != _initialDisplayDenominationsCards || + displayCustomValueCards != _initialDisplayCustomValueCards; + } - bool get hasFiltersChanged => - selectedCountry != _initialSelectedCountry || - displayPrepaidCards != _initialDisplayPrepaidCards || - displayGiftCards != _initialDisplayGiftCards || - displayDenominationsCards != _initialDisplayDenominationsCards || - displayCustomValueCards != _initialDisplayCustomValueCards; Future getCountries() async { availableCountries = await cakePayService.getCountries(); @@ -143,7 +146,7 @@ abstract class CakePayCardsListViewModelBase with Store { vendorsState = CakePayVendorLoadingState(); searchString = text ?? ''; var newVendors = await cakePayService.getVendors( - country: selectedCountry, + country: Country.getCakePayName(selectedCountry), page: currentPage ?? page, search: searchString, giftCards: displayGiftCards, @@ -152,20 +155,20 @@ abstract class CakePayCardsListViewModelBase with Store { onDemand: displayDenominationsCards); cakePayVendors = CakePayVendorList = newVendors; - vendorsState = CakePayVendorLoadedState(); } @action Future fetchNextPage() async { - if (vendorsState is CakePayVendorLoadingState || !hasMoreDataToFetch || isLoadingNextPage) + if (vendorsState is CakePayVendorLoadingState || !hasMoreDataToFetch || isLoadingNextPage) { return; + } isLoadingNextPage = true; page++; try { var newVendors = await cakePayService.getVendors( - country: selectedCountry, + country: Country.getCakePayName(selectedCountry), page: page, search: searchString, giftCards: displayGiftCards, @@ -201,7 +204,7 @@ abstract class CakePayCardsListViewModelBase with Store { } @action - void setSelectedCountry(String country) => selectedCountry = country; + void setSelectedCountry(Country country) => settingsStore.selectedCakePayCountry = country; @action void togglePrepaidCards() => displayPrepaidCards = !displayPrepaidCards; diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 0db2031dd..b8c933a2e 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -634,6 +634,7 @@ "select_buy_provider_notice": "حدد مزود شراء أعلاه. يمكنك تخطي هذه الشاشة عن طريق تعيين مزود شراء الافتراضي في إعدادات التطبيق.", "select_destination": ".ﻲﻃﺎﻴﺘﺣﻻﺍ ﺦﺴﻨﻟﺍ ﻒﻠﻣ ﺔﻬﺟﻭ ﺪﻳﺪﺤﺗ ءﺎﺟﺮﻟﺍ", "select_sell_provider_notice": ".ﻖﻴﺒﻄﺘﻟﺍ ﺕﺍﺩﺍﺪﻋﺇ ﻲﻓ ﻚﺑ ﺹﺎﺨﻟﺍ ﻲﺿﺍﺮﺘﻓﻻﺍ ﻊﻴﺒﻟﺍ ﺩﻭﺰﻣ ﻦﻴﻴﻌﺗ ﻖﻳﺮﻃ ﻦﻋ ﺔﺷﺎﺸﻟﺍ ﻩﺬﻫ ﻲﻄﺨﺗ", + "select_your_country": "الرجاء تحديد بلدك", "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 7e78dd69a..2fd644d01 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -634,6 +634,7 @@ "select_buy_provider_notice": "Изберете доставчик на покупка по -горе. Можете да пропуснете този екран, като зададете вашия доставчик по подразбиране по подразбиране в настройките на приложението.", "select_destination": "Моля, изберете дестинация за архивния файл.", "select_sell_provider_notice": "Изберете доставчик на продажба по-горе. Можете да пропуснете този екран, като зададете своя доставчик на продажба по подразбиране в настройките на приложението.", + "select_your_country": "Моля, изберете вашата страна", "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 f17466a9f..8f3b5bf28 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -634,6 +634,7 @@ "select_buy_provider_notice": "Vyberte výše uvedeného poskytovatele nákupu. Tuto obrazovku můžete přeskočit nastavením výchozího poskytovatele nákupu v nastavení aplikace.", "select_destination": "Vyberte cíl pro záložní soubor.", "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.", + "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.", "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 cb0a59574..77a531d1b 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -635,6 +635,7 @@ "select_buy_provider_notice": "Wählen Sie oben einen Anbieter kaufen. Sie können diese Seite überspringen, indem Sie Ihren Standard-Kaufanbieter in den App-Einstellungen festlegen.", "select_destination": "Bitte wählen Sie das Ziel für die Sicherungsdatei aus.", "select_sell_provider_notice": "Wählen Sie oben einen Verkaufsanbieter aus. Sie können diesen Bildschirm überspringen, indem Sie in den App-Einstellungen Ihren Standard-Verkaufsanbieter festlegen.", + "select_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.", "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 40be8fd08..4fe375ff9 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -636,6 +636,7 @@ "select_buy_provider_notice": "Select a buy provider above. You can skip this screen by setting your default buy provider in app settings.", "select_destination": "Please select destination for the backup file.", "select_sell_provider_notice": "Select a sell provider above. You can skip this screen by setting your default sell provider in app settings.", + "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.", "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 5a693cf32..31498df2d 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -635,6 +635,7 @@ "select_buy_provider_notice": "Selecciona un proveedor de compra arriba. Puede omitir esta pantalla configurando su proveedor de compra predeterminado en la configuración de la aplicación.", "select_destination": "Selecciona el destino del archivo de copia de seguridad.", "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", "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 1865e41f2..f96e9e304 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -634,6 +634,7 @@ "select_buy_provider_notice": "Sélectionnez un fournisseur d'achat ci-dessus. Vous pouvez ignorer cet écran en définissant votre fournisseur d'achat par défaut dans les paramètres de l'application.", "select_destination": "Veuillez sélectionner la destination du fichier de sauvegarde.", "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.", + "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.", "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 52cf14128..5fc64e0b3 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -636,6 +636,7 @@ "select_buy_provider_notice": "Zaɓi mai ba da kyauta a sama. Zaka iya tsallake wannan allon ta hanyar saita mai ba da isasshen busasshen mai ba da isasshen busasshiyar saiti.", "select_destination": "Da fatan za a zaɓi wurin da za a yi wa madadin fayil ɗin.", "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.", + "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.", "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 64f112a42..fb3b78900 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -636,6 +636,7 @@ "select_buy_provider_notice": "ऊपर एक खरीद प्रदाता का चयन करें। आप इस स्क्रीन को ऐप सेटिंग्स में अपना डिफ़ॉल्ट बाय प्रदाता सेट करके छोड़ सकते हैं।", "select_destination": "कृपया बैकअप फ़ाइल के लिए गंतव्य का चयन करें।", "select_sell_provider_notice": "ऊपर एक विक्रय प्रदाता का चयन करें। आप ऐप सेटिंग में अपना डिफ़ॉल्ट विक्रय प्रदाता सेट करके इस स्क्रीन को छोड़ सकते हैं।", + "select_your_country": "कृपया अपने देश का चयन करें", "sell": "बेचना", "sell_alert_content": "हम वर्तमान में केवल बिटकॉइन, एथेरियम और लाइटकॉइन की बिक्री का समर्थन करते हैं। कृपया अपना बिटकॉइन, एथेरियम या लाइटकॉइन वॉलेट बनाएं या उसमें स्विच करें।", "sell_monero_com_alert_content": "मोनेरो बेचना अभी तक समर्थित नहीं है", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 4bbf4f141..230aa955b 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -634,6 +634,7 @@ "select_buy_provider_notice": "Odaberite gornji davatelj kupnje. Ovaj zaslon možete preskočiti postavljanjem zadanog davatelja usluga kupnje u postavkama aplikacija.", "select_destination": "Odaberite odredište za datoteku sigurnosne kopije.", "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.", + "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.", "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 cbaf3a548..63a986103 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -626,6 +626,7 @@ "select_buy_provider_notice": "Ընտրեք գնման մատակարարը վերևում։ Դուք կարող եք բաց թողնել այս էկրանը ձեր լռելայն գնման մատակարարը հավելվածի կարգավորումներում սահմանելով", "select_destination": "Խնդրում ենք ընտրել կրկնօրինակ ֆայլի նպատակակետը", "select_sell_provider_notice": "Ընտրեք վաճառքի մատակարարը վերևում։ Դուք կարող եք բաց թողնել այս էկրանը ձեր լռելայն վաճառքի մատակարարը հավելվածի կարգավորումներում սահմանելով", + "select_your_country": "Խնդրում ենք ընտրել ձեր երկիրը", "sell": "Ծախել", "sell_alert_content": "Մենք ներկայումս պաշտպանում ենք միայն Bitcoin, Ethereum և Litecoin վաճառքը։ Խնդրում ենք ստեղծել կամ միացնել ձեր Bitcoin, Ethereum կամ Litecoin դրամապանակը", "sell_monero_com_alert_content": "Monero-ի վաճառքը դեռ չի պաշտպանվում", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 273087eec..3fa3a958f 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -637,6 +637,7 @@ "select_buy_provider_notice": "Pilih penyedia beli di atas. Anda dapat melewatkan layar ini dengan mengatur penyedia pembelian default Anda di pengaturan aplikasi.", "select_destination": "Silakan pilih tujuan untuk file cadangan.", "select_sell_provider_notice": "Pilih penyedia jual di atas. Anda dapat melewati layar ini dengan mengatur penyedia penjualan default Anda di pengaturan aplikasi.", + "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.", "sell_monero_com_alert_content": "Menjual Monero belum didukung", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 917315aa6..aab18d434 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -636,6 +636,7 @@ "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_destination": "Seleziona la destinazione per il file di backup.", "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_your_country": "Seleziona il tuo paese", "sell": "Vendere", "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 ae3e32ea8..f1fd0acd3 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -635,6 +635,7 @@ "select_buy_provider_notice": "上記の購入プロバイダーを選択してください。デフォルトの購入プロバイダーをアプリ設定で設定して、この画面をスキップできます。", "select_destination": "バックアップファイルの保存先を選択してください。", "select_sell_provider_notice": "上記の販売プロバイダーを選択してください。アプリ設定でデフォルトの販売プロバイダーを設定することで、この画面をスキップできます。", + "select_your_country": "あなたの国を選択してください", "sell": "売る", "sell_alert_content": "現在、ビットコイン、イーサリアム、ライトコインの販売のみをサポートしています。ビットコイン、イーサリアム、またはライトコインのウォレットを作成するか、これらのウォレットに切り替えてください。", "sell_monero_com_alert_content": "モネロの販売はまだサポートされていません", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 2dc1a2dfc..7e3467cf2 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -635,6 +635,7 @@ "select_buy_provider_notice": "위의 구매 제공자를 선택하십시오. 앱 설정에서 기본 구매 제공자를 설정 하여이 화면을 건너 뛸 수 있습니다.", "select_destination": "백업 파일의 대상을 선택하십시오.", "select_sell_provider_notice": "위에서 판매 공급자를 선택하세요. 앱 설정에서 기본 판매 공급자를 설정하면 이 화면을 건너뛸 수 있습니다.", + "select_your_country": "국가를 선택하십시오", "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 f42af6863..10c13cfef 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -634,6 +634,7 @@ "select_buy_provider_notice": "အပေါ်ကဝယ်သူတစ် ဦး ကိုရွေးချယ်ပါ။ သင်၏ default 0 ယ်သူအား app settings တွင် setting လုပ်ခြင်းဖြင့်ဤ screen ကိုကျော်သွားနိုင်သည်။", "select_destination": "အရန်ဖိုင်အတွက် ဦးတည်ရာကို ရွေးပါ။", "select_sell_provider_notice": "အထက်ဖော်ပြပါ အရောင်းဝန်ဆောင်မှုပေးသူကို ရွေးပါ။ အက်ပ်ဆက်တင်များတွင် သင်၏မူလရောင်းချပေးသူကို သတ်မှတ်ခြင်းဖြင့် ဤစခရင်ကို ကျော်နိုင်သည်။", + "select_your_country": "ကျေးဇူးပြု. သင့်နိုင်ငံကိုရွေးချယ်ပါ", "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 73e3df6b6..9552f2439 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -634,6 +634,7 @@ "select_buy_provider_notice": "Selecteer hierboven een koopprovider. U kunt dit scherm overslaan door uw standaard kopenprovider in te stellen in app -instellingen.", "select_destination": "Selecteer de bestemming voor het back-upbestand.", "select_sell_provider_notice": "Selecteer hierboven een verkoopaanbieder. U kunt dit scherm overslaan door uw standaardverkoopprovider in te stellen in de app-instellingen.", + "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.", "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 aaa82a3bf..f3c3e4810 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -634,6 +634,7 @@ "select_buy_provider_notice": "Wybierz powyższe dostawcę zakupu. Możesz pominąć ten ekran, ustawiając domyślnego dostawcę zakupu w ustawieniach aplikacji.", "select_destination": "Wybierz miejsce docelowe dla pliku kopii zapasowej.", "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.", + "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.", "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 194307a3d..410e8cb1c 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -636,6 +636,7 @@ "select_buy_provider_notice": "Selecione um provedor de compra acima. Você pode pular esta tela definindo seu provedor de compra padrão nas configurações de aplicativos.", "select_destination": "Selecione o destino para o arquivo de backup.", "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.", + "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.", "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 f42c28c15..a8ee5a309 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -635,6 +635,7 @@ "select_buy_provider_notice": "Выберите поставщика покупки выше. Вы можете пропустить этот экран, установив поставщика покупки по умолчанию в настройках приложения.", "select_destination": "Пожалуйста, выберите место для файла резервной копии.", "select_sell_provider_notice": "Выберите поставщика услуг продажи выше. Вы можете пропустить этот экран, установив поставщика услуг продаж по умолчанию в настройках приложения.", + "select_your_country": "Пожалуйста, выберите свою страну", "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 9d8fb0af2..5102e150c 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -634,6 +634,7 @@ "select_buy_provider_notice": "เลือกผู้ให้บริการซื้อด้านบน คุณสามารถข้ามหน้าจอนี้ได้โดยการตั้งค่าผู้ให้บริการซื้อเริ่มต้นในการตั้งค่าแอป", "select_destination": "โปรดเลือกปลายทางสำหรับไฟล์สำรอง", "select_sell_provider_notice": "เลือกผู้ให้บริการการขายด้านบน คุณสามารถข้ามหน้าจอนี้ได้โดยการตั้งค่าผู้ให้บริการการขายเริ่มต้นในการตั้งค่าแอป", + "select_your_country": "กรุณาเลือกประเทศของคุณ", "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 ca35e7997..eb78c8d3c 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -634,6 +634,7 @@ "select_buy_provider_notice": "Pumili ng provider ng pagbili sa itaas. Maaari mong laktawan ang screen na ito sa pamamagitan ng pagtatakda ng iyong default na provider ng pagbili sa mga setting ng app.", "select_destination": "Mangyaring piliin ang patutunguhan para sa backup na file.", "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.", + "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.", "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 1fee66369..0b4700397 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -634,6 +634,7 @@ "select_buy_provider_notice": "Yukarıda bir satın alma sağlayıcısı seçin. App ayarlarında varsayılan satın alma sağlayıcınızı ayarlayarak bu ekranı atlayabilirsiniz.", "select_destination": "Lütfen yedekleme dosyası için hedef 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.", + "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.", "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 5d9a56c1e..c9afde7be 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -635,6 +635,7 @@ "select_buy_provider_notice": "Виберіть постачальника купівлі вище. Ви можете пропустити цей екран, встановивши свого постачальника купівлі за замовчуванням у налаштуваннях додатків.", "select_destination": "Виберіть місце призначення для файлу резервної копії.", "select_sell_provider_notice": "Виберіть вище постачальника послуг продажу. Ви можете пропустити цей екран, встановивши постачальника послуг продажу за умовчанням у налаштуваннях програми.", + "select_your_country": "Будь ласка, виберіть свою країну", "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 106f00f96..50c6f1889 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -636,6 +636,7 @@ "select_buy_provider_notice": "اوپر خریدنے والا خریدنے والا منتخب کریں۔ آپ ایپ کی ترتیبات میں اپنے پہلے سے طے شدہ خریدنے والے کو ترتیب دے کر اس اسکرین کو چھوڑ سکتے ہیں۔", "select_destination": "۔ﮟﯾﺮﮐ ﺏﺎﺨﺘﻧﺍ ﺎﮐ ﻝﺰﻨﻣ ﮯﯿﻟ ﮯﮐ ﻞﺋﺎﻓ ﭖﺍ ﮏﯿﺑ ﻡﺮﮐ ﮦﺍﺮﺑ", "select_sell_provider_notice": "۔ﮟﯿﮨ ﮯﺘﮑﺳ ﮌﻮﮭﭼ ﻮﮐ ﻦﯾﺮﮑﺳﺍ ﺱﺍ ﺮﮐ ﮮﺩ ﺐﯿﺗﺮﺗ ﻮﮐ ﮦﺪﻨﻨﮐ ﻢﮨﺍﺮﻓ ﻞﯿﺳ ﭧﻟﺎﻔﯾﮈ ﮯﻨﭘﺍ ﮟﯿﻣ ﺕﺎﺒ", + "select_your_country": "براہ کرم اپنے ملک کو منتخب کریں", "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 a95f32aaa..e3ffbef3e 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -627,6 +627,7 @@ "select_buy_provider_notice": "Chọn nhà cung cấp mua ở trên. Bạn có thể bỏ qua màn hình này bằng cách thiết lập nhà cung cấp mua mặc định trong cài đặt ứng dụng.", "select_destination": "Vui lòng chọn đích cho tệp sao lưu.", "select_sell_provider_notice": "Chọn nhà cung cấp bán ở trên. Bạn có thể bỏ qua màn hình này bằng cách thiết lập nhà cung cấp bán mặc định trong cài đặt ứng dụng.", + "select_your_country": "Vui lòng chọn quốc gia của bạn", "sell": "Bán", "sell_alert_content": "Hiện tại chúng tôi chỉ hỗ trợ bán Bitcoin, Ethereum và Litecoin. Vui lòng tạo hoặc chuyển sang ví Bitcoin, Ethereum hoặc Litecoin của bạn.", "sell_monero_com_alert_content": "Bán Monero chưa được hỗ trợ", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 1a1583da5..19dc9f110 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -635,6 +635,7 @@ "select_buy_provider_notice": "Yan olupese Ra loke. O le skii iboju yii nipa ṣiṣeto olupese rẹ ni awọn eto App.", "select_destination": "Jọwọ yan ibi ti o nlo fun faili afẹyinti.", "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.", + "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ẹ.", "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 79fe8faa5..009a31d86 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -634,6 +634,7 @@ "select_buy_provider_notice": "在上面选择买入提供商。您可以通过在应用程序设置中设置默认的购买提供商来跳过此屏幕。", "select_destination": "请选择备份文件的目的地。", "select_sell_provider_notice": "选择上面的销售提供商。您可以通过在应用程序设置中设置默认销售提供商来跳过此屏幕。", + "select_your_country": "请选择你的国家", "sell": "卖", "sell_alert_content": "我们目前仅支持比特币、以太坊和莱特币的销售。请创建或切换到您的比特币、以太坊或莱特币钱包。", "sell_monero_com_alert_content": "尚不支持出售门罗币", From 0fcfd76afd75b91665e25804859497aa46a051aa Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Thu, 7 Nov 2024 15:46:08 +0100 Subject: [PATCH 49/73] Automated Integration Tests Flows (#1686) * feat: Integration tests setup and tests for Disclaimer, Welcome and Setup Pin Code pages * feat: Integration test flow from start to restoring a wallet successfully done * test: Dashboard view test and linking to flow * feat: Testing the Exchange flow section, selecting sending and receiving currencies * test: Successfully create an exchange section * feat: Implement flow up to sending section * test: Complete Exchange flow * fix dependency issue * test: Final cleanups * feat: Add CI to run automated integration tests withan android emulator * feat: Adjust Automated integration test CI to run on ubuntu 20.04-a * fix: Move integration test CI into PR test build CI * ci: Add automated test ci which is a streamlined replica of pr test build ci * ci: Re-add step to access branch name * ci: Add KVM * ci: Add filepath to trigger the test run from * ci: Add required key * ci: Add required key * ci: Add missing secret key * ci: Add missing secret key * ci: Add nano secrets to workflow * ci: Switch step to free space on runner * ci: Remove timeout from workflow * ci: Confirm impact that removing copy_monero_deps would have on entire workflow time * ci: Update CI and temporarily remove cache related to emulator * ci: Remove dynamic java version * ci: Temporarily switch CI * ci: Switch to 11.x jdk * ci: Temporarily switch CI * ci: Revert ubuntu version * ci: Add more api levels * ci: Add more target options * ci: Settled on stable emulator matrix options * ci: Add more target options * ci: Modify flow * ci: Streamline api levels to 28 and 29 * ci: One more trial * ci: Switch to flutter drive * ci: Reduce options * ci: Remove haven from test * ci: Check for solana in list * ci: Adjust amounts and currencies for exchange flow * ci: Set write response on failure to true * ci: Split ci to funds and non funds related tests * test: Test for Send flow scenario and minor restructuring for test folders and files * chore: cleanup * ci: Pause CI for now * ci: Pause CI for now * ci: Pause CI for now * test: Restore wallets integration automated tests * Fix: Add keys back to currency amount textfield widget * fix: Switch variable name * fix: remove automation for now * tests: Automated tests for Create wallets flow * tests: Further optimize common flows * tests: Add missing await for call * tests: Confirm Seeds Display Properly WIP * tests: Confirm Seeds Display Correctly Automated Tests * fix: Add missing pubspec params for bitcoin and bitcoin_cash * feat: Automated Tests for Transaction History Flow * fix: Add missing pubspec parameter * feat: Automated Integration Tests for Transaction History flow * test: Updating send page robot and also syncing branch with main * test: Modifying tests to flow with wallet grouping implementation * fix: Issue with transaction history test * fix: Modifications to the PR and add automated confirmation for checking that all wallet types are restored or created correctly * test: Attempting automation for testing * fix: Issue from merge conflicts * test: Remove automation of test in this PR --------- Co-authored-by: OmarHatem --- cw_bitcoin/pubspec.yaml | 1 + cw_bitcoin_cash/pubspec.yaml | 1 + cw_core/pubspec.yaml | 1 + .../components/common_test_cases.dart | 121 ++++- .../components/common_test_constants.dart | 2 +- .../components/common_test_flows.dart | 291 +++++++++++- integration_test/funds_related_tests.dart | 9 +- .../robots/dashboard_menu_widget_robot.dart | 39 ++ .../robots/dashboard_page_robot.dart | 62 ++- .../robots/exchange_page_robot.dart | 4 +- .../robots/new_wallet_page_robot.dart | 35 ++ .../robots/pin_code_widget_robot.dart | 11 +- .../robots/pre_seed_page_robot.dart | 20 + .../restore_from_seed_or_key_robot.dart | 17 + .../robots/restore_options_page_robot.dart | 6 +- .../security_and_backup_page_robot.dart | 24 + integration_test/robots/send_page_robot.dart | 9 +- .../robots/transactions_page_robot.dart | 286 +++++++++++ .../wallet_group_description_page_robot.dart | 32 ++ .../robots/wallet_keys_robot.dart | 162 +++++++ .../robots/wallet_list_page_robot.dart | 27 ++ .../robots/wallet_seed_page_robot.dart | 57 +++ .../test_suites/confirm_seeds_flow_test.dart | 107 +++++ .../test_suites/create_wallet_flow_test.dart | 57 +++ .../test_suites/exchange_flow_test.dart | 61 +-- ...estore_wallet_through_seeds_flow_test.dart | 63 +++ .../test_suites/send_flow_test.dart | 37 +- .../transaction_history_flow_test.dart | 70 +++ ios/Podfile.lock | 12 +- lib/src/screens/InfoPage.dart | 13 +- lib/src/screens/dashboard/dashboard_page.dart | 2 +- .../dashboard/pages/nft_details_page.dart | 5 +- .../dashboard/pages/transactions_page.dart | 214 +++++---- .../widgets/anonpay_transaction_row.dart | 1 + .../dashboard/widgets/date_section_raw.dart | 41 +- .../screens/dashboard/widgets/header_row.dart | 3 +- .../dashboard/widgets/menu_widget.dart | 3 +- .../screens/dashboard/widgets/order_row.dart | 78 ++- .../screens/dashboard/widgets/trade_row.dart | 1 + .../dashboard/widgets/transaction_raw.dart | 103 ++-- .../screens/new_wallet/new_wallet_page.dart | 18 +- .../wallet_group_description_page.dart | 2 + .../screens/restore/restore_options_page.dart | 38 +- .../wallet_restore_from_seed_form.dart | 2 + lib/src/screens/seed/pre_seed_page.dart | 10 +- lib/src/screens/seed/wallet_seed_page.dart | 95 ++-- .../settings/security_backup_page.dart | 50 +- .../widgets/settings_cell_with_arrow.dart | 7 +- .../widgets/settings_picker_cell.dart | 24 +- .../widgets/settings_switcher_cell.dart | 3 +- .../setup_2fa/setup_2fa_info_page.dart | 8 +- .../blockexplorer_list_item.dart | 9 +- .../rbf_details_list_fee_picker_item.dart | 26 +- .../standart_list_item.dart | 7 +- .../textfield_list_item.dart | 10 +- .../transaction_details_list_item.dart | 7 +- .../transaction_details_page.dart | 60 +-- .../transaction_expandable_list_item.dart | 9 +- .../widgets/textfield_list_row.dart | 14 +- .../screens/wallet_keys/wallet_keys_page.dart | 36 +- .../screens/wallet_list/wallet_list_page.dart | 2 + lib/src/widgets/blockchain_height_widget.dart | 3 + lib/src/widgets/dashboard_card_widget.dart | 1 + lib/src/widgets/option_tile.dart | 18 +- lib/src/widgets/picker.dart | 3 + lib/src/widgets/seed_language_selector.dart | 11 +- lib/src/widgets/setting_actions.dart | 12 + lib/src/widgets/standard_list.dart | 8 +- .../anonpay/anonpay_transactions_store.dart | 6 +- lib/store/dashboard/orders_store.dart | 24 +- lib/store/dashboard/trades_store.dart | 19 +- lib/utils/date_formatter.dart | 26 +- lib/utils/image_utill.dart | 4 + .../dashboard/action_list_item.dart | 5 + .../anonpay_transaction_list_item.dart | 2 +- .../dashboard/dashboard_view_model.dart | 106 +++-- .../dashboard/date_section_item.dart | 2 +- .../dashboard/formatted_item_list.dart | 15 +- lib/view_model/dashboard/order_list_item.dart | 4 +- lib/view_model/dashboard/trade_list_item.dart | 6 +- .../dashboard/transaction_list_item.dart | 8 +- .../transaction_details_view_model.dart | 449 ++++++++++++++---- lib/view_model/wallet_keys_view_model.dart | 181 ++++--- tool/utils/secret_key.dart | 23 + 84 files changed, 2716 insertions(+), 745 deletions(-) create mode 100644 integration_test/robots/dashboard_menu_widget_robot.dart create mode 100644 integration_test/robots/new_wallet_page_robot.dart create mode 100644 integration_test/robots/pre_seed_page_robot.dart create mode 100644 integration_test/robots/security_and_backup_page_robot.dart create mode 100644 integration_test/robots/transactions_page_robot.dart create mode 100644 integration_test/robots/wallet_group_description_page_robot.dart create mode 100644 integration_test/robots/wallet_keys_robot.dart create mode 100644 integration_test/robots/wallet_list_page_robot.dart create mode 100644 integration_test/robots/wallet_seed_page_robot.dart create mode 100644 integration_test/test_suites/confirm_seeds_flow_test.dart create mode 100644 integration_test/test_suites/create_wallet_flow_test.dart create mode 100644 integration_test/test_suites/restore_wallet_through_seeds_flow_test.dart create mode 100644 integration_test/test_suites/transaction_history_flow_test.dart diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 7a1576a98..bff6104ac 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -73,6 +73,7 @@ dependency_overrides: # The following section is specific to Flutter. flutter: + uses-material-design: true # To add assets to your package, add an assets section, like this: # assets: diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index 14ff407e2..9a5c4f14f 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -49,6 +49,7 @@ dependency_overrides: # The following section is specific to Flutter packages. flutter: + uses-material-design: true # To add assets to your package, add an assets section, like this: # assets: diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml index 070779caa..6e32c2ba1 100644 --- a/cw_core/pubspec.yaml +++ b/cw_core/pubspec.yaml @@ -47,6 +47,7 @@ dependency_overrides: # The following section is specific to Flutter. flutter: + uses-material-design: true # To add assets to your package, add an assets section, like this: # assets: diff --git a/integration_test/components/common_test_cases.dart b/integration_test/components/common_test_cases.dart index 2e2991804..83bbb0449 100644 --- a/integration_test/components/common_test_cases.dart +++ b/integration_test/components/common_test_cases.dart @@ -10,10 +10,16 @@ class CommonTestCases { hasType(); } - Future tapItemByKey(String key, {bool shouldPumpAndSettle = true}) async { + Future tapItemByKey( + String key, { + bool shouldPumpAndSettle = true, + int pumpDuration = 100, + }) async { final widget = find.byKey(ValueKey(key)); await tester.tap(widget); - shouldPumpAndSettle ? await tester.pumpAndSettle() : await tester.pump(); + shouldPumpAndSettle + ? await tester.pumpAndSettle(Duration(milliseconds: pumpDuration)) + : await tester.pump(); } Future tapItemByFinder(Finder finder, {bool shouldPumpAndSettle = true}) async { @@ -31,6 +37,11 @@ class CommonTestCases { expect(typeWidget, findsOneWidget); } + bool isKeyPresent(String key) { + final typeWidget = find.byKey(ValueKey(key)); + return typeWidget.tryEvaluate(); + } + void hasValueKey(String key) { final typeWidget = find.byKey(ValueKey(key)); expect(typeWidget, findsOneWidget); @@ -53,33 +64,86 @@ class CommonTestCases { await tester.pumpAndSettle(); } - Future scrollUntilVisible(String childKey, String parentScrollableKey, - {double delta = 300}) async { - final scrollableWidget = find.descendant( - of: find.byKey(Key(parentScrollableKey)), + Future dragUntilVisible(String childKey, String parentKey) async { + await tester.pumpAndSettle(); + + final itemFinder = find.byKey(ValueKey(childKey)); + final listFinder = find.byKey(ValueKey(parentKey)); + + // Check if the widget is already in the widget tree + if (tester.any(itemFinder)) { + // Widget is already built and in the tree + tester.printToConsole('Child is already present'); + return; + } + + // We can adjust this as needed + final maxScrolls = 200; + + int scrolls = 0; + bool found = false; + + // We start by scrolling down + bool scrollDown = true; + + // Flag to check if we've already reversed direction + bool reversedDirection = false; + + // Find the Scrollable associated with the Parent Ad + final scrollableFinder = find.descendant( + of: listFinder, matching: find.byType(Scrollable), ); - final isAlreadyVisibile = isWidgetVisible(find.byKey(ValueKey(childKey))); - - if (isAlreadyVisibile) return; - - await tester.scrollUntilVisible( - find.byKey(ValueKey(childKey)), - delta, - scrollable: scrollableWidget, + // Ensure that the Scrollable is found + expect( + scrollableFinder, + findsOneWidget, + reason: 'Scrollable descendant of the Parent Widget not found.', ); - } - bool isWidgetVisible(Finder finder) { - try { - final Element element = finder.evaluate().single; - final RenderBox renderBox = element.renderObject as RenderBox; - return renderBox.paintBounds - .shift(renderBox.localToGlobal(Offset.zero)) - .overlaps(tester.binding.renderViews.first.paintBounds); - } catch (e) { - return false; + // Get the initial scroll position + final scrollableState = tester.state(scrollableFinder); + double previousScrollPosition = scrollableState.position.pixels; + + while (!found && scrolls < maxScrolls) { + tester.printToConsole('Scrolling ${scrollDown ? 'down' : 'up'}, attempt $scrolls'); + + // Perform the drag in the current direction + await tester.drag( + scrollableFinder, + scrollDown ? const Offset(0, -100) : const Offset(0, 100), + ); + await tester.pumpAndSettle(); + scrolls++; + + // Update the scroll position after the drag + final currentScrollPosition = scrollableState.position.pixels; + + if (currentScrollPosition == previousScrollPosition) { + // Cannot scroll further in this direction + if (reversedDirection) { + // We've already tried both directions + tester.printToConsole('Cannot scroll further in both directions. Widget not found.'); + break; + } else { + // Reverse the scroll direction + scrollDown = !scrollDown; + reversedDirection = true; + tester.printToConsole('Reached the end, reversing direction'); + } + } else { + // Continue scrolling in the current direction + previousScrollPosition = currentScrollPosition; + } + + // Check if the widget is now in the widget tree + found = tester.any(itemFinder); + } + + if (!found) { + tester.printToConsole('Widget not found after scrolling in both directions.'); + return; } } @@ -91,6 +155,15 @@ class CommonTestCases { await tester.pumpAndSettle(); } + void findWidgetViaDescendant({ + required FinderBase of, + required FinderBase matching, + }) { + final textWidget = find.descendant(of: of, matching: matching); + + expect(textWidget, findsOneWidget); + } + Future defaultSleepTime({int seconds = 2}) async => await Future.delayed(Duration(seconds: seconds)); } diff --git a/integration_test/components/common_test_constants.dart b/integration_test/components/common_test_constants.dart index d8381973e..302d52189 100644 --- a/integration_test/components/common_test_constants.dart +++ b/integration_test/components/common_test_constants.dart @@ -9,5 +9,5 @@ class CommonTestConstants { static final String testWalletName = 'Integrated Testing Wallet'; static final CryptoCurrency testReceiveCurrency = CryptoCurrency.sol; static final CryptoCurrency testDepositCurrency = CryptoCurrency.usdtSol; - static final String testWalletAddress = 'An2Y2fsUYKfYvN1zF89GAqR1e6GUMBg3qA83Y5ZWDf8L'; + static final String testWalletAddress = '5v9gTW1yWPffhnbNKuvtL2frevAf4HpBMw8oYnfqUjhm'; } diff --git a/integration_test/components/common_test_flows.dart b/integration_test/components/common_test_flows.dart index 807509de9..82f714da0 100644 --- a/integration_test/components/common_test_flows.dart +++ b/integration_test/components/common_test_flows.dart @@ -1,41 +1,65 @@ -import 'package:flutter/foundation.dart'; +import 'package:cake_wallet/entities/seed_type.dart'; +import 'package:cake_wallet/reactions/bip39_wallet_utils.dart'; +import 'package:cake_wallet/wallet_types.g.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/main.dart' as app; +import '../robots/dashboard_page_robot.dart'; import '../robots/disclaimer_page_robot.dart'; +import '../robots/new_wallet_page_robot.dart'; import '../robots/new_wallet_type_page_robot.dart'; +import '../robots/pre_seed_page_robot.dart'; import '../robots/restore_from_seed_or_key_robot.dart'; import '../robots/restore_options_page_robot.dart'; import '../robots/setup_pin_code_robot.dart'; +import '../robots/wallet_group_description_page_robot.dart'; +import '../robots/wallet_list_page_robot.dart'; +import '../robots/wallet_seed_page_robot.dart'; import '../robots/welcome_page_robot.dart'; import 'common_test_cases.dart'; +import 'package:cake_wallet/.secrets.g.dart' as secrets; + import 'common_test_constants.dart'; class CommonTestFlows { CommonTestFlows(this._tester) : _commonTestCases = CommonTestCases(_tester), _welcomePageRobot = WelcomePageRobot(_tester), + _preSeedPageRobot = PreSeedPageRobot(_tester), _setupPinCodeRobot = SetupPinCodeRobot(_tester), + _dashboardPageRobot = DashboardPageRobot(_tester), + _newWalletPageRobot = NewWalletPageRobot(_tester), _disclaimerPageRobot = DisclaimerPageRobot(_tester), + _walletSeedPageRobot = WalletSeedPageRobot(_tester), + _walletListPageRobot = WalletListPageRobot(_tester), _newWalletTypePageRobot = NewWalletTypePageRobot(_tester), _restoreOptionsPageRobot = RestoreOptionsPageRobot(_tester), - _restoreFromSeedOrKeysPageRobot = RestoreFromSeedOrKeysPageRobot(_tester); + _restoreFromSeedOrKeysPageRobot = RestoreFromSeedOrKeysPageRobot(_tester), + _walletGroupDescriptionPageRobot = WalletGroupDescriptionPageRobot(_tester); final WidgetTester _tester; final CommonTestCases _commonTestCases; final WelcomePageRobot _welcomePageRobot; + final PreSeedPageRobot _preSeedPageRobot; final SetupPinCodeRobot _setupPinCodeRobot; + final NewWalletPageRobot _newWalletPageRobot; + final DashboardPageRobot _dashboardPageRobot; final DisclaimerPageRobot _disclaimerPageRobot; + final WalletSeedPageRobot _walletSeedPageRobot; + final WalletListPageRobot _walletListPageRobot; final NewWalletTypePageRobot _newWalletTypePageRobot; final RestoreOptionsPageRobot _restoreOptionsPageRobot; final RestoreFromSeedOrKeysPageRobot _restoreFromSeedOrKeysPageRobot; + final WalletGroupDescriptionPageRobot _walletGroupDescriptionPageRobot; + //* ========== Handles flow to start the app afresh and accept disclaimer ============= Future startAppFlow(Key key) async { await app.main(topLevelKey: ValueKey('send_flow_test_app_key')); - + await _tester.pumpAndSettle(); // --------- Disclaimer Page ------------ @@ -46,56 +70,275 @@ class CommonTestFlows { await _disclaimerPageRobot.tapAcceptButton(); } - Future restoreWalletThroughSeedsFlow() async { - await _welcomeToRestoreFromSeedsPath(); - await _restoreFromSeeds(); + //* ========== Handles flow from welcome to creating a new wallet =============== + Future welcomePageToCreateNewWalletFlow( + WalletType walletTypeToCreate, + List walletPin, + ) async { + await _welcomeToCreateWalletPath(walletTypeToCreate, walletPin); + + await _generateNewWalletDetails(); + + await _confirmPreSeedInfo(); + + await _confirmWalletDetails(); } - Future restoreWalletThroughKeysFlow() async { - await _welcomeToRestoreFromSeedsPath(); + //* ========== Handles flow from welcome to restoring wallet from seeds =============== + Future welcomePageToRestoreWalletThroughSeedsFlow( + WalletType walletTypeToRestore, + String walletSeed, + List walletPin, + ) async { + await _welcomeToRestoreFromSeedsOrKeysPath(walletTypeToRestore, walletPin); + await _restoreFromSeeds(walletTypeToRestore, walletSeed); + } + + //* ========== Handles flow from welcome to restoring wallet from keys =============== + Future welcomePageToRestoreWalletThroughKeysFlow( + WalletType walletTypeToRestore, + List walletPin, + ) async { + await _welcomeToRestoreFromSeedsOrKeysPath(walletTypeToRestore, walletPin); await _restoreFromKeys(); } - Future _welcomeToRestoreFromSeedsPath() async { - // --------- Welcome Page --------------- - await _welcomePageRobot.navigateToRestoreWalletPage(); + //* ========== Handles switching to wallet list or menu from dashboard =============== + Future switchToWalletMenuFromDashboardPage() async { + _tester.printToConsole('Switching to Wallet Menu'); + await _dashboardPageRobot.openDrawerMenu(); - // ----------- Restore Options Page ----------- - // Route to restore from seeds page to continue flow - await _restoreOptionsPageRobot.navigateToRestoreFromSeedsPage(); + await _dashboardPageRobot.dashboardMenuWidgetRobot.navigateToWalletMenu(); + } + void confirmAllAvailableWalletTypeIconsDisplayCorrectly() { + for (var walletType in availableWalletTypes) { + final imageUrl = walletTypeToCryptoCurrency(walletType).iconPath; + + final walletIconFinder = find.image( + Image.asset( + imageUrl!, + width: 32, + height: 32, + ).image, + ); + + expect(walletIconFinder, findsAny); + } + } + + //* ========== Handles creating new wallet flow from wallet list/menu =============== + Future createNewWalletFromWalletMenu(WalletType walletTypeToCreate) async { + _tester.printToConsole('Creating ${walletTypeToCreate.name} Wallet'); + await _walletListPageRobot.navigateToCreateNewWalletPage(); + await _commonTestCases.defaultSleepTime(); + + await _selectWalletTypeForWallet(walletTypeToCreate); + await _commonTestCases.defaultSleepTime(); + + // ---- Wallet Group/New Seed Implementation Comes here + await _walletGroupDescriptionPageFlow(true, walletTypeToCreate); + + await _generateNewWalletDetails(); + + await _confirmPreSeedInfo(); + + await _confirmWalletDetails(); + await _commonTestCases.defaultSleepTime(); + } + + Future _walletGroupDescriptionPageFlow(bool isNewSeed, WalletType walletType) async { + if (!isBIP39Wallet(walletType)) return; + + await _walletGroupDescriptionPageRobot.isWalletGroupDescriptionPage(); + + if (isNewSeed) { + await _walletGroupDescriptionPageRobot.navigateToCreateNewSeedPage(); + } else { + await _walletGroupDescriptionPageRobot.navigateToChooseWalletGroup(); + } + } + + //* ========== Handles restore wallet flow from wallet list/menu =============== + Future restoreWalletFromWalletMenu(WalletType walletType, String walletSeed) async { + _tester.printToConsole('Restoring ${walletType.name} Wallet'); + await _walletListPageRobot.navigateToRestoreWalletOptionsPage(); + await _commonTestCases.defaultSleepTime(); + + await _restoreOptionsPageRobot.navigateToRestoreFromSeedsOrKeysPage(); + await _commonTestCases.defaultSleepTime(); + + await _selectWalletTypeForWallet(walletType); + await _commonTestCases.defaultSleepTime(); + + await _restoreFromSeeds(walletType, walletSeed); + await _commonTestCases.defaultSleepTime(); + } + + //* ========== Handles setting up pin code for wallet on first install =============== + Future setupPinCodeForWallet(List pin) async { // ----------- SetupPinCode Page ------------- // Confirm initial defaults - Widgets to be displayed etc await _setupPinCodeRobot.isSetupPinCodePage(); - await _setupPinCodeRobot.enterPinCode(CommonTestConstants.pin, true); - await _setupPinCodeRobot.enterPinCode(CommonTestConstants.pin, false); + await _setupPinCodeRobot.enterPinCode(pin); + await _setupPinCodeRobot.enterPinCode(pin); await _setupPinCodeRobot.tapSuccessButton(); + } + Future _welcomeToCreateWalletPath( + WalletType walletTypeToCreate, + List pin, + ) async { + await _welcomePageRobot.navigateToCreateNewWalletPage(); + + await setupPinCodeForWallet(pin); + + await _selectWalletTypeForWallet(walletTypeToCreate); + } + + Future _welcomeToRestoreFromSeedsOrKeysPath( + WalletType walletTypeToRestore, + List pin, + ) async { + await _welcomePageRobot.navigateToRestoreWalletPage(); + + await _restoreOptionsPageRobot.navigateToRestoreFromSeedsOrKeysPage(); + + await setupPinCodeForWallet(pin); + + await _selectWalletTypeForWallet(walletTypeToRestore); + } + + //* ============ Handles New Wallet Type Page ================== + Future _selectWalletTypeForWallet(WalletType type) async { // ----------- NewWalletType Page ------------- // Confirm scroll behaviour works properly - await _newWalletTypePageRobot - .findParticularWalletTypeInScrollableList(CommonTestConstants.testWalletType); + await _newWalletTypePageRobot.findParticularWalletTypeInScrollableList(type); // Select a wallet and route to next page - await _newWalletTypePageRobot.selectWalletType(CommonTestConstants.testWalletType); + await _newWalletTypePageRobot.selectWalletType(type); await _newWalletTypePageRobot.onNextButtonPressed(); } - Future _restoreFromSeeds() async { + //* ============ Handles New Wallet Page ================== + Future _generateNewWalletDetails() async { + await _newWalletPageRobot.isNewWalletPage(); + + await _newWalletPageRobot.generateWalletName(); + + await _newWalletPageRobot.onNextButtonPressed(); + } + + //* ============ Handles Pre Seed Page ===================== + Future _confirmPreSeedInfo() async { + await _preSeedPageRobot.isPreSeedPage(); + + await _preSeedPageRobot.onConfirmButtonPressed(); + } + + //* ============ Handles Wallet Seed Page ================== + Future _confirmWalletDetails() async { + await _walletSeedPageRobot.isWalletSeedPage(); + + _walletSeedPageRobot.confirmWalletDetailsDisplayCorrectly(); + + _walletSeedPageRobot.confirmWalletSeedReminderDisplays(); + + await _walletSeedPageRobot.onCopySeedsButtonPressed(); + + await _walletSeedPageRobot.onNextButtonPressed(); + + await _walletSeedPageRobot.onConfirmButtonOnSeedAlertDialogPressed(); + } + + //* Main Restore Actions - On the RestoreFromSeed/Keys Page - Restore from Seeds Action + Future _restoreFromSeeds(WalletType type, String walletSeed) async { // ----------- RestoreFromSeedOrKeys Page ------------- - await _restoreFromSeedOrKeysPageRobot.enterWalletNameText(CommonTestConstants.testWalletName); - await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore(secrets.solanaTestWalletSeeds); + + await _restoreFromSeedOrKeysPageRobot.selectWalletNameFromAvailableOptions(); + await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore(walletSeed); + + final numberOfWords = walletSeed.split(' ').length; + + if (numberOfWords == 25 && (type == WalletType.monero)) { + await _restoreFromSeedOrKeysPageRobot + .chooseSeedTypeForMoneroOrWowneroWallets(MoneroSeedType.legacy); + + // Using a constant value of 2831400 for the blockheight as its the restore blockheight for our testing wallet + await _restoreFromSeedOrKeysPageRobot + .enterBlockHeightForWalletRestore(secrets.moneroTestWalletBlockHeight); + } + await _restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonPressed(); } + //* Main Restore Actions - On the RestoreFromSeed/Keys Page - Restore from Keys Action Future _restoreFromKeys() async { await _commonTestCases.swipePage(); await _commonTestCases.defaultSleepTime(); - await _restoreFromSeedOrKeysPageRobot.enterWalletNameText(CommonTestConstants.testWalletName); + await _restoreFromSeedOrKeysPageRobot.selectWalletNameFromAvailableOptions( + isSeedFormEntry: false, + ); await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore(''); await _restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonPressed(); } + + //* ====== Utility Function to get test wallet seeds for each wallet type ======== + String getWalletSeedsByWalletType(WalletType walletType) { + switch (walletType) { + case WalletType.monero: + return secrets.moneroTestWalletSeeds; + case WalletType.bitcoin: + return secrets.bitcoinTestWalletSeeds; + case WalletType.ethereum: + return secrets.ethereumTestWalletSeeds; + case WalletType.litecoin: + return secrets.litecoinTestWalletSeeds; + case WalletType.bitcoinCash: + return secrets.bitcoinCashTestWalletSeeds; + case WalletType.polygon: + return secrets.polygonTestWalletSeeds; + case WalletType.solana: + return secrets.solanaTestWalletSeeds; + case WalletType.tron: + return secrets.tronTestWalletSeeds; + case WalletType.nano: + return secrets.nanoTestWalletSeeds; + case WalletType.wownero: + return secrets.wowneroTestWalletSeeds; + default: + return ''; + } + } + + //* ====== Utility Function to get test receive address for each wallet type ======== + String getReceiveAddressByWalletType(WalletType walletType) { + switch (walletType) { + case WalletType.monero: + return secrets.moneroTestWalletReceiveAddress; + case WalletType.bitcoin: + return secrets.bitcoinTestWalletReceiveAddress; + case WalletType.ethereum: + return secrets.ethereumTestWalletReceiveAddress; + case WalletType.litecoin: + return secrets.litecoinTestWalletReceiveAddress; + case WalletType.bitcoinCash: + return secrets.bitcoinCashTestWalletReceiveAddress; + case WalletType.polygon: + return secrets.polygonTestWalletReceiveAddress; + case WalletType.solana: + return secrets.solanaTestWalletReceiveAddress; + case WalletType.tron: + return secrets.tronTestWalletReceiveAddress; + case WalletType.nano: + return secrets.nanoTestWalletReceiveAddress; + case WalletType.wownero: + return secrets.wowneroTestWalletReceiveAddress; + default: + return ''; + } + } } diff --git a/integration_test/funds_related_tests.dart b/integration_test/funds_related_tests.dart index 9d97d47f8..db24fbc0b 100644 --- a/integration_test/funds_related_tests.dart +++ b/integration_test/funds_related_tests.dart @@ -9,6 +9,7 @@ import 'robots/dashboard_page_robot.dart'; import 'robots/exchange_confirm_page_robot.dart'; import 'robots/exchange_page_robot.dart'; import 'robots/exchange_trade_page_robot.dart'; +import 'package:cake_wallet/.secrets.g.dart' as secrets; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -32,7 +33,11 @@ void main() { await commonTestFlows.startAppFlow(ValueKey('funds_exchange_test_app_key')); - await commonTestFlows.restoreWalletThroughSeedsFlow(); + await commonTestFlows.welcomePageToRestoreWalletThroughSeedsFlow( + CommonTestConstants.testWalletType, + secrets.solanaTestWalletSeeds, + CommonTestConstants.pin, + ); // ----------- RestoreFromSeedOrKeys Page ------------- await dashboardPageRobot.navigateToExchangePage(); @@ -59,7 +64,7 @@ void main() { final onAuthPage = authPageRobot.onAuthPage(); if (onAuthPage) { - await authPageRobot.enterPinCode(CommonTestConstants.pin, false); + await authPageRobot.enterPinCode(CommonTestConstants.pin); } // ----------- Exchange Confirm Page ------------- diff --git a/integration_test/robots/dashboard_menu_widget_robot.dart b/integration_test/robots/dashboard_menu_widget_robot.dart new file mode 100644 index 000000000..f48033dda --- /dev/null +++ b/integration_test/robots/dashboard_menu_widget_robot.dart @@ -0,0 +1,39 @@ +import 'package:cake_wallet/src/screens/dashboard/widgets/menu_widget.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class DashboardMenuWidgetRobot { + DashboardMenuWidgetRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future hasMenuWidget() async { + commonTestCases.hasType(); + } + + void displaysTheCorrectWalletNameAndSubName() { + final menuWidgetState = tester.state(find.byType(MenuWidget)); + + final walletName = menuWidgetState.widget.dashboardViewModel.name; + commonTestCases.hasText(walletName); + + final walletSubName = menuWidgetState.widget.dashboardViewModel.subname; + if (walletSubName.isNotEmpty) { + commonTestCases.hasText(walletSubName); + } + } + + Future navigateToWalletMenu() async { + await commonTestCases.tapItemByKey('dashboard_page_menu_widget_wallet_menu_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future navigateToSecurityAndBackupPage() async { + await commonTestCases.tapItemByKey( + 'dashboard_page_menu_widget_security_and_backup_button_key', + ); + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/robots/dashboard_page_robot.dart b/integration_test/robots/dashboard_page_robot.dart index fc917c3b2..bc5f411ad 100644 --- a/integration_test/robots/dashboard_page_robot.dart +++ b/integration_test/robots/dashboard_page_robot.dart @@ -1,20 +1,44 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/dashboard/dashboard_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/balance_page.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter_test/flutter_test.dart'; import '../components/common_test_cases.dart'; +import 'dashboard_menu_widget_robot.dart'; class DashboardPageRobot { - DashboardPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + DashboardPageRobot(this.tester) + : commonTestCases = CommonTestCases(tester), + dashboardMenuWidgetRobot = DashboardMenuWidgetRobot(tester); final WidgetTester tester; + final DashboardMenuWidgetRobot dashboardMenuWidgetRobot; late CommonTestCases commonTestCases; Future isDashboardPage() async { await commonTestCases.isSpecificPage(); } + Future confirmWalletTypeIsDisplayedCorrectly( + WalletType type, { + bool isHaven = false, + }) async { + final cryptoBalanceWidget = + tester.widget(find.byType(CryptoBalanceWidget)); + final hasAccounts = cryptoBalanceWidget.dashboardViewModel.balanceViewModel.hasAccounts; + + if (hasAccounts) { + final walletName = cryptoBalanceWidget.dashboardViewModel.name; + commonTestCases.hasText(walletName); + } else { + final walletName = walletTypeToString(type); + final assetName = isHaven ? '$walletName Assets' : walletName; + commonTestCases.hasText(assetName); + } + await commonTestCases.defaultSleepTime(seconds: 5); + } + void confirmServiceUpdateButtonDisplays() { commonTestCases.hasValueKey('dashboard_page_services_update_button_key'); } @@ -27,30 +51,40 @@ class DashboardPageRobot { commonTestCases.hasValueKey('dashboard_page_wallet_menu_button_key'); } - Future confirmRightCryptoAssetTitleDisplaysPerPageView(WalletType type, - {bool isHaven = false}) async { + Future confirmRightCryptoAssetTitleDisplaysPerPageView( + WalletType type, { + bool isHaven = false, + }) async { //Balance Page - final walletName = walletTypeToString(type); - final assetName = isHaven ? '$walletName Assets' : walletName; - commonTestCases.hasText(assetName); + await confirmWalletTypeIsDisplayedCorrectly(type, isHaven: isHaven); // Swipe to Cake features Page - await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key', swipeRight: false); - await commonTestCases.defaultSleepTime(); + await swipeDashboardTab(false); commonTestCases.hasText('Cake ${S.current.features}'); // Swipe back to balance - await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key'); - await commonTestCases.defaultSleepTime(); + await swipeDashboardTab(true); // Swipe to Transactions Page - await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key'); - await commonTestCases.defaultSleepTime(); + await swipeDashboardTab(true); commonTestCases.hasText(S.current.transactions); // Swipe back to balance - await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key', swipeRight: false); - await commonTestCases.defaultSleepTime(seconds: 5); + await swipeDashboardTab(false); + await commonTestCases.defaultSleepTime(seconds: 3); + } + + Future swipeDashboardTab(bool swipeRight) async { + await commonTestCases.swipeByPageKey( + key: 'dashboard_page_view_key', + swipeRight: swipeRight, + ); + await commonTestCases.defaultSleepTime(); + } + + Future openDrawerMenu() async { + await commonTestCases.tapItemByKey('dashboard_page_wallet_menu_button_key'); + await commonTestCases.defaultSleepTime(); } Future navigateToBuyPage() async { diff --git a/integration_test/robots/exchange_page_robot.dart b/integration_test/robots/exchange_page_robot.dart index b439e4791..e01b2df9c 100644 --- a/integration_test/robots/exchange_page_robot.dart +++ b/integration_test/robots/exchange_page_robot.dart @@ -123,7 +123,7 @@ class ExchangePageRobot { return; } - await commonTestCases.scrollUntilVisible( + await commonTestCases.dragUntilVisible( 'picker_items_index_${depositCurrency.name}_button_key', 'picker_scrollbar_key', ); @@ -149,7 +149,7 @@ class ExchangePageRobot { return; } - await commonTestCases.scrollUntilVisible( + await commonTestCases.dragUntilVisible( 'picker_items_index_${receiveCurrency.name}_button_key', 'picker_scrollbar_key', ); diff --git a/integration_test/robots/new_wallet_page_robot.dart b/integration_test/robots/new_wallet_page_robot.dart new file mode 100644 index 000000000..f8deb00ae --- /dev/null +++ b/integration_test/robots/new_wallet_page_robot.dart @@ -0,0 +1,35 @@ +import 'package:cake_wallet/src/screens/new_wallet/new_wallet_page.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class NewWalletPageRobot { + NewWalletPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isNewWalletPage() async { + await commonTestCases.isSpecificPage(); + } + + Future enterWalletName(String walletName) async { + await commonTestCases.enterText( + walletName, + 'new_wallet_page_wallet_name_textformfield_key', + ); + await commonTestCases.defaultSleepTime(); + } + + Future generateWalletName() async { + await commonTestCases.tapItemByKey( + 'new_wallet_page_wallet_name_textformfield_generate_name_button_key', + ); + await commonTestCases.defaultSleepTime(); + } + + Future onNextButtonPressed() async { + await commonTestCases.tapItemByKey('new_wallet_page_confirm_button_key'); + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/robots/pin_code_widget_robot.dart b/integration_test/robots/pin_code_widget_robot.dart index b6805e9e0..18dc5fba4 100644 --- a/integration_test/robots/pin_code_widget_robot.dart +++ b/integration_test/robots/pin_code_widget_robot.dart @@ -24,13 +24,12 @@ class PinCodeWidgetRobot { commonTestCases.hasValueKey('pin_code_button_0_key'); } - Future pushPinButton(int index) async { - await commonTestCases.tapItemByKey('pin_code_button_${index}_key'); - } - - Future enterPinCode(List pinCode, bool isFirstEntry) async { + Future enterPinCode(List pinCode, {int pumpDuration = 100}) async { for (int pin in pinCode) { - await pushPinButton(pin); + await commonTestCases.tapItemByKey( + 'pin_code_button_${pin}_key', + pumpDuration: pumpDuration, + ); } await commonTestCases.defaultSleepTime(); diff --git a/integration_test/robots/pre_seed_page_robot.dart b/integration_test/robots/pre_seed_page_robot.dart new file mode 100644 index 000000000..01be1249c --- /dev/null +++ b/integration_test/robots/pre_seed_page_robot.dart @@ -0,0 +1,20 @@ +import 'package:cake_wallet/src/screens/seed/pre_seed_page.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class PreSeedPageRobot { + PreSeedPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isPreSeedPage() async { + await commonTestCases.isSpecificPage(); + } + + Future onConfirmButtonPressed() async { + await commonTestCases.tapItemByKey('pre_seed_page_button_key'); + await commonTestCases.defaultSleepTime(); + } +} 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 43a65095d..9d973061b 100644 --- a/integration_test/robots/restore_from_seed_or_key_robot.dart +++ b/integration_test/robots/restore_from_seed_or_key_robot.dart @@ -1,3 +1,4 @@ +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/validable_annotated_editable_text.dart'; @@ -70,6 +71,22 @@ class RestoreFromSeedOrKeysPageRobot { await tester.pumpAndSettle(); } + Future enterBlockHeightForWalletRestore(String blockHeight) async { + await commonTestCases.enterText( + blockHeight, + 'wallet_restore_from_seed_blockheight_textfield_key', + ); + await tester.pumpAndSettle(); + } + + Future chooseSeedTypeForMoneroOrWowneroWallets(MoneroSeedType selectedType) async { + await commonTestCases.tapItemByKey('wallet_restore_from_seed_seedtype_picker_button_key'); + + await commonTestCases.defaultSleepTime(); + + await commonTestCases.tapItemByKey('picker_items_index_${selectedType.title}_button_key'); + } + Future onPasteSeedPhraseButtonPressed() async { await commonTestCases.tapItemByKey('wallet_restore_from_seed_wallet_seeds_paste_button_key'); } diff --git a/integration_test/robots/restore_options_page_robot.dart b/integration_test/robots/restore_options_page_robot.dart index b3cefc90c..cd1919609 100644 --- a/integration_test/robots/restore_options_page_robot.dart +++ b/integration_test/robots/restore_options_page_robot.dart @@ -14,14 +14,14 @@ class RestoreOptionsPageRobot { } void hasRestoreOptionsButton() { - commonTestCases.hasValueKey('restore_options_from_seeds_button_key'); + commonTestCases.hasValueKey('restore_options_from_seeds_or_keys_button_key'); commonTestCases.hasValueKey('restore_options_from_backup_button_key'); commonTestCases.hasValueKey('restore_options_from_hardware_wallet_button_key'); commonTestCases.hasValueKey('restore_options_from_qr_button_key'); } - Future navigateToRestoreFromSeedsPage() async { - await commonTestCases.tapItemByKey('restore_options_from_seeds_button_key'); + Future navigateToRestoreFromSeedsOrKeysPage() async { + await commonTestCases.tapItemByKey('restore_options_from_seeds_or_keys_button_key'); await commonTestCases.defaultSleepTime(); } diff --git a/integration_test/robots/security_and_backup_page_robot.dart b/integration_test/robots/security_and_backup_page_robot.dart new file mode 100644 index 000000000..eb7c1bc87 --- /dev/null +++ b/integration_test/robots/security_and_backup_page_robot.dart @@ -0,0 +1,24 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class SecurityAndBackupPageRobot { + SecurityAndBackupPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + final CommonTestCases commonTestCases; + + Future isSecurityAndBackupPage() async { + await commonTestCases.isSpecificPage(); + } + + void hasTitle() { + commonTestCases.hasText(S.current.security_and_backup); + } + + Future navigateToShowKeysPage() async { + await commonTestCases.tapItemByKey('security_backup_page_show_keys_button_key'); + } +} diff --git a/integration_test/robots/send_page_robot.dart b/integration_test/robots/send_page_robot.dart index 971556620..20cef948d 100644 --- a/integration_test/robots/send_page_robot.dart +++ b/integration_test/robots/send_page_robot.dart @@ -84,7 +84,7 @@ class SendPageRobot { return; } - await commonTestCases.scrollUntilVisible( + await commonTestCases.dragUntilVisible( 'picker_items_index_${receiveCurrency.name}_button_key', 'picker_scrollbar_key', ); @@ -117,7 +117,7 @@ class SendPageRobot { return; } - await commonTestCases.scrollUntilVisible( + await commonTestCases.dragUntilVisible( 'picker_items_index_${priority.title}_button_key', 'picker_scrollbar_key', ); @@ -198,7 +198,7 @@ class SendPageRobot { tester.printToConsole('Starting inner _handleAuth loop checks'); try { - await authPageRobot.enterPinCode(CommonTestConstants.pin, false); + await authPageRobot.enterPinCode(CommonTestConstants.pin, pumpDuration: 500); tester.printToConsole('Auth done'); await tester.pump(); @@ -213,6 +213,7 @@ class SendPageRobot { } Future handleSendResult() async { + await tester.pump(); tester.printToConsole('Inside handle function'); bool hasError = false; @@ -287,6 +288,8 @@ class SendPageRobot { // Loop to wait for the operation to commit transaction await _waitForCommitTransactionCompletion(); + await tester.pump(); + await commonTestCases.defaultSleepTime(seconds: 4); } else { await commonTestCases.defaultSleepTime(); diff --git a/integration_test/robots/transactions_page_robot.dart b/integration_test/robots/transactions_page_robot.dart new file mode 100644 index 000000000..40a49928f --- /dev/null +++ b/integration_test/robots/transactions_page_robot.dart @@ -0,0 +1,286 @@ +import 'dart:async'; + +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart'; +import 'package:cake_wallet/utils/date_formatter.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/dashboard_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/date_section_item.dart'; +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:cw_core/crypto_currency.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:intl/intl.dart'; + +import '../components/common_test_cases.dart'; + +class TransactionsPageRobot { + TransactionsPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isTransactionsPage() async { + await commonTestCases.isSpecificPage(); + } + + Future confirmTransactionsPageConstantsDisplayProperly() async { + await commonTestCases.defaultSleepTime(); + + final transactionsPage = tester.widget(find.byType(TransactionsPage)); + final dashboardViewModel = transactionsPage.dashboardViewModel; + if (dashboardViewModel.status is SyncingSyncStatus) { + commonTestCases.hasValueKey('transactions_page_syncing_alert_card_key'); + commonTestCases.hasText(S.current.syncing_wallet_alert_title); + commonTestCases.hasText(S.current.syncing_wallet_alert_content); + } + + commonTestCases.hasValueKey('transactions_page_header_row_key'); + commonTestCases.hasText(S.current.transactions); + commonTestCases.hasValueKey('transactions_page_header_row_transaction_filter_button_key'); + } + + Future confirmTransactionHistoryListDisplaysCorrectly(bool hasTxHistoryWhileSyncing) async { + // Retrieve the TransactionsPage widget and its DashboardViewModel + final transactionsPage = tester.widget(find.byType(TransactionsPage)); + final dashboardViewModel = transactionsPage.dashboardViewModel; + + // Define a timeout to prevent infinite loops + // Putting at one hour for cases like monero that takes time to sync + final timeout = Duration(hours: 1); + final pollingInterval = Duration(seconds: 2); + final endTime = DateTime.now().add(timeout); + + while (DateTime.now().isBefore(endTime)) { + final isSynced = dashboardViewModel.status is SyncedSyncStatus; + final itemsLoaded = dashboardViewModel.items.isNotEmpty; + + // Perform item checks if items are loaded + if (itemsLoaded) { + await _performItemChecks(dashboardViewModel); + } else { + // Verify placeholder when items are not loaded + _verifyPlaceholder(); + } + + // Determine if we should exit the loop + if (_shouldExitLoop(hasTxHistoryWhileSyncing, isSynced, itemsLoaded)) { + break; + } + + // Pump the UI and wait for the next polling interval + await tester.pump(pollingInterval); + } + + // After the loop, verify that both status is synced and items are loaded + if (!_isFinalStateValid(dashboardViewModel)) { + throw TimeoutException('Dashboard did not sync and load items within the allotted time.'); + } + } + + bool _shouldExitLoop(bool hasTxHistoryWhileSyncing, bool isSynced, bool itemsLoaded) { + if (hasTxHistoryWhileSyncing) { + // When hasTxHistoryWhileSyncing is true, exit when status is synced + return isSynced; + } else { + // When hasTxHistoryWhileSyncing is false, exit when status is synced and items are loaded + return isSynced && itemsLoaded; + } + } + + void _verifyPlaceholder() { + commonTestCases.hasValueKey('transactions_page_placeholder_transactions_text_key'); + commonTestCases.hasText(S.current.placeholder_transactions); + } + + bool _isFinalStateValid(DashboardViewModel dashboardViewModel) { + final isSynced = dashboardViewModel.status is SyncedSyncStatus; + final itemsLoaded = dashboardViewModel.items.isNotEmpty; + return isSynced && itemsLoaded; + } + + Future _performItemChecks(DashboardViewModel dashboardViewModel) async { + List items = dashboardViewModel.items; + for (var item in items) { + final keyId = (item.key as ValueKey).value; + tester.printToConsole('\n'); + tester.printToConsole(keyId); + + await commonTestCases.dragUntilVisible(keyId, 'transactions_page_list_view_builder_key'); + await tester.pump(); + + final isWidgetVisible = tester.any(find.byKey(ValueKey(keyId))); + if (!isWidgetVisible) { + tester.printToConsole('Moving to next visible item on list'); + continue; + } + ; + await tester.pump(); + + if (item is DateSectionItem) { + await _verifyDateSectionItem(item); + } else if (item is TransactionListItem) { + tester.printToConsole(item.formattedTitle); + tester.printToConsole(item.formattedFiatAmount); + tester.printToConsole('\n'); + await _verifyTransactionListItemDisplay(item, dashboardViewModel); + } else if (item is AnonpayTransactionListItem) { + await _verifyAnonpayTransactionListItemDisplay(item); + } else if (item is TradeListItem) { + await _verifyTradeListItemDisplay(item); + } else if (item is OrderListItem) { + await _verifyOrderListItemDisplay(item); + } + } + } + + Future _verifyDateSectionItem(DateSectionItem item) async { + final title = DateFormatter.convertDateTimeToReadableString(item.date); + tester.printToConsole(title); + await tester.pump(); + + commonTestCases.findWidgetViaDescendant( + of: find.byKey(item.key), + matching: find.text(title), + ); + } + + Future _verifyTransactionListItemDisplay( + TransactionListItem item, + DashboardViewModel dashboardViewModel, + ) async { + final keyId = + '${dashboardViewModel.type.name}_transaction_history_item_${item.transaction.id}_key'; + + if (item.hasTokens && item.assetOfTransaction == null) return; + + //* ==============Confirm it has the right key for this item ======== + commonTestCases.hasValueKey(keyId); + + //* ======Confirm it displays the properly formatted amount========== + commonTestCases.findWidgetViaDescendant( + of: find.byKey(ValueKey(keyId)), + matching: find.text(item.formattedCryptoAmount), + ); + + //* ======Confirm it displays the properly formatted title=========== + final transactionType = dashboardViewModel.getTransactionType(item.transaction); + + final title = item.formattedTitle + item.formattedStatus + transactionType; + + commonTestCases.findWidgetViaDescendant( + of: find.byKey(ValueKey(keyId)), + matching: find.text(title), + ); + + //* ======Confirm it displays the properly formatted date============ + final formattedDate = DateFormat('HH:mm').format(item.transaction.date); + commonTestCases.findWidgetViaDescendant( + of: find.byKey(ValueKey(keyId)), + matching: find.text(formattedDate), + ); + + //* ======Confirm it displays the properly formatted fiat amount===== + final formattedFiatAmount = + dashboardViewModel.balanceViewModel.isFiatDisabled ? '' : item.formattedFiatAmount; + if (formattedFiatAmount.isNotEmpty) { + commonTestCases.findWidgetViaDescendant( + of: find.byKey(ValueKey(keyId)), + matching: find.text(formattedFiatAmount), + ); + } + + //* ======Confirm it displays the right image based on the transaction direction===== + final imageToUse = item.transaction.direction == TransactionDirection.incoming + ? 'assets/images/down_arrow.png' + : 'assets/images/up_arrow.png'; + + find.widgetWithImage(Container, AssetImage(imageToUse)); + } + + Future _verifyAnonpayTransactionListItemDisplay(AnonpayTransactionListItem item) async { + final keyId = 'anonpay_invoice_transaction_list_item_${item.transaction.invoiceId}_key'; + + //* ==============Confirm it has the right key for this item ======== + commonTestCases.hasValueKey(keyId); + + //* ==============Confirm it displays the correct provider ========================= + commonTestCases.hasText(item.transaction.provider); + + //* ===========Confirm it displays the properly formatted amount with currency ======== + final currency = item.transaction.fiatAmount != null + ? item.transaction.fiatEquiv ?? '' + : CryptoCurrency.fromFullName(item.transaction.coinTo).name.toUpperCase(); + + final amount = + item.transaction.fiatAmount?.toString() ?? (item.transaction.amountTo?.toString() ?? ''); + + final amountCurrencyText = amount + ' ' + currency; + + commonTestCases.hasText(amountCurrencyText); + + //* ======Confirm it displays the properly formatted date================= + final formattedDate = DateFormat('HH:mm').format(item.transaction.createdAt); + commonTestCases.hasText(formattedDate); + + //* ===============Confirm it displays the right image==================== + find.widgetWithImage(ClipRRect, AssetImage('assets/images/trocador.png')); + } + + Future _verifyTradeListItemDisplay(TradeListItem item) async { + final keyId = 'trade_list_item_${item.trade.id}_key'; + + //* ==============Confirm it has the right key for this item ======== + commonTestCases.hasValueKey(keyId); + + //* ==============Confirm it displays the correct provider ========================= + final conversionFlow = '${item.trade.from.toString()} → ${item.trade.to.toString()}'; + + commonTestCases.hasText(conversionFlow); + + //* ===========Confirm it displays the properly formatted amount with its crypto tag ======== + + final amountCryptoText = item.tradeFormattedAmount + ' ' + item.trade.from.toString(); + + commonTestCases.hasText(amountCryptoText); + + //* ======Confirm it displays the properly formatted date================= + final createdAtFormattedDate = + item.trade.createdAt != null ? DateFormat('HH:mm').format(item.trade.createdAt!) : null; + + if (createdAtFormattedDate != null) { + commonTestCases.hasText(createdAtFormattedDate); + } + + //* ===============Confirm it displays the right image==================== + commonTestCases.hasValueKey(item.trade.provider.image); + } + + Future _verifyOrderListItemDisplay(OrderListItem item) async { + final keyId = 'order_list_item_${item.order.id}_key'; + + //* ==============Confirm it has the right key for this item ======== + commonTestCases.hasValueKey(keyId); + + //* ==============Confirm it displays the correct provider ========================= + final orderFlow = '${item.order.from!} → ${item.order.to}'; + + commonTestCases.hasText(orderFlow); + + //* ===========Confirm it displays the properly formatted amount with its crypto tag ======== + + final amountCryptoText = item.orderFormattedAmount + ' ' + item.order.to!; + + commonTestCases.hasText(amountCryptoText); + + //* ======Confirm it displays the properly formatted date================= + final createdAtFormattedDate = DateFormat('HH:mm').format(item.order.createdAt); + + commonTestCases.hasText(createdAtFormattedDate); + } +} diff --git a/integration_test/robots/wallet_group_description_page_robot.dart b/integration_test/robots/wallet_group_description_page_robot.dart new file mode 100644 index 000000000..57500dc3c --- /dev/null +++ b/integration_test/robots/wallet_group_description_page_robot.dart @@ -0,0 +1,32 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/new_wallet/wallet_group_description_page.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class WalletGroupDescriptionPageRobot { + WalletGroupDescriptionPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + final CommonTestCases commonTestCases; + + Future isWalletGroupDescriptionPage() async { + await commonTestCases.isSpecificPage(); + } + + void hasTitle() { + commonTestCases.hasText(S.current.wallet_group); + } + + Future navigateToCreateNewSeedPage() async { + await commonTestCases.tapItemByKey( + 'wallet_group_description_page_create_new_seed_button_key', + ); + } + + Future navigateToChooseWalletGroup() async { + await commonTestCases.tapItemByKey( + 'wallet_group_description_page_choose_wallet_group_button_key', + ); + } +} diff --git a/integration_test/robots/wallet_keys_robot.dart b/integration_test/robots/wallet_keys_robot.dart new file mode 100644 index 000000000..f6aeb3a66 --- /dev/null +++ b/integration_test/robots/wallet_keys_robot.dart @@ -0,0 +1,162 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; +import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cw_core/monero_wallet_keys.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_monero/monero_wallet.dart'; +import 'package:cw_wownero/wownero_wallet.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:polyseed/polyseed.dart'; + +import '../components/common_test_cases.dart'; + +class WalletKeysAndSeedPageRobot { + WalletKeysAndSeedPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + final CommonTestCases commonTestCases; + + Future isWalletKeysAndSeedPage() async { + await commonTestCases.isSpecificPage(); + } + + void hasTitle() { + final walletKeysPage = tester.widget(find.byType(WalletKeysPage)); + final walletKeysViewModel = walletKeysPage.walletKeysViewModel; + commonTestCases.hasText(walletKeysViewModel.title); + } + + void hasShareWarning() { + commonTestCases.hasText(S.current.do_not_share_warning_text.toUpperCase()); + } + + Future confirmWalletCredentials(WalletType walletType) async { + final walletKeysPage = tester.widget(find.byType(WalletKeysPage)); + final walletKeysViewModel = walletKeysPage.walletKeysViewModel; + + final appStore = walletKeysViewModel.appStore; + final walletName = walletType.name; + bool hasSeed = appStore.wallet!.seed != null; + bool hasHexSeed = appStore.wallet!.hexSeed != null; + bool hasPrivateKey = appStore.wallet!.privateKey != null; + + if (walletType == WalletType.monero) { + final moneroWallet = appStore.wallet as MoneroWallet; + final lang = PolyseedLang.getByPhrase(moneroWallet.seed); + final legacySeed = moneroWallet.seedLegacy(lang.nameEnglish); + + _confirmMoneroWalletCredentials( + appStore, + walletName, + moneroWallet.seed, + legacySeed, + ); + } + + if (walletType == WalletType.wownero) { + final wowneroWallet = appStore.wallet as WowneroWallet; + final lang = PolyseedLang.getByPhrase(wowneroWallet.seed); + final legacySeed = wowneroWallet.seedLegacy(lang.nameEnglish); + + _confirmMoneroWalletCredentials( + appStore, + walletName, + wowneroWallet.seed, + legacySeed, + ); + } + + if (walletType == WalletType.bitcoin || + walletType == WalletType.litecoin || + walletType == WalletType.bitcoinCash) { + commonTestCases.hasText(appStore.wallet!.seed!); + tester.printToConsole('$walletName wallet has seeds properly displayed'); + } + + if (isEVMCompatibleChain(walletType) || + walletType == WalletType.solana || + walletType == WalletType.tron) { + if (hasSeed) { + commonTestCases.hasText(appStore.wallet!.seed!); + tester.printToConsole('$walletName wallet has seeds properly displayed'); + } + if (hasPrivateKey) { + commonTestCases.hasText(appStore.wallet!.privateKey!); + tester.printToConsole('$walletName wallet has private key properly displayed'); + } + } + + if (walletType == WalletType.nano || walletType == WalletType.banano) { + if (hasSeed) { + commonTestCases.hasText(appStore.wallet!.seed!); + tester.printToConsole('$walletName wallet has seeds properly displayed'); + } + if (hasHexSeed) { + commonTestCases.hasText(appStore.wallet!.hexSeed!); + tester.printToConsole('$walletName wallet has hexSeed properly displayed'); + } + if (hasPrivateKey) { + commonTestCases.hasText(appStore.wallet!.privateKey!); + tester.printToConsole('$walletName wallet has private key properly displayed'); + } + } + + await commonTestCases.defaultSleepTime(seconds: 5); + } + + void _confirmMoneroWalletCredentials( + AppStore appStore, + String walletName, + String seed, + String legacySeed, + ) { + final keys = appStore.wallet!.keys as MoneroWalletKeys; + + final hasPublicSpendKey = commonTestCases.isKeyPresent( + '${walletName}_wallet_public_spend_key_item_key', + ); + final hasPrivateSpendKey = commonTestCases.isKeyPresent( + '${walletName}_wallet_private_spend_key_item_key', + ); + final hasPublicViewKey = commonTestCases.isKeyPresent( + '${walletName}_wallet_public_view_key_item_key', + ); + final hasPrivateViewKey = commonTestCases.isKeyPresent( + '${walletName}_wallet_private_view_key_item_key', + ); + final hasSeeds = seed.isNotEmpty; + final hasSeedLegacy = Polyseed.isValidSeed(seed); + + if (hasPublicSpendKey) { + commonTestCases.hasText(keys.publicSpendKey); + tester.printToConsole('$walletName wallet has public spend key properly displayed'); + } + if (hasPrivateSpendKey) { + commonTestCases.hasText(keys.privateSpendKey); + tester.printToConsole('$walletName wallet has private spend key properly displayed'); + } + if (hasPublicViewKey) { + commonTestCases.hasText(keys.publicViewKey); + tester.printToConsole('$walletName wallet has public view key properly displayed'); + } + if (hasPrivateViewKey) { + commonTestCases.hasText(keys.privateViewKey); + tester.printToConsole('$walletName wallet has private view key properly displayed'); + } + if (hasSeeds) { + commonTestCases.hasText(seed); + tester.printToConsole('$walletName wallet has seeds properly displayed'); + } + if (hasSeedLegacy) { + commonTestCases.hasText(legacySeed); + tester.printToConsole('$walletName wallet has legacy seeds properly displayed'); + } + } + + Future backToDashboard() async { + tester.printToConsole('Going back to dashboard from credentials page'); + await commonTestCases.goBack(); + await commonTestCases.goBack(); + } +} diff --git a/integration_test/robots/wallet_list_page_robot.dart b/integration_test/robots/wallet_list_page_robot.dart new file mode 100644 index 000000000..b46d4ca95 --- /dev/null +++ b/integration_test/robots/wallet_list_page_robot.dart @@ -0,0 +1,27 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class WalletListPageRobot { + WalletListPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isWalletListPage() async { + await commonTestCases.isSpecificPage(); + } + + void displaysCorrectTitle() { + commonTestCases.hasText(S.current.wallets); + } + + Future navigateToCreateNewWalletPage() async { + commonTestCases.tapItemByKey('wallet_list_page_create_new_wallet_button_key'); + } + + Future navigateToRestoreWalletOptionsPage() async { + commonTestCases.tapItemByKey('wallet_list_page_restore_wallet_button_key'); + } +} diff --git a/integration_test/robots/wallet_seed_page_robot.dart b/integration_test/robots/wallet_seed_page_robot.dart new file mode 100644 index 000000000..d52f3b1ec --- /dev/null +++ b/integration_test/robots/wallet_seed_page_robot.dart @@ -0,0 +1,57 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/seed/wallet_seed_page.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class WalletSeedPageRobot { + WalletSeedPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isWalletSeedPage() async { + await commonTestCases.isSpecificPage(); + } + + Future onNextButtonPressed() async { + await commonTestCases.tapItemByKey('wallet_seed_page_next_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future onConfirmButtonOnSeedAlertDialogPressed() async { + await commonTestCases.tapItemByKey('wallet_seed_page_seed_alert_confirm_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future onBackButtonOnSeedAlertDialogPressed() async { + await commonTestCases.tapItemByKey('wallet_seed_page_seed_alert_back_button_key'); + await commonTestCases.defaultSleepTime(); + } + + void confirmWalletDetailsDisplayCorrectly() { + final walletSeedPage = tester.widget(find.byType(WalletSeedPage)); + + final walletSeedViewModel = walletSeedPage.walletSeedViewModel; + + final walletName = walletSeedViewModel.name; + final walletSeeds = walletSeedViewModel.seed; + + commonTestCases.hasText(walletName); + commonTestCases.hasText(walletSeeds); + } + + void confirmWalletSeedReminderDisplays() { + commonTestCases.hasText(S.current.seed_reminder); + } + + Future onSaveSeedsButtonPressed() async { + await commonTestCases.tapItemByKey('wallet_seed_page_save_seeds_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future onCopySeedsButtonPressed() async { + await commonTestCases.tapItemByKey('wallet_seed_page_copy_seeds_button_key'); + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/test_suites/confirm_seeds_flow_test.dart b/integration_test/test_suites/confirm_seeds_flow_test.dart new file mode 100644 index 000000000..bf6fd5a5f --- /dev/null +++ b/integration_test/test_suites/confirm_seeds_flow_test.dart @@ -0,0 +1,107 @@ +import 'package:cake_wallet/wallet_types.g.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../components/common_test_constants.dart'; +import '../components/common_test_flows.dart'; +import '../robots/auth_page_robot.dart'; +import '../robots/dashboard_page_robot.dart'; +import '../robots/security_and_backup_page_robot.dart'; +import '../robots/wallet_keys_robot.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + AuthPageRobot authPageRobot; + CommonTestFlows commonTestFlows; + DashboardPageRobot dashboardPageRobot; + WalletKeysAndSeedPageRobot walletKeysAndSeedPageRobot; + SecurityAndBackupPageRobot securityAndBackupPageRobot; + + testWidgets( + 'Confirm if the seeds display properly', + (tester) async { + authPageRobot = AuthPageRobot(tester); + commonTestFlows = CommonTestFlows(tester); + dashboardPageRobot = DashboardPageRobot(tester); + walletKeysAndSeedPageRobot = WalletKeysAndSeedPageRobot(tester); + securityAndBackupPageRobot = SecurityAndBackupPageRobot(tester); + + // Start the app + await commonTestFlows.startAppFlow( + ValueKey('confirm_creds_display_correctly_flow_app_key'), + ); + + await commonTestFlows.welcomePageToCreateNewWalletFlow( + WalletType.solana, + CommonTestConstants.pin, + ); + + await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(WalletType.solana); + + await _confirmSeedsFlowForWalletType( + WalletType.solana, + authPageRobot, + dashboardPageRobot, + securityAndBackupPageRobot, + walletKeysAndSeedPageRobot, + tester, + ); + + // Do the same for other available wallet types + for (var walletType in availableWalletTypes) { + if (walletType == WalletType.solana) { + continue; + } + + await commonTestFlows.switchToWalletMenuFromDashboardPage(); + + await commonTestFlows.createNewWalletFromWalletMenu(walletType); + + await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(walletType); + + await _confirmSeedsFlowForWalletType( + walletType, + authPageRobot, + dashboardPageRobot, + securityAndBackupPageRobot, + walletKeysAndSeedPageRobot, + tester, + ); + } + + await Future.delayed(Duration(seconds: 15)); + }, + ); +} + +Future _confirmSeedsFlowForWalletType( + WalletType walletType, + AuthPageRobot authPageRobot, + DashboardPageRobot dashboardPageRobot, + SecurityAndBackupPageRobot securityAndBackupPageRobot, + WalletKeysAndSeedPageRobot walletKeysAndSeedPageRobot, + WidgetTester tester, +) async { + await dashboardPageRobot.openDrawerMenu(); + await dashboardPageRobot.dashboardMenuWidgetRobot.navigateToSecurityAndBackupPage(); + + await securityAndBackupPageRobot.navigateToShowKeysPage(); + + final onAuthPage = authPageRobot.onAuthPage(); + if (onAuthPage) { + await authPageRobot.enterPinCode(CommonTestConstants.pin); + } + + await tester.pumpAndSettle(); + + await walletKeysAndSeedPageRobot.isWalletKeysAndSeedPage(); + walletKeysAndSeedPageRobot.hasTitle(); + walletKeysAndSeedPageRobot.hasShareWarning(); + + walletKeysAndSeedPageRobot.confirmWalletCredentials(walletType); + + await walletKeysAndSeedPageRobot.backToDashboard(); +} diff --git a/integration_test/test_suites/create_wallet_flow_test.dart b/integration_test/test_suites/create_wallet_flow_test.dart new file mode 100644 index 000000000..2a50dbbe8 --- /dev/null +++ b/integration_test/test_suites/create_wallet_flow_test.dart @@ -0,0 +1,57 @@ +import 'package:cake_wallet/wallet_types.g.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../components/common_test_constants.dart'; +import '../components/common_test_flows.dart'; +import '../robots/dashboard_page_robot.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + CommonTestFlows commonTestFlows; + DashboardPageRobot dashboardPageRobot; + + testWidgets( + 'Create Wallet Flow', + (tester) async { + commonTestFlows = CommonTestFlows(tester); + dashboardPageRobot = DashboardPageRobot(tester); + + // Start the app + await commonTestFlows.startAppFlow( + ValueKey('create_wallets_through_seeds_test_app_key'), + ); + + await commonTestFlows.welcomePageToCreateNewWalletFlow( + WalletType.solana, + CommonTestConstants.pin, + ); + + // Confirm it actually restores a solana wallet + await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(WalletType.solana); + + // Do the same for other available wallet types + for (var walletType in availableWalletTypes) { + if (walletType == WalletType.solana) { + continue; + } + + await commonTestFlows.switchToWalletMenuFromDashboardPage(); + + await commonTestFlows.createNewWalletFromWalletMenu(walletType); + + await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(walletType); + } + + // Goes to the wallet menu and provides a confirmation that all the wallets were correctly restored + await commonTestFlows.switchToWalletMenuFromDashboardPage(); + + commonTestFlows.confirmAllAvailableWalletTypeIconsDisplayCorrectly(); + + await Future.delayed(Duration(seconds: 5)); + }, + ); +} diff --git a/integration_test/test_suites/exchange_flow_test.dart b/integration_test/test_suites/exchange_flow_test.dart index 6c993634c..c36ef9396 100644 --- a/integration_test/test_suites/exchange_flow_test.dart +++ b/integration_test/test_suites/exchange_flow_test.dart @@ -9,6 +9,7 @@ import '../robots/dashboard_page_robot.dart'; import '../robots/exchange_confirm_page_robot.dart'; import '../robots/exchange_page_robot.dart'; import '../robots/exchange_trade_page_robot.dart'; +import 'package:cake_wallet/.secrets.g.dart' as secrets; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -20,40 +21,42 @@ void main() { ExchangeTradePageRobot exchangeTradePageRobot; ExchangeConfirmPageRobot exchangeConfirmPageRobot; - group('Exchange Flow Tests', () { - testWidgets('Exchange flow', (tester) async { - authPageRobot = AuthPageRobot(tester); - commonTestFlows = CommonTestFlows(tester); - exchangePageRobot = ExchangePageRobot(tester); - dashboardPageRobot = DashboardPageRobot(tester); - exchangeTradePageRobot = ExchangeTradePageRobot(tester); - exchangeConfirmPageRobot = ExchangeConfirmPageRobot(tester); + testWidgets('Exchange flow', (tester) async { + authPageRobot = AuthPageRobot(tester); + commonTestFlows = CommonTestFlows(tester); + exchangePageRobot = ExchangePageRobot(tester); + dashboardPageRobot = DashboardPageRobot(tester); + exchangeTradePageRobot = ExchangeTradePageRobot(tester); + exchangeConfirmPageRobot = ExchangeConfirmPageRobot(tester); - await commonTestFlows.startAppFlow(ValueKey('exchange_app_test_key')); - await commonTestFlows.restoreWalletThroughSeedsFlow(); - await dashboardPageRobot.navigateToExchangePage(); + await commonTestFlows.startAppFlow(ValueKey('exchange_app_test_key')); + await commonTestFlows.welcomePageToRestoreWalletThroughSeedsFlow( + CommonTestConstants.testWalletType, + secrets.solanaTestWalletSeeds, + CommonTestConstants.pin, + ); + await dashboardPageRobot.navigateToExchangePage(); - // ----------- Exchange Page ------------- - await exchangePageRobot.selectDepositCurrency(CommonTestConstants.testDepositCurrency); - await exchangePageRobot.selectReceiveCurrency(CommonTestConstants.testReceiveCurrency); + // ----------- Exchange Page ------------- + await exchangePageRobot.selectDepositCurrency(CommonTestConstants.testDepositCurrency); + await exchangePageRobot.selectReceiveCurrency(CommonTestConstants.testReceiveCurrency); - await exchangePageRobot.enterDepositAmount(CommonTestConstants.exchangeTestAmount); - await exchangePageRobot.enterDepositRefundAddress( - depositAddress: CommonTestConstants.testWalletAddress, - ); - await exchangePageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress); - - await exchangePageRobot.onExchangeButtonPressed(); + await exchangePageRobot.enterDepositAmount(CommonTestConstants.exchangeTestAmount); + await exchangePageRobot.enterDepositRefundAddress( + depositAddress: CommonTestConstants.testWalletAddress, + ); + await exchangePageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress); - await exchangePageRobot.handleErrors(CommonTestConstants.exchangeTestAmount); + await exchangePageRobot.onExchangeButtonPressed(); - final onAuthPage = authPageRobot.onAuthPage(); - if (onAuthPage) { - await authPageRobot.enterPinCode(CommonTestConstants.pin, false); - } + await exchangePageRobot.handleErrors(CommonTestConstants.exchangeTestAmount); - await exchangeConfirmPageRobot.onSavedTradeIdButtonPressed(); - await exchangeTradePageRobot.onGotItButtonPressed(); - }); + final onAuthPage = authPageRobot.onAuthPage(); + if (onAuthPage) { + await authPageRobot.enterPinCode(CommonTestConstants.pin); + } + + await exchangeConfirmPageRobot.onSavedTradeIdButtonPressed(); + await exchangeTradePageRobot.onGotItButtonPressed(); }); } 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 new file mode 100644 index 000000000..a810aa722 --- /dev/null +++ b/integration_test/test_suites/restore_wallet_through_seeds_flow_test.dart @@ -0,0 +1,63 @@ +import 'package:cake_wallet/wallet_types.g.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../components/common_test_constants.dart'; +import '../components/common_test_flows.dart'; +import '../robots/dashboard_page_robot.dart'; +import 'package:cake_wallet/.secrets.g.dart' as secrets; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + CommonTestFlows commonTestFlows; + DashboardPageRobot dashboardPageRobot; + + testWidgets( + 'Restoring Wallets Through Seeds', + (tester) async { + commonTestFlows = CommonTestFlows(tester); + dashboardPageRobot = DashboardPageRobot(tester); + + // Start the app + await commonTestFlows.startAppFlow( + ValueKey('restore_wallets_through_seeds_test_app_key'), + ); + + // Restore the first wallet type: Solana + await commonTestFlows.welcomePageToRestoreWalletThroughSeedsFlow( + WalletType.solana, + secrets.solanaTestWalletSeeds, + CommonTestConstants.pin, + ); + + // Confirm it actually restores a solana wallet + await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(WalletType.solana); + + // Do the same for other available wallet types + for (var walletType in availableWalletTypes) { + if (walletType == WalletType.solana) { + continue; + } + + await commonTestFlows.switchToWalletMenuFromDashboardPage(); + + await commonTestFlows.restoreWalletFromWalletMenu( + walletType, + commonTestFlows.getWalletSeedsByWalletType(walletType), + ); + + await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(walletType); + } + + // Goes to the wallet menu and provides a visual confirmation that all the wallets were correctly restored + await commonTestFlows.switchToWalletMenuFromDashboardPage(); + + commonTestFlows.confirmAllAvailableWalletTypeIconsDisplayCorrectly(); + + await Future.delayed(Duration(seconds: 5)); + }, + ); +} diff --git a/integration_test/test_suites/send_flow_test.dart b/integration_test/test_suites/send_flow_test.dart index 38ac1574f..7a46435b8 100644 --- a/integration_test/test_suites/send_flow_test.dart +++ b/integration_test/test_suites/send_flow_test.dart @@ -6,6 +6,7 @@ import '../components/common_test_constants.dart'; import '../components/common_test_flows.dart'; import '../robots/dashboard_page_robot.dart'; import '../robots/send_page_robot.dart'; +import 'package:cake_wallet/.secrets.g.dart' as secrets; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -14,28 +15,30 @@ void main() { CommonTestFlows commonTestFlows; DashboardPageRobot dashboardPageRobot; - group('Send Flow Tests', () { - testWidgets('Send flow', (tester) async { - commonTestFlows = CommonTestFlows(tester); - sendPageRobot = SendPageRobot(tester: tester); - dashboardPageRobot = DashboardPageRobot(tester); + testWidgets('Send flow', (tester) async { + commonTestFlows = CommonTestFlows(tester); + sendPageRobot = SendPageRobot(tester: tester); + dashboardPageRobot = DashboardPageRobot(tester); - await commonTestFlows.startAppFlow(ValueKey('send_test_app_key')); - await commonTestFlows.restoreWalletThroughSeedsFlow(); - await dashboardPageRobot.navigateToSendPage(); + await commonTestFlows.startAppFlow(ValueKey('send_test_app_key')); + await commonTestFlows.welcomePageToRestoreWalletThroughSeedsFlow( + CommonTestConstants.testWalletType, + secrets.solanaTestWalletSeeds, + CommonTestConstants.pin, + ); + await dashboardPageRobot.navigateToSendPage(); - await sendPageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress); - await sendPageRobot.selectReceiveCurrency(CommonTestConstants.testReceiveCurrency); - await sendPageRobot.enterAmount(CommonTestConstants.sendTestAmount); - await sendPageRobot.selectTransactionPriority(); + await sendPageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress); + await sendPageRobot.selectReceiveCurrency(CommonTestConstants.testReceiveCurrency); + await sendPageRobot.enterAmount(CommonTestConstants.sendTestAmount); + await sendPageRobot.selectTransactionPriority(); - await sendPageRobot.onSendButtonPressed(); + await sendPageRobot.onSendButtonPressed(); - await sendPageRobot.handleSendResult(); + await sendPageRobot.handleSendResult(); - await sendPageRobot.onSendButtonOnConfirmSendingDialogPressed(); + await sendPageRobot.onSendButtonOnConfirmSendingDialogPressed(); - await sendPageRobot.onSentDialogPopUp(); - }); + await sendPageRobot.onSentDialogPopUp(); }); } diff --git a/integration_test/test_suites/transaction_history_flow_test.dart b/integration_test/test_suites/transaction_history_flow_test.dart new file mode 100644 index 000000000..8af6d39fd --- /dev/null +++ b/integration_test/test_suites/transaction_history_flow_test.dart @@ -0,0 +1,70 @@ +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../components/common_test_constants.dart'; +import '../components/common_test_flows.dart'; +import '../robots/dashboard_page_robot.dart'; +import '../robots/transactions_page_robot.dart'; +import 'package:cake_wallet/.secrets.g.dart' as secrets; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + CommonTestFlows commonTestFlows; + DashboardPageRobot dashboardPageRobot; + TransactionsPageRobot transactionsPageRobot; + + /// Two Test Scenarios + /// - Fully Synchronizes and display the transaction history either immediately or few seconds after fully synchronizing + /// - Displays the transaction history progressively as synchronizing happens + testWidgets('Transaction history flow', (tester) async { + commonTestFlows = CommonTestFlows(tester); + dashboardPageRobot = DashboardPageRobot(tester); + transactionsPageRobot = TransactionsPageRobot(tester); + + await commonTestFlows.startAppFlow( + ValueKey('confirm_creds_display_correctly_flow_app_key'), + ); + + /// Test Scenario 1 - Displays transaction history list after fully synchronizing. + /// + /// For Solana/Tron WalletTypes. + await commonTestFlows.welcomePageToRestoreWalletThroughSeedsFlow( + WalletType.solana, + secrets.solanaTestWalletSeeds, + CommonTestConstants.pin, + ); + + await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(WalletType.solana); + + await dashboardPageRobot.swipeDashboardTab(true); + + await transactionsPageRobot.isTransactionsPage(); + + await transactionsPageRobot.confirmTransactionsPageConstantsDisplayProperly(); + + await transactionsPageRobot.confirmTransactionHistoryListDisplaysCorrectly(false); + + /// Test Scenario 2 - Displays transaction history list while synchronizing. + /// + /// For bitcoin/Monero/Wownero WalletTypes. + await commonTestFlows.switchToWalletMenuFromDashboardPage(); + + await commonTestFlows.restoreWalletFromWalletMenu( + WalletType.bitcoin, + secrets.bitcoinTestWalletSeeds, + ); + + await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(WalletType.bitcoin); + + await dashboardPageRobot.swipeDashboardTab(true); + + await transactionsPageRobot.isTransactionsPage(); + + await transactionsPageRobot.confirmTransactionsPageConstantsDisplayProperly(); + + await transactionsPageRobot.confirmTransactionHistoryListDisplaysCorrectly(true); + }); +} diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 322ef6f86..470bdf4e7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -109,15 +109,15 @@ PODS: - FlutterMacOS - permission_handler_apple (9.1.1): - Flutter - - Protobuf (3.27.2) + - Protobuf (3.28.2) - ReachabilitySwift (5.2.3) - reactive_ble_mobile (0.0.1): - Flutter - Protobuf (~> 3.5) - SwiftProtobuf (~> 1.0) - - SDWebImage (5.19.4): - - SDWebImage/Core (= 5.19.4) - - SDWebImage/Core (5.19.4) + - SDWebImage (5.19.7): + - SDWebImage/Core (= 5.19.7) + - SDWebImage/Core (5.19.7) - sensitive_clipboard (0.0.1): - Flutter - share_plus (0.0.1): @@ -271,10 +271,10 @@ SPEC CHECKSUMS: package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 - Protobuf: fb2c13674723f76ff6eede14f78847a776455fa2 + Protobuf: 28c89b24435762f60244e691544ed80f50d82701 ReachabilitySwift: 7f151ff156cea1481a8411701195ac6a984f4979 reactive_ble_mobile: 9ce6723d37ccf701dbffd202d487f23f5de03b4c - SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d + SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3 sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 diff --git a/lib/src/screens/InfoPage.dart b/lib/src/screens/InfoPage.dart index 5398df22c..84b9e8632 100644 --- a/lib/src/screens/InfoPage.dart +++ b/lib/src/screens/InfoPage.dart @@ -21,6 +21,7 @@ abstract class InfoPage extends BasePage { String get pageTitle; String get pageDescription; String get buttonText; + Key? get buttonKey; void Function(BuildContext) get onPressed; @override @@ -39,15 +40,14 @@ abstract class InfoPage extends BasePage { alignment: Alignment.center, padding: EdgeInsets.all(24), child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), + constraints: + BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.3), + constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.3), child: AspectRatio(aspectRatio: 1, child: image), ), ), @@ -61,14 +61,13 @@ abstract class InfoPage extends BasePage { height: 1.7, fontSize: 14, fontWeight: FontWeight.normal, - color: Theme.of(context) - .extension()! - .secondaryTextColor, + color: Theme.of(context).extension()!.secondaryTextColor, ), ), ), ), PrimaryButton( + key: buttonKey, onPressed: () => onPressed(context), text: buttonText, color: Theme.of(context).primaryColor, diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index 953463269..9c012d518 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -140,7 +140,7 @@ class _DashboardPageView extends BasePage { bool get resizeToAvoidBottomInset => false; @override - Widget get endDrawer => MenuWidget(dashboardViewModel); + Widget get endDrawer => MenuWidget(dashboardViewModel, ValueKey('dashboard_page_drawer_menu_widget_key')); @override Widget leading(BuildContext context) { diff --git a/lib/src/screens/dashboard/pages/nft_details_page.dart b/lib/src/screens/dashboard/pages/nft_details_page.dart index 15d2a2b5c..b8352a672 100644 --- a/lib/src/screens/dashboard/pages/nft_details_page.dart +++ b/lib/src/screens/dashboard/pages/nft_details_page.dart @@ -28,7 +28,10 @@ class NFTDetailsPage extends BasePage { bool get resizeToAvoidBottomInset => false; @override - Widget get endDrawer => MenuWidget(dashboardViewModel); + Widget get endDrawer => MenuWidget( + dashboardViewModel, + ValueKey('nft_details_page_menu_widget_key'), + ); @override Widget trailing(BuildContext context) { diff --git a/lib/src/screens/dashboard/pages/transactions_page.dart b/lib/src/screens/dashboard/pages/transactions_page.dart index b6d1c286b..0db9ac35b 100644 --- a/lib/src/screens/dashboard/pages/transactions_page.dart +++ b/lib/src/screens/dashboard/pages/transactions_page.dart @@ -1,11 +1,13 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/anonpay_transaction_row.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/order_row.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/trade_row.dart'; import 'package:cake_wallet/themes/extensions/placeholder_theme.dart'; import 'package:cake_wallet/src/widgets/dashboard_card_widget.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/order_list_item.dart'; +import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/wallet_type.dart'; @@ -14,9 +16,7 @@ import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/header_row.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/date_section_raw.dart'; -import 'package:cake_wallet/src/screens/dashboard/widgets/trade_row.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/transaction_raw.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/dashboard/date_section_item.dart'; import 'package:intl/intl.dart'; @@ -49,6 +49,7 @@ class TransactionsPage extends StatelessWidget { return Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 8), child: DashBoardRoundedCardWidget( + key: ValueKey('transactions_page_syncing_alert_card_key'), onTap: () { try { final uri = Uri.parse( @@ -64,82 +65,93 @@ class TransactionsPage extends StatelessWidget { return Container(); } }), - HeaderRow(dashboardViewModel: dashboardViewModel), - Expanded(child: Observer(builder: (_) { - final items = dashboardViewModel.items; + HeaderRow( + dashboardViewModel: dashboardViewModel, + key: ValueKey('transactions_page_header_row_key'), + ), + Expanded( + child: Observer( + builder: (_) { + final items = dashboardViewModel.items; - return items.isNotEmpty - ? ListView.builder( - itemCount: items.length, - itemBuilder: (context, index) { - final item = items[index]; + return items.isNotEmpty + ? ListView.builder( + key: ValueKey('transactions_page_list_view_builder_key'), + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; - if (item is DateSectionItem) { - return DateSectionRaw(date: item.date); - } - - if (item is TransactionListItem) { - if (item.hasTokens && item.assetOfTransaction == null) { - return Container(); - } - - final transaction = item.transaction; - final transactionType = dashboardViewModel.getTransactionType(transaction); - - List tags = []; - if (dashboardViewModel.type == WalletType.bitcoin) { - if (bitcoin!.txIsReceivedSilentPayment(transaction)) { - tags.add(S.of(context).silent_payment); + if (item is DateSectionItem) { + return DateSectionRaw(date: item.date, key: item.key); } - } - if (dashboardViewModel.type == WalletType.litecoin) { - if (bitcoin!.txIsMweb(transaction)) { - tags.add("MWEB"); + + if (item is TransactionListItem) { + if (item.hasTokens && item.assetOfTransaction == null) { + return Container(); + } + + final transaction = item.transaction; + final transactionType = + dashboardViewModel.getTransactionType(transaction); + + List tags = []; + if (dashboardViewModel.type == WalletType.bitcoin) { + if (bitcoin!.txIsReceivedSilentPayment(transaction)) { + tags.add(S.of(context).silent_payment); + } + } + if (dashboardViewModel.type == WalletType.litecoin) { + if (bitcoin!.txIsMweb(transaction)) { + tags.add("MWEB"); + } + } + + return Observer( + builder: (_) => TransactionRow( + key: item.key, + onTap: () => Navigator.of(context) + .pushNamed(Routes.transactionDetails, arguments: transaction), + direction: transaction.direction, + formattedDate: DateFormat('HH:mm').format(transaction.date), + formattedAmount: item.formattedCryptoAmount, + formattedFiatAmount: + dashboardViewModel.balanceViewModel.isFiatDisabled + ? '' + : item.formattedFiatAmount, + isPending: transaction.isPending, + title: + item.formattedTitle + item.formattedStatus + transactionType, + tags: tags, + ), + ); } - } - return Observer( - builder: (_) => TransactionRow( - onTap: () => Navigator.of(context) - .pushNamed(Routes.transactionDetails, arguments: transaction), - direction: transaction.direction, - formattedDate: DateFormat('HH:mm').format(transaction.date), - formattedAmount: item.formattedCryptoAmount, - formattedFiatAmount: - dashboardViewModel.balanceViewModel.isFiatDisabled - ? '' - : item.formattedFiatAmount, - isPending: transaction.isPending, - title: - item.formattedTitle + item.formattedStatus + transactionType, - tags: tags, - ), - ); - } + if (item is AnonpayTransactionListItem) { + final transactionInfo = item.transaction; - if (item is AnonpayTransactionListItem) { - final transactionInfo = item.transaction; + return AnonpayTransactionRow( + key: item.key, + onTap: () => Navigator.of(context).pushNamed( + Routes.anonPayDetailsPage, + arguments: transactionInfo), + currency: transactionInfo.fiatAmount != null + ? transactionInfo.fiatEquiv ?? '' + : CryptoCurrency.fromFullName(transactionInfo.coinTo) + .name + .toUpperCase(), + provider: transactionInfo.provider, + amount: transactionInfo.fiatAmount?.toString() ?? + (transactionInfo.amountTo?.toString() ?? ''), + createdAt: DateFormat('HH:mm').format(transactionInfo.createdAt), + ); + } - return AnonpayTransactionRow( - onTap: () => Navigator.of(context) - .pushNamed(Routes.anonPayDetailsPage, arguments: transactionInfo), - currency: transactionInfo.fiatAmount != null - ? transactionInfo.fiatEquiv ?? '' - : CryptoCurrency.fromFullName(transactionInfo.coinTo) - .name - .toUpperCase(), - provider: transactionInfo.provider, - amount: transactionInfo.fiatAmount?.toString() ?? - (transactionInfo.amountTo?.toString() ?? ''), - createdAt: DateFormat('HH:mm').format(transactionInfo.createdAt), - ); - } + if (item is TradeListItem) { + final trade = item.trade; - if (item is TradeListItem) { - final trade = item.trade; - - return Observer( - builder: (_) => TradeRow( + return Observer( + builder: (_) => TradeRow( + key: item.key, onTap: () => Navigator.of(context) .pushNamed(Routes.tradeDetails, arguments: trade), provider: trade.provider, @@ -148,36 +160,44 @@ class TransactionsPage extends StatelessWidget { createdAtFormattedDate: trade.createdAt != null ? DateFormat('HH:mm').format(trade.createdAt!) : null, - formattedAmount: item.tradeFormattedAmount)); - } + formattedAmount: item.tradeFormattedAmount, + ), + ); + } - if (item is OrderListItem) { - final order = item.order; + if (item is OrderListItem) { + final order = item.order; - return Observer( - builder: (_) => OrderRow( - onTap: () => Navigator.of(context) - .pushNamed(Routes.orderDetails, arguments: order), - provider: order.provider, - from: order.from!, - to: order.to!, - createdAtFormattedDate: - DateFormat('HH:mm').format(order.createdAt), - formattedAmount: item.orderFormattedAmount, - )); - } + return Observer( + builder: (_) => OrderRow( + key: item.key, + onTap: () => Navigator.of(context) + .pushNamed(Routes.orderDetails, arguments: order), + provider: order.provider, + from: order.from!, + to: order.to!, + createdAtFormattedDate: + DateFormat('HH:mm').format(order.createdAt), + formattedAmount: item.orderFormattedAmount, + ), + ); + } - return Container(color: Colors.transparent, height: 1); - }) - : Center( - child: Text( - S.of(context).placeholder_transactions, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).extension()!.color), - ), - ); - })) + return Container(color: Colors.transparent, height: 1); + }) + : Center( + child: Text( + key: ValueKey('transactions_page_placeholder_transactions_text_key'), + S.of(context).placeholder_transactions, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).extension()!.color, + ), + ), + ); + }, + ), + ) ], ), ), diff --git a/lib/src/screens/dashboard/widgets/anonpay_transaction_row.dart b/lib/src/screens/dashboard/widgets/anonpay_transaction_row.dart index cb8bef0b7..64d4bfe85 100644 --- a/lib/src/screens/dashboard/widgets/anonpay_transaction_row.dart +++ b/lib/src/screens/dashboard/widgets/anonpay_transaction_row.dart @@ -9,6 +9,7 @@ class AnonpayTransactionRow extends StatelessWidget { required this.currency, required this.onTap, required this.amount, + super.key, }); final VoidCallback? onTap; diff --git a/lib/src/screens/dashboard/widgets/date_section_raw.dart b/lib/src/screens/dashboard/widgets/date_section_raw.dart index 73f9f03a1..02fcef7f4 100644 --- a/lib/src/screens/dashboard/widgets/date_section_raw.dart +++ b/lib/src/screens/dashboard/widgets/date_section_raw.dart @@ -1,42 +1,27 @@ import 'package:flutter/material.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:intl/intl.dart'; -import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/utils/date_formatter.dart'; class DateSectionRaw extends StatelessWidget { - DateSectionRaw({required this.date}); + DateSectionRaw({required this.date, super.key}); final DateTime date; @override Widget build(BuildContext context) { - final nowDate = DateTime.now(); - final diffDays = date.difference(nowDate).inDays; - final isToday = nowDate.day == date.day && - nowDate.month == date.month && - nowDate.year == date.year; - final dateSectionDateFormat = DateFormatter.withCurrentLocal(hasTime: false); - var title = ""; - - if (isToday) { - title = S.of(context).today; - } else if (diffDays == 0) { - title = S.of(context).yesterday; - } else if (diffDays > -7 && diffDays < 0) { - final dateFormat = DateFormat.EEEE(); - title = dateFormat.format(date); - } else { - title = dateSectionDateFormat.format(date); - } + final title = DateFormatter.convertDateTimeToReadableString(date); return Container( - height: 35, - alignment: Alignment.center, - color: Colors.transparent, - child: Text(title, - style: TextStyle( - fontSize: 12, - color: Theme.of(context).extension()!.dateSectionRowColor))); + height: 35, + alignment: Alignment.center, + color: Colors.transparent, + child: Text( + title, + style: TextStyle( + fontSize: 12, + color: Theme.of(context).extension()!.dateSectionRowColor, + ), + ), + ); } } diff --git a/lib/src/screens/dashboard/widgets/header_row.dart b/lib/src/screens/dashboard/widgets/header_row.dart index cb4f67fc2..f1f3f0889 100644 --- a/lib/src/screens/dashboard/widgets/header_row.dart +++ b/lib/src/screens/dashboard/widgets/header_row.dart @@ -7,7 +7,7 @@ import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; class HeaderRow extends StatelessWidget { - HeaderRow({required this.dashboardViewModel}); + HeaderRow({required this.dashboardViewModel, super.key}); final DashboardViewModel dashboardViewModel; @@ -34,6 +34,7 @@ class HeaderRow extends StatelessWidget { Semantics( container: true, child: GestureDetector( + key: ValueKey('transactions_page_header_row_transaction_filter_button_key'), onTap: () { showPopUp( context: context, diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index 6d8379b29..f0e330f04 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -9,7 +9,7 @@ import 'package:cw_core/wallet_type.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; class MenuWidget extends StatefulWidget { - MenuWidget(this.dashboardViewModel); + MenuWidget(this.dashboardViewModel, Key? key); final DashboardViewModel dashboardViewModel; @@ -193,6 +193,7 @@ class MenuWidgetState extends State { final isLastTile = index == itemCount - 1; return SettingActionButton( + key: item.key, isLastTile: isLastTile, tileHeight: tileHeight, selectionActive: false, diff --git a/lib/src/screens/dashboard/widgets/order_row.dart b/lib/src/screens/dashboard/widgets/order_row.dart index 8adc6e0d5..221ea5689 100644 --- a/lib/src/screens/dashboard/widgets/order_row.dart +++ b/lib/src/screens/dashboard/widgets/order_row.dart @@ -12,7 +12,10 @@ class OrderRow extends StatelessWidget { required this.to, required this.createdAtFormattedDate, this.onTap, - this.formattedAmount}); + this.formattedAmount, + super.key, + }); + final VoidCallback? onTap; final BuyProviderDescription provider; final String from; @@ -22,8 +25,7 @@ class OrderRow extends StatelessWidget { @override Widget build(BuildContext context) { - final iconColor = - Theme.of(context).extension()!.iconColor; + final iconColor = Theme.of(context).extension()!.iconColor; final providerIcon = getBuyProviderIcon(provider, iconColor: iconColor); @@ -36,46 +38,42 @@ class OrderRow extends StatelessWidget { mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (providerIcon != null) Padding( - padding: EdgeInsets.only(right: 12), - child: providerIcon, - ), + if (providerIcon != null) + Padding( + padding: EdgeInsets.only(right: 12), + child: providerIcon, + ), Expanded( child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('$from → $to', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.textColor - )), - formattedAmount != null - ? Text(formattedAmount! + ' ' + to, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.textColor - )) - : Container() - ]), - SizedBox(height: 5), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(createdAtFormattedDate, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).extension()!.dateSectionRowColor)) - ]) - ], - ) - ) + mainAxisSize: MainAxisSize.min, + children: [ + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text('$from → $to', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.textColor)), + formattedAmount != null + ? Text(formattedAmount! + ' ' + to, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: + Theme.of(context).extension()!.textColor)) + : Container() + ]), + SizedBox(height: 5), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text(createdAtFormattedDate, + style: TextStyle( + fontSize: 14, + color: + Theme.of(context).extension()!.dateSectionRowColor)) + ]) + ], + )) ], ), )); } -} \ No newline at end of file +} diff --git a/lib/src/screens/dashboard/widgets/trade_row.dart b/lib/src/screens/dashboard/widgets/trade_row.dart index 7c809aa9d..84a5d2beb 100644 --- a/lib/src/screens/dashboard/widgets/trade_row.dart +++ b/lib/src/screens/dashboard/widgets/trade_row.dart @@ -13,6 +13,7 @@ class TradeRow extends StatelessWidget { required this.createdAtFormattedDate, this.onTap, this.formattedAmount, + super.key, }); final VoidCallback? onTap; diff --git a/lib/src/screens/dashboard/widgets/transaction_raw.dart b/lib/src/screens/dashboard/widgets/transaction_raw.dart index b18131f3d..2d7cbb809 100644 --- a/lib/src/screens/dashboard/widgets/transaction_raw.dart +++ b/lib/src/screens/dashboard/widgets/transaction_raw.dart @@ -14,6 +14,7 @@ class TransactionRow extends StatelessWidget { required this.tags, required this.title, required this.onTap, + super.key, }); final VoidCallback onTap; @@ -28,33 +29,36 @@ class TransactionRow extends StatelessWidget { @override Widget build(BuildContext context) { return InkWell( - onTap: onTap, - child: Container( - padding: EdgeInsets.fromLTRB(24, 8, 24, 8), - color: Colors.transparent, - child: Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - height: 36, - width: 36, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).extension()!.rowsColor), - child: Image.asset(direction == TransactionDirection.incoming - ? 'assets/images/down_arrow.png' - : 'assets/images/up_arrow.png'), - ), - SizedBox(width: 12), - Expanded( - child: Column( + onTap: onTap, + child: Container( + padding: EdgeInsets.fromLTRB(24, 8, 24, 8), + color: Colors.transparent, + child: Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + height: 36, + width: 36, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).extension()!.rowsColor), + child: Image.asset(direction == TransactionDirection.incoming + ? 'assets/images/down_arrow.png' + : 'assets/images/up_arrow.png'), + ), + SizedBox(width: 12), + Expanded( + child: Column( mainAxisSize: MainAxisSize.min, children: [ - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - Text(title, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + title, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, @@ -65,28 +69,39 @@ class TransactionRow extends StatelessWidget { ), Text(formattedAmount, style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.textColor)) - ]), + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.textColor, + ), + ) + ], + ), SizedBox(height: 5), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(formattedDate, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(formattedDate, + style: TextStyle( + fontSize: 14, + color: Theme.of(context) + .extension()! + .dateSectionRowColor)), + Text( + formattedFiatAmount, style: TextStyle( - fontSize: 14, - color: - Theme.of(context).extension()!.dateSectionRowColor)), - Text(formattedFiatAmount, - style: TextStyle( - fontSize: 14, - color: - Theme.of(context).extension()!.dateSectionRowColor)) - ]) + fontSize: 14, + color: Theme.of(context).extension()!.dateSectionRowColor, + ), + ) + ], + ), ], - )) - ], - ), - )); + ), + ) + ], + ), + ), + ); } } diff --git a/lib/src/screens/new_wallet/new_wallet_page.dart b/lib/src/screens/new_wallet/new_wallet_page.dart index 929e3027a..387904df0 100644 --- a/lib/src/screens/new_wallet/new_wallet_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_page.dart @@ -112,10 +112,13 @@ class _WalletNameFormState extends State { context: context, builder: (_) { return AlertWithOneAction( - alertTitle: S.current.new_wallet, - alertContent: state.error, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); + key: ValueKey('new_wallet_page_failure_dialog_key'), + buttonKey: ValueKey('new_wallet_page_failure_dialog_button_key'), + alertTitle: S.current.new_wallet, + alertContent: state.error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ); }); } }); @@ -152,6 +155,7 @@ class _WalletNameFormState extends State { child: Column( children: [ TextFormField( + key: ValueKey('new_wallet_page_wallet_name_textformfield_key'), onChanged: (value) => _walletNewVM.name = value, controller: _nameController, textAlign: TextAlign.center, @@ -182,6 +186,8 @@ class _WalletNameFormState extends State { suffixIcon: Semantics( label: S.of(context).generate_name, child: IconButton( + key: ValueKey( + 'new_wallet_page_wallet_name_textformfield_generate_name_button_key'), onPressed: () async { final rName = await generateName(); FocusManager.instance.primaryFocus?.unfocus(); @@ -297,6 +303,7 @@ class _WalletNameFormState extends State { builder: (BuildContext build) => Padding( padding: EdgeInsets.only(top: 24), child: SelectButton( + key: ValueKey('new_wallet_page_monero_seed_type_button_key'), text: widget._seedSettingsViewModel.moneroSeedType.title, onTap: () async { await showPopUp( @@ -318,6 +325,7 @@ class _WalletNameFormState extends State { padding: EdgeInsets.only(top: 10), child: SeedLanguageSelector( key: _languageSelectorKey, + buttonKey: ValueKey('new_wallet_page_seed_language_selector_button_key'), initialSelected: defaultSeedLanguage, seedType: _walletNewVM.hasSeedType ? widget._seedSettingsViewModel.moneroSeedType @@ -336,6 +344,7 @@ class _WalletNameFormState extends State { Observer( builder: (context) { return LoadingPrimaryButton( + key: ValueKey('new_wallet_page_confirm_button_key'), onPressed: _confirmForm, text: S.of(context).seed_language_next, color: Colors.green, @@ -347,6 +356,7 @@ class _WalletNameFormState extends State { ), const SizedBox(height: 25), GestureDetector( + key: ValueKey('new_wallet_page_advanced_settings_button_key'), onTap: () { Navigator.of(context).pushNamed(Routes.advancedPrivacySettings, arguments: { "type": _walletNewVM.type, 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 d0936b640..e892e0d49 100644 --- a/lib/src/screens/new_wallet/wallet_group_description_page.dart +++ b/lib/src/screens/new_wallet/wallet_group_description_page.dart @@ -66,6 +66,7 @@ class WalletGroupDescriptionPage extends BasePage { ), ), PrimaryButton( + key: ValueKey('wallet_group_description_page_create_new_seed_button_key'), onPressed: () => Navigator.of(context).pushNamed( Routes.newWallet, arguments: NewWalletArguments(type: selectedWalletType), @@ -76,6 +77,7 @@ class WalletGroupDescriptionPage extends BasePage { ), SizedBox(height: 12), PrimaryButton( + key: ValueKey('wallet_group_description_page_choose_wallet_group_button_key'), onPressed: () => Navigator.of(context).pushNamed( Routes.walletGroupsDisplayPage, arguments: selectedWalletType, diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index 57f5ec727..299846a72 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -56,7 +56,8 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { } if (isMoneroOnly) { - return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS).isNotEmpty; + return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS) + .isNotEmpty; } return true; @@ -80,13 +81,12 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { child: Column( children: [ OptionTile( - key: ValueKey('restore_options_from_seeds_button_key'), - onPressed: () => - Navigator.pushNamed( - context, - Routes.restoreWalletFromSeedKeys, - arguments: widget.isNewInstall, - ), + key: ValueKey('restore_options_from_seeds_or_keys_button_key'), + onPressed: () => Navigator.pushNamed( + context, + Routes.restoreWalletFromSeedKeys, + arguments: widget.isNewInstall, + ), image: imageSeedKeys, title: S.of(context).restore_title_from_seed_keys, description: S.of(context).restore_description_from_seed_keys, @@ -107,7 +107,8 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { padding: EdgeInsets.only(top: 24), child: OptionTile( key: ValueKey('restore_options_from_hardware_wallet_button_key'), - onPressed: () => Navigator.pushNamed(context, Routes.restoreWalletFromHardwareWallet, + onPressed: () => Navigator.pushNamed( + context, Routes.restoreWalletFromHardwareWallet, arguments: widget.isNewInstall), image: imageLedger, title: S.of(context).restore_title_from_hardware_wallet, @@ -120,9 +121,9 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { key: ValueKey('restore_options_from_qr_button_key'), onPressed: () => _onScanQRCode(context), icon: Icon( - Icons.qr_code_rounded, - color: imageColor, - size: 50, + Icons.qr_code_rounded, + color: imageColor, + size: 50, ), title: S.of(context).scan_qr_code, description: S.of(context).cold_or_recover_wallet), @@ -149,20 +150,20 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { buttonAction: () => Navigator.of(context).pop()); }); }); - } Future _onScanQRCode(BuildContext context) async { - final isCameraPermissionGranted = await PermissionHandler.checkPermission(Permission.camera, context); + final isCameraPermissionGranted = + await PermissionHandler.checkPermission(Permission.camera, context); if (!isCameraPermissionGranted) return; bool isPinSet = false; if (widget.isNewInstall) { await Navigator.pushNamed(context, Routes.setupPin, arguments: (PinCodeState setupPinContext, String _) { - setupPinContext.close(); - isPinSet = true; - }); + setupPinContext.close(); + isPinSet = true; + }); } if (!widget.isNewInstall || isPinSet) { try { @@ -174,7 +175,8 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { }); final restoreWallet = await WalletRestoreFromQRCode.scanQRCodeForRestoring(context); - final restoreFromQRViewModel = getIt.get(param1: restoreWallet.type); + final restoreFromQRViewModel = + getIt.get(param1: restoreWallet.type); await restoreFromQRViewModel.create(restoreWallet: restoreWallet); if (restoreFromQRViewModel.state is FailureState) { 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 c8a75cf40..1684f6f92 100644 --- a/lib/src/screens/restore/wallet_restore_from_seed_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_seed_form.dart @@ -191,6 +191,7 @@ class WalletRestoreFromSeedFormState extends State { ), if (widget.type == WalletType.monero || widget.type == WalletType.wownero) GestureDetector( + key: ValueKey('wallet_restore_from_seed_seedtype_picker_button_key'), onTap: () async { await showPopUp( context: context, @@ -264,6 +265,7 @@ class WalletRestoreFromSeedFormState extends State { BlockchainHeightWidget( focusNode: widget.blockHeightFocusNode, key: blockchainHeightKey, + blockHeightTextFieldKey: ValueKey('wallet_restore_from_seed_blockheight_textfield_key'), onHeightOrDateEntered: widget.onHeightOrDateEntered, hasDatePicker: widget.type == WalletType.monero || widget.type == WalletType.wownero, walletType: widget.type, diff --git a/lib/src/screens/seed/pre_seed_page.dart b/lib/src/screens/seed/pre_seed_page.dart index 730dfa5f8..23de4564f 100644 --- a/lib/src/screens/seed/pre_seed_page.dart +++ b/lib/src/screens/seed/pre_seed_page.dart @@ -15,13 +15,15 @@ class PreSeedPage extends InfoPage { String get pageTitle => S.current.pre_seed_title; @override - String get pageDescription => - S.current.pre_seed_description(seedPhraseLength.toString()); + String get pageDescription => S.current.pre_seed_description(seedPhraseLength.toString()); @override String get buttonText => S.current.pre_seed_button_text; @override - void Function(BuildContext) get onPressed => (BuildContext context) => - Navigator.of(context).popAndPushNamed(Routes.seed, arguments: true); + Key? get buttonKey => ValueKey('pre_seed_page_button_key'); + + @override + void Function(BuildContext) get onPressed => + (BuildContext context) => Navigator.of(context).popAndPushNamed(Routes.seed, arguments: true); } diff --git a/lib/src/screens/seed/wallet_seed_page.dart b/lib/src/screens/seed/wallet_seed_page.dart index 200b87b7d..10160839c 100644 --- a/lib/src/screens/seed/wallet_seed_page.dart +++ b/lib/src/screens/seed/wallet_seed_page.dart @@ -33,16 +33,22 @@ class WalletSeedPage extends BasePage { void onClose(BuildContext context) async { if (isNewWalletCreated) { final confirmed = await showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithTwoActions( - alertTitle: S.of(context).seed_alert_title, - alertContent: S.of(context).seed_alert_content, - leftButtonText: S.of(context).seed_alert_back, - rightButtonText: S.of(context).seed_alert_yes, - actionLeftButton: () => Navigator.of(context).pop(false), - actionRightButton: () => Navigator.of(context).pop(true)); - }) ?? + context: context, + builder: (BuildContext context) { + return AlertWithTwoActions( + alertDialogKey: ValueKey('wallet_seed_page_seed_alert_dialog_key'), + alertRightActionButtonKey: + ValueKey('wallet_seed_page_seed_alert_confirm_button_key'), + alertLeftActionButtonKey: ValueKey('wallet_seed_page_seed_alert_back_button_key'), + alertTitle: S.of(context).seed_alert_title, + alertContent: S.of(context).seed_alert_content, + leftButtonText: S.of(context).seed_alert_back, + rightButtonText: S.of(context).seed_alert_yes, + actionLeftButton: () => Navigator.of(context).pop(false), + actionRightButton: () => Navigator.of(context).pop(true), + ); + }, + ) ?? false; if (confirmed) { @@ -62,6 +68,7 @@ class WalletSeedPage extends BasePage { Widget trailing(BuildContext context) { return isNewWalletCreated ? GestureDetector( + key: ValueKey('wallet_seed_page_next_button_key'), onTap: () => onClose(context), child: Container( width: 100, @@ -74,9 +81,9 @@ class WalletSeedPage extends BasePage { child: Text( S.of(context).seed_language_next, style: TextStyle( - fontSize: 14, fontWeight: FontWeight.w600, color: Theme.of(context) - .extension()! - .buttonTextColor), + fontSize: 14, + fontWeight: FontWeight.w600, + color: Theme.of(context).extension()!.buttonTextColor), ), ), ) @@ -93,7 +100,8 @@ class WalletSeedPage extends BasePage { padding: EdgeInsets.all(24), alignment: Alignment.center, child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), + constraints: + BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -106,6 +114,7 @@ class WalletSeedPage extends BasePage { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( + key: ValueKey('wallet_seed_page_wallet_name_text_key'), walletSeedViewModel.name, style: TextStyle( fontSize: 20, @@ -115,12 +124,14 @@ class WalletSeedPage extends BasePage { Padding( padding: EdgeInsets.only(top: 20, left: 16, right: 16), child: Text( + key: ValueKey('wallet_seed_page_wallet_seed_text_key'), walletSeedViewModel.seed, textAlign: TextAlign.center, style: TextStyle( fontSize: 14, fontWeight: FontWeight.normal, - color: Theme.of(context).extension()!.secondaryTextColor), + color: + Theme.of(context).extension()!.secondaryTextColor), ), ) ], @@ -132,12 +143,18 @@ class WalletSeedPage extends BasePage { ? Padding( padding: EdgeInsets.only(bottom: 43, left: 43, right: 43), child: Text( + key: ValueKey( + 'wallet_seed_page_wallet_seed_reminder_text_key', + ), S.of(context).seed_reminder, textAlign: TextAlign.center, style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.normal, - color: Theme.of(context).extension()!.detailsTitlesColor), + fontSize: 12, + fontWeight: FontWeight.normal, + color: Theme.of(context) + .extension()! + .detailsTitlesColor, + ), ), ) : Offstage(), @@ -145,9 +162,10 @@ class WalletSeedPage extends BasePage { mainAxisSize: MainAxisSize.max, children: [ Flexible( - child: Container( - padding: EdgeInsets.only(right: 8.0), - child: PrimaryButton( + child: Container( + padding: EdgeInsets.only(right: 8.0), + child: PrimaryButton( + key: ValueKey('wallet_seed_page_save_seeds_button_key'), onPressed: () { ShareUtil.share( text: walletSeedViewModel.seed, @@ -156,22 +174,29 @@ class WalletSeedPage extends BasePage { }, text: S.of(context).save, color: Colors.green, - textColor: Colors.white), - )), + textColor: Colors.white, + ), + ), + ), Flexible( - child: Container( - padding: EdgeInsets.only(left: 8.0), - child: Builder( + child: Container( + padding: EdgeInsets.only(left: 8.0), + child: Builder( builder: (context) => PrimaryButton( - onPressed: () { - ClipboardUtil.setSensitiveDataToClipboard( - ClipboardData(text: walletSeedViewModel.seed)); - showBar(context, S.of(context).copied_to_clipboard); - }, - text: S.of(context).copy, - color: Theme.of(context).extension()!.indicatorsColor, - textColor: Colors.white)), - )) + key: ValueKey('wallet_seed_page_copy_seeds_button_key'), + onPressed: () { + ClipboardUtil.setSensitiveDataToClipboard( + ClipboardData(text: walletSeedViewModel.seed), + ); + showBar(context, S.of(context).copied_to_clipboard); + }, + text: S.of(context).copy, + color: Theme.of(context).extension()!.indicatorsColor, + textColor: Colors.white, + ), + ), + ), + ) ], ) ], diff --git a/lib/src/screens/settings/security_backup_page.dart b/lib/src/screens/settings/security_backup_page.dart index 04ae53d77..bb1c00ff5 100644 --- a/lib/src/screens/settings/security_backup_page.dart +++ b/lib/src/screens/settings/security_backup_page.dart @@ -16,7 +16,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; class SecurityBackupPage extends BasePage { - SecurityBackupPage(this._securitySettingsViewModel, this._authService, [this._isHardwareWallet = false]); + SecurityBackupPage(this._securitySettingsViewModel, this._authService, + [this._isHardwareWallet = false]); final AuthService _authService; @@ -30,10 +31,13 @@ class SecurityBackupPage extends BasePage { @override Widget body(BuildContext context) { return Container( - padding: EdgeInsets.only(top: 10), - child: Column(mainAxisSize: MainAxisSize.min, children: [ + padding: EdgeInsets.only(top: 10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ if (!_isHardwareWallet) SettingsCellWithArrow( + key: ValueKey('security_backup_page_show_keys_button_key'), title: S.current.show_keys, handler: (_) => _authService.authenticateAction( context, @@ -44,15 +48,17 @@ class SecurityBackupPage extends BasePage { ), if (!SettingsStoreBase.walletPasswordDirectInput) SettingsCellWithArrow( + key: ValueKey('security_backup_page_create_backup_button_key'), title: S.current.create_backup, handler: (_) => _authService.authenticateAction( context, route: Routes.backup, - conditionToDetermineIfToUse2FA: _securitySettingsViewModel - .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + conditionToDetermineIfToUse2FA: + _securitySettingsViewModel.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, ), ), SettingsCellWithArrow( + key: ValueKey('security_backup_page_change_pin_button_key'), title: S.current.settings_change_pin, handler: (_) => _authService.authenticateAction( context, @@ -60,28 +66,30 @@ class SecurityBackupPage extends BasePage { arguments: (PinCodeState setupPinContext, String _) { setupPinContext.close(); }, - conditionToDetermineIfToUse2FA: _securitySettingsViewModel - .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + conditionToDetermineIfToUse2FA: + _securitySettingsViewModel.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, ), ), if (DeviceInfo.instance.isMobile || Platform.isMacOS || Platform.isLinux) Observer(builder: (_) { return SettingsSwitcherCell( + key: ValueKey('security_backup_page_allow_biometrics_button_key'), title: S.current.settings_allow_biometrical_authentication, value: _securitySettingsViewModel.allowBiometricalAuthentication, onValueChange: (BuildContext context, bool value) { if (value) { - _authService.authenticateAction(context, - onAuthSuccess: (isAuthenticatedSuccessfully) async { - if (isAuthenticatedSuccessfully) { - if (await _securitySettingsViewModel.biometricAuthenticated()) { + _authService.authenticateAction( + context, + onAuthSuccess: (isAuthenticatedSuccessfully) async { + if (isAuthenticatedSuccessfully) { + if (await _securitySettingsViewModel.biometricAuthenticated()) { + _securitySettingsViewModel + .setAllowBiometricalAuthentication(isAuthenticatedSuccessfully); + } + } else { _securitySettingsViewModel .setAllowBiometricalAuthentication(isAuthenticatedSuccessfully); } - } else { - _securitySettingsViewModel - .setAllowBiometricalAuthentication(isAuthenticatedSuccessfully); - } }, conditionToDetermineIfToUse2FA: _securitySettingsViewModel .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, @@ -93,6 +101,7 @@ class SecurityBackupPage extends BasePage { }), Observer(builder: (_) { return SettingsPickerCell( + key: ValueKey('security_backup_page_require_pin_after_button_key'), title: S.current.require_pin_after, items: PinCodeRequiredDuration.values, selectedItem: _securitySettingsViewModel.pinCodeRequiredDuration, @@ -104,14 +113,15 @@ class SecurityBackupPage extends BasePage { Observer( builder: (context) { return SettingsCellWithArrow( + key: ValueKey('security_backup_page_totp_2fa_button_key'), title: _securitySettingsViewModel.useTotp2FA ? S.current.modify_2fa : S.current.setup_2fa, - handler: (_) => _authService.authenticateAction( - context, - route: _securitySettingsViewModel.useTotp2FA - ? Routes.modify2FAPage - : Routes.setup2faInfoPage, + handler: (_) => _authService.authenticateAction( + context, + route: _securitySettingsViewModel.useTotp2FA + ? Routes.modify2FAPage + : Routes.setup2faInfoPage, conditionToDetermineIfToUse2FA: _securitySettingsViewModel .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, ), diff --git a/lib/src/screens/settings/widgets/settings_cell_with_arrow.dart b/lib/src/screens/settings/widgets/settings_cell_with_arrow.dart index f0e19a715..cb4f9dc78 100644 --- a/lib/src/screens/settings/widgets/settings_cell_with_arrow.dart +++ b/lib/src/screens/settings/widgets/settings_cell_with_arrow.dart @@ -3,8 +3,11 @@ import 'package:cake_wallet/src/widgets/standard_list.dart'; import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; class SettingsCellWithArrow extends StandardListRow { - SettingsCellWithArrow({required String title, required Function(BuildContext context)? handler}) - : super(title: title, isSelected: false, onTap: handler); + SettingsCellWithArrow({ + required String title, + required Function(BuildContext context)? handler, + Key? key, + }) : super(title: title, isSelected: false, onTap: handler, key: key); @override Widget buildTrailing(BuildContext context) => Image.asset('assets/images/select_arrow.png', diff --git a/lib/src/screens/settings/widgets/settings_picker_cell.dart b/lib/src/screens/settings/widgets/settings_picker_cell.dart index 8e0492330..765ac2991 100644 --- a/lib/src/screens/settings/widgets/settings_picker_cell.dart +++ b/lib/src/screens/settings/widgets/settings_picker_cell.dart @@ -5,19 +5,21 @@ import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/src/widgets/standard_list.dart'; class SettingsPickerCell extends StandardListRow { - SettingsPickerCell( - {required String title, - required this.selectedItem, - required this.items, - this.displayItem, - this.images, - this.searchHintText, - this.isGridView = false, - this.matchingCriteria, - this.onItemSelected}) - : super( + SettingsPickerCell({ + required String title, + required this.selectedItem, + required this.items, + this.displayItem, + this.images, + this.searchHintText, + this.isGridView = false, + this.matchingCriteria, + this.onItemSelected, + Key? key, + }) : super( title: title, isSelected: false, + key: key, onTap: (BuildContext context) async { final selectedAtIndex = items.indexOf(selectedItem); diff --git a/lib/src/screens/settings/widgets/settings_switcher_cell.dart b/lib/src/screens/settings/widgets/settings_switcher_cell.dart index 0e5c04524..6a3c8b4a0 100644 --- a/lib/src/screens/settings/widgets/settings_switcher_cell.dart +++ b/lib/src/screens/settings/widgets/settings_switcher_cell.dart @@ -10,7 +10,8 @@ class SettingsSwitcherCell extends StandardListRow { Decoration? decoration, this.leading, void Function(BuildContext context)? onTap, - }) : super(title: title, isSelected: false, decoration: decoration, onTap: onTap); + Key? key, + }) : super(title: title, isSelected: false, decoration: decoration, onTap: onTap, key: key); final bool value; final void Function(BuildContext context, bool value)? onValueChange; diff --git a/lib/src/screens/setup_2fa/setup_2fa_info_page.dart b/lib/src/screens/setup_2fa/setup_2fa_info_page.dart index ff6187665..8aa0ac3c9 100644 --- a/lib/src/screens/setup_2fa/setup_2fa_info_page.dart +++ b/lib/src/screens/setup_2fa/setup_2fa_info_page.dart @@ -4,7 +4,6 @@ import 'package:cake_wallet/src/screens/InfoPage.dart'; import 'package:flutter/cupertino.dart'; class Setup2FAInfoPage extends InfoPage { - @override String get pageTitle => S.current.pre_seed_title; @@ -15,6 +14,9 @@ class Setup2FAInfoPage extends InfoPage { String get buttonText => S.current.understand; @override - void Function(BuildContext) get onPressed => (BuildContext context) => - Navigator.of(context).popAndPushNamed(Routes.setup_2faPage); + Key? get buttonKey => ValueKey('setup_2fa_info_page_button_key'); + + @override + void Function(BuildContext) get onPressed => + (BuildContext context) => Navigator.of(context).popAndPushNamed(Routes.setup_2faPage); } diff --git a/lib/src/screens/transaction_details/blockexplorer_list_item.dart b/lib/src/screens/transaction_details/blockexplorer_list_item.dart index d5a70daa7..0126a147a 100644 --- a/lib/src/screens/transaction_details/blockexplorer_list_item.dart +++ b/lib/src/screens/transaction_details/blockexplorer_list_item.dart @@ -1,7 +1,12 @@ import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart'; +import 'package:flutter/foundation.dart'; class BlockExplorerListItem extends TransactionDetailsListItem { - BlockExplorerListItem({required String title, required String value, required this.onTap}) - : super(title: title, value: value); + BlockExplorerListItem({ + required String title, + required String value, + required this.onTap, + Key? key, + }) : super(title: title, value: value, key: key); final Function() onTap; } diff --git a/lib/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart b/lib/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart index db3d94500..96ddc94cd 100644 --- a/lib/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart +++ b/lib/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart @@ -1,18 +1,20 @@ import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart'; +import 'package:flutter/widgets.dart'; class StandardPickerListItem extends TransactionDetailsListItem { - StandardPickerListItem( - {required String title, - required String value, - required this.items, - required this.displayItem, - required this.onSliderChanged, - required this.onItemSelected, - required this.selectedIdx, - required this.customItemIndex, - this.maxValue, - required this.customValue}) - : super(title: title, value: value); + StandardPickerListItem({ + required String title, + required String value, + required this.items, + required this.displayItem, + required this.onSliderChanged, + required this.onItemSelected, + required this.selectedIdx, + required this.customItemIndex, + this.maxValue, + required this.customValue, + Key? key, + }) : super(title: title, value: value, key: key); final List items; final String Function(T item, double sliderValue) displayItem; diff --git a/lib/src/screens/transaction_details/standart_list_item.dart b/lib/src/screens/transaction_details/standart_list_item.dart index 705daf970..673eabe42 100644 --- a/lib/src/screens/transaction_details/standart_list_item.dart +++ b/lib/src/screens/transaction_details/standart_list_item.dart @@ -1,6 +1,9 @@ import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart'; class StandartListItem extends TransactionDetailsListItem { - StandartListItem({required String title, required String value}) - : super(title: title, value: value); + StandartListItem({ + required String super.title, + required String super.value, + super.key, + }); } diff --git a/lib/src/screens/transaction_details/textfield_list_item.dart b/lib/src/screens/transaction_details/textfield_list_item.dart index 49ef2705e..846f9acd5 100644 --- a/lib/src/screens/transaction_details/textfield_list_item.dart +++ b/lib/src/screens/transaction_details/textfield_list_item.dart @@ -1,11 +1,17 @@ import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart'; +import 'package:flutter/foundation.dart'; class TextFieldListItem extends TransactionDetailsListItem { TextFieldListItem({ required String title, required String value, - required this.onSubmitted}) - : super(title: title, value: value); + required this.onSubmitted, + Key? key, + }) : super( + title: title, + value: value, + key: key, + ); final Function(String value) onSubmitted; } \ No newline at end of file diff --git a/lib/src/screens/transaction_details/transaction_details_list_item.dart b/lib/src/screens/transaction_details/transaction_details_list_item.dart index 8a8449350..446383d72 100644 --- a/lib/src/screens/transaction_details/transaction_details_list_item.dart +++ b/lib/src/screens/transaction_details/transaction_details_list_item.dart @@ -1,6 +1,9 @@ +import 'package:flutter/foundation.dart'; + abstract class TransactionDetailsListItem { - TransactionDetailsListItem({required this.title, required this.value}); + TransactionDetailsListItem({required this.title, required this.value, this.key}); final String title; final String value; -} \ No newline at end of file + final Key? key; +} diff --git a/lib/src/screens/transaction_details/transaction_details_page.dart b/lib/src/screens/transaction_details/transaction_details_page.dart index 1b088fc31..9484bf4da 100644 --- a/lib/src/screens/transaction_details/transaction_details_page.dart +++ b/lib/src/screens/transaction_details/transaction_details_page.dart @@ -33,38 +33,42 @@ class TransactionDetailsPage extends BasePage { children: [ Expanded( child: SectionStandardList( - sectionCount: 1, - itemCounter: (int _) => transactionDetailsViewModel.items.length, - itemBuilder: (__, index) { - final item = transactionDetailsViewModel.items[index]; + sectionCount: 1, + itemCounter: (int _) => transactionDetailsViewModel.items.length, + itemBuilder: (__, index) { + final item = transactionDetailsViewModel.items[index]; - if (item is StandartListItem) { - return GestureDetector( - onTap: () { - Clipboard.setData(ClipboardData(text: item.value)); - showBar(context, S.of(context).transaction_details_copied(item.title)); - }, - child: ListRow(title: '${item.title}:', value: item.value), - ); - } + if (item is StandartListItem) { + return GestureDetector( + key: item.key, + onTap: () { + Clipboard.setData(ClipboardData(text: item.value)); + showBar(context, S.of(context).transaction_details_copied(item.title)); + }, + child: ListRow(title: '${item.title}:', value: item.value), + ); + } - if (item is BlockExplorerListItem) { - return GestureDetector( - onTap: item.onTap, - child: ListRow(title: '${item.title}:', value: item.value), - ); - } + if (item is BlockExplorerListItem) { + return GestureDetector( + key: item.key, + onTap: item.onTap, + child: ListRow(title: '${item.title}:', value: item.value), + ); + } - if (item is TextFieldListItem) { - return TextFieldListRow( - title: item.title, - value: item.value, - onSubmitted: item.onSubmitted, - ); - } + if (item is TextFieldListItem) { + return TextFieldListRow( + key: item.key, + title: item.title, + value: item.value, + onSubmitted: item.onSubmitted, + ); + } - return Container(); - }), + return Container(); + }, + ), ), Observer( builder: (_) { diff --git a/lib/src/screens/transaction_details/transaction_expandable_list_item.dart b/lib/src/screens/transaction_details/transaction_expandable_list_item.dart index e87405de3..db6cf22ae 100644 --- a/lib/src/screens/transaction_details/transaction_expandable_list_item.dart +++ b/lib/src/screens/transaction_details/transaction_expandable_list_item.dart @@ -1,7 +1,12 @@ import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart'; +import 'package:flutter/foundation.dart'; class StandardExpandableListItem extends TransactionDetailsListItem { - StandardExpandableListItem({required String title, required this.expandableItems}) - : super(title: title, value: ''); + StandardExpandableListItem({ + required String title, + required this.expandableItems, + Key? key, + }) : super(title: title, value: '', key: key); + final List expandableItems; } diff --git a/lib/src/screens/transaction_details/widgets/textfield_list_row.dart b/lib/src/screens/transaction_details/widgets/textfield_list_row.dart index a86645ecb..24016a293 100644 --- a/lib/src/screens/transaction_details/widgets/textfield_list_row.dart +++ b/lib/src/screens/transaction_details/widgets/textfield_list_row.dart @@ -4,12 +4,14 @@ import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; import 'package:flutter/material.dart'; class TextFieldListRow extends StatefulWidget { - TextFieldListRow( - {required this.title, - required this.value, - this.titleFontSize = 14, - this.valueFontSize = 16, - this.onSubmitted}); + TextFieldListRow({ + required this.title, + required this.value, + this.titleFontSize = 14, + this.valueFontSize = 16, + this.onSubmitted, + super.key, + }); final String title; final String value; diff --git a/lib/src/screens/wallet_keys/wallet_keys_page.dart b/lib/src/screens/wallet_keys/wallet_keys_page.dart index 5117f152f..fac760516 100644 --- a/lib/src/screens/wallet_keys/wallet_keys_page.dart +++ b/lib/src/screens/wallet_keys/wallet_keys_page.dart @@ -25,23 +25,25 @@ class WalletKeysPage extends BasePage { @override Widget trailing(BuildContext context) => IconButton( - onPressed: () async { - final url = await walletKeysViewModel.url; + key: ValueKey('wallet_keys_page_fullscreen_qr_button_key'), + onPressed: () async { + final url = await walletKeysViewModel.url; - BrightnessUtil.changeBrightnessForFunction(() async { - await Navigator.pushNamed( - context, - Routes.fullscreenQR, - arguments: QrViewData(data: url.toString(), version: QrVersions.auto), - ); - }); - }, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - hoverColor: Colors.transparent, - icon: Image.asset( - 'assets/images/qr_code_icon.png', - )); + BrightnessUtil.changeBrightnessForFunction(() async { + await Navigator.pushNamed( + context, + Routes.fullscreenQR, + arguments: QrViewData(data: url.toString(), version: QrVersions.auto), + ); + }); + }, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + hoverColor: Colors.transparent, + icon: Image.asset( + 'assets/images/qr_code_icon.png', + ), + ); @override Widget body(BuildContext context) { @@ -60,6 +62,7 @@ class WalletKeysPage extends BasePage { child: Padding( padding: const EdgeInsets.all(8.0), child: AutoSizeText( + key: ValueKey('wallet_keys_page_share_warning_text_key'), S.of(context).do_not_share_warning_text.toUpperCase(), textAlign: TextAlign.center, maxLines: 4, @@ -92,6 +95,7 @@ class WalletKeysPage extends BasePage { final item = walletKeysViewModel.items[index]; return GestureDetector( + key: item.key, onTap: () { ClipboardUtil.setSensitiveDataToClipboard(ClipboardData(text: item.value)); showBar(context, S.of(context).copied_key_to_clipboard(item.title)); diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index afb86cf0a..a9a9d9413 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -318,6 +318,7 @@ class WalletListBodyState extends State { child: Column( children: [ PrimaryImageButton( + key: ValueKey('wallet_list_page_create_new_wallet_button_key'), onPressed: () { //TODO(David): Find a way to optimize this if (isSingleCoin) { @@ -359,6 +360,7 @@ class WalletListBodyState extends State { ), SizedBox(height: 10.0), PrimaryImageButton( + key: ValueKey('wallet_list_page_restore_wallet_button_key'), onPressed: () { if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) { widget.authService.authenticateAction( diff --git a/lib/src/widgets/blockchain_height_widget.dart b/lib/src/widgets/blockchain_height_widget.dart index 9d66c1789..1c6b6da02 100644 --- a/lib/src/widgets/blockchain_height_widget.dart +++ b/lib/src/widgets/blockchain_height_widget.dart @@ -23,6 +23,7 @@ class BlockchainHeightWidget extends StatefulWidget { this.doSingleScan = false, this.bitcoinMempoolAPIEnabled, required this.walletType, + this.blockHeightTextFieldKey, }) : super(key: key); final Function(int)? onHeightChange; @@ -35,6 +36,7 @@ class BlockchainHeightWidget extends StatefulWidget { final Future? bitcoinMempoolAPIEnabled; final Function()? toggleSingleScan; final WalletType walletType; + final Key? blockHeightTextFieldKey; @override State createState() => BlockchainHeightState(); @@ -81,6 +83,7 @@ class BlockchainHeightState extends State { child: Container( padding: EdgeInsets.only(top: 20.0, bottom: 10.0), child: BaseTextFormField( + key: widget.blockHeightTextFieldKey, focusNode: widget.focusNode, controller: restoreHeightController, keyboardType: diff --git a/lib/src/widgets/dashboard_card_widget.dart b/lib/src/widgets/dashboard_card_widget.dart index d9b545040..dc223eb02 100644 --- a/lib/src/widgets/dashboard_card_widget.dart +++ b/lib/src/widgets/dashboard_card_widget.dart @@ -15,6 +15,7 @@ class DashBoardRoundedCardWidget extends StatelessWidget { this.icon, this.onClose, this.customBorder, + super.key, }); final VoidCallback onTap; diff --git a/lib/src/widgets/option_tile.dart b/lib/src/widgets/option_tile.dart index 31f958f54..c2d8b9506 100644 --- a/lib/src/widgets/option_tile.dart +++ b/lib/src/widgets/option_tile.dart @@ -2,14 +2,14 @@ import 'package:cake_wallet/themes/extensions/option_tile_theme.dart'; import 'package:flutter/material.dart'; class OptionTile extends StatelessWidget { - const OptionTile( - {required this.onPressed, - this.image, - this.icon, - required this.title, - required this.description, - super.key}) - : assert(image!=null || icon!=null); + const OptionTile({ + required this.onPressed, + this.image, + this.icon, + required this.title, + required this.description, + super.key, + }) : assert(image != null || icon != null); final VoidCallback onPressed; final Image? image; @@ -34,7 +34,7 @@ class OptionTile extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ - icon ?? image!, + icon ?? image!, Expanded( child: Padding( padding: EdgeInsets.only(left: 16), diff --git a/lib/src/widgets/picker.dart b/lib/src/widgets/picker.dart index 801a79595..ed5c05be6 100644 --- a/lib/src/widgets/picker.dart +++ b/lib/src/widgets/picker.dart @@ -2,6 +2,7 @@ import 'dart:math'; +import 'package:cake_wallet/entities/seed_type.dart'; import 'package:cake_wallet/src/widgets/search_bar_widget.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -310,6 +311,8 @@ class _PickerState extends State> { itemName = item.name; } else if (item is TransactionPriority) { itemName = item.title; + } else if (item is MoneroSeedType) { + itemName = item.title; } else { itemName = ''; } diff --git a/lib/src/widgets/seed_language_selector.dart b/lib/src/widgets/seed_language_selector.dart index 87f3aa573..711bff390 100644 --- a/lib/src/widgets/seed_language_selector.dart +++ b/lib/src/widgets/seed_language_selector.dart @@ -6,12 +6,16 @@ import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:flutter/material.dart'; class SeedLanguageSelector extends StatefulWidget { - SeedLanguageSelector( - {Key? key, required this.initialSelected, this.seedType = MoneroSeedType.defaultSeedType}) - : super(key: key); + SeedLanguageSelector({ + required this.initialSelected, + this.seedType = MoneroSeedType.defaultSeedType, + this.buttonKey, + Key? key, + }) : super(key: key); final String initialSelected; final MoneroSeedType seedType; + final Key? buttonKey; @override SeedLanguageSelectorState createState() => SeedLanguageSelectorState(selected: initialSelected); @@ -25,6 +29,7 @@ class SeedLanguageSelectorState extends State { @override Widget build(BuildContext context) { return SelectButton( + key: widget.buttonKey, image: null, text: "${seedLanguages.firstWhere((e) => e.name == selected).nameLocalized} (${S.of(context).seed_language})", diff --git a/lib/src/widgets/setting_actions.dart b/lib/src/widgets/setting_actions.dart index b9af97f32..048d006cc 100644 --- a/lib/src/widgets/setting_actions.dart +++ b/lib/src/widgets/setting_actions.dart @@ -5,9 +5,11 @@ import 'package:flutter/material.dart'; class SettingActions { final String Function(BuildContext) name; final String image; + final Key key; final void Function(BuildContext) onTap; SettingActions._({ + required this.key, required this.name, required this.image, required this.onTap, @@ -39,6 +41,7 @@ class SettingActions { ]; static SettingActions silentPaymentsSettingAction = SettingActions._( + key: ValueKey('dashboard_page_menu_widget_silent_payment_settings_button_key'), name: (context) => S.of(context).silent_payments_settings, image: 'assets/images/bitcoin_menu.png', onTap: (BuildContext context) { @@ -48,6 +51,7 @@ class SettingActions { ); static SettingActions litecoinMwebSettingAction = SettingActions._( + key: ValueKey('dashboard_page_menu_widget_litecoin_mweb_settings_button_key'), name: (context) => S.of(context).litecoin_mweb_settings, image: 'assets/images/litecoin_menu.png', onTap: (BuildContext context) { @@ -57,6 +61,7 @@ class SettingActions { ); static SettingActions connectionSettingAction = SettingActions._( + key: ValueKey('dashboard_page_menu_widget_connection_and_sync_settings_button_key'), name: (context) => S.of(context).connection_sync, image: 'assets/images/nodes_menu.png', onTap: (BuildContext context) { @@ -66,6 +71,7 @@ class SettingActions { ); static SettingActions walletSettingAction = SettingActions._( + key: ValueKey('dashboard_page_menu_widget_wallet_menu_button_key'), name: (context) => S.of(context).wallets, image: 'assets/images/wallet_menu.png', onTap: (BuildContext context) { @@ -75,6 +81,7 @@ class SettingActions { ); static SettingActions addressBookSettingAction = SettingActions._( + key: ValueKey('dashboard_page_menu_widget_address_book_button_key'), name: (context) => S.of(context).address_book_menu, image: 'assets/images/open_book_menu.png', onTap: (BuildContext context) { @@ -84,6 +91,7 @@ class SettingActions { ); static SettingActions securityBackupSettingAction = SettingActions._( + key: ValueKey('dashboard_page_menu_widget_security_and_backup_button_key'), name: (context) => S.of(context).security_and_backup, image: 'assets/images/key_menu.png', onTap: (BuildContext context) { @@ -93,6 +101,7 @@ class SettingActions { ); static SettingActions privacySettingAction = SettingActions._( + key: ValueKey('dashboard_page_menu_widget_privacy_settings_button_key'), name: (context) => S.of(context).privacy, image: 'assets/images/privacy_menu.png', onTap: (BuildContext context) { @@ -102,6 +111,7 @@ class SettingActions { ); static SettingActions displaySettingAction = SettingActions._( + key: ValueKey('dashboard_page_menu_widget_display_settings_button_key'), name: (context) => S.of(context).display_settings, image: 'assets/images/eye_menu.png', onTap: (BuildContext context) { @@ -111,6 +121,7 @@ class SettingActions { ); static SettingActions otherSettingAction = SettingActions._( + key: ValueKey('dashboard_page_menu_widget_other_settings_button_key'), name: (context) => S.of(context).other_settings, image: 'assets/images/settings_menu.png', onTap: (BuildContext context) { @@ -120,6 +131,7 @@ class SettingActions { ); static SettingActions supportSettingAction = SettingActions._( + key: ValueKey('dashboard_page_menu_widget_support_settings_button_key'), name: (context) => S.of(context).settings_support, image: 'assets/images/question_mark.png', onTap: (BuildContext context) { diff --git a/lib/src/widgets/standard_list.dart b/lib/src/widgets/standard_list.dart index c1fcae052..0780d64cd 100644 --- a/lib/src/widgets/standard_list.dart +++ b/lib/src/widgets/standard_list.dart @@ -4,7 +4,13 @@ import 'package:cake_wallet/src/widgets/standard_list_status_row.dart'; import 'package:flutter/material.dart'; class StandardListRow extends StatelessWidget { - StandardListRow({required this.title, required this.isSelected, this.onTap, this.decoration}); + StandardListRow({ + required this.title, + required this.isSelected, + this.onTap, + this.decoration, + super.key, + }); final String title; final bool isSelected; diff --git a/lib/store/anonpay/anonpay_transactions_store.dart b/lib/store/anonpay/anonpay_transactions_store.dart index c6f05b993..d0384ca5a 100644 --- a/lib/store/anonpay/anonpay_transactions_store.dart +++ b/lib/store/anonpay/anonpay_transactions_store.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart'; +import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; @@ -27,7 +28,10 @@ abstract class AnonpayTransactionsStoreBase with Store { Future updateTransactionList() async { transactions = anonpayInvoiceInfoSource.values .map( - (transaction) => AnonpayTransactionListItem(transaction: transaction), + (transaction) => AnonpayTransactionListItem( + transaction: transaction, + key: ValueKey('anonpay_invoice_transaction_list_item_${transaction.invoiceId}_key'), + ), ) .toList(); } diff --git a/lib/store/dashboard/orders_store.dart b/lib/store/dashboard/orders_store.dart index b5ec658f7..d91ad3512 100644 --- a/lib/store/dashboard/orders_store.dart +++ b/lib/store/dashboard/orders_store.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/view_model/dashboard/order_list_item.dart'; +import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -10,12 +11,10 @@ part 'orders_store.g.dart'; class OrdersStore = OrdersStoreBase with _$OrdersStore; abstract class OrdersStoreBase with Store { - OrdersStoreBase({required this.ordersSource, - required this.settingsStore}) - : orders = [], - orderId = '' { - _onOrdersChanged = - ordersSource.watch().listen((_) async => await updateOrderList()); + OrdersStoreBase({required this.ordersSource, required this.settingsStore}) + : orders = [], + orderId = '' { + _onOrdersChanged = ordersSource.watch().listen((_) async => await updateOrderList()); updateOrderList(); } @@ -38,8 +37,11 @@ abstract class OrdersStoreBase with Store { void setOrder(Order order) => this.order = order; @action - Future updateOrderList() async => orders = - ordersSource.values.map((order) => OrderListItem( - order: order, - settingsStore: settingsStore)).toList(); -} \ No newline at end of file + Future updateOrderList() async => orders = ordersSource.values + .map((order) => OrderListItem( + order: order, + settingsStore: settingsStore, + key: ValueKey('order_list_item_${order.id}_key'), + )) + .toList(); +} diff --git a/lib/store/dashboard/trades_store.dart b/lib/store/dashboard/trades_store.dart index 72442b46f..d0a4592e3 100644 --- a/lib/store/dashboard/trades_store.dart +++ b/lib/store/dashboard/trades_store.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart'; +import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -11,9 +12,8 @@ class TradesStore = TradesStoreBase with _$TradesStore; abstract class TradesStoreBase with Store { TradesStoreBase({required this.tradesSource, required this.settingsStore}) - : trades = [] { - _onTradesChanged = - tradesSource.watch().listen((_) async => await updateTradeList()); + : trades = [] { + _onTradesChanged = tradesSource.watch().listen((_) async => await updateTradeList()); updateTradeList(); } @@ -31,8 +31,11 @@ abstract class TradesStoreBase with Store { void setTrade(Trade trade) => this.trade = trade; @action - Future updateTradeList() async => trades = - tradesSource.values.map((trade) => TradeListItem( - trade: trade, - settingsStore: settingsStore)).toList(); -} \ No newline at end of file + Future updateTradeList() async => trades = tradesSource.values + .map((trade) => TradeListItem( + trade: trade, + settingsStore: settingsStore, + key: ValueKey('trade_list_item_${trade.id}_key'), + )) + .toList(); +} diff --git a/lib/utils/date_formatter.dart b/lib/utils/date_formatter.dart index 9cff68614..58e59966a 100644 --- a/lib/utils/date_formatter.dart +++ b/lib/utils/date_formatter.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/generated/i18n.dart'; import 'package:intl/intl.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -5,8 +6,7 @@ import 'package:cake_wallet/store/settings_store.dart'; class DateFormatter { static String currentLocalFormat({bool hasTime = true, bool reverse = false}) { final isUSA = getIt.get().languageCode.toLowerCase() == 'en'; - final format = - isUSA ? usaStyleFormat(hasTime, reverse) : regularStyleFormat(hasTime, reverse); + final format = isUSA ? usaStyleFormat(hasTime, reverse) : regularStyleFormat(hasTime, reverse); return format; } @@ -20,4 +20,26 @@ class DateFormatter { static String regularStyleFormat(bool hasTime, bool reverse) => hasTime ? (reverse ? 'HH:mm dd.MM.yyyy' : 'dd.MM.yyyy, HH:mm') : 'dd.MM.yyyy'; + + static String convertDateTimeToReadableString(DateTime date) { + final nowDate = DateTime.now(); + final diffDays = date.difference(nowDate).inDays; + final isToday = + nowDate.day == date.day && nowDate.month == date.month && nowDate.year == date.year; + final dateSectionDateFormat = withCurrentLocal(hasTime: false); + var title = ""; + + if (isToday) { + title = S.current.today; + } else if (diffDays == 0) { + title = S.current.yesterday; + } else if (diffDays > -7 && diffDays < 0) { + final dateFormat = DateFormat.EEEE(); + title = dateFormat.format(date); + } else { + title = dateSectionDateFormat.format(date); + } + + return title; + } } diff --git a/lib/utils/image_utill.dart b/lib/utils/image_utill.dart index ef3775e4c..746fde453 100644 --- a/lib/utils/image_utill.dart +++ b/lib/utils/image_utill.dart @@ -11,6 +11,7 @@ class ImageUtil { if (isNetworkImage) { return isSvg ? SvgPicture.network( + key: ValueKey(imagePath), imagePath, height: _height, width: _width, @@ -23,6 +24,7 @@ class ImageUtil { ), ) : Image.network( + key: ValueKey(imagePath), imagePath, height: _height, width: _width, @@ -58,12 +60,14 @@ class ImageUtil { height: _height, width: _width, placeholderBuilder: (_) => Icon(Icons.error), + key: ValueKey(imagePath), ) : Image.asset( imagePath, height: _height, width: _width, errorBuilder: (_, __, ___) => Icon(Icons.error), + key: ValueKey(imagePath), ); } } diff --git a/lib/view_model/dashboard/action_list_item.dart b/lib/view_model/dashboard/action_list_item.dart index b03bd1bdc..1ee4e6a3c 100644 --- a/lib/view_model/dashboard/action_list_item.dart +++ b/lib/view_model/dashboard/action_list_item.dart @@ -1,3 +1,8 @@ +import 'package:flutter/foundation.dart'; + abstract class ActionListItem { + ActionListItem({required this.key}); + DateTime get date; + Key key; } \ No newline at end of file diff --git a/lib/view_model/dashboard/anonpay_transaction_list_item.dart b/lib/view_model/dashboard/anonpay_transaction_list_item.dart index 261e49070..a54a4b334 100644 --- a/lib/view_model/dashboard/anonpay_transaction_list_item.dart +++ b/lib/view_model/dashboard/anonpay_transaction_list_item.dart @@ -2,7 +2,7 @@ import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; class AnonpayTransactionListItem extends ActionListItem { - AnonpayTransactionListItem({required this.transaction}); + AnonpayTransactionListItem({required this.transaction, required super.key}); final AnonpayInvoiceInfo transaction; diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 423babefc..6cf7c49d0 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -48,6 +48,7 @@ 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:http/http.dart' as http; import 'package:mobx/mobx.dart'; @@ -182,10 +183,16 @@ abstract class DashboardViewModelBase with Store { final sortedTransactions = [..._accountTransactions]; sortedTransactions.sort((a, b) => a.date.compareTo(b.date)); - transactions = ObservableList.of(sortedTransactions.map((transaction) => TransactionListItem( - transaction: transaction, - balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore))); + transactions = ObservableList.of( + sortedTransactions.map( + (transaction) => TransactionListItem( + transaction: transaction, + balanceViewModel: balanceViewModel, + settingsStore: appStore.settingsStore, + key: ValueKey('monero_transaction_history_item_${transaction.id}_key'), + ), + ), + ); } else if (_wallet.type == WalletType.wownero) { subname = wow.wownero!.getCurrentAccount(_wallet).label; @@ -206,18 +213,30 @@ abstract class DashboardViewModelBase with Store { final sortedTransactions = [..._accountTransactions]; sortedTransactions.sort((a, b) => a.date.compareTo(b.date)); - transactions = ObservableList.of(sortedTransactions.map((transaction) => TransactionListItem( - transaction: transaction, - balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore))); + transactions = ObservableList.of( + sortedTransactions.map( + (transaction) => TransactionListItem( + transaction: transaction, + balanceViewModel: balanceViewModel, + settingsStore: appStore.settingsStore, + key: ValueKey('wownero_transaction_history_item_${transaction.id}_key'), + ), + ), + ); } else { final sortedTransactions = [...wallet.transactionHistory.transactions.values]; sortedTransactions.sort((a, b) => a.date.compareTo(b.date)); - transactions = ObservableList.of(sortedTransactions.map((transaction) => TransactionListItem( - transaction: transaction, - balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore))); + transactions = ObservableList.of( + sortedTransactions.map( + (transaction) => TransactionListItem( + transaction: transaction, + balanceViewModel: balanceViewModel, + settingsStore: appStore.settingsStore, + key: ValueKey('${_wallet.type.name}_transaction_history_item_${transaction.id}_key'), + ), + ), + ); } // TODO: nano sub-account generation is disabled: @@ -234,9 +253,13 @@ abstract class DashboardViewModelBase with Store { appStore.wallet!.transactionHistory.transactions, transactions, (TransactionInfo? transaction) => TransactionListItem( - transaction: transaction!, - balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore), filter: (TransactionInfo? transaction) { + transaction: transaction!, + balanceViewModel: balanceViewModel, + settingsStore: appStore.settingsStore, + key: ValueKey( + '${_wallet.type.name}_transaction_history_item_${transaction.id}_key', + ), + ), filter: (TransactionInfo? transaction) { if (transaction == null) { return false; } @@ -650,20 +673,29 @@ abstract class DashboardViewModelBase with Store { transactions.clear(); - transactions.addAll(wallet.transactionHistory.transactions.values.map((transaction) => - TransactionListItem( - transaction: transaction, - balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore))); + transactions.addAll( + wallet.transactionHistory.transactions.values.map( + (transaction) => TransactionListItem( + transaction: transaction, + balanceViewModel: balanceViewModel, + settingsStore: appStore.settingsStore, + key: ValueKey('${wallet.type.name}_transaction_history_item_${transaction.id}_key'), + ), + ), + ); } connectMapToListWithTransform( appStore.wallet!.transactionHistory.transactions, transactions, (TransactionInfo? transaction) => TransactionListItem( - transaction: transaction!, - balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore), filter: (TransactionInfo? tx) { + transaction: transaction!, + balanceViewModel: balanceViewModel, + settingsStore: appStore.settingsStore, + key: ValueKey( + '${wallet.type.name}_transaction_history_item_${transaction.id}_key', + ), + ), filter: (TransactionInfo? tx) { if (tx == null) { return false; } @@ -703,10 +735,16 @@ abstract class DashboardViewModelBase with Store { monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id) .toList(); - transactions.addAll(_accountTransactions.map((transaction) => TransactionListItem( - transaction: transaction, - balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore))); + transactions.addAll( + _accountTransactions.map( + (transaction) => TransactionListItem( + transaction: transaction, + balanceViewModel: balanceViewModel, + settingsStore: appStore.settingsStore, + key: ValueKey('monero_transaction_history_item_${transaction.id}_key'), + ), + ), + ); } else if (wallet.type == WalletType.wownero) { final _accountTransactions = wow.wownero! .getTransactionHistory(wallet) @@ -717,10 +755,16 @@ abstract class DashboardViewModelBase with Store { wow.wownero!.getCurrentAccount(wallet).id) .toList(); - transactions.addAll(_accountTransactions.map((transaction) => TransactionListItem( - transaction: transaction, - balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore))); + transactions.addAll( + _accountTransactions.map( + (transaction) => TransactionListItem( + transaction: transaction, + balanceViewModel: balanceViewModel, + settingsStore: appStore.settingsStore, + key: ValueKey('wownero_transaction_history_item_${transaction.id}_key'), + ), + ), + ); } } diff --git a/lib/view_model/dashboard/date_section_item.dart b/lib/view_model/dashboard/date_section_item.dart index 0b361ecce..75250a7ea 100644 --- a/lib/view_model/dashboard/date_section_item.dart +++ b/lib/view_model/dashboard/date_section_item.dart @@ -1,7 +1,7 @@ import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; class DateSectionItem extends ActionListItem { - DateSectionItem(this.date); + DateSectionItem(this.date, {required super.key}); @override final DateTime date; diff --git a/lib/view_model/dashboard/formatted_item_list.dart b/lib/view_model/dashboard/formatted_item_list.dart index a1cbbbf7d..04464ca89 100644 --- a/lib/view_model/dashboard/formatted_item_list.dart +++ b/lib/view_model/dashboard/formatted_item_list.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/date_section_item.dart'; +import 'package:flutter/foundation.dart'; List formattedItemsList(List items) { final formattedList = []; @@ -11,7 +12,12 @@ List formattedItemsList(List items) { if (lastDate == null) { lastDate = transaction.date; - formattedList.add(DateSectionItem(transaction.date)); + formattedList.add( + DateSectionItem( + transaction.date, + key: ValueKey('date_section_item_${transaction.date.microsecondsSinceEpoch}_key'), + ), + ); formattedList.add(transaction); continue; } @@ -26,7 +32,12 @@ List formattedItemsList(List items) { } lastDate = transaction.date; - formattedList.add(DateSectionItem(transaction.date)); + formattedList.add( + DateSectionItem( + transaction.date, + key: ValueKey('date_section_item_${transaction.date.microsecondsSinceEpoch}_key'), + ), + ); formattedList.add(transaction); } diff --git a/lib/view_model/dashboard/order_list_item.dart b/lib/view_model/dashboard/order_list_item.dart index 9120cc1a6..52adf53a6 100644 --- a/lib/view_model/dashboard/order_list_item.dart +++ b/lib/view_model/dashboard/order_list_item.dart @@ -6,7 +6,9 @@ import 'package:cake_wallet/entities/balance_display_mode.dart'; class OrderListItem extends ActionListItem { OrderListItem({ required this.order, - required this.settingsStore}); + required this.settingsStore, + required super.key, + }); final Order order; final SettingsStore settingsStore; diff --git a/lib/view_model/dashboard/trade_list_item.dart b/lib/view_model/dashboard/trade_list_item.dart index 964ba4ffa..55ae4e99f 100644 --- a/lib/view_model/dashboard/trade_list_item.dart +++ b/lib/view_model/dashboard/trade_list_item.dart @@ -4,7 +4,11 @@ import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; class TradeListItem extends ActionListItem { - TradeListItem({required this.trade, required this.settingsStore}); + TradeListItem({ + required this.trade, + required this.settingsStore, + required super.key, + }); final Trade trade; final SettingsStore settingsStore; diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index b218c8e9f..d9b361fd2 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -22,8 +22,12 @@ import 'package:cw_core/keyable.dart'; import 'package:cw_core/wallet_type.dart'; class TransactionListItem extends ActionListItem with Keyable { - TransactionListItem( - {required this.transaction, required this.balanceViewModel, required this.settingsStore}); + TransactionListItem({ + required this.transaction, + required this.balanceViewModel, + required this.settingsStore, + required super.key, + }); final TransactionInfo transaction; final BalanceViewModel balanceViewModel; diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index da6699ae6..a189ebe6c 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -20,6 +20,7 @@ import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:collection/collection.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_priority.dart'; +import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:intl/src/intl/date_format.dart'; import 'package:mobx/mobx.dart'; @@ -52,7 +53,7 @@ abstract class TransactionDetailsViewModelBase with Store { break; case WalletType.bitcoin: _addElectrumListItems(tx, dateFormat); - if(!canReplaceByFee)_checkForRBF(tx); + if (!canReplaceByFee) _checkForRBF(tx); break; case WalletType.litecoin: case WalletType.bitcoinCash: @@ -83,8 +84,7 @@ abstract class TransactionDetailsViewModelBase with Store { break; } - final descriptionKey = - '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}'; + final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}'; final description = transactionDescriptionBox.values.firstWhere( (val) => val.id == descriptionKey || val.id == transactionInfo.txHash, orElse: () => TransactionDescription(id: descriptionKey)); @@ -93,15 +93,20 @@ abstract class TransactionDetailsViewModelBase with Store { final recipientAddress = description.recipientAddress; if (recipientAddress?.isNotEmpty ?? false) { - items.add(StandartListItem( - title: S.current.transaction_details_recipient_address, - value: recipientAddress!)); + items.add( + StandartListItem( + title: S.current.transaction_details_recipient_address, + value: recipientAddress!, + key: ValueKey('standard_list_item_${recipientAddress}_key'), + ), + ); } } final type = wallet.type; - items.add(BlockExplorerListItem( + items.add( + BlockExplorerListItem( title: S.current.view_in_block_explorer, value: _explorerDescription(type), onTap: () async { @@ -109,9 +114,13 @@ abstract class TransactionDetailsViewModelBase with Store { final uri = Uri.parse(_explorerUrl(type, tx.txHash)); if (await canLaunchUrl(uri)) await launchUrl(uri, mode: LaunchMode.externalApplication); } catch (e) {} - })); + }, + key: ValueKey('block_explorer_list_item_${type.name}_wallet_type_key'), + ), + ); - items.add(TextFieldListItem( + items.add( + TextFieldListItem( title: S.current.note_tap_to_change, value: description.note, onSubmitted: (value) { @@ -122,7 +131,10 @@ abstract class TransactionDetailsViewModelBase with Store { } else { transactionDescriptionBox.add(description); } - })); + }, + key: ValueKey('textfield_list_item_note_entry_key'), + ), + ); } final TransactionInfo transactionInfo; @@ -209,14 +221,38 @@ abstract class TransactionDetailsViewModelBase with Store { final addressIndex = tx.additionalInfo['addressIndex'] as int; final feeFormatted = tx.feeFormatted(); final _items = [ - StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.txHash), StandartListItem( - title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), - StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'), - StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + 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 (feeFormatted != null) - StandartListItem(title: S.current.transaction_details_fee, value: feeFormatted), - if (key?.isNotEmpty ?? false) StandartListItem(title: S.current.transaction_key, value: key!), + StandartListItem( + title: S.current.transaction_details_fee, + value: feeFormatted, + key: ValueKey('standard_list_item_transaction_details_fee_key'), + ), + if (key?.isNotEmpty ?? false) + StandartListItem( + title: S.current.transaction_key, + value: key!, + key: ValueKey('standard_list_item_transaction_key'), + ), ]; if (tx.direction == TransactionDirection.incoming) { @@ -226,14 +262,21 @@ abstract class TransactionDetailsViewModelBase with Store { if (address.isNotEmpty) { isRecipientAddressShown = true; - _items.add(StandartListItem( - title: S.current.transaction_details_recipient_address, - value: address, - )); + _items.add( + StandartListItem( + title: S.current.transaction_details_recipient_address, + value: address, + key: ValueKey('standard_list_item_transaction_details_recipient_address_key'), + ), + ); } if (label.isNotEmpty) { - _items.add(StandartListItem(title: S.current.address_label, value: label)); + _items.add(StandartListItem( + title: S.current.address_label, + value: label, + key: ValueKey('standard_list_item_address_label_key'), + )); } } catch (e) { print(e.toString()); @@ -245,14 +288,37 @@ abstract class TransactionDetailsViewModelBase with Store { void _addElectrumListItems(TransactionInfo tx, DateFormat dateFormat) { final _items = [ - StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.txHash), StandartListItem( - title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), - StandartListItem(title: S.current.confirmations, value: tx.confirmations.toString()), - StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'), - StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + 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.confirmations, + value: tx.confirmations.toString(), + key: ValueKey('standard_list_item_transaction_confirmations_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()!), + StandartListItem( + title: S.current.transaction_details_fee, + value: tx.feeFormatted()!, + key: ValueKey('standard_list_item_transaction_details_fee_key'), + ), ]; items.addAll(_items); @@ -260,30 +326,80 @@ abstract class TransactionDetailsViewModelBase with Store { void _addHavenListItems(TransactionInfo tx, DateFormat dateFormat) { items.addAll([ - StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.txHash), StandartListItem( - title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), - StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'), - StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + 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()!), + StandartListItem( + title: S.current.transaction_details_fee, + value: tx.feeFormatted()!, + key: ValueKey('standard_list_item_transaction_details_fee_key'), + ), ]); } void _addEthereumListItems(TransactionInfo tx, DateFormat dateFormat) { final _items = [ - StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.txHash), StandartListItem( - title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), - StandartListItem(title: S.current.confirmations, value: tx.confirmations.toString()), - StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'), - StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + 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.confirmations, + value: tx.confirmations.toString(), + key: ValueKey('standard_list_item_transaction_confirmations_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()!), + 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!), + StandartListItem( + title: S.current.transaction_details_recipient_address, + value: tx.to!, + key: ValueKey('standard_list_item_transaction_details_recipient_address_key'), + ), if (tx.direction == TransactionDirection.incoming && tx.from != null) - StandartListItem(title: S.current.transaction_details_source_address, value: tx.from!), + StandartListItem( + title: S.current.transaction_details_source_address, + value: tx.from!, + key: ValueKey('standard_list_item_transaction_details_source_address_key'), + ), ]; items.addAll(_items); @@ -291,16 +407,43 @@ abstract class TransactionDetailsViewModelBase with Store { void _addNanoListItems(TransactionInfo tx, DateFormat dateFormat) { final _items = [ - StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.txHash), - if (showRecipientAddress && tx.to != null) - StandartListItem(title: S.current.transaction_details_recipient_address, value: tx.to!), - if (showRecipientAddress && tx.from != null) - StandartListItem(title: S.current.transaction_details_source_address, value: tx.from!), - StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), StandartListItem( - title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), - StandartListItem(title: S.current.confirmed_tx, value: (tx.confirmations > 0).toString()), - StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'), + title: S.current.transaction_details_transaction_id, + value: tx.txHash, + key: ValueKey('standard_list_item_transaction_details_id_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 (showRecipientAddress && tx.from != null) + StandartListItem( + title: S.current.transaction_details_source_address, + value: tx.from!, + key: ValueKey('standard_list_item_transaction_details_source_address_key'), + ), + StandartListItem( + title: S.current.transaction_details_amount, + value: tx.amountFormatted(), + key: ValueKey('standard_list_item_transaction_details_amount_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.confirmed_tx, + value: (tx.confirmations > 0).toString(), + key: ValueKey('standard_list_item_transaction_confirmed_key'), + ), + StandartListItem( + title: S.current.transaction_details_height, + value: '${tx.height}', + key: ValueKey('standard_list_item_transaction_details_height_key'), + ), ]; items.addAll(_items); @@ -308,18 +451,49 @@ abstract class TransactionDetailsViewModelBase with Store { void _addPolygonListItems(TransactionInfo tx, DateFormat dateFormat) { final _items = [ - StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.txHash), StandartListItem( - title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), - StandartListItem(title: S.current.confirmations, value: tx.confirmations.toString()), - StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'), - StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + 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.confirmations, + value: tx.confirmations.toString(), + key: ValueKey('standard_list_item_transaction_confirmations_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()!), + StandartListItem( + title: S.current.transaction_details_fee, + value: tx.feeFormatted()!, + key: ValueKey('standard_list_item_transaction_details_fee_key'), + ), if (showRecipientAddress && tx.to != null && tx.direction == TransactionDirection.outgoing) - StandartListItem(title: S.current.transaction_details_recipient_address, value: tx.to!), + StandartListItem( + title: S.current.transaction_details_recipient_address, + value: tx.to!, + key: ValueKey('standard_list_item_transaction_details_recipient_address_key'), + ), if (tx.direction == TransactionDirection.incoming && tx.from != null) - StandartListItem(title: S.current.transaction_details_source_address, value: tx.from!), + StandartListItem( + title: S.current.transaction_details_source_address, + value: tx.from!, + key: ValueKey('standard_list_item_transaction_details_source_address_key'), + ), ]; items.addAll(_items); @@ -327,16 +501,39 @@ abstract class TransactionDetailsViewModelBase with Store { void _addSolanaListItems(TransactionInfo tx, DateFormat dateFormat) { final _items = [ - StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.txHash), StandartListItem( - title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), - StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + 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_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()!), + 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!), + 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!), + StandartListItem( + title: S.current.transaction_details_source_address, + value: tx.from!, + key: ValueKey('standard_list_item_transaction_details_source_address_key'), + ), ]; items.addAll(_items); @@ -354,7 +551,13 @@ abstract class TransactionDetailsViewModelBase with Store { newFee = bitcoin!.getFeeAmountForPriority( wallet, bitcoin!.getBitcoinTransactionPriorityMedium(), inputsCount, outputsCount); - RBFListItems.add(StandartListItem(title: S.current.old_fee, value: tx.feeFormatted() ?? '0.0')); + RBFListItems.add( + StandartListItem( + title: S.current.old_fee, + value: tx.feeFormatted() ?? '0.0', + key: ValueKey('standard_list_item_rbf_old_fee_key'), + ), + ); if (transactionInfo.fee != null && rawTransaction.isNotEmpty) { final size = bitcoin!.getTransactionVSize(wallet, rawTransaction); @@ -371,7 +574,9 @@ abstract class TransactionDetailsViewModelBase with Store { final customItemIndex = customItem != null ? priorities.indexOf(customItem) : null; final maxCustomFeeRate = sendViewModel.maxCustomFeeRate?.toDouble(); - RBFListItems.add(StandardPickerListItem( + RBFListItems.add( + StandardPickerListItem( + key: ValueKey('standard_picker_list_item_transaction_priorities_key'), title: S.current.estimated_new_fee, value: bitcoin!.formatterBitcoinAmountToString(amount: newFee) + ' ${walletTypeToCryptoCurrency(wallet.type)}', @@ -387,42 +592,73 @@ abstract class TransactionDetailsViewModelBase with Store { onItemSelected: (dynamic item, double sliderValue) { transactionPriority = item as TransactionPriority; return setNewFee(value: sliderValue, priority: transactionPriority!); - })); + }, + ), + ); if (transactionInfo.inputAddresses != null && transactionInfo.inputAddresses!.isNotEmpty) { - RBFListItems.add(StandardExpandableListItem( - title: S.current.inputs, expandableItems: transactionInfo.inputAddresses!)); + RBFListItems.add( + StandardExpandableListItem( + key: ValueKey('standard_expandable_list_item_transaction_input_addresses_key'), + title: S.current.inputs, + expandableItems: transactionInfo.inputAddresses!, + ), + ); } if (transactionInfo.outputAddresses != null && transactionInfo.outputAddresses!.isNotEmpty) { final outputAddresses = transactionInfo.outputAddresses!.map((element) { if (element.contains('OP_RETURN:') && element.length > 40) { - return element.substring(0, 40) + '...'; + return element.substring(0, 40) + '...'; } return element; }).toList(); RBFListItems.add( - StandardExpandableListItem(title: S.current.outputs, expandableItems: outputAddresses)); + StandardExpandableListItem( + title: S.current.outputs, + expandableItems: outputAddresses, + key: ValueKey('standard_expandable_list_item_transaction_output_addresses_key'), + ), + ); } } void _addTronListItems(TransactionInfo tx, DateFormat dateFormat) { final _items = [ - StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.txHash), StandartListItem( - title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), - StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + 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_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()!), + 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: tron!.getTronBase58Address(tx.to!, wallet)), + title: S.current.transaction_details_recipient_address, + value: tron!.getTronBase58Address(tx.to!, wallet), + key: ValueKey('standard_list_item_transaction_details_recipient_address_key'), + ), if (tx.from != null) StandartListItem( - title: S.current.transaction_details_source_address, - value: tron!.getTronBase58Address(tx.from!, wallet)), + title: S.current.transaction_details_source_address, + value: tron!.getTronBase58Address(tx.from!, wallet), + key: ValueKey('standard_list_item_transaction_details_source_address_key'), + ), ]; items.addAll(_items); @@ -455,7 +691,7 @@ abstract class TransactionDetailsViewModelBase with Store { return bitcoin!.formatterBitcoinAmountToString(amount: newFee); } - void replaceByFee(String newFee) => sendViewModel.replaceByFee(transactionInfo, newFee,); + void replaceByFee(String newFee) => sendViewModel.replaceByFee(transactionInfo, newFee); @computed String get pendingTransactionFiatAmountValueFormatted => sendViewModel.isFiatDisabled @@ -473,14 +709,38 @@ abstract class TransactionDetailsViewModelBase with Store { final addressIndex = tx.additionalInfo['addressIndex'] as int; final feeFormatted = tx.feeFormatted(); final _items = [ - StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.txHash), StandartListItem( - title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), - StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'), - StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + 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 (feeFormatted != null) - StandartListItem(title: S.current.transaction_details_fee, value: feeFormatted), - if (key?.isNotEmpty ?? false) StandartListItem(title: S.current.transaction_key, value: key!), + StandartListItem( + title: S.current.transaction_details_fee, + value: feeFormatted, + key: ValueKey('standard_list_item_transaction_details_fee_key'), + ), + if (key?.isNotEmpty ?? false) + StandartListItem( + title: S.current.transaction_key, + value: key!, + key: ValueKey('standard_list_item_transaction_key'), + ), ]; if (tx.direction == TransactionDirection.incoming) { @@ -490,14 +750,23 @@ abstract class TransactionDetailsViewModelBase with Store { if (address.isNotEmpty) { isRecipientAddressShown = true; - _items.add(StandartListItem( - title: S.current.transaction_details_recipient_address, - value: address, - )); + _items.add( + StandartListItem( + title: S.current.transaction_details_recipient_address, + value: address, + key: ValueKey('standard_list_item_transaction_details_recipient_address_key'), + ), + ); } if (label.isNotEmpty) { - _items.add(StandartListItem(title: S.current.address_label, value: label)); + _items.add( + StandartListItem( + title: S.current.address_label, + value: label, + key: ValueKey('standard_list_item_address_label_key'), + ), + ); } } catch (e) { print(e.toString()); diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index 9921ae30a..6455fc0e3 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -10,6 +10,7 @@ import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/wallet_base.dart'; 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:polyseed/polyseed.dart'; @@ -24,6 +25,7 @@ abstract class WalletKeysViewModelBase with Store { _appStore.wallet!.type == WalletType.bitcoinCash ? S.current.wallet_seed : S.current.wallet_keys, + _walletName = _appStore.wallet!.type.name, _restoreHeight = _appStore.wallet!.walletInfo.restoreHeight, _restoreHeightByTransactions = 0, items = ObservableList() { @@ -38,12 +40,10 @@ abstract class WalletKeysViewModelBase with Store { _appStore.wallet!.type == WalletType.wownero) { final accountTransactions = _getWalletTransactions(_appStore.wallet!); if (accountTransactions.isNotEmpty) { - final incomingAccountTransactions = accountTransactions - .where((tx) => tx.direction == TransactionDirection.incoming); + final incomingAccountTransactions = + accountTransactions.where((tx) => tx.direction == TransactionDirection.incoming); if (incomingAccountTransactions.isNotEmpty) { - incomingAccountTransactions - .toList() - .sort((a, b) => a.date.compareTo(b.date)); + incomingAccountTransactions.toList().sort((a, b) => a.date.compareTo(b.date)); _restoreHeightByTransactions = _getRestoreHeightByTransactions( _appStore.wallet!.type, incomingAccountTransactions.first.date); } @@ -55,6 +55,10 @@ abstract class WalletKeysViewModelBase with Store { final String title; + final String _walletName; + + AppStore get appStore => _appStore; + final AppStore _appStore; final int _restoreHeight; @@ -70,37 +74,56 @@ abstract class WalletKeysViewModelBase with Store { items.addAll([ if (keys['publicSpendKey'] != null) StandartListItem( - title: S.current.spend_key_public, - value: keys['publicSpendKey']!), + key: ValueKey('${_walletName}_wallet_public_spend_key_item_key'), + title: S.current.spend_key_public, + value: keys['publicSpendKey']!, + ), if (keys['privateSpendKey'] != null) StandartListItem( - title: S.current.spend_key_private, - value: keys['privateSpendKey']!), + key: ValueKey('${_walletName}_wallet_private_spend_key_item_key'), + title: S.current.spend_key_private, + value: keys['privateSpendKey']!, + ), if (keys['publicViewKey'] != null) StandartListItem( - title: S.current.view_key_public, value: keys['publicViewKey']!), + key: ValueKey('${_walletName}_wallet_public_view_key_item_key'), + title: S.current.view_key_public, + value: keys['publicViewKey']!, + ), if (keys['privateViewKey'] != null) StandartListItem( - title: S.current.view_key_private, - value: keys['privateViewKey']!), + key: ValueKey('${_walletName}_wallet_private_view_key_item_key'), + title: S.current.view_key_private, + value: keys['privateViewKey']!, + ), if (_appStore.wallet!.seed!.isNotEmpty) - StandartListItem(title: S.current.wallet_seed, value: _appStore.wallet!.seed!), + StandartListItem( + key: ValueKey('${_walletName}_wallet_seed_item_key'), + title: S.current.wallet_seed, + value: _appStore.wallet!.seed!, + ), ]); - if (_appStore.wallet?.seed != null && - Polyseed.isValidSeed(_appStore.wallet!.seed!)) { + if (_appStore.wallet?.seed != null && Polyseed.isValidSeed(_appStore.wallet!.seed!)) { final lang = PolyseedLang.getByPhrase(_appStore.wallet!.seed!); - items.add(StandartListItem( + items.add( + StandartListItem( + key: ValueKey('${_walletName}_wallet_seed_legacy_item_key'), title: S.current.wallet_seed_legacy, - value: (_appStore.wallet as MoneroWalletBase) - .seedLegacy(lang.nameEnglish))); + value: (_appStore.wallet as MoneroWalletBase).seedLegacy(lang.nameEnglish), + ), + ); } final restoreHeight = monero!.getRestoreHeight(_appStore.wallet!); if (restoreHeight != null) { - items.add(StandartListItem( + items.add( + StandartListItem( + key: ValueKey('${_walletName}_wallet_restore_height_item_key'), title: S.current.wallet_recovery_height, - value: restoreHeight.toString())); + value: restoreHeight.toString(), + ), + ); } } @@ -110,21 +133,34 @@ abstract class WalletKeysViewModelBase with Store { items.addAll([ if (keys['publicSpendKey'] != null) StandartListItem( - title: S.current.spend_key_public, - value: keys['publicSpendKey']!), + key: ValueKey('${_walletName}_wallet_public_spend_key_item_key'), + title: S.current.spend_key_public, + value: keys['publicSpendKey']!, + ), if (keys['privateSpendKey'] != null) StandartListItem( - title: S.current.spend_key_private, - value: keys['privateSpendKey']!), + key: ValueKey('${_walletName}_wallet_private_spend_key_item_key'), + title: S.current.spend_key_private, + value: keys['privateSpendKey']!, + ), if (keys['publicViewKey'] != null) StandartListItem( - title: S.current.view_key_public, value: keys['publicViewKey']!), + key: ValueKey('${_walletName}_wallet_public_view_key_item_key'), + title: S.current.view_key_public, + value: keys['publicViewKey']!, + ), if (keys['privateViewKey'] != null) StandartListItem( - title: S.current.view_key_private, - value: keys['privateViewKey']!), + key: ValueKey('${_walletName}_wallet_private_view_key_item_key'), + title: S.current.view_key_private, + value: keys['privateViewKey']!, + ), if (_appStore.wallet!.seed!.isNotEmpty) - StandartListItem(title: S.current.wallet_seed, value: _appStore.wallet!.seed!), + StandartListItem( + key: ValueKey('${_walletName}_wallet_seed_item_key'), + title: S.current.wallet_seed, + value: _appStore.wallet!.seed!, + ), ]); } @@ -134,29 +170,45 @@ abstract class WalletKeysViewModelBase with Store { items.addAll([ if (keys['publicSpendKey'] != null) StandartListItem( - title: S.current.spend_key_public, - value: keys['publicSpendKey']!), + key: ValueKey('${_walletName}_wallet_public_spend_key_item_key'), + title: S.current.spend_key_public, + value: keys['publicSpendKey']!, + ), if (keys['privateSpendKey'] != null) StandartListItem( - title: S.current.spend_key_private, - value: keys['privateSpendKey']!), + key: ValueKey('${_walletName}_wallet_private_spend_key_item_key'), + title: S.current.spend_key_private, + value: keys['privateSpendKey']!, + ), if (keys['publicViewKey'] != null) StandartListItem( - title: S.current.view_key_public, value: keys['publicViewKey']!), + key: ValueKey('${_walletName}_wallet_public_view_key_item_key'), + title: S.current.view_key_public, + value: keys['publicViewKey']!, + ), if (keys['privateViewKey'] != null) StandartListItem( - title: S.current.view_key_private, - value: keys['privateViewKey']!), + key: ValueKey('${_walletName}_wallet_private_view_key_item_key'), + title: S.current.view_key_private, + value: keys['privateViewKey']!, + ), if (_appStore.wallet!.seed!.isNotEmpty) - StandartListItem(title: S.current.wallet_seed, value: _appStore.wallet!.seed!), + StandartListItem( + key: ValueKey('${_walletName}_wallet_seed_item_key'), + title: S.current.wallet_seed, + value: _appStore.wallet!.seed!, + ), ]); - if (_appStore.wallet?.seed != null && - Polyseed.isValidSeed(_appStore.wallet!.seed!)) { + if (_appStore.wallet?.seed != null && Polyseed.isValidSeed(_appStore.wallet!.seed!)) { final lang = PolyseedLang.getByPhrase(_appStore.wallet!.seed!); - items.add(StandartListItem( + items.add( + StandartListItem( + key: ValueKey('${_walletName}_wallet_seed_legacy_item_key'), title: S.current.wallet_seed_legacy, - value: wownero!.getLegacySeed(_appStore.wallet!, lang.nameEnglish))); + value: wownero!.getLegacySeed(_appStore.wallet!, lang.nameEnglish), + ), + ); } } @@ -173,7 +225,10 @@ abstract class WalletKeysViewModelBase with Store { // if (keys['publicKey'] != null) // StandartListItem(title: S.current.public_key, value: keys['publicKey']!), StandartListItem( - title: S.current.wallet_seed, value: _appStore.wallet!.seed!), + key: ValueKey('${_walletName}_wallet_seed_item_key'), + title: S.current.wallet_seed, + value: _appStore.wallet!.seed!, + ), ]); } @@ -183,31 +238,43 @@ abstract class WalletKeysViewModelBase with Store { items.addAll([ if (_appStore.wallet!.privateKey != null) StandartListItem( - title: S.current.private_key, - value: _appStore.wallet!.privateKey!), + key: ValueKey('${_walletName}_wallet_private_key_item_key'), + title: S.current.private_key, + value: _appStore.wallet!.privateKey!, + ), if (_appStore.wallet!.seed != null) StandartListItem( - title: S.current.wallet_seed, value: _appStore.wallet!.seed!), + key: ValueKey('${_walletName}_wallet_seed_item_key'), + title: S.current.wallet_seed, + value: _appStore.wallet!.seed!, + ), ]); } - bool nanoBased = _appStore.wallet!.type == WalletType.nano || - _appStore.wallet!.type == WalletType.banano; + bool nanoBased = + _appStore.wallet!.type == WalletType.nano || _appStore.wallet!.type == WalletType.banano; if (nanoBased) { // we always have the hex version of the seed and private key: items.addAll([ if (_appStore.wallet!.seed != null) StandartListItem( - title: S.current.wallet_seed, value: _appStore.wallet!.seed!), + key: ValueKey('${_walletName}_wallet_seed_item_key'), + title: S.current.wallet_seed, + value: _appStore.wallet!.seed!, + ), if (_appStore.wallet!.hexSeed != null) StandartListItem( - title: S.current.seed_hex_form, - value: _appStore.wallet!.hexSeed!), + key: ValueKey('${_walletName}_wallet_hex_seed_key'), + title: S.current.seed_hex_form, + value: _appStore.wallet!.hexSeed!, + ), if (_appStore.wallet!.privateKey != null) StandartListItem( - title: S.current.private_key, - value: _appStore.wallet!.privateKey!), + key: ValueKey('${_walletName}_wallet_private_key_item_key'), + title: S.current.private_key, + value: _appStore.wallet!.privateKey!, + ), ]); } } @@ -273,8 +340,7 @@ abstract class WalletKeysViewModelBase with Store { if (_appStore.wallet!.seed != null) 'seed': _appStore.wallet!.seed!, if (_appStore.wallet!.seed == null && _appStore.wallet!.hexSeed != null) 'hexSeed': _appStore.wallet!.hexSeed!, - if (_appStore.wallet!.seed == null && - _appStore.wallet!.privateKey != null) + if (_appStore.wallet!.seed == null && _appStore.wallet!.privateKey != null) 'private_key': _appStore.wallet!.privateKey!, if (restoreHeightResult != null) ...{'height': restoreHeightResult}, if (_appStore.wallet!.passphrase != null) 'passphrase': _appStore.wallet!.passphrase! @@ -292,11 +358,7 @@ abstract class WalletKeysViewModelBase with Store { } 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(); + return wownero!.getTransactionHistory(wallet).transactions.values.toList(); } return []; } @@ -312,6 +374,5 @@ abstract class WalletKeysViewModelBase with Store { return 0; } - String getRoundedRestoreHeight(int height) => - ((height / 1000).floor() * 1000).toString(); + String getRoundedRestoreHeight(int height) => ((height / 1000).floor() * 1000).toString(); } diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index d3b652935..88cbbfce3 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -44,12 +44,35 @@ class SecretKey { SecretKey('cakePayApiKey', () => ''), SecretKey('CSRFToken', () => ''), SecretKey('authorization', () => ''), + SecretKey('moneroTestWalletSeeds', () => ''), + SecretKey('moneroLegacyTestWalletSeeds ', () => ''), + SecretKey('bitcoinTestWalletSeeds', () => ''), + SecretKey('ethereumTestWalletSeeds', () => ''), + SecretKey('litecoinTestWalletSeeds', () => ''), + SecretKey('bitcoinCashTestWalletSeeds', () => ''), + SecretKey('polygonTestWalletSeeds', () => ''), + SecretKey('solanaTestWalletSeeds', () => ''), + SecretKey('polygonTestWalletSeeds', () => ''), + SecretKey('tronTestWalletSeeds', () => ''), + SecretKey('nanoTestWalletSeeds', () => ''), + SecretKey('wowneroTestWalletSeeds', () => ''), + SecretKey('moneroTestWalletReceiveAddress', () => ''), + SecretKey('bitcoinTestWalletReceiveAddress', () => ''), + SecretKey('ethereumTestWalletReceiveAddress', () => ''), + SecretKey('litecoinTestWalletReceiveAddress', () => ''), + SecretKey('bitco inCashTestWalletReceiveAddress', () => ''), + SecretKey('polygonTestWalletReceiveAddress', () => ''), + SecretKey('solanaTestWalletReceiveAddress', () => ''), + SecretKey('tronTestWalletReceiveAddress', () => ''), + SecretKey('nanoTestWalletReceiveAddress', () => ''), + SecretKey('wowneroTestWalletReceiveAddress', () => ''), SecretKey('etherScanApiKey', () => ''), SecretKey('polygonScanApiKey', () => ''), SecretKey('letsExchangeBearerToken', () => ''), SecretKey('letsExchangeAffiliateId', () => ''), SecretKey('stealthExBearerToken', () => ''), SecretKey('stealthExAdditionalFeePercent', () => ''), + SecretKey('moneroTestWalletBlockHeight', () => ''), ]; static final evmChainsSecrets = [ From 389c334f1076779a134cd037b2af5b1b1c11df2b Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Fri, 8 Nov 2024 12:50:21 +0200 Subject: [PATCH 50/73] Electrum enhancements (#1794) * Enhance the code for sending/sending-ALL for Electrum * remove prints [skip ci] --- cw_bitcoin/lib/electrum_wallet.dart | 159 +++++++++++----------------- cw_bitcoin/pubspec.lock | 4 +- 2 files changed, 63 insertions(+), 100 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 7988a4479..6245aa787 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -11,7 +11,6 @@ import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:collection/collection.dart'; import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; -import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; @@ -597,8 +596,8 @@ abstract class ElectrumWalletBase UtxoDetails _createUTXOS({ required bool sendAll, - required int credentialsAmount, required bool paysToSilentPayment, + int credentialsAmount = 0, int? inputsCount, UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) { @@ -732,13 +731,11 @@ abstract class ElectrumWalletBase List outputs, int feeRate, { String? memo, - int credentialsAmount = 0, bool hasSilentPayment = false, UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) async { final utxoDetails = _createUTXOS( sendAll: true, - credentialsAmount: credentialsAmount, paysToSilentPayment: hasSilentPayment, coinTypeToSpendFrom: coinTypeToSpendFrom, ); @@ -764,23 +761,11 @@ abstract class ElectrumWalletBase throw BitcoinTransactionWrongBalanceException(amount: utxoDetails.allInputsAmount + fee); } - if (amount <= 0) { - throw BitcoinTransactionWrongBalanceException(); - } - // Attempting to send less than the dust limit if (_isBelowDust(amount)) { throw BitcoinTransactionNoDustException(); } - if (credentialsAmount > 0) { - final amountLeftForFee = amount - credentialsAmount; - if (amountLeftForFee > 0 && _isBelowDust(amountLeftForFee)) { - amount -= amountLeftForFee; - fee += amountLeftForFee; - } - } - if (outputs.length == 1) { outputs[0] = BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount)); } @@ -810,6 +795,11 @@ abstract class ElectrumWalletBase bool hasSilentPayment = false, UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) async { + // Attempting to send less than the dust limit + if (_isBelowDust(credentialsAmount)) { + throw BitcoinTransactionNoDustException(); + } + final utxoDetails = _createUTXOS( sendAll: false, credentialsAmount: credentialsAmount, @@ -894,7 +884,43 @@ abstract class ElectrumWalletBase final lastOutput = updatedOutputs.last; final amountLeftForChange = amountLeftForChangeAndFee - fee; - if (!_isBelowDust(amountLeftForChange)) { + if (_isBelowDust(amountLeftForChange)) { + // If has change that is lower than dust, will end up with tx rejected by network rules + // so remove the change amount + updatedOutputs.removeLast(); + outputs.removeLast(); + + if (amountLeftForChange < 0) { + if (!spendingAllCoins) { + return estimateTxForAmount( + credentialsAmount, + outputs, + updatedOutputs, + feeRate, + inputsCount: utxoDetails.utxos.length + 1, + memo: memo, + useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, + hasSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, + ); + } else { + throw BitcoinTransactionWrongBalanceException(); + } + } + + return EstimatedTxResult( + utxos: utxoDetails.utxos, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + publicKeys: utxoDetails.publicKeys, + fee: fee, + amount: amount, + hasChange: false, + isSendAll: spendingAllCoins, + memo: memo, + spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, + spendsSilentPayment: utxoDetails.spendsSilentPayment, + ); + } else { // Here, lastOutput already is change, return the amount left without the fee to the user's address. updatedOutputs[updatedOutputs.length - 1] = BitcoinOutput( address: lastOutput.address, @@ -908,88 +934,20 @@ abstract class ElectrumWalletBase isSilentPayment: lastOutput.isSilentPayment, isChange: true, ); - } else { - // If has change that is lower than dust, will end up with tx rejected by network rules, so estimate again without the added change - updatedOutputs.removeLast(); - outputs.removeLast(); - // Still has inputs to spend before failing - if (!spendingAllCoins) { - return estimateTxForAmount( - credentialsAmount, - outputs, - updatedOutputs, - feeRate, - inputsCount: utxoDetails.utxos.length + 1, - memo: memo, - hasSilentPayment: hasSilentPayment, - useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, - coinTypeToSpendFrom: coinTypeToSpendFrom, - ); - } - - final estimatedSendAll = await estimateSendAllTx( - updatedOutputs, - feeRate, + return EstimatedTxResult( + utxos: utxoDetails.utxos, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + publicKeys: utxoDetails.publicKeys, + fee: fee, + amount: amount, + hasChange: true, + isSendAll: spendingAllCoins, memo: memo, - coinTypeToSpendFrom: coinTypeToSpendFrom, - ); - - if (estimatedSendAll.amount == credentialsAmount) { - return estimatedSendAll; - } - - // Estimate to user how much is needed to send to cover the fee - final maxAmountWithReturningChange = utxoDetails.allInputsAmount - _dustAmount - fee - 1; - throw BitcoinTransactionNoDustOnChangeException( - bitcoinAmountToString(amount: maxAmountWithReturningChange), - bitcoinAmountToString(amount: estimatedSendAll.amount), + spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, + spendsSilentPayment: utxoDetails.spendsSilentPayment, ); } - - // Attempting to send less than the dust limit - if (_isBelowDust(amount)) { - throw BitcoinTransactionNoDustException(); - } - - final totalAmount = amount + fee; - - if (totalAmount > (balance[currency]!.confirmed + balance[currency]!.secondConfirmed)) { - throw BitcoinTransactionWrongBalanceException(); - } - - if (totalAmount > utxoDetails.allInputsAmount) { - if (spendingAllCoins) { - throw BitcoinTransactionWrongBalanceException(); - } else { - updatedOutputs.removeLast(); - outputs.removeLast(); - return estimateTxForAmount( - credentialsAmount, - outputs, - updatedOutputs, - feeRate, - inputsCount: utxoDetails.utxos.length + 1, - memo: memo, - useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, - hasSilentPayment: hasSilentPayment, - coinTypeToSpendFrom: coinTypeToSpendFrom, - ); - } - } - - return EstimatedTxResult( - utxos: utxoDetails.utxos, - inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, - publicKeys: utxoDetails.publicKeys, - fee: fee, - amount: amount, - hasChange: true, - isSendAll: false, - memo: memo, - spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, - spendsSilentPayment: utxoDetails.spendsSilentPayment, - ); } Future calcFee({ @@ -1080,15 +1038,20 @@ abstract class ElectrumWalletBase : feeRate(transactionCredentials.priority!); EstimatedTxResult estimatedTx; - final updatedOutputs = - outputs.map((e) => BitcoinOutput(address: e.address, value: e.value)).toList(); + final updatedOutputs = outputs + .map((e) => BitcoinOutput( + address: e.address, + value: e.value, + isSilentPayment: e.isSilentPayment, + isChange: e.isChange, + )) + .toList(); if (sendAll) { estimatedTx = await estimateSendAllTx( updatedOutputs, feeRateInt, memo: memo, - credentialsAmount: credentialsAmount, hasSilentPayment: hasSilentPayment, coinTypeToSpendFrom: coinTypeToSpendFrom, ); diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 5cba9b734..0e4921a3a 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -568,10 +568,10 @@ packages: dependency: "direct main" description: name: ledger_flutter_plus - sha256: ea3ed586e1697776dacf42ac979095f1ca3bd143bf007cbe5c78e09cb6943f42 + sha256: c7b04008553193dbca7e17b430768eecc372a72b0ff3625b5e7fc5e5c8d3231b url: "https://pub.dev" source: hosted - version: "1.2.5" + version: "1.4.1" ledger_litecoin: dependency: "direct main" description: From 8e12fb1ad9c4475abb846937946fc7c9ede6daf8 Mon Sep 17 00:00:00 2001 From: Rafael Date: Sat, 9 Nov 2024 02:20:06 -0300 Subject: [PATCH 51/73] fix: paymentUrls is null (#1786) * fix: paymentUrls is null * feat: potential secure storage error --- lib/cake_pay/cake_pay_order.dart | 1 - lib/cake_pay/cake_pay_service.dart | 6 ++++-- .../cake_pay/cards/cake_pay_confirm_purchase_card_page.dart | 6 +++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/cake_pay/cake_pay_order.dart b/lib/cake_pay/cake_pay_order.dart index 8cb6e784a..acdf86ea0 100644 --- a/lib/cake_pay/cake_pay_order.dart +++ b/lib/cake_pay/cake_pay_order.dart @@ -1,4 +1,3 @@ - class CakePayOrder { final String orderId; final List cards; diff --git a/lib/cake_pay/cake_pay_service.dart b/lib/cake_pay/cake_pay_service.dart index 39f2ca77a..cf2ec254c 100644 --- a/lib/cake_pay/cake_pay_service.dart +++ b/lib/cake_pay/cake_pay_service.dart @@ -82,10 +82,12 @@ class CakePayService { } /// Logout - Future logout(String email) async { + Future logout([String? email]) async { await secureStorage.delete(key: cakePayUsernameStorageKey); await secureStorage.delete(key: cakePayUserTokenKey); - await cakePayApi.logoutUser(email: email, apiKey: cakePayApiKey); + if (email != null) { + await cakePayApi.logoutUser(email: email, apiKey: cakePayApiKey); + } } /// Purchase Gift Card 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 81f6a354f..309c435a2 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 @@ -258,7 +258,11 @@ class CakePayBuyCardDetailPage extends BasePage { if (!isLogged) { Navigator.of(context).pushNamed(Routes.cakePayWelcomePage); } else { - await cakePayPurchaseViewModel.createOrder(); + try { + await cakePayPurchaseViewModel.createOrder(); + } catch (_) { + await cakePayPurchaseViewModel.cakePayService.logout(); + } } } From cea3084bb320dde27fbdbb78fba579311b58b704 Mon Sep 17 00:00:00 2001 From: cyan Date: Sat, 9 Nov 2024 19:59:47 +0100 Subject: [PATCH 52/73] WIP: CW-665 Implement AirGapped Monero Transactions (#1535) * replace qr scanner with fast_scanner * bump java version * fix qr code scanning * add flashlight and camera switch * airgap work * commitTransactionUR * bump fast_scanner, fix configure script * add option to restore wallet from NERO qr format * fixes to the flow and logic use gsed or otherwise it fails? * remove Expanded() to fix URQR on release builds * cache key to allow app updates * rename cache key * [skip ci] cache key.jks in cache_dependencies * update fast_scanner to work on ios, with light mlkit dependency * ui fixes * error handling fix * update fast_scanner to drop iOS dependency * changes from review * Update lib/entities/qr_scanner.dart * changes from review * remove useless commit * don't set state multiple times remove return Container() for non monero wallets * return on fail don't handle empty qr codes * set node as trusted display primary address in seed screen * fix wow and haven * migrate node to trusted * - update trusted node for existing users - update locales - fix conflicts - move menu item --------- Co-authored-by: Omar Hatem --- .github/workflows/cache_dependencies.yml | 3 +- .github/workflows/pr_test_build_android.yml | 4 +- android/app/build.gradle | 5 + android/gradle.properties | 2 +- assets/node_list.yml | 1 + .../lib/pending_bitcoin_transaction.dart | 5 + .../src/pending_bitcoin_cash_transaction.dart | 4 + cw_core/lib/monero_wallet_keys.dart | 4 +- cw_core/lib/pending_transaction.dart | 3 + cw_evm/lib/pending_evm_chain_transaction.dart | 5 + .../wallet_restore_from_keys_exception.dart | 5 + cw_haven/lib/haven_wallet.dart | 1 + cw_haven/lib/pending_haven_transaction.dart | 5 + cw_monero/lib/api/account_list.dart | 1 + cw_monero/lib/api/transaction_history.dart | 24 +- cw_monero/lib/api/wallet_manager.dart | 2 + cw_monero/lib/monero_wallet.dart | 32 ++ cw_monero/lib/pending_monero_transaction.dart | 24 +- cw_nano/lib/pending_nano_transaction.dart | 5 + cw_shared_external/pubspec.lock | 189 +++++++++ cw_solana/lib/pending_solana_transaction.dart | 5 + cw_tron/lib/pending_tron_transaction.dart | 5 + .../wallet_restore_from_keys_exception.dart | 1 + .../lib/pending_wownero_transaction.dart | 5 + cw_wownero/lib/wownero_wallet.dart | 1 + lib/di.dart | 7 + lib/entities/default_settings_migration.dart | 15 +- lib/entities/qr_scanner.dart | 369 +++++++++++++++++- lib/main.dart | 2 +- lib/monero/cw_monero.dart | 24 ++ lib/router.dart | 4 + lib/routes.dart | 1 + .../cake_pay_confirm_purchase_card_page.dart | 4 +- .../dashboard/widgets/menu_widget.dart | 4 +- .../exchange_trade/exchange_trade_page.dart | 2 +- lib/src/screens/send/send_page.dart | 2 +- .../desktop_settings_page.dart | 7 +- .../transaction_details/rbf_details_page.dart | 2 +- lib/src/screens/ur/animated_ur_page.dart | 184 +++++++++ .../wc_connections_listing_view.dart | 2 +- lib/src/widgets/address_text_field.dart | 2 +- lib/src/widgets/setting_actions.dart | 11 + lib/view_model/animated_ur_model.dart | 10 + .../dashboard/dashboard_view_model.dart | 6 + .../node_create_or_edit_view_model.dart | 2 +- lib/view_model/restore/restore_wallet.dart | 14 +- .../restore/wallet_restore_from_qr_code.dart | 28 +- lib/view_model/send/send_view_model.dart | 18 +- lib/view_model/wallet_keys_view_model.dart | 12 + linux/flutter/generated_plugins.cmake | 1 - macos/Flutter/GeneratedPluginRegistrant.swift | 4 +- macos/Podfile.lock | 6 + pubspec_base.yaml | 6 +- res/values/strings_ar.arb | 13 +- res/values/strings_bg.arb | 13 +- res/values/strings_cs.arb | 13 +- res/values/strings_de.arb | 8 +- res/values/strings_en.arb | 10 +- res/values/strings_es.arb | 13 +- res/values/strings_fr.arb | 13 +- res/values/strings_ha.arb | 13 +- res/values/strings_hi.arb | 13 +- res/values/strings_hr.arb | 13 +- res/values/strings_hy.arb | 16 +- res/values/strings_id.arb | 13 +- res/values/strings_it.arb | 13 +- res/values/strings_ja.arb | 13 +- res/values/strings_ko.arb | 13 +- res/values/strings_my.arb | 13 +- res/values/strings_nl.arb | 13 +- res/values/strings_pl.arb | 13 +- res/values/strings_pt.arb | 10 +- res/values/strings_ru.arb | 13 +- res/values/strings_th.arb | 13 +- res/values/strings_tl.arb | 10 +- res/values/strings_tr.arb | 13 +- res/values/strings_uk.arb | 13 +- res/values/strings_ur.arb | 13 +- res/values/strings_vi.arb | 16 +- res/values/strings_yo.arb | 13 +- res/values/strings_zh.arb | 13 +- scripts/macos/gen_common.sh | 4 +- tool/configure.dart | 8 + windows/flutter/generated_plugins.cmake | 1 - 84 files changed, 1318 insertions(+), 141 deletions(-) create mode 100644 cw_shared_external/pubspec.lock create mode 100644 lib/src/screens/ur/animated_ur_page.dart create mode 100644 lib/view_model/animated_ur_model.dart diff --git a/.github/workflows/cache_dependencies.yml b/.github/workflows/cache_dependencies.yml index c0042bf5c..0fcd5c45b 100644 --- a/.github/workflows/cache_dependencies.yml +++ b/.github/workflows/cache_dependencies.yml @@ -62,10 +62,9 @@ jobs: /opt/android/cake_wallet/cw_haven/android/.cxx /opt/android/cake_wallet/scripts/monero_c/release key: ${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh' ,'**/cache_dependencies.yml') }} - - if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }} name: Generate Externals run: | cd /opt/android/cake_wallet/scripts/android/ source ./app_env.sh cakewallet - ./build_monero_all.sh + ./build_monero_all.sh \ No newline at end of file diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index c9021fac0..6d0e541ce 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -82,7 +82,8 @@ jobs: path: | /opt/android/cake_wallet/cw_haven/android/.cxx /opt/android/cake_wallet/scripts/monero_c/release - key: ${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh' ,'**/cache_dependencies.yml') }} + /opt/android/cake_wallet/android/app/key.jks + key: android_${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh' ,'**/cache_dependencies.yml') }} - if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }} name: Generate Externals @@ -116,6 +117,7 @@ jobs: ./build_mwebd.sh --dont-install - name: Generate KeyStore + if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }} run: | cd /opt/android/cake_wallet/android/app 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 $STORE_PASS -keypass $KEY_PASS diff --git a/android/app/build.gradle b/android/app/build.gradle index 2f5427531..5005a8bab 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -92,3 +92,8 @@ dependencies { androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' } +configurations { + implementation.exclude module:'proto-google-common-protos' + implementation.exclude module:'protolite-well-known-types' + implementation.exclude module:'protobuf-javalite' +} \ No newline at end of file diff --git a/android/gradle.properties b/android/gradle.properties index 38c8d4544..66dd09454 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx3072M android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/assets/node_list.yml b/assets/node_list.yml index d04a9a2e8..6191129b3 100644 --- a/assets/node_list.yml +++ b/assets/node_list.yml @@ -1,6 +1,7 @@ - uri: xmr-node.cakewallet.com:18081 is_default: true + trusted: true - uri: cakexmrl7bonq7ovjka5kuwuyd3f7qnkz6z6s6dmsy3uckwra7bvggyd.onion:18081 is_default: false diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 140965655..411c7de16 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -153,4 +153,9 @@ class PendingBitcoinTransaction with PendingTransaction { inputAddresses: _tx.inputs.map((input) => input.txId).toList(), outputAddresses: outputAddresses, fee: fee); + + @override + Future commitUR() { + throw UnimplementedError(); + } } diff --git a/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart b/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart index e1fa9d6e0..05483ce54 100644 --- a/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart +++ b/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart @@ -85,4 +85,8 @@ class PendingBitcoinCashTransaction with PendingTransaction { fee: fee, isReplaced: false, ); + @override + Future commitUR() { + throw UnimplementedError(); + } } diff --git a/cw_core/lib/monero_wallet_keys.dart b/cw_core/lib/monero_wallet_keys.dart index 1435002a8..fe3784986 100644 --- a/cw_core/lib/monero_wallet_keys.dart +++ b/cw_core/lib/monero_wallet_keys.dart @@ -1,10 +1,12 @@ class MoneroWalletKeys { const MoneroWalletKeys( - {required this.privateSpendKey, + {required this.primaryAddress, + required this.privateSpendKey, required this.privateViewKey, required this.publicSpendKey, required this.publicViewKey}); + final String primaryAddress; final String publicViewKey; final String privateViewKey; final String publicSpendKey; diff --git a/cw_core/lib/pending_transaction.dart b/cw_core/lib/pending_transaction.dart index 0a6103a5f..78eba68a3 100644 --- a/cw_core/lib/pending_transaction.dart +++ b/cw_core/lib/pending_transaction.dart @@ -14,5 +14,8 @@ mixin PendingTransaction { int? get outputCount => null; PendingChange? change; + bool shouldCommitUR() => false; + Future commit(); + Future commitUR(); } diff --git a/cw_evm/lib/pending_evm_chain_transaction.dart b/cw_evm/lib/pending_evm_chain_transaction.dart index 0b367da68..6861b41f8 100644 --- a/cw_evm/lib/pending_evm_chain_transaction.dart +++ b/cw_evm/lib/pending_evm_chain_transaction.dart @@ -47,4 +47,9 @@ class PendingEVMChainTransaction with PendingTransaction { return '0x${Hex.HEX.encode(txid)}'; } + + @override + Future commitUR() { + throw UnimplementedError(); + } } 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 index c6b6c6ef7..3ff5f2438 100644 --- a/cw_haven/lib/api/exceptions/wallet_restore_from_keys_exception.dart +++ b/cw_haven/lib/api/exceptions/wallet_restore_from_keys_exception.dart @@ -2,4 +2,9 @@ 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/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart index 06a838100..e2e598bbe 100644 --- a/cw_haven/lib/haven_wallet.dart +++ b/cw_haven/lib/haven_wallet.dart @@ -73,6 +73,7 @@ abstract class HavenWalletBase @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(), diff --git a/cw_haven/lib/pending_haven_transaction.dart b/cw_haven/lib/pending_haven_transaction.dart index 1d95de08c..dbf075044 100644 --- a/cw_haven/lib/pending_haven_transaction.dart +++ b/cw_haven/lib/pending_haven_transaction.dart @@ -48,4 +48,9 @@ class PendingHavenTransaction with PendingTransaction { rethrow; } } + + @override + Future commitUR() { + throw UnimplementedError(); + } } diff --git a/cw_monero/lib/api/account_list.dart b/cw_monero/lib/api/account_list.dart index 7cb95a507..e3bb25c97 100644 --- a/cw_monero/lib/api/account_list.dart +++ b/cw_monero/lib/api/account_list.dart @@ -2,6 +2,7 @@ import 'package:cw_monero/api/wallet.dart'; import 'package:monero/monero.dart' as monero; monero.wallet? wptr = null; +bool get isViewOnly => int.tryParse(monero.Wallet_secretSpendKey(wptr!)) == 0; int _wlptrForW = 0; monero.WalletListener? _wlptr = null; diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index a308b682e..7748a16af 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -13,7 +13,13 @@ import 'package:mutex/mutex.dart'; String getTxKey(String txId) { - return monero.Wallet_getTxKey(wptr!, txid: txId); + final txKey = monero.Wallet_getTxKey(wptr!, txid: txId); + final status = monero.Wallet_status(wptr!); + if (status != 0) { + final error = monero.Wallet_errorString(wptr!); + return txId+"_"+error; + } + return txKey; } final txHistoryMutex = Mutex(); monero.TransactionHistory? txhistory; @@ -178,12 +184,13 @@ PendingTransactionDescription createTransactionMultDestSync( ); } -void commitTransactionFromPointerAddress({required int address}) => - commitTransaction(transactionPointer: monero.PendingTransaction.fromAddress(address)); +String? commitTransactionFromPointerAddress({required int address, required bool useUR}) => + commitTransaction(transactionPointer: monero.PendingTransaction.fromAddress(address), useUR: useUR); -void commitTransaction({required monero.PendingTransaction transactionPointer}) { - - final txCommit = monero.PendingTransaction_commit(transactionPointer, filename: '', overwrite: false); +String? commitTransaction({required monero.PendingTransaction transactionPointer, required bool useUR}) { + final txCommit = useUR + ? monero.PendingTransaction_commitUR(transactionPointer, 120) + : monero.PendingTransaction_commit(transactionPointer, filename: '', overwrite: false); final String? error = (() { final status = monero.PendingTransaction_status(transactionPointer.cast()); @@ -196,6 +203,11 @@ void commitTransaction({required monero.PendingTransaction transactionPointer}) if (error != null) { throw CreationTransactionException(message: error); } + if (useUR) { + return txCommit as String?; + } else { + return null; + } } Future _createTransactionSync(Map args) async { diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart index dca7586fb..61fd6edf0 100644 --- a/cw_monero/lib/api/wallet_manager.dart +++ b/cw_monero/lib/api/wallet_manager.dart @@ -425,3 +425,5 @@ Future restoreFromSpendKey( }); bool isWalletExist({required String path}) => _isWalletExist(path); + +bool isViewOnlyBySpendKey() => int.tryParse(monero.Wallet_secretSpendKey(wptr!)) == 0; \ No newline at end of file diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 0ae2202ba..bc8c3c94d 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -19,6 +19,7 @@ import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/coins_info.dart'; import 'package:cw_monero/api/monero_output.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; @@ -121,6 +122,7 @@ abstract class MoneroWalletBase extends WalletBase MoneroWalletKeys( + primaryAddress: monero_wallet.getAddress(accountIndex: 0, addressIndex: 0), privateSpendKey: monero_wallet.getSecretSpendKey(), privateViewKey: monero_wallet.getSecretViewKey(), publicSpendKey: monero_wallet.getPublicSpendKey(), @@ -230,6 +232,36 @@ abstract class MoneroWalletBase extends WalletBase submitTransactionUR(String ur) async { + final retStatus = monero.Wallet_submitTransactionUR(wptr!, ur); + final status = monero.Wallet_status(wptr!); + if (status != 0) { + final err = monero.Wallet_errorString(wptr!); + throw MoneroTransactionCreationException("unable to broadcast signed transaction: $err"); + } + return retStatus; + } + + bool importKeyImagesUR(String ur) { + final retStatus = monero.Wallet_importKeyImagesUR(wptr!, ur); + final status = monero.Wallet_status(wptr!); + if (status != 0) { + final err = monero.Wallet_errorString(wptr!); + throw Exception("unable to import key images: $err"); + } + return retStatus; + } + + String exportOutputsUR(bool all) { + final str = monero.Wallet_exportOutputsUR(wptr!, all: all); + final status = monero.Wallet_status(wptr!); + if (status != 0) { + final err = monero.Wallet_errorString(wptr!); + throw MoneroTransactionCreationException("unable to export UR: $err"); + } + return str; + } + @override Future createTransaction(Object credentials) async { final _credentials = credentials as MoneroTransactionCreationCredentials; diff --git a/cw_monero/lib/pending_monero_transaction.dart b/cw_monero/lib/pending_monero_transaction.dart index 2a75fc26b..eb714eeb3 100644 --- a/cw_monero/lib/pending_monero_transaction.dart +++ b/cw_monero/lib/pending_monero_transaction.dart @@ -1,3 +1,4 @@ +import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; import 'package:cw_monero/api/transaction_history.dart' as monero_transaction_history; @@ -35,11 +36,32 @@ class PendingMoneroTransaction with PendingTransaction { String get feeFormatted => AmountConverter.amountIntToString( CryptoCurrency.xmr, pendingTransactionDescription.fee); + bool shouldCommitUR() => isViewOnly; + @override Future commit() async { try { monero_transaction_history.commitTransactionFromPointerAddress( - address: pendingTransactionDescription.pointerAddress); + address: pendingTransactionDescription.pointerAddress, + useUR: false); + } catch (e) { + final message = e.toString(); + + if (message.contains('Reason: double spend')) { + throw DoubleSpendException(); + } + + rethrow; + } + } + + @override + Future commitUR() async { + try { + final ret = monero_transaction_history.commitTransactionFromPointerAddress( + address: pendingTransactionDescription.pointerAddress, + useUR: true); + return ret; } catch (e) { final message = e.toString(); diff --git a/cw_nano/lib/pending_nano_transaction.dart b/cw_nano/lib/pending_nano_transaction.dart index a027100fd..51a4ef6c1 100644 --- a/cw_nano/lib/pending_nano_transaction.dart +++ b/cw_nano/lib/pending_nano_transaction.dart @@ -37,4 +37,9 @@ class PendingNanoTransaction with PendingTransaction { await nanoClient.processBlock(block, "send"); } } + + @override + Future commitUR() { + throw UnimplementedError(); + } } diff --git a/cw_shared_external/pubspec.lock b/cw_shared_external/pubspec.lock new file mode 100644 index 000000000..203ecd5c3 --- /dev/null +++ b/cw_shared_external/pubspec.lock @@ -0,0 +1,189 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + 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" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + url: "https://pub.dev" + source: hosted + version: "10.0.4" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + 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: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + meta: + dependency: transitive + description: + name: meta + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + url: "https://pub.dev" + source: hosted + version: "1.12.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + 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" + 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: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + url: "https://pub.dev" + source: hosted + version: "0.7.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: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + url: "https://pub.dev" + source: hosted + version: "14.2.1" +sdks: + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/cw_solana/lib/pending_solana_transaction.dart b/cw_solana/lib/pending_solana_transaction.dart index 38347ed13..e01446000 100644 --- a/cw_solana/lib/pending_solana_transaction.dart +++ b/cw_solana/lib/pending_solana_transaction.dart @@ -40,4 +40,9 @@ class PendingSolanaTransaction with PendingTransaction { @override String get id => ''; + + @override + Future commitUR() { + throw UnimplementedError(); + } } diff --git a/cw_tron/lib/pending_tron_transaction.dart b/cw_tron/lib/pending_tron_transaction.dart index b6d064b31..2420f083b 100644 --- a/cw_tron/lib/pending_tron_transaction.dart +++ b/cw_tron/lib/pending_tron_transaction.dart @@ -30,4 +30,9 @@ class PendingTronTransaction with PendingTransaction { @override String get id => ''; + + @override + Future commitUR() { + throw UnimplementedError(); + } } diff --git a/cw_wownero/lib/api/exceptions/wallet_restore_from_keys_exception.dart b/cw_wownero/lib/api/exceptions/wallet_restore_from_keys_exception.dart index ad576faa2..6c461ee4c 100644 --- a/cw_wownero/lib/api/exceptions/wallet_restore_from_keys_exception.dart +++ b/cw_wownero/lib/api/exceptions/wallet_restore_from_keys_exception.dart @@ -3,5 +3,6 @@ class WalletRestoreFromKeysException implements Exception { final String message; + @override String toString() => message; } \ No newline at end of file diff --git a/cw_wownero/lib/pending_wownero_transaction.dart b/cw_wownero/lib/pending_wownero_transaction.dart index 1fc1805eb..967f63756 100644 --- a/cw_wownero/lib/pending_wownero_transaction.dart +++ b/cw_wownero/lib/pending_wownero_transaction.dart @@ -50,4 +50,9 @@ class PendingWowneroTransaction with PendingTransaction { rethrow; } } + + @override + Future commitUR() { + throw UnimplementedError(); + } } diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index 331957d67..5927d6434 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -120,6 +120,7 @@ abstract class WowneroWalletBase @override MoneroWalletKeys get keys => MoneroWalletKeys( + primaryAddress: wownero_wallet.getAddress(accountIndex: 0, addressIndex: 0), privateSpendKey: wownero_wallet.getSecretSpendKey(), privateViewKey: wownero_wallet.getSecretViewKey(), publicSpendKey: wownero_wallet.getPublicSpendKey(), diff --git a/lib/di.dart b/lib/di.dart index 540a546fd..9cff29798 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -125,6 +125,7 @@ import 'package:cake_wallet/src/screens/subaddress/address_edit_or_create_page.d import 'package:cake_wallet/src/screens/support/support_page.dart'; import 'package:cake_wallet/src/screens/support_chat/support_chat_page.dart'; import 'package:cake_wallet/src/screens/support_other_links/support_other_links_page.dart'; +import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart'; import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart'; import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart'; import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; @@ -134,6 +135,7 @@ import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cake_wallet/view_model/animated_ur_model.dart'; import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart'; import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart'; import 'package:cake_wallet/view_model/anonpay_details_view_model.dart'; @@ -903,6 +905,11 @@ Future setup({ getIt.registerFactory(() => WalletKeysViewModel(getIt.get())); getIt.registerFactory(() => WalletKeysPage(getIt.get())); + + getIt.registerFactory(() => AnimatedURModel(getIt.get())); + + getIt.registerFactoryParam((String urQr, _) => + AnimatedURPage(getIt.get(), urQr: urQr)); getIt.registerFactoryParam( (ContactRecord? contact, _) => ContactViewModel(_contactSource, contact: contact)); diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index af6f1ce78..d863b136e 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -259,6 +259,10 @@ Future defaultSettingsMigration( case 42: updateBtcElectrumNodeToUseSSL(nodes, sharedPreferences); break; + case 43: + _updateCakeXmrNode(nodes); + break; + default: break; } @@ -273,6 +277,15 @@ Future defaultSettingsMigration( await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, version); } +void _updateCakeXmrNode(Box nodes) { + final node = nodes.values.firstWhereOrNull((element) => element.uriRaw == newCakeWalletMoneroUri); + + if (node != null && !node.trusted) { + node.trusted = true; + node.save(); + } +} + void updateBtcElectrumNodeToUseSSL(Box nodes, SharedPreferences sharedPreferences) { final btcElectrumNode = nodes.values.firstWhereOrNull((element) => element.uriRaw == newCakeWalletBitcoinUri); @@ -843,7 +856,7 @@ Future changeDefaultMoneroNode( } }); - final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero); + final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero, trusted: true); await nodeSource.add(newCakeWalletNode); diff --git a/lib/entities/qr_scanner.dart b/lib/entities/qr_scanner.dart index c6873a5bc..2e2637566 100644 --- a/lib/entities/qr_scanner.dart +++ b/lib/entities/qr_scanner.dart @@ -1,15 +1,376 @@ -import 'package:barcode_scan2/barcode_scan2.dart'; +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:fast_scanner/fast_scanner.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; var isQrScannerShown = false; -Future presentQRScanner() async { +Future presentQRScanner(BuildContext context) async { isQrScannerShown = true; try { - final result = await BarcodeScanner.scan(); + final result = await Navigator.of(context).push( + MaterialPageRoute( + builder:(context) { + return BarcodeScannerSimple(); + }, + ), + ); isQrScannerShown = false; - return result.rawContent.trim(); + return result??''; } catch (e) { isQrScannerShown = false; rethrow; } } + +// https://github.com/MrCyjaneK/fast_scanner/blob/master/example/lib/barcode_scanner_simple.dart +class BarcodeScannerSimple extends StatefulWidget { + const BarcodeScannerSimple({super.key}); + + @override + State createState() => _BarcodeScannerSimpleState(); +} + +class _BarcodeScannerSimpleState extends State { + Barcode? _barcode; + bool popped = false; + + List urCodes = []; + late var ur = URQRToURQRData(urCodes); + + void _handleBarcode(BarcodeCapture barcodes) { + try { + _handleBarcodeInternal(barcodes); + } catch (e) { + showPopUp( + context: context, + builder: (context) { + return AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: S.of(context).error_dialog_content, + buttonText: 'ok', + buttonAction: () { + Navigator.of(context).pop(); + }, + ); + }, + ); + print(e); + } + } + + void _handleBarcodeInternal(BarcodeCapture barcodes) { + for (final barcode in barcodes.barcodes) { + // don't handle unknown QR codes + if (barcode.rawValue?.trim().isEmpty??false == false) continue; + if (barcode.rawValue!.startsWith("ur:")) { + if (urCodes.contains(barcode.rawValue)) continue; + setState(() { + urCodes.add(barcode.rawValue!); + ur = URQRToURQRData(urCodes); + }); + if (ur.progress == 1) { + setState(() { + popped = true; + }); + SchedulerBinding.instance.addPostFrameCallback((_) { + Navigator.of(context).pop(ur.inputs.join("\n")); + }); + }; + } + } + if (urCodes.isNotEmpty) return; + if (mounted) { + setState(() { + _barcode = barcodes.barcodes.firstOrNull; + }); + if (_barcode != null && popped != true) { + setState(() { + popped = true; + }); + SchedulerBinding.instance.addPostFrameCallback((_) { + Navigator.of(context).pop(_barcode?.rawValue ?? ""); + }); + } + } + } + + final MobileScannerController ctrl = MobileScannerController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Scan'), + actions: [ + SwitchCameraButton(controller: ctrl), + ToggleFlashlightButton(controller: ctrl), + ], + ), + backgroundColor: Colors.black, + body: Stack( + children: [ + MobileScanner( + onDetect: _handleBarcode, + controller: ctrl, + ), + if (ur.inputs.length != 0) + Center(child: + Text( + "${ur.inputs.length}/${ur.count}", + style: Theme.of(context).textTheme.displayLarge?.copyWith(color: Colors.white) + ), + ), + SizedBox( + child: Center( + child: SizedBox( + width: 250, + height: 250, + child: CustomPaint( + painter: ProgressPainter( + urQrProgress: URQrProgress( + expectedPartCount: ur.count - 1, + processedPartsCount: ur.inputs.length, + receivedPartIndexes: _urParts(), + percentage: ur.progress, + ), + ), + ), + ), + ), + ), + ], + ), + ); + } + + List _urParts() { + List l = []; + for (var inp in ur.inputs) { + try { + l.add(int.parse(inp.split("/")[1].split("-")[0])); + } catch (e) {} + } + return l; + } +} + + +class ToggleFlashlightButton extends StatelessWidget { + const ToggleFlashlightButton({required this.controller, super.key}); + + final MobileScannerController controller; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, state, child) { + if (!state.isInitialized || !state.isRunning) { + return const SizedBox.shrink(); + } + + switch (state.torchState) { + case TorchState.auto: + return IconButton( + iconSize: 32.0, + icon: const Icon(Icons.flash_auto), + onPressed: () async { + await controller.toggleTorch(); + }, + ); + case TorchState.off: + return IconButton( + iconSize: 32.0, + icon: const Icon(Icons.flash_off), + onPressed: () async { + await controller.toggleTorch(); + }, + ); + case TorchState.on: + return IconButton( + iconSize: 32.0, + icon: const Icon(Icons.flash_on), + onPressed: () async { + await controller.toggleTorch(); + }, + ); + case TorchState.unavailable: + return const Icon( + Icons.no_flash, + color: Colors.grey, + ); + } + }, + ); + } +} + +class SwitchCameraButton extends StatelessWidget { + const SwitchCameraButton({required this.controller, super.key}); + + final MobileScannerController controller; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, state, child) { + if (!state.isInitialized || !state.isRunning) { + return const SizedBox.shrink(); + } + + final int? availableCameras = state.availableCameras; + + if (availableCameras != null && availableCameras < 2) { + return const SizedBox.shrink(); + } + + final Widget icon; + + switch (state.cameraDirection) { + case CameraFacing.front: + icon = const Icon(Icons.camera_front); + case CameraFacing.back: + icon = const Icon(Icons.camera_rear); + } + + return IconButton( + iconSize: 32.0, + icon: icon, + onPressed: () async { + await controller.switchCamera(); + }, + ); + }, + ); + } +} + +class URQRData { + URQRData( + {required this.tag, + required this.str, + required this.progress, + required this.count, + required this.error, + required this.inputs}); + final String tag; + final String str; + final double progress; + final int count; + final String error; + final List inputs; + Map toJson() { + return { + "tag": tag, + "str": str, + "progress": progress, + "count": count, + "error": error, + "inputs": inputs, + }; + } +} + +URQRData URQRToURQRData(List urqr_) { + final urqr = urqr_.toSet().toList(); + urqr.sort((s1, s2) { + final s1s = s1.split("/"); + final s1frameStr = s1s[1].split("-"); + final s1curFrame = int.parse(s1frameStr[0]); + final s2s = s2.split("/"); + final s2frameStr = s2s[1].split("-"); + final s2curFrame = int.parse(s2frameStr[0]); + return s1curFrame - s2curFrame; + }); + + String tag = ''; + int count = 0; + String bw = ''; + for (var elm in urqr) { + final s = elm.substring(elm.indexOf(":") + 1); // strip down ur: prefix + final s2 = s.split("/"); + tag = s2[0]; + final frameStr = s2[1].split("-"); + // final curFrame = int.parse(frameStr[0]); + count = int.parse(frameStr[1]); + final byteWords = s2[2]; + bw += byteWords; + } + String? error; + + return URQRData( + tag: tag, + str: bw, + progress: count == 0 ? 0 : (urqr.length / count), + count: count, + error: error ?? "", + inputs: urqr, + ); +} + +class ProgressPainter extends CustomPainter { + final URQrProgress urQrProgress; + + ProgressPainter({required this.urQrProgress}); + + @override + void paint(Canvas canvas, Size size) { + final c = Offset(size.width / 2.0, size.height / 2.0); + final radius = size.width * 0.9; + final rect = Rect.fromCenter(center: c, width: radius, height: radius); + const fullAngle = 360.0; + var startAngle = 0.0; + for (int i = 0; i < urQrProgress.expectedPartCount.toInt(); i++) { + var sweepAngle = + (1 / urQrProgress.expectedPartCount) * fullAngle * pi / 180.0; + drawSector(canvas, urQrProgress.receivedPartIndexes.contains(i), rect, + startAngle, sweepAngle); + startAngle += sweepAngle; + } + } + + void drawSector(Canvas canvas, bool isActive, Rect rect, double startAngle, + double sweepAngle) { + final paint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 8 + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round + ..color = isActive ? const Color(0xffff6600) : Colors.white70; + canvas.drawArc(rect, startAngle, sweepAngle, false, paint); + } + + @override + bool shouldRepaint(covariant ProgressPainter oldDelegate) { + return urQrProgress != oldDelegate.urQrProgress; + } +} + +class URQrProgress { + int expectedPartCount; + int processedPartsCount; + List receivedPartIndexes; + double percentage; + + URQrProgress({ + required this.expectedPartCount, + required this.processedPartsCount, + required this.receivedPartIndexes, + required this.percentage, + }); + + bool equals(URQrProgress? progress) { + if (progress == null) { + return false; + } + return processedPartsCount == progress.processedPartsCount; + } +} diff --git a/lib/main.dart b/lib/main.dart index 29b216b22..51fab4dd1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -204,7 +204,7 @@ Future initializeAppConfigs() async { transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 42, + initialMigrationVersion: 43, ); } diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index b1cf49482..dfda4d600 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -248,6 +248,7 @@ class CWMonero extends Monero { final moneroWallet = wallet as MoneroWallet; final keys = moneroWallet.keys; return { + 'primaryAddress': keys.primaryAddress, 'privateSpendKey': keys.privateSpendKey, 'privateViewKey': keys.privateViewKey, 'publicSpendKey': keys.publicSpendKey, @@ -357,9 +358,32 @@ class CWMonero extends Monero { Future getCurrentHeight() async { return monero_wallet_api.getCurrentHeight(); } + + @override + bool importKeyImagesUR(Object wallet, String ur) { + final moneroWallet = wallet as MoneroWallet; + return moneroWallet.importKeyImagesUR(ur); + } + + + @override + Future commitTransactionUR(Object wallet, String ur) { + final moneroWallet = wallet as MoneroWallet; + return moneroWallet.submitTransactionUR(ur); + } + + @override + String exportOutputsUR(Object wallet, bool all) { + final moneroWallet = wallet as MoneroWallet; + return moneroWallet.exportOutputsUR(all); + } @override void monerocCheck() { checkIfMoneroCIsFine(); } + + bool isViewOnly() { + return isViewOnlyBySpendKey(); + } } diff --git a/lib/router.dart b/lib/router.dart index 6c234ff80..4b99aabc0 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -96,6 +96,7 @@ import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dar import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; +import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart'; import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart'; import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart'; import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart'; @@ -732,6 +733,9 @@ Route createRoute(RouteSettings settings) { case Routes.setup2faInfoPage: return MaterialPageRoute(builder: (_) => getIt.get()); + case Routes.urqrAnimatedPage: + return MaterialPageRoute(builder: (_) => getIt.get(param1: settings.arguments)); + case Routes.homeSettings: return CupertinoPageRoute( builder: (_) => getIt.get(param1: settings.arguments), diff --git a/lib/routes.dart b/lib/routes.dart index 5f11b11a3..63c41bde5 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -108,6 +108,7 @@ class Routes { static const signPage = '/sign_page'; static const connectDevices = '/device/connect'; + static const urqrAnimatedPage = '/urqr/animated_page'; static const walletGroupsDisplayPage = '/wallet_groups_display_page'; static const walletGroupDescription = '/wallet_group_description'; } 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 309c435a2..9357df2c3 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 @@ -347,8 +347,8 @@ class CakePayBuyCardDetailPage extends BasePage { rightButtonText: S.of(popupContext).send, leftButtonText: S.of(popupContext).cancel, actionRightButton: () async { - Navigator.of(popupContext).pop(); - await cakePayPurchaseViewModel.sendViewModel.commitTransaction(); + Navigator.of(context).pop(); + await cakePayPurchaseViewModel.sendViewModel.commitTransaction(context); }, actionLeftButton: () => Navigator.of(popupContext).pop())); }, diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index f0e330f04..b49a08584 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -101,6 +101,9 @@ class MenuWidgetState extends State { if (!widget.dashboardViewModel.hasSilentPayments) { items.removeWhere((element) => element.name(context) == S.of(context).silent_payments_settings); } + if (!widget.dashboardViewModel.isMoneroViewOnly) { + items.removeWhere((element) => element.name(context) == S.of(context).export_outputs); + } if (!widget.dashboardViewModel.hasMweb) { items.removeWhere((element) => element.name(context) == S.of(context).litecoin_mweb_settings); } @@ -189,7 +192,6 @@ class MenuWidgetState extends State { index--; final item = items[index]; - final isLastTile = index == itemCount - 1; return SettingActionButton( diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index 0f3cc7bd9..130ffa6b0 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -277,7 +277,7 @@ class ExchangeTradeState extends State { actionRightButton: () async { Navigator.of(popupContext).pop(); await widget.exchangeTradeViewModel.sendViewModel - .commitTransaction(); + .commitTransaction(context); transactionStatePopup(); }, actionLeftButton: () => Navigator.of(popupContext).pop(), diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index bce82312d..7003ceafb 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -498,7 +498,7 @@ class SendPage extends BasePage { ValueKey('send_page_confirm_sending_dialog_cancel_button_key'), actionRightButton: () async { Navigator.of(_dialogContext).pop(); - sendViewModel.commitTransaction(); + sendViewModel.commitTransaction(context); await showPopUp( context: context, builder: (BuildContext _dialogContext) { 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 79f74065a..6f7afe2ff 100644 --- a/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart +++ b/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/src/widgets/setting_action_button.dart'; import 'package:cake_wallet/src/widgets/setting_actions.dart'; import 'package:cake_wallet/typography.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/router.dart' as Router; import 'package:cake_wallet/themes/extensions/menu_theme.dart'; @@ -60,8 +61,10 @@ class _DesktopSettingsPageState extends State { return Container(); } - if (!widget.dashboardViewModel.hasMweb && - item.name(context) == S.of(context).litecoin_mweb_settings) { + if ((!widget.dashboardViewModel.isMoneroViewOnly && + item.name(context) == S.of(context).export_outputs) || + (!widget.dashboardViewModel.hasMweb && + item.name(context) == S.of(context).litecoin_mweb_settings)) { return Container(); } diff --git a/lib/src/screens/transaction_details/rbf_details_page.dart b/lib/src/screens/transaction_details/rbf_details_page.dart index b117a0b68..2c5edd8b4 100644 --- a/lib/src/screens/transaction_details/rbf_details_page.dart +++ b/lib/src/screens/transaction_details/rbf_details_page.dart @@ -168,7 +168,7 @@ class RBFDetailsPage extends BasePage { leftButtonText: S.of(popupContext).cancel, actionRightButton: () async { Navigator.of(popupContext).pop(); - await transactionDetailsViewModel.sendViewModel.commitTransaction(); + await transactionDetailsViewModel.sendViewModel.commitTransaction(context); try { Navigator.of(popupContext).pop(); } catch (_) {} diff --git a/lib/src/screens/ur/animated_ur_page.dart b/lib/src/screens/ur/animated_ur_page.dart new file mode 100644 index 000000000..dc40e6a12 --- /dev/null +++ b/lib/src/screens/ur/animated_ur_page.dart @@ -0,0 +1,184 @@ +import 'dart:async'; + +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/qr_scanner.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/receive/widgets/qr_image.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/primary_button.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/animated_ur_model.dart'; +import 'package:cake_wallet/view_model/dashboard/wallet_balance.dart'; +import 'package:cw_core/balance.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +// ur:xmr-txunsigned - unsigned transaction +// should show a scanner afterwards. + +class AnimatedURPage extends BasePage { + final bool isAll; + AnimatedURPage(this.animatedURmodel, {required String urQr, this.isAll = false}) { + if (urQr == "export-outputs") { + this.urQr = monero!.exportOutputsUR(animatedURmodel.wallet, false); + } else if (urQr == "export-outputs-all") { + this.urQr = monero!.exportOutputsUR(animatedURmodel.wallet, true); + } else { + this.urQr = urQr; + } + } + + late String urQr; + + final AnimatedURModel animatedURmodel; + + String get urQrType { + final first = urQr.trim().split("\n")[0]; + return first.split('/')[0]; + } + + @override + Widget body(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(top: 64.0), + child: URQR( + frames: urQr.trim().split("\n"), + ), + ), + SizedBox(height: 32), + if (urQrType == "ur:xmr-txunsigned" || urQrType == "ur:xmr-output") + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: SizedBox( + width: double.maxFinite, + child: PrimaryButton( + onPressed: () => _continue(context), + text: "Continue", + color: Theme.of(context).primaryColor, + textColor: Colors.white, + ), + ), + ), + SizedBox(height: 32), + if (urQrType == "ur:xmr-output" && !isAll) Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: SizedBox( + width: double.maxFinite, + child: PrimaryButton( + onPressed: () => _exportAll(context), + text: "Export all", + color: Theme.of(context).colorScheme.secondary, + textColor: Colors.white, + ), + ), + ), + ], + ); + } + + void _exportAll(BuildContext context) { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) { + return AnimatedURPage(animatedURmodel, urQr: "export-outputs-all", isAll: true); + }, + ), + ); + } + + Future _continue(BuildContext context) async { + try { + switch (urQrType) { + case "ur:xmr-txunsigned": // ur:xmr-txsigned + final ur = await presentQRScanner(context); + final result = await monero!.commitTransactionUR(animatedURmodel.wallet, ur); + if (result) { + Navigator.of(context).pop(true); + } + break; + case "ur:xmr-output": // xmr-keyimage + final ur = await presentQRScanner(context); + final result = await monero!.importKeyImagesUR(animatedURmodel.wallet, ur); + if (result) { + Navigator.of(context).pop(true); + } + break; + default: + throw UnimplementedError("unable to handle UR: ${urQrType}"); + } + } catch (e) { + await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: e.toString(), + buttonText: S.of(context).ok, + buttonAction: () => Navigator.pop(context, true)); + }); + } + } +} + +class URQR extends StatefulWidget { + URQR({super.key, required this.frames}); + + List frames; + + @override + // ignore: library_private_types_in_public_api + _URQRState createState() => _URQRState(); +} + +const urFrameTime = 1000 ~/ 5; + +class _URQRState extends State { + Timer? t; + int frame = 0; + @override + void initState() { + super.initState(); + setState(() { + t = Timer.periodic(const Duration(milliseconds: urFrameTime), (timer) { + _nextFrame(); + }); + }); + } + + void _nextFrame() { + setState(() { + frame++; + }); + } + + @override + void dispose() { + t?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: QrImage( + data: widget.frames[frame % widget.frames.length], version: -1, + size: 400, + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/src/screens/wallet_connect/wc_connections_listing_view.dart b/lib/src/screens/wallet_connect/wc_connections_listing_view.dart index eda2a748f..3a78f6af6 100644 --- a/lib/src/screens/wallet_connect/wc_connections_listing_view.dart +++ b/lib/src/screens/wallet_connect/wc_connections_listing_view.dart @@ -65,7 +65,7 @@ class WCPairingsWidget extends BasePage { bool isCameraPermissionGranted = await PermissionHandler.checkPermission(Permission.camera, context); if (!isCameraPermissionGranted) return; - uri = await presentQRScanner(); + uri = await presentQRScanner(context); } else { uri = await _showEnterWalletConnectURIPopUp(context); } diff --git a/lib/src/widgets/address_text_field.dart b/lib/src/widgets/address_text_field.dart index 0b1ef4796..d904768f0 100644 --- a/lib/src/widgets/address_text_field.dart +++ b/lib/src/widgets/address_text_field.dart @@ -231,7 +231,7 @@ class AddressTextField extends StatelessWidget { bool isCameraPermissionGranted = await PermissionHandler.checkPermission(Permission.camera, context); if (!isCameraPermissionGranted) return; - final code = await presentQRScanner(); + final code = await presentQRScanner(context); if (code.isEmpty) { return; } diff --git a/lib/src/widgets/setting_actions.dart b/lib/src/widgets/setting_actions.dart index 048d006cc..da9f26bc6 100644 --- a/lib/src/widgets/setting_actions.dart +++ b/lib/src/widgets/setting_actions.dart @@ -21,6 +21,7 @@ class SettingActions { addressBookSettingAction, silentPaymentsSettingAction, litecoinMwebSettingAction, + exportOutputsAction, securityBackupSettingAction, privacySettingAction, displaySettingAction, @@ -50,6 +51,16 @@ class SettingActions { }, ); + static SettingActions exportOutputsAction = SettingActions._( + key: ValueKey('dashboard_page_menu_widget_export_outputs_settings_button_key'), + name: (context) => S.of(context).export_outputs, + image: 'assets/images/monero_menu.png', + onTap: (BuildContext context) { + Navigator.pop(context); + Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, arguments: 'export-outputs'); + }, + ); + static SettingActions litecoinMwebSettingAction = SettingActions._( key: ValueKey('dashboard_page_menu_widget_litecoin_mweb_settings_button_key'), name: (context) => S.of(context).litecoin_mweb_settings, diff --git a/lib/view_model/animated_ur_model.dart b/lib/view_model/animated_ur_model.dart new file mode 100644 index 000000000..f613d26be --- /dev/null +++ b/lib/view_model/animated_ur_model.dart @@ -0,0 +1,10 @@ +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:mobx/mobx.dart'; + +class AnimatedURModel with Store { + AnimatedURModel(this.appStore) + : wallet = appStore.wallet!; + final AppStore appStore; + final WalletBase wallet; +} \ 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 6cf7c49d0..934cbdee7 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -393,6 +393,12 @@ abstract class DashboardViewModelBase with Store { wallet.type == WalletType.wownero || wallet.type == WalletType.haven; + @computed + bool get isMoneroViewOnly { + if (wallet.type != WalletType.monero) return false; + return monero!.isViewOnly(); + } + @computed String? get getMoneroError { if (wallet.type != WalletType.monero) return null; 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 86ceb654c..8b3c70c5e 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 @@ -213,7 +213,7 @@ abstract class NodeCreateOrEditViewModelBase with Store { bool isCameraPermissionGranted = await PermissionHandler.checkPermission(Permission.camera, context); if (!isCameraPermissionGranted) return; - String code = await presentQRScanner(); + String code = await presentQRScanner(context); if (code.isEmpty) { throw Exception('Unexpected scan QR code value: value is empty'); diff --git a/lib/view_model/restore/restore_wallet.dart b/lib/view_model/restore/restore_wallet.dart index 2c2a25005..cc3ad4123 100644 --- a/lib/view_model/restore/restore_wallet.dart +++ b/lib/view_model/restore/restore_wallet.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:cake_wallet/view_model/restore/restore_mode.dart'; import 'package:cw_core/wallet_type.dart'; @@ -32,6 +34,16 @@ class RestoredWallet { final String? privateKey; factory RestoredWallet.fromKey(Map json) { + try { + final codeParsed = jsonDecode(json['raw_qr'].toString()); + if (codeParsed["version"] == 0) { + json['address'] = codeParsed["primaryAddress"]; + json['view_key'] = codeParsed["privateViewKey"]; + json['height'] = codeParsed["restoreHeight"].toString(); + } + } catch (e) { + // fine, we don't care, it is only for monero anyway + } final height = json['height'] as String?; return RestoredWallet( restoreMode: json['mode'] as WalletRestoreMode, @@ -39,7 +51,7 @@ class RestoredWallet { address: json['address'] as String?, spendKey: json['spend_key'] as String?, viewKey: json['view_key'] as String?, - height: height != null ? int.parse(height) : 0, + height: height != null ? int.tryParse(height)??0 : 0, privateKey: json['private_key'] as String?, ); } 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 23850befa..c1a19ea57 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:cake_wallet/core/seed_validator.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/qr_scanner.dart'; @@ -48,6 +50,17 @@ class WalletRestoreFromQRCode { final extracted = sortedKeys.firstWhereOrNull((key) => code.toLowerCase().contains(key)); + 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; + } + } + return _walletTypeMap[extracted]; } @@ -75,7 +88,7 @@ class WalletRestoreFromQRCode { } static Future scanQRCodeForRestoring(BuildContext context) async { - String code = await presentQRScanner(); + String code = await presentQRScanner(context); if (code.isEmpty) throw Exception('Unexpected scan QR code value: value is empty'); WalletType? walletType; @@ -109,7 +122,7 @@ class WalletRestoreFromQRCode { queryParameters['address'] = _extractAddressFromUrl(code, walletType!); } - Map credentials = {'type': walletType, ...queryParameters}; + Map credentials = {'type': walletType, ...queryParameters, 'raw_qr': code}; credentials['mode'] = _determineWalletRestoreMode(credentials); @@ -205,6 +218,17 @@ class WalletRestoreFromQRCode { return WalletRestoreMode.keys; } + if (type == WalletType.monero) { + final codeParsed = json.decode(credentials['raw_qr'].toString()); + if (codeParsed["version"] != 0) throw UnimplementedError("Found view-only restore with unsupported version"); + if (codeParsed["primaryAddress"] == null || + codeParsed["privateViewKey"] == null || + codeParsed["restoreHeight"] == null) { + throw UnimplementedError("Missing one or more attributes in the JSON"); + } + return WalletRestoreMode.keys; + } + throw Exception('Unexpected restore mode: restore params are invalid'); } } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 1403eb109..4621e8d16 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; @@ -10,7 +11,9 @@ import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/wallet_contact.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; +import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/solana/solana.dart'; +import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; @@ -23,6 +26,7 @@ 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'; +import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/entities/template.dart'; @@ -457,7 +461,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor } @action - Future commitTransaction() async { + Future commitTransaction(BuildContext context) async { if (pendingTransaction == null) { throw Exception("Pending transaction doesn't exist. It should not be happened."); } @@ -476,7 +480,17 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor try { state = TransactionCommitting(); - await pendingTransaction!.commit(); + + if (pendingTransaction!.shouldCommitUR()) { + final urstr = await pendingTransaction!.commitUR(); + final result = await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, arguments: urstr); + if (result == null) { + state = FailureState("Canceled by user"); + return; + } + } else { + await pendingTransaction!.commit(); + } if (walletType == WalletType.nano) { nano!.updateTransactions(wallet); diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index 6455fc0e3..81eda7cc8 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -72,6 +72,10 @@ abstract class WalletKeysViewModelBase with Store { final keys = monero!.getKeys(_appStore.wallet!); items.addAll([ + if (keys['primaryAddress'] != null) + StandartListItem( + title: S.current.primary_address, + value: keys['primaryAddress']!), if (keys['publicSpendKey'] != null) StandartListItem( key: ValueKey('${_walletName}_wallet_public_spend_key_item_key'), @@ -131,6 +135,10 @@ abstract class WalletKeysViewModelBase with Store { final keys = haven!.getKeys(_appStore.wallet!); items.addAll([ + if (keys['primaryAddress'] != null) + StandartListItem( + title: S.current.primary_address, + value: keys['primaryAddress']!), if (keys['publicSpendKey'] != null) StandartListItem( key: ValueKey('${_walletName}_wallet_public_spend_key_item_key'), @@ -168,6 +176,10 @@ abstract class WalletKeysViewModelBase with Store { final keys = wownero!.getKeys(_appStore.wallet!); items.addAll([ + if (keys['primaryAddress'] != null) + StandartListItem( + title: S.current.primary_address, + value: keys['primaryAddress']!), if (keys['publicSpendKey'] != null) StandartListItem( key: ValueKey('${_walletName}_wallet_public_spend_key_item_key'), diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index f52be7481..4b9eb3b2d 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -10,7 +10,6 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST - sp_scanner ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 9895e6305..52b44e53e 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,9 +6,9 @@ import FlutterMacOS import Foundation import connectivity_plus -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 @@ -23,9 +23,9 @@ import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) - 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")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index d6199d028..001d75696 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -8,6 +8,8 @@ PODS: - FlutterMacOS - devicelocale (0.0.1): - FlutterMacOS + - fast_scanner (5.1.1): + - FlutterMacOS - flutter_inappwebview_macos (0.0.1): - FlutterMacOS - OrderedSet (~> 5.0) @@ -42,6 +44,7 @@ DEPENDENCIES: - cw_mweb (from `Flutter/ephemeral/.symlinks/plugins/cw_mweb/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`) - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`) - flutter_local_authentication (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_authentication/macos`) - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) @@ -69,6 +72,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos devicelocale: :path: Flutter/ephemeral/.symlinks/plugins/devicelocale/macos + fast_scanner: + :path: Flutter/ephemeral/.symlinks/plugins/fast_scanner/macos flutter_inappwebview_macos: :path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos flutter_local_authentication: @@ -99,6 +104,7 @@ SPEC CHECKSUMS: cw_mweb: 7440b12ead811dda972a9918442ea2a458e8742c device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f devicelocale: 9f0f36ac651cabae2c33f32dcff4f32b61c38225 + fast_scanner: d31bae07e2653403a69dac99fb710c1722b16a97 flutter_inappwebview_macos: 9600c9df9fdb346aaa8933812009f8d94304203d flutter_local_authentication: 85674893931e1c9cfa7c9e4f5973cb8c56b018b0 flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 2abd8401a..fa70b5837 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -14,8 +14,10 @@ dependencies: # provider: ^6.0.3 rxdart: ^0.28.0 yaml: ^3.1.1 - #barcode_scan: any - barcode_scan2: ^4.2.1 + fast_scanner: + git: + url: https://github.com/MrCyjaneK/fast_scanner + ref: c8311b46cc67dd02250970a54d2a4526168187f3 http: ^1.1.0 path_provider: ^2.0.11 mobx: ^2.1.4 diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index b8c933a2e..fc0ea8ea9 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -136,7 +136,7 @@ "clearnet_link": "رابط Clearnet", "close": "يغلق", "coin_control": "التحكم في العملة (اختياري)", - "cold_or_recover_wallet": "أضف محفظة باردة أو استعد محفظة ورقية", + "cold_or_recover_wallet": "أضف محفظة للقراءة فقط من Cupcake أو محفظة باردة أو استعاد محفظة ورقية", "color_theme": "سمة اللون", "commit_transaction_amount_fee": "تنفيذ الصفقة\nالمبلغ: ${amount}\nالرسوم: ${fee}", "confirm": "تأكيد", @@ -161,6 +161,7 @@ "contact_list_contacts": "جهات الاتصال", "contact_list_wallets": "محافظ", "contact_name": "اسم جهة الاتصال", + "contact_name_exists": " .ﻒﻠﺘﺨﻣ ﻢﺳﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ .ﻞﻌﻔﻟﺎﺑ ﺓﺩﻮﺟﻮﻣ ﻢﺳﻻﺍ ﺍﺬﻬﺑ ﻝﺎﺼﺗﺍ ﺔﻬﺟ", "contact_support": "اتصل بالدعم", "continue_text": "التالي", "contract_warning": "تم وضع علامة على عنوان العقد هذا على أنه احتيالي محتمل. يرجى المعالجة بحذر.", @@ -296,6 +297,7 @@ "expiry_and_validity": "انتهاء الصلاحية والصلاحية", "export_backup": "تصدير نسخة احتياطية", "export_logs": "سجلات التصدير", + "export_outputs": "مخرجات التصدير", "extra_id": "معرف إضافي:", "extracted_address_content": "سوف ترسل الأموال إلى\n${recipient_name}", "failed_authentication": "${state_error} فشل المصادقة.", @@ -336,7 +338,9 @@ "haven_app": "Haven بواسطة Cake Wallet", "haven_app_wallet_text": "محفظة رائعة ل Haven", "help": "مساعده", + "hidden_addresses": "العناوين المخفية", "hidden_balance": "الميزان الخفي", + "hide": "يخفي", "hide_details": "أخف التفاصيل", "high_contrast_theme": "موضوع عالي التباين", "home_screen_settings": "إعدادات الشاشة الرئيسية", @@ -500,6 +504,7 @@ "pre_seed_title": "مهم", "prepaid_cards": "البطاقات المدفوعة مسبقا", "prevent_screenshots": "منع لقطات الشاشة وتسجيل الشاشة", + "primary_address": "العنوان الأساسي", "privacy": "خصوصية", "privacy_policy": "سياسة الخصوصية", "privacy_settings": "إعدادات الخصوصية", @@ -696,6 +701,7 @@ "share": "يشارك", "share_address": "شارك العنوان", "shared_seed_wallet_groups": "مجموعات محفظة البذور المشتركة", + "show": "يعرض", "show_details": "اظهر التفاصيل", "show_keys": "اظهار السييد / المفاتيح", "show_market_place": "إظهار السوق", @@ -942,6 +948,5 @@ "you_pay": "انت تدفع", "you_will_get": "حول الى", "you_will_send": "تحويل من", - "yy": "YY", - "contact_name_exists": " .ﻒﻠﺘﺨﻣ ﻢﺳﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ .ﻞﻌﻔﻟﺎﺑ ﺓﺩﻮﺟﻮﻣ ﻢﺳﻻﺍ ﺍﺬﻬﺑ ﻝﺎﺼﺗﺍ ﺔﻬﺟ" -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 2fd644d01..e66a40b36 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -136,7 +136,7 @@ "clearnet_link": "Clearnet връзка", "close": "затвори", "coin_control": "Управление на монетите (не е задължително)", - "cold_or_recover_wallet": "Добавете студен портфейл или възстановете хартиен портфейл", + "cold_or_recover_wallet": "Добавете портфейл само за четене от Cupcake или студен портфейл или възстановете хартиен портфейл", "color_theme": "Цвят", "commit_transaction_amount_fee": "Изпълняване на транзакция\nСума: ${amount}\nТакса: ${fee}", "confirm": "Потвърждаване", @@ -161,6 +161,7 @@ "contact_list_contacts": "Контакти", "contact_list_wallets": "Моите портфейли", "contact_name": "Име на контакт", + "contact_name_exists": "Вече съществува контакт с това име. Моля, изберете друго име.", "contact_support": "Свържи се с отдел поддръжка", "continue_text": "Напред", "contract_warning": "Този адрес на договора е маркиран като потенциално измамник. Моля, обработете с повишено внимание.", @@ -296,6 +297,7 @@ "expiry_and_validity": "Изтичане и валидност", "export_backup": "Експортиране на резервно копие", "export_logs": "Експортни дневници", + "export_outputs": "Експортни резултати", "extra_id": "Допълнително ID:", "extracted_address_content": "Ще изпратите средства на \n${recipient_name}", "failed_authentication": "Неуспешно удостоверяване. ${state_error}", @@ -336,7 +338,9 @@ "haven_app": "Haven от Cake Wallet", "haven_app_wallet_text": "Невероятен портфейл за Haven", "help": "Помощ", + "hidden_addresses": "Скрити адреси", "hidden_balance": "Скрит баланс", + "hide": "Скрий", "hide_details": "Скриване на подробностите", "high_contrast_theme": "Тема с висок контраст", "home_screen_settings": "Настройки на началния екран", @@ -500,6 +504,7 @@ "pre_seed_title": "ВАЖНО", "prepaid_cards": "Предплатени карти", "prevent_screenshots": "Предотвратете екранни снимки и запис на екрана", + "primary_address": "Първичен адрес", "privacy": "Поверителност", "privacy_policy": "Политика за поверителността", "privacy_settings": "Настройки за поверителност", @@ -696,6 +701,7 @@ "share": "Дял", "share_address": "Сподели адрес", "shared_seed_wallet_groups": "Споделени групи за портфейли за семена", + "show": "Показване", "show_details": "Показване на подробностите", "show_keys": "Покажи seed/keys", "show_market_place": "Покажи пазар", @@ -942,6 +948,5 @@ "you_pay": "Вие плащате", "you_will_get": "Обръщане в", "you_will_send": "Обръщане от", - "yy": "гг", - "contact_name_exists": "Вече съществува контакт с това име. Моля, изберете друго име." -} + "yy": "гг" +} \ No newline at end of file diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 8f3b5bf28..4911030b2 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -136,7 +136,7 @@ "clearnet_link": "Odkaz na Clearnet", "close": "zavřít", "coin_control": "Volba mincí (nepovinné)", - "cold_or_recover_wallet": "Přidejte studenou peněženku nebo obnovte papírovou peněženku", + "cold_or_recover_wallet": "Přidejte peněženku pouze pro čtení z Cupcake nebo studené peněženky nebo obnovte papírovou peněženku", "color_theme": "Barevný motiv", "commit_transaction_amount_fee": "Odeslat transakci\nČástka: ${amount}\nPoplatek: ${fee}", "confirm": "Potvrdit", @@ -161,6 +161,7 @@ "contact_list_contacts": "Kontakty", "contact_list_wallets": "Moje peněženky", "contact_name": "Jméno kontaktu", + "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ě.", @@ -296,6 +297,7 @@ "expiry_and_validity": "Vypršení a platnost", "export_backup": "Exportovat zálohu", "export_logs": "Vývozní protokoly", + "export_outputs": "Vývozní výstupy", "extra_id": "Extra ID:", "extracted_address_content": "Prostředky budete posílat na\n${recipient_name}", "failed_authentication": "Ověřování selhalo. ${state_error}", @@ -336,7 +338,9 @@ "haven_app": "Haven od Cake Wallet", "haven_app_wallet_text": "Úžasná peněženka pro Haven", "help": "pomoc", + "hidden_addresses": "Skryté adresy", "hidden_balance": "Skrytý zůstatek", + "hide": "Skrýt", "hide_details": "Skrýt detaily", "high_contrast_theme": "Téma s vysokým kontrastem", "home_screen_settings": "Nastavení domovské obrazovky", @@ -500,6 +504,7 @@ "pre_seed_title": "DŮLEŽITÉ", "prepaid_cards": "Předplacené karty", "prevent_screenshots": "Zabránit vytváření snímků obrazovky a nahrávání obrazovky", + "primary_address": "Primární adresa", "privacy": "Soukromí", "privacy_policy": "Zásady ochrany soukromí", "privacy_settings": "Nastavení soukromí", @@ -696,6 +701,7 @@ "share": "Podíl", "share_address": "Sdílet adresu", "shared_seed_wallet_groups": "Skupiny sdílených semen", + "show": "Show", "show_details": "Zobrazit detaily", "show_keys": "Zobrazit seed/klíče", "show_market_place": "Zobrazit trh", @@ -942,6 +948,5 @@ "you_pay": "Zaplatíte", "you_will_get": "Směnit na", "you_will_send": "Směnit z", - "yy": "YY", - "contact_name_exists": "Kontakt s tímto jménem již existuje. Vyberte prosím jiný název." -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 77a531d1b..a69174dd6 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -136,7 +136,7 @@ "clearnet_link": "Clearnet-Link", "close": "Schließen", "coin_control": "Coin Control (optional)", - "cold_or_recover_wallet": "Fügen Sie eine Cold Wallet hinzu oder stellen Sie eine Paper Wallet wieder her", + "cold_or_recover_wallet": "Fügen Sie eine schreibgeschützte Brieftasche von Cupcake oder eine kalte Brieftasche hinzu oder erholen Sie sich eine Brieftasche", "color_theme": "Farbthema", "commit_transaction_amount_fee": "Transaktion absenden\nBetrag: ${amount}\nGebühr: ${fee}", "confirm": "Bestätigen", @@ -161,6 +161,7 @@ "contact_list_contacts": "Kontakte", "contact_list_wallets": "Meine Wallets", "contact_name": "Name des Kontakts", + "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 Vertragsadresse wurde als potenziell betrügerisch gekennzeichnet. Bitte verarbeiten Sie mit Vorsicht.", @@ -296,6 +297,7 @@ "expiry_and_validity": "Ablauf und Gültigkeit", "export_backup": "Sicherung exportieren", "export_logs": "Exportprotokolle", + "export_outputs": "Exportausgaben", "extra_id": "Extra ID:", "extracted_address_content": "Sie senden Geld an\n${recipient_name}", "failed_authentication": "Authentifizierung fehlgeschlagen. ${state_error}", @@ -336,7 +338,9 @@ "haven_app": "Haven von Cake Wallet", "haven_app_wallet_text": "Eine großartige Wallet für Haven", "help": "hilfe", + "hidden_addresses": "Versteckte Adressen", "hidden_balance": "Verstecktes Guthaben", + "hide": "Verstecken", "hide_details": "Details ausblenden", "high_contrast_theme": "Kontrastreiches Thema", "home_screen_settings": "Einstellungen für den Startbildschirm", @@ -501,6 +505,7 @@ "pre_seed_title": "WICHTIG", "prepaid_cards": "Karten mit Guthaben", "prevent_screenshots": "Verhindern Sie Screenshots und Bildschirmaufzeichnungen", + "primary_address": "Primäradresse", "privacy": "Datenschutz", "privacy_policy": "Datenschutzrichtlinie", "privacy_settings": "Datenschutzeinstellungen", @@ -697,6 +702,7 @@ "share": "Teilen", "share_address": "Adresse teilen ", "shared_seed_wallet_groups": "Gemeinsame Walletsseed Gruppen", + "show": "Zeigen", "show_details": "Details anzeigen", "show_keys": "Seed/Schlüssel anzeigen", "show_market_place": "Marktplatz anzeigen", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 4fe375ff9..322e38b85 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -136,7 +136,7 @@ "clearnet_link": "Clearnet link", "close": "Close", "coin_control": "Coin control (optional)", - "cold_or_recover_wallet": "Add a cold wallet or recover a paper wallet", + "cold_or_recover_wallet": "Add a read-only wallet from Cupcake or a cold wallet or recover a paper wallet", "color_theme": "Color theme", "commit_transaction_amount_fee": "Commit transaction\nAmount: ${amount}\nFee: ${fee}", "confirm": "Confirm", @@ -161,6 +161,7 @@ "contact_list_contacts": "Contacts", "contact_list_wallets": "My Wallets", "contact_name": "Contact Name", + "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.", @@ -296,6 +297,7 @@ "expiry_and_validity": "Expiry and Validity", "export_backup": "Export backup", "export_logs": "Export logs", + "export_outputs": "Export outputs", "extra_id": "Extra ID:", "extracted_address_content": "You will be sending funds to\n${recipient_name}", "failed_authentication": "Failed authentication. ${state_error}", @@ -502,6 +504,7 @@ "pre_seed_title": "IMPORTANT", "prepaid_cards": "Prepaid Cards", "prevent_screenshots": "Prevent screenshots and screen recording", + "primary_address": "Primary Address", "privacy": "Privacy", "privacy_policy": "Privacy Policy", "privacy_settings": "Privacy settings", @@ -945,6 +948,5 @@ "you_pay": "You Pay", "you_will_get": "Convert to", "you_will_send": "Convert from", - "yy": "YY", - "contact_name_exists": "A contact with that name already exists. Please choose a different name." -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 31498df2d..5a8ff0582 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -136,7 +136,7 @@ "clearnet_link": "enlace Clearnet", "close": "Cerca", "coin_control": "Control de monedas (opcional)", - "cold_or_recover_wallet": "Agrega una billetera fría o recupera una billetera de papel", + "cold_or_recover_wallet": "Agregue una billetera de solo lectura de Cupcake o una billetera en frío o recupere una billetera de papel", "color_theme": "Tema de color", "commit_transaction_amount_fee": "Confirmar transacción\nCantidad: ${amount}\nCuota: ${fee}", "confirm": "Confirmar", @@ -161,6 +161,7 @@ "contact_list_contacts": "Contactos", "contact_list_wallets": "Mis billeteras", "contact_name": "Nombre de contacto", + "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.", @@ -296,6 +297,7 @@ "expiry_and_validity": "Vencimiento y validez", "export_backup": "Exportar copia de seguridad", "export_logs": "Registros de exportación", + "export_outputs": "Exportaciones de exportación", "extra_id": "ID adicional:", "extracted_address_content": "Enviará fondos a\n${recipient_name}", "failed_authentication": "Autenticación fallida. ${state_error}", @@ -336,7 +338,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Increíble billetera para Haven", "help": "ayuda", + "hidden_addresses": "Direcciones ocultas", "hidden_balance": "Balance oculto", + "hide": "Esconder", "hide_details": "Ocultar detalles", "high_contrast_theme": "Tema de alto contraste", "home_screen_settings": "Configuración de la pantalla de inicio", @@ -501,6 +505,7 @@ "pre_seed_title": "IMPORTANTE", "prepaid_cards": "Tajetas prepagadas", "prevent_screenshots": "Evitar capturas de pantalla y grabación de pantalla", + "primary_address": "Dirección principal", "privacy": "Privacidad", "privacy_policy": "Política de privacidad", "privacy_settings": "Configuración de privacidad", @@ -697,6 +702,7 @@ "share": "Compartir", "share_address": "Compartir dirección", "shared_seed_wallet_groups": "Grupos de billetera de semillas compartidas", + "show": "Espectáculo", "show_details": "Mostrar detalles", "show_keys": "Mostrar semilla/claves", "show_market_place": "Mostrar mercado", @@ -943,6 +949,5 @@ "you_pay": "Tú pagas", "you_will_get": "Convertir a", "you_will_send": "Convertir de", - "yy": "YY", - "contact_name_exists": "Ya existe un contacto con ese nombre. Elija un nombre diferente." -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index f96e9e304..6abe72681 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -136,7 +136,7 @@ "clearnet_link": "Lien Clearnet", "close": "Fermer", "coin_control": "Contrôle optionnel des pièces (coins)", - "cold_or_recover_wallet": "Ajoutez un portefeuille froid (cold wallet) ou récupérez un portefeuille papier (paper wallet)", + "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", "color_theme": "Thème", "commit_transaction_amount_fee": "Valider la transaction\nMontant : ${amount}\nFrais : ${fee}", "confirm": "Confirmer", @@ -161,6 +161,7 @@ "contact_list_contacts": "Contacts", "contact_list_wallets": "Mes portefeuilles (wallets)", "contact_name": "Nom de Contact", + "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.", @@ -296,6 +297,7 @@ "expiry_and_validity": "Expiration et validité", "export_backup": "Exporter la sauvegarde", "export_logs": "Journaux d'exportation", + "export_outputs": "Exportation des sorties", "extra_id": "ID supplémentaire :", "extracted_address_content": "Vous allez envoyer des fonds à\n${recipient_name}", "failed_authentication": "Échec d'authentification. ${state_error}", @@ -336,7 +338,9 @@ "haven_app": "Haven par Cake Wallet", "haven_app_wallet_text": "Super portefeuille (wallet) pour Haven", "help": "aide", + "hidden_addresses": "Adresses cachées", "hidden_balance": "Solde Caché", + "hide": "Cacher", "hide_details": "Masquer les détails", "high_contrast_theme": "Thème à contraste élevé", "home_screen_settings": "Paramètres de l'écran d'accueil", @@ -500,6 +504,7 @@ "pre_seed_title": "IMPORTANT", "prepaid_cards": "Cartes prépayées", "prevent_screenshots": "Empêcher les captures d'écran et l'enregistrement d'écran", + "primary_address": "Adresse primaire", "privacy": "Confidentialité", "privacy_policy": "Politique de confidentialité", "privacy_settings": "Paramètres de confidentialité", @@ -696,6 +701,7 @@ "share": "Partager", "share_address": "Partager l'adresse", "shared_seed_wallet_groups": "Groupes de portefeuilles partagés", + "show": "Montrer", "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é", @@ -942,6 +948,5 @@ "you_pay": "Vous payez", "you_will_get": "Convertir vers", "you_will_send": "Convertir depuis", - "yy": "AA", - "contact_name_exists": "Un contact portant ce nom existe déjà. Veuillez choisir un autre nom." -} + "yy": "AA" +} \ No newline at end of file diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 5fc64e0b3..5a53d6795 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -136,7 +136,7 @@ "clearnet_link": "Lambar makomar kwayoyi", "close": "Rufa", "coin_control": "Sarrafa tsabar kuɗi (na zaɓi)", - "cold_or_recover_wallet": "Samun kashi na baya ko samun kashi na kasa", + "cold_or_recover_wallet": "Aara wani walat mai karanta-kawai Cupcake ko walat ɗin mai sanyi ko murmurewa takarda takarda", "color_theme": "Jigon launi", "commit_transaction_amount_fee": "Aikata ciniki\nAdadi: ${amount}\nKuda: ${fee}", "confirm": "Tabbatar", @@ -161,6 +161,7 @@ "contact_list_contacts": "Lambobin sadarwa", "contact_list_wallets": "Wallets dina", "contact_name": "Sunan Tuntuɓi", + "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.", @@ -296,6 +297,7 @@ "expiry_and_validity": "Karewa da inganci", "export_backup": "Ajiyayyen fitarwa", "export_logs": "Injin fitarwa", + "export_outputs": "Fitarwar fitarwa", "extra_id": "Karin ID:", "extracted_address_content": "Za ku aika da kudade zuwa\n${recipient_name}", "failed_authentication": "Binne wajen shiga. ${state_error}", @@ -336,7 +338,9 @@ "haven_app": "Haven da Cake Wallet", "haven_app_wallet_text": "Aikace-aikacen e-wallet ga Haven", "help": "taimako", + "hidden_addresses": "Adireshin ɓoye", "hidden_balance": "BOYE KUDI", + "hide": "Ɓoye", "hide_details": "Ɓoye cikakkun bayanai", "high_contrast_theme": "Babban Jigon Kwatance", "home_screen_settings": "Saitunan allo na gida", @@ -502,6 +506,7 @@ "pre_seed_title": "MUHIMMANCI", "prepaid_cards": "Katunan shirye-shirye", "prevent_screenshots": "Fada lambobi da jarrabobi na kayan lambobi", + "primary_address": "Adireshin farko", "privacy": "Keɓantawa", "privacy_policy": "takardar kebantawa", "privacy_settings": "Saitunan sirri", @@ -698,6 +703,7 @@ "share": "Raba", "share_address": "Raba adireshin", "shared_seed_wallet_groups": "Raba ƙungiya walat", + "show": "Nuna", "show_details": "Nuna Cikakkun bayanai", "show_keys": "Nuna iri/maɓallai", "show_market_place": "Nuna dan kasuwa", @@ -944,6 +950,5 @@ "you_pay": "Ka Bayar", "you_will_get": "Maida zuwa", "you_will_send": "Maida daga", - "yy": "YY", - "contact_name_exists": "An riga an sami lamba tare da wannan sunan. Da fatan za a zaɓi suna daban." -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index fb3b78900..d130c2b5f 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -136,7 +136,7 @@ "clearnet_link": "क्लियरनेट लिंक", "close": "बंद करना", "coin_control": "सिक्का नियंत्रण (वैकल्पिक)", - "cold_or_recover_wallet": "कोल्ड वॉलेट जोड़ें या पेपर वॉलेट पुनर्प्राप्त करें", + "cold_or_recover_wallet": "Cupcake या एक कोल्ड वॉलेट से एक रीड-ओनली वॉलेट जोड़ें या एक पेपर वॉलेट को पुनर्प्राप्त करें", "color_theme": "रंग विषय", "commit_transaction_amount_fee": "लेन-देन करें\nरकम: ${amount}\nशुल्क: ${fee}", "confirm": "की पुष्टि करें", @@ -161,6 +161,7 @@ "contact_list_contacts": "संपर्क", "contact_list_wallets": "मेरा बटुआ", "contact_name": "संपर्क नाम", + "contact_name_exists": "उस नाम का एक संपर्क पहले से मौजूद है. कृपया कोई भिन्न नाम चुनें.", "contact_support": "सहायता से संपर्क करें", "continue_text": "जारी रहना", "contract_warning": "इस अनुबंध के पते को संभावित रूप से धोखाधड़ी के रूप में चिह्नित किया गया है। कृपया सावधानी के साथ प्रक्रिया करें।", @@ -296,6 +297,7 @@ "expiry_and_validity": "समाप्ति और वैधता", "export_backup": "निर्यात बैकअप", "export_logs": "निर्यात लॉग", + "export_outputs": "निर्यात आउटपुट", "extra_id": "अतिरिक्त आईडी:", "extracted_address_content": "आपको धनराशि भेजी जाएगी\n${recipient_name}", "failed_authentication": "प्रमाणीकरण विफल. ${state_error}", @@ -336,7 +338,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "मदद करना", + "hidden_addresses": "छिपे हुए पते", "hidden_balance": "छिपा हुआ संतुलन", + "hide": "छिपाना", "hide_details": "विवरण छुपाएं", "high_contrast_theme": "उच्च कंट्रास्ट थीम", "home_screen_settings": "होम स्क्रीन सेटिंग्स", @@ -501,6 +505,7 @@ "pre_seed_title": "महत्वपूर्ण", "prepaid_cards": "पूर्वदत्त कार्ड", "prevent_screenshots": "स्क्रीनशॉट और स्क्रीन रिकॉर्डिंग रोकें", + "primary_address": "प्राथमिक पता", "privacy": "गोपनीयता", "privacy_policy": "गोपनीयता नीति", "privacy_settings": "गोपनीयता सेटिंग्स", @@ -698,6 +703,7 @@ "share": "शेयर करना", "share_address": "पता साझा करें", "shared_seed_wallet_groups": "साझा बीज बटुए समूह", + "show": "दिखाओ", "show_details": "विवरण दिखाएं", "show_keys": "बीज / कुंजियाँ दिखाएँ", "show_market_place": "बाज़ार दिखाएँ", @@ -944,6 +950,5 @@ "you_pay": "आप भुगतान करते हैं", "you_will_get": "में बदलें", "you_will_send": "से रूपांतरित करें", - "yy": "वाईवाई", - "contact_name_exists": "उस नाम का एक संपर्क पहले से मौजूद है. कृपया कोई भिन्न नाम चुनें." -} + "yy": "वाईवाई" +} \ No newline at end of file diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 230aa955b..037e6e9f2 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -136,7 +136,7 @@ "clearnet_link": "Clearnet veza", "close": "Zatvoriti", "coin_control": "Kontrola novca (nije obavezno)", - "cold_or_recover_wallet": "Dodajte hladni novčanik ili povratite papirnati novčanik", + "cold_or_recover_wallet": "Dodajte novčanik samo za čitanje od Cupcake ili hladnog novčanika ili oporavite papirni novčanik", "color_theme": "Shema boja", "commit_transaction_amount_fee": "Izvrši transakciju \nAmount: ${amount}\nFee: ${fee}", "confirm": "Potvrdi", @@ -161,6 +161,7 @@ "contact_list_contacts": "Kontakti", "contact_list_wallets": "Moji novčanici", "contact_name": "Ime kontakta", + "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.", @@ -296,6 +297,7 @@ "expiry_and_validity": "Istek i valjanost", "export_backup": "Izvezi sigurnosnu kopiju", "export_logs": "Izvozni trupci", + "export_outputs": "Izvoz izlaza", "extra_id": "Dodatni ID:", "extracted_address_content": "Poslat ćete sredstva primatelju\n${recipient_name}", "failed_authentication": "Autentifikacija neuspješna. ${state_error}", @@ -336,7 +338,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "pomozite", + "hidden_addresses": "Skrivene adrese", "hidden_balance": "Skriven iznos", + "hide": "Sakriti", "hide_details": "Sakrij pojedinosti", "high_contrast_theme": "Tema visokog kontrasta", "home_screen_settings": "Postavke početnog zaslona", @@ -500,6 +504,7 @@ "pre_seed_title": "VAŽNO", "prepaid_cards": "Unaprijed plaćene kartice", "prevent_screenshots": "Spriječite snimke zaslona i snimanje zaslona", + "primary_address": "Primarna adresa", "privacy": "Privatnost", "privacy_policy": "Pravila privatnosti", "privacy_settings": "Postavke privatnosti", @@ -696,6 +701,7 @@ "share": "Udio", "share_address": "Podijeli adresu", "shared_seed_wallet_groups": "Zajedničke grupe za sjeme novčanika", + "show": "Pokazati", "show_details": "Prikaži pojedinosti", "show_keys": "Prikaži pristupni izraz/ključ", "show_market_place": "Prikaži tržište", @@ -942,6 +948,5 @@ "you_pay": "Vi plaćate", "you_will_get": "Razmijeni u", "you_will_send": "Razmijeni iz", - "yy": "GG", - "contact_name_exists": "Kontakt s tim imenom već postoji. Odaberite drugo ime." -} + "yy": "GG" +} \ No newline at end of file diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 63a986103..04a7cddf5 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -136,7 +136,7 @@ "clearnet_link": "Բաց ցանցի հղում", "close": "Փակել", "coin_control": "Մետաղադրամի վերահսկում (ըստ ցանկության)", - "cold_or_recover_wallet": "Ավելացնել սառը դրամապանակ կամ վերականգնել թղթային դրամապանակ", + "cold_or_recover_wallet": "Cupcake կամ ցուրտ դրամապանակից ավելացնել միայն ընթերցված դրամապանակ կամ վերականգնել թղթի դրամապանակը", "color_theme": "Գույների տեսք", "commit_transaction_amount_fee": "Հաստատել գործարքը\nՍկզբնական գումար. ${amount}\nՄիջնորդավճար. ${fee}", "confirm": "Հաստատել", @@ -161,6 +161,7 @@ "contact_list_contacts": "Կոնտակտներ", "contact_list_wallets": "Իմ դրամապանակներ", "contact_name": "Կոնտակտի անուն", + "contact_name_exists": "Այդ անվանման հետ կապ կա արդեն: Խնդրում ենք ընտրել այլ անուն:", "contact_support": "Հետադարձ կապ", "continue_text": "Շարունակել", "contract_warning": "Պայմանագրի այս հասցեն դրոշմել է որպես հնարավոր կեղծ: Խնդրում ենք զգուշությամբ մշակել:", @@ -296,6 +297,7 @@ "expiry_and_validity": "Վավերականություն և լրացում", "export_backup": "Արտահանել կրկնօրինակը", "export_logs": "Արտահանման տեղեկամատյաններ", + "export_outputs": "Արտահանման արդյունքներ", "extra_id": "Լրացուցիչ ID", "extracted_address_content": "Դուք կուղարկեք գումար ${recipient_name}", "failed_authentication": "Վավերացումը ձախողվեց. ${state_error}", @@ -336,7 +338,9 @@ "haven_app": "Haven ծրագիր", "haven_app_wallet_text": "Հիանալի հաշվեհամար Haven համար", "help": "Օգնություն", + "hidden_addresses": "Թաքնված հասցեներ", "hidden_balance": "Թաքնված մնացորդ", + "hide": "Թաքցնել", "hide_details": "Թաքցնել մանրամասները", "high_contrast_theme": "Բարձր հակադրության տեսք", "home_screen_settings": "Գլխավոր էկրանի կարգավորումներ", @@ -366,14 +370,22 @@ "ledger_error_wrong_app": "Խնդրում ենք համոզվել, որ դուք բացել եք ճիշտ ծրագիրը ձեր Ledger-ում", "ledger_please_enable_bluetooth": "Խնդրում ենք միացնել Bluetooth-ը ձեր Ledger-ը հայտնաբերելու համար", "light_theme": "Լուսավոր", + "litecoin_enable_mweb_sync": "Միացնել MWEB սկան", + "litecoin_mweb": "Մուեբ", + "litecoin_mweb_always_scan": "Սահմանեք Mweb Միշտ սկանավորում", "litecoin_mweb_description": "Mweb- ը նոր արձանագրություն է, որը բերում է ավելի արագ, ավելի էժան եւ ավելի մասնավոր գործարքներ դեպի LITECOIN", "litecoin_mweb_dismiss": "Հեռացնել", + "litecoin_mweb_display_card": "Show ույց տալ Mweb քարտը", "litecoin_mweb_enable": "Միացնել Mweb- ը", "litecoin_mweb_enable_later": "Կարող եք ընտրել Mweb- ը կրկին միացնել ցուցադրման պարամետրերը:", "litecoin_mweb_logs": "Mweb տեղեկամատյաններ", "litecoin_mweb_node": "Mweb հանգույց", "litecoin_mweb_pegin": "Peg in", "litecoin_mweb_pegout": "Հափշտակել", + "litecoin_mweb_scanning": "Mweb սկանավորում", + "litecoin_mweb_settings": "Mweb- ի պարամետրերը", + "litecoin_mweb_warning": "Mweb- ի օգտագործումը սկզբում ներբեռնվի 600 ՄԲ տվյալներ եւ կարող է տեւել 30 րոպե, կախված ցանցի արագությունից: Այս նախնական տվյալները միայն մեկ անգամ ներբեռնելու են եւ հասանելի կլինեն բոլոր Litecoin դրամապանակների համար", + "litecoin_what_is_mweb": "Ինչ է Mweb- ը:", "live_fee_rates": "Ապակի վարձավճարներ API- ի միջոցով", "load_more": "Բեռնել ավելին", "loading_your_wallet": "Ձեր հաշվեհամարը բեռնում է", @@ -492,6 +504,7 @@ "pre_seed_title": "ԿԱՐԵՎՈՐ", "prepaid_cards": "Նախավճարային քարտեր", "prevent_screenshots": "Կանխել էկրանի պատկերները և տեսագրությունը", + "primary_address": "Առաջնային հասցե", "privacy": "Գաղտնիություն", "privacy_policy": "Գաղտնիության քաղաքականություն", "privacy_settings": "Գաղտնիության կարգավորումներ", @@ -688,6 +701,7 @@ "share": "Կիսվել", "share_address": "Կիսվել հասցեով", "shared_seed_wallet_groups": "Համօգտագործված սերմերի դրամապանակների խմբեր", + "show": "Ցուցահանդես", "show_details": "Ցուցադրել մանրամասներ", "show_keys": "Ցուցադրել բանալիներ", "show_market_place": "Ցուցադրել շուկան", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 3fa3a958f..231ac037d 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -136,7 +136,7 @@ "clearnet_link": "Tautan clearnet", "close": "Menutup", "coin_control": "Kontrol koin (opsional)", - "cold_or_recover_wallet": "Tambahkan dompet dingin atau pulihkan dompet kertas", + "cold_or_recover_wallet": "Tambahkan dompet hanya baca dari Cupcake atau dompet dingin atau memulihkan dompet kertas", "color_theme": "Tema warna", "commit_transaction_amount_fee": "Lakukan transaksi\nJumlah: ${amount}\nBiaya: ${fee}", "confirm": "Konfirmasi", @@ -161,6 +161,7 @@ "contact_list_contacts": "Kontak", "contact_list_wallets": "Dompet Saya", "contact_name": "Nama Kontak", + "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.", @@ -296,6 +297,7 @@ "expiry_and_validity": "Kedaluwarsa dan validitas", "export_backup": "Ekspor cadangan", "export_logs": "Log ekspor", + "export_outputs": "Ekspor output", "extra_id": "ID tambahan:", "extracted_address_content": "Anda akan mengirim dana ke\n${recipient_name}", "failed_authentication": "Otentikasi gagal. ${state_error}", @@ -336,7 +338,9 @@ "haven_app": "Haven Oleh Cake Wallet", "haven_app_wallet_text": "Dompet luar biasa untuk Haven", "help": "bantuan", + "hidden_addresses": "Alamat tersembunyi", "hidden_balance": "Saldo Tersembunyi", + "hide": "Bersembunyi", "hide_details": "Sembunyikan Rincian", "high_contrast_theme": "Tema Kontras Tinggi", "home_screen_settings": "Pengaturan layar awal", @@ -502,6 +506,7 @@ "pre_seed_title": "PENTING", "prepaid_cards": "Kartu prabayar", "prevent_screenshots": "Cegah tangkapan layar dan perekaman layar", + "primary_address": "Alamat utama", "privacy": "Privasi", "privacy_policy": "Kebijakan Privasi", "privacy_settings": "Pengaturan privasi", @@ -699,6 +704,7 @@ "share": "Membagikan", "share_address": "Bagikan alamat", "shared_seed_wallet_groups": "Kelompok dompet benih bersama", + "show": "Menunjukkan", "show_details": "Tampilkan Rincian", "show_keys": "Tampilkan seed/kunci", "show_market_place": "Tampilkan Pasar", @@ -945,6 +951,5 @@ "you_pay": "Anda Membayar", "you_will_get": "Konversi ke", "you_will_send": "Konversi dari", - "yy": "YY", - "contact_name_exists": "Kontak dengan nama tersebut sudah ada. Silakan pilih nama lain." -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index aab18d434..b06a35537 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -136,7 +136,7 @@ "clearnet_link": "Collegamento Clearnet", "close": "Chiudere", "coin_control": "Controllo monete (opzionale)", - "cold_or_recover_wallet": "Aggiungi un cold wallet o recupera un paper wallet", + "cold_or_recover_wallet": "Aggiungi un portafoglio di sola lettura da Cupcake o un portafoglio freddo o recupera un portafoglio di carta", "color_theme": "Colore tema", "commit_transaction_amount_fee": "Invia transazione\nAmmontare: ${amount}\nCommissione: ${fee}", "confirm": "Conferma", @@ -162,6 +162,7 @@ "contact_list_contacts": "Contatti", "contact_list_wallets": "I miei portafogli", "contact_name": "Nome Contatto", + "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.", @@ -297,6 +298,7 @@ "expiry_and_validity": "Scadenza e validità", "export_backup": "Esporta backup", "export_logs": "Registri di esportazione", + "export_outputs": "Output di esportazione", "extra_id": "Extra ID:", "extracted_address_content": "Invierai i tuoi fondi a\n${recipient_name}", "failed_authentication": "Autenticazione fallita. ${state_error}", @@ -337,7 +339,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Portafoglio fantastico per Haven", "help": "aiuto", + "hidden_addresses": "Indirizzi nascosti", "hidden_balance": "Saldo Nascosto", + "hide": "Nascondere", "hide_details": "Nascondi dettagli", "high_contrast_theme": "Tema ad alto contrasto", "home_screen_settings": "Impostazioni della schermata iniziale", @@ -502,6 +506,7 @@ "pre_seed_title": "IMPORTANTE", "prepaid_cards": "Carte prepagata", "prevent_screenshots": "Impedisci screenshot e registrazione dello schermo", + "primary_address": "Indirizzo primario", "privacy": "Privacy", "privacy_policy": "Informativa sulla privacy", "privacy_settings": "Impostazioni privacy", @@ -698,6 +703,7 @@ "share": "Condividere", "share_address": "Condividi indirizzo", "shared_seed_wallet_groups": "Gruppi di portafoglio di semi condivisi", + "show": "Spettacolo", "show_details": "Mostra dettagli", "show_keys": "Mostra seme/chiavi", "show_market_place": "Mostra mercato", @@ -945,6 +951,5 @@ "you_pay": "Tu paghi", "you_will_get": "Converti a", "you_will_send": "Conveti da", - "yy": "YY", - "contact_name_exists": "Esiste già un contatto con quel nome. Scegli un nome diverso." -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index f1fd0acd3..d55c4d5a1 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -136,7 +136,7 @@ "clearnet_link": "クリアネット リンク", "close": "近い", "coin_control": "コインコントロール(オプション)", - "cold_or_recover_wallet": "コールド ウォレットを追加するか、ペーパー ウォレットを復元する", + "cold_or_recover_wallet": "Cupcakeまたはコールドウォレットから読み取り専用ウォレットを追加するか、紙の財布を回収する", "color_theme": "カラーテーマ", "commit_transaction_amount_fee": "トランザクションをコミット\n量: ${amount}\n費用: ${fee}", "confirm": "確認する", @@ -161,6 +161,7 @@ "contact_list_contacts": "連絡先", "contact_list_wallets": "マイウォレット", "contact_name": "連絡先", + "contact_name_exists": "その名前の連絡先はすでに存在します。別の名前を選択してください。", "contact_support": "サポートに連絡する", "continue_text": "持続する", "contract_warning": "この契約住所は、潜在的に不正としてフラグが立てられています。注意して処理してください。", @@ -296,6 +297,7 @@ "expiry_and_validity": "有効期限と有効性", "export_backup": "バックアップのエクスポート", "export_logs": "ログをエクスポートします", + "export_outputs": "エクスポート出力", "extra_id": "追加ID:", "extracted_address_content": "に送金します\n${recipient_name}", "failed_authentication": "認証失敗. ${state_error}", @@ -337,7 +339,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "ヘルプ", + "hidden_addresses": "隠されたアドレス", "hidden_balance": "隠れたバランス", + "hide": "隠れる", "hide_details": "詳細を非表示", "high_contrast_theme": "ハイコントラストテーマ", "home_screen_settings": "ホーム画面の設定", @@ -501,6 +505,7 @@ "pre_seed_title": "重要", "prepaid_cards": "プリペイドカード", "prevent_screenshots": "スクリーンショットと画面録画を防止する", + "primary_address": "主なアドレス", "privacy": "プライバシー", "privacy_policy": "プライバシーポリシー", "privacy_settings": "プライバシー設定", @@ -697,6 +702,7 @@ "share": "共有", "share_address": "住所を共有する", "shared_seed_wallet_groups": "共有シードウォレットグループ", + "show": "見せる", "show_details": "詳細を表示", "show_keys": "シード/キーを表示する", "show_market_place": "マーケットプレイスを表示", @@ -943,6 +949,5 @@ "you_pay": "あなたが支払う", "you_will_get": "に変換", "you_will_send": "から変換", - "yy": "YY", - "contact_name_exists": "その名前の連絡先はすでに存在します。別の名前を選択してください。" -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 7e3467cf2..303527fea 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -136,7 +136,7 @@ "clearnet_link": "클리어넷 링크", "close": "닫다", "coin_control": "코인 제어 (옵션)", - "cold_or_recover_wallet": "콜드 지갑 추가 또는 종이 지갑 복구", + "cold_or_recover_wallet": "Cupcake 또는 차가운 지갑에서 읽기 전용 지갑을 추가하거나 종이 지갑을 복구하십시오.", "color_theme": "색상 테마", "commit_transaction_amount_fee": "커밋 거래\n양: ${amount}\n보수: ${fee}", "confirm": "확인", @@ -161,6 +161,7 @@ "contact_list_contacts": "콘택트 렌즈", "contact_list_wallets": "내 지갑", "contact_name": "담당자 이름", + "contact_name_exists": "해당 이름을 가진 연락처가 이미 존재합니다. 다른 이름을 선택하세요.", "contact_support": "지원팀에 문의", "continue_text": "잇다", "contract_warning": "이 계약 주소는 잠재적으로 사기성으로 표시되었습니다. 주의해서 처리하십시오.", @@ -296,6 +297,7 @@ "expiry_and_validity": "만료와 타당성", "export_backup": "백업 내보내기", "export_logs": "내보내기 로그", + "export_outputs": "내보내기 출력", "extra_id": "추가 ID:", "extracted_address_content": "당신은에 자금을 보낼 것입니다\n${recipient_name}", "failed_authentication": "인증 실패. ${state_error}", @@ -336,7 +338,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "돕다", + "hidden_addresses": "숨겨진 주소", "hidden_balance": "숨겨진 균형", + "hide": "숨다", "hide_details": "세부 정보 숨기기", "high_contrast_theme": "고대비 테마", "home_screen_settings": "홈 화면 설정", @@ -501,6 +505,7 @@ "pre_seed_title": "중대한", "prepaid_cards": "선불 카드", "prevent_screenshots": "스크린샷 및 화면 녹화 방지", + "primary_address": "기본 주소", "privacy": "프라이버시", "privacy_policy": "개인 정보 보호 정책", "privacy_settings": "개인정보 설정", @@ -697,6 +702,7 @@ "share": "공유하다", "share_address": "주소 공유", "shared_seed_wallet_groups": "공유 종자 지갑 그룹", + "show": "보여주다", "show_details": "세부정보 표시", "show_keys": "시드 / 키 표시", "show_market_place": "마켓플레이스 표시", @@ -944,6 +950,5 @@ "you_will_get": "로 변환하다", "you_will_send": "다음에서 변환", "YY": "YY", - "yy": "YY", - "contact_name_exists": "해당 이름을 가진 연락처가 이미 존재합니다. 다른 이름을 선택하세요." -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 10c13cfef..9b9b44657 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -136,7 +136,7 @@ "clearnet_link": "Clearnet လင့်ခ်", "close": "အနီးကပ်", "coin_control": "အကြွေစေ့ထိန်းချုပ်မှု (ချန်လှပ်ထားနိုင်သည်)", - "cold_or_recover_wallet": "အေးသောပိုက်ဆံအိတ်ထည့်ပါ သို့မဟုတ် စက္ကူပိုက်ဆံအိတ်ကို ပြန်ယူပါ။", + "cold_or_recover_wallet": "Cupcake သို့မဟုတ်အအေးပိုက်ဆံအိတ်မှဖတ်ရန်သာပိုက်ဆံအိတ်တစ်ခုထည့်ပါသို့မဟုတ်စက္ကူပိုက်ဆံအိတ်ကိုပြန်လည်ရယူပါ", "color_theme": "အရောင်အပြင်အဆင်", "commit_transaction_amount_fee": "ငွေလွှဲခြင်း\nပမာဏ- ${amount}\nအခကြေးငွေ- ${fee}", "confirm": "အတည်ပြုပါ။", @@ -161,6 +161,7 @@ "contact_list_contacts": "အဆက်အသွယ်များ", "contact_list_wallets": "ကျွန်ုပ်၏ ပိုက်ဆံအိတ်များ", "contact_name": "ဆက်သွယ်ရန်အမည်", + "contact_name_exists": "ထိုအမည်နှင့် အဆက်အသွယ်တစ်ခု ရှိနှင့်ပြီးဖြစ်သည်။ အခြားအမည်တစ်ခုကို ရွေးပါ။", "contact_support": "ပံ့ပိုးကူညီမှုထံ ဆက်သွယ်ပါ။", "continue_text": "ဆက်လက်", "contract_warning": "ဒီစာချုပ်လိပ်စာအလားအလာအလားအလာအလားအလာအလံများကိုအလံလွှင့်တင်ခဲ့သည်။ ကျေးဇူးပြုပြီးသတိဖြင့်လုပ်ငန်းစဉ်။", @@ -296,6 +297,7 @@ "expiry_and_validity": "သက်တမ်းကုန်ဆုံးခြင်းနှင့်တရားဝင်မှု", "export_backup": "အရန်ကူးထုတ်ရန်", "export_logs": "ပို့ကုန်မှတ်တမ်းများ", + "export_outputs": "ပို့ကုန်ထုတ်ကုန်များ", "extra_id": "အပို ID-", "extracted_address_content": "သင်သည် \n${recipient_name} သို့ ရန်ပုံငွေများ ပေးပို့ပါမည်", "failed_authentication": "အထောက်အထားစိစစ်ခြင်း မအောင်မြင်ပါ။. ${state_error}", @@ -336,7 +338,9 @@ "haven_app": "ဟေးဗင် ကိတ် ဝေါလက်", "haven_app_wallet_text": "ဟေဗင်အတွက် အံ့ဩစရာကောင်းတဲ့ ပိုက်ဆံအုံး", "help": "ကူညီပါ", + "hidden_addresses": "လျှို့ဝှက်လိပ်စာများ", "hidden_balance": "Hidden Balance", + "hide": "သားရေ", "hide_details": "အသေးစိတ်ကို ဝှက်ပါ။", "high_contrast_theme": "အလင်းအမှောင် မြင့်မားသော အပြင်အဆင်", "home_screen_settings": "ပင်မစခရင် ဆက်တင်များ", @@ -500,6 +504,7 @@ "pre_seed_title": "အရေးကြီးသည်။", "prepaid_cards": "ကြိုတင်ငွေဖြည့်ကဒ်များ", "prevent_screenshots": "ဖန်သားပြင်ဓာတ်ပုံများနှင့် မျက်နှာပြင်ရိုက်ကူးခြင်းကို တားဆီးပါ။", + "primary_address": "အဓိကလိပ်စာ", "privacy": "ကိုယ်ရေးကိုယ်တာ", "privacy_policy": "ကိုယ်ရေးအချက်အလက်မူဝါဒ", "privacy_settings": "Privacy settings တွေကို", @@ -696,6 +701,7 @@ "share": "မျှဝေပါ။", "share_address": "လိပ်စာမျှဝေပါ။", "shared_seed_wallet_groups": "shared မျိုးစေ့ပိုက်ဆံအိတ်အုပ်စုများ", + "show": "ပြသ", "show_details": "အသေးစိတ်ပြ", "show_keys": "မျိုးစေ့ /သော့များကို ပြပါ။", "show_market_place": "စျေးကွက်ကိုပြသပါ။", @@ -942,6 +948,5 @@ "you_pay": "သင်ပေးချေပါ။", "you_will_get": "သို့ပြောင်းပါ။", "you_will_send": "မှပြောင်းပါ။", - "yy": "YY", - "contact_name_exists": "ထိုအမည်နှင့် အဆက်အသွယ်တစ်ခု ရှိနှင့်ပြီးဖြစ်သည်။ အခြားအမည်တစ်ခုကို ရွေးပါ။" -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 9552f2439..657450f61 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -136,7 +136,7 @@ "clearnet_link": "Clearnet-link", "close": "Dichtbij", "coin_control": "Muntcontrole (optioneel)", - "cold_or_recover_wallet": "Voeg een cold wallet toe of herstel een paper wallet", + "cold_or_recover_wallet": "Voeg een alleen-lezen portemonnee toe van Cupcake of een koude portemonnee of herstel een papieren portemonnee", "color_theme": "Kleur thema", "commit_transaction_amount_fee": "Verricht transactie\nBedrag: ${amount}\nhonorarium: ${fee}", "confirm": "Bevestigen", @@ -161,6 +161,7 @@ "contact_list_contacts": "Contacten", "contact_list_wallets": "Mijn portefeuilles", "contact_name": "Contactnaam", + "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.", @@ -296,6 +297,7 @@ "expiry_and_validity": "Vervallen en geldigheid", "export_backup": "Back-up exporteren", "export_logs": "Exporteer logboeken", + "export_outputs": "Exportuitgangen exporteren", "extra_id": "Extra ID:", "extracted_address_content": "U stuurt geld naar\n${recipient_name}", "failed_authentication": "Mislukte authenticatie. ${state_error}", @@ -336,7 +338,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "helpen", + "hidden_addresses": "Verborgen adressen", "hidden_balance": "Verborgen balans", + "hide": "Verbergen", "hide_details": "Details verbergen", "high_contrast_theme": "Thema met hoog contrast", "home_screen_settings": "Instellingen voor het startscherm", @@ -500,6 +504,7 @@ "pre_seed_title": "BELANGRIJK", "prepaid_cards": "Prepaid-kaarten", "prevent_screenshots": "Voorkom screenshots en schermopname", + "primary_address": "Primair adres", "privacy": "Privacy", "privacy_policy": "Privacybeleid", "privacy_settings": "Privacy-instellingen", @@ -696,6 +701,7 @@ "share": "Deel", "share_address": "Deel adres", "shared_seed_wallet_groups": "Gedeelde zaadportelgroepen", + "show": "Show", "show_details": "Toon details", "show_keys": "Toon zaad/sleutels", "show_market_place": "Toon Marktplaats", @@ -943,6 +949,5 @@ "you_pay": "U betaalt", "you_will_get": "Converteren naar", "you_will_send": "Converteren van", - "yy": "JJ", - "contact_name_exists": "Er bestaat al een contact met die naam. Kies een andere naam." -} + "yy": "JJ" +} \ No newline at end of file diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index f3c3e4810..e47c93dcf 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -136,7 +136,7 @@ "clearnet_link": "łącze Clearnet", "close": "Zamknąć", "coin_control": "Kontrola monet (opcjonalnie)", - "cold_or_recover_wallet": "Dodaj zimny portfel lub odzyskaj portfel papierowy", + "cold_or_recover_wallet": "Dodaj portfel tylko do odczytu od Cupcake lub zimnego portfela lub odzyskaj papierowy portfel", "color_theme": "Motyw kolorystyczny", "commit_transaction_amount_fee": "Zatwierdź transakcję\nIlość: ${amount}\nOpłata: ${fee}", "confirm": "Potwierdzać", @@ -161,6 +161,7 @@ "contact_list_contacts": "Łączność", "contact_list_wallets": "Moje portfele", "contact_name": "Nazwa Kontaktu", + "contact_name_exists": "Kontakt o tej nazwie już istnieje. Proszę wybrać inną nazwę.", "contact_support": "Skontaktuj się z pomocą techniczną", "continue_text": "Dalej", "contract_warning": "Ten adres umowy został oznaczony jako potencjalnie nieuczciwy. Prosimy o ostrożność.", @@ -296,6 +297,7 @@ "expiry_and_validity": "Wygaśnięcie i ważność", "export_backup": "Eksportuj kopię zapasową", "export_logs": "Dzienniki eksportu", + "export_outputs": "Wyjścia eksportowe", "extra_id": "Dodatkowy ID:", "extracted_address_content": "Wysyłasz środki na\n${recipient_name}", "failed_authentication": "Nieudane uwierzytelnienie. ${state_error}", @@ -336,7 +338,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "pomoc", + "hidden_addresses": "Ukryte adresy", "hidden_balance": "Ukryte saldo", + "hide": "Ukrywać", "hide_details": "Ukryj szczegóły", "high_contrast_theme": "Motyw o wysokim kontraście", "home_screen_settings": "Ustawienia ekranu głównego", @@ -500,6 +504,7 @@ "pre_seed_title": "WAŻNY", "prepaid_cards": "Karty przedpłacone", "prevent_screenshots": "Zapobiegaj zrzutom ekranu i nagrywaniu ekranu", + "primary_address": "Adres podstawowy", "privacy": "Prywatność", "privacy_policy": "Polityka prywatności", "privacy_settings": "Ustawienia prywatności", @@ -696,6 +701,7 @@ "share": "Udział", "share_address": "Udostępnij adres", "shared_seed_wallet_groups": "Wspólne grupy portfeli nasion", + "show": "Pokazywać", "show_details": "Pokaż szczegóły", "show_keys": "Pokaż seed/klucze", "show_market_place": "Pokaż rynek", @@ -942,6 +948,5 @@ "you_pay": "Płacisz", "you_will_get": "Konwertuj na", "you_will_send": "Konwertuj z", - "yy": "RR", - "contact_name_exists": "Kontakt o tej nazwie już istnieje. Proszę wybrać inną nazwę." -} + "yy": "RR" +} \ No newline at end of file diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 410e8cb1c..d5aa95c16 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -136,7 +136,7 @@ "clearnet_link": "link clear net", "close": "Fechar", "coin_control": "Controle de moedas (opcional)", - "cold_or_recover_wallet": "Adicione uma cold wallet ou recupere uma paper wallet", + "cold_or_recover_wallet": "Adicione uma carteira somente leitura de Cupcake ou uma carteira fria ou recupere uma carteira de papel", "color_theme": "Tema de cor", "commit_transaction_amount_fee": "Confirmar transação\nQuantia: ${amount}\nTaxa: ${fee}", "confirm": "Confirmar", @@ -161,6 +161,7 @@ "contact_list_contacts": "Contatos", "contact_list_wallets": "minhas carteiras", "contact_name": "Nome do contato", + "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.", @@ -296,6 +297,7 @@ "expiry_and_validity": "Expiração e validade", "export_backup": "Backup de exportação", "export_logs": "Exportar logs", + "export_outputs": "Saídas de exportação", "extra_id": "ID extra:", "extracted_address_content": "Você enviará fundos para\n${recipient_name}", "failed_authentication": "Falha na autenticação. ${state_error}", @@ -336,7 +338,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "ajuda", + "hidden_addresses": "Endereços ocultos", "hidden_balance": "Saldo escondido", + "hide": "Esconder", "hide_details": "Ocultar detalhes", "high_contrast_theme": "Tema de alto contraste", "home_screen_settings": "Configurações da tela inicial", @@ -502,6 +506,7 @@ "pre_seed_title": "IMPORTANTE", "prepaid_cards": "Cartões pré-pagos", "prevent_screenshots": "Evite capturas de tela e gravação de tela", + "primary_address": "Endereço primário", "privacy": "Privacidade", "privacy_policy": "Política de privacidade", "privacy_settings": "Configurações de privacidade", @@ -698,6 +703,7 @@ "share": "Compartilhar", "share_address": "Compartilhar endereço", "shared_seed_wallet_groups": "Grupos de carteira de sementes compartilhados", + "show": "Mostrar", "show_details": "Mostrar detalhes", "show_keys": "Mostrar semente/chaves", "show_market_place": "Mostrar mercado", @@ -946,4 +952,4 @@ "you_will_get": "Converter para", "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 a8ee5a309..f999b4aaa 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -136,7 +136,7 @@ "clearnet_link": "Клирнет ссылка", "close": "Закрывать", "coin_control": "Контроль монет (необязательно)", - "cold_or_recover_wallet": "Добавьте холодный кошелек или восстановите бумажный кошелек", + "cold_or_recover_wallet": "Добавить кошелек только для чтения из Cupcake или холодный кошелек или восстановить бумажный кошелек", "color_theme": "Цветовая тема", "commit_transaction_amount_fee": "Подтвердить транзакцию \nСумма: ${amount}\nКомиссия: ${fee}", "confirm": "Подтвердить", @@ -161,6 +161,7 @@ "contact_list_contacts": "Контакты", "contact_list_wallets": "Мои кошельки", "contact_name": "Имя контакта", + "contact_name_exists": "Контакт с таким именем уже существует. Пожалуйста, выберите другое имя.", "contact_support": "Связаться со службой поддержки", "continue_text": "Продолжить", "contract_warning": "Этот адрес контракта был отмечен как потенциально мошеннический. Пожалуйста, обработайтесь с осторожностью.", @@ -296,6 +297,7 @@ "expiry_and_validity": "Истечение и достоверность", "export_backup": "Экспорт резервной копии", "export_logs": "Экспортные журналы", + "export_outputs": "Экспортные выходы", "extra_id": "Дополнительный ID:", "extracted_address_content": "Вы будете отправлять средства\n${recipient_name}", "failed_authentication": "Ошибка аутентификации. ${state_error}", @@ -336,7 +338,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "помощь", + "hidden_addresses": "Скрытые адреса", "hidden_balance": "Скрытый баланс", + "hide": "Скрывать", "hide_details": "Скрыть детали", "high_contrast_theme": "Высококонтрастная тема", "home_screen_settings": "Настройки главного экрана", @@ -501,6 +505,7 @@ "pre_seed_title": "ВАЖНО", "prepaid_cards": "Предоплаченные карты", "prevent_screenshots": "Предотвратить скриншоты и запись экрана", + "primary_address": "Первичный адрес", "privacy": "Конфиденциальность", "privacy_policy": "Политика конфиденциальности", "privacy_settings": "Настройки конфиденциальности", @@ -697,6 +702,7 @@ "share": "Делиться", "share_address": "Поделиться адресом", "shared_seed_wallet_groups": "Общие группы кошелька семян", + "show": "Показывать", "show_details": "Показать детали", "show_keys": "Показать мнемоническую фразу/ключи", "show_market_place": "Показать торговую площадку", @@ -943,6 +949,5 @@ "you_pay": "Вы платите", "you_will_get": "Конвертировать в", "you_will_send": "Конвертировать из", - "yy": "ГГ", - "contact_name_exists": "Контакт с таким именем уже существует. Пожалуйста, выберите другое имя." -} + "yy": "ГГ" +} \ No newline at end of file diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 5102e150c..a35e16a97 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -136,7 +136,7 @@ "clearnet_link": "ลิงค์เคลียร์เน็ต", "close": "ปิด", "coin_control": "การควบคุมเหรียญ (ตัวเลือก)", - "cold_or_recover_wallet": "เพิ่มกระเป๋าเงินเย็นหรือกู้คืนกระเป๋าเงินกระดาษ", + "cold_or_recover_wallet": "เพิ่มกระเป๋าเงินแบบอ่านอย่างเดียวจาก Cupcake หรือกระเป๋าเงินเย็นหรือกู้คืนกระเป๋ากระดาษ", "color_theme": "ธีมสี", "commit_transaction_amount_fee": "ยืนยันธุรกรรม\nจำนวน: ${amount}\nค่าธรรมเนียม: ${fee}", "confirm": "ยืนยัน", @@ -161,6 +161,7 @@ "contact_list_contacts": "ติดต่อ", "contact_list_wallets": "กระเป๋าเงินของฉัน", "contact_name": "ชื่อผู้ติดต่อ", + "contact_name_exists": "มีผู้ติดต่อชื่อนั้นอยู่แล้ว โปรดเลือกชื่ออื่น", "contact_support": "ติดต่อฝ่ายสนับสนุน", "continue_text": "ดำเนินการต่อ", "contract_warning": "ที่อยู่สัญญานี้ได้รับการตั้งค่าสถานะว่าเป็นการฉ้อโกง กรุณาดำเนินการด้วยความระมัดระวัง", @@ -296,6 +297,7 @@ "expiry_and_validity": "หมดอายุและถูกต้อง", "export_backup": "ส่งออกข้อมูลสำรอง", "export_logs": "บันทึกการส่งออก", + "export_outputs": "เอาต์พุตส่งออก", "extra_id": "ไอดีเพิ่มเติม:", "extracted_address_content": "คุณกำลังจะส่งเงินไปยัง\n${recipient_name}", "failed_authentication": "การยืนยันสิทธิ์ล้มเหลว ${state_error}", @@ -336,7 +338,9 @@ "haven_app": "Haven ของ Cake Wallet", "haven_app_wallet_text": "กระเป๋าสตางค์ที่สวยงามสำหรับ Haven", "help": "ช่วยเหลือ", + "hidden_addresses": "ที่อยู่ที่ซ่อนอยู่", "hidden_balance": "ยอดคงเหลือซ่อนอยู่", + "hide": "ซ่อน", "hide_details": "ซ่อนรายละเอียด", "high_contrast_theme": "ธีมความคมชัดสูง", "home_screen_settings": "การตั้งค่าหน้าจอหลัก", @@ -500,6 +504,7 @@ "pre_seed_title": "สำคัญ", "prepaid_cards": "บัตรเติมเงิน", "prevent_screenshots": "ป้องกันภาพหน้าจอและการบันทึกหน้าจอ", + "primary_address": "ที่อยู่ปฐมภูมิ", "privacy": "ความเป็นส่วนตัว", "privacy_policy": "นโยบายความเป็นส่วนตัว", "privacy_settings": "การตั้งค่าความเป็นส่วนตัว", @@ -696,6 +701,7 @@ "share": "แบ่งปัน", "share_address": "แชร์ที่อยู่", "shared_seed_wallet_groups": "กลุ่มกระเป๋าเงินที่ใช้ร่วมกัน", + "show": "แสดง", "show_details": "แสดงรายละเอียด", "show_keys": "แสดงซีด/คีย์", "show_market_place": "แสดงตลาดกลาง", @@ -942,6 +948,5 @@ "you_pay": "คุณจ่าย", "you_will_get": "แปลงเป็น", "you_will_send": "แปลงจาก", - "yy": "ปี", - "contact_name_exists": "มีผู้ติดต่อชื่อนั้นอยู่แล้ว โปรดเลือกชื่ออื่น" -} + "yy": "ปี" +} \ No newline at end of file diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index eb78c8d3c..090bdb8a7 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -136,7 +136,7 @@ "clearnet_link": "Link ng Clearnet", "close": "Isara", "coin_control": "Coin control (opsyonal)", - "cold_or_recover_wallet": "Magdagdag ng isang cold wallet o mabawi ang isang paper wallet", + "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", "color_theme": "Color theme", "commit_transaction_amount_fee": "Gumawa ng transaksyon\nHalaga: ${amount}\nFee: ${fee}", "confirm": "Kumpirmahin", @@ -161,6 +161,7 @@ "contact_list_contacts": "Mga Contact", "contact_list_wallets": "Mga Wallet Ko", "contact_name": "Pangalan ng Contact", + "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.", @@ -296,6 +297,7 @@ "expiry_and_validity": "Pag-expire at Bisa", "export_backup": "I-export ang backup", "export_logs": "Mga log ng pag -export", + "export_outputs": "Mga output ng pag -export", "extra_id": "Dagdag na ID:", "extracted_address_content": "Magpapadala ka ng pondo sa\n${recipient_name}", "failed_authentication": "Nabigo ang pagpapatunay. ${state_error}", @@ -336,7 +338,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Kahanga-hangang wallet para sa Haven", "help": "Tulong", + "hidden_addresses": "Nakatagong mga address", "hidden_balance": "Nakatagong Balanse", + "hide": "Itago", "hide_details": "Itago ang mga detalye", "high_contrast_theme": "High Contrast Theme", "home_screen_settings": "Mga setting ng home screen", @@ -500,6 +504,7 @@ "pre_seed_title": "Mahalaga", "prepaid_cards": "Mga Prepaid Card", "prevent_screenshots": "Maiwasan ang mga screenshot at pag -record ng screen", + "primary_address": "Pangunahing address", "privacy": "Privacy", "privacy_policy": "Patakaran sa Pagkapribado", "privacy_settings": "Settings para sa pagsasa-pribado", @@ -696,6 +701,7 @@ "share": "Ibahagi", "share_address": "Ibahagi ang address", "shared_seed_wallet_groups": "Ibinahaging mga pangkat ng pitaka ng binhi", + "show": "Ipakita", "show_details": "Ipakita ang mga detalye", "show_keys": "Ipakita ang mga seed/key", "show_market_place": "Ipakita ang Marketplace", @@ -943,4 +949,4 @@ "you_will_get": "I-convert sa", "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 0b4700397..3a426de9e 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -136,7 +136,7 @@ "clearnet_link": "Net bağlantı", "close": "Kapalı", "coin_control": "Koin kontrolü (isteğe bağlı)", - "cold_or_recover_wallet": "Soğuk bir cüzdan ekleyin veya bir kağıt cüzdanı kurtarın", + "cold_or_recover_wallet": "Cupcake veya soğuk bir cüzdandan salt okunur bir cüzdan ekleyin veya bir kağıt cüzdanı kurtar", "color_theme": "Renk teması", "commit_transaction_amount_fee": "Transferi gerçekleştir\nMiktar: ${amount}\nKomisyon: ${fee}", "confirm": "Onayla", @@ -161,6 +161,7 @@ "contact_list_contacts": "Rehberim", "contact_list_wallets": "Cüzdanlarım", "contact_name": "Kişi ismi", + "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.", @@ -296,6 +297,7 @@ "expiry_and_validity": "Sona erme ve geçerlilik", "export_backup": "Yedeği dışa aktar", "export_logs": "Dışa aktarma günlükleri", + "export_outputs": "İhracat çıktıları", "extra_id": "Ekstra ID:", "extracted_address_content": "Parayı buraya gönderceksin:\n${recipient_name}", "failed_authentication": "Doğrulama başarısız oldu. ${state_error}", @@ -336,7 +338,9 @@ "haven_app": "Cake Wallet tarafından Haven", "haven_app_wallet_text": "Haven için harika cüzdan ", "help": "yardım", + "hidden_addresses": "Gizli adresler", "hidden_balance": "Gizli Bakiye", + "hide": "Saklamak", "hide_details": "Detayları Gizle", "high_contrast_theme": "Yüksek Kontrastlı Tema", "home_screen_settings": "Ana ekran ayarları", @@ -500,6 +504,7 @@ "pre_seed_title": "UYARI", "prepaid_cards": "Ön ödemeli kartlar", "prevent_screenshots": "Ekran görüntülerini ve ekran kaydını önleyin", + "primary_address": "Birincil adres", "privacy": "Gizlilik", "privacy_policy": "Gizlilik Politikası", "privacy_settings": "Gizlilik ayarları", @@ -696,6 +701,7 @@ "share": "Paylaşmak", "share_address": "Adresi paylaş", "shared_seed_wallet_groups": "Paylaşılan tohum cüzdan grupları", + "show": "Göstermek", "show_details": "Detayları Göster", "show_keys": "Tohumları/anahtarları göster", "show_market_place": "Pazar Yerini Göster", @@ -942,6 +948,5 @@ "you_pay": "Şu kadar ödeyeceksin: ", "you_will_get": "Biçimine dönüştür:", "you_will_send": "Biçiminden dönüştür:", - "yy": "YY", - "contact_name_exists": "Bu isimde bir kişi zaten mevcut. Lütfen farklı bir ad seçin." -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index c9afde7be..63665875f 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -136,7 +136,7 @@ "clearnet_link": "Посилання Clearnet", "close": "Закрити", "coin_control": "Контроль монет (необов’язково)", - "cold_or_recover_wallet": "Додайте холодний гаманець або відновіть паперовий гаманець", + "cold_or_recover_wallet": "Додайте гаманець лише для читання від Cupcake або холодного гаманця або відновіть паперовий гаманець", "color_theme": "Кольорова тема", "commit_transaction_amount_fee": "Підтвердити транзакцію \nСума: ${amount}\nКомісія: ${fee}", "confirm": "Підтвердити", @@ -161,6 +161,7 @@ "contact_list_contacts": "Контакти", "contact_list_wallets": "Мої гаманці", "contact_name": "Ім'я контакту", + "contact_name_exists": "Контакт із такою назвою вже існує. Виберіть інше ім'я.", "contact_support": "Звернутися до служби підтримки", "continue_text": "Продовжити", "contract_warning": "Ця адреса контракту була позначена як потенційно шахрайська. Будь ласка, обробляйте обережно.", @@ -296,6 +297,7 @@ "expiry_and_validity": "Закінчення та обгрунтованість", "export_backup": "Експортувати резервну копію", "export_logs": "Експортні журнали", + "export_outputs": "Експортні результати", "extra_id": "Додатковий ID:", "extracted_address_content": "Ви будете відправляти кошти\n${recipient_name}", "failed_authentication": "Помилка аутентифікації. ${state_error}", @@ -336,7 +338,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "допомога", + "hidden_addresses": "Приховані адреси", "hidden_balance": "Прихований баланс", + "hide": "Сховати", "hide_details": "Приховати деталі", "high_contrast_theme": "Тема високої контрастності", "home_screen_settings": "Налаштування головного екрана", @@ -500,6 +504,7 @@ "pre_seed_title": "ВАЖЛИВО", "prepaid_cards": "Передплачені картки", "prevent_screenshots": "Запобігати знімкам екрана та запису екрана", + "primary_address": "Первинна адреса", "privacy": "Конфіденційність", "privacy_policy": "Політика конфіденційності", "privacy_settings": "Налаштування конфіденційності", @@ -697,6 +702,7 @@ "share": "Поділіться", "share_address": "Поділитися адресою", "shared_seed_wallet_groups": "Спільні групи насіннєвих гаманців", + "show": "Показувати", "show_details": "Показати деталі", "show_keys": "Показати мнемонічну фразу/ключі", "show_market_place": "Відображати маркетплейс", @@ -943,6 +949,5 @@ "you_pay": "Ви платите", "you_will_get": "Конвертувати в", "you_will_send": "Конвертувати з", - "yy": "YY", - "contact_name_exists": "Контакт із такою назвою вже існує. Виберіть інше ім'я." -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 50c6f1889..2a40fbe13 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -136,7 +136,7 @@ "clearnet_link": "کلیرنیٹ لنک", "close": "بند کریں", "coin_control": "سکے کنٹرول (اختیاری)", - "cold_or_recover_wallet": "ٹھنڈا پرس ڈالیں یا کاغذ کا پرس بازیافت کریں", + "cold_or_recover_wallet": "Cupcake یا سرد بٹوے سے صرف ایک پڑھنے والا پرس شامل کریں یا کاغذ کا پرس بازیافت کریں", "color_theme": "رنگین تھیم", "commit_transaction_amount_fee": "لین دین کا ارتکاب کریں\\nرقم: ${amount}\\nفیس: ${fee}", "confirm": "تصدیق کریں۔", @@ -161,6 +161,7 @@ "contact_list_contacts": "رابطے", "contact_list_wallets": "میرے بٹوے", "contact_name": "رابطے کا نام", + "contact_name_exists": " ۔ﮟﯾﺮﮐ ﺐﺨﺘﻨﻣ ﻡﺎﻧ ﻒﻠﺘﺨﻣ ﮏﯾﺍ ﻡﺮﮐ ﮦﺍﺮﺑ ۔ﮯﮨ ﺩﻮﺟﻮﻣ ﮯﺳ ﮯﻠﮩﭘ ﮧﻄﺑﺍﺭ ﮏﯾﺍ ﮫﺗﺎﺳ ﮯﮐ ﻡﺎﻧ ﺱﺍ", "contact_support": "سپورٹ سے رابطہ کریں۔", "continue_text": "جاری رہے", "contract_warning": "اس معاہدے کے پتے کو ممکنہ طور پر جعلی قرار دیا گیا ہے۔ براہ کرم احتیاط کے ساتھ کارروائی کریں۔", @@ -296,6 +297,7 @@ "expiry_and_validity": "میعاد ختم اور صداقت", "export_backup": "بیک اپ برآمد کریں۔", "export_logs": "نوشتہ جات برآمد کریں", + "export_outputs": "برآمد کے نتائج", "extra_id": "اضافی ID:", "extracted_address_content": "آپ فنڈز بھیج رہے ہوں گے\n${recipient_name}", "failed_authentication": "ناکام تصدیق۔ ${state_error}", @@ -336,7 +338,9 @@ "haven_app": "Haven از Cake والیٹ", "haven_app_wallet_text": "Havek کے لیے زبردست پرس", "help": "مدد", + "hidden_addresses": "پوشیدہ پتے", "hidden_balance": "پوشیدہ بیلنس", + "hide": "چھپائیں", "hide_details": "تفصیلات چھپائیں۔", "high_contrast_theme": "ہائی کنٹراسٹ تھیم", "home_screen_settings": "ہوم اسکرین کی ترتیبات", @@ -502,6 +506,7 @@ "pre_seed_title": "اہم", "prepaid_cards": "پری پیڈ کارڈز", "prevent_screenshots": "اسکرین شاٹس اور اسکرین ریکارڈنگ کو روکیں۔", + "primary_address": "بنیادی پتہ", "privacy": "رازداری", "privacy_policy": "رازداری کی پالیسی", "privacy_settings": "رازداری کی ترتیبات", @@ -698,6 +703,7 @@ "share": "بانٹیں", "share_address": "پتہ شیئر کریں۔", "shared_seed_wallet_groups": "مشترکہ بیج پرس گروپ", + "show": "دکھائیں", "show_details": "تفصیلات دکھائیں", "show_keys": "بیج / چابیاں دکھائیں۔", "show_market_place": "بازار دکھائیں۔", @@ -944,6 +950,5 @@ "you_pay": "تم ادا کرو", "you_will_get": "میں تبدیل کریں۔", "you_will_send": "سے تبدیل کریں۔", - "yy": "YY", - "contact_name_exists": " ۔ﮟﯾﺮﮐ ﺐﺨﺘﻨﻣ ﻡﺎﻧ ﻒﻠﺘﺨﻣ ﮏﯾﺍ ﻡﺮﮐ ﮦﺍﺮﺑ ۔ﮯﮨ ﺩﻮﺟﻮﻣ ﮯﺳ ﮯﻠﮩﭘ ﮧﻄﺑﺍﺭ ﮏﯾﺍ ﮫﺗﺎﺳ ﮯﮐ ﻡﺎﻧ ﺱﺍ" -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index e3ffbef3e..9fee9f4fc 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -136,7 +136,7 @@ "clearnet_link": "Liên kết Clearnet", "close": "Đóng", "coin_control": "Kiểm soát đồng xu (tùy chọn)", - "cold_or_recover_wallet": "Thêm ví lạnh hoặc khôi phục ví giấy", + "cold_or_recover_wallet": "Thêm ví chỉ đọc từ Cupcake hoặc ví lạnh hoặc thu hồi ví giấy", "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", @@ -162,6 +162,7 @@ "contact_list_contacts": "Danh bạ", "contact_list_wallets": "Ví của tôi", "contact_name": "Tên liên hệ", + "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.", @@ -297,6 +298,7 @@ "expiry_and_validity": "Hạn và hiệu lực", "export_backup": "Xuất sao lưu", "export_logs": "Nhật ký xuất khẩu", + "export_outputs": "Đầu ra xuất khẩu", "extra_id": "ID bổ sung:", "extracted_address_content": "Bạn sẽ gửi tiền cho\n${recipient_name}", "failed_authentication": "Xác thực không thành công. ${state_error}", @@ -337,7 +339,9 @@ "haven_app": "Haven bởi Cake Wallet", "haven_app_wallet_text": "Ví tuyệt vời cho Haven", "help": "Trợ giúp", + "hidden_addresses": "Địa chỉ ẩn", "hidden_balance": "Số dư ẩn", + "hide": "Trốn", "hide_details": "Ẩn chi tiết", "high_contrast_theme": "Chủ đề độ tương phản cao", "home_screen_settings": "Cài đặt màn hình chính", @@ -367,14 +371,22 @@ "ledger_error_wrong_app": "Vui lòng đảm bảo bạn đã mở đúng ứng dụng trên Ledger của mình", "ledger_please_enable_bluetooth": "Vui lòng bật Bluetooth để phát hiện Ledger của bạn", "light_theme": "Chủ đề sáng", + "litecoin_enable_mweb_sync": "Bật quét MWEB", + "litecoin_mweb": "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", + "litecoin_mweb_display_card": "Hiển thị thẻ MWEB", "litecoin_mweb_enable": "Bật MWEB", "litecoin_mweb_enable_later": "Bạn có thể chọn bật lại MWEB trong cài đặt hiển thị.", "litecoin_mweb_logs": "Nhật ký MWEB", "litecoin_mweb_node": "Nút MWEB", "litecoin_mweb_pegin": "Chốt vào", "litecoin_mweb_pegout": "Chốt ra", + "litecoin_mweb_scanning": "Quét MWEB", + "litecoin_mweb_settings": "Cài đặt MWEB", + "litecoin_mweb_warning": "Sử dụng MWEB ban đầu sẽ tải xuống ~ 600MB dữ liệu và có thể mất tới 30 phút tùy thuộc vào tốc độ mạng. Dữ liệu ban đầu này sẽ chỉ tải xuống một lần và có sẵn cho tất cả các ví Litecoin", + "litecoin_what_is_mweb": "MWEB là gì?", "live_fee_rates": "Tỷ lệ phí hiện tại qua API", "load_more": "Tải thêm", "loading_your_wallet": "Đang tải ví của bạn", @@ -493,6 +505,7 @@ "pre_seed_title": "QUAN TRỌNG", "prepaid_cards": "Thẻ trả trước", "prevent_screenshots": "Ngăn chặn ảnh chụp màn hình và ghi hình màn hình", + "primary_address": "Địa chỉ chính", "privacy": "Quyền riêng tư", "privacy_policy": "Chính sách quyền riêng tư", "privacy_settings": "Cài đặt quyền riêng tư", @@ -689,6 +702,7 @@ "share": "Chia sẻ", "share_address": "Chia sẻ địa chỉ", "shared_seed_wallet_groups": "Nhóm ví hạt được chia sẻ", + "show": "Trình diễn", "show_details": "Hiển thị chi tiết", "show_keys": "Hiển thị hạt giống/khóa", "show_market_place": "Hiển thị Thị trường", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 19dc9f110..1dd6eafda 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -136,7 +136,7 @@ "clearnet_link": "Kọja ilọ oke", "close": "sunmo", "coin_control": "Ìdarí owó ẹyọ (ìyàn nìyí)", - "cold_or_recover_wallet": "Fi owo aisan tabi yiyewo owo iwe iwe", + "cold_or_recover_wallet": "Ṣafikun apamọwọ kika-nikan lati Cupcake tabi apamọwọ tutu tabi gba owo apamọwọ iwe kan", "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í", @@ -161,6 +161,7 @@ "contact_list_contacts": "Àwọn olùbásọ̀rọ̀", "contact_list_wallets": "Àwọn àpamọ́wọ́ mi", "contact_name": "Orúkọ olùbásọ̀rọ̀", + "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.", @@ -297,6 +298,7 @@ "expiry_and_validity": "Ipari ati idaniloju", "export_backup": "Sún ẹ̀dà nípamọ́ síta", "export_logs": "Wọle si okeere", + "export_outputs": "Agbohunlu okeere", "extra_id": "Àmì ìdánimọ̀ tó fikún:", "extracted_address_content": "Ẹ máa máa fi owó ránṣẹ́ sí\n${recipient_name}", "failed_authentication": "Ìfẹ̀rílàdí pipòfo. ${state_error}", @@ -337,7 +339,9 @@ "haven_app": "Haven latí ọwọ́ Cake Wallet", "haven_app_wallet_text": "Àpamọ́wọ́ Haven wà pa", "help": "ìranlọ́wọ́", + "hidden_addresses": "Awọn adirẹsi ti o farapamọ", "hidden_balance": "Ìyókù owó dídé", + "hide": "Pamọ", "hide_details": "Dé ìsọfúnni kékeré", "high_contrast_theme": "Akori Iyatọ giga", "home_screen_settings": "Awọn eto iboju ile", @@ -501,6 +505,7 @@ "pre_seed_title": "Ó TI ṢE PÀTÀKÌ", "prepaid_cards": "Awọn kaadi ti a ti sanwo", "prevent_screenshots": "Pese asapọ ti awọn ẹrọ eto aṣa", + "primary_address": "Adirẹsi akọkọ", "privacy": "Ìdáwà", "privacy_policy": "Òfin Aládàáni", "privacy_settings": "Ààtò àdáni", @@ -697,6 +702,7 @@ "share": "Pinpin", "share_address": "Pín àdírẹ́sì", "shared_seed_wallet_groups": "Awọn ẹgbẹ ti a pin irugbin", + "show": "Fihan", "show_details": "Fi ìsọfúnni kékeré hàn", "show_keys": "Wo hóró / àwọn kọ́kọ́rọ́", "show_market_place": "Wa Sopọ Pataki", @@ -943,6 +949,5 @@ "you_pay": "Ẹ sàn", "you_will_get": "Ṣe pàṣípààrọ̀ sí", "you_will_send": "Ṣe pàṣípààrọ̀ láti", - "yy": "Ọd", - "contact_name_exists": "Olubasọrọ pẹlu orukọ yẹn ti wa tẹlẹ. Jọwọ yan orukọ ti o yatọ." -} + "yy": "Ọd" +} \ No newline at end of file diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 009a31d86..92a540d19 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -136,7 +136,7 @@ "clearnet_link": "明网链接", "close": "关闭", "coin_control": "硬幣控制(可選)", - "cold_or_recover_wallet": "添加冷钱包或恢复纸钱包", + "cold_or_recover_wallet": "从Cupcake或冷钱包中添加只读的钱包或恢复纸钱包", "color_theme": "主题", "commit_transaction_amount_fee": "提交交易\n金额: ${amount}\n手续费: ${fee}", "confirm": "确认", @@ -161,6 +161,7 @@ "contact_list_contacts": "联系人", "contact_list_wallets": "我的钱包", "contact_name": "联系人姓名", + "contact_name_exists": "已存在具有该名称的联系人。请选择不同的名称。", "contact_support": "联系支持", "continue_text": "继续", "contract_warning": "该合同地址已被标记为潜在的欺诈性。请谨慎处理。", @@ -296,6 +297,7 @@ "expiry_and_validity": "到期和有效性", "export_backup": "导出备份", "export_logs": "导出日志", + "export_outputs": "导出输出", "extra_id": "额外ID:", "extracted_address_content": "您将汇款至\n${recipient_name}", "failed_authentication": "身份验证失败. ${state_error}", @@ -336,7 +338,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "帮助", + "hidden_addresses": "隐藏的地址", "hidden_balance": "隐藏余额", + "hide": "隐藏", "hide_details": "隐藏细节", "high_contrast_theme": "高对比度主题", "home_screen_settings": "主屏幕设置", @@ -500,6 +504,7 @@ "pre_seed_title": "重要", "prepaid_cards": "预付费卡", "prevent_screenshots": "防止截屏和录屏", + "primary_address": "主要地址", "privacy": "隐私", "privacy_policy": "隐私政策", "privacy_settings": "隐私设置", @@ -696,6 +701,7 @@ "share": "分享", "share_address": "分享地址", "shared_seed_wallet_groups": "共享种子钱包组", + "show": "展示", "show_details": "显示详细信息", "show_keys": "显示种子/密钥", "show_market_place": "显示市场", @@ -942,6 +948,5 @@ "you_pay": "你付钱", "you_will_get": "转换到", "you_will_send": "转换自", - "yy": "YY", - "contact_name_exists": "已存在具有该名称的联系人。请选择不同的名称。" -} + "yy": "YY" +} \ No newline at end of file diff --git a/scripts/macos/gen_common.sh b/scripts/macos/gen_common.sh index 87fd54b43..d75ac919e 100755 --- a/scripts/macos/gen_common.sh +++ b/scripts/macos/gen_common.sh @@ -9,7 +9,7 @@ gen_podspec() { DEFAULT_FILE_PATH="${CW_PLUGIN_DIR}/${DEFAULT_FILENAME}" rm -f $DEFAULT_FILE_PATH cp $BASE_FILE_PATH $DEFAULT_FILE_PATH - sed -i '' "s/#___VALID_ARCHS___#/${ARCH}/g" $DEFAULT_FILE_PATH + gsed -i "s/#___VALID_ARCHS___#/${ARCH}/g" $DEFAULT_FILE_PATH } gen_project() { @@ -17,7 +17,7 @@ gen_project() { CW_DIR="`pwd`/../../macos/Runner.xcodeproj" DEFAULT_FILENAME="project.pbxproj" DEFAULT_FILE_PATH="${CW_DIR}/${DEFAULT_FILENAME}" - sed -i '' "s/ARCHS =.*/ARCHS = \"${ARCH}\";/g" $DEFAULT_FILE_PATH + gsed -i "s/ARCHS =.*/ARCHS = \"${ARCH}\";/g" $DEFAULT_FILE_PATH } gen() { diff --git a/tool/configure.dart b/tool/configure.dart index 97541c2fa..c5d4a6863 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -276,6 +276,7 @@ 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_monero/monero_unspent.dart'; +import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/monero_wallet_service.dart'; import 'package:cw_monero/api/wallet_manager.dart'; import 'package:cw_monero/monero_wallet.dart'; @@ -379,6 +380,12 @@ abstract class Monero { Future getCurrentHeight(); + Future commitTransactionUR(Object wallet, String ur); + + String exportOutputsUR(Object wallet, bool all); + + bool importKeyImagesUR(Object wallet, String ur); + WalletCredentials createMoneroRestoreWalletFromKeysCredentials({ required String name, required String spendKey, @@ -398,6 +405,7 @@ abstract class Monero { int formatterMoneroParseAmount({required String amount}); Account getCurrentAccount(Object wallet); void monerocCheck(); + bool isViewOnly(); void setCurrentAccount(Object wallet, int id, String label, String? balance); void onStartup(); int getTransactionInfoAccountId(TransactionInfo tx); diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 92431a6fb..26d481f85 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -13,7 +13,6 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST - sp_scanner ) set(PLUGIN_BUNDLED_LIBRARIES) From 02f53055b1a72b5e7d33cc5bda2565412857b187 Mon Sep 17 00:00:00 2001 From: Serhii Date: Sat, 9 Nov 2024 21:00:56 +0200 Subject: [PATCH 53/73] Cw 657 new buy sell flow (#1553) * init commit * onramper * moonPay * dfx provider * meld * dfx payment methods * fiat buy credentials * moonpay payment method * payment loading state * dfx sell quote * onramper launch trade * meld launch trade * country picker * update option tile * buy/sell action * meld refactor * update pr_test_build.yml * ui fixes * revert country picker commit * update the ui * recommended providers * payment method [skip ci] * provider option tile * remove buy action * minor fixes * update the best rate when the amount is changed * fixes for currency title * fix icons * code refactoring * null issue * code review fixes * Update pr_test_build_linux.yml * Update meld_buy_provider.dart * Update meld_buy_provider.dart * add show wallets action * remove default sell / buy provider setting * localisation * providerTypes * icons * remove duplicate file [skip ci] * minor changes [skip ci] * fixes from review * disable dfx for non eur/chf currencies fix providers to be fetched with the selected currency * fix breaking from loop if one element failed * fix minor naming issue from merging conflicts * add fiat check for moonpay * fix address validation * merge conflict * fix destination and source currency * minor fix * minor fix * update the flow * fix bch address format * fix wallet addresses * fix initial fetching amount. * Update address_validator.dart * review comments * revert switch case to return null * minor fix --------- Co-authored-by: OmarHatem --- .github/workflows/pr_test_build_android.yml | 2 + .github/workflows/pr_test_build_linux.yml | 2 + assets/images/apple_pay_logo.png | Bin 0 -> 56018 bytes assets/images/apple_pay_round_dark.svg | 9 + assets/images/apple_pay_round_light.svg | 9 + assets/images/bank.png | Bin 0 -> 1323 bytes assets/images/bank_dark.svg | 8 + assets/images/bank_light.svg | 8 + assets/images/buy_sell.png | Bin 0 -> 9931 bytes assets/images/card.svg | 1 + assets/images/card_dark.svg | 7 + assets/images/dollar_coin.svg | 12 + assets/images/google_pay_icon.png | Bin 0 -> 12336 bytes assets/images/meld_logo.svg | 30 + assets/images/revolut.png | Bin 0 -> 11588 bytes assets/images/skrill.svg | 15 + assets/images/usd_round_dark.svg | 4 + assets/images/usd_round_light.svg | 2 + assets/images/wallet_new.png | Bin 0 -> 5619 bytes cw_core/lib/currency_for_wallet_type.dart | 31 ++ lib/buy/buy_provider.dart | 32 +- lib/buy/buy_quote.dart | 302 ++++++++++ lib/buy/dfx/dfx_buy_provider.dart | 273 +++++++-- lib/buy/meld/meld_buy_provider.dart | 266 +++++++++ lib/buy/moonpay/moonpay_provider.dart | 451 +++++++++------ lib/buy/onramper/onramper_buy_provider.dart | 359 ++++++++++-- lib/buy/payment_method.dart | 287 ++++++++++ lib/buy/robinhood/robinhood_buy_provider.dart | 102 +++- lib/buy/sell_buy_states.dart | 20 + lib/buy/wyre/wyre_buy_provider.dart | 9 +- lib/core/backup_service.dart | 19 +- lib/core/selectable_option.dart | 47 ++ lib/di.dart | 32 +- lib/entities/main_actions.dart | 65 +-- lib/entities/preferences_key.dart | 4 +- lib/entities/provider_types.dart | 42 +- lib/router.dart | 16 +- lib/routes.dart | 2 + .../screens/{InfoPage.dart => Info_page.dart} | 0 lib/src/screens/buy/buy_options_page.dart | 74 --- .../screens/buy/buy_sell_options_page.dart | 48 ++ lib/src/screens/buy/buy_sell_page.dart | 469 ++++++++++++++++ .../buy/payment_method_options_page.dart | 47 ++ .../desktop_dashboard_actions.dart | 27 +- .../exchange/widgets/exchange_card.dart | 58 +- .../mobile_exchange_cards_section.dart | 91 ++- lib/src/screens/seed/pre_seed_page.dart | 2 +- lib/src/screens/select_options_page.dart | 199 +++++++ .../screens/settings/other_settings_page.dart | 16 - lib/src/screens/settings/privacy_page.dart | 12 +- .../setup_2fa/setup_2fa_info_page.dart | 2 +- lib/src/widgets/address_text_field.dart | 7 +- lib/src/widgets/provider_optoin_tile.dart | 527 ++++++++++++++++++ lib/store/settings_store.dart | 79 +-- lib/typography.dart | 2 + lib/view_model/buy/buy_sell_view_model.dart | 446 +++++++++++++++ .../dashboard/dashboard_view_model.dart | 45 +- .../settings/other_settings_view_model.dart | 45 -- .../settings/privacy_settings_view_model.dart | 10 +- 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 | 13 +- res/values/strings_ur.arb | 3 + res/values/strings_vi.arb | 1 + res/values/strings_yo.arb | 3 + res/values/strings_zh.arb | 3 + tool/utils/secret_key.dart | 2 + 88 files changed, 4082 insertions(+), 686 deletions(-) create mode 100644 assets/images/apple_pay_logo.png create mode 100644 assets/images/apple_pay_round_dark.svg create mode 100644 assets/images/apple_pay_round_light.svg create mode 100644 assets/images/bank.png create mode 100644 assets/images/bank_dark.svg create mode 100644 assets/images/bank_light.svg create mode 100644 assets/images/buy_sell.png create mode 100644 assets/images/card.svg create mode 100644 assets/images/card_dark.svg create mode 100644 assets/images/dollar_coin.svg create mode 100644 assets/images/google_pay_icon.png create mode 100644 assets/images/meld_logo.svg create mode 100644 assets/images/revolut.png create mode 100644 assets/images/skrill.svg create mode 100644 assets/images/usd_round_dark.svg create mode 100644 assets/images/usd_round_light.svg create mode 100644 assets/images/wallet_new.png create mode 100644 lib/buy/buy_quote.dart create mode 100644 lib/buy/meld/meld_buy_provider.dart create mode 100644 lib/buy/payment_method.dart create mode 100644 lib/buy/sell_buy_states.dart create mode 100644 lib/core/selectable_option.dart rename lib/src/screens/{InfoPage.dart => Info_page.dart} (100%) delete mode 100644 lib/src/screens/buy/buy_options_page.dart create mode 100644 lib/src/screens/buy/buy_sell_options_page.dart create mode 100644 lib/src/screens/buy/buy_sell_page.dart create mode 100644 lib/src/screens/buy/payment_method_options_page.dart create mode 100644 lib/src/screens/select_options_page.dart create mode 100644 lib/src/widgets/provider_optoin_tile.dart create mode 100644 lib/view_model/buy/buy_sell_view_model.dart diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 6d0e541ce..a12fd422f 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -194,6 +194,8 @@ jobs: echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart + echo "const meldTestApiKey = '${{ secrets.MELD_TEST_API_KEY }}';" >> lib/.secrets.g.dart + echo "const meldTestPublicKey = '${{ secrets.MELD_TEST_PUBLIC_KEY}}';" >> lib/.secrets.g.dar echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 5ea0cb377..f3ce1045d 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -175,6 +175,8 @@ jobs: echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart + echo "const meldTestApiKey = '${{ secrets.MELD_TEST_API_KEY }}';" >> lib/.secrets.g.dart + echo "const meldTestPublicKey = '${{ secrets.MELD_TEST_PUBLIC_KEY}}';" >> lib/.secrets.g.dar echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart diff --git a/assets/images/apple_pay_logo.png b/assets/images/apple_pay_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..346007e3b164651dab29b53c8368c6d48fe3310e GIT binary patch literal 56018 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 Hk^KJyr@mf2 literal 0 HcmV?d00001 diff --git a/assets/images/apple_pay_round_dark.svg b/assets/images/apple_pay_round_dark.svg new file mode 100644 index 000000000..82443bfb4 --- /dev/null +++ b/assets/images/apple_pay_round_dark.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/assets/images/apple_pay_round_light.svg b/assets/images/apple_pay_round_light.svg new file mode 100644 index 000000000..2beb1248f --- /dev/null +++ b/assets/images/apple_pay_round_light.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/assets/images/bank.png b/assets/images/bank.png new file mode 100644 index 0000000000000000000000000000000000000000..9dc68147aad18488da6007bca88b92ba13697559 GIT binary patch literal 1323 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrVEN?f;uumf=j~kE^`fC7ZTBNI zG^94Nh;~MO6uz<|Tsk>DadpBM^%V=m!fHy&yw<#7RXXCQ+be(3>q^iAr-jdw^jeip z8d~3+kT}P-*e-4EOw-quljh!;V4J@7_rCWtZ>FEyuwG=cwTi9I%T61)Q~%k7p9^{( zSBafsvbvXhQlHl?^ODII>|fsf*SKWAbkF>&$4WitdsOi#CrR2Xb6idoeVop6P`_3R z$k``WuKxLJb)0j)$1kgXX>OVC{Y&~Ibbi$yzaR6&Hm-7Rt@a+iZr9_-t}Ajl_O5pp zXp)L=S70&S$LGkARU_LZaOH;;i{grpr5sKHk6#P4EI?MMUL&wbYW)kx13wnv#T5me z%X2Q@=~sRG{{Q#(Gym3n{Hi)H>E3F~O6N7Nv-%Kgte0-SY;ie_KSF`Us_ek%~(udo=9(dmBx!=>u;LdwS?IMNh8&BTu ze04;-)l+}XEk>Sm-K_cMt0(`{_3_GP{&4o(w4;A?y{>c4V4M3-s3Fstp~?6$kHBn6 z#*VXVv>9%yYlK2^Kd>m?ZQiSMzEpIfr&>Fy^YOy^ka?*DAx z-0pj?E@JMVKTPrJMkbuRX0JMbCb9S$ZV}_%r&Zhh#ab=nmExbxZ!Hfl|J1T@cFGBt zW_uOen_DY9Uj7ggHrXn$%=^#c&+8SJKL2!i$@A!U!UiT$ypz)HpI-gSP&b{vG#5z0OmkXBMW%l;zcRN3K|K#}wFLzm&hF+S;_-{X>t<*$0k)LIq zx2M=YyX$@p$hRxJ^u>L~b6Iimn_HXYOqcZaF2C}=`o|%qxwd5&79U%ato;7lkF`^; zHZT8@v18J{TS*zeL_Ta;QvIpI?NYk#=j}XpvKJOBwnv?*S^ac}vhMM{Uj0&w`+DbO zRz5E;+*a^Lv+9aOY0i`DP9IBdy?=D`(8iZ(g0@*DR%Ss}HaXX2j^B&6Fx#sJ5!v-` z&FhM)nE8FZ0u2^cvqYZpJaBiPe2C>hf1g?_XM?<~FPAWbJy1f8p{~S4(}&^Di;SQd z3_mVjTw=iZVew+mM5aNY;l^(^!S823m*>1JG_lg#;_7pYcTIP13ewyisT{DOw5hn)QMl5|rBb+-!=&e>NVM3^#!8#r8Knqjk_b$MN@^z>DX z7IsZ-5>cz!++TMe5NY!5xp1X<$ISKrx9c(~YW!!c{2hJkt)%`Xknx_belF{r5}E)9 CLv&mK literal 0 HcmV?d00001 diff --git a/assets/images/bank_dark.svg b/assets/images/bank_dark.svg new file mode 100644 index 000000000..670120796 --- /dev/null +++ b/assets/images/bank_dark.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/assets/images/bank_light.svg b/assets/images/bank_light.svg new file mode 100644 index 000000000..804716289 --- /dev/null +++ b/assets/images/bank_light.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/assets/images/buy_sell.png b/assets/images/buy_sell.png new file mode 100644 index 0000000000000000000000000000000000000000..0fbffe56fb0e74a762e0b48e79c5930d5c116498 GIT binary patch literal 9931 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 \ No newline at end of file diff --git a/assets/images/card_dark.svg b/assets/images/card_dark.svg new file mode 100644 index 000000000..2e5bcf986 --- /dev/null +++ b/assets/images/card_dark.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/assets/images/dollar_coin.svg b/assets/images/dollar_coin.svg new file mode 100644 index 000000000..22218f332 --- /dev/null +++ b/assets/images/dollar_coin.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/google_pay_icon.png b/assets/images/google_pay_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a3ca38311cf31e5304b3fa2eb541d7e59dfe2863 GIT binary patch literal 12336 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 O_efn=ty;x4^8W!pu>c + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/revolut.png b/assets/images/revolut.png new file mode 100644 index 0000000000000000000000000000000000000000..bbe342592660209396ef45a6a119154437f93259 GIT binary patch literal 11588 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 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/usd_round_dark.svg b/assets/images/usd_round_dark.svg new file mode 100644 index 000000000..f329dd617 --- /dev/null +++ b/assets/images/usd_round_dark.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/images/usd_round_light.svg b/assets/images/usd_round_light.svg new file mode 100644 index 000000000..f5965c597 --- /dev/null +++ b/assets/images/usd_round_light.svg @@ -0,0 +1,2 @@ + + diff --git a/assets/images/wallet_new.png b/assets/images/wallet_new.png new file mode 100644 index 0000000000000000000000000000000000000000..47c43bfca99bc5cf9958e150081d4f885ed27802 GIT binary patch literal 5619 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 title; - Future launchProvider(BuildContext context, bool? isBuyAction); + Future? launchProvider( + {required BuildContext context, + required Quote quote, + required double amount, + required bool isBuyAction, + required String cryptoCurrencyAddress, + String? countryCode}) => + null; Future requestUrl(String amount, String sourceCurrency) => throw UnimplementedError(); Future findOrderById(String id) => throw UnimplementedError(); - Future calculateAmount(String amount, String sourceCurrency) => throw UnimplementedError(); + Future calculateAmount(String amount, String sourceCurrency) => + throw UnimplementedError(); + + Future> getAvailablePaymentTypes( + String fiatCurrency, String cryptoCurrency, bool isBuyAction) async => + []; + + Future?> fetchQuote( + {required CryptoCurrency cryptoCurrency, + required FiatCurrency fiatCurrency, + required double amount, + required bool isBuyAction, + required String walletAddress, + PaymentType? paymentType, + String? countryCode}) async => + null; } diff --git a/lib/buy/buy_quote.dart b/lib/buy/buy_quote.dart new file mode 100644 index 000000000..72ab7bd7d --- /dev/null +++ b/lib/buy/buy_quote.dart @@ -0,0 +1,302 @@ +import 'package:cake_wallet/buy/buy_provider.dart'; +import 'package:cake_wallet/buy/payment_method.dart'; +import 'package:cake_wallet/core/selectable_option.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/entities/provider_types.dart'; +import 'package:cake_wallet/exchange/limits.dart'; +import 'package:cw_core/crypto_currency.dart'; + +enum ProviderRecommendation { bestRate, lowKyc, successRate } + +extension RecommendationTitle on ProviderRecommendation { + String get title { + switch (this) { + case ProviderRecommendation.bestRate: + return 'BEST RATE'; + case ProviderRecommendation.lowKyc: + return 'LOW KYC'; + case ProviderRecommendation.successRate: + return 'SUCCESS RATE'; + } + } +} + +ProviderRecommendation? getRecommendationFromString(String title) { + switch (title) { + case 'BEST RATE': + return ProviderRecommendation.bestRate; + case 'LowKyc': + return ProviderRecommendation.lowKyc; + case 'SuccessRate': + return ProviderRecommendation.successRate; + default: + return null; + } +} + +class Quote extends SelectableOption { + Quote({ + required this.rate, + required this.feeAmount, + required this.networkFee, + required this.transactionFee, + required this.payout, + required this.provider, + required this.paymentType, + required this.recommendations, + this.isBuyAction = true, + this.quoteId, + this.rampId, + this.rampName, + this.rampIconPath, + this.limits, + }) : super(title: provider.isAggregator ? rampName ?? '' : provider.title); + + final double rate; + final double feeAmount; + final double networkFee; + final double transactionFee; + final double payout; + final PaymentType paymentType; + final BuyProvider provider; + final String? quoteId; + final List recommendations; + String? rampId; + String? rampName; + String? rampIconPath; + bool _isSelected = false; + bool _isBestRate = false; + bool isBuyAction; + Limits? limits; + + late FiatCurrency _fiatCurrency; + late CryptoCurrency _cryptoCurrency; + + + bool get isSelected => _isSelected; + bool get isBestRate => _isBestRate; + FiatCurrency get fiatCurrency => _fiatCurrency; + CryptoCurrency get cryptoCurrency => _cryptoCurrency; + + @override + bool get isOptionSelected => this._isSelected; + + @override + String get lightIconPath => + provider.isAggregator ? rampIconPath ?? provider.lightIcon : provider.lightIcon; + + @override + String get darkIconPath => + provider.isAggregator ? rampIconPath ?? provider.darkIcon : provider.darkIcon; + + @override + List get badges => recommendations.map((e) => e.title).toList(); + + @override + String get topLeftSubTitle => + this.rate > 0 ? '1 $cryptoName = ${rate.toStringAsFixed(2)} $fiatName' : ''; + + @override + String get bottomLeftSubTitle { + if (limits != null) { + final min = limits!.min; + final max = limits!.max; + return 'min: ${min} ${fiatCurrency.toString()} | max: ${max == double.infinity ? '' : '${max} ${fiatCurrency.toString()}'}'; + } + return ''; + } + + String get fiatName => isBuyAction ? fiatCurrency.toString() : cryptoCurrency.toString(); + + String get cryptoName => isBuyAction ? cryptoCurrency.toString() : fiatCurrency.toString(); + + @override + String? get topRightSubTitle => ''; + + @override + String get topRightSubTitleLightIconPath => provider.isAggregator ? provider.lightIcon : ''; + + @override + String get topRightSubTitleDarkIconPath => provider.isAggregator ? provider.darkIcon : ''; + + String get quoteTitle => '${provider.title} - ${paymentType.name}'; + + String get formatedFee => '$feeAmount ${isBuyAction ? fiatCurrency : cryptoCurrency}'; + + set setIsSelected(bool isSelected) => _isSelected = isSelected; + set setIsBestRate(bool isBestRate) => _isBestRate = isBestRate; + set setFiatCurrency(FiatCurrency fiatCurrency) => _fiatCurrency = fiatCurrency; + set setCryptoCurrency(CryptoCurrency cryptoCurrency) => _cryptoCurrency = cryptoCurrency; + set setLimits(Limits limits) => this.limits = limits; + + factory Quote.fromOnramperJson(Map json, bool isBuyAction, + Map metaData, PaymentType paymentType) { + final rate = _toDouble(json['rate']) ?? 0.0; + final networkFee = _toDouble(json['networkFee']) ?? 0.0; + final transactionFee = _toDouble(json['transactionFee']) ?? 0.0; + final feeAmount = double.parse((networkFee + transactionFee).toStringAsFixed(2)); + + final rampId = json['ramp'] as String? ?? ''; + final rampData = metaData[rampId] ?? {}; + final rampName = rampData['displayName'] as String? ?? ''; + final rampIconPath = rampData['svg'] as String? ?? ''; + + final recommendations = json['recommendations'] != null + ? List.from(json['recommendations'] as List) + : []; + + final enumRecommendations = recommendations + .map((e) => getRecommendationFromString(e)) + .whereType() + .toList(); + + final availablePaymentMethods = json['availablePaymentMethods'] as List? ?? []; + double minLimit = 0.0; + double maxLimit = double.infinity; + + for (var paymentMethod in availablePaymentMethods) { + if (paymentMethod is Map) { + final details = paymentMethod['details'] as Map?; + + if (details != null) { + final limits = details['limits'] as Map?; + + if (limits != null && limits.isNotEmpty) { + final firstLimitEntry = limits.values.first as Map?; + if (firstLimitEntry != null) { + minLimit = _toDouble(firstLimitEntry['min'])?.roundToDouble() ?? 0.0; + maxLimit = _toDouble(firstLimitEntry['max'])?.roundToDouble() ?? double.infinity; + break; + } + } + } + } + } + + return Quote( + rate: rate, + feeAmount: feeAmount, + networkFee: networkFee, + transactionFee: transactionFee, + payout: json['payout'] as double? ?? 0.0, + rampId: rampId, + rampName: rampName, + rampIconPath: rampIconPath, + paymentType: paymentType, + quoteId: json['quoteId'] as String? ?? '', + recommendations: enumRecommendations, + provider: ProvidersHelper.getProviderByType(ProviderType.onramper)!, + isBuyAction: isBuyAction, + limits: Limits(min: minLimit, max: maxLimit), + ); + } + + factory Quote.fromMoonPayJson( + Map json, bool isBuyAction, PaymentType paymentType) { + final rate = isBuyAction + ? json['quoteCurrencyPrice'] as double? ?? 0.0 + : json['baseCurrencyPrice'] as double? ?? 0.0; + final fee = _toDouble(json['feeAmount']) ?? 0.0; + final networkFee = _toDouble(json['networkFeeAmount']) ?? 0.0; + final transactionFee = _toDouble(json['extraFeeAmount']) ?? 0.0; + final feeAmount = double.parse((fee + networkFee + transactionFee).toStringAsFixed(2)); + + final baseCurrency = json['baseCurrency'] as Map?; + + double minLimit = 0.0; + double maxLimit = double.infinity; + + if (baseCurrency != null) { + minLimit = _toDouble(baseCurrency['minAmount']) ?? minLimit; + maxLimit = _toDouble(baseCurrency['maxAmount']) ?? maxLimit; + } + + return Quote( + rate: rate, + feeAmount: feeAmount, + networkFee: networkFee, + transactionFee: transactionFee, + payout: _toDouble(json['quoteCurrencyAmount']) ?? 0.0, + paymentType: paymentType, + recommendations: [], + quoteId: json['signature'] as String? ?? '', + provider: ProvidersHelper.getProviderByType(ProviderType.moonpay)!, + isBuyAction: isBuyAction, + limits: Limits(min: minLimit, max: maxLimit), + ); + } + + factory Quote.fromDFXJson( + Map json, + bool isBuyAction, + PaymentType paymentType, + ) { + final rate = _toDouble(json['exchangeRate']) ?? 0.0; + final fees = json['fees'] as Map; + + final minVolume = _toDouble(json['minVolume']) ?? 0.0; + final maxVolume = _toDouble(json['maxVolume']) ?? double.infinity; + + return Quote( + rate: isBuyAction ? rate : 1 / rate, + feeAmount: _toDouble(json['feeAmount']) ?? 0.0, + networkFee: _toDouble(fees['network']) ?? 0.0, + transactionFee: _toDouble(fees['rate']) ?? 0.0, + payout: _toDouble(json['payout']) ?? 0.0, + paymentType: paymentType, + recommendations: [ProviderRecommendation.lowKyc], + provider: ProvidersHelper.getProviderByType(ProviderType.dfx)!, + isBuyAction: isBuyAction, + limits: Limits(min: minVolume, max: maxVolume), + ); + } + + factory Quote.fromRobinhoodJson( + Map json, bool isBuyAction, PaymentType paymentType) { + final networkFee = json['networkFee'] as Map; + final processingFee = json['processingFee'] as Map; + final networkFeeAmount = _toDouble(networkFee['fiatAmount']) ?? 0.0; + final transactionFeeAmount = _toDouble(processingFee['fiatAmount']) ?? 0.0; + final feeAmount = double.parse((networkFeeAmount + transactionFeeAmount).toStringAsFixed(2)); + + return Quote( + rate: _toDouble(json['price']) ?? 0.0, + feeAmount: feeAmount, + networkFee: _toDouble(networkFee['fiatAmount']) ?? 0.0, + transactionFee: _toDouble(processingFee['fiatAmount']) ?? 0.0, + payout: _toDouble(json['cryptoAmount']) ?? 0.0, + paymentType: paymentType, + recommendations: [], + provider: ProvidersHelper.getProviderByType(ProviderType.robinhood)!, + isBuyAction: isBuyAction, + limits: Limits(min: 0.0, max: double.infinity), + ); + } + + factory Quote.fromMeldJson(Map json, bool isBuyAction, PaymentType paymentType) { + final quotes = json['quotes'][0] as Map; + return Quote( + rate: quotes['exchangeRate'] as double? ?? 0.0, + feeAmount: quotes['totalFee'] as double? ?? 0.0, + networkFee: quotes['networkFee'] as double? ?? 0.0, + transactionFee: quotes['transactionFee'] as double? ?? 0.0, + payout: quotes['payout'] as double? ?? 0.0, + paymentType: paymentType, + recommendations: [], + provider: ProvidersHelper.getProviderByType(ProviderType.meld)!, + isBuyAction: isBuyAction, + limits: Limits(min: 0.0, max: double.infinity), + ); + } + + static double? _toDouble(dynamic value) { + if (value is int) { + return value.toDouble(); + } else if (value is double) { + return value; + } else if (value is String) { + return double.tryParse(value); + } + return null; + } +} diff --git a/lib/buy/dfx/dfx_buy_provider.dart b/lib/buy/dfx/dfx_buy_provider.dart index b3ed72498..c1ed762b1 100644 --- a/lib/buy/dfx/dfx_buy_provider.dart +++ b/lib/buy/dfx/dfx_buy_provider.dart @@ -1,13 +1,17 @@ import 'dart:convert'; +import 'dart:developer'; 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/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; @@ -15,10 +19,12 @@ import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; class DFXBuyProvider extends BuyProvider { - DFXBuyProvider({required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM}) + DFXBuyProvider( + {required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM}) : super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: ledgerVM); static const _baseUrl = 'api.dfx.swiss'; + // static const _signMessagePath = '/v1/auth/signMessage'; static const _authPath = '/v1/auth'; static const walletName = 'CakeWallet'; @@ -35,24 +41,8 @@ class DFXBuyProvider extends BuyProvider { @override String get darkIcon => 'assets/images/dfx_dark.png'; - String get assetOut { - switch (wallet.type) { - case WalletType.bitcoin: - return 'BTC'; - case WalletType.bitcoinCash: - return 'BCH'; - case WalletType.litecoin: - return 'LTC'; - case WalletType.monero: - return 'XMR'; - case WalletType.ethereum: - return 'ETH'; - case WalletType.polygon: - return 'MATIC'; - default: - throw Exception("WalletType is not available for DFX ${wallet.type}"); - } - } + @override + bool get isAggregator => false; String get blockchain { switch (wallet.type) { @@ -60,21 +50,13 @@ class DFXBuyProvider extends BuyProvider { case WalletType.bitcoinCash: case WalletType.litecoin: return 'Bitcoin'; - case WalletType.monero: - return 'Monero'; - case WalletType.ethereum: - return 'Ethereum'; - case WalletType.polygon: - return 'Polygon'; default: - throw Exception("WalletType is not available for DFX ${wallet.type}"); + return walletTypeToString(wallet.type); } } - String get walletAddress => - wallet.walletAddresses.primaryAddress ?? wallet.walletAddresses.address; - Future getSignMessage() async => + Future getSignMessage(String walletAddress) async => "By_signing_this_message,_you_confirm_that_you_are_the_sole_owner_of_the_provided_Blockchain_address._Your_ID:_$walletAddress"; // // Lets keep this just in case, but we can avoid this API Call @@ -92,8 +74,9 @@ class DFXBuyProvider extends BuyProvider { // } // } - Future auth() async { - final signMessage = await getSignature(await getSignMessage()); + Future auth(String walletAddress) async { + final signMessage = await getSignature( + await getSignMessage(walletAddress), walletAddress); final requestBody = jsonEncode({ 'wallet': walletName, @@ -120,7 +103,7 @@ class DFXBuyProvider extends BuyProvider { } } - Future getSignature(String message) async { + Future getSignature(String message, String walletAddress) async { switch (wallet.type) { case WalletType.ethereum: case WalletType.polygon: @@ -135,8 +118,178 @@ class DFXBuyProvider extends BuyProvider { } } + Future> fetchFiatCredentials(String fiatCurrency) async { + final url = Uri.https(_baseUrl, '/v1/fiat'); + + try { + final response = await http.get(url, headers: {'accept': 'application/json'}); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body) as List; + for (final item in data) { + if (item['name'] == fiatCurrency) return item as Map; + } + log('DFX does not support fiat: $fiatCurrency'); + return {}; + } else { + log('DFX Failed to fetch fiat currencies: ${response.statusCode}'); + return {}; + } + } catch (e) { + print('DFX Error fetching fiat currencies: $e'); + return {}; + } + } + + Future> fetchAssetCredential(String assetsName) async { + final url = Uri.https(_baseUrl, '/v1/asset', {'blockchains': blockchain}); + + try { + final response = await http.get(url, headers: {'accept': 'application/json'}); + + if (response.statusCode == 200) { + final responseData = jsonDecode(response.body); + + if (responseData is List && responseData.isNotEmpty) { + return responseData.first as Map; + } else if (responseData is Map) { + return responseData; + } else { + log('DFX: Does not support this asset name : ${blockchain}'); + } + } else { + log('DFX: Failed to fetch assets: ${response.statusCode}'); + } + } catch (e) { + log('DFX: Error fetching assets: $e'); + } + return {}; + } + + Future> getAvailablePaymentTypes( + String fiatCurrency, String cryptoCurrency, bool isBuyAction) async { + final List paymentMethods = []; + + if (isBuyAction) { + final fiatBuyCredentials = await fetchFiatCredentials(fiatCurrency); + if (fiatBuyCredentials.isNotEmpty) { + fiatBuyCredentials.forEach((key, value) { + if (key == 'limits') { + final limits = value as Map; + limits.forEach((paymentMethodKey, paymentMethodValue) { + final min = _toDouble(paymentMethodValue['minVolume']); + final max = _toDouble(paymentMethodValue['maxVolume']); + if (min != null && max != null && min > 0 && max > 0) { + final paymentMethod = PaymentMethod.fromDFX( + paymentMethodKey, _getPaymentTypeByString(paymentMethodKey)); + paymentMethods.add(paymentMethod); + } + }); + } + }); + } + } else { + final assetCredentials = await fetchAssetCredential(cryptoCurrency); + if (assetCredentials.isNotEmpty) { + if (assetCredentials['sellable'] == true) { + final availablePaymentTypes = [ + PaymentType.bankTransfer, + PaymentType.creditCard, + PaymentType.sepa + ]; + availablePaymentTypes.forEach((element) { + final paymentMethod = PaymentMethod.fromDFX(normalizePaymentMethod(element)!, element); + paymentMethods.add(paymentMethod); + }); + } + } + } + + return paymentMethods; + } + @override - Future launchProvider(BuildContext context, bool? isBuyAction) async { + Future?> fetchQuote( + {required CryptoCurrency cryptoCurrency, + required FiatCurrency fiatCurrency, + required double amount, + required bool isBuyAction, + required String walletAddress, + PaymentType? paymentType, + String? countryCode}) async { + /// if buying with any currency other than eur or chf then DFX is not supported + + if (isBuyAction && (fiatCurrency != FiatCurrency.eur && fiatCurrency != FiatCurrency.chf)) { + return null; + } + + String? paymentMethod; + if (paymentType != null && paymentType != PaymentType.all) { + paymentMethod = normalizePaymentMethod(paymentType); + if (paymentMethod == null) paymentMethod = paymentType.name; + } else { + paymentMethod = 'Bank'; + } + + final action = isBuyAction ? 'buy' : 'sell'; + + if (isBuyAction && cryptoCurrency != wallet.currency) return null; + + final fiatCredentials = await fetchFiatCredentials(fiatCurrency.name.toString()); + if (fiatCredentials['id'] == null) return null; + + final assetCredentials = await fetchAssetCredential(cryptoCurrency.title.toString()); + if (assetCredentials['id'] == null) return null; + + log('DFX: Fetching $action quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod'); + + final url = Uri.https(_baseUrl, '/v1/$action/quote'); + final headers = {'accept': 'application/json', 'Content-Type': 'application/json'}; + final body = jsonEncode({ + 'currency': {'id': fiatCredentials['id'] as int}, + 'asset': {'id': assetCredentials['id']}, + 'amount': amount, + 'targetAmount': 0, + 'paymentMethod': paymentMethod, + 'discountCode': '' + }); + + try { + final response = await http.put(url, headers: headers, body: body); + final responseData = jsonDecode(response.body); + + if (response.statusCode == 200) { + if (responseData is Map) { + final paymentType = _getPaymentTypeByString(responseData['paymentMethod'] as String?); + final quote = Quote.fromDFXJson(responseData, isBuyAction, paymentType); + quote.setFiatCurrency = fiatCurrency; + quote.setCryptoCurrency = cryptoCurrency; + return [quote]; + } else { + print('DFX: Unexpected data type: ${responseData.runtimeType}'); + return null; + } + } else { + if (responseData is Map && responseData.containsKey('message')) { + print('DFX Error: ${responseData['message']}'); + } else { + print('DFX Failed to fetch buy quote: ${response.statusCode}'); + } + return null; + } + } catch (e) { + print('DFX Error fetching buy quote: $e'); + return null; + } + } + + Future? launchProvider( + {required BuildContext context, + required Quote quote, + required double amount, + required bool isBuyAction, + required String cryptoCurrencyAddress, + String? countryCode}) async { if (wallet.isHardwareWallet) { if (!ledgerVM!.isConnected) { await Navigator.of(context).pushNamed(Routes.connectDevices, @@ -152,26 +305,21 @@ class DFXBuyProvider extends BuyProvider { } try { - final assetOut = this.assetOut; - final blockchain = this.blockchain; - final actionType = isBuyAction == true ? '/buy' : '/sell'; + final actionType = isBuyAction ? '/buy' : '/sell'; - final accessToken = await auth(); + final accessToken = await auth(cryptoCurrencyAddress); final uri = Uri.https('services.dfx.swiss', actionType, { 'session': accessToken, 'lang': 'en', - 'asset-out': assetOut, + 'asset-out': isBuyAction ? quote.cryptoCurrency.toString() : quote.fiatCurrency.toString(), 'blockchain': blockchain, - 'asset-in': 'EUR', + 'asset-in': isBuyAction ? quote.fiatCurrency.toString() : quote.cryptoCurrency.toString(), + 'amount': amount.toString() //TODO: Amount does not work }); if (await canLaunchUrl(uri)) { - if (DeviceInfo.instance.isMobile) { - Navigator.of(context).pushNamed(Routes.webViewPage, arguments: [title, uri]); - } else { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } + await launchUrl(uri, mode: LaunchMode.externalApplication); } else { throw Exception('Could not launch URL'); } @@ -187,4 +335,39 @@ class DFXBuyProvider extends BuyProvider { }); } } + + String? normalizePaymentMethod(PaymentType paymentMethod) { + switch (paymentMethod) { + case PaymentType.bankTransfer: + return 'Bank'; + case PaymentType.creditCard: + return 'Card'; + case PaymentType.sepa: + return 'Instant'; + default: + return null; + } + } + + PaymentType _getPaymentTypeByString(String? paymentMethod) { + switch (paymentMethod) { + case 'Bank': + return PaymentType.bankTransfer; + case 'Card': + return PaymentType.creditCard; + case 'Instant': + return PaymentType.sepa; + default: + return PaymentType.all; + } + } + + double? _toDouble(dynamic value) { + if (value is int) { + return value.toDouble(); + } else if (value is double) { + return value; + } + return null; + } } diff --git a/lib/buy/meld/meld_buy_provider.dart b/lib/buy/meld/meld_buy_provider.dart new file mode 100644 index 000000000..696301f2e --- /dev/null +++ b/lib/buy/meld/meld_buy_provider.dart @@ -0,0 +1,266 @@ +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/entities/provider_types.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/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 MeldBuyProvider extends BuyProvider { + MeldBuyProvider({required WalletBase wallet, bool isTestEnvironment = false}) + : super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: null); + + static const _isProduction = false; + + static const _baseUrl = _isProduction ? 'api.meld.io' : 'api-sb.meld.io'; + static const _providersProperties = '/service-providers/properties'; + static const _paymentMethodsPath = '/payment-methods'; + static const _quotePath = '/payments/crypto/quote'; + + static const String sandboxUrl = 'sb.fluidmoney.xyz'; + static const String productionUrl = 'fluidmoney.xyz'; + + static const String _baseWidgetUrl = _isProduction ? productionUrl : sandboxUrl; + + static String get _testApiKey => secrets.meldTestApiKey; + + static String get _testPublicKey => '' ; //secrets.meldTestPublicKey; + + @override + String get title => 'Meld'; + + @override + String get providerDescription => 'Meld Buy Provider'; + + @override + String get lightIcon => 'assets/images/meld_logo.svg'; + + @override + String get darkIcon => 'assets/images/meld_logo.svg'; + + @override + bool get isAggregator => true; + + @override + Future> getAvailablePaymentTypes( + String fiatCurrency, String cryptoCurrency, bool isBuyAction) async { + final params = {'fiatCurrencies': fiatCurrency, 'statuses': 'LIVE,RECENTLY_ADDED,BUILDING'}; + + final path = '$_providersProperties$_paymentMethodsPath'; + final url = Uri.https(_baseUrl, path, params); + + try { + final response = await http.get( + url, + headers: { + 'Authorization': _isProduction ? '' : _testApiKey, + 'Meld-Version': '2023-12-19', + 'accept': 'application/json', + 'content-type': 'application/json', + }, + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body) as List; + final paymentMethods = + data.map((e) => PaymentMethod.fromMeldJson(e as Map)).toList(); + return paymentMethods; + } else { + print('Meld: Failed to fetch payment types'); + return List.empty(); + } + } catch (e) { + print('Meld: Failed to fetch payment types: $e'); + return List.empty(); + } + } + + @override + Future?> fetchQuote( + {required CryptoCurrency cryptoCurrency, + required FiatCurrency fiatCurrency, + required double amount, + required bool isBuyAction, + required String walletAddress, + PaymentType? paymentType, + String? countryCode}) async { + String? paymentMethod; + if (paymentType != null && paymentType != PaymentType.all) { + paymentMethod = normalizePaymentMethod(paymentType); + if (paymentMethod == null) paymentMethod = paymentType.name; + } + + log('Meld: Fetching buy quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount'); + + final url = Uri.https(_baseUrl, _quotePath); + final headers = { + 'Authorization': _testApiKey, + 'Meld-Version': '2023-12-19', + 'accept': 'application/json', + 'content-type': 'application/json', + }; + final body = jsonEncode({ + 'countryCode': countryCode, + 'destinationCurrencyCode': isBuyAction ? fiatCurrency.name : cryptoCurrency.title, + 'sourceAmount': amount, + 'sourceCurrencyCode': isBuyAction ? cryptoCurrency.title : fiatCurrency.name, + if (paymentMethod != null) 'paymentMethod': paymentMethod, + }); + + try { + final response = await http.post(url, headers: headers, body: body); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body) as Map; + final paymentType = _getPaymentTypeByString(data['paymentMethodType'] as String?); + final quote = Quote.fromMeldJson(data, isBuyAction, paymentType); + + quote.setFiatCurrency = fiatCurrency; + quote.setCryptoCurrency = cryptoCurrency; + + return [quote]; + } else { + return null; + } + } catch (e) { + print('Error fetching buy quote: $e'); + return null; + } + } + + Future? launchProvider( + {required BuildContext context, + required Quote quote, + required double amount, + required bool isBuyAction, + required String cryptoCurrencyAddress, + String? countryCode}) async { + final actionType = isBuyAction ? 'BUY' : 'SELL'; + + final params = { + 'publicKey': _isProduction ? '' : _testPublicKey, + 'countryCode': countryCode, + //'paymentMethodType': normalizePaymentMethod(paymentMethod.paymentMethodType), + 'sourceAmount': amount.toString(), + 'sourceCurrencyCode': quote.fiatCurrency, + 'destinationCurrencyCode': quote.cryptoCurrency, + 'walletAddress': cryptoCurrencyAddress, + 'transactionType': actionType + }; + + final uri = Uri.https(_baseWidgetUrl, '', 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: "Meld", + alertContent: S.of(context).buy_provider_unavailable + ': $e', + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + } + } + + String? normalizePaymentMethod(PaymentType paymentType) { + switch (paymentType) { + case PaymentType.creditCard: + return 'CREDIT_DEBIT_CARD'; + case PaymentType.applePay: + return 'APPLE_PAY'; + case PaymentType.googlePay: + return 'GOOGLE_PAY'; + case PaymentType.neteller: + return 'NETELLER'; + case PaymentType.skrill: + return 'SKRILL'; + case PaymentType.sepa: + return 'SEPA'; + case PaymentType.sepaInstant: + return 'SEPA_INSTANT'; + case PaymentType.ach: + return 'ACH'; + case PaymentType.achInstant: + return 'INSTANT_ACH'; + case PaymentType.Khipu: + return 'KHIPU'; + case PaymentType.ovo: + return 'OVO'; + case PaymentType.zaloPay: + return 'ZALOPAY'; + case PaymentType.zaloBankTransfer: + return 'ZA_BANK_TRANSFER'; + case PaymentType.gcash: + return 'GCASH'; + case PaymentType.imps: + return 'IMPS'; + case PaymentType.dana: + return 'DANA'; + case PaymentType.ideal: + return 'IDEAL'; + default: + return null; + } + } + + PaymentType _getPaymentTypeByString(String? paymentMethod) { + switch (paymentMethod?.toUpperCase()) { + case 'CREDIT_DEBIT_CARD': + return PaymentType.creditCard; + case 'APPLE_PAY': + return PaymentType.applePay; + case 'GOOGLE_PAY': + return PaymentType.googlePay; + case 'NETELLER': + return PaymentType.neteller; + case 'SKRILL': + return PaymentType.skrill; + case 'SEPA': + return PaymentType.sepa; + case 'SEPA_INSTANT': + return PaymentType.sepaInstant; + case 'ACH': + return PaymentType.ach; + case 'INSTANT_ACH': + return PaymentType.achInstant; + case 'KHIPU': + return PaymentType.Khipu; + case 'OVO': + return PaymentType.ovo; + case 'ZALOPAY': + return PaymentType.zaloPay; + case 'ZA_BANK_TRANSFER': + return PaymentType.zaloBankTransfer; + case 'GCASH': + return PaymentType.gcash; + case 'IMPS': + return PaymentType.imps; + case 'DANA': + return PaymentType.dana; + case 'IDEAL': + return PaymentType.ideal; + default: + return PaymentType.all; + } + } +} diff --git a/lib/buy/moonpay/moonpay_provider.dart b/lib/buy/moonpay/moonpay_provider.dart index 67ee75d7c..b93c0f02d 100644 --- a/lib/buy/moonpay/moonpay_provider.dart +++ b/lib/buy/moonpay/moonpay_provider.dart @@ -1,19 +1,20 @@ import 'dart:convert'; +import 'dart:developer'; import 'package:cake_wallet/.secrets.g.dart' as secrets; -import 'package:cake_wallet/buy/buy_amount.dart'; import 'package:cake_wallet/buy/buy_exception.dart'; import 'package:cake_wallet/buy/buy_provider.dart'; import 'package:cake_wallet/buy/buy_provider_description.dart'; +import 'package:cake_wallet/buy/buy_quote.dart'; import 'package:cake_wallet/buy/order.dart'; +import 'package:cake_wallet/buy/payment_method.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/palette.dart'; -import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/themes/theme_base.dart'; -import 'package:cake_wallet/utils/device_info.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; @@ -39,6 +40,15 @@ class MoonPayProvider extends BuyProvider { static const _baseBuyProductUrl = 'buy.moonpay.com'; static const _cIdBaseUrl = 'exchange-helper.cakewallet.com'; static const _apiUrl = 'https://api.moonpay.com'; + static const _baseUrl = 'api.moonpay.com'; + static const _currenciesPath = '/v3/currencies'; + static const _buyQuote = '/buy_quote'; + static const _sellQuote = '/sell_quote'; + + static const _transactionsSuffix = '/v1/transactions'; + + final String baseBuyUrl; + final String baseSellUrl; @override String get providerDescription => @@ -53,6 +63,17 @@ class MoonPayProvider extends BuyProvider { @override String get darkIcon => 'assets/images/moonpay_dark.png'; + @override + bool get isAggregator => false; + + static String get _apiKey => secrets.moonPayApiKey; + + String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase(); + + String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId='; + + static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey; + static String themeToMoonPayTheme(ThemeBase theme) { switch (theme.type) { case ThemeType.bright: @@ -63,28 +84,12 @@ class MoonPayProvider extends BuyProvider { } } - static String get _apiKey => secrets.moonPayApiKey; - - final String baseBuyUrl; - final String baseSellUrl; - - String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase(); - - String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId='; - - static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey; - Future getMoonpaySignature(String query) async { final uri = Uri.https(_cIdBaseUrl, "/api/moonpay"); - final response = await post( - uri, - headers: { - 'Content-Type': 'application/json', - 'x-api-key': _exchangeHelperApiKey, - }, - body: json.encode({'query': query}), - ); + final response = await post(uri, + headers: {'Content-Type': 'application/json', 'x-api-key': _exchangeHelperApiKey}, + body: json.encode({'query': query})); if (response.statusCode == 200) { return (jsonDecode(response.body) as Map)['signature'] as String; @@ -94,85 +99,195 @@ class MoonPayProvider extends BuyProvider { } } - Future requestSellMoonPayUrl({ - required CryptoCurrency currency, - required String refundWalletAddress, - required SettingsStore settingsStore, - }) async { - final params = { - 'theme': themeToMoonPayTheme(settingsStore.currentTheme), - 'language': settingsStore.languageCode, - 'colorCode': settingsStore.currentTheme.type == ThemeType.dark - ? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}' - : '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}', - 'defaultCurrencyCode': _normalizeCurrency(currency), - 'refundWalletAddress': refundWalletAddress, - }; + Future> fetchFiatCredentials( + String fiatCurrency, String cryptocurrency, String? paymentMethod) async { + final params = {'baseCurrencyCode': fiatCurrency.toLowerCase(), 'apiKey': _apiKey}; - if (_apiKey.isNotEmpty) { - params['apiKey'] = _apiKey; + if (paymentMethod != null) params['paymentMethod'] = paymentMethod; + + final path = '$_currenciesPath/${cryptocurrency.toLowerCase()}/limits'; + final url = Uri.https(_baseUrl, path, params); + + try { + final response = await get(url, headers: {'accept': 'application/json'}); + if (response.statusCode == 200) { + return jsonDecode(response.body) as Map; + } else { + print('MoonPay does not support fiat: $fiatCurrency'); + return {}; + } + } catch (e) { + print('MoonPay Error fetching fiat currencies: $e'); + return {}; } - - final originalUri = Uri.https( - baseSellUrl, - '', - params, - ); - - if (isTestEnvironment) { - return originalUri; - } - - final signature = await getMoonpaySignature('?${originalUri.query}'); - - final query = Map.from(originalUri.queryParameters); - query['signature'] = signature; - final signedUri = originalUri.replace(queryParameters: query); - return signedUri; } - // BUY: - static const _currenciesSuffix = '/v3/currencies'; - static const _quoteSuffix = '/buy_quote'; - static const _transactionsSuffix = '/v1/transactions'; - static const _ipAddressSuffix = '/v4/ip_address'; + Future> getAvailablePaymentTypes( + String fiatCurrency, String cryptoCurrency, bool isBuyAction) async { + final List paymentMethods = []; + + if (isBuyAction) { + final fiatBuyCredentials = await fetchFiatCredentials(fiatCurrency, cryptoCurrency, null); + if (fiatBuyCredentials.isNotEmpty) { + final paymentMethod = fiatBuyCredentials['paymentMethod'] as String?; + paymentMethods.add(PaymentMethod.fromMoonPayJson( + fiatBuyCredentials, _getPaymentTypeByString(paymentMethod))); + return paymentMethods; + } + } + + 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 { + String? paymentMethod; + + if (paymentType != null && paymentType != PaymentType.all) { + paymentMethod = normalizePaymentMethod(paymentType); + if (paymentMethod == null) paymentMethod = paymentType.name; + } else { + paymentMethod = 'credit_debit_card'; + } + + final action = isBuyAction ? 'buy' : 'sell'; + + final formattedCryptoCurrency = _normalizeCurrency(cryptoCurrency); + final baseCurrencyCode = + isBuyAction ? fiatCurrency.name.toLowerCase() : cryptoCurrency.title.toLowerCase(); - Future requestBuyMoonPayUrl({ - required CryptoCurrency currency, - required SettingsStore settingsStore, - required String walletAddress, - String? amount, - }) async { final params = { - 'theme': themeToMoonPayTheme(settingsStore.currentTheme), - 'language': settingsStore.languageCode, - 'colorCode': settingsStore.currentTheme.type == ThemeType.dark + 'baseCurrencyCode': baseCurrencyCode, + 'baseCurrencyAmount': amount.toString(), + 'amount': amount.toString(), + 'paymentMethod': paymentMethod, + 'areFeesIncluded': 'false', + 'apiKey': _apiKey + }; + + log('MoonPay: Fetching $action quote: ${isBuyAction ? formattedCryptoCurrency : fiatCurrency.name.toLowerCase()} -> ${isBuyAction ? baseCurrencyCode : formattedCryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod'); + + final quotePath = isBuyAction ? _buyQuote : _sellQuote; + + final path = '$_currenciesPath/$formattedCryptoCurrency$quotePath'; + final url = Uri.https(_baseUrl, path, params); + try { + final response = await get(url); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body) as Map; + + // Check if the response is for the correct fiat currency + if (isBuyAction) { + final fiatCurrencyCode = data['baseCurrencyCode'] as String?; + if (fiatCurrencyCode == null || fiatCurrencyCode != fiatCurrency.name.toLowerCase()) + return null; + } else { + final quoteCurrency = data['quoteCurrency'] as Map?; + if (quoteCurrency == null || quoteCurrency['code'] != fiatCurrency.name.toLowerCase()) + return null; + } + + final paymentMethods = data['paymentMethod'] as String?; + final quote = + Quote.fromMoonPayJson(data, isBuyAction, _getPaymentTypeByString(paymentMethods)); + + quote.setFiatCurrency = fiatCurrency; + quote.setCryptoCurrency = cryptoCurrency; + + return [quote]; + } else { + print('Moon Pay: Error fetching buy quote: '); + return null; + } + } catch (e) { + print('Moon Pay: Error fetching buy quote: $e'); + return null; + } + } + + @override + Future? launchProvider( + {required BuildContext context, + required Quote quote, + required double amount, + required bool isBuyAction, + required String cryptoCurrencyAddress, + String? countryCode}) async { + + final Map params = { + 'theme': themeToMoonPayTheme(_settingsStore.currentTheme), + 'language': _settingsStore.languageCode, + 'colorCode': _settingsStore.currentTheme.type == ThemeType.dark ? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}' : '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}', - 'baseCurrencyCode': settingsStore.fiatCurrency.title, - 'baseCurrencyAmount': amount ?? '0', - 'currencyCode': _normalizeCurrency(currency), - 'walletAddress': walletAddress, + 'baseCurrencyCode': isBuyAction ? quote.fiatCurrency.name : quote.cryptoCurrency.name, + 'baseCurrencyAmount': amount.toString(), + 'walletAddress': cryptoCurrencyAddress, 'lockAmount': 'false', 'showAllCurrencies': 'false', 'showWalletAddressForm': 'false', - 'enabledPaymentMethods': - 'credit_debit_card,apple_pay,google_pay,samsung_pay,sepa_bank_transfer,gbp_bank_transfer,gbp_open_banking_payment', + if (isBuyAction) + 'enabledPaymentMethods': normalizePaymentMethod(quote.paymentType) ?? + 'credit_debit_card,apple_pay,google_pay,samsung_pay,sepa_bank_transfer,gbp_bank_transfer,gbp_open_banking_payment', + if (!isBuyAction) 'refundWalletAddress': cryptoCurrencyAddress }; - if (_apiKey.isNotEmpty) { - params['apiKey'] = _apiKey; - } + if (isBuyAction) params['currencyCode'] = quote.cryptoCurrency.name; + if (!isBuyAction) params['quoteCurrencyCode'] = quote.cryptoCurrency.name; - final originalUri = Uri.https( - baseBuyUrl, - '', - params, - ); + try { + { + final uri = await requestMoonPayUrl( + walletAddress: cryptoCurrencyAddress, + settingsStore: _settingsStore, + isBuyAction: isBuyAction, + amount: amount.toString(), + params: params); - if (isTestEnvironment) { - return originalUri; + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } else { + throw Exception('Could not launch URL'); + } + } + } catch (e) { + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: 'MoonPay', + alertContent: 'The MoonPay service is currently unavailable: $e', + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ); + }, + ); + } } + } + + Future requestMoonPayUrl({ + required String walletAddress, + required SettingsStore settingsStore, + required bool isBuyAction, + required Map params, + String? amount, + }) async { + if (_apiKey.isNotEmpty) params['apiKey'] = _apiKey; + + final baseUrl = isBuyAction ? baseBuyUrl : baseSellUrl; + final originalUri = Uri.https(baseUrl, '', params); + + if (isTestEnvironment) return originalUri; final signature = await getMoonpaySignature('?${originalUri.query}'); final query = Map.from(originalUri.queryParameters); @@ -181,33 +296,6 @@ class MoonPayProvider extends BuyProvider { return signedUri; } - Future calculateAmount(String amount, String sourceCurrency) async { - final url = _apiUrl + - _currenciesSuffix + - '/$currencyCode' + - _quoteSuffix + - '/?apiKey=' + - _apiKey + - '&baseCurrencyAmount=' + - amount + - '&baseCurrencyCode=' + - sourceCurrency.toLowerCase(); - final uri = Uri.parse(url); - final response = await get(uri); - - if (response.statusCode != 200) { - throw BuyException(title: providerDescription, content: 'Quote is not found!'); - } - - final responseJSON = json.decode(response.body) as Map; - final sourceAmount = responseJSON['totalAmount'] as double; - final destAmount = responseJSON['quoteCurrencyAmount'] as double; - final minSourceAmount = responseJSON['baseCurrency']['minAmount'] as int; - - return BuyAmount( - sourceAmount: sourceAmount, destAmount: destAmount, minAmount: minSourceAmount); - } - Future findOrderById(String id) async { final url = _apiUrl + _transactionsSuffix + '/$id' + '?apiKey=' + _apiKey; final uri = Uri.parse(url); @@ -235,74 +323,83 @@ class MoonPayProvider extends BuyProvider { walletId: wallet.id); } - static Future onEnabled() async { - final url = _apiUrl + _ipAddressSuffix + '?apiKey=' + _apiKey; - var isBuyEnable = false; - final uri = Uri.parse(url); - final response = await get(uri); - - try { - final responseJSON = json.decode(response.body) as Map; - isBuyEnable = responseJSON['isBuyAllowed'] as bool; - } catch (e) { - isBuyEnable = false; - print(e.toString()); - } - - return isBuyEnable; - } - - @override - Future launchProvider(BuildContext context, bool? isBuyAction) async { - try { - late final Uri uri; - if (isBuyAction ?? true) { - uri = await requestBuyMoonPayUrl( - currency: wallet.currency, - walletAddress: wallet.walletAddresses.address, - settingsStore: _settingsStore, - ); - } else { - uri = await requestSellMoonPayUrl( - currency: wallet.currency, - refundWalletAddress: wallet.walletAddresses.address, - settingsStore: _settingsStore, - ); - } - - if (await canLaunchUrl(uri)) { - if (DeviceInfo.instance.isMobile) { - Navigator.of(context).pushNamed(Routes.webViewPage, arguments: ['MoonPay', uri]); - } else { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } - } else { - throw Exception('Could not launch URL'); - } - } catch (e) { - if (context.mounted) { - await showDialog( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: 'MoonPay', - alertContent: 'The MoonPay service is currently unavailable: $e', - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop(), - ); - }, - ); - } - } - } - String _normalizeCurrency(CryptoCurrency currency) { - if (currency == CryptoCurrency.maticpoly) { - return "POL_POLYGON"; - } else if (currency == CryptoCurrency.matic) { - return "POL"; + if (currency.tag == 'POLY') { + return '${currency.title.toLowerCase()}_polygon'; + } + + if (currency.tag == 'TRX') { + return '${currency.title.toLowerCase()}_trx'; } return currency.toString().toLowerCase(); } + + String? normalizePaymentMethod(PaymentType paymentMethod) { + switch (paymentMethod) { + case PaymentType.creditCard: + return 'credit_debit_card'; + case PaymentType.debitCard: + return 'credit_debit_card'; + case PaymentType.ach: + return 'ach_bank_transfer'; + case PaymentType.applePay: + return 'apple_pay'; + case PaymentType.googlePay: + return 'google_pay'; + case PaymentType.sepa: + return 'sepa_bank_transfer'; + case PaymentType.paypal: + return 'paypal'; + case PaymentType.sepaOpenBankingPayment: + return 'sepa_open_banking_payment'; + case PaymentType.gbpOpenBankingPayment: + return 'gbp_open_banking_payment'; + case PaymentType.lowCostAch: + return 'low_cost_ach'; + case PaymentType.mobileWallet: + return 'mobile_wallet'; + case PaymentType.pixInstantPayment: + return 'pix_instant_payment'; + case PaymentType.yellowCardBankTransfer: + return 'yellow_card_bank_transfer'; + case PaymentType.fiatBalance: + return 'fiat_balance'; + default: + return null; + } + } + + PaymentType _getPaymentTypeByString(String? paymentMethod) { + switch (paymentMethod) { + case 'ach_bank_transfer': + return PaymentType.ach; + case 'apple_pay': + return PaymentType.applePay; + case 'credit_debit_card': + return PaymentType.creditCard; + case 'fiat_balance': + return PaymentType.fiatBalance; + case 'gbp_open_banking_payment': + return PaymentType.gbpOpenBankingPayment; + case 'google_pay': + return PaymentType.googlePay; + case 'low_cost_ach': + return PaymentType.lowCostAch; + case 'mobile_wallet': + return PaymentType.mobileWallet; + case 'paypal': + return PaymentType.paypal; + case 'pix_instant_payment': + return PaymentType.pixInstantPayment; + case 'sepa_bank_transfer': + return PaymentType.sepa; + case 'sepa_open_banking_payment': + return PaymentType.sepaOpenBankingPayment; + case 'yellow_card_bank_transfer': + return PaymentType.yellowCardBankTransfer; + default: + return PaymentType.all; + } + } } diff --git a/lib/buy/onramper/onramper_buy_provider.dart b/lib/buy/onramper/onramper_buy_provider.dart index 1f1c86962..ee9c9bb74 100644 --- a/lib/buy/onramper/onramper_buy_provider.dart +++ b/lib/buy/onramper/onramper_buy_provider.dart @@ -1,13 +1,19 @@ +import 'dart:convert'; +import 'dart:developer'; + 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/generated/i18n.dart'; -import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cake_wallet/utils/device_info.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/currency.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; class OnRamperBuyProvider extends BuyProvider { @@ -16,9 +22,15 @@ class OnRamperBuyProvider extends BuyProvider { : super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: null); static const _baseUrl = 'buy.onramper.com'; + static const _baseApiUrl = 'api.onramper.com'; + static const quotes = '/quotes'; + static const paymentTypes = '/payment-types'; + static const supported = '/supported'; final SettingsStore _settingsStore; + String get _apiKey => secrets.onramperApiKey; + @override String get title => 'Onramper'; @@ -31,74 +43,327 @@ class OnRamperBuyProvider extends BuyProvider { @override String get darkIcon => 'assets/images/onramper_dark.png'; - String get _apiKey => secrets.onramperApiKey; + @override + bool get isAggregator => true; - String get _normalizeCryptoCurrency { - switch (wallet.currency) { - case CryptoCurrency.ltc: - return "LTC_LITECOIN"; - case CryptoCurrency.xmr: - return "XMR_MONERO"; - case CryptoCurrency.bch: - return "BCH_BITCOINCASH"; - case CryptoCurrency.nano: - return "XNO_NANO"; - default: - return wallet.currency.title; + Future> getAvailablePaymentTypes( + String fiatCurrency, String cryptoCurrency, bool isBuyAction) async { + final params = { + 'fiatCurrency': fiatCurrency, + 'type': isBuyAction ? 'buy' : 'sell', + 'isRecurringPayment': 'false' + }; + + final url = Uri.https(_baseApiUrl, '$supported$paymentTypes/$fiatCurrency', params); + + try { + final response = + await http.get(url, headers: {'Authorization': _apiKey, 'accept': 'application/json'}); + + if (response.statusCode == 200) { + final Map data = jsonDecode(response.body) as Map; + final List message = data['message'] as List; + return message + .map((item) => PaymentMethod.fromOnramperJson(item as Map)) + .toList(); + } else { + print('Failed to fetch available payment types'); + return []; + } + } catch (e) { + print('Failed to fetch available payment types: $e'); + return []; } } - String getColorStr(Color color) { - return color.value.toRadixString(16).replaceAll(RegExp(r'^ff'), ""); + Future> getOnrampMetadata() async { + final url = Uri.https(_baseApiUrl, '$supported/onramps/all'); + + try { + final response = + await http.get(url, headers: {'Authorization': _apiKey, 'accept': 'application/json'}); + + if (response.statusCode == 200) { + final Map data = jsonDecode(response.body) as Map; + + final List onramps = data['message'] as List; + + final Map result = { + for (var onramp in onramps) + (onramp['id'] as String): { + 'displayName': onramp['displayName'] as String, + 'svg': onramp['icons']['svg'] as String + } + }; + + return result; + } else { + print('Failed to fetch onramp metadata'); + return {}; + } + } catch (e) { + print('Error occurred: $e'); + return {}; + } } - Uri requestOnramperUrl(BuildContext context, bool? isBuyAction) { - String primaryColor, - secondaryColor, - primaryTextColor, - secondaryTextColor, - containerColor, - cardColor; + @override + Future?> fetchQuote( + {required CryptoCurrency cryptoCurrency, + required FiatCurrency fiatCurrency, + required double amount, + required bool isBuyAction, + required String walletAddress, + PaymentType? paymentType, + String? countryCode}) async { + String? paymentMethod; - primaryColor = getColorStr(Theme.of(context).primaryColor); - secondaryColor = getColorStr(Theme.of(context).colorScheme.background); - primaryTextColor = - getColorStr(Theme.of(context).extension()!.titleColor); - secondaryTextColor = getColorStr( - Theme.of(context).extension()!.secondaryTextColor); - containerColor = getColorStr(Theme.of(context).colorScheme.background); - cardColor = getColorStr(Theme.of(context).cardColor); + if (paymentType != null && paymentType != PaymentType.all) { + paymentMethod = normalizePaymentMethod(paymentType); + if (paymentMethod == null) paymentMethod = paymentType.name; + } + + final actionType = isBuyAction ? 'buy' : 'sell'; + + final normalizedCryptoCurrency = _getNormalizeCryptoCurrency(cryptoCurrency); + + final params = { + 'amount': amount.toString(), + if (paymentMethod != null) 'paymentMethod': paymentMethod, + 'clientName': 'CakeWallet', + 'type': actionType, + 'walletAddress': walletAddress, + 'isRecurringPayment': 'false', + 'input': 'source', + }; + + log('Onramper: Fetching $actionType quote: ${isBuyAction ? normalizedCryptoCurrency : fiatCurrency.name} -> ${isBuyAction ? fiatCurrency.name : normalizedCryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod'); + + final sourceCurrency = isBuyAction ? fiatCurrency.name : normalizedCryptoCurrency; + final destinationCurrency = isBuyAction ? normalizedCryptoCurrency : fiatCurrency.name; + + final url = Uri.https(_baseApiUrl, '$quotes/${sourceCurrency}/${destinationCurrency}', params); + final headers = {'Authorization': _apiKey, 'accept': 'application/json'}; + + try { + final response = await http.get(url, headers: headers); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body) as List; + if (data.isEmpty) return null; + + List validQuotes = []; + + final onrampMetadata = await getOnrampMetadata(); + + for (var item in data) { + + if (item['errors'] != null) continue; + + final paymentMethod = (item as Map)['paymentMethod'] as String; + + final rampId = item['ramp'] as String?; + final rampMetaData = onrampMetadata[rampId] as Map?; + + if (rampMetaData == null) continue; + + final quote = Quote.fromOnramperJson( + item, isBuyAction, onrampMetadata, _getPaymentTypeByString(paymentMethod)); + quote.setFiatCurrency = fiatCurrency; + quote.setCryptoCurrency = cryptoCurrency; + validQuotes.add(quote); + } + + if (validQuotes.isEmpty) return null; + + return validQuotes; + } else { + print('Onramper: Failed to fetch rate'); + return null; + } + } catch (e) { + print('Onramper: Failed to fetch rate $e'); + return null; + } + } + + Future? launchProvider( + {required BuildContext context, + required Quote quote, + required double amount, + required bool isBuyAction, + required String cryptoCurrencyAddress, + String? countryCode}) async { + final actionType = isBuyAction ? 'buy' : 'sell'; + final prefix = actionType == 'sell' ? actionType + '_' : ''; + + final primaryColor = getColorStr(Theme.of(context).primaryColor); + final secondaryColor = getColorStr(Theme.of(context).colorScheme.background); + final primaryTextColor = getColorStr(Theme.of(context).extension()!.titleColor); + final secondaryTextColor = + getColorStr(Theme.of(context).extension()!.secondaryTextColor); + final containerColor = getColorStr(Theme.of(context).colorScheme.background); + var cardColor = getColorStr(Theme.of(context).cardColor); if (_settingsStore.currentTheme.title == S.current.high_contrast_theme) { cardColor = getColorStr(Colors.white); } - final networkName = - wallet.currency.fullName?.toUpperCase().replaceAll(" ", ""); + final defaultCrypto = _getNormalizeCryptoCurrency(quote.cryptoCurrency); - return Uri.https(_baseUrl, '', { + final paymentMethod = normalizePaymentMethod(quote.paymentType); + + final uri = Uri.https(_baseUrl, '', { 'apiKey': _apiKey, - 'defaultCrypto': _normalizeCryptoCurrency, - 'sell_defaultCrypto': _normalizeCryptoCurrency, - 'networkWallets': '${networkName}:${wallet.walletAddresses.address}', + 'mode': actionType, + '${prefix}defaultFiat': quote.fiatCurrency.name, + '${prefix}defaultCrypto': quote.cryptoCurrency.name, + '${prefix}defaultAmount': amount.toString(), + if (paymentMethod != null) '${prefix}defaultPaymentMethod': paymentMethod, + 'onlyOnramps': quote.rampId, + 'networkWallets': '$defaultCrypto:$cryptoCurrencyAddress', + 'walletAddress': cryptoCurrencyAddress, 'supportSwap': "false", 'primaryColor': primaryColor, 'secondaryColor': secondaryColor, + 'containerColor': containerColor, 'primaryTextColor': primaryTextColor, 'secondaryTextColor': secondaryTextColor, - 'containerColor': containerColor, 'cardColor': cardColor, - 'mode': isBuyAction == true ? 'buy' : 'sell', }); - } - Future launchProvider(BuildContext context, bool? isBuyAction) async { - final uri = requestOnramperUrl(context, isBuyAction); - if (DeviceInfo.instance.isMobile) { - Navigator.of(context) - .pushNamed(Routes.webViewPage, arguments: [title, uri]); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); } else { - await launchUrl(uri); + throw Exception('Could not launch URL'); } } + + List mainCurrency = [ + CryptoCurrency.btc, + CryptoCurrency.eth, + CryptoCurrency.sol, + ]; + + String _tagToNetwork(String tag) { + switch (tag) { + case 'OMNI': + return tag; + case 'POL': + return 'POLYGON'; + default: + return CryptoCurrency.fromString(tag).fullName ?? tag; + } + } + + String _getNormalizeCryptoCurrency(Currency currency) { + if (currency is CryptoCurrency) { + if (!mainCurrency.contains(currency)) { + final network = currency.tag == null ? currency.fullName : _tagToNetwork(currency.tag!); + return '${currency.title}_${network?.replaceAll(' ', '')}'.toUpperCase(); + } + return currency.title.toUpperCase(); + } + return currency.name.toUpperCase(); + } + + String? normalizePaymentMethod(PaymentType paymentType) { + switch (paymentType) { + case PaymentType.bankTransfer: + return 'banktransfer'; + case PaymentType.creditCard: + return 'creditcard'; + case PaymentType.debitCard: + return 'debitcard'; + case PaymentType.applePay: + return 'applepay'; + case PaymentType.googlePay: + return 'googlepay'; + case PaymentType.revolutPay: + return 'revolutpay'; + case PaymentType.neteller: + return 'neteller'; + case PaymentType.skrill: + return 'skrill'; + case PaymentType.sepa: + return 'sepabanktransfer'; + case PaymentType.sepaInstant: + return 'sepainstant'; + case PaymentType.ach: + return 'ach'; + case PaymentType.achInstant: + return 'iach'; + case PaymentType.Khipu: + return 'khipu'; + case PaymentType.palomaBanktTansfer: + return 'palomabanktransfer'; + case PaymentType.ovo: + return 'ovo'; + case PaymentType.zaloPay: + return 'zalopay'; + case PaymentType.zaloBankTransfer: + return 'zalobanktransfer'; + case PaymentType.gcash: + return 'gcash'; + case PaymentType.imps: + return 'imps'; + case PaymentType.dana: + return 'dana'; + case PaymentType.ideal: + return 'ideal'; + default: + return null; + } + } + + PaymentType _getPaymentTypeByString(String paymentMethod) { + switch (paymentMethod.toLowerCase()) { + case 'banktransfer': + return PaymentType.bankTransfer; + case 'creditcard': + return PaymentType.creditCard; + case 'debitcard': + return PaymentType.debitCard; + case 'applepay': + return PaymentType.applePay; + case 'googlepay': + return PaymentType.googlePay; + case 'revolutpay': + return PaymentType.revolutPay; + case 'neteller': + return PaymentType.neteller; + case 'skrill': + return PaymentType.skrill; + case 'sepabanktransfer': + return PaymentType.sepa; + case 'sepainstant': + return PaymentType.sepaInstant; + case 'ach': + return PaymentType.ach; + case 'iach': + return PaymentType.achInstant; + case 'khipu': + return PaymentType.Khipu; + case 'palomabanktransfer': + return PaymentType.palomaBanktTansfer; + case 'ovo': + return PaymentType.ovo; + case 'zalopay': + return PaymentType.zaloPay; + case 'zalobanktransfer': + return PaymentType.zaloBankTransfer; + case 'gcash': + return PaymentType.gcash; + case 'imps': + return PaymentType.imps; + case 'dana': + return PaymentType.dana; + case 'ideal': + return PaymentType.ideal; + default: + return PaymentType.all; + } + } + + String getColorStr(Color color) => color.value.toRadixString(16).replaceAll(RegExp(r'^ff'), ""); } diff --git a/lib/buy/payment_method.dart b/lib/buy/payment_method.dart new file mode 100644 index 000000000..cf85c441b --- /dev/null +++ b/lib/buy/payment_method.dart @@ -0,0 +1,287 @@ +import 'dart:ui'; + +import 'package:cake_wallet/core/selectable_option.dart'; + +enum PaymentType { + all, + bankTransfer, + creditCard, + debitCard, + applePay, + googlePay, + revolutPay, + neteller, + skrill, + sepa, + sepaInstant, + ach, + achInstant, + Khipu, + palomaBanktTansfer, + ovo, + zaloPay, + zaloBankTransfer, + gcash, + imps, + dana, + ideal, + paypal, + sepaOpenBankingPayment, + gbpOpenBankingPayment, + lowCostAch, + mobileWallet, + pixInstantPayment, + yellowCardBankTransfer, + fiatBalance, + bancontact, +} + +extension PaymentTypeTitle on PaymentType { + String? get title { + switch (this) { + case PaymentType.all: + return 'All Payment Methods'; + case PaymentType.bankTransfer: + return 'Bank Transfer'; + case PaymentType.creditCard: + return 'Credit Card'; + case PaymentType.debitCard: + return 'Debit Card'; + case PaymentType.applePay: + return 'Apple Pay'; + case PaymentType.googlePay: + return 'Google Pay'; + case PaymentType.revolutPay: + return 'Revolut Pay'; + case PaymentType.neteller: + return 'Neteller'; + case PaymentType.skrill: + return 'Skrill'; + case PaymentType.sepa: + return 'SEPA'; + case PaymentType.sepaInstant: + return 'SEPA Instant'; + case PaymentType.ach: + return 'ACH'; + case PaymentType.achInstant: + return 'ACH Instant'; + case PaymentType.Khipu: + return 'Khipu'; + case PaymentType.palomaBanktTansfer: + return 'Paloma Bank Transfer'; + case PaymentType.ovo: + return 'OVO'; + case PaymentType.zaloPay: + return 'Zalo Pay'; + case PaymentType.zaloBankTransfer: + return 'Zalo Bank Transfer'; + case PaymentType.gcash: + return 'GCash'; + case PaymentType.imps: + return 'IMPS'; + case PaymentType.dana: + return 'DANA'; + case PaymentType.ideal: + return 'iDEAL'; + case PaymentType.paypal: + return 'PayPal'; + case PaymentType.sepaOpenBankingPayment: + return 'SEPA Open Banking Payment'; + case PaymentType.gbpOpenBankingPayment: + return 'GBP Open Banking Payment'; + case PaymentType.lowCostAch: + return 'Low Cost ACH'; + case PaymentType.mobileWallet: + return 'Mobile Wallet'; + case PaymentType.pixInstantPayment: + return 'PIX Instant Payment'; + case PaymentType.yellowCardBankTransfer: + return 'Yellow Card Bank Transfer'; + case PaymentType.fiatBalance: + return 'Fiat Balance'; + case PaymentType.bancontact: + return 'Bancontact'; + default: + return null; + } + } + + String? get lightIconPath { + switch (this) { + case PaymentType.all: + return 'assets/images/usd_round_light.svg'; + case PaymentType.creditCard: + case PaymentType.debitCard: + case PaymentType.yellowCardBankTransfer: + return 'assets/images/card.svg'; + case PaymentType.bankTransfer: + return 'assets/images/bank_light.svg'; + case PaymentType.skrill: + return 'assets/images/skrill.svg'; + case PaymentType.applePay: + return 'assets/images/apple_pay_round_light.svg'; + default: + return null; + } + } + + String? get darkIconPath { + switch (this) { + case PaymentType.all: + return 'assets/images/usd_round_dark.svg'; + case PaymentType.creditCard: + case PaymentType.debitCard: + case PaymentType.yellowCardBankTransfer: + return 'assets/images/card_dark.svg'; + case PaymentType.bankTransfer: + return 'assets/images/bank_dark.svg'; + case PaymentType.skrill: + return 'assets/images/skrill.svg'; + case PaymentType.applePay: + return 'assets/images/apple_pay_round_dark.svg'; + default: + return null; + } + } + + String? get description { + switch (this) { + default: + return null; + } + } +} + +class PaymentMethod extends SelectableOption { + PaymentMethod({ + required this.paymentMethodType, + required this.customTitle, + required this.customIconPath, + this.customDescription, + }) : super(title: paymentMethodType.title ?? customTitle); + + final PaymentType paymentMethodType; + final String customTitle; + final String customIconPath; + final String? customDescription; + bool isSelected = false; + + @override + String? get description => paymentMethodType.description ?? customDescription; + + @override + String get lightIconPath => paymentMethodType.lightIconPath ?? customIconPath; + + @override + String get darkIconPath => paymentMethodType.darkIconPath ?? customIconPath; + + @override + bool get isOptionSelected => isSelected; + + factory PaymentMethod.all() { + return PaymentMethod( + paymentMethodType: PaymentType.all, + customTitle: 'All Payment Methods', + customIconPath: 'assets/images/dollar_coin.svg'); + } + + factory PaymentMethod.fromOnramperJson(Map json) { + final type = PaymentMethod.getPaymentTypeId(json['paymentTypeId'] as String?); + return PaymentMethod( + paymentMethodType: type, + customTitle: json['name'] as String? ?? 'Unknown', + customIconPath: json['icon'] as String? ?? 'assets/images/card.png', + customDescription: json['description'] as String?); + } + + factory PaymentMethod.fromDFX(String paymentMethod, PaymentType paymentType) { + return PaymentMethod( + paymentMethodType: paymentType, + customTitle: paymentMethod, + customIconPath: 'assets/images/card.png'); + } + + factory PaymentMethod.fromMoonPayJson(Map json, PaymentType paymentType) { + return PaymentMethod( + paymentMethodType: paymentType, + customTitle: json['paymentMethod'] as String, + customIconPath: 'assets/images/card.png'); + } + + factory PaymentMethod.fromMeldJson(Map json) { + final type = PaymentMethod.getPaymentTypeId(json['paymentMethod'] as String?); + final logos = json['logos'] as Map; + return PaymentMethod( + paymentMethodType: type, + customTitle: json['name'] as String? ?? 'Unknown', + customIconPath: logos['dark'] as String? ?? 'assets/images/card.png', + customDescription: json['description'] as String?); + } + + static PaymentType getPaymentTypeId(String? type) { + switch (type?.toLowerCase()) { + case 'banktransfer': + case 'bank': + case 'yellow_card_bank_transfer': + return PaymentType.bankTransfer; + case 'creditcard': + case 'card': + case 'credit_debit_card': + return PaymentType.creditCard; + case 'debitcard': + return PaymentType.debitCard; + case 'applepay': + case 'apple_pay': + return PaymentType.applePay; + case 'googlepay': + case 'google_pay': + return PaymentType.googlePay; + case 'revolutpay': + return PaymentType.revolutPay; + case 'neteller': + return PaymentType.neteller; + case 'skrill': + return PaymentType.skrill; + case 'sepabanktransfer': + case 'sepa': + case 'sepa_bank_transfer': + return PaymentType.sepa; + case 'sepainstant': + case 'sepa_instant': + return PaymentType.sepaInstant; + case 'ach': + case 'ach_bank_transfer': + return PaymentType.ach; + case 'iach': + case 'instant_ach': + return PaymentType.achInstant; + case 'khipu': + return PaymentType.Khipu; + case 'palomabanktransfer': + return PaymentType.palomaBanktTansfer; + case 'ovo': + return PaymentType.ovo; + case 'zalopay': + return PaymentType.zaloPay; + case 'zalobanktransfer': + case 'za_bank_transfer': + return PaymentType.zaloBankTransfer; + case 'gcash': + return PaymentType.gcash; + case 'imps': + return PaymentType.imps; + case 'dana': + return PaymentType.dana; + case 'ideal': + return PaymentType.ideal; + case 'paypal': + return PaymentType.paypal; + case 'sepa_open_banking_payment': + return PaymentType.sepaOpenBankingPayment; + case 'bancontact': + return PaymentType.bancontact; + default: + return PaymentType.all; + } + } +} diff --git a/lib/buy/robinhood/robinhood_buy_provider.dart b/lib/buy/robinhood/robinhood_buy_provider.dart index 2d809772e..e8de5a59c 100644 --- a/lib/buy/robinhood/robinhood_buy_provider.dart +++ b/lib/buy/robinhood/robinhood_buy_provider.dart @@ -1,13 +1,18 @@ import 'dart:convert'; +import 'dart:developer'; 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/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/connect_device/connect_device_page.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/hardware_wallet/ledger_view_model.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; @@ -15,7 +20,8 @@ import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; class RobinhoodBuyProvider extends BuyProvider { - RobinhoodBuyProvider({required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM}) + RobinhoodBuyProvider( + {required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM}) : super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: ledgerVM); static const _baseUrl = 'applink.robinhood.com'; @@ -33,6 +39,9 @@ class RobinhoodBuyProvider extends BuyProvider { @override String get darkIcon => 'assets/images/robinhood_dark.png'; + @override + bool get isAggregator => false; + String get _applicationId => secrets.robinhoodApplicationId; String get _apiSecret => secrets.exchangeHelperApiKey; @@ -86,7 +95,13 @@ class RobinhoodBuyProvider extends BuyProvider { }); } - Future launchProvider(BuildContext context, bool? isBuyAction) async { + Future? launchProvider( + {required BuildContext context, + required Quote quote, + required double amount, + required bool isBuyAction, + required String cryptoCurrencyAddress, + String? countryCode}) async { if (wallet.isHardwareWallet) { if (!ledgerVM!.isConnected) { await Navigator.of(context).pushNamed(Routes.connectDevices, @@ -116,4 +131,87 @@ class RobinhoodBuyProvider extends BuyProvider { }); } } + + @override + Future?> fetchQuote( + {required CryptoCurrency cryptoCurrency, + required FiatCurrency fiatCurrency, + required double amount, + required bool isBuyAction, + required String walletAddress, + PaymentType? paymentType, + String? countryCode}) async { + String? paymentMethod; + + if (paymentType != null && paymentType != PaymentType.all) { + paymentMethod = normalizePaymentMethod(paymentType); + if (paymentMethod == null) paymentMethod = paymentType.name; + } + + final action = isBuyAction ? 'buy' : 'sell'; + log('Robinhood: Fetching $action quote: ${isBuyAction ? cryptoCurrency.title : fiatCurrency.name.toUpperCase()} -> ${isBuyAction ? fiatCurrency.name.toUpperCase() : cryptoCurrency.title}, amount: $amount paymentMethod: $paymentMethod'); + + final queryParams = { + 'applicationId': _applicationId, + 'fiatCode': fiatCurrency.name, + 'assetCode': cryptoCurrency.title, + 'fiatAmount': amount.toString(), + if (paymentMethod != null) 'paymentMethod': paymentMethod, + }; + + final uri = + Uri.https('api.robinhood.com', '/catpay/v1/${cryptoCurrency.title}/quote/', queryParams); + + try { + final response = await http.get(uri, headers: {'accept': 'application/json'}); + final responseData = jsonDecode(response.body) as Map; + + if (response.statusCode == 200) { + final paymentType = _getPaymentTypeByString(responseData['paymentMethod'] as String?); + final quote = Quote.fromRobinhoodJson(responseData, isBuyAction, paymentType); + quote.setFiatCurrency = fiatCurrency; + quote.setCryptoCurrency = cryptoCurrency; + return [quote]; + } else { + if (responseData.containsKey('message')) { + log('Robinhood Error: ${responseData['message']}'); + } else { + print('Robinhood Failed to fetch $action quote: ${response.statusCode}'); + } + return null; + } + } catch (e) { + log('Robinhood: Failed to fetch $action quote: $e'); + return null; + } + + // ● buying_power + // ● crypto_balance + // ● debit_card + // ● bank_transfer + } + + String? normalizePaymentMethod(PaymentType paymentMethod) { + switch (paymentMethod) { + case PaymentType.creditCard: + return 'debit_card'; + case PaymentType.debitCard: + return 'debit_card'; + case PaymentType.bankTransfer: + return 'bank_transfer'; + default: + return null; + } + } + + PaymentType _getPaymentTypeByString(String? paymentMethod) { + switch (paymentMethod) { + case 'debit_card': + return PaymentType.debitCard; + case 'bank_transfer': + return PaymentType.bankTransfer; + default: + return PaymentType.all; + } + } } diff --git a/lib/buy/sell_buy_states.dart b/lib/buy/sell_buy_states.dart new file mode 100644 index 000000000..26ea20205 --- /dev/null +++ b/lib/buy/sell_buy_states.dart @@ -0,0 +1,20 @@ +abstract class PaymentMethodLoadingState {} + +class InitialPaymentMethod extends PaymentMethodLoadingState {} + +class PaymentMethodLoading extends PaymentMethodLoadingState {} + +class PaymentMethodLoaded extends PaymentMethodLoadingState {} + +class PaymentMethodFailed extends PaymentMethodLoadingState {} + + +abstract class BuySellQuotLoadingState {} + +class InitialBuySellQuotState extends BuySellQuotLoadingState {} + +class BuySellQuotLoading extends BuySellQuotLoadingState {} + +class BuySellQuotLoaded extends BuySellQuotLoadingState {} + +class BuySellQuotFailed extends BuySellQuotLoadingState {} \ No newline at end of file diff --git a/lib/buy/wyre/wyre_buy_provider.dart b/lib/buy/wyre/wyre_buy_provider.dart index e09186ad5..78b109ac0 100644 --- a/lib/buy/wyre/wyre_buy_provider.dart +++ b/lib/buy/wyre/wyre_buy_provider.dart @@ -42,6 +42,9 @@ class WyreBuyProvider extends BuyProvider { @override String get darkIcon => 'assets/images/robinhood_dark.png'; + @override + bool get isAggregator => false; + String get trackUrl => isTestEnvironment ? _trackTestUrl : _trackProductUrl; String baseApiUrl; @@ -148,10 +151,4 @@ class WyreBuyProvider extends BuyProvider { receiveAddress: wallet.walletAddresses.address, walletId: wallet.id); } - - @override - Future launchProvider(BuildContext context, bool? isBuyAction) { - // TODO: implement launchProvider - throw UnimplementedError(); - } } diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index d65530eb5..992ed6288 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -268,9 +268,7 @@ class BackupService { final currentFiatCurrency = data[PreferencesKey.currentFiatCurrencyKey] as String?; final shouldSaveRecipientAddress = data[PreferencesKey.shouldSaveRecipientAddressKey] as bool?; final isAppSecure = data[PreferencesKey.isAppSecureKey] as bool?; - final disableBuy = data[PreferencesKey.disableBuyKey] as bool?; - final disableSell = data[PreferencesKey.disableSellKey] as bool?; - final defaultBuyProvider = data[PreferencesKey.defaultBuyProvider] as int?; + final disableTradeOption = data[PreferencesKey.disableTradeOption] as bool?; final currentTransactionPriorityKeyLegacy = data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?; final currentBitcoinElectrumSererId = @@ -323,14 +321,8 @@ class BackupService { if (isAppSecure != null) await _sharedPreferences.setBool(PreferencesKey.isAppSecureKey, isAppSecure); - if (disableBuy != null) - await _sharedPreferences.setBool(PreferencesKey.disableBuyKey, disableBuy); - - if (disableSell != null) - await _sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell); - - if (defaultBuyProvider != null) - await _sharedPreferences.setInt(PreferencesKey.defaultBuyProvider, defaultBuyProvider); + if (disableTradeOption != null) + await _sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption); if (currentTransactionPriorityKeyLegacy != null) await _sharedPreferences.setInt( @@ -516,10 +508,7 @@ class BackupService { _sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey), PreferencesKey.shouldSaveRecipientAddressKey: _sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey), - PreferencesKey.disableBuyKey: _sharedPreferences.getBool(PreferencesKey.disableBuyKey), - PreferencesKey.disableSellKey: _sharedPreferences.getBool(PreferencesKey.disableSellKey), - PreferencesKey.defaultBuyProvider: - _sharedPreferences.getInt(PreferencesKey.defaultBuyProvider), + PreferencesKey.disableTradeOption: _sharedPreferences.getBool(PreferencesKey.disableTradeOption), PreferencesKey.currentPinLength: _sharedPreferences.getInt(PreferencesKey.currentPinLength), PreferencesKey.currentTransactionPriorityKeyLegacy: _sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy), diff --git a/lib/core/selectable_option.dart b/lib/core/selectable_option.dart new file mode 100644 index 000000000..0d7511c95 --- /dev/null +++ b/lib/core/selectable_option.dart @@ -0,0 +1,47 @@ +abstract class SelectableItem { + SelectableItem({required this.title}); + final String title; +} + +class OptionTitle extends SelectableItem { + OptionTitle({required String title}) : super(title: title); + +} + +abstract class SelectableOption extends SelectableItem { + SelectableOption({required String title}) : super(title: title); + + String get lightIconPath; + + String get darkIconPath; + + String? get description => null; + + String? get topLeftSubTitle => null; + + String? get topLeftSubTitleIconPath => null; + + String? get topRightSubTitle => null; + + String? get topRightSubTitleLightIconPath => null; + + String? get topRightSubTitleDarkIconPath => null; + + String? get bottomLeftSubTitle => null; + + String? get bottomLeftSubTitleIconPath => null; + + String? get bottomRightSubTitle => null; + + String? get bottomRightSubTitleLightIconPath => null; + + String? get bottomRightSubTitleDarkIconPath => null; + + List get badges => []; + + bool get isOptionSelected => false; + + set isOptionSelected(bool isSelected) => false; +} + + diff --git a/lib/di.dart b/lib/di.dart index 9cff29798..8dd1029a9 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -19,6 +19,7 @@ import 'package:cake_wallet/core/backup_service.dart'; import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/core/new_wallet_type_arguments.dart'; import 'package:cake_wallet/core/secure_storage.dart'; +import 'package:cake_wallet/core/selectable_option.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart'; import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; @@ -34,6 +35,8 @@ import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.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'; +import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart'; import 'package:cake_wallet/src/screens/receive/address_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'; @@ -61,7 +64,6 @@ import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dar import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/screens/backup/backup_page.dart'; import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart'; -import 'package:cake_wallet/src/screens/buy/buy_options_page.dart'; import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart'; import 'package:cake_wallet/src/screens/buy/webview_page.dart'; import 'package:cake_wallet/src/screens/contact/contact_list_page.dart'; @@ -135,6 +137,7 @@ import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart'; import 'package:cake_wallet/view_model/animated_ur_model.dart'; import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart'; import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart'; @@ -248,6 +251,8 @@ 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/meld/meld_buy_provider.dart'; +import 'src/screens/buy/buy_sell_page.dart'; import 'cake_pay/cake_pay_payment_credantials.dart'; final getIt = GetIt.instance; @@ -1004,6 +1009,10 @@ Future setup({ wallet: getIt.get().wallet!, )); + getIt.registerFactory(() => MeldBuyProvider( + wallet: getIt.get().wallet!, + )); + getIt.registerFactoryParam((title, uri) => WebViewPage(title, uri)); getIt.registerFactory(() => PayfuraBuyProvider( @@ -1193,8 +1202,25 @@ Future setup({ getIt.registerFactory(() => BuyAmountViewModel()); - getIt.registerFactoryParam( - (isBuyOption, _) => BuySellOptionsPage(getIt.get(), isBuyOption)); + getIt.registerFactory(() => BuySellViewModel(getIt.get())); + + getIt.registerFactory(() => BuySellPage(getIt.get())); + + getIt.registerFactoryParam, void>((List args, _) { + final items = args.first as List; + final pickAnOption = args[1] as void Function(SelectableOption option)?; + final confirmOption = args[2] as void Function(BuildContext contex)?; + return BuyOptionsPage( + items: items, pickAnOption: pickAnOption, confirmOption: confirmOption); + }); + + getIt.registerFactoryParam, void>((List args, _) { + final items = args.first as List; + final pickAnOption = args[1] as void Function(SelectableOption option)?; + + return PaymentMethodOptionsPage( + items: items, pickAnOption: pickAnOption); + }); getIt.registerFactory(() { final wallet = getIt.get().wallet; diff --git a/lib/entities/main_actions.dart b/lib/entities/main_actions.dart index c1dd71cc9..94be0d2b7 100644 --- a/lib/entities/main_actions.dart +++ b/lib/entities/main_actions.dart @@ -23,31 +23,18 @@ class MainActions { }); static List all = [ - buyAction, + showWalletsAction, receiveAction, exchangeAction, sendAction, - sellAction, + tradeAction, ]; - static MainActions buyAction = MainActions._( - name: (context) => S.of(context).buy, - image: 'assets/images/buy.png', - isEnabled: (viewModel) => viewModel.isEnabledBuyAction, - canShow: (viewModel) => viewModel.hasBuyAction, + static MainActions showWalletsAction = MainActions._( + name: (context) => S.of(context).wallets, + image: 'assets/images/wallet_new.png', onTap: (BuildContext context, DashboardViewModel viewModel) async { - if (!viewModel.isEnabledBuyAction) { - return; - } - - final defaultBuyProvider = viewModel.defaultBuyProvider; - try { - defaultBuyProvider != null - ? await defaultBuyProvider.launchProvider(context, true) - : await Navigator.of(context).pushNamed(Routes.buySellPage, arguments: true); - } catch (e) { - await _showErrorDialog(context, defaultBuyProvider.toString(), e.toString()); - } + Navigator.pushNamed(context, Routes.walletList); }, ); @@ -79,39 +66,15 @@ class MainActions { }, ); - static MainActions sellAction = MainActions._( - name: (context) => S.of(context).sell, - image: 'assets/images/sell.png', - isEnabled: (viewModel) => viewModel.isEnabledSellAction, - canShow: (viewModel) => viewModel.hasSellAction, - onTap: (BuildContext context, DashboardViewModel viewModel) async { - if (!viewModel.isEnabledSellAction) { - return; - } - final defaultSellProvider = viewModel.defaultSellProvider; - try { - defaultSellProvider != null - ? await defaultSellProvider.launchProvider(context, false) - : await Navigator.of(context).pushNamed(Routes.buySellPage, arguments: false); - } catch (e) { - await _showErrorDialog(context, defaultSellProvider.toString(), e.toString()); - } + static MainActions tradeAction = MainActions._( + name: (context) => '${S.of(context).buy} / ${S.of(context).sell}', + image: 'assets/images/buy_sell.png', + isEnabled: (viewModel) => viewModel.isEnabledTradeAction, + canShow: (viewModel) => viewModel.hasTradeAction, + onTap: (BuildContext context, DashboardViewModel viewModel) async { + if (!viewModel.isEnabledTradeAction) return; + await Navigator.of(context).pushNamed(Routes.buySellPage, arguments: false); }, ); - - static Future _showErrorDialog( - BuildContext context, String title, String errorMessage) async { - await showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: title, - alertContent: errorMessage, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop(), - ); - }, - ); - } } \ No newline at end of file diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 0bb526e5d..5ed7a7ed6 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -21,10 +21,8 @@ class PreferencesKey { static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; static const shouldSaveRecipientAddressKey = 'save_recipient_address'; static const isAppSecureKey = 'is_app_secure'; - static const disableBuyKey = 'disable_buy'; - static const disableSellKey = 'disable_sell'; + static const disableTradeOption = 'disable_buy'; static const disableBulletinKey = 'disable_bulletin'; - static const defaultBuyProvider = 'default_buy_provider'; static const walletListOrder = 'wallet_list_order'; static const contactListOrder = 'contact_list_order'; static const walletListAscending = 'wallet_list_ascending'; diff --git a/lib/entities/provider_types.dart b/lib/entities/provider_types.dart index b9dd4ef2a..42ec74c12 100644 --- a/lib/entities/provider_types.dart +++ b/lib/entities/provider_types.dart @@ -1,24 +1,18 @@ import 'package:cake_wallet/buy/buy_provider.dart'; import 'package:cake_wallet/buy/dfx/dfx_buy_provider.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'; import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart'; import 'package:cake_wallet/di.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:http/http.dart'; -enum ProviderType { - askEachTime, - robinhood, - dfx, - onramper, - moonpay, -} +enum ProviderType { robinhood, dfx, onramper, moonpay, meld } extension ProviderTypeName on ProviderType { String get title { switch (this) { - case ProviderType.askEachTime: - return 'Ask each time'; case ProviderType.robinhood: return 'Robinhood Connect'; case ProviderType.dfx: @@ -27,13 +21,13 @@ extension ProviderTypeName on ProviderType { return 'Onramper'; case ProviderType.moonpay: return 'MoonPay'; + case ProviderType.meld: + return 'Meld'; } } String get id { switch (this) { - case ProviderType.askEachTime: - return 'ask_each_time_provider'; case ProviderType.robinhood: return 'robinhood_connect_provider'; case ProviderType.dfx: @@ -42,6 +36,8 @@ extension ProviderTypeName on ProviderType { return 'onramper_provider'; case ProviderType.moonpay: return 'moonpay_provider'; + case ProviderType.meld: + return 'meld_provider'; } } } @@ -52,14 +48,13 @@ class ProvidersHelper { case WalletType.nano: case WalletType.banano: case WalletType.wownero: - return [ProviderType.askEachTime, ProviderType.onramper]; + return [ProviderType.onramper]; case WalletType.monero: - return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx]; + return [ProviderType.onramper, ProviderType.dfx]; case WalletType.bitcoin: case WalletType.polygon: case WalletType.ethereum: return [ - ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx, ProviderType.robinhood, @@ -68,10 +63,13 @@ class ProvidersHelper { case WalletType.litecoin: case WalletType.bitcoinCash: case WalletType.solana: - return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay]; + return [ + ProviderType.onramper, + ProviderType.robinhood, + ProviderType.moonpay + ]; case WalletType.tron: return [ - ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay, @@ -88,28 +86,24 @@ class ProvidersHelper { case WalletType.ethereum: case WalletType.polygon: return [ - ProviderType.askEachTime, ProviderType.onramper, ProviderType.moonpay, ProviderType.dfx, ]; case WalletType.litecoin: case WalletType.bitcoinCash: - return [ProviderType.askEachTime, ProviderType.moonpay]; + return [ProviderType.moonpay]; case WalletType.solana: return [ - ProviderType.askEachTime, ProviderType.onramper, - ProviderType.robinhood, ProviderType.moonpay, ]; case WalletType.tron: return [ - ProviderType.askEachTime, - ProviderType.robinhood, ProviderType.moonpay, ]; case WalletType.monero: + return [ProviderType.dfx]; case WalletType.nano: case WalletType.banano: case WalletType.none: @@ -129,7 +123,9 @@ class ProvidersHelper { return getIt.get(); case ProviderType.moonpay: return getIt.get(); - case ProviderType.askEachTime: + case ProviderType.meld: + return getIt.get(); + default: return null; } } diff --git a/lib/router.dart b/lib/router.dart index 4b99aabc0..781a6e057 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -17,8 +17,9 @@ import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dar import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/screens/backup/backup_page.dart'; import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart'; -import 'package:cake_wallet/src/screens/buy/buy_options_page.dart'; +import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart'; import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart'; +import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart'; import 'package:cake_wallet/src/screens/buy/webview_page.dart'; import 'package:cake_wallet/src/screens/cake_pay/auth/cake_pay_account_page.dart'; import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart'; @@ -129,7 +130,8 @@ import 'package:cw_core/wallet_type.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - +import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart'; +import 'src/screens/buy/buy_sell_page.dart'; import 'src/screens/dashboard/pages/nft_import_page.dart'; late RouteSettings currentRouteSettings; @@ -571,7 +573,15 @@ Route createRoute(RouteSettings settings) { case Routes.buySellPage: final args = settings.arguments as bool; - return MaterialPageRoute(builder: (_) => getIt.get(param1: args)); + return MaterialPageRoute(builder: (_) => getIt.get(param1: args)); + + case Routes.buyOptionsPage: + final args = settings.arguments as List; + return MaterialPageRoute(builder: (_) => getIt.get(param1: args)); + + case Routes.paymentMethodOptionsPage: + final args = settings.arguments as List; + return MaterialPageRoute(builder: (_) => getIt.get(param1: args)); case Routes.buyWebView: final args = settings.arguments as List; diff --git a/lib/routes.dart b/lib/routes.dart index 63c41bde5..0b8beb0ea 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -59,6 +59,8 @@ class Routes { static const supportOtherLinks = '/support/other'; static const orderDetails = '/order_details'; static const buySellPage = '/buy_sell_page'; + static const buyOptionsPage = '/buy_sell_options'; + static const paymentMethodOptionsPage = '/payment_method_options'; static const buyWebView = '/buy_web_view'; static const unspentCoinsList = '/unspent_coins_list'; static const unspentCoinsDetails = '/unspent_coins_details'; diff --git a/lib/src/screens/InfoPage.dart b/lib/src/screens/Info_page.dart similarity index 100% rename from lib/src/screens/InfoPage.dart rename to lib/src/screens/Info_page.dart diff --git a/lib/src/screens/buy/buy_options_page.dart b/lib/src/screens/buy/buy_options_page.dart deleted file mode 100644 index 38f3ed968..000000000 --- a/lib/src/screens/buy/buy_options_page.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/option_tile.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; -import 'package:cake_wallet/themes/extensions/option_tile_theme.dart'; -import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; -import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; -import 'package:flutter/material.dart'; - -class BuySellOptionsPage extends BasePage { - BuySellOptionsPage(this.dashboardViewModel, this.isBuyAction); - - final DashboardViewModel dashboardViewModel; - final bool isBuyAction; - - @override - String get title => isBuyAction ? S.current.buy : S.current.sell; - - @override - AppBarStyle get appBarStyle => AppBarStyle.regular; - - @override - Widget body(BuildContext context) { - final isLightMode = Theme.of(context).extension()?.useDarkImage ?? false; - final availableProviders = isBuyAction - ? dashboardViewModel.availableBuyProviders - : dashboardViewModel.availableSellProviders; - - return ScrollableWithBottomSection( - content: Container( - child: Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: 330), - child: Column( - children: [ - ...availableProviders.map((provider) { - final icon = Image.asset( - isLightMode ? provider.lightIcon : provider.darkIcon, - height: 40, - width: 40, - ); - - return Padding( - padding: EdgeInsets.only(top: 24), - child: OptionTile( - image: icon, - title: provider.toString(), - description: provider.providerDescription, - onPressed: () => provider.launchProvider(context, isBuyAction), - ), - ); - }).toList(), - ], - ), - ), - ), - ), - bottomSection: Padding( - padding: EdgeInsets.fromLTRB(24, 24, 24, 32), - child: Text( - isBuyAction - ? S.of(context).select_buy_provider_notice - : S.of(context).select_sell_provider_notice, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - color: Theme.of(context).extension()!.detailsTitlesColor, - ), - ), - ), - ); - } -} diff --git a/lib/src/screens/buy/buy_sell_options_page.dart b/lib/src/screens/buy/buy_sell_options_page.dart new file mode 100644 index 000000000..900810f68 --- /dev/null +++ b/lib/src/screens/buy/buy_sell_options_page.dart @@ -0,0 +1,48 @@ +import 'package:cake_wallet/core/selectable_option.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/select_options_page.dart'; +import 'package:flutter/cupertino.dart'; + +class BuyOptionsPage extends SelectOptionsPage { + BuyOptionsPage({required this.items, this.pickAnOption, this.confirmOption}); + + final List items; + final Function(SelectableOption option)? pickAnOption; + final Function(BuildContext context)? confirmOption; + + @override + String get pageTitle => S.current.choose_a_provider; + + @override + EdgeInsets? get contentPadding => null; + + @override + EdgeInsets? get tilePadding => EdgeInsets.only(top: 8); + + @override + EdgeInsets? get innerPadding => EdgeInsets.symmetric(horizontal: 24, vertical: 8); + + @override + double? get imageHeight => 40; + + @override + double? get imageWidth => 40; + + @override + Color? get selectedBackgroundColor => null; + + @override + double? get tileBorderRadius => 30; + + @override + String get bottomSectionText => ''; + + @override + void Function(SelectableOption option)? get onOptionTap => pickAnOption; + + @override + String get primaryButtonText => S.current.confirm; + + @override + void Function(BuildContext context)? get primaryButtonAction => confirmOption; +} diff --git a/lib/src/screens/buy/buy_sell_page.dart b/lib/src/screens/buy/buy_sell_page.dart new file mode 100644 index 000000000..d2f16fe3c --- /dev/null +++ b/lib/src/screens/buy/buy_sell_page.dart @@ -0,0 +1,469 @@ +import 'package:cake_wallet/buy/sell_buy_states.dart'; +import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/entities/parse_address_from_domain.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/exchange/widgets/desktop_exchange_cards_section.dart'; +import 'package:cake_wallet/src/screens/exchange/widgets/exchange_card.dart'; +import 'package:cake_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart'; +import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/provider_optoin_tile.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.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/exchange_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; +import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/currency.dart'; +import 'package:cw_core/wallet_type.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'; + +class BuySellPage extends BasePage { + BuySellPage(this.buySellViewModel); + + final BuySellViewModel buySellViewModel; + final cryptoCurrencyKey = GlobalKey(); + final fiatCurrencyKey = GlobalKey(); + final _formKey = GlobalKey(); + final _fiatAmountFocus = FocusNode(); + final _cryptoAmountFocus = FocusNode(); + final _cryptoAddressFocus = FocusNode(); + var _isReactionsSet = false; + + final arrowBottomPurple = Image.asset( + 'assets/images/arrow_bottom_purple_icon.png', + color: Colors.white, + height: 8, + ); + final arrowBottomCakeGreen = Image.asset( + 'assets/images/arrow_bottom_cake_green.png', + color: Colors.white, + height: 8, + ); + + late final String? depositWalletName; + late final String? receiveWalletName; + + @override + String get title => S.current.buy + '/' + S.current.sell; + + @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 + Function(BuildContext)? get pushToNextWidget => (context) { + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.focusedChild?.unfocus(); + } + }; + + @override + Widget trailing(BuildContext context) => TrailButton( + caption: S.of(context).clear, + onPressed: () { + _formKey.currentState?.reset(); + buySellViewModel.reset(); + }); + + @override + Widget? leading(BuildContext context) { + final _backButton = Icon( + Icons.arrow_back_ios, + color: titleColor(context), + size: 16, + ); + final _closeButton = + currentTheme.type == ThemeType.dark ? closeButtonImageDarkTheme : closeButtonImage; + + bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI; + + return MergeSemantics( + child: SizedBox( + height: isMobileView ? 37 : 45, + width: isMobileView ? 37 : 45, + child: ButtonTheme( + minWidth: double.minPositive, + child: Semantics( + label: !isMobileView ? S.of(context).close : S.of(context).seed_alert_back, + child: TextButton( + style: ButtonStyle( + overlayColor: MaterialStateColor.resolveWith((states) => Colors.transparent), + ), + onPressed: () => onClose(context), + child: !isMobileView ? _closeButton : _backButton, + ), + ), + ), + ), + ); + } + + @override + Widget body(BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) => _setReactions(context, buySellViewModel)); + + return KeyboardActions( + disableScroll: true, + config: KeyboardActionsConfig( + keyboardActionsPlatform: KeyboardActionsPlatform.IOS, + keyboardBarColor: Theme.of(context).extension()!.keyboardBarColor, + nextFocus: false, + actions: [ + KeyboardActionsItem( + focusNode: _fiatAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()]), + KeyboardActionsItem( + focusNode: _cryptoAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()]) + ]), + child: Container( + color: Theme.of(context).colorScheme.background, + child: Form( + key: _formKey, + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.only(bottom: 24), + content: Observer( + builder: (_) => Column(children: [ + _exchangeCardsSection(context), + Padding( + padding: EdgeInsets.symmetric(horizontal: 24), + child: Column( + children: [ + SizedBox(height: 12), + _buildPaymentMethodTile(context), + ], + ), + ), + ])), + bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), + bottomSection: Column(children: [ + Observer( + builder: (_) => LoadingPrimaryButton( + text: S.current.choose_a_provider, + onPressed: () async { + if(!_formKey.currentState!.validate()) return; + buySellViewModel.onTapChoseProvider(context); + }, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + isDisabled: false, + isLoading: !buySellViewModel.isReadyToTrade)), + ]), + )), + )); + } + + Widget _buildPaymentMethodTile(BuildContext context) { + if (buySellViewModel.paymentMethodState is PaymentMethodLoading || + buySellViewModel.paymentMethodState is InitialPaymentMethod) { + return OptionTilePlaceholder( + withBadge: false, + withSubtitle: false, + borderRadius: 30, + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8), + leadingIcon: Icons.arrow_forward_ios, + isDarkTheme: buySellViewModel.isDarkTheme); + } + if (buySellViewModel.paymentMethodState is PaymentMethodFailed) { + return OptionTilePlaceholder(errorText: 'No payment methods available', borderRadius: 30); + } + if (buySellViewModel.paymentMethodState is PaymentMethodLoaded && + buySellViewModel.selectedPaymentMethod != null) { + return Observer(builder: (_) { + final selectedPaymentMethod = buySellViewModel.selectedPaymentMethod!; + return ProviderOptionTile( + lightImagePath: selectedPaymentMethod.lightIconPath, + darkImagePath: selectedPaymentMethod.darkIconPath, + title: selectedPaymentMethod.title, + onPressed: () => _pickPaymentMethod(context), + leadingIcon: Icons.arrow_forward_ios, + isLightMode: !buySellViewModel.isDarkTheme, + borderRadius: 30, + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8), + titleTextStyle: + textLargeBold(color: Theme.of(context).extension()!.titleColor), + ); + }); + } + return OptionTilePlaceholder(errorText: 'No payment methods available', borderRadius: 30); + } + + void _pickPaymentMethod(BuildContext context) async { + final currentOption = buySellViewModel.selectedPaymentMethod; + await Navigator.of(context).pushNamed( + Routes.paymentMethodOptionsPage, + arguments: [ + buySellViewModel.paymentMethods, + buySellViewModel.changeOption, + ], + ); + + buySellViewModel.selectedPaymentMethod; + if (currentOption != null && + currentOption.paymentMethodType != + buySellViewModel.selectedPaymentMethod?.paymentMethodType) { + await buySellViewModel.calculateBestRate(); + } + } + + void _setReactions(BuildContext context, BuySellViewModel buySellViewModel) { + if (_isReactionsSet) { + return; + } + + final fiatAmountController = fiatCurrencyKey.currentState!.amountController; + final cryptoAmountController = cryptoCurrencyKey.currentState!.amountController; + final cryptoAddressController = cryptoCurrencyKey.currentState!.addressController; + + _onCurrencyChange(buySellViewModel.cryptoCurrency, buySellViewModel, cryptoCurrencyKey); + _onCurrencyChange(buySellViewModel.fiatCurrency, buySellViewModel, fiatCurrencyKey); + + reaction( + (_) => buySellViewModel.wallet.name, + (String _) => + _onWalletNameChange(buySellViewModel, buySellViewModel.cryptoCurrency, cryptoCurrencyKey)); + + reaction( + (_) => buySellViewModel.cryptoCurrency, + (CryptoCurrency currency) => + _onCurrencyChange(currency, buySellViewModel, cryptoCurrencyKey)); + + reaction( + (_) => buySellViewModel.fiatCurrency, + (FiatCurrency currency) => + _onCurrencyChange(currency, buySellViewModel, fiatCurrencyKey)); + + reaction((_) => buySellViewModel.fiatAmount, (String amount) { + if (fiatCurrencyKey.currentState!.amountController.text != amount) { + fiatCurrencyKey.currentState!.amountController.text = amount; + } + }); + + reaction((_) => buySellViewModel.isCryptoCurrencyAddressEnabled, (bool isEnabled) { + cryptoCurrencyKey.currentState!.isAddressEditable(isEditable: isEnabled); + }); + + reaction((_) => buySellViewModel.cryptoAmount, (String amount) { + if (cryptoCurrencyKey.currentState!.amountController.text != amount) { + cryptoCurrencyKey.currentState!.amountController.text = amount; + } + }); + + reaction((_) => buySellViewModel.cryptoCurrencyAddress, (String address) { + if (cryptoAddressController != address) { + cryptoCurrencyKey.currentState!.addressController.text = address; + } + }); + + fiatAmountController.addListener(() { + if (fiatAmountController.text != buySellViewModel.fiatAmount) { + buySellViewModel.changeFiatAmount(amount: fiatAmountController.text); + } + }); + + cryptoAmountController.addListener(() { + if (cryptoAmountController.text != buySellViewModel.cryptoAmount) { + buySellViewModel.changeCryptoAmount(amount: cryptoAmountController.text); + } + }); + + cryptoAddressController.addListener(() { + buySellViewModel.changeCryptoCurrencyAddress(cryptoAddressController.text); + }); + + _cryptoAddressFocus.addListener(() async { + if (!_cryptoAddressFocus.hasFocus && cryptoAddressController.text.isNotEmpty) { + final domain = cryptoAddressController.text; + buySellViewModel.cryptoCurrencyAddress = + await fetchParsedAddress(context, domain, buySellViewModel.cryptoCurrency); + } + }); + + reaction((_) => buySellViewModel.wallet.walletAddresses.addressForExchange, (String address) { + if (buySellViewModel.cryptoCurrency == CryptoCurrency.xmr) { + cryptoCurrencyKey.currentState!.changeAddress(address: address); + } + }); + + reaction((_) => buySellViewModel.isReadyToTrade, (bool isReady) { + if (isReady) { + if (cryptoAmountController.text.isNotEmpty && + cryptoAmountController.text != S.current.fetching) { + buySellViewModel.changeCryptoAmount(amount: cryptoAmountController.text); + } else if (fiatAmountController.text.isNotEmpty && + fiatAmountController.text != S.current.fetching) { + buySellViewModel.changeFiatAmount(amount: fiatAmountController.text); + } + } + }); + + _isReactionsSet = true; + } + + void _onCurrencyChange(Currency currency, BuySellViewModel buySellViewModel, + GlobalKey key) { + final isCurrentTypeWallet = currency == buySellViewModel.wallet.currency; + + key.currentState!.changeSelectedCurrency(currency); + key.currentState!.changeWalletName(isCurrentTypeWallet ? buySellViewModel.wallet.name : ''); + + key.currentState!.changeAddress( + address: isCurrentTypeWallet ? buySellViewModel.wallet.walletAddresses.addressForExchange : ''); + + key.currentState!.changeAmount(amount: ''); + } + + void _onWalletNameChange(BuySellViewModel buySellViewModel, CryptoCurrency currency, + GlobalKey key) { + final isCurrentTypeWallet = currency == buySellViewModel.wallet.currency; + + if (isCurrentTypeWallet) { + key.currentState!.changeWalletName(buySellViewModel.wallet.name); + key.currentState!.addressController.text = buySellViewModel.wallet.walletAddresses.addressForExchange; + } else if (key.currentState!.addressController.text == + buySellViewModel.wallet.walletAddresses.addressForExchange) { + key.currentState!.changeWalletName(''); + key.currentState!.addressController.text = ''; + } + } + + void disposeBestRateSync() => {}; + + Widget _exchangeCardsSection(BuildContext context) { + final fiatExchangeCard = Observer( + builder: (_) => ExchangeCard( + cardInstanceName: 'fiat_currency_trade_card', + onDispose: disposeBestRateSync, + amountFocusNode: _fiatAmountFocus, + key: fiatCurrencyKey, + title: 'FIAT ${S.of(context).amount}', + initialCurrency: buySellViewModel.fiatCurrency, + initialWalletName: '', + initialAddress: '', + initialIsAmountEditable: true, + isAmountEstimated: false, + currencyRowPadding: EdgeInsets.zero, + addressRowPadding: EdgeInsets.zero, + isMoneroWallet: buySellViewModel.wallet == WalletType.monero, + showAddressField: false, + showLimitsField: false, + currencies: buySellViewModel.fiatCurrencies, + onCurrencySelected: (currency) => + buySellViewModel.changeFiatCurrency(currency: currency), + imageArrow: arrowBottomPurple, + currencyButtonColor: Colors.transparent, + addressButtonsColor: + Theme.of(context).extension()!.textFieldButtonColor, + borderColor: + Theme.of(context).extension()!.textFieldBorderTopPanelColor, + onPushPasteButton: (context) async {}, + onPushAddressBookButton: (context) async {}, + )); + + final cryptoExchangeCard = Observer( + builder: (_) => ExchangeCard( + cardInstanceName: 'crypto_currency_trade_card', + onDispose: disposeBestRateSync, + amountFocusNode: _cryptoAmountFocus, + addressFocusNode: _cryptoAddressFocus, + key: cryptoCurrencyKey, + title: 'Crypto ${S.of(context).amount}', + initialCurrency: buySellViewModel.cryptoCurrency, + initialWalletName: '', + initialAddress: buySellViewModel.cryptoCurrency == buySellViewModel.wallet.currency + ? buySellViewModel.wallet.walletAddresses.addressForExchange + : buySellViewModel.cryptoCurrencyAddress, + initialIsAmountEditable: true, + isAmountEstimated: true, + showLimitsField: false, + currencyRowPadding: EdgeInsets.zero, + addressRowPadding: EdgeInsets.zero, + isMoneroWallet: buySellViewModel.wallet == WalletType.monero, + currencies: buySellViewModel.cryptoCurrencies, + onCurrencySelected: (currency) => + buySellViewModel.changeCryptoCurrency(currency: currency), + imageArrow: arrowBottomCakeGreen, + currencyButtonColor: Colors.transparent, + addressButtonsColor: + Theme.of(context).extension()!.textFieldButtonColor, + borderColor: + Theme.of(context).extension()!.textFieldBorderBottomPanelColor, + addressTextFieldValidator: AddressValidator(type: buySellViewModel.cryptoCurrency), + onPushPasteButton: (context) async {}, + onPushAddressBookButton: (context) async {}, + )); + + if (responsiveLayoutUtil.shouldRenderMobileUI) { + return Observer( + builder: (_) { + if (buySellViewModel.isBuyAction) { + return MobileExchangeCardsSection( + firstExchangeCard: fiatExchangeCard, + secondExchangeCard: cryptoExchangeCard, + onBuyTap: () => null, + onSellTap: () => + buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null, + isBuySellOption: true, + ); + } else { + return MobileExchangeCardsSection( + firstExchangeCard: cryptoExchangeCard, + secondExchangeCard: fiatExchangeCard, + onBuyTap: () => + !buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null, + onSellTap: () => null, + isBuySellOption: true, + ); + } + }, + ); + } + + return Observer( + builder: (_) { + if (buySellViewModel.isBuyAction) { + return DesktopExchangeCardsSection( + firstExchangeCard: fiatExchangeCard, + secondExchangeCard: cryptoExchangeCard, + ); + } else { + return DesktopExchangeCardsSection( + firstExchangeCard: cryptoExchangeCard, + secondExchangeCard: fiatExchangeCard, + ); + } + }, + ); + } + + Future fetchParsedAddress( + BuildContext context, String domain, CryptoCurrency currency) async { + final parsedAddress = await getIt.get().resolve(context, domain, currency); + final address = await extractAddressFromParsed(context, parsedAddress); + return address; + } +} diff --git a/lib/src/screens/buy/payment_method_options_page.dart b/lib/src/screens/buy/payment_method_options_page.dart new file mode 100644 index 000000000..541f91ab4 --- /dev/null +++ b/lib/src/screens/buy/payment_method_options_page.dart @@ -0,0 +1,47 @@ +import 'package:cake_wallet/core/selectable_option.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/select_options_page.dart'; +import 'package:flutter/cupertino.dart'; + +class PaymentMethodOptionsPage extends SelectOptionsPage { + PaymentMethodOptionsPage({required this.items, this.pickAnOption}); + + final List items; + final Function(SelectableOption option)? pickAnOption; + + @override + String get pageTitle => S.current.choose_a_payment_method; + + @override + EdgeInsets? get contentPadding => null; + + @override + EdgeInsets? get tilePadding => EdgeInsets.only(top: 12); + + @override + EdgeInsets? get innerPadding => EdgeInsets.symmetric(horizontal: 24, vertical: 12); + + @override + double? get imageHeight => null; + + @override + double? get imageWidth => null; + + @override + Color? get selectedBackgroundColor => null; + + @override + double? get tileBorderRadius => 30; + + @override + String get bottomSectionText => ''; + + @override + void Function(SelectableOption option)? get onOptionTap => pickAnOption; + + @override + String get primaryButtonText => S.current.confirm; + + @override + void Function(BuildContext context)? get primaryButtonAction => null; +} diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart index d36c06013..7bb5f77f8 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart @@ -21,6 +21,14 @@ class DesktopDashboardActions extends StatelessWidget { return Column( children: [ const SizedBox(height: 16), + DesktopActionButton( + title: MainActions.showWalletsAction.name(context), + image: MainActions.showWalletsAction.image, + canShow: MainActions.showWalletsAction.canShow?.call(dashboardViewModel), + isEnabled: MainActions.showWalletsAction.isEnabled?.call(dashboardViewModel), + onTap: () async => + await MainActions.showWalletsAction.onTap(context, dashboardViewModel), + ), DesktopActionButton( title: MainActions.exchangeAction.name(context), image: MainActions.exchangeAction.image, @@ -55,20 +63,11 @@ class DesktopDashboardActions extends StatelessWidget { children: [ Expanded( child: DesktopActionButton( - title: MainActions.buyAction.name(context), - image: MainActions.buyAction.image, - canShow: MainActions.buyAction.canShow?.call(dashboardViewModel), - isEnabled: MainActions.buyAction.isEnabled?.call(dashboardViewModel), - onTap: () async => await MainActions.buyAction.onTap(context, dashboardViewModel), - ), - ), - Expanded( - child: DesktopActionButton( - title: MainActions.sellAction.name(context), - image: MainActions.sellAction.image, - canShow: MainActions.sellAction.canShow?.call(dashboardViewModel), - isEnabled: MainActions.sellAction.isEnabled?.call(dashboardViewModel), - onTap: () async => await MainActions.sellAction.onTap(context, dashboardViewModel), + title: MainActions.tradeAction.name(context), + image: MainActions.tradeAction.image, + canShow: MainActions.tradeAction.canShow?.call(dashboardViewModel), + isEnabled: MainActions.tradeAction.isEnabled?.call(dashboardViewModel), + onTap: () async => await MainActions.tradeAction.onTap(context, dashboardViewModel), ), ), ], diff --git a/lib/src/screens/exchange/widgets/exchange_card.dart b/lib/src/screens/exchange/widgets/exchange_card.dart index 75a2eadd7..7c4cc948d 100644 --- a/lib/src/screens/exchange/widgets/exchange_card.dart +++ b/lib/src/screens/exchange/widgets/exchange_card.dart @@ -18,7 +18,7 @@ import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; -class ExchangeCard extends StatefulWidget { +class ExchangeCard extends StatefulWidget { ExchangeCard({ Key? key, required this.initialCurrency, @@ -40,19 +40,23 @@ class ExchangeCard extends StatefulWidget { this.borderColor = Colors.transparent, this.hasAllAmount = false, this.isAllAmountEnabled = false, + this.showAddressField = true, + this.showLimitsField = true, this.amountFocusNode, this.addressFocusNode, this.allAmount, + this.currencyRowPadding, + this.addressRowPadding, this.onPushPasteButton, this.onPushAddressBookButton, this.onDispose, required this.cardInstanceName, }) : super(key: key); - final List currencies; - final Function(CryptoCurrency) onCurrencySelected; + final List currencies; + final Function(T) onCurrencySelected; final String title; - final CryptoCurrency initialCurrency; + final T initialCurrency; final String initialWalletName; final String initialAddress; final bool initialIsAmountEditable; @@ -70,18 +74,22 @@ class ExchangeCard extends StatefulWidget { final FocusNode? amountFocusNode; final FocusNode? addressFocusNode; final bool hasAllAmount; + final bool showAddressField; + final bool showLimitsField; final bool isAllAmountEnabled; final VoidCallback? allAmount; + final EdgeInsets? currencyRowPadding; + final EdgeInsets? addressRowPadding; final void Function(BuildContext context)? onPushPasteButton; final void Function(BuildContext context)? onPushAddressBookButton; final Function()? onDispose; final String cardInstanceName; @override - ExchangeCardState createState() => ExchangeCardState(); + ExchangeCardState createState() => ExchangeCardState(); } -class ExchangeCardState extends State { +class ExchangeCardState extends State> { ExchangeCardState() : _title = '', _min = '', @@ -89,7 +97,6 @@ class ExchangeCardState extends State { _isAmountEditable = false, _isAddressEditable = false, _walletName = '', - _selectedCurrency = CryptoCurrency.btc, _isAmountEstimated = false, _isMoneroWallet = false, _cardInstanceName = ''; @@ -101,7 +108,7 @@ class ExchangeCardState extends State { String _title; String? _min; String? _max; - CryptoCurrency _selectedCurrency; + late T _selectedCurrency; String _walletName; bool _isAmountEditable; bool _isAddressEditable; @@ -118,7 +125,8 @@ class ExchangeCardState extends State { _selectedCurrency = widget.initialCurrency; _isAmountEstimated = widget.isAmountEstimated; _isMoneroWallet = widget.isMoneroWallet; - addressController.text = widget.initialAddress; + addressController.text = _normalizeAddressFormat(widget.initialAddress); + super.initState(); } @@ -136,7 +144,7 @@ class ExchangeCardState extends State { }); } - void changeSelectedCurrency(CryptoCurrency currency) { + void changeSelectedCurrency(T currency) { setState(() => _selectedCurrency = currency); } @@ -157,7 +165,7 @@ class ExchangeCardState extends State { } void changeAddress({required String address}) { - setState(() => addressController.text = address); + setState(() => addressController.text = _normalizeAddressFormat(address)); } void changeAmount({required String amount}) { @@ -222,7 +230,7 @@ class ExchangeCardState extends State { Divider(height: 1, color: Theme.of(context).extension()!.textFieldHintColor), Padding( padding: EdgeInsets.only(top: 5), - child: Container( + child: widget.showLimitsField ? Container( height: 15, child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [ _min != null @@ -247,7 +255,7 @@ class ExchangeCardState extends State { ), ) : Offstage(), - ])), + ])) : Offstage(), ), !_isAddressEditable && widget.hasRefundAddress ? Padding( @@ -261,10 +269,11 @@ class ExchangeCardState extends State { )) : Offstage(), _isAddressEditable + ? widget.showAddressField ? FocusTraversalOrder( order: NumericFocusOrder(2), child: Padding( - padding: EdgeInsets.only(top: 20), + padding: widget.addressRowPadding ?? EdgeInsets.only(top: 20), child: AddressTextField( addressKey: ValueKey('${_cardInstanceName}_editable_address_textfield_key'), focusNode: widget.addressFocusNode, @@ -280,26 +289,29 @@ class ExchangeCardState extends State { widget.amountFocusNode?.requestFocus(); amountController.text = paymentRequest.amount; }, - placeholder: widget.hasRefundAddress ? S.of(context).refund_address : null, + placeholder: + widget.hasRefundAddress ? S.of(context).refund_address : null, options: [ AddressTextFieldOption.paste, AddressTextFieldOption.qrCode, AddressTextFieldOption.addressBook, ], isBorderExist: false, - textStyle: - TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white), + textStyle: TextStyle( + fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white), hintStyle: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: Theme.of(context).extension()!.hintTextColor), + color: + Theme.of(context).extension()!.hintTextColor), buttonColor: widget.addressButtonsColor, validator: widget.addressTextFieldValidator, onPushPasteButton: widget.onPushPasteButton, onPushAddressBookButton: widget.onPushAddressBookButton, selectedCurrency: _selectedCurrency), ), - ) + ) + : Offstage() : Padding( padding: EdgeInsets.only(top: 10), child: Builder( @@ -402,7 +414,7 @@ class ExchangeCardState extends State { hintText: S.of(context).search_currency, isMoneroWallet: _isMoneroWallet, isConvertFrom: widget.hasRefundAddress, - onItemSelected: (Currency item) => widget.onCurrencySelected(item as CryptoCurrency), + onItemSelected: (Currency item) => widget.onCurrencySelected(item as T), ), ); } @@ -424,4 +436,10 @@ class ExchangeCardState extends State { actionLeftButton: () => Navigator.of(dialogContext).pop()); }); } + + String _normalizeAddressFormat(String address) { + if (address.startsWith('bitcoincash:')) address = address.substring(12); + return address; + } } + diff --git a/lib/src/screens/exchange/widgets/mobile_exchange_cards_section.dart b/lib/src/screens/exchange/widgets/mobile_exchange_cards_section.dart index 126bca835..d53f16339 100644 --- a/lib/src/screens/exchange/widgets/mobile_exchange_cards_section.dart +++ b/lib/src/screens/exchange/widgets/mobile_exchange_cards_section.dart @@ -1,20 +1,29 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart'; import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; import 'package:flutter/material.dart'; class MobileExchangeCardsSection extends StatelessWidget { final Widget firstExchangeCard; final Widget secondExchangeCard; + final bool isBuySellOption; + final VoidCallback? onBuyTap; + final VoidCallback? onSellTap; const MobileExchangeCardsSection({ Key? key, required this.firstExchangeCard, required this.secondExchangeCard, + this.isBuySellOption = false, + this.onBuyTap, + this.onSellTap, }) : super(key: key); @override Widget build(BuildContext context) { return Container( - padding: EdgeInsets.only(bottom: 32), + padding: EdgeInsets.only(bottom: isBuySellOption ? 8 : 32), decoration: BoxDecoration( borderRadius: BorderRadius.only( bottomLeft: Radius.circular(24), @@ -45,8 +54,18 @@ class MobileExchangeCardsSection extends StatelessWidget { end: Alignment.bottomRight, ), ), - padding: EdgeInsets.fromLTRB(24, 100, 24, 32), - child: firstExchangeCard, + padding: EdgeInsets.fromLTRB(24, 90, 24, isBuySellOption ? 8 : 32), + child: Column( + children: [ + if (isBuySellOption) Column( + children: [ + const SizedBox(height: 16), + BuySellOptionButtons(onBuyTap: onBuyTap, onSellTap: onSellTap), + ], + ), + firstExchangeCard, + ], + ), ), Padding( padding: EdgeInsets.only(top: 29, left: 24, right: 24), @@ -57,3 +76,69 @@ class MobileExchangeCardsSection extends StatelessWidget { ); } } + +class BuySellOptionButtons extends StatefulWidget { + final VoidCallback? onBuyTap; + final VoidCallback? onSellTap; + + const BuySellOptionButtons({this.onBuyTap, this.onSellTap}); + + @override + _BuySellOptionButtonsState createState() => _BuySellOptionButtonsState(); +} + +class _BuySellOptionButtonsState extends State { + bool isBuySelected = true; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 24), + child: Row( + children: [ + Expanded(flex: 2, child: SizedBox()), + Expanded( + flex: 5, + child: SelectButton( + height: 44, + text: S.of(context).buy, + isSelected: isBuySelected, + showTrailingIcon: false, + textColor: Colors.white, + image: Image.asset('assets/images/buy.png', height: 25, width: 25), + padding: EdgeInsets.only(left: 10, right: 30), + color: isBuySelected + ? null + : Theme.of(context).extension()!.textFieldButtonColor, + onTap: () { + setState(() => isBuySelected = true); + if (widget.onBuyTap != null) widget.onBuyTap!(); + }, + ), + ), + Expanded(child: const SizedBox()), + Expanded( + flex: 5, + child: SelectButton( + height: 44, + text: S.of(context).sell, + isSelected: !isBuySelected, + showTrailingIcon: false, + textColor: Colors.white, + image: Image.asset('assets/images/sell.png', height: 25, width: 25), + padding: EdgeInsets.only(left: 10, right: 30), + color: !isBuySelected + ? null + : Theme.of(context).extension()!.textFieldButtonColor, + onTap: () { + setState(() => isBuySelected = false); + if (widget.onSellTap != null) widget.onSellTap!(); + }, + ), + ), + Expanded(flex: 2, child: SizedBox()), + ], + ), + ); + } +} diff --git a/lib/src/screens/seed/pre_seed_page.dart b/lib/src/screens/seed/pre_seed_page.dart index 23de4564f..475f45fca 100644 --- a/lib/src/screens/seed/pre_seed_page.dart +++ b/lib/src/screens/seed/pre_seed_page.dart @@ -1,6 +1,6 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/InfoPage.dart'; +import 'package:cake_wallet/src/screens/Info_page.dart'; import 'package:flutter/cupertino.dart'; class PreSeedPage extends InfoPage { diff --git a/lib/src/screens/select_options_page.dart b/lib/src/screens/select_options_page.dart new file mode 100644 index 000000000..70cb2abc1 --- /dev/null +++ b/lib/src/screens/select_options_page.dart @@ -0,0 +1,199 @@ +import 'package:cake_wallet/core/selectable_option.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/provider_optoin_tile.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/themes/extensions/option_tile_theme.dart'; +import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; +import 'package:flutter/material.dart'; + +abstract class SelectOptionsPage extends BasePage { + SelectOptionsPage(); + + String get pageTitle; + + EdgeInsets? get contentPadding; + + EdgeInsets? get tilePadding; + + EdgeInsets? get innerPadding; + + double? get imageHeight; + + double? get imageWidth; + + Color? get selectedBackgroundColor; + + double? get tileBorderRadius; + + String get bottomSectionText; + + bool get primaryButtonEnabled => true; + + String get primaryButtonText => ''; + + List get items; + + void Function(SelectableOption option)? get onOptionTap; + + void Function(BuildContext context)? get primaryButtonAction; + + @override + String get title => pageTitle; + + @override + Widget body(BuildContext context) { + return ScrollableWithBottomSection( + content: BodySelectOptionsPage( + items: items, + onOptionTap: onOptionTap, + tilePadding: tilePadding, + tileBorderRadius: tileBorderRadius, + imageHeight: imageHeight, + imageWidth: imageWidth, + innerPadding: innerPadding), + bottomSection: Padding( + padding: contentPadding ?? EdgeInsets.zero, + child: Column( + children: [ + Text( + bottomSectionText, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Theme.of(context).extension()!.detailsTitlesColor, + ), + ), + if (primaryButtonEnabled) + LoadingPrimaryButton( + text: primaryButtonText, + onPressed: () { + primaryButtonAction != null + ? primaryButtonAction!(context) + : Navigator.pop(context); + }, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + isDisabled: false, + isLoading: false) + ], + ), + ), + ); + } +} + +class BodySelectOptionsPage extends StatefulWidget { + const BodySelectOptionsPage({ + required this.items, + this.onOptionTap, + this.tilePadding, + this.tileBorderRadius, + this.imageHeight, + this.imageWidth, + this.innerPadding, + }); + + final List items; + final void Function(SelectableOption option)? onOptionTap; + final EdgeInsets? tilePadding; + final double? tileBorderRadius; + final double? imageHeight; + final double? imageWidth; + final EdgeInsets? innerPadding; + + @override + _BodySelectOptionsPageState createState() => _BodySelectOptionsPageState(); +} + +class _BodySelectOptionsPageState extends State { + late List _items; + + @override + void initState() { + super.initState(); + _items = widget.items; + } + + void _handleOptionTap(SelectableOption option) { + setState(() { + for (var item in _items) { + if (item is SelectableOption) { + item.isOptionSelected = false; + } + } + option.isOptionSelected = true; + }); + widget.onOptionTap?.call(option); + } + + @override + Widget build(BuildContext context) { + final isLightMode = Theme.of(context).extension()?.useDarkImage ?? false; + + Color titleColor = + isLightMode ? Theme.of(context).appBarTheme.titleTextStyle!.color! : Colors.white; + + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 350), + child: Column( + children: _items.map((item) { + if (item is OptionTitle) { + return Padding( + padding: const EdgeInsets.only(top: 18, bottom: 8), + child: Container( + width: double.infinity, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: titleColor, + width: 1, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + item.title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: titleColor, + ), + ), + ), + ), + ); + } else if (item is SelectableOption) { + return Padding( + padding: widget.tilePadding ?? const EdgeInsets.only(top: 24), + child: ProviderOptionTile( + title: item.title, + lightImagePath: item.lightIconPath, + darkImagePath: item.darkIconPath, + imageHeight: widget.imageHeight, + imageWidth: widget.imageWidth, + padding: widget.innerPadding, + description: item.description, + topLeftSubTitle: item.topLeftSubTitle, + topRightSubTitle: item.topRightSubTitle, + rightSubTitleLightIconPath: item.topRightSubTitleLightIconPath, + rightSubTitleDarkIconPath: item.topRightSubTitleDarkIconPath, + bottomLeftSubTitle: item.bottomLeftSubTitle, + badges: item.badges, + isSelected: item.isOptionSelected, + borderRadius: widget.tileBorderRadius, + isLightMode: isLightMode, + onPressed: () => _handleOptionTap(item), + ), + ); + } + return const SizedBox.shrink(); + }).toList(), + ), + ), + ); + } +} diff --git a/lib/src/screens/settings/other_settings_page.dart b/lib/src/screens/settings/other_settings_page.dart index 137f699f5..f6a6288f5 100644 --- a/lib/src/screens/settings/other_settings_page.dart +++ b/lib/src/screens/settings/other_settings_page.dart @@ -57,22 +57,6 @@ class OtherSettingsPage extends BasePage { handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.changeRep), ), - if(_otherSettingsViewModel.isEnabledBuyAction) - SettingsPickerCell( - title: S.current.default_buy_provider, - items: _otherSettingsViewModel.availableBuyProvidersTypes, - displayItem: _otherSettingsViewModel.getBuyProviderType, - selectedItem: _otherSettingsViewModel.buyProviderType, - onItemSelected: _otherSettingsViewModel.onBuyProviderTypeSelected - ), - if(_otherSettingsViewModel.isEnabledSellAction) - SettingsPickerCell( - title: S.current.default_sell_provider, - items: _otherSettingsViewModel.availableSellProvidersTypes, - displayItem: _otherSettingsViewModel.getSellProviderType, - selectedItem: _otherSettingsViewModel.sellProviderType, - onItemSelected: _otherSettingsViewModel.onSellProviderTypeSelected, - ), SettingsCellWithArrow( title: S.current.settings_terms_and_conditions, handler: (BuildContext context) => diff --git a/lib/src/screens/settings/privacy_page.dart b/lib/src/screens/settings/privacy_page.dart index 53e7686e8..8652c4af6 100644 --- a/lib/src/screens/settings/privacy_page.dart +++ b/lib/src/screens/settings/privacy_page.dart @@ -73,16 +73,10 @@ class PrivacyPage extends BasePage { _privacySettingsViewModel.setIsAppSecure(value); }), SettingsSwitcherCell( - title: S.current.disable_buy, - value: _privacySettingsViewModel.disableBuy, + title: S.current.disable_trade_option, + value: _privacySettingsViewModel.disableTradeOption, onValueChange: (BuildContext _, bool value) { - _privacySettingsViewModel.setDisableBuy(value); - }), - SettingsSwitcherCell( - title: S.current.disable_sell, - value: _privacySettingsViewModel.disableSell, - onValueChange: (BuildContext _, bool value) { - _privacySettingsViewModel.setDisableSell(value); + _privacySettingsViewModel.setDisableTradeOption(value); }), SettingsSwitcherCell( title: S.current.disable_bulletin, diff --git a/lib/src/screens/setup_2fa/setup_2fa_info_page.dart b/lib/src/screens/setup_2fa/setup_2fa_info_page.dart index 8aa0ac3c9..834d01c26 100644 --- a/lib/src/screens/setup_2fa/setup_2fa_info_page.dart +++ b/lib/src/screens/setup_2fa/setup_2fa_info_page.dart @@ -1,6 +1,6 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/InfoPage.dart'; +import 'package:cake_wallet/src/screens/Info_page.dart'; import 'package:flutter/cupertino.dart'; class Setup2FAInfoPage extends InfoPage { diff --git a/lib/src/widgets/address_text_field.dart b/lib/src/widgets/address_text_field.dart index d904768f0..9b407dedb 100644 --- a/lib/src/widgets/address_text_field.dart +++ b/lib/src/widgets/address_text_field.dart @@ -1,20 +1,21 @@ import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cw_core/currency.dart'; import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/entities/qr_scanner.dart'; import 'package:cake_wallet/entities/contact_base.dart'; -import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; import 'package:cake_wallet/utils/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart'; enum AddressTextFieldOption { paste, qrCode, addressBook, walletAddresses } -class AddressTextField extends StatelessWidget { + +class AddressTextField extends StatelessWidget{ AddressTextField({ required this.controller, this.isActive = true, @@ -58,7 +59,7 @@ class AddressTextField extends StatelessWidget { final Function(BuildContext context)? onPushAddressBookButton; final Function(BuildContext context)? onPushAddressPickerButton; final Function(ContactBase contact)? onSelectedContact; - final CryptoCurrency? selectedCurrency; + final T? selectedCurrency; final Key? addressKey; @override diff --git a/lib/src/widgets/provider_optoin_tile.dart b/lib/src/widgets/provider_optoin_tile.dart new file mode 100644 index 000000000..85396a97d --- /dev/null +++ b/lib/src/widgets/provider_optoin_tile.dart @@ -0,0 +1,527 @@ +import 'package:cake_wallet/themes/extensions/option_tile_theme.dart'; +import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class ProviderOptionTile extends StatelessWidget { + const ProviderOptionTile({ + required this.onPressed, + required this.lightImagePath, + required this.darkImagePath, + required this.title, + this.topLeftSubTitle, + this.topRightSubTitle, + this.bottomLeftSubTitle, + this.bottomRightSubTitle, + this.leftSubTitleIconPath, + this.rightSubTitleLightIconPath, + this.rightSubTitleDarkIconPath, + this.description, + this.badges, + this.borderRadius, + this.imageHeight, + this.imageWidth, + this.padding, + this.titleTextStyle, + this.firstSubTitleTextStyle, + this.secondSubTitleTextStyle, + this.leadingIcon, + this.selectedBackgroundColor, + this.isSelected = false, + required this.isLightMode, + }); + + final VoidCallback onPressed; + final String lightImagePath; + final String darkImagePath; + final String title; + final String? topLeftSubTitle; + final String? topRightSubTitle; + final String? bottomLeftSubTitle; + final String? bottomRightSubTitle; + final String? leftSubTitleIconPath; + final String? rightSubTitleLightIconPath; + final String? rightSubTitleDarkIconPath; + final String? description; + final List? badges; + final double? borderRadius; + final double? imageHeight; + final double? imageWidth; + final EdgeInsets? padding; + final TextStyle? titleTextStyle; + final TextStyle? firstSubTitleTextStyle; + final TextStyle? secondSubTitleTextStyle; + final IconData? leadingIcon; + final Color? selectedBackgroundColor; + final bool isSelected; + final bool isLightMode; + + @override + Widget build(BuildContext context) { + final backgroundColor = isSelected + ? isLightMode + ? Theme.of(context).extension()!.currentTileBackgroundColor + : Theme.of(context).extension()!.titleColor + : Theme.of(context).cardColor; + + final textColor = isSelected + ? isLightMode + ? Colors.white + : Theme.of(context).cardColor + : Theme.of(context).extension()!.titleColor; + + final badgeColor = isSelected + ? Theme.of(context).cardColor + : Theme.of(context).extension()!.titleColor; + + final badgeTextColor = isSelected + ? Theme.of(context).extension()!.titleColor + : Theme.of(context).cardColor; + + final imagePath = isSelected + ? isLightMode + ? darkImagePath + : lightImagePath + : isLightMode + ? lightImagePath + : darkImagePath; + + final rightSubTitleIconPath = isSelected + ? isLightMode + ? rightSubTitleDarkIconPath + : rightSubTitleLightIconPath + : isLightMode + ? rightSubTitleLightIconPath + : rightSubTitleDarkIconPath; + + return GestureDetector( + onTap: onPressed, + child: Container( + width: double.infinity, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(borderRadius ?? 12)), + border: isSelected && !isLightMode ? Border.all(color: textColor) : null, + color: backgroundColor, + ), + child: Padding( + padding: padding ?? const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + children: [ + getImage(imagePath, height: imageHeight, width: imageWidth), + SizedBox(width: 8), + Expanded( + child: Container( + child: Row( + children: [ + Expanded( + child: Text(title, + style: titleTextStyle ?? textLargeBold(color: textColor))), + Row( + children: [ + if (leadingIcon != null) + Icon(leadingIcon, size: 16, color: textColor), + ], + ) + ], + ), + ), + ), + ], + ), + if (topLeftSubTitle != null || topRightSubTitle != null) + subTitleWidget( + leftSubTitle: topLeftSubTitle, + subTitleIconPath: leftSubTitleIconPath, + textColor: textColor, + rightSubTitle: topRightSubTitle, + rightSubTitleIconPath: rightSubTitleIconPath), + if (bottomLeftSubTitle != null || bottomRightSubTitle != null) + subTitleWidget( + leftSubTitle: bottomLeftSubTitle, + textColor: textColor, + subTitleFontSize: 12), + if (badges != null && badges!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 12), + child: Row(children: [ + ...badges! + .map((badge) => Badge( + title: badge, textColor: badgeTextColor, backgroundColor: badgeColor)) + .toList() + ]), + ) + ], + ), + ), + ), + ); + } +} + +class subTitleWidget extends StatelessWidget { + const subTitleWidget({ + super.key, + this.leftSubTitle, + this.subTitleIconPath, + required this.textColor, + this.rightSubTitle, + this.rightSubTitleIconPath, + this.subTitleFontSize = 16, + }); + + final String? leftSubTitle; + final String? subTitleIconPath; + final Color textColor; + final String? rightSubTitle; + final String? rightSubTitleIconPath; + final double subTitleFontSize; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + leftSubTitle != null || subTitleIconPath != null + ? Row( + children: [ + if (subTitleIconPath != null && subTitleIconPath!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(right: 6), + child: getImage(subTitleIconPath!), + ), + Text( + leftSubTitle ?? '', + style: TextStyle( + fontSize: subTitleFontSize, + fontWeight: FontWeight.w700, + color: textColor), + ), + ], + ) + : Offstage(), + rightSubTitle != null || rightSubTitleIconPath != null + ? Row( + children: [ + if (rightSubTitleIconPath != null && rightSubTitleIconPath!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(right: 4), + child: getImage(rightSubTitleIconPath!, imageColor: textColor), + ), + Text( + rightSubTitle ?? '', + style: TextStyle( + fontSize: subTitleFontSize, fontWeight: FontWeight.w700, color: textColor), + ), + ], + ) + : Offstage(), + ], + ); + } +} + +class Badge extends StatelessWidget { + Badge({required this.textColor, required this.backgroundColor, required this.title}); + + final String title; + 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: Text( + title, + style: TextStyle( + color: textColor, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ); + } +} + +Widget getImage(String imagePath, {double? height, double? width, Color? imageColor}) { + final bool isNetworkImage = imagePath.startsWith('http') || imagePath.startsWith('https'); + final bool isSvg = imagePath.endsWith('.svg'); + final double imageHeight = height ?? 35; + final double imageWidth = width ?? 35; + + if (isNetworkImage) { + return isSvg + ? SvgPicture.network( + imagePath, + height: imageHeight, + width: imageWidth, + colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null, + placeholderBuilder: (BuildContext context) => Container( + height: imageHeight, + width: imageWidth, + child: Center( + child: CircularProgressIndicator(), + ), + ), + ) + : Image.network( + imagePath, + height: imageHeight, + width: imageWidth, + loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { + if (loadingProgress == null) { + return child; + } + return Container( + height: imageHeight, + width: imageWidth, + child: Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ), + ); + }, + errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) { + return Container( + height: imageHeight, + width: imageWidth, + ); + }, + ); + } else { + return isSvg + ? SvgPicture.asset( + imagePath, + height: imageHeight, + width: imageWidth, + colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null, + ) + : Image.asset(imagePath, height: imageHeight, width: imageWidth); + } +} + +class OptionTilePlaceholder extends StatefulWidget { + OptionTilePlaceholder({ + this.borderRadius, + this.imageHeight, + this.imageWidth, + this.padding, + this.leadingIcon, + this.withBadge = true, + this.withSubtitle = true, + this.isDarkTheme = false, + this.errorText, + }); + + final double? borderRadius; + final double? imageHeight; + final double? imageWidth; + final EdgeInsets? padding; + final IconData? leadingIcon; + final bool withBadge; + final bool withSubtitle; + final bool isDarkTheme; + final String? errorText; + + @override + _OptionTilePlaceholderState createState() => _OptionTilePlaceholderState(); +} + +class _OptionTilePlaceholderState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + + _controller = AnimationController( + duration: const Duration(seconds: 2), + vsync: this, + )..repeat(); + + _animation = CurvedAnimation( + parent: _controller, + curve: Curves.linear, + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final backgroundColor = Theme.of(context).cardColor; + final titleColor = Theme.of(context).extension()!.titleColor.withOpacity(0.4); + + return widget.errorText != null + ? Container( + width: double.infinity, + padding: widget.padding ?? EdgeInsets.all(16), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius ?? 12)), + color: backgroundColor, + ), + child: Column( + children: [ + Text( + widget.errorText!, + style: TextStyle( + color: titleColor, + fontSize: 16, + ), + ), + if (widget.withSubtitle) SizedBox(height: 8), + Text( + '', + style: TextStyle( + color: titleColor, + fontSize: 16, + ), + ), + ], + ), + ) + : AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Stack( + children: [ + Container( + width: double.infinity, + padding: widget.padding ?? EdgeInsets.all(16), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius ?? 12)), + color: backgroundColor, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + children: [ + Container( + height: widget.imageHeight ?? 35, + width: widget.imageWidth ?? 35, + decoration: BoxDecoration( + color: titleColor, + shape: BoxShape.circle, + ), + ), + SizedBox(width: 8), + Expanded( + child: Container( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + height: 20, + width: 70, + decoration: BoxDecoration( + color: titleColor, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + if (widget.leadingIcon != null) + Icon(widget.leadingIcon, size: 16, color: titleColor), + ], + ), + ), + ), + ], + ), + if (widget.withSubtitle) + Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + height: 20, + width: 170, + decoration: BoxDecoration( + color: titleColor, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + ], + ), + ), + if (widget.withBadge) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Row( + children: [ + Container( + height: 30, + width: 70, + decoration: BoxDecoration( + color: titleColor, + borderRadius: BorderRadius.all(Radius.circular(20)), + ), + ), + SizedBox(width: 8), + Container( + height: 30, + width: 70, + decoration: BoxDecoration( + color: titleColor, + borderRadius: BorderRadius.all(Radius.circular(20)), + ), + ), + ], + ), + ), + ], + ), + ), + Positioned.fill( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius ?? 12)), + gradient: LinearGradient( + begin: Alignment(-2, -4), + end: Alignment(2, 4), + stops: [ + _animation.value - 0.2, + _animation.value, + _animation.value + 0.2, + ], + colors: [ + backgroundColor.withOpacity(widget.isDarkTheme ? 0.4 : 0.7), + backgroundColor.withOpacity(widget.isDarkTheme ? 0.7 : 0.4), + backgroundColor.withOpacity(widget.isDarkTheme ? 0.4 : 0.7), + ], + ), + ), + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 50d51d2ed..1ecaf50cc 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -61,8 +61,7 @@ abstract class SettingsStoreBase with Store { required BitcoinSeedType initialBitcoinSeedType, required NanoSeedType initialNanoSeedType, required bool initialAppSecure, - required bool initialDisableBuy, - required bool initialDisableSell, + required bool initialDisableTrade, required FilterListOrderType initialWalletListOrder, required FilterListOrderType initialContactListOrder, required bool initialDisableBulletin, @@ -150,8 +149,7 @@ abstract class SettingsStoreBase with Store { useTOTP2FA = initialUseTOTP2FA, numberOfFailedTokenTrials = initialFailedTokenTrial, isAppSecure = initialAppSecure, - disableBuy = initialDisableBuy, - disableSell = initialDisableSell, + disableTradeOption = initialDisableTrade, disableBulletin = initialDisableBulletin, walletListOrder = initialWalletListOrder, contactListOrder = initialContactListOrder, @@ -178,9 +176,7 @@ abstract class SettingsStoreBase with Store { initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings, currentSyncMode = initialSyncMode, currentSyncAll = initialSyncAll, - priority = ObservableMap(), - defaultBuyProviders = ObservableMap(), - defaultSellProviders = ObservableMap() { + priority = ObservableMap() { //this.nodes = ObservableMap.of(nodes); if (initialMoneroTransactionPriority != null) { @@ -221,30 +217,6 @@ abstract class SettingsStoreBase with Store { initializeTrocadorProviderStates(); - WalletType.values.forEach((walletType) { - final key = 'buyProvider_${walletType.toString()}'; - final providerId = sharedPreferences.getString(key); - if (providerId != null) { - defaultBuyProviders[walletType] = ProviderType.values.firstWhere( - (provider) => provider.id == providerId, - orElse: () => ProviderType.askEachTime); - } else { - defaultBuyProviders[walletType] = ProviderType.askEachTime; - } - }); - - WalletType.values.forEach((walletType) { - final key = 'sellProvider_${walletType.toString()}'; - final providerId = sharedPreferences.getString(key); - if (providerId != null) { - defaultSellProviders[walletType] = ProviderType.values.firstWhere( - (provider) => provider.id == providerId, - orElse: () => ProviderType.askEachTime); - } else { - defaultSellProviders[walletType] = ProviderType.askEachTime; - } - }); - reaction( (_) => fiatCurrency, (FiatCurrency fiatCurrency) => sharedPreferences.setString( @@ -267,20 +239,6 @@ abstract class SettingsStoreBase with Store { reaction((_) => shouldShowRepWarning, (bool val) => sharedPreferences.setBool(PreferencesKey.shouldShowRepWarning, val)); - defaultBuyProviders.observe((change) { - final String key = 'buyProvider_${change.key.toString()}'; - if (change.newValue != null) { - sharedPreferences.setString(key, change.newValue!.id); - } - }); - - defaultSellProviders.observe((change) { - final String key = 'sellProvider_${change.key.toString()}'; - if (change.newValue != null) { - sharedPreferences.setString(key, change.newValue!.id); - } - }); - priority.observe((change) { final String? key; switch (change.key) { @@ -329,14 +287,9 @@ abstract class SettingsStoreBase with Store { }); } - reaction((_) => disableBuy, - (bool disableBuy) => sharedPreferences.setBool(PreferencesKey.disableBuyKey, disableBuy)); - - reaction( - (_) => disableSell, - (bool disableSell) => - sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell)); - + reaction((_) => disableTradeOption, + (bool disableTradeOption) => sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption)); + reaction( (_) => disableBulletin, (bool disableBulletin) => @@ -680,10 +633,7 @@ abstract class SettingsStoreBase with Store { bool isAppSecure; @observable - bool disableBuy; - - @observable - bool disableSell; + bool disableTradeOption; @observable FilterListOrderType contactListOrder; @@ -769,12 +719,6 @@ abstract class SettingsStoreBase with Store { @observable ObservableMap trocadorProviderStates = ObservableMap(); - @observable - ObservableMap defaultBuyProviders; - - @observable - ObservableMap defaultSellProviders; - @observable SortBalanceBy sortBalanceBy; @@ -956,8 +900,7 @@ abstract class SettingsStoreBase with Store { final shouldSaveRecipientAddress = sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ?? false; final isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? false; - final disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? false; - final disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? false; + final disableTradeOption = sharedPreferences.getBool(PreferencesKey.disableTradeOption) ?? false; final disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? false; final walletListOrder = FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0]; @@ -1255,8 +1198,7 @@ abstract class SettingsStoreBase with Store { initialBitcoinSeedType: bitcoinSeedType, initialNanoSeedType: nanoSeedType, initialAppSecure: isAppSecure, - initialDisableBuy: disableBuy, - initialDisableSell: disableSell, + initialDisableTrade: disableTradeOption, initialDisableBulletin: disableBulletin, initialWalletListOrder: walletListOrder, initialWalletListAscending: walletListAscending, @@ -1406,8 +1348,7 @@ abstract class SettingsStoreBase with Store { numberOfFailedTokenTrials = sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials; isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure; - disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? disableBuy; - disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? disableSell; + disableTradeOption = sharedPreferences.getBool(PreferencesKey.disableTradeOption) ?? disableTradeOption; disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? disableBulletin; walletListOrder = diff --git a/lib/typography.dart b/lib/typography.dart index 8ae2cb4e5..816f116b4 100644 --- a/lib/typography.dart +++ b/lib/typography.dart @@ -22,6 +22,8 @@ TextStyle textMediumSemiBold({Color? color}) => _cakeSemiBold(22, color); TextStyle textLarge({Color? color}) => _cakeRegular(18, color); +TextStyle textLargeBold({Color? color}) => _cakeBold(18, color); + TextStyle textLargeSemiBold({Color? color}) => _cakeSemiBold(24, color); TextStyle textXLarge({Color? color}) => _cakeRegular(32, color); diff --git a/lib/view_model/buy/buy_sell_view_model.dart b/lib/view_model/buy/buy_sell_view_model.dart new file mode 100644 index 000000000..e1c53ee56 --- /dev/null +++ b/lib/view_model/buy/buy_sell_view_model.dart @@ -0,0 +1,446 @@ +import 'dart:async'; + +import 'package:cake_wallet/buy/buy_provider.dart'; +import 'package:cake_wallet/buy/buy_quote.dart'; +import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; +import 'package:cake_wallet/buy/payment_method.dart'; +import 'package:cake_wallet/buy/sell_buy_states.dart'; +import 'package:cake_wallet/core/selectable_option.dart'; +import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/entities/provider_types.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/currency_for_wallet_type.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:intl/intl.dart'; +import 'package:mobx/mobx.dart'; + +part 'buy_sell_view_model.g.dart'; + +class BuySellViewModel = BuySellViewModelBase with _$BuySellViewModel; + +abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with Store { + BuySellViewModelBase( + AppStore appStore, + ) : _cryptoNumberFormat = NumberFormat(), + cryptoAmount = '', + fiatAmount = '', + cryptoCurrencyAddress = '', + isCryptoCurrencyAddressEnabled = false, + cryptoCurrencies = [], + fiatCurrencies = [], + paymentMethodState = InitialPaymentMethod(), + buySellQuotState = InitialBuySellQuotState(), + cryptoCurrency = appStore.wallet!.currency, + fiatCurrency = appStore.settingsStore.fiatCurrency, + providerList = [], + sortedRecommendedQuotes = ObservableList(), + sortedQuotes = ObservableList(), + paymentMethods = ObservableList(), + settingsStore = appStore.settingsStore, + super(appStore: appStore) { + const excludeFiatCurrencies = []; + const excludeCryptoCurrencies = []; + + fiatCurrencies = + FiatCurrency.all.where((currency) => !excludeFiatCurrencies.contains(currency)).toList(); + cryptoCurrencies = CryptoCurrency.all + .where((currency) => !excludeCryptoCurrencies.contains(currency)) + .toList(); + _initialize(); + + isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency); + } + + final NumberFormat _cryptoNumberFormat; + late Timer bestRateSync; + + List get availableBuyProviders { + final providerTypes = ProvidersHelper.getAvailableBuyProviderTypes( + walletTypeForCurrency(cryptoCurrency) ?? wallet.type); + return providerTypes + .map((type) => ProvidersHelper.getProviderByType(type)) + .where((provider) => provider != null) + .cast() + .toList(); + } + + List get availableSellProviders { + final providerTypes = ProvidersHelper.getAvailableSellProviderTypes( + walletTypeForCurrency(cryptoCurrency) ?? wallet.type); + return providerTypes + .map((type) => ProvidersHelper.getProviderByType(type)) + .where((provider) => provider != null) + .cast() + .toList(); + } + + @override + void onWalletChange(wallet) { + cryptoCurrency = wallet.currency; + } + + bool get isDarkTheme => settingsStore.currentTheme.type == ThemeType.dark; + + double get amount { + final formattedFiatAmount = double.tryParse(fiatAmount) ?? 200.0; + final formattedCryptoAmount = + double.tryParse(cryptoAmount) ?? (cryptoCurrency == CryptoCurrency.btc ? 0.001 : 1); + + return isBuyAction ? formattedFiatAmount : formattedCryptoAmount; + } + + SettingsStore settingsStore; + + Quote? bestRateQuote; + + Quote? selectedQuote; + + @observable + List cryptoCurrencies; + + @observable + List fiatCurrencies; + + @observable + bool isBuyAction = true; + + @observable + List providerList; + + @observable + ObservableList sortedRecommendedQuotes; + + @observable + ObservableList sortedQuotes; + + @observable + ObservableList paymentMethods; + + @observable + FiatCurrency fiatCurrency; + + @observable + CryptoCurrency cryptoCurrency; + + @observable + String cryptoAmount; + + @observable + String fiatAmount; + + @observable + String cryptoCurrencyAddress; + + @observable + bool isCryptoCurrencyAddressEnabled; + + @observable + PaymentMethod? selectedPaymentMethod; + + @observable + PaymentMethodLoadingState paymentMethodState; + + @observable + BuySellQuotLoadingState buySellQuotState; + + @computed + bool get isReadyToTrade { + final hasSelectedQuote = selectedQuote != null; + final hasSelectedPaymentMethod = selectedPaymentMethod != null; + final isPaymentMethodLoaded = paymentMethodState is PaymentMethodLoaded; + final isBuySellQuotLoaded = buySellQuotState is BuySellQuotLoaded; + + return hasSelectedQuote && + hasSelectedPaymentMethod && + isPaymentMethodLoaded && + isBuySellQuotLoaded; + } + + @action + void reset() { + cryptoCurrency = wallet.currency; + fiatCurrency = settingsStore.fiatCurrency; + isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency); + _initialize(); + } + + @action + void changeBuySellAction() { + isBuyAction = !isBuyAction; + _initialize(); + } + + @action + void changeFiatCurrency({required FiatCurrency currency}) { + fiatCurrency = currency; + _onPairChange(); + } + + @action + void changeCryptoCurrency({required CryptoCurrency currency}) { + cryptoCurrency = currency; + _onPairChange(); + isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency); + } + + @action + void changeCryptoCurrencyAddress(String address) => cryptoCurrencyAddress = address; + + @action + Future changeFiatAmount({required String amount}) async { + fiatAmount = amount; + + if (amount.isEmpty) { + fiatAmount = ''; + cryptoAmount = ''; + return; + } + + final enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0; + + if (!isReadyToTrade) { + cryptoAmount = S.current.fetching; + return; + } + + if (bestRateQuote != null) { + _cryptoNumberFormat.maximumFractionDigits = cryptoCurrency.decimals; + cryptoAmount = _cryptoNumberFormat + .format(enteredAmount / bestRateQuote!.rate) + .toString() + .replaceAll(RegExp('\\,'), ''); + } else { + await calculateBestRate(); + } + } + + @action + Future changeCryptoAmount({required String amount}) async { + cryptoAmount = amount; + + if (amount.isEmpty) { + fiatAmount = ''; + cryptoAmount = ''; + return; + } + + final enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0; + + if (!isReadyToTrade) { + fiatAmount = S.current.fetching; + } + + if (bestRateQuote != null) { + fiatAmount = _cryptoNumberFormat + .format(enteredAmount * bestRateQuote!.rate) + .toString() + .replaceAll(RegExp('\\,'), ''); + } else { + await calculateBestRate(); + } + } + + @action + void changeOption(SelectableOption option) { + if (option is Quote) { + sortedRecommendedQuotes.forEach((element) => element.setIsSelected = false); + sortedQuotes.forEach((element) => element.setIsSelected = false); + option.setIsSelected = true; + selectedQuote = option; + } else if (option is PaymentMethod) { + paymentMethods.forEach((element) => element.isSelected = false); + option.isSelected = true; + selectedPaymentMethod = option; + } else { + throw ArgumentError('Unknown option type'); + } + } + + void onTapChoseProvider(BuildContext context) async { + final initialQuotes = List.from(sortedRecommendedQuotes + sortedQuotes); + await calculateBestRate(); + final newQuotes = (sortedRecommendedQuotes + sortedQuotes); + + for (var quote in newQuotes) quote.limits = null; + + final newQuoteProviders = newQuotes + .map((quote) => quote.provider.isAggregator ? quote.rampName : quote.provider.title) + .toSet(); + + final outOfLimitQuotes = initialQuotes.where((initialQuote) { + return !newQuoteProviders.contains( + initialQuote.provider.isAggregator ? initialQuote.rampName : initialQuote.provider.title); + }).map((missingQuote) { + final quote = Quote( + rate: missingQuote.rate, + feeAmount: missingQuote.feeAmount, + networkFee: missingQuote.networkFee, + transactionFee: missingQuote.transactionFee, + payout: missingQuote.payout, + rampId: missingQuote.rampId, + rampName: missingQuote.rampName, + rampIconPath: missingQuote.rampIconPath, + paymentType: missingQuote.paymentType, + quoteId: missingQuote.quoteId, + recommendations: missingQuote.recommendations, + provider: missingQuote.provider, + isBuyAction: missingQuote.isBuyAction, + limits: missingQuote.limits, + ); + quote.setFiatCurrency = missingQuote.fiatCurrency; + quote.setCryptoCurrency = missingQuote.cryptoCurrency; + return quote; + }).toList(); + + final updatedQuoteOptions = List.from([ + OptionTitle(title: 'Recommended'), + ...sortedRecommendedQuotes, + if (sortedQuotes.isNotEmpty) OptionTitle(title: 'All Providers'), + ...sortedQuotes, + if (outOfLimitQuotes.isNotEmpty) OptionTitle(title: 'Out of Limits'), + ...outOfLimitQuotes, + ]); + + await Navigator.of(context).pushNamed( + Routes.buyOptionsPage, + arguments: [ + updatedQuoteOptions, + changeOption, + launchTrade, + ], + ).then((value) => calculateBestRate()); + } + + void _onPairChange() { + _initialize(); + } + + void _setProviders() => + providerList = isBuyAction ? availableBuyProviders : availableSellProviders; + + Future _initialize() async { + _setProviders(); + cryptoAmount = ''; + fiatAmount = ''; + cryptoCurrencyAddress = _getInitialCryptoCurrencyAddress(); + paymentMethodState = InitialPaymentMethod(); + buySellQuotState = InitialBuySellQuotState(); + await _getAvailablePaymentTypes(); + await calculateBestRate(); + } + + String _getInitialCryptoCurrencyAddress() { + return cryptoCurrency == wallet.currency ? wallet.walletAddresses.address : ''; + } + + @action + Future _getAvailablePaymentTypes() async { + paymentMethodState = PaymentMethodLoading(); + selectedPaymentMethod = null; + final result = await Future.wait(providerList.map((element) => element + .getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency.title, isBuyAction) + .timeout( + Duration(seconds: 10), + onTimeout: () => [], + ))); + + final Map uniquePaymentMethods = {}; + for (var methods in result) { + for (var method in methods) { + uniquePaymentMethods[method.paymentMethodType] = method; + } + } + + paymentMethods = ObservableList.of(uniquePaymentMethods.values); + if (paymentMethods.isNotEmpty) { + paymentMethods.insert(0, PaymentMethod.all()); + selectedPaymentMethod = paymentMethods.first; + selectedPaymentMethod!.isSelected = true; + paymentMethodState = PaymentMethodLoaded(); + } else { + paymentMethodState = PaymentMethodFailed(); + } + } + + @action + Future calculateBestRate() async { + buySellQuotState = BuySellQuotLoading(); + + final result = await Future.wait?>(providerList.map((element) => element + .fetchQuote( + cryptoCurrency: cryptoCurrency, + fiatCurrency: fiatCurrency, + amount: amount, + paymentType: selectedPaymentMethod?.paymentMethodType, + isBuyAction: isBuyAction, + walletAddress: wallet.walletAddresses.address, + ) + .timeout( + Duration(seconds: 10), + onTimeout: () => null, + ))); + + sortedRecommendedQuotes.clear(); + sortedQuotes.clear(); + + final validQuotes = result + .where((element) => element != null && element.isNotEmpty) + .expand((element) => element!) + .toList(); + + if (validQuotes.isEmpty) { + buySellQuotState = BuySellQuotFailed(); + return; + } + + validQuotes.sort((a, b) => a.rate.compareTo(b.rate)); + + final Set addedProviders = {}; + final List uniqueProviderQuotes = validQuotes.where((element) { + if (addedProviders.contains(element.provider.title)) return false; + addedProviders.add(element.provider.title); + return true; + }).toList(); + + sortedRecommendedQuotes.addAll(uniqueProviderQuotes); + + sortedQuotes = ObservableList.of( + validQuotes.where((element) => !uniqueProviderQuotes.contains(element)).toList()); + + if (sortedRecommendedQuotes.isNotEmpty) { + sortedRecommendedQuotes.first + ..setIsBestRate = true + ..recommendations.insert(0, ProviderRecommendation.bestRate); + bestRateQuote = sortedRecommendedQuotes.first; + + sortedRecommendedQuotes.sort((a, b) { + if (a.provider is OnRamperBuyProvider) return -1; + if (b.provider is OnRamperBuyProvider) return 1; + return 0; + }); + + selectedQuote = sortedRecommendedQuotes.first; + sortedRecommendedQuotes.first.setIsSelected = true; + } + + buySellQuotState = BuySellQuotLoaded(); + } + + @action + Future launchTrade(BuildContext context) async { + final provider = selectedQuote!.provider; + await provider.launchProvider( + context: context, + quote: selectedQuote!, + amount: amount, + isBuyAction: isBuyAction, + cryptoCurrencyAddress: cryptoCurrencyAddress, + ); + } +} diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 934cbdee7..a78012ae9 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -71,8 +71,7 @@ abstract class DashboardViewModelBase with Store { required this.anonpayTransactionsStore, required this.sharedPreferences, required this.keyService}) - : hasSellAction = false, - hasBuyAction = false, + : hasTradeAction = false, hasExchangeAction = false, isShowFirstYatIntroduction = false, isShowSecondYatIntroduction = false, @@ -521,37 +520,8 @@ abstract class DashboardViewModelBase with Store { Map> filterItems; - BuyProvider? get defaultBuyProvider => ProvidersHelper.getProviderByType( - settingsStore.defaultBuyProviders[wallet.type] ?? ProviderType.askEachTime); - - BuyProvider? get defaultSellProvider => ProvidersHelper.getProviderByType( - settingsStore.defaultSellProviders[wallet.type] ?? ProviderType.askEachTime); - bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled; - List get availableBuyProviders { - final providerTypes = ProvidersHelper.getAvailableBuyProviderTypes(wallet.type); - return providerTypes - .map((type) => ProvidersHelper.getProviderByType(type)) - .where((provider) => provider != null) - .cast() - .toList(); - } - - bool get hasBuyProviders => ProvidersHelper.getAvailableBuyProviderTypes(wallet.type).isNotEmpty; - - List get availableSellProviders { - final providerTypes = ProvidersHelper.getAvailableSellProviderTypes(wallet.type); - return providerTypes - .map((type) => ProvidersHelper.getProviderByType(type)) - .where((provider) => provider != null) - .cast() - .toList(); - } - - bool get hasSellProviders => - ProvidersHelper.getAvailableSellProviderTypes(wallet.type).isNotEmpty; - bool get shouldShowYatPopup => settingsStore.shouldShowYatPopup; @action @@ -564,16 +534,10 @@ abstract class DashboardViewModelBase with Store { bool hasExchangeAction; @computed - bool get isEnabledBuyAction => !settingsStore.disableBuy && hasBuyProviders; + bool get isEnabledTradeAction => !settingsStore.disableTradeOption; @observable - bool hasBuyAction; - - @computed - bool get isEnabledSellAction => !settingsStore.disableSell && hasSellProviders; - - @observable - bool hasSellAction; + bool hasTradeAction; @computed bool get isEnabledBulletinAction => !settingsStore.disableBulletin; @@ -776,8 +740,7 @@ abstract class DashboardViewModelBase with Store { void updateActions() { hasExchangeAction = !isHaven; - hasBuyAction = !isHaven; - hasSellAction = !isHaven; + hasTradeAction = !isHaven; } @computed diff --git a/lib/view_model/settings/other_settings_view_model.dart b/lib/view_model/settings/other_settings_view_model.dart index 9af8c67cf..3036e8ae9 100644 --- a/lib/view_model/settings/other_settings_view_model.dart +++ b/lib/view_model/settings/other_settings_view_model.dart @@ -65,29 +65,6 @@ abstract class OtherSettingsViewModelBase with Store { _wallet.type == WalletType.solana || _wallet.type == WalletType.tron); - @computed - bool get isEnabledBuyAction => - !_settingsStore.disableBuy && _wallet.type != WalletType.haven; - - @computed - bool get isEnabledSellAction => - !_settingsStore.disableSell && _wallet.type != WalletType.haven; - - List get availableBuyProvidersTypes { - return ProvidersHelper.getAvailableBuyProviderTypes(walletType); - } - - List get availableSellProvidersTypes => - ProvidersHelper.getAvailableSellProviderTypes(walletType); - - ProviderType get buyProviderType => - _settingsStore.defaultBuyProviders[walletType] ?? - ProviderType.askEachTime; - - ProviderType get sellProviderType => - _settingsStore.defaultSellProviders[walletType] ?? - ProviderType.askEachTime; - String getDisplayPriority(dynamic priority) { final _priority = priority as TransactionPriority; @@ -115,20 +92,6 @@ abstract class OtherSettingsViewModelBase with Store { return priority.toString(); } - String getBuyProviderType(dynamic buyProviderType) { - final _buyProviderType = buyProviderType as ProviderType; - return _buyProviderType == ProviderType.askEachTime - ? S.current.ask_each_time - : _buyProviderType.title; - } - - String getSellProviderType(dynamic sellProviderType) { - final _sellProviderType = sellProviderType as ProviderType; - return _sellProviderType == ProviderType.askEachTime - ? S.current.ask_each_time - : _sellProviderType.title; - } - void onDisplayPrioritySelected(TransactionPriority priority) => _settingsStore.priority[walletType] = priority; @@ -157,12 +120,4 @@ abstract class OtherSettingsViewModelBase with Store { } return null; } - - @action - ProviderType onBuyProviderTypeSelected(ProviderType buyProviderType) => - _settingsStore.defaultBuyProviders[walletType] = buyProviderType; - - @action - ProviderType onSellProviderTypeSelected(ProviderType sellProviderType) => - _settingsStore.defaultSellProviders[walletType] = sellProviderType; } diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index c1e0fb1ce..eaa9f9e84 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -59,10 +59,7 @@ abstract class PrivacySettingsViewModelBase with Store { bool get isAppSecure => _settingsStore.isAppSecure; @computed - bool get disableBuy => _settingsStore.disableBuy; - - @computed - bool get disableSell => _settingsStore.disableSell; + bool get disableTradeOption => _settingsStore.disableTradeOption; @computed bool get disableBulletin => _settingsStore.disableBulletin; @@ -119,10 +116,7 @@ abstract class PrivacySettingsViewModelBase with Store { void setIsAppSecure(bool value) => _settingsStore.isAppSecure = value; @action - void setDisableBuy(bool value) => _settingsStore.disableBuy = value; - - @action - void setDisableSell(bool value) => _settingsStore.disableSell = value; + void setDisableTradeOption(bool value) => _settingsStore.disableTradeOption = value; @action void setDisableBulletin(bool value) => _settingsStore.disableBulletin = value; diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index fc0ea8ea9..9ebab6b6f 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -123,6 +123,8 @@ "change_rep_successful": "تم تغيير ممثل بنجاح", "change_wallet_alert_content": "هل تريد تغيير المحفظة الحالية إلى ${wallet_name}؟", "change_wallet_alert_title": "تغيير المحفظة الحالية", + "choose_a_payment_method": "اختر طريقة الدفع", + "choose_a_provider": "اختر مزودًا", "choose_account": "اختر حساب", "choose_address": "\n\nالرجاء اختيار عنوان:", "choose_card_value": "اختر قيمة بطاقة", @@ -217,6 +219,7 @@ "disable_fee_api_warning": "من خلال إيقاف تشغيل هذا ، قد تكون معدلات الرسوم غير دقيقة في بعض الحالات ، لذلك قد ينتهي بك الأمر إلى دفع مبالغ زائدة أو دفع رسوم المعاملات الخاصة بك", "disable_fiat": "تعطيل fiat", "disable_sell": "قم بتعطيل إجراء البيع", + "disable_trade_option": "تعطيل خيار التجارة", "disableBatteryOptimization": "تعطيل تحسين البطارية", "disableBatteryOptimizationDescription": "هل تريد تعطيل تحسين البطارية من أجل جعل الخلفية مزامنة تعمل بحرية وسلاسة؟", "disabled": "معطلة", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index e66a40b36..91256938d 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Успешно промени представител", "change_wallet_alert_content": "Искате ли да смените сегашния портфейл на ${wallet_name}?", "change_wallet_alert_title": "Смяна на сегашния портфейл", + "choose_a_payment_method": "Изберете начин на плащане", + "choose_a_provider": "Изберете доставчик", "choose_account": "Избиране на профил", "choose_address": "\n\nМоля, изберете адреса:", "choose_card_value": "Изберете стойност на картата", @@ -217,6 +219,7 @@ "disable_fee_api_warning": "Като изключите това, таксите могат да бъдат неточни в някои случаи, така че може да се препланите или да не плащате таксите за вашите транзакции", "disable_fiat": "Деактивиране на fiat", "disable_sell": "Деактивирайте действието за продажба", + "disable_trade_option": "Деактивирайте опцията за търговия", "disableBatteryOptimization": "Деактивирайте оптимизацията на батерията", "disableBatteryOptimizationDescription": "Искате ли да деактивирате оптимизацията на батерията, за да направите синхронизирането на фона да работи по -свободно и гладко?", "disabled": "Деактивирано", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 4911030b2..0fe38166c 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Úspěšně změnil zástupce", "change_wallet_alert_content": "Opravdu chcete změnit aktivní peněženku na ${wallet_name}?", "change_wallet_alert_title": "Přepnout peněženku", + "choose_a_payment_method": "Vyberte metodu platby", + "choose_a_provider": "Vyberte poskytovatele", "choose_account": "Zvolte částku", "choose_address": "\n\nProsím vyberte adresu:", "choose_card_value": "Vyberte hodnotu karty", @@ -217,6 +219,7 @@ "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", + "disable_trade_option": "Zakázat možnost TRADE", "disableBatteryOptimization": "Zakázat optimalizaci baterie", "disableBatteryOptimizationDescription": "Chcete deaktivovat optimalizaci baterie, aby se synchronizovala pozadí volně a hladce?", "disabled": "Zakázáno", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index a69174dd6..212ce05f7 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Vertreter erfolgreich gerändert", "change_wallet_alert_content": "Möchten Sie die aktuelle Wallet zu ${wallet_name} ändern?", "change_wallet_alert_title": "Aktuelle Wallet ändern", + "choose_a_payment_method": "Wählen Sie eine Zahlungsmethode", + "choose_a_provider": "Wählen Sie einen Anbieter", "choose_account": "Konto auswählen", "choose_address": "\n\nBitte wählen Sie die Adresse:", "choose_card_value": "Wählen Sie einen Kartenwert", @@ -217,6 +219,7 @@ "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", + "disable_trade_option": "Handelsoption deaktivieren", "disableBatteryOptimization": "Batterieoptimierung deaktivieren", "disableBatteryOptimizationDescription": "Möchten Sie die Batterieoptimierung deaktivieren, um die Hintergrundsynchronisierung reibungsloser zu gestalten?", "disabled": "Deaktiviert", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 322e38b85..15e0c04b3 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Successfully changed representative", "change_wallet_alert_content": "Do you want to change current wallet to ${wallet_name}?", "change_wallet_alert_title": "Change current wallet", + "choose_a_payment_method": "Choose a payment method", + "choose_a_provider": "Choose a provider", "choose_account": "Choose account", "choose_address": "\n\nPlease choose the address:", "choose_card_value": "Choose a card value", @@ -217,6 +219,7 @@ "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", + "disable_trade_option": "Disable trade option", "disableBatteryOptimization": "Disable Battery Optimization", "disableBatteryOptimizationDescription": "Do you want to disable battery optimization in order to make background sync run more freely and smoothly?", "disabled": "Disabled", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 5a8ff0582..83c0a09f0 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Representante cambiado con éxito", "change_wallet_alert_content": "¿Quieres cambiar la billetera actual a ${wallet_name}?", "change_wallet_alert_title": "Cambiar billetera actual", + "choose_a_payment_method": "Elija un método de pago", + "choose_a_provider": "Elija un proveedor", "choose_account": "Elegir cuenta", "choose_address": "\n\nPor favor elija la dirección:", "choose_card_value": "Elige un valor de tarjeta", @@ -217,6 +219,7 @@ "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", + "disable_trade_option": "Deshabilitar la opción de comercio", "disableBatteryOptimization": "Deshabilitar la optimización de la batería", "disableBatteryOptimizationDescription": "¿Desea deshabilitar la optimización de la batería para que la sincronización de fondo se ejecute más libremente y sin problemas?", "disabled": "Desactivado", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 6abe72681..549ec5275 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Représentant changé avec succès", "change_wallet_alert_content": "Souhaitez-vous changer le portefeuille (wallet) actuel vers ${wallet_name} ?", "change_wallet_alert_title": "Changer le portefeuille (wallet) actuel", + "choose_a_payment_method": "Choisissez un mode de paiement", + "choose_a_provider": "Choisissez un fournisseur", "choose_account": "Choisir le compte", "choose_address": "\n\nMerci de choisir l'adresse :", "choose_card_value": "Choisissez une valeur de carte", @@ -217,6 +219,7 @@ "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", + "disable_trade_option": "Désactiver l'option de commerce", "disableBatteryOptimization": "Désactiver l'optimisation de la batterie", "disableBatteryOptimizationDescription": "Voulez-vous désactiver l'optimisation de la batterie afin de faire fonctionner la synchronisation d'arrière-plan plus librement et en douceur?", "disabled": "Désactivé", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 5a53d6795..222a9cf2f 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -123,6 +123,8 @@ "change_rep_successful": "An samu nasarar canzawa wakilin", "change_wallet_alert_content": "Kana so ka canja walat yanzu zuwa ${wallet_name}?", "change_wallet_alert_title": "Canja walat yanzu", + "choose_a_payment_method": "Zabi hanyar biyan kuɗi", + "choose_a_provider": "Zabi mai bada", "choose_account": "Zaɓi asusu", "choose_address": "\n\n Da fatan za a zaɓi adireshin:", "choose_card_value": "Zabi darajar katin", @@ -217,6 +219,7 @@ "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", + "disable_trade_option": "Musaki zaɓi na kasuwanci", "disableBatteryOptimization": "Kashe ingantawa baturi", "disableBatteryOptimizationDescription": "Shin kana son kashe ingantawa baturi don yin setnc bankwali gudu da yar kyauta da kyau?", "disabled": "tsaya", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index d130c2b5f..1b2f3a1b3 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -123,6 +123,8 @@ "change_rep_successful": "सफलतापूर्वक बदलकर प्रतिनिधि", "change_wallet_alert_content": "क्या आप करंट वॉलेट को बदलना चाहते हैं ${wallet_name}?", "change_wallet_alert_title": "वर्तमान बटुआ बदलें", + "choose_a_payment_method": "एक भुगतान विधि का चयन करें", + "choose_a_provider": "एक प्रदाता चुनें", "choose_account": "खाता चुनें", "choose_address": "\n\nकृपया पता चुनें:", "choose_card_value": "एक कार्ड मूल्य चुनें", @@ -217,6 +219,7 @@ "disable_fee_api_warning": "इसे बंद करने से, कुछ मामलों में शुल्क दरें गलत हो सकती हैं, इसलिए आप अपने लेनदेन के लिए फीस को कम कर सकते हैं या कम कर सकते हैं", "disable_fiat": "िएट को अक्षम करें", "disable_sell": "बेचने की कार्रवाई अक्षम करें", + "disable_trade_option": "व्यापार विकल्प अक्षम करें", "disableBatteryOptimization": "बैटरी अनुकूलन अक्षम करें", "disableBatteryOptimizationDescription": "क्या आप बैकग्राउंड सिंक को अधिक स्वतंत्र और सुचारू रूप से चलाने के लिए बैटरी ऑप्टिमाइज़ेशन को अक्षम करना चाहते हैं?", "disabled": "अक्षम", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 037e6e9f2..4e106fdf6 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Uspješno promijenjena reprezentativna", "change_wallet_alert_content": "Želite li promijeniti trenutni novčanik u ${wallet_name}?", "change_wallet_alert_title": "Izmijeni trenutni novčanik", + "choose_a_payment_method": "Odaberite način plaćanja", + "choose_a_provider": "Odaberite davatelja usluga", "choose_account": "Odaberi račun", "choose_address": "\n\nOdaberite adresu:", "choose_card_value": "Odaberite vrijednost kartice", @@ -217,6 +219,7 @@ "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", + "disable_trade_option": "Onemogući trgovinsku opciju", "disableBatteryOptimization": "Onemogući optimizaciju baterije", "disableBatteryOptimizationDescription": "Želite li onemogućiti optimizaciju baterije kako bi se pozadinska sinkronizacija radila slobodnije i glatko?", "disabled": "Onemogućeno", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 04a7cddf5..40142ca5b 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Ներկայացուցչի փոփոխությունը հաջողությամբ կատարվեց", "change_wallet_alert_content": "Ցանկանում եք փոխել ընթացիկ դրամապանակը ${wallet_name}?", "change_wallet_alert_title": "Փոխել ընթացիկ դրամապանակը", + "choose_a_payment_method": "Ընտրեք վճարման եղանակ", + "choose_a_provider": "Ընտրեք մատակարար", "choose_account": "Ընտրեք հաշիվը", "choose_address": "\n\nԽնդրում ենք ընտրեք հասցեն", "choose_card_value": "Ընտրեք քարտի արժեք", @@ -217,6 +219,7 @@ "disable_fee_api_warning": "Դրանից անջատելով, վճարների տեմպերը որոշ դեպքերում կարող են անճիշտ լինել, այնպես որ դուք կարող եք վերջ տալ ձեր գործարքների համար վճարների գերավճարների կամ գերավճարների վրա", "disable_fiat": "Անջատել ֆիատ", "disable_sell": "Անջատել վաճառք գործողությունը", + "disable_trade_option": "Անջատեք առեւտրի տարբերակը", "disableBatteryOptimization": "Անջատել մարտկոցի օպտիմիզացիան", "disableBatteryOptimizationDescription": "Դուք ցանկանում եք անջատել մարտկոցի օպտիմիզացիան ֆոնային համաժամացման ավելի ազատ և հարթ ընթացքի համար?", "disabled": "Անջատված", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 231ac037d..04336b76e 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Berhasil mengubah perwakilan", "change_wallet_alert_content": "Apakah Anda ingin mengganti dompet saat ini ke ${wallet_name}?", "change_wallet_alert_title": "Ganti dompet saat ini", + "choose_a_payment_method": "Pilih metode pembayaran", + "choose_a_provider": "Pilih penyedia", "choose_account": "Pilih akun", "choose_address": "\n\nSilakan pilih alamat:", "choose_card_value": "Pilih nilai kartu", @@ -217,6 +219,7 @@ "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", + "disable_trade_option": "Nonaktifkan opsi perdagangan", "disableBatteryOptimization": "Nonaktifkan optimasi baterai", "disableBatteryOptimizationDescription": "Apakah Anda ingin menonaktifkan optimasi baterai untuk membuat sinkronisasi latar belakang berjalan lebih bebas dan lancar?", "disabled": "Dinonaktifkan", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index b06a35537..ad15ab3a9 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Rappresentante modificato con successo", "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", + "choose_a_provider": "Scegli un fornitore", "choose_account": "Scegli account", "choose_address": "\n\nSi prega di scegliere l'indirizzo:", "choose_card_value": "Scegli un valore della carta", @@ -218,6 +220,7 @@ "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_fiat": "Disabilita fiat", "disable_sell": "Disabilita l'azione di vendita", + "disable_trade_option": "Disabilita l'opzione commerciale", "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?", "disabled": "Disabilitato", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index d55c4d5a1..520a73ade 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -123,6 +123,8 @@ "change_rep_successful": "代表者の変更に成功しました", "change_wallet_alert_content": "現在のウォレットをに変更しますか ${wallet_name}?", "change_wallet_alert_title": "現在のウォレットを変更する", + "choose_a_payment_method": "支払い方法を選択します", + "choose_a_provider": "プロバイダーを選択します", "choose_account": "アカウントを選択", "choose_address": "\n\n住所を選択してください:", "choose_card_value": "カード値を選択します", @@ -217,6 +219,7 @@ "disable_fee_api_warning": "これをオフにすることで、料金金利は場合によっては不正確になる可能性があるため、取引の費用が過払いまたは不足している可能性があります", "disable_fiat": "フィアットを無効にする", "disable_sell": "販売アクションを無効にする", + "disable_trade_option": "取引オプションを無効にします", "disableBatteryOptimization": "バッテリーの最適化を無効にします", "disableBatteryOptimizationDescription": "バックグラウンドシンクをより自由かつスムーズに実行するために、バッテリーの最適化を無効にしたいですか?", "disabled": "無効", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 303527fea..1d9748866 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -123,6 +123,8 @@ "change_rep_successful": "대리인이 성공적으로 변경되었습니다", "change_wallet_alert_content": "현재 지갑을 다음으로 변경 하시겠습니까 ${wallet_name}?", "change_wallet_alert_title": "현재 지갑 변경", + "choose_a_payment_method": "결제 방법을 선택하십시오", + "choose_a_provider": "제공자를 선택하십시오", "choose_account": "계정을 선택하십시오", "choose_address": "\n\n주소를 선택하십시오:", "choose_card_value": "카드 값을 선택하십시오", @@ -217,6 +219,7 @@ "disable_fee_api_warning": "이것을 끄면 경우에 따라 수수료가 부정확 할 수 있으므로 거래 수수료를 초과 지불하거나 지불 할 수 있습니다.", "disable_fiat": "법정화폐 비활성화", "disable_sell": "판매 조치 비활성화", + "disable_trade_option": "거래 옵션 비활성화", "disableBatteryOptimization": "배터리 최적화를 비활성화합니다", "disableBatteryOptimizationDescription": "백그라운드 동기화를보다 자유롭고 매끄럽게 실행하기 위해 배터리 최적화를 비활성화하고 싶습니까?", "disabled": "장애가 있는", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 9b9b44657..e6be67060 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -123,6 +123,8 @@ "change_rep_successful": "အောင်မြင်စွာကိုယ်စားလှယ်ပြောင်းလဲသွားတယ်", "change_wallet_alert_content": "လက်ရှိပိုက်ဆံအိတ်ကို ${wallet_name} သို့ ပြောင်းလိုပါသလား။", "change_wallet_alert_title": "လက်ရှိပိုက်ဆံအိတ်ကို ပြောင်းပါ။", + "choose_a_payment_method": "ငွေပေးချေမှုနည်းလမ်းကိုရွေးချယ်ပါ", + "choose_a_provider": "ပံ့ပိုးပေးရွေးချယ်ပါ", "choose_account": "အကောင့်ကို ရွေးပါ။", "choose_address": "\n\nလိပ်စာကို ရွေးပါ-", "choose_card_value": "ကဒ်တန်ဖိုးတစ်ခုရွေးပါ", @@ -217,6 +219,7 @@ "disable_fee_api_warning": "ဤအရာကိုဖွင့်ခြင်းအားဖြင့်အချို့သောကိစ္စရပ်များတွင်အခကြေးငွေနှုန်းထားများသည်တိကျမှုရှိနိုင်သည်,", "disable_fiat": "Fiat ကိုပိတ်ပါ။", "disable_sell": "ရောင်းချခြင်းလုပ်ဆောင်ချက်ကို ပိတ်ပါ။", + "disable_trade_option": "ကုန်သွယ်ရေး option ကိုပိတ်ပါ", "disableBatteryOptimization": "ဘက်ထရီ optimization ကိုပိတ်ပါ", "disableBatteryOptimizationDescription": "နောက်ခံထပ်တူပြုခြင်းနှင့်ချောချောမွေ့မွေ့ပြုလုပ်နိုင်ရန်ဘက်ထရီ optimization ကိုသင်ပိတ်ထားလိုပါသလား။", "disabled": "မသန်စွမ်း", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 657450f61..82c3899d4 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Met succes veranderde vertegenwoordiger", "change_wallet_alert_content": "Wilt u de huidige portemonnee wijzigen in ${wallet_name}?", "change_wallet_alert_title": "Wijzig huidige portemonnee", + "choose_a_payment_method": "Kies een betaalmethode", + "choose_a_provider": "Kies een provider", "choose_account": "Kies account", "choose_address": "\n\nKies het adres:", "choose_card_value": "Kies een kaartwaarde", @@ -217,6 +219,7 @@ "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", + "disable_trade_option": "Schakel handelsoptie uit", "disableBatteryOptimization": "Schakel de batterijoptimalisatie uit", "disableBatteryOptimizationDescription": "Wilt u de optimalisatie van de batterij uitschakelen om achtergrondsynchronisatie te laten werken, vrijer en soepeler?", "disabled": "Gehandicapt", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index e47c93dcf..ed54624bf 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Pomyślnie zmienił przedstawiciela", "change_wallet_alert_content": "Czy chcesz zmienić obecny portfel na ${wallet_name}?", "change_wallet_alert_title": "Zmień obecny portfel", + "choose_a_payment_method": "Wybierz metodę płatności", + "choose_a_provider": "Wybierz dostawcę", "choose_account": "Wybierz konto", "choose_address": "\n\nWybierz adres:", "choose_card_value": "Wybierz wartość karty", @@ -217,6 +219,7 @@ "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", + "disable_trade_option": "Wyłącz opcję handlu", "disableBatteryOptimization": "Wyłącz optymalizację baterii", "disableBatteryOptimizationDescription": "Czy chcesz wyłączyć optymalizację baterii, aby synchronizacja tła działała swobodniej i płynnie?", "disabled": "Wyłączone", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index d5aa95c16..7b43b5b12 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Mudou com sucesso o representante", "change_wallet_alert_content": "Quer mudar a carteira atual para ${wallet_name}?", "change_wallet_alert_title": "Alterar carteira atual", + "choose_a_payment_method": "Escolha um método de pagamento", + "choose_a_provider": "Escolha um provedor", "choose_account": "Escolha uma conta", "choose_address": "\n\nEscolha o endereço:", "choose_card_value": "Escolha um valor de cartão", @@ -217,6 +219,7 @@ "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", + "disable_trade_option": "Desativar a opção comercial", "disableBatteryOptimization": "Desative a otimização da bateria", "disableBatteryOptimizationDescription": "Deseja desativar a otimização da bateria para fazer a sincronização de fundo funcionar de forma mais livre e suave?", "disabled": "Desabilitado", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index f999b4aaa..2795e1ffc 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Успешно изменил представитель", "change_wallet_alert_content": "Вы хотите изменить текущий кошелек на ${wallet_name}?", "change_wallet_alert_title": "Изменить текущий кошелек", + "choose_a_payment_method": "Выберите способ оплаты", + "choose_a_provider": "Выберите поставщика", "choose_account": "Выберите аккаунт", "choose_address": "\n\nПожалуйста, выберите адрес:", "choose_card_value": "Выберите значение карты", @@ -217,6 +219,7 @@ "disable_fee_api_warning": "Выключив это, в некоторых случаях ставки платы могут быть неточными, так что вы можете в конечном итоге переплачивать или недоплачивать сборы за ваши транзакции", "disable_fiat": "Отключить фиат", "disable_sell": "Отключить действие продажи", + "disable_trade_option": "Отключить возможность торговли", "disableBatteryOptimization": "Отключить оптимизацию батареи", "disableBatteryOptimizationDescription": "Вы хотите отключить оптимизацию батареи, чтобы сделать фона синхронизации более свободно и плавно?", "disabled": "Отключено", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index a35e16a97..596861646 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -123,6 +123,8 @@ "change_rep_successful": "เปลี่ยนตัวแทนสำเร็จ", "change_wallet_alert_content": "คุณต้องการเปลี่ยนกระเป๋าปัจจุบันเป็น ${wallet_name} หรือไม่?", "change_wallet_alert_title": "เปลี่ยนกระเป๋าปัจจุบัน", + "choose_a_payment_method": "เลือกวิธีการชำระเงิน", + "choose_a_provider": "เลือกผู้ให้บริการ", "choose_account": "เลือกบัญชี", "choose_address": "\n\nโปรดเลือกที่อยู่:", "choose_card_value": "เลือกค่าบัตร", @@ -217,6 +219,7 @@ "disable_fee_api_warning": "โดยการปิดสิ่งนี้อัตราค่าธรรมเนียมอาจไม่ถูกต้องในบางกรณีดังนั้นคุณอาจจบลงด้วยการจ่ายเงินมากเกินไปหรือจ่ายค่าธรรมเนียมสำหรับการทำธุรกรรมของคุณมากเกินไป", "disable_fiat": "ปิดใช้งานสกุลเงินตรา", "disable_sell": "ปิดการใช้งานการขาย", + "disable_trade_option": "ปิดใช้งานตัวเลือกการค้า", "disableBatteryOptimization": "ปิดใช้งานการเพิ่มประสิทธิภาพแบตเตอรี่", "disableBatteryOptimizationDescription": "คุณต้องการปิดใช้งานการเพิ่มประสิทธิภาพแบตเตอรี่เพื่อให้การซิงค์พื้นหลังทำงานได้อย่างอิสระและราบรื่นมากขึ้นหรือไม่?", "disabled": "ปิดใช้งาน", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 090bdb8a7..f4678e00d 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Matagumpay na nagbago ng representative", "change_wallet_alert_content": "Gusto mo bang palitan ang kasalukuyang wallet sa ${wallet_name}?", "change_wallet_alert_title": "Baguhin ang kasalukuyang wallet", + "choose_a_payment_method": "Pumili ng isang paraan ng pagbabayad", + "choose_a_provider": "Pumili ng isang provider", "choose_account": "Pumili ng account", "choose_address": "Mangyaring piliin ang address:", "choose_card_value": "Pumili ng isang halaga ng card", @@ -217,6 +219,7 @@ "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", + "disable_trade_option": "Huwag paganahin ang pagpipilian sa kalakalan", "disableBatteryOptimization": "Huwag Paganahin ang Pag-optimize ng Baterya", "disableBatteryOptimizationDescription": "Nais mo bang huwag paganahin ang pag-optimize ng baterya upang gawing mas malaya at maayos ang background sync?", "disabled": "Hindi pinagana", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 3a426de9e..d8b3ae3cf 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Temsilciyi başarıyla değiştirdi", "change_wallet_alert_content": "Şimdiki cüzdanı ${wallet_name} cüzdanı ile değiştirmek istediğinden emin misin?", "change_wallet_alert_title": "Şimdiki cüzdanı değiştir", + "choose_a_payment_method": "Bir Ödeme Yöntemi Seçin", + "choose_a_provider": "Bir Sağlayıcı Seçin", "choose_account": "Hesabı seç", "choose_address": "\n\nLütfen adresi seçin:", "choose_card_value": "Bir kart değeri seçin", @@ -217,6 +219,7 @@ "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", + "disable_trade_option": "Ticaret seçeneğini devre dışı bırakın", "disableBatteryOptimization": "Pil optimizasyonunu devre dışı bırakın", "disableBatteryOptimizationDescription": "Arka plan senkronizasyonunu daha özgür ve sorunsuz bir şekilde çalıştırmak için pil optimizasyonunu devre dışı bırakmak istiyor musunuz?", "disabled": "Devre dışı", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 63665875f..f27300c18 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Успішно змінив представник", "change_wallet_alert_content": "Ви хочете змінити поточний гаманець на ${wallet_name}?", "change_wallet_alert_title": "Змінити поточний гаманець", + "choose_a_payment_method": "Виберіть метод оплати", + "choose_a_provider": "Виберіть постачальника", "choose_account": "Оберіть акаунт", "choose_address": "\n\nБудь ласка, оберіть адресу:", "choose_card_value": "Виберіть значення картки", @@ -185,7 +187,7 @@ "creating_new_wallet_error": "Помилка: ${description}", "creation_date": "Дата створення", "custom": "на замовлення", - "custom_drag": "На замовлення (утримуйте та перетягується)", + "custom_drag": "На замовлення (утримуйте та перетягуйте)", "custom_redeem_amount": "Власна сума викупу", "custom_value": "Спеціальне значення", "dark_theme": "Темна", @@ -206,17 +208,18 @@ "description": "опис", "destination_tag": "Тег призначення:", "dfx_option_description": "Купуйте криптовалюту з EUR & CHF. Для роздрібних та корпоративних клієнтів у Європі", - "didnt_get_code": "Не отримуєте код?", + "didnt_get_code": "Не отримали код?", "digit_pin": "-значний PIN", "digital_and_physical_card": " цифрова та фізична передплачена дебетова картка", "disable": "Вимкнути", "disable_bulletin": "Вимкнути статус послуги", "disable_buy": "Вимкнути дію покупки", "disable_cake_2fa": "Вимкнути Cake 2FA", - "disable_exchange": "Вимкнути exchange", + "disable_exchange": "Вимкнути можливість обміну", "disable_fee_api_warning": "Вимкнувши це, ставки плати в деяких випадках можуть бути неточними, тому ви можете переплатити або недооплатити плату за свої транзакції", "disable_fiat": "Вимкнути фиат", "disable_sell": "Вимкнути дію продажу", + "disable_trade_option": "Вимкнути можливість торгівлі", "disableBatteryOptimization": "Вимкнути оптимізацію акумулятора", "disableBatteryOptimizationDescription": "Ви хочете відключити оптимізацію акумулятора, щоб зробити фонову синхронізацію більш вільно та плавно?", "disabled": "Вимкнено", @@ -226,14 +229,14 @@ "do_not_have_enough_gas_asset": "У вас недостатньо ${currency}, щоб здійснити трансакцію з поточними умовами мережі блокчейн. Вам потрібно більше ${currency}, щоб сплатити комісію мережі блокчейн, навіть якщо ви надсилаєте інший актив.", "do_not_send": "Не надсилайте", "do_not_share_warning_text": "Не діліться цим нікому, включно зі службою підтримки.\n\nВаші кошти можуть і будуть вкрадені!", - "do_not_show_me": "Не показуй мені це знову", + "do_not_show_me": "Не показувати це знову", "domain_looks_up": "Пошук доменів", "donation_link_details": "Деталі посилання для пожертв", "e_sign_consent": "Згода електронного підпису", "edit": "Редагувати", "edit_backup_password": "Змінити пароль резервної копії", "edit_node": "Редагувати вузол", - "edit_token": "Редагувати маркер", + "edit_token": "Редагувати токен", "electrum_address_disclaimer": "Ми створюємо нові адреси щоразу, коли ви використовуєте їх, але попередні адреси продовжують працювати", "email_address": "Адреса електронної пошти", "enable": "Ввімкнути", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 2a40fbe13..00f336d72 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -123,6 +123,8 @@ "change_rep_successful": "نمائندہ کو کامیابی کے ساتھ تبدیل کیا", "change_wallet_alert_content": "کیا آپ موجودہ والیٹ کو ${wallet_name} میں تبدیل کرنا چاہتے ہیں؟", "change_wallet_alert_title": "موجودہ پرس تبدیل کریں۔", + "choose_a_payment_method": "ادائیگی کا طریقہ منتخب کریں", + "choose_a_provider": "فراہم کنندہ کا انتخاب کریں", "choose_account": "اکاؤنٹ کا انتخاب کریں۔", "choose_address": "\\n\\nبراہ کرم پتہ منتخب کریں:", "choose_card_value": "کارڈ کی قیمت کا انتخاب کریں", @@ -217,6 +219,7 @@ "disable_fee_api_warning": "اس کو بند کرنے سے ، کچھ معاملات میں فیس کی شرح غلط ہوسکتی ہے ، لہذا آپ اپنے لین دین کے لئے فیسوں کو زیادہ ادائیگی یا ادائیگی ختم کرسکتے ہیں۔", "disable_fiat": "فیاٹ کو غیر فعال کریں۔", "disable_sell": "فروخت کی کارروائی کو غیر فعال کریں۔", + "disable_trade_option": "تجارت کے آپشن کو غیر فعال کریں", "disableBatteryOptimization": "بیٹری کی اصلاح کو غیر فعال کریں", "disableBatteryOptimizationDescription": "کیا آپ پس منظر کی مطابقت پذیری کو زیادہ آزادانہ اور آسانی سے چلانے کے لئے بیٹری کی اصلاح کو غیر فعال کرنا چاہتے ہیں؟", "disabled": "معذور", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 9fee9f4fc..1291b505e 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -218,6 +218,7 @@ "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", + "disable_trade_option": "Tắt tùy chọn thương mại", "disableBatteryOptimization": "Vô hiệu hóa Tối ưu hóa Pin", "disableBatteryOptimizationDescription": "Bạn có muốn vô hiệu hóa tối ưu hóa pin để đồng bộ hóa nền hoạt động mượt mà hơn không?", "disabled": "Đã vô hiệu hóa", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 1dd6eafda..c9275b018 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Ni ifijišẹ yipada aṣoju", "change_wallet_alert_content": "Ṣe ẹ fẹ́ pààrọ̀ àpamọ́wọ́ yìí sí ${wallet_name}?", "change_wallet_alert_title": "Ẹ pààrọ̀ àpamọ́wọ́ yìí", + "choose_a_payment_method": "Yan ọna isanwo kan", + "choose_a_provider": "Yan olupese", "choose_account": "Yan àkáǹtì", "choose_address": "\n\nẸ jọ̀wọ́ yan àdírẹ́sì:", "choose_card_value": "Yan iye kaadi", @@ -217,6 +219,7 @@ "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ọ", + "disable_trade_option": "Mu aṣayan iṣowo ṣiṣẹ", "disableBatteryOptimization": "Mu Ifasi batiri", "disableBatteryOptimizationDescription": "Ṣe o fẹ lati mu iṣapelo batiri si lati le ṣiṣe ayẹwo ẹhin ati laisiyonu?", "disabled": "Wọ́n tí a ti pa", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 92a540d19..e508d3c2c 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -123,6 +123,8 @@ "change_rep_successful": "成功改变了代表", "change_wallet_alert_content": "您是否想将当前钱包改为 ${wallet_name}?", "change_wallet_alert_title": "更换当前钱包", + "choose_a_payment_method": "选择付款方式", + "choose_a_provider": "选择一个提供商", "choose_account": "选择账户", "choose_address": "\n\n請選擇地址:", "choose_card_value": "选择卡值", @@ -217,6 +219,7 @@ "disable_fee_api_warning": "通过将其关闭,在某些情况下,收费率可能不准确,因此您最终可能会超额付款或支付交易费用", "disable_fiat": "禁用法令", "disable_sell": "禁用卖出操作", + "disable_trade_option": "禁用贸易选项", "disableBatteryOptimization": "禁用电池优化", "disableBatteryOptimizationDescription": "您是否要禁用电池优化以使背景同步更加自由,平稳地运行?", "disabled": "禁用", diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index 88cbbfce3..affe4017c 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -44,6 +44,8 @@ class SecretKey { SecretKey('cakePayApiKey', () => ''), SecretKey('CSRFToken', () => ''), SecretKey('authorization', () => ''), + SecretKey('meldTestApiKey', () => ''), + SecretKey('meldTestPublicKey', () => ''), SecretKey('moneroTestWalletSeeds', () => ''), SecretKey('moneroLegacyTestWalletSeeds ', () => ''), SecretKey('bitcoinTestWalletSeeds', () => ''), From 288164603723279711dd6a894d48d7645821fb36 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Sat, 9 Nov 2024 21:07:17 +0200 Subject: [PATCH 54/73] Updatable builds (#1791) * save and increment build number for each PR * cd into repo path * trial * try generating the release build without a keystore * cache keystore to maintain same signature for PR builds * weird fix * potential fix * fix key path * meh * Update cache_dependencies.yml --- .github/workflows/cache_dependencies.yml | 15 ++++++- .github/workflows/pr_test_build_android.yml | 49 ++++++++++++++++++++- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cache_dependencies.yml b/.github/workflows/cache_dependencies.yml index 0fcd5c45b..ce098b7f1 100644 --- a/.github/workflows/cache_dependencies.yml +++ b/.github/workflows/cache_dependencies.yml @@ -67,4 +67,17 @@ jobs: run: | cd /opt/android/cake_wallet/scripts/android/ source ./app_env.sh cakewallet - ./build_monero_all.sh \ No newline at end of file + ./build_monero_all.sh + + - name: Cache Keystore + id: cache-keystore + uses: actions/cache@v3 + with: + path: /opt/android/cake_wallet/android/app/key.jks + key: $STORE_PASS + + - if: ${{ steps.cache-keystore.outputs.cache-hit != 'true' }} + name: Generate KeyStore + run: | + cd /opt/android/cake_wallet/android/app + 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 $STORE_PASS -keypass $KEY_PASS diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index a12fd422f..1a67ed0ec 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -116,8 +116,15 @@ jobs: cd /opt/android/cake_wallet/scripts/android/ ./build_mwebd.sh --dont-install - - name: Generate KeyStore - if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }} + - name: Cache Keystore + id: cache-keystore + uses: actions/cache@v3 + with: + path: /opt/android/cake_wallet/android/app/key.jks + key: $STORE_PASS + + - if: ${{ steps.cache-keystore.outputs.cache-hit != 'true' }} + name: Generate KeyStore run: | cd /opt/android/cake_wallet/android/app 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 $STORE_PASS -keypass $KEY_PASS @@ -205,6 +212,36 @@ jobs: run: | echo -e "id=com.cakewallet.test_${{ env.PR_NUMBER }}\nname=${{ env.BRANCH_NAME }}" > /opt/android/cake_wallet/android/app.properties + # Step 3: Download previous build number + - name: Download previous build number + id: download-build-number + run: | + # Download the artifact if it exists + if [[ ! -f build_number.txt ]]; then + echo "1" > build_number.txt + fi + + # Step 4: Read and Increment Build Number + - name: Increment Build Number + id: increment-build-number + run: | + # Read current build number from file + BUILD_NUMBER=$(cat build_number.txt) + BUILD_NUMBER=$((BUILD_NUMBER + 1)) + echo "New build number: $BUILD_NUMBER" + + # Save the incremented build number + echo "$BUILD_NUMBER" > build_number.txt + + # Export the build number to use in later steps + echo "BUILD_NUMBER=$BUILD_NUMBER" >> $GITHUB_ENV + + # Step 5: Update pubspec.yaml with new build number + - name: Update build number + run: | + cd /opt/android/cake_wallet + sed -i "s/^version: .*/version: 1.0.$BUILD_NUMBER/" pubspec.yaml + - name: Build run: | cd /opt/android/cake_wallet @@ -235,6 +272,13 @@ jobs: with: path: /opt/android/cake_wallet/build/app/outputs/flutter-apk/test-apk/ + # Re-upload updated build number for the next run + - name: Upload updated build number + uses: actions/upload-artifact@v3 + with: + name: build_number + path: build_number.txt + - name: Send Test APK continue-on-error: true uses: adrey/slack-file-upload-action@1.0.5 @@ -245,3 +289,4 @@ jobs: title: "${{ env.BRANCH_NAME }}.apk" filename: ${{ env.BRANCH_NAME }}.apk initial_comment: ${{ github.event.head_commit.message }} + From 5301ee4584429e57667a4f5a582b3b1538eec659 Mon Sep 17 00:00:00 2001 From: Serhii Date: Sat, 9 Nov 2024 23:44:08 +0200 Subject: [PATCH 55/73] Update onramper_buy_provider.dart (#1796) --- lib/buy/onramper/onramper_buy_provider.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/buy/onramper/onramper_buy_provider.dart b/lib/buy/onramper/onramper_buy_provider.dart index ee9c9bb74..dade41d43 100644 --- a/lib/buy/onramper/onramper_buy_provider.dart +++ b/lib/buy/onramper/onramper_buy_provider.dart @@ -217,7 +217,7 @@ class OnRamperBuyProvider extends BuyProvider { 'apiKey': _apiKey, 'mode': actionType, '${prefix}defaultFiat': quote.fiatCurrency.name, - '${prefix}defaultCrypto': quote.cryptoCurrency.name, + '${prefix}defaultCrypto': defaultCrypto, '${prefix}defaultAmount': amount.toString(), if (paymentMethod != null) '${prefix}defaultPaymentMethod': paymentMethod, 'onlyOnramps': quote.rampId, From ed69dc803b70d13b83b6d5445171fe99b18e9dee Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Sun, 10 Nov 2024 00:14:38 +0200 Subject: [PATCH 56/73] fix cache key --- .github/workflows/pr_test_build_android.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 1a67ed0ec..1f2a9636e 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -82,8 +82,7 @@ jobs: path: | /opt/android/cake_wallet/cw_haven/android/.cxx /opt/android/cake_wallet/scripts/monero_c/release - /opt/android/cake_wallet/android/app/key.jks - key: android_${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh' ,'**/cache_dependencies.yml') }} + key: ${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh' ,'**/cache_dependencies.yml') }} - if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }} name: Generate Externals From 89525b189732973b7e8b32f5be5157a96d0cdab1 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Sun, 10 Nov 2024 01:16:21 +0200 Subject: [PATCH 57/73] temporarily disable key caching --- .github/workflows/pr_test_build_android.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 1f2a9636e..774404e3b 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -115,15 +115,15 @@ jobs: cd /opt/android/cake_wallet/scripts/android/ ./build_mwebd.sh --dont-install - - name: Cache Keystore - id: cache-keystore - uses: actions/cache@v3 - with: - path: /opt/android/cake_wallet/android/app/key.jks - key: $STORE_PASS - - - if: ${{ steps.cache-keystore.outputs.cache-hit != 'true' }} - name: Generate KeyStore +# - name: Cache Keystore +# id: cache-keystore +# uses: actions/cache@v3 +# with: +# path: /opt/android/cake_wallet/android/app/key.jks +# key: $STORE_PASS +# +# - if: ${{ steps.cache-keystore.outputs.cache-hit != 'true' }} + - name: Generate KeyStore run: | cd /opt/android/cake_wallet/android/app 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 $STORE_PASS -keypass $KEY_PASS From e148f645086e0c6797aab2121d8e707fa8cc18c7 Mon Sep 17 00:00:00 2001 From: Serhii Date: Mon, 11 Nov 2024 19:39:45 +0200 Subject: [PATCH 58/73] fix the UI issue with sending outputs (#1798) --- lib/view_model/send/send_view_model.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 4621e8d16..69011aa74 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -404,8 +404,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor final updatedOutputs = bitcoin!.updateOutputs(pendingTransaction!, outputs); if (outputs.length == updatedOutputs.length) { - outputs.clear(); - outputs.addAll(updatedOutputs); + outputs.replaceRange(0, outputs.length, updatedOutputs); } } From 2c37e427e9eb7e2c882fdd7577f7ab8cfa28c558 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 12 Nov 2024 04:26:09 +0100 Subject: [PATCH 59/73] Cw 660 add ledger monero (#1747) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix conflicts with main * fix for multiple wallets * Add tron to windows application configuration. * Add macOS option for description message in configure_cake_wallet.sh * Include missed monero dll for windows. * fix conflicts with main * Disable haven configuration for iOS as default. Add ability to configure cakewallet for iOS with for configuration script. Remove cw_shared configuration for cw_monero. * fix: scan fixes, add date, allow sending while scanning * add missing nano secrets file [skip ci] * ios library * don't pull prebuilds android * Add auto generation of manifest file for android project even for iOS, macOS, Windows. * feat: sync fixes, sp settings * feat: fix resyncing * store crash fix * make init async so it won't lag disable print starts * fix monero_c build issues * libstdc++ * Fix MacOS saving wallet file issue Fix Secure Storage issue (somehow) * update pubspec.lock * fix build script * Use dylib as iOS framework. Use custom path for loading of iOS framework for monero.dart. Add script for generate iOS framework for monero wallet. * fix: date from height logic, status disconnected & chain tip get * fix: params * feat: electrum migration if using cake electrum * fix nodes update versions * re-enable tron * update sp_scanner to work on iOS [skip ci] * bump monero_c hash * bump monero_c commit * bump moneroc version * bump monero_c commit * Add ability to build monero wallet lib as universal lib. Update macOS build guide. Change default arch for macOS project to . * fix: wrong socket for old electrum nodes * Fix unchecked wallet type call * get App Dir correctly in default_settings_migration.dart * handle previous issue with fetching linux documents directory [skip ci] * backup fix * fix NTFS issues * Close the wallet when the wallet gets changed * fix: double balance * feat: node domain * fix: menu name * bump monero_c commit * fix: update tip on set scanning * fix: connection switching back and forth * feat: check if node is electrs, and supports sp * chore: fix build * minor enhancements * fixes and enhancements * solve conflicts with main * Only stop wallet on rename and delete * fix: status toggle * minor enhancement * Monero.com fixes * bump monero_c commit * update sp_scanner to include windows and linux * Update macOS build guide. Change brew dependencies for build unbound locally. * fix conflicts and update macos build guide * remove build cache when on gh actions * update secure storage * free up even more storage * free up more storage * Add initial wownero * fix conflicts * fix workflow issue * build wownero * ios and windows changes * macos * complete wownero flow (app side) * add keychain group entitlement and update script for RunnerBase on macos * update secure_storage version to 8.1.0 in configure.dart * add wownero framework * update ios builds * proper path for wownero and monero * finalizing wownero * finalizing wownero * free up even more storage * revert commenting of build gradle configs * revert commenting of secrets [skip ci] * free more storage * minor fixes * link android wownero libraries * bump monero_c commit * wownero fixes * rename target * build_single.sh using clean env * bump monero_c commit * minor fix * Add wownero polyseed * fix conflicts with main * fix: wallet seed display fix: wownero not refreshing * fix: wallet seed display fix: wownero not refreshing * bump monero_c commit * minor fixes * fix: incorrectly displaying XMR instead of WOW * fix: incorrect restore height in wownero * bump monero_c commit * Add Inno Setup Script for windows exe installer * drop libc++_shared.so * fixes from comments * Fix CMake for windows * Merge latest monero dart changes [skip ci] * bump monero_c commit * add wownero to build scripts for macos [skip ci] * add 14 word seed support to wownero * UI fixes for wownero seed restore * minor fixes * reformat code to pass lints * Add debug ledger code * Add Litecoin Hardware Wallet Creation * Add Litecoin Hardware Wallet Creation * Fix Bitcoin not sending on Ledger * Fixes to sending LTC using Ledger * CW-679 Fix merge conflicts * CW-679 Fix merge conflicts * CW-679 Minor fixes * CW-679 Add derivation Path of change address * Add create Monero Wallet from Ledger * bug fix to create Monero Wallet from Ledger * ledger flutter plus refactoring * ledger flutter plus refactoring * ledger flutter plus refactoring * Ups :| * Ups :| I forgot USB * Handle BT Off * Fix Issue with A14 and USB * Small Ledger Quality of life improvements * Small Ledger Quality of life improvements * Small Ledger Quality of life improvements * Small Ledger Quality of life improvements * Small Ledger Quality of life improvements * Small Ledger Quality of life improvements * Small Ledger Quality of life improvements * Pls work * Pls work * Pls work * Pls work * Fix overpopulation * Fix ble device detection and support for Stax and Flex * clean up pubspec * clean up * MWeb merge fix * MWeb merge fix * Migrate to Ledger Flutter Plus * Add connect device page before loading the wallet (Only monero) * Add connect device page before loading the wallet (Only monero) * Fix merge error * Fix merge error * Fix merge error && Allow for wallet switching * Please compile now * Move monero/ledger.dart from monero_c to cw_monero * Upgrade ledger_flutter_plus * Add more popups if action on the device is needed. * Update monero_c dependency hash * Yay ledger monero is even more efficient and avoids memory leaks 🥳 * [skip-ci] more code * Fix Minor Bug * Fix Minor Bug * Apply requested changes * [skip ci] Apply requested changes * Minor Cleanup * Welp I'm dumb :/ * Implement requested changes * Increase ledger refresh speed * Add Monero Ledger keep connection alive * Add Monero Ledger keep connection alive --------- Co-authored-by: OmarHatem Co-authored-by: Czarek Nakamoto Co-authored-by: m Co-authored-by: Rafael Saes Co-authored-by: Matthew Fosse --- build-guide-win.md | 38 + cw_bitcoin/pubspec.lock | 8 +- .../lib/hardware/device_connection_type.dart | 1 + cw_core/lib/wallet_service.dart | 4 + cw_monero/lib/api/wallet.dart | 4 +- cw_monero/lib/api/wallet_manager.dart | 147 +- cw_monero/lib/ledger.dart | 88 + cw_monero/lib/monero_wallet.dart | 7 + cw_monero/lib/monero_wallet_service.dart | 108 +- cw_monero/pubspec.lock | 130 +- cw_monero/pubspec.yaml | 4 +- .../connection_to_node_exception.dart | 5 + cw_wownero/lib/api/structs/account_row.dart | 12 + .../lib/api/structs/coins_info_row.dart | 73 + .../lib/api/structs/subaddress_row.dart | 15 + .../lib/api/structs/transaction_info_row.dart | 41 + cw_wownero/lib/api/structs/ut8_box.dart | 8 + cw_wownero/lib/cw_wownero.dart | 8 + cw_wownero/lib/cw_wownero_method_channel.dart | 17 + .../lib/cw_wownero_platform_interface.dart | 29 + cw_wownero/lib/mywownero.dart | 1689 +++++++++++++++++ cw_wownero/pubspec.lock | 92 +- cw_wownero/pubspec.yaml | 3 +- lib/core/wallet_loading_service.dart | 14 +- lib/di.dart | 15 +- .../require_hardware_wallet_connection.dart | 25 + lib/monero/cw_monero.dart | 25 + .../on_authentication_state_change.dart | 57 +- lib/router.dart | 9 +- .../connect_device/connect_device_page.dart | 66 +- .../monero_hardware_wallet_options_page.dart | 230 +++ .../screens/wallet_list/wallet_list_page.dart | 135 +- .../dashboard/dashboard_view_model.dart | 2 +- .../hardware_wallet/ledger_view_model.dart | 3 + .../wallet_hardware_restore_view_model.dart | 15 +- .../wallet_list/wallet_list_view_model.dart | 19 +- scripts/prepare_moneroc.sh | 4 +- tool/configure.dart | 11 +- 38 files changed, 2912 insertions(+), 249 deletions(-) create mode 100644 build-guide-win.md create mode 100644 cw_monero/lib/ledger.dart create mode 100644 cw_wownero/lib/api/exceptions/connection_to_node_exception.dart create mode 100644 cw_wownero/lib/api/structs/account_row.dart create mode 100644 cw_wownero/lib/api/structs/coins_info_row.dart create mode 100644 cw_wownero/lib/api/structs/subaddress_row.dart create mode 100644 cw_wownero/lib/api/structs/transaction_info_row.dart create mode 100644 cw_wownero/lib/api/structs/ut8_box.dart create mode 100644 cw_wownero/lib/cw_wownero.dart create mode 100644 cw_wownero/lib/cw_wownero_method_channel.dart create mode 100644 cw_wownero/lib/cw_wownero_platform_interface.dart create mode 100644 cw_wownero/lib/mywownero.dart create mode 100644 lib/entities/hardware_wallet/require_hardware_wallet_connection.dart create mode 100644 lib/src/screens/connect_device/monero_hardware_wallet_options_page.dart diff --git a/build-guide-win.md b/build-guide-win.md new file mode 100644 index 000000000..6ace961af --- /dev/null +++ b/build-guide-win.md @@ -0,0 +1,38 @@ +# 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 Windwos`, `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 downlaod 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/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 0e4921a3a..8eafb2a44 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -386,10 +386,10 @@ packages: dependency: transitive description: name: flutter_web_bluetooth - sha256: "52ce64f65d7321c4bf6abfe9dac02fb888731339a5e0ad6de59fb916c20c9f02" + sha256: fcd03e2e5f82edcedcbc940f1b6a0635a50757374183254f447640886c53208e url: "https://pub.dev" source: hosted - version: "0.2.3" + version: "0.2.4" flutter_web_plugins: dependency: transitive description: flutter @@ -560,7 +560,7 @@ packages: description: path: "packages/ledger-bitcoin" ref: HEAD - resolved-ref: dbb5c4956949dc734af3fc8febdbabed89da72aa + resolved-ref: "07cd61ef76a2a017b6d5ef233396740163265457" url: "https://github.com/cake-tech/ledger-flutter-plus-plugins" source: git version: "0.0.3" @@ -577,7 +577,7 @@ packages: description: path: "packages/ledger-litecoin" ref: HEAD - resolved-ref: dbb5c4956949dc734af3fc8febdbabed89da72aa + resolved-ref: "07cd61ef76a2a017b6d5ef233396740163265457" url: "https://github.com/cake-tech/ledger-flutter-plus-plugins" source: git version: "0.0.2" diff --git a/cw_core/lib/hardware/device_connection_type.dart b/cw_core/lib/hardware/device_connection_type.dart index 9a3069552..76a501af1 100644 --- a/cw_core/lib/hardware/device_connection_type.dart +++ b/cw_core/lib/hardware/device_connection_type.dart @@ -7,6 +7,7 @@ enum DeviceConnectionType { static List supportedConnectionTypes(WalletType walletType, [bool isIOS = false]) { switch (walletType) { + case WalletType.monero: case WalletType.bitcoin: case WalletType.litecoin: case WalletType.ethereum: diff --git a/cw_core/lib/wallet_service.dart b/cw_core/lib/wallet_service.dart index d90ae30bc..85126853e 100644 --- a/cw_core/lib/wallet_service.dart +++ b/cw_core/lib/wallet_service.dart @@ -61,4 +61,8 @@ abstract class WalletService false; } diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index 8e03cff3e..928fd7ef1 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -119,7 +119,7 @@ Future setupNodeSync( daemonUsername: login ?? '', daemonPassword: password ?? ''); }); - // monero.Wallet_init3(wptr!, argv0: '', defaultLogBaseName: 'moneroc', console: true); + // monero.Wallet_init3(wptr!, argv0: '', defaultLogBaseName: 'moneroc', console: true, logPath: ''); final status = monero.Wallet_status(wptr!); @@ -330,4 +330,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 61fd6edf0..7f9dbd8fa 100644 --- a/cw_monero/lib/api/wallet_manager.dart +++ b/cw_monero/lib/api/wallet_manager.dart @@ -7,19 +7,18 @@ import 'package:cw_monero/api/exceptions/wallet_creation_exception.dart'; import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart'; import 'package:cw_monero/api/exceptions/wallet_restore_from_keys_exception.dart'; import 'package:cw_monero/api/exceptions/wallet_restore_from_seed_exception.dart'; -import 'package:cw_monero/api/wallet.dart'; import 'package:cw_monero/api/transaction_history.dart'; +import 'package:cw_monero/api/wallet.dart'; +import 'package:cw_monero/ledger.dart'; import 'package:monero/monero.dart' as monero; class MoneroCException implements Exception { final String message; MoneroCException(this.message); - + @override - String toString() { - return message; - } + String toString() => message; } void checkIfMoneroCIsFine() { @@ -43,7 +42,6 @@ void checkIfMoneroCIsFine() { throw MoneroCException("monero_c and monero.dart wrapper export list mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsExp'\ndart: '$dartCsExp'"); } } - monero.WalletManager? _wmPtr; final monero.WalletManager wmPtr = Pointer.fromAddress((() { try { @@ -60,6 +58,13 @@ final monero.WalletManager wmPtr = Pointer.fromAddress((() { return _wmPtr!.address; })()); +void createWalletPointer() { + final newWptr = monero.WalletManager_createWallet(wmPtr, + path: "", password: "", language: "", networkType: 0); + + wptr = newWptr; +} + void createWalletSync( {required String path, required String password, @@ -124,24 +129,24 @@ void restoreWalletFromKeysSync( int restoreHeight = 0}) { txhistory = null; var newWptr = (spendKey != "") - ? monero.WalletManager_createDeterministicWalletFromSpendKey( - wmPtr, - path: path, - password: password, - language: language, - spendKeyString: spendKey, - newWallet: true, // TODO(mrcyjanek): safe to remove - restoreHeight: restoreHeight) - : monero.WalletManager_createWalletFromKeys( - wmPtr, - path: path, - password: password, - restoreHeight: restoreHeight, - addressString: address, - viewKeyString: viewKey, - spendKeyString: spendKey, - nettype: 0, - ); + ? monero.WalletManager_createDeterministicWalletFromSpendKey(wmPtr, + path: path, + password: password, + language: language, + spendKeyString: spendKey, + newWallet: true, + // TODO(mrcyjanek): safe to remove + restoreHeight: restoreHeight) + : monero.WalletManager_createWalletFromKeys( + wmPtr, + path: path, + password: password, + restoreHeight: restoreHeight, + addressString: address, + viewKeyString: viewKey, + spendKeyString: spendKey, + nettype: 0, + ); final status = monero.Wallet_status(newWptr); if (status != 0) { @@ -156,7 +161,7 @@ void restoreWalletFromKeysSync( if (viewKey != viewKeyRestored && viewKey != "") { monero.WalletManager_closeWallet(wmPtr, newWptr, false); File(path).deleteSync(); - File(path+".keys").deleteSync(); + File(path + ".keys").deleteSync(); newWptr = monero.WalletManager_createWalletFromKeys( wmPtr, path: path, @@ -199,7 +204,7 @@ void restoreWalletFromSpendKeySync( // viewKeyString: '', // nettype: 0, // ); - + txhistory = null; final newWptr = monero.WalletManager_createDeterministicWalletFromSpendKey( wmPtr, @@ -230,41 +235,39 @@ void restoreWalletFromSpendKeySync( String _lastOpenedWallet = ""; -// void restoreMoneroWalletFromDevice( -// {required String path, -// required String password, -// required String deviceName, -// int nettype = 0, -// int restoreHeight = 0}) { -// -// final pathPointer = path.toNativeUtf8(); -// final passwordPointer = password.toNativeUtf8(); -// final deviceNamePointer = deviceName.toNativeUtf8(); -// final errorMessagePointer = ''.toNativeUtf8(); -// -// final isWalletRestored = restoreWalletFromDeviceNative( -// pathPointer, -// passwordPointer, -// deviceNamePointer, -// nettype, -// restoreHeight, -// errorMessagePointer) != 0; -// -// calloc.free(pathPointer); -// calloc.free(passwordPointer); -// -// storeSync(); -// -// if (!isWalletRestored) { -// throw WalletRestoreFromKeysException( -// message: convertUTF8ToString(pointer: errorMessagePointer)); -// } -// } +Future restoreWalletFromHardwareWallet( + {required String path, + required String password, + required String deviceName, + int nettype = 0, + int restoreHeight = 0}) async { + txhistory = null; + + final newWptrAddr = await Isolate.run(() { + return monero.WalletManager_createWalletFromDevice(wmPtr, + path: path, + password: password, + restoreHeight: restoreHeight, + deviceName: deviceName) + .address; + }); + final newWptr = Pointer.fromAddress(newWptrAddr); + + final status = monero.Wallet_status(newWptr); + + if (status != 0) { + final error = monero.Wallet_errorString(newWptr); + throw WalletRestoreFromSeedException(message: error); + } + wptr = newWptr; + + openedWalletsByPath[path] = wptr!; +} Map openedWalletsByPath = {}; -void loadWallet( - {required String path, required String password, int nettype = 0}) { +Future loadWallet( + {required String path, required String password, int nettype = 0}) async { if (openedWalletsByPath[path] != null) { txhistory = null; wptr = openedWalletsByPath[path]!; @@ -278,8 +281,29 @@ void loadWallet( }); } txhistory = null; - final newWptr = monero.WalletManager_openWallet(wmPtr, - path: path, password: password); + + /// Get the device type + /// 0: Software Wallet + /// 1: Ledger + /// 2: Trezor + final deviceType = monero.WalletManager_queryWalletDevice(wmPtr, + keysFileName: "$path.keys", password: password, kdfRounds: 1); + + if (deviceType == 1) { + final dummyWPtr = wptr ?? + monero.WalletManager_openWallet(wmPtr, path: '', password: ''); + enableLedgerExchange(dummyWPtr, gLedger!); + } + + final addr = wmPtr.address; + final newWptrAddr = await Isolate.run(() { + return monero.WalletManager_openWallet(Pointer.fromAddress(addr), + path: path, password: password) + .address; + }); + + final newWptr = Pointer.fromAddress(newWptrAddr); + _lastOpenedWallet = path; final status = monero.Wallet_status(newWptr); if (status != 0) { @@ -287,6 +311,7 @@ void loadWallet( print(err); throw WalletOpeningException(message: err); } + wptr = newWptr; openedWalletsByPath[path] = wptr!; } @@ -351,7 +376,7 @@ Future _openWallet(Map args) async => loadWallet( bool _isWalletExist(String path) => isWalletExistSync(path: path); -void openWallet( +Future openWallet( {required String path, required String password, int nettype = 0}) async => diff --git a/cw_monero/lib/ledger.dart b/cw_monero/lib/ledger.dart new file mode 100644 index 000000000..074975df3 --- /dev/null +++ b/cw_monero/lib/ledger.dart @@ -0,0 +1,88 @@ +import 'dart:async'; +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:ffi/ffi.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus_dart.dart'; +import 'package:monero/monero.dart' as monero; +// import 'package:polyseed/polyseed.dart'; + +LedgerConnection? gLedger; + +Timer? _ledgerExchangeTimer; +Timer? _ledgerKeepAlive; + +void enableLedgerExchange(monero.wallet ptr, LedgerConnection connection) { + _ledgerExchangeTimer?.cancel(); + _ledgerExchangeTimer = Timer.periodic(Duration(milliseconds: 1), (_) async { + final ledgerRequestLength = monero.Wallet_getSendToDeviceLength(ptr); + final ledgerRequest = monero.Wallet_getSendToDevice(ptr) + .cast() + .asTypedList(ledgerRequestLength); + if (ledgerRequestLength > 0) { + _ledgerKeepAlive?.cancel(); + + final Pointer emptyPointer = malloc(0); + monero.Wallet_setDeviceSendData( + ptr, emptyPointer.cast(), 0); + malloc.free(emptyPointer); + + // print("> ${ledgerRequest.toHexString()}"); + final response = await exchange(connection, ledgerRequest); + // print("< ${response.toHexString()}"); + + final Pointer result = malloc(response.length); + for (var i = 0; i < response.length; i++) { + result.asTypedList(response.length)[i] = response[i]; + } + + monero.Wallet_setDeviceReceivedData( + ptr, result.cast(), response.length); + malloc.free(result); + keepAlive(connection); + } + }); +} + +void keepAlive(LedgerConnection connection) { + if (connection.connectionType == ConnectionType.ble) { + UniversalBle.onConnectionChange = (String deviceId, bool isConnected) { + print("[Monero] Ledger Disconnected"); + _ledgerKeepAlive?.cancel(); + }; + _ledgerKeepAlive = Timer.periodic(Duration(seconds: 10), (_) async { + try { + UniversalBle.setNotifiable( + connection.device.id, + connection.device.deviceInfo.serviceId, + connection.device.deviceInfo.notifyCharacteristicKey, + BleInputProperty.notification, + ); + } catch (_){} + }); + } +} + +void disableLedgerExchange() { + _ledgerExchangeTimer?.cancel(); + _ledgerKeepAlive?.cancel(); + gLedger?.disconnect(); + gLedger = null; +} + +Future exchange(LedgerConnection connection, Uint8List data) async => + connection.sendOperation(ExchangeOperation(data)); + +class ExchangeOperation extends LedgerRawOperation { + final Uint8List inputData; + + ExchangeOperation(this.inputData); + + @override + Future read(ByteDataReader reader) async => + reader.read(reader.remainingLength); + + @override + Future> write(ByteDataWriter writer) async => [inputData]; +} diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index bc8c3c94d..5f53b30ba 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -28,6 +28,7 @@ import 'package:cw_monero/api/wallet.dart' as monero_wallet; import 'package:cw_monero/api/wallet_manager.dart'; import 'package:cw_monero/exceptions/monero_transaction_creation_exception.dart'; import 'package:cw_monero/exceptions/monero_transaction_no_inputs_exception.dart'; +import 'package:cw_monero/ledger.dart'; import 'package:cw_monero/monero_transaction_creation_credentials.dart'; import 'package:cw_monero/monero_transaction_history.dart'; import 'package:cw_monero/monero_transaction_info.dart'; @@ -36,6 +37,7 @@ import 'package:cw_monero/monero_wallet_addresses.dart'; import 'package:cw_monero/pending_monero_transaction.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:mobx/mobx.dart'; import 'package:monero/monero.dart' as monero; @@ -828,4 +830,9 @@ abstract class MoneroWalletBase extends WalletBase { + MoneroRestoreWalletFromHardwareCredentials> { MoneroWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); final Box walletInfoSource; @@ -81,7 +92,7 @@ class MoneroWalletService extends WalletService< final lang = PolyseedLang.getByEnglishName(credentials.language); final heightOverride = - getMoneroHeigthByDate(date: DateTime.now().subtract(Duration(days: 2))); + getMoneroHeigthByDate(date: DateTime.now().subtract(Duration(days: 2))); return _restoreFromPolyseed( path, credentials.password!, polyseed, credentials.walletInfo!, lang, @@ -91,9 +102,9 @@ class MoneroWalletService extends WalletService< await monero_wallet_manager.createWallet( path: path, password: credentials.password!, language: credentials.language); final wallet = MoneroWallet( - walletInfo: credentials.walletInfo!, - unspentCoinsInfo: unspentCoinsInfoSource, - password: credentials.password!); + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + password: credentials.password!); await wallet.init(); return wallet; @@ -128,11 +139,11 @@ class MoneroWalletService extends WalletService< await monero_wallet_manager .openWalletAsync({'path': path, 'password': password}); final walletInfo = walletInfoSource.values.firstWhere( - (info) => info.id == WalletBase.idFor(name, getType())); + (info) => info.id == WalletBase.idFor(name, getType())); final wallet = MoneroWallet( - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfoSource, - password: password); + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource, + password: password); final isValid = wallet.walletAddresses.validate(); if (!isValid) { @@ -185,10 +196,9 @@ class MoneroWalletService extends WalletService< } @override - Future rename( - String currentName, String password, String newName) async { + Future rename(String currentName, String password, String newName) async { final currentWalletInfo = walletInfoSource.values.firstWhere( - (info) => info.id == WalletBase.idFor(currentName, getType())); + (info) => info.id == WalletBase.idFor(currentName, getType())); final currentWallet = MoneroWallet( walletInfo: currentWalletInfo, unspentCoinsInfo: unspentCoinsInfoSource, @@ -218,9 +228,9 @@ class MoneroWalletService extends WalletService< viewKey: credentials.viewKey, spendKey: credentials.spendKey); final wallet = MoneroWallet( - walletInfo: credentials.walletInfo!, - unspentCoinsInfo: unspentCoinsInfoSource, - password: credentials.password!); + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + password: credentials.password!); await wallet.init(); return wallet; @@ -232,9 +242,34 @@ class MoneroWalletService extends WalletService< } @override - Future restoreFromHardwareWallet(MoneroNewWalletCredentials credentials) { - throw UnimplementedError( - "Restoring a Monero wallet from a hardware wallet is not yet supported!"); + Future restoreFromHardwareWallet( + MoneroRestoreWalletFromHardwareCredentials credentials) async { + try { + final path = await pathForWallet(name: credentials.name, type: getType()); + final password = credentials.password; + final height = credentials.height; + + if (wptr == null ) monero_wallet_manager.createWalletPointer(); + + enableLedgerExchange(wptr!, credentials.ledgerConnection); + await monero_wallet_manager.restoreWalletFromHardwareWallet( + path: path, + password: password!, + restoreHeight: height!, + deviceName: 'Ledger'); + + final wallet = MoneroWallet( + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + password: credentials.password!); + await wallet.init(); + + return wallet; + } catch (e) { + // TODO: Implement Exception for wallet list service. + print('MoneroWalletsManager Error: $e'); + rethrow; + } } @override @@ -253,9 +288,9 @@ class MoneroWalletService extends WalletService< seed: credentials.mnemonic, restoreHeight: credentials.height!); final wallet = MoneroWallet( - walletInfo: credentials.walletInfo!, - unspentCoinsInfo: unspentCoinsInfoSource, - password: credentials.password!); + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + password: credentials.password!); await wallet.init(); return wallet; @@ -283,8 +318,8 @@ class MoneroWalletService extends WalletService< } } - Future _restoreFromPolyseed( - String path, String password, Polyseed polyseed, WalletInfo walletInfo, PolyseedLang lang, + Future _restoreFromPolyseed(String path, String password, Polyseed polyseed, + WalletInfo walletInfo, PolyseedLang lang, {PolyseedCoin coin = PolyseedCoin.POLYSEED_MONERO, int? overrideHeight}) async { final height = overrideHeight ?? getMoneroHeigthByDate(date: DateTime.fromMillisecondsSinceEpoch(polyseed.birthday * 1000)); @@ -329,7 +364,9 @@ class MoneroWalletService extends WalletService< dir.listSync().forEach((f) { final file = File(f.path); - final name = f.path.split('/').last; + final name = f.path + .split('/') + .last; final newPath = newWalletDirPath + '/$name'; final newFile = File(newPath); @@ -366,4 +403,11 @@ class MoneroWalletService extends WalletService< return ''; } } + + @override + bool requireHardwareWalletConnection(String name) { + final walletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(name, getType())); + return walletInfo.isHardwareWallet; + } } diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index ee1d48df1..a6a03dce5 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: "58082b3f0dca697204dbab0ef9ff208bfaea7767ea771076af9a343488428dda" + sha256: "6b151826fcc95ff246cd219a0bf4c753ea14f4081ad71c61939becf3aba27f70" url: "https://pub.dev" source: hosted - version: "1.5.3" + version: "1.5.5" async: dependency: transitive description: @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bluez: + dependency: transitive + description: + name: bluez + sha256: "203a1924e818a9dd74af2b2c7a8f375ab8e5edf0e486bba8f90a0d8a17ed9fce" + url: "https://pub.dev" + source: hosted + version: "0.8.2" boolean_selector: dependency: transitive description: @@ -209,6 +217,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.4" + dbus: + dependency: transitive + description: + name: dbus + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + url: "https://pub.dev" + source: hosted + version: "0.7.10" encrypt: dependency: "direct main" description: @@ -229,10 +245,10 @@ packages: dependency: "direct main" description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" file: dependency: transitive description: @@ -267,6 +283,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_bluetooth: + dependency: transitive + description: + name: flutter_web_bluetooth + sha256: "52ce64f65d7321c4bf6abfe9dac02fb888731339a5e0ad6de59fb916c20c9f02" + url: "https://pub.dev" + source: hosted + version: "0.2.3" frontend_server_client: dependency: transitive description: @@ -295,18 +319,18 @@ packages: dependency: transitive description: name: hashlib - sha256: d41795742c10947930630118c6836608deeb9047cd05aee32d2baeb697afd66a + sha256: f572f2abce09fc7aee53f15927052b9732ea1053e540af8cae211111ee0b99b1 url: "https://pub.dev" source: hosted - version: "1.19.2" + version: "1.21.0" hashlib_codecs: dependency: transitive description: name: hashlib_codecs - sha256: "2b570061f5a4b378425be28a576c1e11783450355ad4345a19f606ff3d96db0f" + sha256: "8cea9ccafcfeaa7324d2ae52c61c69f7ff71f4237507a018caab31b9e416e3b1" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.0" hive: dependency: transitive description: @@ -327,10 +351,10 @@ packages: dependency: "direct main" description: name: http - sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" http_multi_server: dependency: transitive description: @@ -403,6 +427,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + ledger_flutter_plus: + dependency: "direct main" + description: + name: ledger_flutter_plus + sha256: c7b04008553193dbca7e17b430768eecc372a72b0ff3625b5e7fc5e5c8d3231b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + ledger_usb_plus: + dependency: transitive + description: + name: ledger_usb_plus + sha256: "21cc5d976cf7edb3518bd2a0c4164139cbb0817d2e4f2054707fc4edfdf9ce87" + url: "https://pub.dev" + source: hosted + version: "1.0.4" logging: dependency: transitive description: @@ -439,10 +479,10 @@ packages: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.6" mobx: dependency: "direct main" description: @@ -463,8 +503,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b" - resolved-ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b" + ref: caaf1e56b1d2a254b332fdf848926fb963af4a3b + resolved-ref: caaf1e56b1d2a254b332fdf848926fb963af4a3b url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" @@ -504,10 +544,10 @@ packages: dependency: "direct main" description: name: path_provider - sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" path_provider_android: dependency: transitive description: @@ -544,10 +584,18 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" platform: dependency: transitive description: @@ -612,6 +660,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" shelf: dependency: transitive description: @@ -737,6 +793,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + universal_ble: + dependency: transitive + description: + name: universal_ble + sha256: "0dfbd6b64bff3ad61ed7a895c232530d9614e9b01ab261a74433a43267edb7f3" + url: "https://pub.dev" + source: hosted + version: "0.12.0" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" unorm_dart: dependency: transitive description: @@ -785,22 +857,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.5" - win32: - dependency: transitive - description: - name: win32 - sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" - url: "https://pub.dev" - source: hosted - version: "5.5.0" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" yaml: dependency: transitive description: @@ -811,4 +883,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.3.0 <4.0.0" - flutter: ">=3.16.6" + flutter: ">=3.19.0" diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index cb1f5519f..6fa235ee4 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -25,9 +25,11 @@ dependencies: monero: git: url: https://github.com/mrcyjanek/monero_c - ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash + ref: caaf1e56b1d2a254b332fdf848926fb963af4a3b +# ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash path: impls/monero.dart mutex: ^3.1.0 + ledger_flutter_plus: ^1.4.1 dev_dependencies: flutter_test: diff --git a/cw_wownero/lib/api/exceptions/connection_to_node_exception.dart b/cw_wownero/lib/api/exceptions/connection_to_node_exception.dart new file mode 100644 index 000000000..483b0a174 --- /dev/null +++ b/cw_wownero/lib/api/exceptions/connection_to_node_exception.dart @@ -0,0 +1,5 @@ +class ConnectionToNodeException implements Exception { + ConnectionToNodeException({required this.message}); + + final String message; +} \ No newline at end of file diff --git a/cw_wownero/lib/api/structs/account_row.dart b/cw_wownero/lib/api/structs/account_row.dart new file mode 100644 index 000000000..aa492ee0f --- /dev/null +++ b/cw_wownero/lib/api/structs/account_row.dart @@ -0,0 +1,12 @@ +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_wownero/lib/api/structs/coins_info_row.dart b/cw_wownero/lib/api/structs/coins_info_row.dart new file mode 100644 index 000000000..ff6f6ce73 --- /dev/null +++ b/cw_wownero/lib/api/structs/coins_info_row.dart @@ -0,0 +1,73 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +class CoinsInfoRow extends Struct { + @Int64() + external int blockHeight; + + external Pointer hash; + + @Uint64() + external int internalOutputIndex; + + @Uint64() + external int globalOutputIndex; + + @Int8() + external int spent; + + @Int8() + external int frozen; + + @Uint64() + external int spentHeight; + + @Uint64() + external int amount; + + @Int8() + external int rct; + + @Int8() + external int keyImageKnown; + + @Uint64() + external int pkIndex; + + @Uint32() + external int subaddrIndex; + + @Uint32() + external int subaddrAccount; + + external Pointer address; + + external Pointer addressLabel; + + external Pointer keyImage; + + @Uint64() + external int unlockTime; + + @Int8() + external int unlocked; + + external Pointer pubKey; + + @Int8() + external int coinbase; + + external Pointer description; + + String getHash() => hash.toDartString(); + + String getAddress() => address.toDartString(); + + String getAddressLabel() => addressLabel.toDartString(); + + String getKeyImage() => keyImage.toDartString(); + + String getPubKey() => pubKey.toDartString(); + + String getDescription() => description.toDartString(); +} diff --git a/cw_wownero/lib/api/structs/subaddress_row.dart b/cw_wownero/lib/api/structs/subaddress_row.dart new file mode 100644 index 000000000..d593a793d --- /dev/null +++ b/cw_wownero/lib/api/structs/subaddress_row.dart @@ -0,0 +1,15 @@ +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_wownero/lib/api/structs/transaction_info_row.dart b/cw_wownero/lib/api/structs/transaction_info_row.dart new file mode 100644 index 000000000..bdcc64d3f --- /dev/null +++ b/cw_wownero/lib/api/structs/transaction_info_row.dart @@ -0,0 +1,41 @@ +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; + + @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(); +} diff --git a/cw_wownero/lib/api/structs/ut8_box.dart b/cw_wownero/lib/api/structs/ut8_box.dart new file mode 100644 index 000000000..53e678c88 --- /dev/null +++ b/cw_wownero/lib/api/structs/ut8_box.dart @@ -0,0 +1,8 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +class Utf8Box extends Struct { + external Pointer value; + + String getValue() => value.toDartString(); +} diff --git a/cw_wownero/lib/cw_wownero.dart b/cw_wownero/lib/cw_wownero.dart new file mode 100644 index 000000000..33a55e305 --- /dev/null +++ b/cw_wownero/lib/cw_wownero.dart @@ -0,0 +1,8 @@ + +import 'cw_wownero_platform_interface.dart'; + +class CwWownero { + Future getPlatformVersion() { + return CwWowneroPlatform.instance.getPlatformVersion(); + } +} diff --git a/cw_wownero/lib/cw_wownero_method_channel.dart b/cw_wownero/lib/cw_wownero_method_channel.dart new file mode 100644 index 000000000..d797f5f81 --- /dev/null +++ b/cw_wownero/lib/cw_wownero_method_channel.dart @@ -0,0 +1,17 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'cw_wownero_platform_interface.dart'; + +/// An implementation of [CwWowneroPlatform] that uses method channels. +class MethodChannelCwWownero extends CwWowneroPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('cw_wownero'); + + @override + Future getPlatformVersion() async { + final version = await methodChannel.invokeMethod('getPlatformVersion'); + return version; + } +} diff --git a/cw_wownero/lib/cw_wownero_platform_interface.dart b/cw_wownero/lib/cw_wownero_platform_interface.dart new file mode 100644 index 000000000..78b21592c --- /dev/null +++ b/cw_wownero/lib/cw_wownero_platform_interface.dart @@ -0,0 +1,29 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'cw_wownero_method_channel.dart'; + +abstract class CwWowneroPlatform extends PlatformInterface { + /// Constructs a CwWowneroPlatform. + CwWowneroPlatform() : super(token: _token); + + static final Object _token = Object(); + + static CwWowneroPlatform _instance = MethodChannelCwWownero(); + + /// The default instance of [CwWowneroPlatform] to use. + /// + /// Defaults to [MethodChannelCwWownero]. + static CwWowneroPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [CwWowneroPlatform] when + /// they register themselves. + static set instance(CwWowneroPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future getPlatformVersion() { + throw UnimplementedError('platformVersion() has not been implemented.'); + } +} diff --git a/cw_wownero/lib/mywownero.dart b/cw_wownero/lib/mywownero.dart new file mode 100644 index 000000000..d50e48b64 --- /dev/null +++ b/cw_wownero/lib/mywownero.dart @@ -0,0 +1,1689 @@ +const prefixLength = 3; + +String swapEndianBytes(String original) { + if (original.length != 8) { + return ''; + } + + return original[6] + + original[7] + + original[4] + + original[5] + + original[2] + + original[3] + + original[0] + + original[1]; +} + +List tructWords(List wordSet) { + final start = 0; + final end = prefixLength; + + return wordSet.map((word) => word.substring(start, end)).toList(); +} + +String mnemonicDecode(String seed) { + final n = englistWordSet.length; + var out = ''; + var wlist = seed.split(' '); + wlist.removeLast(); + + for (var i = 0; i < wlist.length; i += 3) { + final w1 = + tructWords(englistWordSet).indexOf(wlist[i].substring(0, prefixLength)); + final w2 = tructWords(englistWordSet) + .indexOf(wlist[i + 1].substring(0, prefixLength)); + final w3 = tructWords(englistWordSet) + .indexOf(wlist[i + 2].substring(0, prefixLength)); + + if (w1 == -1 || w2 == -1 || w3 == -1) { + print("invalid word in mnemonic"); + return ''; + } + + final x = w1 + n * (((n - w1) + w2) % n) + n * n * (((n - w2) + w3) % n); + + if (x % n != w1) { + print("Something went wrong when decoding your private key, please try again"); + return ''; + } + + final _res = '0000000' + x.toRadixString(16); + final start = _res.length - 8; + final end = _res.length; + final res = _res.substring(start, end); + + out += swapEndianBytes(res); + } + + return out; +} + +final englistWordSet = [ + "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_wownero/pubspec.lock b/cw_wownero/pubspec.lock index c90340800..b0347c023 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bluez: + dependency: transitive + description: + name: bluez + sha256: "203a1924e818a9dd74af2b2c7a8f375ab8e5edf0e486bba8f90a0d8a17ed9fce" + url: "https://pub.dev" + source: hosted + version: "0.8.2" boolean_selector: dependency: transitive description: @@ -209,6 +217,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.4" + dbus: + dependency: transitive + description: + name: dbus + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + url: "https://pub.dev" + source: hosted + version: "0.7.10" encrypt: dependency: "direct main" description: @@ -267,6 +283,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_bluetooth: + dependency: transitive + description: + name: flutter_web_bluetooth + sha256: "52ce64f65d7321c4bf6abfe9dac02fb888731339a5e0ad6de59fb916c20c9f02" + url: "https://pub.dev" + source: hosted + version: "0.2.3" frontend_server_client: dependency: transitive description: @@ -403,6 +427,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + ledger_flutter_plus: + dependency: transitive + description: + name: ledger_flutter_plus + sha256: ea3ed586e1697776dacf42ac979095f1ca3bd143bf007cbe5c78e09cb6943f42 + url: "https://pub.dev" + source: hosted + version: "1.2.5" + ledger_usb_plus: + dependency: transitive + description: + name: ledger_usb_plus + sha256: "21cc5d976cf7edb3518bd2a0c4164139cbb0817d2e4f2054707fc4edfdf9ce87" + url: "https://pub.dev" + source: hosted + version: "1.0.4" logging: dependency: transitive description: @@ -463,8 +503,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b" - resolved-ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b" + ref: caaf1e56b1d2a254b332fdf848926fb963af4a3b + resolved-ref: caaf1e56b1d2a254b332fdf848926fb963af4a3b url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" @@ -540,6 +580,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" platform: dependency: transitive description: @@ -552,10 +600,10 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.8" pointycastle: dependency: transitive description: @@ -596,6 +644,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" shelf: dependency: transitive description: @@ -721,6 +777,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + universal_ble: + dependency: transitive + description: + name: universal_ble + sha256: "0dfbd6b64bff3ad61ed7a895c232530d9614e9b01ab261a74433a43267edb7f3" + url: "https://pub.dev" + source: hosted + version: "0.12.0" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" unorm_dart: dependency: transitive description: @@ -777,6 +849,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" yaml: dependency: transitive description: @@ -786,5 +866,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=3.2.0-0 <4.0.0" - flutter: ">=3.7.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/cw_wownero/pubspec.yaml b/cw_wownero/pubspec.yaml index 6943e60c3..582c15447 100644 --- a/cw_wownero/pubspec.yaml +++ b/cw_wownero/pubspec.yaml @@ -25,7 +25,8 @@ dependencies: monero: git: url: https://github.com/mrcyjanek/monero_c - ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash + ref: caaf1e56b1d2a254b332fdf848926fb963af4a3b +# ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash path: impls/monero.dart mutex: ^3.1.0 diff --git a/lib/core/wallet_loading_service.dart b/lib/core/wallet_loading_service.dart index e58e14652..10e438e0c 100644 --- a/lib/core/wallet_loading_service.dart +++ b/lib/core/wallet_loading_service.dart @@ -14,7 +14,11 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; class WalletLoadingService { - WalletLoadingService(this.sharedPreferences, this.keyService, this.walletServiceFactory); + WalletLoadingService( + this.sharedPreferences, + this.keyService, + this.walletServiceFactory, + ); final SharedPreferences sharedPreferences; final KeyService keyService; @@ -77,7 +81,8 @@ class WalletLoadingService { await updateMoneroWalletPassword(wallet); } - await sharedPreferences.setString(PreferencesKey.currentWalletName, wallet.name); + await sharedPreferences.setString( + PreferencesKey.currentWalletName, wallet.name); await sharedPreferences.setInt( PreferencesKey.currentWalletType, serializeToInt(wallet.type)); @@ -129,4 +134,9 @@ class WalletLoadingService { return "\n\n$type ($name): ${await walletService.getSeeds(name, password, type)}"; } + + bool requireHardwareWalletConnection(WalletType type, String name) { + final walletService = walletServiceFactory.call(type); + return walletService.requireHardwareWalletConnection(name); + } } diff --git a/lib/di.dart b/lib/di.dart index 8dd1029a9..facdec912 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -32,12 +32,14 @@ import 'package:cake_wallet/entities/biometric_auth.dart'; import 'package:cake_wallet/entities/contact.dart'; 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/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/receive/address_list_page.dart'; +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/view_model/link_view_model.dart'; @@ -184,7 +186,6 @@ import 'package:cake_wallet/src/screens/transaction_details/transaction_details_ import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart'; -import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/authentication_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; @@ -586,7 +587,7 @@ Future setup({ ); } else { // wallet is already loaded: - if (appStore.wallet != null) { + if (appStore.wallet != null || requireHardwareWalletConnection()) { // goes to the dashboard: authStore.allowed(); // trigger any deep links: @@ -780,10 +781,12 @@ Future setup({ ); } - getIt.registerFactory(() => WalletListPage( - walletListViewModel: getIt.get(), - authService: getIt.get(), - )); + getIt.registerFactoryParam( + (Function(BuildContext)? onWalletLoaded, _) => WalletListPage( + walletListViewModel: getIt.get(), + authService: getIt.get(), + onWalletLoaded: onWalletLoaded, + )); getIt.registerFactoryParam( (WalletListViewModel walletListViewModel, _) => WalletEditViewModel( diff --git a/lib/entities/hardware_wallet/require_hardware_wallet_connection.dart b/lib/entities/hardware_wallet/require_hardware_wallet_connection.dart new file mode 100644 index 000000000..a64fa5873 --- /dev/null +++ b/lib/entities/hardware_wallet/require_hardware_wallet_connection.dart @@ -0,0 +1,25 @@ +import 'package:cake_wallet/core/wallet_loading_service.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +bool requireHardwareWalletConnection() { + final name = getIt + .get() + .getString(PreferencesKey.currentWalletName); + final typeRaw = + getIt.get().getInt(PreferencesKey.currentWalletType); + + if (typeRaw == null) { + return false; + } + + if (name == null) { + throw Exception('Incorrect current wallet name: $name'); + } + + final type = deserializeFromInt(typeRaw); + final walletLoadingService = getIt.get(); + return walletLoadingService.requireHardwareWalletConnection(type, name); +} diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index dfda4d600..09d9889c5 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -225,6 +225,19 @@ class CWMonero extends Monero { language: language, height: height); + @override + WalletCredentials createMoneroRestoreWalletFromHardwareCredentials({ + required String name, + required String password, + required int height, + required ledger.LedgerConnection ledgerConnection, + }) => + MoneroRestoreWalletFromHardwareCredentials( + name: name, + password: password, + height: height, + ledgerConnection: ledgerConnection); + @override WalletCredentials createMoneroRestoreWalletFromSeedCredentials( {required String name, @@ -383,6 +396,18 @@ class CWMonero extends Monero { checkIfMoneroCIsFine(); } + @override + void setLedgerConnection(Object wallet, ledger.LedgerConnection connection) { + final moneroWallet = wallet as MoneroWallet; + moneroWallet.setLedgerConnection(connection); + } + + @override + void setGlobalLedgerConnection(ledger.LedgerConnection connection) { + gLedger = connection; + keepAlive(connection); + } + bool isViewOnly() { return isViewOnlyBySpendKey(); } diff --git a/lib/reactions/on_authentication_state_change.dart b/lib/reactions/on_authentication_state_change.dart index 1aa0a12c6..b411c5a15 100644 --- a/lib/reactions/on_authentication_state_change.dart +++ b/lib/reactions/on_authentication_state_change.dart @@ -1,18 +1,28 @@ import 'dart:async'; +import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/hardware_wallet/require_hardware_wallet_connection.dart'; +import 'package:cake_wallet/entities/load_current_wallet.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/utils/exception_handler.dart'; +import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/store/authentication_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/utils/exception_handler.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/widgets.dart'; import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/entities/load_current_wallet.dart'; -import 'package:cake_wallet/store/authentication_store.dart'; import 'package:rxdart/subjects.dart'; ReactionDisposer? _onAuthenticationStateChange; dynamic loginError; -StreamController authenticatedErrorStreamController = BehaviorSubject(); +StreamController authenticatedErrorStreamController = + BehaviorSubject(); void startAuthenticationStateChange( AuthenticationStore authenticationStore, @@ -27,18 +37,49 @@ void startAuthenticationStateChange( _onAuthenticationStateChange ??= autorun((_) async { final state = authenticationStore.state; - if (state == AuthenticationState.installed && !SettingsStoreBase.walletPasswordDirectInput) { + if (state == AuthenticationState.installed && + !SettingsStoreBase.walletPasswordDirectInput) { try { - await loadCurrentWallet(); + if (!requireHardwareWalletConnection()) await loadCurrentWallet(); } catch (error, stack) { loginError = error; - ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stack)); + ExceptionHandler.onError( + FlutterErrorDetails(exception: error, stack: stack)); } return; } if (state == AuthenticationState.allowed) { - await navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false); + if (requireHardwareWalletConnection()) { + await navigatorKey.currentState!.pushNamedAndRemoveUntil( + Routes.connectDevices, + (route) => false, + arguments: ConnectDevicePageParams( + walletType: WalletType.monero, + onConnectDevice: (context, ledgerVM) async { + monero!.setGlobalLedgerConnection(ledgerVM.connection); + showPopUp( + context: context, + builder: (BuildContext context) => AlertWithOneAction( + alertTitle: S.of(context).proceed_on_device, + alertContent: S.of(context).proceed_on_device_description, + buttonText: S.of(context).cancel, + buttonAction: () => Navigator.of(context).pop()), + ); + await loadCurrentWallet(); + getIt.get().resetCurrentSheet(); + await navigatorKey.currentState! + .pushNamedAndRemoveUntil(Routes.dashboard, (route) => false); + }, + allowChangeWallet: true, + ), + ); + + // await navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.connectDevices, (route) => false, arguments: ConnectDevicePageParams(walletType: walletType, onConnectDevice: onConnectDevice)); + } else { + await navigatorKey.currentState! + .pushNamedAndRemoveUntil(Routes.dashboard, (route) => false); + } if (!(await authenticatedErrorStreamController.stream.isEmpty)) { ExceptionHandler.showError( (await authenticatedErrorStreamController.stream.first).toString()); diff --git a/lib/router.dart b/lib/router.dart index 781a6e057..1382da28f 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -24,6 +24,7 @@ import 'package:cake_wallet/src/screens/buy/webview_page.dart'; import 'package:cake_wallet/src/screens/cake_pay/auth/cake_pay_account_page.dart'; import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart'; import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; +import 'package:cake_wallet/src/screens/connect_device/monero_hardware_wallet_options_page.dart'; import 'package:cake_wallet/src/screens/connect_device/select_hardware_wallet_account_page.dart'; import 'package:cake_wallet/src/screens/contact/contact_list_page.dart'; import 'package:cake_wallet/src/screens/contact/contact_page.dart'; @@ -212,6 +213,9 @@ Route createRoute(RouteSettings settings) { final type = arguments[0] as WalletType; final walletVM = getIt.get(param1: type); + if (type == WalletType.monero) + return CupertinoPageRoute(builder: (_) => MoneroHardwareWalletOptionsPage(walletVM)); + return CupertinoPageRoute(builder: (_) => SelectHardwareWalletAccountPage(walletVM)); case Routes.setupPin: @@ -403,8 +407,11 @@ Route createRoute(RouteSettings settings) { return CupertinoPageRoute(builder: (_) => getIt.get()); case Routes.walletList: + final onWalletLoaded = settings.arguments as Function(BuildContext)?; return MaterialPageRoute( - fullscreenDialog: true, builder: (_) => getIt.get()); + fullscreenDialog: true, + builder: (_) => getIt.get(param1: onWalletLoaded), + ); case Routes.walletEdit: return MaterialPageRoute( diff --git a/lib/src/screens/connect_device/connect_device_page.dart b/lib/src/screens/connect_device/connect_device_page.dart index 9e331e818..c2cc40229 100644 --- a/lib/src/screens/connect_device/connect_device_page.dart +++ b/lib/src/screens/connect_device/connect_device_page.dart @@ -2,9 +2,12 @@ import 'dart:async'; import 'dart:io'; 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/connect_device/widgets/device_tile.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/wallet_list_theme.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cw_core/wallet_type.dart'; @@ -17,35 +20,46 @@ typedef OnConnectDevice = void Function(BuildContext, LedgerViewModel); class ConnectDevicePageParams { final WalletType walletType; final OnConnectDevice onConnectDevice; + final bool allowChangeWallet; - ConnectDevicePageParams( - {required this.walletType, required this.onConnectDevice}); + ConnectDevicePageParams({ + required this.walletType, + required this.onConnectDevice, + this.allowChangeWallet = false, + }); } class ConnectDevicePage extends BasePage { final WalletType walletType; final OnConnectDevice onConnectDevice; + final bool allowChangeWallet; final LedgerViewModel ledgerVM; ConnectDevicePage(ConnectDevicePageParams params, this.ledgerVM) : walletType = params.walletType, - onConnectDevice = params.onConnectDevice; + onConnectDevice = params.onConnectDevice, + allowChangeWallet = params.allowChangeWallet; @override String get title => S.current.restore_title_from_hardware_wallet; @override - Widget body(BuildContext context) => - ConnectDevicePageBody(walletType, onConnectDevice, ledgerVM); + Widget body(BuildContext context) => ConnectDevicePageBody( + walletType, onConnectDevice, allowChangeWallet, ledgerVM); } class ConnectDevicePageBody extends StatefulWidget { final WalletType walletType; final OnConnectDevice onConnectDevice; + final bool allowChangeWallet; final LedgerViewModel ledgerVM; const ConnectDevicePageBody( - this.walletType, this.onConnectDevice, this.ledgerVM); + this.walletType, + this.onConnectDevice, + this.allowChangeWallet, + this.ledgerVM, + ); @override ConnectDevicePageBodyState createState() => ConnectDevicePageBodyState(); @@ -102,14 +116,16 @@ class ConnectDevicePageBodyState extends State { Future _refreshBleDevices() async { try { - _bleRefresh = widget.ledgerVM - .scanForBleDevices() - .listen((device) => setState(() => bleDevices.add(device))) - ..onError((e) { - throw e.toString(); - }); - _bleRefreshTimer?.cancel(); - _bleRefreshTimer = null; + if (widget.ledgerVM.bleIsEnabled) { + _bleRefresh = widget.ledgerVM + .scanForBleDevices() + .listen((device) => setState(() => bleDevices.add(device))) + ..onError((e) { + throw e.toString(); + }); + _bleRefreshTimer?.cancel(); + _bleRefreshTimer = null; + } } catch (e) { print(e); } @@ -227,9 +243,7 @@ class ConnectDevicePageBodyState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .titleColor, + color: Theme.of(context).extension()!.titleColor, ), ), ), @@ -247,11 +261,27 @@ class ConnectDevicePageBodyState extends State { ), ) .toList(), - ] + ], + if (widget.allowChangeWallet) ...[ + PrimaryButton( + text: S.of(context).wallets, + color: Theme.of(context).extension()!.createNewWalletButtonBackgroundColor, + textColor: Theme.of(context).extension()!.restoreWalletButtonTextColor, + onPressed: _onChangeWallet, + ) + ], ], ), ), ), ); } + + void _onChangeWallet() { + Navigator.of(context).pushNamed( + Routes.walletList, + arguments: (BuildContext context) => Navigator.of(context) + .pushNamedAndRemoveUntil(Routes.dashboard, (route) => false), + ); + } } diff --git a/lib/src/screens/connect_device/monero_hardware_wallet_options_page.dart b/lib/src/screens/connect_device/monero_hardware_wallet_options_page.dart new file mode 100644 index 000000000..f8ace97bc --- /dev/null +++ b/lib/src/screens/connect_device/monero_hardware_wallet_options_page.dart @@ -0,0 +1,230 @@ +import 'package:cake_wallet/core/wallet_name_validator.dart'; +import 'package:cake_wallet/entities/generate_name.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/blockchain_height_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/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/extensions/new_wallet_theme.dart'; +import 'package:cake_wallet/themes/extensions/send_page_theme.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/wallet_hardware_restore_view_model.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'; + +class MoneroHardwareWalletOptionsPage extends BasePage { + MoneroHardwareWalletOptionsPage(this._walletHardwareRestoreVM); + + final WalletHardwareRestoreViewModel _walletHardwareRestoreVM; + + @override + String get title => S.current.restore_title_from_hardware_wallet; + + @override + Widget body(BuildContext context) => + _MoneroHardwareWalletOptionsForm(_walletHardwareRestoreVM); +} + +class _MoneroHardwareWalletOptionsForm extends StatefulWidget { + const _MoneroHardwareWalletOptionsForm(this._walletHardwareRestoreVM); + + final WalletHardwareRestoreViewModel _walletHardwareRestoreVM; + + @override + _MoneroHardwareWalletOptionsFormState createState() => + _MoneroHardwareWalletOptionsFormState(_walletHardwareRestoreVM); +} + +class _MoneroHardwareWalletOptionsFormState + extends State<_MoneroHardwareWalletOptionsForm> { + _MoneroHardwareWalletOptionsFormState(this._walletHardwareRestoreVM) + : _formKey = GlobalKey(), + _blockchainHeightKey = GlobalKey(), + _blockHeightFocusNode = FocusNode(), + _controller = TextEditingController(); + + final GlobalKey _formKey; + final GlobalKey _blockchainHeightKey; + final FocusNode _blockHeightFocusNode; + final WalletHardwareRestoreViewModel _walletHardwareRestoreVM; + final TextEditingController _controller; + + @override + void initState() { + super.initState(); + _setEffects(context); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(top: 24), + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), + content: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.only(top: 0), + child: Form( + key: _formKey, + child: Stack( + alignment: Alignment.centerRight, + children: [ + TextFormField( + onChanged: (value) => + _walletHardwareRestoreVM.name = value, + controller: _controller, + style: TextStyle( + fontSize: 20.0, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .extension()! + .titleColor, + ), + decoration: InputDecoration( + hintStyle: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .extension()! + .hintTextColor, + ), + hintText: S.of(context).wallet_name, + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context) + .extension()! + .underlineColor, + width: 1.0, + ), + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context) + .extension()! + .underlineColor, + width: 1.0, + ), + ), + suffixIcon: Semantics( + label: S.of(context).generate_name, + child: IconButton( + onPressed: _onGenerateName, + icon: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6.0), + color: Theme.of(context).hintColor, + ), + width: 34, + height: 34, + child: Image.asset( + 'assets/images/refresh_icon.png', + color: Theme.of(context) + .extension()! + .textFieldButtonIconColor, + ), + ), + ), + ), + ), + validator: WalletNameValidator(), + ), + ], + ), + ), + ), + Padding( + padding: EdgeInsets.only(top: 20), + child: BlockchainHeightWidget( + focusNode: _blockHeightFocusNode, + key: _blockchainHeightKey, + hasDatePicker: true, + walletType: WalletType.monero, + ), + ), + ], + ), + ), + ), + bottomSectionPadding: EdgeInsets.all(24), + bottomSection: Observer( + builder: (context) => LoadingPrimaryButton( + onPressed: _confirmForm, + text: S.of(context).seed_language_next, + color: Colors.green, + textColor: Colors.white, + isDisabled: _walletHardwareRestoreVM.name.isEmpty, + ), + ), + ), + ); + } + + Future _onGenerateName() async { + final rName = await generateName(); + FocusManager.instance.primaryFocus?.unfocus(); + + setState(() { + _controller.text = rName; + _walletHardwareRestoreVM.name = rName; + _controller.selection = TextSelection.fromPosition( + TextPosition(offset: _controller.text.length)); + }); + } + + Future _confirmForm() async { + showPopUp( + context: context, + builder: (BuildContext context) => AlertWithOneAction( + alertTitle: S.of(context).proceed_on_device, + alertContent: S.of(context).proceed_on_device_description, + buttonText: S.of(context).cancel, + buttonAction: () => Navigator.of(context).pop(), + ), + ); + + final options = {'height': _blockchainHeightKey.currentState?.height ?? -1}; + await _walletHardwareRestoreVM.create(options: options); + } + + bool _effectsInstalled = false; + + void _setEffects(BuildContext context) { + if (_effectsInstalled) return; + + reaction((_) => _walletHardwareRestoreVM.error, (String? error) { + if (error != null) { + if (error == S.current.ledger_connection_error) + Navigator.of(context).pop(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + showPopUp( + context: context, + builder: (BuildContext context) => AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: error, + buttonText: S.of(context).ok, + buttonAction: () { + _walletHardwareRestoreVM.error = null; + Navigator.of(context).pop(); + }, + ), + ); + }); + } + }); + + _effectsInstalled = true; + } +} diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index a9a9d9413..46eaa6143 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -1,44 +1,56 @@ +import 'package:another_flushbar/flushbar.dart'; +import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/new_wallet_arguments.dart'; import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart'; import 'package:cake_wallet/entities/wallet_list_order_types.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/auth/auth_page.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/filter_list_widget.dart'; import 'package:cake_wallet/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart'; import 'package:cake_wallet/src/screens/wallet_list/edit_wallet_button_widget.dart'; import 'package:cake_wallet/src/screens/wallet_list/filtered_list.dart'; import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cake_wallet/src/screens/auth/auth_page.dart'; -import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/themes/extensions/filter_theme.dart'; import 'package:cake_wallet/themes/extensions/wallet_list_theme.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; -import 'package:another_flushbar/flushbar.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; +import 'package:cake_wallet/wallet_type_utils.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/wallet_type_utils.dart'; class WalletListPage extends BasePage { - WalletListPage({required this.walletListViewModel, required this.authService}); + WalletListPage({ + required this.walletListViewModel, + required this.authService, + this.onWalletLoaded, + }); final WalletListViewModel walletListViewModel; final AuthService authService; + final Function(BuildContext)? onWalletLoaded; @override String get title => S.current.wallets; @override - Widget body(BuildContext context) => - WalletListBody(walletListViewModel: walletListViewModel, authService: authService); + Widget body(BuildContext context) => WalletListBody( + walletListViewModel: walletListViewModel, + authService: authService, + onWalletLoaded: + onWalletLoaded ?? (context) => Navigator.of(context).pop(), + ); @override Widget trailing(BuildContext context) { @@ -89,10 +101,15 @@ class WalletListPage extends BasePage { } class WalletListBody extends StatefulWidget { - WalletListBody({required this.walletListViewModel, required this.authService}); + WalletListBody({ + required this.walletListViewModel, + required this.authService, + required this.onWalletLoaded, + }); final WalletListViewModel walletListViewModel; final AuthService authService; + final Function(BuildContext) onWalletLoaded; @override WalletListBodyState createState() => WalletListBodyState(); @@ -118,8 +135,8 @@ class WalletListBodyState extends State { @override Widget build(BuildContext context) { - final newWalletImage = - Image.asset('assets/images/new_wallet.png', height: 12, width: 12, color: Colors.white); + final newWalletImage = Image.asset('assets/images/new_wallet.png', + height: 12, width: 12, color: Colors.white); final restoreWalletImage = Image.asset('assets/images/restore_wallet.png', height: 12, width: 12, @@ -180,8 +197,7 @@ class WalletListBodyState extends State { trailingWidget: EditWalletButtonWidget( width: 74, isGroup: true, - isExpanded: - widget.walletListViewModel.expansionTileStateTrack[index]!, + isExpanded: widget.walletListViewModel.expansionTileStateTrack[index]!, onTap: () { final wallet = widget.walletListViewModel .convertWalletInfoToWalletListItem(group.wallets.first); @@ -198,8 +214,7 @@ class WalletListBodyState extends State { }, ), childWallets: group.wallets.map((walletInfo) { - return widget.walletListViewModel - .convertWalletInfoToWalletListItem(walletInfo); + return widget.walletListViewModel.convertWalletInfoToWalletListItem(walletInfo); }).toList(), isSelected: false, onChildItemTapped: (wallet) => @@ -329,8 +344,7 @@ class WalletListBodyState extends State { arguments: NewWalletArguments( type: widget.walletListViewModel.currentWalletType, ), - conditionToDetermineIfToUse2FA: - widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, + conditionToDetermineIfToUse2FA: widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, ); } else { Navigator.of(context).pushNamed( @@ -345,8 +359,7 @@ class WalletListBodyState extends State { widget.authService.authenticateAction( context, route: Routes.newWalletType, - conditionToDetermineIfToUse2FA: - widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, + conditionToDetermineIfToUse2FA: widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, ); } else { Navigator.of(context).pushNamed(Routes.newWalletType); @@ -367,8 +380,7 @@ class WalletListBodyState extends State { context, route: Routes.restoreOptions, arguments: false, - conditionToDetermineIfToUse2FA: - widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, + conditionToDetermineIfToUse2FA: widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, ); } else { Navigator.of(context).pushNamed(Routes.restoreOptions, arguments: false); @@ -387,39 +399,6 @@ class WalletListBodyState extends State { ); } - Image _imageFor({required WalletType type, bool? isTestnet}) { - switch (type) { - case WalletType.bitcoin: - if (isTestnet == true) { - return tBitcoinIcon; - } - return bitcoinIcon; - case WalletType.monero: - return moneroIcon; - case WalletType.litecoin: - return litecoinIcon; - case WalletType.haven: - return havenIcon; - case WalletType.ethereum: - return ethereumIcon; - case WalletType.bitcoinCash: - return bitcoinCashIcon; - case WalletType.nano: - case WalletType.banano: - return nanoIcon; - case WalletType.polygon: - return polygonIcon; - case WalletType.solana: - return solanaIcon; - case WalletType.tron: - return tronIcon; - case WalletType.wownero: - return wowneroIcon; - case WalletType.none: - return nonWalletTypeIcon; - } - } - Future _loadWallet(WalletListItem wallet) async { if (SettingsStoreBase.walletPasswordDirectInput) { Navigator.of(context).pushNamed(Routes.walletUnlockLoadable, @@ -438,12 +417,36 @@ class WalletListBodyState extends State { await widget.authService.authenticateAction( context, onAuthSuccess: (isAuthenticatedSuccessfully) async { - if (!isAuthenticatedSuccessfully) { - return; - } + if (!isAuthenticatedSuccessfully) return; try { - changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name)); + if (widget.walletListViewModel + .requireHardwareWalletConnection(wallet)) { + await Navigator.of(context).pushNamed( + Routes.connectDevices, + arguments: ConnectDevicePageParams( + walletType: WalletType.monero, + onConnectDevice: (context, ledgerVM) async { + monero!.setGlobalLedgerConnection(ledgerVM.connection); + Navigator.of(context).pop(); + }, + ), + ); + + showPopUp( + context: context, + builder: (BuildContext context) => AlertWithOneAction( + alertTitle: S.of(context).proceed_on_device, + alertContent: S.of(context).proceed_on_device_description, + buttonText: S.of(context).cancel, + buttonAction: () => Navigator.of(context).pop()), + ); + } + + + + changeProcessText( + S.of(context).wallet_list_loading_wallet(wallet.name)); await widget.walletListViewModel.loadWallet(wallet); await hideProgressText(); // only pop the wallets route in mobile as it will go back to dashboard page @@ -451,13 +454,15 @@ class WalletListBodyState extends State { if (responsiveLayoutUtil.shouldRenderMobileUI) { WidgetsBinding.instance.addPostFrameCallback((_) { if (this.mounted) { - Navigator.of(context).pop(); + widget.onWalletLoaded.call(context); } }); } } catch (e) { if (this.mounted) { - changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString())); + changeProcessText(S + .of(context) + .wallet_list_failed_to_load(wallet.name, e.toString())); } } }, diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index a78012ae9..808657f66 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -428,7 +428,7 @@ abstract class DashboardViewModelBase with Store { // to not cause work duplication, this will do the job as well, it will be slightly less precise // about what happened - but still enough. // if (keys['privateSpendKey'] == List.generate(64, (index) => "0").join("")) "Private spend key is 0", - if (keys['privateViewKey'] == List.generate(64, (index) => "0").join("")) + if (keys['privateViewKey'] == List.generate(64, (index) => "0").join("") && !wallet.isHardwareWallet) "private view key is 0", // if (keys['publicSpendKey'] == List.generate(64, (index) => "0").join("")) "public spend key is 0", if (keys['publicViewKey'] == List.generate(64, (index) => "0").join("")) diff --git a/lib/view_model/hardware_wallet/ledger_view_model.dart b/lib/view_model/hardware_wallet/ledger_view_model.dart index 19b190fe3..96f5930c0 100644 --- a/lib/view_model/hardware_wallet/ledger_view_model.dart +++ b/lib/view_model/hardware_wallet/ledger_view_model.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/ethereum/ethereum.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/utils/device_info.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; @@ -114,6 +115,8 @@ abstract class LedgerViewModelBase with Store { void setLedger(WalletBase wallet) { switch (wallet.type) { + case WalletType.monero: + return monero!.setLedgerConnection(wallet, connection); case WalletType.bitcoin: case WalletType.litecoin: return bitcoin!.setLedgerConnection(wallet, connection); diff --git a/lib/view_model/wallet_hardware_restore_view_model.dart b/lib/view_model/wallet_hardware_restore_view_model.dart index 91e0de685..0971622a5 100644 --- a/lib/view_model/wallet_hardware_restore_view_model.dart +++ b/lib/view_model/wallet_hardware_restore_view_model.dart @@ -1,7 +1,9 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/core/generate_wallet_password.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cake_wallet/ethereum/ethereum.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/view_model/hardware_wallet/ledger_view_model.dart'; @@ -56,8 +58,8 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with List accounts; switch (type) { case WalletType.bitcoin: - accounts = await bitcoin! - .getHardwareWalletBitcoinAccounts(ledgerViewModel, index: _nextIndex, limit: limit); + accounts = await bitcoin! + .getHardwareWalletBitcoinAccounts(ledgerViewModel, index: _nextIndex, limit: limit); break; case WalletType.litecoin: accounts = await bitcoin! @@ -104,6 +106,15 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with case WalletType.polygon: credentials = polygon!.createPolygonHardwareWalletCredentials(name: name, hwAccountData: selectedAccount!); break; + case WalletType.monero: + final password = walletPassword ?? generateWalletPassword(); + + credentials = monero!.createMoneroRestoreWalletFromHardwareCredentials( + name: name, + ledgerConnection: ledgerViewModel.connection, + password: password, + height: _options['height'] as int? ?? 0, + ); default: throw Exception('Unexpected type: ${type.toString()}'); } 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 725af843f..c903b535f 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -68,6 +68,10 @@ abstract class WalletListViewModelBase with Store { WalletType get currentWalletType => _appStore.wallet!.type; + bool requireHardwareWalletConnection(WalletListItem walletItem) => + _walletLoadingService.requireHardwareWalletConnection( + walletItem.type, walletItem.name); + @action Future loadWallet(WalletListItem walletItem) async { // bool switchingToSameWalletType = walletItem.type == _appStore.wallet?.type; @@ -87,7 +91,8 @@ abstract class WalletListViewModelBase with Store { singleWalletsList.clear(); wallets.addAll( - _walletInfoSource.values.map((info) => convertWalletInfoToWalletListItem(info)), + _walletInfoSource.values + .map((info) => convertWalletInfoToWalletListItem(info)), ); //========== Split into shared seed groups and single wallets list @@ -95,7 +100,8 @@ abstract class WalletListViewModelBase with Store { for (var group in _walletManager.walletGroups) { if (group.wallets.length == 1) { - singleWalletsList.add(convertWalletInfoToWalletListItem(group.wallets.first)); + singleWalletsList + .add(convertWalletInfoToWalletListItem(group.wallets.first)); } else { multiWalletGroups.add(group); } @@ -148,9 +154,11 @@ abstract class WalletListViewModelBase with Store { List walletInfoSourceCopy = _walletInfoSource.values.toList(); await _walletInfoSource.clear(); if (ascending) { - walletInfoSourceCopy.sort((a, b) => a.type.toString().compareTo(b.type.toString())); + walletInfoSourceCopy + .sort((a, b) => a.type.toString().compareTo(b.type.toString())); } else { - walletInfoSourceCopy.sort((a, b) => b.type.toString().compareTo(a.type.toString())); + walletInfoSourceCopy + .sort((a, b) => b.type.toString().compareTo(a.type.toString())); } await _walletInfoSource.addAll(walletInfoSourceCopy); updateList(); @@ -213,7 +221,8 @@ abstract class WalletListViewModelBase with Store { name: info.name, type: info.type, key: info.key, - isCurrent: info.name == _appStore.wallet?.name && info.type == _appStore.wallet?.type, + isCurrent: info.name == _appStore.wallet?.name && + info.type == _appStore.wallet?.type, isEnabled: availableWalletTypes.contains(info.type), isTestnet: info.network?.toLowerCase().contains('testnet') ?? false, ); diff --git a/scripts/prepare_moneroc.sh b/scripts/prepare_moneroc.sh index 24f4d201c..0d90e431a 100755 --- a/scripts/prepare_moneroc.sh +++ b/scripts/prepare_moneroc.sh @@ -6,9 +6,9 @@ cd "$(dirname "$0")" if [[ ! -d "monero_c" ]]; then - git clone https://github.com/mrcyjanek/monero_c --branch rewrite-wip + git clone https://github.com/mrcyjanek/monero_c --branch master cd monero_c - git checkout 6eb571ea498ed7b854934785f00fabfd0dadf75b + git checkout 1d8e0fb30b53c28756f23676d5a3e1a99a9b3051 git reset --hard git submodule update --init --force --recursive ./apply_patches.sh monero diff --git a/tool/configure.dart b/tool/configure.dart index c5d4a6863..fb8291385 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -270,20 +270,22 @@ 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:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; import 'package:polyseed/polyseed.dart';"""; const moneroCWHeaders = """ +import 'package:cw_core/account.dart' as monero_account; 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_monero/api/wallet_manager.dart'; +import 'package:cw_monero/api/wallet.dart' as monero_wallet_api; +import 'package:cw_monero/ledger.dart'; import 'package:cw_monero/monero_unspent.dart'; import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/monero_wallet_service.dart'; -import 'package:cw_monero/api/wallet_manager.dart'; import 'package:cw_monero/monero_wallet.dart'; import 'package:cw_monero/monero_transaction_info.dart'; import 'package:cw_monero/monero_transaction_creation_credentials.dart'; -import 'package:cw_core/account.dart' as monero_account; -import 'package:cw_monero/api/wallet.dart' as monero_wallet_api; import 'package:cw_monero/mnemonics/english.dart'; import 'package:cw_monero/mnemonics/chinese_simplified.dart'; import 'package:cw_monero/mnemonics/dutch.dart'; @@ -395,6 +397,7 @@ abstract class Monero { required String language, required int height}); WalletCredentials createMoneroRestoreWalletFromSeedCredentials({required String name, required String password, required int height, required String mnemonic}); + WalletCredentials createMoneroRestoreWalletFromHardwareCredentials({required String name, required String password, required int height, required ledger.LedgerConnection ledgerConnection}); WalletCredentials createMoneroNewWalletCredentials({required String name, required String language, required bool isPolyseed, String? password}); Map getKeys(Object wallet); int? getRestoreHeight(Object wallet); @@ -411,6 +414,8 @@ abstract class Monero { int getTransactionInfoAccountId(TransactionInfo tx); WalletService createMoneroWalletService(Box walletInfoSource, Box unspentCoinSource); Map pendingTransactionInfo(Object transaction); + void setLedgerConnection(Object wallet, ledger.LedgerConnection connection); + void setGlobalLedgerConnection(ledger.LedgerConnection connection); } abstract class MoneroSubaddressList { From 96db38c0aa3b2628118302e137e7b1969b5367d0 Mon Sep 17 00:00:00 2001 From: Serhii Date: Tue, 12 Nov 2024 05:29:32 +0200 Subject: [PATCH 60/73] Cw 744 improve address book (#1771) * add sort function to contact list * fix UI * prevent duplicate contact names * dispose contact source subscription * fix custom order issue * update the address book UI * fix saving custom order * fix merge conflict issue * fix the address book filter by the selected currency * add dropdown for wallets with multiple address types * minor fixes * add dropdown for wallets with multiple address types * Update lib/entities/contact.dart [skip ci] * Update lib/src/screens/contact/contact_list_page.dart [skip ci] * Update lib/src/screens/contact/contact_list_page.dart [skip ci] --------- Co-authored-by: Omar Hatem --- .../screens/contact/contact_list_page.dart | 62 ++++++++++++++++--- .../contact_list/contact_list_view_model.dart | 35 +++++++---- 2 files changed, 75 insertions(+), 22 deletions(-) diff --git a/lib/src/screens/contact/contact_list_page.dart b/lib/src/screens/contact/contact_list_page.dart index 130380b09..f71747953 100644 --- a/lib/src/screens/contact/contact_list_page.dart +++ b/lib/src/screens/contact/contact_list_page.dart @@ -11,6 +11,7 @@ import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/standard_list.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/filter_theme.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; @@ -160,25 +161,60 @@ class _ContactPageBodyState extends State with SingleTickerProv Widget _buildWalletContacts(BuildContext context) { final walletContacts = widget.contactListViewModel.walletContactsToShow; + final groupedContacts = >{}; + for (var contact in walletContacts) { + final baseName = _extractBaseName(contact.name); + groupedContacts.putIfAbsent(baseName, () => []).add(contact); + } + return ListView.builder( - shrinkWrap: true, - itemCount: walletContacts.length * 2, + itemCount: groupedContacts.length * 2, itemBuilder: (context, index) { if (index.isOdd) { return StandardListSeparator(); } else { - final walletInfo = walletContacts[index ~/ 2]; - return generateRaw(context, walletInfo); + final groupIndex = index ~/ 2; + final groupName = groupedContacts.keys.elementAt(groupIndex); + final groupContacts = groupedContacts[groupName]!; + + if (groupContacts.length == 1) { + final contact = groupContacts[0]; + return generateRaw(context, contact); + } else { + final activeContact = groupContacts.firstWhere( + (contact) => contact.name.contains('Active'), + orElse: () => groupContacts[0], + ); + + return ExpansionTile( + title: Text( + groupName, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Theme.of(context).extension()!.titleColor, + ), + ), + leading: _buildCurrencyIcon(activeContact), + tilePadding: EdgeInsets.zero, + childrenPadding: const EdgeInsets.only(left: 16), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + expandedAlignment: Alignment.topLeft, + children: groupContacts.map((contact) => generateRaw(context, contact)).toList(), + ); + } } }, ); } + String _extractBaseName(String name) { + final bracketIndex = name.indexOf('('); + return (bracketIndex != -1) ? name.substring(0, bracketIndex).trim() : name; + } + Widget generateRaw(BuildContext context, ContactBase contact) { - final image = contact.type.iconPath; - final currencyIcon = image != null - ? Image.asset(image, height: 24, width: 24) - : const SizedBox(height: 24, width: 24); + final currencyIcon = _buildCurrencyIcon(contact); return GestureDetector( onTap: () async { @@ -219,6 +255,13 @@ class _ContactPageBodyState extends State with SingleTickerProv ); } + Widget _buildCurrencyIcon(ContactBase contact) { + final image = contact.type.iconPath; + return image != null + ? Image.asset(image, height: 24, width: 24) + : const SizedBox(height: 24, width: 24); + } + Future showNameAndAddressDialog(BuildContext context, String name, String address) async { return await showPopUp( context: context, @@ -263,12 +306,13 @@ class _ContactListBodyState extends State { @override void dispose() { widget.tabController.removeListener(_handleTabChange); + widget.contactListViewModel.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - final contacts = widget.contactListViewModel.contacts; + final contacts = widget.contactListViewModel.contactsToShow; return Scaffold( body: Container( child: FilteredList( 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 0015463a5..25f222891 100644 --- a/lib/view_model/contact_list/contact_list_view_model.dart +++ b/lib/view_model/contact_list/contact_list_view_model.dart @@ -28,7 +28,8 @@ abstract class ContactListViewModelBase with Store { isAutoGenerateEnabled = settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.enabled { walletInfoSource.values.forEach((info) { - if ([WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type) && info.addressInfos != null) { + if ([WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type) && + info.addressInfos != null) { for (var key in info.addressInfos!.keys) { final value = info.addressInfos![key]; final address = value?.first; @@ -60,15 +61,19 @@ abstract class ContactListViewModelBase with Store { address, name, walletTypeToCryptoCurrency(info.type, - isTestnet: - info.network == null ? false : info.network!.toLowerCase().contains("testnet")), + isTestnet: info.network == null + ? false + : info.network!.toLowerCase().contains("testnet")), )); }); } } else { walletContacts.add(WalletContact( info.address, - _createName(info.name, "", key: [WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type) ? 0 : null), + _createName(info.name, "", + key: [WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type) + ? 0 + : null), walletTypeToCryptoCurrency(info.type), )); } @@ -82,8 +87,11 @@ abstract class ContactListViewModelBase with Store { } String _createName(String walletName, String label, {int? key = null}) { - final actualLabel = label.replaceAll(RegExp(r'active', caseSensitive: false), S.current.active).replaceAll(RegExp(r'silent payments', caseSensitive: false), S.current.silent_payments); - return '$walletName${key == null ? "" : " [#${key}]"} ${actualLabel.isNotEmpty ? "($actualLabel)" : ""}'.trim(); + final actualLabel = label + .replaceAll(RegExp(r'active', caseSensitive: false), S.current.active) + .replaceAll(RegExp(r'silent payments', caseSensitive: false), S.current.silent_payments); + return '$walletName${key == null ? "" : " [#${key}]"} ${actualLabel.isNotEmpty ? "($actualLabel)" : ""}' + .trim(); } final bool isAutoGenerateEnabled; @@ -108,18 +116,19 @@ abstract class ContactListViewModelBase with Store { Future delete(ContactRecord contact) async => contact.original.delete(); ObservableList get contactsToShow => - ObservableList.of(contacts.where((element) => _isValidForCurrency(element))); + ObservableList.of(contacts.where((element) => _isValidForCurrency(element, false))); @computed List get walletContactsToShow => - walletContacts.where((element) => _isValidForCurrency(element)).toList(); + walletContacts.where((element) => _isValidForCurrency(element, true)).toList(); - bool _isValidForCurrency(ContactBase element) { - if (element.name.contains('Silent Payments')) return false; - if (element.name.contains('MWEB')) return false; + bool _isValidForCurrency(ContactBase element, bool isWalletContact) { + if (_currency == null) return true; + if (!element.name.contains('Active') && + isWalletContact && + (element.type == CryptoCurrency.btc || element.type == CryptoCurrency.ltc)) return false; - return _currency == null || - element.type == _currency || + return element.type == _currency || (element.type.tag != null && _currency?.tag != null && element.type.tag == _currency?.tag) || From c4698576a3fc8c2d1da51ebdb82267e456b9d014 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Wed, 13 Nov 2024 01:19:50 +0200 Subject: [PATCH 61/73] add xmr to unstoppable domain (#1800) --- lib/entities/parse_address_from_domain.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index 481db5620..11ba4724c 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -41,7 +41,8 @@ class AddressResolver { 'kresus', 'anime', 'manga', - 'binanceus' + 'binanceus', + 'xmr', ]; static String? extractAddressByType({required String raw, required CryptoCurrency type}) { From 0adb69d095a80526c02101877b2141ac20e133ab Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Wed, 13 Nov 2024 10:44:43 +0100 Subject: [PATCH 62/73] Better Ledger Connection handling (#1802) --- cw_monero/lib/ledger.dart | 6 +----- cw_monero/lib/monero_wallet_service.dart | 5 +++++ lib/monero/cw_monero.dart | 4 ++++ lib/view_model/hardware_wallet/ledger_view_model.dart | 4 +++- tool/configure.dart | 1 + 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/cw_monero/lib/ledger.dart b/cw_monero/lib/ledger.dart index 074975df3..c947d0944 100644 --- a/cw_monero/lib/ledger.dart +++ b/cw_monero/lib/ledger.dart @@ -47,10 +47,6 @@ void enableLedgerExchange(monero.wallet ptr, LedgerConnection connection) { void keepAlive(LedgerConnection connection) { if (connection.connectionType == ConnectionType.ble) { - UniversalBle.onConnectionChange = (String deviceId, bool isConnected) { - print("[Monero] Ledger Disconnected"); - _ledgerKeepAlive?.cancel(); - }; _ledgerKeepAlive = Timer.periodic(Duration(seconds: 10), (_) async { try { UniversalBle.setNotifiable( @@ -59,7 +55,7 @@ void keepAlive(LedgerConnection connection) { connection.device.deviceInfo.notifyCharacteristicKey, BleInputProperty.notification, ); - } catch (_){} + } catch (_) {} }); } } diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index b2089dfc7..6f49640be 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -146,6 +146,11 @@ class MoneroWalletService extends WalletService< password: password); final isValid = wallet.walletAddresses.validate(); + if (wallet.isHardwareWallet) { + wallet.setLedgerConnection(gLedger!); + gLedger = null; + } + if (!isValid) { await restoreOrResetWalletFiles(name); wallet.close(shouldCleanup: false); diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index 09d9889c5..89b1579fc 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -402,6 +402,10 @@ class CWMonero extends Monero { moneroWallet.setLedgerConnection(connection); } + void resetLedgerConnection() { + disableLedgerExchange(); + } + @override void setGlobalLedgerConnection(ledger.LedgerConnection connection) { gLedger = connection; diff --git a/lib/view_model/hardware_wallet/ledger_view_model.dart b/lib/view_model/hardware_wallet/ledger_view_model.dart index 96f5930c0..3cd131efa 100644 --- a/lib/view_model/hardware_wallet/ledger_view_model.dart +++ b/lib/view_model/hardware_wallet/ledger_view_model.dart @@ -98,7 +98,9 @@ abstract class LedgerViewModelBase with Store { print('Ledger Device State Changed: $event'); if (event == sdk.BleConnectionState.disconnected) { _connection = null; - _connectionChangeListener?.cancel(); + if (type == WalletType.monero) { + monero!.resetLedgerConnection(); + } } }); } diff --git a/tool/configure.dart b/tool/configure.dart index fb8291385..f0f79cfb1 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -415,6 +415,7 @@ abstract class Monero { WalletService createMoneroWalletService(Box walletInfoSource, Box unspentCoinSource); Map pendingTransactionInfo(Object transaction); void setLedgerConnection(Object wallet, ledger.LedgerConnection connection); + void resetLedgerConnection(); void setGlobalLedgerConnection(ledger.LedgerConnection connection); } From ed5e701d801cd88d4905a06a6f9e2b3aa6dca510 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Wed, 13 Nov 2024 11:55:46 +0200 Subject: [PATCH 63/73] Potential fix for the race condition of loading a wallet while the name in the shared prefs hasn't updated yet (#1801) --- lib/reactions/on_current_wallet_change.dart | 7 ------- lib/store/app_store.dart | 7 +++++++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 6630f1dfc..d7e6f90c0 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -10,9 +10,6 @@ import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:mobx/mobx.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:cake_wallet/di.dart'; -import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/reactions/check_connection.dart'; import 'package:cake_wallet/reactions/on_wallet_sync_status_change.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; @@ -65,10 +62,6 @@ void startCurrentWalletChangeReaction( startWalletSyncStatusChangeReaction(wallet, fiatConversionStore); startCheckConnectionReaction(wallet, settingsStore); - await getIt.get().setString(PreferencesKey.currentWalletName, wallet.name); - await getIt - .get() - .setInt(PreferencesKey.currentWalletType, serializeToInt(wallet.type)); if (wallet.type == WalletType.monero || wallet.type == WalletType.wownero || diff --git a/lib/store/app_store.dart b/lib/store/app_store.dart index cd8881633..24d5dc6a4 100644 --- a/lib/store/app_store.dart +++ b/lib/store/app_store.dart @@ -1,8 +1,10 @@ import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart'; import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/wallet_base.dart'; @@ -11,6 +13,7 @@ import 'package:cake_wallet/store/wallet_list_store.dart'; import 'package:cake_wallet/store/authentication_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/node_list_store.dart'; +import 'package:shared_preferences/shared_preferences.dart'; part 'app_store.g.dart'; @@ -47,5 +50,9 @@ abstract class AppStoreBase with Store { getIt.get().create(); await getIt.get().init(); } + await getIt.get().setString(PreferencesKey.currentWalletName, wallet.name); + await getIt + .get() + .setInt(PreferencesKey.currentWalletType, serializeToInt(wallet.type)); } } From cf7f019a925bec52086a63f6785e4c4dfa329298 Mon Sep 17 00:00:00 2001 From: Serhii Date: Thu, 14 Nov 2024 03:20:08 +0200 Subject: [PATCH 64/73] fix the UI issue with address book (#1803) --- .../screens/contact/contact_list_page.dart | 19 +++++++++++++------ .../contact_list/contact_list_view_model.dart | 7 ++++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/lib/src/screens/contact/contact_list_page.dart b/lib/src/screens/contact/contact_list_page.dart index f71747953..878a8bb54 100644 --- a/lib/src/screens/contact/contact_list_page.dart +++ b/lib/src/screens/contact/contact_list_page.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/contact_base.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/wallet_list_order_types.dart'; @@ -11,14 +12,12 @@ import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/standard_list.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; -import 'package:cake_wallet/themes/extensions/filter_theme.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; class ContactListPage extends BasePage { @@ -92,16 +91,19 @@ class ContactPageBody extends StatefulWidget { class _ContactPageBodyState extends State with SingleTickerProviderStateMixin { late TabController _tabController; + late ContactListViewModel contactListViewModel; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); + contactListViewModel = widget.contactListViewModel; } @override void dispose() { _tabController.dispose(); + contactListViewModel.dispose(); super.dispose(); } @@ -306,13 +308,17 @@ class _ContactListBodyState extends State { @override void dispose() { widget.tabController.removeListener(_handleTabChange); - widget.contactListViewModel.dispose(); + if (widget.contactListViewModel.settingsStore.contactListOrder == FilterListOrderType.Custom) { + widget.contactListViewModel.saveCustomOrder(); + } super.dispose(); } @override Widget build(BuildContext context) { - final contacts = widget.contactListViewModel.contactsToShow; + final contacts = widget.contactListViewModel.isEditable + ? widget.contactListViewModel.contacts + : widget.contactListViewModel.contactsToShow; return Scaffold( body: Container( child: FilteredList( @@ -351,8 +357,9 @@ class _ContactListBodyState extends State { }, ), ), - floatingActionButton: - _isContactsTabActive ? filterButtonWidget(context, widget.contactListViewModel) : null, + floatingActionButton: _isContactsTabActive && widget.contactListViewModel.isEditable + ? filterButtonWidget(context, widget.contactListViewModel) + : null, ); } 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 25f222891..eb3fb837e 100644 --- a/lib/view_model/contact_list/contact_list_view_model.dart +++ b/lib/view_model/contact_list/contact_list_view_model.dart @@ -136,10 +136,11 @@ abstract class ContactListViewModelBase with Store { _currency?.tag == element.type.toString(); } - void dispose() async { - _subscription?.cancel(); + void dispose() => _subscription?.cancel(); + + void saveCustomOrder() { final List contactsSourceCopy = contacts.map((e) => e.original).toList(); - await reorderContacts(contactsSourceCopy); + reorderContacts(contactsSourceCopy); } void reorderAccordingToContactList() => From 9be990a3c094b76a22e55e89c4144ecbfeee9ca6 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Sat, 16 Nov 2024 11:06:49 -0600 Subject: [PATCH 65/73] Move the api key of trocador to the headers (#1808) --- .../provider/trocador_exchange_provider.dart | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/exchange/provider/trocador_exchange_provider.dart b/lib/exchange/provider/trocador_exchange_provider.dart index 652543607..5529d824a 100644 --- a/lib/exchange/provider/trocador_exchange_provider.dart +++ b/lib/exchange/provider/trocador_exchange_provider.dart @@ -89,13 +89,12 @@ class TrocadorExchangeProvider extends ExchangeProvider { required CryptoCurrency to, required bool isFixedRateMode}) async { final params = { - 'api_key': apiKey, 'ticker': _normalizeCurrency(from), 'name': from.name, }; final uri = await _getUri(coinPath, params); - final response = await get(uri); + final response = await get(uri, headers: {'API-Key': apiKey}); if (response.statusCode != 200) throw Exception('Unexpected http status: ${response.statusCode}'); @@ -123,7 +122,6 @@ class TrocadorExchangeProvider extends ExchangeProvider { if (amount == 0) return 0.0; final params = { - 'api_key': apiKey, 'ticker_from': _normalizeCurrency(from), 'ticker_to': _normalizeCurrency(to), 'network_from': _networkFor(from), @@ -136,7 +134,8 @@ class TrocadorExchangeProvider extends ExchangeProvider { }; final uri = await _getUri(newRatePath, params); - final response = await get(uri); + final response = await get(uri, headers: {'API-Key': apiKey}); + final responseJSON = json.decode(response.body) as Map; final fromAmount = double.parse(responseJSON['amount_from'].toString()); final toAmount = double.parse(responseJSON['amount_to'].toString()); @@ -161,7 +160,6 @@ class TrocadorExchangeProvider extends ExchangeProvider { required bool isSendAll, }) async { final params = { - 'api_key': apiKey, 'ticker_from': _normalizeCurrency(request.fromCurrency), 'ticker_to': _normalizeCurrency(request.toCurrency), 'network_from': _networkFor(request.fromCurrency), @@ -202,7 +200,7 @@ class TrocadorExchangeProvider extends ExchangeProvider { params['provider'] = firstAvailableProvider; final uri = await _getUri(createTradePath, params); - final response = await get(uri); + final response = await get(uri, headers: {'API-Key': apiKey}); if (response.statusCode == 400) { final responseJSON = json.decode(response.body) as Map; @@ -248,8 +246,8 @@ class TrocadorExchangeProvider extends ExchangeProvider { @override Future findTradeById({required String id}) async { - final uri = await _getUri(tradePath, {'api_key': apiKey, 'id': id}); - return get(uri).then((response) { + final uri = await _getUri(tradePath, {'id': id}); + return get(uri, headers: {'API-Key': apiKey}).then((response) { if (response.statusCode != 200) throw Exception('Unexpected http status: ${response.statusCode}'); From ff5fbd794649619085568e6ff572227a83fff23e Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Sat, 16 Nov 2024 16:23:54 -0600 Subject: [PATCH 66/73] v4.21.0 Release Candidate (#1795) * new versions * minor fix of cache key * fix cache hit * - potential improvement for sync status - update with latest main * disable thorchain by default [skip ci] * update monero_c commit hash * minor fixes update versions * remove monero ledger * increase macos build number [skip ci] --- assets/text/Monerocom_Release_Notes.txt | 4 +- assets/text/Release_Notes.txt | 10 +-- cw_bitcoin/lib/electrum_wallet.dart | 9 +- cw_bitcoin/pubspec.lock | 4 +- .../lib/hardware/device_connection_type.dart | 2 +- cw_monero/pubspec.lock | 4 +- cw_monero/pubspec.yaml | 2 +- cw_wownero/pubspec.lock | 88 +------------------ cw_wownero/pubspec.yaml | 2 +- ios/Podfile.lock | 38 +++----- ios/Runner.xcodeproj/project.pbxproj | 18 ++++ lib/buy/meld/meld_buy_provider.dart | 4 - lib/entities/default_settings_migration.dart | 8 +- .../screens/contact/contact_list_page.dart | 18 ++-- .../screens/restore/restore_options_page.dart | 5 +- linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + 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/prepare_moneroc.sh | 2 +- scripts/windows/build_exe_installer.iss | 2 +- windows/flutter/generated_plugins.cmake | 1 + 24 files changed, 92 insertions(+), 160 deletions(-) diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index 613ea4281..46f21e172 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,3 +1,3 @@ -Monero enhancements -Introducing StealthEx and LetxExchange +Add airgapped Monero wallet support (best used with our new offline app Cupcake) +New Buy & Sell flow Bug fixes \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index 61aafb6e4..6764826a7 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,7 +1,5 @@ -Added Litecoin MWEB -Added wallet groups -Silent Payment enhancements for speed & reliability -Monero enhancements -Introducing StealthEx and LetxExchange -Additional ERC20 tokens scam detection +Add Litecoin Ledger support +Add airgapped Monero wallet support (best used with our new offline app Cupcake) +MWEB fixes and enhancements +New Buy & Sell flow Bug fixes \ No newline at end of file diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 6245aa787..e58fad00f 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -574,6 +574,8 @@ abstract class ElectrumWalletBase Future connectToNode({required Node node}) async { this.node = node; + if (syncStatus is ConnectingSyncStatus) return; + try { syncStatus = ConnectingSyncStatus(); @@ -2216,13 +2218,14 @@ abstract class ElectrumWalletBase if (syncStatus is NotConnectedSyncStatus || syncStatus is LostConnectionSyncStatus || syncStatus is ConnectingSyncStatus) { - syncStatus = AttemptingSyncStatus(); - startSync(); + syncStatus = ConnectedSyncStatus(); } break; case ConnectionStatus.disconnected: - if (syncStatus is! NotConnectedSyncStatus) { + if (syncStatus is! NotConnectedSyncStatus && + syncStatus is! ConnectingSyncStatus && + syncStatus is! SyncronizingSyncStatus) { syncStatus = NotConnectedSyncStatus(); } break; diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 8eafb2a44..c8a83f90c 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -87,8 +87,8 @@ packages: dependency: "direct overridden" description: path: "." - ref: cake-update-v8 - resolved-ref: fc045a11db3d85d806ca67f75e8b916c706745a2 + ref: cake-update-v9 + resolved-ref: "86969a14e337383e14965f5fb45a72a63e5009bc" url: "https://github.com/cake-tech/bitcoin_base" source: git version: "4.7.0" diff --git a/cw_core/lib/hardware/device_connection_type.dart b/cw_core/lib/hardware/device_connection_type.dart index 76a501af1..466d58e2a 100644 --- a/cw_core/lib/hardware/device_connection_type.dart +++ b/cw_core/lib/hardware/device_connection_type.dart @@ -7,7 +7,7 @@ enum DeviceConnectionType { static List supportedConnectionTypes(WalletType walletType, [bool isIOS = false]) { switch (walletType) { - case WalletType.monero: + // case WalletType.monero: case WalletType.bitcoin: case WalletType.litecoin: case WalletType.ethereum: diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index a6a03dce5..31d44ac63 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -503,8 +503,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: caaf1e56b1d2a254b332fdf848926fb963af4a3b - resolved-ref: caaf1e56b1d2a254b332fdf848926fb963af4a3b + ref: d72c15f4339791a7bbdf17e9d827b7b56ca144e4 + resolved-ref: d72c15f4339791a7bbdf17e9d827b7b56ca144e4 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 6fa235ee4..71cbfa243 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: caaf1e56b1d2a254b332fdf848926fb963af4a3b + ref: d72c15f4339791a7bbdf17e9d827b7b56ca144e4 # ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash path: impls/monero.dart mutex: ^3.1.0 diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index b0347c023..a861c6bac 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -41,14 +41,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" - bluez: - dependency: transitive - description: - name: bluez - sha256: "203a1924e818a9dd74af2b2c7a8f375ab8e5edf0e486bba8f90a0d8a17ed9fce" - url: "https://pub.dev" - source: hosted - version: "0.8.2" boolean_selector: dependency: transitive description: @@ -217,14 +209,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.4" - dbus: - dependency: transitive - description: - name: dbus - sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" - url: "https://pub.dev" - source: hosted - version: "0.7.10" encrypt: dependency: "direct main" description: @@ -283,14 +267,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_web_bluetooth: - dependency: transitive - description: - name: flutter_web_bluetooth - sha256: "52ce64f65d7321c4bf6abfe9dac02fb888731339a5e0ad6de59fb916c20c9f02" - url: "https://pub.dev" - source: hosted - version: "0.2.3" frontend_server_client: dependency: transitive description: @@ -427,22 +403,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" - ledger_flutter_plus: - dependency: transitive - description: - name: ledger_flutter_plus - sha256: ea3ed586e1697776dacf42ac979095f1ca3bd143bf007cbe5c78e09cb6943f42 - url: "https://pub.dev" - source: hosted - version: "1.2.5" - ledger_usb_plus: - dependency: transitive - description: - name: ledger_usb_plus - sha256: "21cc5d976cf7edb3518bd2a0c4164139cbb0817d2e4f2054707fc4edfdf9ce87" - url: "https://pub.dev" - source: hosted - version: "1.0.4" logging: dependency: transitive description: @@ -503,8 +463,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: caaf1e56b1d2a254b332fdf848926fb963af4a3b - resolved-ref: caaf1e56b1d2a254b332fdf848926fb963af4a3b + ref: d72c15f4339791a7bbdf17e9d827b7b56ca144e4 + resolved-ref: d72c15f4339791a7bbdf17e9d827b7b56ca144e4 url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" @@ -580,14 +540,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.1" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 - url: "https://pub.dev" - source: hosted - version: "6.0.2" platform: dependency: transitive description: @@ -644,14 +596,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" - rxdart: - dependency: transitive - description: - name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.dev" - source: hosted - version: "0.28.0" shelf: dependency: transitive description: @@ -777,22 +721,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" - universal_ble: - dependency: transitive - description: - name: universal_ble - sha256: "0dfbd6b64bff3ad61ed7a895c232530d9614e9b01ab261a74433a43267edb7f3" - url: "https://pub.dev" - source: hosted - version: "0.12.0" - universal_platform: - dependency: transitive - description: - name: universal_platform - sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" - url: "https://pub.dev" - source: hosted - version: "1.1.0" unorm_dart: dependency: transitive description: @@ -849,14 +777,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" - xml: - dependency: transitive - description: - name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 - url: "https://pub.dev" - source: hosted - version: "6.5.0" yaml: dependency: transitive description: @@ -866,5 +786,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=3.3.0 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.2.0-0 <4.0.0" + flutter: ">=3.7.0" diff --git a/cw_wownero/pubspec.yaml b/cw_wownero/pubspec.yaml index 582c15447..28ece82ef 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: caaf1e56b1d2a254b332fdf848926fb963af4a3b + ref: d72c15f4339791a7bbdf17e9d827b7b56ca144e4 # ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash path: impls/monero.dart mutex: ^3.1.0 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 470bdf4e7..abcac01fb 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,8 +1,4 @@ PODS: - - barcode_scan2 (0.0.1): - - Flutter - - MTBBarcodeScanner - - SwiftProtobuf - connectivity_plus (0.0.1): - Flutter - ReachabilitySwift @@ -76,6 +72,8 @@ PODS: - DKPhotoGallery/Resource (0.0.19): - SDWebImage - SwiftyGif + - fast_scanner (5.1.1): + - Flutter - file_picker (0.0.1): - DKImagePickerController/PhotoGallery - Flutter @@ -100,7 +98,6 @@ PODS: - Flutter - integration_test (0.0.1): - Flutter - - MTBBarcodeScanner (5.0.11) - OrderedSet (5.0.0) - package_info_plus (0.4.5): - Flutter @@ -109,12 +106,7 @@ PODS: - FlutterMacOS - permission_handler_apple (9.1.1): - Flutter - - Protobuf (3.28.2) - ReachabilitySwift (5.2.3) - - reactive_ble_mobile (0.0.1): - - Flutter - - Protobuf (~> 3.5) - - SwiftProtobuf (~> 1.0) - SDWebImage (5.19.7): - SDWebImage/Core (= 5.19.7) - SDWebImage/Core (5.19.7) @@ -127,11 +119,13 @@ PODS: - FlutterMacOS - sp_scanner (0.0.1): - Flutter - - SwiftProtobuf (1.27.1) - SwiftyGif (5.4.5) - Toast (4.1.1) - uni_links (0.0.1): - Flutter + - universal_ble (0.0.1): + - Flutter + - FlutterMacOS - url_launcher_ios (0.0.1): - Flutter - wakelock_plus (0.0.1): @@ -140,7 +134,6 @@ PODS: - Flutter DEPENDENCIES: - - barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - CryptoSwift - cw_haven (from `.symlinks/plugins/cw_haven/ios`) @@ -149,6 +142,7 @@ DEPENDENCIES: - 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`) + - fast_scanner (from `.symlinks/plugins/fast_scanner/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) @@ -161,12 +155,12 @@ DEPENDENCIES: - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - - reactive_ble_mobile (from `.symlinks/plugins/reactive_ble_mobile/ios`) - sensitive_clipboard (from `.symlinks/plugins/sensitive_clipboard/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sp_scanner (from `.symlinks/plugins/sp_scanner/ios`) - uni_links (from `.symlinks/plugins/uni_links/ios`) + - universal_ble (from `.symlinks/plugins/universal_ble/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) - workmanager (from `.symlinks/plugins/workmanager/ios`) @@ -176,18 +170,13 @@ SPEC REPOS: - CryptoSwift - DKImagePickerController - DKPhotoGallery - - MTBBarcodeScanner - OrderedSet - - Protobuf - ReachabilitySwift - SDWebImage - - SwiftProtobuf - SwiftyGif - Toast EXTERNAL SOURCES: - barcode_scan2: - :path: ".symlinks/plugins/barcode_scan2/ios" connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" cw_haven: @@ -202,6 +191,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/device_info_plus/ios" devicelocale: :path: ".symlinks/plugins/devicelocale/ios" + fast_scanner: + :path: ".symlinks/plugins/fast_scanner/ios" file_picker: :path: ".symlinks/plugins/file_picker/ios" Flutter: @@ -226,8 +217,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" - reactive_ble_mobile: - :path: ".symlinks/plugins/reactive_ble_mobile/ios" sensitive_clipboard: :path: ".symlinks/plugins/sensitive_clipboard/ios" share_plus: @@ -238,6 +227,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/sp_scanner/ios" uni_links: :path: ".symlinks/plugins/uni_links/ios" + universal_ble: + :path: ".symlinks/plugins/universal_ble/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" wakelock_plus: @@ -246,7 +237,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/workmanager/ios" SPEC CHECKSUMS: - barcode_scan2: 0af2bb63c81b4565aab6cd78278e4c0fa136dbb0 connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d CryptoSwift: c63a805d8bb5e5538e88af4e44bb537776af11ea cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a @@ -257,6 +247,7 @@ SPEC CHECKSUMS: devicelocale: b22617f40038496deffba44747101255cee005b0 DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 + fast_scanner: 44c00940355a51258cd6c2085734193cd23d95bc file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 @@ -266,23 +257,20 @@ SPEC CHECKSUMS: fluttertoast: 48c57db1b71b0ce9e6bba9f31c940ff4b001293c in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d integration_test: 13825b8a9334a850581300559b8839134b124670 - MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 - Protobuf: 28c89b24435762f60244e691544ed80f50d82701 ReachabilitySwift: 7f151ff156cea1481a8411701195ac6a984f4979 - reactive_ble_mobile: 9ce6723d37ccf701dbffd202d487f23f5de03b4c SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3 sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sp_scanner: eaa617fa827396b967116b7f1f43549ca62e9a12 - SwiftProtobuf: b109bd17979d7993a84da14b1e1fdd8b0ded934a SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e uni_links: d97da20c7701486ba192624d99bffaaffcfc298a + universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 09c75feee..85d39167a 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -230,6 +230,7 @@ 97C146EC1CF9000F007C117D /* Resources */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, F6F67323547956BC4F7B67F1 /* [CP] Embed Pods Frameworks */, + 777FE2B16F25A3E820834145 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -343,6 +344,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; }; + 777FE2B16F25A3E820834145 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; diff --git a/lib/buy/meld/meld_buy_provider.dart b/lib/buy/meld/meld_buy_provider.dart index 696301f2e..1ac3931d1 100644 --- a/lib/buy/meld/meld_buy_provider.dart +++ b/lib/buy/meld/meld_buy_provider.dart @@ -5,14 +5,10 @@ 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/entities/provider_types.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/currency.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:flutter/material.dart'; import 'dart:developer'; diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index d863b136e..f873314c0 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -252,7 +252,7 @@ Future defaultSettingsMigration( await removeMoneroWorld(sharedPreferences: sharedPreferences, nodes: nodes); break; case 41: - _deselectQuantex(sharedPreferences); + _deselectExchangeProvider(sharedPreferences, "Quantex"); await _addSethNode(nodes, sharedPreferences); await updateTronNodesWithNowNodes(sharedPreferences: sharedPreferences, nodes: nodes); break; @@ -261,6 +261,8 @@ Future defaultSettingsMigration( break; case 43: _updateCakeXmrNode(nodes); + _deselectExchangeProvider(sharedPreferences, "THORChain"); + _deselectExchangeProvider(sharedPreferences, "SimpleSwap"); break; default: @@ -295,12 +297,12 @@ void updateBtcElectrumNodeToUseSSL(Box nodes, SharedPreferences sharedPref } } -void _deselectQuantex(SharedPreferences sharedPreferences) { +void _deselectExchangeProvider(SharedPreferences sharedPreferences, String providerName) { final Map exchangeProvidersSelection = json.decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map; - exchangeProvidersSelection['Quantex'] = false; + exchangeProvidersSelection[providerName] = false; sharedPreferences.setString( PreferencesKey.exchangeProvidersSelection, diff --git a/lib/src/screens/contact/contact_list_page.dart b/lib/src/screens/contact/contact_list_page.dart index 878a8bb54..166288135 100644 --- a/lib/src/screens/contact/contact_list_page.dart +++ b/lib/src/screens/contact/contact_list_page.dart @@ -240,14 +240,16 @@ class _ContactPageBodyState extends State with SingleTickerProv mainAxisAlignment: MainAxisAlignment.start, children: [ currencyIcon, - Padding( - padding: EdgeInsets.only(left: 12), - child: Text( - contact.name, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - color: Theme.of(context).extension()!.titleColor, + Expanded( + child: Padding( + padding: EdgeInsets.only(left: 12), + child: Text( + contact.name, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Theme.of(context).extension()!.titleColor, + ), ), ), ), diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index 299846a72..d1d419d60 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -56,8 +56,9 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { } if (isMoneroOnly) { - return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS) - .isNotEmpty; + // return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS) + // .isNotEmpty; + return false; } return true; diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 4b9eb3b2d..f52be7481 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + sp_scanner ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 52b44e53e..42b9fa84c 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import connectivity_plus +import cw_mweb import device_info_plus import devicelocale import fast_scanner @@ -23,6 +24,7 @@ import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) + 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")) diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 8beffbcc2..7eee6d6ae 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.17.0" -MONERO_COM_BUILD_NUMBER=103 +MONERO_COM_VERSION="1.18.0" +MONERO_COM_BUILD_NUMBER=105 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.20.1" -CAKEWALLET_BUILD_NUMBER=233 +CAKEWALLET_VERSION="4.21.0" +CAKEWALLET_BUILD_NUMBER=236 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 bc3b39747..fe18758c8 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.17.0" -MONERO_COM_BUILD_NUMBER=101 +MONERO_COM_VERSION="1.18.0" +MONERO_COM_BUILD_NUMBER=103 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.20.1" -CAKEWALLET_BUILD_NUMBER=277 +CAKEWALLET_VERSION="4.21.0" +CAKEWALLET_BUILD_NUMBER=281 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/linux/app_env.sh b/scripts/linux/app_env.sh index 2dabf083b..1cdb43ced 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.10.1" -CAKEWALLET_BUILD_NUMBER=37 +CAKEWALLET_VERSION="1.11.0" +CAKEWALLET_BUILD_NUMBER=38 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 026ea034b..8f3b68ab5 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.7.0" -MONERO_COM_BUILD_NUMBER=34 +MONERO_COM_VERSION="1.8.0" +MONERO_COM_BUILD_NUMBER=36 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.13.1" -CAKEWALLET_BUILD_NUMBER=93 +CAKEWALLET_VERSION="1.14.0" +CAKEWALLET_BUILD_NUMBER=95 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 0d90e431a..3596bd18b 100755 --- a/scripts/prepare_moneroc.sh +++ b/scripts/prepare_moneroc.sh @@ -8,7 +8,7 @@ if [[ ! -d "monero_c" ]]; then git clone https://github.com/mrcyjanek/monero_c --branch master cd monero_c - git checkout 1d8e0fb30b53c28756f23676d5a3e1a99a9b3051 + git checkout d72c15f4339791a7bbdf17e9d827b7b56ca144e4 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 f65900cca..25f94cb1f 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.1.1" +#define MyAppVersion "0.2.0" #define MyAppPublisher "Cake Labs LLC" #define MyAppURL "https://cakewallet.com/" #define MyAppExeName "CakeWallet.exe" diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 26d481f85..92431a6fb 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -13,6 +13,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + sp_scanner ) set(PLUGIN_BUNDLED_LIBRARIES) From 29b659e748e800921e7c1c2127c006d1cb11c649 Mon Sep 17 00:00:00 2001 From: XD22 Date: Thu, 21 Nov 2024 22:04:46 +0200 Subject: [PATCH 67/73] Fix Whitespace in secret_key.dart (#1812) --- tool/utils/secret_key.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index affe4017c..d67ab7605 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -62,7 +62,7 @@ class SecretKey { SecretKey('bitcoinTestWalletReceiveAddress', () => ''), SecretKey('ethereumTestWalletReceiveAddress', () => ''), SecretKey('litecoinTestWalletReceiveAddress', () => ''), - SecretKey('bitco inCashTestWalletReceiveAddress', () => ''), + SecretKey('bitcoinCashTestWalletReceiveAddress', () => ''), SecretKey('polygonTestWalletReceiveAddress', () => ''), SecretKey('solanaTestWalletReceiveAddress', () => ''), SecretKey('tronTestWalletReceiveAddress', () => ''), From cfe78d4e5f9f838ae1be6a7d2d3397dc2771bd4a Mon Sep 17 00:00:00 2001 From: cyan Date: Sun, 24 Nov 2024 09:49:42 +0100 Subject: [PATCH 68/73] Fix error reporting in transaction creation (#1821) --- cw_monero/lib/api/transaction_history.dart | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index 7748a16af..f55545801 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -192,14 +192,23 @@ String? commitTransaction({required monero.PendingTransaction transactionPointer ? monero.PendingTransaction_commitUR(transactionPointer, 120) : monero.PendingTransaction_commit(transactionPointer, filename: '', overwrite: false); - final String? error = (() { + String? error = (() { final status = monero.PendingTransaction_status(transactionPointer.cast()); if (status == 0) { return null; } - return monero.Wallet_errorString(wptr!); + return monero.PendingTransaction_errorString(transactionPointer.cast()); })(); + if (error == null) { + error = (() { + final status = monero.Wallet_status(wptr!); + if (status == 0) { + return null; + } + return monero.Wallet_errorString(wptr!); + })(); + } if (error != null) { throw CreationTransactionException(message: error); } From 505ea265337148c921d271518626265eca07bc91 Mon Sep 17 00:00:00 2001 From: cyan Date: Mon, 25 Nov 2024 15:08:30 +0100 Subject: [PATCH 69/73] CW-798 Fix macos 15 issues (#1775) * update flutter to 3.24.3 * bump flutter to 3.24.4, fix all android issues (i hope) * update uni_links path * update sensitive_clipboard * update dependencies * update fast_scanner * update the ref * Update how_to_add_new_wallet_type.md [skip ci] * Update how_to_add_new_wallet_type.md [skip ci] * Update how_to_add_new_wallet_type.md [skip ci] * Update how_to_add_new_wallet_type.md [skip ci] --------- Co-authored-by: Omar Hatem --- .github/workflows/cache_dependencies.yml | 2 +- .github/workflows/pr_test_build_android.yml | 6 +- .github/workflows/pr_test_build_linux.yml | 4 +- android/app/build.gradle | 5 +- android/build.gradle | 4 +- android/gradle.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- build-guide-linux.md | 4 +- configure_cake_wallet.sh | 6 +- cw_bitcoin/pubspec.lock | 118 +++++---- cw_bitcoin/pubspec.yaml | 2 +- cw_core/pubspec.lock | 225 +++++++++------- cw_core/pubspec.yaml | 2 +- cw_haven/pubspec.lock | 248 ++++++++++-------- cw_haven/pubspec.yaml | 2 +- .../.plugin_symlinks/path_provider_linux | 1 + cw_monero/pubspec.lock | 108 ++++---- cw_monero/pubspec.yaml | 2 +- cw_mweb/android/build.gradle | 16 +- cw_mweb/android/settings.gradle | 2 + cw_nano/pubspec.lock | 236 +++++++++-------- cw_wownero/pubspec.lock | 248 ++++++++++-------- cw_wownero/pubspec.yaml | 2 +- how_to_add_new_wallet_type.md | 10 +- howto-build-android.md | 12 +- howto-build-ios.md | 8 +- howto-build-macos.md | 8 +- howto-build-windows.md | 4 +- ios/Podfile.lock | 75 ++---- ios/Runner/AppDelegate.swift | 2 +- lib/locales/hausa_intl.dart | 8 + lib/locales/yoruba_intl.dart | 8 + linux/flutter/generated_plugins.cmake | 1 - macos/Flutter/GeneratedPluginRegistrant.swift | 2 - model_generator.sh | 25 +- pubspec_base.yaml | 48 ++-- pubspec_description.yaml | 2 +- run-android.sh | 7 +- scripts/android/inject_app_details.sh | 12 +- scripts/android/pubspec_gen.sh | 4 +- scripts/ios/app_config.sh | 10 +- scripts/linux/app_config.sh | 8 +- scripts/macos/app_config.sh | 15 +- scripts/macos/gen_common.sh | 6 +- scripts/universal_sed.sh | 23 ++ tool/append_translation.dart | 2 +- tool/configure.dart | 3 +- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 2 +- 49 files changed, 841 insertions(+), 714 deletions(-) create mode 120000 cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux create mode 100644 scripts/universal_sed.sh diff --git a/.github/workflows/cache_dependencies.yml b/.github/workflows/cache_dependencies.yml index ce098b7f1..938027e81 100644 --- a/.github/workflows/cache_dependencies.yml +++ b/.github/workflows/cache_dependencies.yml @@ -34,7 +34,7 @@ jobs: - name: Flutter action uses: subosito/flutter-action@v1 with: - flutter-version: "3.19.6" + flutter-version: "3.24.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 774404e3b..e3617f81e 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -53,7 +53,7 @@ jobs: - name: Flutter action uses: subosito/flutter-action@v1 with: - flutter-version: "3.19.6" + flutter-version: "3.24.4" channel: stable - name: Install package dependencies @@ -131,12 +131,12 @@ jobs: - name: Generate key properties run: | cd /opt/android/cake_wallet - flutter packages pub run tool/generate_android_key_properties.dart keyAlias=testKey storeFile=key.jks storePassword=$STORE_PASS keyPassword=$KEY_PASS + dart run tool/generate_android_key_properties.dart keyAlias=testKey storeFile=key.jks storePassword=$STORE_PASS keyPassword=$KEY_PASS - name: Generate localization run: | cd /opt/android/cake_wallet - flutter packages pub run tool/generate_localization.dart + dart run tool/generate_localization.dart - name: Build generated code run: | diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index f3ce1045d..9e25f9f7a 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -38,7 +38,7 @@ jobs: - name: Flutter action uses: subosito/flutter-action@v1 with: - flutter-version: "3.19.6" + flutter-version: "3.24.4" channel: stable - name: Install package dependencies @@ -111,7 +111,7 @@ jobs: - name: Generate localization run: | cd /opt/android/cake_wallet - flutter packages pub run tool/generate_localization.dart + dart run tool/generate_localization.dart - name: Build generated code run: | diff --git a/android/app/build.gradle b/android/app/build.gradle index 5005a8bab..628e2ffe6 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -38,11 +38,14 @@ if (appPropertiesFile.exists()) { android { compileSdkVersion 34 + buildToolsVersion "34.0.0" lintOptions { disable 'InvalidPackage' } + namespace 'com.cakewallet.cake_wallet' + defaultConfig { applicationId appProperties['id'] minSdkVersion 24 @@ -80,7 +83,7 @@ android { } } - ndkVersion "25.1.8937393" + ndkVersion "27.0.12077973" } flutter { diff --git a/android/build.gradle b/android/build.gradle index 7ddb75179..66de0bdca 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.8.21' + ext.kotlin_version = '2.0.21' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.3.0' + 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" } diff --git a/android/gradle.properties b/android/gradle.properties index 66dd09454..d130e538a 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx3072M +org.gradle.jvmargs=-Xmx4096M android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 733c691d3..0cc4e42b9 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip diff --git a/build-guide-linux.md b/build-guide-linux.md index 55264fbfa..99c2ed0c8 100644 --- a/build-guide-linux.md +++ b/build-guide-linux.md @@ -120,7 +120,7 @@ Install Flutter package dependencies with this command: 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: -`$ flutter packages pub run tool/generate_new_secrets.dart` +`$ dart run tool/generate_new_secrets.dart` We will generate mobx models for the project. @@ -128,7 +128,7 @@ We will generate mobx models for the project. Then we need to generate localization files. -`$ flutter packages pub run tool/generate_localization.dart` +`$ dart run tool/generate_localization.dart` ### 5. Build! diff --git a/configure_cake_wallet.sh b/configure_cake_wallet.sh index 90ce1c446..a083ec7ff 100755 --- a/configure_cake_wallet.sh +++ b/configure_cake_wallet.sh @@ -1,5 +1,5 @@ #!/bin/bash - +set -x -e IOS="ios" ANDROID="android" MACOS="macos" @@ -36,6 +36,6 @@ fi source ./app_env.sh cakewallet ./app_config.sh cd ../.. && flutter pub get -flutter packages pub run tool/generate_localization.dart -./model_generator.sh +dart run tool/generate_localization.dart +#./model_generator.sh #cd macos && pod install diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index c8a83f90c..aa9f3ba05 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.0" asn1lib: dependency: transitive description: @@ -145,10 +145,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.2" build_resolvers: dependency: "direct dev" description: @@ -161,10 +161,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.4.13" build_runner_core: dependency: transitive description: @@ -250,18 +250,18 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" cryptography: dependency: "direct main" description: @@ -360,10 +360,10 @@ packages: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -431,10 +431,10 @@ packages: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" grpc: dependency: "direct main" description: @@ -503,10 +503,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" io: dependency: transitive description: @@ -535,26 +535,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" ledger_bitcoin: dependency: "direct main" description: @@ -577,7 +577,7 @@ packages: description: path: "packages/ledger-litecoin" ref: HEAD - resolved-ref: "07cd61ef76a2a017b6d5ef233396740163265457" + resolved-ref: "3dee36713e6ebec9dceb59b9ccae7f243a53ea9e" url: "https://github.com/cake-tech/ledger-flutter-plus-plugins" source: git version: "0.0.2" @@ -593,10 +593,10 @@ packages: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" matcher: dependency: transitive description: @@ -609,26 +609,26 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.15.0" mime: dependency: transitive description: name: mime - sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "2.0.0" mobx: dependency: "direct main" description: @@ -681,10 +681,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.2.12" path_provider_foundation: dependency: transitive description: @@ -729,10 +729,10 @@ packages: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -809,18 +809,18 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" + sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.3" shared_preferences_foundation: dependency: transitive description: @@ -849,10 +849,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: "59dc807b94d29d52ddbb1b3c0d3b9d0a67fc535a64e62a5542c8db0513fcb6c2" + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: @@ -873,10 +873,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -967,10 +967,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.2" timing: dependency: transitive description: @@ -991,10 +991,10 @@ packages: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" universal_ble: dependency: transitive description: @@ -1031,10 +1031,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.5" watcher: dependency: "direct overridden" description: @@ -1047,18 +1047,26 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "0.5.1" + 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: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "3.0.1" xdg_directories: dependency: transitive description: @@ -1092,5 +1100,5 @@ packages: source: hosted version: "2.2.1" sdks: - dart: ">=3.3.0 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index bff6104ac..522199c82 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: http: ^1.1.0 mobx: ^2.0.7+4 flutter_mobx: ^2.0.6+1 - intl: ^0.18.0 + intl: ^0.19.0 shared_preferences: ^2.0.15 cw_core: path: ../cw_core diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index c2bdda5f1..44ef15a41 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -5,34 +5,39 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 url: "https://pub.dev" source: hosted - version: "64.0.0" + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" analyzer: dependency: transitive description: name: analyzer - sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.7.0" args: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.6.0" asn1lib: dependency: transitive description: name: asn1lib - sha256: "21afe4333076c02877d14f4a89df111e658a6d466cbfc802eb705eb91bd5adfd" + sha256: "6b151826fcc95ff246cd219a0bf4c753ea14f4081ad71c61939becf3aba27f70" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.5.5" async: dependency: transitive description: @@ -69,10 +74,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.2" build_resolvers: dependency: "direct dev" description: @@ -85,18 +90,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.13" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: c9e32d21dd6626b5c163d48b037ce906bbe428bc23ab77bcd77bb21e593b6185 + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "7.2.11" + version: "7.3.2" built_collection: dependency: transitive description: @@ -109,10 +114,10 @@ packages: dependency: transitive description: name: built_value - sha256: c9aabae0718ec394e5bc3c7272e6bb0dc0b32201a08fe185ec1d8401d3e39309 + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.8.1" + version: "8.9.2" cake_backup: dependency: "direct main" description: @@ -166,42 +171,42 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" cryptography: dependency: transitive description: name: cryptography - sha256: df156c5109286340817d21fa7b62f9140f17915077127dd70f8bd7a2a0997a35 + sha256: d146b76d33d94548cf035233fbc2f4338c1242fa119013bead807d033fc4ae05 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.7.0" cupertino_icons: dependency: transitive description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.8" dart_style: dependency: transitive description: name: dart_style - sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.7" encrypt: dependency: "direct main" description: @@ -222,26 +227,26 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.3" file: dependency: "direct main" description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -251,10 +256,10 @@ packages: dependency: "direct main" description: name: flutter_mobx - sha256: "4a5d062ff85ed3759f4aac6410ff0ffae32e324b2e71ca722ae1b37b32e865f4" + sha256: "859fbf452fa9c2519d2700b125dd7fb14c508bbdd7fb65e26ca8ff6c92280e2e" url: "https://pub.dev" source: hosted - version: "2.2.0+2" + version: "2.2.1+1" flutter_test: dependency: "direct dev" description: flutter @@ -264,10 +269,10 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" glob: dependency: transitive description: @@ -280,10 +285,10 @@ packages: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" hive: dependency: transitive description: @@ -304,10 +309,10 @@ packages: dependency: "direct main" description: name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.2" http_multi_server: dependency: transitive description: @@ -328,10 +333,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" io: dependency: transitive description: @@ -352,42 +357,50 @@ packages: dependency: transitive description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" logging: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + 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: @@ -400,42 +413,42 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.15.0" mime: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" mobx: dependency: "direct main" description: name: mobx - sha256: "74ee54012dc7c1b3276eaa960a600a7418ef5f9997565deb8fca1fd88fb36b78" + sha256: "63920b27b32ad1910adfe767ab1750e4c212e8923232a1f891597b362074ea5e" url: "https://pub.dev" source: hosted - version: "2.3.0+1" + version: "2.3.3+2" mobx_codegen: dependency: "direct dev" description: name: mobx_codegen - sha256: b26c7f9c20b38f0ea572c1ed3f29d8e027cb265538bbd1aed3ec198642cfca42 + sha256: "8e0d8653a0c720ad933cd8358f6f89f740ce89203657c13f25bea772ef1fff7c" url: "https://pub.dev" source: hosted - version: "2.6.0+1" + version: "2.6.1" nested: dependency: transitive description: @@ -464,26 +477,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.12" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -504,18 +517,18 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" platform: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -528,10 +541,10 @@ packages: dependency: transitive description: name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" url: "https://pub.dev" source: hosted - version: "3.7.4" + version: "3.9.1" pool: dependency: transitive description: @@ -544,10 +557,10 @@ packages: dependency: transitive description: name: provider - sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c url: "https://pub.dev" source: hosted - version: "6.1.1" + version: "6.1.2" pub_semver: dependency: transitive description: @@ -560,10 +573,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.3.0" shelf: dependency: transitive description: @@ -576,10 +589,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -589,10 +602,10 @@ packages: dependency: "direct main" description: name: socks5_proxy - sha256: "1d21b5606169654bbf4cfb904e8e6ed897e9f763358709f87310c757096d909a" + sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.6" source_gen: dependency: transitive description: @@ -661,10 +674,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.2" timing: dependency: transitive description: @@ -685,10 +698,10 @@ packages: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" unorm_dart: dependency: "direct main" description: @@ -709,10 +722,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.5" watcher: dependency: "direct overridden" description: @@ -721,30 +734,38 @@ packages: 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: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.0" - win32: - dependency: transitive - description: - name: win32 - sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" - url: "https://pub.dev" - source: hosted - version: "5.0.9" + version: "3.0.1" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" yaml: dependency: transitive description: @@ -754,5 +775,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0-0 <4.0.0" - flutter: ">=3.10.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml index 6e32c2ba1..19eda51e0 100644 --- a/cw_core/pubspec.yaml +++ b/cw_core/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: path_provider: ^2.0.11 mobx: ^2.0.7+4 flutter_mobx: ^2.0.6+1 - intl: ^0.18.0 + intl: ^0.19.0 encrypt: ^5.0.1 cake_backup: git: diff --git a/cw_haven/pubspec.lock b/cw_haven/pubspec.lock index 1369675f5..cb5d3e2c3 100644 --- a/cw_haven/pubspec.lock +++ b/cw_haven/pubspec.lock @@ -21,18 +21,18 @@ packages: dependency: transitive description: name: args - sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611" + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.6.0" asn1lib: dependency: transitive description: name: asn1lib - sha256: ab96a1cb3beeccf8145c52e449233fe68364c9641623acd3adad66f8184f1039 + sha256: "6b151826fcc95ff246cd219a0bf4c753ea14f4081ad71c61939becf3aba27f70" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.5" async: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" build_config: dependency: transitive description: @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" build_resolvers: dependency: "direct dev" description: @@ -85,18 +85,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.4.13" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" url: "https://pub.dev" source: hosted - version: "7.2.7" + version: "7.2.10" built_collection: dependency: transitive description: @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: built_value - sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725" + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.4.3" + version: "8.9.2" cake_backup: dependency: transitive description: @@ -134,10 +134,10 @@ packages: dependency: transitive description: name: checked_yaml - sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" clock: dependency: transitive description: @@ -150,10 +150,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.10.0" collection: dependency: transitive description: @@ -166,34 +166,34 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" crypto: dependency: transitive description: name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.6" cryptography: dependency: transitive description: name: cryptography - sha256: df156c5109286340817d21fa7b62f9140f17915077127dd70f8bd7a2a0997a35 + sha256: d146b76d33d94548cf035233fbc2f4338c1242fa119013bead807d033fc4ae05 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.7.0" cupertino_icons: dependency: transitive description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.8" cw_core: dependency: "direct main" description: @@ -213,10 +213,10 @@ packages: dependency: transitive description: name: encrypt - sha256: "4fd4e4fdc21b9d7d4141823e1e6515cd94e7b8d84749504c232999fba25d9bbb" + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "5.0.3" fake_async: dependency: transitive description: @@ -229,26 +229,26 @@ packages: dependency: "direct main" description: name: ffi - sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.3" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -258,10 +258,10 @@ packages: dependency: "direct main" description: name: flutter_mobx - sha256: "0da4add0016387a7bf309a0d0c41d36c6b3ae25ed7a176409267f166509e723e" + sha256: "859fbf452fa9c2519d2700b125dd7fb14c508bbdd7fb65e26ca8ff6c92280e2e" url: "https://pub.dev" source: hosted - version: "2.0.6+5" + version: "2.2.1+1" flutter_test: dependency: "direct dev" description: flutter @@ -271,10 +271,10 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" glob: dependency: transitive description: @@ -287,10 +287,10 @@ packages: dependency: transitive description: name: graphs - sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.2" hive: dependency: transitive description: @@ -311,10 +311,10 @@ packages: dependency: "direct main" description: name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.2" http_multi_server: dependency: transitive description: @@ -335,10 +335,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" io: dependency: transitive description: @@ -359,42 +359,42 @@ packages: dependency: transitive description: name: json_annotation - sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "4.9.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" logging: dependency: transitive description: name: logging - sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.3.0" matcher: dependency: transitive description: @@ -407,42 +407,50 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.15.0" mime: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" mobx: dependency: "direct main" description: name: mobx - sha256: f1862bd92c6a903fab67338f27e2f731117c3cb9ea37cee1a487f9e4e0de314a + sha256: "63920b27b32ad1910adfe767ab1750e4c212e8923232a1f891597b362074ea5e" url: "https://pub.dev" source: hosted - version: "2.1.3+1" + version: "2.3.3+2" mobx_codegen: dependency: "direct dev" description: name: mobx_codegen - sha256: "86122e410d8ea24dda0c69adb5c2a6ccadd5ce02ad46e144764e0d0184a06181" + sha256: d4beb9cea4b7b014321235f8fdc7c2193ee0fe1d1198e9da7403f8bc85c4407c url: "https://pub.dev" source: hosted - version: "2.1.1" + 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: @@ -463,26 +471,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.4" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 + sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.12" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -495,42 +503,42 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" platform: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.8" pointycastle: dependency: transitive description: name: pointycastle - sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346 + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" url: "https://pub.dev" source: hosted - version: "3.6.2" + version: "3.9.1" pool: dependency: transitive description: @@ -539,38 +547,46 @@ packages: 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: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a" + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" shelf: dependency: transitive description: name: shelf - sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -580,10 +596,10 @@ packages: dependency: transitive description: name: socks5_proxy - sha256: "1d21b5606169654bbf4cfb904e8e6ed897e9f763358709f87310c757096d909a" + sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.6" source_gen: dependency: transitive description: @@ -652,10 +668,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.2" timing: dependency: transitive description: @@ -676,10 +692,10 @@ packages: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.4.0" unorm_dart: dependency: transitive description: @@ -700,10 +716,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.5" watcher: dependency: "direct overridden" description: @@ -712,38 +728,46 @@ packages: 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: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.3.0" - win32: - dependency: transitive - description: - name: win32 - sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 - url: "https://pub.dev" - source: hosted - version: "3.1.3" + version: "3.0.1" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" yaml: dependency: transitive description: name: yaml - sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" sdks: - dart: ">=3.2.0-0 <4.0.0" - flutter: ">=3.7.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/cw_haven/pubspec.yaml b/cw_haven/pubspec.yaml index d868c986d..452fed93a 100644 --- a/cw_haven/pubspec.yaml +++ b/cw_haven/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: path_provider: ^2.0.11 mobx: ^2.0.7+4 flutter_mobx: ^2.0.6+1 - intl: ^0.18.0 + intl: ^0.19.0 cw_core: path: ../cw_core diff --git a/cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux b/cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux new file mode 120000 index 000000000..a2b4915e7 --- /dev/null +++ b/cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux @@ -0,0 +1 @@ +/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 31d44ac63..551108adc 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.0" asn1lib: dependency: transitive description: @@ -77,10 +77,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.2" build_resolvers: dependency: "direct dev" description: @@ -93,10 +93,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.4.13" build_runner_core: dependency: transitive description: @@ -174,18 +174,18 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" cryptography: dependency: transitive description: @@ -253,18 +253,18 @@ packages: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -311,10 +311,10 @@ packages: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" hashlib: dependency: transitive description: @@ -375,10 +375,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" io: dependency: transitive description: @@ -407,26 +407,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" ledger_flutter_plus: dependency: "direct main" description: @@ -447,10 +447,10 @@ packages: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" matcher: dependency: transitive description: @@ -463,26 +463,26 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.15.0" mime: dependency: transitive description: name: mime - sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "2.0.0" mobx: dependency: "direct main" description: @@ -552,10 +552,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.2.12" path_provider_foundation: dependency: transitive description: @@ -600,10 +600,10 @@ packages: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -680,10 +680,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -765,10 +765,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.2" timing: dependency: transitive description: @@ -789,10 +789,10 @@ packages: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" universal_ble: dependency: transitive description: @@ -829,10 +829,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.5" watcher: dependency: "direct overridden" description: @@ -845,18 +845,26 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "0.5.1" + 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: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "3.0.1" xdg_directories: dependency: transitive description: @@ -882,5 +890,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.3.0 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index 71cbfa243..d8e506f58 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: path_provider: ^2.0.11 mobx: ^2.0.7+4 flutter_mobx: ^2.0.6+1 - intl: ^0.18.0 + intl: ^0.19.0 encrypt: ^5.0.1 polyseed: ^0.0.6 cw_core: diff --git a/cw_mweb/android/build.gradle b/cw_mweb/android/build.gradle index 7e67b98ad..0d17a47ec 100644 --- a/cw_mweb/android/build.gradle +++ b/cw_mweb/android/build.gradle @@ -2,14 +2,14 @@ group 'com.cakewallet.mweb' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.7.10' + ext.kotlin_version = '2.0.21' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.3.0' + classpath 'com.android.tools.build:gradle:8.7.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -33,15 +33,19 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 31 + compileSdkVersion 33 + + if (project.android.hasProperty("namespace")) { + namespace 'com.cakewallet.mweb' + } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } sourceSets { diff --git a/cw_mweb/android/settings.gradle b/cw_mweb/android/settings.gradle index 88fbd66fb..4a0c212c0 100644 --- a/cw_mweb/android/settings.gradle +++ b/cw_mweb/android/settings.gradle @@ -1 +1,3 @@ rootProject.name = 'cw_mweb' +id "com.android.application" version "8.3.2" apply false +id "org.jetbrains.kotlin.android" version "2.0.20" apply false diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock index dd955e344..f4d5c00f8 100644 --- a/cw_nano/pubspec.lock +++ b/cw_nano/pubspec.lock @@ -21,18 +21,18 @@ packages: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.6.0" asn1lib: dependency: transitive description: name: asn1lib - sha256: "58082b3f0dca697204dbab0ef9ff208bfaea7767ea771076af9a343488428dda" + sha256: "6b151826fcc95ff246cd219a0bf4c753ea14f4081ad71c61939becf3aba27f70" url: "https://pub.dev" source: hosted - version: "1.5.3" + version: "1.5.5" async: dependency: transitive description: @@ -77,10 +77,10 @@ packages: dependency: transitive description: name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" build_config: dependency: transitive description: @@ -93,10 +93,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.2" build_resolvers: dependency: transitive description: @@ -109,10 +109,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.4.13" build_runner_core: dependency: "direct overridden" description: @@ -190,18 +190,18 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" cryptography: dependency: transitive description: @@ -245,18 +245,18 @@ packages: dependency: "direct main" description: name: ed25519_hd_key - sha256: c5c9f11a03f5789bf9dcd9ae88d641571c802640851f1cacdb13123f171b3a26 + sha256: "31e191ec97492873067e46dc9cc0c7d55170559c83a478400feffa0627acaccf" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" encrypt: dependency: transitive description: name: encrypt - sha256: "4fd4e4fdc21b9d7d4141823e1e6515cd94e7b8d84749504c232999fba25d9bbb" + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "5.0.3" fake_async: dependency: transitive description: @@ -269,26 +269,26 @@ packages: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" fixnum_nanodart: dependency: transitive description: @@ -306,10 +306,10 @@ packages: dependency: transitive description: name: flutter_mobx - sha256: "0da4add0016387a7bf309a0d0c41d36c6b3ae25ed7a176409267f166509e723e" + sha256: "859fbf452fa9c2519d2700b125dd7fb14c508bbdd7fb65e26ca8ff6c92280e2e" url: "https://pub.dev" source: hosted - version: "2.0.6+5" + version: "2.2.1+1" flutter_test: dependency: "direct dev" description: flutter @@ -324,10 +324,10 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" glob: dependency: transitive description: @@ -340,10 +340,10 @@ packages: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" hex: dependency: "direct main" description: @@ -372,10 +372,10 @@ packages: dependency: "direct main" description: name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.2" http_multi_server: dependency: transitive description: @@ -396,10 +396,10 @@ packages: dependency: transitive description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" io: dependency: transitive description: @@ -420,34 +420,34 @@ packages: dependency: transitive description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" libcrypto: dependency: "direct main" description: @@ -460,10 +460,10 @@ packages: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" matcher: dependency: transitive description: @@ -476,26 +476,26 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.15.0" mime: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" mobx: dependency: "direct main" description: @@ -529,6 +529,14 @@ packages: url: "https://github.com/perishllc/nanoutil.git" source: git version: "1.0.3" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" package_config: dependency: transitive description: @@ -549,26 +557,26 @@ packages: dependency: transitive description: name: path_provider - sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.4" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 + sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.12" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -581,34 +589,34 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" pinenacl: dependency: transitive description: name: pinenacl - sha256: "3a5503637587d635647c93ea9a8fecf48a420cc7deebe6f1fc85c2a5637ab327" + sha256: "57e907beaacbc3c024a098910b6240758e899674de07d6949a67b52fd984cbdf" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.0" platform: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -621,10 +629,10 @@ packages: dependency: transitive description: name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" url: "https://pub.dev" source: hosted - version: "3.7.3" + version: "3.9.1" pool: dependency: transitive description: @@ -633,6 +641,14 @@ packages: 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: @@ -645,50 +661,50 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.3.0" rational: dependency: transitive description: name: rational - sha256: ba58e9e18df9abde280e8b10051e4bce85091e41e8e7e411b6cde2e738d357cf + sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336 url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.3" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "671e7a931f55a08aa45be2a13fe7247f2a41237897df434b30d2012388191833" + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.5.3" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: @@ -701,18 +717,18 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shelf: dependency: transitive description: @@ -725,10 +741,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -738,10 +754,10 @@ packages: dependency: transitive description: name: socks5_proxy - sha256: "1d21b5606169654bbf4cfb904e8e6ed897e9f763358709f87310c757096d909a" + sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.6" source_gen: dependency: transitive description: @@ -810,10 +826,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.2" timing: dependency: transitive description: @@ -834,10 +850,10 @@ packages: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" unorm_dart: dependency: transitive description: @@ -858,10 +874,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.5" watcher: dependency: "direct overridden" description: @@ -870,30 +886,38 @@ packages: 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: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.0" - win32: - dependency: transitive - description: - name: win32 - sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" - url: "https://pub.dev" - source: hosted - version: "5.5.0" + version: "3.0.1" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" yaml: dependency: transitive description: @@ -903,5 +927,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.3.0 <4.0.0" - flutter: ">=3.16.6" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index a861c6bac..7e2c3c76f 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -21,18 +21,18 @@ packages: dependency: transitive description: name: args - sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611" + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.6.0" asn1lib: dependency: transitive description: name: asn1lib - sha256: ab96a1cb3beeccf8145c52e449233fe68364c9641623acd3adad66f8184f1039 + sha256: "6b151826fcc95ff246cd219a0bf4c753ea14f4081ad71c61939becf3aba27f70" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.5" async: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" build_config: dependency: transitive description: @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" build_resolvers: dependency: "direct dev" description: @@ -85,18 +85,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.4.13" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" url: "https://pub.dev" source: hosted - version: "7.2.7" + version: "7.2.10" built_collection: dependency: transitive description: @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: built_value - sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725" + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.4.3" + version: "8.9.2" cake_backup: dependency: transitive description: @@ -134,10 +134,10 @@ packages: dependency: transitive description: name: checked_yaml - sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" clock: dependency: transitive description: @@ -150,10 +150,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.10.0" collection: dependency: transitive description: @@ -166,26 +166,26 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" crypto: dependency: transitive description: name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.6" cryptography: dependency: transitive description: name: cryptography - sha256: df156c5109286340817d21fa7b62f9140f17915077127dd70f8bd7a2a0997a35 + sha256: d146b76d33d94548cf035233fbc2f4338c1242fa119013bead807d033fc4ae05 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.7.0" cupertino_icons: dependency: transitive description: @@ -213,10 +213,10 @@ packages: dependency: "direct main" description: name: encrypt - sha256: "4fd4e4fdc21b9d7d4141823e1e6515cd94e7b8d84749504c232999fba25d9bbb" + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "5.0.3" fake_async: dependency: transitive description: @@ -229,26 +229,26 @@ packages: dependency: "direct main" description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.3" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -258,10 +258,10 @@ packages: dependency: "direct main" description: name: flutter_mobx - sha256: "0da4add0016387a7bf309a0d0c41d36c6b3ae25ed7a176409267f166509e723e" + sha256: "859fbf452fa9c2519d2700b125dd7fb14c508bbdd7fb65e26ca8ff6c92280e2e" url: "https://pub.dev" source: hosted - version: "2.0.6+5" + version: "2.2.1+1" flutter_test: dependency: "direct dev" description: flutter @@ -271,10 +271,10 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" glob: dependency: transitive description: @@ -287,26 +287,26 @@ packages: dependency: transitive description: name: graphs - sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.2" hashlib: dependency: transitive description: name: hashlib - sha256: d41795742c10947930630118c6836608deeb9047cd05aee32d2baeb697afd66a + sha256: f572f2abce09fc7aee53f15927052b9732ea1053e540af8cae211111ee0b99b1 url: "https://pub.dev" source: hosted - version: "1.19.2" + version: "1.21.0" hashlib_codecs: dependency: transitive description: name: hashlib_codecs - sha256: "2b570061f5a4b378425be28a576c1e11783450355ad4345a19f606ff3d96db0f" + sha256: "8cea9ccafcfeaa7324d2ae52c61c69f7ff71f4237507a018caab31b9e416e3b1" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.0" hive: dependency: transitive description: @@ -327,10 +327,10 @@ packages: dependency: "direct main" description: name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.2" http_multi_server: dependency: transitive description: @@ -351,10 +351,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" io: dependency: transitive description: @@ -375,42 +375,42 @@ packages: dependency: transitive description: name: json_annotation - sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "4.9.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" logging: dependency: transitive description: name: logging - sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.3.0" matcher: dependency: transitive description: @@ -423,42 +423,42 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.15.0" mime: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" mobx: dependency: "direct main" description: name: mobx - sha256: f1862bd92c6a903fab67338f27e2f731117c3cb9ea37cee1a487f9e4e0de314a + sha256: "63920b27b32ad1910adfe767ab1750e4c212e8923232a1f891597b362074ea5e" url: "https://pub.dev" source: hosted - version: "2.1.3+1" + version: "2.3.3+2" mobx_codegen: dependency: "direct dev" description: name: mobx_codegen - sha256: "86122e410d8ea24dda0c69adb5c2a6ccadd5ce02ad46e144764e0d0184a06181" + sha256: d4beb9cea4b7b014321235f8fdc7c2193ee0fe1d1198e9da7403f8bc85c4407c url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.0" monero: dependency: "direct main" description: @@ -476,6 +476,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" package_config: dependency: transitive description: @@ -496,26 +504,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.4" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 + sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.12" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -528,26 +536,26 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" platform: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -560,10 +568,10 @@ packages: dependency: transitive description: name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" url: "https://pub.dev" source: hosted - version: "3.7.3" + version: "3.9.1" polyseed: dependency: "direct main" description: @@ -580,38 +588,46 @@ packages: 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: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a" + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" shelf: dependency: transitive description: name: shelf - sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -621,10 +637,10 @@ packages: dependency: transitive description: name: socks5_proxy - sha256: "1d21b5606169654bbf4cfb904e8e6ed897e9f763358709f87310c757096d909a" + sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.6" source_gen: dependency: transitive description: @@ -693,10 +709,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.2" timing: dependency: transitive description: @@ -717,10 +733,10 @@ packages: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.4.0" unorm_dart: dependency: transitive description: @@ -741,10 +757,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.5" watcher: dependency: "direct overridden" description: @@ -753,38 +769,46 @@ packages: 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: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.3.0" - win32: - dependency: transitive - description: - name: win32 - sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 - url: "https://pub.dev" - source: hosted - version: "3.1.3" + version: "3.0.1" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" yaml: dependency: transitive description: name: yaml - sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" sdks: - dart: ">=3.2.0-0 <4.0.0" - flutter: ">=3.7.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/cw_wownero/pubspec.yaml b/cw_wownero/pubspec.yaml index 28ece82ef..48e65453a 100644 --- a/cw_wownero/pubspec.yaml +++ b/cw_wownero/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: path_provider: ^2.0.11 mobx: ^2.0.7+4 flutter_mobx: ^2.0.6+1 - intl: ^0.18.0 + intl: ^0.19.0 encrypt: ^5.0.1 polyseed: ^0.0.6 cw_core: diff --git a/how_to_add_new_wallet_type.md b/how_to_add_new_wallet_type.md index d71e181e0..74350e5d8 100644 --- a/how_to_add_new_wallet_type.md +++ b/how_to_add_new_wallet_type.md @@ -23,7 +23,7 @@ - Add the code to run the code generation needed for the files in the `cw_walletx` package to the `model_generator.sh` script - cd cw_walletx && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. + cd cw_walletx && flutter pub get && dart run build_runner build --delete-conflicting-outputs && cd .. - Add the relevant dev_dependencies for generating the files also - build_runner @@ -78,9 +78,9 @@ A `Proxy` class is used to communicate with the specific wallet package we have. ./app_config.sh - cd cw_walletx && flutter pub get && flutter packages pub run build_runner build + cd cw_walletx && flutter pub get && dart run build_runner build - flutter packages pub run build_runner build --delete-conflicting-outputs + dart run build_runner build --delete-conflicting-outputs Moving forward, our interactions with the cw_walletx package would be through the proxy class and its methods. @@ -191,9 +191,9 @@ You can add as many node entries as desired. - Run the following commands after to generate modified files in cw_core and lib - cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. + cd cw_core && flutter pub get && dart run build_runner build --delete-conflicting-outputs && cd .. - flutter packages pub run build_runner build --delete-conflicting-outputs + dart run build_runner build --delete-conflicting-outputs - Lastly, before we run the app to test what we’ve done so far, - Go to `lib/src/dashboard/widgets/menu_widget.dart` and add an icon for walletX to be used within the app. diff --git a/howto-build-android.md b/howto-build-android.md index 57d29f459..5afecfba3 100644 --- a/howto-build-android.md +++ b/howto-build-android.md @@ -8,7 +8,7 @@ The following are the system requirements to build Cake Wallet for your Android Ubuntu >= 20.04 Android SDK 29 or higher (better to have the latest one 33) Android NDK 17c -Flutter 3.19.x +Flutter 3.24.4 ``` ### 1. Installing Package Dependencies @@ -51,7 +51,7 @@ You may download and install the latest version of Android Studio [here](https:/ ### 3. Installing Flutter -Install Flutter with version `3.19.x`. For this please check section [Install Flutter manually](https://docs.flutter.dev/get-started/install/linux#install-flutter-manually). +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 @@ -66,7 +66,7 @@ Verify that the Android toolchain, Flutter, and Android Studio have been correct 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.19.x, on Linux, locale en_US.UTF-8) +[✓] 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) ``` @@ -126,17 +126,17 @@ Install Flutter package dependencies with this command: 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: -`$ flutter packages pub run tool/generate_new_secrets.dart` +`$ 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). -`$ flutter packages pub run tool/generate_android_key_properties.dart keyAlias=key storeFile=$HOME/key.jks storePassword= keyPassword=` +`$ 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. -`$ flutter packages pub run tool/generate_localization.dart` +`$ dart run tool/generate_localization.dart` Finally build mobx models for the app: diff --git a/howto-build-ios.md b/howto-build-ios.md index 544d4359e..753e17e93 100644 --- a/howto-build-ios.md +++ b/howto-build-ios.md @@ -7,7 +7,7 @@ The following are the system requirements to build Cake Wallet for your iOS devi ``` macOS >= 14.0 Xcode 15.3 -Flutter 3.19.x +Flutter 3.24.4 ``` ### 1. Installing Package Dependencies @@ -26,7 +26,7 @@ You may download and install the latest version of [Xcode](https://developer.app ### 3. Installing Flutter -Need to install flutter with version `3.19.x`. For this please check section [Install Flutter](https://docs.flutter.dev/get-started/install/macos/mobile-ios?tab=download). +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 @@ -41,7 +41,7 @@ Verify that the Flutter and Xcode have been correctly installed on your system w 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.19.x, on macOS 14.x.x) +[✓] Flutter (Channel stable, 3.24.4, on macOS 14.x.x) [✓] Xcode - develop for iOS and macOS (Xcode 15.3) ``` @@ -82,7 +82,7 @@ Install Flutter package dependencies with this command: 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: -`$ flutter packages pub run tool/generate_new_secrets.dart` +`$ dart run tool/generate_new_secrets.dart` Then we need to generate localization files and mobx models. diff --git a/howto-build-macos.md b/howto-build-macos.md index 2e535e5be..a497e1ffa 100644 --- a/howto-build-macos.md +++ b/howto-build-macos.md @@ -7,7 +7,7 @@ The following are the system requirements to build Cake Wallet for your macOS de ``` macOS >= 14.0 Xcode 15.3 -Flutter 3.19.x +Flutter 3.24.4 ``` ### 1. Installing Package Dependencies @@ -28,7 +28,7 @@ You may download and install the latest version of [Xcode](https://developer.app ### 3. Installing Flutter -Need to install flutter with version `3.19.x`. For this please check section [Install Flutter](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). +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 @@ -43,7 +43,7 @@ Verify that Flutter and Xcode have been correctly installed on your system with 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.19.x, on macOS 14.x.x) +[✓] Flutter (Channel stable, 3.24.4, on macOS 14.x.x) [✓] Xcode - develop for iOS and macOS (Xcode 15.3) ``` @@ -93,7 +93,7 @@ Install Flutter package dependencies with this command: 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: -`$ flutter packages pub run tool/generate_new_secrets.dart` +`$ dart run tool/generate_new_secrets.dart` Then we need to generate localization files and mobx models. diff --git a/howto-build-windows.md b/howto-build-windows.md index 504f8f785..3ebecaa61 100644 --- a/howto-build-windows.md +++ b/howto-build-windows.md @@ -6,12 +6,12 @@ The following are the system requirements to build CakeWallet for your Windows P ``` Windows 10 or later (64-bit), x86-64 based -Flutter 3.19.x +Flutter 3.24.4 ``` ### 1. Installing Flutter -Install Flutter with version `3.19.x`. Follow the Flutter [installation guide](https://docs.flutter.dev/get-started/install/windows). +Install Flutter with version `3.24.4`. Follow the Flutter [installation guide](https://docs.flutter.dev/get-started/install/windows). ### 2. Install Development Tools diff --git a/ios/Podfile.lock b/ios/Podfile.lock index abcac01fb..bb55b01c6 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2,39 +2,7 @@ PODS: - connectivity_plus (0.0.1): - Flutter - ReachabilitySwift - - CryptoSwift (1.8.2) - - 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 + - CryptoSwift (1.8.3) - device_display_brightness (0.0.1): - Flutter - device_info_plus (0.0.1): @@ -81,10 +49,10 @@ PODS: - flutter_inappwebview_ios (0.0.1): - Flutter - flutter_inappwebview_ios/Core (= 0.0.1) - - OrderedSet (~> 5.0) + - OrderedSet (~> 6.0.3) - flutter_inappwebview_ios/Core (0.0.1): - Flutter - - OrderedSet (~> 5.0) + - OrderedSet (~> 6.0.3) - flutter_local_authentication (1.2.0): - Flutter - flutter_mailer (0.0.1): @@ -98,7 +66,8 @@ PODS: - Flutter - integration_test (0.0.1): - Flutter - - OrderedSet (5.0.0) + - MTBBarcodeScanner (5.0.11) + - OrderedSet (6.0.3) - package_info_plus (0.4.5): - Flutter - path_provider_foundation (0.0.1): @@ -106,7 +75,7 @@ PODS: - FlutterMacOS - permission_handler_apple (9.1.1): - Flutter - - ReachabilitySwift (5.2.3) + - ReachabilitySwift (5.2.4) - SDWebImage (5.19.7): - SDWebImage/Core (= 5.19.7) - SDWebImage/Core (5.19.7) @@ -117,6 +86,7 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS + - SwiftProtobuf (1.28.2) - sp_scanner (0.0.1): - Flutter - SwiftyGif (5.4.5) @@ -136,9 +106,6 @@ 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`) @@ -158,7 +125,6 @@ DEPENDENCIES: - sensitive_clipboard (from `.symlinks/plugins/sensitive_clipboard/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sp_scanner (from `.symlinks/plugins/sp_scanner/ios`) - uni_links (from `.symlinks/plugins/uni_links/ios`) - universal_ble (from `.symlinks/plugins/universal_ble/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -179,12 +145,6 @@ 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: @@ -223,8 +183,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" - sp_scanner: - :path: ".symlinks/plugins/sp_scanner/ios" uni_links: :path: ".symlinks/plugins/uni_links/ios" universal_ble: @@ -238,10 +196,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d - CryptoSwift: c63a805d8bb5e5538e88af4e44bb537776af11ea - cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a - cw_mweb: 87af74f9659fed0c1a2cbfb44413f1070e79e3ae - cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 + CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483 device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 devicelocale: b22617f40038496deffba44747101255cee005b0 @@ -250,22 +205,24 @@ SPEC CHECKSUMS: fast_scanner: 44c00940355a51258cd6c2085734193cd23d95bc file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 + flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 flutter_local_authentication: 1172a4dd88f6306dadce067454e2c4caf07977bb flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83 flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be fluttertoast: 48c57db1b71b0ce9e6bba9f31c940ff4b001293c in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d - integration_test: 13825b8a9334a850581300559b8839134b124670 - OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c - package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 + MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb + OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 + package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 - ReachabilitySwift: 7f151ff156cea1481a8411701195ac6a984f4979 + ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3 sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 - share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad + share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + SwiftProtobuf: 4dbaffec76a39a8dc5da23b40af1a5dc01a4c02d sp_scanner: eaa617fa827396b967116b7f1f43549ca62e9a12 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 477d47f7b..0a4db4ddd 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -2,7 +2,7 @@ import UIKit import Flutter import workmanager -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/lib/locales/hausa_intl.dart b/lib/locales/hausa_intl.dart index 6cf757b60..99e47d7da 100644 --- a/lib/locales/hausa_intl.dart +++ b/lib/locales/hausa_intl.dart @@ -795,6 +795,14 @@ class HaMaterialLocalizations extends GlobalMaterialLocalizations { @override // TODO: implement shareButtonLabel String get shareButtonLabel => "shareButtonLabel"; + + @override + // TODO: implement clearButtonTooltip + String get clearButtonTooltip => "clearButtonTooltip"; + + @override + // TODO: implement selectedDateLabel + String get selectedDateLabel => "selectedDateLabel"; } /// Cupertino Support diff --git a/lib/locales/yoruba_intl.dart b/lib/locales/yoruba_intl.dart index 3c720b80e..bb2385274 100644 --- a/lib/locales/yoruba_intl.dart +++ b/lib/locales/yoruba_intl.dart @@ -794,6 +794,14 @@ class YoMaterialLocalizations extends GlobalMaterialLocalizations { @override // TODO: implement shareButtonLabel String get shareButtonLabel => "shareButtonLabel"; + + @override + // TODO: implement clearButtonTooltip + String get clearButtonTooltip => "clearButtonTooltip"; + + @override + // TODO: implement selectedDateLabel + String get selectedDateLabel => "selectedDateLabel"; } /// Cupertino Support diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index f52be7481..4b9eb3b2d 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -10,7 +10,6 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST - sp_scanner ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 42b9fa84c..52b44e53e 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,7 +6,6 @@ import FlutterMacOS import Foundation import connectivity_plus -import cw_mweb import device_info_plus import devicelocale import fast_scanner @@ -24,7 +23,6 @@ import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) - 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")) diff --git a/model_generator.sh b/model_generator.sh index 293923d1e..730817c24 100755 --- a/model_generator.sh +++ b/model_generator.sh @@ -1,15 +1,18 @@ #!/bin/bash -cd cw_core; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. -cd cw_evm; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. -cd cw_monero; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. -cd cw_bitcoin; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. -cd cw_haven; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. -cd cw_nano; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. -cd cw_bitcoin_cash; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. -cd cw_solana; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. -cd cw_tron; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. -cd cw_wownero; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. +set -x -e + +cd cw_core; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .. +cd cw_evm; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .. +cd cw_monero; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .. +cd cw_bitcoin; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .. +cd cw_haven; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .. +cd cw_nano; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .. +cd cw_bitcoin_cash; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .. +cd cw_solana; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .. +cd cw_tron; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .. +cd cw_wownero; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .. cd cw_polygon; flutter pub get; cd .. cd cw_ethereum; flutter pub get; cd .. cd cw_mweb && flutter pub get && cd .. -flutter packages pub run build_runner build --delete-conflicting-outputs +dart run build_runner build --delete-conflicting-outputs + diff --git a/pubspec_base.yaml b/pubspec_base.yaml index fa70b5837..39fe953d0 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -3,21 +3,21 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - intl: ^0.18.0 - url_launcher: ^6.1.4 + intl: ^0.19.0 + url_launcher: 6.3.1 qr_flutter: git: url: https://github.com/cake-tech/qr.flutter.git ref: cake-4.0.2 version: 4.0.2 - shared_preferences: ^2.0.15 + shared_preferences: 2.3.2 # provider: ^6.0.3 rxdart: ^0.28.0 yaml: ^3.1.1 fast_scanner: git: url: https://github.com/MrCyjaneK/fast_scanner - ref: c8311b46cc67dd02250970a54d2a4526168187f3 + ref: c5a08720216a508bf1fe3d062ad19d2836545a42 http: ^1.1.0 path_provider: ^2.0.11 mobx: ^2.1.4 @@ -29,20 +29,22 @@ dependencies: dio: ^4.0.6 hive: ^2.2.3 hive_flutter: ^1.1.0 - local_auth_android: 1.0.21 + local_auth_android: ^1.0.46 flutter_local_authentication: git: url: https://github.com/cake-tech/flutter_local_authentication package_info_plus: ^8.0.1 - devicelocale: - git: - url: https://github.com/cake-tech/flutter-devicelocale + devicelocale: 0.8.1 auto_size_text: ^3.0.0 dotted_border: ^2.0.0+2 smooth_page_indicator: ^1.0.0+2 - flutter_inappwebview: ^6.0.0 + flutter_inappwebview: ^6.1.5 flutter_spinkit: ^5.1.0 - uni_links: ^0.5.1 + uni_links: + git: + url: https://github.com/MrCyjaneK/uni_links + ref: 8e9efa4d9beb19e4ac44009576337f1ce51c22e2 + path: uni_links lottie: ^1.3.0 animate_do: ^2.1.0 cupertino_icons: ^1.0.5 @@ -63,12 +65,15 @@ dependencies: ref: master permission_handler: ^10.0.0 device_display_brightness: - git: - url: https://github.com/cake-tech/device_display_brightness.git - ref: master - workmanager: ^0.5.1 + git: + url: https://github.com/MrCyjaneK/device_display_brightness.git + ref: 4cac18c446ce686f3d75b1565badbd7da439bbd9 + workmanager: ^0.5.2 wakelock_plus: ^1.2.5 - flutter_mailer: ^2.0.2 + flutter_mailer: + git: + url: https://github.com/taljacobson/flutter_mailer + ref: 2a7d04d61f56e1ca166ab42e91e0daf1bfddfaf2 device_info_plus: ^9.1.0 base32: 2.1.3 in_app_review: ^2.0.6 @@ -77,19 +82,22 @@ dependencies: url: https://github.com/cake-tech/cake_backup.git ref: main version: 1.0.0 - flutter_plugin_android_lifecycle: 2.0.9 + flutter_plugin_android_lifecycle: 2.0.23 path_provider_android: ^2.2.1 - shared_preferences_android: 2.0.17 - url_launcher_android: 6.0.24 + shared_preferences_android: 2.3.3 + url_launcher_android: 6.3.14 url_launcher_linux: 3.1.1 # https://github.com/flutter/flutter/issues/153083 - sensitive_clipboard: ^1.0.0 + sensitive_clipboard: + git: + url: https://github.com/MrCyjaneK/sensitive_clipboard + ref: 288c7ee2d63b459bc735f7dc89321b29a1f12fae walletconnect_flutter_v2: ^2.1.4 eth_sig_util: ^0.0.9 ens_dart: git: url: https://github.com/cake-tech/ens_dart.git ref: main - fluttertoast: 8.1.4 + fluttertoast: 8.2.8 # tor: # git: # url: https://github.com/cake-tech/tor.git diff --git a/pubspec_description.yaml b/pubspec_description.yaml index b51fe96d6..928bc4f1f 100644 --- a/pubspec_description.yaml +++ b/pubspec_description.yaml @@ -4,4 +4,4 @@ version: 0.0.0 publish_to: none environment: - sdk: ">=3.1.0 <4.0.0" \ No newline at end of file + sdk: ^3.5.0 \ No newline at end of file diff --git a/run-android.sh b/run-android.sh index 880d86b6f..cb0c34038 100755 --- a/run-android.sh +++ b/run-android.sh @@ -1,5 +1,5 @@ #!/bin/bash - +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/universal_sed.sh" # Get the current git branch get_current_branch() { if git rev-parse --git-dir > /dev/null 2>&1; then @@ -15,9 +15,8 @@ get_current_branch() { update_app_properties() { local branch=$1 local file_path="./android/app.properties" - - sed -i "s/^id=.*/id=com.cakewallet.$branch/" "$file_path" - sed -i "s/^name=.*/name=$branch-Cake Wallet/" "$file_path" + universal_sed "s/^id=.*/id=com.cakewallet.$branch/" "$file_path" + universal_sed "s/^name=.*/name=$branch-Cake Wallet/" "$file_path" } # only update app.properties if getting the current branch was successful diff --git a/scripts/android/inject_app_details.sh b/scripts/android/inject_app_details.sh index 2957b91e3..737ebbb88 100755 --- a/scripts/android/inject_app_details.sh +++ b/scripts/android/inject_app_details.sh @@ -1,5 +1,5 @@ #!/bin/bash - +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/universal_sed.sh" if [ -z "$APP_ANDROID_TYPE" ]; then echo "Please set APP_ANDROID_TYPE" exit 1 @@ -7,9 +7,9 @@ fi cd ../.. set -x -sed -i "0,/version:/{s/version:.*/version: ${APP_ANDROID_VERSION}+${APP_ANDROID_BUILD_NUMBER}/}" ./pubspec.yaml -sed -i "0,/version:/{s/__APP_PACKAGE__/${APP_ANDROID_PACKAGE}/}" ./android/app/src/main/AndroidManifest.xml -sed -i "0,/__APP_SCHEME__/s/__APP_SCHEME__/${APP_ANDROID_SCHEME}/" ./android/app/src/main/AndroidManifest.xml -sed -i "0,/version:/{s/__versionCode__/${APP_ANDROID_BUILD_NUMBER}/}" ./android/app/src/main/AndroidManifest.xml -sed -i "0,/version:/{s/__versionName__/${APP_ANDROID_VERSION}/}" ./android/app/src/main/AndroidManifest.xml +universal_sed "0,/version:/{s/version:.*/version: ${APP_ANDROID_VERSION}+${APP_ANDROID_BUILD_NUMBER}/}" ./pubspec.yaml +universal_sed "0,/version:/{s/__APP_PACKAGE__/${APP_ANDROID_PACKAGE}/}" ./android/app/src/main/AndroidManifest.xml +universal_sed "0,/__APP_SCHEME__/s/__APP_SCHEME__/${APP_ANDROID_SCHEME}/" ./android/app/src/main/AndroidManifest.xml +universal_sed "0,/version:/{s/__versionCode__/${APP_ANDROID_BUILD_NUMBER}/}" ./android/app/src/main/AndroidManifest.xml +universal_sed "0,/version:/{s/__versionName__/${APP_ANDROID_VERSION}/}" ./android/app/src/main/AndroidManifest.xml cd scripts/android diff --git a/scripts/android/pubspec_gen.sh b/scripts/android/pubspec_gen.sh index 468f548f3..febc4f9e9 100755 --- a/scripts/android/pubspec_gen.sh +++ b/scripts/android/pubspec_gen.sh @@ -23,7 +23,7 @@ esac cd ../.. cp -rf pubspec_description.yaml pubspec.yaml flutter pub get -flutter pub run tool/generate_pubspec.dart +dart run tool/generate_pubspec.dart flutter pub get -flutter packages pub run tool/configure.dart $CONFIG_ARGS +dart run tool/configure.dart $CONFIG_ARGS cd scripts/android \ No newline at end of file diff --git a/scripts/ios/app_config.sh b/scripts/ios/app_config.sh index 2232491a6..8b217dc54 100755 --- a/scripts/ios/app_config.sh +++ b/scripts/ios/app_config.sh @@ -1,5 +1,6 @@ #!/bin/bash - +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/universal_sed.sh" +set -x -e MONERO_COM="monero.com" CAKEWALLET="cakewallet" HAVEN="haven" @@ -22,7 +23,7 @@ cp -rf ./ios/Runner/InfoBase.plist ./ios/Runner/Info.plist /usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:1:CFBundleURLName string ${APP_IOS_TYPE}" ./ios/Runner/Info.plist /usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:1:CFBundleURLSchemes array" ./ios/Runner/Info.plist /usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:1:CFBundleURLSchemes: string ${APP_IOS_TYPE}" ./ios/Runner/Info.plist -sed -i '' "s/PRODUCT_BUNDLE_IDENTIFIER = .*;/PRODUCT_BUNDLE_IDENTIFIER = $APP_IOS_BUNDLE_ID;/g" ./ios/Runner.xcodeproj/project.pbxproj +universal_sed "s/PRODUCT_BUNDLE_IDENTIFIER = .*;/PRODUCT_BUNDLE_IDENTIFIER = $APP_IOS_BUNDLE_ID;/g" ./ios/Runner.xcodeproj/project.pbxproj CONFIG_ARGS="" @@ -42,11 +43,12 @@ case $APP_IOS_TYPE in CONFIG_ARGS="--haven" ;; esac +CONFIG_ARGS="--monero --ethereum --polygon --nano --solana --tron --wownero" cp -rf pubspec_description.yaml pubspec.yaml flutter pub get -flutter pub run tool/generate_pubspec.dart +dart run tool/generate_pubspec.dart flutter pub get -flutter packages pub run tool/configure.dart $CONFIG_ARGS +dart run tool/configure.dart $CONFIG_ARGS cd $DIR $DIR/app_icon.sh diff --git a/scripts/linux/app_config.sh b/scripts/linux/app_config.sh index b4ca1423c..0fc41bd0f 100755 --- a/scripts/linux/app_config.sh +++ b/scripts/linux/app_config.sh @@ -1,5 +1,5 @@ #!/bin/bash - +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/universal_sed.sh" CAKEWALLET="cakewallet" DIR=`pwd` @@ -18,8 +18,8 @@ esac cp -rf pubspec_description.yaml pubspec.yaml flutter pub get -flutter pub run tool/generate_pubspec.dart +dart run tool/generate_pubspec.dart flutter pub get -flutter packages pub run tool/configure.dart $CONFIG_ARGS -sed -i '0,/version: 0.0.0/s//version: '"${APP_LINUX_VERSION}"'+'"${APP_LINUX_BUILD_NUMBER}"'/' pubspec.yaml +dart run tool/configure.dart $CONFIG_ARGS +universal_sed '0,/version: 0.0.0/s//version: '"${APP_LINUX_VERSION}"'+'"${APP_LINUX_BUILD_NUMBER}"'/' pubspec.yaml cd $DIR diff --git a/scripts/macos/app_config.sh b/scripts/macos/app_config.sh index 92a8636bd..452205dd9 100755 --- a/scripts/macos/app_config.sh +++ b/scripts/macos/app_config.sh @@ -1,4 +1,5 @@ #!/bin/bash +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/universal_sed.sh" MONERO_COM="monero.com" CAKEWALLET="cakewallet" @@ -24,11 +25,11 @@ cp -rf ./macos/Runner/DebugProfileBase.entitlements ./macos/Runner/DebugProfile. cp -rf ./macos/Runner/ReleaseBase.entitlements ./macos/Runner/Release.entitlements cp -rf ./macos/Runner/RunnerBase.entitlements ./macos/Runner/Runner.entitlements cp -rf ./macos/Runner/Configs/AppInfoBase.xcconfig ./macos/Runner/Configs/AppInfo.xcconfig -sed -i '' "s/\${BUNDLE_ID}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/DebugProfile.entitlements -sed -i '' "s/\${BUNDLE_ID}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/Release.entitlements -sed -i '' "s/\${BUNDLE_ID}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/Runner.entitlements -sed -i '' "s/\${PRODUCT_NAME}/${APP_MACOS_NAME}/g" ./macos/Runner/Configs/AppInfo.xcconfig -sed -i '' "s/PRODUCT_BUNDLE_IDENTIFIER = .*;/PRODUCT_BUNDLE_IDENTIFIER = $APP_MACOS_BUNDLE_ID;/g" ./macos/Runner/Configs/AppInfo.xcconfig +universal_sed "s/\${BUNDLE_ID}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/DebugProfile.entitlements +universal_sed "s/\${BUNDLE_ID}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/Release.entitlements +universal_sed "s/\${BUNDLE_ID}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/Runner.entitlements +universal_sed "s/\${PRODUCT_NAME}/${APP_MACOS_NAME}/g" ./macos/Runner/Configs/AppInfo.xcconfig +universal_sed "s/PRODUCT_BUNDLE_IDENTIFIER = .*;/PRODUCT_BUNDLE_IDENTIFIER = $APP_MACOS_BUNDLE_ID;/g" ./macos/Runner/Configs/AppInfo.xcconfig CONFIG_ARGS="" case $APP_MACOS_TYPE in @@ -40,8 +41,8 @@ esac cp -rf pubspec_description.yaml pubspec.yaml flutter pub get -flutter pub run tool/generate_pubspec.dart +dart run tool/generate_pubspec.dart flutter pub get -flutter packages pub run tool/configure.dart $CONFIG_ARGS +dart run tool/configure.dart $CONFIG_ARGS cd $DIR $DIR/app_icon.sh diff --git a/scripts/macos/gen_common.sh b/scripts/macos/gen_common.sh index d75ac919e..d1d40edc9 100755 --- a/scripts/macos/gen_common.sh +++ b/scripts/macos/gen_common.sh @@ -1,5 +1,5 @@ #!/bin/sh - +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/universal_sed.sh" gen_podspec() { ARCH=$1 CW_PLUGIN_DIR="`pwd`/../../cw_monero/macos" @@ -9,7 +9,7 @@ gen_podspec() { DEFAULT_FILE_PATH="${CW_PLUGIN_DIR}/${DEFAULT_FILENAME}" rm -f $DEFAULT_FILE_PATH cp $BASE_FILE_PATH $DEFAULT_FILE_PATH - gsed -i "s/#___VALID_ARCHS___#/${ARCH}/g" $DEFAULT_FILE_PATH + universal_sed "s/#___VALID_ARCHS___#/${ARCH}/g" $DEFAULT_FILE_PATH } gen_project() { @@ -17,7 +17,7 @@ gen_project() { CW_DIR="`pwd`/../../macos/Runner.xcodeproj" DEFAULT_FILENAME="project.pbxproj" DEFAULT_FILE_PATH="${CW_DIR}/${DEFAULT_FILENAME}" - gsed -i "s/ARCHS =.*/ARCHS = \"${ARCH}\";/g" $DEFAULT_FILE_PATH + universal_sed "s/ARCHS =.*/ARCHS = \"${ARCH}\";/g" $DEFAULT_FILE_PATH } gen() { diff --git a/scripts/universal_sed.sh b/scripts/universal_sed.sh new file mode 100644 index 000000000..8c0e3d9a4 --- /dev/null +++ b/scripts/universal_sed.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +detect_sed() { + if sed --version 2>/dev/null | grep -q "GNU"; then + SED_TYPE="GNU" + else + SED_TYPE="BSD" + fi +} + +universal_sed() { + local expression=$1 + local file=$2 + + if [[ "$SED_TYPE" == "GNU" ]]; then + pwd + sed -i "$expression" "$file" + else + sed -i '' "$expression" "$file" + fi +} + +detect_sed diff --git a/tool/append_translation.dart b/tool/append_translation.dart index d196421e9..5a3658cd0 100644 --- a/tool/append_translation.dart +++ b/tool/append_translation.dart @@ -2,7 +2,7 @@ import 'utils/translation/arb_file_utils.dart'; import 'utils/translation/translation_constants.dart'; import 'utils/translation/translation_utils.dart'; -/// flutter packages pub run tool/append_translation.dart "hello_world" "Hello World!" +/// dart run tool/append_translation.dart "hello_world" "Hello World!" void main(List args) async { if (args.length < 2) { diff --git a/tool/configure.dart b/tool/configure.dart index f0f79cfb1..7199b8a82 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -1424,8 +1424,7 @@ Future generatePubspec({ git: url: https://github.com/cake-tech/flutter_secure_storage.git path: flutter_secure_storage - ref: cake-8.1.0 - version: 8.1.0 + ref: ca897a08677edb443b366352dd7412735e098e7b """; const cwEthereum = """ cw_ethereum: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index ad540a359..4deae3420 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -17,6 +18,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); + FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); FlutterLocalAuthenticationPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterLocalAuthenticationPluginCApi")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 92431a6fb..f8f89611c 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST connectivity_plus + flutter_inappwebview_windows flutter_local_authentication flutter_secure_storage_windows permission_handler_windows @@ -13,7 +14,6 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST - sp_scanner ) set(PLUGIN_BUNDLED_LIBRARIES) From 367c36b4d5c2d57d98c9ec058854274517846bb3 Mon Sep 17 00:00:00 2001 From: cyan Date: Mon, 25 Nov 2024 17:00:37 +0100 Subject: [PATCH 70/73] fix sed (#1829) --- scripts/android/inject_app_details.sh | 12 ++++++------ scripts/universal_sed.sh | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/scripts/android/inject_app_details.sh b/scripts/android/inject_app_details.sh index 737ebbb88..9a8b12d9c 100755 --- a/scripts/android/inject_app_details.sh +++ b/scripts/android/inject_app_details.sh @@ -6,10 +6,10 @@ if [ -z "$APP_ANDROID_TYPE" ]; then fi cd ../.. -set -x -universal_sed "0,/version:/{s/version:.*/version: ${APP_ANDROID_VERSION}+${APP_ANDROID_BUILD_NUMBER}/}" ./pubspec.yaml -universal_sed "0,/version:/{s/__APP_PACKAGE__/${APP_ANDROID_PACKAGE}/}" ./android/app/src/main/AndroidManifest.xml -universal_sed "0,/__APP_SCHEME__/s/__APP_SCHEME__/${APP_ANDROID_SCHEME}/" ./android/app/src/main/AndroidManifest.xml -universal_sed "0,/version:/{s/__versionCode__/${APP_ANDROID_BUILD_NUMBER}/}" ./android/app/src/main/AndroidManifest.xml -universal_sed "0,/version:/{s/__versionName__/${APP_ANDROID_VERSION}/}" ./android/app/src/main/AndroidManifest.xml +universal_sed "0,/version:/ {s/version:.*/version: ${APP_ANDROID_VERSION}+${APP_ANDROID_BUILD_NUMBER}/;}" ./pubspec.yaml +universal_sed "0,/version:/ {s/__APP_PACKAGE__/${APP_ANDROID_PACKAGE}/;}" ./android/app/src/main/AndroidManifest.xml +universal_sed "0,/__APP_SCHEME__/ {s/__APP_SCHEME__/${APP_ANDROID_SCHEME}/;}" ./android/app/src/main/AndroidManifest.xml +universal_sed "0,/version:/ {s/__versionCode__/${APP_ANDROID_BUILD_NUMBER}/;}" ./android/app/src/main/AndroidManifest.xml +universal_sed "0,/version:/ {s/__versionName__/${APP_ANDROID_VERSION}/;}" ./android/app/src/main/AndroidManifest.xml + cd scripts/android diff --git a/scripts/universal_sed.sh b/scripts/universal_sed.sh index 8c0e3d9a4..d8a95684e 100644 --- a/scripts/universal_sed.sh +++ b/scripts/universal_sed.sh @@ -13,7 +13,6 @@ universal_sed() { local file=$2 if [[ "$SED_TYPE" == "GNU" ]]; then - pwd sed -i "$expression" "$file" else sed -i '' "$expression" "$file" From 59e8550e4e194b54dd828ff7064fede3b4224c84 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Mon, 25 Nov 2024 18:26:41 +0200 Subject: [PATCH 71/73] Generic fixes (#1823) * add timeout for mempool fee api and make it only in bitcoin * disable Monero Ledger for desktop * handle onramper tag issue * better handle main actions UI * make service status scrollable with a better UI * fix stupid race condition * minor handling * update btc fee api update our xmr node to use ssl * manually add supported unstoppable domains for now * change bitcoin default node code enhancement * revert debugging code [skip ci] * minor enhancements [skip ci] * increase sync indicator size [skip ci] * fix selecting USA country not triggering the reaction * fix scrolling on cake features page [skip ci] --- assets/bitcoin_electrum_server_list.yml | 3 - assets/node_list.yml | 1 + cw_bitcoin/lib/electrum_wallet.dart | 7 +- cw_monero/lib/api/wallet_manager.dart | 14 +- lib/buy/onramper/onramper_buy_provider.dart | 8 +- lib/entities/default_settings_migration.dart | 70 ++++++++-- lib/entities/main_actions.dart | 4 +- lib/entities/parse_address_from_domain.dart | 91 +++++++++--- lib/main.dart | 3 +- lib/reactions/on_current_wallet_change.dart | 2 + lib/src/screens/dashboard/dashboard_page.dart | 46 ++++--- .../dashboard/pages/cake_features_page.dart | 130 ++++++++---------- .../widgets/sync_indicator_icon.dart | 2 +- lib/src/widgets/blockchain_height_widget.dart | 12 +- lib/src/widgets/services_updates_widget.dart | 10 +- lib/store/app_store.dart | 4 +- .../cake_pay_cards_list_view_model.dart | 6 +- res/values/strings_fr.arb | 2 +- scripts/macos/app_env.sh | 8 +- 19 files changed, 271 insertions(+), 152 deletions(-) diff --git a/assets/bitcoin_electrum_server_list.yml b/assets/bitcoin_electrum_server_list.yml index 20a28cd24..83da6a0b2 100644 --- a/assets/bitcoin_electrum_server_list.yml +++ b/assets/bitcoin_electrum_server_list.yml @@ -1,6 +1,3 @@ -- - uri: electrum.cakewallet.com:50002 - useSSL: true - uri: btc-electrum.cakewallet.com:50002 useSSL: true diff --git a/assets/node_list.yml b/assets/node_list.yml index 6191129b3..49cc00a94 100644 --- a/assets/node_list.yml +++ b/assets/node_list.yml @@ -2,6 +2,7 @@ uri: xmr-node.cakewallet.com:18081 is_default: true trusted: true + useSSL: true - uri: cakexmrl7bonq7ovjka5kuwuyd3f7qnkz6z6s6dmsy3uckwra7bvggyd.onion:18081 is_default: false diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index e58fad00f..73f7e09f5 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -486,10 +486,11 @@ abstract class ElectrumWalletBase @action Future updateFeeRates() async { - if (await checkIfMempoolAPIIsEnabled()) { + if (await checkIfMempoolAPIIsEnabled() && type == WalletType.bitcoin) { try { - final response = - await http.get(Uri.parse("http://mempool.cakewallet.com:8999/api/v1/fees/recommended")); + final response = await http + .get(Uri.parse("https://mempool.cakewallet.com/api/v1/fees/recommended")) + .timeout(Duration(seconds: 5)); final result = json.decode(response.body) as Map; final slowFee = (result['economyFee'] as num?)?.toInt() ?? 0; diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart index 7f9dbd8fa..d10f7e25a 100644 --- a/cw_monero/lib/api/wallet_manager.dart +++ b/cw_monero/lib/api/wallet_manager.dart @@ -286,8 +286,18 @@ Future loadWallet( /// 0: Software Wallet /// 1: Ledger /// 2: Trezor - final deviceType = monero.WalletManager_queryWalletDevice(wmPtr, - keysFileName: "$path.keys", password: password, kdfRounds: 1); + late final deviceType; + + if (Platform.isAndroid || Platform.isIOS) { + deviceType = monero.WalletManager_queryWalletDevice( + wmPtr, + keysFileName: "$path.keys", + password: password, + kdfRounds: 1, + ); + } else { + deviceType = 0; + } if (deviceType == 1) { final dummyWPtr = wptr ?? diff --git a/lib/buy/onramper/onramper_buy_provider.dart b/lib/buy/onramper/onramper_buy_provider.dart index dade41d43..e4bf38275 100644 --- a/lib/buy/onramper/onramper_buy_provider.dart +++ b/lib/buy/onramper/onramper_buy_provider.dart @@ -251,8 +251,12 @@ class OnRamperBuyProvider extends BuyProvider { return tag; case 'POL': return 'POLYGON'; - default: - return CryptoCurrency.fromString(tag).fullName ?? tag; + default: + try { + return CryptoCurrency.fromString(tag).fullName!; + } catch (_) { + return tag; + } } } diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index f873314c0..9e0e3457d 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -260,10 +260,22 @@ Future defaultSettingsMigration( updateBtcElectrumNodeToUseSSL(nodes, sharedPreferences); break; case 43: - _updateCakeXmrNode(nodes); + await _updateCakeXmrNode(nodes); _deselectExchangeProvider(sharedPreferences, "THORChain"); _deselectExchangeProvider(sharedPreferences, "SimpleSwap"); break; + case 44: + await _updateCakeXmrNode(nodes); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.bitcoin, + newDefaultUri: newCakeWalletBitcoinUri, + currentNodePreferenceKey: PreferencesKey.currentBitcoinElectrumSererIdKey, + useSSL: true, + oldUri: 'cakewallet.com', + ); + break; default: break; @@ -279,17 +291,54 @@ Future defaultSettingsMigration( await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, version); } -void _updateCakeXmrNode(Box nodes) { +/// generic function for changing any wallet default node +/// instead of making a new function for each change +Future _changeDefaultNode({ + required Box nodes, + required SharedPreferences sharedPreferences, + required WalletType type, + required String newDefaultUri, + required String currentNodePreferenceKey, + required bool useSSL, + required String oldUri, // leave empty 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 = currentNode.uriRaw.contains(oldUri); + + if (shouldReplace) { + var newNodeId = + nodes.values.firstWhereOrNull((element) => element.uriRaw == newDefaultUri)?.key; + + // new node doesn't exist, then add it + if (newNodeId == null) { + final newNode = Node( + uri: newDefaultUri, + type: type, + useSSL: useSSL, + ); + + await nodes.add(newNode); + newNodeId = newNode.key; + } + + await sharedPreferences.setInt(currentNodePreferenceKey, newNodeId as int); + } +} + +Future _updateCakeXmrNode(Box nodes) async { final node = nodes.values.firstWhereOrNull((element) => element.uriRaw == newCakeWalletMoneroUri); - if (node != null && !node.trusted) { + if (node != null) { node.trusted = true; - node.save(); + node.useSSL = true; + await node.save(); } } void updateBtcElectrumNodeToUseSSL(Box nodes, SharedPreferences sharedPreferences) { - final btcElectrumNode = nodes.values.firstWhereOrNull((element) => element.uriRaw == newCakeWalletBitcoinUri); + final btcElectrumNode = + nodes.values.firstWhereOrNull((element) => element.uriRaw == newCakeWalletBitcoinUri); if (btcElectrumNode != null) { btcElectrumNode.useSSL = true; @@ -538,7 +587,6 @@ Node? getBitcoinCashDefaultElectrumServer({required Box nodes}) { } Node getMoneroDefaultNode({required Box nodes}) { - final timeZone = DateTime.now().timeZoneOffset.inHours; var nodeUri = newCakeWalletMoneroUri; try { @@ -858,7 +906,8 @@ Future changeDefaultMoneroNode( } }); - final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero, trusted: true); + final newCakeWalletNode = + Node(uri: newCakeWalletMoneroUri, type: WalletType.monero, trusted: true); await nodeSource.add(newCakeWalletNode); @@ -897,7 +946,7 @@ Future updateBtcNanoWalletInfos(Box walletsInfoSource) async { Future changeDefaultNanoNode( Box nodeSource, SharedPreferences sharedPreferences) async { const oldNanoNodeUriPattern = 'rpc.nano.to'; - final currentNanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); + final currentNanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final currentNanoNode = nodeSource.values.firstWhere((node) => node.key == currentNanoNodeId); final newCakeWalletNode = Node( @@ -909,7 +958,8 @@ Future changeDefaultNanoNode( await nodeSource.add(newCakeWalletNode); if (currentNanoNode.uri.toString().contains(oldNanoNodeUriPattern)) { - await sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, newCakeWalletNode.key as int); + await sharedPreferences.setInt( + PreferencesKey.currentNanoNodeIdKey, newCakeWalletNode.key as int); } } @@ -924,7 +974,7 @@ Future changeDefaultBitcoinNode( currentBitcoinNode.uri.toString().contains(cakeWalletBitcoinNodeUriPattern); final newCakeWalletBitcoinNode = - Node(uri: newCakeWalletBitcoinUri, type: WalletType.bitcoin, useSSL: false); + Node(uri: newCakeWalletBitcoinUri, type: WalletType.bitcoin, useSSL: true); if (!nodeSource.values.any((element) => element.uriRaw == newCakeWalletBitcoinUri)) { await nodeSource.add(newCakeWalletBitcoinNode); diff --git a/lib/entities/main_actions.dart b/lib/entities/main_actions.dart index 94be0d2b7..29c161c5f 100644 --- a/lib/entities/main_actions.dart +++ b/lib/entities/main_actions.dart @@ -1,7 +1,5 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.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:flutter/material.dart'; @@ -68,7 +66,7 @@ class MainActions { static MainActions tradeAction = MainActions._( - name: (context) => '${S.of(context).buy} / ${S.of(context).sell}', + name: (context) => '${S.of(context).buy}/${S.of(context).sell}', image: 'assets/images/buy_sell.png', isEnabled: (viewModel) => viewModel.isEnabledTradeAction, canShow: (viewModel) => viewModel.hasTradeAction, diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index 11ba4724c..42ab19b31 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -26,23 +26,80 @@ class AddressResolver { final SettingsStore settingsStore; static const unstoppableDomains = [ - 'crypto', - 'zil', - 'x', - 'wallet', - 'bitcoin', - '888', - 'nft', - 'dao', - 'blockchain', - 'polygon', - 'klever', - 'hi', - 'kresus', - 'anime', - 'manga', - 'binanceus', - 'xmr', + "888", + "altimist", + "anime", + "austin", + "bald", + "benji", + "bet", + "binanceus", + "bitcoin", + "bitget", + "blockchain", + "ca", + "chomp", + "clay", + "co", + "com", + "crypto", + "dao", + "dfz", + "digital", + "dream", + "eth", + "ethermail", + "farms", + "fun", + "go", + "group", + "hi", + "host", + "info", + "io", + "klever", + "kresus", + "kryptic", + "lfg", + "life", + "live", + "ltd", + "manga", + "metropolis", + "moon", + "mumu", + "net", + "nft", + "online", + "org", + "pog", + "polygon", + "press", + "pro", + "propykeys", + "pudgy", + "pw", + "raiin", + "secret", + "site", + "smobler", + "space", + "stepn", + "store", + "tball", + "tech", + "ubu", + "uno", + "unstoppable", + "wallet", + "website", + "wifi", + "witg", + "wrkx", + "x", + "xmr", + "xyz", + "zil", ]; static String? extractAddressByType({required String raw, required CryptoCurrency type}) { diff --git a/lib/main.dart b/lib/main.dart index 51fab4dd1..d67fda098 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,7 +16,6 @@ import 'package:cake_wallet/exchange/exchange_template.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/locales/locale.dart'; -import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/reactions/bootstrap.dart'; import 'package:cake_wallet/router.dart' as Router; import 'package:cake_wallet/routes.dart'; @@ -204,7 +203,7 @@ Future initializeAppConfigs() async { transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 43, + initialMigrationVersion: 44, ); } diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index d7e6f90c0..b804ff14e 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -63,6 +63,8 @@ void startCurrentWalletChangeReaction( startWalletSyncStatusChangeReaction(wallet, fiatConversionStore); startCheckConnectionReaction(wallet, settingsStore); + await Future.delayed(Duration.zero); + if (wallet.type == WalletType.monero || wallet.type == WalletType.wownero || wallet.type == WalletType.bitcoin || diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index 9c012d518..cabf73b21 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -291,32 +291,34 @@ class _DashboardPageView extends BasePage { children: MainActions.all .where((element) => element.canShow?.call(dashboardViewModel) ?? true) .map( - (action) => Semantics( - button: true, - enabled: (action.isEnabled?.call(dashboardViewModel) ?? true), - child: ActionButton( - key: ValueKey( - 'dashboard_page_${action.name(context)}_action_button_key'), - image: Image.asset( - action.image, - height: 24, - width: 24, - color: action.isEnabled?.call(dashboardViewModel) ?? true - ? Theme.of(context) - .extension()! - .mainActionsIconColor + (action) => Expanded( + child: Semantics( + button: true, + enabled: (action.isEnabled?.call(dashboardViewModel) ?? true), + child: ActionButton( + key: ValueKey( + 'dashboard_page_${action.name(context)}_action_button_key'), + image: Image.asset( + action.image, + height: 24, + width: 24, + color: action.isEnabled?.call(dashboardViewModel) ?? true + ? Theme.of(context) + .extension()! + .mainActionsIconColor + : Theme.of(context) + .extension()! + .labelTextColor, + ), + title: action.name(context), + onClick: () async => + await action.onTap(context, dashboardViewModel), + textColor: action.isEnabled?.call(dashboardViewModel) ?? true + ? null : Theme.of(context) .extension()! .labelTextColor, ), - title: action.name(context), - onClick: () async => - await action.onTap(context, dashboardViewModel), - textColor: action.isEnabled?.call(dashboardViewModel) ?? true - ? null - : Theme.of(context) - .extension()! - .labelTextColor, ), ), ) diff --git a/lib/src/screens/dashboard/pages/cake_features_page.dart b/lib/src/screens/dashboard/pages/cake_features_page.dart index 37bc3a55f..775cb6c3f 100644 --- a/lib/src/screens/dashboard/pages/cake_features_page.dart +++ b/lib/src/screens/dashboard/pages/cake_features_page.dart @@ -10,91 +10,81 @@ import 'package:cake_wallet/view_model/dashboard/cake_features_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:flutter_svg/flutter_svg.dart'; class CakeFeaturesPage extends StatelessWidget { CakeFeaturesPage({required this.dashboardViewModel, required this.cakeFeaturesViewModel}); final DashboardViewModel dashboardViewModel; final CakeFeaturesViewModel cakeFeaturesViewModel; - final _scrollController = ScrollController(); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 10.0), - child: RawScrollbar( - thumbColor: Colors.white.withOpacity(0.15), - radius: Radius.circular(20), - thumbVisibility: true, - thickness: 2, - controller: _scrollController, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 50), - Text( - 'Cake ${S.of(context).features}', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.pageTitleTextColor, - ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 50), + Text( + 'Cake ${S.of(context).features}', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.pageTitleTextColor, ), - Expanded( - child: ListView( - controller: _scrollController, - children: [ - SizedBox(height: 20), - DashBoardRoundedCardWidget( - onTap: () => _navigatorToGiftCardsPage(context), - title: 'Cake Pay', - subTitle: S.of(context).cake_pay_subtitle, - image: Image.asset( - 'assets/images/cards.png', - height: 100, - width: 115, - fit: BoxFit.cover, - ), + ), + Expanded( + child: ListView( + children: [ + SizedBox(height: 20), + DashBoardRoundedCardWidget( + onTap: () => _navigatorToGiftCardsPage(context), + title: 'Cake Pay', + subTitle: S.of(context).cake_pay_subtitle, + image: Image.asset( + 'assets/images/cards.png', + height: 100, + width: 115, + fit: BoxFit.cover, ), - SizedBox(height: 10), - DashBoardRoundedCardWidget( - onTap: () => _launchUrl("cake.nano-gpt.com"), - title: "NanoGPT", - subTitle: S.of(context).nanogpt_subtitle, - image: Image.asset( - 'assets/images/nanogpt.png', - height: 80, - width: 80, - fit: BoxFit.cover, - ), + ), + SizedBox(height: 10), + DashBoardRoundedCardWidget( + onTap: () => _launchUrl("cake.nano-gpt.com"), + title: "NanoGPT", + subTitle: S.of(context).nanogpt_subtitle, + image: Image.asset( + 'assets/images/nanogpt.png', + height: 80, + width: 80, + fit: BoxFit.cover, ), - SizedBox(height: 10), - Observer( - builder: (context) { - if (!dashboardViewModel.hasSignMessages) { - return const SizedBox(); - } - return DashBoardRoundedCardWidget( - onTap: () => Navigator.of(context).pushNamed(Routes.signPage), - title: S.current.sign_verify_message, - subTitle: S.current.sign_verify_message_sub, - icon: Icon( - Icons.speaker_notes_rounded, - color: - Theme.of(context).extension()!.pageTitleTextColor, - size: 75, - ), - ); - }, - ), - ], - ), + ), + SizedBox(height: 10), + Observer( + builder: (context) { + if (!dashboardViewModel.hasSignMessages) { + return const SizedBox(); + } + return DashBoardRoundedCardWidget( + onTap: () => Navigator.of(context).pushNamed(Routes.signPage), + title: S.current.sign_verify_message, + subTitle: S.current.sign_verify_message_sub, + icon: Icon( + Icons.speaker_notes_rounded, + color: + Theme.of(context).extension()!.pageTitleTextColor, + size: 75, + ), + ); + }, + ), + ], ), - ], - ), + ), + ], ), ), ); diff --git a/lib/src/screens/dashboard/widgets/sync_indicator_icon.dart b/lib/src/screens/dashboard/widgets/sync_indicator_icon.dart index 21133a438..aca3231ec 100644 --- a/lib/src/screens/dashboard/widgets/sync_indicator_icon.dart +++ b/lib/src/screens/dashboard/widgets/sync_indicator_icon.dart @@ -8,7 +8,7 @@ class SyncIndicatorIcon extends StatelessWidget { {this.boolMode = true, this.isSynced = false, this.value = waiting, - this.size = 4.0}); + this.size = 6.0}); final bool boolMode; final bool isSynced; diff --git a/lib/src/widgets/blockchain_height_widget.dart b/lib/src/widgets/blockchain_height_widget.dart index 1c6b6da02..650ee684d 100644 --- a/lib/src/widgets/blockchain_height_widget.dart +++ b/lib/src/widgets/blockchain_height_widget.dart @@ -191,11 +191,13 @@ class BlockchainHeightState extends State { height = wownero!.getHeightByDate(date: date); } } - setState(() { - dateController.text = DateFormat('yyyy-MM-dd').format(date); - restoreHeightController.text = '$height'; - _changeHeight(height); - }); + if (mounted) { + setState(() { + dateController.text = DateFormat('yyyy-MM-dd').format(date); + restoreHeightController.text = '$height'; + _changeHeight(height); + }); + } } } diff --git a/lib/src/widgets/services_updates_widget.dart b/lib/src/widgets/services_updates_widget.dart index 1787b7118..17f231303 100644 --- a/lib/src/widgets/services_updates_widget.dart +++ b/lib/src/widgets/services_updates_widget.dart @@ -92,15 +92,17 @@ class _ServicesUpdatesWidgetState extends State { ); } return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 20), - child: Stack( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Column( children: [ - body, + Expanded(child: body), Align( alignment: Alignment.bottomCenter, child: Padding( padding: EdgeInsets.symmetric( - horizontal: MediaQuery.of(context).size.width / 8), + horizontal: MediaQuery.of(context).size.width / 8, + vertical: 20, + ), child: PrimaryImageButton( onPressed: () { try { diff --git a/lib/store/app_store.dart b/lib/store/app_store.dart index 24d5dc6a4..ff3ba0535 100644 --- a/lib/store/app_store.dart +++ b/lib/store/app_store.dart @@ -50,8 +50,8 @@ abstract class AppStoreBase with Store { getIt.get().create(); await getIt.get().init(); } - await getIt.get().setString(PreferencesKey.currentWalletName, wallet.name); - await getIt + getIt.get().setString(PreferencesKey.currentWalletName, wallet.name); + getIt .get() .setInt(PreferencesKey.currentWalletType, serializeToInt(wallet.type)); } diff --git a/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart b/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart index 8585da9da..442bd51b4 100644 --- a/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart +++ b/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart @@ -204,7 +204,11 @@ abstract class CakePayCardsListViewModelBase with Store { } @action - void setSelectedCountry(Country country) => settingsStore.selectedCakePayCountry = country; + void setSelectedCountry(Country country) { + // just so it triggers the reaction even when selecting the default country + settingsStore.selectedCakePayCountry = null; + settingsStore.selectedCakePayCountry = country; + } @action void togglePrepaidCards() => displayPrepaidCards = !displayPrepaidCards; diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 549ec5275..741adc472 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -919,7 +919,7 @@ "wallet_seed_legacy": "Graine de portefeuille hérité", "wallet_store_monero_wallet": "Portefeuille (Wallet) Monero", "walletConnect": "WalletConnect", - "wallets": "Portefeuilles (Wallets)", + "wallets": "Portefeuilles", "warning": "Avertissement", "welcome": "Bienvenue sur", "welcome_to_cakepay": "Bienvenue sur Cake Pay !", diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index 8f3b68ab5..930a1e5ed 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.8.0" -MONERO_COM_BUILD_NUMBER=36 +MONERO_COM_VERSION="1.8.1" +MONERO_COM_BUILD_NUMBER=37 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.14.0" -CAKEWALLET_BUILD_NUMBER=95 +CAKEWALLET_VERSION="1.14.1" +CAKEWALLET_BUILD_NUMBER=96 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then From 8ceaa013ebfd6edb00b80d6fb46dc74f8e1e1ab7 Mon Sep 17 00:00:00 2001 From: cyan Date: Mon, 25 Nov 2024 19:55:05 +0100 Subject: [PATCH 72/73] Fix sed (again) (#1830) * fix sed * 0, -> 1, so it doesn't silently error out * remve hardcoded CONFIG_ARGS --- scripts/android/inject_app_details.sh | 10 +++++----- scripts/ios/app_config.sh | 1 - 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/scripts/android/inject_app_details.sh b/scripts/android/inject_app_details.sh index 9a8b12d9c..2e50ea31b 100755 --- a/scripts/android/inject_app_details.sh +++ b/scripts/android/inject_app_details.sh @@ -6,10 +6,10 @@ if [ -z "$APP_ANDROID_TYPE" ]; then fi cd ../.. -universal_sed "0,/version:/ {s/version:.*/version: ${APP_ANDROID_VERSION}+${APP_ANDROID_BUILD_NUMBER}/;}" ./pubspec.yaml -universal_sed "0,/version:/ {s/__APP_PACKAGE__/${APP_ANDROID_PACKAGE}/;}" ./android/app/src/main/AndroidManifest.xml -universal_sed "0,/__APP_SCHEME__/ {s/__APP_SCHEME__/${APP_ANDROID_SCHEME}/;}" ./android/app/src/main/AndroidManifest.xml -universal_sed "0,/version:/ {s/__versionCode__/${APP_ANDROID_BUILD_NUMBER}/;}" ./android/app/src/main/AndroidManifest.xml -universal_sed "0,/version:/ {s/__versionName__/${APP_ANDROID_VERSION}/;}" ./android/app/src/main/AndroidManifest.xml +universal_sed "1,/version:/ {s/version:.*/version: ${APP_ANDROID_VERSION}+${APP_ANDROID_BUILD_NUMBER}/;}" ./pubspec.yaml +universal_sed "1,/version:/ {s/__APP_PACKAGE__/${APP_ANDROID_PACKAGE}/;}" ./android/app/src/main/AndroidManifest.xml +universal_sed "1,/__APP_SCHEME__/ {s/__APP_SCHEME__/${APP_ANDROID_SCHEME}/;}" ./android/app/src/main/AndroidManifest.xml +universal_sed "1,/version:/ {s/__versionCode__/${APP_ANDROID_BUILD_NUMBER}/;}" ./android/app/src/main/AndroidManifest.xml +universal_sed "1,/version:/ {s/__versionName__/${APP_ANDROID_VERSION}/;}" ./android/app/src/main/AndroidManifest.xml cd scripts/android diff --git a/scripts/ios/app_config.sh b/scripts/ios/app_config.sh index 8b217dc54..396ccd7f0 100755 --- a/scripts/ios/app_config.sh +++ b/scripts/ios/app_config.sh @@ -43,7 +43,6 @@ case $APP_IOS_TYPE in CONFIG_ARGS="--haven" ;; esac -CONFIG_ARGS="--monero --ethereum --polygon --nano --solana --tron --wownero" cp -rf pubspec_description.yaml pubspec.yaml flutter pub get From 83cfc7dd703c04613c6b83f2a9234a3c98229660 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Mon, 25 Nov 2024 21:05:36 +0200 Subject: [PATCH 73/73] fix building with haven --- cw_haven/android/build.gradle | 18 +++++++++++++++--- cw_shared_external/android/build.gradle | 15 ++++++++++++++- linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 ++ pubspec_base.yaml | 2 +- scripts/android/inject_app_details.sh | 2 +- windows/flutter/generated_plugins.cmake | 1 + 7 files changed, 35 insertions(+), 6 deletions(-) diff --git a/cw_haven/android/build.gradle b/cw_haven/android/build.gradle index d29c31d4e..8eb728a67 100644 --- a/cw_haven/android/build.gradle +++ b/cw_haven/android/build.gradle @@ -2,14 +2,14 @@ group 'com.cakewallet.cw_haven' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.7.10' + ext.kotlin_version = '2.0.21' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.3.0' + classpath 'com.android.tools.build:gradle:8.7.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -25,8 +25,20 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 28 + 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' } diff --git a/cw_shared_external/android/build.gradle b/cw_shared_external/android/build.gradle index 64b550364..8d2b1b13d 100644 --- a/cw_shared_external/android/build.gradle +++ b/cw_shared_external/android/build.gradle @@ -25,7 +25,20 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 30 + compileSdkVersion 33 + + if (project.android.hasProperty("namespace")) { + namespace 'com.cakewallet.cw_shared_external' + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = '17' + } sourceSets { main.java.srcDirs += 'src/main/kotlin' diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 4b9eb3b2d..f52be7481 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + sp_scanner ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 52b44e53e..42b9fa84c 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import connectivity_plus +import cw_mweb import device_info_plus import devicelocale import fast_scanner @@ -23,6 +24,7 @@ import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) + 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")) diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 39fe953d0..221f1d9bf 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -26,7 +26,7 @@ dependencies: share_plus: ^10.0.0 # date_range_picker: ^1.0.6 #https://api.flutter.dev/flutter/material/showDateRangePicker.html - dio: ^4.0.6 + dio: ^5.7.0 hive: ^2.2.3 hive_flutter: ^1.1.0 local_auth_android: ^1.0.46 diff --git a/scripts/android/inject_app_details.sh b/scripts/android/inject_app_details.sh index 2e50ea31b..7b0d74798 100755 --- a/scripts/android/inject_app_details.sh +++ b/scripts/android/inject_app_details.sh @@ -6,10 +6,10 @@ if [ -z "$APP_ANDROID_TYPE" ]; then fi cd ../.. +set -x universal_sed "1,/version:/ {s/version:.*/version: ${APP_ANDROID_VERSION}+${APP_ANDROID_BUILD_NUMBER}/;}" ./pubspec.yaml universal_sed "1,/version:/ {s/__APP_PACKAGE__/${APP_ANDROID_PACKAGE}/;}" ./android/app/src/main/AndroidManifest.xml universal_sed "1,/__APP_SCHEME__/ {s/__APP_SCHEME__/${APP_ANDROID_SCHEME}/;}" ./android/app/src/main/AndroidManifest.xml universal_sed "1,/version:/ {s/__versionCode__/${APP_ANDROID_BUILD_NUMBER}/;}" ./android/app/src/main/AndroidManifest.xml universal_sed "1,/version:/ {s/__versionName__/${APP_ANDROID_VERSION}/;}" ./android/app/src/main/AndroidManifest.xml - cd scripts/android diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index f8f89611c..e0f2c11c0 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -14,6 +14,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + sp_scanner ) set(PLUGIN_BUNDLED_LIBRARIES)