diff --git a/.github/assets/Logo_CakeWallet.png b/.github/assets/Logo_CakeWallet.png index 459a6b37c..9c9d52d1a 100644 Binary files a/.github/assets/Logo_CakeWallet.png and b/.github/assets/Logo_CakeWallet.png differ diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 6a7444745..c55148f4c 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -9,7 +9,7 @@ jobs: PR_test_build: runs-on: linux-amd64 container: - image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly + image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-ndkr28-go1.24.1-ruststablenightly env: STORE_PASS: test@cake_wallet KEY_PASS: test@cake_wallet diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 9a167d0c0..6a5e10ee3 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -9,7 +9,7 @@ jobs: PR_test_build: runs-on: linux-amd64 container: - image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly + image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-ndkr28-go1.24.1-ruststablenightly env: STORE_PASS: test@cake_wallet KEY_PASS: test@cake_wallet diff --git a/Dockerfile b/Dockerfile index 151b7af20..f03ba1815 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# docker buildx build --push --pull --platform linux/amd64,linux/arm64 . -f Dockerfile -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly +# docker buildx build --push --pull --platform linux/amd64,linux/arm64 . -f Dockerfile -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-ndkr28-go1.24.1-ruststablenightly # Heavily inspired by cirrusci images # https://github.com/cirruslabs/docker-images-android/blob/master/sdk/tools/Dockerfile @@ -15,7 +15,7 @@ LABEL org.opencontainers.image.source=https://github.com/cake-tech/cake_wallet ENV GOLANG_VERSION=1.24.1 # Pin Flutter version to latest known-working version -ENV FLUTTER_VERSION=3.27.0 +ENV FLUTTER_VERSION=3.27.4 # Pin Android Studio, platform, and build tools versions to latest known-working version # Comes from https://developer.android.com/studio/#command-tools @@ -138,11 +138,12 @@ RUN ARCH=$(uname -m) && \ "build-tools;35.0.0" # Install extra NDK dependency for sp_scanner -ENV ANDROID_NDK_VERSION=27.2.12479018 +ENV ANDROID_NDK_VERSION=28.2.13676358 RUN ARCH=$(uname -m) && \ if [ "$ARCH" != "x86_64" ]; then exit 0; fi \ && yes | sdkmanager "ndk;$ANDROID_NDK_VERSION" \ - "ndk;27.0.12077973" + "ndk;27.0.12077973" \ + "ndk;27.2.12479018" # Install dependencies for tests # Comes from https://github.com/ReactiveCircus/android-emulator-runner diff --git a/android/app/build.gradle b/android/app/build.gradle index 29ae280f6..0b009085b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -94,7 +94,7 @@ android { } } - ndkVersion "27.0.12077973" + ndkVersion "28.2.13676358" } flutter { diff --git a/assets/banano_node_list.yml b/assets/banano_node_list.yml index dc6852867..a7178963f 100644 --- a/assets/banano_node_list.yml +++ b/assets/banano_node_list.yml @@ -2,4 +2,5 @@ uri: kaliumapi.appditto.com path: /api useSSL: true - is_default: true \ No newline at end of file + is_default: true + isEnabledForAutoSwitching: true \ No newline at end of file diff --git a/assets/bitcoin_cash_electrum_server_list.yml b/assets/bitcoin_cash_electrum_server_list.yml index 948e5f3dc..770bdbc05 100644 --- a/assets/bitcoin_cash_electrum_server_list.yml +++ b/assets/bitcoin_cash_electrum_server_list.yml @@ -2,9 +2,11 @@ uri: bitcoincash.stackwallet.com:50002 is_default: true useSSL: true + isEnabledForAutoSwitching: true - uri: bch.aftrek.org:50002 useSSL: true + isEnabledForAutoSwitching: true - uri: node.minisatoshi.cash:50002 useSSL: true diff --git a/assets/bitcoin_electrum_server_list.yml b/assets/bitcoin_electrum_server_list.yml index 83da6a0b2..c3a92e948 100644 --- a/assets/bitcoin_electrum_server_list.yml +++ b/assets/bitcoin_electrum_server_list.yml @@ -2,8 +2,14 @@ uri: btc-electrum.cakewallet.com:50002 useSSL: true isDefault: true + isEnabledForAutoSwitching: true - uri: electrs.cakewallet.com:50001 - uri: fulcrum.sethforprivacy.com:50002 useSSL: true + isEnabledForAutoSwitching: true +- + uri: electrum.cakewallet.com:50002 + useSSL: true + isEnabledForAutoSwitching: true diff --git a/assets/decred_node_list.yml b/assets/decred_node_list.yml index cb171e701..9f5938e8c 100644 --- a/assets/decred_node_list.yml +++ b/assets/decred_node_list.yml @@ -1,6 +1,8 @@ - uri: default-spv-nodes is_default: true + isEnabledForAutoSwitching: true - uri: dcrd.sethforprivacy.com:9108 - useSSL: true \ No newline at end of file + useSSL: true + isEnabledForAutoSwitching: true \ No newline at end of file diff --git a/assets/ethereum_server_list.yml b/assets/ethereum_server_list.yml index ed425c3c7..2eae58bc5 100644 --- a/assets/ethereum_server_list.yml +++ b/assets/ethereum_server_list.yml @@ -2,6 +2,7 @@ uri: ethereum-rpc.publicnode.com useSSL: true isDefault: true + isEnabledForAutoSwitching: true - uri: eth.llamarpc.com - @@ -10,5 +11,6 @@ uri: eth-mainnet.public.blastapi.io - uri: eth.nownodes.io + isEnabledForAutoSwitching: true - uri: ethereum.publicnode.com \ No newline at end of file diff --git a/assets/images/deps_icon.png b/assets/images/deps_icon.png new file mode 100644 index 000000000..c281b43b9 Binary files /dev/null and b/assets/images/deps_icon.png differ diff --git a/assets/images/ndeps_icon.png b/assets/images/ndeps_icon.png new file mode 100644 index 000000000..be8287bc1 Binary files /dev/null and b/assets/images/ndeps_icon.png differ diff --git a/assets/images/restore_backup.png b/assets/images/restore_backup.png new file mode 100644 index 000000000..01157f0e6 Binary files /dev/null and b/assets/images/restore_backup.png differ diff --git a/assets/images/restore_backup_dark.png b/assets/images/restore_backup_dark.png new file mode 100644 index 000000000..7029a86da Binary files /dev/null and b/assets/images/restore_backup_dark.png differ diff --git a/assets/images/restore_cupcake.png b/assets/images/restore_cupcake.png new file mode 100644 index 000000000..b05b3a6a7 Binary files /dev/null and b/assets/images/restore_cupcake.png differ diff --git a/assets/images/restore_cupcake_dark.png b/assets/images/restore_cupcake_dark.png new file mode 100644 index 000000000..48fe965cb Binary files /dev/null and b/assets/images/restore_cupcake_dark.png differ diff --git a/assets/images/restore_hot_wallet.png b/assets/images/restore_hot_wallet.png new file mode 100644 index 000000000..61fefeca5 Binary files /dev/null and b/assets/images/restore_hot_wallet.png differ diff --git a/assets/images/restore_hot_wallet_dark.png b/assets/images/restore_hot_wallet_dark.png new file mode 100644 index 000000000..0790c210a Binary files /dev/null and b/assets/images/restore_hot_wallet_dark.png differ diff --git a/assets/images/restore_hw.png b/assets/images/restore_hw.png new file mode 100644 index 000000000..4636c57e4 Binary files /dev/null and b/assets/images/restore_hw.png differ diff --git a/assets/images/restore_hw_dark.png b/assets/images/restore_hw_dark.png new file mode 100644 index 000000000..d6023d8f1 Binary files /dev/null and b/assets/images/restore_hw_dark.png differ diff --git a/assets/images/restore_qr.png b/assets/images/restore_qr.png index 5f8cffb95..0f99d2f56 100644 Binary files a/assets/images/restore_qr.png and b/assets/images/restore_qr.png differ diff --git a/assets/images/restore_qr_dark.png b/assets/images/restore_qr_dark.png new file mode 100644 index 000000000..cb33cfe17 Binary files /dev/null and b/assets/images/restore_qr_dark.png differ diff --git a/assets/litecoin_electrum_server_list.yml b/assets/litecoin_electrum_server_list.yml index 550b900e1..18b982aa1 100644 --- a/assets/litecoin_electrum_server_list.yml +++ b/assets/litecoin_electrum_server_list.yml @@ -2,9 +2,11 @@ uri: ltc-electrum.cakewallet.com:50002 useSSL: true isDefault: true + isEnabledForAutoSwitching: true - uri: litecoin.stackwallet.com:20063 useSSL: true + isEnabledForAutoSwitching: true - uri: electrum-ltc.bysh.me:50002 useSSL: true diff --git a/assets/nano_node_list.yml b/assets/nano_node_list.yml index cda931b5e..aa1d8e3c9 100644 --- a/assets/nano_node_list.yml +++ b/assets/nano_node_list.yml @@ -1,14 +1,17 @@ - uri: nano.nownodes.io useSSL: true + isEnabledForAutoSwitching: true - uri: rpc.nano.to is_default: true useSSL: true + isEnabledForAutoSwitching: true - uri: node.nautilus.io path: /api useSSL: true + isEnabledForAutoSwitching: true - uri: app.natrium.io path: /api diff --git a/assets/nano_pow_node_list.yml b/assets/nano_pow_node_list.yml index 3bbc7c3fb..ec1b10ff9 100644 --- a/assets/nano_pow_node_list.yml +++ b/assets/nano_pow_node_list.yml @@ -2,8 +2,11 @@ uri: rpc.nano.to useSSL: true is_default: true + isEnabledForAutoSwitching: true - uri: workers.perish.co + isEnabledForAutoSwitching: true - uri: worker.nanoriver.cc useSSL: true + isEnabledForAutoSwitching: true diff --git a/assets/node_list.yml b/assets/node_list.yml index 917dadfd9..851d69042 100644 --- a/assets/node_list.yml +++ b/assets/node_list.yml @@ -3,6 +3,7 @@ is_default: true trusted: true useSSL: true + isEnabledForAutoSwitching: true - uri: cakexmrl7bonq7ovjka5kuwuyd3f7qnkz6z6s6dmsy3uckwra7bvggyd.onion:18081 is_default: false @@ -10,6 +11,8 @@ uri: node.sethforprivacy.com:443 useSSL: true is_default: false + isEnabledForAutoSwitching: true - uri: nodes.hashvault.pro:18081 is_default: false + isEnabledForAutoSwitching: true diff --git a/assets/polygon_node_list.yml b/assets/polygon_node_list.yml index 3b2cdcdc3..74dc23ef4 100644 --- a/assets/polygon_node_list.yml +++ b/assets/polygon_node_list.yml @@ -4,7 +4,9 @@ uri: polygon-bor-rpc.publicnode.com useSSL: true isDefault: true + isEnabledForAutoSwitching: true - uri: polygon.llamarpc.com - - uri: matic.nownodes.io \ No newline at end of file + uri: matic.nownodes.io + isEnabledForAutoSwitching: true \ No newline at end of file diff --git a/assets/solana_node_list.yml b/assets/solana_node_list.yml index 3ba74d980..e1d7971c6 100644 --- a/assets/solana_node_list.yml +++ b/assets/solana_node_list.yml @@ -4,10 +4,13 @@ - uri: api.mainnet-beta.solana.com:443 useSSL: true + isEnabledForAutoSwitching: true - uri: solana-rpc.publicnode.com:443 useSSL: true + isEnabledForAutoSwitching: true - uri: solana-mainnet.core.chainstack.com useSSL: true - is_default: true \ No newline at end of file + is_default: true + isEnabledForAutoSwitching: true \ No newline at end of file diff --git a/assets/tron_node_list.yml b/assets/tron_node_list.yml index 1e34de712..595bba971 100644 --- a/assets/tron_node_list.yml +++ b/assets/tron_node_list.yml @@ -6,6 +6,8 @@ uri: api.trongrid.io is_default: true useSSL: true + isEnabledForAutoSwitching: true - uri: trx.nownodes.io - useSSL: true \ No newline at end of file + useSSL: true + isEnabledForAutoSwitching: true \ No newline at end of file diff --git a/assets/wownero_node_list.yml b/assets/wownero_node_list.yml index a4873b3e4..893f82984 100644 --- a/assets/wownero_node_list.yml +++ b/assets/wownero_node_list.yml @@ -2,10 +2,12 @@ uri: node3.monerodevs.org:34568 is_default: true useSSL: false + isEnabledForAutoSwitching: true - uri: node2.monerodevs.org:34568 is_default: false useSSL: false + isEnabledForAutoSwitching: true - uri: node.monerodevs.org:34568 is_default: false diff --git a/assets/zano_node_list.yml b/assets/zano_node_list.yml index f7b874fcb..26a1b797d 100644 --- a/assets/zano_node_list.yml +++ b/assets/zano_node_list.yml @@ -1,7 +1,9 @@ - uri: 37.27.100.59:10500 useSSL: false + isEnabledForAutoSwitching: true - uri: zano.cakewallet.com:11211 is_default: true - useSSL: false \ No newline at end of file + useSSL: false + isEnabledForAutoSwitching: true \ 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 07083e111..8491ae8e3 100644 --- a/cw_bitcoin/lib/bitcoin_receive_page_option.dart +++ b/cw_bitcoin/lib/bitcoin_receive_page_option.dart @@ -28,6 +28,16 @@ class BitcoinReceivePageOption implements ReceivePageOption { BitcoinReceivePageOption.p2pkh ]; + static const allViewOnly = [ + BitcoinReceivePageOption.p2wpkh, + // TODO: uncomment this after we properly derive keys and not use m/84 for + // all of them (as this breaks cupcake) + // BitcoinReceivePageOption.p2tr, + // BitcoinReceivePageOption.p2wsh, + // BitcoinReceivePageOption.p2sh, + // BitcoinReceivePageOption.p2pkh + ]; + static const allLitecoin = [ BitcoinReceivePageOption.p2wpkh, BitcoinReceivePageOption.mweb, diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index d286df424..a5c209e91 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -21,9 +21,11 @@ import 'package:cw_bitcoin/psbt/v0_deserialize.dart'; import 'package:cw_bitcoin/psbt/v0_finalizer.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/encryption_file_utils.dart'; +import 'package:cw_core/output_info.dart'; import 'package:cw_core/payjoin_session.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; import 'package:flutter/foundation.dart'; @@ -32,6 +34,9 @@ import 'package:ledger_bitcoin/ledger_bitcoin.dart'; import 'package:ledger_bitcoin/psbt.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:mobx/mobx.dart'; +import 'package:ur/cbor_lite.dart'; +import 'package:ur/ur.dart'; +import 'package:ur/ur_decoder.dart'; part 'bitcoin_wallet.g.dart'; @@ -280,6 +285,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Future buildPsbt({ required List outputs, + required List cwOutputs, required BigInt fee, required BasedUtxoNetwork network, required List utxos, @@ -308,7 +314,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } return PSBTTransactionBuild( - inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF) + inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF, cwOutputs: cwOutputs) .psbt; } @@ -318,6 +324,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { required BigInt fee, required BasedUtxoNetwork network, required List utxos, + required List cwOutputs, required Map publicKeys, String? memo, bool enableRBF = false, @@ -331,6 +338,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { fee: fee, network: network, utxos: utxos, + cwOutputs: cwOutputs, publicKeys: publicKeys, masterFingerprint: masterFingerprint, memo: memo, @@ -351,7 +359,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { as PendingBitcoinTransaction; final payjoinUri = credentials.payjoinUri; - if (payjoinUri == null) return tx; + if (payjoinUri == null && !tx.shouldCommitUR()) return tx; final transaction = await buildPsbt( utxos: tx.utxos, @@ -363,20 +371,26 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { isChange: e.isChange, )) .toList(), + cwOutputs: credentials.outputs, fee: BigInt.from(tx.fee), network: network, memo: credentials.outputs.first.memo, outputOrdering: BitcoinOrdering.none, enableRBF: true, publicKeys: tx.publicKeys!, - masterFingerprint: Uint8List(0)); + masterFingerprint: Uint8List.fromList([0, 0, 0, 0])); - final originalPsbt = await signPsbt( - base64.encode(transaction.asPsbtV0()), getUtxoWithPrivateKeys()); + if (tx.shouldCommitUR()) { + tx.unsignedPsbt = transaction.asPsbtV0(); + return tx; + } + + final originalPsbt = + await signPsbt(base64.encode(transaction.asPsbtV0()), getUtxoWithPrivateKeys()); tx.commitOverride = () async { - final sender = await payjoinManager.initSender( - payjoinUri, originalPsbt, int.parse(tx.feeRate)); + final sender = + await payjoinManager.initSender(payjoinUri!, originalPsbt, int.parse(tx.feeRate)); payjoinManager.spawnNewSender( sender: sender, pjUrl: payjoinUri, amount: BigInt.from(tx.amount)); }; @@ -404,6 +418,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { feeRate: "", network: network, hasChange: true, + isViewOnly: false, ).commit(); } @@ -429,6 +444,44 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { return base64Encode(psbt.asPsbtV0()); } + Future commitPsbtUR(List urCodes) async { + if (urCodes.isEmpty) throw Exception("No QR code got scanned"); + bool isUr = urCodes.any((str) { + return str.startsWith("ur:psbt/"); + }); + if (isUr) { + final ur = URDecoder(); + for (final inp in urCodes) { + ur.receivePart(inp); + } + final result = (ur.result as UR); + final cbor = result.cbor; + final cborDecoder = CBORDecoder(cbor); + final out = cborDecoder.decodeBytes(); + final bytes = out.$1; + final base64psbt = base64Encode(bytes); + final psbt = PsbtV2()..deserializeV0(base64Decode(base64psbt)); + + // psbt.finalize(); + final finalized = base64Encode(psbt.serialize()); + await commitPsbt(finalized); + } else { + final btcTx = BtcTransaction.fromRaw(urCodes.first); + + return PendingBitcoinTransaction( + btcTx, + type, + electrumClient: electrumClient, + amount: 0, + fee: 0, + feeRate: "", + network: network, + hasChange: true, + isViewOnly: false, + ).commit(); + } + } + @override Future signMessage(String message, {String? address = null}) async { if (walletInfo.isHardwareWallet) { diff --git a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart index 177d61e87..3d71a0c39 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart @@ -54,6 +54,17 @@ class BitcoinRestoreWalletFromWIFCredentials extends WalletCredentials { final String wif; } +class BitcoinWalletFromKeysCredentials extends WalletCredentials { + BitcoinWalletFromKeysCredentials({ + required String name, + required String password, + required this.xpub, + WalletInfo? walletInfo, + }) : super(name: name, password: password, walletInfo: walletInfo); + + final String xpub; +} + class BitcoinRestoreWalletFromHardware extends WalletCredentials { BitcoinRestoreWalletFromHardware({ required String name, diff --git a/cw_bitcoin/lib/bitcoin_wallet_keys.dart b/cw_bitcoin/lib/bitcoin_wallet_keys.dart index 0a4afc10d..4ed0da49c 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_keys.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_keys.dart @@ -1,7 +1,15 @@ class BitcoinWalletKeys { - const BitcoinWalletKeys({required this.wif, required this.privateKey, required this.publicKey}); + const BitcoinWalletKeys({required this.wif, required this.privateKey, required this.publicKey, required this.xpub}); final String wif; final String privateKey; final String publicKey; + final String xpub; + + Map toJson() => { + 'wif': wif, + 'privateKey': privateKey, + 'publicKey': publicKey, + 'xpub': xpub + }; } \ No newline at end of file diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index 33113d7ef..8d41298ee 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -20,7 +20,7 @@ import 'package:bip39/bip39.dart' as bip39; class BitcoinWalletService extends WalletService< BitcoinNewWalletCredentials, BitcoinRestoreWalletFromSeedCredentials, - BitcoinRestoreWalletFromWIFCredentials, + BitcoinWalletFromKeysCredentials, BitcoinRestoreWalletFromHardware> { BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.payjoinSessionSource, this.isDirect); @@ -165,9 +165,25 @@ class BitcoinWalletService extends WalletService< } @override - Future restoreFromKeys(BitcoinRestoreWalletFromWIFCredentials credentials, - {bool? isTestnet}) async => - throw UnimplementedError(); + Future restoreFromKeys(BitcoinWalletFromKeysCredentials credentials, + {bool? isTestnet}) async { + final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet; + credentials.walletInfo?.network = network.value; + + final wallet = await BitcoinWallet( + password: credentials.password!, + xpub: credentials.xpub, + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + networkParam: network, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + payjoinBox: payjoinSessionSource, + ); + + await wallet.save(); + await wallet.init(); + return wallet; + } @override Future restoreFromSeed(BitcoinRestoreWalletFromSeedCredentials credentials, diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 2ddd30df6..4cd42a0fe 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -203,15 +203,25 @@ class ElectrumClient { return []; }); - Future> getBalance(String scriptHash) => - call(method: 'blockchain.scripthash.get_balance', params: [scriptHash]) - .then((dynamic result) { - if (result is Map) { - return result; - } - - return {}; - }); + Future> getBalance(String scriptHash, {bool throwOnError = false}) async { + try { + final result = await call(method: 'blockchain.scripthash.get_balance', params: [scriptHash]); + if (result is Map) { + return result; + } + + if (throwOnError) { + throw Exception('Invalid response format for getBalance'); + } + + return {}; + } catch (e) { + if (throwOnError) { + rethrow; + } + return {}; + } + } Future>> getHistory(String scriptHash) => call(method: 'blockchain.scripthash.get_history', params: [scriptHash]) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index bf20cac80..91736b3c1 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -42,6 +42,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/unspent_coin_type.dart'; +import 'package:cw_core/output_info.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; @@ -216,7 +217,7 @@ abstract class ElectrumWalletBase @override bool isTestnet; - bool get hasSilentPaymentsScanning => type == WalletType.bitcoin; + bool get hasSilentPaymentsScanning => type == WalletType.bitcoin && keys.privateKey.isNotEmpty; @observable bool nodeSupportsSilentPayments = true; @@ -282,11 +283,26 @@ abstract class ElectrumWalletBase } @override - BitcoinWalletKeys get keys => BitcoinWalletKeys( - wif: WifEncoder.encode(hd.privateKey.raw, netVer: network.wifNetVer), - privateKey: hd.privateKey.toHex(), - publicKey: hd.publicKey.toHex(), - ); + BitcoinWalletKeys get keys { + String? wif; + String? privateKey; + String? publicKey; + try { + wif = WifEncoder.encode(hd.privateKey.raw, netVer: network.wifNetVer); + } catch (_) {} + try { + privateKey = hd.privateKey.toHex(); + } catch (_) {} + try { + publicKey = hd.publicKey.toHex(); + } catch (_) {} + return BitcoinWalletKeys( + wif: wif ?? '', + privateKey: privateKey ?? '', + publicKey: publicKey ?? '', + xpub: xpub, + ); + } String _password; List unspentCoins; @@ -672,7 +688,7 @@ abstract class ElectrumWalletBase ); spendsSilentPayment = true; isSilentPayment = true; - } else if (!isHardwareWallet) { + } else if (!isHardwareWallet && keys.privateKey.isNotEmpty) { privkey = generateECPrivate(hd: hd, index: utx.bitcoinAddressRecord.index, network: network); } @@ -780,7 +796,11 @@ abstract class ElectrumWalletBase } if (outputs.length == 1) { - outputs[0] = BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount)); + outputs[0] = BitcoinOutput( + address: outputs.last.address, + value: BigInt.from(amount), + isSilentPayment: hasSilentPayment, + ); } return EstimatedTxResult( @@ -888,7 +908,17 @@ abstract class ElectrumWalletBase ); updatedOutputs.clear(); - updatedOutputs.addAll(temp); + for (int i = 0; i < temp.length; i++) { + final output = temp[i]; + final oldOutput = outputs[i]; + + updatedOutputs.add(BitcoinOutput( + address: output.address, + value: output.value, + isSilentPayment: oldOutput.isSilentPayment, + isChange: output.isChange, + )); + } if (fee == 0) { throw BitcoinTransactionNoFeeException(); @@ -1088,6 +1118,14 @@ abstract class ElectrumWalletBase ); } + for (final output in updatedOutputs) { + // TODO: get from server + // if (output.isSilentPayment && output.value.toInt() > silentPaymentsMin) { + if (output.isSilentPayment && output.value.toInt() <= 1000) { + throw BitcoinTransactionNoDustException(); + } + } + if (walletInfo.isHardwareWallet) { final transaction = await buildHardwareWalletTransaction( utxos: estimatedTx.utxos, @@ -1098,6 +1136,7 @@ abstract class ElectrumWalletBase memo: estimatedTx.memo, outputOrdering: BitcoinOrdering.none, enableRBF: true, + cwOutputs: transactionCredentials.outputs, ); return PendingBitcoinTransaction( @@ -1110,7 +1149,8 @@ abstract class ElectrumWalletBase network: network, hasChange: estimatedTx.hasChange, isSendAll: estimatedTx.isSendAll, - hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot + hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot, + isViewOnly: false, )..addListener((transaction) async { transactionHistory.addOne(transaction); await updateBalance(); @@ -1144,6 +1184,7 @@ abstract class ElectrumWalletBase bool hasTaprootInputs = false; final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) { + if (keys.privateKey.isEmpty) return ""; String error = "Cannot find private key."; ECPrivateInfo? key; @@ -1181,18 +1222,21 @@ abstract class ElectrumWalletBase } }); - return PendingBitcoinTransaction(transaction, type, - electrumClient: electrumClient, - amount: estimatedTx.amount, - fee: estimatedTx.fee, - feeRate: feeRateInt.toString(), - network: network, - hasChange: estimatedTx.hasChange, - isSendAll: estimatedTx.isSendAll, - hasTaprootInputs: hasTaprootInputs, - utxos: estimatedTx.utxos, - publicKeys: estimatedTx.publicKeys) - ..addListener((transaction) async { + return PendingBitcoinTransaction( + transaction, + type, + electrumClient: electrumClient, + amount: estimatedTx.amount, + fee: estimatedTx.fee, + feeRate: feeRateInt.toString(), + network: network, + hasChange: estimatedTx.hasChange, + isSendAll: estimatedTx.isSendAll, + hasTaprootInputs: hasTaprootInputs, + utxos: estimatedTx.utxos, + publicKeys: estimatedTx.publicKeys, + isViewOnly: keys.privateKey.isEmpty, + )..addListener((transaction) async { transactionHistory.addOne(transaction); if (estimatedTx.spendsSilentPayment) { transactionHistory.transactions.values.forEach((tx) { @@ -1220,6 +1264,7 @@ abstract class ElectrumWalletBase required BigInt fee, required BasedUtxoNetwork network, required List utxos, + required List cwOutputs, required Map publicKeys, String? memo, bool enableRBF = false, @@ -1357,6 +1402,8 @@ abstract class ElectrumWalletBase @action @override Future rescan({required int height, bool? doSingleScan}) async { + if (keys.privateKey.isEmpty) return; + silentPaymentsScanningActive = true; _setListeners(height, doSingleScan: doSingleScan); } @@ -1853,6 +1900,7 @@ abstract class ElectrumWalletBase network: network, hasChange: changeOutputs.isNotEmpty, feeRate: newFee.toString(), + isViewOnly: keys.privateKey.isEmpty, )..addListener((transaction) async { transactionHistory.transactions.values.forEach((tx) { if (tx.id == hash) { @@ -2300,6 +2348,26 @@ abstract class ElectrumWalletBase await save(); } + @override + Future checkNodeHealth() async { + try { + final addresses = walletAddresses.allAddresses + .where((address) => RegexUtils.addressTypeFromStr(address.address, network) is! MwebAddress) + .toList(); + + if (addresses.isEmpty) { + return false; + } + + final firstAddress = addresses.first; + final sh = firstAddress.getScriptHash(network); + await electrumClient.getBalance(sh, throwOnError: true); + return true; + } catch (e) { + return false; + } + } + @override void setExceptionHandler(void Function(FlutterErrorDetails) onError) => _onError = onError; diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index d3ba83a61..7b32547b8 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -38,6 +38,7 @@ import 'package:cw_bitcoin/litecoin_wallet_addresses.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; +import 'package:cw_core/output_info.dart'; import 'package:flutter/foundation.dart'; import 'package:grpc/grpc.dart'; import 'package:hive/hive.dart'; @@ -1383,6 +1384,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { required BigInt fee, required BasedUtxoNetwork network, required List utxos, + required List cwOutputs, required Map publicKeys, String? memo, bool enableRBF = false, diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 43c4b3963..82bcbd79e 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -1,3 +1,7 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:bbqrdart/bbqrdart.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:grpc/grpc.dart'; import 'package:cw_bitcoin/exceptions.dart'; @@ -11,6 +15,9 @@ import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_mweb/cw_mweb.dart'; import 'package:cw_mweb/mwebd.pb.dart'; +import 'package:ur/cbor_lite.dart'; +import 'package:ur/ur.dart'; +import 'package:ur/ur_encoder.dart'; class PendingBitcoinTransaction with PendingTransaction { PendingBitcoinTransaction( @@ -28,6 +35,8 @@ class PendingBitcoinTransaction with PendingTransaction { this.utxos = const [], this.publicKeys, this.commitOverride, + this.unsignedPsbt, + required this.isViewOnly, }) : _listeners = []; final WalletType type; @@ -40,6 +49,7 @@ class PendingBitcoinTransaction with PendingTransaction { final bool isSendAll; final bool hasChange; final bool hasTaprootInputs; + final bool isViewOnly; List utxos; bool isMweb; String? changeAddressOverride; @@ -49,6 +59,8 @@ class PendingBitcoinTransaction with PendingTransaction { final Map? publicKeys; Future Function()? commitOverride; + Uint8List? unsignedPsbt; + @override String get id => idOverride ?? _tx.txId(); @@ -89,9 +101,11 @@ class PendingBitcoinTransaction with PendingTransaction { try { final change = _tx.outputs.firstWhere((out) => out.isChange); if (changeAddressOverride != null) { - return PendingChange(changeAddressOverride!, BtcUtils.fromSatoshi(change.amount)); + return PendingChange( + changeAddressOverride!, BtcUtils.fromSatoshi(change.amount)); } - return PendingChange(change.scriptPubKey.toAddress(), BtcUtils.fromSatoshi(change.amount)); + return PendingChange( + change.scriptPubKey.toAddress(), BtcUtils.fromSatoshi(change.amount)); } catch (_) { return null; } @@ -140,12 +154,14 @@ class PendingBitcoinTransaction with PendingTransaction { Future _ltcCommit() async { try { - final resp = await CwMweb.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); } catch (e) { - throw BitcoinTransactionCommitFailed(errorMessage: "Unknown error: ${e.toString()}"); + throw BitcoinTransactionCommitFailed( + errorMessage: "Unknown error: ${e.toString()}"); } } @@ -164,7 +180,8 @@ class PendingBitcoinTransaction with PendingTransaction { _listeners.forEach((listener) => listener(transactionInfo())); } - void addListener(void Function(ElectrumTransactionInfo transaction) listener) => + void addListener( + void Function(ElectrumTransactionInfo transaction) listener) => _listeners.add(listener); ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type, @@ -179,9 +196,40 @@ class PendingBitcoinTransaction with PendingTransaction { inputAddresses: _tx.inputs.map((input) => input.txId).toList(), outputAddresses: outputAddresses, fee: fee); - + @override - Future commitUR() { - throw UnimplementedError(); + bool shouldCommitUR() => isViewOnly; + + @override + Future> commitUR() { + var sourceBytes = unsignedPsbt!; + var cborEncoder = CBOREncoder(); + cborEncoder.encodeBytes(sourceBytes); + var ur = UR("psbt", cborEncoder.getBytes()); + var urLegacy = UR("crypto-psbt", cborEncoder.getBytes()); + // var ur = UR("psbt", Uint8List.fromList(List.generate(64*1024, (int x) => x % 256))); + var encoded = UREncoder(ur, 120); + var encodedLegacy = UREncoder(urLegacy, 120); + List values = []; + List valuesLegacy = []; + while (!encoded.isComplete) { + values.add(encoded.nextPart()); + valuesLegacy.add(encodedLegacy.nextPart()); + } + + final bbqrObj = BBQRPsbt.fromUint8List(sourceBytes); + List bbqr = [ + bbqrObj.asString(), + ]; + while (!bbqrObj.isDone) { + bbqrObj.next(); + bbqr.add(bbqrObj.asString()); + } + + return Future.value({ + "PSBT (bcur)": values.join("\n"), + "PSBT (bbqr)": bbqr.join("\n"), + "PSBT (bcur legacy)": valuesLegacy.join("\n"), + }); } } diff --git a/cw_bitcoin/lib/psbt/transaction_builder.dart b/cw_bitcoin/lib/psbt/transaction_builder.dart index 8cb979730..4692e0906 100644 --- a/cw_bitcoin/lib/psbt/transaction_builder.dart +++ b/cw_bitcoin/lib/psbt/transaction_builder.dart @@ -1,7 +1,9 @@ +import 'dart:convert'; import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:convert/convert.dart'; +import 'package:cw_core/output_info.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:ledger_bitcoin/psbt.dart'; @@ -9,7 +11,7 @@ class PSBTTransactionBuild { final PsbtV2 psbt = PsbtV2(); PSBTTransactionBuild( - {required List inputs, required List outputs, bool enableRBF = true}) { + {required List inputs, required List outputs, required List cwOutputs, bool enableRBF = true}) { psbt.setGlobalTxVersion(2); psbt.setGlobalInputCount(inputs.length); psbt.setGlobalOutputCount(outputs.length); @@ -24,7 +26,7 @@ class PSBTTransactionBuild { psbt.setInputPreviousTxId(i, Uint8List.fromList(hex.decode(input.utxo.txHash).reversed.toList())); psbt.setInputOutputIndex(i, input.utxo.vout); psbt.setInputSequence(i, enableRBF ? 0x1 : 0xffffffff); - + if (input.utxo.isSegwit()) { setInputSegwit(i, input); @@ -43,6 +45,33 @@ class PSBTTransactionBuild { if (output is BitcoinOutput) { psbt.setOutputScript(i, Uint8List.fromList(output.address.toScriptPubKey().toBytes())); psbt.setOutputAmount(i, output.value.toInt()); + if (cwOutputs.isNotEmpty) { + try { + final cwOutput = cwOutputs + .where((e) => [e.address, e.extractedAddress] + .map((e) => e?.toLowerCase()) + .contains(output.address.toAddress().toLowerCase())) + .firstOrNull; + if (cwOutput != null) { + final bip353Name = utf8.encode(cwOutput.extra['bip353_name'] as String); + final bip353Proof = base64.decode(cwOutput.extra['bip353_proof'] as String); + + if (bip353Name.length > 255) { + printV('BIP353 name is too long, skipping'); + continue; + } + final proof = Uint8List.fromList([ + bip353Name.length, + ...bip353Name, + ...bip353Proof, + ]); + + psbt.setOutputDNSSECProof(i, proof); + } + } catch (e) { + printV('Error setting DNSSEC proof: $e'); + } + } } } } diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index c68e850e4..58eb5e7f7 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -46,6 +46,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bbqrdart: + dependency: "direct main" + description: + path: "." + ref: b0f3f72911221bafed0ab77359fccd64bc79d524 + resolved-ref: b0f3f72911221bafed0ab77359fccd64bc79d524 + url: "https://github.com/mrcyjanek/bbqrdart" + source: git + version: "1.0.0" bech32: dependency: "direct main" description: @@ -588,8 +597,8 @@ packages: dependency: "direct main" description: path: "packages/ledger-bitcoin" - ref: trunk - resolved-ref: e93254f3ff3f996fb91f65a1e7ceffb9f510b4c8 + ref: e8ab1a98493dfdd2277edde07a66b8d378ca70f6 + resolved-ref: e8ab1a98493dfdd2277edde07a66b8d378ca70f6 url: "https://github.com/cake-tech/ledger-flutter-plus-plugins" source: git version: "0.0.3" @@ -1101,6 +1110,15 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.0" + ur: + dependency: "direct main" + description: + path: "." + ref: "5738f70d0ec3d50977ac3dd01fed62939600238b" + resolved-ref: "5738f70d0ec3d50977ac3dd01fed62939600238b" + url: "https://github.com/bukata-sa/bc-ur-dart" + source: git + version: "0.1.0" vector_math: dependency: transitive description: @@ -1182,5 +1200,5 @@ packages: source: hosted version: "2.2.2" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.6.2 <4.0.0" flutter: ">=3.27.0" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 1fca85c31..062ac025b 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -49,7 +49,7 @@ dependencies: git: url: https://github.com/cake-tech/ledger-flutter-plus-plugins path: packages/ledger-bitcoin - ref: trunk + ref: e8ab1a98493dfdd2277edde07a66b8d378ca70f6 ledger_litecoin: git: url: https://github.com/cake-tech/ledger-flutter-plus-plugins @@ -58,6 +58,14 @@ dependencies: git: url: https://github.com/sneurlax/socks_socket ref: e6232c53c1595469931ababa878759a067c02e94 + ur: + git: + url: https://github.com/bukata-sa/bc-ur-dart + ref: 5738f70d0ec3d50977ac3dd01fed62939600238b + bbqrdart: + git: + url: https://github.com/mrcyjanek/bbqrdart + ref: b0f3f72911221bafed0ab77359fccd64bc79d524 dev_dependencies: flutter_test: 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 6f1b6a6f7..24dc1970d 100644 --- a/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart +++ b/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart @@ -89,7 +89,7 @@ class PendingBitcoinCashTransaction with PendingTransaction { isReplaced: false, ); @override - Future commitUR() { + Future> commitUR() { throw UnimplementedError(); } } diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 0d6ab9f3a..ea5389356 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -113,6 +113,8 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen CryptoCurrency.flip, CryptoCurrency.deuro, CryptoCurrency.usdtbsc, + CryptoCurrency.ndeps, + CryptoCurrency.deps, ]; static const havenCurrencies = [ @@ -235,6 +237,9 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const flip = CryptoCurrency(title: 'FLIP', tag: 'ETH', fullName: 'Chainflip', raw: 97, name: 'flip', iconPath: 'assets/images/flip_icon.png', decimals: 18); static const deuro = CryptoCurrency(title: 'DEURO', tag: 'ETH', fullName: 'Decentralized Euro', raw: 98, name: 'deuro', iconPath: 'assets/images/deuro_icon.png', decimals: 18); static const usdtbsc = CryptoCurrency(title: 'USDT', tag: 'BSC', fullName: 'USDT Binance coin', raw: 99, name: 'usdtbsc', iconPath: 'assets/images/usdtbsc_icon.png', decimals: 18); + static const ndeps = CryptoCurrency(title: 'NDEPS', tag: 'ETH', fullName: 'Native Decentralized Euro Protocol Share', raw: 100, name: 'ndeps', iconPath: 'assets/images/ndeps_icon.png', decimals: 18); + static const deps = CryptoCurrency(title: 'DEPS', tag: 'ETH', fullName: 'Decentralized Euro Protocol Share', raw: 101, name: 'deps', iconPath: 'assets/images/deps_icon.png', decimals: 18); + static final Map _rawCurrencyMap = [...all, ...havenCurrencies].fold>({}, (acc, item) { diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index fdffb844b..ff94001f2 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -26,6 +26,7 @@ class Node extends HiveObject with Keyable { this.trusted = false, this.socksProxyAddress, this.path = '', + this.isEnabledForAutoSwitching = false, String? uri, WalletType? type, }) { @@ -44,7 +45,8 @@ class Node extends HiveObject with Keyable { password = map['password'] as String?, useSSL = map['useSSL'] as bool?, trusted = map['trusted'] as bool? ?? false, - socksProxyAddress = map['socksProxyPort'] as String?; + socksProxyAddress = map['socksProxyPort'] as String?, + isEnabledForAutoSwitching = map['isEnabledForAutoSwitching'] as bool? ?? false; static const typeId = NODE_TYPE_ID; static const boxName = 'Nodes'; @@ -82,6 +84,9 @@ class Node extends HiveObject with Keyable { @HiveField(10) bool? supportsMweb; + @HiveField(11, defaultValue: false) + bool isEnabledForAutoSwitching; + bool get isSSL => useSSL ?? false; bool get useSocksProxy => socksProxyAddress == null ? false : socksProxyAddress!.isNotEmpty; diff --git a/cw_core/lib/output_info.dart b/cw_core/lib/output_info.dart index 9e3ac4ffc..1db1c2ed6 100644 --- a/cw_core/lib/output_info.dart +++ b/cw_core/lib/output_info.dart @@ -8,7 +8,8 @@ class OutputInfo { this.fiatAmount, this.note, this.extractedAddress, - this.memo}); + this.memo, + this.extra = const {}}); final String? fiatAmount; final String? cryptoAmount; @@ -19,4 +20,5 @@ class OutputInfo { final bool isParsedAddress; final int? formattedCryptoAmount; final String? memo; + final Map extra; } \ No newline at end of file diff --git a/cw_core/lib/pending_transaction.dart b/cw_core/lib/pending_transaction.dart index a06ddfcc8..599047fbd 100644 --- a/cw_core/lib/pending_transaction.dart +++ b/cw_core/lib/pending_transaction.dart @@ -18,5 +18,5 @@ mixin PendingTransaction { bool shouldCommitUR() => false; Future commit(); - Future commitUR(); + Future> commitUR(); } diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 0ca7ff2b1..0d7650e15 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -112,4 +112,9 @@ abstract class WalletBase true; + + /// This is used to check if the current node is healthy by making a lightweight RPC call + /// Each wallet implementation should override this to make a single, efficient call + /// Returns true if the node is healthy, false otherwise + Future checkNodeHealth(); } diff --git a/cw_decred/lib/api/libdcrwallet.dart b/cw_decred/lib/api/libdcrwallet.dart index b5e5bbe73..a6d1e1017 100644 --- a/cw_decred/lib/api/libdcrwallet.dart +++ b/cw_decred/lib/api/libdcrwallet.dart @@ -454,7 +454,7 @@ class Libwallet { return res.payload; } - Future balance(String walletName) async { + Future balance(String walletName, {bool throwOnError = false}) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; @@ -468,6 +468,9 @@ class Libwallet { try { return jsonDecode(res.payload); } catch (_) { + if (throwOnError) { + rethrow; + } return {}; } } diff --git a/cw_decred/lib/pending_transaction.dart b/cw_decred/lib/pending_transaction.dart index 53699b60d..23c76920c 100644 --- a/cw_decred/lib/pending_transaction.dart +++ b/cw_decred/lib/pending_transaction.dart @@ -36,7 +36,7 @@ class DecredPendingTransaction with PendingTransaction { } @override - Future commitUR() { + Future> commitUR() { throw UnimplementedError(); } } diff --git a/cw_decred/lib/wallet.dart b/cw_decred/lib/wallet.dart index 97aee775d..fb177f4ca 100644 --- a/cw_decred/lib/wallet.dart +++ b/cw_decred/lib/wallet.dart @@ -26,7 +26,6 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/pending_transaction.dart'; -import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/unspent_coins_info.dart'; @@ -594,6 +593,9 @@ abstract class DecredWalletBase ); } + @override + Future checkNodeHealth() async => await checkSync(); + @override void setExceptionHandler(void Function(FlutterErrorDetails) onError) => onError; diff --git a/cw_ethereum/lib/default_ethereum_erc20_tokens.dart b/cw_ethereum/lib/default_ethereum_erc20_tokens.dart index 630424967..dac309389 100644 --- a/cw_ethereum/lib/default_ethereum_erc20_tokens.dart +++ b/cw_ethereum/lib/default_ethereum_erc20_tokens.dart @@ -304,6 +304,20 @@ class DefaultEthereumErc20Tokens { decimal: 18, enabled: false, ), + Erc20Token( + name: "Native Decentralized Euro Protocol Share", + symbol: "NDEPS", + contractAddress: "0xc71104001A3CCDA1BEf1177d765831Bd1bfE8eE6", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Decentralized Euro Protocol Share", + symbol: "DEPS", + contractAddress: "0x103747924e74708139a9400e4ab4bea79fffa380", + decimal: 18, + enabled: false, + ), ]; List get initialErc20Tokens => _defaultTokens.map((token) { diff --git a/cw_evm/lib/evm_chain_client.dart b/cw_evm/lib/evm_chain_client.dart index b7377c54e..29a88dcce 100644 --- a/cw_evm/lib/evm_chain_client.dart +++ b/cw_evm/lib/evm_chain_client.dart @@ -65,10 +65,13 @@ abstract class EVMChainClient { // }); } - Future getBalance(EthereumAddress address) async { + Future getBalance(EthereumAddress address, {bool throwOnError = false}) async { try { return await _client!.getBalance(address); } catch (_) { + if (throwOnError) { + rethrow; + } return EtherAmount.zero(); } } diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index fe97e4ea8..9f998f714 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -164,6 +164,36 @@ abstract class EVMChainWalletBase EncryptionFileUtils encryptionFileUtils, ); + @override + Future checkNodeHealth() async { + try { + // Check native balance + await _client.getBalance(_evmChainPrivateKey.address, throwOnError: true); + + // Check USDC token balance + String usdcContractAddress; + if (_client.chainId == 1) { + // Ethereum mainnet + usdcContractAddress = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"; + } else if (_client.chainId == 137) { + // Polygon mainnet + usdcContractAddress = "0x2791bca1f2de4661ed88a30c99a7a9449aa84174"; + } else { + // If we are not on Ethereum or Polygon, we skip ERC20 token check + return true; + } + + await _client.fetchERC20Balances( + _evmChainPrivateKey.address, + usdcContractAddress, + ); + + return true; + } catch (e) { + return false; + } + } + //! Common Methods across child classes String idFor(String name, WalletType type) => '${walletTypeToString(type).toLowerCase()}_$name'; @@ -379,15 +409,21 @@ abstract class EVMChainWalletBase Future startSync() async { try { syncStatus = AttemptingSyncStatus(); + + // Verify node health before attempting to sync + final isHealthy = await checkNodeHealth(); + if (!isHealthy) { + syncStatus = FailedSyncStatus(); + return; + } + await _updateBalance(); await _updateTransactions(); - await _updateEstimatedGasFeeParams(); _updateFeesTimer ??= Timer.periodic(const Duration(seconds: 30), (timer) async { await _updateEstimatedGasFeeParams(); }); - syncStatus = SyncedSyncStatus(); } catch (e) { syncStatus = FailedSyncStatus(); @@ -825,7 +861,7 @@ abstract class EVMChainWalletBase _transactionsUpdateTimer!.cancel(); } - _transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 15), (_) { + _transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 20), (_) { _updateTransactions(); _updateBalance(); }); diff --git a/cw_evm/lib/evm_erc20_balance.dart b/cw_evm/lib/evm_erc20_balance.dart index 952d50ba8..a29b74188 100644 --- a/cw_evm/lib/evm_erc20_balance.dart +++ b/cw_evm/lib/evm_erc20_balance.dart @@ -1,6 +1,5 @@ import 'dart:convert'; -import 'dart:math'; -import 'package:intl/intl.dart'; +import 'package:cw_core/format_fixed.dart'; import 'package:cw_core/balance.dart'; @@ -12,17 +11,10 @@ class EVMChainERC20Balance extends Balance { final int exponent; @override - String get formattedAdditionalBalance => _balance(); + String get formattedAdditionalBalance => formatFixed(balance, exponent, fractionalDigits: 12); @override - String get formattedAvailableBalance => _balance(); - - String _balance() { - NumberFormat formatter = NumberFormat('0.00##########', 'en_US'); - double numBalance = (balance / BigInt.from(10).pow(exponent)).toDouble(); - String formattedBalance = formatter.format(numBalance); - return formattedBalance; - } + String get formattedAvailableBalance => formatFixed(balance, exponent, fractionalDigits: 12); String toJSON() => json.encode({ 'balanceInWei': balance.toString(), diff --git a/cw_evm/lib/pending_evm_chain_transaction.dart b/cw_evm/lib/pending_evm_chain_transaction.dart index 921a4a849..50213d6a4 100644 --- a/cw_evm/lib/pending_evm_chain_transaction.dart +++ b/cw_evm/lib/pending_evm_chain_transaction.dart @@ -53,7 +53,7 @@ class PendingEVMChainTransaction with PendingTransaction { } @override - Future commitUR() { + Future> commitUR() { throw UnimplementedError(); } } diff --git a/cw_monero/lib/ledger.dart b/cw_monero/lib/ledger.dart index 6e32d4d68..7a3c95a27 100644 --- a/cw_monero/lib/ledger.dart +++ b/cw_monero/lib/ledger.dart @@ -34,7 +34,8 @@ void enableLedgerExchange(LedgerConnection connection) { api.MoneroWallet.setDeviceReceivedData( result.cast(), response.length); - api.MoneroFree().free(result.cast()); + malloc.free(result); + // api.MoneroFree().free(result.cast()); } callable = NativeCallable.listener(callback); diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 72d02c506..40e544c4a 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -184,6 +184,24 @@ abstract class MoneroWalletBase extends WalletBase? updateBalance() => null; + @override + Future checkNodeHealth() async { + try { + // Check if the wallet is currently connected to the daemon + final isConnected = monero_wallet.isConnectedSync(); + + if (!isConnected) { + return false; // It's not connected to daemon + } + + // Check to get current node height to ensure daemon is responsive + final nodeHeight = await monero_wallet.getNodeHeight(); + return nodeHeight > 0; + } catch (e) { + return false; + } + } + @override Future close({bool shouldCleanup = false}) async { if (isHardwareWallet) { @@ -336,14 +354,23 @@ abstract class MoneroWalletBase extends WalletBase exportOutputsUR() { + final str = currentWallet!.exportOutputsUR(all: false); + int status = currentWallet!.status(); if (status != 0) { final err = currentWallet!.errorString(); - throw MoneroTransactionCreationException("unable to export UR: $err"); + throw MoneroTransactionCreationException("unable to export outputs: $err"); } - return str; + final strAll = currentWallet!.exportOutputsUR(all: true); + status = currentWallet!.status(); + if (status != 0) { + final err = currentWallet!.errorString(); + throw MoneroTransactionCreationException("unable to export outputs: $err"); + } + return { + "Outputs (partial)": str, + "Outputs (all)": strAll, + }; } bool needExportOutputs(int amount) { diff --git a/cw_monero/lib/pending_monero_transaction.dart b/cw_monero/lib/pending_monero_transaction.dart index 760c6baa4..e1d191eac 100644 --- a/cw_monero/lib/pending_monero_transaction.dart +++ b/cw_monero/lib/pending_monero_transaction.dart @@ -42,6 +42,7 @@ class PendingMoneroTransaction with PendingTransaction { String get feeFormattedValue => AmountConverter.amountIntToString( CryptoCurrency.xmr, pendingTransactionDescription.fee); + @override bool shouldCommitUR() => isViewOnly; @override @@ -67,7 +68,7 @@ class PendingMoneroTransaction with PendingTransaction { } @override - Future commitUR() async { + Future> commitUR() async { try { final ret = await monero_transaction_history.commitTransactionFromPointerAddress( address: pendingTransactionDescription.pointerAddress, @@ -77,7 +78,10 @@ class PendingMoneroTransaction with PendingTransaction { await Future.delayed(const Duration(milliseconds: 250)); await wallet.fetchTransactions(); }()); - return ret; + if (ret == null) return {}; + return { + "xmr-txsigned": ret, + }; } catch (e) { final message = e.toString(); diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 2de22ed11..911d01144 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -573,8 +573,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2 - resolved-ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2 + ref: b576312e4d466569cd03482b61c597b39a9f4dc3 + resolved-ref: b576312e4d466569cd03482b61c597b39a9f4dc3 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 a11e85b6c..98a4045ec 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -26,7 +26,7 @@ dependencies: monero: git: url: https://github.com/mrcyjanek/monero_c - ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2 + ref: b576312e4d466569cd03482b61c597b39a9f4dc3 path: impls/monero.dart mutex: ^3.1.0 ledger_flutter_plus: ^1.4.1 diff --git a/cw_nano/lib/nano_client.dart b/cw_nano/lib/nano_client.dart index c852a8c0d..812291f93 100644 --- a/cw_nano/lib/nano_client.dart +++ b/cw_nano/lib/nano_client.dart @@ -92,7 +92,7 @@ class NanoClient { return NanoBalance(currentBalance: cur, receivableBalance: rec); } - Future getAccountInfo(String address) async { + Future getAccountInfo(String address, {bool throwOnError = false}) async { try { final response = await ProxyWrapper().post( clearnetUri: _node!.uri, @@ -110,6 +110,9 @@ class NanoClient { return AccountInfoResponse.fromJson(data); } catch (e) { printV("error while getting account info $e"); + if (throwOnError) { + rethrow; + } return null; } } diff --git a/cw_nano/lib/nano_wallet.dart b/cw_nano/lib/nano_wallet.dart index eb2d2581c..a07612544 100644 --- a/cw_nano/lib/nano_wallet.dart +++ b/cw_nano/lib/nano_wallet.dart @@ -518,8 +518,19 @@ abstract class NanoWalletBase return _client.getN2Reps(); } + @override Future? updateBalance() async => await _updateBalance(); + @override + Future checkNodeHealth() async { + try { + await _client.getAccountInfo(_publicAddress!, throwOnError: true); + return true; + } catch (_) { + return false; + } + } + @override Future renameWalletFiles(String newWalletName) async { final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type); diff --git a/cw_nano/lib/pending_nano_transaction.dart b/cw_nano/lib/pending_nano_transaction.dart index 1530dcea4..8a759f55b 100644 --- a/cw_nano/lib/pending_nano_transaction.dart +++ b/cw_nano/lib/pending_nano_transaction.dart @@ -42,7 +42,7 @@ class PendingNanoTransaction with PendingTransaction { } @override - Future commitUR() { + Future> commitUR() { throw UnimplementedError(); } } diff --git a/cw_solana/lib/pending_solana_transaction.dart b/cw_solana/lib/pending_solana_transaction.dart index e21b00e86..8ba1c4b9e 100644 --- a/cw_solana/lib/pending_solana_transaction.dart +++ b/cw_solana/lib/pending_solana_transaction.dart @@ -44,7 +44,7 @@ class PendingSolanaTransaction with PendingTransaction { String get id => ''; @override - Future commitUR() { + Future> commitUR() { throw UnimplementedError(); } } diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart index d57da5af0..f91f66223 100644 --- a/cw_solana/lib/solana_client.dart +++ b/cw_solana/lib/solana_client.dart @@ -51,7 +51,7 @@ class SolanaWalletClient { } } - Future getBalance(String walletAddress) async { + Future getBalance(String walletAddress, {bool throwOnError = false}) async { try { final balance = await _provider!.requestWithContext( SolanaRPCGetBalance( @@ -65,6 +65,9 @@ class SolanaWalletClient { return solBalance; } catch (_) { + if (throwOnError) { + rethrow; + } return 0.0; } } @@ -88,31 +91,38 @@ class SolanaWalletClient { } Future getSplTokenBalance( - String mintAddress, String walletAddress) async { - // Fetch the token accounts (a token can have multiple accounts for various uses) - final tokenAccounts = await getSPLTokenAccounts(mintAddress, walletAddress); + String mintAddress, String walletAddress, {bool throwOnError = false}) async { + try { + // Fetch the token accounts (a token can have multiple accounts for various uses) + final tokenAccounts = await getSPLTokenAccounts(mintAddress, walletAddress); - // Handle scenario where there is no token account - if (tokenAccounts == null || tokenAccounts.isEmpty) { + // Handle scenario where there is no token account + if (tokenAccounts == null || tokenAccounts.isEmpty) { + return null; + } + + // Sum the balances of all accounts with the specified mint address + double totalBalance = 0.0; + + for (var tokenAccount in tokenAccounts) { + final tokenAmountResult = await _provider!.request( + SolanaRPCGetTokenAccountBalance(account: tokenAccount.pubkey), + ); + + final balance = tokenAmountResult.uiAmountString; + + final balanceAsDouble = double.tryParse(balance ?? '0.0') ?? 0.0; + + totalBalance += balanceAsDouble; + } + + return SolanaBalance(totalBalance); + } catch (_) { + if (throwOnError) { + rethrow; + } return null; } - - // Sum the balances of all accounts with the specified mint address - double totalBalance = 0.0; - - for (var tokenAccount in tokenAccounts) { - final tokenAmountResult = await _provider!.request( - SolanaRPCGetTokenAccountBalance(account: tokenAccount.pubkey), - ); - - final balance = tokenAmountResult.uiAmountString; - - final balanceAsDouble = double.tryParse(balance ?? '0.0') ?? 0.0; - - totalBalance += balanceAsDouble; - } - - return SolanaBalance(totalBalance); } Future getFeeForMessage(String message, Commitment commitment) async { diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index 455f510ac..87acaf4d5 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -377,6 +377,13 @@ abstract class SolanaWalletBase try { syncStatus = AttemptingSyncStatus(); + // Verify node health before attempting to sync + final isHealthy = await checkNodeHealth(); + if (!isHealthy) { + syncStatus = FailedSyncStatus(); + return; + } + await Future.wait([ _updateBalance(), _updateNativeSOLTransactions(), @@ -477,6 +484,22 @@ abstract class SolanaWalletBase @override Future? updateBalance() async => await _updateBalance(); + @override + Future checkNodeHealth() async { + try { + // Check native balance + await _client.getBalance(solanaAddress, throwOnError: true); + + // Check USDC token balance + const usdcMintAddress = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; + await _client.getSplTokenBalance(usdcMintAddress, solanaAddress, throwOnError: true); + + return true; + } catch (e) { + return false; + } + } + List get splTokenCurrencies => splTokensBox.values.toList(); void addInitialTokens() { @@ -554,6 +577,7 @@ abstract class SolanaWalletBase _updateBalance(); _updateNativeSOLTransactions(); _updateSPLTokenTransactions(); + _getEstimatedFees(); }); } @@ -564,7 +588,7 @@ abstract class SolanaWalletBase // Sign the message bytes with the wallet's private key final signature = (_solanaPrivateKey.sign(messageBytes)); - + return Base58Encoder.encode(signature); } diff --git a/cw_tron/lib/pending_tron_transaction.dart b/cw_tron/lib/pending_tron_transaction.dart index c1061db38..b1d601f8e 100644 --- a/cw_tron/lib/pending_tron_transaction.dart +++ b/cw_tron/lib/pending_tron_transaction.dart @@ -35,7 +35,7 @@ class PendingTronTransaction with PendingTransaction { String get id => ''; @override - Future commitUR() { + Future> commitUR() { throw UnimplementedError(); } } diff --git a/cw_tron/lib/tron_client.dart b/cw_tron/lib/tron_client.dart index f73e5a306..c7c530d53 100644 --- a/cw_tron/lib/tron_client.dart +++ b/cw_tron/lib/tron_client.dart @@ -102,12 +102,15 @@ class TronClient { } } - Future getBalance(TronAddress address) async { + Future getBalance(TronAddress address, {bool throwOnError = false}) async { try { final accountDetails = await _provider!.request(TronRequestGetAccount(address: address)); return accountDetails?.balance ?? BigInt.zero; } catch (_) { + if (throwOnError) { + rethrow; + } return BigInt.zero; } } @@ -476,7 +479,7 @@ class TronClient { } } - Future fetchTronTokenBalances(String userAddress, String contractAddress) async { + Future fetchTronTokenBalances(String userAddress, String contractAddress, {bool throwOnError = false}) async { try { final ownerAddress = TronAddress(userAddress); @@ -499,6 +502,9 @@ class TronClient { return TronBalance(outputResult); } catch (_) { + if (throwOnError) { + rethrow; + } return TronBalance(BigInt.zero); } } diff --git a/cw_tron/lib/tron_wallet.dart b/cw_tron/lib/tron_wallet.dart index eb80696ce..ce1b22d0e 100644 --- a/cw_tron/lib/tron_wallet.dart +++ b/cw_tron/lib/tron_wallet.dart @@ -276,6 +276,13 @@ abstract class TronWalletBase Future startSync() async { try { syncStatus = AttemptingSyncStatus(); + + // Verify node health before attempting to sync + final isHealthy = await checkNodeHealth(); + if (!isHealthy) { + syncStatus = FailedSyncStatus(); + return; + } await _updateBalance(); await fetchTransactions(); fetchTrc20ExcludedTransactions(); @@ -505,6 +512,22 @@ abstract class TronWalletBase @override Future? updateBalance() async => await _updateBalance(); + @override + Future checkNodeHealth() async { + try { + // Check native balance + await _client.getBalance(_tronPublicKey.toAddress(), throwOnError: true); + + // Check USDT token balance + const usdtContractAddress = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"; + await _client.fetchTronTokenBalances(_tronAddress, usdtContractAddress, throwOnError: true); + + return true; + } catch (e) { + return false; + } + } + List get tronTokenCurrencies => tronTokensBox.values.toList(); Future addTronToken(TronToken token) async { diff --git a/cw_wownero/lib/pending_wownero_transaction.dart b/cw_wownero/lib/pending_wownero_transaction.dart index e908e6e2c..b8aa7c4ed 100644 --- a/cw_wownero/lib/pending_wownero_transaction.dart +++ b/cw_wownero/lib/pending_wownero_transaction.dart @@ -55,7 +55,7 @@ class PendingWowneroTransaction with PendingTransaction { } @override - Future commitUR() { + Future> commitUR() { throw UnimplementedError(); } } diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index 2befaa20d..a90d32ee3 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -174,6 +174,24 @@ abstract class WowneroWalletBase @override Future? updateBalance() => null; + @override + Future checkNodeHealth() async { + try { + // Check if the wallet is currently connected to the daemon + final isConnected = wownero_wallet.isConnectedSync(); + + if (!isConnected) { + return false; // It's not connected to daemon + } + + // Check to get current node height to ensure daemon is responsive + final nodeHeight = await wownero_wallet.getNodeHeight(); + return nodeHeight > 0; + } catch (e) { + return false; + } + } + @override Future close({bool shouldCleanup = false}) async { _listener?.stop(); diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index 0fa39123b..491f65e54 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -493,8 +493,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2 - resolved-ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2 + ref: b576312e4d466569cd03482b61c597b39a9f4dc3 + resolved-ref: b576312e4d466569cd03482b61c597b39a9f4dc3 url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" diff --git a/cw_wownero/pubspec.yaml b/cw_wownero/pubspec.yaml index 4fda054b5..63f6251a3 100644 --- a/cw_wownero/pubspec.yaml +++ b/cw_wownero/pubspec.yaml @@ -24,7 +24,7 @@ dependencies: monero: git: url: https://github.com/mrcyjanek/monero_c - ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2 # monero_c hash + ref: b576312e4d466569cd03482b61c597b39a9f4dc3 # monero_c hash path: impls/monero.dart mutex: ^3.1.0 diff --git a/cw_zano/lib/model/pending_zano_transaction.dart b/cw_zano/lib/model/pending_zano_transaction.dart index fd2ab45fe..415e58303 100644 --- a/cw_zano/lib/model/pending_zano_transaction.dart +++ b/cw_zano/lib/model/pending_zano_transaction.dart @@ -49,7 +49,7 @@ class PendingZanoTransaction with PendingTransaction { } @override - Future commitUR() { + Future> commitUR() { throw UnimplementedError(); } } diff --git a/cw_zano/lib/zano_wallet.dart b/cw_zano/lib/zano_wallet.dart index 7f68b71e6..c5534f6c8 100644 --- a/cw_zano/lib/zano_wallet.dart +++ b/cw_zano/lib/zano_wallet.dart @@ -411,6 +411,17 @@ abstract class ZanoWalletBase @override Future? updateBalance() => null; + @override + Future checkNodeHealth() async { + try { + final status = await getWalletStatus(); + + return status.isDaemonConnected; + } catch (_) { + return false; + } + } + Future updateTransactions() async { try { if (_isTransactionUpdating) { diff --git a/cw_zano/pubspec.lock b/cw_zano/pubspec.lock index 41edf154e..f31862441 100644 --- a/cw_zano/pubspec.lock +++ b/cw_zano/pubspec.lock @@ -498,8 +498,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2 - resolved-ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2 + ref: b576312e4d466569cd03482b61c597b39a9f4dc3 + resolved-ref: b576312e4d466569cd03482b61c597b39a9f4dc3 url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" diff --git a/cw_zano/pubspec.yaml b/cw_zano/pubspec.yaml index 529fe04eb..c57c72a24 100644 --- a/cw_zano/pubspec.yaml +++ b/cw_zano/pubspec.yaml @@ -25,7 +25,7 @@ dependencies: monero: git: url: https://github.com/mrcyjanek/monero_c - ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2 # monero_c hash + ref: b576312e4d466569cd03482b61c597b39a9f4dc3 # monero_c hash path: impls/monero.dart dev_dependencies: flutter_test: diff --git a/docs/builds/ANDROID.md b/docs/builds/ANDROID.md index d7bc584ad..2461df4bd 100644 --- a/docs/builds/ANDROID.md +++ b/docs/builds/ANDROID.md @@ -18,8 +18,8 @@ In order to build the latest version of Cake Wallet, simply run the following: git clone --branch main https://github.com/cake-tech/cake_wallet.git # NOTE: Replace `main` with the latest release tag available at https://github.com/cake-tech/cake_wallet/releases/latest. cd cake_wallet -# docker build -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly . # Uncomment to build the docker image yourself instead of pulling it from the registry -docker run -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly bash -x << EOF +# docker build -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-ndkr28-go1.24.1-ruststablenightly . # Uncomment to build the docker image yourself instead of pulling it from the registry +docker run -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-ndkr28-go1.24.1-ruststablenightly bash -x << EOF set -x -e pushd scripts/android source ./app_env.sh cakewallet diff --git a/docs/builds/IOS.md b/docs/builds/IOS.md index 1d23b917c..44abaa805 100644 --- a/docs/builds/IOS.md +++ b/docs/builds/IOS.md @@ -7,7 +7,7 @@ The following are the system requirements to build Cake Wallet for your iOS devi ```txt macOS 15.3.1 Xcode 16.2 -Flutter 3.27.0 +Flutter 3.27.4 ``` NOTE: Newer versions of macOS and Xcode may also work, but have not been confirmed to work by the Cake team. @@ -43,9 +43,9 @@ To enable iOS build support for Xcode, perform the following: ### 3. Installing Flutter -Install Flutter, specifically version `3.27.0` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). +Install Flutter, specifically version `3.27.4` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). -NOTE: as `3.27.0` is not the latest version, you'll need to download it from instead of the link in the docs above. +NOTE: as `3.27.4` is not the latest version, you'll need to download it from instead of the link in the docs above. ### 4. Installing Rust @@ -65,7 +65,7 @@ The output of this command should appear like this, indicating successful instal ```zsh Doctor summary (to see all details, run flutter doctor -v): -[✓] Flutter (Channel stable, 3.27.0, on macOS 15.x.x) +[✓] Flutter (Channel stable, 3.27.4, on macOS 15.x.x) [✓] Xcode - develop for iOS and macOS (Xcode 16.2) ``` diff --git a/docs/builds/LINUX.md b/docs/builds/LINUX.md index 0f438a1d6..7207f125c 100644 --- a/docs/builds/LINUX.md +++ b/docs/builds/LINUX.md @@ -20,8 +20,8 @@ In order to build the latest version of Cake Wallet, simply run the following: git clone --branch main https://github.com/cake-tech/cake_wallet.git # NOTE: Replace `main` with the latest release tag available at https://github.com/cake-tech/cake_wallet/releases/latest. cd cake_wallet -# docker build -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly . # Uncomment to build the docker image yourself instead of pulling it from the registry -docker run --privileged -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly bash -x << EOF +# docker build -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-ndkr28-go1.24.1-ruststablenightly . # Uncomment to build the docker image yourself instead of pulling it from the registry +docker run --privileged -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-ndkr28-go1.24.1-ruststablenightly bash -x << EOF set -x -e pushd scripts ./gen_android_manifest.sh diff --git a/docs/builds/MACOS.md b/docs/builds/MACOS.md index 7e0f39aab..46a9842a4 100644 --- a/docs/builds/MACOS.md +++ b/docs/builds/MACOS.md @@ -7,7 +7,7 @@ The following are the system requirements to build Cake Wallet for your macOS de ```txt macOS 15.3.1 Xcode 16.2 -Flutter 3.27.0 +Flutter 3.27.4 ``` ### 1. Installing dependencies @@ -34,9 +34,9 @@ sudo xcodebuild -runFirstLaunch ### 3. Installing Flutter -Install Flutter, specifically version `3.27.0` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). +Install Flutter, specifically version `3.27.4` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). -NOTE: as `3.27.0` is not the latest version, you'll need to download it from instead of the link in the docs above. +NOTE: as `3.27.4` is not the latest version, you'll need to download it from instead of the link in the docs above. ### 4. Installing Rust @@ -56,7 +56,7 @@ The output of this command should appear like this, indicating successful instal ```zsh Doctor summary (to see all details, run flutter doctor -v): -[✓] Flutter (Channel stable, 3.27.0, on macOS 15.x.x) +[✓] Flutter (Channel stable, 3.27.4, on macOS 15.x.x) ... [✓] Xcode - develop for iOS and macOS (Xcode 16.2) ... diff --git a/docs/builds/WINDOWS.md b/docs/builds/WINDOWS.md index 7a86dac49..4fec78dc0 100644 --- a/docs/builds/WINDOWS.md +++ b/docs/builds/WINDOWS.md @@ -6,18 +6,18 @@ The following are the system requirements to build Cake Wallet for your Windows ```txt Windows 10 or later (64-bit), x86-64 based -Flutter 3.27.0 +Flutter 3.27.4 ``` ### 1. Installing Flutter -Install Flutter, specifically version `3.27.0` by following the [official docs](https://docs.flutter.dev/get-started/install/windows). +Install Flutter, specifically version `3.27.4` by following the [official docs](https://docs.flutter.dev/get-started/install/windows). In order for Flutter to function, you'll also need to enable Developer Mode: Start Menu > search for "Run" > type `ms-settings:developers`, and turn on Developer Mode. -NOTE: as `3.27.0` is not the latest version, you'll need to download it from instead of the link in the docs above. +NOTE: as `3.27.4` is not the latest version, you'll need to download it from instead of the link in the docs above. ### 2. Install Development Tools diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 630474f22..2a189c181 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -18,6 +18,14 @@ class CWBitcoin extends Bitcoin { passphrase: passphrase, ); + @override + WalletCredentials createBitcoinWalletFromKeys({ + required String name, + required String password, + required String xpub, + }) => + BitcoinWalletFromKeysCredentials(name: name, password: password, xpub: xpub); + @override WalletCredentials createBitcoinRestoreWalletFromWIFCredentials( {required String name, @@ -62,11 +70,7 @@ class CWBitcoin extends Bitcoin { final bitcoinWallet = wallet as ElectrumWallet; final keys = bitcoinWallet.keys; - return { - 'wif': keys.wif, - 'privateKey': keys.privateKey, - 'publicKey': keys.publicKey - }; + return bitcoinWallet.keys.toJson(); } @override @@ -126,7 +130,8 @@ class CWBitcoin extends Bitcoin { extractedAddress: out.extractedAddress, isParsedAddress: out.isParsedAddress, formattedCryptoAmount: out.formattedCryptoAmount, - memo: out.memo)) + memo: out.memo, + extra: out.extra)) .toList(), priority: priority as BitcoinTransactionPriority, feeRate: bitcoinFeeRate, @@ -275,7 +280,14 @@ class CWBitcoin extends Bitcoin { } @override - List getBitcoinReceivePageOptions() => BitcoinReceivePageOption.all; + List getBitcoinReceivePageOptions(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + final keys = bitcoinWallet.keys; + if (keys.privateKey.isEmpty) { + return BitcoinReceivePageOption.allViewOnly; + } + return BitcoinReceivePageOption.all; + } @override List getLitecoinReceivePageOptions() { @@ -727,6 +739,12 @@ class CWBitcoin extends Bitcoin { } } + @override + Future commitPsbtUR(Object wallet, List urCodes) { + final _wallet = wallet as BitcoinWalletBase; + return _wallet.commitPsbtUR(urCodes); + } + @override String getPayjoinEndpoint(Object wallet) { final _wallet = wallet as ElectrumWallet; diff --git a/lib/core/node_switching_service.dart b/lib/core/node_switching_service.dart new file mode 100644 index 000000000..ae5d21762 --- /dev/null +++ b/lib/core/node_switching_service.dart @@ -0,0 +1,126 @@ +import 'dart:async'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:hive/hive.dart'; + +class NodeSwitchingService { + NodeSwitchingService({ + required this.appStore, + required this.settingsStore, + required this.nodeSource, + }); + + static const int _healthCheckIntervalSeconds = 30; + + Timer? _healthCheckTimer; + + bool _isSwitching = false; + bool get isSwitching => _isSwitching; + + final AppStore appStore; + final SettingsStore settingsStore; + final Box nodeSource; + + // Track used nodes to cycle through all trusted nodes + final Map> _usedNodeKeys = {}; + + void startHealthCheckTimer() { + _healthCheckTimer?.cancel(); + _healthCheckTimer = Timer.periodic( + Duration(seconds: _healthCheckIntervalSeconds), + (_) => performHealthCheck(), + ); + + performHealthCheck(); + } + + void stopHealthCheckTimer() { + _healthCheckTimer?.cancel(); + _healthCheckTimer = null; + } + + Future performHealthCheck() async { + if (appStore.wallet == null) return; + + if (!settingsStore.enableAutomaticNodeSwitching) return; + + if (_isSwitching) return; + + try { + final isHealthy = await appStore.wallet!.checkNodeHealth(); + + if (!isHealthy) { + await _switchToNextTrustedNode(); + } + } catch (e) { + printV('Error during health check: $e'); + } + } + + /// Switch to the next available trusted node + Future _switchToNextTrustedNode() async { + _isSwitching = true; + + try { + final walletType = appStore.wallet!.type; + final currentNode = settingsStore.getCurrentNode(walletType); + + // Get all trusted nodes for this wallet type + final trustedNodes = nodeSource.values + .where((node) => node.type == walletType && node.isEnabledForAutoSwitching) + .toList(); + + if (trustedNodes.isEmpty) { + printV('No trusted nodes available for switching'); + return; + } + + // Initialize used nodes list for this wallet type if it does not exist + _usedNodeKeys.putIfAbsent(walletType, () => []); + + // Add current node to used list if not already there + if (!_usedNodeKeys[walletType]!.contains(currentNode.key)) { + _usedNodeKeys[walletType]!.add(currentNode.key); + } + + // Get next unused trusted node from the list + Node? nextNode; + for (final node in trustedNodes) { + if (!_usedNodeKeys[walletType]!.contains(node.key)) { + nextNode = node; + break; + } + } + + // If all trusted nodes have been used, reset the list and start over + if (nextNode == null) { + printV('All trusted nodes have been tried, resetting and starting over'); + _usedNodeKeys[walletType]!.clear(); + nextNode = trustedNodes.first; + } + + // Add the next node to used list + _usedNodeKeys[walletType]!.add(nextNode.key); + + printV('Switching from ${currentNode.uriRaw} to ${nextNode.uriRaw}'); + printV('Used nodes for ${walletType}: ${_usedNodeKeys[walletType]}'); + + // Update the current node in settings + settingsStore.nodes[walletType] = nextNode; + + // Connect the wallet to the new node + await appStore.wallet!.connectToNode(node: nextNode); + + await appStore.wallet!.startSync(); + + printV('Successfully switched to node: ${nextNode.uriRaw}'); + } catch (e) { + printV('Error switching to next trusted node: $e'); + } finally { + _isSwitching = false; + } + } +} diff --git a/lib/di.dart b/lib/di.dart index 6440285e6..7cf32ed0b 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -23,6 +23,7 @@ import 'package:cake_wallet/core/totp_request_details.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; import 'package:cake_wallet/core/yat_service.dart'; +import 'package:cake_wallet/core/node_switching_service.dart'; import 'package:cake_wallet/entities/biometric_auth.dart'; import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/contact_record.dart'; @@ -977,7 +978,7 @@ Future setup({ getIt.registerFactory(() => AnimatedURModel(getIt.get())); - getIt.registerFactoryParam((String urQr, _) => + getIt.registerFactoryParam, void>((Map urQr, _) => AnimatedURPage(getIt.get(), urQr: urQr)); getIt.registerFactoryParam( @@ -1516,5 +1517,11 @@ Future setup({ getIt.registerFactory(() => DEuroSavingsPage(getIt())); + getIt.registerLazySingleton(() => NodeSwitchingService( + appStore: getIt.get(), + settingsStore: getIt.get(), + nodeSource: _nodeSource, + )); + _isSetupFinished = true; } diff --git a/lib/entities/bip_353_record.dart b/lib/entities/bip_353_record.dart index 8685185da..e8451fd79 100644 --- a/lib/entities/bip_353_record.dart +++ b/lib/entities/bip_353_record.dart @@ -1,3 +1,6 @@ +import 'dart:convert'; +import 'dart:isolate'; + import 'package:basic_utils/basic_utils.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/alert_with_picker_option.dart'; @@ -5,6 +8,7 @@ import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; +import 'package:dnssec_proof/dnssec_proof.dart'; import 'package:flutter/material.dart'; class Bip353Record { @@ -22,6 +26,19 @@ class Bip353Record { 'address': 'On-Chain Address', }; + static Future fetchDnsProof(String bip353Name) async { + if (bip353Name.startsWith('₿')) { + bip353Name = bip353Name.substring(1); + } + final parts = bip353Name.split('@'); + if (parts.length != 2) return null; + final userPart = parts[0]; + final domainPart = parts[1]; + final bip353Domain = '$userPart.user._bitcoin-payment.$domainPart.'; + final proof = await Isolate.run(() => DnsProver.getTxtProof(bip353Domain)); + return base64.encode(proof); + } + static Future?> fetchUriByCryptoCurrency( String bip353Name, String asset) async { try { @@ -30,7 +47,10 @@ class Bip353Record { if (parts.length != 2) return null; - final userPart = parts[0]; + String userPart = parts[0]; + if (userPart.startsWith('₿')) { + userPart = userPart.substring(1); + } final domainPart = parts[1]; // 2. Construct the correct subdomain: "user._bitcoin-payment.domain" @@ -42,6 +62,10 @@ class Bip353Record { RRecordType.TXT, dnssec: true, ); + final proof = await fetchDnsProof(bip353Name); + if (proof == null) { + throw Exception('DNSSEC proof not found'); + } if (txtRecords == null) return null; diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index a5dfec97b..35634c391 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -510,7 +510,10 @@ Future defaultSettingsMigration( providerName: "SwapTrade", enabled: true, ); - break; + break; + case 50: + migrateExistingNodesToUseAutoSwitching(nodes: nodes, powNodes: powNodes); + break; default: break; } @@ -1088,7 +1091,7 @@ Future checkCurrentNodes( if (currentBitcoinElectrumServer == null) { final cakeWalletElectrum = - Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin, useSSL: false); + Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin, useSSL: false, isEnabledForAutoSwitching: true); await nodeSource.add(cakeWalletElectrum); final cakeWalletElectrumTestnet = Node(uri: publicBitcoinTestnetElectrumUri, type: WalletType.bitcoin, useSSL: false); @@ -1188,7 +1191,7 @@ Future resetBitcoinElectrumServer( if (cakeWalletNode == null) { cakeWalletNode = - Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin, useSSL: false); + Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin, useSSL: false, isEnabledForAutoSwitching: true); // final cakeWalletElectrumTestnet = // Node(uri: publicBitcoinTestnetElectrumUri, type: WalletType.bitcoin, useSSL: false); // await nodeSource.add(cakeWalletElectrumTestnet); @@ -1272,3 +1275,47 @@ Future removeMoneroWorld( ); } } + +Future migrateExistingNodesToUseAutoSwitching( + {required Box nodes, required Box powNodes}) async { + final listOfDefaultNodesWithAutoSwitching = [ + 'bitcoincash.stackwallet.com:50002', + 'bch.aftrek.org:50002', + 'btc-electrum.cakewallet.com:50002', + 'fulcrum.sethforprivacy.com:50002', + 'default-spv-nodes', + 'dcrd.sethforprivacy.com:9108', + 'ethereum-rpc.publicnode.com', + 'eth.nownodes.io', + 'ltc-electrum.cakewallet.com:50002', + 'litecoin.stackwallet.com:20063', + 'nano.nownodes.io', + 'rpc.nano.to', + 'node.nautilus.io', + 'rpc.nano.to', + 'workers.perish.co', + 'worker.nanoriver.cc', + 'xmr-node.cakewallet.com:18081', + 'node.sethforprivacy.com:443', + 'nodes.hashvault.pro:18081', + 'polygon-bor-rpc.publicnode.com', + 'matic.nownodes.io', + 'api.mainnet-beta.solana.com:443', + 'solana-rpc.publicnode.com:443', + 'solana-mainnet.core.chainstack.com', + 'api.trongrid.io', + 'trx.nownodes.io', + 'node3.monerodevs.org:34568', + 'node2.monerodevs.org:34568', + '37.27.100.59:10500', + 'zano.cakewallet.com:11211', + 'electrum.cakewallet.com:50002', + ]; + for (var node in [...nodes.values.toList(), ...powNodes.values.toList()]) { + if (listOfDefaultNodesWithAutoSwitching.contains(node.uriRaw)) { + node.isEnabledForAutoSwitching = true; + await node.save(); + } + } +} + diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index 01e6322cc..fa7184e8e 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -344,7 +344,13 @@ class AddressResolver { if (bip353AddressMap != null && bip353AddressMap.isNotEmpty) { final chosenAddress = await Bip353Record.pickBip353AddressChoice(context, text, bip353AddressMap); if (chosenAddress != null) { - return ParsedAddress.fetchBip353AddressAddress(address: chosenAddress, name: text); + try { + final dnsProof = await Bip353Record.fetchDnsProof(text); + return ParsedAddress.fetchBip353AddressAddress(address: chosenAddress, name: text, dnsProof: dnsProof); + } catch (e) { + printV('Bip353Record.fetchBip353AddressAddress error: $e'); + return ParsedAddress.fetchBip353AddressAddress(address: chosenAddress, name: text); + } } } diff --git a/lib/entities/parsed_address.dart b/lib/entities/parsed_address.dart index 1f2529221..74acab80a 100644 --- a/lib/entities/parsed_address.dart +++ b/lib/entities/parsed_address.dart @@ -26,6 +26,7 @@ class ParsedAddress { this.profileImageUrl = '', this.profileName = '', this.parseFrom = ParseFrom.notParsed, + this.bip353DnsProof, }); factory ParsedAddress.fetchEmojiAddress({ @@ -59,11 +60,13 @@ class ParsedAddress { factory ParsedAddress.fetchBip353AddressAddress ({ required String address, required String name, + String? dnsProof, }) { return ParsedAddress( addresses: [address], name: name, parseFrom: ParseFrom.bip353, + bip353DnsProof: dnsProof, ); } @@ -178,4 +181,5 @@ class ParsedAddress { final String profileImageUrl; final String profileName; final ParseFrom parseFrom; + final String? bip353DnsProof; } diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index aca8f79b4..d0e6a56fc 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -112,4 +112,5 @@ class PreferencesKey { '${PreferencesKey.walletConnectPairingTopicsList}_${publicKey}'; static String backgroundSyncLastTrigger(String walletId) => 'background_sync_last_trigger_${walletId}'; static const backgroundSyncNotificationsEnabled = 'background_sync_notifications_enabled'; + static const enableAutomaticNodeSwitching = 'enable_automatic_node_switching'; } diff --git a/lib/main.dart b/lib/main.dart index 67110ee0c..0b9f1b323 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/app_scroll_behavior.dart'; import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/background_sync.dart'; +import 'package:cake_wallet/core/node_switching_service.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/default_settings_migration.dart'; @@ -249,7 +250,7 @@ Future initializeAppConfigs({bool loadWallet = true}) async { payjoinSessionSource: payjoinSessionSource, anonpayInvoiceInfo: anonpayInvoiceInfo, havenSeedStore: havenSeedStore, - initialMigrationVersion: 49, + initialMigrationVersion: 50, ); } @@ -327,6 +328,7 @@ class AppState extends State with SingleTickerProviderStateMixin { final authService = getIt.get(); final linkViewModel = getIt.get(); final tradeMonitor = getIt.get(); + final nodeSwitchingService = getIt.get(); final settingsStore = appStore.settingsStore; final statusBarColor = Colors.transparent; final authenticationStore = getIt.get(); @@ -351,6 +353,7 @@ class AppState extends State with SingleTickerProviderStateMixin { authService: authService, linkViewModel: linkViewModel, tradeMonitor: tradeMonitor, + nodeSwitchingService: nodeSwitchingService, child: ThemeProvider( themeStore: appStore.themeStore, materialAppBuilder: (context, theme, darkTheme, themeMode) => MaterialApp( diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index 257ff895e..9c51749e8 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -399,9 +399,9 @@ class CWMonero extends Monero { } @override - String exportOutputsUR(Object wallet, bool all) { + Map exportOutputsUR(Object wallet) { final moneroWallet = wallet as MoneroWallet; - return moneroWallet.exportOutputsUR(all); + return moneroWallet.exportOutputsUR(); } @override diff --git a/lib/reactions/bootstrap.dart b/lib/reactions/bootstrap.dart index 6fb4c5ab2..5f16c5b54 100644 --- a/lib/reactions/bootstrap.dart +++ b/lib/reactions/bootstrap.dart @@ -14,6 +14,7 @@ import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/authentication_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; +import 'package:cake_wallet/core/node_switching_service.dart'; Future bootstrapOffline() async { final authenticationStore = getIt.get(); @@ -40,4 +41,6 @@ void bootstrapOnline(GlobalKey navigatorKey, {required bool load startCurrentFiatApiModeChangeReaction(appStore, settingsStore, fiatConversionStore); startOnCurrentNodeChangeReaction(appStore); startFiatRateUpdate(appStore, settingsStore, fiatConversionStore); + + getIt.get().startHealthCheckTimer(); } \ No newline at end of file diff --git a/lib/src/screens/connect_device/connect_device_page.dart b/lib/src/screens/connect_device/connect_device_page.dart index 9e332369e..ffa9e5470 100644 --- a/lib/src/screens/connect_device/connect_device_page.dart +++ b/lib/src/screens/connect_device/connect_device_page.dart @@ -197,9 +197,9 @@ class ConnectDevicePageBodyState extends State { Padding( padding: EdgeInsets.only(left: 20, right: 20, bottom: 20), child: Text( - Platform.isIOS - ? S.of(context).connect_your_hardware_wallet_ios - : S.of(context).connect_your_hardware_wallet, + Platform.isAndroid + ? S.of(context).connect_your_hardware_wallet + : S.of(context).connect_your_hardware_wallet_ios, style: Theme.of(context) .textTheme.titleMedium, textAlign: TextAlign.center, diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index 961d2ad8c..a3ab59be5 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -174,8 +174,12 @@ class ExchangeTradeState extends State { onPressed: () async { Navigator.of(context).pushNamed(Routes.exchangeTradeExternalSendPage); }, - color: Theme.of(context).colorScheme.surfaceContainer, - textColor: Theme.of(context).colorScheme.onSecondaryContainer, + color: widget.exchangeTradeViewModel.isSendable + ? Theme.of(context).colorScheme.surfaceContainer + : Theme.of(context).colorScheme.primary, + textColor: widget.exchangeTradeViewModel.isSendable + ? Theme.of(context).colorScheme.onSecondaryContainer + : Theme.of(context).colorScheme.onPrimary, ), SizedBox(height: 16), Observer( @@ -183,18 +187,19 @@ class ExchangeTradeState extends State { final trade = widget.exchangeTradeViewModel.trade; final sendingState = widget.exchangeTradeViewModel.sendViewModel.state; - return widget.exchangeTradeViewModel.isSendable && - !(sendingState is TransactionCommitted) - ? LoadingPrimaryButton( - key: ValueKey('exchange_trade_page_send_from_cake_button_key'), - isDisabled: trade.inputAddress == null || trade.inputAddress!.isEmpty, - isLoading: sendingState is IsExecutingState, - onPressed: () => widget.exchangeTradeViewModel.confirmSending(), - text: S.current.send_from_cake_wallet, - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimary, - ) - : Offstage(); + return Offstage( + offstage: !(widget.exchangeTradeViewModel.isSendable && + !(sendingState is TransactionCommitted)), + child: LoadingPrimaryButton( + key: ValueKey('exchange_trade_page_send_from_cake_button_key'), + isDisabled: trade.inputAddress == null || trade.inputAddress!.isEmpty, + isLoading: sendingState is IsExecutingState, + onPressed: () => widget.exchangeTradeViewModel.confirmSending(), + text: S.current.send_from_cake_wallet, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + ), + ); }, ), ], diff --git a/lib/src/screens/integrations/deuro/savings_page.dart b/lib/src/screens/integrations/deuro/savings_page.dart index 38054c09d..8aee7fd83 100644 --- a/lib/src/screens/integrations/deuro/savings_page.dart +++ b/lib/src/screens/integrations/deuro/savings_page.dart @@ -9,7 +9,9 @@ import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/tooltip_bottom_sheet.dart'; import 'package:cake_wallet/src/widgets/gradient_background.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/integrations/deuro_view_model.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; @@ -37,86 +39,71 @@ class DEuroSavingsPage extends BasePage { @override String get title => S.current.deuro_savings; - Widget trailing(BuildContext context) => MergeSemantics( - child: SizedBox( - height: 37, - width: 37, - child: ButtonTheme( - minWidth: double.minPositive, - child: Semantics( - label: "Refresh", - child: TextButton( - style: TextButton.styleFrom( - foregroundColor: Theme.of(context).colorScheme.onSurface, - overlayColor: WidgetStateColor.resolveWith((states) => Colors.transparent), - ), - onPressed: _dEuroViewModel.reloadSavingsUserData, - child: Icon( - Icons.refresh, - color: pageIconColor(context), - size: 20, - ), - ), - ), - ), - ), - ); - @override Widget body(BuildContext context) { WidgetsBinding.instance.addPostFrameCallback((_) => _setReactions(context, _dEuroViewModel)); - return Container( - width: double.infinity, - child: Column( - children: [ - Observer( - builder: (_) => SavingsCard( - isDarkTheme: currentTheme.isDark, - interestRate: "${_dEuroViewModel.interestRate}%", - savingsBalance: _dEuroViewModel.savingsBalance, - fiatSavingsBalance: _dEuroViewModel.fiatSavingsBalance, - currency: CryptoCurrency.deuro, - fiatCurrency: _dEuroViewModel.isFiatDisabled ? null : _dEuroViewModel.fiat, - onAddSavingsPressed: () => _onSavingsAdd(context), - onRemoveSavingsPressed: () => _onSavingsRemove(context), - onApproveSavingsPressed: _dEuroViewModel.prepareApproval, - onTooltipPressed: () => _onSavingsTooltipPressed(context), - isEnabled: _dEuroViewModel.isEnabled, - isLoading: _dEuroViewModel.isLoading, - ), - ), - Observer( - builder: (_) => InterestCardWidget( - isDarkTheme: currentTheme.isDark, - title: S.of(context).deuro_savings_collect_interest, - fiatAccruedInterest: _dEuroViewModel.fiatAccruedInterest, - fiatCurrency: _dEuroViewModel.isFiatDisabled ? null : _dEuroViewModel.fiat, - accruedInterest: _dEuroViewModel.accruedInterest, - onCollectInterest: _dEuroViewModel.prepareCollectInterest, - onReinvestInterest: _dEuroViewModel.prepareReinvestInterest, - onTooltipPressed: () => _onInterestTooltipPressed(context), - isEnabled: _dEuroViewModel.isEnabled, - ), - ), - Spacer(), - SafeArea( - child: Padding( - padding: EdgeInsets.only(bottom: 30), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - InfoChip( - label: S.of(context).deuro_about_deuro, - icon: CupertinoIcons.info, - onPressed: () => _showWelcomeTooltip(context), + return RefreshIndicator( + displacement: responsiveLayoutUtil.screenHeight * 0.1, + onRefresh: _dEuroViewModel.reloadSavingsUserData, + child: CustomScrollView( + slivers: [ + SliverFillRemaining( + child: SizedBox( + width: double.infinity, + child: Column( + children: [ + Observer( + builder: (_) => SavingsCard( + isDarkTheme: currentTheme.isDark, + interestRate: "${_dEuroViewModel.interestRateFormated}%", + savingsBalance: _dEuroViewModel.savingsBalanceFormated, + fiatSavingsBalance: _dEuroViewModel.fiatSavingsBalanceFormated, + currency: CryptoCurrency.deuro, + fiatCurrency: _dEuroViewModel.isFiatDisabled ? null : _dEuroViewModel.fiat, + onAddSavingsPressed: () => _onSavingsAdd(context), + onRemoveSavingsPressed: () => _onSavingsRemove(context), + onApproveSavingsPressed: _dEuroViewModel.prepareApproval, + onTooltipPressed: () => _onSavingsTooltipPressed(context), + isEnabled: _dEuroViewModel.isEnabled, + isLoading: _dEuroViewModel.isLoading, + ), + ), + Observer( + builder: (_) => InterestCardWidget( + isDarkTheme: currentTheme.isDark, + title: S.of(context).deuro_savings_collect_interest, + fiatAccruedInterest: _dEuroViewModel.fiatAccruedInterestFormated, + fiatCurrency: _dEuroViewModel.isFiatDisabled ? null : _dEuroViewModel.fiat, + accruedInterest: _dEuroViewModel.accruedInterestFormated, + onCollectInterest: _onCollectInterest, + onReinvestInterest: _onReinvestInterest, + onTooltipPressed: () => _onInterestTooltipPressed(context), + isEnabled: _dEuroViewModel.isSavingsActionsEnabled, + ), + ), + Spacer(), + SafeArea( + child: Padding( + padding: EdgeInsets.only(bottom: 30), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + InfoChip( + label: S.of(context).deuro_about_deuro, + icon: CupertinoIcons.info, + onPressed: () => _showWelcomeTooltip(context), + ), + SizedBox(width: 10), + InfoChip( + label: S.of(context).website, + icon: CupertinoIcons.globe, + onPressed: () => launchUrlString("https://deuro.com/"), + ) + ], + ), + ), ), - SizedBox(width: 10), - InfoChip( - label: S.of(context).website, - icon: CupertinoIcons.globe, - onPressed: () => launchUrlString("https://deuro.com/"), - ) ], ), ), @@ -126,14 +113,36 @@ class DEuroSavingsPage extends BasePage { ); } + bool _editSheetIsOpen = false; + Future _onSavingsAdd(BuildContext context) async { + if (_editSheetIsOpen) return; + _editSheetIsOpen = true; final amount = await _showEditBottomSheet(context, isAdding: true); if (amount != null) _dEuroViewModel.prepareSavingsEdit(amount, true); + _editSheetIsOpen = false; } Future _onSavingsRemove(BuildContext context) async { + if (_editSheetIsOpen) return; + _editSheetIsOpen = true; final amount = await _showEditBottomSheet(context, isAdding: false); if (amount != null) _dEuroViewModel.prepareSavingsEdit(amount, false); + _editSheetIsOpen = false; + } + + Future _onReinvestInterest() async { + if (_editSheetIsOpen) return; + _editSheetIsOpen = true; + await _dEuroViewModel.prepareReinvestInterest(); + _editSheetIsOpen = false; + } + + Future _onCollectInterest() async { + if (_editSheetIsOpen) return; + _editSheetIsOpen = true; + await _dEuroViewModel.prepareCollectInterest(); + _editSheetIsOpen = false; } bool _isReactionsSet = false; @@ -175,10 +184,10 @@ class DEuroSavingsPage extends BasePage { currency: CryptoCurrency.deuro, amount: S.of(bottomSheetContext).send_amount, amountValue: _dEuroViewModel.actionType == DEuroActionType.reinvest - ? _dEuroViewModel.accruedInterest + ? _dEuroViewModel.accruedInterestFormated : tx.amountFormatted, fiatAmountValue: _dEuroViewModel.actionType == DEuroActionType.reinvest - ? _dEuroViewModel.fiatAccruedInterest + ? _dEuroViewModel.fiatAccruedInterestFormated : _dEuroViewModel.pendingTransactionFiatAmountFormatted, fee: S.of(bottomSheetContext).send_estimated_fee, feeValue: tx.feeFormatted, @@ -301,11 +310,11 @@ class DEuroSavingsPage extends BasePage { showModalBottomSheet( context: context, - builder: (BuildContext bottomSheetContext) => InfoBottomSheet( + builder: (BuildContext bottomSheetContext) => TooltipSheet( currentTheme: currentTheme, titleText: title, titleIconPath: CryptoCurrency.deuro.iconPath, - content: content, + tooltip: content, footerType: FooterType.doubleActionButton, doubleActionRightButtonText: S.of(context).close, rightActionButtonKey: ValueKey('deuro_page_tooltip_dialog_${key}_ok_button_key'), @@ -358,7 +367,8 @@ class DEuroSavingsPage extends BasePage { content: S.of(context).deuro_tooltip_no_eth, singleActionButtonKey: ValueKey('deuro_page_tooltip_dialog_no_eth_ok_button_key'), singleActionButtonText: S.of(context).close, - onSingleActionButtonPressed: () => Navigator.of(bottomSheetContext).pop(), footerType: FooterType.singleActionButton, + onSingleActionButtonPressed: () => Navigator.of(bottomSheetContext).pop(), + footerType: FooterType.singleActionButton, ), ); } @@ -375,7 +385,9 @@ class DEuroSavingsPage extends BasePage { balanceTitle: isAdding ? S.of(context).deuro_savings_available_to_add : S.of(context).deuro_savings_available_to_remove, - balance: isAdding ? _dEuroViewModel.accountBalance : _dEuroViewModel.savingsBalance, + balance: isAdding + ? _dEuroViewModel.accountBalanceFormated + : _dEuroViewModel.savingsBalanceFormated, footerType: FooterType.none, maxHeight: MediaQuery.of(context).size.height * 0.8, ), diff --git a/lib/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart b/lib/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart index 2c9b66d57..558a39182 100644 --- a/lib/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart +++ b/lib/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart @@ -3,6 +3,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/integrations/deuro/widgets/numpad.dart'; import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cw_core/parse_fixed.dart'; import 'package:flutter/material.dart'; class SavingsEditSheet extends BaseBottomSheet { @@ -14,7 +15,8 @@ class SavingsEditSheet extends BaseBottomSheet { super.titleIconPath, this.balance, this.balanceTitle, - required super.footerType, required super.maxHeight, + required super.footerType, + required super.maxHeight, }); @override @@ -53,8 +55,35 @@ class _SavingsEditBodyState extends State<_SavingsEditBody> { } String amount = '0'; + bool isValid = false; final FocusNode _numpadFocusNode = FocusNode(); + void _onPressedAll() => setState(() { + amount = widget.balance!; + isValid = _validate(); + }); + + void _onNumberPressed(int i) => setState(() { + amount = amount == '0' ? i.toString() : '${amount}${i}'; + isValid = _validate(); + }); + + void _onDeletePressed() => setState(() { + amount = amount.length > 1 ? amount.substring(0, amount.length - 1) : '0'; + isValid = _validate(); + }); + + void _onDecimalPressed() => setState(() { + amount = '${amount.replaceAll('.', '')}.'; + isValid = _validate(); + }); + + bool _validate() { + final amountBigInt = parseFixed(amount, 18); + final balanceBigInt = parseFixed(widget.balance!, 18); + return balanceBigInt >= amountBigInt && amountBigInt > BigInt.zero; + } + @override Widget build(BuildContext context) => SafeArea( child: Column(children: [ @@ -80,18 +109,14 @@ class _SavingsEditBodyState extends State<_SavingsEditBody> { AssetBalanceRow( title: widget.balanceTitle!, amount: widget.balance!, - onAllPressed: () => setState(() => amount = widget.balance!), + onAllPressed: _onPressedAll, ), ], NumberPad( focusNode: _numpadFocusNode, - onNumberPressed: (i) => setState( - () => amount = amount == '0' ? i.toString() : '${amount}${i}', - ), - onDeletePressed: () => setState( - () => amount = amount.length > 1 ? amount.substring(0, amount.length - 1) : '0', - ), - onDecimalPressed: () => setState(() => amount = '${amount.replaceAll('.', '')}.'), + onNumberPressed: _onNumberPressed, + onDeletePressed: _onDeletePressed, + onDecimalPressed: _onDecimalPressed, ), Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), @@ -101,7 +126,7 @@ class _SavingsEditBodyState extends State<_SavingsEditBody> { color: Theme.of(context).colorScheme.primary, textColor: Theme.of(context).colorScheme.onPrimary, isLoading: false, - isDisabled: false, + isDisabled: !isValid, ), ) ]), diff --git a/lib/src/screens/nodes/widgets/node_form.dart b/lib/src/screens/nodes/widgets/node_form.dart index 11bfea20a..5036d8a2a 100644 --- a/lib/src/screens/nodes/widgets/node_form.dart +++ b/lib/src/screens/nodes/widgets/node_form.dart @@ -32,6 +32,7 @@ class NodeForm extends StatelessWidget { ..setLogin((editingNode!.login ?? '')) ..setSSL((editingNode!.isSSL)) ..setTrusted((editingNode!.trusted)) + ..setIsEnabledForAutoSwitching((editingNode!.isEnabledForAutoSwitching)) ..setSocksProxy((editingNode!.useSocksProxy)) ..setSocksProxyAddress((editingNode!.socksProxyAddress ?? '')); } @@ -153,6 +154,25 @@ class NodeForm extends StatelessWidget { ), ), SizedBox(height: 10.0), + Padding( + padding: EdgeInsets.only(top: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + Observer( + builder: (_) => StandardCheckbox( + value: nodeViewModel.isEnabledForAutoSwitching, + borderColor: Theme.of(context).colorScheme.surfaceContainerHighest, + iconColor: Theme.of(context).colorScheme.primary, + onChanged: (value) => nodeViewModel.isEnabledForAutoSwitching = value, + caption: S.current.enable_for_auto_switching, + ), + ), + ], + ), + ), + SizedBox(height: 10.0), if (nodeViewModel.hasAuthCredentials) ...[ Row( children: [ diff --git a/lib/src/screens/receive/widgets/qr_widget.dart b/lib/src/screens/receive/widgets/qr_widget.dart index 0018e632a..d6d53ee09 100644 --- a/lib/src/screens/receive/widgets/qr_widget.dart +++ b/lib/src/screens/receive/widgets/qr_widget.dart @@ -115,7 +115,8 @@ class QRWidget extends StatelessWidget { ), ), if (addressListViewModel.isPayjoinUnavailable && - !addressListViewModel.isSilentPayments) ...[ + !addressListViewModel.isSilentPayments && + !addressListViewModel.isCupcake) ...[ GestureDetector( onTap: () => _onPayjoinInactivePressed(context), child: Row( diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index ee8347f25..ea1bff6bb 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -6,6 +6,7 @@ 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/option_tile.dart'; import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cake_wallet/utils/permission_handler.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; @@ -45,10 +46,6 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { bool isRestoring = false; bool get _doesSupportHardwareWallets { - if (!DeviceInfo.instance.isMobile) { - return false; - } - if (isMoneroOnly) { return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS) .isNotEmpty; @@ -57,13 +54,40 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { return true; } + String get imageRestoreHWPath => + widget.themeType == ThemeType.dark + ? 'assets/images/restore_hw_dark.png' + : 'assets/images/restore_hw.png'; + + String get imageRestoreCupcakePath => + widget.themeType == ThemeType.dark + ? 'assets/images/restore_cupcake_dark.png' + : 'assets/images/restore_cupcake.png'; + + String get imageRestoreQRPath => + widget.themeType == ThemeType.dark + ? 'assets/images/restore_qr_dark.png' + : 'assets/images/restore_qr.png'; + + + String get imageRestoreHotWalletPath => + widget.themeType == ThemeType.dark + ? 'assets/images/restore_hot_wallet_dark.png' + : 'assets/images/restore_hot_wallet.png'; + + + String get imageRestoreBackupPath => + widget.themeType == ThemeType.dark + ? 'assets/images/restore_backup_dark.png' + : 'assets/images/restore_backup.png'; + @override Widget build(BuildContext context) { - final imageColor = Theme.of(context).colorScheme.primary; - 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); + final imageRestoreHW = Image.asset(imageRestoreHWPath, width: 55); + final imageRestoreCupcake = Image.asset(imageRestoreCupcakePath, width: 55); + final imageRestoreQR = Image.asset(imageRestoreQRPath, width: 55); + final imageSeedKeys = Image.asset(imageRestoreHotWalletPath, width: 55); + final imageRestoreBackup = Image.asset(imageRestoreBackupPath, width: 55); return Center( child: Container( @@ -80,15 +104,16 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { title: S.of(context).restore_title_from_seed_keys, description: S.of(context).restore_description_from_seed_keys, ), - if (widget.isNewInstall) + if (FeatureFlag.hasBitcoinViewOnly && DeviceInfo.instance.isMobile) Padding( padding: EdgeInsets.only(top: 24), child: OptionTile( - key: ValueKey('restore_options_from_backup_button_key'), - onPressed: () => Navigator.pushNamed(context, Routes.restoreFromBackup), - image: imageBackup, - title: S.of(context).restore_title_from_backup, - description: S.of(context).restore_description_from_backup, + key: ValueKey('restore_options_from_cupcake_button_key'), + onPressed: () => _onScanQRCode(context), + image: imageRestoreCupcake, + title: S.of(context).restore_title_from_cupcake, + description: S.of(context).restore_description_from_cupcake, + tag: S.of(context).new_tag, ), ), if (_doesSupportHardwareWallets) @@ -98,25 +123,33 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { key: ValueKey('restore_options_from_hardware_wallet_button_key'), onPressed: () => Navigator.pushNamed(context, Routes.restoreWalletFromHardwareWallet), - image: imageLedger, + image: imageRestoreHW, title: S.of(context).restore_title_from_hardware_wallet, description: S.of(context).restore_description_from_hardware_wallet, ), ), + if (widget.isNewInstall) + Padding( + padding: EdgeInsets.only(top: 24), + child: OptionTile( + key: ValueKey('restore_options_from_backup_button_key'), + onPressed: () => Navigator.pushNamed(context, Routes.restoreFromBackup), + image: imageRestoreBackup, + title: S.of(context).restore_title_from_backup, + description: S.of(context).restore_description_from_backup, + ), + ), if (DeviceInfo.instance.isMobile) Padding( padding: EdgeInsets.only(top: 24), child: OptionTile( key: ValueKey('restore_options_from_qr_button_key'), onPressed: () => _onScanQRCode(context), - icon: Icon( - Icons.qr_code_rounded, - color: imageColor, - size: 50, - ), + image: imageRestoreQR, title: S.of(context).scan_qr_code, - description: S.of(context).cold_or_recover_wallet), - ) + description: S.of(context).cold_or_recover_wallet, + ), + ), ], ), )), diff --git a/lib/src/screens/restore/wallet_restore_from_keys_form.dart b/lib/src/screens/restore/wallet_restore_from_keys_form.dart index 9ca5c2508..5713ad7d0 100644 --- a/lib/src/screens/restore/wallet_restore_from_keys_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_keys_form.dart @@ -198,14 +198,14 @@ class WalletRestoreFromKeysFormState extends State { Widget _restoreFromKeysFormFields() { // Decred can only restore a view only wallet with an account pubkey. Other // fields are not used. - if (widget.walletRestoreViewModel.type == WalletType.decred) { + if (widget.walletRestoreViewModel.onlyViewKeyRestore) { return Column( children: [ BaseTextFormField( controller: viewKeyController, hintText: S.of(context).view_key_public, maxLines: null, - ) + ), ], ); } @@ -253,13 +253,14 @@ class WalletRestoreFromKeysFormState extends State { maxLines: null, ), ), - BlockchainHeightWidget( - key: blockchainHeightKey, - hasDatePicker: widget.walletRestoreViewModel.type != WalletType.haven, - onHeightChange: (_) => null, - onHeightOrDateEntered: widget.onHeightOrDateEntered, - walletType: widget.walletRestoreViewModel.type, - ), + if (widget.walletRestoreViewModel.hasBlockchainHeightSelector) + BlockchainHeightWidget( + key: blockchainHeightKey, + hasDatePicker: widget.walletRestoreViewModel.type != WalletType.haven, + onHeightChange: (_) => null, + onHeightOrDateEntered: widget.onHeightOrDateEntered, + walletType: widget.walletRestoreViewModel.type, + ), ], ); } diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index dd4a284be..411178c83 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -199,7 +199,7 @@ class WalletRestorePage extends BasePage { credentials['seed'] = walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.text; - if (walletRestoreViewModel.hasBlockchainHeightLanguageSelector) { + if (walletRestoreViewModel.hasBlockchainHeightSelector) { credentials['height'] = walletRestoreFromSeedFormKey.currentState!.blockchainHeightKey.currentState?.height ?? -1; @@ -219,7 +219,7 @@ class WalletRestorePage extends BasePage { credentials['name'] = walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text; credentials['viewKey'] = walletRestoreFromKeysFormKey.currentState!.viewKeyController.text; - if (walletRestoreViewModel.type != WalletType.decred) { + if (!walletRestoreViewModel.onlyViewKeyRestore) { credentials['address'] = walletRestoreFromKeysFormKey.currentState!.addressController.text; credentials['spendKey'] = @@ -519,7 +519,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> } }, onViewKeyEntered: (bool entered) { - if (walletRestoreViewModel.type == WalletType.decred) { + if (widget.walletRestoreViewModel.onlyViewKeyRestore) { walletRestoreViewModel.isButtonEnabled = entered; } }, @@ -536,7 +536,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> key: widget.walletRestoreFromSeedFormKey, restoredWallet: walletRestoreViewModel.restoredWallet, seedSettingsViewModel: widget.seedSettingsViewModel, - displayBlockHeightSelector: widget.walletRestoreViewModel.hasBlockchainHeightLanguageSelector, + displayBlockHeightSelector: widget.walletRestoreViewModel.hasBlockchainHeightSelector, displayLanguageSelector: widget.walletRestoreViewModel.hasSeedLanguageSelector, type: widget.walletRestoreViewModel.type, blockHeightFocusNode: widget.blockHeightFocusNode, @@ -563,7 +563,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> } void _validateOnChange({bool isPolyseed = false}) { - if (!isPolyseed && walletRestoreViewModel.hasBlockchainHeightLanguageSelector) { + if (!isPolyseed && walletRestoreViewModel.hasBlockchainHeightSelector) { final hasHeight = walletRestoreFromSeedFormKey .currentState?.blockchainHeightKey.currentState?.restoreHeightController.text.isNotEmpty; diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index 19e8cab5f..a73c20fb8 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -2,8 +2,10 @@ import 'dart:async'; import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/core/node_switching_service.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; import 'package:cake_wallet/core/trade_monitor.dart'; +import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -29,6 +31,7 @@ class Root extends StatefulWidget { required this.authService, required this.linkViewModel, required this.tradeMonitor, + required this.nodeSwitchingService, }) : super(key: key); final AuthenticationStore authenticationStore; @@ -38,6 +41,7 @@ class Root extends StatefulWidget { final Widget child; final LinkViewModel linkViewModel; final TradeMonitor tradeMonitor; + final NodeSwitchingService nodeSwitchingService; @override RootState createState() => RootState(); @@ -161,6 +165,10 @@ class RootState extends State with WidgetsBindingObserver { } widget.tradeMonitor.resumeTradeMonitoring(); + + // Trigger node health check when app resumes + widget.nodeSwitchingService.performHealthCheck(); + break; default: break; diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 032ed0a89..e75cba9ed 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -450,7 +450,7 @@ class SendPage extends BasePage { } if (monero!.needExportOutputs(sendViewModel.wallet, amount)) { await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, - arguments: 'export-outputs'); + arguments: monero!.exportOutputsUR(sendViewModel.wallet)); await Future.delayed( Duration(seconds: 1)); // wait for monero to refresh the state } @@ -570,6 +570,7 @@ class SendPage extends BasePage { return ConfirmSendingBottomSheet( key: ValueKey('send_page_confirm_sending_bottom_sheet_key'), titleText: S.of(bottomSheetContext).confirm_transaction, + accessibleNavigationModeSlideActionButtonText: S.of(bottomSheetContext).send, currentTheme: currentTheme, footerType: FooterType.slideActionButton, walletType: sendViewModel.walletType, diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index d8b3517aa..8c372b161 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -258,7 +258,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin output.setSendAll(await sendViewModel.sendingBalance), ), diff --git a/lib/src/screens/settings/manage_nodes_page.dart b/lib/src/screens/settings/manage_nodes_page.dart index a5db773cc..92d1d5e76 100644 --- a/lib/src/screens/settings/manage_nodes_page.dart +++ b/lib/src/screens/settings/manage_nodes_page.dart @@ -2,6 +2,7 @@ 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/nodes/widgets/node_list_row.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.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/utils/show_pop_up.dart'; @@ -34,7 +35,23 @@ class ManageNodesPage extends BasePage { onTap: (_) async => await Navigator.of(context).pushNamed(Routes.newNode), ), ), - SizedBox(height: 20), + Observer( + builder: (_) => SettingsSwitcherCell( + key: ValueKey('manage_nodes_page_enable_auto_node_switching_button_key'), + title: S.current.enable_auto_node_switching, + value: isPow + ? powNodeListViewModel!.enableAutomaticNodeSwitching + : nodeListViewModel!.enableAutomaticNodeSwitching, + onValueChange: (BuildContext context, bool value) { + if (isPow) { + powNodeListViewModel!.setEnableAutomaticNodeSwitching(value); + } else { + nodeListViewModel!.setEnableAutomaticNodeSwitching(value); + } + }, + ), + ), + SizedBox(height: 8), Observer( builder: (BuildContext context) { int itemsCount = diff --git a/lib/src/screens/settings/other_settings_page.dart b/lib/src/screens/settings/other_settings_page.dart index 2ff9dcdaf..78125fbeb 100644 --- a/lib/src/screens/settings/other_settings_page.dart +++ b/lib/src/screens/settings/other_settings_page.dart @@ -36,22 +36,22 @@ class OtherSettingsPage extends BasePage { if (_otherSettingsViewModel.displayTransactionPriority) _otherSettingsViewModel.walletType == WalletType.bitcoin ? SettingsPriorityPickerCell( - title: S.current.settings_fee_priority, - items: priorityForWalletType(_otherSettingsViewModel.walletType), - displayItem: _otherSettingsViewModel.getDisplayBitcoinPriority, - selectedItem: _otherSettingsViewModel.transactionPriority, - customItemIndex: _otherSettingsViewModel.customPriorityItemIndex, - onItemSelected: _otherSettingsViewModel.onDisplayBitcoinPrioritySelected, - customValue: _otherSettingsViewModel.customBitcoinFeeRate, - maxValue: _otherSettingsViewModel.maxCustomFeeRate?.toDouble(), + title: S.current.settings_fee_priority, + items: priorityForWalletType(_otherSettingsViewModel.walletType), + displayItem: _otherSettingsViewModel.getDisplayBitcoinPriority, + selectedItem: _otherSettingsViewModel.transactionPriority, + customItemIndex: _otherSettingsViewModel.customPriorityItemIndex, + onItemSelected: _otherSettingsViewModel.onDisplayBitcoinPrioritySelected, + customValue: _otherSettingsViewModel.customBitcoinFeeRate, + maxValue: _otherSettingsViewModel.maxCustomFeeRate?.toDouble(), ) : SettingsPickerCell( - title: S.current.settings_fee_priority, - items: priorityForWalletType(_otherSettingsViewModel.walletType), - displayItem: _otherSettingsViewModel.getDisplayPriority, - selectedItem: _otherSettingsViewModel.transactionPriority, - onItemSelected: _otherSettingsViewModel.onDisplayPrioritySelected, - ), + title: S.current.settings_fee_priority, + items: priorityForWalletType(_otherSettingsViewModel.walletType), + displayItem: _otherSettingsViewModel.getDisplayPriority, + selectedItem: _otherSettingsViewModel.transactionPriority, + onItemSelected: _otherSettingsViewModel.onDisplayPrioritySelected, + ), if (_otherSettingsViewModel.changeRepresentativeEnabled) SettingsCellWithArrow( title: S.current.change_rep, diff --git a/lib/src/screens/ur/animated_ur_page.dart b/lib/src/screens/ur/animated_ur_page.dart index a4ef3517f..2bd9276b5 100644 --- a/lib/src/screens/ur/animated_ur_page.dart +++ b/lib/src/screens/ur/animated_ur_page.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:bbqrdart/bbqrdart.dart'; +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/entities/qr_scanner.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; @@ -7,31 +9,49 @@ 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/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/animated_ur_model.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.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; + AnimatedURPage(this.animatedURmodel, { + required Map this.urQr, + this.isAll = false, + }) { + if (urQr.keys.first.startsWith("export-outputs") && animatedURmodel.wallet.type == WalletType.monero) { + if (animatedURmodel.wallet.type == WalletType.monero) { + urQr = monero!.exportOutputsUR(animatedURmodel.wallet); + } else { + throw UnimplementedError("unable to handle UR: ${urQr.keys.first}"); + } + } + for (var key in urQr.keys) { + if (key.isEmpty || urQr[key]!.isEmpty) { + urQr.remove(key); + continue; + } + urQr[key] = urQr[key]!.trim(); } } - late String urQr; + Map urQr = {}; + final AnimatedURModel animatedURmodel; String get urQrType { - final first = urQr.trim().split("\n")[0]; + if (urQr.values.first.trim().substring(0, 2) == BBQR.header) { + return BBQR.header; + } + if (urQr.isEmpty) return "unknown"; + final first = urQr.values.first.trim().split("\n")[0]; return first.split('/')[0]; } @@ -43,11 +63,11 @@ class AnimatedURPage extends BasePage { Padding( padding: const EdgeInsets.only(top: 64.0), child: URQR( - frames: urQr.trim().split("\n"), + urqr: urQr, ), ), + if (["ur:xmr-txunsigned", "ur:xmr-output", "ur:psbt", BBQR.header].contains(urQrType)) ...{ SizedBox(height: 32), - if (urQrType == "ur:xmr-txunsigned" || urQrType == "ur:xmr-output") Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: SizedBox( @@ -60,30 +80,8 @@ class AnimatedURPage extends BasePage { ), ), ), - 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: Theme.of(context).colorScheme.onPrimary, - ), - ), - ), - ], - ); - } - - void _exportAll(BuildContext context) { - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (context) { - return AnimatedURPage(animatedURmodel, urQr: "export-outputs-all", isAll: true); }, - ), + ], ); } @@ -106,6 +104,11 @@ class AnimatedURPage extends BasePage { Navigator.of(context).pop(true); } break; + case "ur:psbt": // psbt + final ur = await presentQRScanner(context); + if (ur == null) return; + await bitcoin!.commitPsbtUR(animatedURmodel.wallet, ur.trim().split("\n")); + Navigator.of(context).pop(true); default: throw UnimplementedError("unable to handle UR: ${urQrType}"); } @@ -124,9 +127,9 @@ class AnimatedURPage extends BasePage { } class URQR extends StatefulWidget { - URQR({super.key, required this.frames}); + URQR({super.key, required this.urqr}); - List frames; + final Map urqr; @override // ignore: library_private_types_in_public_api @@ -160,6 +163,24 @@ class _URQRState extends State { super.dispose(); } + late String selected = (widget.urqr.isEmpty) ? "unknown" : widget.urqr.keys.first; + int selectedInt = 0; + + List get frames { + return widget.urqr[selected]?.split("\n") ?? []; + } + + late String nextLabel = widget.urqr.keys.toList()[(selectedInt + 1) % widget.urqr.length] ; + + void next() { + final keys = widget.urqr.keys.toList(); + setState(() { + selectedInt++; + nextLabel = keys[(selectedInt + 1) % keys.length]; + selected = keys[(selectedInt) % keys.length]; + }); + } + @override Widget build(BuildContext context) { return Column( @@ -168,10 +189,37 @@ class _URQRState extends State { children: [ Center( child: QrImage( - data: widget.frames[frame % widget.frames.length], version: -1, + data: frames[frame % frames.length], + version: -1, size: 400, ), ), + if (widget.urqr.values.length > 1) + SizedBox( + width: double.maxFinite, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: SizedBox( + width: double.maxFinite, + child: PrimaryButton( + onPressed: next, + text: nextLabel, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + ), + ), + ), + ), + if (FeatureFlag.hasDevOptions) ...{ + TextButton( + onPressed: () { + Clipboard.setData(ClipboardData(text: """Current frame (${frame % frames.length}): ${frames[frame % frames.length]}, +All frames: + - ${frames.join("\n - ")}""")); + }, + child: Text("[dev] copy debug info"), + ), + } ], ); } diff --git a/lib/src/screens/wallet_keys/wallet_keys_page.dart b/lib/src/screens/wallet_keys/wallet_keys_page.dart index 917891854..6bffd5f12 100644 --- a/lib/src/screens/wallet_keys/wallet_keys_page.dart +++ b/lib/src/screens/wallet_keys/wallet_keys_page.dart @@ -82,7 +82,7 @@ class _WalletKeysPageBodyState extends State showLegacySeedTab = widget.walletKeysViewModel.legacySeedSplit.isNotEmpty; isLegacySeedOnly = widget.walletKeysViewModel.isLegacySeedOnly; - final totalTabs = 1 + (showKeyTab ? 1 : 0) + (showLegacySeedTab ? 1 : 0); + final totalTabs = (_hasSeeds ? 1 : 0) + (showKeyTab ? 1 : 0) + (showLegacySeedTab ? 1 : 0); _tabController = TabController(length: totalTabs, vsync: this); } @@ -126,7 +126,7 @@ class _WalletKeysPageBodyState extends State dividerColor: Colors.transparent, padding: EdgeInsets.zero, tabs: [ - Tab(text: S.of(context).widgets_seed, key: ValueKey('wallet_keys_page_seed')), + if (_hasSeeds) Tab(text: S.of(context).widgets_seed, key: ValueKey('wallet_keys_page_seed')), if (showKeyTab) Tab(text: S.of(context).keys, key: ValueKey('wallet_keys_page_keys'),), if (showLegacySeedTab) Tab(text: S.of(context).legacy, key: ValueKey('wallet_keys_page_seed_legacy')), ], @@ -137,10 +137,11 @@ class _WalletKeysPageBodyState extends State child: TabBarView( controller: _tabController, children: [ - Padding( - padding: const EdgeInsets.only(left: 22, right: 22), - child: _buildSeedTab(context, false), - ), + if (_hasSeeds) + Padding( + padding: const EdgeInsets.only(left: 22, right: 22), + child: _buildSeedTab(context, false), + ), if (showKeyTab) Padding( padding: const EdgeInsets.only(left: 22, right: 22), diff --git a/lib/src/widgets/bottom_sheet/cake_pay_transaction_sent_bottom_sheet.dart b/lib/src/widgets/bottom_sheet/cake_pay_transaction_sent_bottom_sheet.dart index 6be6d389f..da91926b2 100644 --- a/lib/src/widgets/bottom_sheet/cake_pay_transaction_sent_bottom_sheet.dart +++ b/lib/src/widgets/bottom_sheet/cake_pay_transaction_sent_bottom_sheet.dart @@ -1,9 +1,11 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/rounded_icon_button.dart'; import 'package:cake_wallet/utils/image_utill.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; class CakePayTransactionSentBottomSheet extends StatelessWidget { const CakePayTransactionSentBottomSheet({ @@ -39,18 +41,36 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget { final String paymentId; final String paymentIdValue; - TextStyle _titleStyle(BuildContext ctx) => Theme.of(ctx).textTheme.bodyLarge!; + TextStyle _titleStyle(BuildContext ctx) => + Theme + .of(ctx) + .textTheme + .bodyLarge!; - TextStyle _valueStyle(BuildContext ctx) => Theme.of(ctx).textTheme.titleMedium!.copyWith( + TextStyle _valueStyle(BuildContext ctx) => + Theme + .of(ctx) + .textTheme + .titleMedium! + .copyWith( fontSize: 18, fontWeight: FontWeight.w600, ); - TextStyle _subStyle(BuildContext ctx) => Theme.of(ctx).textTheme.labelSmall!.copyWith( - color: Theme.of(ctx).colorScheme.onSurfaceVariant, + TextStyle _subStyle(BuildContext ctx) => + Theme + .of(ctx) + .textTheme + .labelSmall! + .copyWith( + color: Theme + .of(ctx) + .colorScheme + .onSurfaceVariant, ); - Widget _buildHeader(BuildContext ctx) => Column( + Widget _buildHeader(BuildContext ctx) => + Column( children: [ const SizedBox(height: 12), Container( @@ -58,7 +78,10 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget { height: 5, decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), - color: Theme.of(ctx).colorScheme.onSurface, + color: Theme + .of(ctx) + .colorScheme + .onSurface, ), ), const SizedBox(height: 20), @@ -69,11 +92,15 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget { const SizedBox(width: 6), Text( titleText, - style: Theme.of(ctx).textTheme.titleLarge!.copyWith( - fontSize: 20, - fontWeight: FontWeight.w600, - decoration: TextDecoration.none, - ), + style: Theme + .of(ctx) + .textTheme + .titleLarge! + .copyWith( + fontSize: 20, + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, + ), ), ], ), @@ -81,7 +108,8 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget { ], ); - Widget _buildBody(BuildContext context) => Padding( + Widget _buildBody(BuildContext context) => + Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Column( children: [ @@ -112,25 +140,49 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget { itemSubTitleStyle: _subStyle(context), imagePath: output.parsedAddress.profileImageUrl, ), + const SizedBox(height: 8), + _StandardTile( + itemTitle: paymentId + ': \n' + paymentIdValue, + titleStyle: _titleStyle(context).copyWith( + fontSize: 14 + ), + itemValue: '', + itemValueStyle: _valueStyle(context), + copyButton: true, + copyButtonOnPressed: () async => + await Clipboard.setData(ClipboardData(text: paymentIdValue)), + ), + Padding( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), child: Column( children: [ - Text(paymentId + ': ' + paymentIdValue, - style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center), const SizedBox(height: 18), Image.asset('assets/images/envelope.png'), const SizedBox(height: 18), Text( - S.of(context).cake_pay_card_email_delivered_message, - style: Theme.of(context).textTheme.bodyLarge, + S + .of(context) + .cake_pay_card_email_delivered_message, + style: Theme + .of(context) + .textTheme + .bodyLarge, textAlign: TextAlign.center, ), const SizedBox(height: 24), PrimaryButton( - text: S.of(context).close, - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimaryContainer, + text: S + .of(context) + .close, + color: Theme + .of(context) + .colorScheme + .primary, + textColor: Theme + .of(context) + .colorScheme + .onPrimaryContainer, onPressed: onClose, ), ], @@ -142,12 +194,18 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget { @override Widget build(BuildContext context) { - final maxHeight = MediaQuery.of(context).size.height * 0.9; + final maxHeight = MediaQuery + .of(context) + .size + .height * 0.9; return ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(30)), child: Material( - color: Theme.of(context).colorScheme.surface, + color: Theme + .of(context) + .colorScheme + .surface, child: ConstrainedBox( constraints: BoxConstraints(maxHeight: maxHeight), child: SingleChildScrollView( @@ -174,6 +232,8 @@ class _StandardTile extends StatelessWidget { this.itemSubTitle, this.itemSubTitleStyle, this.imagePath, + this.copyButton = false, + this.copyButtonOnPressed, }); final String itemTitle; @@ -183,6 +243,8 @@ class _StandardTile extends StatelessWidget { final String? itemSubTitle; final TextStyle? itemSubTitleStyle; final String? imagePath; + final bool copyButton; + final VoidCallback? copyButtonOnPressed; @override Widget build(BuildContext context) { @@ -193,12 +255,17 @@ class _StandardTile extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), - color: Theme.of(context).colorScheme.surfaceContainerLowest.withAlpha(80)), + color: Theme + .of(context) + .colorScheme + .surfaceContainerLowest + .withAlpha(80)), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Row( + mainAxisAlignment: copyButton ? MainAxisAlignment.spaceBetween : MainAxisAlignment.start, children: [ if (imagePath != null) Padding( @@ -216,10 +283,19 @@ class _StandardTile extends StatelessWidget { itemTitle, style: titleStyle, overflow: TextOverflow.ellipsis, - maxLines: 1, - softWrap: false, + maxLines: 2, + softWrap: true, )), ), + SizedBox(width: 8), + if (copyButton) + RoundedIconButton( + icon: Icons.copy_all_outlined, + onPressed: copyButtonOnPressed ?? () {}, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + ), ], ), ), diff --git a/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart index a8c643ac4..47cacea1e 100644 --- a/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart +++ b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart @@ -1,5 +1,5 @@ import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/widgets/standard_slide_button_widget.dart'; +import 'package:cake_wallet/src/widgets/rounded_icon_button.dart'; import 'package:cake_wallet/themes/core/material_base_theme.dart'; import 'package:cake_wallet/themes/utils/custom_theme_colors.dart'; import 'package:cake_wallet/utils/address_formatter.dart'; @@ -10,6 +10,7 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/pending_transaction.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 'base_bottom_sheet_widget.dart'; @@ -45,7 +46,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { _currentTheme = currentTheme, super( titleText: titleText, - maxHeight: 900, + maxHeight: 900, titleIconPath: titleIconPath, currentTheme: currentTheme, footerType: footerType, @@ -115,6 +116,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { itemSubTitleTextStyle: itemSubTitleTextStyle, tileBackgroundColor: tileBackgroundColor, applyAddressFormatting: false, + copyButton: true, )), ), if (explanation != null) @@ -307,20 +309,20 @@ class StandardInfoTile extends StatelessWidget { } class AddressTile extends StatelessWidget { - const AddressTile({ - super.key, - required this.itemTitle, - required this.itemTitleTextStyle, - required this.address, - required this.itemSubTitleTextStyle, - required this.tileBackgroundColor, - required this.walletType, - this.amountTextStyle, - this.applyAddressFormatting = true, - this.imagePath, - this.amount, - this.itemSubTitle, - }); + const AddressTile( + {super.key, + required this.itemTitle, + required this.itemTitleTextStyle, + required this.address, + required this.itemSubTitleTextStyle, + required this.tileBackgroundColor, + required this.walletType, + this.amountTextStyle, + this.applyAddressFormatting = true, + this.imagePath, + this.amount, + this.itemSubTitle, + this.copyButton = false}); final String itemTitle; final TextStyle itemTitleTextStyle; @@ -333,6 +335,7 @@ class AddressTile extends StatelessWidget { final bool applyAddressFormatting; final String? imagePath; final String? itemSubTitle; + final bool copyButton; @override Widget build(BuildContext context) { @@ -387,9 +390,24 @@ class AddressTile extends StatelessWidget { fontWeight: FontWeight.w600, decoration: TextDecoration.none, )) - : Text( - address, - style: itemTitleTextStyle, + : Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(address, + style: copyButton + ? itemTitleTextStyle.copyWith(fontSize: 12) + : itemTitleTextStyle), + SizedBox(width: 8), + if (copyButton) + RoundedIconButton( + icon: Icons.copy_all_outlined, + onPressed: () async => + await Clipboard.setData(ClipboardData(text: address)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + ), + ], ), itemSubTitle == null ? Container() diff --git a/lib/src/widgets/bottom_sheet/tooltip_bottom_sheet.dart b/lib/src/widgets/bottom_sheet/tooltip_bottom_sheet.dart new file mode 100644 index 000000000..6b75d3753 --- /dev/null +++ b/lib/src/widgets/bottom_sheet/tooltip_bottom_sheet.dart @@ -0,0 +1,46 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart'; +import 'package:flutter/material.dart'; + +class TooltipSheet extends BaseBottomSheet { + final String tooltip; + + const TooltipSheet({ + required super.currentTheme, + required super.titleText, + super.titleIconPath, + required this.tooltip, + required super.footerType, + super.maxHeight = 900, + super.singleActionButtonText, + super.onSingleActionButtonPressed, + super.singleActionButtonKey, + super.doubleActionLeftButtonText, + super.doubleActionRightButtonText, + super.onLeftActionButtonPressed, + super.onRightActionButtonPressed, + super.leftActionButtonKey, + super.rightActionButtonKey, + }); + + @override + Widget contentWidget(BuildContext context) => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Theme.of(context).colorScheme.surfaceContainer), + margin: const EdgeInsets.all(12), + padding: const EdgeInsets.all(12), + child: AutoSizeText( + tooltip, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurfaceVariant, + decoration: TextDecoration.none, + ), + ), + ); + + Widget footerWidget(BuildContext context) => SizedBox.shrink(); +} diff --git a/lib/src/widgets/option_tile.dart b/lib/src/widgets/option_tile.dart index e5e5ba62d..cdc4fb9f4 100644 --- a/lib/src/widgets/option_tile.dart +++ b/lib/src/widgets/option_tile.dart @@ -7,6 +7,7 @@ class OptionTile extends StatelessWidget { this.icon, required this.title, required this.description, + this.tag, super.key, }) : assert(image != null || icon != null); @@ -15,6 +16,7 @@ class OptionTile extends StatelessWidget { final Icon? icon; final String title; final String description; + final String? tag; @override Widget build(BuildContext context) { @@ -36,25 +38,34 @@ class OptionTile extends StatelessWidget { icon ?? image!, Expanded( child: Padding( - padding: EdgeInsets.only(left: 16), + padding: EdgeInsets.only(left: 20), child: Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - title, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 20, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurface, - ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(maxWidth: 220), + child: Text(title, style: Theme.of(context).textTheme.titleMedium), + ), + if (tag != null) Container( + decoration: BoxDecoration( + border: Border.all(), + borderRadius: BorderRadius.circular(20), + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + padding: EdgeInsets.symmetric(horizontal: 5), + margin: EdgeInsets.only(left: 5), + child: Text(tag!)) + ], ), Padding( padding: EdgeInsets.only(top: 5), child: Text( description, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( + style: Theme.of(context).textTheme.bodySmall!.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), diff --git a/lib/src/widgets/rounded_icon_button.dart b/lib/src/widgets/rounded_icon_button.dart new file mode 100644 index 000000000..72d5491f3 --- /dev/null +++ b/lib/src/widgets/rounded_icon_button.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +class RoundedIconButton extends StatelessWidget { + const RoundedIconButton( + {this.icon, + this.iconWidget, + required this.onPressed, + this.shape, + this.width, + this.height, + this.iconSize, + this.fillColor}); + + final IconData? icon; + final Widget? iconWidget; + final VoidCallback onPressed; + final ShapeBorder? shape; + final double? width; + final double? height; + final double? iconSize; + final Color? fillColor; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return RawMaterialButton( + onPressed: onPressed, + fillColor: fillColor ?? colorScheme.surfaceContainerHighest, + elevation: 0, + constraints: BoxConstraints.tightFor(width: width ?? 30, height: height ?? 30), + padding: EdgeInsets.zero, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: shape ?? const CircleBorder(), + child: icon != null + ? Icon(icon, size: iconSize ?? 14, color: colorScheme.onSurface) + : iconWidget); + } +} diff --git a/lib/src/widgets/setting_actions.dart b/lib/src/widgets/setting_actions.dart index e170d61e6..ae0552e2e 100644 --- a/lib/src/widgets/setting_actions.dart +++ b/lib/src/widgets/setting_actions.dart @@ -45,7 +45,7 @@ class SettingActions { name: (context) => S.of(context).export_outputs, image: 'assets/images/monero_menu.png', onTap: (BuildContext context) { - Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, arguments: 'export-outputs'); + Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, arguments: {'export-outputs': 'export-outputs'}); }, ); diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 219ef962d..37e118b4e 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -128,6 +128,7 @@ abstract class SettingsStoreBase with Store { required this.mwebEnabled, required this.hasEnabledMwebBefore, required this.mwebNodeUri, + required bool initialEnableAutomaticNodeSwitching, TransactionPriority? initialBitcoinTransactionPriority, TransactionPriority? initialMoneroTransactionPriority, TransactionPriority? initialWowneroTransactionPriority, @@ -186,6 +187,7 @@ abstract class SettingsStoreBase with Store { currentSyncMode = initialSyncMode, currentSyncAll = initialSyncAll, currentBuiltinTor = initialBuiltinTor, + enableAutomaticNodeSwitching = initialEnableAutomaticNodeSwitching, priority = ObservableMap() { //this.nodes = ObservableMap.of(nodes); @@ -613,6 +615,11 @@ abstract class SettingsStoreBase with Store { (_) => mwebNodeUri, (String mwebNodeUri) => _sharedPreferences.setString(PreferencesKey.mwebNodeUri, mwebNodeUri)); + + reaction( + (_) => enableAutomaticNodeSwitching, + (bool enableAutomaticNodeSwitching) => _sharedPreferences.setBool( + PreferencesKey.enableAutomaticNodeSwitching, enableAutomaticNodeSwitching)); this.nodes.observe((change) { if (change.newValue != null && change.key != null) { @@ -859,6 +866,9 @@ abstract class SettingsStoreBase with Store { @observable String mwebNodeUri; + @observable + bool enableAutomaticNodeSwitching; + final SecureStorage _secureStorage; final SharedPreferences _sharedPreferences; @@ -1034,6 +1044,8 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getBool(PreferencesKey.hasEnabledMwebBefore) ?? false; final mwebNodeUri = sharedPreferences.getString(PreferencesKey.mwebNodeUri) ?? "ltc-electrum.cakewallet.com:9333"; + final enableAutomaticNodeSwitching = + sharedPreferences.getBool(PreferencesKey.enableAutomaticNodeSwitching) ?? true; // If no value if (pinLength == null || pinLength == 0) { @@ -1335,6 +1347,7 @@ abstract class SettingsStoreBase with Store { mwebEnabled: mwebEnabled, mwebNodeUri: mwebNodeUri, hasEnabledMwebBefore: hasEnabledMwebBefore, + initialEnableAutomaticNodeSwitching: enableAutomaticNodeSwitching, initialMoneroTransactionPriority: moneroTransactionPriority, initialWowneroTransactionPriority: wowneroTransactionPriority, initialZanoTransactionPriority: zanoTransactionPriority, @@ -1478,6 +1491,9 @@ abstract class SettingsStoreBase with Store { actionlistDisplayMode = ObservableList(); actionlistDisplayMode.addAll(deserializeActionlistDisplayModes( sharedPreferences.getInt(PreferencesKey.displayActionListModeKey) ?? defaultActionsMode)); + enableAutomaticNodeSwitching = + sharedPreferences.getBool(PreferencesKey.enableAutomaticNodeSwitching) ?? + enableAutomaticNodeSwitching; var pinLength = sharedPreferences.getInt(PreferencesKey.currentPinLength); // If no value if (pinLength == null || pinLength == 0) { diff --git a/lib/utils/feature_flag.dart b/lib/utils/feature_flag.dart index 5fc6e54b2..8b2dc94dd 100644 --- a/lib/utils/feature_flag.dart +++ b/lib/utils/feature_flag.dart @@ -9,4 +9,5 @@ class FeatureFlag { static final bool isInAppTorEnabled = (Platform.isAndroid); static const int verificationWordsCount = kDebugMode ? 0 : 2; static const bool hasDevOptions = bool.fromEnvironment('hasDevOptions', defaultValue: kDebugMode); + static const bool hasBitcoinViewOnly = true; } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 7d7f43bcd..e1ef832e7 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -536,7 +536,10 @@ abstract class DashboardViewModelBase with Store { } @computed - bool get hasSilentPayments => wallet.type == WalletType.bitcoin && !wallet.isHardwareWallet; + bool get hasSilentPayments => + wallet.type == WalletType.bitcoin && + (bitcoin!.getWalletKeys(wallet)["privateKey"] ?? "").isNotEmpty && + !wallet.isHardwareWallet; @computed bool get showSilentPaymentsCard => hasSilentPayments && settingsStore.silentPaymentsCardDisplay; diff --git a/lib/view_model/dashboard/receive_option_view_model.dart b/lib/view_model/dashboard/receive_option_view_model.dart index f15d7dad6..1ae6851a0 100644 --- a/lib/view_model/dashboard/receive_option_view_model.dart +++ b/lib/view_model/dashboard/receive_option_view_model.dart @@ -22,7 +22,7 @@ abstract class ReceiveOptionViewModelBase with Store { switch (walletType) { case WalletType.bitcoin: _options = [ - ...bitcoin!.getBitcoinReceivePageOptions(), + ...bitcoin!.getBitcoinReceivePageOptions(_wallet), ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) ]; break; diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 76a49e1d5..50102bdc9 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -95,13 +95,9 @@ abstract class ExchangeTradeViewModelBase with Store { @observable bool isSendable; - @computed - String get extraInfo => switch (trade.from) { - CryptoCurrency.xlm => '\n\n' + S.current.xlm_extra_info, - CryptoCurrency.xrp => '\n\n' + S.current.xrp_extra_info, - CryptoCurrency.ton => '\n\n' + S.current.ton_extra_info, - _ => '' - }; + String get extraInfo => trade.extraId != null && trade.extraId!.isNotEmpty + ? '\n\n' + S.current.exchange_extra_info + : ''; @computed String get pendingTransactionFiatAmountValueFormatted => sendViewModel.isFiatDisabled @@ -203,10 +199,9 @@ abstract class ExchangeTradeViewModelBase with Store { ), ]); - if (trade.extraId != null) { - final shouldAddExtraId = trade.from == CryptoCurrency.xrp || trade.from == CryptoCurrency.xlm || trade.from == CryptoCurrency.ton; + final isExtraIdExist = trade.extraId != null && trade.extraId!.isNotEmpty; - if (shouldAddExtraId) { + if (isExtraIdExist) { final title = trade.from == CryptoCurrency.xrp ? S.current.destination_tag : trade.from == CryptoCurrency.xlm || trade.from == CryptoCurrency.ton @@ -215,14 +210,12 @@ abstract class ExchangeTradeViewModelBase with Store { items.add( ExchangeTradeItem( - title: title, - data: '${trade.extraId}', - isCopied: true, - isReceiveDetail: !shouldAddExtraId, - isExternalSendDetail: shouldAddExtraId - ), + title: title, + data: trade.extraId ?? '', + isCopied: true, + isReceiveDetail: !isExtraIdExist, + isExternalSendDetail: isExtraIdExist), ); - } } items.add( diff --git a/lib/view_model/hardware_wallet/ledger_view_model.dart b/lib/view_model/hardware_wallet/ledger_view_model.dart index 132a5deb9..1bc00fb02 100644 --- a/lib/view_model/hardware_wallet/ledger_view_model.dart +++ b/lib/view_model/hardware_wallet/ledger_view_model.dart @@ -9,7 +9,6 @@ import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; -import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cw_core/hardware/device_connection_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -30,10 +29,6 @@ abstract class LedgerViewModelBase with Store { late final sdk.LedgerInterface ledgerPlusUSB; bool get _doesSupportHardwareWallets { - if (!DeviceInfo.instance.isMobile) { - return false; - } - if (isMoneroOnly) { return DeviceConnectionType.supportedConnectionTypes( WalletType.monero, Platform.isIOS) @@ -65,6 +60,8 @@ abstract class LedgerViewModelBase with Store { if (bleIsEnabled && !_bleIsInitialized) { ledgerPlusBLE = sdk.LedgerInterface.ble( onPermissionRequest: (_) async { + if (Platform.isMacOS) return true; + Map statuses = await [ Permission.bluetoothScan, Permission.bluetoothConnect, diff --git a/lib/view_model/integrations/deuro_view_model.dart b/lib/view_model/integrations/deuro_view_model.dart index 5d989233a..31c3ff4a6 100644 --- a/lib/view_model/integrations/deuro_view_model.dart +++ b/lib/view_model/integrations/deuro_view_model.dart @@ -21,6 +21,8 @@ class DEuroViewModel = DEuroViewModelBase with _$DEuroViewModel; abstract class DEuroViewModelBase with Store { final AppStore _appStore; + static BigInt get MIN_ACCRUED_INTEREST => BigInt.parse("1000000000000"); + DEuroViewModelBase( this._appStore, this.balanceViewModel, @@ -44,39 +46,31 @@ abstract class DEuroViewModelBase with Store { @action void acceptDisclaimer() => _settingsStore.shouldShowDEuroDisclaimer = false; - @computed - String get pendingTransactionFiatAmountFormatted => - isFiatDisabled ? '' : '$pendingTransactionFiatAmount ${fiat.title}'; - - @computed - String get pendingTransactionFeeFiatAmountFormatted => - isFiatDisabled ? '' : '$pendingTransactionFeeFiatAmount ${fiat.title}'; - FiatCurrency get fiat => _settingsStore.fiatCurrency; @computed - String get pendingTransactionFiatAmount => - transaction == null ? '0.00' : _getDEuroFiatAmount(transaction!.amountFormatted); - - @computed - String get pendingTransactionFeeFiatAmount { - try { - if (transaction != null) { - final currency = CryptoCurrency.eth; - return calculateFiatAmount( - price: _fiatConversationStore.prices[currency]!, - cryptoAmount: transaction!.feeFormattedValue, - ); - } else { - return '0.00'; - } - } catch (_) { - return '0.00'; - } + String get pendingTransactionFiatAmountFormatted { + final amount = transaction == null ? '0.00' : _getDEuroFiatAmount(transaction!.amountFormatted); + return isFiatDisabled ? '' : '$amount ${fiat.title}'; } @computed - String get accountBalance { + String get pendingTransactionFeeFiatAmountFormatted { + var amount = '0.00'; + try { + if (transaction != null) { + final feeCurrency = CryptoCurrency.eth; + amount = calculateFiatAmount( + price: _fiatConversationStore.prices[feeCurrency]!, + cryptoAmount: transaction!.feeFormattedValue, + ); + } + } catch (_) {} + return isFiatDisabled ? '' : '$amount ${fiat.title}'; + } + + @computed + String get accountBalanceFormated { final dEuroKey = balanceViewModel.balances.keys .firstWhereOrNull((e) => e.title == CryptoCurrency.deuro.title); if (dEuroKey == null) return '0.00'; @@ -84,22 +78,30 @@ abstract class DEuroViewModelBase with Store { } @observable - String savingsBalance = '0.00'; + BigInt savingsBalance = BigInt.zero; @computed - String get fiatSavingsBalance => _getDEuroFiatAmount(savingsBalance); + String get savingsBalanceFormated => + ethereum!.formatterEthereumAmountToDouble(amount: savingsBalance).toStringAsFixed(6); + + @computed + String get fiatSavingsBalanceFormated => _getDEuroFiatAmount(savingsBalanceFormated); @observable ExecutionState state = InitialExecutionState(); @observable - String interestRate = '0'; + String interestRateFormated = '0'; @observable - String accruedInterest = '0.00'; + BigInt accruedInterest = BigInt.zero; @computed - String get fiatAccruedInterest => _getDEuroFiatAmount(accruedInterest); + String get accruedInterestFormated => + ethereum!.formatterEthereumAmountToDouble(amount: accruedInterest).toStringAsFixed(6); + + @computed + String get fiatAccruedInterestFormated => _getDEuroFiatAmount(accruedInterestFormated); @observable BigInt approvedTokens = BigInt.zero; @@ -107,6 +109,9 @@ abstract class DEuroViewModelBase with Store { @computed bool get isEnabled => approvedTokens > BigInt.zero; + @computed + bool get isSavingsActionsEnabled => isEnabled && accruedInterest >= MIN_ACCRUED_INTEREST; + @observable bool isLoading = true; @@ -121,17 +126,9 @@ abstract class DEuroViewModelBase with Store { @action Future reloadSavingsUserData() async { - final savingsBalanceRaw = ethereum!.getDEuroSavingsBalance(_appStore.wallet!); - final accruedInterestRaw = ethereum!.getDEuroAccruedInterest(_appStore.wallet!); - approvedTokens = await ethereum!.getDEuroSavingsApproved(_appStore.wallet!); - - savingsBalance = ethereum! - .formatterEthereumAmountToDouble(amount: await savingsBalanceRaw) - .toStringAsFixed(6); - accruedInterest = ethereum! - .formatterEthereumAmountToDouble(amount: await accruedInterestRaw) - .toStringAsFixed(6); + savingsBalance = await ethereum!.getDEuroSavingsBalance(_appStore.wallet!); + accruedInterest = await ethereum!.getDEuroAccruedInterest(_appStore.wallet!); isLoading = false; } @@ -139,7 +136,7 @@ abstract class DEuroViewModelBase with Store { Future reloadInterestRate() async { final interestRateRaw = await ethereum!.getDEuroInterestRate(_appStore.wallet!); - interestRate = (interestRateRaw / BigInt.from(10000)).toString(); + interestRateFormated = (interestRateRaw / BigInt.from(10000)).toString(); } @action @@ -175,7 +172,7 @@ abstract class DEuroViewModelBase with Store { } } - Future prepareCollectInterest() => prepareSavingsEdit(accruedInterest, false); + Future prepareCollectInterest() => prepareSavingsEdit(accruedInterestFormated, false); Future prepareReinvestInterest() async { try { @@ -230,13 +227,13 @@ abstract class DEuroViewModelBase with Store { String _getDEuroFiatAmount(String amount) { try { - CryptoCurrency deuro = CryptoCurrency.deuro; + var dEuro = CryptoCurrency.deuro; final keys = _fiatConversationStore.prices.keys.toList(); for (final key in keys) { - if (key.title == "DEURO") deuro = key; + if (key.title == "DEURO") dEuro = key; } return calculateFiatAmount( - price: _fiatConversationStore.prices[deuro]!, + price: _fiatConversationStore.prices[dEuro]!, cryptoAmount: amount, ); } catch (_) { 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 78544aa82..715f7e05f 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 @@ -26,6 +26,7 @@ abstract class NodeCreateOrEditViewModelBase with Store { login = '', password = '', trusted = false, + isEnabledForAutoSwitching = false, useSocksProxy = false, socksProxyAddress = ''; @@ -56,6 +57,9 @@ abstract class NodeCreateOrEditViewModelBase with Store { @observable bool trusted; + @observable + bool isEnabledForAutoSwitching; + @observable bool useSocksProxy; @@ -118,6 +122,7 @@ abstract class NodeCreateOrEditViewModelBase with Store { password = ''; useSSL = false; trusted = false; + isEnabledForAutoSwitching = false; useSocksProxy = false; socksProxyAddress = ''; } @@ -143,6 +148,9 @@ abstract class NodeCreateOrEditViewModelBase with Store { @action void setTrusted(bool val) => trusted = val; + @action + void setIsEnabledForAutoSwitching(bool val) => isEnabledForAutoSwitching = val; + @action void setSocksProxy(bool val) => useSocksProxy = val; @@ -159,6 +167,7 @@ abstract class NodeCreateOrEditViewModelBase with Store { password: password, useSSL: useSSL, trusted: trusted, + isEnabledForAutoSwitching: isEnabledForAutoSwitching, socksProxyAddress: socksProxyAddress); try { state = IsExecutingState(); @@ -190,6 +199,7 @@ abstract class NodeCreateOrEditViewModelBase with Store { password: password, useSSL: useSSL, trusted: trusted, + isEnabledForAutoSwitching: isEnabledForAutoSwitching, socksProxyAddress: socksProxyAddress); try { connectionState = IsExecutingState(); diff --git a/lib/view_model/node_list/node_list_view_model.dart b/lib/view_model/node_list/node_list_view_model.dart index 9cd9ce959..463def285 100644 --- a/lib/view_model/node_list/node_list_view_model.dart +++ b/lib/view_model/node_list/node_list_view_model.dart @@ -41,6 +41,13 @@ abstract class NodeListViewModelBase with Store { S.current.change_current_node(uri) + '${uri.endsWith('.onion') || uri.contains('.onion:') ? '\n' + (CakeTor.instance.enabled ? '' : S.current.orbot_running_alert) : ''}'; + @computed + bool get enableAutomaticNodeSwitching => settingsStore.enableAutomaticNodeSwitching; + + @action + void setEnableAutomaticNodeSwitching(bool value) => + settingsStore.enableAutomaticNodeSwitching = value; + final ObservableList nodes; final SettingsStore settingsStore; final Box _nodeSource; diff --git a/lib/view_model/node_list/pow_node_list_view_model.dart b/lib/view_model/node_list/pow_node_list_view_model.dart index 5100676f4..310a0d311 100644 --- a/lib/view_model/node_list/pow_node_list_view_model.dart +++ b/lib/view_model/node_list/pow_node_list_view_model.dart @@ -41,6 +41,13 @@ abstract class PowNodeListViewModelBase with Store { S.current.change_current_node(uri) + '${uri.endsWith('.onion') || uri.contains('.onion:') ? '\n' + (CakeTor.instance.enabled ? '' : S.current.orbot_running_alert) : ''}'; + @computed + bool get enableAutomaticNodeSwitching => settingsStore.enableAutomaticNodeSwitching; + + @action + void setEnableAutomaticNodeSwitching(bool value) => + settingsStore.enableAutomaticNodeSwitching = value; + final ObservableList nodes; final SettingsStore settingsStore; final Box _nodeSource; diff --git a/lib/view_model/restore/restore_wallet.dart b/lib/view_model/restore/restore_wallet.dart index cc3ad4123..153a9960a 100644 --- a/lib/view_model/restore/restore_wallet.dart +++ b/lib/view_model/restore/restore_wallet.dart @@ -35,7 +35,7 @@ class RestoredWallet { factory RestoredWallet.fromKey(Map json) { try { - final codeParsed = jsonDecode(json['raw_qr'].toString()); + final codeParsed = jsonDecode(json['raw_qr'].toString()); if (codeParsed["version"] == 0) { json['address'] = codeParsed["primaryAddress"]; json['view_key'] = codeParsed["privateViewKey"]; @@ -44,6 +44,7 @@ class RestoredWallet { } catch (e) { // fine, we don't care, it is only for monero anyway } + json['view_key'] ??= json['xpub']; final height = json['height'] as String?; return RestoredWallet( restoreMode: json['mode'] as WalletRestoreMode, @@ -51,7 +52,7 @@ class RestoredWallet { address: json['address'] as String?, spendKey: json['spend_key'] as String?, viewKey: json['view_key'] as String?, - height: height != null ? int.tryParse(height)??0 : 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 530ba1725..e522a20b2 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -54,6 +54,8 @@ class WalletRestoreFromQRCode { final extracted = sortedKeys.firstWhereOrNull((key) => code.toLowerCase().contains(key)); + if (code.startsWith("xpub")) return WalletType.bitcoin; + if (extracted == null) { // Special case for view-only monero wallet try { @@ -117,11 +119,15 @@ class WalletRestoreFromQRCode { formattedUri = seedPhrase != null ? '$walletType:?seed=$seedPhrase' - : throw Exception('Failed to determine valid seed phrase'); + : code.startsWith('xpub') + ? '$walletType:?xpub=$code' + : throw Exception('Failed to determine valid seed phrase'); } else { final index = code.indexOf(':'); final query = code.substring(index + 1).replaceAll('?', '&'); - formattedUri = '$walletType:?$query'; + formattedUri = code.startsWith('xpub') + ? '$walletType:?xpub=$code' + :'$walletType:?$query'; } final uri = Uri.parse(formattedUri); @@ -158,6 +164,10 @@ class WalletRestoreFromQRCode { throw Exception('Unexpected restore mode: tx_payment_id is invalid'); } + if (credentials.containsKey("xpub")) { + return WalletRestoreMode.keys; + } + if (credentials['seed'] != null) { final seedValue = credentials['seed'] as String; final words = SeedValidator.getWordList(type: type, language: 'english'); diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 9926f7c8c..f4925a766 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -289,6 +289,15 @@ abstract class OutputBase with Store { } } + Map get extra { + final fields = {}; + if (parsedAddress.parseFrom == ParseFrom.bip353) { + fields['bip353_name'] = parsedAddress.name; + fields['bip353_proof'] = parsedAddress.bip353DnsProof; + } + return fields; + } + void _setCryptoNumMaximumFractionDigits() { var maximumFractionDigits = 0; diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index a767065ec..22df67bf9 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -182,9 +182,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor if (pendingTransaction != null) { final currency = pendingTransactionFeeCurrency(walletType); final fiat = calculateFiatAmount( - price: _fiatConversationStore.prices[currency]!, - cryptoAmount: pendingTransaction!.feeFormattedValue, - ); + price: _fiatConversationStore.prices[currency]!, + cryptoAmount: pendingTransaction!.feeFormattedValue, + ); return fiat; } else { return '0.00'; @@ -210,8 +210,15 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor CryptoCurrency get currency => wallet.currency; - Validator get amountValidator => - AmountValidator(currency: walletTypeToCryptoCurrency(wallet.type)); + Validator amountValidator(Output output) => AmountValidator( + currency: walletTypeToCryptoCurrency(wallet.type), + minValue: isSendToSilentPayments(output) + ? + // TODO: get from server + // bitcoin!.silentPaymentsMinAmount + '0.00001' + : null, + ); Validator get allAmountValidator => AllAmountValidator(); @@ -289,6 +296,15 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor // If silent payments scanning, can still send payments (wallet.type == WalletType.bitcoin && wallet.syncStatus is SyncingSyncStatus); + bool isSendToSilentPayments(Output output) => + wallet.type == WalletType.bitcoin && + (RegExp(AddressValidator.silentPaymentAddressPatternMainnet).hasMatch(output.address) || + RegExp(AddressValidator.silentPaymentAddressPatternMainnet) + .hasMatch(output.extractedAddress) || + (output.parsedAddress.addresses.isNotEmpty && + RegExp(AddressValidator.silentPaymentAddressPatternMainnet) + .hasMatch(output.parsedAddress.addresses[0]))); + @computed List