From ec34b0fe93a3a9b1d5c85ca887868f4cdd6dbe1b Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Wed, 29 Oct 2025 08:47:21 +0100 Subject: [PATCH 01/68] feat: add Lightning Network support for Bitcoin wallets --- .../lib/bitcoin_receive_page_option.dart | 7 + cw_bitcoin/lib/bitcoin_wallet.dart | 98 ++++++++------ cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 1 + cw_bitcoin/lib/electrum_wallet_addresses.dart | 30 ++++- .../lib/lightning/lightning_addres_type.dart | 22 ++++ .../lib/lightning/lightning_wallet.dart | 124 ++++++++++++++++++ .../pending_lightning_transaction.dart | 44 +++++++ cw_bitcoin/pubspec.lock | 9 ++ cw_bitcoin/pubspec.yaml | 3 + .../dashboard/balance_view_model.dart | 56 ++++---- 10 files changed, 324 insertions(+), 70 deletions(-) create mode 100644 cw_bitcoin/lib/lightning/lightning_addres_type.dart create mode 100644 cw_bitcoin/lib/lightning/lightning_wallet.dart create mode 100644 cw_bitcoin/lib/lightning/pending_lightning_transaction.dart diff --git a/cw_bitcoin/lib/bitcoin_receive_page_option.dart b/cw_bitcoin/lib/bitcoin_receive_page_option.dart index 8491ae8e3f..5e07ac63b8 100644 --- a/cw_bitcoin/lib/bitcoin_receive_page_option.dart +++ b/cw_bitcoin/lib/bitcoin_receive_page_option.dart @@ -1,4 +1,5 @@ import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/lightning/lightning_addres_type.dart'; import 'package:cw_core/receive_page_option.dart'; class BitcoinReceivePageOption implements ReceivePageOption { @@ -10,6 +11,7 @@ class BitcoinReceivePageOption implements ReceivePageOption { static const mweb = BitcoinReceivePageOption._('MWEB'); static const silent_payments = BitcoinReceivePageOption._('Silent Payments'); + static const lightning = BitcoinReceivePageOption._('Lightning'); const BitcoinReceivePageOption._(this.value); @@ -20,6 +22,7 @@ class BitcoinReceivePageOption implements ReceivePageOption { } static const all = [ + BitcoinReceivePageOption.lightning, BitcoinReceivePageOption.silent_payments, BitcoinReceivePageOption.p2wpkh, BitcoinReceivePageOption.p2tr, @@ -55,6 +58,8 @@ class BitcoinReceivePageOption implements ReceivePageOption { return P2shAddressType.p2wpkhInP2sh; case BitcoinReceivePageOption.silent_payments: return SilentPaymentsAddresType.p2sp; + case BitcoinReceivePageOption.lightning: + return LightningAddressType.p2l; case BitcoinReceivePageOption.mweb: return SegwitAddresType.mweb; case BitcoinReceivePageOption.p2wpkh: @@ -77,6 +82,8 @@ class BitcoinReceivePageOption implements ReceivePageOption { return BitcoinReceivePageOption.p2sh; case SilentPaymentsAddresType.p2sp: return BitcoinReceivePageOption.silent_payments; + case LightningAddressType.p2l: + return BitcoinReceivePageOption.lightning; case SegwitAddresType.p2wpkh: default: return BitcoinReceivePageOption.p2wpkh; diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 0a2b54913a..6b3cc56f64 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -13,6 +13,7 @@ import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; import 'package:cw_bitcoin/hardware/bitcoin_hardware_wallet_service.dart'; +import 'package:cw_bitcoin/lightning/lightning_wallet.dart'; import 'package:cw_bitcoin/payjoin/manager.dart'; import 'package:cw_bitcoin/payjoin/storage.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; @@ -23,6 +24,7 @@ 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/parse_fixed.dart'; import 'package:cw_core/payjoin_session.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/unspent_coins_info.dart'; @@ -31,9 +33,7 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; -import 'package:ledger_bitcoin/ledger_bitcoin.dart'; import 'package:ledger_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'; @@ -92,24 +92,38 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { // String sideDerivationPath = derivationPath.substring(0, derivationPath.length - 1) + "1"; // final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType); + if (mnemonic != null) { + lightningWallet = LightningWallet( + mnemonic: mnemonic, + apiKey: + "MIIBdzCCASmgAwIBAgIHPpJHKP1qXzAFBgMrZXAwEDEOMAwGA1UEAxMFQnJlZXowHhcNMjUxMDIzMTQwNDQ4WhcNMzUxMDIxMTQwNDQ4WjAxMRQwEgYDVQQKEwtDYWtlIFdhbGxldDEZMBcGA1UEAxMQU2V0aCBGb3IgUHJpdmFjeTAqMAUGAytlcAMhANCD9cvfIDwcoiDKKYdT9BunHLS2/OuKzV8NS0SzqV13o4GAMH4wDgYDVR0PAQH/BAQDAgWgMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFNo5o+5ea0sNMlW/75VgGJCv2AcJMB8GA1UdIwQYMBaAFN6q1pJW843ndJIW/Ey2ILJrKJhrMB4GA1UdEQQXMBWBE3NldGhAY2FrZXdhbGxldC5jb20wBQYDK2VwA0EAl+naPfCBseV7eS4SoP0q0kvo2GHCywXoIbnlBa0y+/wlfu+oILtsGv3jGQ2egCnpgHe87yzR0ygclzz8r/jdAQ==", + lnurlDomain: "breez.tips", + ); + } + payjoinManager = PayjoinManager(PayjoinStorage(payjoinBox), this); - walletAddresses = BitcoinWalletAddresses(walletInfo, - initialAddresses: initialAddresses, - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex, - initialSilentAddresses: initialSilentAddresses, - initialSilentAddressIndex: initialSilentAddressIndex, - mainHd: hd, - sideHd: accountHD.childKey(Bip32KeyIndex(1)), - network: networkParam ?? network, - masterHd: - seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null, - isHardwareWallet: walletInfo.isHardwareWallet, - payjoinManager: payjoinManager); + walletAddresses = BitcoinWalletAddresses( + walletInfo, + initialAddresses: initialAddresses, + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, + initialSilentAddresses: initialSilentAddresses, + initialSilentAddressIndex: initialSilentAddressIndex, + mainHd: hd, + sideHd: accountHD.childKey(Bip32KeyIndex(1)), + network: networkParam ?? network, + masterHd: seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null, + isHardwareWallet: walletInfo.isHardwareWallet, + payjoinManager: payjoinManager, + lightningWallet: lightningWallet, + ); + + if (lightningWallet != null) { + walletAddresses.setLightningAddress(walletInfo.name); + } autorun((_) { - this.walletAddresses.isEnabledAutoGenerateSubaddress = - this.isEnabledAutoGenerateSubaddress; + this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; }); } @@ -146,8 +160,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { break; case DerivationType.electrum: default: - seedBytes = - await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); + seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); break; } @@ -233,8 +246,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { if (mnemonic != null) { switch (derivationInfo.derivationType) { case DerivationType.electrum: - seedBytes = - await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); + seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); break; case DerivationType.bip39: default: @@ -274,11 +286,24 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { super.close(shouldCleanup: shouldCleanup); } + @override + Future fetchBalances() async { + final balance = await super.fetchBalances(); + if (lightningWallet == null) { + return balance; + } + + final lBalance = await lightningWallet!.getBalance(); + + return ElectrumBalance(confirmed: balance.confirmed, unconfirmed: balance.unconfirmed, frozen: balance.frozen, secondConfirmed: lBalance.toInt()); + } + + late final LightningWallet? lightningWallet; + late final PayjoinManager payjoinManager; bool get isPayjoinAvailable => unspentCoinsInfo.values - .where((element) => - element.walletId == id && element.isSending && !element.isFrozen) + .where((element) => element.walletId == id && element.isSending && !element.isFrozen) .isNotEmpty; Future buildPsbt({ @@ -296,10 +321,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { }) async { final psbtReadyInputs = []; for (final utxo in utxos) { - final rawTx = - await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); - final publicKeyAndDerivationPath = - publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; + final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); + final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; psbtReadyInputs.add(PSBTReadyUtxoWithAddress( utxo: utxo.utxo, @@ -355,8 +378,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Future createTransaction(Object credentials) async { credentials = credentials as BitcoinTransactionCredentials; - final tx = (await super.createTransaction(credentials)) - as PendingBitcoinTransaction; + if (lightningWallet?.isCompatible(credentials.outputs.first.address) == true) { + return lightningWallet!.createTransaction(credentials.outputs.first.address, + parseFixed(credentials.outputs.first.cryptoAmount ?? "0", 9)); + } + + final tx = (await super.createTransaction(credentials)) as PendingBitcoinTransaction; final payjoinUri = credentials.payjoinUri; if (payjoinUri == null && !tx.shouldCommitUR()) return tx; @@ -381,8 +408,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { masterFingerprint: Uint8List.fromList([0, 0, 0, 0])); if (tx.shouldCommitUR()) { - tx.unsignedPsbt = transaction.asPsbtV0(); - return tx; + tx.unsignedPsbt = transaction.asPsbtV0(); + return tx; } final originalPsbt = @@ -406,8 +433,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Future commitPsbt(String finalizedPsbt) { final psbt = PsbtV2()..deserializeV0(base64.decode(finalizedPsbt)); - final btcTx = - BtcTransaction.fromRaw(BytesUtils.toHexString(psbt.extract())); + final btcTx = BtcTransaction.fromRaw(BytesUtils.toHexString(psbt.extract())); return PendingBitcoinTransaction( btcTx, @@ -422,8 +448,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { ).commit(); } - Future signPsbt( - String preProcessedPsbt, List utxos) async { + Future signPsbt(String preProcessedPsbt, List utxos) async { final psbt = PsbtV2()..deserializeV0(base64Decode(preProcessedPsbt)); await psbt.signWithUTXO(utxos, (txDigest, utxo, key, sighash) { @@ -486,8 +511,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Future signMessage(String message, {String? address = null}) async { if (walletInfo.isHardwareWallet) { final addressEntry = address != null - ? walletAddresses.allAddresses - .firstWhere((element) => element.address == address) + ? walletAddresses.allAddresses.firstWhere((element) => element.address == address) : null; final index = addressEntry?.index ?? 0; final isChange = addressEntry?.isHidden == true ? 1 : 0; diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index fcd0b7d8cc..8e36ffebf8 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -27,6 +27,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S super.initialSilentAddresses, super.initialSilentAddressIndex = 0, super.masterHd, + super.lightningWallet, }) : super(walletInfo); final PayjoinManager payjoinManager; diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 18d2898b4d..eddcadb563 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -3,9 +3,12 @@ import 'dart:io' show Platform; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/bitcoin_unspent.dart'; +import 'package:cw_bitcoin/lightning/lightning_addres_type.dart'; +import 'package:cw_bitcoin/lightning/lightning_wallet.dart'; +import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -51,6 +54,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { List? initialMwebAddresses, Bip32Slip10Secp256k1? masterHd, BitcoinAddressType? initialAddressPageType, + this.lightningWallet, }) : _addresses = ObservableList.of((initialAddresses ?? []).toSet()), addressesByReceiveType = ObservableList.of(([]).toSet()), @@ -64,7 +68,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { currentChangeAddressIndexByType = initialChangeAddressIndex ?? {}, _addressPageType = initialAddressPageType ?? (walletInfo.addressPageType != null - ? BitcoinAddressType.fromValue(walletInfo.addressPageType!) + ? walletInfo.addressPageType == LightningAddressType.p2l.value + ? LightningAddressType.p2l + : BitcoinAddressType.fromValue(walletInfo.addressPageType!) : SegwitAddresType.p2wpkh), silentAddresses = ObservableList.of( (initialSilentAddresses ?? []).toSet()), @@ -103,7 +109,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { )); } } - updateAddressesByMatch(); } @@ -123,6 +128,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final Bip32Slip10Secp256k1 mainHd; final Bip32Slip10Secp256k1 sideHd; final bool isHardwareWallet; + final LightningWallet? lightningWallet; @observable ObservableMap lockedReceiveAddressByType; @@ -139,6 +145,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @observable String? activeSilentAddress; + @observable + String? lightningAddress; + @computed List get allAddresses => _addresses; @@ -153,6 +162,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return silentAddress.toString(); } + if (addressPageType == LightningAddressType.p2l) { + return lightningAddress ?? ":("; + } + final typeMatchingAddresses = _addresses.where((addr) => !addr.isHidden && _isAddressPageTypeMatch(addr)).toList(); final typeMatchingReceiveAddresses = @@ -220,7 +233,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { addressRecord.type == addressPageType) { lockedReceiveAddressByType[addressPageType] = addr; } - } catch (e) { printV("ElectrumWalletAddressBase: set address ($addr): $e"); } @@ -736,4 +748,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { silentAddresses.remove(addressRecord); updateAddressesByMatch(); } + + @action + Future setLightningAddress(String walletName) async { + if (lightningWallet == null) return; + + final path = await pathForWalletDir(name: walletName, type: WalletType.bitcoin); + await lightningWallet!.init(path); + lightningAddress = await lightningWallet!.registerAddress(walletName.replaceAll(" ", "")); + + } } diff --git a/cw_bitcoin/lib/lightning/lightning_addres_type.dart b/cw_bitcoin/lib/lightning/lightning_addres_type.dart new file mode 100644 index 0000000000..275867b34f --- /dev/null +++ b/cw_bitcoin/lib/lightning/lightning_addres_type.dart @@ -0,0 +1,22 @@ +import 'package:bitcoin_base/src/bitcoin/address/address.dart'; + +class LightningAddressType implements BitcoinAddressType { + const LightningAddressType._(this.value); + static const LightningAddressType p2l = LightningAddressType._("Lightning"); + + @override + bool get isP2sh => false; + @override + bool get isSegwit => false; + + @override + final String value; + + @override + int get hashLength { + return 32; + } + + @override + String toString() => value; +} diff --git a/cw_bitcoin/lib/lightning/lightning_wallet.dart b/cw_bitcoin/lib/lightning/lightning_wallet.dart new file mode 100644 index 0000000000..cf2520c16a --- /dev/null +++ b/cw_bitcoin/lib/lightning/lightning_wallet.dart @@ -0,0 +1,124 @@ +import 'package:breez_sdk_spark_flutter/breez_sdk_spark.dart'; +import 'package:cw_bitcoin/lightning/pending_lightning_transaction.dart'; +import 'package:cw_core/pending_transaction.dart'; + +class LightningWallet { + final String mnemonic; + final String apiKey; + final String lnurlDomain; + final Network network; + late BreezSdk sdk; + + LightningWallet({ + required this.mnemonic, + required this.apiKey, + required this.lnurlDomain, + this.network = Network.mainnet, + }); + + Future init(String appPath) async { + await BreezSdkSparkLib.init(); + + final seed = Seed.mnemonic(mnemonic: mnemonic, passphrase: null); + final config = defaultConfig(network: Network.mainnet).copyWith( + lnurlDomain: lnurlDomain, + apiKey: apiKey, + ); + + final connectRequest = ConnectRequest( + config: config, + seed: seed, + storageDir: "$appPath/", + ); + + sdk = await connect(request: connectRequest); + } + + Future getAddress() async => (await sdk.getLightningAddress())?.lightningAddress; + + Future getBalance() async => + (await sdk.getInfo(request: GetInfoRequest(ensureSynced: true))).balanceSats; + + Future registerAddress(String username) async => (await sdk.registerLightningAddress( + request: RegisterLightningAddressRequest(username: username))) + .lightningAddress; + + Future isCompatible(String input) async { + try { + final inputType = await sdk.parse(input: input); + return (inputType is InputType_Bolt11Invoice) || (inputType is InputType_LightningAddress); + } catch (_) { + return false; + } + } + + Future createTransaction(String address, BigInt? amountSats) async { + final inputType = await sdk.parse(input: address); + + if (inputType is InputType_Bolt11Invoice) { + final request = PrepareSendPaymentRequest( + paymentRequest: inputType.field0.invoice.bolt11, amount: amountSats); + final prepareResponse = await sdk.prepareSendPayment(request: request); + + final paymentMethod = prepareResponse.paymentMethod; + if (paymentMethod is SendPaymentMethod_Bolt11Invoice) { + // Fees to pay via Lightning + final lightningFeeSats = paymentMethod.lightningFeeSats; + // Or fees to pay (if available) via a Spark transfer + final sparkTransferFeeSats = paymentMethod.sparkTransferFeeSats; + + return PendingLightningTransaction( + id: paymentMethod.invoiceDetails.paymentHash, + amount: paymentMethod.invoiceDetails.amountMsat?.toInt() ?? 0, + fee: lightningFeeSats.toInt() + (sparkTransferFeeSats?.toInt() ?? 0), + commitOverride: () => + sdk.sendPayment(request: SendPaymentRequest(prepareResponse: prepareResponse)), + ); + } + } else if (inputType is InputType_LightningAddress) { + final optionalValidateSuccessActionUrl = true; + + final request = PrepareLnurlPayRequest( + amountSats: amountSats!, + payRequest: inputType.field0.payRequest, + validateSuccessActionUrl: optionalValidateSuccessActionUrl, + ); + final prepareResponse = await sdk.prepareLnurlPay(request: request); + + final feeSats = prepareResponse.feeSats; + + return PendingLightningTransaction( + id: prepareResponse.invoiceDetails.paymentHash, + amount: prepareResponse.invoiceDetails.amountMsat?.toInt() ?? 0, + fee: feeSats.toInt(), + commitOverride: () => + sdk.lnurlPay(request: LnurlPayRequest(prepareResponse: prepareResponse)), + ); + } + + // If not returned earlier + throw UnimplementedError(); + } +} + +extension _ConfigCopyWith on Config { + Config copyWith({ + String? apiKey, + String? lnurlDomain, + Network? network, + int? syncIntervalSecs, + Fee? maxDepositClaimFee, + bool? preferSparkOverLightning, + bool? useDefaultExternalInputParsers, + }) => + Config( + lnurlDomain: lnurlDomain ?? this.lnurlDomain, + apiKey: apiKey ?? this.apiKey, + network: network ?? this.network, + syncIntervalSecs: syncIntervalSecs ?? this.syncIntervalSecs, + maxDepositClaimFee: maxDepositClaimFee ?? this.maxDepositClaimFee, + preferSparkOverLightning: preferSparkOverLightning ?? this.preferSparkOverLightning, + useDefaultExternalInputParsers: + useDefaultExternalInputParsers ?? this.useDefaultExternalInputParsers, + ); +} diff --git a/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart b/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart new file mode 100644 index 0000000000..72b4423783 --- /dev/null +++ b/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart @@ -0,0 +1,44 @@ +import 'package:cw_bitcoin/bitcoin_amount_format.dart'; +import 'package:cw_core/pending_transaction.dart'; + +class PendingLightningTransaction with PendingTransaction { + PendingLightningTransaction({ + required this.id, + required this.amount, + required this.fee, + this.isSendAll = false, + required this.commitOverride, + }); + + final int amount; + final int fee; + final bool isSendAll; + Future Function() commitOverride; + + @override + final String id; + + @override + String get hex => ""; + + @override + String get amountFormatted => bitcoinAmountToString(amount: amount); + + @override + String get feeFormatted => "$feeFormattedValue BTC"; + + @override + String get feeFormattedValue => bitcoinAmountToString(amount: fee); + + @override + int? get outputCount => 1; + + @override + Future commit() => commitOverride.call(); + + @override + bool shouldCommitUR() => false; + + @override + Future> commitUR() => throw UnimplementedError(); +} diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 681e68d478..38aea938ca 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -130,6 +130,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + breez_sdk_spark_flutter: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "5bc8fb5f3a5c84e2e3dd55f5d48b01152f425765" + url: "https://github.com/breez/breez-sdk-spark-flutter" + source: git + version: "0.3.2" bs58check: dependency: transitive description: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 942a5069a2..940ce80b6a 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -72,6 +72,9 @@ dependencies: git: url: https://github.com/mrcyjanek/bbqrdart ref: e867e3d0156d0b29858100f30adc2625b9dae586 + breez_sdk_spark_flutter: + git: + url: https://github.com/breez/breez-sdk-spark-flutter dev_dependencies: flutter_test: diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 552d318e4a..8d57356f37 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -1,27 +1,26 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/entities/balance_display_mode.dart'; +import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/sort_balance_types.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; -import 'package:cw_core/transaction_history.dart'; -import 'package:cw_core/wallet_base.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/entities/balance_display_mode.dart'; -import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; -import 'package:cake_wallet/store/app_store.dart'; -import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:mobx/mobx.dart'; part 'balance_view_model.g.dart'; class BalanceRecord { const BalanceRecord( - { - required this.availableBalance, + {required this.availableBalance, required this.additionalBalance, required this.secondAvailableBalance, required this.secondAdditionalBalance, @@ -150,18 +149,15 @@ abstract class BalanceViewModelBase with Store { @computed String get availableBalanceLabel { - if (displayMode == BalanceDisplayMode.hiddenBalance) { return S.current.show_balance; - } - else { + } else { return S.current.xmr_available_balance; } } @computed String get additionalBalanceLabel { - switch (wallet.type) { case WalletType.haven: case WalletType.ethereum: @@ -225,8 +221,10 @@ abstract class BalanceViewModelBase with Store { fiatAdditionalBalance: isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●', fiatAvailableBalance: isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●', fiatFrozenBalance: isFiatDisabled ? '' : '', - fiatSecondAvailableBalance: isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●', - fiatSecondAdditionalBalance: isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●', + fiatSecondAvailableBalance: + isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●', + fiatSecondAdditionalBalance: + isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●', asset: key, formattedAssetTitle: _formatterAsset(key))); } @@ -300,8 +298,16 @@ abstract class BalanceViewModelBase with Store { mwebEnabled && _hasSecondAdditionalBalanceForWalletType(wallet.type); @computed - bool get hasSecondAvailableBalance => - mwebEnabled && _hasSecondAvailableBalanceForWalletType(wallet.type); + bool get hasSecondAvailableBalance { + switch (wallet.type) { + case WalletType.bitcoin: + return true; + case WalletType.litecoin: + return mwebEnabled; + default: + return false; + } + } bool _hasAdditionalBalanceForWalletType(WalletType type) { switch (type) { @@ -317,16 +323,9 @@ abstract class BalanceViewModelBase with Store { bool _hasSecondAdditionalBalanceForWalletType(WalletType type) { if (wallet.type == WalletType.litecoin) { - if ((wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) != 0) { - return true; - } - } - return false; - } - - bool _hasSecondAvailableBalanceForWalletType(WalletType type) { - if (wallet.type == WalletType.litecoin) { - return true; + return (wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) != 0; + } else if (wallet.type == WalletType.bitcoin) { + return (wallet.balance[CryptoCurrency.btc]?.secondAdditional ?? 0) != 0; } return false; } @@ -395,7 +394,6 @@ abstract class BalanceViewModelBase with Store { return balance; } - @observable bool isShowCard; From 288085f01838169a5a760490f89cb768b999b61d Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Wed, 29 Oct 2025 12:32:44 +0100 Subject: [PATCH 02/68] refactor: rename `fiatConvertationStore` to `fiatConversionStore` for consistency and update related occurrences across codebase --- lib/di.dart | 2 +- .../dashboard/balance_view_model.dart | 143 ++++++------------ .../dashboard/home_settings_view_model.dart | 2 +- .../dashboard/transaction_list_item.dart | 12 +- 4 files changed, 58 insertions(+), 101 deletions(-) diff --git a/lib/di.dart b/lib/di.dart index fde64bd72d..4138842595 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -543,7 +543,7 @@ Future setup({ getIt.registerFactory(() => BalanceViewModel( appStore: getIt.get(), settingsStore: getIt.get(), - fiatConvertationStore: getIt.get())); + fiatConversionStore: getIt.get())); getIt.registerFactory( () => ExchangeViewModel( diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 8d57356f37..ef203ceae6 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -19,19 +19,20 @@ import 'package:mobx/mobx.dart'; part 'balance_view_model.g.dart'; class BalanceRecord { - const BalanceRecord( - {required this.availableBalance, - required this.additionalBalance, - required this.secondAvailableBalance, - required this.secondAdditionalBalance, - required this.frozenBalance, - required this.fiatAvailableBalance, - required this.fiatAdditionalBalance, - required this.fiatFrozenBalance, - required this.fiatSecondAvailableBalance, - required this.fiatSecondAdditionalBalance, - required this.asset, - required this.formattedAssetTitle}); + const BalanceRecord({ + required this.availableBalance, + required this.additionalBalance, + required this.secondAvailableBalance, + required this.secondAdditionalBalance, + required this.frozenBalance, + required this.fiatAvailableBalance, + required this.fiatAdditionalBalance, + required this.fiatFrozenBalance, + required this.fiatSecondAvailableBalance, + required this.fiatSecondAdditionalBalance, + required this.asset, + required this.formattedAssetTitle, + }); final String fiatAdditionalBalance; final String fiatAvailableBalance; @@ -51,7 +52,7 @@ class BalanceViewModel = BalanceViewModelBase with _$BalanceViewModel; abstract class BalanceViewModelBase with Store { BalanceViewModelBase( - {required this.appStore, required this.settingsStore, required this.fiatConvertationStore}) + {required this.appStore, required this.settingsStore, required this.fiatConversionStore}) : isReversing = false, isShowCard = appStore.wallet?.walletInfo.isShowIntroCakePayCard ?? false, wallet = appStore.wallet! { @@ -62,9 +63,7 @@ abstract class BalanceViewModelBase with Store { _checkMweb(); - reaction((_) => settingsStore.mwebAlwaysScan, (bool value) { - _checkMweb(); - }); + reaction((_) => settingsStore.mwebAlwaysScan, (_) => _checkMweb()); } void _checkMweb() { @@ -75,9 +74,7 @@ abstract class BalanceViewModelBase with Store { final AppStore appStore; final SettingsStore settingsStore; - final FiatConversionStore fiatConvertationStore; - - bool get canReverse => false; + final FiatConversionStore fiatConversionStore; @observable bool isReversing; @@ -85,17 +82,12 @@ abstract class BalanceViewModelBase with Store { @observable WalletBase, TransactionInfo> wallet; - @computed - bool get hasSilentPayments => wallet.type == WalletType.bitcoin && !wallet.isHardwareWallet; - @computed double get price { - final price = fiatConvertationStore.prices[appStore.wallet!.currency]; + final price = fiatConversionStore.prices[appStore.wallet!.currency]; - if (price == null) { - // price should update on next fetch: - return 0; - } + // price should update on next fetch: + if (price == null) return 0; return price; } @@ -109,12 +101,10 @@ abstract class BalanceViewModelBase with Store { @computed bool get isHomeScreenSettingsEnabled => isEVMCompatibleChain(wallet.type) || - wallet.type == WalletType.solana || - wallet.type == WalletType.tron || - wallet.type == WalletType.zano; + [WalletType.solana, WalletType.tron, WalletType.zano].contains(wallet.type); @computed - bool get hasAccounts => wallet.type == WalletType.monero || wallet.type == WalletType.wownero; + bool get hasAccounts => [WalletType.monero, WalletType.wownero].contains(wallet.type); @computed SortBalanceBy get sortBalanceBy => settingsStore.sortBalanceBy; @@ -198,9 +188,7 @@ abstract class BalanceViewModelBase with Store { String additionalBalance(CryptoCurrency cryptoCurrency) { final balance = _currencyBalance(cryptoCurrency); - if (displayMode == BalanceDisplayMode.hiddenBalance) { - return '0.0'; - } + if (displayMode == BalanceDisplayMode.hiddenBalance) return '0.0'; return balance.formattedAdditionalBalance; } @@ -229,7 +217,7 @@ abstract class BalanceViewModelBase with Store { formattedAssetTitle: _formatterAsset(key))); } final fiatCurrency = settingsStore.fiatCurrency; - final price = key.isPotentialScam ? 0.0 : fiatConvertationStore.prices[key] ?? 0; + final price = key.isPotentialScam ? 0.0 : fiatConversionStore.prices[key] ?? 0; // if (price == null) { // throw Exception('Price is null for: $key'); @@ -287,15 +275,21 @@ abstract class BalanceViewModelBase with Store { bool mwebEnabled = false; bool hasAdditionalBalance(CryptoCurrency currency) { - bool isWalletTypeActivated = _hasAdditionalBalanceForWalletType(wallet.type); - bool isNotZeroAmount = additionalBalance(currency) != "0.0"; + final isWalletTypeActivated = _hasAdditionalBalanceForWalletType(wallet.type); + final isNotZeroAmount = additionalBalance(currency) != "0.0"; return isWalletTypeActivated && isNotZeroAmount; } @computed - bool get hasSecondAdditionalBalance => - mwebEnabled && _hasSecondAdditionalBalanceForWalletType(wallet.type); + bool get hasSecondAdditionalBalance { + if (wallet.type == WalletType.litecoin && mwebEnabled) { + return (wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) != 0; + } else if (wallet.type == WalletType.bitcoin) { + return (wallet.balance[CryptoCurrency.btc]?.secondAdditional ?? 0) != 0; + } + return false; + } @computed bool get hasSecondAvailableBalance { @@ -309,26 +303,8 @@ abstract class BalanceViewModelBase with Store { } } - bool _hasAdditionalBalanceForWalletType(WalletType type) { - switch (type) { - case WalletType.monero: - case WalletType.wownero: - case WalletType.zano: - case WalletType.decred: - return true; - default: - return false; - } - } - - bool _hasSecondAdditionalBalanceForWalletType(WalletType type) { - if (wallet.type == WalletType.litecoin) { - return (wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) != 0; - } else if (wallet.type == WalletType.bitcoin) { - return (wallet.balance[CryptoCurrency.btc]?.secondAdditional ?? 0) != 0; - } - return false; - } + bool _hasAdditionalBalanceForWalletType(WalletType type) => + [WalletType.monero, WalletType.wownero, WalletType.zano, WalletType.decred].contains(type); @computed List get formattedBalances { @@ -336,25 +312,15 @@ abstract class BalanceViewModelBase with Store { balance.sort((BalanceRecord a, BalanceRecord b) { if (wallet.currency == CryptoCurrency.xhv) { - if (b.asset == CryptoCurrency.xhv) { - return 1; - } + if (b.asset == CryptoCurrency.xhv) return 1; if (b.asset == CryptoCurrency.xusd) { - if (a.asset == CryptoCurrency.xhv) { - return -1; - } - - return 1; - } - - if (b.asset == CryptoCurrency.xbtc) { + if (a.asset == CryptoCurrency.xhv) return -1; return 1; } - if (b.asset == CryptoCurrency.xeur) { - return 1; - } + if (b.asset == CryptoCurrency.xbtc) return 1; + if (b.asset == CryptoCurrency.xeur) return 1; return 0; } @@ -367,9 +333,9 @@ abstract class BalanceViewModelBase with Store { switch (sortBalanceBy) { case SortBalanceBy.FiatBalance: final aFiatBalance = _getFiatBalance( - price: fiatConvertationStore.prices[a.asset] ?? 0, cryptoAmount: a.availableBalance); + price: fiatConversionStore.prices[a.asset] ?? 0, cryptoAmount: a.availableBalance); final bFiatBalance = _getFiatBalance( - price: fiatConvertationStore.prices[b.asset] ?? 0, cryptoAmount: b.availableBalance); + price: fiatConversionStore.prices[b.asset] ?? 0, cryptoAmount: b.availableBalance); return (double.tryParse(bFiatBalance) ?? 0) .compareTo((double.tryParse(aFiatBalance)) ?? 0); @@ -387,9 +353,7 @@ abstract class BalanceViewModelBase with Store { Balance _currencyBalance(CryptoCurrency cryptoCurrency) { final balance = wallet.balance[cryptoCurrency]; - if (balance == null) { - throw Exception('No balance for ${wallet.currency}'); - } + if (balance == null) throw Exception('No balance for ${wallet.currency}'); return balance; } @@ -402,9 +366,7 @@ abstract class BalanceViewModelBase with Store { @action void _onWalletChange( WalletBase, TransactionInfo>? wallet) { - if (wallet == null) { - return; - } + if (wallet == null) return; this.wallet = wallet; _onCurrentWalletChangeReaction?.reaction.dispose(); @@ -437,18 +399,13 @@ abstract class BalanceViewModelBase with Store { } String _formatterAsset(CryptoCurrency asset) { - switch (wallet.type) { - case WalletType.haven: - final assetStringified = asset.toString(); - - if (asset != CryptoCurrency.xhv && assetStringified[0].toUpperCase() == 'X') { - return assetStringified.replaceFirst('X', 'x'); - } - - return asset.toString(); - default: - return asset.toString(); + final assetString = asset.toString(); + if (wallet.type == WalletType.haven && asset != CryptoCurrency.xhv && + assetString[0].toUpperCase() == 'X') { + return assetString.replaceFirst('X', 'x'); } + + return asset.toString(); } String getFormattedFrozenBalance(Balance walletBalance) => diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index 7ec420de73..ec55b00578 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -427,7 +427,7 @@ abstract class HomeSettingsViewModelBase with Store { void _updateFiatPrices(CryptoCurrency token) async { if (token.isPotentialScam) return; // don't fetch price data for potential scam tokens try { - _balanceViewModel.fiatConvertationStore.prices[token] = + _balanceViewModel.fiatConversionStore.prices[token] = await FiatConversionService.fetchPrice( crypto: token, fiat: _settingsStore.fiatCurrency, diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 33df3ea241..8336a2374c 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -185,21 +185,21 @@ class TransactionListItem extends ActionListItem with Keyable { break; case WalletType.ethereum: final asset = ethereum!.assetOfTransaction(balanceViewModel.wallet, transaction); - final price = balanceViewModel.fiatConvertationStore.prices[asset]; + final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( cryptoAmount: ethereum!.formatterEthereumAmountToDouble(transaction: transaction), price: price); break; case WalletType.polygon: final asset = polygon!.assetOfTransaction(balanceViewModel.wallet, transaction); - final price = balanceViewModel.fiatConvertationStore.prices[asset]; + final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( cryptoAmount: polygon!.formatterPolygonAmountToDouble(transaction: transaction), price: price); break; case WalletType.base: final asset = base!.assetOfTransaction(balanceViewModel.wallet, transaction); - final price = balanceViewModel.fiatConvertationStore.prices[asset]; + final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( cryptoAmount: base!.formatterBaseAmountToDouble(transaction: transaction), price: price); @@ -219,7 +219,7 @@ class TransactionListItem extends ActionListItem with Keyable { break; case WalletType.solana: final asset = solana!.assetOfTransaction(balanceViewModel.wallet, transaction); - final price = balanceViewModel.fiatConvertationStore.prices[asset]; + final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( cryptoAmount: solana!.getTransactionAmountRaw(transaction), price: price, @@ -227,7 +227,7 @@ class TransactionListItem extends ActionListItem with Keyable { break; case WalletType.tron: final asset = tron!.assetOfTransaction(balanceViewModel.wallet, transaction); - final price = balanceViewModel.fiatConvertationStore.prices[asset]; + final price = balanceViewModel.fiatConversionStore.prices[asset]; final cryptoAmount = tron!.getTransactionAmountRaw(transaction); amount = calculateFiatAmountRaw( cryptoAmount: cryptoAmount, @@ -240,7 +240,7 @@ class TransactionListItem extends ActionListItem with Keyable { amount = "0.00"; break; } - final price = balanceViewModel.fiatConvertationStore.prices[asset]; + final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( cryptoAmount: zano!.formatterIntAmountToDouble(amount: transaction.amount, currency: asset, forFee: false), price: price); From 068adeef963a2e7c0a47e2c92c34d0dcbfb1368b Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Thu, 30 Oct 2025 16:26:56 +0100 Subject: [PATCH 03/68] feat: enhance address validation with Lightning Network invoice support for BTC & refactor wallet type/token checks in view model --- cw_bitcoin/lib/bitcoin_wallet.dart | 6 +- cw_bitcoin/lib/electrum_wallet_addresses.dart | 9 +- .../lib/lightning/lightning_addres_type.dart | 2 + .../lib/lightning/lightning_wallet.dart | 15 +- cw_bitcoin/test/cw_bitcoin_test.dart | 25 ++- integration_test/robots/send_page_robot.dart | 4 + lib/bitcoin/cw_bitcoin.dart | 2 + lib/core/address_validator.dart | 39 +++-- lib/src/screens/send/send_page.dart | 156 +++++++++--------- lib/view_model/send/send_view_model.dart | 96 +++++------ tool/configure.dart | 1 + 11 files changed, 197 insertions(+), 158 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 6b3cc56f64..997a5dfb89 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -378,9 +378,11 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Future createTransaction(Object credentials) async { credentials = credentials as BitcoinTransactionCredentials; - if (lightningWallet?.isCompatible(credentials.outputs.first.address) == true) { + if ((await lightningWallet?.isCompatible(credentials.outputs.first.address)) == true) { + final amount = parseFixed(credentials.outputs.first.cryptoAmount ?? "0", 9); + return lightningWallet!.createTransaction(credentials.outputs.first.address, - parseFixed(credentials.outputs.first.cryptoAmount ?? "0", 9)); + amount > BigInt.zero ? amount : null); } final tx = (await super.createTransaction(credentials)) as PendingBitcoinTransaction; diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index eddcadb563..7ef455793f 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -120,8 +120,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final ObservableList addressesByReceiveType; final ObservableList receiveAddresses; final ObservableList changeAddresses; + // TODO: add this variable in `bitcoin_wallet_addresses` and just add a cast in cw_bitcoin to use it final ObservableList silentAddresses; + // TODO: add this variable in `litecoin_wallet_addresses` and just add a cast in cw_bitcoin to use it final ObservableList mwebAddresses; final BasedUtxoNetwork network; @@ -755,7 +757,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final path = await pathForWalletDir(name: walletName, type: WalletType.bitcoin); await lightningWallet!.init(path); - lightningAddress = await lightningWallet!.registerAddress(walletName.replaceAll(" ", "")); + lightningAddress = await lightningWallet!.getAddress(); + + if (lightningAddress == null) { + lightningAddress = + await lightningWallet!.registerAddress(walletName.replaceAll(" ", "").toLowerCase()); + } } } diff --git a/cw_bitcoin/lib/lightning/lightning_addres_type.dart b/cw_bitcoin/lib/lightning/lightning_addres_type.dart index 275867b34f..c12f980e77 100644 --- a/cw_bitcoin/lib/lightning/lightning_addres_type.dart +++ b/cw_bitcoin/lib/lightning/lightning_addres_type.dart @@ -4,6 +4,8 @@ class LightningAddressType implements BitcoinAddressType { const LightningAddressType._(this.value); static const LightningAddressType p2l = LightningAddressType._("Lightning"); + static const String Bolt11InvoiceMatcher = r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$'; + @override bool get isP2sh => false; @override diff --git a/cw_bitcoin/lib/lightning/lightning_wallet.dart b/cw_bitcoin/lib/lightning/lightning_wallet.dart index cf2520c16a..b19105c52b 100644 --- a/cw_bitcoin/lib/lightning/lightning_wallet.dart +++ b/cw_bitcoin/lib/lightning/lightning_wallet.dart @@ -2,6 +2,8 @@ import 'package:breez_sdk_spark_flutter/breez_sdk_spark.dart'; import 'package:cw_bitcoin/lightning/pending_lightning_transaction.dart'; import 'package:cw_core/pending_transaction.dart'; +bool _breezSdkSparkLibUninitialized = true; + class LightningWallet { final String mnemonic; final String apiKey; @@ -17,7 +19,10 @@ class LightningWallet { }); Future init(String appPath) async { - await BreezSdkSparkLib.init(); + if(_breezSdkSparkLibUninitialized) { + await BreezSdkSparkLib.init(); + _breezSdkSparkLibUninitialized = false; + } final seed = Seed.mnemonic(mnemonic: mnemonic, passphrase: null); final config = defaultConfig(network: Network.mainnet).copyWith( @@ -39,9 +44,11 @@ class LightningWallet { Future getBalance() async => (await sdk.getInfo(request: GetInfoRequest(ensureSynced: true))).balanceSats; - Future registerAddress(String username) async => (await sdk.registerLightningAddress( - request: RegisterLightningAddressRequest(username: username))) - .lightningAddress; + Future registerAddress(String username) async { + return (await sdk.registerLightningAddress( + request: RegisterLightningAddressRequest(username: username))) + .lightningAddress; + } Future isCompatible(String input) async { try { diff --git a/cw_bitcoin/test/cw_bitcoin_test.dart b/cw_bitcoin/test/cw_bitcoin_test.dart index 2a7ad6fe46..3fb24b185a 100644 --- a/cw_bitcoin/test/cw_bitcoin_test.dart +++ b/cw_bitcoin/test/cw_bitcoin_test.dart @@ -1,12 +1,23 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:cw_bitcoin/cw_bitcoin.dart'; - void main() { - test('adds one to input values', () { - final calculator = Calculator(); - expect(calculator.addOne(2), 3); - expect(calculator.addOne(-7), -6); - expect(calculator.addOne(0), 1); + group('lightning matchers', () { + final RegExp lightningInvoiceRegex = + RegExp(r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$', caseSensitive: false); + + test('Valid invoice', () { + final content = + "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw508d6qejxtdg4y5r3zarvary0c5xw7kpqdxssqfsqqqyqqqqlgqqqqqeqqjq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgqfsqqqyqqqqlgqqqqqeqqjq9qrsgq"; + expect(lightningInvoiceRegex.hasMatch(content), true); + }); + test('Valid invoice with prefix', () { + final content = + "lightning:lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw508d6qejxtdg4y5r3zarvary0c5xw7kpqdxssqfsqqqyqqqqlgqqqqqeqqjq9qrsgq"; + expect(lightningInvoiceRegex.hasMatch(content), true); + }); + test('Invalid invoice', () { + final content = "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"; // This is a Bitcoin address + expect(lightningInvoiceRegex.hasMatch(content), false); + }); }); } diff --git a/integration_test/robots/send_page_robot.dart b/integration_test/robots/send_page_robot.dart index 84d0156eaa..72ab1d9a03 100644 --- a/integration_test/robots/send_page_robot.dart +++ b/integration_test/robots/send_page_robot.dart @@ -104,6 +104,10 @@ class SendPageRobot { commonTestCases.hasValueKey('send_page_unspent_coin_button_key'); } + if (sendViewModel.hasCurrencyChanger) { + commonTestCases.hasValueKey('send_page_change_asset_button_key'); + } + if (sendViewModel.sendTemplateViewModel.hasMultiRecipient) { commonTestCases.hasValueKey('send_page_add_receiver_button_key'); } diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 4a9b2f1f64..b04fe0294d 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -687,6 +687,8 @@ class CWBitcoin extends Bitcoin { } List updateOutputs(PendingTransaction pendingTransaction, List outputs) { + if (pendingTransaction is PendingLightningTransaction) return outputs; + final pendingTx = pendingTransaction as PendingBitcoinTransaction; if (!pendingTx.hasSilentPayment) { diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 059a92c448..df072f8a32 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -12,21 +12,29 @@ const AFTER_REGEX = '(\$|\\s)'; class AddressValidator extends TextValidator { AddressValidator({required CryptoCurrency type, bool isTestnet = false}) : super( - errorMessage: S.current.error_text_address, - useAdditionalValidation: type == CryptoCurrency.btc || type == CryptoCurrency.ltc - ? (String txt) => BitcoinAddressUtils.validateAddress( - address: txt, - network: type == CryptoCurrency.btc - ? isTestnet - ? BitcoinNetwork.testnet - : BitcoinNetwork.mainnet - : LitecoinNetwork.mainnet, - ) - : type == CryptoCurrency.zano - ? zano?.validateAddress - : null, - pattern: getPattern(type, isTestnet: isTestnet), - length: getLength(type)); + errorMessage: S.current.error_text_address, + useAdditionalValidation: [CryptoCurrency.btc, CryptoCurrency.ltc].contains(type) + ? (String txt) { + final RegExp lightningInvoiceRegex = RegExp( + r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$', + caseSensitive: false); + if (lightningInvoiceRegex.hasMatch(txt)) return true; + + return BitcoinAddressUtils.validateAddress( + address: txt, + network: type == CryptoCurrency.btc + ? isTestnet + ? BitcoinNetwork.testnet + : BitcoinNetwork.mainnet + : LitecoinNetwork.mainnet, + ); + } + : type == CryptoCurrency.zano + ? zano?.validateAddress + : null, + pattern: getPattern(type, isTestnet: isTestnet), + length: getLength(type), + ); static String getPattern(CryptoCurrency type, {bool isTestnet = false}) { var pattern = ""; @@ -53,6 +61,7 @@ class AddressValidator extends TextValidator { '|(bc1q[ac-hj-np-z02-9]{25,39})' '|(bc1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))' '|(bc1q[ac-hj-np-z02-9]{40,80})' + '|(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]' '|(${silentPaymentAddressPatternMainnet})(\$|\s)'; } case CryptoCurrency.ltc: diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index f5b151482b..cd3e695aa3 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -1,13 +1,13 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/auth_service.dart'; -import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/entities/template.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; -import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; @@ -32,12 +32,12 @@ import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/payment/payment_view_model.dart'; import 'package:cake_wallet/view_model/send/output.dart'; -import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; +import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; @@ -93,8 +93,7 @@ class SendPage extends BasePage { size: 16, ); final _closeButton = currentTheme.isDark ? closeButtonImageDarkTheme : closeButtonImage; - - bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI; + final isMobileView = responsiveLayoutUtil.shouldRenderMobileUI; return MergeSemantics( child: SizedBox( @@ -145,27 +144,25 @@ class SendPage extends BasePage { @override Widget trailing(context) => Observer( - builder: (_) { - return sendViewModel.isBatchSending - ? TrailButton( - caption: S.of(context).remove, - onPressed: () { - var pageToJump = (controller.page?.round() ?? 0) - 1; - pageToJump = pageToJump > 0 ? pageToJump : 0; - final output = _defineCurrentOutput(); - sendViewModel.removeOutput(output); - controller.jumpToPage(pageToJump); - }, - ) - : TrailButton( - caption: S.of(context).clear, - onPressed: () { - final output = _defineCurrentOutput(); - _formKey.currentState?.reset(); - output.reset(); - }, - ); - }, + builder: (_) => sendViewModel.isBatchSending + ? TrailButton( + caption: S.of(context).remove, + onPressed: () { + var pageToJump = (controller.page?.round() ?? 0) - 1; + pageToJump = pageToJump > 0 ? pageToJump : 0; + final output = _defineCurrentOutput(); + sendViewModel.removeOutput(output); + controller.jumpToPage(pageToJump); + }, + ) + : TrailButton( + caption: S.of(context).clear, + onPressed: () { + final output = _defineCurrentOutput(); + _formKey.currentState?.reset(); + output.reset(); + }, + ), ); @override @@ -175,9 +172,9 @@ class SendPage extends BasePage { return Observer(builder: (_) { List sendCards = []; List keyboardActions = []; - for (var output in sendViewModel.outputs) { - var cryptoAmountFocus = FocusNode(); - var fiatAmountFocus = FocusNode(); + for (final output in sendViewModel.outputs) { + final cryptoAmountFocus = FocusNode(); + final fiatAmountFocus = FocusNode(); sendCards.add( SendCard( currentTheme: currentTheme, @@ -376,6 +373,19 @@ class SendPage extends BasePage { bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), bottomSection: Column( children: [ + if (sendViewModel.hasCurrencyChanger) + Observer( + builder: (_) => Padding( + padding: EdgeInsets.only(bottom: 12), + child: PrimaryButton( + key: ValueKey('send_page_change_asset_button_key'), + onPressed: () => presentCurrencyPicker(context), + text: 'Change your asset (${sendViewModel.selectedCryptoCurrency})', + color: Colors.transparent, + textColor: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), if (sendViewModel.sendTemplateViewModel.hasMultiRecipient) Padding( padding: EdgeInsets.only(bottom: 12), @@ -474,7 +484,8 @@ class SendPage extends BasePage { sendViewModel.state is TransactionCommitting || sendViewModel.state is IsAwaitingDeviceResponseState || sendViewModel.state is LoadingTemplateExecutingState, - isDisabled: !sendViewModel.isReadyForSend || sendViewModel.state is ExecutedSuccessfullyState, + isDisabled: !sendViewModel.isReadyForSend || + sendViewModel.state is ExecutedSuccessfullyState, ); }, ) @@ -491,9 +502,7 @@ class SendPage extends BasePage { BuildContext? loadingBottomSheetContext; void _setEffects(BuildContext context) { - if (_effectsInstalled) { - return; - } + if (_effectsInstalled) return; if (sendViewModel.isElectrumWallet) { bitcoin!.updateFeeRates(sendViewModel.wallet); @@ -515,16 +524,14 @@ class SendPage extends BasePage { (_) { showPopUp( context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - key: ValueKey('send_page_send_failure_dialog_key'), - buttonKey: ValueKey('send_page_send_failure_dialog_button_key'), - alertTitle: S.of(context).error, - alertContent: state.error, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop(), - ); - }, + builder: (context) => AlertWithOneAction( + key: ValueKey('send_page_send_failure_dialog_key'), + buttonKey: ValueKey('send_page_send_failure_dialog_button_key'), + alertTitle: S.of(context).error, + alertContent: state.error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ), ); }, ); @@ -543,7 +550,7 @@ class SendPage extends BasePage { showModalBottomSheet( context: context, isDismissible: false, - builder: (BuildContext context) { + builder: (context) { loadingBottomSheetContext = context; return LoadingBottomSheet( titleText: S.of(context).generating_transaction, @@ -597,9 +604,7 @@ class SendPage extends BasePage { if (state is TransactionCommitted) { WidgetsBinding.instance.addPostFrameCallback((_) async { - if (!context.mounted) { - return; - } + if (!context.mounted) return; newContactAddress = newContactAddress ?? sendViewModel.newContactAddress(); @@ -770,24 +775,32 @@ class SendPage extends BasePage { } Output _defineCurrentOutput() { - if (controller.page == null) { - throw Exception('Controller page is null'); - } + if (controller.page == null) throw Exception('Controller page is null'); final itemCount = controller.page!.round(); return sendViewModel.outputs[itemCount]; } - void showErrorValidationAlert(BuildContext context) async { - await showPopUp( + void showErrorValidationAlert(BuildContext context) => showPopUp( context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.of(context).error, - alertContent: 'Please, check receiver forms', - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); - } + builder: (context) => AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: 'Please, check receiver forms', + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ), + ); + + void presentCurrencyPicker(BuildContext context) => showPopUp( + builder: (_) => Picker( + items: sendViewModel.currencies, + displayItem: (item) => item.toString(), + selectedAtIndex: sendViewModel.currencies.indexOf(sendViewModel.selectedCryptoCurrency), + title: S.of(context).please_select, + mainAxisAlignment: MainAxisAlignment.center, + onItemSelected: (cur) => sendViewModel.selectedCryptoCurrency = cur, + ), + context: context, + ); bool isRegularElectrumAddress(String address) { final supportedTypes = [CryptoCurrency.btc, CryptoCurrency.ltc, CryptoCurrency.bch]; @@ -800,7 +813,7 @@ class SendPage extends BasePage { final trimmed = address.trim(); bool isValid = false; - for (var type in supportedTypes) { + for (final type in supportedTypes) { final addressPattern = AddressValidator.getAddressFromStringPattern(type); if (addressPattern != null) { final regex = RegExp('^$addressPattern\$'); @@ -811,23 +824,16 @@ class SendPage extends BasePage { } } - for (var pattern in excludedPatterns) { - if (pattern.hasMatch(trimmed)) { - return false; - } + for (final pattern in excludedPatterns) { + if (pattern.hasMatch(trimmed)) return false; } return isValid; } String _sendButtonText(BuildContext context) { - if (!sendViewModel.isReadyForSend) { - return S.of(context).synchronizing; - } - if (sendViewModel.payjoinUri != null) { - return S.of(context).send_payjoin; - } else { - return S.of(context).send; - } + if (!sendViewModel.isReadyForSend) return S.of(context).synchronizing; + if (sendViewModel.payjoinUri != null) return S.of(context).send_payjoin; + return S.of(context).send; } } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 1b446e0203..734eb6c71c 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -57,6 +57,7 @@ import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; @@ -100,7 +101,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor this.transactionDescriptionBox, this.hardwareWalletViewModel, this.unspentCoinsListViewModel, - this.feesViewModel, { + this.feesViewModel, + this.walletInfoSource, { this.coinTypeToSpendFrom = UnspentCoinType.nonMweb, }) : state = InitialExecutionState(), currencies = appStore.wallet!.balance.keys.toList(), @@ -138,21 +140,15 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor bool get isEVMWallet => isEVMCompatibleChain(walletType); @action - void setShowAddressBookPopup(bool value) { - _settingsStore.showAddressBookPopupEnabled = value; - } + void setShowAddressBookPopup(bool value) => _settingsStore.showAddressBookPopupEnabled = value; @action - void addOutput() { - outputs - .add(Output(wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency)); - } + void addOutput() => outputs + .add(Output(wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency)); @action void removeOutput(Output output) { - if (isBatchSending) { - outputs.remove(output); - } + if (isBatchSending) outputs.remove(output); } @action @@ -185,9 +181,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor @computed String get pendingTransactionFiatAmount { - if (pendingTransaction == null) { - return '0.00'; - } + if (pendingTransaction == null) return '0.00'; try { final fiat = calculateFiatAmount( @@ -310,11 +304,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor @computed String get pendingTransactionFiatAmountFormatted => - isFiatDisabled ? '' : pendingTransactionFiatAmount + ' ' + fiat.title; + isFiatDisabled ? '' : '$pendingTransactionFiatAmount ${fiat.title}'; @computed String get pendingTransactionFeeFiatAmountFormatted => - isFiatDisabled ? '' : pendingTransactionFeeFiatAmount + ' ' + fiat.title; + isFiatDisabled ? '' : '$pendingTransactionFeeFiatAmount ${fiat.title}'; @computed bool get isReadyForSend => @@ -360,13 +354,12 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor List currencies; - bool get hasYat => outputs - .any((out) => out.isParsedAddress && out.parsedAddress.parseFrom == ParseFrom.yatRecord); - WalletType get walletType => wallet.type; String? get walletCurrencyName => wallet.currency.fullName?.toLowerCase() ?? wallet.currency.name; + bool get hasCurrencyChanger => walletType == WalletType.haven; + @computed FiatCurrency get fiatCurrency => _settingsStore.fiatCurrency; @@ -393,19 +386,12 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor .toList(); @action - bool checkIfAddressIsAContact(String address) { - final contactList = contactsToShow.where((element) => element.address == address).toList(); - - return contactList.isNotEmpty; - } + bool checkIfAddressIsAContact(String address) => + contactsToShow.where((element) => element.address == address).toList().isNotEmpty; @action - bool checkIfWalletIsAnInternalWallet(String address) { - final walletContactList = - walletContactsToShow.where((element) => element.address == address).toList(); - - return walletContactList.isNotEmpty; - } + bool checkIfWalletIsAnInternalWallet(String address) => + walletContactsToShow.where((element) => element.address == address).toList().isNotEmpty; @computed bool get shouldDisplayTOTP2FAForContact => _settingsStore.shouldRequireTOTP2FAForSendsToContact; @@ -499,13 +485,14 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor if (wallet.isHardwareWallet) { state = IsAwaitingDeviceResponseState(); - if (walletType == WalletType.monero) + if (walletType == WalletType.monero) { _ledgerTxStateTimer = Timer.periodic(Duration(seconds: 1), (timer) { if (monero!.getLastLedgerCommand() == "INS_CLSAG") { timer.cancel(); state = IsDeviceSigningResponseState(); } }); + } } // Swaps.xyz (EVM) path @@ -873,11 +860,13 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor final priority = _settingsStore.priority[wallet.type]; if (priority == null && - wallet.type != WalletType.nano && - wallet.type != WalletType.banano && - wallet.type != WalletType.solana && - wallet.type != WalletType.tron && - wallet.type != WalletType.arbitrum) { + [ + WalletType.nano, + WalletType.banano, + WalletType.solana, + WalletType.tron, + WalletType.arbitrium + ].contains(wallet.type)) { throw Exception('Priority is null for wallet type: ${wallet.type}'); } @@ -962,24 +951,21 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor Set.from(contactListViewModel.contacts.map((contact) => contact.address)) ..addAll(contactListViewModel.walletContacts.map((contact) => contact.address)); - for (var output in outputs) { - String address; - if (output.isParsedAddress) { - address = output.parsedAddress.addresses.first; - } else { - address = output.address; - } + for (final output in outputs) { + final address = + output.isParsedAddress ? output.parsedAddress.addresses.first : output.address; if (address.isNotEmpty && !contactAddresses.contains(address) && selectedCryptoCurrency.raw != -1) { return ContactRecord( - contactListViewModel.contactSource, - Contact( - name: '', - address: address, - type: selectedCryptoCurrency, - )); + contactListViewModel.contactSource, + Contact( + name: '', + address: address, + type: selectedCryptoCurrency, + ), + ); } } return null; @@ -1054,11 +1040,13 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor return errorMessage; } - if (walletType == WalletType.ethereum || - walletType == WalletType.polygon || - walletType == WalletType.base || - walletType == WalletType.arbitrum || - walletType == WalletType.haven) { + if ([ + WalletType.ethereum, + WalletType.polygon, + WalletType.base, + WalletType.haven, + WalletType.arbitrium + ].contains(walletType)) { if (errorMessage.contains('gas required exceeds allowance')) { return S.current.gas_exceeds_allowance; } diff --git a/tool/configure.dart b/tool/configure.dart index 47b3379406..fb934083ee 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -144,6 +144,7 @@ import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; +import 'package:cw_bitcoin/lightning/pending_lightning_transaction.dart'; import 'package:cw_bitcoin/litecoin_wallet_service.dart'; import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:cw_bitcoin/hardware/bitcoin_ledger_service.dart'; From 9cccb6d6629aef3b3aff23ddcb0bc905dad84aed Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Fri, 31 Oct 2025 16:16:22 +0100 Subject: [PATCH 04/68] feat: add support for Lightning invoice detection, refactor MWEB deposit/withdraw actions, and integrate Lightning transaction creation with updated priority handling --- cw_bitcoin/lib/bitcoin_wallet.dart | 8 +- cw_bitcoin/lib/electrum_wallet.dart | 1 + .../lib/lightning/lightning_wallet.dart | 56 +++++++++- cw_core/lib/unspent_coin_type.dart | 2 +- lib/bitcoin/cw_bitcoin.dart | 10 ++ .../pages/balance/balance_row_widget.dart | 102 +++++++++--------- lib/src/screens/send/widgets/send_card.dart | 4 + lib/view_model/send/send_view_model.dart | 7 ++ 8 files changed, 133 insertions(+), 57 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 997a5dfb89..e5147a536a 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -27,6 +27,7 @@ import 'package:cw_core/output_info.dart'; import 'package:cw_core/parse_fixed.dart'; import 'package:cw_core/payjoin_session.dart'; import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/utils/zpub.dart'; import 'package:cw_core/wallet_info.dart'; @@ -378,11 +379,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Future createTransaction(Object credentials) async { credentials = credentials as BitcoinTransactionCredentials; - if ((await lightningWallet?.isCompatible(credentials.outputs.first.address)) == true) { - final amount = parseFixed(credentials.outputs.first.cryptoAmount ?? "0", 9); + if ((credentials.coinTypeToSpendFrom == UnspentCoinType.lightning && lightningWallet != null) || + (await lightningWallet?.isCompatible(credentials.outputs.first.address)) == true) { + final amount = parseFixed(credentials.outputs.first.cryptoAmount?.isNotEmpty == true ? credentials.outputs.first.cryptoAmount! : "0", 9); return lightningWallet!.createTransaction(credentials.outputs.first.address, - amount > BigInt.zero ? amount : null); + amount > BigInt.zero ? amount : null, credentials.priority); } final tx = (await super.createTransaction(credentials)) as PendingBitcoinTransaction; diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 7a0fecd3e7..73591a643d 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -714,6 +714,7 @@ abstract class ElectrumWalletBase case UnspentCoinType.nonMweb: return utx.bitcoinAddressRecord.type != SegwitAddresType.mweb; case UnspentCoinType.any: + case UnspentCoinType.lightning: return true; } }).toList(); diff --git a/cw_bitcoin/lib/lightning/lightning_wallet.dart b/cw_bitcoin/lib/lightning/lightning_wallet.dart index b19105c52b..b015f8d0c2 100644 --- a/cw_bitcoin/lib/lightning/lightning_wallet.dart +++ b/cw_bitcoin/lib/lightning/lightning_wallet.dart @@ -1,4 +1,5 @@ import 'package:breez_sdk_spark_flutter/breez_sdk_spark.dart'; +import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/lightning/pending_lightning_transaction.dart'; import 'package:cw_core/pending_transaction.dart'; @@ -19,7 +20,7 @@ class LightningWallet { }); Future init(String appPath) async { - if(_breezSdkSparkLibUninitialized) { + if (_breezSdkSparkLibUninitialized) { await BreezSdkSparkLib.init(); _breezSdkSparkLibUninitialized = false; } @@ -41,12 +42,17 @@ class LightningWallet { Future getAddress() async => (await sdk.getLightningAddress())?.lightningAddress; + Future getDepositAddress() async => + (await sdk.receivePayment( + request: ReceivePaymentRequest(paymentMethod: ReceivePaymentMethod.bitcoinAddress()))) + .paymentRequest; + Future getBalance() async => (await sdk.getInfo(request: GetInfoRequest(ensureSynced: true))).balanceSats; Future registerAddress(String username) async { return (await sdk.registerLightningAddress( - request: RegisterLightningAddressRequest(username: username))) + request: RegisterLightningAddressRequest(username: username))) .lightningAddress; } @@ -59,7 +65,8 @@ class LightningWallet { } } - Future createTransaction(String address, BigInt? amountSats) async { + Future createTransaction(String address, BigInt? amountSats, + BitcoinTransactionPriority? priority) async { final inputType = await sdk.parse(input: address); if (inputType is InputType_Bolt11Invoice) { @@ -76,7 +83,7 @@ class LightningWallet { return PendingLightningTransaction( id: paymentMethod.invoiceDetails.paymentHash, - amount: paymentMethod.invoiceDetails.amountMsat?.toInt() ?? 0, + amount: ((paymentMethod.invoiceDetails.amountMsat?.toInt() ?? 0) / 1000).round(), fee: lightningFeeSats.toInt() + (sparkTransferFeeSats?.toInt() ?? 0), commitOverride: () => sdk.sendPayment(request: SendPaymentRequest(prepareResponse: prepareResponse)), @@ -101,6 +108,45 @@ class LightningWallet { commitOverride: () => sdk.lnurlPay(request: LnurlPayRequest(prepareResponse: prepareResponse)), ); + } else if (inputType is InputType_BitcoinAddress) { + final request = PrepareSendPaymentRequest( + paymentRequest: inputType.field0.address, amount: amountSats); + final prepareResponse = await sdk.prepareSendPayment(request: request); + + final paymentMethod = prepareResponse.paymentMethod; + if (paymentMethod is SendPaymentMethod_BitcoinAddress) { + final feeQuote = paymentMethod.feeQuote; + OnchainConfirmationSpeed onchainConfirmationSpeed; + int fee; + + switch (priority) { + case BitcoinTransactionPriority.fast: + fee = (feeQuote.speedFast.userFeeSat + feeQuote.speedFast.l1BroadcastFeeSat).toInt(); + onchainConfirmationSpeed = OnchainConfirmationSpeed.fast; + break; + case BitcoinTransactionPriority.medium: + fee = + (feeQuote.speedMedium.userFeeSat + feeQuote.speedMedium.l1BroadcastFeeSat).toInt(); + onchainConfirmationSpeed = OnchainConfirmationSpeed.medium; + break; + case BitcoinTransactionPriority.slow: + default: + fee = (feeQuote.speedSlow.userFeeSat + feeQuote.speedSlow.l1BroadcastFeeSat).toInt(); + onchainConfirmationSpeed = OnchainConfirmationSpeed.slow; + } + + return PendingLightningTransaction( + id: "", // ToDo: Find out where to get it + amount: prepareResponse.amount.toInt(), + fee: fee, + commitOverride: () async { + final options = + SendPaymentOptions.bitcoinAddress(confirmationSpeed: onchainConfirmationSpeed); + await sdk.sendPayment( + request: SendPaymentRequest(prepareResponse: prepareResponse, options: options)); + }, + ); + } } // If not returned earlier @@ -126,6 +172,6 @@ extension _ConfigCopyWith on Config { maxDepositClaimFee: maxDepositClaimFee ?? this.maxDepositClaimFee, preferSparkOverLightning: preferSparkOverLightning ?? this.preferSparkOverLightning, useDefaultExternalInputParsers: - useDefaultExternalInputParsers ?? this.useDefaultExternalInputParsers, + useDefaultExternalInputParsers ?? this.useDefaultExternalInputParsers, ); } diff --git a/cw_core/lib/unspent_coin_type.dart b/cw_core/lib/unspent_coin_type.dart index a042610fc9..859457c498 100644 --- a/cw_core/lib/unspent_coin_type.dart +++ b/cw_core/lib/unspent_coin_type.dart @@ -1 +1 @@ -enum UnspentCoinType { mweb, nonMweb, any } \ No newline at end of file +enum UnspentCoinType { mweb, nonMweb, any, lightning } diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index b04fe0294d..27ffcef0d0 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -248,6 +248,7 @@ class CWBitcoin extends Bitcoin { return element.bitcoinAddressRecord.type == SegwitAddresType.mweb; case UnspentCoinType.nonMweb: return element.bitcoinAddressRecord.type != SegwitAddresType.mweb; + case UnspentCoinType.lightning: case UnspentCoinType.any: return true; } @@ -766,6 +767,15 @@ class CWBitcoin extends Bitcoin { } } + Future getUnusedSpakDepositAddress(Object wallet) async { + try { + final bitcoinWallet = wallet as BitcoinWallet; + return wallet.lightningWallet?.getDepositAddress(); + } catch (_) { + return null; + } + } + @override Future commitPsbtUR(Object wallet, List urCodes) { final _wallet = wallet as BitcoinWalletBase; diff --git a/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart b/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart index 766914f769..3dac4cea13 100644 --- a/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart +++ b/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart @@ -12,6 +12,7 @@ import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/unspent_coin_type.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -505,23 +506,7 @@ class BalanceRowWidget extends StatelessWidget { child: Semantics( label: S.of(context).litecoin_mweb_pegin, child: OutlinedButton( - onPressed: () { - final mwebAddress = - bitcoin!.getUnusedMwebAddress(dashboardViewModel.wallet); - PaymentRequest? paymentRequest = null; - if ((mwebAddress?.isNotEmpty ?? false)) { - paymentRequest = PaymentRequest.fromUri( - Uri.parse("litecoin:${mwebAddress}")); - } - Navigator.pushNamed( - context, - Routes.send, - arguments: { - 'paymentRequest': paymentRequest, - 'coinTypeToSpendFrom': UnspentCoinType.nonMweb, - }, - ); - }, + onPressed: () => depositToL2(context), style: OutlinedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.primary, side: BorderSide( @@ -563,23 +548,7 @@ class BalanceRowWidget extends StatelessWidget { child: Semantics( label: S.of(context).litecoin_mweb_pegout, child: OutlinedButton( - onPressed: () { - final litecoinAddress = - bitcoin!.getUnusedSegwitAddress(dashboardViewModel.wallet); - PaymentRequest? paymentRequest = null; - if ((litecoinAddress?.isNotEmpty ?? false)) { - paymentRequest = PaymentRequest.fromUri( - Uri.parse("litecoin:${litecoinAddress}")); - } - Navigator.pushNamed( - context, - Routes.send, - arguments: { - 'paymentRequest': paymentRequest, - 'coinTypeToSpendFrom': UnspentCoinType.mweb, - }, - ); - }, + onPressed: () => withdrawFromL2(context), style: OutlinedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.surface, side: BorderSide( @@ -632,20 +601,57 @@ class BalanceRowWidget extends StatelessWidget { ); } - // double getShadowSpread(){ - // double spread = 3; - // else if (!dashboardViewModel.settingsStore.currentTheme.isDark) spread = 3; - // else if (dashboardViewModel.settingsStore.currentTheme.isDark) spread = 1; - // return spread; - // } - // - // - // double getShadowBlur(){ - // double blur = 7; - // else if (dashboardViewModel.settingsStore.currentTheme.isDark) blur = 7; - // else if (dashboardViewModel.settingsStore.currentTheme.isDark) blur = 3; - // return blur; - // } + Future depositToL2(BuildContext context) async { + PaymentRequest? paymentRequest = null; + + if (dashboardViewModel.type == WalletType.litecoin) { + final depositAddress = bitcoin!.getUnusedMwebAddress(dashboardViewModel.wallet); + if ((depositAddress?.isNotEmpty ?? false)) { + paymentRequest = PaymentRequest.fromUri(Uri.parse("litecoin:$depositAddress")); + } + } else if (dashboardViewModel.type == WalletType.bitcoin) { + final depositAddress = await bitcoin!.getUnusedSpakDepositAddress(dashboardViewModel.wallet); + if ((depositAddress?.isNotEmpty ?? false)) { + paymentRequest = PaymentRequest.fromUri(Uri.parse("bitcoin:$depositAddress")); + } + } + + Navigator.pushNamed( + context, + Routes.send, + arguments: { + 'paymentRequest': paymentRequest, + 'coinTypeToSpendFrom': UnspentCoinType.nonMweb, + }, + ); + } + + Future withdrawFromL2(BuildContext context) async { + PaymentRequest? paymentRequest = null; + UnspentCoinType unspentCoinType = UnspentCoinType.any; + final withdrawAddress = bitcoin!.getUnusedSegwitAddress(dashboardViewModel.wallet); + + if (dashboardViewModel.type == WalletType.litecoin) { + if ((withdrawAddress?.isNotEmpty ?? false)) { + paymentRequest = PaymentRequest.fromUri(Uri.parse("litecoin:$withdrawAddress")); + } + unspentCoinType = UnspentCoinType.mweb; + } else if (dashboardViewModel.type == WalletType.bitcoin) { + if ((withdrawAddress?.isNotEmpty ?? false)) { + paymentRequest = PaymentRequest.fromUri(Uri.parse("bitcoin:$withdrawAddress")); + } + unspentCoinType = UnspentCoinType.lightning; + } + + Navigator.pushNamed( + context, + Routes.send, + arguments: { + 'paymentRequest': paymentRequest, + 'coinTypeToSpendFrom': unspentCoinType, + }, + ); + } void _showBalanceDescription(BuildContext context, String content) { showPopUp(context: context, builder: (_) => InformationPage(information: content)); diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index b0d4f767ef..20e3b2ceb3 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -818,6 +818,10 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin Date: Fri, 31 Oct 2025 16:16:45 +0100 Subject: [PATCH 05/68] feat: add method to retrieve unused Spark deposit address for Bitcoin wallets --- tool/configure.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/tool/configure.dart b/tool/configure.dart index fb934083ee..dbca11d49f 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -269,6 +269,7 @@ abstract class Bitcoin { bool getMwebEnabled(Object wallet); String? getUnusedMwebAddress(Object wallet); String? getUnusedSegwitAddress(Object wallet); + Future getUnusedSpakDepositAddress(Object wallet); Future commitPsbtUR(Object wallet, List urCodes); void updatePayjoinState(Object wallet, bool state); From 345196927fc5083902f6ef82bcf85be2431ccc28 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Sat, 1 Nov 2025 00:32:21 +0100 Subject: [PATCH 06/68] feat: add Breez API key support and update secrets handling for Bitcoin Lightning wallet integration in workflows --- .github/workflows/automated_integration_test.yml | 2 ++ .github/workflows/pr_test_build_android.yml | 4 +++- .github/workflows/pr_test_build_linux.yml | 2 ++ cw_bitcoin/lib/bitcoin_wallet.dart | 4 ++-- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/automated_integration_test.yml b/.github/workflows/automated_integration_test.yml index 95a3197d4f..eee1407c1d 100644 --- a/.github/workflows/automated_integration_test.yml +++ b/.github/workflows/automated_integration_test.yml @@ -57,6 +57,7 @@ jobs: - name: Add secrets run: | touch lib/.secrets.g.dart + touch cw_bitcoin/lib/.secrets.g.dart touch cw_evm/lib/.secrets.g.dart touch cw_solana/lib/.secrets.g.dart touch cw_core/lib/.secrets.g.dart @@ -130,6 +131,7 @@ jobs: echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const nowNodesApiKey = '${{ secrets.EVM_NOWNODES_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart + echo "const breezApiKey = '${{ secrets.BREEZ_API_KEY }}';" >> cw_bitcoin/lib/.secrets.g.dart echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart echo "const exolixCakeWalletApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart echo "const exolixMoneroApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 05753d8d36..625a1e50d9 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -51,6 +51,7 @@ jobs: - name: Add secrets run: | touch lib/.secrets.g.dart + touch cw_bitcoin/lib/.secrets.g.dart touch cw_evm/lib/.secrets.g.dart touch cw_solana/lib/.secrets.g.dart touch cw_core/lib/.secrets.g.dart @@ -124,6 +125,7 @@ jobs: echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const nowNodesApiKey = '${{ secrets.EVM_NOWNODES_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart + echo "const breezApiKey = '${{ secrets.BREEZ_API_KEY }}';" >> cw_bitcoin/lib/.secrets.g.dart echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart echo "const exolixCakeWalletApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart echo "const exolixMoneroApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart @@ -337,4 +339,4 @@ jobs: cd build/app/outputs/flutter-apk for i in arm64-v8a x86_64; do ../../../../scripts/android/check_16kb_align.sh app-$i-release.apk - done \ No newline at end of file + done diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 92eae19db2..10bd5557b7 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -44,6 +44,7 @@ jobs: - name: Add secrets run: | touch lib/.secrets.g.dart + touch cw_bitcoin/lib/.secrets.g.dart touch cw_evm/lib/.secrets.g.dart touch cw_solana/lib/.secrets.g.dart touch cw_core/lib/.secrets.g.dart @@ -117,6 +118,7 @@ jobs: echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const nowNodesApiKey = '${{ secrets.EVM_NOWNODES_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart + echo "const breezApiKey = '${{ secrets.BREEZ_API_KEY }}';" >> cw_bitcoin/lib/.secrets.g.dart echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart echo "const exolixCakeWalletApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart echo "const exolixMoneroApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index e5147a536a..561c31528d 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:bip39/bip39.dart' as bip39; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_bitcoin/.secrets.g.dart' as secrets; import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; @@ -96,8 +97,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { if (mnemonic != null) { lightningWallet = LightningWallet( mnemonic: mnemonic, - apiKey: - "MIIBdzCCASmgAwIBAgIHPpJHKP1qXzAFBgMrZXAwEDEOMAwGA1UEAxMFQnJlZXowHhcNMjUxMDIzMTQwNDQ4WhcNMzUxMDIxMTQwNDQ4WjAxMRQwEgYDVQQKEwtDYWtlIFdhbGxldDEZMBcGA1UEAxMQU2V0aCBGb3IgUHJpdmFjeTAqMAUGAytlcAMhANCD9cvfIDwcoiDKKYdT9BunHLS2/OuKzV8NS0SzqV13o4GAMH4wDgYDVR0PAQH/BAQDAgWgMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFNo5o+5ea0sNMlW/75VgGJCv2AcJMB8GA1UdIwQYMBaAFN6q1pJW843ndJIW/Ey2ILJrKJhrMB4GA1UdEQQXMBWBE3NldGhAY2FrZXdhbGxldC5jb20wBQYDK2VwA0EAl+naPfCBseV7eS4SoP0q0kvo2GHCywXoIbnlBa0y+/wlfu+oILtsGv3jGQ2egCnpgHe87yzR0ygclzz8r/jdAQ==", + apiKey: secrets.breezApiKey, lnurlDomain: "breez.tips", ); } From b06aea3b00754970f6fe2abf1c2b9d596cd20c10 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Sat, 1 Nov 2025 00:33:45 +0100 Subject: [PATCH 07/68] chore: update Breez SDK dependency to version 0.3.4 in pubspec files --- cw_bitcoin/pubspec.lock | 6 +++--- cw_bitcoin/pubspec.yaml | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 38aea938ca..e281c7f7b4 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -134,11 +134,11 @@ packages: dependency: "direct main" description: path: "." - ref: HEAD - resolved-ref: "5bc8fb5f3a5c84e2e3dd55f5d48b01152f425765" + ref: "92f62dc2037cf08003e418aadda58f451c021f42" + resolved-ref: "92f62dc2037cf08003e418aadda58f451c021f42" url: "https://github.com/breez/breez-sdk-spark-flutter" source: git - version: "0.3.2" + version: "0.3.4" bs58check: dependency: transitive description: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 940ce80b6a..439492bda0 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -75,6 +75,7 @@ dependencies: breez_sdk_spark_flutter: git: url: https://github.com/breez/breez-sdk-spark-flutter + ref: 92f62dc2037cf08003e418aadda58f451c021f42 dev_dependencies: flutter_test: From e11f468212832856a44601096455e12ad27e2a02 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Sat, 1 Nov 2025 13:28:12 +0100 Subject: [PATCH 08/68] Add bitcoin secrets config [skip ci] --- .gitignore | 1 + scripts/android/app_env.sh | 4 ++-- tool/generate_secrets_config.dart | 3 +++ tool/import_secrets_config.dart | 3 +++ tool/utils/secret_key.dart | 4 ++++ 5 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index cd2230504d..018d05ca22 100644 --- a/.gitignore +++ b/.gitignore @@ -97,6 +97,7 @@ android/app/key.jks **/tool/.solana-secrets-config.json **/tool/.nano-secrets-config.json **/tool/.tron-secrets-config.json +**/tool/.bitcoin-secrets-config.json **/lib/.secrets.g.dart **/cw_evm/lib/.secrets.g.dart **/cw_solana/lib/.secrets.g.dart diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 21bf2c1e99..99e0380e07 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -21,8 +21,8 @@ MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="5.5.2" -CAKEWALLET_BUILD_NUMBER=4284 +CAKEWALLET_VERSION="5.6.0" +CAKEWALLET_BUILD_NUMBER=4285 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/tool/generate_secrets_config.dart b/tool/generate_secrets_config.dart index 8e9762b7a0..0b32e60ad5 100644 --- a/tool/generate_secrets_config.dart +++ b/tool/generate_secrets_config.dart @@ -8,6 +8,7 @@ const evmChainsConfigPath = 'tool/.evm-secrets-config.json'; const solanaConfigPath = 'tool/.solana-secrets-config.json'; const nanoConfigPath = 'tool/.nano-secrets-config.json'; const tronConfigPath = 'tool/.tron-secrets-config.json'; +const bitcoinConfigPath = 'tool/.bitcoin-secrets-config.json'; Future main(List args) async => generateSecretsConfig(args); @@ -41,6 +42,7 @@ Future generateSecretsConfig(List args) async { final solanaConfigFile = File(solanaConfigPath); final nanoConfigFile = File(nanoConfigPath); final tronConfigFile = File(tronConfigPath); + final bitcoinConfigFile = File(bitcoinConfigPath); final secrets = {}; @@ -66,4 +68,5 @@ Future generateSecretsConfig(List args) async { await writeConfig(solanaConfigFile, SecretKey.solanaSecrets); await writeConfig(nanoConfigFile, SecretKey.nanoSecrets); await writeConfig(tronConfigFile, SecretKey.tronSecrets); + await writeConfig(bitcoinConfigFile, SecretKey.bitcoinSecrets); } diff --git a/tool/import_secrets_config.dart b/tool/import_secrets_config.dart index 42379021f5..dd333c7e2b 100644 --- a/tool/import_secrets_config.dart +++ b/tool/import_secrets_config.dart @@ -14,6 +14,9 @@ const solanaOutputPath = 'cw_solana/lib/.secrets.g.dart'; const tronConfigPath = 'tool/.tron-secrets-config.json'; const tronOutputPath = 'cw_tron/lib/.secrets.g.dart'; +const bitcoinConfigPath = 'tool/.bitcoin-secrets-config.json'; +const bitcoinOutputPath = 'cw_bitcoin/lib/.secrets.g.dart'; + const nanoConfigPath = 'tool/.nano-secrets-config.json'; const nanoOutputPath = 'cw_nano/lib/.secrets.g.dart'; diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index 61ccea60b9..8e6a6c6c9e 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -111,6 +111,10 @@ class SecretKey { SecretKey('tronNowNodesApiKey', () => ''), ]; + static final bitcoinSecrets = [ + SecretKey('breezApiKey', () => ''), + ]; + final String name; final String Function() generate; } From 0273df79977beca316cf91481e125403a7fcd43d Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Mon, 3 Nov 2025 19:46:02 +0100 Subject: [PATCH 09/68] feat: extend Lightning wallet functionality with transaction history fetching --- cw_bitcoin/lib/bitcoin_wallet.dart | 29 ++++++-- .../lib/lightning/lightning_addres_type.dart | 1 + .../lib/lightning/lightning_wallet.dart | 72 ++++++++++++++----- 3 files changed, 82 insertions(+), 20 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 561c31528d..9b212ff5f9 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -11,6 +11,7 @@ import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; +import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; import 'package:cw_bitcoin/hardware/bitcoin_hardware_wallet_service.dart'; @@ -119,7 +120,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { lightningWallet: lightningWallet, ); - if (lightningWallet != null) { walletAddresses.setLightningAddress(walletInfo.name); } @@ -296,7 +296,23 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final lBalance = await lightningWallet!.getBalance(); - return ElectrumBalance(confirmed: balance.confirmed, unconfirmed: balance.unconfirmed, frozen: balance.frozen, secondConfirmed: lBalance.toInt()); + return ElectrumBalance( + confirmed: balance.confirmed, + unconfirmed: balance.unconfirmed, + frozen: balance.frozen, + secondConfirmed: lBalance.toInt(), + ); + } + + @override + Future> fetchTransactions() async { + if (lightningWallet != null) { + final lnHistory = await lightningWallet!.getTransactionHistory(); + transactionHistory.addMany(lnHistory); + await transactionHistory.save(); + } + + return super.fetchTransactions(); } late final LightningWallet? lightningWallet; @@ -379,9 +395,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Future createTransaction(Object credentials) async { credentials = credentials as BitcoinTransactionCredentials; + final isLNCompatible = await lightningWallet?.isCompatible(credentials.outputs.first.address); if ((credentials.coinTypeToSpendFrom == UnspentCoinType.lightning && lightningWallet != null) || - (await lightningWallet?.isCompatible(credentials.outputs.first.address)) == true) { - final amount = parseFixed(credentials.outputs.first.cryptoAmount?.isNotEmpty == true ? credentials.outputs.first.cryptoAmount! : "0", 9); + isLNCompatible == true) { + final amount = parseFixed( + credentials.outputs.first.cryptoAmount?.isNotEmpty == true + ? credentials.outputs.first.cryptoAmount! + : "0", + 9); return lightningWallet!.createTransaction(credentials.outputs.first.address, amount > BigInt.zero ? amount : null, credentials.priority); diff --git a/cw_bitcoin/lib/lightning/lightning_addres_type.dart b/cw_bitcoin/lib/lightning/lightning_addres_type.dart index c12f980e77..f0b13fca18 100644 --- a/cw_bitcoin/lib/lightning/lightning_addres_type.dart +++ b/cw_bitcoin/lib/lightning/lightning_addres_type.dart @@ -5,6 +5,7 @@ class LightningAddressType implements BitcoinAddressType { static const LightningAddressType p2l = LightningAddressType._("Lightning"); static const String Bolt11InvoiceMatcher = r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$'; + static const String Bolt12OfferMatcher = r'^(lightning:)?(lno1)[a-z0-9]+$'; @override bool get isP2sh => false; diff --git a/cw_bitcoin/lib/lightning/lightning_wallet.dart b/cw_bitcoin/lib/lightning/lightning_wallet.dart index b015f8d0c2..6ae40da518 100644 --- a/cw_bitcoin/lib/lightning/lightning_wallet.dart +++ b/cw_bitcoin/lib/lightning/lightning_wallet.dart @@ -1,7 +1,11 @@ import 'package:breez_sdk_spark_flutter/breez_sdk_spark.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; +import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/lightning/pending_lightning_transaction.dart'; import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_type.dart'; bool _breezSdkSparkLibUninitialized = true; @@ -42,17 +46,16 @@ class LightningWallet { Future getAddress() async => (await sdk.getLightningAddress())?.lightningAddress; - Future getDepositAddress() async => - (await sdk.receivePayment( + Future getDepositAddress() async => (await sdk.receivePayment( request: ReceivePaymentRequest(paymentMethod: ReceivePaymentMethod.bitcoinAddress()))) - .paymentRequest; + .paymentRequest; Future getBalance() async => (await sdk.getInfo(request: GetInfoRequest(ensureSynced: true))).balanceSats; Future registerAddress(String username) async { return (await sdk.registerLightningAddress( - request: RegisterLightningAddressRequest(username: username))) + request: RegisterLightningAddressRequest(username: username))) .lightningAddress; } @@ -65,8 +68,8 @@ class LightningWallet { } } - Future createTransaction(String address, BigInt? amountSats, - BitcoinTransactionPriority? priority) async { + Future createTransaction( + String address, BigInt? amountSats, BitcoinTransactionPriority? priority) async { final inputType = await sdk.parse(input: address); if (inputType is InputType_Bolt11Invoice) { @@ -76,17 +79,18 @@ class LightningWallet { final paymentMethod = prepareResponse.paymentMethod; if (paymentMethod is SendPaymentMethod_Bolt11Invoice) { - // Fees to pay via Lightning final lightningFeeSats = paymentMethod.lightningFeeSats; - // Or fees to pay (if available) via a Spark transfer final sparkTransferFeeSats = paymentMethod.sparkTransferFeeSats; return PendingLightningTransaction( id: paymentMethod.invoiceDetails.paymentHash, amount: ((paymentMethod.invoiceDetails.amountMsat?.toInt() ?? 0) / 1000).round(), fee: lightningFeeSats.toInt() + (sparkTransferFeeSats?.toInt() ?? 0), - commitOverride: () => - sdk.sendPayment(request: SendPaymentRequest(prepareResponse: prepareResponse)), + commitOverride: () async { + final res = await sdk.sendPayment( + request: SendPaymentRequest(prepareResponse: prepareResponse)); + printV(res.payment.status.name); + }, ); } } else if (inputType is InputType_LightningAddress) { @@ -105,20 +109,21 @@ class LightningWallet { id: prepareResponse.invoiceDetails.paymentHash, amount: prepareResponse.invoiceDetails.amountMsat?.toInt() ?? 0, fee: feeSats.toInt(), - commitOverride: () => - sdk.lnurlPay(request: LnurlPayRequest(prepareResponse: prepareResponse)), + commitOverride: () async { + await sdk.lnurlPay(request: LnurlPayRequest(prepareResponse: prepareResponse)); + }, ); } else if (inputType is InputType_BitcoinAddress) { - final request = PrepareSendPaymentRequest( - paymentRequest: inputType.field0.address, amount: amountSats); + final request = + PrepareSendPaymentRequest(paymentRequest: inputType.field0.address, amount: amountSats); final prepareResponse = await sdk.prepareSendPayment(request: request); final paymentMethod = prepareResponse.paymentMethod; if (paymentMethod is SendPaymentMethod_BitcoinAddress) { final feeQuote = paymentMethod.feeQuote; + OnchainConfirmationSpeed onchainConfirmationSpeed; int fee; - switch (priority) { case BitcoinTransactionPriority.fast: fee = (feeQuote.speedFast.userFeeSat + feeQuote.speedFast.l1BroadcastFeeSat).toInt(); @@ -152,6 +157,41 @@ class LightningWallet { // If not returned earlier throw UnimplementedError(); } + + Future> getTransactionHistory() async { + final request = ListPaymentsRequest( + typeFilter: [PaymentType.send, PaymentType.receive], + // statusFilter: [PaymentStatus.completed], + assetFilter: AssetFilter.bitcoin(), + offset: 0, + limit: 50, + sortAscending: false, // Sort order (true = oldest first, false = newest first) + ); + final response = await sdk.listPayments(request: request); + final payments = response.payments; + + Map txHistory = {}; + for (final payment in payments) { + TransactionDirection direction = TransactionDirection.outgoing; + + if (payment.method == PaymentMethod.deposit) { + direction = TransactionDirection.incoming; + } + + txHistory[payment.id] = ElectrumTransactionInfo( + WalletType.bitcoin, + id: payment.id, + amount: payment.amount.toInt(), + direction: direction, + isPending: payment.status == PaymentStatus.pending, + date: DateTime.fromMillisecondsSinceEpoch(payment.timestamp.toInt() * 1000), + confirmations: payment.status == PaymentStatus.pending ? 0 : 10, + + ); + } + + return txHistory; + } } extension _ConfigCopyWith on Config { @@ -172,6 +212,6 @@ extension _ConfigCopyWith on Config { maxDepositClaimFee: maxDepositClaimFee ?? this.maxDepositClaimFee, preferSparkOverLightning: preferSparkOverLightning ?? this.preferSparkOverLightning, useDefaultExternalInputParsers: - useDefaultExternalInputParsers ?? this.useDefaultExternalInputParsers, + useDefaultExternalInputParsers ?? this.useDefaultExternalInputParsers, ); } From 3f507e51d16bbebf63faee8a2d32cb0453140864 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 4 Nov 2025 10:06:01 +0100 Subject: [PATCH 10/68] feat: add LNURL-pay address detection and support in address parsing flow for Bitcoin Lightning integration --- lib/core/address_validator.dart | 1 + lib/entities/lnurlpay_record.dart | 75 +++++++++++++++++++ lib/entities/parse_address_from_domain.dart | 9 +++ lib/entities/parsed_address.dart | 11 ++- .../widgets/extract_address_from_parsed.dart | 5 ++ 5 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 lib/entities/lnurlpay_record.dart diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index df072f8a32..c700835341 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -19,6 +19,7 @@ class AddressValidator extends TextValidator { r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$', caseSensitive: false); if (lightningInvoiceRegex.hasMatch(txt)) return true; + if (txt.contains("@")) return true; return BitcoinAddressUtils.validateAddress( address: txt, diff --git a/lib/entities/lnurlpay_record.dart b/lib/entities/lnurlpay_record.dart new file mode 100644 index 0000000000..3fbb01bc3a --- /dev/null +++ b/lib/entities/lnurlpay_record.dart @@ -0,0 +1,75 @@ +import 'dart:convert'; + +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; + +class LNUrlPayRecord { + LNUrlPayRecord({ + required this.address, + required this.name, + }); + + final String name; + final String address; + + static Future checkWellKnownUsername(String username, CryptoCurrency currency) async { + if (currency != CryptoCurrency.btc) return null; + + // split the string by the @ symbol: + try { + final List splitStrs = username.split("@"); + String name = splitStrs.first.toLowerCase(); + final String domain = splitStrs.last; + + if (splitStrs.length == 3) { + // for username like @alice@domain.org instead of alice@domain.org + name = splitStrs[1]; + } + + if (name.isEmpty) { + name = "_"; + } + + // lookup domain/.well-known/nano-currency.json and check if it has a nano address: + final response = await ProxyWrapper().get( + clearnetUri: Uri.parse("https://$domain/.well-known/lnurlp/$name"), + headers: {"Accept": "application/json"}, + ); + + if (response.statusCode == 200) { + return username; + } + } catch (e) { + printV("error checking well-known username: $e"); + } + return null; + } + + static String formatDomainName(String name) { + String formattedName = name; + + if (name.contains("@")) { + formattedName = name.replaceAll("@", "."); + } + + return formattedName; + } + + static Future fetchAddressAndName({ + required String formattedName, + required CryptoCurrency currency, + }) async { + String name = formattedName; + + printV("formattedName: $formattedName"); + + final address = await checkWellKnownUsername(formattedName, currency); + + if (address == null) { + return null; + } + + return LNUrlPayRecord(address: address, name: name); + } +} diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index 4428108752..a772f49514 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/yat_service.dart'; import 'package:cake_wallet/entities/ens_record.dart'; +import 'package:cake_wallet/entities/lnurlpay_record.dart'; import 'package:cake_wallet/entities/openalias_record.dart'; import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/entities/unstoppable_domain_address.dart'; @@ -297,6 +298,14 @@ class AddressResolver { return ParsedAddress.fetchWellKnownAddress(address: record.address, name: text); } } + + if (walletType == WalletType.bitcoin && currency == CryptoCurrency.btc) { + final record = + await LNUrlPayRecord.fetchAddressAndName(formattedName: text, currency: currency); + if (record != null) { + return ParsedAddress.fetchLNUrlPayAddress(address: record.address, name: text); + } + } } if (!text.startsWith('@') && text.contains('@') && !text.contains('.')) { diff --git a/lib/entities/parsed_address.dart b/lib/entities/parsed_address.dart index 74acab80a7..a5159cbcf0 100644 --- a/lib/entities/parsed_address.dart +++ b/lib/entities/parsed_address.dart @@ -15,7 +15,8 @@ enum ParseFrom { thorChain, wellKnown, zanoAlias, - bip353 + bip353, + lnurlpay } class ParsedAddress { @@ -175,6 +176,14 @@ class ParsedAddress { ); } + factory ParsedAddress.fetchLNUrlPayAddress({required String address, required String name}) { + return ParsedAddress( + addresses: [address], + name: name, + parseFrom: ParseFrom.lnurlpay, + ); + } + final List addresses; final String name; final String description; diff --git a/lib/src/screens/send/widgets/extract_address_from_parsed.dart b/lib/src/screens/send/widgets/extract_address_from_parsed.dart index b37f87b7ff..74b7439356 100644 --- a/lib/src/screens/send/widgets/extract_address_from_parsed.dart +++ b/lib/src/screens/send/widgets/extract_address_from_parsed.dart @@ -78,6 +78,11 @@ Future extractAddressFromParsed( content = S.of(context).extracted_address_content('${parsedAddress.name} (BIP-353)'); address = parsedAddress.addresses.first; break; + case ParseFrom.lnurlpay: + title = S.of(context).address_detected; + content = S.of(context).extracted_address_content('${parsedAddress.name} (Lightning)'); + address = parsedAddress.addresses.first; + break; case ParseFrom.yatRecord: if (parsedAddress.name.isEmpty) { title = S.of(context).yat_error; From 3b139de3d44138cbdfbdcb4eae8264408785fdeb Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 4 Nov 2025 14:43:22 +0100 Subject: [PATCH 11/68] refactor: simplify `ReceivePageOption` logic --- cw_bitcoin/lib/bitcoin_wallet.dart | 2 + cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 16 ++++ cw_bitcoin/lib/litecoin_wallet_addresses.dart | 20 ++++- {lib/core => cw_core/lib}/payment_uris.dart | 79 +++++-------------- cw_core/lib/wallet_addresses.dart | 8 +- cw_core/pubspec.yaml | 3 +- cw_decred/lib/wallet.dart | 2 +- cw_decred/lib/wallet_addresses.dart | 25 +++--- lib/bitcoin/cw_bitcoin.dart | 24 ------ .../screens/dashboard/pages/address_page.dart | 3 +- .../screens/receive/widgets/qr_widget.dart | 2 +- lib/utils/payment_request.dart | 2 +- .../dashboard/receive_option_view_model.dart | 48 ++--------- .../exchange/exchange_trade_view_model.dart | 2 +- .../wallet_address_list_view_model.dart | 2 +- tool/configure.dart | 2 - 16 files changed, 89 insertions(+), 151 deletions(-) rename {lib/core => cw_core/lib}/payment_uris.dart (87%) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 9b212ff5f9..304618e714 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -101,6 +101,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { apiKey: secrets.breezApiKey, lnurlDomain: "breez.tips", ); + } else { + lightningWallet = null; } payjoinManager = PayjoinManager(PayjoinStorage(payjoinBox), this); diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 8e36ffebf8..91308f630e 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -1,8 +1,10 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/bip/bip/bip32/bip32.dart'; +import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/payjoin/manager.dart'; import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; @@ -89,4 +91,18 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S if (!_isPayjoinConnectivityError(e.toString())) rethrow; } } + + @override + List get receivePageOptions { + if (isHardwareWallet) { + return [ + ...BitcoinReceivePageOption.allViewOnly, + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ]; + } + return [ + ...BitcoinReceivePageOption.all, + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ]; + } } diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 76228c16de..2095d4bd4e 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -5,9 +5,11 @@ import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; -import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; +import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; @@ -46,6 +48,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with bool generating = false; List get scanSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; + List get spendPubkey => mwebHd!.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed; @@ -208,4 +211,19 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with .where((element) => element.type == SegwitAddresType.p2wpkh && !element.isUsed); return addresses.first.address; } + + @override + List get receivePageOptions { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows || isHardwareWallet) { + return [ + ...BitcoinReceivePageOption.allLitecoin + .where((element) => element != BitcoinReceivePageOption.mweb), + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ]; + } + return [ + ...BitcoinReceivePageOption.allLitecoin, + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ]; + } } diff --git a/lib/core/payment_uris.dart b/cw_core/lib/payment_uris.dart similarity index 87% rename from lib/core/payment_uris.dart rename to cw_core/lib/payment_uris.dart index 38f4f6d2ef..fc2bd2b880 100644 --- a/lib/core/payment_uris.dart +++ b/cw_core/lib/payment_uris.dart @@ -13,10 +13,7 @@ class MoneroURI extends PaymentURI { @override String toString() { var base = 'monero:$address'; - - if (amount.isNotEmpty) { - base += '?tx_amount=${amount.replaceAll(',', '.')}'; - } + if (amount.isNotEmpty) base += '?tx_amount=${amount.replaceAll(',', '.')}'; return base; } @@ -28,10 +25,7 @@ class HavenURI extends PaymentURI { @override String toString() { var base = 'haven:$address'; - - if (amount.isNotEmpty) { - base += '?tx_amount=${amount.replaceAll(',', '.')}'; - } + if (amount.isNotEmpty) base += '?tx_amount=${amount.replaceAll(',', '.')}'; return base; } @@ -62,10 +56,7 @@ class LitecoinURI extends PaymentURI { @override String toString() { var base = 'litecoin:$address'; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } + if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; return base; } @@ -77,10 +68,7 @@ class EthereumURI extends PaymentURI { @override String toString() { var base = 'ethereum:$address'; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } + if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; return base; } @@ -92,10 +80,7 @@ class BaseURI extends PaymentURI { @override String toString() { var base = 'base:$address'; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } + if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; return base; } @@ -122,10 +107,7 @@ class BitcoinCashURI extends PaymentURI { @override String toString() { var base = address; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } + if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; return base; } @@ -151,10 +133,7 @@ class PolygonURI extends PaymentURI { @override String toString() { var base = 'polygon:$address'; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } + if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; return base; } @@ -166,10 +145,7 @@ class SolanaURI extends PaymentURI { @override String toString() { var base = 'solana:$address'; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } + if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; return base; } @@ -181,10 +157,7 @@ class TronURI extends PaymentURI { @override String toString() { var base = 'tron:$address'; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } + if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; return base; } @@ -196,10 +169,7 @@ class WowneroURI extends PaymentURI { @override String toString() { var base = 'wownero:$address'; - - if (amount.isNotEmpty) { - base += '?tx_amount=${amount.replaceAll(',', '.')}'; - } + if (amount.isNotEmpty) base += '?tx_amount=${amount.replaceAll(',', '.')}'; return base; } @@ -211,11 +181,8 @@ class ZanoURI extends PaymentURI { @override String toString() { - var base = 'zano:' + address; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } + var base = 'zano:$address'; + if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; return base; } @@ -227,11 +194,8 @@ class DecredURI extends PaymentURI { @override String toString() { - var base = 'decred:' + address; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } + var base = 'decred:$address'; + if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; return base; } @@ -243,11 +207,8 @@ class DogeURI extends PaymentURI { @override String toString() { - var base = 'doge:' + address; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } + var base = 'doge:$address'; + if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; return base; } @@ -271,13 +232,9 @@ class ERC681URI extends PaymentURI { final targetAddress = contractAddress ?? address; uri += targetAddress; - if (chainId != 1) { - uri += '@$chainId'; - } + if (chainId != 1) uri += '@$chainId'; - if (contractAddress != null) { - uri += '/transfer'; - } + if (contractAddress != null) uri += '/transfer'; final params = {}; diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index 4d4d2c0a5a..ac4128e427 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -1,9 +1,10 @@ +import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; abstract class WalletAddresses { - WalletAddresses(this.walletInfo) + WalletAddresses(this.walletInfo, [this.isTestnet = false]) : addressesMap = {}, allAddressesMap = {}, addressInfos = {}, @@ -17,6 +18,8 @@ abstract class WalletAddresses { final WalletInfo walletInfo; + final bool isTestnet; + String get address; String get latestAddress { @@ -79,4 +82,7 @@ abstract class WalletAddresses { bool containsAddress(String address) => addressesMap.containsKey(address) || allAddressesMap.containsKey(address); + + List get receivePageOptions => ReceivePageOptions; + } diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml index 7c963f246e..49188f5177 100644 --- a/cw_core/pubspec.yaml +++ b/cw_core/pubspec.yaml @@ -2,11 +2,10 @@ name: cw_core description: A new Flutter package project. version: 0.0.1 publish_to: none -author: Cake Wallet homepage: https://cakewallet.com environment: - sdk: ">=2.17.5 <3.0.0" + sdk: '>=3.0.6 <4.0.0' flutter: ">=1.20.0" dependencies: diff --git a/cw_decred/lib/wallet.dart b/cw_decred/lib/wallet.dart index 432edb4bc0..97c118331b 100644 --- a/cw_decred/lib/wallet.dart +++ b/cw_decred/lib/wallet.dart @@ -54,7 +54,7 @@ abstract class DecredWalletBase derivationInfo.derivationPath == DecredWalletService.pubkeyRestorePathTestnet, super(walletInfo, derivationInfo) { - walletAddresses = DecredWalletAddresses(walletInfo, libwallet); + walletAddresses = DecredWalletAddresses(walletInfo, libwallet, isTestnet); transactionHistory = DecredTransactionHistory(); reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) { diff --git a/cw_decred/lib/wallet_addresses.dart b/cw_decred/lib/wallet_addresses.dart index e4af108b9d..96f007c5cc 100644 --- a/cw_decred/lib/wallet_addresses.dart +++ b/cw_decred/lib/wallet_addresses.dart @@ -1,8 +1,8 @@ import 'dart:convert'; +import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:mobx/mobx.dart'; -import 'package:cw_core/address_info.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_decred/api/libdcrwallet.dart'; @@ -12,9 +12,8 @@ part 'wallet_addresses.g.dart'; class DecredWalletAddresses = DecredWalletAddressesBase with _$DecredWalletAddresses; abstract class DecredWalletAddressesBase extends WalletAddresses with Store { - DecredWalletAddressesBase(WalletInfo walletInfo, Libwallet libwallet) - : _libwallet = libwallet, - super(walletInfo); + DecredWalletAddressesBase(super.walletInfo, Libwallet libwallet, super.isTestnet) + : _libwallet = libwallet; final Libwallet _libwallet; String currentAddr = ''; @@ -26,14 +25,10 @@ abstract class DecredWalletAddressesBase extends WalletAddresses with Store { @override @computed - String get address { - return selectedAddr; - } + String get address => selectedAddr; @override - set address(value) { - selectedAddr = value; - } + set address(value) => selectedAddr = value; @override Future init() async { @@ -145,6 +140,16 @@ abstract class DecredWalletAddressesBase extends WalletAddresses with Store { selectedAddr = addr; await saveAddressesInBox(); } + + @override + List get receivePageOptions { + return isTestnet + ? [ + ReceivePageOption.testnet, + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ] + : ReceivePageOptions; + } } class LibAddresses { diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 27ffcef0d0..d26bc20e08 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -304,30 +304,6 @@ class CWBitcoin extends Bitcoin { return bitcoinWallet.walletAddresses.addressPageType == SilentPaymentsAddresType.p2sp; } - @override - 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(Object wallet) { - final litecoinWallet = wallet as ElectrumWallet; - if (Platform.isLinux || - Platform.isMacOS || - Platform.isWindows || - litecoinWallet.isHardwareWallet) { - return BitcoinReceivePageOption.allLitecoin - .where((element) => element != BitcoinReceivePageOption.mweb) - .toList(); - } - return BitcoinReceivePageOption.allLitecoin; - } - @override BitcoinAddressType getBitcoinAddressType(ReceivePageOption option) { switch (option) { diff --git a/lib/src/screens/dashboard/pages/address_page.dart b/lib/src/screens/dashboard/pages/address_page.dart index c7e5c8793c..d4a71e37c0 100644 --- a/lib/src/screens/dashboard/pages/address_page.dart +++ b/lib/src/screens/dashboard/pages/address_page.dart @@ -306,8 +306,7 @@ class AddressPage extends BasePage { } break; default: - if (addressListViewModel.type == WalletType.bitcoin || - addressListViewModel.type == WalletType.litecoin) { + if ([WalletType.bitcoin, WalletType.litecoin].contains(addressListViewModel.type)) { addressListViewModel.setAddressType(bitcoin!.getBitcoinAddressType(option)); } } diff --git a/lib/src/screens/receive/widgets/qr_widget.dart b/lib/src/screens/receive/widgets/qr_widget.dart index 6a40850b64..7e4df944c8 100644 --- a/lib/src/screens/receive/widgets/qr_widget.dart +++ b/lib/src/screens/receive/widgets/qr_widget.dart @@ -1,4 +1,4 @@ -import 'package:cake_wallet/core/payment_uris.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/routes.dart'; diff --git a/lib/utils/payment_request.dart b/lib/utils/payment_request.dart index b574ab260d..6149379d71 100644 --- a/lib/utils/payment_request.dart +++ b/lib/utils/payment_request.dart @@ -1,4 +1,4 @@ -import 'package:cake_wallet/core/payment_uris.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cake_wallet/nano/nano.dart'; class PaymentRequest { diff --git a/lib/view_model/dashboard/receive_option_view_model.dart b/lib/view_model/dashboard/receive_option_view_model.dart index 8aab2736d6..69b47e7b9f 100644 --- a/lib/view_model/dashboard/receive_option_view_model.dart +++ b/lib/view_model/dashboard/receive_option_view_model.dart @@ -11,59 +11,21 @@ class ReceiveOptionViewModel = ReceiveOptionViewModelBase with _$ReceiveOptionVi abstract class ReceiveOptionViewModelBase with Store { ReceiveOptionViewModelBase(this._wallet, this.initialPageOption) : selectedReceiveOption = initialPageOption ?? - (_wallet.type == WalletType.bitcoin || - _wallet.type == WalletType.litecoin + ([WalletType.bitcoin, WalletType.litecoin].contains(_wallet.type) ? bitcoin!.getSelectedAddressType(_wallet) - : (_wallet.type == WalletType.decred && _wallet.isTestnet) + : (_wallet.type == WalletType.decred && _wallet.isTestnet) ? ReceivePageOption.testnet - : ReceivePageOption.mainnet), - _options = [] { - final walletType = _wallet.type; - switch (walletType) { - case WalletType.bitcoin: - _options = [ - ...bitcoin!.getBitcoinReceivePageOptions(_wallet), - ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) - ]; - break; - case WalletType.litecoin: - _options = [ - ...bitcoin!.getLitecoinReceivePageOptions(_wallet), - ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) - ]; - break; - case WalletType.haven: - _options = [ReceivePageOption.mainnet]; - break; - case WalletType.decred: - if (_wallet.isTestnet) { - _options = [ - ReceivePageOption.testnet, - ...ReceivePageOptions.where( - (element) => element != ReceivePageOption.mainnet) - ]; - } else { - _options = ReceivePageOptions; - } - break; - default: - _options = ReceivePageOptions; - } - } + : ReceivePageOption.mainnet); final WalletBase _wallet; final ReceivePageOption? initialPageOption; - List _options; - @observable ReceivePageOption selectedReceiveOption; - List get options => _options; + List get options => _wallet.walletAddresses.receivePageOptions; @action - void selectReceiveOption(ReceivePageOption option) { - selectedReceiveOption = option; - } + void selectReceiveOption(ReceivePageOption option) => selectedReceiveOption = option; } diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index fec47f3f35..3f4240cf21 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:cake_wallet/core/payment_uris.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index fd6b29bdd5..adc60cad02 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -4,7 +4,7 @@ import 'dart:core'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/fiat_conversion_service.dart'; -import 'package:cake_wallet/core/payment_uris.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; diff --git a/tool/configure.dart b/tool/configure.dart index dbca11d49f..7534457aca 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -228,8 +228,6 @@ abstract class Bitcoin { Map> getElectrumDerivations(); Future setAddressType(Object wallet, dynamic option); ReceivePageOption getSelectedAddressType(Object wallet); - List getBitcoinReceivePageOptions(Object wallet); - List getLitecoinReceivePageOptions(Object wallet); BitcoinAddressType getBitcoinAddressType(ReceivePageOption option); bool isPayjoinAvailable(Object wallet); bool hasSelectedSilentPayments(Object wallet); From 0c0b04cee9fa47322fe0f127abbe747f653aee30 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 4 Nov 2025 16:24:15 +0100 Subject: [PATCH 12/68] refactor: centralize `PaymentURI` generation logic across wallet types --- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 5 +++++ cw_bitcoin/lib/electrum_wallet_addresses.dart | 4 ++++ cw_bitcoin/lib/litecoin_wallet_addresses.dart | 4 ++++ .../lib/src/bitcoin_cash_wallet_addresses.dart | 4 ++++ cw_core/lib/payment_uris.dart | 12 ------------ cw_core/lib/wallet_addresses.dart | 6 ++++-- cw_decred/lib/wallet_addresses.dart | 4 ++++ .../lib/src/dogecoin_wallet_addresses.dart | 4 ++++ cw_evm/lib/evm_chain_wallet.dart | 2 +- cw_evm/lib/evm_chain_wallet_addresses.dart | 17 ++++++++++++++++- cw_monero/lib/monero_wallet_addresses.dart | 5 ++++- cw_nano/lib/nano_wallet_addresses.dart | 5 +++++ cw_solana/lib/solana_wallet_addresses.dart | 4 ++++ cw_tron/lib/tron_wallet_addresses.dart | 4 ++++ cw_wownero/lib/wownero_wallet_addresses.dart | 6 ++++-- cw_zano/lib/zano_wallet_addresses.dart | 5 ++++- .../exchange/exchange_trade_view_model.dart | 2 -- 17 files changed, 71 insertions(+), 22 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 91308f630e..3f572e2401 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -4,6 +4,7 @@ import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/payjoin/manager.dart'; import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -105,4 +106,8 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) ]; } + + @override + PaymentURI getPaymentUri(String amount) => + BitcoinURI(amount: amount, address: address, pjUri: payjoinEndpoint ?? ''); } diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 7ef455793f..18de7eb3ad 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -7,6 +7,7 @@ import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/lightning/lightning_addres_type.dart'; import 'package:cw_bitcoin/lightning/lightning_wallet.dart'; import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; @@ -765,4 +766,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { await lightningWallet!.registerAddress(walletName.replaceAll(" ", "").toLowerCase()); } } + + @override + PaymentURI getPaymentUri(String amount) => BitcoinURI(amount: amount, address: address); } diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 2095d4bd4e..8d6e24b5f8 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -9,6 +9,7 @@ import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -226,4 +227,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) ]; } + + @override + PaymentURI getPaymentUri(String amount) => LitecoinURI(amount: amount, address: address); } diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart index fe0ebc8284..681fc00d73 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -2,6 +2,7 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -28,4 +29,7 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi required Bip32Slip10Secp256k1 hd, BitcoinAddressType? addressType}) => generateP2PKHAddress(hd: hd, index: index, network: network); + + @override + PaymentURI getPaymentUri(String amount) => BitcoinCashURI(amount: amount, address: address); } diff --git a/cw_core/lib/payment_uris.dart b/cw_core/lib/payment_uris.dart index fc2bd2b880..b4ba788e51 100644 --- a/cw_core/lib/payment_uris.dart +++ b/cw_core/lib/payment_uris.dart @@ -19,18 +19,6 @@ class MoneroURI extends PaymentURI { } } -class HavenURI extends PaymentURI { - HavenURI({required super.amount, required super.address}); - - @override - String toString() { - var base = 'haven:$address'; - if (amount.isNotEmpty) base += '?tx_amount=${amount.replaceAll(',', '.')}'; - - return base; - } -} - class BitcoinURI extends PaymentURI { BitcoinURI({required super.amount, required super.address, this.pjUri = ''}); diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index ac4128e427..b2d28df7df 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -1,3 +1,4 @@ +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; @@ -23,8 +24,8 @@ abstract class WalletAddresses { String get address; String get latestAddress { - if (walletInfo.type == WalletType.monero || walletInfo.type == WalletType.wownero) { - if (addressesMap.keys.length == 0) return address; + if ([WalletType.monero, WalletType.wownero].contains(walletInfo.type)) { + if (addressesMap.keys.isEmpty) return address; return addressesMap[addressesMap.keys.last] ?? address; } return _localAddress ?? address; @@ -85,4 +86,5 @@ abstract class WalletAddresses { List get receivePageOptions => ReceivePageOptions; + PaymentURI getPaymentUri(String amount); } diff --git a/cw_decred/lib/wallet_addresses.dart b/cw_decred/lib/wallet_addresses.dart index 96f007c5cc..f7d0a8baec 100644 --- a/cw_decred/lib/wallet_addresses.dart +++ b/cw_decred/lib/wallet_addresses.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:mobx/mobx.dart'; @@ -150,6 +151,9 @@ abstract class DecredWalletAddressesBase extends WalletAddresses with Store { ] : ReceivePageOptions; } + + @override + PaymentURI getPaymentUri(String amount) => DecredURI(amount: amount, address: address); } class LibAddresses { diff --git a/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart b/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart index 75d06c1484..3dc72526fd 100644 --- a/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart +++ b/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart @@ -2,6 +2,7 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -26,4 +27,7 @@ abstract class DogeCoinWalletAddressesBase extends ElectrumWalletAddresses with required Bip32Slip10Secp256k1 hd, BitcoinAddressType? addressType}) => generateP2PKHAddress(hd: hd, index: index, network: network); + + @override + PaymentURI getPaymentUri(String amount) => DogeURI(amount: amount, address: address); } diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index 0db8781c7a..f952117485 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -82,7 +82,7 @@ abstract class EVMChainWalletBase _hexPrivateKey = privateKey, _isTransactionUpdating = false, _client = client, - walletAddresses = EVMChainWalletAddresses(walletInfo), + walletAddresses = EVMChainWalletAddresses(walletInfo, client.chainId), balance = ObservableMap.of( { // Not sure of this yet, will it work? will it not? diff --git a/cw_evm/lib/evm_chain_wallet_addresses.dart b/cw_evm/lib/evm_chain_wallet_addresses.dart index 7dd501cc5e..bfa4938a32 100644 --- a/cw_evm/lib/evm_chain_wallet_addresses.dart +++ b/cw_evm/lib/evm_chain_wallet_addresses.dart @@ -1,5 +1,6 @@ import 'dart:developer'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -9,10 +10,12 @@ part 'evm_chain_wallet_addresses.g.dart'; class EVMChainWalletAddresses = EVMChainWalletAddressesBase with _$EVMChainWalletAddresses; abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store { - EVMChainWalletAddressesBase(WalletInfo walletInfo) + EVMChainWalletAddressesBase(WalletInfo walletInfo, this.chainId) : address = '', super(walletInfo); + final int chainId; + @override @observable String address; @@ -36,4 +39,16 @@ abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store { log(e.toString()); } } + + @override + PaymentURI getPaymentUri(String amount) { + switch (chainId) { + case 8453: + return BaseURI(amount: amount, address: address); + case 137: + return PolygonURI(amount: amount, address: address); + default: + return EthereumURI(amount: amount, address: address); + } + } } diff --git a/cw_monero/lib/monero_wallet_addresses.dart b/cw_monero/lib/monero_wallet_addresses.dart index 0b38ac5fd6..51c3e0f0a9 100644 --- a/cw_monero/lib/monero_wallet_addresses.dart +++ b/cw_monero/lib/monero_wallet_addresses.dart @@ -1,5 +1,5 @@ import 'package:cw_core/account.dart'; -import 'package:cw_core/address_info.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/subaddress.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; @@ -155,4 +155,7 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { @override bool containsAddress(String address) => addressInfos[account?.id ?? 0]?.any((it) => it.address == address) ?? false; + + @override + PaymentURI getPaymentUri(String amount) => MoneroURI(amount: amount, address: address); } diff --git a/cw_nano/lib/nano_wallet_addresses.dart b/cw_nano/lib/nano_wallet_addresses.dart index f1ff14a854..f52cf4ca1f 100644 --- a/cw_nano/lib/nano_wallet_addresses.dart +++ b/cw_nano/lib/nano_wallet_addresses.dart @@ -1,4 +1,5 @@ import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; @@ -15,6 +16,7 @@ abstract class NanoWalletAddressesBase extends WalletAddresses with Store { : accountList = NanoAccountList(walletInfo.address), address = '', super(walletInfo); + @override @observable String address; @@ -51,4 +53,7 @@ abstract class NanoWalletAddressesBase extends WalletAddresses with Store { printV(e.toString()); } } + + @override + PaymentURI getPaymentUri(String amount) => NanoURI(amount: amount, address: address); } diff --git a/cw_solana/lib/solana_wallet_addresses.dart b/cw_solana/lib/solana_wallet_addresses.dart index 7e9bd90089..634c73f375 100644 --- a/cw_solana/lib/solana_wallet_addresses.dart +++ b/cw_solana/lib/solana_wallet_addresses.dart @@ -1,3 +1,4 @@ +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; @@ -34,4 +35,7 @@ abstract class SolanaWalletAddressesBase extends WalletAddresses with Store { printV(e.toString()); } } + + @override + PaymentURI getPaymentUri(String amount) => SolanaURI(amount: amount, address: address); } diff --git a/cw_tron/lib/tron_wallet_addresses.dart b/cw_tron/lib/tron_wallet_addresses.dart index 095f97fa9a..99767e9654 100644 --- a/cw_tron/lib/tron_wallet_addresses.dart +++ b/cw_tron/lib/tron_wallet_addresses.dart @@ -1,5 +1,6 @@ import 'dart:developer'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -36,4 +37,7 @@ abstract class TronWalletAddressesBase extends WalletAddresses with Store { log(e.toString()); } } + + @override + PaymentURI getPaymentUri(String amount) => TronURI(amount: amount, address: address); } diff --git a/cw_wownero/lib/wownero_wallet_addresses.dart b/cw_wownero/lib/wownero_wallet_addresses.dart index 936c187247..c95d397631 100644 --- a/cw_wownero/lib/wownero_wallet_addresses.dart +++ b/cw_wownero/lib/wownero_wallet_addresses.dart @@ -1,10 +1,9 @@ import 'package:cw_core/account.dart'; -import 'package:cw_core/address_info.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/subaddress.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:cw_wownero/api/transaction_history.dart'; import 'package:cw_wownero/api/subaddress_list.dart' as subaddress_list; import 'package:cw_wownero/api/wallet.dart'; import 'package:cw_wownero/wownero_account_list.dart'; @@ -151,4 +150,7 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { @override bool containsAddress(String address) => addressInfos[account?.id ?? 0]?.any((it) => it.address == address) ?? false; + + @override + PaymentURI getPaymentUri(String amount) => WowneroURI(amount: amount, address: address); } diff --git a/cw_zano/lib/zano_wallet_addresses.dart b/cw_zano/lib/zano_wallet_addresses.dart index 39e61be7f0..1562ea8eee 100644 --- a/cw_zano/lib/zano_wallet_addresses.dart +++ b/cw_zano/lib/zano_wallet_addresses.dart @@ -1,7 +1,7 @@ +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:cw_zano/zano_wallet_api.dart'; import 'package:mobx/mobx.dart'; part 'zano_wallet_addresses.g.dart'; @@ -38,4 +38,7 @@ abstract class ZanoWalletAddressesBase extends WalletAddresses with Store { printV(e.toString()); } } + + @override + PaymentURI getPaymentUri(String amount) => ZanoURI(amount: amount, address: address); } diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 3f4240cf21..09b478da79 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -442,8 +442,6 @@ abstract class ExchangeTradeViewModelBase with Store { return ZanoURI(amount: amount, address: inputAddress); case WalletType.decred: return DecredURI(amount: amount, address: inputAddress); - case WalletType.haven: - return HavenURI(amount: amount, address: inputAddress); case WalletType.nano: return NanoURI(amount: amount, address: inputAddress); default: From 1d8c612198425f8d7c00623f838cc63929011eba Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 4 Nov 2025 20:51:58 +0100 Subject: [PATCH 13/68] feat: enhance `PaymentURI` handling with asynchronous support and Lightning-specific functionality --- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 12 ++++ .../lib/lightning/lightning_wallet.dart | 14 +++- cw_core/lib/payment_uris.dart | 14 ++++ cw_core/lib/wallet_addresses.dart | 7 ++ .../wallet_address_list_view_model.dart | 70 ++++++------------- 5 files changed, 66 insertions(+), 51 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 3f572e2401..9cc085587c 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -2,8 +2,10 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/bip/bip/bip32/bip32.dart'; import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; +import 'package:cw_bitcoin/lightning/lightning_addres_type.dart'; import 'package:cw_bitcoin/payjoin/manager.dart'; import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/parse_fixed.dart'; import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/unspent_coin_type.dart'; @@ -110,4 +112,14 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S @override PaymentURI getPaymentUri(String amount) => BitcoinURI(amount: amount, address: address, pjUri: payjoinEndpoint ?? ''); + + Future getPaymentRequestUri(String amount) async { + if (addressPageType is LightningAddressType && lightningWallet != null) { + final amountSats = amount.isNotEmpty ? parseFixed(amount, 9) : null; + final invoice = await lightningWallet!.getBolt11Invoice(amountSats, "Send to Cake Wallet"); + return LightningPaymentRequest(address: address, amount: amount, bolt11Invoice: invoice); + } + print(amount); + return getPaymentUri(amount); + } } diff --git a/cw_bitcoin/lib/lightning/lightning_wallet.dart b/cw_bitcoin/lib/lightning/lightning_wallet.dart index 6ae40da518..4d44dc3b58 100644 --- a/cw_bitcoin/lib/lightning/lightning_wallet.dart +++ b/cw_bitcoin/lib/lightning/lightning_wallet.dart @@ -59,6 +59,19 @@ class LightningWallet { .lightningAddress; } + Future getBolt11Invoice(BigInt? amount, String description) async { + final response = await sdk.receivePayment( + request: ReceivePaymentRequest( + paymentMethod: ReceivePaymentMethod.bolt11Invoice( + description: description, + amountSats: amount, + ), + ), + ); + + return response.paymentRequest; + } + Future isCompatible(String input) async { try { final inputType = await sdk.parse(input: input); @@ -186,7 +199,6 @@ class LightningWallet { isPending: payment.status == PaymentStatus.pending, date: DateTime.fromMillisecondsSinceEpoch(payment.timestamp.toInt() * 1000), confirmations: payment.status == PaymentStatus.pending ? 0 : 10, - ); } diff --git a/cw_core/lib/payment_uris.dart b/cw_core/lib/payment_uris.dart index b4ba788e51..806726d4d9 100644 --- a/cw_core/lib/payment_uris.dart +++ b/cw_core/lib/payment_uris.dart @@ -34,10 +34,24 @@ class BitcoinURI extends PaymentURI { qp['pj'] = pjUri; } + print(qp); return Uri(scheme: 'bitcoin', path: address, queryParameters: qp).toString(); } } +class LightningPaymentRequest extends PaymentURI { + LightningPaymentRequest({ + required super.amount, + required super.address, + required this.bolt11Invoice, + }); + + final String bolt11Invoice; + + @override + String toString() => bolt11Invoice; +} + class LitecoinURI extends PaymentURI { LitecoinURI({required super.amount, required super.address}); diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index b2d28df7df..c802065202 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -86,5 +86,12 @@ abstract class WalletAddresses { List get receivePageOptions => ReceivePageOptions; + /// Get a [PaymentURI] for the current [address] + /// e.g. ethereum:0x0 PaymentURI getPaymentUri(String amount); + + + /// Get a [PaymentURI] for the current [address] asynchronously + /// this can be used if a payment requires a api call beforehand + Future getPaymentRequestUri(String amount) async => getPaymentRequestUri(amount); } diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index adc60cad02..92f4c24a78 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -1,11 +1,11 @@ -import 'dart:developer' as dev; import 'dart:core'; +import 'dart:developer' as dev; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/fiat_conversion_service.dart'; -import 'package:cw_core/payment_uris.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; @@ -16,23 +16,23 @@ import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/reactions/wallet_utils.dart'; import 'package:cake_wallet/solana/solana.dart'; -import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:cake_wallet/tron/tron.dart'; -import 'package:cake_wallet/utils/qr_util.dart'; -import 'package:cake_wallet/zano/zano.dart'; import 'package:cake_wallet/utils/list_item.dart'; +import 'package:cake_wallet/utils/qr_util.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_hidden_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cake_wallet/zano/zano.dart'; import 'package:cw_core/amount_converter.dart'; import 'package:cw_core/currency.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:intl/intl.dart'; import 'package:mobx/mobx.dart'; @@ -49,9 +49,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo }) : _baseItems = [], selectedCurrency = walletTypeToCryptoCurrency(appStore.wallet!.type), _cryptoNumberFormat = NumberFormat(_cryptoNumberPattern), - hasAccounts = [WalletType.monero, WalletType.wownero, WalletType.haven] - .contains(appStore.wallet!.type), - amount = '', + hasAccounts = [WalletType.monero, WalletType.wownero].contains(appStore.wallet!.type), _settingsStore = appStore.settingsStore, super(appStore: appStore) { _init(); @@ -62,7 +60,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo _init(); selectedCurrency = walletTypeToCryptoCurrency(wallet.type); - hasAccounts = [WalletType.monero, WalletType.wownero, WalletType.haven].contains(wallet.type); + hasAccounts = [WalletType.monero, WalletType.wownero].contains(wallet.type); } static const String _cryptoNumberPattern = '0.00000000'; @@ -95,7 +93,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo int get selectedCurrencyIndex => currencies.indexOf(selectedCurrency); @observable - String amount; + String amount = ''; @computed WalletType get type => wallet.type; @@ -112,46 +110,14 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo bool get isPayjoinUnavailable => wallet.type == WalletType.bitcoin && _settingsStore.usePayjoin && payjoinEndpoint.isEmpty; - @computed - PaymentURI get uri { - switch (wallet.type) { - case WalletType.monero: - return MoneroURI(amount: amount, address: address.address); - case WalletType.haven: - return HavenURI(amount: amount, address: address.address); - case WalletType.bitcoin: - return BitcoinURI(amount: amount, address: address.address, pjUri: payjoinEndpoint); - case WalletType.litecoin: - return LitecoinURI(amount: amount, address: address.address); - case WalletType.ethereum: - return EthereumURI(amount: amount, address: address.address); - case WalletType.bitcoinCash: - return BitcoinCashURI(amount: amount, address: address.address); - case WalletType.banano: - return NanoURI(amount: amount, address: address.address); - case WalletType.nano: - return NanoURI(amount: amount, address: address.address); - case WalletType.polygon: - return PolygonURI(amount: amount, address: address.address); - case WalletType.solana: - return SolanaURI(amount: amount, address: address.address); - case WalletType.tron: - return TronURI(amount: amount, address: address.address); - case WalletType.wownero: - return WowneroURI(amount: amount, address: address.address); - case WalletType.zano: - return ZanoURI(amount: amount, address: address.address); - case WalletType.decred: - return DecredURI(amount: amount, address: address.address); - case WalletType.dogecoin: - return DogeURI(amount: amount, address: address.address); - case WalletType.base: - return BaseURI(amount: amount, address: address.address); - case WalletType.arbitrum: - return ArbitrumURI(amount: amount, address: address.address); - case WalletType.none: - throw Exception('Unexpected type: ${type.toString()}'); - } + @observable + late PaymentURI uri; + + @action + Future refreshUri() async { + print(amount); + uri = await wallet.walletAddresses.getPaymentRequestUri(amount); + print(uri); } @computed @@ -518,6 +484,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo void _init() { _baseItems = []; + uri = wallet.walletAddresses.getPaymentUri(amount); if (wallet.walletAddresses.hiddenAddresses.isNotEmpty) { _baseItems.add(WalletAddressHiddenListHeader()); @@ -537,6 +504,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo if (wallet.isEnabledAutoGenerateSubaddress) { wallet.walletAddresses.address = wallet.walletAddresses.latestAddress; } + + reaction((_) => amount, (_) => refreshUri()); + reaction((_) => address, (_) => refreshUri()); } @action From eedd44ee5f6c7e54337b953e254a0289750dbd61 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 4 Nov 2025 21:52:00 +0100 Subject: [PATCH 14/68] refactor: streamline `PaymentURI` logic and remove redundant URI implementations across wallet types --- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 2 +- cw_bitcoin/lib/electrum_wallet_addresses.dart | 4 - cw_bitcoin/lib/litecoin_wallet_addresses.dart | 4 - .../src/bitcoin_cash_wallet_addresses.dart | 2 +- .../lib/hardware/device_connection_type.dart | 6 + cw_core/lib/payment_uris.dart | 249 ++++++------------ cw_core/lib/wallet_addresses.dart | 9 +- cw_decred/lib/wallet_addresses.dart | 77 +++--- .../lib/src/dogecoin_wallet_addresses.dart | 14 +- cw_evm/lib/evm_chain_wallet.dart | 2 +- cw_evm/lib/evm_chain_wallet_addresses.dart | 17 +- cw_monero/lib/monero_wallet_addresses.dart | 2 +- cw_nano/lib/nano_wallet_addresses.dart | 4 - cw_solana/lib/solana_wallet_addresses.dart | 4 - cw_tron/lib/tron_wallet_addresses.dart | 4 - cw_wownero/lib/wownero_wallet_addresses.dart | 5 +- cw_zano/lib/zano_wallet_addresses.dart | 4 - .../exchange/exchange_trade_view_model.dart | 26 +- .../wallet_address_list_view_model.dart | 2 - 19 files changed, 147 insertions(+), 290 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 9cc085587c..7df16b020e 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -111,7 +111,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S @override PaymentURI getPaymentUri(String amount) => - BitcoinURI(amount: amount, address: address, pjUri: payjoinEndpoint ?? ''); + BitcoinURI(address: address, amount: amount, pjUri: payjoinEndpoint ?? ''); Future getPaymentRequestUri(String amount) async { if (addressPageType is LightningAddressType && lightningWallet != null) { diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 18de7eb3ad..7ef455793f 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -7,7 +7,6 @@ import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/lightning/lightning_addres_type.dart'; import 'package:cw_bitcoin/lightning/lightning_wallet.dart'; import 'package:cw_core/pathForWallet.dart'; -import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; @@ -766,7 +765,4 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { await lightningWallet!.registerAddress(walletName.replaceAll(" ", "").toLowerCase()); } } - - @override - PaymentURI getPaymentUri(String amount) => BitcoinURI(amount: amount, address: address); } diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 8d6e24b5f8..2095d4bd4e 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -9,7 +9,6 @@ import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/utils.dart'; -import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -227,7 +226,4 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) ]; } - - @override - PaymentURI getPaymentUri(String amount) => LitecoinURI(amount: amount, address: address); } diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart index 681fc00d73..25c11b7639 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -31,5 +31,5 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi generateP2PKHAddress(hd: hd, index: index, network: network); @override - PaymentURI getPaymentUri(String amount) => BitcoinCashURI(amount: amount, address: address); + PaymentURI getPaymentUri(String amount) => BitcoinCashURI(address: address, amount: amount); } diff --git a/cw_core/lib/hardware/device_connection_type.dart b/cw_core/lib/hardware/device_connection_type.dart index 76f07edf18..fcdb1545ae 100644 --- a/cw_core/lib/hardware/device_connection_type.dart +++ b/cw_core/lib/hardware/device_connection_type.dart @@ -36,6 +36,12 @@ enum DeviceConnectionType { WalletType.polygon, ].contains(walletType); break; + case HardwareWalletType.cupcake: + case HardwareWalletType.coldcard: + case HardwareWalletType.seedsigner: + case HardwareWalletType.keystone: + // This should not be thrown since it should never reach this code for these HardwareWalletTypes + throw UnimplementedError(); } return isSupported diff --git a/cw_core/lib/payment_uris.dart b/cw_core/lib/payment_uris.dart index 806726d4d9..ed172ca044 100644 --- a/cw_core/lib/payment_uris.dart +++ b/cw_core/lib/payment_uris.dart @@ -1,26 +1,41 @@ -import 'package:cw_core/format_fixed.dart'; +import "package:cw_core/format_fixed.dart"; -abstract class PaymentURI { - PaymentURI({required this.amount, required this.address}); +class PaymentURI { + const PaymentURI({required this.scheme, required this.address, required this.amount}); - final String amount; + final String scheme; final String address; + final String amount; + + String toString() { + final queryParameters = {}; + + if (amount.isNotEmpty) queryParameters["amount"] = amount.replaceAll(",", "."); + + return Uri(scheme: scheme, path: address, queryParameters: queryParameters).toString(); + } } class MoneroURI extends PaymentURI { - MoneroURI({required super.amount, required super.address}); + const MoneroURI({required super.address, required super.amount, super.scheme = "monero"}); @override String toString() { - var base = 'monero:$address'; - if (amount.isNotEmpty) base += '?tx_amount=${amount.replaceAll(',', '.')}'; + final queryParameters = {}; - return base; + if (amount.isNotEmpty) queryParameters["tx_amount"] = amount.replaceAll(",", "."); + + return Uri(scheme: scheme, path: address, queryParameters: queryParameters).toString(); } } class BitcoinURI extends PaymentURI { - BitcoinURI({required super.amount, required super.address, this.pjUri = ''}); + const BitcoinURI({ + required super.address, + required super.amount, + this.pjUri = "", + super.scheme = "bitcoin", + }); final String pjUri; @@ -28,23 +43,22 @@ class BitcoinURI extends PaymentURI { String toString() { final qp = {}; - if (amount.isNotEmpty) qp['amount'] = amount.replaceAll(',', '.'); + if (amount.isNotEmpty) qp["amount"] = amount.replaceAll(",", "."); if (pjUri.isNotEmpty && !address.startsWith("sp")) { - qp['pjos'] = '0'; - qp['pj'] = pjUri; + qp["pjos"] = "0"; + qp["pj"] = pjUri; } - print(qp); - return Uri(scheme: 'bitcoin', path: address, queryParameters: qp).toString(); + return Uri(scheme: "bitcoin", path: address, queryParameters: qp).toString(); } } class LightningPaymentRequest extends PaymentURI { - LightningPaymentRequest({ - required super.amount, - required super.address, - required this.bolt11Invoice, - }); + const LightningPaymentRequest( + {required super.address, + required super.amount, + required this.bolt11Invoice, + super.scheme = "lightning"}); final String bolt11Invoice; @@ -104,7 +118,7 @@ class ArbitrumURI extends PaymentURI { } class BitcoinCashURI extends PaymentURI { - BitcoinCashURI({required super.amount, required super.address}); + const BitcoinCashURI({required super.address, required super.amount, super.scheme = ""}); @override String toString() { @@ -115,145 +129,40 @@ class BitcoinCashURI extends PaymentURI { } } -class NanoURI extends PaymentURI { - NanoURI({required super.amount, required super.address}); - - @override - String toString() { - var base = 'nano:$address'; - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } - - return base; - } -} - -class PolygonURI extends PaymentURI { - PolygonURI({required super.amount, required super.address}); - - @override - String toString() { - var base = 'polygon:$address'; - if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; - - return base; - } -} - -class SolanaURI extends PaymentURI { - SolanaURI({required super.amount, required super.address}); - - @override - String toString() { - var base = 'solana:$address'; - if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; - - return base; - } -} - -class TronURI extends PaymentURI { - TronURI({required super.amount, required super.address}); - - @override - String toString() { - var base = 'tron:$address'; - if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; - - return base; - } -} - -class WowneroURI extends PaymentURI { - WowneroURI({required super.amount, required super.address}); - - @override - String toString() { - var base = 'wownero:$address'; - if (amount.isNotEmpty) base += '?tx_amount=${amount.replaceAll(',', '.')}'; - - return base; - } -} - -class ZanoURI extends PaymentURI { - ZanoURI({required String amount, required String address}) - : super(amount: amount, address: address); - - @override - String toString() { - var base = 'zano:$address'; - if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; - - return base; - } -} - -class DecredURI extends PaymentURI { - DecredURI({required String amount, required String address}) - : super(amount: amount, address: address); - - @override - String toString() { - var base = 'decred:$address'; - if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; - - return base; - } -} - -class DogeURI extends PaymentURI { - DogeURI({required String amount, required String address}) - : super(amount: amount, address: address); - - @override - String toString() { - var base = 'doge:$address'; - if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; - - return base; - } -} - class ERC681URI extends PaymentURI { final int chainId; final String? contractAddress; - ERC681URI({ + const ERC681URI({ required this.chainId, required super.address, required super.amount, required this.contractAddress, + super.scheme = "ethereum", }); @override String toString() { - var uri = 'ethereum:'; + var uri = '$scheme:'; final targetAddress = contractAddress ?? address; uri += targetAddress; - if (chainId != 1) uri += '@$chainId'; + if (chainId != 1) uri += "@$chainId"; - if (contractAddress != null) uri += '/transfer'; + if (contractAddress != null) uri += "/transfer"; final params = {}; if (contractAddress != null) { - params['address'] = address; - if (amount.isNotEmpty) { - params['uint256'] = _formatAmountForERC20(amount); - } + params["address"] = address; + if (amount.isNotEmpty) params["uint256"] = _formatAmountForERC20(amount); } else { - if (amount.isNotEmpty) { - params['value'] = _formatAmountForNative(amount); - } + if (amount.isNotEmpty) params["value"] = _formatAmountForNative(amount); } if (params.isNotEmpty) { - uri += '?'; - uri += params.entries.map((e) => '${e.key}=${e.value}').join('&'); + uri += "?${params.entries.map((e) => "${e.key}=${e.value}").join("&")}"; } return uri; @@ -263,12 +172,11 @@ class ERC681URI extends PaymentURI { String _formatAmountForERC20(String amount) { try { // Convert decimal amount to BigInt (assuming 18 decimals) - final amountDouble = double.parse(amount.replaceAll(',', '.')); + final amountDouble = double.parse(amount.replaceAll(",", ".")); final amountBigInt = BigInt.from(amountDouble * 1e18); return amountBigInt.toString(); } catch (e) { - // Fallback to original amount if parsing fails - return amount.replaceAll(',', '.'); + return amount.replaceAll(",", "."); } } @@ -276,13 +184,12 @@ class ERC681URI extends PaymentURI { String _formatAmountForNative(String amount) { try { // Convert decimal amount to double for scientific notation - final amountDouble = double.parse(amount.replaceAll(',', '.')); + final amountDouble = double.parse(amount.replaceAll(",", ".")); // Use scientific notation as recommended by ERC-681 - return '${amountDouble}e18'; + return "${amountDouble}e18"; } catch (e) { - // Fallback to original amount if parsing fails - return amount.replaceAll(',', '.'); + return amount.replaceAll(",", "."); } } @@ -290,7 +197,7 @@ class ERC681URI extends PaymentURI { final (isContract, targetAddress) = _getTargetAddress(uri.path); final chainId = _getChainID(uri.path); - final address = isContract ? uri.queryParameters["address"] ?? '' : targetAddress; + final address = isContract ? uri.queryParameters["address"] ?? "" : targetAddress; final amountParam = isContract ? uri.queryParameters["uint256"] : uri.queryParameters["value"]; var formatedAmount = ""; @@ -311,15 +218,12 @@ class ERC681URI extends PaymentURI { } static int _getChainID(String path) { - return int.parse(RegExp( - r'@\d*', - ).firstMatch(path)?.group(0)?.replaceAll("@", "") ?? - "1"); + return int.parse(RegExp(r"@\d*").firstMatch(path)?.group(0)?.replaceAll("@", "") ?? "1"); } static (bool, String) _getTargetAddress(String path) { final targetAddress = - RegExp(r'^(0x)?[0-9a-f]{40}', caseSensitive: false).firstMatch(path)!.group(0)!; + RegExp(r"^(0x)?[0-9a-f]{40}", caseSensitive: false).firstMatch(path)!.group(0)!; return (path.contains("/"), targetAddress); } @@ -330,70 +234,67 @@ class ERC681URI extends PaymentURI { /// - Scientific notation: "0.123e18", "1e6" → expanded to integer /// - Decimal ETH: "0.123456" → shifted by 18 decimals static String _normalizeToIntegerWei(String input) { - final raw = input.replaceAll(',', '.').trim(); + final raw = input.replaceAll(",", ".").trim(); // First we check if it's already a plain integer (basically just a number with no dot, no exponent) try { - final isPlainInteger = RegExp(r'^[+-]?\d+$').hasMatch(raw) && - !raw.contains('.') && - !raw.toLowerCase().contains('e'); - if (isPlainInteger) return raw.replaceFirst(RegExp(r'^\+'), ''); + final isPlainInteger = RegExp(r"^[+-]?\d+$").hasMatch(raw) && + !raw.contains(".") && + !raw.toLowerCase().contains("e"); + if (isPlainInteger) return raw.replaceFirst(RegExp(r"^\+"), ""); // Then we check if it's a scientific notation - final sci = RegExp(r'^[+-]?(\d+\.?\d*|\d*\.?\d+)[eE][+-]?\d+$'); + final sci = RegExp(r"^[+-]?(\d+\.?\d*|\d*\.?\d+)[eE][+-]?\d+$"); if (sci.hasMatch(raw)) { - final mantissaStr = raw.toLowerCase().split('e')[0]; - final exp = int.parse(raw.toLowerCase().split('e')[1]); + final mantissaStr = raw.toLowerCase().split("e")[0]; + final exp = int.parse(raw.toLowerCase().split("e")[1]); return _expandDecimal(mantissaStr, exp); } // Lastly, we check if it's a fixed decimal ETH amount, here we shift by 18 to get wei for the amount - if (raw.contains('.')) { + if (raw.contains(".")) { return _expandDecimal(raw, 18); } return raw; } catch (e) { return raw; } - - // If none of these checks work, we return the raw input } /// Expands a decimal string by shifting the decimal point `expShift` places /// to the right and returns an integer string (digits only, optional leading minus). /// Examples: - /// _expandDecimal('0.123456', 18) -> '123456000000000000' - /// _expandDecimal('1.2', 3) -> '1200' + /// _expandDecimal("0.123456", 18) -> "123456000000000000" + /// _expandDecimal("1.2", 3) -> "1200" static String _expandDecimal(String decimalStr, int expShift) { var s = decimalStr.trim(); - var sign = ''; - if (s.startsWith('-') || s.startsWith('+')) { - sign = s[0] == '-' ? '-' : ''; + var sign = ""; + if (s.startsWith("-") || s.startsWith("+")) { + sign = s[0] == "-" ? "-" : ""; s = s.substring(1); } // First we split the integer and fractional parts - final parts = s.split('.'); - final intPart = parts[0].isEmpty ? '0' : parts[0]; - final fracPart = parts.length > 1 ? parts[1] : ''; - final digits = (intPart + fracPart).replaceFirst(RegExp(r'^0+'), ''); + final parts = s.split("."); + final intPart = parts[0].isEmpty ? "0" : parts[0]; + final fracPart = parts.length > 1 ? parts[1] : ""; + final digits = (intPart + fracPart).replaceFirst(RegExp(r"^0+"), ""); final fracLen = fracPart.length; // Then we calculate the effective shift = desired shift minus existing fractional digits final shift = expShift - fracLen; if (shift >= 0) { - final head = digits.isEmpty ? '0' : digits; - final zeros = List.filled(shift, '0').join(); + final head = digits.isEmpty ? "0" : digits; + final zeros = List.filled(shift, "0").join(); final res = head + zeros; - return sign + (res.isEmpty ? '0' : res); + return sign + (res.isEmpty ? "0" : res); } else { // Need to insert a decimal point within digits; return integer by truncating final cut = digits.length + shift; - if (cut <= 0) { - return '0'; - } + if (cut <= 0) return "0"; + final res = digits.substring(0, cut); - return sign + (res.isEmpty ? '0' : res); + return sign + (res.isEmpty ? "0" : res); } } } diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index c802065202..44fa821789 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -88,10 +88,13 @@ abstract class WalletAddresses { /// Get a [PaymentURI] for the current [address] /// e.g. ethereum:0x0 - PaymentURI getPaymentUri(String amount); - + PaymentURI getPaymentUri(String amount) => PaymentURI( + scheme: walletTypeToString(walletInfo.type).toLowerCase(), + address: address, + amount: amount, + ); /// Get a [PaymentURI] for the current [address] asynchronously /// this can be used if a payment requires a api call beforehand - Future getPaymentRequestUri(String amount) async => getPaymentRequestUri(amount); + Future getPaymentRequestUri(String amount) async => getPaymentUri(amount); } diff --git a/cw_decred/lib/wallet_addresses.dart b/cw_decred/lib/wallet_addresses.dart index f7d0a8baec..273a8e0510 100644 --- a/cw_decred/lib/wallet_addresses.dart +++ b/cw_decred/lib/wallet_addresses.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:mobx/mobx.dart'; @@ -13,10 +12,10 @@ part 'wallet_addresses.g.dart'; class DecredWalletAddresses = DecredWalletAddressesBase with _$DecredWalletAddresses; abstract class DecredWalletAddressesBase extends WalletAddresses with Store { - DecredWalletAddressesBase(super.walletInfo, Libwallet libwallet, super.isTestnet) - : _libwallet = libwallet; + DecredWalletAddressesBase(super.walletInfo, this._libwallet, super.isTestnet); + final Libwallet _libwallet; - String currentAddr = ''; + String _currentAddr = ''; @observable bool isEnabledAutoGenerateSubaddress = true; @@ -43,14 +42,13 @@ abstract class DecredWalletAddressesBase extends WalletAddresses with Store { @override Future updateAddressesInBox() async { - final addrs = await libAddresses(); - final allAddrs = new List.from(addrs.usedAddrs)..addAll(addrs.unusedAddrs); + final addrs = await _libAddresses(); + final allAddrs = List.from(addrs.usedAddrs)..addAll(addrs.unusedAddrs); // Add all addresses. allAddrs.forEach((addr) { - if (addressesMap.containsKey(addr)) { - return; - } + if (addressesMap.containsKey(addr)) return; + addressesMap[addr] = ""; addressInfos[0] ??= []; addressInfos[0]?.add( @@ -66,44 +64,37 @@ abstract class DecredWalletAddressesBase extends WalletAddresses with Store { // Add used addresses. addrs.usedAddrs.forEach((addr) { - if (!usedAddresses.contains(addr)) { - usedAddresses.add(addr); - } + if (!usedAddresses.contains(addr)) usedAddresses.add(addr); }); - if (addrs.unusedAddrs.length > 0 && addrs.unusedAddrs[0] != currentAddr) { - currentAddr = addrs.unusedAddrs[0]; - selectedAddr = currentAddr; + if (addrs.unusedAddrs.length > 0 && addrs.unusedAddrs[0] != _currentAddr) { + _currentAddr = addrs.unusedAddrs[0]; + selectedAddr = _currentAddr; } await saveAddressesInBox(); } List getAddressInfos() { - if (addressInfos.containsKey(0)) { - return addressInfos[0]!; - } + if (addressInfos.containsKey(0)) return addressInfos[0]!; + return []; } Future updateAddress(String address, String label) async { - if (!addressInfos.containsKey(0)) { - return; - } + if (!addressInfos.containsKey(0)) return; + addressInfos[0]!.forEach((info) { - if (info.address == address) { - info.label = label; - } + if (info.address == address) info.label = label; }); await saveAddressesInBox(); } - Future libAddresses() async { + Future<_LibAddresses> _libAddresses() async { final nUsed = "10"; var nUnused = "1"; - if (this.isEnabledAutoGenerateSubaddress) { - nUnused = "3"; - } + if (this.isEnabledAutoGenerateSubaddress) nUnused = "3"; + try { final res = await _libwallet.addresses(walletInfo.name, nUsed, nUnused); final decoded = json.decode(res); @@ -111,10 +102,10 @@ abstract class DecredWalletAddressesBase extends WalletAddresses with Store { final unusedAddrs = List.from(decoded["unused"] ?? []); // index is the index of the first unused address. final index = decoded["index"] ?? 0; - return new LibAddresses(usedAddrs, unusedAddrs, index); + return _LibAddresses(usedAddrs, unusedAddrs, index); } catch (e) { printV(e); - return LibAddresses([], [], 0); + return _LibAddresses([], [], 0); } } @@ -122,9 +113,8 @@ abstract class DecredWalletAddressesBase extends WalletAddresses with Store { // NOTE: This will ignore the gap limit and may cause problems when restoring from seed if too // many addresses are taken and not used. final addr = await _libwallet.newExternalAddress(walletInfo.name) ?? ''; - if (addr == "") { - return; - } + if (addr == "") return; + if (!addressesMap.containsKey(addr)) { addressesMap[addr] = ""; addressInfos[0] ??= []; @@ -143,22 +133,17 @@ abstract class DecredWalletAddressesBase extends WalletAddresses with Store { } @override - List get receivePageOptions { - return isTestnet - ? [ - ReceivePageOption.testnet, - ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) - ] - : ReceivePageOptions; - } - - @override - PaymentURI getPaymentUri(String amount) => DecredURI(amount: amount, address: address); + List get receivePageOptions => isTestnet + ? [ + ReceivePageOption.testnet, + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ] + : ReceivePageOptions; } -class LibAddresses { +class _LibAddresses { final List usedAddrs, unusedAddrs; final int firstUnusedAddrIndex; - LibAddresses(this.usedAddrs, this.unusedAddrs, this.firstUnusedAddrIndex); + _LibAddresses(this.usedAddrs, this.unusedAddrs, this.firstUnusedAddrIndex); } diff --git a/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart b/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart index 3dc72526fd..8f12dcc1ca 100644 --- a/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart +++ b/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart @@ -11,7 +11,8 @@ part 'dogecoin_wallet_addresses.g.dart'; class DogeCoinWalletAddresses = DogeCoinWalletAddressesBase with _$DogeCoinWalletAddresses; abstract class DogeCoinWalletAddressesBase extends ElectrumWalletAddresses with Store { - DogeCoinWalletAddressesBase(WalletInfo walletInfo, { + DogeCoinWalletAddressesBase( + WalletInfo walletInfo, { required super.mainHd, required super.sideHd, required super.network, @@ -19,15 +20,18 @@ abstract class DogeCoinWalletAddressesBase extends ElectrumWalletAddresses with super.initialAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, - super.initialAddressPageType + super.initialAddressPageType, }) : super(walletInfo); @override - String getAddress({required int index, + String getAddress({ + required int index, required Bip32Slip10Secp256k1 hd, - BitcoinAddressType? addressType}) => + BitcoinAddressType? addressType, + }) => generateP2PKHAddress(hd: hd, index: index, network: network); @override - PaymentURI getPaymentUri(String amount) => DogeURI(amount: amount, address: address); + PaymentURI getPaymentUri(String amount) => + PaymentURI(scheme: "doge", address: address, amount: amount); } diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index f952117485..0db8781c7a 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -82,7 +82,7 @@ abstract class EVMChainWalletBase _hexPrivateKey = privateKey, _isTransactionUpdating = false, _client = client, - walletAddresses = EVMChainWalletAddresses(walletInfo, client.chainId), + walletAddresses = EVMChainWalletAddresses(walletInfo), balance = ObservableMap.of( { // Not sure of this yet, will it work? will it not? diff --git a/cw_evm/lib/evm_chain_wallet_addresses.dart b/cw_evm/lib/evm_chain_wallet_addresses.dart index bfa4938a32..7dd501cc5e 100644 --- a/cw_evm/lib/evm_chain_wallet_addresses.dart +++ b/cw_evm/lib/evm_chain_wallet_addresses.dart @@ -1,6 +1,5 @@ import 'dart:developer'; -import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -10,12 +9,10 @@ part 'evm_chain_wallet_addresses.g.dart'; class EVMChainWalletAddresses = EVMChainWalletAddressesBase with _$EVMChainWalletAddresses; abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store { - EVMChainWalletAddressesBase(WalletInfo walletInfo, this.chainId) + EVMChainWalletAddressesBase(WalletInfo walletInfo) : address = '', super(walletInfo); - final int chainId; - @override @observable String address; @@ -39,16 +36,4 @@ abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store { log(e.toString()); } } - - @override - PaymentURI getPaymentUri(String amount) { - switch (chainId) { - case 8453: - return BaseURI(amount: amount, address: address); - case 137: - return PolygonURI(amount: amount, address: address); - default: - return EthereumURI(amount: amount, address: address); - } - } } diff --git a/cw_monero/lib/monero_wallet_addresses.dart b/cw_monero/lib/monero_wallet_addresses.dart index 51c3e0f0a9..9a7264c035 100644 --- a/cw_monero/lib/monero_wallet_addresses.dart +++ b/cw_monero/lib/monero_wallet_addresses.dart @@ -157,5 +157,5 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { addressInfos[account?.id ?? 0]?.any((it) => it.address == address) ?? false; @override - PaymentURI getPaymentUri(String amount) => MoneroURI(amount: amount, address: address); + PaymentURI getPaymentUri(String amount) => MoneroURI(address: address, amount: amount); } diff --git a/cw_nano/lib/nano_wallet_addresses.dart b/cw_nano/lib/nano_wallet_addresses.dart index f52cf4ca1f..d29433e39e 100644 --- a/cw_nano/lib/nano_wallet_addresses.dart +++ b/cw_nano/lib/nano_wallet_addresses.dart @@ -1,5 +1,4 @@ import 'package:cw_core/cake_hive.dart'; -import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; @@ -53,7 +52,4 @@ abstract class NanoWalletAddressesBase extends WalletAddresses with Store { printV(e.toString()); } } - - @override - PaymentURI getPaymentUri(String amount) => NanoURI(amount: amount, address: address); } diff --git a/cw_solana/lib/solana_wallet_addresses.dart b/cw_solana/lib/solana_wallet_addresses.dart index 634c73f375..7e9bd90089 100644 --- a/cw_solana/lib/solana_wallet_addresses.dart +++ b/cw_solana/lib/solana_wallet_addresses.dart @@ -1,4 +1,3 @@ -import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; @@ -35,7 +34,4 @@ abstract class SolanaWalletAddressesBase extends WalletAddresses with Store { printV(e.toString()); } } - - @override - PaymentURI getPaymentUri(String amount) => SolanaURI(amount: amount, address: address); } diff --git a/cw_tron/lib/tron_wallet_addresses.dart b/cw_tron/lib/tron_wallet_addresses.dart index 99767e9654..095f97fa9a 100644 --- a/cw_tron/lib/tron_wallet_addresses.dart +++ b/cw_tron/lib/tron_wallet_addresses.dart @@ -1,6 +1,5 @@ import 'dart:developer'; -import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -37,7 +36,4 @@ abstract class TronWalletAddressesBase extends WalletAddresses with Store { log(e.toString()); } } - - @override - PaymentURI getPaymentUri(String amount) => TronURI(amount: amount, address: address); } diff --git a/cw_wownero/lib/wownero_wallet_addresses.dart b/cw_wownero/lib/wownero_wallet_addresses.dart index c95d397631..ef17237f48 100644 --- a/cw_wownero/lib/wownero_wallet_addresses.dart +++ b/cw_wownero/lib/wownero_wallet_addresses.dart @@ -62,7 +62,7 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { WowneroSubaddressList subaddressList; WowneroAccountList accountList; - + @override Set usedAddresses = Set(); @@ -152,5 +152,6 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { addressInfos[account?.id ?? 0]?.any((it) => it.address == address) ?? false; @override - PaymentURI getPaymentUri(String amount) => WowneroURI(amount: amount, address: address); + PaymentURI getPaymentUri(String amount) => + MoneroURI(scheme: "wownero", address: address, amount: amount); } diff --git a/cw_zano/lib/zano_wallet_addresses.dart b/cw_zano/lib/zano_wallet_addresses.dart index 1562ea8eee..be25c9383e 100644 --- a/cw_zano/lib/zano_wallet_addresses.dart +++ b/cw_zano/lib/zano_wallet_addresses.dart @@ -1,4 +1,3 @@ -import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; @@ -38,7 +37,4 @@ abstract class ZanoWalletAddressesBase extends WalletAddresses with Store { printV(e.toString()); } } - - @override - PaymentURI getPaymentUri(String amount) => ZanoURI(amount: amount, address: address); } diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 09b478da79..a2cf0d738e 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -414,13 +414,11 @@ abstract class ExchangeTradeViewModelBase with Store { switch (wallet.type) { case WalletType.bitcoin: - return BitcoinURI(amount: amount, address: inputAddress); - case WalletType.litecoin: - return LitecoinURI(amount: amount, address: inputAddress); + return BitcoinURI(address: inputAddress, amount: amount); case WalletType.bitcoinCash: - return BitcoinCashURI(amount: amount, address: inputAddress); + return BitcoinCashURI(address: inputAddress, amount: amount); case WalletType.dogecoin: - return DogeURI(amount: amount, address: inputAddress); + return PaymentURI(scheme: "doge", address: inputAddress, amount: amount); case WalletType.ethereum: return _createERC681URI(fromCurrency, inputAddress, amount); // TODO: Expand ERC681URI support to Polygon(modify decoding flow for QRs, pay anything, and deep link handling) @@ -435,17 +433,17 @@ abstract class ExchangeTradeViewModelBase with Store { case WalletType.tron: return TronURI(amount: amount, address: inputAddress); case WalletType.monero: - return MoneroURI(amount: amount, address: inputAddress); + return MoneroURI(address: inputAddress, amount: amount); case WalletType.wownero: - return WowneroURI(amount: amount, address: inputAddress); - case WalletType.zano: - return ZanoURI(amount: amount, address: inputAddress); - case WalletType.decred: - return DecredURI(amount: amount, address: inputAddress); - case WalletType.nano: - return NanoURI(amount: amount, address: inputAddress); + return MoneroURI( + scheme: walletTypeToString(wallet.type).toLowerCase(), + address: inputAddress, + amount: amount); default: - return null; + return PaymentURI( + scheme: walletTypeToString(wallet.type).toLowerCase(), + address: inputAddress, + amount: amount); } } diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 92f4c24a78..db7111017a 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -115,9 +115,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @action Future refreshUri() async { - print(amount); uri = await wallet.walletAddresses.getPaymentRequestUri(amount); - print(uri); } @computed From 008f2af20dbb80f1c403a7e82b8dc2c2cf096434 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 4 Nov 2025 21:53:54 +0100 Subject: [PATCH 15/68] refactor: remove redundant debug print statement from `bitcoin_wallet_addresses.dart` --- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 7df16b020e..c14ab11849 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -119,7 +119,6 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S final invoice = await lightningWallet!.getBolt11Invoice(amountSats, "Send to Cake Wallet"); return LightningPaymentRequest(address: address, amount: amount, bolt11Invoice: invoice); } - print(amount); return getPaymentUri(amount); } } From db80ebfe493dcc9b066ef8836475d8a4ef479da0 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Wed, 5 Nov 2025 14:39:07 +0100 Subject: [PATCH 16/68] refactor: improve consistency in widget styling and centralized label logic, add Bitcoin Lightning deposit/withdraw support --- .../pages/balance/balance_row_widget.dart | 144 ++++++++++-------- res/values/strings_ar.arb | 2 + res/values/strings_bg.arb | 2 + res/values/strings_cs.arb | 2 + res/values/strings_de.arb | 4 +- res/values/strings_en.arb | 2 + res/values/strings_es.arb | 2 + res/values/strings_fr.arb | 2 + res/values/strings_ha.arb | 2 + res/values/strings_hi.arb | 2 + res/values/strings_hr.arb | 2 + res/values/strings_hy.arb | 2 + res/values/strings_id.arb | 2 + res/values/strings_it.arb | 2 + res/values/strings_ja.arb | 2 + res/values/strings_ko.arb | 2 + res/values/strings_my.arb | 2 + res/values/strings_nl.arb | 2 + res/values/strings_pl.arb | 2 + res/values/strings_pt.arb | 2 + res/values/strings_ru.arb | 2 + res/values/strings_th.arb | 2 + res/values/strings_tl.arb | 2 + res/values/strings_tr.arb | 2 + res/values/strings_uk.arb | 2 + res/values/strings_ur.arb | 2 + res/values/strings_vi.arb | 2 + res/values/strings_yo.arb | 2 + res/values/strings_zh.arb | 2 + 29 files changed, 139 insertions(+), 63 deletions(-) diff --git a/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart b/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart index 3dac4cea13..68d2ca2782 100644 --- a/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart +++ b/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart @@ -14,6 +14,7 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -148,14 +149,12 @@ class BalanceRowWidget extends StatelessWidget { maxLines: 1, textAlign: TextAlign.start, ), - SizedBox(height: 6), + const SizedBox(height: 6), if (isTestnet) Text( S.of(context).testnet_coins_no_value, textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - height: 1, - ), + style: Theme.of(context).textTheme.bodyMedium?.copyWith(height: 1), ), if (!isTestnet) Text( @@ -216,7 +215,7 @@ class BalanceRowWidget extends StatelessWidget { if (currency.isPotentialScam) Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - margin: EdgeInsets.only(top: 4), + margin: const EdgeInsets.only(top: 4), decoration: BoxDecoration( color: Theme.of(context).colorScheme.errorContainer, borderRadius: BorderRadius.circular(8), @@ -244,7 +243,7 @@ class BalanceRowWidget extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 26), + const SizedBox(height: 26), Row( children: [ Text( @@ -257,7 +256,7 @@ class BalanceRowWidget extends StatelessWidget { ), ], ), - SizedBox(height: 8), + const SizedBox(height: 8), AutoSizeText( frozenBalance, style: Theme.of(context).textTheme.bodyLarge!.copyWith( @@ -268,14 +267,12 @@ class BalanceRowWidget extends StatelessWidget { maxLines: 1, textAlign: TextAlign.center, ), - SizedBox(height: 4), + const SizedBox(height: 4), if (!isTestnet) Text( frozenFiatBalance, textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodySmall!.copyWith( - height: 1, - ), + style: Theme.of(context).textTheme.bodySmall!.copyWith(height: 1), ), ], ), @@ -283,7 +280,7 @@ class BalanceRowWidget extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 24), + const SizedBox(height: 24), Text( '${additionalBalanceLabel}', textAlign: TextAlign.center, @@ -292,7 +289,7 @@ class BalanceRowWidget extends StatelessWidget { height: 1, ), ), - SizedBox(height: 8), + const SizedBox(height: 8), AutoSizeText( additionalBalance, style: Theme.of(context).textTheme.bodyLarge!.copyWith( @@ -303,14 +300,12 @@ class BalanceRowWidget extends StatelessWidget { maxLines: 1, textAlign: TextAlign.center, ), - SizedBox(height: 4), + const SizedBox(height: 4), if (!isTestnet) Text( '${additionalFiatBalance}', textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodySmall!.copyWith( - height: 1, - ), + style: Theme.of(context).textTheme.bodySmall!.copyWith(height: 1), ), ], ), @@ -333,15 +328,6 @@ class BalanceRowWidget extends StatelessWidget { begin: Alignment.topCenter, end: Alignment.bottomCenter, ), - // boxShadow: [ - // BoxShadow( - // color: Theme.of(context) - // .extension()! - // .cardBorderColor - // .withAlpha(50), - // spreadRadius: dashboardViewModel.getShadowSpread(), - // blurRadius: dashboardViewModel.getShadowBlur()) - // ], ), child: TextButton( onPressed: _showToast, @@ -360,27 +346,48 @@ class BalanceRowWidget extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - Container( - child: Column( - children: [ - Container( - child: ImageIcon( - AssetImage('assets/images/mweb_logo.png'), - size: 40, - ), - ), - const SizedBox(height: 10), - Text( - 'MWEB', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontSize: 16, - fontWeight: FontWeight.w700, - color: Theme.of(context).colorScheme.onSurface, - height: 1, - ), - ), - ], - ), + Column( + children: [ + ImageIcon( + AssetImage('assets/images/mweb_logo.png'), + size: 40, + ), + const SizedBox(height: 10), + Text( + 'MWEB', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.onSurface, + height: 1, + ), + ), + ], + ), + ], + ), + if (currency == CryptoCurrency.btc) + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Column( + children: [ + SvgPicture.asset( + 'assets/images/lightning-icon.svg', + width: 40, + height: 40, + ), + const SizedBox(height: 10), + Text( + 'Lightning', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.onSurface, + height: 1, + ), + ), + ], ), ], ), @@ -392,15 +399,11 @@ class BalanceRowWidget extends StatelessWidget { children: [ GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () => launchUrl( - Uri.parse( - "https://docs.cakewallet.com/cryptos/litecoin#mweb"), - mode: LaunchMode.externalApplication, - ), + onTap: onPressedHelp, child: Row( children: [ Text( - '${secondAvailableBalanceLabel}', + secondAvailableBalanceLabel, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: @@ -434,7 +437,7 @@ class BalanceRowWidget extends StatelessWidget { SizedBox(height: 6), if (!isTestnet) Text( - '${secondAvailableFiatBalance}', + secondAvailableFiatBalance, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontSize: 16, @@ -462,7 +465,7 @@ class BalanceRowWidget extends StatelessWidget { children: [ SizedBox(height: 24), Text( - '${secondAdditionalBalanceLabel}', + secondAdditionalBalanceLabel, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, @@ -498,13 +501,13 @@ class BalanceRowWidget extends StatelessWidget { ), IntrinsicHeight( child: Container( - padding: EdgeInsets.symmetric(horizontal: 12), + padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Semantics( - label: S.of(context).litecoin_mweb_pegin, + label: depositToL2Label, child: OutlinedButton( onPressed: () => depositToL2(context), style: OutlinedButton.styleFrom( @@ -519,7 +522,7 @@ class BalanceRowWidget extends StatelessWidget { ), ), child: Container( - padding: EdgeInsets.symmetric(vertical: 12), + padding: const EdgeInsets.symmetric(vertical: 12), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -531,7 +534,7 @@ class BalanceRowWidget extends StatelessWidget { ), const SizedBox(width: 8), Text( - S.of(context).litecoin_mweb_pegin, + depositToL2Label, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onPrimary, fontWeight: FontWeight.w700, @@ -546,7 +549,7 @@ class BalanceRowWidget extends StatelessWidget { SizedBox(width: 16), Expanded( child: Semantics( - label: S.of(context).litecoin_mweb_pegout, + label: withdrawFromL2Label, child: OutlinedButton( onPressed: () => withdrawFromL2(context), style: OutlinedButton.styleFrom( @@ -573,7 +576,7 @@ class BalanceRowWidget extends StatelessWidget { ), const SizedBox(width: 8), Text( - S.of(context).litecoin_mweb_pegout, + withdrawFromL2Label, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context) .colorScheme @@ -591,7 +594,7 @@ class BalanceRowWidget extends StatelessWidget { ), ), ), - SizedBox(height: 16), + const SizedBox(height: 16), ], ), ), @@ -601,6 +604,14 @@ class BalanceRowWidget extends StatelessWidget { ); } + String get depositToL2Label => dashboardViewModel.type == WalletType.litecoin + ? S.current.litecoin_mweb_pegin + : S.current.bitcoin_lightning_deposit; + + String get withdrawFromL2Label => dashboardViewModel.type == WalletType.litecoin + ? S.current.litecoin_mweb_pegout + : S.current.bitcoin_lightning_withdraw; + Future depositToL2(BuildContext context) async { PaymentRequest? paymentRequest = null; @@ -653,6 +664,15 @@ class BalanceRowWidget extends StatelessWidget { ); } + void onPressedHelp() { + var helpUri = Uri.parse("https://docs.cakewallet.com/cryptos/bitcoin#lightning"); + if (dashboardViewModel.type == WalletType.litecoin) { + helpUri = Uri.parse("https://docs.cakewallet.com/cryptos/litecoin#mweb"); + } + + launchUrl(helpUri, mode: LaunchMode.externalApplication); + } + void _showBalanceDescription(BuildContext context, String content) { showPopUp(context: context, builder: (_) => InformationPage(information: content)); } diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 22dec10127..6421af3f3b 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "امسح بصمة إصبعك للمصادقة", "bitcoin_dark_theme": "موضوع البيتكوين الظلام", "bitcoin_light_theme": "موضوع البيتكوين الخفيفة", + "bitcoin_lightning_deposit": "إيداع", + "bitcoin_lightning_withdraw": "ينسحب", "bitcoin_payments_require_1_confirmation": "تتطلب مدفوعات Bitcoin تأكيدًا واحدًا ، والذي قد يستغرق 20 دقيقة أو أكثر. شكرا لصبرك! سيتم إرسال بريد إلكتروني إليك عند تأكيد الدفع.", "block_height": "ارتفاع كتلة", "block_remaining": "1 كتلة متبقية", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 5ccf69bd9b..8bbf2603c5 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "Сканирайте своя пръстов отпечатък", "bitcoin_dark_theme": "Тъмна тема за биткойн", "bitcoin_light_theme": "Лека биткойн тема", + "bitcoin_lightning_deposit": "Депозит", + "bitcoin_lightning_withdraw": "Оттегляне", "bitcoin_payments_require_1_confirmation": "Плащанията с Bitcoin изискват потвърждение, което може да отнеме 20 минути или повече. Благодарим за търпението! Ще получите имейл, когато плащането е потвърдено.", "block_height": "Височина на блока", "block_remaining": "1 блок останал", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index d34969614c..a0be8ad828 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "Naskenujte otisk prstu pro ověření", "bitcoin_dark_theme": "Tmavé téma bitcoinů", "bitcoin_light_theme": "Světlé téma bitcoinů", + "bitcoin_lightning_deposit": "Vklad", + "bitcoin_lightning_withdraw": "Odebrat", "bitcoin_payments_require_1_confirmation": "U plateb Bitcoinem je vyžadováno alespoň 1 potvrzení, což může trvat 20 minut i déle. Děkujeme za vaši trpělivost! Až bude platba potvrzena, budete informováni e-mailem.", "block_height": "Výška bloku", "block_remaining": "1 blok zbývající", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index b238504450..91647c2d3b 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "Scannen Sie Ihren Fingerabdruck zur Authentifizierung", "bitcoin_dark_theme": "Dunkles Bitcoin-Thema", "bitcoin_light_theme": "Bitcoin Light-Thema", + "bitcoin_lightning_deposit": "Einzahlen", + "bitcoin_lightning_withdraw": "Auszahlen", "bitcoin_payments_require_1_confirmation": "Bitcoin-Zahlungen erfordern 1 Bestätigung, was 20 Minuten oder länger dauern kann. Danke für Ihre Geduld! Sie erhalten eine E-Mail, wenn die Zahlung bestätigt ist.", "block_height": "Blockhöhe", "block_remaining": "1 Block verbleibend", @@ -1162,4 +1164,4 @@ "youCanGoBackToYourDapp": "Sie können jetzt zu Ihrem Dapp zurückkehren", "your": "Dein", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index f436f41ac0..9702c20216 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "Scan your fingerprint to authenticate", "bitcoin_dark_theme": "Bitcoin Dark Theme", "bitcoin_light_theme": "Bitcoin Light Theme", + "bitcoin_lightning_deposit": "Deposit", + "bitcoin_lightning_withdraw": "Withdraw", "bitcoin_payments_require_1_confirmation": "Bitcoin payments require 1 confirmation, which can take 20 minutes or longer. Thanks for your patience! You will be emailed when the payment is confirmed.", "block_height": "Block height", "block_remaining": "1 Block Remaining", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index f3fd8c57d5..c93208f14e 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "Escanee su huella dactilar para autenticarse", "bitcoin_dark_theme": "Tema oscuro de Bitcoin", "bitcoin_light_theme": "Tema claro de Bitcoin", + "bitcoin_lightning_deposit": "Depósito", + "bitcoin_lightning_withdraw": "Retirar", "bitcoin_payments_require_1_confirmation": "Los pagos de Bitcoin requieren 1 confirmación, que puede tardar 20 minutos o más. ¡Gracias por tu paciencia! Recibirás un correo electrónico cuando se confirme el pago.", "block_height": "Altura del bloque", "block_remaining": "1 bloque restante", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 67c62b7a48..76508d39d9 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "Scannez votre empreinte digitale pour vous authentifier", "bitcoin_dark_theme": "Thème sombre Bitcoin", "bitcoin_light_theme": "Thème clair Bitcoin", + "bitcoin_lightning_deposit": "Dépôt", + "bitcoin_lightning_withdraw": "Retirer", "bitcoin_payments_require_1_confirmation": "Les paiements Bitcoin nécessitent 1 confirmation, ce qui peut prendre 20 minutes ou plus. Merci pour votre patience ! Vous serez averti par e-mail lorsque le paiement sera confirmé.", "block_height": "Hauteur de bloc", "block_remaining": "1 bloc restant", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 5058e465a3..f9593dd0e3 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "Duba hoton yatsa don tantancewa", "bitcoin_dark_theme": "Bitcoin Dark Jigo", "bitcoin_light_theme": "Jigon Hasken Bitcoin", + "bitcoin_lightning_deposit": "Yi ajiya", + "bitcoin_lightning_withdraw": "Janye", "bitcoin_payments_require_1_confirmation": "Akwatin Bitcoin na buɗe 1 sambumbu, da yake za ta samu mintuna 20 ko yawa. Ina kira ga sabuwar lafiya! Zaka sanarwa ta email lokacin da aka samu akwatin samun lambar waya.", "block_height": "Toshe tsawo", "block_remaining": "1 toshe ragowar", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 625b8e1b55..9a81ce8bee 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "प्रमाणित करने के लिए अपने फ़िंगरप्रिंट को स्कैन करें", "bitcoin_dark_theme": "बिटकॉइन डार्क थीम", "bitcoin_light_theme": "बिटकॉइन लाइट थीम", + "bitcoin_lightning_deposit": "जमा", + "bitcoin_lightning_withdraw": "निकालना", "bitcoin_payments_require_1_confirmation": "बिटकॉइन भुगतान के लिए 1 पुष्टिकरण की आवश्यकता होती है, जिसमें 20 मिनट या अधिक समय लग सकता है। आपके धैर्य के लिए धन्यवाद! भुगतान की पुष्टि होने पर आपको ईमेल किया जाएगा।", "block_height": "ब्लॉक ऊंचाई", "block_remaining": "1 ब्लॉक शेष", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 726e00bad1..18192d1af6 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "Skenirajte svoj otisak prsta za autentifikaciju", "bitcoin_dark_theme": "Bitcoin Tamna tema", "bitcoin_light_theme": "Bitcoin Light Theme", + "bitcoin_lightning_deposit": "Polog", + "bitcoin_lightning_withdraw": "Povući", "bitcoin_payments_require_1_confirmation": "Bitcoin plaćanja zahtijevaju 1 potvrdu, što može potrajati 20 minuta ili dulje. Hvala na Vašem strpljenju! Dobit ćete e-poruku kada plaćanje bude potvrđeno.", "block_height": "Visina bloka", "block_remaining": "Preostalo 1 blok", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index b750b1e61a..c49e612709 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "Սկանավեք ձեր մատնահետքը նույնականացման համար", "bitcoin_dark_theme": "Bitcoin մութ տեսք", "bitcoin_light_theme": "Bitcoin պայծառ տեսք", + "bitcoin_lightning_deposit": "Ավանդ", + "bitcoin_lightning_withdraw": "Հանել", "bitcoin_payments_require_1_confirmation": "Bitcoin վճարումները պահանջում են 1 հաստատում, որը կարող է տևել 20 րոպե կամ ավելի: Շնորհակալություն ձեր համբերության համար: Դուք էլ. նամակ կստանաք, երբ վճարումը հաստատվի։", "block_height": "Բլոկի բարձրությունը", "block_remaining": "1 Բլոկ է մնացել", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 12489e80ac..cb02ce7d28 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "Pindai sidik jari Anda untuk mengautentikasi", "bitcoin_dark_theme": "Tema Gelap Bitcoin", "bitcoin_light_theme": "Tema Cahaya Bitcoin", + "bitcoin_lightning_deposit": "Deposito", + "bitcoin_lightning_withdraw": "Menarik", "bitcoin_payments_require_1_confirmation": "Pembayaran Bitcoin memerlukan 1 konfirmasi, yang bisa memakan waktu 20 menit atau lebih. Terima kasih atas kesabaran Anda! Anda akan diemail saat pembayaran dikonfirmasi.", "block_height": "Tinggi blok", "block_remaining": "1 blok tersisa", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 55f676cf39..cb95a595b0 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "Scansiona la tua impronta per autenticarti", "bitcoin_dark_theme": "Tema scuro Bitcoin", "bitcoin_light_theme": "Tema chiaro Bitcoin", + "bitcoin_lightning_deposit": "Depositare", + "bitcoin_lightning_withdraw": "Ritirare", "bitcoin_payments_require_1_confirmation": "I pagamenti in bitcoin richiedono 1 conferma, che può richiedere 20 minuti o più. Grazie per la vostra pazienza! Riceverai un'e-mail quando il pagamento sarà confermato.", "block_height": "Altezza del blocco", "block_remaining": "1 blocco rimanente", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 261943d259..41d34a7edb 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "प指紋をスキャンして認証する", "bitcoin_dark_theme": "ビットコインダークテーマ", "bitcoin_light_theme": "ビットコインライトテーマ", + "bitcoin_lightning_deposit": "デポジット", + "bitcoin_lightning_withdraw": "撤回する", "bitcoin_payments_require_1_confirmation": "ビットコインの支払いには 1 回の確認が必要で、これには 20 分以上かかる場合があります。お待ち頂きまして、ありがとうございます!支払いが確認されると、メールが送信されます。", "block_height": "ブロックの高さ", "block_remaining": "残り1ブロック", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index f2f4cd5979..a281f0aafb 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "인증하려면 지문을 스캔하세요", "bitcoin_dark_theme": "비트코인 다크 테마", "bitcoin_light_theme": "비트코인 라이트 테마", + "bitcoin_lightning_deposit": "보증금", + "bitcoin_lightning_withdraw": "철회하다", "bitcoin_payments_require_1_confirmation": "비트코인 결제는 1번의 확인이 필요하며, 이는 20분 이상 소요될 수 있습니다. 기다려 주셔서 감사합니다! 결제가 확인되면 이메일로 알려드립니다.", "block_height": "블록 높이", "block_remaining": "1 블록 남음", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 39eabdfbc0..d1c539a177 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "စစ်မှန်ကြောင်းအထောက်အထားပြရန် သင့်လက်ဗွေကို စကန်ဖတ်ပါ။", "bitcoin_dark_theme": "Bitcoin Dark Theme", "bitcoin_light_theme": "Bitcoin Light အပြင်အဆင်", + "bitcoin_lightning_deposit": "အပ်ငေှ", + "bitcoin_lightning_withdraw": "ဆုတ်ခွာ", "bitcoin_payments_require_1_confirmation": "Bitcoin ငွေပေးချေမှုများသည် မိနစ် 20 သို့မဟုတ် ထို့ထက်ပိုကြာနိုင်သည် 1 အတည်ပြုချက် လိုအပ်သည်။ မင်းရဲ့စိတ်ရှည်မှုအတွက် ကျေးဇူးတင်ပါတယ်။ ငွေပေးချေမှုကို အတည်ပြုပြီးသောအခါ သင့်ထံ အီးမေးလ်ပို့ပါမည်။", "block_height": "ပိတ်ပင်တားဆီးမှုအမြင့်", "block_remaining": "ကျန်ရှိနေသေးသော block", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index dd9ad4e3ca..9e26107024 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "Scan uw vingerafdruk om te verifiëren", "bitcoin_dark_theme": "Bitcoin donker thema", "bitcoin_light_theme": "Bitcoin Light-thema", + "bitcoin_lightning_deposit": "Borg", + "bitcoin_lightning_withdraw": "Terugtrekken", "bitcoin_payments_require_1_confirmation": "Bitcoin-betalingen vereisen 1 bevestiging, wat 20 minuten of langer kan duren. Dank voor uw geduld! U ontvangt een e-mail wanneer de betaling is bevestigd.", "block_height": "Blokhoogte", "block_remaining": "1 blok resterend", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 5b27af5109..757da393f1 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "Zeskanuj odcisk palca, aby się uwierzytelnić", "bitcoin_dark_theme": "Ciemny motyw Bitcoin", "bitcoin_light_theme": "Jasny motyw Bitcoin", + "bitcoin_lightning_deposit": "Depozyt", + "bitcoin_lightning_withdraw": "Wycofać", "bitcoin_payments_require_1_confirmation": "Płatności Bitcoin wymagają jednego potwierdzenia, co może zająć 20 minut lub dłużej. Dziękujemy za cierpliwość! Otrzymasz e‑mail, gdy płatność zostanie potwierdzona.", "block_height": "Wysokość bloku", "block_remaining": "Pozostał 1 blok", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index f8831534e7..93611fddb5 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "Digitalize sua impressão digital para autenticar", "bitcoin_dark_theme": "Tema escuro Bitcoin", "bitcoin_light_theme": "Tema claro de bitcoin", + "bitcoin_lightning_deposit": "Depósito", + "bitcoin_lightning_withdraw": "Retirar", "bitcoin_payments_require_1_confirmation": "Os pagamentos em Bitcoin exigem 1 confirmação, o que pode levar 20 minutos ou mais. Obrigado pela sua paciência! Você receberá um e-mail quando o pagamento for confirmado.", "block_height": "Altura do bloco", "block_remaining": "1 bloco restante", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index de31f95f46..425a9b28f7 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "Отсканируйте свой отпечаток пальца для аутентификации", "bitcoin_dark_theme": "Биткойн Темная тема", "bitcoin_light_theme": "Светлая биткойн-тема", + "bitcoin_lightning_deposit": "Депозит", + "bitcoin_lightning_withdraw": "Отзывать", "bitcoin_payments_require_1_confirmation": "Биткойн-платежи требуют 1 подтверждения, что может занять 20 минут или дольше. Спасибо тебе за твое терпение! Вы получите электронное письмо, когда платеж будет подтвержден.", "block_height": "Высота блока", "block_remaining": "1 Блок остался", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 6b4fcbd559..6457e3ebcd 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "สแกนลายนิ้วมือของคุณเพื่อยืนยันตัวตน", "bitcoin_dark_theme": "ธีมมืด Bitcoin", "bitcoin_light_theme": "ธีมแสง Bitcoin", + "bitcoin_lightning_deposit": "เงินฝาก", + "bitcoin_lightning_withdraw": "ถอน", "bitcoin_payments_require_1_confirmation": "การชำระเงินด้วย Bitcoin ต้องการการยืนยัน 1 ครั้ง ซึ่งอาจใช้เวลา 20 นาทีหรือนานกว่านั้น ขอบคุณสำหรับความอดทนของคุณ! คุณจะได้รับอีเมลเมื่อการชำระเงินได้รับการยืนยัน", "block_height": "ความสูงของบล็อก", "block_remaining": "เหลือ 1 บล็อก", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 31f530b520..f4c7202077 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "I-scan ang iyong fingerprint para ma-authenticate", "bitcoin_dark_theme": "Bitcoin Dark Theme", "bitcoin_light_theme": "Bitcoin Light Theme", + "bitcoin_lightning_deposit": "Deposito", + "bitcoin_lightning_withdraw": "Umatras", "bitcoin_payments_require_1_confirmation": "Ang mga pagbabayad sa Bitcoin ay nangangailangan ng 1 kumpirmasyon, na maaaring tumagal ng 20 minuto o mas mahaba. Salamat sa iyong pasensya! Mag-email ka kapag nakumpirma ang pagbabayad.", "block_height": "I -block ang taas", "block_remaining": "1 Bloke ang Natitira", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 1236c3c83e..b290c0030e 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "Kimlik doğrulaması için parmak izini okutun", "bitcoin_dark_theme": "Bitcoin Karanlık Teması", "bitcoin_light_theme": "Bitcoin Hafif Tema", + "bitcoin_lightning_deposit": "Mevduat", + "bitcoin_lightning_withdraw": "Geri çekilmek", "bitcoin_payments_require_1_confirmation": "Bitcoin ödemeleri, 20 dakika veya daha uzun sürebilen 1 onay gerektirir. Sabrınız için teşekkürler! Ödeme onaylandığında e-posta ile bilgilendirileceksiniz.", "block_height": "Blok yüksekliği", "block_remaining": "Kalan 1 blok", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index cabc4a937c..bab1edf2d8 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "Відскануйте свій відбиток пальця для аутентифікації", "bitcoin_dark_theme": "Темна тема Bitcoin", "bitcoin_light_theme": "Світла тема Bitcoin", + "bitcoin_lightning_deposit": "депозит", + "bitcoin_lightning_withdraw": "Вилучити", "bitcoin_payments_require_1_confirmation": "Платежі Bitcoin потребують 1 підтвердження, яке може зайняти 20 хвилин або більше. Дякую за Ваше терпіння! Ви отримаєте електронний лист, коли платіж буде підтверджено.", "block_height": "Висота блоку", "block_remaining": "1 блок, що залишився", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 8f361ae6a0..fd3240d4a7 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "تصدیق کرنے کے لیے اپنے فنگر پرنٹ کو اسکین کریں۔", "bitcoin_dark_theme": "بٹ کوائن ڈارک تھیم", "bitcoin_light_theme": "بٹ کوائن لائٹ تھیم", + "bitcoin_lightning_deposit": "جمع کروائیں", + "bitcoin_lightning_withdraw": "واپس لے لو", "bitcoin_payments_require_1_confirmation": "بٹ کوائن کی ادائیگی میں 1 تصدیق کی ضرورت ہوتی ہے ، جس میں 20 منٹ یا اس سے زیادہ وقت لگ سکتا ہے۔ آپ کے صبر کا شکریہ! ادائیگی کی تصدیق ہونے پر آپ کو ای میل کیا جائے گا۔", "block_height": "اونچائی کو بلاک کریں", "block_remaining": "1 بلاک باقی", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index be4d2a4010..b93e252ed4 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "Quét vân tay để xác thực", "bitcoin_dark_theme": "Chủ đề Bitcoin tối", "bitcoin_light_theme": "Chủ đề Bitcoin sáng", + "bitcoin_lightning_deposit": "Tiền gửi", + "bitcoin_lightning_withdraw": "Rút", "bitcoin_payments_require_1_confirmation": "Các khoản thanh toán Bitcoin yêu cầu 1 xác nhận, có thể mất 20 phút hoặc lâu hơn. Cảm ơn bạn đã kiên nhẫn! Bạn sẽ nhận được email khi thanh toán được xác nhận.", "block_height": "Chiều cao khối", "block_remaining": "1 khối còn lại", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 7f2a603479..ff53fd283b 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "Ya ìka ọwọ́ yín láti ṣe ìfẹ̀rílàdí", "bitcoin_dark_theme": "Bitcoin Dark Akori", "bitcoin_light_theme": "Bitcoin Light Akori", + "bitcoin_lightning_deposit": "Owo ifipamọ", + "bitcoin_lightning_withdraw": "Yọkuro", "bitcoin_payments_require_1_confirmation": "Àwọn àránṣẹ́ Bitcoin nílò ìjẹ́rìísí kan. Ó lè lo ìṣéjú ogun tàbí ìṣéjú jù. A dúpẹ́ fún sùúrù yín! Ẹ máa gba ímeèlì t'ó bá jẹ́rìísí àránṣẹ́ náà.", "block_height": "Dènà giga", "block_remaining": "1 bulọọki to ku", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index cee2aa801c..a08d072f8b 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -95,6 +95,8 @@ "biometric_auth_reason": "扫描指纹进行身份认证", "bitcoin_dark_theme": "比特币黑暗主题", "bitcoin_light_theme": "比特币浅色主题", + "bitcoin_lightning_deposit": "订金", + "bitcoin_lightning_withdraw": "提取", "bitcoin_payments_require_1_confirmation": "比特币支付需要 1 次确认,这可能需要 20 分钟或更长时间。谢谢你的耐心!确认付款后,您将收到电子邮件。", "block_height": "块高度", "block_remaining": "剩下1个块", From 37bf40d118727f39c04fb7c24e503607825954e0 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Wed, 5 Nov 2025 16:16:37 +0100 Subject: [PATCH 17/68] feat: reload balance and tx history after sending a lightning transaction --- assets/images/lightning-icon.svg | 46 +++++++++++++++++++ .../lib/lightning/lightning_wallet.dart | 8 ++-- .../pending_lightning_transaction.dart | 8 +++- cw_bitcoin/pubspec.yaml | 2 +- lib/view_model/send/send_view_model.dart | 5 +- 5 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 assets/images/lightning-icon.svg diff --git a/assets/images/lightning-icon.svg b/assets/images/lightning-icon.svg new file mode 100644 index 0000000000..aa4d3a9225 --- /dev/null +++ b/assets/images/lightning-icon.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cw_bitcoin/lib/lightning/lightning_wallet.dart b/cw_bitcoin/lib/lightning/lightning_wallet.dart index 4d44dc3b58..087bfa1079 100644 --- a/cw_bitcoin/lib/lightning/lightning_wallet.dart +++ b/cw_bitcoin/lib/lightning/lightning_wallet.dart @@ -2,7 +2,6 @@ import 'package:breez_sdk_spark_flutter/breez_sdk_spark.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/lightning/pending_lightning_transaction.dart'; -import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; @@ -81,7 +80,7 @@ class LightningWallet { } } - Future createTransaction( + Future createTransaction( String address, BigInt? amountSats, BitcoinTransactionPriority? priority) async { final inputType = await sdk.parse(input: address); @@ -120,10 +119,11 @@ class LightningWallet { return PendingLightningTransaction( id: prepareResponse.invoiceDetails.paymentHash, - amount: prepareResponse.invoiceDetails.amountMsat?.toInt() ?? 0, + amount: ((prepareResponse.invoiceDetails.amountMsat?.toInt() ?? 0) / 1000).round(), fee: feeSats.toInt(), commitOverride: () async { - await sdk.lnurlPay(request: LnurlPayRequest(prepareResponse: prepareResponse)); + final res = await sdk.lnurlPay(request: LnurlPayRequest(prepareResponse: prepareResponse)); + printV(res.payment.status.name); }, ); } else if (inputType is InputType_BitcoinAddress) { diff --git a/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart b/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart index 72b4423783..cb75b7d2b3 100644 --- a/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart +++ b/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart @@ -14,6 +14,7 @@ class PendingLightningTransaction with PendingTransaction { final int fee; final bool isSendAll; Future Function() commitOverride; + final List _listeners =[]; @override final String id; @@ -34,11 +35,16 @@ class PendingLightningTransaction with PendingTransaction { int? get outputCount => 1; @override - Future commit() => commitOverride.call(); + Future commit() async { + await commitOverride.call(); + _listeners.forEach((e) => e.call()); + } @override bool shouldCommitUR() => false; @override Future> commitUR() => throw UnimplementedError(); + + void addListener(void Function() listener) => _listeners.add(listener); } diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 439492bda0..ca6f75f0fa 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -75,7 +75,7 @@ dependencies: breez_sdk_spark_flutter: git: url: https://github.com/breez/breez-sdk-spark-flutter - ref: 92f62dc2037cf08003e418aadda58f451c021f42 + ref: bca05bc9085f778e95916d55e9a75133c27755a2 dev_dependencies: flutter_test: diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 4090cb59e9..217d7cf45d 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -57,7 +57,6 @@ import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; @@ -807,9 +806,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor // Immediate transaction update for EVM chains, Solana, Tron, and Nano if (isEVMWallet || - [WalletType.solana, WalletType.tron, WalletType.nano].contains(walletType)) { + [WalletType.bitcoin, WalletType.solana, WalletType.tron, WalletType.nano] + .contains(walletType)) { Future.delayed(Duration(seconds: 4), () async { try { + await wallet.updateBalance(); await wallet.updateTransactionsHistory(); } catch (e) { printV('Failed to update transactions after send: $e'); From fda146c7f5a65128e3bb56af0d62d60ea2749c28 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Thu, 6 Nov 2025 09:53:07 +0100 Subject: [PATCH 18/68] feat: improve address formatting for human-readable addresses and update the default LNURL domain --- assets/images/btc_chain_qr_lightning.svg | 5 +++++ cw_bitcoin/lib/bitcoin_wallet.dart | 2 +- cw_bitcoin/lib/electrum_wallet_addresses.dart | 12 ++++++++++-- lib/utils/address_formatter.dart | 7 ++++++- .../wallet_address_list_view_model.dart | 5 ++++- 5 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 assets/images/btc_chain_qr_lightning.svg diff --git a/assets/images/btc_chain_qr_lightning.svg b/assets/images/btc_chain_qr_lightning.svg new file mode 100644 index 0000000000..b18ac0b9f8 --- /dev/null +++ b/assets/images/btc_chain_qr_lightning.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 304618e714..3b903897c1 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -99,7 +99,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { lightningWallet = LightningWallet( mnemonic: mnemonic, apiKey: secrets.breezApiKey, - lnurlDomain: "breez.tips", + lnurlDomain: "cake.cash", ); } else { lightningWallet = null; diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 7ef455793f..03b32bf4e7 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -1,4 +1,5 @@ import 'dart:io' show Platform; +import 'dart:math'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; @@ -761,8 +762,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { lightningAddress = await lightningWallet!.getAddress(); if (lightningAddress == null) { - lightningAddress = - await lightningWallet!.registerAddress(walletName.replaceAll(" ", "").toLowerCase()); + final randomNumber = Random.secure().nextInt(9999); + final username = "${walletName.replaceAll(" ", "")}$randomNumber".toLowerCase(); + try { + lightningAddress = await lightningWallet!.registerAddress(username); + } catch (e) { + printV(e); + printV(username); + rethrow; + } } } } diff --git a/lib/utils/address_formatter.dart b/lib/utils/address_formatter.dart index f2083c7724..bd46985828 100644 --- a/lib/utils/address_formatter.dart +++ b/lib/utils/address_formatter.dart @@ -15,6 +15,11 @@ class AddressFormatter { final cleanAddress = address.replaceAll('bitcoincash:', ''); final isMWEB = address.startsWith('ltcmweb'); final chunkSize = walletType != null ? _getChunkSize(walletType) : 4; + final isHumanReadable = address.contains("@"); + + if (isHumanReadable) { + return Text(address, style: evenTextStyle, textAlign: textAlign ?? TextAlign.start); + } if (shouldTruncate) { return _buildTruncatedAddress( @@ -158,4 +163,4 @@ class AddressFormatter { return 4; } } -} \ No newline at end of file +} diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index db7111017a..d4d2a69add 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -435,7 +435,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo } @computed - String get qrImage => getQrImage(type); + String get qrImage { + if (uri is LightningPaymentRequest) return 'assets/images/btc_chain_qr_lightning.svg'; + return getQrImage(type); + } @computed String get monoImage => getChainMonoImage(type); From 0ee2d9f558dd23b80ed019fc90e4974ee75ffc3f Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Thu, 6 Nov 2025 10:34:46 +0100 Subject: [PATCH 19/68] fix: merge conflicts --- cw_bitcoin/pubspec.lock | 6 +++--- lib/view_model/dashboard/transaction_list_item.dart | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index e281c7f7b4..4bbd0c3dea 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -134,11 +134,11 @@ packages: dependency: "direct main" description: path: "." - ref: "92f62dc2037cf08003e418aadda58f451c021f42" - resolved-ref: "92f62dc2037cf08003e418aadda58f451c021f42" + ref: bca05bc9085f778e95916d55e9a75133c27755a2 + resolved-ref: bca05bc9085f778e95916d55e9a75133c27755a2 url: "https://github.com/breez/breez-sdk-spark-flutter" source: git - version: "0.3.4" + version: "0.3.5-rc1" bs58check: dependency: transitive description: diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 8336a2374c..27c2ed1ab1 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -206,7 +206,7 @@ class TransactionListItem extends ActionListItem with Keyable { break; case WalletType.arbitrum: final asset = arbitrum!.assetOfTransaction(balanceViewModel.wallet, transaction); - final price = balanceViewModel.fiatConvertationStore.prices[asset]; + final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( cryptoAmount: arbitrum!.formatterArbitrumAmountToDouble(transaction: transaction), price: price); From c28e513d1b135f1071ad3d21035620a6f858424f Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Fri, 7 Nov 2025 14:42:52 +0100 Subject: [PATCH 20/68] feat: add error handling for LightningWallet initialization and adjust transaction direction logic --- cw_bitcoin/lib/bitcoin_wallet.dart | 15 ++++++++++----- cw_bitcoin/lib/lightning/lightning_wallet.dart | 6 ++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 3b903897c1..b1f7861f1e 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -31,6 +31,7 @@ import 'package:cw_core/payjoin_session.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/utils/zpub.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; @@ -96,11 +97,15 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { // final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType); if (mnemonic != null) { - lightningWallet = LightningWallet( - mnemonic: mnemonic, - apiKey: secrets.breezApiKey, - lnurlDomain: "cake.cash", - ); + try { + lightningWallet = LightningWallet( + mnemonic: mnemonic, + apiKey: secrets.breezApiKey, + lnurlDomain: "cake.cash", + ); + } catch (e) { + printV(e); + } } else { lightningWallet = null; } diff --git a/cw_bitcoin/lib/lightning/lightning_wallet.dart b/cw_bitcoin/lib/lightning/lightning_wallet.dart index 087bfa1079..315d7406dd 100644 --- a/cw_bitcoin/lib/lightning/lightning_wallet.dart +++ b/cw_bitcoin/lib/lightning/lightning_wallet.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:breez_sdk_spark_flutter/breez_sdk_spark.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; @@ -187,6 +189,10 @@ class LightningWallet { for (final payment in payments) { TransactionDirection direction = TransactionDirection.outgoing; + if (payment.paymentType == PaymentType.receive) { + direction = TransactionDirection.incoming; + } + if (payment.method == PaymentMethod.deposit) { direction = TransactionDirection.incoming; } From 1fea4061db7f2b37583e01d568353191adaa3028 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Sun, 16 Nov 2025 11:34:10 +0100 Subject: [PATCH 21/68] integrate homepage from new ui mockup --- .../WixMadeforText-VariableFont_wght.ttf | Bin 0 -> 152580 bytes assets/new-ui/3dots.svg | 3 + assets/new-ui/Apps.svg | 21 ++ assets/new-ui/Charts.svg | 10 + assets/new-ui/Contacts.svg | 3 + assets/new-ui/Home.svg | 16 + assets/new-ui/Wallets.svg | 3 + assets/new-ui/addr-book.svg | 3 + assets/new-ui/bitcoin.svg | 13 + assets/new-ui/btcqr.png | Bin 0 -> 188035 bytes assets/new-ui/copy-icon.svg | 11 + assets/new-ui/exchange.svg | 11 + assets/new-ui/history-received.svg | 3 + assets/new-ui/history-receiving.svg | 3 + assets/new-ui/history-sending.svg | 3 + assets/new-ui/history-sent.svg | 3 + assets/new-ui/lightning.svg | 14 + assets/new-ui/receive.svg | 10 + assets/new-ui/scan.svg | 10 + assets/new-ui/send.svg | 3 + assets/new-ui/settings.png | Bin 0 -> 1249 bytes assets/new-ui/switcher-bitcoin-off.svg | 3 + assets/new-ui/switcher-bitcoin.svg | 4 + assets/new-ui/switcher-lightning-off.svg | 3 + assets/new-ui/switcher-lightning.svg | 4 + assets/new-ui/top-settings.svg | 5 + assets/new-ui/wallet-trezor.svg | 10 + cw_core/lib/payment_uris.dart | 8 +- lib/di.dart | 19 +- lib/entities/new_main_actions.dart | 11 +- lib/new-ui/new_dashboard.dart | 100 ++++++ lib/new-ui/pages/receive_page.dart | 66 ++++ lib/new-ui/pages/scan_page.dart | 10 + lib/new-ui/pages/send_page.dart | 10 + .../action_row/coin_action_button.dart | 56 ++++ .../action_row/coin_action_row.dart | 65 ++++ .../coins_page/assets_history/asset_tile.dart | 74 +++++ .../assets_history/assets_section.dart | 23 ++ .../assets_history/assets_top_bar.dart | 64 ++++ .../assets_history/history_section.dart | 55 ++++ .../assets_history/history_tile.dart | 109 +++++++ .../assets_history/lightning_assets.dart | 39 +++ .../coins_page/cards/balance_card.dart | 127 ++++++++ .../widgets/coins_page/cards/cards_view.dart | 138 +++++++++ lib/new-ui/widgets/coins_page/top_bar.dart | 78 +++++ .../widgets/coins_page/wallet_info.dart | 50 +++ lib/new-ui/widgets/line_tab_switcher.dart | 134 ++++++++ lib/new-ui/widgets/modern_button.dart | 62 ++++ lib/new-ui/widgets/navbar/navbar.dart | 68 +++++ lib/new-ui/widgets/navbar/navbar_button.dart | 67 ++++ .../receive_page/receive_amount_input.dart | 90 ++++++ .../receive_page/receive_bottom_buttons.dart | 97 ++++++ .../widgets/receive_page/receive_qr_code.dart | 43 +++ .../receive_seed_type_selector.dart | 51 ++++ .../receive_page/receive_seed_widget.dart | 40 +++ .../widgets/receive_page/receive_top_bar.dart | 27 ++ lib/router.dart | 264 ++++++++++------ .../widgets/new_main_navbar_widget.dart | 286 ++++++++---------- lib/typography.dart | 4 +- lib/utils/feature_flag.dart | 4 +- .../exchange/exchange_trade_view_model.dart | 11 - lib/view_model/send/send_view_model.dart | 7 +- 62 files changed, 2237 insertions(+), 292 deletions(-) create mode 100644 assets/fonts/WixMadeforText-VariableFont_wght.ttf create mode 100644 assets/new-ui/3dots.svg create mode 100644 assets/new-ui/Apps.svg create mode 100644 assets/new-ui/Charts.svg create mode 100644 assets/new-ui/Contacts.svg create mode 100644 assets/new-ui/Home.svg create mode 100644 assets/new-ui/Wallets.svg create mode 100644 assets/new-ui/addr-book.svg create mode 100644 assets/new-ui/bitcoin.svg create mode 100644 assets/new-ui/btcqr.png create mode 100644 assets/new-ui/copy-icon.svg create mode 100644 assets/new-ui/exchange.svg create mode 100644 assets/new-ui/history-received.svg create mode 100644 assets/new-ui/history-receiving.svg create mode 100644 assets/new-ui/history-sending.svg create mode 100644 assets/new-ui/history-sent.svg create mode 100644 assets/new-ui/lightning.svg create mode 100644 assets/new-ui/receive.svg create mode 100644 assets/new-ui/scan.svg create mode 100644 assets/new-ui/send.svg create mode 100644 assets/new-ui/settings.png create mode 100644 assets/new-ui/switcher-bitcoin-off.svg create mode 100644 assets/new-ui/switcher-bitcoin.svg create mode 100644 assets/new-ui/switcher-lightning-off.svg create mode 100644 assets/new-ui/switcher-lightning.svg create mode 100644 assets/new-ui/top-settings.svg create mode 100644 assets/new-ui/wallet-trezor.svg create mode 100644 lib/new-ui/new_dashboard.dart create mode 100644 lib/new-ui/pages/receive_page.dart create mode 100644 lib/new-ui/pages/scan_page.dart create mode 100644 lib/new-ui/pages/send_page.dart create mode 100644 lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart create mode 100644 lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart create mode 100644 lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart create mode 100644 lib/new-ui/widgets/coins_page/assets_history/assets_section.dart create mode 100644 lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart create mode 100644 lib/new-ui/widgets/coins_page/assets_history/history_section.dart create mode 100644 lib/new-ui/widgets/coins_page/assets_history/history_tile.dart create mode 100644 lib/new-ui/widgets/coins_page/assets_history/lightning_assets.dart create mode 100644 lib/new-ui/widgets/coins_page/cards/balance_card.dart create mode 100644 lib/new-ui/widgets/coins_page/cards/cards_view.dart create mode 100644 lib/new-ui/widgets/coins_page/top_bar.dart create mode 100644 lib/new-ui/widgets/coins_page/wallet_info.dart create mode 100644 lib/new-ui/widgets/line_tab_switcher.dart create mode 100644 lib/new-ui/widgets/modern_button.dart create mode 100644 lib/new-ui/widgets/navbar/navbar.dart create mode 100644 lib/new-ui/widgets/navbar/navbar_button.dart create mode 100644 lib/new-ui/widgets/receive_page/receive_amount_input.dart create mode 100644 lib/new-ui/widgets/receive_page/receive_bottom_buttons.dart create mode 100644 lib/new-ui/widgets/receive_page/receive_qr_code.dart create mode 100644 lib/new-ui/widgets/receive_page/receive_seed_type_selector.dart create mode 100644 lib/new-ui/widgets/receive_page/receive_seed_widget.dart create mode 100644 lib/new-ui/widgets/receive_page/receive_top_bar.dart diff --git a/assets/fonts/WixMadeforText-VariableFont_wght.ttf b/assets/fonts/WixMadeforText-VariableFont_wght.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5994d24be65d6829efef10c85d95cf116e221782 GIT binary patch literal 152580 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 + + \ No newline at end of file diff --git a/assets/new-ui/Apps.svg b/assets/new-ui/Apps.svg new file mode 100644 index 0000000000..334d800349 --- /dev/null +++ b/assets/new-ui/Apps.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/Charts.svg b/assets/new-ui/Charts.svg new file mode 100644 index 0000000000..cc86078e64 --- /dev/null +++ b/assets/new-ui/Charts.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/new-ui/Contacts.svg b/assets/new-ui/Contacts.svg new file mode 100644 index 0000000000..a0b0f28945 --- /dev/null +++ b/assets/new-ui/Contacts.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/Home.svg b/assets/new-ui/Home.svg new file mode 100644 index 0000000000..31a55b115b --- /dev/null +++ b/assets/new-ui/Home.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/Wallets.svg b/assets/new-ui/Wallets.svg new file mode 100644 index 0000000000..04c91d0e87 --- /dev/null +++ b/assets/new-ui/Wallets.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/addr-book.svg b/assets/new-ui/addr-book.svg new file mode 100644 index 0000000000..c25d3b7dbc --- /dev/null +++ b/assets/new-ui/addr-book.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/bitcoin.svg b/assets/new-ui/bitcoin.svg new file mode 100644 index 0000000000..391f3c289e --- /dev/null +++ b/assets/new-ui/bitcoin.svg @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/assets/new-ui/btcqr.png b/assets/new-ui/btcqr.png new file mode 100644 index 0000000000000000000000000000000000000000..a5c34ed1f5271059157fcf2f59bd435c077bd482 GIT binary patch literal 188035 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 literal 0 HcmV?d00001 diff --git a/assets/new-ui/copy-icon.svg b/assets/new-ui/copy-icon.svg new file mode 100644 index 0000000000..3cc98258d7 --- /dev/null +++ b/assets/new-ui/copy-icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/new-ui/exchange.svg b/assets/new-ui/exchange.svg new file mode 100644 index 0000000000..0bf048de8e --- /dev/null +++ b/assets/new-ui/exchange.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/new-ui/history-received.svg b/assets/new-ui/history-received.svg new file mode 100644 index 0000000000..3dab9e7312 --- /dev/null +++ b/assets/new-ui/history-received.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/history-receiving.svg b/assets/new-ui/history-receiving.svg new file mode 100644 index 0000000000..cec2958dc6 --- /dev/null +++ b/assets/new-ui/history-receiving.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/history-sending.svg b/assets/new-ui/history-sending.svg new file mode 100644 index 0000000000..f40ec89a0c --- /dev/null +++ b/assets/new-ui/history-sending.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/history-sent.svg b/assets/new-ui/history-sent.svg new file mode 100644 index 0000000000..649e03d087 --- /dev/null +++ b/assets/new-ui/history-sent.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/lightning.svg b/assets/new-ui/lightning.svg new file mode 100644 index 0000000000..9788dcc17e --- /dev/null +++ b/assets/new-ui/lightning.svg @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/assets/new-ui/receive.svg b/assets/new-ui/receive.svg new file mode 100644 index 0000000000..fc420ad694 --- /dev/null +++ b/assets/new-ui/receive.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/new-ui/scan.svg b/assets/new-ui/scan.svg new file mode 100644 index 0000000000..b3e6a3258b --- /dev/null +++ b/assets/new-ui/scan.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/new-ui/send.svg b/assets/new-ui/send.svg new file mode 100644 index 0000000000..cde7609f63 --- /dev/null +++ b/assets/new-ui/send.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/settings.png b/assets/new-ui/settings.png new file mode 100644 index 0000000000000000000000000000000000000000..b6cfb5b0884b7eea4d95b17850aa9c71f52da7b7 GIT binary patch literal 1249 zcmV<71Rnc|P)V$O*f<+AU@x+=XO*e|vAuHHn;D-V;}dwYa@iZ? z6Tl5JB&iC_kDBiZK?$bS-D;SaXJ9Oq35cKuNGTtLkSVn$1%wh}A%sjB2nvM#?#sF9X!N&>Y}9DnYOmcL zQUKI!0{cf7x2I9m#m_mZ)WMUDrBj{UAG*%i=JYSqqym8I2xemWK(^>CVQ| zo^38+Q$CUMToK*QUG7Io(r#}qzs@h5j7D9^HOT=&sU9|?lb_@H@nsXr?C00Q?~cs~ zQBrxzPmY^o{N2ZC3+HoG5^M;3;tb(+t0;Qej1WG`e`}bG(7v=)Ql5cb-hFm_xu+YO zF<}4XYN1NX=kQQ>amIl5`eF*st|ZX|7kQ1bz@osb~cv>IP|F^ zfDi0-3KCyoA{!&t*QH#cvf)vwfk4%Xq!uS4ij#N$->TK?Hx?oXkVt06GFx%24WS<$#T_Y2{)``Gc` ze~rdnoAL~nK}?^A0SDv&Da49t+?a`{QobS!Sdph2OKtK1%7-2mgvT6)DE{V)ioY2H z(t--b7V7pbTW!Oy4eus-BCTM@WA5N=C3asW$zkorjrdDLQUkb4;zlE_)lD7M7vRV1 z^4gfdYs2Bsop0a(p;VkC$r(pWVeDZI_}bmi!&e73Mta>y#hK9u*wF#`0J?j(lpO0R z5b!jw$rIiNOvd9k6bNXLV)BHy0rH0*o%&xVL{aHx=#w7g2{jo7V;3gV>9;pP2Bb&e zPj|=@l02PJSyFsCEo6&4pg#2Ep9-v|;EY&8F=WTptn~Iq=|qN*i_MEsIQBV-&Xf|n zNbfht<2#8`T!orx5}v4DU;P@A3Kj1wYYWRXjFi1M_4;*3S4!xRF>PGTnH>z3C>Uq2 z4h|Xk0LpFIpjE z{SyfM87KsplF?n)8lVhL^Sw}tTHM}P>Wd1|i_FX+&vbNSg--l*bGc_U0yT-`u!u}d zei-6OAK2zFDW8p-kvg=L-H+!w2JSa-NQm_uwVXaN0sVg)jkSDT!*kS-NAirFP5Dg5 zjx;AAb8;WdEU}JgEmKvR=$PNJVa!QoCB&g^E++$A@V!2>2K&&RD&*1}!FNI_&j|XZ zo*6T%40;F(h@b{A-Di`50YN{U1oMH9fWyDULErG>^?_rqQb4>1zMu?PHargg00000 LNkvXXu0mjfc7{ks literal 0 HcmV?d00001 diff --git a/assets/new-ui/switcher-bitcoin-off.svg b/assets/new-ui/switcher-bitcoin-off.svg new file mode 100644 index 0000000000..d529c77e28 --- /dev/null +++ b/assets/new-ui/switcher-bitcoin-off.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/switcher-bitcoin.svg b/assets/new-ui/switcher-bitcoin.svg new file mode 100644 index 0000000000..1fadb6eb10 --- /dev/null +++ b/assets/new-ui/switcher-bitcoin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/new-ui/switcher-lightning-off.svg b/assets/new-ui/switcher-lightning-off.svg new file mode 100644 index 0000000000..d72c681b09 --- /dev/null +++ b/assets/new-ui/switcher-lightning-off.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/switcher-lightning.svg b/assets/new-ui/switcher-lightning.svg new file mode 100644 index 0000000000..f9c5f40a1f --- /dev/null +++ b/assets/new-ui/switcher-lightning.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/new-ui/top-settings.svg b/assets/new-ui/top-settings.svg new file mode 100644 index 0000000000..ba716f8d5f --- /dev/null +++ b/assets/new-ui/top-settings.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/new-ui/wallet-trezor.svg b/assets/new-ui/wallet-trezor.svg new file mode 100644 index 0000000000..d0747c4444 --- /dev/null +++ b/assets/new-ui/wallet-trezor.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/cw_core/lib/payment_uris.dart b/cw_core/lib/payment_uris.dart index ed172ca044..6a6a60b362 100644 --- a/cw_core/lib/payment_uris.dart +++ b/cw_core/lib/payment_uris.dart @@ -67,7 +67,7 @@ class LightningPaymentRequest extends PaymentURI { } class LitecoinURI extends PaymentURI { - LitecoinURI({required super.amount, required super.address}); + LitecoinURI({required super.amount, required super.address, required super.scheme}); @override String toString() { @@ -79,7 +79,7 @@ class LitecoinURI extends PaymentURI { } class EthereumURI extends PaymentURI { - EthereumURI({required super.amount, required super.address}); + EthereumURI({required super.amount, required super.address, required super.scheme}); @override String toString() { @@ -91,7 +91,7 @@ class EthereumURI extends PaymentURI { } class BaseURI extends PaymentURI { - BaseURI({required super.amount, required super.address}); + BaseURI({required super.amount, required super.address, required super.scheme}); @override String toString() { @@ -103,7 +103,7 @@ class BaseURI extends PaymentURI { } class ArbitrumURI extends PaymentURI { - ArbitrumURI({required super.amount, required super.address}); + ArbitrumURI({required super.amount, required super.address, required super.scheme}); @override String toString() { diff --git a/lib/di.dart b/lib/di.dart index 4138842595..5d970b06bb 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -11,6 +11,7 @@ import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart'; import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; +import 'package:cake_wallet/new-ui/new_dashboard.dart'; import 'package:cake_wallet/order/order.dart'; import 'package:cake_wallet/core/backup_service_v3.dart'; import 'package:cake_wallet/core/new_wallet_arguments.dart'; @@ -748,6 +749,10 @@ Future setup({ addressListViewModel: getIt.get(), )); + getIt.registerFactory(() => NewDashboard( + dashboardViewModel: getIt.get(), + )); + getIt.registerFactory(() { final GlobalKey _navigatorKey = GlobalKey(); return DesktopSidebarWrapper( @@ -1015,7 +1020,7 @@ Future setup({ getIt.registerFactory(() => WalletKeysViewModel(getIt.get())); getIt.registerFactory(() => WalletKeysPage(getIt.get())); - + getIt.registerFactory(() => AnimatedURModel(getIt.get())); getIt.registerFactoryParam, void>((Map urQr, _) => @@ -1581,24 +1586,24 @@ Future setup({ getIt.registerFactory(() => DevSharedPreferencesPage(getIt.get())); getIt.registerFactory(() => DevSecurePreferencesPage(getIt.get())); - + getIt.registerFactory(() => BackgroundSyncLogsViewModel()); - + getIt.registerFactory(() => DevBackgroundSyncLogsPage(getIt.get())); - + getIt.registerFactory(() => SocketHealthLogsViewModel()); getIt.registerFactory(() => DevSocketHealthLogsPage(getIt.get())); - + getIt.registerFactory(() => DevNetworkRequests()); - + getIt.registerFactory(() => DevQRToolsPage()); getIt.registerFactory(() => ExchangeProviderLogsViewModel()); getIt.registerFactory(() => DevExchangeProviderLogsPage(getIt.get())); getIt.registerFactory(() => StartTorPage(StartTorViewModel(),)); - + getIt.registerFactory(() => DEuroViewModel( getIt(), getIt(), diff --git a/lib/entities/new_main_actions.dart b/lib/entities/new_main_actions.dart index 0f904ae2a6..609b04cf4d 100644 --- a/lib/entities/new_main_actions.dart +++ b/lib/entities/new_main_actions.dart @@ -1,5 +1,4 @@ import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:flutter/material.dart'; @@ -31,14 +30,14 @@ class NewMainActions { static NewMainActions homeAction = NewMainActions._( name: (context) => 'Home', //TODO S.of(context).home, - image: 'assets/images/main_actions/home.svg', + image: 'assets/new-ui/Home.svg', key: ValueKey('dashboard_page_home_action_button_key'), onTap: () {}, ); static NewMainActions walletsAction = NewMainActions._( name: (context) => S.of(context).wallets, - image: 'assets/images/main_actions/wallets.svg', + image: 'assets/new-ui/Wallets.svg', key: ValueKey('dashboard_page_wallets_action_button_key'), onTap: () {}, ); @@ -46,21 +45,21 @@ class NewMainActions { static NewMainActions contactsAction = NewMainActions._( name: (context) => 'Contacts', //TODO S.of(context).contacts, - image: 'assets/images/main_actions/contacts.svg', + image: 'assets/new-ui/Contacts.svg', key: ValueKey('dashboard_page_contacts_action_button_key'), onTap: () {}, ); static NewMainActions appsAction = NewMainActions._( name: (context) => 'Apps', //TODO S.of(context).apps, - image: 'assets/images/main_actions/apps.svg', + image: 'assets/new-ui/Apps.svg', key: ValueKey('dashboard_page_apps_action_button_key'), onTap: () {}, ); static NewMainActions chartsAction = NewMainActions._( name: (context) => 'Charts', //TODO S.of(context).charts, - image: 'assets/images/main_actions/charts.svg', + image: 'assets/new-ui/Charts.svg', key: ValueKey('dashboard_page_charts_action_button_key'), onTap: () {}, ); diff --git a/lib/new-ui/new_dashboard.dart b/lib/new-ui/new_dashboard.dart new file mode 100644 index 0000000000..ba8b88fd12 --- /dev/null +++ b/lib/new-ui/new_dashboard.dart @@ -0,0 +1,100 @@ +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/lightning_assets.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/new_main_navbar_widget.dart'; +import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart'; +import 'package:flutter/material.dart'; +import '../view_model/dashboard/dashboard_view_model.dart'; +import 'widgets/coins_page/cards/cards_view.dart'; +import 'widgets/coins_page/action_row/coin_action_row.dart'; +import 'widgets/coins_page/assets_history/history_section.dart'; +import 'widgets/coins_page/top_bar.dart'; +import 'widgets/coins_page/wallet_info.dart'; + +class NewDashboard extends StatefulWidget { + NewDashboard({super.key, required this.dashboardViewModel}) { + this.accountListViewModel = + dashboardViewModel.balanceViewModel.hasAccounts ? getIt.get() : null; + } + + final DashboardViewModel dashboardViewModel; + late final MoneroAccountListViewModel? accountListViewModel; + + + @override + State createState() => _NewDashboardState(); +} + +class _NewDashboardState extends State { + bool _lightningMode = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + SafeArea( + child: Container( + height: MediaQuery.of(context).size.height, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Theme.of(context).colorScheme.surfaceBright, + Theme.of(context).colorScheme.surface, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: SingleChildScrollView( + physics: BouncingScrollPhysics(), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + TopBar( + dashboardViewModel: widget.dashboardViewModel, + lightningMode: _lightningMode, + onLightningSwitchPress: () { + setState(() { + _lightningMode = !_lightningMode; + }); + }, + ), + WalletInfo(lightningMode: _lightningMode, usesHardwareWallet: + widget.dashboardViewModel.wallet.isHardwareWallet, + name: widget.dashboardViewModel.wallet.name + ), + CardsView(dashboardViewModel: widget.dashboardViewModel, + accountListViewModel: widget.accountListViewModel, + lightningMode: _lightningMode, + ), + CoinActionRow(), + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + transitionBuilder: (child, animation) { + return FadeTransition(opacity: animation, child: child); + }, + layoutBuilder: (currentChild, previousChildren) { + return Stack( + alignment: Alignment.topCenter, + children: [ + ...previousChildren, + if (currentChild != null) currentChild, + ], + ); + }, + child: _lightningMode + ? LightningAssets(dashboardViewModel: widget.dashboardViewModel,) + : HistorySection(dashboardViewModel: widget.dashboardViewModel,), + ), + ], + ), + ), + ), + ), + NewMainNavBar(dashboardViewModel: widget.dashboardViewModel) + ], + ), + ); + } +} diff --git a/lib/new-ui/pages/receive_page.dart b/lib/new-ui/pages/receive_page.dart new file mode 100644 index 0000000000..a6a17cb7b6 --- /dev/null +++ b/lib/new-ui/pages/receive_page.dart @@ -0,0 +1,66 @@ +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_amount_input.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_bottom_buttons.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_qr_code.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_seed_type_selector.dart'; +import 'package:flutter/material.dart'; + +import '../widgets/receive_page/receive_seed_widget.dart'; +import '../widgets/receive_page/receive_top_bar.dart'; + +class ReceivePage extends StatefulWidget { + const ReceivePage({super.key}); + + @override + State createState() => _ReceivePageState(); +} + +class _ReceivePageState extends State { + bool _largeQrMode = false; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Theme.of(context).colorScheme.surfaceBright, + Theme.of(context).colorScheme.surface, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + borderRadius: BorderRadius.circular(30), + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox(height: 12), + ReceiveTopBar(), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + mainAxisSize: MainAxisSize.max, + children: [ + ReceiveQrCode( + onTap: () { + setState(() { + _largeQrMode = !_largeQrMode; + }); + }, + largeQrMode: _largeQrMode, + ), + ReceiveSeedTypeSelector(), + ReceiveSeedWidget(), + ReceiveAmountInput(largeQrMode: _largeQrMode), + ReceiveBottomButtons(largeQrMode: _largeQrMode), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/new-ui/pages/scan_page.dart b/lib/new-ui/pages/scan_page.dart new file mode 100644 index 0000000000..75bbddaa37 --- /dev/null +++ b/lib/new-ui/pages/scan_page.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class ScanPage extends StatelessWidget { + const ScanPage({super.key}); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/lib/new-ui/pages/send_page.dart b/lib/new-ui/pages/send_page.dart new file mode 100644 index 0000000000..c53515f2e1 --- /dev/null +++ b/lib/new-ui/pages/send_page.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class SendPage extends StatelessWidget { + const SendPage({super.key}); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart b/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart new file mode 100644 index 0000000000..d925cf8759 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class CoinActionButton extends StatelessWidget { + const CoinActionButton({ + super.key, + required this.icon, + required this.label, + required this.action, + }); + + final SvgPicture icon; + final String label; + final VoidCallback action; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [Color(0xFF2B3A67), Color(0xFF1C2A4F)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + border: Border.all( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + width: 1, + ), + ), + child: IconButton( + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + onPressed: action, + icon: icon, + color: Theme.of(context).colorScheme.primary, + ), + ), + Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + style: TextStyle( + fontSize: 15, + color: Theme.of(context).colorScheme.onSurface, + ), + label, + ), + ), + ], + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart new file mode 100644 index 0000000000..5ac17a11d8 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart @@ -0,0 +1,65 @@ +import 'package:cake_wallet/new-ui/pages/send_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +import '../../../pages/receive_page.dart'; +import '../../../pages/scan_page.dart'; +import 'coin_action_button.dart'; + +class CoinActionRow extends StatelessWidget { + const CoinActionRow({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + spacing: 24.0, + children: [ + CoinActionButton( + icon: SvgPicture.asset("assets/new-ui/send.svg"), + label: "Send", + action: () { + showModalBottomSheet( + context: context, + builder: (context) => SendPage(), + ); + }, + ), + CoinActionButton( + icon: SvgPicture.asset("assets/new-ui/receive.svg"), + label: "Receive", + action: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => FractionallySizedBox( + heightFactor: 0.9, + child: ReceivePage(), + ), + ); + }, + ), + CoinActionButton( + icon: SvgPicture.asset("assets/new-ui/exchange.svg"), + label: "Swap", + action: () {}, + ), + CoinActionButton( + icon: SvgPicture.asset("assets/new-ui/scan.svg"), + label: "Scan", + action: () { + showModalBottomSheet( + context: context, + + builder: (context) => ScanPage(), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart b/lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart new file mode 100644 index 0000000000..b811604fb6 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart @@ -0,0 +1,74 @@ +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:flutter/material.dart'; + +class AssetTile extends StatelessWidget { + const AssetTile({super.key, required this.dashboardViewModel}); + + final DashboardViewModel dashboardViewModel; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 6.0), + child: Container( + width: double.infinity, + height: 80, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Theme.of(context).colorScheme.surfaceContainerHigh, + Theme.of(context).colorScheme.surfaceContainer, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + borderRadius: BorderRadius.all(Radius.circular(20)), + border: Border.all( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + width: 1, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container(width: 45, height: 45, child: Image.asset("assets/images/crypto/tether.webp")), + SizedBox(width: 8.0), + Column( + spacing: 4.0, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "DummyCoin", + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text( + "0.000 DMC", + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ], + ), + + Text( + "\$0.00", + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/assets_section.dart b/lib/new-ui/widgets/coins_page/assets_history/assets_section.dart new file mode 100644 index 0000000000..44eff375b9 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/assets_section.dart @@ -0,0 +1,23 @@ +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:flutter/material.dart'; + + +import 'asset_tile.dart'; + +class AssetsSection extends StatelessWidget { + const AssetsSection({super.key, required this.dashboardViewModel}); + + final DashboardViewModel dashboardViewModel; + + @override + Widget build(BuildContext context) { + return ListView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: 1, + itemBuilder: (context, index) { + return AssetTile(dashboardViewModel: dashboardViewModel,); + }, + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart b/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart new file mode 100644 index 0000000000..2a282ef846 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart @@ -0,0 +1,64 @@ +import 'package:cake_wallet/new-ui/widgets/line_tab_switcher.dart'; +import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; +import 'package:flutter/material.dart'; + +class AssetsTopBar extends StatelessWidget { + const AssetsTopBar({ + super.key, + required this.onTabChange, + required this.selectedTab, + }); + + final void Function(int) onTabChange; + final int selectedTab; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LineTabSwitcher( + tabs: const ["Assets", "History"], + onTabChange: onTabChange, + selectedTab: selectedTab, + ), + Row( + spacing: 8.0, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 48, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(99999), + ), + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(999999), + ), + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainer, + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + spacing: 4.0, + children: [Icon(Icons.settings, color: Theme.of(context).colorScheme.primary), Text("Tokens", style: TextStyle(color: Theme.of(context).colorScheme.primary),)], + ), + ), + ), + ), + ModernButton(size: 48, onPressed:(){}, icon: Icon(Icons.question_mark)), + ], + ), + ], + ), + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_section.dart b/lib/new-ui/widgets/coins_page/assets_history/history_section.dart new file mode 100644 index 0000000000..745517cbbc --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/history_section.dart @@ -0,0 +1,55 @@ +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_tile.dart'; +import 'package:cake_wallet/utils/date_formatter.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/date_section_item.dart'; +import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart'; +import 'package:flutter/material.dart'; + + +class HistorySection extends StatelessWidget { + const HistorySection({super.key, required this.dashboardViewModel}); + + final DashboardViewModel dashboardViewModel; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 8.0), + child: ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: dashboardViewModel.items.length, + itemBuilder: (context, index) { + final prevItem = index == 0 ? null : dashboardViewModel.items[index - 1]; + final item = dashboardViewModel.items[index]; + final nextItem = index == dashboardViewModel.items.length - 1 ? null : dashboardViewModel.items[index + 1]; + + + if(item is TransactionListItem) { + final transaction = item.transaction; + final transactionType = + dashboardViewModel.getTransactionType(transaction); + + return HistoryTile( + title: item.formattedTitle + item.formattedStatus + transactionType, + date: DateFormatter.convertDateTimeToReadableString(item.date), + amount: item.formattedCryptoAmount, + amountFiat: item.formattedFiatAmount, + roundedBottom: !(nextItem is TransactionListItem), + roundedTop: !(prevItem is TransactionListItem), + bottomSeparator: nextItem is TransactionListItem, + direction: item.transaction.direction, + pending: item.transaction.isPending + ); + + + } else if(item is DateSectionItem){ + return Text(DateFormatter.convertDateTimeToReadableString(item.date)); + } + + else return Text(item.runtimeType.toString()); + }, + ), + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_tile.dart b/lib/new-ui/widgets/coins_page/assets_history/history_tile.dart new file mode 100644 index 0000000000..185d753adc --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/history_tile.dart @@ -0,0 +1,109 @@ +import 'package:cw_core/transaction_direction.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class HistoryTile extends StatelessWidget { + const HistoryTile( + {super.key, + required this.title, + required this.date, + required this.amount, + required this.amountFiat, + required this.roundedTop, + required this.roundedBottom, + required this.direction, + required this.pending, + required this.bottomSeparator}); + + final String title; + final String date; + final String amount; + final String amountFiat; + final bool roundedTop; + final bool roundedBottom; + final bool bottomSeparator; + final TransactionDirection direction; + final bool pending; + + String _getDirectionIcon() { + if (pending) { + return direction == TransactionDirection.incoming + ? 'assets/new-ui/history-receiving.svg' + : 'assets/new-ui/history-sending.svg'; + } else { + return direction == TransactionDirection.incoming + ? 'assets/new-ui/history-received.svg' + : 'assets/new-ui/history-sent.svg'; + } + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(roundedTop ? 12.0 : 0.0), + topRight: Radius.circular(roundedTop ? 12.0 : 0.0), + bottomLeft: Radius.circular(roundedBottom ? 12.0 : 0.0), + bottomRight: Radius.circular(roundedBottom ? 12.0 : 0.0), + )), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12.0, + horizontal: 12.0, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 0.0), + child: SizedBox( + height: 50, + width: 50, + child: SvgPicture.asset(_getDirectionIcon()), + ), + ), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title), + Text(date), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(amount), + Text(amountFiat), + ], + ), + ], + ), + ), + ], + ), + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.0), + child: SizedBox( + height: 1, + child: DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/lightning_assets.dart b/lib/new-ui/widgets/coins_page/assets_history/lightning_assets.dart new file mode 100644 index 0000000000..3100d4331f --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/lightning_assets.dart @@ -0,0 +1,39 @@ +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:flutter/material.dart'; +import 'assets_section.dart'; +import 'history_section.dart'; + +class LightningAssets extends StatefulWidget { + const LightningAssets({super.key, required this.dashboardViewModel}); + + static const List tabs = ["Assets", "History"]; + final DashboardViewModel dashboardViewModel; + + @override + State createState() => _LightningAssetsState(); +} + +class _LightningAssetsState extends State { + int _selectedTab = 0; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + AssetsTopBar( + onTabChange: (index) { + setState(() { + _selectedTab = index; + }); + }, + selectedTab: _selectedTab, + ), + [ + AssetsSection(dashboardViewModel: widget.dashboardViewModel,), + HistorySection(dashboardViewModel: widget.dashboardViewModel,), + ][_selectedTab], + ], + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/cards/balance_card.dart b/lib/new-ui/widgets/coins_page/cards/balance_card.dart new file mode 100644 index 0000000000..bcdf43bd89 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/cards/balance_card.dart @@ -0,0 +1,127 @@ +import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class BalanceCard extends StatelessWidget { + const BalanceCard({ + super.key, + required this.width, + required this.balanceRecord, + required this.selected, required this.accountName, required this.accountBalance, + }); + + final double width; + final String accountBalance; + final String accountName; + final BalanceRecord balanceRecord; + final bool selected; + + @override + Widget build(BuildContext context) { + final Duration textFadeDuration = Duration(milliseconds: 80); + + return Container( + width: width, + height: width * 2.0 / 3, + decoration: BoxDecoration( + border: Border.all(color: Color(0x77FFFFFF), width: 1), + gradient: LinearGradient( + colors: [Colors.lightBlueAccent, Colors.blue], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + borderRadius: BorderRadius.circular(20), + ), + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + accountName, + style: TextStyle(color: Colors.black, fontSize: 20), + ), + + AnimatedOpacity( + opacity: selected ? 0 : 1, + duration: textFadeDuration, + child: Text( + accountBalance, + style: TextStyle(color: Colors.black, fontSize: 14), + ), + ), + ], + ), + AnimatedOpacity( + opacity: selected ? 1 : 0, + duration: textFadeDuration, + child: Row( + spacing: 8.0, + children: [ + Text( + balanceRecord.availableBalance, + style: TextStyle(color: Colors.black, fontSize: 28), + ), + Text( + balanceRecord.asset.name.toUpperCase(), + style: TextStyle(color: Colors.black45, fontSize: 28), + ), + ], + ), + ), + Text( + balanceRecord.fiatAvailableBalance, + style: TextStyle(color: Colors.black45, fontSize: 20), + ), + ], + ), + + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + decoration: BoxDecoration( + color: Color(0x44FFFFFF), + borderRadius: BorderRadius.circular(10000000), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(6.0), + child: Text( + "Buy", + style: TextStyle(color: Colors.black, fontSize: 16), + ), + ), + Icon(Icons.arrow_forward, color: Colors.black45), + ], + ), + ), + SvgPicture.asset( + "assets/new-ui/switcher-bitcoin.svg", + height: 50, + width: 50, + colorFilter: const ColorFilter.mode( + Color(0x44FFFFFF), + BlendMode.srcIn, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/cards/cards_view.dart b/lib/new-ui/widgets/coins_page/cards/cards_view.dart new file mode 100644 index 0000000000..b4b517e5c4 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/cards/cards_view.dart @@ -0,0 +1,138 @@ +import 'dart:math'; + +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +import 'balance_card.dart'; + +class CardsView extends StatefulWidget { + const CardsView({super.key, required this.dashboardViewModel, required this.accountListViewModel, required this.lightningMode}); + + final DashboardViewModel dashboardViewModel; + final MoneroAccountListViewModel? accountListViewModel; + final bool lightningMode; + + + @override + _CardsViewState createState() => _CardsViewState(); +} + +class _CardsViewState extends State { + int? _selectedIndex = 0; + + static const Duration animDuration = Duration(milliseconds: 200); + static const double overlapAmount = 60.0; + late final double cardWidth = MediaQuery.of(context).size.width * 0.85; + late final int numCards; + + @override + void initState() { + super.initState(); + numCards = widget.accountListViewModel?.accounts.length ?? 1; + } + + Widget _buildCard(int index, double parentWidth) { + final int numCards = widget.accountListViewModel?.accounts.length ?? 1; + final double baseTop = overlapAmount * (numCards - 1); + final double scaleFactor = 0.96; + + final int howFarBehind = (_selectedIndex! - index + numCards) % numCards; + final double scale = pow(scaleFactor, howFarBehind).toDouble(); + + final double top = baseTop - (howFarBehind * overlapAmount); + + final double left = (parentWidth - cardWidth) / 2.0; + + return AnimatedPositioned( + key: ValueKey('box_$index'), + duration: animDuration, + curve: Curves.easeOut, + top: top, + left: left, + child: AnimatedScale( + duration: animDuration, + curve: Curves.easeOut, + scale: scale, + child: GestureDetector( + onTap: () { + setState(() { + if(widget.accountListViewModel != null) + widget.accountListViewModel!.select(widget.accountListViewModel!.accounts[index]); + _selectedIndex = index; + }); + }, + child: Observer( + builder: (_){return BalanceCard( + width: cardWidth, + accountName: (widget.accountListViewModel?.accounts[index].label) ?? "Primary account", + accountBalance: widget.accountListViewModel?.accounts[index].balance ?? "", + balanceRecord: widget.dashboardViewModel.balanceViewModel.formattedBalances.elementAt(0), + selected: _selectedIndex == index, + );} + ), + ), + ), + ); + } + + double _getBoxHeight() { + return + /* height of initial card */ + (2 / 3) * (cardWidth) + + /* height of bg card * amount of bg cards */ + overlapAmount * ((widget.accountListViewModel?.accounts.length ??1) - 1); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final double parentWidth = constraints.maxWidth; + List children = []; + + if (_selectedIndex! >= (widget.accountListViewModel?.accounts.length ?? 1)) { + _selectedIndex = 0; + } + + for ( + int i = _selectedIndex!; + i < (widget.accountListViewModel?.accounts.length ?? 1) + _selectedIndex!; + i++ + ) { + if (i != _selectedIndex) { + children.add(_buildCard(i % (widget.accountListViewModel?.accounts.length ?? 1), parentWidth)); + } + } + + if (_selectedIndex != null) { + children.add(_buildCard(_selectedIndex!, parentWidth)); + } + + return Observer( + builder: (_){return Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: AnimatedContainer( + duration: Duration(milliseconds: 200), + curve: Curves.easeOut, + width: double.infinity, + height: _getBoxHeight(), + child: AnimatedSwitcher( + duration: Duration(milliseconds: 200), + transitionBuilder: (child, animation) => + FadeTransition(opacity: animation, child: child), + child: SizedBox( + key: ValueKey(_getBoxHeight()), + width: double.infinity, + height: _getBoxHeight(), + child: Stack(alignment: Alignment.center, children: children), + ), + ), + ), + );} + ); + }, + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/top_bar.dart b/lib/new-ui/widgets/coins_page/top_bar.dart new file mode 100644 index 0000000000..0c7e0ce15f --- /dev/null +++ b/lib/new-ui/widgets/coins_page/top_bar.dart @@ -0,0 +1,78 @@ +import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class TopBar extends StatelessWidget { + const TopBar({ + super.key, + required this.lightningMode, + required this.onLightningSwitchPress, required this.dashboardViewModel, + }); + + final bool lightningMode; + final VoidCallback onLightningSwitchPress; + final DashboardViewModel dashboardViewModel; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(18.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if(dashboardViewModel.balanceViewModel.hasSecondAdditionalBalance || + dashboardViewModel.balanceViewModel.hasSecondAvailableBalance) + SizedBox( + child: AnimatedSwitcher( + duration: Duration(milliseconds: 200), + transitionBuilder: (child, animation) => + FadeTransition(opacity: animation, child: child), + child: ElevatedButton( + key: ValueKey(lightningMode), + style: ElevatedButton.styleFrom( + padding: EdgeInsets.all(4), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(900.0)), + ), + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainer, + ), + onPressed: onLightningSwitchPress, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SvgPicture.asset( + lightningMode + ? 'assets/new-ui/switcher-lightning.svg' + : 'assets/new-ui/switcher-bitcoin.svg', + width: 40, + height: 40, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + SvgPicture.asset( + lightningMode + ? 'assets/new-ui/switcher-bitcoin-off.svg' + : 'assets/new-ui/switcher-lightning-off.svg', + width: 40, + height: 40, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + ], + ), + ), + ), + ), + ModernButton.svg(size: 44, onPressed: (){}, svgPath: "assets/new-ui/top-settings.svg",), + ], + ), + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/wallet_info.dart b/lib/new-ui/widgets/coins_page/wallet_info.dart new file mode 100644 index 0000000000..68adc8b130 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/wallet_info.dart @@ -0,0 +1,50 @@ +import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class WalletInfo extends StatelessWidget { + const WalletInfo({super.key, required this.lightningMode, required this.name, required this.usesHardwareWallet}); + + final bool lightningMode; + final String name; + final bool usesHardwareWallet; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + + children: [ + AnimatedSwitcher( + duration: Duration(milliseconds: 150), + transitionBuilder: (child, animation) { + return SizeTransition( + axis: Axis.horizontal, + sizeFactor: animation, + child: FadeTransition(opacity: animation, child: child), + ); + }, + child: !usesHardwareWallet + ? SizedBox.shrink(key: ValueKey("empty")) + : Padding( + padding: const EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 0.0), + child: SvgPicture.asset( + "assets/new-ui/wallet-trezor.svg", + key: ValueKey("wallet"), + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, + BlendMode.srcIn, + ), + ), + ), + ), + Text(name, style: TextStyle(fontSize: 20)), + SizedBox(width: 8), + ModernButton.svg(size: 20, onPressed: (){}, svgPath: "assets/new-ui/3dots.svg",) + ], + ); + } +} diff --git a/lib/new-ui/widgets/line_tab_switcher.dart b/lib/new-ui/widgets/line_tab_switcher.dart new file mode 100644 index 0000000000..6fdb533d02 --- /dev/null +++ b/lib/new-ui/widgets/line_tab_switcher.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +class LineTabSwitcher extends StatefulWidget { + const LineTabSwitcher({ + super.key, + required this.tabs, + required this.onTabChange, + required this.selectedTab, + }); + + final List tabs; + final void Function(int index) onTabChange; + final int selectedTab; + + @override + State createState() => _LineTabSwitcherState(); +} + +class _LineTabSwitcherState extends State { + List textWidgetKeys = []; + List textWidgetSizes = []; + bool textWidgetsMeasured = false; + + double _calcBarLeft() { + double left = 0; + + if (textWidgetKeys.isEmpty || textWidgetSizes.isEmpty) { + return 0; + } + + for (int i = 0; i < widget.selectedTab; i++) { + left += textWidgetSizes[i].width + 16.0; + } + + left += 8.0; + + return left; + } + + @override + void initState() { + super.initState(); + textWidgetKeys = List.generate(widget.tabs.length, (index) => GlobalKey()); + WidgetsBinding.instance.addPostFrameCallback((_) => measure()); + } + + void measure() { + setState(() { + textWidgetSizes = textWidgetKeys + .map((k) => k.currentContext!.size) + .whereType() + .toList(); + textWidgetsMeasured = true; + }); + } + + @override + Widget build(BuildContext context) { + if (!textWidgetsMeasured) { + WidgetsBinding.instance.addPostFrameCallback((_) => measure()); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + width: 200, + height: 40, + child: ListView.builder( + physics: NeverScrollableScrollPhysics(), + scrollDirection: Axis.horizontal, + itemCount: widget.tabs.length, + itemBuilder: (context, index) { + return GestureDetector( + onTap: () { + widget.onTabChange(index); + }, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedDefaultTextStyle( + duration: Duration(milliseconds: 150), + style: DefaultTextStyle.of(context).style.copyWith( + inherit: true, + fontSize: 22, + color: widget.selectedTab == index + ? Theme.of(context).colorScheme.onSurface + : Theme.of(context).colorScheme.onSurfaceVariant, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + widget.tabs[index], + key: textWidgetKeys[index], + ), + ), + ), + ], + ), + ); + }, + ), + ), + Container( + width: 200, + height: 2, + child: Stack( + children: [ + AnimatedPositioned( + curve: Curves.easeOut, + left: _calcBarLeft(), + bottom: 0, + duration: Duration(milliseconds: 150), + child: AnimatedSize( + duration: Duration(milliseconds: 150), + child: Container( + height: 2, + width: textWidgetSizes.isEmpty + ? 0 + : textWidgetSizes[widget.selectedTab].width, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/new-ui/widgets/modern_button.dart b/lib/new-ui/widgets/modern_button.dart new file mode 100644 index 0000000000..2c0db7f12a --- /dev/null +++ b/lib/new-ui/widgets/modern_button.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class ModernButton extends StatelessWidget { + final double size; + final String? svgPath; + final Widget? icon; + final VoidCallback onPressed; + final Color? color; + + static const iconSvgSizeRatio = 2/3; + + + const ModernButton({ + super.key, + required this.size, + required this.icon, + required this.onPressed, + this.color + }) : svgPath = null; + + const ModernButton.svg({ + super.key, + required this.size, + required this.svgPath, + required this.onPressed, + this.color + }) : icon = null; + + @override + Widget build(BuildContext context) { + final color = Theme.of(context).colorScheme.primary; + final Widget resolvedIcon = svgPath != null + ? SvgPicture.asset( + svgPath!, + width: size, + height: size, + fit: BoxFit.contain, + alignment: Alignment.center, + allowDrawingOutsideViewBox: true, + colorFilter: ColorFilter.mode(color, BlendMode.srcIn), + ) + : IconTheme( + data: IconThemeData(color: color, size: size*iconSvgSizeRatio), + child: icon!, + ); + + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(size), + ), + width: size, + height: size, + child: IconButton( + padding: EdgeInsets.zero, + onPressed: onPressed, + icon: resolvedIcon, + ), + ); + } +} diff --git a/lib/new-ui/widgets/navbar/navbar.dart b/lib/new-ui/widgets/navbar/navbar.dart new file mode 100644 index 0000000000..f19f6e69b6 --- /dev/null +++ b/lib/new-ui/widgets/navbar/navbar.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +import 'navbar_button.dart'; + +class Navbar extends StatefulWidget { + const Navbar({super.key}); + + @override + State createState() => _NavbarState(); +} + +class NavbarItemData { + final String iconPath; + final String text; + + NavbarItemData(this.iconPath, this.text); +} + +class _NavbarState extends State { + int _selectedIndex = 0; + + final List _items = [ + NavbarItemData("assets/Home.svg", "Home"), + NavbarItemData("assets/Wallets.svg", "Wallets"), + NavbarItemData("assets/Contacts.svg", "Contacts"), + NavbarItemData("assets/Apps.svg", "Apps"), + NavbarItemData("assets/Charts.svg", "Charts"), + ]; + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.all(32.0), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(99999), + color: Theme.of( + context, + ).colorScheme.surfaceContainerHighest.withAlpha(170), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 12.0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: List.generate(_items.length, (index) { + return NavbarButton( + data: _items[index], + onPressed: () { + setState(() { + _selectedIndex = index; + }); + }, + selected: _selectedIndex == index, + ); + }), + ), + ), + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/navbar/navbar_button.dart b/lib/new-ui/widgets/navbar/navbar_button.dart new file mode 100644 index 0000000000..79c3af9eac --- /dev/null +++ b/lib/new-ui/widgets/navbar/navbar_button.dart @@ -0,0 +1,67 @@ +import 'package:cake_wallet/new-ui/widgets/navbar/navbar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class NavbarButton extends StatelessWidget { + const NavbarButton({ + super.key, + required this.data, + required this.selected, + required this.onPressed, + }); + + final NavbarItemData data; + final VoidCallback onPressed; + final bool selected; + + @override + Widget build(BuildContext context) { + return AnimatedSize( + curve: Curves.easeOut, + duration: Duration(milliseconds: 100), + child: AnimatedContainer( + curve: Curves.easeOut, + duration: Duration(milliseconds: 100), + decoration: BoxDecoration( + color: selected + ? Color(0x79BDCFFF) + : Theme.of( + context, + ).colorScheme.surfaceContainerHighest.withAlpha(0), + borderRadius: BorderRadius.circular(1242357), + ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + constraints: BoxConstraints(), + padding: EdgeInsets.zero, + icon: SvgPicture.asset( + data.iconPath, + width: selected ? 24 : 36, + height: selected ? 24 : 36, + colorFilter: ColorFilter.mode( + selected + ? Theme.of(context).colorScheme.onSurface + : Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + onPressed: onPressed, + ), + if (selected) + Padding( + padding: const EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 0.0), + child: Text( + data.text, + style: TextStyle(fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/receive_page/receive_amount_input.dart b/lib/new-ui/widgets/receive_page/receive_amount_input.dart new file mode 100644 index 0000000000..1a793d015c --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_amount_input.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; + +class ReceiveAmountInput extends StatelessWidget { + const ReceiveAmountInput({super.key, required this.largeQrMode}); + + final bool largeQrMode; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedContainer( + duration: Duration(milliseconds: 300), + height: 56, + width: largeQrMode ? 250 : 160, + decoration: BoxDecoration( + // color: largeQrMode + // ? Theme.of(context).colorScheme.surface + // // no it can't just be transparent. might be framework bug actually + // : Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10), + bottomLeft: Radius.circular(10), + topRight: Radius.circular(0), + bottomRight: Radius.circular(0), + ), + border: Border.all( + color: Theme.of(context).colorScheme.surfaceContainer, + width: 2, + ), + ), + child: AnimatedScale( + duration: Duration(milliseconds: 500), + scale: largeQrMode ? 1.3 : 1, + curve: Curves.easeOut, + child: TextField( + enabled: !largeQrMode, + textAlign: TextAlign.center, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + hint: Text( + "0.00000000", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.5), + ), + ), + border: InputBorder.none, + ), + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + ), + ), + ), + Container( + height: 56, + width: 74, + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(0), + bottomLeft: Radius.circular(0), + topRight: Radius.circular(10), + bottomRight: Radius.circular(10), + ), + color: Theme.of(context).colorScheme.surfaceContainer, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 4.0, + children: [ + Text( + "BTC", + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + Icon( + Icons.keyboard_arrow_down, + color: Theme.of(context).colorScheme.primary, + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/new-ui/widgets/receive_page/receive_bottom_buttons.dart b/lib/new-ui/widgets/receive_page/receive_bottom_buttons.dart new file mode 100644 index 0000000000..5df32e216b --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_bottom_buttons.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; + +class ReceiveBottomButtons extends StatelessWidget { + final bool largeQrMode; + const ReceiveBottomButtons({super.key, required this.largeQrMode}); + + @override + Widget build(BuildContext context) { + final double targetHeight = largeQrMode ? 0 : 150; + final double targetOpacity = largeQrMode ? 0 : 1; + + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + height: targetHeight, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: targetOpacity, + curve: Curves.easeOut, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainer, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric(vertical: 16), + ), + onPressed: () {}, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.book_outlined, + size: 20, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 10), + Text( + 'Accounts & Addresses', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric(vertical: 16), + ), + onPressed: () {}, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Copy Address', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + const SizedBox(width: 10), + Icon( + Icons.copy_all_outlined, + size: 20, + color: Theme.of(context).colorScheme.onPrimary, + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/receive_page/receive_qr_code.dart b/lib/new-ui/widgets/receive_page/receive_qr_code.dart new file mode 100644 index 0000000000..055567d73e --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_qr_code.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +class ReceiveQrCode extends StatelessWidget { + const ReceiveQrCode({ + super.key, + required this.onTap, + required this.largeQrMode, + }); + + final VoidCallback onTap; + final bool largeQrMode; + + @override + Widget build(BuildContext context) { + final double targetY = largeQrMode ? 40 : 0; + + return GestureDetector( + onTap: onTap, + child: TweenAnimationBuilder( + tween: Tween(begin: 0, end: targetY), + duration: const Duration(milliseconds: 500), + curve: Curves.easeOut, + builder: (context, value, child) { + return Transform.translate( + offset: Offset(0, value), + child: AnimatedContainer( + duration: const Duration(milliseconds: 500), + curve: Curves.easeOutCubic, + width: largeQrMode ? 400 : 250, + height: largeQrMode ? 400 : 250, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Colors.white, + ), + padding: const EdgeInsets.all(8.0), + child: Image.asset("assets/btcqr.png"), + ), + ); + }, + ), + ); + } +} diff --git a/lib/new-ui/widgets/receive_page/receive_seed_type_selector.dart b/lib/new-ui/widgets/receive_page/receive_seed_type_selector.dart new file mode 100644 index 0000000000..d136604b4d --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_seed_type_selector.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class ReceiveSeedTypeSelector extends StatelessWidget { + const ReceiveSeedTypeSelector({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + spacing: 12.0, + children: [ + SvgPicture.asset( + width: 32, + height: 32, + "assets/new-ui/switcher-bitcoin-off.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + Text( + "Standard", + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.primary, + ), + ), + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(999999), + ), + child: IconButton( + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + onPressed: () {}, + icon: (Icon( + color: Theme.of(context).colorScheme.primary, + size: 20, + Icons.keyboard_arrow_down, + )), + ), + ), + ], + ); + } +} diff --git a/lib/new-ui/widgets/receive_page/receive_seed_widget.dart b/lib/new-ui/widgets/receive_page/receive_seed_widget.dart new file mode 100644 index 0000000000..a95fcbb1b7 --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_seed_widget.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +class ReceiveSeedWidget extends StatelessWidget { + const ReceiveSeedWidget({super.key}); + + static const List dummyWalletStrings = [ + 'bc1q', + 'xy2k', + 'gdyg', + 'jrsq', + 'tzq2', + 'n0yr', + 'f249', + '3p83', + 'kkfj', + 'hx0wlh', + ]; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 80.0), + child: Wrap( + alignment: WrapAlignment.center, + spacing: 8.0, + runSpacing: 4.0, + children: List.generate( + dummyWalletStrings.length, + (index) => Text( + dummyWalletStrings[index], + style: TextStyle( + fontSize: 16, + color: index % 2 != 0 ? Colors.grey : Colors.white, + ), + ), + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/receive_page/receive_top_bar.dart b/lib/new-ui/widgets/receive_page/receive_top_bar.dart new file mode 100644 index 0000000000..cbd80167b2 --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_top_bar.dart @@ -0,0 +1,27 @@ +import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; +import 'package:flutter/material.dart'; + +class ReceiveTopBar extends StatelessWidget { + const ReceiveTopBar({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ModernButton(size: 52, onPressed: () { + Navigator.of(context).pop(); + }, icon: Icon(Icons.close)), + + Text("Receive", style: TextStyle(fontSize: 22)), + ModernButton(size: 52, onPressed: () { + Navigator.of(context).pop(); + }, icon: Icon(Icons.share)), + ], + ), + ); + } +} diff --git a/lib/router.dart b/lib/router.dart index 02fe8275df..ee0a77c20b 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/core/new_wallet_arguments.dart'; +import 'package:cake_wallet/new-ui/new_dashboard.dart'; import 'package:cake_wallet/order/order.dart'; import 'package:cake_wallet/core/new_wallet_type_arguments.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; @@ -40,6 +41,7 @@ import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart'; import 'package:cake_wallet/src/screens/dev/moneroc_cache_debug.dart'; import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart'; import 'package:cake_wallet/src/screens/dev/network_requests.dart'; +import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cake_wallet/src/screens/dev/qr_tools_page.dart'; import 'package:cake_wallet/src/screens/dev/secure_preferences_page.dart'; import 'package:cake_wallet/src/screens/dev/shared_preferences_page.dart'; @@ -148,6 +150,7 @@ import 'package:cw_core/nano_account.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/unspent_coin_type.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/cupertino.dart'; @@ -163,15 +166,19 @@ Route handleRouteWithPlatformAwareness( bool fullscreenDialog = false, }) { if (Platform.isIOS) { - return CupertinoPageRoute(builder: builder, fullscreenDialog: fullscreenDialog); + return CupertinoPageRoute( + builder: builder, fullscreenDialog: fullscreenDialog); } else { - return MaterialPageRoute(builder: builder, fullscreenDialog: fullscreenDialog); + return MaterialPageRoute( + builder: builder, fullscreenDialog: fullscreenDialog); } } Route createRoute(RouteSettings settings) { currentRouteSettings = settings; + printV(settings.name); + switch (settings.name) { case Routes.welcome: return MaterialPageRoute( @@ -222,7 +229,8 @@ Route createRoute(RouteSettings settings) { case Routes.walletGroupsDisplayPage: final type = settings.arguments as WalletType; - final walletGroupsDisplayVM = getIt.get(param1: type); + final walletGroupsDisplayVM = + getIt.get(param1: type); return handleRouteWithPlatformAwareness( (_) => WalletGroupsDisplayPage( @@ -247,21 +255,25 @@ Route createRoute(RouteSettings settings) { case Routes.chooseHardwareWalletAccount: final arguments = settings.arguments as List; final type = arguments[0] as WalletType; - final hardwareWallet = arguments [1] as HardwareWalletType; + final hardwareWallet = arguments[1] as HardwareWalletType; final walletVM = getIt.get( - param1: type, param2: getIt(param1: hardwareWallet)); + param1: type, + param2: getIt(param1: hardwareWallet)); if (type == WalletType.monero) - return handleRouteWithPlatformAwareness((_) => MoneroHardwareWalletOptionsPage(walletVM)); + return handleRouteWithPlatformAwareness( + (_) => MoneroHardwareWalletOptionsPage(walletVM)); - return handleRouteWithPlatformAwareness((_) => SelectHardwareWalletAccountPage(walletVM)); + return handleRouteWithPlatformAwareness( + (_) => SelectHardwareWalletAccountPage(walletVM)); case Routes.setupPin: Function(PinCodeState, String)? callback; if (settings.arguments is Function(PinCodeState, String)) { - callback = settings.arguments as Function(PinCodeState, String); + callback = + settings.arguments as Function(PinCodeState, String); } return handleRouteWithPlatformAwareness( @@ -274,7 +286,8 @@ Route createRoute(RouteSettings settings) { param1: NewWalletTypeArguments( onTypeSelected: (BuildContext context, WalletType type) { final arg = {'walletType': type}; - Navigator.of(context).pushNamed(Routes.restoreWallet, arguments: arg); + Navigator.of(context) + .pushNamed(Routes.restoreWallet, arguments: arg); }, isCreate: false, ), @@ -294,7 +307,8 @@ Route createRoute(RouteSettings settings) { case Routes.restoreWalletFromSeedKeys: if (isSingleCoin) { return handleRouteWithPlatformAwareness( - (context) => getIt.get(param1: availableWalletTypes.first), + (context) => + getIt.get(param1: availableWalletTypes.first), ); } return handleRouteWithPlatformAwareness( @@ -302,7 +316,8 @@ Route createRoute(RouteSettings settings) { param1: NewWalletTypeArguments( onTypeSelected: (BuildContext context, WalletType type) { final arg = {'walletType': type}; - Navigator.of(context).pushNamed(Routes.restoreWallet, arguments: arg); + Navigator.of(context) + .pushNamed(Routes.restoreWallet, arguments: arg); }, isCreate: false, ), @@ -312,19 +327,23 @@ Route createRoute(RouteSettings settings) { case Routes.restoreWalletFromHardwareWallet: final arguments = settings.arguments as Map?; final showUnavailable = (arguments?['showUnavailable'] as bool?) ?? true; - final onSelect = arguments?['onSelect'] as void Function(BuildContext, HardwareWalletType)?; + final onSelect = arguments?['onSelect'] as void Function( + BuildContext, HardwareWalletType)?; final availableHardwareWalletTypes = - arguments?['availableHardwareWalletTypes'] as List?; + arguments?['availableHardwareWalletTypes'] + as List?; - return handleRouteWithPlatformAwareness((_) => SelectDeviceManufacturerPage( - showUnavailable: showUnavailable, - onSelect: onSelect, - availableHardwareWalletTypes: availableHardwareWalletTypes, - )); + return handleRouteWithPlatformAwareness( + (_) => SelectDeviceManufacturerPage( + showUnavailable: showUnavailable, + onSelect: onSelect, + availableHardwareWalletTypes: availableHardwareWalletTypes, + )); case Routes.connectHardwareWallet: final arguments = settings.arguments as List; - final hardwareWalletType = (arguments[0] as HardwareWalletType?) ?? HardwareWalletType.ledger; + final hardwareWalletType = + (arguments[0] as HardwareWalletType?) ?? HardwareWalletType.ledger; if (isSingleCoin) { return handleRouteWithPlatformAwareness( @@ -332,9 +351,13 @@ Route createRoute(RouteSettings settings) { ConnectDevicePageParams( walletType: availableWalletTypes.first, hardwareWalletType: hardwareWalletType, - onConnectDevice: (BuildContext context, _) => Navigator.of(context).pushNamed( - Routes.chooseHardwareWalletAccount, - arguments: [availableWalletTypes.first, hardwareWalletType]), + onConnectDevice: (BuildContext context, _) => + Navigator.of(context).pushNamed( + Routes.chooseHardwareWalletAccount, + arguments: [ + availableWalletTypes.first, + hardwareWalletType + ]), isReconnect: false, ), getIt.get(), @@ -346,7 +369,8 @@ Route createRoute(RouteSettings settings) { param1: NewWalletTypeArguments( onTypeSelected: (BuildContext context, WalletType type) { if (hardwareWalletType == HardwareWalletType.trezor) { - Navigator.of(context).pushNamed(Routes.chooseHardwareWalletAccount, + Navigator.of(context).pushNamed( + Routes.chooseHardwareWalletAccount, arguments: [type, hardwareWalletType]); return; } @@ -354,13 +378,15 @@ Route createRoute(RouteSettings settings) { final arguments = ConnectDevicePageParams( walletType: type, hardwareWalletType: hardwareWalletType, - onConnectDevice: (BuildContext context, _) => Navigator.of(context).pushNamed( - Routes.chooseHardwareWalletAccount, - arguments: [type, hardwareWalletType]), + onConnectDevice: (BuildContext context, _) => + Navigator.of(context).pushNamed( + Routes.chooseHardwareWalletAccount, + arguments: [type, hardwareWalletType]), isReconnect: false, ); - Navigator.of(context).pushNamed(Routes.connectDevices, arguments: arguments); + Navigator.of(context) + .pushNamed(Routes.connectDevices, arguments: arguments); }, isCreate: false, hardwareWalletType: hardwareWalletType, @@ -381,14 +407,16 @@ Route createRoute(RouteSettings settings) { case Routes.seed: return handleRouteWithPlatformAwareness( - (context) => getIt.get(param1: settings.arguments as bool), + (context) => + getIt.get(param1: settings.arguments as bool), ); case Routes.restoreWallet: final args = settings.arguments as Map?; final walletType = args?['walletType'] as WalletType; return MaterialPageRoute( - builder: (_) => getIt.get(param1: walletType, param2: args)); + builder: (_) => + getIt.get(param1: walletType, param2: args)); case Routes.restoreWalletChooseDerivation: return MaterialPageRoute( @@ -396,16 +424,21 @@ Route createRoute(RouteSettings settings) { param1: settings.arguments as List)); case Routes.sweepingWalletPage: - return CupertinoPageRoute(builder: (_) => getIt.get()); + return CupertinoPageRoute( + builder: (_) => getIt.get()); case Routes.dashboard: return CupertinoPageRoute( - settings: settings, builder: (_) => getIt.get()); + settings: settings, builder: (_) => + FeatureFlag.hasNewUi? + getIt.get(): + getIt.get()); case Routes.send: final args = settings.arguments as Map?; final initialPaymentRequest = args?['paymentRequest'] as PaymentRequest?; - final coinTypeToSpendFrom = args?['coinTypeToSpendFrom'] as UnspentCoinType?; + final coinTypeToSpendFrom = + args?['coinTypeToSpendFrom'] as UnspentCoinType?; return handleRouteWithPlatformAwareness( (context) => getIt.get( @@ -416,10 +449,12 @@ Route createRoute(RouteSettings settings) { case Routes.sendTemplate: return CupertinoPageRoute( - fullscreenDialog: true, builder: (_) => getIt.get()); + fullscreenDialog: true, + builder: (_) => getIt.get()); case Routes.receive: - return CupertinoPageRoute(builder: (context) => getIt.get()); + return CupertinoPageRoute( + builder: (context) => getIt.get()); case Routes.addressPage: return handleRouteWithPlatformAwareness( @@ -429,29 +464,34 @@ Route createRoute(RouteSettings settings) { case Routes.transactionDetails: return CupertinoPageRoute( fullscreenDialog: true, - builder: (_) => - getIt.get(param1: settings.arguments as TransactionInfo)); + builder: (_) => getIt.get( + param1: settings.arguments as TransactionInfo)); case Routes.bumpFeePage: return CupertinoPageRoute( fullscreenDialog: true, - builder: (_) => getIt.get(param1: settings.arguments as List)); + builder: (_) => getIt.get( + param1: settings.arguments as List)); case Routes.newSubaddress: return CupertinoPageRoute( - builder: (_) => getIt.get(param1: settings.arguments)); + builder: (_) => + getIt.get(param1: settings.arguments)); case Routes.disclaimer: return CupertinoPageRoute(builder: (_) => DisclaimerPage()); case Routes.readDisclaimer: - return CupertinoPageRoute(builder: (_) => DisclaimerPage(isReadOnly: true)); + return CupertinoPageRoute( + builder: (_) => DisclaimerPage(isReadOnly: true)); case Routes.readThirdPartyDisclaimer: - return CupertinoPageRoute(builder: (_) => ThirdPartyDisclaimerPage()); + return CupertinoPageRoute( + builder: (_) => ThirdPartyDisclaimerPage()); case Routes.changeRep: - return CupertinoPageRoute(builder: (_) => getIt.get()); + return CupertinoPageRoute( + builder: (_) => getIt.get()); case Routes.walletList: final onWalletLoaded = settings.arguments as Function(BuildContext)?; @@ -463,8 +503,8 @@ Route createRoute(RouteSettings settings) { case Routes.walletEdit: return MaterialPageRoute( fullscreenDialog: true, - builder: (_) => - getIt.get(param1: settings.arguments as WalletEditPageArguments), + builder: (_) => getIt.get( + param1: settings.arguments as WalletEditPageArguments), ); case Routes.auth: @@ -477,7 +517,8 @@ Route createRoute(RouteSettings settings) { instanceName: 'wallet_unlock_verifiable', param2: true) : getIt.get( - param1: settings.arguments as OnAuthenticationFinished, param2: true)); + param1: settings.arguments as OnAuthenticationFinished, + param2: true)); case Routes.totpAuthCodePage: final args = settings.arguments as TotpAuthArgumentsModel; @@ -503,13 +544,15 @@ Route createRoute(RouteSettings settings) { ? WillPopScope( child: getIt.get( param1: WalletUnlockArguments( - callback: settings.arguments as OnAuthenticationFinished), + callback: + settings.arguments as OnAuthenticationFinished), param2: false, instanceName: 'wallet_unlock_verifiable'), onWillPop: () async => false) : WillPopScope( child: getIt.get( - param1: settings.arguments as OnAuthenticationFinished, param2: false), + param1: settings.arguments as OnAuthenticationFinished, + param2: false), onWillPop: () async => false)); case Routes.silentPaymentsSettings: @@ -554,11 +597,13 @@ Route createRoute(RouteSettings settings) { case Routes.trocadorProvidersPage: return CupertinoPageRoute( - fullscreenDialog: true, builder: (_) => getIt.get()); + fullscreenDialog: true, + builder: (_) => getIt.get()); case Routes.domainLookupsPage: return CupertinoPageRoute( - fullscreenDialog: true, builder: (_) => getIt.get()); + fullscreenDialog: true, + builder: (_) => getIt.get()); case Routes.displaySettingsPage: return handleRouteWithPlatformAwareness( @@ -574,17 +619,20 @@ Route createRoute(RouteSettings settings) { final args = settings.arguments as Map?; return CupertinoPageRoute( builder: (_) => getIt.get( - param1: args?['editingNode'] as Node?, param2: args?['isSelected'] as bool?)); + param1: args?['editingNode'] as Node?, + param2: args?['isSelected'] as bool?)); case Routes.login: return CupertinoPageRoute( builder: (context) => WillPopScope( child: SettingsStoreBase.walletPasswordDirectInput - ? getIt.get(instanceName: 'wallet_password_login') + ? getIt.get( + instanceName: 'wallet_password_login') : getIt.get(instanceName: 'login'), onWillPop: () async => // FIX-ME: Additional check does it works correctly - (await SystemChannels.platform.invokeMethod('SystemNavigator.pop') ?? + (await SystemChannels.platform + .invokeMethod('SystemNavigator.pop') ?? false)), fullscreenDialog: true); @@ -592,7 +640,8 @@ Route createRoute(RouteSettings settings) { final args = settings.arguments as Map?; return CupertinoPageRoute( builder: (_) => getIt.get( - param1: args?['editingNode'] as Node?, param2: args?['isSelected'] as bool?)); + param1: args?['editingNode'] as Node?, + param2: args?['isSelected'] as bool?)); case Routes.accountCreation: return CupertinoPageRoute( @@ -601,8 +650,8 @@ Route createRoute(RouteSettings settings) { case Routes.nanoAccountCreation: return CupertinoPageRoute( - builder: (_) => - getIt.get(param1: settings.arguments as NanoAccount?)); + builder: (_) => getIt.get( + param1: settings.arguments as NanoAccount?)); case Routes.addressBook: return handleRouteWithPlatformAwareness( @@ -615,11 +664,13 @@ Route createRoute(RouteSettings settings) { builder: (_) => getIt.get(param1: selectedCurrency)); case Routes.pickerWalletAddress: - return MaterialPageRoute(builder: (_) => getIt.get()); + return MaterialPageRoute( + builder: (_) => getIt.get()); case Routes.addressBookAddContact: return handleRouteWithPlatformAwareness( - (context) => getIt.get(param1: settings.arguments as ContactRecord?), + (context) => getIt.get( + param1: settings.arguments as ContactRecord?), ); case Routes.showKeys: @@ -628,19 +679,23 @@ Route createRoute(RouteSettings settings) { ); case Routes.exchangeTrade: - return CupertinoPageRoute(builder: (_) => getIt.get()); + return CupertinoPageRoute( + builder: (_) => getIt.get()); case Routes.exchangeConfirm: - return MaterialPageRoute(builder: (_) => getIt.get()); + return MaterialPageRoute( + builder: (_) => getIt.get()); case Routes.tradeDetails: return MaterialPageRoute( fullscreenDialog: true, - builder: (_) => getIt.get(param1: settings.arguments as Trade)); + builder: (_) => + getIt.get(param1: settings.arguments as Trade)); case Routes.orderDetails: return MaterialPageRoute( - builder: (_) => getIt.get(param1: settings.arguments as Order)); + builder: (_) => + getIt.get(param1: settings.arguments as Order)); case Routes.buySellPage: final args = settings.arguments as bool; @@ -650,7 +705,8 @@ Route createRoute(RouteSettings settings) { case Routes.buyOptionsPage: final args = settings.arguments as List; - return MaterialPageRoute(builder: (_) => getIt.get(param1: args)); + return MaterialPageRoute( + builder: (_) => getIt.get(param1: args)); case Routes.paymentMethodOptionsPage: final args = settings.arguments as List; @@ -661,15 +717,18 @@ Route createRoute(RouteSettings settings) { final args = settings.arguments as List; return MaterialPageRoute( - fullscreenDialog: true, builder: (_) => getIt.get(param1: args)); + fullscreenDialog: true, + builder: (_) => getIt.get(param1: args)); case Routes.exchange: return handleRouteWithPlatformAwareness( - (context) => getIt.get(param1: settings.arguments as PaymentRequest?), + (context) => getIt.get( + param1: settings.arguments as PaymentRequest?), ); case Routes.exchangeTemplate: - return CupertinoPageRoute(builder: (_) => getIt.get()); + return CupertinoPageRoute( + builder: (_) => getIt.get()); case Routes.rescan: return MaterialPageRoute(builder: (_) => getIt.get()); @@ -681,11 +740,13 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute(builder: (_) => getIt.get()); case Routes.walletGroupExistingSeedDescriptionPage: - return MaterialPageRoute(builder: (_) => WalletGroupExistingSeedDescriptionPage()); + return MaterialPageRoute( + builder: (_) => WalletGroupExistingSeedDescriptionPage()); case Routes.transactionSuccessPage: return MaterialPageRoute( - builder: (_) => getIt.get(param1: settings.arguments as String)); + builder: (_) => getIt.get( + param1: settings.arguments as String)); case Routes.backup: return handleRouteWithPlatformAwareness( @@ -693,11 +754,13 @@ Route createRoute(RouteSettings settings) { ); case Routes.editBackupPassword: - return CupertinoPageRoute(builder: (_) => getIt.get()); + return CupertinoPageRoute( + builder: (_) => getIt.get()); case Routes.restoreFromBackup: return CupertinoPageRoute( - fullscreenDialog: true, builder: (_) => getIt.get()); + fullscreenDialog: true, + builder: (_) => getIt.get()); case Routes.support: return handleRouteWithPlatformAwareness( @@ -705,7 +768,8 @@ Route createRoute(RouteSettings settings) { ); case Routes.supportLiveChat: - return CupertinoPageRoute(builder: (_) => getIt.get()); + return CupertinoPageRoute( + builder: (_) => getIt.get()); case Routes.supportOtherLinks: return handleRouteWithPlatformAwareness( @@ -715,7 +779,8 @@ Route createRoute(RouteSettings settings) { case Routes.unspentCoinsList: final coinTypeToSpendFrom = settings.arguments as UnspentCoinType?; return handleRouteWithPlatformAwareness( - (context) => getIt.get(param1: coinTypeToSpendFrom), + (context) => + getIt.get(param1: coinTypeToSpendFrom), ); case Routes.unspentCoinsDetails: @@ -752,7 +817,6 @@ Route createRoute(RouteSettings settings) { (context) => getIt.get(param1: args), ); - case Routes.cakePayAccountPage: return handleRouteWithPlatformAwareness( (context) => getIt.get(), @@ -782,41 +846,47 @@ Route createRoute(RouteSettings settings) { toggleUseTestnet: toggleTestnet, advancedPrivacySettingsViewModel: getIt.get(param1: type), - nodeViewModel: getIt.get(param1: type, param2: false), + nodeViewModel: + getIt.get(param1: type, param2: false), seedSettingsViewModel: getIt.get(), ), ); case Routes.anonPayInvoicePage: final args = settings.arguments as List; - return CupertinoPageRoute(builder: (_) => getIt.get(param1: args)); + return CupertinoPageRoute( + builder: (_) => getIt.get(param1: args)); case Routes.anonPayReceivePage: final anonReceivePageArgs = settings.arguments as AnonPayReceivePageArgs; return CupertinoPageRoute( - builder: (_) => getIt.get(param1: anonReceivePageArgs)); + builder: (_) => + getIt.get(param1: anonReceivePageArgs)); case Routes.anonPayDetailsPage: final anonInvoiceViewData = settings.arguments as AnonpayInvoiceInfo; return CupertinoPageRoute( - builder: (_) => getIt.get(param1: anonInvoiceViewData)); + builder: (_) => + getIt.get(param1: anonInvoiceViewData)); case Routes.payjoinDetails: final arguments = settings.arguments as List; final sessionId = arguments.first as String; final transactionInfo = arguments[1] as TransactionInfo?; return CupertinoPageRoute( - builder: (_) => - getIt.get(param1: sessionId, param2: transactionInfo)); + builder: (_) => getIt.get( + param1: sessionId, param2: transactionInfo)); case Routes.desktop_actions: return PageRouteBuilder( opaque: false, - pageBuilder: (_, __, ___) => DesktopDashboardActions(getIt()), + pageBuilder: (_, __, ___) => + DesktopDashboardActions(getIt()), ); case Routes.desktop_settings_page: - return CupertinoPageRoute(builder: (_) => getIt.get()); + return CupertinoPageRoute( + builder: (_) => getIt.get()); case Routes.empty_no_route: return MaterialPageRoute(builder: (_) => SizedBox.shrink()); @@ -831,17 +901,21 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute(builder: (_) => getIt.get()); case Routes.setup_2faQRPage: - return MaterialPageRoute(builder: (_) => getIt.get()); + return MaterialPageRoute( + builder: (_) => getIt.get()); case Routes.modify2FAPage: - return MaterialPageRoute(builder: (_) => getIt.get()); + return MaterialPageRoute( + builder: (_) => getIt.get()); case Routes.setup2faInfoPage: - return MaterialPageRoute(builder: (_) => getIt.get()); + return MaterialPageRoute( + builder: (_) => getIt.get()); case Routes.urqrAnimatedPage: return MaterialPageRoute( - builder: (_) => getIt.get(param1: settings.arguments)); + builder: (_) => + getIt.get(param1: settings.arguments)); case Routes.homeSettings: return CupertinoPageRoute( @@ -863,10 +937,12 @@ Route createRoute(RouteSettings settings) { ); case Routes.manageNodes: - return MaterialPageRoute(builder: (_) => getIt.get(param1: false)); + return MaterialPageRoute( + builder: (_) => getIt.get(param1: false)); case Routes.managePowNodes: - return MaterialPageRoute(builder: (_) => getIt.get(param1: true)); + return MaterialPageRoute( + builder: (_) => getIt.get(param1: true)); case Routes.walletConnectConnectionsListing: return MaterialPageRoute( @@ -902,7 +978,9 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute( builder: (_) => ConnectDevicePage( - params, getIt.get(param1: params.hardwareWalletType))); + params, + getIt.get( + param1: params.hardwareWalletType))); case Routes.walletGroupDescription: final walletType = settings.arguments as WalletType; @@ -942,12 +1020,12 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute( builder: (_) => getIt.get(), ); - + case Routes.devSocketHealthLogs: return CupertinoPageRoute( builder: (_) => getIt.get(), ); - + case Routes.devQRTools: return MaterialPageRoute( builder: (_) => getIt.get(), @@ -957,12 +1035,12 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute( builder: (_) => getIt.get(), ); - + case Routes.devExchangeProviderLogs: return MaterialPageRoute( builder: (_) => getIt.get(), ); - + case Routes.devMoneroCallProfiler: return MaterialPageRoute( builder: (_) => getIt.get(), @@ -977,7 +1055,7 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute( builder: (_) => getIt.get(), ); - + case Routes.startTor: return MaterialPageRoute( builder: (_) => getIt.get(), @@ -991,6 +1069,8 @@ Route createRoute(RouteSettings settings) { default: return MaterialPageRoute( builder: (_) => Scaffold( - body: Center(child: Text(S.current.router_no_route(settings.name ?? 'No route'))))); + body: Center( + child: Text(S.current + .router_no_route(settings.name ?? 'No route'))))); } } diff --git a/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart index 2c1dc74e96..2a708e4869 100644 --- a/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart +++ b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart @@ -1,4 +1,3 @@ -import 'dart:math' as math; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -20,36 +19,38 @@ class NewMainNavBar extends StatefulWidget { } class _NEWNewMainNavBarState extends State { - static const kBarFlex = 0.85; static const barHeight = 64.0; static const barBottomPadding = 32.0; static const iconWidth = 28.0; static const iconHeight = 28.0; + static const iconHorizontalPadding = 12.0; static const pillIconWidth = 20.0; static const pillIconHeight = 20.0; - static const pillIconSpacing = 8.0; - static const pillHorizontalPadding = 14.0; + static const pillIconSpacing = 4.0; + static const pillHorizontalPadding = 16.0; static const barBorderRadius = 50.0; static const pillBorderRadius = 50.0; - static const barResizeDuration = Duration(milliseconds: 400); + static const barHorizontalPadding = 12.0; + + static const barResizeDuration = Duration(milliseconds: 100); static const inactiveIconMoveDuration = Duration(milliseconds: 150); static const inactiveIconFadeDuration = Duration(milliseconds: 100); static const inactiveIconAppearDuration = Duration(milliseconds: 250); - static const pillMoveDuration = Duration(milliseconds: 300); - static const pillResizeDuration = Duration(milliseconds: 200); + static const pillMoveDuration = Duration(milliseconds: 150); + static const pillResizeDuration = Duration(milliseconds: 100); static const pillTextStyle = TextStyle( fontSize: 16, fontWeight: FontWeight.w500, ); - late int selectedIndex; - bool _fadeSelected = true; + int selectedIndex = 0; + bool _fadeSelected = false; bool _firstFrame = true; @override @@ -68,11 +69,11 @@ class _NEWNewMainNavBarState extends State { setState(() { selectedIndex = index; - _fadeSelected = false; + _fadeSelected = true; }); // delay fade (tweak duration) - Future.delayed(const Duration(milliseconds: 50), () { + Future.delayed(const Duration(milliseconds: 00), () { if (!mounted) return; if (index == selectedIndex) { setState(() => _fadeSelected = true); @@ -98,7 +99,21 @@ class _NEWNewMainNavBarState extends State { return pillIconWidth + pillIconSpacing + textPainter.width + - pillHorizontalPadding * 2; + pillHorizontalPadding; + } + + double calcLeft(int index, double pillWidth) { + final double baseOffset = (iconWidth+iconHorizontalPadding) * index; + + double additionalSpacing; + if (index > selectedIndex) additionalSpacing = pillWidth-iconWidth; + else additionalSpacing = 0; + + return baseOffset + additionalSpacing; + } + + double calcBarWidth(double pillWidth) { + return (iconWidth+iconHorizontalPadding)*NewMainActions.all.length+(pillWidth-iconWidth)+barHorizontalPadding; } @override @@ -115,56 +130,12 @@ class _NEWNewMainNavBarState extends State { (action) => action.canShow?.call(widget.dashboardViewModel) ?? true) .toList(); - final screenWidth = MediaQuery.of(context).size.width; final pillWidth = _estimatePillWidthForAction( context, visibleActions[selectedIndex], color: activeColor); - final baseWidth = screenWidth * 0.65; + final barWidth = calcBarWidth(pillWidth); - final double baselinePillWidth = - pillIconWidth + pillIconSpacing + (pillHorizontalPadding * 2) + 8; - - // Dynamic bar width - final barWidth = math.max( - baseWidth, - baseWidth + (pillWidth - baselinePillWidth) * kBarFlex, - ); - - final int itemCount = visibleActions.length; - const double edgePadding = 10.0; - final double firstItemLeft = edgePadding; - final double lastItemLeft = barWidth - pillWidth - edgePadding; - - // Center alignment for middle (3rd) icon - final double centerOfBar = barWidth / 2; - final double halfPill = pillWidth / 2; - final double centerItemLeft = centerOfBar - halfPill; - - // Base even spacing between first → center → last - final double secondItemLeft = - firstItemLeft + (centerItemLeft - firstItemLeft) / 2; - final double fourthItemLeft = - centerItemLeft + (lastItemLeft - centerItemLeft) / 2; - - // Spacing correction function - double spacingCorrection(int index) { - const double maxCorrection = 6.0; - final double factor = - (index - (itemCount - 1) / 2).abs() / ((itemCount - 1) / 2); - return maxCorrection * factor; - } - - // Apply correction: shift outer icons inward slightly - final List positions = [ - firstItemLeft + spacingCorrection(0), - secondItemLeft + spacingCorrection(1) / 2, - centerItemLeft, - fourthItemLeft - spacingCorrection(3) / 2, - lastItemLeft - spacingCorrection(4), - ]; - - final double left = positions[selectedIndex]; final currentAction = visibleActions[selectedIndex]; return Align( @@ -187,70 +158,67 @@ class _NEWNewMainNavBarState extends State { color: backgroundColor, borderRadius: BorderRadius.circular(barBorderRadius), ), - child: Stack( - alignment: Alignment.center, - children: [ - AnimatedPill( - left: left, - pillColor: pillColor, - currentAction: currentAction, - pillIconHeight: pillIconHeight, - pillIconWidth: pillIconWidth, - pillIconSpacing: pillIconSpacing, - pillBorderRadius: pillBorderRadius, - contentColor: activeColor, - estimateWidthForAction: pillWidth, - pillTextStyle: pillTextStyle, - pillMoveDuration: pillMoveDuration, - pillResizeDuration: pillResizeDuration, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - for (int i = 0; i < visibleActions.length; i++) - GestureDetector( - onTap: () => _onItemTap(i), - child: AnimatedContainer( - duration: _firstFrame - ? Duration.zero - : inactiveIconMoveDuration, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: barHorizontalPadding), + child: Stack( + alignment: Alignment.center, + children: [ + AnimatedPill( + left: calcLeft(selectedIndex, pillWidth), + pillColor: pillColor, + currentAction: currentAction, + pillIconHeight: pillIconHeight, + pillIconWidth: pillIconWidth, + pillIconSpacing: pillIconSpacing, + pillBorderRadius: pillBorderRadius, + contentColor: activeColor, + estimateWidthForAction: pillWidth, + pillTextStyle: pillTextStyle, + pillMoveDuration: pillMoveDuration, + pillResizeDuration: pillResizeDuration, + ), + for (int i = 0; i < visibleActions.length; i++) + AnimatedPositioned( + duration: pillResizeDuration, + left: calcLeft(i, pillWidth), + curve: Curves.easeOutCubic, + child: GestureDetector( + onTap: () => _onItemTap(i), + child: AnimatedContainer( + duration: _firstFrame + ? Duration.zero + : inactiveIconMoveDuration, + curve: Curves.easeOutCubic, + width: + i == selectedIndex ? pillWidth : iconWidth, + height: iconHeight, + alignment: Alignment.center, + child: AnimatedOpacity( + duration: inactiveIconFadeDuration, curve: Curves.easeOutCubic, - width: i == selectedIndex - ? pillWidth - : iconWidth, - height: iconHeight, - alignment: Alignment.center, - child: AnimatedOpacity( - duration: inactiveIconFadeDuration, + opacity: (i == selectedIndex && _fadeSelected) + ? 0.0 + : 1.0, + child: AnimatedScale( + duration: inactiveIconAppearDuration, curve: Curves.easeOutCubic, - opacity: - (i == selectedIndex && _fadeSelected) - ? 0.0 - : 1.0, - child: AnimatedScale( - duration: inactiveIconAppearDuration, - curve: Curves.easeOutCubic, - scale: - (i == selectedIndex) ? 0.95 : 1.0, - child: SvgPicture.asset( - visibleActions[i].image, - width: iconWidth, - height: iconHeight, - colorFilter: ColorFilter.mode( - inactiveColor, - BlendMode.srcIn, - ), + scale: (i == selectedIndex) ? 0.95 : 1.0, + child: SvgPicture.asset( + visibleActions[i].image, + width: iconWidth, + height: iconHeight, + colorFilter: ColorFilter.mode( + inactiveColor, + BlendMode.srcIn, ), ), ), ), ), - ], - ), - ) - ], + ), + ), + ], + ), )), ), ), @@ -294,60 +262,46 @@ class AnimatedPill extends StatelessWidget { @override Widget build(BuildContext context) { return AnimatedPositioned( - duration: pillMoveDuration, - curve: Curves.easeOutCubic, - left: left, - top: 12, - bottom: 12, - child: TweenAnimationBuilder( - tween: Tween( - begin: estimateWidthForAction, - end: estimateWidthForAction, - ), - duration: pillResizeDuration, + duration: pillMoveDuration, curve: Curves.easeOutCubic, - builder: (context, width, child) { - return AnimatedContainer( - duration: pillResizeDuration, - curve: Curves.easeOutCubic, - width: width + 4, - decoration: BoxDecoration( - color: pillColor, - borderRadius: BorderRadius.circular(pillBorderRadius), - ), - clipBehavior: Clip.hardEdge, - alignment: Alignment.center, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 2), - child: FittedBox( - fit: BoxFit.scaleDown, - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset( - currentAction.image, - width: pillIconWidth, - height: pillIconHeight, - colorFilter: ColorFilter.mode( - contentColor, - BlendMode.srcIn, - ), - ), - SizedBox(width: pillIconSpacing), - Text( - currentAction.name(context), - style: pillTextStyle.copyWith(color: contentColor), - overflow: TextOverflow.fade, - softWrap: false, - ), - ], + left: left, + top: 12, + bottom: 12, + child: AnimatedContainer( + duration: pillResizeDuration, + curve: Curves.easeOutCubic, + width: estimateWidthForAction, + decoration: BoxDecoration( + color: pillColor, + borderRadius: BorderRadius.circular(pillBorderRadius), + ), + clipBehavior: Clip.hardEdge, + alignment: Alignment.center, + child: FittedBox( + fit: BoxFit.scaleDown, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + currentAction.image, + width: pillIconWidth, + height: pillIconHeight, + colorFilter: ColorFilter.mode( + contentColor, + BlendMode.srcIn, + ), ), - ), + SizedBox(width: pillIconSpacing), + Text( + currentAction.name(context), + style: pillTextStyle.copyWith(color: contentColor), + overflow: TextOverflow.fade, + softWrap: false, + ), + ], ), - ); - }, - ), - ); + ), + )); } } diff --git a/lib/typography.dart b/lib/typography.dart index 816f116b41..c08a98e168 100644 --- a/lib/typography.dart +++ b/lib/typography.dart @@ -1,6 +1,8 @@ +import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:flutter/material.dart'; const latoFont = "Lato"; +const wixFont = "Wix Madefor Text"; TextStyle textXxSmall({Color? color}) => _cakeRegular(10, color); @@ -54,7 +56,7 @@ TextStyle _textStyle({ Color? color, }) => TextStyle( - fontFamily: latoFont, + fontFamily: FeatureFlag.hasNewUi ? wixFont : latoFont, fontSize: size, fontWeight: fontWeight, color: color ?? Colors.white, diff --git a/lib/utils/feature_flag.dart b/lib/utils/feature_flag.dart index 661595a414..4591aeffdf 100644 --- a/lib/utils/feature_flag.dart +++ b/lib/utils/feature_flag.dart @@ -10,7 +10,9 @@ class FeatureFlag { static const bool isBackgroundSyncEnabled = true; static bool get isInAppTorEnabled => CakeTor.instance is! CakeTorDisabled; static const int verificationWordsCount = kDebugMode ? 0 : 2; - static const bool hasDevOptions = bool.fromEnvironment('hasDevOptions', defaultValue: kDebugMode); + static const bool hasDevOptions = + bool.fromEnvironment('hasDevOptions', defaultValue: kDebugMode); static const bool hasBitcoinViewOnly = true; static const bool customBackgroundEnabled = false; + static const bool hasNewUi = true; } diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index a2cf0d738e..40f0fe0057 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -18,7 +18,6 @@ import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_item.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; @@ -422,16 +421,6 @@ abstract class ExchangeTradeViewModelBase with Store { case WalletType.ethereum: return _createERC681URI(fromCurrency, inputAddress, amount); // TODO: Expand ERC681URI support to Polygon(modify decoding flow for QRs, pay anything, and deep link handling) - case WalletType.polygon: - return PolygonURI(amount: amount, address: inputAddress); - case WalletType.base: - return BaseURI(amount: amount, address: inputAddress); - case WalletType.arbitrum: - return ArbitrumURI(amount: amount, address: inputAddress); - case WalletType.solana: - return SolanaURI(amount: amount, address: inputAddress); - case WalletType.tron: - return TronURI(amount: amount, address: inputAddress); case WalletType.monero: return MoneroURI(address: inputAddress, amount: amount); case WalletType.wownero: diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 217d7cf45d..aa7fe20fc5 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -100,8 +100,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor this.transactionDescriptionBox, this.hardwareWalletViewModel, this.unspentCoinsListViewModel, - this.feesViewModel, - this.walletInfoSource, { + this.feesViewModel, { this.coinTypeToSpendFrom = UnspentCoinType.nonMweb, }) : state = InitialExecutionState(), currencies = appStore.wallet!.balance.keys.toList(), @@ -873,7 +872,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor WalletType.banano, WalletType.solana, WalletType.tron, - WalletType.arbitrium + WalletType.arbitrum ].contains(wallet.type)) { throw Exception('Priority is null for wallet type: ${wallet.type}'); } @@ -1053,7 +1052,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor WalletType.polygon, WalletType.base, WalletType.haven, - WalletType.arbitrium + WalletType.arbitrum ].contains(walletType)) { if (errorMessage.contains('gas required exceeds allowance')) { return S.current.gas_exceeds_allowance; From c5399afa8f3240556f30e97a94c192a10f73d80d Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Sun, 16 Nov 2025 11:50:31 +0100 Subject: [PATCH 22/68] fix import --- lib/src/screens/send/send_page.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index cd3e695aa3..d6547b3cfe 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -21,6 +21,7 @@ import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.da import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; +import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/simple_checkbox.dart'; From c92afe6eb05481d16a94108242ab30d32eca1f71 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Wed, 29 Oct 2025 08:47:21 +0100 Subject: [PATCH 23/68] feat: add Lightning Network support for Bitcoin wallets --- .../lib/bitcoin_receive_page_option.dart | 7 + cw_bitcoin/lib/bitcoin_wallet.dart | 98 ++++++++------ cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 1 + cw_bitcoin/lib/electrum_wallet_addresses.dart | 30 ++++- .../lib/lightning/lightning_addres_type.dart | 22 ++++ .../lib/lightning/lightning_wallet.dart | 124 ++++++++++++++++++ .../pending_lightning_transaction.dart | 44 +++++++ cw_bitcoin/pubspec.lock | 9 ++ cw_bitcoin/pubspec.yaml | 3 + .../dashboard/balance_view_model.dart | 56 ++++---- 10 files changed, 324 insertions(+), 70 deletions(-) create mode 100644 cw_bitcoin/lib/lightning/lightning_addres_type.dart create mode 100644 cw_bitcoin/lib/lightning/lightning_wallet.dart create mode 100644 cw_bitcoin/lib/lightning/pending_lightning_transaction.dart diff --git a/cw_bitcoin/lib/bitcoin_receive_page_option.dart b/cw_bitcoin/lib/bitcoin_receive_page_option.dart index 8491ae8e3f..5e07ac63b8 100644 --- a/cw_bitcoin/lib/bitcoin_receive_page_option.dart +++ b/cw_bitcoin/lib/bitcoin_receive_page_option.dart @@ -1,4 +1,5 @@ import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/lightning/lightning_addres_type.dart'; import 'package:cw_core/receive_page_option.dart'; class BitcoinReceivePageOption implements ReceivePageOption { @@ -10,6 +11,7 @@ class BitcoinReceivePageOption implements ReceivePageOption { static const mweb = BitcoinReceivePageOption._('MWEB'); static const silent_payments = BitcoinReceivePageOption._('Silent Payments'); + static const lightning = BitcoinReceivePageOption._('Lightning'); const BitcoinReceivePageOption._(this.value); @@ -20,6 +22,7 @@ class BitcoinReceivePageOption implements ReceivePageOption { } static const all = [ + BitcoinReceivePageOption.lightning, BitcoinReceivePageOption.silent_payments, BitcoinReceivePageOption.p2wpkh, BitcoinReceivePageOption.p2tr, @@ -55,6 +58,8 @@ class BitcoinReceivePageOption implements ReceivePageOption { return P2shAddressType.p2wpkhInP2sh; case BitcoinReceivePageOption.silent_payments: return SilentPaymentsAddresType.p2sp; + case BitcoinReceivePageOption.lightning: + return LightningAddressType.p2l; case BitcoinReceivePageOption.mweb: return SegwitAddresType.mweb; case BitcoinReceivePageOption.p2wpkh: @@ -77,6 +82,8 @@ class BitcoinReceivePageOption implements ReceivePageOption { return BitcoinReceivePageOption.p2sh; case SilentPaymentsAddresType.p2sp: return BitcoinReceivePageOption.silent_payments; + case LightningAddressType.p2l: + return BitcoinReceivePageOption.lightning; case SegwitAddresType.p2wpkh: default: return BitcoinReceivePageOption.p2wpkh; diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 0a2b54913a..6b3cc56f64 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -13,6 +13,7 @@ import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; import 'package:cw_bitcoin/hardware/bitcoin_hardware_wallet_service.dart'; +import 'package:cw_bitcoin/lightning/lightning_wallet.dart'; import 'package:cw_bitcoin/payjoin/manager.dart'; import 'package:cw_bitcoin/payjoin/storage.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; @@ -23,6 +24,7 @@ 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/parse_fixed.dart'; import 'package:cw_core/payjoin_session.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/unspent_coins_info.dart'; @@ -31,9 +33,7 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; -import 'package:ledger_bitcoin/ledger_bitcoin.dart'; import 'package:ledger_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'; @@ -92,24 +92,38 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { // String sideDerivationPath = derivationPath.substring(0, derivationPath.length - 1) + "1"; // final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType); + if (mnemonic != null) { + lightningWallet = LightningWallet( + mnemonic: mnemonic, + apiKey: + "MIIBdzCCASmgAwIBAgIHPpJHKP1qXzAFBgMrZXAwEDEOMAwGA1UEAxMFQnJlZXowHhcNMjUxMDIzMTQwNDQ4WhcNMzUxMDIxMTQwNDQ4WjAxMRQwEgYDVQQKEwtDYWtlIFdhbGxldDEZMBcGA1UEAxMQU2V0aCBGb3IgUHJpdmFjeTAqMAUGAytlcAMhANCD9cvfIDwcoiDKKYdT9BunHLS2/OuKzV8NS0SzqV13o4GAMH4wDgYDVR0PAQH/BAQDAgWgMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFNo5o+5ea0sNMlW/75VgGJCv2AcJMB8GA1UdIwQYMBaAFN6q1pJW843ndJIW/Ey2ILJrKJhrMB4GA1UdEQQXMBWBE3NldGhAY2FrZXdhbGxldC5jb20wBQYDK2VwA0EAl+naPfCBseV7eS4SoP0q0kvo2GHCywXoIbnlBa0y+/wlfu+oILtsGv3jGQ2egCnpgHe87yzR0ygclzz8r/jdAQ==", + lnurlDomain: "breez.tips", + ); + } + payjoinManager = PayjoinManager(PayjoinStorage(payjoinBox), this); - walletAddresses = BitcoinWalletAddresses(walletInfo, - initialAddresses: initialAddresses, - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex, - initialSilentAddresses: initialSilentAddresses, - initialSilentAddressIndex: initialSilentAddressIndex, - mainHd: hd, - sideHd: accountHD.childKey(Bip32KeyIndex(1)), - network: networkParam ?? network, - masterHd: - seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null, - isHardwareWallet: walletInfo.isHardwareWallet, - payjoinManager: payjoinManager); + walletAddresses = BitcoinWalletAddresses( + walletInfo, + initialAddresses: initialAddresses, + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, + initialSilentAddresses: initialSilentAddresses, + initialSilentAddressIndex: initialSilentAddressIndex, + mainHd: hd, + sideHd: accountHD.childKey(Bip32KeyIndex(1)), + network: networkParam ?? network, + masterHd: seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null, + isHardwareWallet: walletInfo.isHardwareWallet, + payjoinManager: payjoinManager, + lightningWallet: lightningWallet, + ); + + if (lightningWallet != null) { + walletAddresses.setLightningAddress(walletInfo.name); + } autorun((_) { - this.walletAddresses.isEnabledAutoGenerateSubaddress = - this.isEnabledAutoGenerateSubaddress; + this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; }); } @@ -146,8 +160,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { break; case DerivationType.electrum: default: - seedBytes = - await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); + seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); break; } @@ -233,8 +246,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { if (mnemonic != null) { switch (derivationInfo.derivationType) { case DerivationType.electrum: - seedBytes = - await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); + seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); break; case DerivationType.bip39: default: @@ -274,11 +286,24 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { super.close(shouldCleanup: shouldCleanup); } + @override + Future fetchBalances() async { + final balance = await super.fetchBalances(); + if (lightningWallet == null) { + return balance; + } + + final lBalance = await lightningWallet!.getBalance(); + + return ElectrumBalance(confirmed: balance.confirmed, unconfirmed: balance.unconfirmed, frozen: balance.frozen, secondConfirmed: lBalance.toInt()); + } + + late final LightningWallet? lightningWallet; + late final PayjoinManager payjoinManager; bool get isPayjoinAvailable => unspentCoinsInfo.values - .where((element) => - element.walletId == id && element.isSending && !element.isFrozen) + .where((element) => element.walletId == id && element.isSending && !element.isFrozen) .isNotEmpty; Future buildPsbt({ @@ -296,10 +321,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { }) async { final psbtReadyInputs = []; for (final utxo in utxos) { - final rawTx = - await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); - final publicKeyAndDerivationPath = - publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; + final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); + final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; psbtReadyInputs.add(PSBTReadyUtxoWithAddress( utxo: utxo.utxo, @@ -355,8 +378,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Future createTransaction(Object credentials) async { credentials = credentials as BitcoinTransactionCredentials; - final tx = (await super.createTransaction(credentials)) - as PendingBitcoinTransaction; + if (lightningWallet?.isCompatible(credentials.outputs.first.address) == true) { + return lightningWallet!.createTransaction(credentials.outputs.first.address, + parseFixed(credentials.outputs.first.cryptoAmount ?? "0", 9)); + } + + final tx = (await super.createTransaction(credentials)) as PendingBitcoinTransaction; final payjoinUri = credentials.payjoinUri; if (payjoinUri == null && !tx.shouldCommitUR()) return tx; @@ -381,8 +408,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { masterFingerprint: Uint8List.fromList([0, 0, 0, 0])); if (tx.shouldCommitUR()) { - tx.unsignedPsbt = transaction.asPsbtV0(); - return tx; + tx.unsignedPsbt = transaction.asPsbtV0(); + return tx; } final originalPsbt = @@ -406,8 +433,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Future commitPsbt(String finalizedPsbt) { final psbt = PsbtV2()..deserializeV0(base64.decode(finalizedPsbt)); - final btcTx = - BtcTransaction.fromRaw(BytesUtils.toHexString(psbt.extract())); + final btcTx = BtcTransaction.fromRaw(BytesUtils.toHexString(psbt.extract())); return PendingBitcoinTransaction( btcTx, @@ -422,8 +448,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { ).commit(); } - Future signPsbt( - String preProcessedPsbt, List utxos) async { + Future signPsbt(String preProcessedPsbt, List utxos) async { final psbt = PsbtV2()..deserializeV0(base64Decode(preProcessedPsbt)); await psbt.signWithUTXO(utxos, (txDigest, utxo, key, sighash) { @@ -486,8 +511,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Future signMessage(String message, {String? address = null}) async { if (walletInfo.isHardwareWallet) { final addressEntry = address != null - ? walletAddresses.allAddresses - .firstWhere((element) => element.address == address) + ? walletAddresses.allAddresses.firstWhere((element) => element.address == address) : null; final index = addressEntry?.index ?? 0; final isChange = addressEntry?.isHidden == true ? 1 : 0; diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index fcd0b7d8cc..8e36ffebf8 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -27,6 +27,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S super.initialSilentAddresses, super.initialSilentAddressIndex = 0, super.masterHd, + super.lightningWallet, }) : super(walletInfo); final PayjoinManager payjoinManager; diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 18d2898b4d..eddcadb563 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -3,9 +3,12 @@ import 'dart:io' show Platform; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/bitcoin_unspent.dart'; +import 'package:cw_bitcoin/lightning/lightning_addres_type.dart'; +import 'package:cw_bitcoin/lightning/lightning_wallet.dart'; +import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -51,6 +54,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { List? initialMwebAddresses, Bip32Slip10Secp256k1? masterHd, BitcoinAddressType? initialAddressPageType, + this.lightningWallet, }) : _addresses = ObservableList.of((initialAddresses ?? []).toSet()), addressesByReceiveType = ObservableList.of(([]).toSet()), @@ -64,7 +68,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { currentChangeAddressIndexByType = initialChangeAddressIndex ?? {}, _addressPageType = initialAddressPageType ?? (walletInfo.addressPageType != null - ? BitcoinAddressType.fromValue(walletInfo.addressPageType!) + ? walletInfo.addressPageType == LightningAddressType.p2l.value + ? LightningAddressType.p2l + : BitcoinAddressType.fromValue(walletInfo.addressPageType!) : SegwitAddresType.p2wpkh), silentAddresses = ObservableList.of( (initialSilentAddresses ?? []).toSet()), @@ -103,7 +109,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { )); } } - updateAddressesByMatch(); } @@ -123,6 +128,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final Bip32Slip10Secp256k1 mainHd; final Bip32Slip10Secp256k1 sideHd; final bool isHardwareWallet; + final LightningWallet? lightningWallet; @observable ObservableMap lockedReceiveAddressByType; @@ -139,6 +145,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @observable String? activeSilentAddress; + @observable + String? lightningAddress; + @computed List get allAddresses => _addresses; @@ -153,6 +162,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return silentAddress.toString(); } + if (addressPageType == LightningAddressType.p2l) { + return lightningAddress ?? ":("; + } + final typeMatchingAddresses = _addresses.where((addr) => !addr.isHidden && _isAddressPageTypeMatch(addr)).toList(); final typeMatchingReceiveAddresses = @@ -220,7 +233,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { addressRecord.type == addressPageType) { lockedReceiveAddressByType[addressPageType] = addr; } - } catch (e) { printV("ElectrumWalletAddressBase: set address ($addr): $e"); } @@ -736,4 +748,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { silentAddresses.remove(addressRecord); updateAddressesByMatch(); } + + @action + Future setLightningAddress(String walletName) async { + if (lightningWallet == null) return; + + final path = await pathForWalletDir(name: walletName, type: WalletType.bitcoin); + await lightningWallet!.init(path); + lightningAddress = await lightningWallet!.registerAddress(walletName.replaceAll(" ", "")); + + } } diff --git a/cw_bitcoin/lib/lightning/lightning_addres_type.dart b/cw_bitcoin/lib/lightning/lightning_addres_type.dart new file mode 100644 index 0000000000..275867b34f --- /dev/null +++ b/cw_bitcoin/lib/lightning/lightning_addres_type.dart @@ -0,0 +1,22 @@ +import 'package:bitcoin_base/src/bitcoin/address/address.dart'; + +class LightningAddressType implements BitcoinAddressType { + const LightningAddressType._(this.value); + static const LightningAddressType p2l = LightningAddressType._("Lightning"); + + @override + bool get isP2sh => false; + @override + bool get isSegwit => false; + + @override + final String value; + + @override + int get hashLength { + return 32; + } + + @override + String toString() => value; +} diff --git a/cw_bitcoin/lib/lightning/lightning_wallet.dart b/cw_bitcoin/lib/lightning/lightning_wallet.dart new file mode 100644 index 0000000000..cf2520c16a --- /dev/null +++ b/cw_bitcoin/lib/lightning/lightning_wallet.dart @@ -0,0 +1,124 @@ +import 'package:breez_sdk_spark_flutter/breez_sdk_spark.dart'; +import 'package:cw_bitcoin/lightning/pending_lightning_transaction.dart'; +import 'package:cw_core/pending_transaction.dart'; + +class LightningWallet { + final String mnemonic; + final String apiKey; + final String lnurlDomain; + final Network network; + late BreezSdk sdk; + + LightningWallet({ + required this.mnemonic, + required this.apiKey, + required this.lnurlDomain, + this.network = Network.mainnet, + }); + + Future init(String appPath) async { + await BreezSdkSparkLib.init(); + + final seed = Seed.mnemonic(mnemonic: mnemonic, passphrase: null); + final config = defaultConfig(network: Network.mainnet).copyWith( + lnurlDomain: lnurlDomain, + apiKey: apiKey, + ); + + final connectRequest = ConnectRequest( + config: config, + seed: seed, + storageDir: "$appPath/", + ); + + sdk = await connect(request: connectRequest); + } + + Future getAddress() async => (await sdk.getLightningAddress())?.lightningAddress; + + Future getBalance() async => + (await sdk.getInfo(request: GetInfoRequest(ensureSynced: true))).balanceSats; + + Future registerAddress(String username) async => (await sdk.registerLightningAddress( + request: RegisterLightningAddressRequest(username: username))) + .lightningAddress; + + Future isCompatible(String input) async { + try { + final inputType = await sdk.parse(input: input); + return (inputType is InputType_Bolt11Invoice) || (inputType is InputType_LightningAddress); + } catch (_) { + return false; + } + } + + Future createTransaction(String address, BigInt? amountSats) async { + final inputType = await sdk.parse(input: address); + + if (inputType is InputType_Bolt11Invoice) { + final request = PrepareSendPaymentRequest( + paymentRequest: inputType.field0.invoice.bolt11, amount: amountSats); + final prepareResponse = await sdk.prepareSendPayment(request: request); + + final paymentMethod = prepareResponse.paymentMethod; + if (paymentMethod is SendPaymentMethod_Bolt11Invoice) { + // Fees to pay via Lightning + final lightningFeeSats = paymentMethod.lightningFeeSats; + // Or fees to pay (if available) via a Spark transfer + final sparkTransferFeeSats = paymentMethod.sparkTransferFeeSats; + + return PendingLightningTransaction( + id: paymentMethod.invoiceDetails.paymentHash, + amount: paymentMethod.invoiceDetails.amountMsat?.toInt() ?? 0, + fee: lightningFeeSats.toInt() + (sparkTransferFeeSats?.toInt() ?? 0), + commitOverride: () => + sdk.sendPayment(request: SendPaymentRequest(prepareResponse: prepareResponse)), + ); + } + } else if (inputType is InputType_LightningAddress) { + final optionalValidateSuccessActionUrl = true; + + final request = PrepareLnurlPayRequest( + amountSats: amountSats!, + payRequest: inputType.field0.payRequest, + validateSuccessActionUrl: optionalValidateSuccessActionUrl, + ); + final prepareResponse = await sdk.prepareLnurlPay(request: request); + + final feeSats = prepareResponse.feeSats; + + return PendingLightningTransaction( + id: prepareResponse.invoiceDetails.paymentHash, + amount: prepareResponse.invoiceDetails.amountMsat?.toInt() ?? 0, + fee: feeSats.toInt(), + commitOverride: () => + sdk.lnurlPay(request: LnurlPayRequest(prepareResponse: prepareResponse)), + ); + } + + // If not returned earlier + throw UnimplementedError(); + } +} + +extension _ConfigCopyWith on Config { + Config copyWith({ + String? apiKey, + String? lnurlDomain, + Network? network, + int? syncIntervalSecs, + Fee? maxDepositClaimFee, + bool? preferSparkOverLightning, + bool? useDefaultExternalInputParsers, + }) => + Config( + lnurlDomain: lnurlDomain ?? this.lnurlDomain, + apiKey: apiKey ?? this.apiKey, + network: network ?? this.network, + syncIntervalSecs: syncIntervalSecs ?? this.syncIntervalSecs, + maxDepositClaimFee: maxDepositClaimFee ?? this.maxDepositClaimFee, + preferSparkOverLightning: preferSparkOverLightning ?? this.preferSparkOverLightning, + useDefaultExternalInputParsers: + useDefaultExternalInputParsers ?? this.useDefaultExternalInputParsers, + ); +} diff --git a/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart b/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart new file mode 100644 index 0000000000..72b4423783 --- /dev/null +++ b/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart @@ -0,0 +1,44 @@ +import 'package:cw_bitcoin/bitcoin_amount_format.dart'; +import 'package:cw_core/pending_transaction.dart'; + +class PendingLightningTransaction with PendingTransaction { + PendingLightningTransaction({ + required this.id, + required this.amount, + required this.fee, + this.isSendAll = false, + required this.commitOverride, + }); + + final int amount; + final int fee; + final bool isSendAll; + Future Function() commitOverride; + + @override + final String id; + + @override + String get hex => ""; + + @override + String get amountFormatted => bitcoinAmountToString(amount: amount); + + @override + String get feeFormatted => "$feeFormattedValue BTC"; + + @override + String get feeFormattedValue => bitcoinAmountToString(amount: fee); + + @override + int? get outputCount => 1; + + @override + Future commit() => commitOverride.call(); + + @override + bool shouldCommitUR() => false; + + @override + Future> commitUR() => throw UnimplementedError(); +} diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 681e68d478..38aea938ca 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -130,6 +130,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + breez_sdk_spark_flutter: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "5bc8fb5f3a5c84e2e3dd55f5d48b01152f425765" + url: "https://github.com/breez/breez-sdk-spark-flutter" + source: git + version: "0.3.2" bs58check: dependency: transitive description: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 942a5069a2..940ce80b6a 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -72,6 +72,9 @@ dependencies: git: url: https://github.com/mrcyjanek/bbqrdart ref: e867e3d0156d0b29858100f30adc2625b9dae586 + breez_sdk_spark_flutter: + git: + url: https://github.com/breez/breez-sdk-spark-flutter dev_dependencies: flutter_test: diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 552d318e4a..8d57356f37 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -1,27 +1,26 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/entities/balance_display_mode.dart'; +import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/sort_balance_types.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; -import 'package:cw_core/transaction_history.dart'; -import 'package:cw_core/wallet_base.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/entities/balance_display_mode.dart'; -import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; -import 'package:cake_wallet/store/app_store.dart'; -import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:mobx/mobx.dart'; part 'balance_view_model.g.dart'; class BalanceRecord { const BalanceRecord( - { - required this.availableBalance, + {required this.availableBalance, required this.additionalBalance, required this.secondAvailableBalance, required this.secondAdditionalBalance, @@ -150,18 +149,15 @@ abstract class BalanceViewModelBase with Store { @computed String get availableBalanceLabel { - if (displayMode == BalanceDisplayMode.hiddenBalance) { return S.current.show_balance; - } - else { + } else { return S.current.xmr_available_balance; } } @computed String get additionalBalanceLabel { - switch (wallet.type) { case WalletType.haven: case WalletType.ethereum: @@ -225,8 +221,10 @@ abstract class BalanceViewModelBase with Store { fiatAdditionalBalance: isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●', fiatAvailableBalance: isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●', fiatFrozenBalance: isFiatDisabled ? '' : '', - fiatSecondAvailableBalance: isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●', - fiatSecondAdditionalBalance: isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●', + fiatSecondAvailableBalance: + isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●', + fiatSecondAdditionalBalance: + isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●', asset: key, formattedAssetTitle: _formatterAsset(key))); } @@ -300,8 +298,16 @@ abstract class BalanceViewModelBase with Store { mwebEnabled && _hasSecondAdditionalBalanceForWalletType(wallet.type); @computed - bool get hasSecondAvailableBalance => - mwebEnabled && _hasSecondAvailableBalanceForWalletType(wallet.type); + bool get hasSecondAvailableBalance { + switch (wallet.type) { + case WalletType.bitcoin: + return true; + case WalletType.litecoin: + return mwebEnabled; + default: + return false; + } + } bool _hasAdditionalBalanceForWalletType(WalletType type) { switch (type) { @@ -317,16 +323,9 @@ abstract class BalanceViewModelBase with Store { bool _hasSecondAdditionalBalanceForWalletType(WalletType type) { if (wallet.type == WalletType.litecoin) { - if ((wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) != 0) { - return true; - } - } - return false; - } - - bool _hasSecondAvailableBalanceForWalletType(WalletType type) { - if (wallet.type == WalletType.litecoin) { - return true; + return (wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) != 0; + } else if (wallet.type == WalletType.bitcoin) { + return (wallet.balance[CryptoCurrency.btc]?.secondAdditional ?? 0) != 0; } return false; } @@ -395,7 +394,6 @@ abstract class BalanceViewModelBase with Store { return balance; } - @observable bool isShowCard; From c03783e9314c3ac081f81682e7662fff3e814cae Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Wed, 29 Oct 2025 12:32:44 +0100 Subject: [PATCH 24/68] refactor: rename `fiatConvertationStore` to `fiatConversionStore` for consistency and update related occurrences across codebase --- lib/di.dart | 2 +- .../dashboard/balance_view_model.dart | 143 ++++++------------ .../dashboard/home_settings_view_model.dart | 2 +- .../dashboard/transaction_list_item.dart | 12 +- 4 files changed, 58 insertions(+), 101 deletions(-) diff --git a/lib/di.dart b/lib/di.dart index fde64bd72d..4138842595 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -543,7 +543,7 @@ Future setup({ getIt.registerFactory(() => BalanceViewModel( appStore: getIt.get(), settingsStore: getIt.get(), - fiatConvertationStore: getIt.get())); + fiatConversionStore: getIt.get())); getIt.registerFactory( () => ExchangeViewModel( diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 8d57356f37..ef203ceae6 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -19,19 +19,20 @@ import 'package:mobx/mobx.dart'; part 'balance_view_model.g.dart'; class BalanceRecord { - const BalanceRecord( - {required this.availableBalance, - required this.additionalBalance, - required this.secondAvailableBalance, - required this.secondAdditionalBalance, - required this.frozenBalance, - required this.fiatAvailableBalance, - required this.fiatAdditionalBalance, - required this.fiatFrozenBalance, - required this.fiatSecondAvailableBalance, - required this.fiatSecondAdditionalBalance, - required this.asset, - required this.formattedAssetTitle}); + const BalanceRecord({ + required this.availableBalance, + required this.additionalBalance, + required this.secondAvailableBalance, + required this.secondAdditionalBalance, + required this.frozenBalance, + required this.fiatAvailableBalance, + required this.fiatAdditionalBalance, + required this.fiatFrozenBalance, + required this.fiatSecondAvailableBalance, + required this.fiatSecondAdditionalBalance, + required this.asset, + required this.formattedAssetTitle, + }); final String fiatAdditionalBalance; final String fiatAvailableBalance; @@ -51,7 +52,7 @@ class BalanceViewModel = BalanceViewModelBase with _$BalanceViewModel; abstract class BalanceViewModelBase with Store { BalanceViewModelBase( - {required this.appStore, required this.settingsStore, required this.fiatConvertationStore}) + {required this.appStore, required this.settingsStore, required this.fiatConversionStore}) : isReversing = false, isShowCard = appStore.wallet?.walletInfo.isShowIntroCakePayCard ?? false, wallet = appStore.wallet! { @@ -62,9 +63,7 @@ abstract class BalanceViewModelBase with Store { _checkMweb(); - reaction((_) => settingsStore.mwebAlwaysScan, (bool value) { - _checkMweb(); - }); + reaction((_) => settingsStore.mwebAlwaysScan, (_) => _checkMweb()); } void _checkMweb() { @@ -75,9 +74,7 @@ abstract class BalanceViewModelBase with Store { final AppStore appStore; final SettingsStore settingsStore; - final FiatConversionStore fiatConvertationStore; - - bool get canReverse => false; + final FiatConversionStore fiatConversionStore; @observable bool isReversing; @@ -85,17 +82,12 @@ abstract class BalanceViewModelBase with Store { @observable WalletBase, TransactionInfo> wallet; - @computed - bool get hasSilentPayments => wallet.type == WalletType.bitcoin && !wallet.isHardwareWallet; - @computed double get price { - final price = fiatConvertationStore.prices[appStore.wallet!.currency]; + final price = fiatConversionStore.prices[appStore.wallet!.currency]; - if (price == null) { - // price should update on next fetch: - return 0; - } + // price should update on next fetch: + if (price == null) return 0; return price; } @@ -109,12 +101,10 @@ abstract class BalanceViewModelBase with Store { @computed bool get isHomeScreenSettingsEnabled => isEVMCompatibleChain(wallet.type) || - wallet.type == WalletType.solana || - wallet.type == WalletType.tron || - wallet.type == WalletType.zano; + [WalletType.solana, WalletType.tron, WalletType.zano].contains(wallet.type); @computed - bool get hasAccounts => wallet.type == WalletType.monero || wallet.type == WalletType.wownero; + bool get hasAccounts => [WalletType.monero, WalletType.wownero].contains(wallet.type); @computed SortBalanceBy get sortBalanceBy => settingsStore.sortBalanceBy; @@ -198,9 +188,7 @@ abstract class BalanceViewModelBase with Store { String additionalBalance(CryptoCurrency cryptoCurrency) { final balance = _currencyBalance(cryptoCurrency); - if (displayMode == BalanceDisplayMode.hiddenBalance) { - return '0.0'; - } + if (displayMode == BalanceDisplayMode.hiddenBalance) return '0.0'; return balance.formattedAdditionalBalance; } @@ -229,7 +217,7 @@ abstract class BalanceViewModelBase with Store { formattedAssetTitle: _formatterAsset(key))); } final fiatCurrency = settingsStore.fiatCurrency; - final price = key.isPotentialScam ? 0.0 : fiatConvertationStore.prices[key] ?? 0; + final price = key.isPotentialScam ? 0.0 : fiatConversionStore.prices[key] ?? 0; // if (price == null) { // throw Exception('Price is null for: $key'); @@ -287,15 +275,21 @@ abstract class BalanceViewModelBase with Store { bool mwebEnabled = false; bool hasAdditionalBalance(CryptoCurrency currency) { - bool isWalletTypeActivated = _hasAdditionalBalanceForWalletType(wallet.type); - bool isNotZeroAmount = additionalBalance(currency) != "0.0"; + final isWalletTypeActivated = _hasAdditionalBalanceForWalletType(wallet.type); + final isNotZeroAmount = additionalBalance(currency) != "0.0"; return isWalletTypeActivated && isNotZeroAmount; } @computed - bool get hasSecondAdditionalBalance => - mwebEnabled && _hasSecondAdditionalBalanceForWalletType(wallet.type); + bool get hasSecondAdditionalBalance { + if (wallet.type == WalletType.litecoin && mwebEnabled) { + return (wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) != 0; + } else if (wallet.type == WalletType.bitcoin) { + return (wallet.balance[CryptoCurrency.btc]?.secondAdditional ?? 0) != 0; + } + return false; + } @computed bool get hasSecondAvailableBalance { @@ -309,26 +303,8 @@ abstract class BalanceViewModelBase with Store { } } - bool _hasAdditionalBalanceForWalletType(WalletType type) { - switch (type) { - case WalletType.monero: - case WalletType.wownero: - case WalletType.zano: - case WalletType.decred: - return true; - default: - return false; - } - } - - bool _hasSecondAdditionalBalanceForWalletType(WalletType type) { - if (wallet.type == WalletType.litecoin) { - return (wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) != 0; - } else if (wallet.type == WalletType.bitcoin) { - return (wallet.balance[CryptoCurrency.btc]?.secondAdditional ?? 0) != 0; - } - return false; - } + bool _hasAdditionalBalanceForWalletType(WalletType type) => + [WalletType.monero, WalletType.wownero, WalletType.zano, WalletType.decred].contains(type); @computed List get formattedBalances { @@ -336,25 +312,15 @@ abstract class BalanceViewModelBase with Store { balance.sort((BalanceRecord a, BalanceRecord b) { if (wallet.currency == CryptoCurrency.xhv) { - if (b.asset == CryptoCurrency.xhv) { - return 1; - } + if (b.asset == CryptoCurrency.xhv) return 1; if (b.asset == CryptoCurrency.xusd) { - if (a.asset == CryptoCurrency.xhv) { - return -1; - } - - return 1; - } - - if (b.asset == CryptoCurrency.xbtc) { + if (a.asset == CryptoCurrency.xhv) return -1; return 1; } - if (b.asset == CryptoCurrency.xeur) { - return 1; - } + if (b.asset == CryptoCurrency.xbtc) return 1; + if (b.asset == CryptoCurrency.xeur) return 1; return 0; } @@ -367,9 +333,9 @@ abstract class BalanceViewModelBase with Store { switch (sortBalanceBy) { case SortBalanceBy.FiatBalance: final aFiatBalance = _getFiatBalance( - price: fiatConvertationStore.prices[a.asset] ?? 0, cryptoAmount: a.availableBalance); + price: fiatConversionStore.prices[a.asset] ?? 0, cryptoAmount: a.availableBalance); final bFiatBalance = _getFiatBalance( - price: fiatConvertationStore.prices[b.asset] ?? 0, cryptoAmount: b.availableBalance); + price: fiatConversionStore.prices[b.asset] ?? 0, cryptoAmount: b.availableBalance); return (double.tryParse(bFiatBalance) ?? 0) .compareTo((double.tryParse(aFiatBalance)) ?? 0); @@ -387,9 +353,7 @@ abstract class BalanceViewModelBase with Store { Balance _currencyBalance(CryptoCurrency cryptoCurrency) { final balance = wallet.balance[cryptoCurrency]; - if (balance == null) { - throw Exception('No balance for ${wallet.currency}'); - } + if (balance == null) throw Exception('No balance for ${wallet.currency}'); return balance; } @@ -402,9 +366,7 @@ abstract class BalanceViewModelBase with Store { @action void _onWalletChange( WalletBase, TransactionInfo>? wallet) { - if (wallet == null) { - return; - } + if (wallet == null) return; this.wallet = wallet; _onCurrentWalletChangeReaction?.reaction.dispose(); @@ -437,18 +399,13 @@ abstract class BalanceViewModelBase with Store { } String _formatterAsset(CryptoCurrency asset) { - switch (wallet.type) { - case WalletType.haven: - final assetStringified = asset.toString(); - - if (asset != CryptoCurrency.xhv && assetStringified[0].toUpperCase() == 'X') { - return assetStringified.replaceFirst('X', 'x'); - } - - return asset.toString(); - default: - return asset.toString(); + final assetString = asset.toString(); + if (wallet.type == WalletType.haven && asset != CryptoCurrency.xhv && + assetString[0].toUpperCase() == 'X') { + return assetString.replaceFirst('X', 'x'); } + + return asset.toString(); } String getFormattedFrozenBalance(Balance walletBalance) => diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index 7ec420de73..ec55b00578 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -427,7 +427,7 @@ abstract class HomeSettingsViewModelBase with Store { void _updateFiatPrices(CryptoCurrency token) async { if (token.isPotentialScam) return; // don't fetch price data for potential scam tokens try { - _balanceViewModel.fiatConvertationStore.prices[token] = + _balanceViewModel.fiatConversionStore.prices[token] = await FiatConversionService.fetchPrice( crypto: token, fiat: _settingsStore.fiatCurrency, diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 33df3ea241..8336a2374c 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -185,21 +185,21 @@ class TransactionListItem extends ActionListItem with Keyable { break; case WalletType.ethereum: final asset = ethereum!.assetOfTransaction(balanceViewModel.wallet, transaction); - final price = balanceViewModel.fiatConvertationStore.prices[asset]; + final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( cryptoAmount: ethereum!.formatterEthereumAmountToDouble(transaction: transaction), price: price); break; case WalletType.polygon: final asset = polygon!.assetOfTransaction(balanceViewModel.wallet, transaction); - final price = balanceViewModel.fiatConvertationStore.prices[asset]; + final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( cryptoAmount: polygon!.formatterPolygonAmountToDouble(transaction: transaction), price: price); break; case WalletType.base: final asset = base!.assetOfTransaction(balanceViewModel.wallet, transaction); - final price = balanceViewModel.fiatConvertationStore.prices[asset]; + final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( cryptoAmount: base!.formatterBaseAmountToDouble(transaction: transaction), price: price); @@ -219,7 +219,7 @@ class TransactionListItem extends ActionListItem with Keyable { break; case WalletType.solana: final asset = solana!.assetOfTransaction(balanceViewModel.wallet, transaction); - final price = balanceViewModel.fiatConvertationStore.prices[asset]; + final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( cryptoAmount: solana!.getTransactionAmountRaw(transaction), price: price, @@ -227,7 +227,7 @@ class TransactionListItem extends ActionListItem with Keyable { break; case WalletType.tron: final asset = tron!.assetOfTransaction(balanceViewModel.wallet, transaction); - final price = balanceViewModel.fiatConvertationStore.prices[asset]; + final price = balanceViewModel.fiatConversionStore.prices[asset]; final cryptoAmount = tron!.getTransactionAmountRaw(transaction); amount = calculateFiatAmountRaw( cryptoAmount: cryptoAmount, @@ -240,7 +240,7 @@ class TransactionListItem extends ActionListItem with Keyable { amount = "0.00"; break; } - final price = balanceViewModel.fiatConvertationStore.prices[asset]; + final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( cryptoAmount: zano!.formatterIntAmountToDouble(amount: transaction.amount, currency: asset, forFee: false), price: price); From 7e26e842f8270f91ce3d8bcb20a87046aab6fa7f Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Thu, 30 Oct 2025 16:26:56 +0100 Subject: [PATCH 25/68] feat: enhance address validation with Lightning Network invoice support for BTC & refactor wallet type/token checks in view model --- cw_bitcoin/lib/bitcoin_wallet.dart | 6 +- cw_bitcoin/lib/electrum_wallet_addresses.dart | 9 +- .../lib/lightning/lightning_addres_type.dart | 2 + .../lib/lightning/lightning_wallet.dart | 15 +- cw_bitcoin/test/cw_bitcoin_test.dart | 25 ++- integration_test/robots/send_page_robot.dart | 4 + lib/bitcoin/cw_bitcoin.dart | 2 + lib/core/address_validator.dart | 39 +++-- lib/src/screens/send/send_page.dart | 156 +++++++++--------- lib/view_model/send/send_view_model.dart | 96 +++++------ tool/configure.dart | 1 + 11 files changed, 197 insertions(+), 158 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 6b3cc56f64..997a5dfb89 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -378,9 +378,11 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Future createTransaction(Object credentials) async { credentials = credentials as BitcoinTransactionCredentials; - if (lightningWallet?.isCompatible(credentials.outputs.first.address) == true) { + if ((await lightningWallet?.isCompatible(credentials.outputs.first.address)) == true) { + final amount = parseFixed(credentials.outputs.first.cryptoAmount ?? "0", 9); + return lightningWallet!.createTransaction(credentials.outputs.first.address, - parseFixed(credentials.outputs.first.cryptoAmount ?? "0", 9)); + amount > BigInt.zero ? amount : null); } final tx = (await super.createTransaction(credentials)) as PendingBitcoinTransaction; diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index eddcadb563..7ef455793f 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -120,8 +120,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final ObservableList addressesByReceiveType; final ObservableList receiveAddresses; final ObservableList changeAddresses; + // TODO: add this variable in `bitcoin_wallet_addresses` and just add a cast in cw_bitcoin to use it final ObservableList silentAddresses; + // TODO: add this variable in `litecoin_wallet_addresses` and just add a cast in cw_bitcoin to use it final ObservableList mwebAddresses; final BasedUtxoNetwork network; @@ -755,7 +757,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final path = await pathForWalletDir(name: walletName, type: WalletType.bitcoin); await lightningWallet!.init(path); - lightningAddress = await lightningWallet!.registerAddress(walletName.replaceAll(" ", "")); + lightningAddress = await lightningWallet!.getAddress(); + + if (lightningAddress == null) { + lightningAddress = + await lightningWallet!.registerAddress(walletName.replaceAll(" ", "").toLowerCase()); + } } } diff --git a/cw_bitcoin/lib/lightning/lightning_addres_type.dart b/cw_bitcoin/lib/lightning/lightning_addres_type.dart index 275867b34f..c12f980e77 100644 --- a/cw_bitcoin/lib/lightning/lightning_addres_type.dart +++ b/cw_bitcoin/lib/lightning/lightning_addres_type.dart @@ -4,6 +4,8 @@ class LightningAddressType implements BitcoinAddressType { const LightningAddressType._(this.value); static const LightningAddressType p2l = LightningAddressType._("Lightning"); + static const String Bolt11InvoiceMatcher = r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$'; + @override bool get isP2sh => false; @override diff --git a/cw_bitcoin/lib/lightning/lightning_wallet.dart b/cw_bitcoin/lib/lightning/lightning_wallet.dart index cf2520c16a..b19105c52b 100644 --- a/cw_bitcoin/lib/lightning/lightning_wallet.dart +++ b/cw_bitcoin/lib/lightning/lightning_wallet.dart @@ -2,6 +2,8 @@ import 'package:breez_sdk_spark_flutter/breez_sdk_spark.dart'; import 'package:cw_bitcoin/lightning/pending_lightning_transaction.dart'; import 'package:cw_core/pending_transaction.dart'; +bool _breezSdkSparkLibUninitialized = true; + class LightningWallet { final String mnemonic; final String apiKey; @@ -17,7 +19,10 @@ class LightningWallet { }); Future init(String appPath) async { - await BreezSdkSparkLib.init(); + if(_breezSdkSparkLibUninitialized) { + await BreezSdkSparkLib.init(); + _breezSdkSparkLibUninitialized = false; + } final seed = Seed.mnemonic(mnemonic: mnemonic, passphrase: null); final config = defaultConfig(network: Network.mainnet).copyWith( @@ -39,9 +44,11 @@ class LightningWallet { Future getBalance() async => (await sdk.getInfo(request: GetInfoRequest(ensureSynced: true))).balanceSats; - Future registerAddress(String username) async => (await sdk.registerLightningAddress( - request: RegisterLightningAddressRequest(username: username))) - .lightningAddress; + Future registerAddress(String username) async { + return (await sdk.registerLightningAddress( + request: RegisterLightningAddressRequest(username: username))) + .lightningAddress; + } Future isCompatible(String input) async { try { diff --git a/cw_bitcoin/test/cw_bitcoin_test.dart b/cw_bitcoin/test/cw_bitcoin_test.dart index 2a7ad6fe46..3fb24b185a 100644 --- a/cw_bitcoin/test/cw_bitcoin_test.dart +++ b/cw_bitcoin/test/cw_bitcoin_test.dart @@ -1,12 +1,23 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:cw_bitcoin/cw_bitcoin.dart'; - void main() { - test('adds one to input values', () { - final calculator = Calculator(); - expect(calculator.addOne(2), 3); - expect(calculator.addOne(-7), -6); - expect(calculator.addOne(0), 1); + group('lightning matchers', () { + final RegExp lightningInvoiceRegex = + RegExp(r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$', caseSensitive: false); + + test('Valid invoice', () { + final content = + "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw508d6qejxtdg4y5r3zarvary0c5xw7kpqdxssqfsqqqyqqqqlgqqqqqeqqjq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgqfsqqqyqqqqlgqqqqqeqqjq9qrsgq"; + expect(lightningInvoiceRegex.hasMatch(content), true); + }); + test('Valid invoice with prefix', () { + final content = + "lightning:lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw508d6qejxtdg4y5r3zarvary0c5xw7kpqdxssqfsqqqyqqqqlgqqqqqeqqjq9qrsgq"; + expect(lightningInvoiceRegex.hasMatch(content), true); + }); + test('Invalid invoice', () { + final content = "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"; // This is a Bitcoin address + expect(lightningInvoiceRegex.hasMatch(content), false); + }); }); } diff --git a/integration_test/robots/send_page_robot.dart b/integration_test/robots/send_page_robot.dart index 84d0156eaa..72ab1d9a03 100644 --- a/integration_test/robots/send_page_robot.dart +++ b/integration_test/robots/send_page_robot.dart @@ -104,6 +104,10 @@ class SendPageRobot { commonTestCases.hasValueKey('send_page_unspent_coin_button_key'); } + if (sendViewModel.hasCurrencyChanger) { + commonTestCases.hasValueKey('send_page_change_asset_button_key'); + } + if (sendViewModel.sendTemplateViewModel.hasMultiRecipient) { commonTestCases.hasValueKey('send_page_add_receiver_button_key'); } diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 4a9b2f1f64..b04fe0294d 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -687,6 +687,8 @@ class CWBitcoin extends Bitcoin { } List updateOutputs(PendingTransaction pendingTransaction, List outputs) { + if (pendingTransaction is PendingLightningTransaction) return outputs; + final pendingTx = pendingTransaction as PendingBitcoinTransaction; if (!pendingTx.hasSilentPayment) { diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 059a92c448..df072f8a32 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -12,21 +12,29 @@ const AFTER_REGEX = '(\$|\\s)'; class AddressValidator extends TextValidator { AddressValidator({required CryptoCurrency type, bool isTestnet = false}) : super( - errorMessage: S.current.error_text_address, - useAdditionalValidation: type == CryptoCurrency.btc || type == CryptoCurrency.ltc - ? (String txt) => BitcoinAddressUtils.validateAddress( - address: txt, - network: type == CryptoCurrency.btc - ? isTestnet - ? BitcoinNetwork.testnet - : BitcoinNetwork.mainnet - : LitecoinNetwork.mainnet, - ) - : type == CryptoCurrency.zano - ? zano?.validateAddress - : null, - pattern: getPattern(type, isTestnet: isTestnet), - length: getLength(type)); + errorMessage: S.current.error_text_address, + useAdditionalValidation: [CryptoCurrency.btc, CryptoCurrency.ltc].contains(type) + ? (String txt) { + final RegExp lightningInvoiceRegex = RegExp( + r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$', + caseSensitive: false); + if (lightningInvoiceRegex.hasMatch(txt)) return true; + + return BitcoinAddressUtils.validateAddress( + address: txt, + network: type == CryptoCurrency.btc + ? isTestnet + ? BitcoinNetwork.testnet + : BitcoinNetwork.mainnet + : LitecoinNetwork.mainnet, + ); + } + : type == CryptoCurrency.zano + ? zano?.validateAddress + : null, + pattern: getPattern(type, isTestnet: isTestnet), + length: getLength(type), + ); static String getPattern(CryptoCurrency type, {bool isTestnet = false}) { var pattern = ""; @@ -53,6 +61,7 @@ class AddressValidator extends TextValidator { '|(bc1q[ac-hj-np-z02-9]{25,39})' '|(bc1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))' '|(bc1q[ac-hj-np-z02-9]{40,80})' + '|(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]' '|(${silentPaymentAddressPatternMainnet})(\$|\s)'; } case CryptoCurrency.ltc: diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index f5b151482b..cd3e695aa3 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -1,13 +1,13 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/auth_service.dart'; -import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/entities/template.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; -import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; @@ -32,12 +32,12 @@ import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/payment/payment_view_model.dart'; import 'package:cake_wallet/view_model/send/output.dart'; -import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; +import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; @@ -93,8 +93,7 @@ class SendPage extends BasePage { size: 16, ); final _closeButton = currentTheme.isDark ? closeButtonImageDarkTheme : closeButtonImage; - - bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI; + final isMobileView = responsiveLayoutUtil.shouldRenderMobileUI; return MergeSemantics( child: SizedBox( @@ -145,27 +144,25 @@ class SendPage extends BasePage { @override Widget trailing(context) => Observer( - builder: (_) { - return sendViewModel.isBatchSending - ? TrailButton( - caption: S.of(context).remove, - onPressed: () { - var pageToJump = (controller.page?.round() ?? 0) - 1; - pageToJump = pageToJump > 0 ? pageToJump : 0; - final output = _defineCurrentOutput(); - sendViewModel.removeOutput(output); - controller.jumpToPage(pageToJump); - }, - ) - : TrailButton( - caption: S.of(context).clear, - onPressed: () { - final output = _defineCurrentOutput(); - _formKey.currentState?.reset(); - output.reset(); - }, - ); - }, + builder: (_) => sendViewModel.isBatchSending + ? TrailButton( + caption: S.of(context).remove, + onPressed: () { + var pageToJump = (controller.page?.round() ?? 0) - 1; + pageToJump = pageToJump > 0 ? pageToJump : 0; + final output = _defineCurrentOutput(); + sendViewModel.removeOutput(output); + controller.jumpToPage(pageToJump); + }, + ) + : TrailButton( + caption: S.of(context).clear, + onPressed: () { + final output = _defineCurrentOutput(); + _formKey.currentState?.reset(); + output.reset(); + }, + ), ); @override @@ -175,9 +172,9 @@ class SendPage extends BasePage { return Observer(builder: (_) { List sendCards = []; List keyboardActions = []; - for (var output in sendViewModel.outputs) { - var cryptoAmountFocus = FocusNode(); - var fiatAmountFocus = FocusNode(); + for (final output in sendViewModel.outputs) { + final cryptoAmountFocus = FocusNode(); + final fiatAmountFocus = FocusNode(); sendCards.add( SendCard( currentTheme: currentTheme, @@ -376,6 +373,19 @@ class SendPage extends BasePage { bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), bottomSection: Column( children: [ + if (sendViewModel.hasCurrencyChanger) + Observer( + builder: (_) => Padding( + padding: EdgeInsets.only(bottom: 12), + child: PrimaryButton( + key: ValueKey('send_page_change_asset_button_key'), + onPressed: () => presentCurrencyPicker(context), + text: 'Change your asset (${sendViewModel.selectedCryptoCurrency})', + color: Colors.transparent, + textColor: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), if (sendViewModel.sendTemplateViewModel.hasMultiRecipient) Padding( padding: EdgeInsets.only(bottom: 12), @@ -474,7 +484,8 @@ class SendPage extends BasePage { sendViewModel.state is TransactionCommitting || sendViewModel.state is IsAwaitingDeviceResponseState || sendViewModel.state is LoadingTemplateExecutingState, - isDisabled: !sendViewModel.isReadyForSend || sendViewModel.state is ExecutedSuccessfullyState, + isDisabled: !sendViewModel.isReadyForSend || + sendViewModel.state is ExecutedSuccessfullyState, ); }, ) @@ -491,9 +502,7 @@ class SendPage extends BasePage { BuildContext? loadingBottomSheetContext; void _setEffects(BuildContext context) { - if (_effectsInstalled) { - return; - } + if (_effectsInstalled) return; if (sendViewModel.isElectrumWallet) { bitcoin!.updateFeeRates(sendViewModel.wallet); @@ -515,16 +524,14 @@ class SendPage extends BasePage { (_) { showPopUp( context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - key: ValueKey('send_page_send_failure_dialog_key'), - buttonKey: ValueKey('send_page_send_failure_dialog_button_key'), - alertTitle: S.of(context).error, - alertContent: state.error, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop(), - ); - }, + builder: (context) => AlertWithOneAction( + key: ValueKey('send_page_send_failure_dialog_key'), + buttonKey: ValueKey('send_page_send_failure_dialog_button_key'), + alertTitle: S.of(context).error, + alertContent: state.error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ), ); }, ); @@ -543,7 +550,7 @@ class SendPage extends BasePage { showModalBottomSheet( context: context, isDismissible: false, - builder: (BuildContext context) { + builder: (context) { loadingBottomSheetContext = context; return LoadingBottomSheet( titleText: S.of(context).generating_transaction, @@ -597,9 +604,7 @@ class SendPage extends BasePage { if (state is TransactionCommitted) { WidgetsBinding.instance.addPostFrameCallback((_) async { - if (!context.mounted) { - return; - } + if (!context.mounted) return; newContactAddress = newContactAddress ?? sendViewModel.newContactAddress(); @@ -770,24 +775,32 @@ class SendPage extends BasePage { } Output _defineCurrentOutput() { - if (controller.page == null) { - throw Exception('Controller page is null'); - } + if (controller.page == null) throw Exception('Controller page is null'); final itemCount = controller.page!.round(); return sendViewModel.outputs[itemCount]; } - void showErrorValidationAlert(BuildContext context) async { - await showPopUp( + void showErrorValidationAlert(BuildContext context) => showPopUp( context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.of(context).error, - alertContent: 'Please, check receiver forms', - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); - } + builder: (context) => AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: 'Please, check receiver forms', + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ), + ); + + void presentCurrencyPicker(BuildContext context) => showPopUp( + builder: (_) => Picker( + items: sendViewModel.currencies, + displayItem: (item) => item.toString(), + selectedAtIndex: sendViewModel.currencies.indexOf(sendViewModel.selectedCryptoCurrency), + title: S.of(context).please_select, + mainAxisAlignment: MainAxisAlignment.center, + onItemSelected: (cur) => sendViewModel.selectedCryptoCurrency = cur, + ), + context: context, + ); bool isRegularElectrumAddress(String address) { final supportedTypes = [CryptoCurrency.btc, CryptoCurrency.ltc, CryptoCurrency.bch]; @@ -800,7 +813,7 @@ class SendPage extends BasePage { final trimmed = address.trim(); bool isValid = false; - for (var type in supportedTypes) { + for (final type in supportedTypes) { final addressPattern = AddressValidator.getAddressFromStringPattern(type); if (addressPattern != null) { final regex = RegExp('^$addressPattern\$'); @@ -811,23 +824,16 @@ class SendPage extends BasePage { } } - for (var pattern in excludedPatterns) { - if (pattern.hasMatch(trimmed)) { - return false; - } + for (final pattern in excludedPatterns) { + if (pattern.hasMatch(trimmed)) return false; } return isValid; } String _sendButtonText(BuildContext context) { - if (!sendViewModel.isReadyForSend) { - return S.of(context).synchronizing; - } - if (sendViewModel.payjoinUri != null) { - return S.of(context).send_payjoin; - } else { - return S.of(context).send; - } + if (!sendViewModel.isReadyForSend) return S.of(context).synchronizing; + if (sendViewModel.payjoinUri != null) return S.of(context).send_payjoin; + return S.of(context).send; } } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 1b446e0203..734eb6c71c 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -57,6 +57,7 @@ import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; @@ -100,7 +101,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor this.transactionDescriptionBox, this.hardwareWalletViewModel, this.unspentCoinsListViewModel, - this.feesViewModel, { + this.feesViewModel, + this.walletInfoSource, { this.coinTypeToSpendFrom = UnspentCoinType.nonMweb, }) : state = InitialExecutionState(), currencies = appStore.wallet!.balance.keys.toList(), @@ -138,21 +140,15 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor bool get isEVMWallet => isEVMCompatibleChain(walletType); @action - void setShowAddressBookPopup(bool value) { - _settingsStore.showAddressBookPopupEnabled = value; - } + void setShowAddressBookPopup(bool value) => _settingsStore.showAddressBookPopupEnabled = value; @action - void addOutput() { - outputs - .add(Output(wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency)); - } + void addOutput() => outputs + .add(Output(wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency)); @action void removeOutput(Output output) { - if (isBatchSending) { - outputs.remove(output); - } + if (isBatchSending) outputs.remove(output); } @action @@ -185,9 +181,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor @computed String get pendingTransactionFiatAmount { - if (pendingTransaction == null) { - return '0.00'; - } + if (pendingTransaction == null) return '0.00'; try { final fiat = calculateFiatAmount( @@ -310,11 +304,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor @computed String get pendingTransactionFiatAmountFormatted => - isFiatDisabled ? '' : pendingTransactionFiatAmount + ' ' + fiat.title; + isFiatDisabled ? '' : '$pendingTransactionFiatAmount ${fiat.title}'; @computed String get pendingTransactionFeeFiatAmountFormatted => - isFiatDisabled ? '' : pendingTransactionFeeFiatAmount + ' ' + fiat.title; + isFiatDisabled ? '' : '$pendingTransactionFeeFiatAmount ${fiat.title}'; @computed bool get isReadyForSend => @@ -360,13 +354,12 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor List currencies; - bool get hasYat => outputs - .any((out) => out.isParsedAddress && out.parsedAddress.parseFrom == ParseFrom.yatRecord); - WalletType get walletType => wallet.type; String? get walletCurrencyName => wallet.currency.fullName?.toLowerCase() ?? wallet.currency.name; + bool get hasCurrencyChanger => walletType == WalletType.haven; + @computed FiatCurrency get fiatCurrency => _settingsStore.fiatCurrency; @@ -393,19 +386,12 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor .toList(); @action - bool checkIfAddressIsAContact(String address) { - final contactList = contactsToShow.where((element) => element.address == address).toList(); - - return contactList.isNotEmpty; - } + bool checkIfAddressIsAContact(String address) => + contactsToShow.where((element) => element.address == address).toList().isNotEmpty; @action - bool checkIfWalletIsAnInternalWallet(String address) { - final walletContactList = - walletContactsToShow.where((element) => element.address == address).toList(); - - return walletContactList.isNotEmpty; - } + bool checkIfWalletIsAnInternalWallet(String address) => + walletContactsToShow.where((element) => element.address == address).toList().isNotEmpty; @computed bool get shouldDisplayTOTP2FAForContact => _settingsStore.shouldRequireTOTP2FAForSendsToContact; @@ -499,13 +485,14 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor if (wallet.isHardwareWallet) { state = IsAwaitingDeviceResponseState(); - if (walletType == WalletType.monero) + if (walletType == WalletType.monero) { _ledgerTxStateTimer = Timer.periodic(Duration(seconds: 1), (timer) { if (monero!.getLastLedgerCommand() == "INS_CLSAG") { timer.cancel(); state = IsDeviceSigningResponseState(); } }); + } } // Swaps.xyz (EVM) path @@ -873,11 +860,13 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor final priority = _settingsStore.priority[wallet.type]; if (priority == null && - wallet.type != WalletType.nano && - wallet.type != WalletType.banano && - wallet.type != WalletType.solana && - wallet.type != WalletType.tron && - wallet.type != WalletType.arbitrum) { + [ + WalletType.nano, + WalletType.banano, + WalletType.solana, + WalletType.tron, + WalletType.arbitrium + ].contains(wallet.type)) { throw Exception('Priority is null for wallet type: ${wallet.type}'); } @@ -962,24 +951,21 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor Set.from(contactListViewModel.contacts.map((contact) => contact.address)) ..addAll(contactListViewModel.walletContacts.map((contact) => contact.address)); - for (var output in outputs) { - String address; - if (output.isParsedAddress) { - address = output.parsedAddress.addresses.first; - } else { - address = output.address; - } + for (final output in outputs) { + final address = + output.isParsedAddress ? output.parsedAddress.addresses.first : output.address; if (address.isNotEmpty && !contactAddresses.contains(address) && selectedCryptoCurrency.raw != -1) { return ContactRecord( - contactListViewModel.contactSource, - Contact( - name: '', - address: address, - type: selectedCryptoCurrency, - )); + contactListViewModel.contactSource, + Contact( + name: '', + address: address, + type: selectedCryptoCurrency, + ), + ); } } return null; @@ -1054,11 +1040,13 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor return errorMessage; } - if (walletType == WalletType.ethereum || - walletType == WalletType.polygon || - walletType == WalletType.base || - walletType == WalletType.arbitrum || - walletType == WalletType.haven) { + if ([ + WalletType.ethereum, + WalletType.polygon, + WalletType.base, + WalletType.haven, + WalletType.arbitrium + ].contains(walletType)) { if (errorMessage.contains('gas required exceeds allowance')) { return S.current.gas_exceeds_allowance; } diff --git a/tool/configure.dart b/tool/configure.dart index 47b3379406..fb934083ee 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -144,6 +144,7 @@ import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; +import 'package:cw_bitcoin/lightning/pending_lightning_transaction.dart'; import 'package:cw_bitcoin/litecoin_wallet_service.dart'; import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:cw_bitcoin/hardware/bitcoin_ledger_service.dart'; From ab6d724c4090496e0aa32226f6d68a5946e898de Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Fri, 31 Oct 2025 16:16:22 +0100 Subject: [PATCH 26/68] feat: add support for Lightning invoice detection, refactor MWEB deposit/withdraw actions, and integrate Lightning transaction creation with updated priority handling --- cw_bitcoin/lib/bitcoin_wallet.dart | 8 +- cw_bitcoin/lib/electrum_wallet.dart | 1 + .../lib/lightning/lightning_wallet.dart | 56 +++++++++- cw_core/lib/unspent_coin_type.dart | 2 +- lib/bitcoin/cw_bitcoin.dart | 10 ++ .../pages/balance/balance_row_widget.dart | 102 +++++++++--------- lib/src/screens/send/widgets/send_card.dart | 4 + lib/view_model/send/send_view_model.dart | 7 ++ 8 files changed, 133 insertions(+), 57 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 997a5dfb89..e5147a536a 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -27,6 +27,7 @@ import 'package:cw_core/output_info.dart'; import 'package:cw_core/parse_fixed.dart'; import 'package:cw_core/payjoin_session.dart'; import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/utils/zpub.dart'; import 'package:cw_core/wallet_info.dart'; @@ -378,11 +379,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Future createTransaction(Object credentials) async { credentials = credentials as BitcoinTransactionCredentials; - if ((await lightningWallet?.isCompatible(credentials.outputs.first.address)) == true) { - final amount = parseFixed(credentials.outputs.first.cryptoAmount ?? "0", 9); + if ((credentials.coinTypeToSpendFrom == UnspentCoinType.lightning && lightningWallet != null) || + (await lightningWallet?.isCompatible(credentials.outputs.first.address)) == true) { + final amount = parseFixed(credentials.outputs.first.cryptoAmount?.isNotEmpty == true ? credentials.outputs.first.cryptoAmount! : "0", 9); return lightningWallet!.createTransaction(credentials.outputs.first.address, - amount > BigInt.zero ? amount : null); + amount > BigInt.zero ? amount : null, credentials.priority); } final tx = (await super.createTransaction(credentials)) as PendingBitcoinTransaction; diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 7a0fecd3e7..73591a643d 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -714,6 +714,7 @@ abstract class ElectrumWalletBase case UnspentCoinType.nonMweb: return utx.bitcoinAddressRecord.type != SegwitAddresType.mweb; case UnspentCoinType.any: + case UnspentCoinType.lightning: return true; } }).toList(); diff --git a/cw_bitcoin/lib/lightning/lightning_wallet.dart b/cw_bitcoin/lib/lightning/lightning_wallet.dart index b19105c52b..b015f8d0c2 100644 --- a/cw_bitcoin/lib/lightning/lightning_wallet.dart +++ b/cw_bitcoin/lib/lightning/lightning_wallet.dart @@ -1,4 +1,5 @@ import 'package:breez_sdk_spark_flutter/breez_sdk_spark.dart'; +import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/lightning/pending_lightning_transaction.dart'; import 'package:cw_core/pending_transaction.dart'; @@ -19,7 +20,7 @@ class LightningWallet { }); Future init(String appPath) async { - if(_breezSdkSparkLibUninitialized) { + if (_breezSdkSparkLibUninitialized) { await BreezSdkSparkLib.init(); _breezSdkSparkLibUninitialized = false; } @@ -41,12 +42,17 @@ class LightningWallet { Future getAddress() async => (await sdk.getLightningAddress())?.lightningAddress; + Future getDepositAddress() async => + (await sdk.receivePayment( + request: ReceivePaymentRequest(paymentMethod: ReceivePaymentMethod.bitcoinAddress()))) + .paymentRequest; + Future getBalance() async => (await sdk.getInfo(request: GetInfoRequest(ensureSynced: true))).balanceSats; Future registerAddress(String username) async { return (await sdk.registerLightningAddress( - request: RegisterLightningAddressRequest(username: username))) + request: RegisterLightningAddressRequest(username: username))) .lightningAddress; } @@ -59,7 +65,8 @@ class LightningWallet { } } - Future createTransaction(String address, BigInt? amountSats) async { + Future createTransaction(String address, BigInt? amountSats, + BitcoinTransactionPriority? priority) async { final inputType = await sdk.parse(input: address); if (inputType is InputType_Bolt11Invoice) { @@ -76,7 +83,7 @@ class LightningWallet { return PendingLightningTransaction( id: paymentMethod.invoiceDetails.paymentHash, - amount: paymentMethod.invoiceDetails.amountMsat?.toInt() ?? 0, + amount: ((paymentMethod.invoiceDetails.amountMsat?.toInt() ?? 0) / 1000).round(), fee: lightningFeeSats.toInt() + (sparkTransferFeeSats?.toInt() ?? 0), commitOverride: () => sdk.sendPayment(request: SendPaymentRequest(prepareResponse: prepareResponse)), @@ -101,6 +108,45 @@ class LightningWallet { commitOverride: () => sdk.lnurlPay(request: LnurlPayRequest(prepareResponse: prepareResponse)), ); + } else if (inputType is InputType_BitcoinAddress) { + final request = PrepareSendPaymentRequest( + paymentRequest: inputType.field0.address, amount: amountSats); + final prepareResponse = await sdk.prepareSendPayment(request: request); + + final paymentMethod = prepareResponse.paymentMethod; + if (paymentMethod is SendPaymentMethod_BitcoinAddress) { + final feeQuote = paymentMethod.feeQuote; + OnchainConfirmationSpeed onchainConfirmationSpeed; + int fee; + + switch (priority) { + case BitcoinTransactionPriority.fast: + fee = (feeQuote.speedFast.userFeeSat + feeQuote.speedFast.l1BroadcastFeeSat).toInt(); + onchainConfirmationSpeed = OnchainConfirmationSpeed.fast; + break; + case BitcoinTransactionPriority.medium: + fee = + (feeQuote.speedMedium.userFeeSat + feeQuote.speedMedium.l1BroadcastFeeSat).toInt(); + onchainConfirmationSpeed = OnchainConfirmationSpeed.medium; + break; + case BitcoinTransactionPriority.slow: + default: + fee = (feeQuote.speedSlow.userFeeSat + feeQuote.speedSlow.l1BroadcastFeeSat).toInt(); + onchainConfirmationSpeed = OnchainConfirmationSpeed.slow; + } + + return PendingLightningTransaction( + id: "", // ToDo: Find out where to get it + amount: prepareResponse.amount.toInt(), + fee: fee, + commitOverride: () async { + final options = + SendPaymentOptions.bitcoinAddress(confirmationSpeed: onchainConfirmationSpeed); + await sdk.sendPayment( + request: SendPaymentRequest(prepareResponse: prepareResponse, options: options)); + }, + ); + } } // If not returned earlier @@ -126,6 +172,6 @@ extension _ConfigCopyWith on Config { maxDepositClaimFee: maxDepositClaimFee ?? this.maxDepositClaimFee, preferSparkOverLightning: preferSparkOverLightning ?? this.preferSparkOverLightning, useDefaultExternalInputParsers: - useDefaultExternalInputParsers ?? this.useDefaultExternalInputParsers, + useDefaultExternalInputParsers ?? this.useDefaultExternalInputParsers, ); } diff --git a/cw_core/lib/unspent_coin_type.dart b/cw_core/lib/unspent_coin_type.dart index a042610fc9..859457c498 100644 --- a/cw_core/lib/unspent_coin_type.dart +++ b/cw_core/lib/unspent_coin_type.dart @@ -1 +1 @@ -enum UnspentCoinType { mweb, nonMweb, any } \ No newline at end of file +enum UnspentCoinType { mweb, nonMweb, any, lightning } diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index b04fe0294d..27ffcef0d0 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -248,6 +248,7 @@ class CWBitcoin extends Bitcoin { return element.bitcoinAddressRecord.type == SegwitAddresType.mweb; case UnspentCoinType.nonMweb: return element.bitcoinAddressRecord.type != SegwitAddresType.mweb; + case UnspentCoinType.lightning: case UnspentCoinType.any: return true; } @@ -766,6 +767,15 @@ class CWBitcoin extends Bitcoin { } } + Future getUnusedSpakDepositAddress(Object wallet) async { + try { + final bitcoinWallet = wallet as BitcoinWallet; + return wallet.lightningWallet?.getDepositAddress(); + } catch (_) { + return null; + } + } + @override Future commitPsbtUR(Object wallet, List urCodes) { final _wallet = wallet as BitcoinWalletBase; diff --git a/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart b/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart index 766914f769..3dac4cea13 100644 --- a/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart +++ b/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart @@ -12,6 +12,7 @@ import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/unspent_coin_type.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -505,23 +506,7 @@ class BalanceRowWidget extends StatelessWidget { child: Semantics( label: S.of(context).litecoin_mweb_pegin, child: OutlinedButton( - onPressed: () { - final mwebAddress = - bitcoin!.getUnusedMwebAddress(dashboardViewModel.wallet); - PaymentRequest? paymentRequest = null; - if ((mwebAddress?.isNotEmpty ?? false)) { - paymentRequest = PaymentRequest.fromUri( - Uri.parse("litecoin:${mwebAddress}")); - } - Navigator.pushNamed( - context, - Routes.send, - arguments: { - 'paymentRequest': paymentRequest, - 'coinTypeToSpendFrom': UnspentCoinType.nonMweb, - }, - ); - }, + onPressed: () => depositToL2(context), style: OutlinedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.primary, side: BorderSide( @@ -563,23 +548,7 @@ class BalanceRowWidget extends StatelessWidget { child: Semantics( label: S.of(context).litecoin_mweb_pegout, child: OutlinedButton( - onPressed: () { - final litecoinAddress = - bitcoin!.getUnusedSegwitAddress(dashboardViewModel.wallet); - PaymentRequest? paymentRequest = null; - if ((litecoinAddress?.isNotEmpty ?? false)) { - paymentRequest = PaymentRequest.fromUri( - Uri.parse("litecoin:${litecoinAddress}")); - } - Navigator.pushNamed( - context, - Routes.send, - arguments: { - 'paymentRequest': paymentRequest, - 'coinTypeToSpendFrom': UnspentCoinType.mweb, - }, - ); - }, + onPressed: () => withdrawFromL2(context), style: OutlinedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.surface, side: BorderSide( @@ -632,20 +601,57 @@ class BalanceRowWidget extends StatelessWidget { ); } - // double getShadowSpread(){ - // double spread = 3; - // else if (!dashboardViewModel.settingsStore.currentTheme.isDark) spread = 3; - // else if (dashboardViewModel.settingsStore.currentTheme.isDark) spread = 1; - // return spread; - // } - // - // - // double getShadowBlur(){ - // double blur = 7; - // else if (dashboardViewModel.settingsStore.currentTheme.isDark) blur = 7; - // else if (dashboardViewModel.settingsStore.currentTheme.isDark) blur = 3; - // return blur; - // } + Future depositToL2(BuildContext context) async { + PaymentRequest? paymentRequest = null; + + if (dashboardViewModel.type == WalletType.litecoin) { + final depositAddress = bitcoin!.getUnusedMwebAddress(dashboardViewModel.wallet); + if ((depositAddress?.isNotEmpty ?? false)) { + paymentRequest = PaymentRequest.fromUri(Uri.parse("litecoin:$depositAddress")); + } + } else if (dashboardViewModel.type == WalletType.bitcoin) { + final depositAddress = await bitcoin!.getUnusedSpakDepositAddress(dashboardViewModel.wallet); + if ((depositAddress?.isNotEmpty ?? false)) { + paymentRequest = PaymentRequest.fromUri(Uri.parse("bitcoin:$depositAddress")); + } + } + + Navigator.pushNamed( + context, + Routes.send, + arguments: { + 'paymentRequest': paymentRequest, + 'coinTypeToSpendFrom': UnspentCoinType.nonMweb, + }, + ); + } + + Future withdrawFromL2(BuildContext context) async { + PaymentRequest? paymentRequest = null; + UnspentCoinType unspentCoinType = UnspentCoinType.any; + final withdrawAddress = bitcoin!.getUnusedSegwitAddress(dashboardViewModel.wallet); + + if (dashboardViewModel.type == WalletType.litecoin) { + if ((withdrawAddress?.isNotEmpty ?? false)) { + paymentRequest = PaymentRequest.fromUri(Uri.parse("litecoin:$withdrawAddress")); + } + unspentCoinType = UnspentCoinType.mweb; + } else if (dashboardViewModel.type == WalletType.bitcoin) { + if ((withdrawAddress?.isNotEmpty ?? false)) { + paymentRequest = PaymentRequest.fromUri(Uri.parse("bitcoin:$withdrawAddress")); + } + unspentCoinType = UnspentCoinType.lightning; + } + + Navigator.pushNamed( + context, + Routes.send, + arguments: { + 'paymentRequest': paymentRequest, + 'coinTypeToSpendFrom': unspentCoinType, + }, + ); + } void _showBalanceDescription(BuildContext context, String content) { showPopUp(context: context, builder: (_) => InformationPage(information: content)); diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index b0d4f767ef..20e3b2ceb3 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -818,6 +818,10 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin Date: Fri, 31 Oct 2025 16:16:45 +0100 Subject: [PATCH 27/68] feat: add method to retrieve unused Spark deposit address for Bitcoin wallets --- tool/configure.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/tool/configure.dart b/tool/configure.dart index fb934083ee..dbca11d49f 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -269,6 +269,7 @@ abstract class Bitcoin { bool getMwebEnabled(Object wallet); String? getUnusedMwebAddress(Object wallet); String? getUnusedSegwitAddress(Object wallet); + Future getUnusedSpakDepositAddress(Object wallet); Future commitPsbtUR(Object wallet, List urCodes); void updatePayjoinState(Object wallet, bool state); From 5995afba45bf7249693200fa334f94de04ef42db Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Sat, 1 Nov 2025 00:32:21 +0100 Subject: [PATCH 28/68] feat: add Breez API key support and update secrets handling for Bitcoin Lightning wallet integration in workflows --- .github/workflows/automated_integration_test.yml | 2 ++ .github/workflows/pr_test_build_android.yml | 4 +++- .github/workflows/pr_test_build_linux.yml | 2 ++ cw_bitcoin/lib/bitcoin_wallet.dart | 4 ++-- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/automated_integration_test.yml b/.github/workflows/automated_integration_test.yml index 95a3197d4f..eee1407c1d 100644 --- a/.github/workflows/automated_integration_test.yml +++ b/.github/workflows/automated_integration_test.yml @@ -57,6 +57,7 @@ jobs: - name: Add secrets run: | touch lib/.secrets.g.dart + touch cw_bitcoin/lib/.secrets.g.dart touch cw_evm/lib/.secrets.g.dart touch cw_solana/lib/.secrets.g.dart touch cw_core/lib/.secrets.g.dart @@ -130,6 +131,7 @@ jobs: echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const nowNodesApiKey = '${{ secrets.EVM_NOWNODES_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart + echo "const breezApiKey = '${{ secrets.BREEZ_API_KEY }}';" >> cw_bitcoin/lib/.secrets.g.dart echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart echo "const exolixCakeWalletApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart echo "const exolixMoneroApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 7d7a196c13..53ce074e69 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -51,6 +51,7 @@ jobs: - name: Add secrets run: | touch lib/.secrets.g.dart + touch cw_bitcoin/lib/.secrets.g.dart touch cw_evm/lib/.secrets.g.dart touch cw_solana/lib/.secrets.g.dart touch cw_core/lib/.secrets.g.dart @@ -124,6 +125,7 @@ jobs: echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const nowNodesApiKey = '${{ secrets.EVM_NOWNODES_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart + echo "const breezApiKey = '${{ secrets.BREEZ_API_KEY }}';" >> cw_bitcoin/lib/.secrets.g.dart echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart echo "const exolixCakeWalletApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart echo "const exolixMoneroApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart @@ -340,4 +342,4 @@ jobs: cd build/app/outputs/flutter-apk for i in arm64-v8a x86_64; do ../../../../scripts/android/check_16kb_align.sh app-$i-release.apk - done \ No newline at end of file + done diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 92eae19db2..10bd5557b7 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -44,6 +44,7 @@ jobs: - name: Add secrets run: | touch lib/.secrets.g.dart + touch cw_bitcoin/lib/.secrets.g.dart touch cw_evm/lib/.secrets.g.dart touch cw_solana/lib/.secrets.g.dart touch cw_core/lib/.secrets.g.dart @@ -117,6 +118,7 @@ jobs: echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const nowNodesApiKey = '${{ secrets.EVM_NOWNODES_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart + echo "const breezApiKey = '${{ secrets.BREEZ_API_KEY }}';" >> cw_bitcoin/lib/.secrets.g.dart echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart echo "const exolixCakeWalletApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart echo "const exolixMoneroApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index e5147a536a..561c31528d 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:bip39/bip39.dart' as bip39; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_bitcoin/.secrets.g.dart' as secrets; import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; @@ -96,8 +97,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { if (mnemonic != null) { lightningWallet = LightningWallet( mnemonic: mnemonic, - apiKey: - "MIIBdzCCASmgAwIBAgIHPpJHKP1qXzAFBgMrZXAwEDEOMAwGA1UEAxMFQnJlZXowHhcNMjUxMDIzMTQwNDQ4WhcNMzUxMDIxMTQwNDQ4WjAxMRQwEgYDVQQKEwtDYWtlIFdhbGxldDEZMBcGA1UEAxMQU2V0aCBGb3IgUHJpdmFjeTAqMAUGAytlcAMhANCD9cvfIDwcoiDKKYdT9BunHLS2/OuKzV8NS0SzqV13o4GAMH4wDgYDVR0PAQH/BAQDAgWgMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFNo5o+5ea0sNMlW/75VgGJCv2AcJMB8GA1UdIwQYMBaAFN6q1pJW843ndJIW/Ey2ILJrKJhrMB4GA1UdEQQXMBWBE3NldGhAY2FrZXdhbGxldC5jb20wBQYDK2VwA0EAl+naPfCBseV7eS4SoP0q0kvo2GHCywXoIbnlBa0y+/wlfu+oILtsGv3jGQ2egCnpgHe87yzR0ygclzz8r/jdAQ==", + apiKey: secrets.breezApiKey, lnurlDomain: "breez.tips", ); } From 7ab3997bba7cd01aad9e4344dc19404a1e882fb6 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Sat, 1 Nov 2025 00:33:45 +0100 Subject: [PATCH 29/68] chore: update Breez SDK dependency to version 0.3.4 in pubspec files --- cw_bitcoin/pubspec.lock | 6 +++--- cw_bitcoin/pubspec.yaml | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 38aea938ca..e281c7f7b4 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -134,11 +134,11 @@ packages: dependency: "direct main" description: path: "." - ref: HEAD - resolved-ref: "5bc8fb5f3a5c84e2e3dd55f5d48b01152f425765" + ref: "92f62dc2037cf08003e418aadda58f451c021f42" + resolved-ref: "92f62dc2037cf08003e418aadda58f451c021f42" url: "https://github.com/breez/breez-sdk-spark-flutter" source: git - version: "0.3.2" + version: "0.3.4" bs58check: dependency: transitive description: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 940ce80b6a..439492bda0 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -75,6 +75,7 @@ dependencies: breez_sdk_spark_flutter: git: url: https://github.com/breez/breez-sdk-spark-flutter + ref: 92f62dc2037cf08003e418aadda58f451c021f42 dev_dependencies: flutter_test: From c02775d09b21153f193606b152191f7c23497fb1 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Sat, 1 Nov 2025 13:28:12 +0100 Subject: [PATCH 30/68] Add bitcoin secrets config [skip ci] --- .gitignore | 1 + scripts/android/app_env.sh | 4 ++-- tool/generate_secrets_config.dart | 3 +++ tool/import_secrets_config.dart | 3 +++ tool/utils/secret_key.dart | 4 ++++ 5 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index cd2230504d..018d05ca22 100644 --- a/.gitignore +++ b/.gitignore @@ -97,6 +97,7 @@ android/app/key.jks **/tool/.solana-secrets-config.json **/tool/.nano-secrets-config.json **/tool/.tron-secrets-config.json +**/tool/.bitcoin-secrets-config.json **/lib/.secrets.g.dart **/cw_evm/lib/.secrets.g.dart **/cw_solana/lib/.secrets.g.dart diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 21bf2c1e99..99e0380e07 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -21,8 +21,8 @@ MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="5.5.2" -CAKEWALLET_BUILD_NUMBER=4284 +CAKEWALLET_VERSION="5.6.0" +CAKEWALLET_BUILD_NUMBER=4285 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/tool/generate_secrets_config.dart b/tool/generate_secrets_config.dart index 8e9762b7a0..0b32e60ad5 100644 --- a/tool/generate_secrets_config.dart +++ b/tool/generate_secrets_config.dart @@ -8,6 +8,7 @@ const evmChainsConfigPath = 'tool/.evm-secrets-config.json'; const solanaConfigPath = 'tool/.solana-secrets-config.json'; const nanoConfigPath = 'tool/.nano-secrets-config.json'; const tronConfigPath = 'tool/.tron-secrets-config.json'; +const bitcoinConfigPath = 'tool/.bitcoin-secrets-config.json'; Future main(List args) async => generateSecretsConfig(args); @@ -41,6 +42,7 @@ Future generateSecretsConfig(List args) async { final solanaConfigFile = File(solanaConfigPath); final nanoConfigFile = File(nanoConfigPath); final tronConfigFile = File(tronConfigPath); + final bitcoinConfigFile = File(bitcoinConfigPath); final secrets = {}; @@ -66,4 +68,5 @@ Future generateSecretsConfig(List args) async { await writeConfig(solanaConfigFile, SecretKey.solanaSecrets); await writeConfig(nanoConfigFile, SecretKey.nanoSecrets); await writeConfig(tronConfigFile, SecretKey.tronSecrets); + await writeConfig(bitcoinConfigFile, SecretKey.bitcoinSecrets); } diff --git a/tool/import_secrets_config.dart b/tool/import_secrets_config.dart index 42379021f5..dd333c7e2b 100644 --- a/tool/import_secrets_config.dart +++ b/tool/import_secrets_config.dart @@ -14,6 +14,9 @@ const solanaOutputPath = 'cw_solana/lib/.secrets.g.dart'; const tronConfigPath = 'tool/.tron-secrets-config.json'; const tronOutputPath = 'cw_tron/lib/.secrets.g.dart'; +const bitcoinConfigPath = 'tool/.bitcoin-secrets-config.json'; +const bitcoinOutputPath = 'cw_bitcoin/lib/.secrets.g.dart'; + const nanoConfigPath = 'tool/.nano-secrets-config.json'; const nanoOutputPath = 'cw_nano/lib/.secrets.g.dart'; diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index 61ccea60b9..8e6a6c6c9e 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -111,6 +111,10 @@ class SecretKey { SecretKey('tronNowNodesApiKey', () => ''), ]; + static final bitcoinSecrets = [ + SecretKey('breezApiKey', () => ''), + ]; + final String name; final String Function() generate; } From a33a3750b3e3db792718d96314d7110642218c58 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Mon, 3 Nov 2025 19:46:02 +0100 Subject: [PATCH 31/68] feat: extend Lightning wallet functionality with transaction history fetching --- cw_bitcoin/lib/bitcoin_wallet.dart | 29 ++++++-- .../lib/lightning/lightning_addres_type.dart | 1 + .../lib/lightning/lightning_wallet.dart | 72 ++++++++++++++----- 3 files changed, 82 insertions(+), 20 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 561c31528d..9b212ff5f9 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -11,6 +11,7 @@ import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; +import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; import 'package:cw_bitcoin/hardware/bitcoin_hardware_wallet_service.dart'; @@ -119,7 +120,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { lightningWallet: lightningWallet, ); - if (lightningWallet != null) { walletAddresses.setLightningAddress(walletInfo.name); } @@ -296,7 +296,23 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final lBalance = await lightningWallet!.getBalance(); - return ElectrumBalance(confirmed: balance.confirmed, unconfirmed: balance.unconfirmed, frozen: balance.frozen, secondConfirmed: lBalance.toInt()); + return ElectrumBalance( + confirmed: balance.confirmed, + unconfirmed: balance.unconfirmed, + frozen: balance.frozen, + secondConfirmed: lBalance.toInt(), + ); + } + + @override + Future> fetchTransactions() async { + if (lightningWallet != null) { + final lnHistory = await lightningWallet!.getTransactionHistory(); + transactionHistory.addMany(lnHistory); + await transactionHistory.save(); + } + + return super.fetchTransactions(); } late final LightningWallet? lightningWallet; @@ -379,9 +395,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Future createTransaction(Object credentials) async { credentials = credentials as BitcoinTransactionCredentials; + final isLNCompatible = await lightningWallet?.isCompatible(credentials.outputs.first.address); if ((credentials.coinTypeToSpendFrom == UnspentCoinType.lightning && lightningWallet != null) || - (await lightningWallet?.isCompatible(credentials.outputs.first.address)) == true) { - final amount = parseFixed(credentials.outputs.first.cryptoAmount?.isNotEmpty == true ? credentials.outputs.first.cryptoAmount! : "0", 9); + isLNCompatible == true) { + final amount = parseFixed( + credentials.outputs.first.cryptoAmount?.isNotEmpty == true + ? credentials.outputs.first.cryptoAmount! + : "0", + 9); return lightningWallet!.createTransaction(credentials.outputs.first.address, amount > BigInt.zero ? amount : null, credentials.priority); diff --git a/cw_bitcoin/lib/lightning/lightning_addres_type.dart b/cw_bitcoin/lib/lightning/lightning_addres_type.dart index c12f980e77..f0b13fca18 100644 --- a/cw_bitcoin/lib/lightning/lightning_addres_type.dart +++ b/cw_bitcoin/lib/lightning/lightning_addres_type.dart @@ -5,6 +5,7 @@ class LightningAddressType implements BitcoinAddressType { static const LightningAddressType p2l = LightningAddressType._("Lightning"); static const String Bolt11InvoiceMatcher = r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$'; + static const String Bolt12OfferMatcher = r'^(lightning:)?(lno1)[a-z0-9]+$'; @override bool get isP2sh => false; diff --git a/cw_bitcoin/lib/lightning/lightning_wallet.dart b/cw_bitcoin/lib/lightning/lightning_wallet.dart index b015f8d0c2..6ae40da518 100644 --- a/cw_bitcoin/lib/lightning/lightning_wallet.dart +++ b/cw_bitcoin/lib/lightning/lightning_wallet.dart @@ -1,7 +1,11 @@ import 'package:breez_sdk_spark_flutter/breez_sdk_spark.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; +import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/lightning/pending_lightning_transaction.dart'; import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_type.dart'; bool _breezSdkSparkLibUninitialized = true; @@ -42,17 +46,16 @@ class LightningWallet { Future getAddress() async => (await sdk.getLightningAddress())?.lightningAddress; - Future getDepositAddress() async => - (await sdk.receivePayment( + Future getDepositAddress() async => (await sdk.receivePayment( request: ReceivePaymentRequest(paymentMethod: ReceivePaymentMethod.bitcoinAddress()))) - .paymentRequest; + .paymentRequest; Future getBalance() async => (await sdk.getInfo(request: GetInfoRequest(ensureSynced: true))).balanceSats; Future registerAddress(String username) async { return (await sdk.registerLightningAddress( - request: RegisterLightningAddressRequest(username: username))) + request: RegisterLightningAddressRequest(username: username))) .lightningAddress; } @@ -65,8 +68,8 @@ class LightningWallet { } } - Future createTransaction(String address, BigInt? amountSats, - BitcoinTransactionPriority? priority) async { + Future createTransaction( + String address, BigInt? amountSats, BitcoinTransactionPriority? priority) async { final inputType = await sdk.parse(input: address); if (inputType is InputType_Bolt11Invoice) { @@ -76,17 +79,18 @@ class LightningWallet { final paymentMethod = prepareResponse.paymentMethod; if (paymentMethod is SendPaymentMethod_Bolt11Invoice) { - // Fees to pay via Lightning final lightningFeeSats = paymentMethod.lightningFeeSats; - // Or fees to pay (if available) via a Spark transfer final sparkTransferFeeSats = paymentMethod.sparkTransferFeeSats; return PendingLightningTransaction( id: paymentMethod.invoiceDetails.paymentHash, amount: ((paymentMethod.invoiceDetails.amountMsat?.toInt() ?? 0) / 1000).round(), fee: lightningFeeSats.toInt() + (sparkTransferFeeSats?.toInt() ?? 0), - commitOverride: () => - sdk.sendPayment(request: SendPaymentRequest(prepareResponse: prepareResponse)), + commitOverride: () async { + final res = await sdk.sendPayment( + request: SendPaymentRequest(prepareResponse: prepareResponse)); + printV(res.payment.status.name); + }, ); } } else if (inputType is InputType_LightningAddress) { @@ -105,20 +109,21 @@ class LightningWallet { id: prepareResponse.invoiceDetails.paymentHash, amount: prepareResponse.invoiceDetails.amountMsat?.toInt() ?? 0, fee: feeSats.toInt(), - commitOverride: () => - sdk.lnurlPay(request: LnurlPayRequest(prepareResponse: prepareResponse)), + commitOverride: () async { + await sdk.lnurlPay(request: LnurlPayRequest(prepareResponse: prepareResponse)); + }, ); } else if (inputType is InputType_BitcoinAddress) { - final request = PrepareSendPaymentRequest( - paymentRequest: inputType.field0.address, amount: amountSats); + final request = + PrepareSendPaymentRequest(paymentRequest: inputType.field0.address, amount: amountSats); final prepareResponse = await sdk.prepareSendPayment(request: request); final paymentMethod = prepareResponse.paymentMethod; if (paymentMethod is SendPaymentMethod_BitcoinAddress) { final feeQuote = paymentMethod.feeQuote; + OnchainConfirmationSpeed onchainConfirmationSpeed; int fee; - switch (priority) { case BitcoinTransactionPriority.fast: fee = (feeQuote.speedFast.userFeeSat + feeQuote.speedFast.l1BroadcastFeeSat).toInt(); @@ -152,6 +157,41 @@ class LightningWallet { // If not returned earlier throw UnimplementedError(); } + + Future> getTransactionHistory() async { + final request = ListPaymentsRequest( + typeFilter: [PaymentType.send, PaymentType.receive], + // statusFilter: [PaymentStatus.completed], + assetFilter: AssetFilter.bitcoin(), + offset: 0, + limit: 50, + sortAscending: false, // Sort order (true = oldest first, false = newest first) + ); + final response = await sdk.listPayments(request: request); + final payments = response.payments; + + Map txHistory = {}; + for (final payment in payments) { + TransactionDirection direction = TransactionDirection.outgoing; + + if (payment.method == PaymentMethod.deposit) { + direction = TransactionDirection.incoming; + } + + txHistory[payment.id] = ElectrumTransactionInfo( + WalletType.bitcoin, + id: payment.id, + amount: payment.amount.toInt(), + direction: direction, + isPending: payment.status == PaymentStatus.pending, + date: DateTime.fromMillisecondsSinceEpoch(payment.timestamp.toInt() * 1000), + confirmations: payment.status == PaymentStatus.pending ? 0 : 10, + + ); + } + + return txHistory; + } } extension _ConfigCopyWith on Config { @@ -172,6 +212,6 @@ extension _ConfigCopyWith on Config { maxDepositClaimFee: maxDepositClaimFee ?? this.maxDepositClaimFee, preferSparkOverLightning: preferSparkOverLightning ?? this.preferSparkOverLightning, useDefaultExternalInputParsers: - useDefaultExternalInputParsers ?? this.useDefaultExternalInputParsers, + useDefaultExternalInputParsers ?? this.useDefaultExternalInputParsers, ); } From 2c311c9eb80a41f351bf6f857d1425b3b42087f8 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 4 Nov 2025 10:06:01 +0100 Subject: [PATCH 32/68] feat: add LNURL-pay address detection and support in address parsing flow for Bitcoin Lightning integration --- lib/core/address_validator.dart | 1 + lib/entities/lnurlpay_record.dart | 75 +++++++++++++++++++ lib/entities/parse_address_from_domain.dart | 9 +++ lib/entities/parsed_address.dart | 11 ++- .../widgets/extract_address_from_parsed.dart | 5 ++ 5 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 lib/entities/lnurlpay_record.dart diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index df072f8a32..c700835341 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -19,6 +19,7 @@ class AddressValidator extends TextValidator { r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$', caseSensitive: false); if (lightningInvoiceRegex.hasMatch(txt)) return true; + if (txt.contains("@")) return true; return BitcoinAddressUtils.validateAddress( address: txt, diff --git a/lib/entities/lnurlpay_record.dart b/lib/entities/lnurlpay_record.dart new file mode 100644 index 0000000000..3fbb01bc3a --- /dev/null +++ b/lib/entities/lnurlpay_record.dart @@ -0,0 +1,75 @@ +import 'dart:convert'; + +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; + +class LNUrlPayRecord { + LNUrlPayRecord({ + required this.address, + required this.name, + }); + + final String name; + final String address; + + static Future checkWellKnownUsername(String username, CryptoCurrency currency) async { + if (currency != CryptoCurrency.btc) return null; + + // split the string by the @ symbol: + try { + final List splitStrs = username.split("@"); + String name = splitStrs.first.toLowerCase(); + final String domain = splitStrs.last; + + if (splitStrs.length == 3) { + // for username like @alice@domain.org instead of alice@domain.org + name = splitStrs[1]; + } + + if (name.isEmpty) { + name = "_"; + } + + // lookup domain/.well-known/nano-currency.json and check if it has a nano address: + final response = await ProxyWrapper().get( + clearnetUri: Uri.parse("https://$domain/.well-known/lnurlp/$name"), + headers: {"Accept": "application/json"}, + ); + + if (response.statusCode == 200) { + return username; + } + } catch (e) { + printV("error checking well-known username: $e"); + } + return null; + } + + static String formatDomainName(String name) { + String formattedName = name; + + if (name.contains("@")) { + formattedName = name.replaceAll("@", "."); + } + + return formattedName; + } + + static Future fetchAddressAndName({ + required String formattedName, + required CryptoCurrency currency, + }) async { + String name = formattedName; + + printV("formattedName: $formattedName"); + + final address = await checkWellKnownUsername(formattedName, currency); + + if (address == null) { + return null; + } + + return LNUrlPayRecord(address: address, name: name); + } +} diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index 4428108752..a772f49514 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/yat_service.dart'; import 'package:cake_wallet/entities/ens_record.dart'; +import 'package:cake_wallet/entities/lnurlpay_record.dart'; import 'package:cake_wallet/entities/openalias_record.dart'; import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/entities/unstoppable_domain_address.dart'; @@ -297,6 +298,14 @@ class AddressResolver { return ParsedAddress.fetchWellKnownAddress(address: record.address, name: text); } } + + if (walletType == WalletType.bitcoin && currency == CryptoCurrency.btc) { + final record = + await LNUrlPayRecord.fetchAddressAndName(formattedName: text, currency: currency); + if (record != null) { + return ParsedAddress.fetchLNUrlPayAddress(address: record.address, name: text); + } + } } if (!text.startsWith('@') && text.contains('@') && !text.contains('.')) { diff --git a/lib/entities/parsed_address.dart b/lib/entities/parsed_address.dart index 74acab80a7..a5159cbcf0 100644 --- a/lib/entities/parsed_address.dart +++ b/lib/entities/parsed_address.dart @@ -15,7 +15,8 @@ enum ParseFrom { thorChain, wellKnown, zanoAlias, - bip353 + bip353, + lnurlpay } class ParsedAddress { @@ -175,6 +176,14 @@ class ParsedAddress { ); } + factory ParsedAddress.fetchLNUrlPayAddress({required String address, required String name}) { + return ParsedAddress( + addresses: [address], + name: name, + parseFrom: ParseFrom.lnurlpay, + ); + } + final List addresses; final String name; final String description; diff --git a/lib/src/screens/send/widgets/extract_address_from_parsed.dart b/lib/src/screens/send/widgets/extract_address_from_parsed.dart index b37f87b7ff..74b7439356 100644 --- a/lib/src/screens/send/widgets/extract_address_from_parsed.dart +++ b/lib/src/screens/send/widgets/extract_address_from_parsed.dart @@ -78,6 +78,11 @@ Future extractAddressFromParsed( content = S.of(context).extracted_address_content('${parsedAddress.name} (BIP-353)'); address = parsedAddress.addresses.first; break; + case ParseFrom.lnurlpay: + title = S.of(context).address_detected; + content = S.of(context).extracted_address_content('${parsedAddress.name} (Lightning)'); + address = parsedAddress.addresses.first; + break; case ParseFrom.yatRecord: if (parsedAddress.name.isEmpty) { title = S.of(context).yat_error; From 9ac2850bbeb69eb6ee0aeb001da8aaf22472490c Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 4 Nov 2025 14:43:22 +0100 Subject: [PATCH 33/68] refactor: simplify `ReceivePageOption` logic --- cw_bitcoin/lib/bitcoin_wallet.dart | 2 + cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 16 ++++ cw_bitcoin/lib/litecoin_wallet_addresses.dart | 20 ++++- {lib/core => cw_core/lib}/payment_uris.dart | 79 +++++-------------- cw_core/lib/wallet_addresses.dart | 8 +- cw_core/pubspec.yaml | 3 +- cw_decred/lib/wallet.dart | 2 +- cw_decred/lib/wallet_addresses.dart | 25 +++--- lib/bitcoin/cw_bitcoin.dart | 24 ------ .../screens/dashboard/pages/address_page.dart | 3 +- .../screens/receive/widgets/qr_widget.dart | 2 +- lib/utils/payment_request.dart | 2 +- .../dashboard/receive_option_view_model.dart | 48 ++--------- .../exchange/exchange_trade_view_model.dart | 2 +- .../wallet_address_list_view_model.dart | 2 +- tool/configure.dart | 2 - 16 files changed, 89 insertions(+), 151 deletions(-) rename {lib/core => cw_core/lib}/payment_uris.dart (87%) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 9b212ff5f9..304618e714 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -101,6 +101,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { apiKey: secrets.breezApiKey, lnurlDomain: "breez.tips", ); + } else { + lightningWallet = null; } payjoinManager = PayjoinManager(PayjoinStorage(payjoinBox), this); diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 8e36ffebf8..91308f630e 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -1,8 +1,10 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/bip/bip/bip32/bip32.dart'; +import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/payjoin/manager.dart'; import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; @@ -89,4 +91,18 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S if (!_isPayjoinConnectivityError(e.toString())) rethrow; } } + + @override + List get receivePageOptions { + if (isHardwareWallet) { + return [ + ...BitcoinReceivePageOption.allViewOnly, + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ]; + } + return [ + ...BitcoinReceivePageOption.all, + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ]; + } } diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 76228c16de..2095d4bd4e 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -5,9 +5,11 @@ import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; -import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; +import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; @@ -46,6 +48,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with bool generating = false; List get scanSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; + List get spendPubkey => mwebHd!.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed; @@ -208,4 +211,19 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with .where((element) => element.type == SegwitAddresType.p2wpkh && !element.isUsed); return addresses.first.address; } + + @override + List get receivePageOptions { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows || isHardwareWallet) { + return [ + ...BitcoinReceivePageOption.allLitecoin + .where((element) => element != BitcoinReceivePageOption.mweb), + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ]; + } + return [ + ...BitcoinReceivePageOption.allLitecoin, + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ]; + } } diff --git a/lib/core/payment_uris.dart b/cw_core/lib/payment_uris.dart similarity index 87% rename from lib/core/payment_uris.dart rename to cw_core/lib/payment_uris.dart index 38f4f6d2ef..fc2bd2b880 100644 --- a/lib/core/payment_uris.dart +++ b/cw_core/lib/payment_uris.dart @@ -13,10 +13,7 @@ class MoneroURI extends PaymentURI { @override String toString() { var base = 'monero:$address'; - - if (amount.isNotEmpty) { - base += '?tx_amount=${amount.replaceAll(',', '.')}'; - } + if (amount.isNotEmpty) base += '?tx_amount=${amount.replaceAll(',', '.')}'; return base; } @@ -28,10 +25,7 @@ class HavenURI extends PaymentURI { @override String toString() { var base = 'haven:$address'; - - if (amount.isNotEmpty) { - base += '?tx_amount=${amount.replaceAll(',', '.')}'; - } + if (amount.isNotEmpty) base += '?tx_amount=${amount.replaceAll(',', '.')}'; return base; } @@ -62,10 +56,7 @@ class LitecoinURI extends PaymentURI { @override String toString() { var base = 'litecoin:$address'; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } + if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; return base; } @@ -77,10 +68,7 @@ class EthereumURI extends PaymentURI { @override String toString() { var base = 'ethereum:$address'; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } + if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; return base; } @@ -92,10 +80,7 @@ class BaseURI extends PaymentURI { @override String toString() { var base = 'base:$address'; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } + if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; return base; } @@ -122,10 +107,7 @@ class BitcoinCashURI extends PaymentURI { @override String toString() { var base = address; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } + if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; return base; } @@ -151,10 +133,7 @@ class PolygonURI extends PaymentURI { @override String toString() { var base = 'polygon:$address'; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } + if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; return base; } @@ -166,10 +145,7 @@ class SolanaURI extends PaymentURI { @override String toString() { var base = 'solana:$address'; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } + if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; return base; } @@ -181,10 +157,7 @@ class TronURI extends PaymentURI { @override String toString() { var base = 'tron:$address'; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } + if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; return base; } @@ -196,10 +169,7 @@ class WowneroURI extends PaymentURI { @override String toString() { var base = 'wownero:$address'; - - if (amount.isNotEmpty) { - base += '?tx_amount=${amount.replaceAll(',', '.')}'; - } + if (amount.isNotEmpty) base += '?tx_amount=${amount.replaceAll(',', '.')}'; return base; } @@ -211,11 +181,8 @@ class ZanoURI extends PaymentURI { @override String toString() { - var base = 'zano:' + address; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } + var base = 'zano:$address'; + if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; return base; } @@ -227,11 +194,8 @@ class DecredURI extends PaymentURI { @override String toString() { - var base = 'decred:' + address; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } + var base = 'decred:$address'; + if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; return base; } @@ -243,11 +207,8 @@ class DogeURI extends PaymentURI { @override String toString() { - var base = 'doge:' + address; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } + var base = 'doge:$address'; + if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; return base; } @@ -271,13 +232,9 @@ class ERC681URI extends PaymentURI { final targetAddress = contractAddress ?? address; uri += targetAddress; - if (chainId != 1) { - uri += '@$chainId'; - } + if (chainId != 1) uri += '@$chainId'; - if (contractAddress != null) { - uri += '/transfer'; - } + if (contractAddress != null) uri += '/transfer'; final params = {}; diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index 4d4d2c0a5a..ac4128e427 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -1,9 +1,10 @@ +import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; abstract class WalletAddresses { - WalletAddresses(this.walletInfo) + WalletAddresses(this.walletInfo, [this.isTestnet = false]) : addressesMap = {}, allAddressesMap = {}, addressInfos = {}, @@ -17,6 +18,8 @@ abstract class WalletAddresses { final WalletInfo walletInfo; + final bool isTestnet; + String get address; String get latestAddress { @@ -79,4 +82,7 @@ abstract class WalletAddresses { bool containsAddress(String address) => addressesMap.containsKey(address) || allAddressesMap.containsKey(address); + + List get receivePageOptions => ReceivePageOptions; + } diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml index 7c963f246e..49188f5177 100644 --- a/cw_core/pubspec.yaml +++ b/cw_core/pubspec.yaml @@ -2,11 +2,10 @@ name: cw_core description: A new Flutter package project. version: 0.0.1 publish_to: none -author: Cake Wallet homepage: https://cakewallet.com environment: - sdk: ">=2.17.5 <3.0.0" + sdk: '>=3.0.6 <4.0.0' flutter: ">=1.20.0" dependencies: diff --git a/cw_decred/lib/wallet.dart b/cw_decred/lib/wallet.dart index 432edb4bc0..97c118331b 100644 --- a/cw_decred/lib/wallet.dart +++ b/cw_decred/lib/wallet.dart @@ -54,7 +54,7 @@ abstract class DecredWalletBase derivationInfo.derivationPath == DecredWalletService.pubkeyRestorePathTestnet, super(walletInfo, derivationInfo) { - walletAddresses = DecredWalletAddresses(walletInfo, libwallet); + walletAddresses = DecredWalletAddresses(walletInfo, libwallet, isTestnet); transactionHistory = DecredTransactionHistory(); reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) { diff --git a/cw_decred/lib/wallet_addresses.dart b/cw_decred/lib/wallet_addresses.dart index e4af108b9d..96f007c5cc 100644 --- a/cw_decred/lib/wallet_addresses.dart +++ b/cw_decred/lib/wallet_addresses.dart @@ -1,8 +1,8 @@ import 'dart:convert'; +import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:mobx/mobx.dart'; -import 'package:cw_core/address_info.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_decred/api/libdcrwallet.dart'; @@ -12,9 +12,8 @@ part 'wallet_addresses.g.dart'; class DecredWalletAddresses = DecredWalletAddressesBase with _$DecredWalletAddresses; abstract class DecredWalletAddressesBase extends WalletAddresses with Store { - DecredWalletAddressesBase(WalletInfo walletInfo, Libwallet libwallet) - : _libwallet = libwallet, - super(walletInfo); + DecredWalletAddressesBase(super.walletInfo, Libwallet libwallet, super.isTestnet) + : _libwallet = libwallet; final Libwallet _libwallet; String currentAddr = ''; @@ -26,14 +25,10 @@ abstract class DecredWalletAddressesBase extends WalletAddresses with Store { @override @computed - String get address { - return selectedAddr; - } + String get address => selectedAddr; @override - set address(value) { - selectedAddr = value; - } + set address(value) => selectedAddr = value; @override Future init() async { @@ -145,6 +140,16 @@ abstract class DecredWalletAddressesBase extends WalletAddresses with Store { selectedAddr = addr; await saveAddressesInBox(); } + + @override + List get receivePageOptions { + return isTestnet + ? [ + ReceivePageOption.testnet, + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ] + : ReceivePageOptions; + } } class LibAddresses { diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 27ffcef0d0..d26bc20e08 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -304,30 +304,6 @@ class CWBitcoin extends Bitcoin { return bitcoinWallet.walletAddresses.addressPageType == SilentPaymentsAddresType.p2sp; } - @override - 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(Object wallet) { - final litecoinWallet = wallet as ElectrumWallet; - if (Platform.isLinux || - Platform.isMacOS || - Platform.isWindows || - litecoinWallet.isHardwareWallet) { - return BitcoinReceivePageOption.allLitecoin - .where((element) => element != BitcoinReceivePageOption.mweb) - .toList(); - } - return BitcoinReceivePageOption.allLitecoin; - } - @override BitcoinAddressType getBitcoinAddressType(ReceivePageOption option) { switch (option) { diff --git a/lib/src/screens/dashboard/pages/address_page.dart b/lib/src/screens/dashboard/pages/address_page.dart index c7e5c8793c..d4a71e37c0 100644 --- a/lib/src/screens/dashboard/pages/address_page.dart +++ b/lib/src/screens/dashboard/pages/address_page.dart @@ -306,8 +306,7 @@ class AddressPage extends BasePage { } break; default: - if (addressListViewModel.type == WalletType.bitcoin || - addressListViewModel.type == WalletType.litecoin) { + if ([WalletType.bitcoin, WalletType.litecoin].contains(addressListViewModel.type)) { addressListViewModel.setAddressType(bitcoin!.getBitcoinAddressType(option)); } } diff --git a/lib/src/screens/receive/widgets/qr_widget.dart b/lib/src/screens/receive/widgets/qr_widget.dart index 6a40850b64..7e4df944c8 100644 --- a/lib/src/screens/receive/widgets/qr_widget.dart +++ b/lib/src/screens/receive/widgets/qr_widget.dart @@ -1,4 +1,4 @@ -import 'package:cake_wallet/core/payment_uris.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/routes.dart'; diff --git a/lib/utils/payment_request.dart b/lib/utils/payment_request.dart index b574ab260d..6149379d71 100644 --- a/lib/utils/payment_request.dart +++ b/lib/utils/payment_request.dart @@ -1,4 +1,4 @@ -import 'package:cake_wallet/core/payment_uris.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cake_wallet/nano/nano.dart'; class PaymentRequest { diff --git a/lib/view_model/dashboard/receive_option_view_model.dart b/lib/view_model/dashboard/receive_option_view_model.dart index 8aab2736d6..69b47e7b9f 100644 --- a/lib/view_model/dashboard/receive_option_view_model.dart +++ b/lib/view_model/dashboard/receive_option_view_model.dart @@ -11,59 +11,21 @@ class ReceiveOptionViewModel = ReceiveOptionViewModelBase with _$ReceiveOptionVi abstract class ReceiveOptionViewModelBase with Store { ReceiveOptionViewModelBase(this._wallet, this.initialPageOption) : selectedReceiveOption = initialPageOption ?? - (_wallet.type == WalletType.bitcoin || - _wallet.type == WalletType.litecoin + ([WalletType.bitcoin, WalletType.litecoin].contains(_wallet.type) ? bitcoin!.getSelectedAddressType(_wallet) - : (_wallet.type == WalletType.decred && _wallet.isTestnet) + : (_wallet.type == WalletType.decred && _wallet.isTestnet) ? ReceivePageOption.testnet - : ReceivePageOption.mainnet), - _options = [] { - final walletType = _wallet.type; - switch (walletType) { - case WalletType.bitcoin: - _options = [ - ...bitcoin!.getBitcoinReceivePageOptions(_wallet), - ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) - ]; - break; - case WalletType.litecoin: - _options = [ - ...bitcoin!.getLitecoinReceivePageOptions(_wallet), - ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) - ]; - break; - case WalletType.haven: - _options = [ReceivePageOption.mainnet]; - break; - case WalletType.decred: - if (_wallet.isTestnet) { - _options = [ - ReceivePageOption.testnet, - ...ReceivePageOptions.where( - (element) => element != ReceivePageOption.mainnet) - ]; - } else { - _options = ReceivePageOptions; - } - break; - default: - _options = ReceivePageOptions; - } - } + : ReceivePageOption.mainnet); final WalletBase _wallet; final ReceivePageOption? initialPageOption; - List _options; - @observable ReceivePageOption selectedReceiveOption; - List get options => _options; + List get options => _wallet.walletAddresses.receivePageOptions; @action - void selectReceiveOption(ReceivePageOption option) { - selectedReceiveOption = option; - } + void selectReceiveOption(ReceivePageOption option) => selectedReceiveOption = option; } diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index fec47f3f35..3f4240cf21 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:cake_wallet/core/payment_uris.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index fd6b29bdd5..adc60cad02 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -4,7 +4,7 @@ import 'dart:core'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/fiat_conversion_service.dart'; -import 'package:cake_wallet/core/payment_uris.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; diff --git a/tool/configure.dart b/tool/configure.dart index dbca11d49f..7534457aca 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -228,8 +228,6 @@ abstract class Bitcoin { Map> getElectrumDerivations(); Future setAddressType(Object wallet, dynamic option); ReceivePageOption getSelectedAddressType(Object wallet); - List getBitcoinReceivePageOptions(Object wallet); - List getLitecoinReceivePageOptions(Object wallet); BitcoinAddressType getBitcoinAddressType(ReceivePageOption option); bool isPayjoinAvailable(Object wallet); bool hasSelectedSilentPayments(Object wallet); From b0baf54dadfa27a35999f8dc597376535b718976 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 4 Nov 2025 16:24:15 +0100 Subject: [PATCH 34/68] refactor: centralize `PaymentURI` generation logic across wallet types --- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 5 +++++ cw_bitcoin/lib/electrum_wallet_addresses.dart | 4 ++++ cw_bitcoin/lib/litecoin_wallet_addresses.dart | 4 ++++ .../lib/src/bitcoin_cash_wallet_addresses.dart | 4 ++++ cw_core/lib/payment_uris.dart | 12 ------------ cw_core/lib/wallet_addresses.dart | 6 ++++-- cw_decred/lib/wallet_addresses.dart | 4 ++++ .../lib/src/dogecoin_wallet_addresses.dart | 4 ++++ cw_evm/lib/evm_chain_wallet.dart | 2 +- cw_evm/lib/evm_chain_wallet_addresses.dart | 17 ++++++++++++++++- cw_monero/lib/monero_wallet_addresses.dart | 5 ++++- cw_nano/lib/nano_wallet_addresses.dart | 5 +++++ cw_solana/lib/solana_wallet_addresses.dart | 4 ++++ cw_tron/lib/tron_wallet_addresses.dart | 4 ++++ cw_wownero/lib/wownero_wallet_addresses.dart | 6 ++++-- cw_zano/lib/zano_wallet_addresses.dart | 5 ++++- .../exchange/exchange_trade_view_model.dart | 2 -- 17 files changed, 71 insertions(+), 22 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 91308f630e..3f572e2401 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -4,6 +4,7 @@ import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/payjoin/manager.dart'; import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -105,4 +106,8 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) ]; } + + @override + PaymentURI getPaymentUri(String amount) => + BitcoinURI(amount: amount, address: address, pjUri: payjoinEndpoint ?? ''); } diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 7ef455793f..18de7eb3ad 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -7,6 +7,7 @@ import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/lightning/lightning_addres_type.dart'; import 'package:cw_bitcoin/lightning/lightning_wallet.dart'; import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; @@ -765,4 +766,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { await lightningWallet!.registerAddress(walletName.replaceAll(" ", "").toLowerCase()); } } + + @override + PaymentURI getPaymentUri(String amount) => BitcoinURI(amount: amount, address: address); } diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 2095d4bd4e..8d6e24b5f8 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -9,6 +9,7 @@ import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -226,4 +227,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) ]; } + + @override + PaymentURI getPaymentUri(String amount) => LitecoinURI(amount: amount, address: address); } diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart index fe0ebc8284..681fc00d73 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -2,6 +2,7 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -28,4 +29,7 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi required Bip32Slip10Secp256k1 hd, BitcoinAddressType? addressType}) => generateP2PKHAddress(hd: hd, index: index, network: network); + + @override + PaymentURI getPaymentUri(String amount) => BitcoinCashURI(amount: amount, address: address); } diff --git a/cw_core/lib/payment_uris.dart b/cw_core/lib/payment_uris.dart index fc2bd2b880..b4ba788e51 100644 --- a/cw_core/lib/payment_uris.dart +++ b/cw_core/lib/payment_uris.dart @@ -19,18 +19,6 @@ class MoneroURI extends PaymentURI { } } -class HavenURI extends PaymentURI { - HavenURI({required super.amount, required super.address}); - - @override - String toString() { - var base = 'haven:$address'; - if (amount.isNotEmpty) base += '?tx_amount=${amount.replaceAll(',', '.')}'; - - return base; - } -} - class BitcoinURI extends PaymentURI { BitcoinURI({required super.amount, required super.address, this.pjUri = ''}); diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index ac4128e427..b2d28df7df 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -1,3 +1,4 @@ +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; @@ -23,8 +24,8 @@ abstract class WalletAddresses { String get address; String get latestAddress { - if (walletInfo.type == WalletType.monero || walletInfo.type == WalletType.wownero) { - if (addressesMap.keys.length == 0) return address; + if ([WalletType.monero, WalletType.wownero].contains(walletInfo.type)) { + if (addressesMap.keys.isEmpty) return address; return addressesMap[addressesMap.keys.last] ?? address; } return _localAddress ?? address; @@ -85,4 +86,5 @@ abstract class WalletAddresses { List get receivePageOptions => ReceivePageOptions; + PaymentURI getPaymentUri(String amount); } diff --git a/cw_decred/lib/wallet_addresses.dart b/cw_decred/lib/wallet_addresses.dart index 96f007c5cc..f7d0a8baec 100644 --- a/cw_decred/lib/wallet_addresses.dart +++ b/cw_decred/lib/wallet_addresses.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:mobx/mobx.dart'; @@ -150,6 +151,9 @@ abstract class DecredWalletAddressesBase extends WalletAddresses with Store { ] : ReceivePageOptions; } + + @override + PaymentURI getPaymentUri(String amount) => DecredURI(amount: amount, address: address); } class LibAddresses { diff --git a/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart b/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart index 75d06c1484..3dc72526fd 100644 --- a/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart +++ b/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart @@ -2,6 +2,7 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -26,4 +27,7 @@ abstract class DogeCoinWalletAddressesBase extends ElectrumWalletAddresses with required Bip32Slip10Secp256k1 hd, BitcoinAddressType? addressType}) => generateP2PKHAddress(hd: hd, index: index, network: network); + + @override + PaymentURI getPaymentUri(String amount) => DogeURI(amount: amount, address: address); } diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index 0db8781c7a..f952117485 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -82,7 +82,7 @@ abstract class EVMChainWalletBase _hexPrivateKey = privateKey, _isTransactionUpdating = false, _client = client, - walletAddresses = EVMChainWalletAddresses(walletInfo), + walletAddresses = EVMChainWalletAddresses(walletInfo, client.chainId), balance = ObservableMap.of( { // Not sure of this yet, will it work? will it not? diff --git a/cw_evm/lib/evm_chain_wallet_addresses.dart b/cw_evm/lib/evm_chain_wallet_addresses.dart index 7dd501cc5e..bfa4938a32 100644 --- a/cw_evm/lib/evm_chain_wallet_addresses.dart +++ b/cw_evm/lib/evm_chain_wallet_addresses.dart @@ -1,5 +1,6 @@ import 'dart:developer'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -9,10 +10,12 @@ part 'evm_chain_wallet_addresses.g.dart'; class EVMChainWalletAddresses = EVMChainWalletAddressesBase with _$EVMChainWalletAddresses; abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store { - EVMChainWalletAddressesBase(WalletInfo walletInfo) + EVMChainWalletAddressesBase(WalletInfo walletInfo, this.chainId) : address = '', super(walletInfo); + final int chainId; + @override @observable String address; @@ -36,4 +39,16 @@ abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store { log(e.toString()); } } + + @override + PaymentURI getPaymentUri(String amount) { + switch (chainId) { + case 8453: + return BaseURI(amount: amount, address: address); + case 137: + return PolygonURI(amount: amount, address: address); + default: + return EthereumURI(amount: amount, address: address); + } + } } diff --git a/cw_monero/lib/monero_wallet_addresses.dart b/cw_monero/lib/monero_wallet_addresses.dart index 0b38ac5fd6..51c3e0f0a9 100644 --- a/cw_monero/lib/monero_wallet_addresses.dart +++ b/cw_monero/lib/monero_wallet_addresses.dart @@ -1,5 +1,5 @@ import 'package:cw_core/account.dart'; -import 'package:cw_core/address_info.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/subaddress.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; @@ -155,4 +155,7 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { @override bool containsAddress(String address) => addressInfos[account?.id ?? 0]?.any((it) => it.address == address) ?? false; + + @override + PaymentURI getPaymentUri(String amount) => MoneroURI(amount: amount, address: address); } diff --git a/cw_nano/lib/nano_wallet_addresses.dart b/cw_nano/lib/nano_wallet_addresses.dart index f1ff14a854..f52cf4ca1f 100644 --- a/cw_nano/lib/nano_wallet_addresses.dart +++ b/cw_nano/lib/nano_wallet_addresses.dart @@ -1,4 +1,5 @@ import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; @@ -15,6 +16,7 @@ abstract class NanoWalletAddressesBase extends WalletAddresses with Store { : accountList = NanoAccountList(walletInfo.address), address = '', super(walletInfo); + @override @observable String address; @@ -51,4 +53,7 @@ abstract class NanoWalletAddressesBase extends WalletAddresses with Store { printV(e.toString()); } } + + @override + PaymentURI getPaymentUri(String amount) => NanoURI(amount: amount, address: address); } diff --git a/cw_solana/lib/solana_wallet_addresses.dart b/cw_solana/lib/solana_wallet_addresses.dart index 7e9bd90089..634c73f375 100644 --- a/cw_solana/lib/solana_wallet_addresses.dart +++ b/cw_solana/lib/solana_wallet_addresses.dart @@ -1,3 +1,4 @@ +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; @@ -34,4 +35,7 @@ abstract class SolanaWalletAddressesBase extends WalletAddresses with Store { printV(e.toString()); } } + + @override + PaymentURI getPaymentUri(String amount) => SolanaURI(amount: amount, address: address); } diff --git a/cw_tron/lib/tron_wallet_addresses.dart b/cw_tron/lib/tron_wallet_addresses.dart index 095f97fa9a..99767e9654 100644 --- a/cw_tron/lib/tron_wallet_addresses.dart +++ b/cw_tron/lib/tron_wallet_addresses.dart @@ -1,5 +1,6 @@ import 'dart:developer'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -36,4 +37,7 @@ abstract class TronWalletAddressesBase extends WalletAddresses with Store { log(e.toString()); } } + + @override + PaymentURI getPaymentUri(String amount) => TronURI(amount: amount, address: address); } diff --git a/cw_wownero/lib/wownero_wallet_addresses.dart b/cw_wownero/lib/wownero_wallet_addresses.dart index 936c187247..c95d397631 100644 --- a/cw_wownero/lib/wownero_wallet_addresses.dart +++ b/cw_wownero/lib/wownero_wallet_addresses.dart @@ -1,10 +1,9 @@ import 'package:cw_core/account.dart'; -import 'package:cw_core/address_info.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/subaddress.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:cw_wownero/api/transaction_history.dart'; import 'package:cw_wownero/api/subaddress_list.dart' as subaddress_list; import 'package:cw_wownero/api/wallet.dart'; import 'package:cw_wownero/wownero_account_list.dart'; @@ -151,4 +150,7 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { @override bool containsAddress(String address) => addressInfos[account?.id ?? 0]?.any((it) => it.address == address) ?? false; + + @override + PaymentURI getPaymentUri(String amount) => WowneroURI(amount: amount, address: address); } diff --git a/cw_zano/lib/zano_wallet_addresses.dart b/cw_zano/lib/zano_wallet_addresses.dart index 39e61be7f0..1562ea8eee 100644 --- a/cw_zano/lib/zano_wallet_addresses.dart +++ b/cw_zano/lib/zano_wallet_addresses.dart @@ -1,7 +1,7 @@ +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:cw_zano/zano_wallet_api.dart'; import 'package:mobx/mobx.dart'; part 'zano_wallet_addresses.g.dart'; @@ -38,4 +38,7 @@ abstract class ZanoWalletAddressesBase extends WalletAddresses with Store { printV(e.toString()); } } + + @override + PaymentURI getPaymentUri(String amount) => ZanoURI(amount: amount, address: address); } diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 3f4240cf21..09b478da79 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -442,8 +442,6 @@ abstract class ExchangeTradeViewModelBase with Store { return ZanoURI(amount: amount, address: inputAddress); case WalletType.decred: return DecredURI(amount: amount, address: inputAddress); - case WalletType.haven: - return HavenURI(amount: amount, address: inputAddress); case WalletType.nano: return NanoURI(amount: amount, address: inputAddress); default: From 689c56ea8d7d09c191bab0306ae9f5852055ec04 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 4 Nov 2025 20:51:58 +0100 Subject: [PATCH 35/68] feat: enhance `PaymentURI` handling with asynchronous support and Lightning-specific functionality --- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 12 ++++ .../lib/lightning/lightning_wallet.dart | 14 +++- cw_core/lib/payment_uris.dart | 14 ++++ cw_core/lib/wallet_addresses.dart | 7 ++ .../wallet_address_list_view_model.dart | 70 ++++++------------- 5 files changed, 66 insertions(+), 51 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 3f572e2401..9cc085587c 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -2,8 +2,10 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/bip/bip/bip32/bip32.dart'; import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; +import 'package:cw_bitcoin/lightning/lightning_addres_type.dart'; import 'package:cw_bitcoin/payjoin/manager.dart'; import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/parse_fixed.dart'; import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/unspent_coin_type.dart'; @@ -110,4 +112,14 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S @override PaymentURI getPaymentUri(String amount) => BitcoinURI(amount: amount, address: address, pjUri: payjoinEndpoint ?? ''); + + Future getPaymentRequestUri(String amount) async { + if (addressPageType is LightningAddressType && lightningWallet != null) { + final amountSats = amount.isNotEmpty ? parseFixed(amount, 9) : null; + final invoice = await lightningWallet!.getBolt11Invoice(amountSats, "Send to Cake Wallet"); + return LightningPaymentRequest(address: address, amount: amount, bolt11Invoice: invoice); + } + print(amount); + return getPaymentUri(amount); + } } diff --git a/cw_bitcoin/lib/lightning/lightning_wallet.dart b/cw_bitcoin/lib/lightning/lightning_wallet.dart index 6ae40da518..4d44dc3b58 100644 --- a/cw_bitcoin/lib/lightning/lightning_wallet.dart +++ b/cw_bitcoin/lib/lightning/lightning_wallet.dart @@ -59,6 +59,19 @@ class LightningWallet { .lightningAddress; } + Future getBolt11Invoice(BigInt? amount, String description) async { + final response = await sdk.receivePayment( + request: ReceivePaymentRequest( + paymentMethod: ReceivePaymentMethod.bolt11Invoice( + description: description, + amountSats: amount, + ), + ), + ); + + return response.paymentRequest; + } + Future isCompatible(String input) async { try { final inputType = await sdk.parse(input: input); @@ -186,7 +199,6 @@ class LightningWallet { isPending: payment.status == PaymentStatus.pending, date: DateTime.fromMillisecondsSinceEpoch(payment.timestamp.toInt() * 1000), confirmations: payment.status == PaymentStatus.pending ? 0 : 10, - ); } diff --git a/cw_core/lib/payment_uris.dart b/cw_core/lib/payment_uris.dart index b4ba788e51..806726d4d9 100644 --- a/cw_core/lib/payment_uris.dart +++ b/cw_core/lib/payment_uris.dart @@ -34,10 +34,24 @@ class BitcoinURI extends PaymentURI { qp['pj'] = pjUri; } + print(qp); return Uri(scheme: 'bitcoin', path: address, queryParameters: qp).toString(); } } +class LightningPaymentRequest extends PaymentURI { + LightningPaymentRequest({ + required super.amount, + required super.address, + required this.bolt11Invoice, + }); + + final String bolt11Invoice; + + @override + String toString() => bolt11Invoice; +} + class LitecoinURI extends PaymentURI { LitecoinURI({required super.amount, required super.address}); diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index b2d28df7df..c802065202 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -86,5 +86,12 @@ abstract class WalletAddresses { List get receivePageOptions => ReceivePageOptions; + /// Get a [PaymentURI] for the current [address] + /// e.g. ethereum:0x0 PaymentURI getPaymentUri(String amount); + + + /// Get a [PaymentURI] for the current [address] asynchronously + /// this can be used if a payment requires a api call beforehand + Future getPaymentRequestUri(String amount) async => getPaymentRequestUri(amount); } diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index adc60cad02..92f4c24a78 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -1,11 +1,11 @@ -import 'dart:developer' as dev; import 'dart:core'; +import 'dart:developer' as dev; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/fiat_conversion_service.dart'; -import 'package:cw_core/payment_uris.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; @@ -16,23 +16,23 @@ import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/reactions/wallet_utils.dart'; import 'package:cake_wallet/solana/solana.dart'; -import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:cake_wallet/tron/tron.dart'; -import 'package:cake_wallet/utils/qr_util.dart'; -import 'package:cake_wallet/zano/zano.dart'; import 'package:cake_wallet/utils/list_item.dart'; +import 'package:cake_wallet/utils/qr_util.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_hidden_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cake_wallet/zano/zano.dart'; import 'package:cw_core/amount_converter.dart'; import 'package:cw_core/currency.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:intl/intl.dart'; import 'package:mobx/mobx.dart'; @@ -49,9 +49,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo }) : _baseItems = [], selectedCurrency = walletTypeToCryptoCurrency(appStore.wallet!.type), _cryptoNumberFormat = NumberFormat(_cryptoNumberPattern), - hasAccounts = [WalletType.monero, WalletType.wownero, WalletType.haven] - .contains(appStore.wallet!.type), - amount = '', + hasAccounts = [WalletType.monero, WalletType.wownero].contains(appStore.wallet!.type), _settingsStore = appStore.settingsStore, super(appStore: appStore) { _init(); @@ -62,7 +60,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo _init(); selectedCurrency = walletTypeToCryptoCurrency(wallet.type); - hasAccounts = [WalletType.monero, WalletType.wownero, WalletType.haven].contains(wallet.type); + hasAccounts = [WalletType.monero, WalletType.wownero].contains(wallet.type); } static const String _cryptoNumberPattern = '0.00000000'; @@ -95,7 +93,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo int get selectedCurrencyIndex => currencies.indexOf(selectedCurrency); @observable - String amount; + String amount = ''; @computed WalletType get type => wallet.type; @@ -112,46 +110,14 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo bool get isPayjoinUnavailable => wallet.type == WalletType.bitcoin && _settingsStore.usePayjoin && payjoinEndpoint.isEmpty; - @computed - PaymentURI get uri { - switch (wallet.type) { - case WalletType.monero: - return MoneroURI(amount: amount, address: address.address); - case WalletType.haven: - return HavenURI(amount: amount, address: address.address); - case WalletType.bitcoin: - return BitcoinURI(amount: amount, address: address.address, pjUri: payjoinEndpoint); - case WalletType.litecoin: - return LitecoinURI(amount: amount, address: address.address); - case WalletType.ethereum: - return EthereumURI(amount: amount, address: address.address); - case WalletType.bitcoinCash: - return BitcoinCashURI(amount: amount, address: address.address); - case WalletType.banano: - return NanoURI(amount: amount, address: address.address); - case WalletType.nano: - return NanoURI(amount: amount, address: address.address); - case WalletType.polygon: - return PolygonURI(amount: amount, address: address.address); - case WalletType.solana: - return SolanaURI(amount: amount, address: address.address); - case WalletType.tron: - return TronURI(amount: amount, address: address.address); - case WalletType.wownero: - return WowneroURI(amount: amount, address: address.address); - case WalletType.zano: - return ZanoURI(amount: amount, address: address.address); - case WalletType.decred: - return DecredURI(amount: amount, address: address.address); - case WalletType.dogecoin: - return DogeURI(amount: amount, address: address.address); - case WalletType.base: - return BaseURI(amount: amount, address: address.address); - case WalletType.arbitrum: - return ArbitrumURI(amount: amount, address: address.address); - case WalletType.none: - throw Exception('Unexpected type: ${type.toString()}'); - } + @observable + late PaymentURI uri; + + @action + Future refreshUri() async { + print(amount); + uri = await wallet.walletAddresses.getPaymentRequestUri(amount); + print(uri); } @computed @@ -518,6 +484,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo void _init() { _baseItems = []; + uri = wallet.walletAddresses.getPaymentUri(amount); if (wallet.walletAddresses.hiddenAddresses.isNotEmpty) { _baseItems.add(WalletAddressHiddenListHeader()); @@ -537,6 +504,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo if (wallet.isEnabledAutoGenerateSubaddress) { wallet.walletAddresses.address = wallet.walletAddresses.latestAddress; } + + reaction((_) => amount, (_) => refreshUri()); + reaction((_) => address, (_) => refreshUri()); } @action From e0932b4e9af2e331a54a92b423443d9d7167f13c Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 4 Nov 2025 21:52:00 +0100 Subject: [PATCH 36/68] refactor: streamline `PaymentURI` logic and remove redundant URI implementations across wallet types --- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 2 +- cw_bitcoin/lib/electrum_wallet_addresses.dart | 4 - cw_bitcoin/lib/litecoin_wallet_addresses.dart | 4 - .../src/bitcoin_cash_wallet_addresses.dart | 2 +- .../lib/hardware/device_connection_type.dart | 6 + cw_core/lib/payment_uris.dart | 249 ++++++------------ cw_core/lib/wallet_addresses.dart | 9 +- cw_decred/lib/wallet_addresses.dart | 77 +++--- .../lib/src/dogecoin_wallet_addresses.dart | 14 +- cw_evm/lib/evm_chain_wallet.dart | 2 +- cw_evm/lib/evm_chain_wallet_addresses.dart | 17 +- cw_monero/lib/monero_wallet_addresses.dart | 2 +- cw_nano/lib/nano_wallet_addresses.dart | 4 - cw_solana/lib/solana_wallet_addresses.dart | 4 - cw_tron/lib/tron_wallet_addresses.dart | 4 - cw_wownero/lib/wownero_wallet_addresses.dart | 5 +- cw_zano/lib/zano_wallet_addresses.dart | 4 - .../exchange/exchange_trade_view_model.dart | 26 +- .../wallet_address_list_view_model.dart | 2 - 19 files changed, 147 insertions(+), 290 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 9cc085587c..7df16b020e 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -111,7 +111,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S @override PaymentURI getPaymentUri(String amount) => - BitcoinURI(amount: amount, address: address, pjUri: payjoinEndpoint ?? ''); + BitcoinURI(address: address, amount: amount, pjUri: payjoinEndpoint ?? ''); Future getPaymentRequestUri(String amount) async { if (addressPageType is LightningAddressType && lightningWallet != null) { diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 18de7eb3ad..7ef455793f 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -7,7 +7,6 @@ import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/lightning/lightning_addres_type.dart'; import 'package:cw_bitcoin/lightning/lightning_wallet.dart'; import 'package:cw_core/pathForWallet.dart'; -import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; @@ -766,7 +765,4 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { await lightningWallet!.registerAddress(walletName.replaceAll(" ", "").toLowerCase()); } } - - @override - PaymentURI getPaymentUri(String amount) => BitcoinURI(amount: amount, address: address); } diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 8d6e24b5f8..2095d4bd4e 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -9,7 +9,6 @@ import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/utils.dart'; -import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -227,7 +226,4 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) ]; } - - @override - PaymentURI getPaymentUri(String amount) => LitecoinURI(amount: amount, address: address); } diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart index 681fc00d73..25c11b7639 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -31,5 +31,5 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi generateP2PKHAddress(hd: hd, index: index, network: network); @override - PaymentURI getPaymentUri(String amount) => BitcoinCashURI(amount: amount, address: address); + PaymentURI getPaymentUri(String amount) => BitcoinCashURI(address: address, amount: amount); } diff --git a/cw_core/lib/hardware/device_connection_type.dart b/cw_core/lib/hardware/device_connection_type.dart index 76f07edf18..fcdb1545ae 100644 --- a/cw_core/lib/hardware/device_connection_type.dart +++ b/cw_core/lib/hardware/device_connection_type.dart @@ -36,6 +36,12 @@ enum DeviceConnectionType { WalletType.polygon, ].contains(walletType); break; + case HardwareWalletType.cupcake: + case HardwareWalletType.coldcard: + case HardwareWalletType.seedsigner: + case HardwareWalletType.keystone: + // This should not be thrown since it should never reach this code for these HardwareWalletTypes + throw UnimplementedError(); } return isSupported diff --git a/cw_core/lib/payment_uris.dart b/cw_core/lib/payment_uris.dart index 806726d4d9..ed172ca044 100644 --- a/cw_core/lib/payment_uris.dart +++ b/cw_core/lib/payment_uris.dart @@ -1,26 +1,41 @@ -import 'package:cw_core/format_fixed.dart'; +import "package:cw_core/format_fixed.dart"; -abstract class PaymentURI { - PaymentURI({required this.amount, required this.address}); +class PaymentURI { + const PaymentURI({required this.scheme, required this.address, required this.amount}); - final String amount; + final String scheme; final String address; + final String amount; + + String toString() { + final queryParameters = {}; + + if (amount.isNotEmpty) queryParameters["amount"] = amount.replaceAll(",", "."); + + return Uri(scheme: scheme, path: address, queryParameters: queryParameters).toString(); + } } class MoneroURI extends PaymentURI { - MoneroURI({required super.amount, required super.address}); + const MoneroURI({required super.address, required super.amount, super.scheme = "monero"}); @override String toString() { - var base = 'monero:$address'; - if (amount.isNotEmpty) base += '?tx_amount=${amount.replaceAll(',', '.')}'; + final queryParameters = {}; - return base; + if (amount.isNotEmpty) queryParameters["tx_amount"] = amount.replaceAll(",", "."); + + return Uri(scheme: scheme, path: address, queryParameters: queryParameters).toString(); } } class BitcoinURI extends PaymentURI { - BitcoinURI({required super.amount, required super.address, this.pjUri = ''}); + const BitcoinURI({ + required super.address, + required super.amount, + this.pjUri = "", + super.scheme = "bitcoin", + }); final String pjUri; @@ -28,23 +43,22 @@ class BitcoinURI extends PaymentURI { String toString() { final qp = {}; - if (amount.isNotEmpty) qp['amount'] = amount.replaceAll(',', '.'); + if (amount.isNotEmpty) qp["amount"] = amount.replaceAll(",", "."); if (pjUri.isNotEmpty && !address.startsWith("sp")) { - qp['pjos'] = '0'; - qp['pj'] = pjUri; + qp["pjos"] = "0"; + qp["pj"] = pjUri; } - print(qp); - return Uri(scheme: 'bitcoin', path: address, queryParameters: qp).toString(); + return Uri(scheme: "bitcoin", path: address, queryParameters: qp).toString(); } } class LightningPaymentRequest extends PaymentURI { - LightningPaymentRequest({ - required super.amount, - required super.address, - required this.bolt11Invoice, - }); + const LightningPaymentRequest( + {required super.address, + required super.amount, + required this.bolt11Invoice, + super.scheme = "lightning"}); final String bolt11Invoice; @@ -104,7 +118,7 @@ class ArbitrumURI extends PaymentURI { } class BitcoinCashURI extends PaymentURI { - BitcoinCashURI({required super.amount, required super.address}); + const BitcoinCashURI({required super.address, required super.amount, super.scheme = ""}); @override String toString() { @@ -115,145 +129,40 @@ class BitcoinCashURI extends PaymentURI { } } -class NanoURI extends PaymentURI { - NanoURI({required super.amount, required super.address}); - - @override - String toString() { - var base = 'nano:$address'; - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } - - return base; - } -} - -class PolygonURI extends PaymentURI { - PolygonURI({required super.amount, required super.address}); - - @override - String toString() { - var base = 'polygon:$address'; - if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; - - return base; - } -} - -class SolanaURI extends PaymentURI { - SolanaURI({required super.amount, required super.address}); - - @override - String toString() { - var base = 'solana:$address'; - if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; - - return base; - } -} - -class TronURI extends PaymentURI { - TronURI({required super.amount, required super.address}); - - @override - String toString() { - var base = 'tron:$address'; - if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; - - return base; - } -} - -class WowneroURI extends PaymentURI { - WowneroURI({required super.amount, required super.address}); - - @override - String toString() { - var base = 'wownero:$address'; - if (amount.isNotEmpty) base += '?tx_amount=${amount.replaceAll(',', '.')}'; - - return base; - } -} - -class ZanoURI extends PaymentURI { - ZanoURI({required String amount, required String address}) - : super(amount: amount, address: address); - - @override - String toString() { - var base = 'zano:$address'; - if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; - - return base; - } -} - -class DecredURI extends PaymentURI { - DecredURI({required String amount, required String address}) - : super(amount: amount, address: address); - - @override - String toString() { - var base = 'decred:$address'; - if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; - - return base; - } -} - -class DogeURI extends PaymentURI { - DogeURI({required String amount, required String address}) - : super(amount: amount, address: address); - - @override - String toString() { - var base = 'doge:$address'; - if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; - - return base; - } -} - class ERC681URI extends PaymentURI { final int chainId; final String? contractAddress; - ERC681URI({ + const ERC681URI({ required this.chainId, required super.address, required super.amount, required this.contractAddress, + super.scheme = "ethereum", }); @override String toString() { - var uri = 'ethereum:'; + var uri = '$scheme:'; final targetAddress = contractAddress ?? address; uri += targetAddress; - if (chainId != 1) uri += '@$chainId'; + if (chainId != 1) uri += "@$chainId"; - if (contractAddress != null) uri += '/transfer'; + if (contractAddress != null) uri += "/transfer"; final params = {}; if (contractAddress != null) { - params['address'] = address; - if (amount.isNotEmpty) { - params['uint256'] = _formatAmountForERC20(amount); - } + params["address"] = address; + if (amount.isNotEmpty) params["uint256"] = _formatAmountForERC20(amount); } else { - if (amount.isNotEmpty) { - params['value'] = _formatAmountForNative(amount); - } + if (amount.isNotEmpty) params["value"] = _formatAmountForNative(amount); } if (params.isNotEmpty) { - uri += '?'; - uri += params.entries.map((e) => '${e.key}=${e.value}').join('&'); + uri += "?${params.entries.map((e) => "${e.key}=${e.value}").join("&")}"; } return uri; @@ -263,12 +172,11 @@ class ERC681URI extends PaymentURI { String _formatAmountForERC20(String amount) { try { // Convert decimal amount to BigInt (assuming 18 decimals) - final amountDouble = double.parse(amount.replaceAll(',', '.')); + final amountDouble = double.parse(amount.replaceAll(",", ".")); final amountBigInt = BigInt.from(amountDouble * 1e18); return amountBigInt.toString(); } catch (e) { - // Fallback to original amount if parsing fails - return amount.replaceAll(',', '.'); + return amount.replaceAll(",", "."); } } @@ -276,13 +184,12 @@ class ERC681URI extends PaymentURI { String _formatAmountForNative(String amount) { try { // Convert decimal amount to double for scientific notation - final amountDouble = double.parse(amount.replaceAll(',', '.')); + final amountDouble = double.parse(amount.replaceAll(",", ".")); // Use scientific notation as recommended by ERC-681 - return '${amountDouble}e18'; + return "${amountDouble}e18"; } catch (e) { - // Fallback to original amount if parsing fails - return amount.replaceAll(',', '.'); + return amount.replaceAll(",", "."); } } @@ -290,7 +197,7 @@ class ERC681URI extends PaymentURI { final (isContract, targetAddress) = _getTargetAddress(uri.path); final chainId = _getChainID(uri.path); - final address = isContract ? uri.queryParameters["address"] ?? '' : targetAddress; + final address = isContract ? uri.queryParameters["address"] ?? "" : targetAddress; final amountParam = isContract ? uri.queryParameters["uint256"] : uri.queryParameters["value"]; var formatedAmount = ""; @@ -311,15 +218,12 @@ class ERC681URI extends PaymentURI { } static int _getChainID(String path) { - return int.parse(RegExp( - r'@\d*', - ).firstMatch(path)?.group(0)?.replaceAll("@", "") ?? - "1"); + return int.parse(RegExp(r"@\d*").firstMatch(path)?.group(0)?.replaceAll("@", "") ?? "1"); } static (bool, String) _getTargetAddress(String path) { final targetAddress = - RegExp(r'^(0x)?[0-9a-f]{40}', caseSensitive: false).firstMatch(path)!.group(0)!; + RegExp(r"^(0x)?[0-9a-f]{40}", caseSensitive: false).firstMatch(path)!.group(0)!; return (path.contains("/"), targetAddress); } @@ -330,70 +234,67 @@ class ERC681URI extends PaymentURI { /// - Scientific notation: "0.123e18", "1e6" → expanded to integer /// - Decimal ETH: "0.123456" → shifted by 18 decimals static String _normalizeToIntegerWei(String input) { - final raw = input.replaceAll(',', '.').trim(); + final raw = input.replaceAll(",", ".").trim(); // First we check if it's already a plain integer (basically just a number with no dot, no exponent) try { - final isPlainInteger = RegExp(r'^[+-]?\d+$').hasMatch(raw) && - !raw.contains('.') && - !raw.toLowerCase().contains('e'); - if (isPlainInteger) return raw.replaceFirst(RegExp(r'^\+'), ''); + final isPlainInteger = RegExp(r"^[+-]?\d+$").hasMatch(raw) && + !raw.contains(".") && + !raw.toLowerCase().contains("e"); + if (isPlainInteger) return raw.replaceFirst(RegExp(r"^\+"), ""); // Then we check if it's a scientific notation - final sci = RegExp(r'^[+-]?(\d+\.?\d*|\d*\.?\d+)[eE][+-]?\d+$'); + final sci = RegExp(r"^[+-]?(\d+\.?\d*|\d*\.?\d+)[eE][+-]?\d+$"); if (sci.hasMatch(raw)) { - final mantissaStr = raw.toLowerCase().split('e')[0]; - final exp = int.parse(raw.toLowerCase().split('e')[1]); + final mantissaStr = raw.toLowerCase().split("e")[0]; + final exp = int.parse(raw.toLowerCase().split("e")[1]); return _expandDecimal(mantissaStr, exp); } // Lastly, we check if it's a fixed decimal ETH amount, here we shift by 18 to get wei for the amount - if (raw.contains('.')) { + if (raw.contains(".")) { return _expandDecimal(raw, 18); } return raw; } catch (e) { return raw; } - - // If none of these checks work, we return the raw input } /// Expands a decimal string by shifting the decimal point `expShift` places /// to the right and returns an integer string (digits only, optional leading minus). /// Examples: - /// _expandDecimal('0.123456', 18) -> '123456000000000000' - /// _expandDecimal('1.2', 3) -> '1200' + /// _expandDecimal("0.123456", 18) -> "123456000000000000" + /// _expandDecimal("1.2", 3) -> "1200" static String _expandDecimal(String decimalStr, int expShift) { var s = decimalStr.trim(); - var sign = ''; - if (s.startsWith('-') || s.startsWith('+')) { - sign = s[0] == '-' ? '-' : ''; + var sign = ""; + if (s.startsWith("-") || s.startsWith("+")) { + sign = s[0] == "-" ? "-" : ""; s = s.substring(1); } // First we split the integer and fractional parts - final parts = s.split('.'); - final intPart = parts[0].isEmpty ? '0' : parts[0]; - final fracPart = parts.length > 1 ? parts[1] : ''; - final digits = (intPart + fracPart).replaceFirst(RegExp(r'^0+'), ''); + final parts = s.split("."); + final intPart = parts[0].isEmpty ? "0" : parts[0]; + final fracPart = parts.length > 1 ? parts[1] : ""; + final digits = (intPart + fracPart).replaceFirst(RegExp(r"^0+"), ""); final fracLen = fracPart.length; // Then we calculate the effective shift = desired shift minus existing fractional digits final shift = expShift - fracLen; if (shift >= 0) { - final head = digits.isEmpty ? '0' : digits; - final zeros = List.filled(shift, '0').join(); + final head = digits.isEmpty ? "0" : digits; + final zeros = List.filled(shift, "0").join(); final res = head + zeros; - return sign + (res.isEmpty ? '0' : res); + return sign + (res.isEmpty ? "0" : res); } else { // Need to insert a decimal point within digits; return integer by truncating final cut = digits.length + shift; - if (cut <= 0) { - return '0'; - } + if (cut <= 0) return "0"; + final res = digits.substring(0, cut); - return sign + (res.isEmpty ? '0' : res); + return sign + (res.isEmpty ? "0" : res); } } } diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index c802065202..44fa821789 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -88,10 +88,13 @@ abstract class WalletAddresses { /// Get a [PaymentURI] for the current [address] /// e.g. ethereum:0x0 - PaymentURI getPaymentUri(String amount); - + PaymentURI getPaymentUri(String amount) => PaymentURI( + scheme: walletTypeToString(walletInfo.type).toLowerCase(), + address: address, + amount: amount, + ); /// Get a [PaymentURI] for the current [address] asynchronously /// this can be used if a payment requires a api call beforehand - Future getPaymentRequestUri(String amount) async => getPaymentRequestUri(amount); + Future getPaymentRequestUri(String amount) async => getPaymentUri(amount); } diff --git a/cw_decred/lib/wallet_addresses.dart b/cw_decred/lib/wallet_addresses.dart index f7d0a8baec..273a8e0510 100644 --- a/cw_decred/lib/wallet_addresses.dart +++ b/cw_decred/lib/wallet_addresses.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:mobx/mobx.dart'; @@ -13,10 +12,10 @@ part 'wallet_addresses.g.dart'; class DecredWalletAddresses = DecredWalletAddressesBase with _$DecredWalletAddresses; abstract class DecredWalletAddressesBase extends WalletAddresses with Store { - DecredWalletAddressesBase(super.walletInfo, Libwallet libwallet, super.isTestnet) - : _libwallet = libwallet; + DecredWalletAddressesBase(super.walletInfo, this._libwallet, super.isTestnet); + final Libwallet _libwallet; - String currentAddr = ''; + String _currentAddr = ''; @observable bool isEnabledAutoGenerateSubaddress = true; @@ -43,14 +42,13 @@ abstract class DecredWalletAddressesBase extends WalletAddresses with Store { @override Future updateAddressesInBox() async { - final addrs = await libAddresses(); - final allAddrs = new List.from(addrs.usedAddrs)..addAll(addrs.unusedAddrs); + final addrs = await _libAddresses(); + final allAddrs = List.from(addrs.usedAddrs)..addAll(addrs.unusedAddrs); // Add all addresses. allAddrs.forEach((addr) { - if (addressesMap.containsKey(addr)) { - return; - } + if (addressesMap.containsKey(addr)) return; + addressesMap[addr] = ""; addressInfos[0] ??= []; addressInfos[0]?.add( @@ -66,44 +64,37 @@ abstract class DecredWalletAddressesBase extends WalletAddresses with Store { // Add used addresses. addrs.usedAddrs.forEach((addr) { - if (!usedAddresses.contains(addr)) { - usedAddresses.add(addr); - } + if (!usedAddresses.contains(addr)) usedAddresses.add(addr); }); - if (addrs.unusedAddrs.length > 0 && addrs.unusedAddrs[0] != currentAddr) { - currentAddr = addrs.unusedAddrs[0]; - selectedAddr = currentAddr; + if (addrs.unusedAddrs.length > 0 && addrs.unusedAddrs[0] != _currentAddr) { + _currentAddr = addrs.unusedAddrs[0]; + selectedAddr = _currentAddr; } await saveAddressesInBox(); } List getAddressInfos() { - if (addressInfos.containsKey(0)) { - return addressInfos[0]!; - } + if (addressInfos.containsKey(0)) return addressInfos[0]!; + return []; } Future updateAddress(String address, String label) async { - if (!addressInfos.containsKey(0)) { - return; - } + if (!addressInfos.containsKey(0)) return; + addressInfos[0]!.forEach((info) { - if (info.address == address) { - info.label = label; - } + if (info.address == address) info.label = label; }); await saveAddressesInBox(); } - Future libAddresses() async { + Future<_LibAddresses> _libAddresses() async { final nUsed = "10"; var nUnused = "1"; - if (this.isEnabledAutoGenerateSubaddress) { - nUnused = "3"; - } + if (this.isEnabledAutoGenerateSubaddress) nUnused = "3"; + try { final res = await _libwallet.addresses(walletInfo.name, nUsed, nUnused); final decoded = json.decode(res); @@ -111,10 +102,10 @@ abstract class DecredWalletAddressesBase extends WalletAddresses with Store { final unusedAddrs = List.from(decoded["unused"] ?? []); // index is the index of the first unused address. final index = decoded["index"] ?? 0; - return new LibAddresses(usedAddrs, unusedAddrs, index); + return _LibAddresses(usedAddrs, unusedAddrs, index); } catch (e) { printV(e); - return LibAddresses([], [], 0); + return _LibAddresses([], [], 0); } } @@ -122,9 +113,8 @@ abstract class DecredWalletAddressesBase extends WalletAddresses with Store { // NOTE: This will ignore the gap limit and may cause problems when restoring from seed if too // many addresses are taken and not used. final addr = await _libwallet.newExternalAddress(walletInfo.name) ?? ''; - if (addr == "") { - return; - } + if (addr == "") return; + if (!addressesMap.containsKey(addr)) { addressesMap[addr] = ""; addressInfos[0] ??= []; @@ -143,22 +133,17 @@ abstract class DecredWalletAddressesBase extends WalletAddresses with Store { } @override - List get receivePageOptions { - return isTestnet - ? [ - ReceivePageOption.testnet, - ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) - ] - : ReceivePageOptions; - } - - @override - PaymentURI getPaymentUri(String amount) => DecredURI(amount: amount, address: address); + List get receivePageOptions => isTestnet + ? [ + ReceivePageOption.testnet, + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ] + : ReceivePageOptions; } -class LibAddresses { +class _LibAddresses { final List usedAddrs, unusedAddrs; final int firstUnusedAddrIndex; - LibAddresses(this.usedAddrs, this.unusedAddrs, this.firstUnusedAddrIndex); + _LibAddresses(this.usedAddrs, this.unusedAddrs, this.firstUnusedAddrIndex); } diff --git a/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart b/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart index 3dc72526fd..8f12dcc1ca 100644 --- a/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart +++ b/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart @@ -11,7 +11,8 @@ part 'dogecoin_wallet_addresses.g.dart'; class DogeCoinWalletAddresses = DogeCoinWalletAddressesBase with _$DogeCoinWalletAddresses; abstract class DogeCoinWalletAddressesBase extends ElectrumWalletAddresses with Store { - DogeCoinWalletAddressesBase(WalletInfo walletInfo, { + DogeCoinWalletAddressesBase( + WalletInfo walletInfo, { required super.mainHd, required super.sideHd, required super.network, @@ -19,15 +20,18 @@ abstract class DogeCoinWalletAddressesBase extends ElectrumWalletAddresses with super.initialAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, - super.initialAddressPageType + super.initialAddressPageType, }) : super(walletInfo); @override - String getAddress({required int index, + String getAddress({ + required int index, required Bip32Slip10Secp256k1 hd, - BitcoinAddressType? addressType}) => + BitcoinAddressType? addressType, + }) => generateP2PKHAddress(hd: hd, index: index, network: network); @override - PaymentURI getPaymentUri(String amount) => DogeURI(amount: amount, address: address); + PaymentURI getPaymentUri(String amount) => + PaymentURI(scheme: "doge", address: address, amount: amount); } diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index f952117485..0db8781c7a 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -82,7 +82,7 @@ abstract class EVMChainWalletBase _hexPrivateKey = privateKey, _isTransactionUpdating = false, _client = client, - walletAddresses = EVMChainWalletAddresses(walletInfo, client.chainId), + walletAddresses = EVMChainWalletAddresses(walletInfo), balance = ObservableMap.of( { // Not sure of this yet, will it work? will it not? diff --git a/cw_evm/lib/evm_chain_wallet_addresses.dart b/cw_evm/lib/evm_chain_wallet_addresses.dart index bfa4938a32..7dd501cc5e 100644 --- a/cw_evm/lib/evm_chain_wallet_addresses.dart +++ b/cw_evm/lib/evm_chain_wallet_addresses.dart @@ -1,6 +1,5 @@ import 'dart:developer'; -import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -10,12 +9,10 @@ part 'evm_chain_wallet_addresses.g.dart'; class EVMChainWalletAddresses = EVMChainWalletAddressesBase with _$EVMChainWalletAddresses; abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store { - EVMChainWalletAddressesBase(WalletInfo walletInfo, this.chainId) + EVMChainWalletAddressesBase(WalletInfo walletInfo) : address = '', super(walletInfo); - final int chainId; - @override @observable String address; @@ -39,16 +36,4 @@ abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store { log(e.toString()); } } - - @override - PaymentURI getPaymentUri(String amount) { - switch (chainId) { - case 8453: - return BaseURI(amount: amount, address: address); - case 137: - return PolygonURI(amount: amount, address: address); - default: - return EthereumURI(amount: amount, address: address); - } - } } diff --git a/cw_monero/lib/monero_wallet_addresses.dart b/cw_monero/lib/monero_wallet_addresses.dart index 51c3e0f0a9..9a7264c035 100644 --- a/cw_monero/lib/monero_wallet_addresses.dart +++ b/cw_monero/lib/monero_wallet_addresses.dart @@ -157,5 +157,5 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { addressInfos[account?.id ?? 0]?.any((it) => it.address == address) ?? false; @override - PaymentURI getPaymentUri(String amount) => MoneroURI(amount: amount, address: address); + PaymentURI getPaymentUri(String amount) => MoneroURI(address: address, amount: amount); } diff --git a/cw_nano/lib/nano_wallet_addresses.dart b/cw_nano/lib/nano_wallet_addresses.dart index f52cf4ca1f..d29433e39e 100644 --- a/cw_nano/lib/nano_wallet_addresses.dart +++ b/cw_nano/lib/nano_wallet_addresses.dart @@ -1,5 +1,4 @@ import 'package:cw_core/cake_hive.dart'; -import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; @@ -53,7 +52,4 @@ abstract class NanoWalletAddressesBase extends WalletAddresses with Store { printV(e.toString()); } } - - @override - PaymentURI getPaymentUri(String amount) => NanoURI(amount: amount, address: address); } diff --git a/cw_solana/lib/solana_wallet_addresses.dart b/cw_solana/lib/solana_wallet_addresses.dart index 634c73f375..7e9bd90089 100644 --- a/cw_solana/lib/solana_wallet_addresses.dart +++ b/cw_solana/lib/solana_wallet_addresses.dart @@ -1,4 +1,3 @@ -import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; @@ -35,7 +34,4 @@ abstract class SolanaWalletAddressesBase extends WalletAddresses with Store { printV(e.toString()); } } - - @override - PaymentURI getPaymentUri(String amount) => SolanaURI(amount: amount, address: address); } diff --git a/cw_tron/lib/tron_wallet_addresses.dart b/cw_tron/lib/tron_wallet_addresses.dart index 99767e9654..095f97fa9a 100644 --- a/cw_tron/lib/tron_wallet_addresses.dart +++ b/cw_tron/lib/tron_wallet_addresses.dart @@ -1,6 +1,5 @@ import 'dart:developer'; -import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -37,7 +36,4 @@ abstract class TronWalletAddressesBase extends WalletAddresses with Store { log(e.toString()); } } - - @override - PaymentURI getPaymentUri(String amount) => TronURI(amount: amount, address: address); } diff --git a/cw_wownero/lib/wownero_wallet_addresses.dart b/cw_wownero/lib/wownero_wallet_addresses.dart index c95d397631..ef17237f48 100644 --- a/cw_wownero/lib/wownero_wallet_addresses.dart +++ b/cw_wownero/lib/wownero_wallet_addresses.dart @@ -62,7 +62,7 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { WowneroSubaddressList subaddressList; WowneroAccountList accountList; - + @override Set usedAddresses = Set(); @@ -152,5 +152,6 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { addressInfos[account?.id ?? 0]?.any((it) => it.address == address) ?? false; @override - PaymentURI getPaymentUri(String amount) => WowneroURI(amount: amount, address: address); + PaymentURI getPaymentUri(String amount) => + MoneroURI(scheme: "wownero", address: address, amount: amount); } diff --git a/cw_zano/lib/zano_wallet_addresses.dart b/cw_zano/lib/zano_wallet_addresses.dart index 1562ea8eee..be25c9383e 100644 --- a/cw_zano/lib/zano_wallet_addresses.dart +++ b/cw_zano/lib/zano_wallet_addresses.dart @@ -1,4 +1,3 @@ -import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; @@ -38,7 +37,4 @@ abstract class ZanoWalletAddressesBase extends WalletAddresses with Store { printV(e.toString()); } } - - @override - PaymentURI getPaymentUri(String amount) => ZanoURI(amount: amount, address: address); } diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 09b478da79..a2cf0d738e 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -414,13 +414,11 @@ abstract class ExchangeTradeViewModelBase with Store { switch (wallet.type) { case WalletType.bitcoin: - return BitcoinURI(amount: amount, address: inputAddress); - case WalletType.litecoin: - return LitecoinURI(amount: amount, address: inputAddress); + return BitcoinURI(address: inputAddress, amount: amount); case WalletType.bitcoinCash: - return BitcoinCashURI(amount: amount, address: inputAddress); + return BitcoinCashURI(address: inputAddress, amount: amount); case WalletType.dogecoin: - return DogeURI(amount: amount, address: inputAddress); + return PaymentURI(scheme: "doge", address: inputAddress, amount: amount); case WalletType.ethereum: return _createERC681URI(fromCurrency, inputAddress, amount); // TODO: Expand ERC681URI support to Polygon(modify decoding flow for QRs, pay anything, and deep link handling) @@ -435,17 +433,17 @@ abstract class ExchangeTradeViewModelBase with Store { case WalletType.tron: return TronURI(amount: amount, address: inputAddress); case WalletType.monero: - return MoneroURI(amount: amount, address: inputAddress); + return MoneroURI(address: inputAddress, amount: amount); case WalletType.wownero: - return WowneroURI(amount: amount, address: inputAddress); - case WalletType.zano: - return ZanoURI(amount: amount, address: inputAddress); - case WalletType.decred: - return DecredURI(amount: amount, address: inputAddress); - case WalletType.nano: - return NanoURI(amount: amount, address: inputAddress); + return MoneroURI( + scheme: walletTypeToString(wallet.type).toLowerCase(), + address: inputAddress, + amount: amount); default: - return null; + return PaymentURI( + scheme: walletTypeToString(wallet.type).toLowerCase(), + address: inputAddress, + amount: amount); } } diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 92f4c24a78..db7111017a 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -115,9 +115,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @action Future refreshUri() async { - print(amount); uri = await wallet.walletAddresses.getPaymentRequestUri(amount); - print(uri); } @computed From 21b4ad1654743c4aa4c696b4f6eb670e929b2c3c Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 4 Nov 2025 21:53:54 +0100 Subject: [PATCH 37/68] refactor: remove redundant debug print statement from `bitcoin_wallet_addresses.dart` --- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 7df16b020e..c14ab11849 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -119,7 +119,6 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S final invoice = await lightningWallet!.getBolt11Invoice(amountSats, "Send to Cake Wallet"); return LightningPaymentRequest(address: address, amount: amount, bolt11Invoice: invoice); } - print(amount); return getPaymentUri(amount); } } From 193a26075d689550be1a6dafd69e83e964c56209 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Wed, 5 Nov 2025 14:39:07 +0100 Subject: [PATCH 38/68] refactor: improve consistency in widget styling and centralized label logic, add Bitcoin Lightning deposit/withdraw support --- .../pages/balance/balance_row_widget.dart | 144 ++++++++++-------- res/values/strings_ar.arb | 2 + res/values/strings_bg.arb | 2 + res/values/strings_cs.arb | 2 + res/values/strings_de.arb | 4 +- res/values/strings_en.arb | 2 + res/values/strings_es.arb | 2 + res/values/strings_fr.arb | 2 + res/values/strings_ha.arb | 2 + res/values/strings_hi.arb | 2 + res/values/strings_hr.arb | 2 + res/values/strings_hy.arb | 2 + res/values/strings_id.arb | 2 + res/values/strings_it.arb | 2 + res/values/strings_ja.arb | 2 + res/values/strings_ko.arb | 2 + res/values/strings_my.arb | 2 + res/values/strings_nl.arb | 2 + res/values/strings_pl.arb | 2 + res/values/strings_pt.arb | 2 + res/values/strings_ru.arb | 2 + res/values/strings_th.arb | 2 + res/values/strings_tl.arb | 2 + res/values/strings_tr.arb | 2 + res/values/strings_uk.arb | 2 + res/values/strings_ur.arb | 2 + res/values/strings_vi.arb | 2 + res/values/strings_yo.arb | 2 + res/values/strings_zh.arb | 2 + 29 files changed, 139 insertions(+), 63 deletions(-) diff --git a/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart b/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart index 3dac4cea13..68d2ca2782 100644 --- a/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart +++ b/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart @@ -14,6 +14,7 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -148,14 +149,12 @@ class BalanceRowWidget extends StatelessWidget { maxLines: 1, textAlign: TextAlign.start, ), - SizedBox(height: 6), + const SizedBox(height: 6), if (isTestnet) Text( S.of(context).testnet_coins_no_value, textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - height: 1, - ), + style: Theme.of(context).textTheme.bodyMedium?.copyWith(height: 1), ), if (!isTestnet) Text( @@ -216,7 +215,7 @@ class BalanceRowWidget extends StatelessWidget { if (currency.isPotentialScam) Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - margin: EdgeInsets.only(top: 4), + margin: const EdgeInsets.only(top: 4), decoration: BoxDecoration( color: Theme.of(context).colorScheme.errorContainer, borderRadius: BorderRadius.circular(8), @@ -244,7 +243,7 @@ class BalanceRowWidget extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 26), + const SizedBox(height: 26), Row( children: [ Text( @@ -257,7 +256,7 @@ class BalanceRowWidget extends StatelessWidget { ), ], ), - SizedBox(height: 8), + const SizedBox(height: 8), AutoSizeText( frozenBalance, style: Theme.of(context).textTheme.bodyLarge!.copyWith( @@ -268,14 +267,12 @@ class BalanceRowWidget extends StatelessWidget { maxLines: 1, textAlign: TextAlign.center, ), - SizedBox(height: 4), + const SizedBox(height: 4), if (!isTestnet) Text( frozenFiatBalance, textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodySmall!.copyWith( - height: 1, - ), + style: Theme.of(context).textTheme.bodySmall!.copyWith(height: 1), ), ], ), @@ -283,7 +280,7 @@ class BalanceRowWidget extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 24), + const SizedBox(height: 24), Text( '${additionalBalanceLabel}', textAlign: TextAlign.center, @@ -292,7 +289,7 @@ class BalanceRowWidget extends StatelessWidget { height: 1, ), ), - SizedBox(height: 8), + const SizedBox(height: 8), AutoSizeText( additionalBalance, style: Theme.of(context).textTheme.bodyLarge!.copyWith( @@ -303,14 +300,12 @@ class BalanceRowWidget extends StatelessWidget { maxLines: 1, textAlign: TextAlign.center, ), - SizedBox(height: 4), + const SizedBox(height: 4), if (!isTestnet) Text( '${additionalFiatBalance}', textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodySmall!.copyWith( - height: 1, - ), + style: Theme.of(context).textTheme.bodySmall!.copyWith(height: 1), ), ], ), @@ -333,15 +328,6 @@ class BalanceRowWidget extends StatelessWidget { begin: Alignment.topCenter, end: Alignment.bottomCenter, ), - // boxShadow: [ - // BoxShadow( - // color: Theme.of(context) - // .extension()! - // .cardBorderColor - // .withAlpha(50), - // spreadRadius: dashboardViewModel.getShadowSpread(), - // blurRadius: dashboardViewModel.getShadowBlur()) - // ], ), child: TextButton( onPressed: _showToast, @@ -360,27 +346,48 @@ class BalanceRowWidget extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - Container( - child: Column( - children: [ - Container( - child: ImageIcon( - AssetImage('assets/images/mweb_logo.png'), - size: 40, - ), - ), - const SizedBox(height: 10), - Text( - 'MWEB', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontSize: 16, - fontWeight: FontWeight.w700, - color: Theme.of(context).colorScheme.onSurface, - height: 1, - ), - ), - ], - ), + Column( + children: [ + ImageIcon( + AssetImage('assets/images/mweb_logo.png'), + size: 40, + ), + const SizedBox(height: 10), + Text( + 'MWEB', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.onSurface, + height: 1, + ), + ), + ], + ), + ], + ), + if (currency == CryptoCurrency.btc) + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Column( + children: [ + SvgPicture.asset( + 'assets/images/lightning-icon.svg', + width: 40, + height: 40, + ), + const SizedBox(height: 10), + Text( + 'Lightning', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.onSurface, + height: 1, + ), + ), + ], ), ], ), @@ -392,15 +399,11 @@ class BalanceRowWidget extends StatelessWidget { children: [ GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () => launchUrl( - Uri.parse( - "https://docs.cakewallet.com/cryptos/litecoin#mweb"), - mode: LaunchMode.externalApplication, - ), + onTap: onPressedHelp, child: Row( children: [ Text( - '${secondAvailableBalanceLabel}', + secondAvailableBalanceLabel, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: @@ -434,7 +437,7 @@ class BalanceRowWidget extends StatelessWidget { SizedBox(height: 6), if (!isTestnet) Text( - '${secondAvailableFiatBalance}', + secondAvailableFiatBalance, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontSize: 16, @@ -462,7 +465,7 @@ class BalanceRowWidget extends StatelessWidget { children: [ SizedBox(height: 24), Text( - '${secondAdditionalBalanceLabel}', + secondAdditionalBalanceLabel, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, @@ -498,13 +501,13 @@ class BalanceRowWidget extends StatelessWidget { ), IntrinsicHeight( child: Container( - padding: EdgeInsets.symmetric(horizontal: 12), + padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Semantics( - label: S.of(context).litecoin_mweb_pegin, + label: depositToL2Label, child: OutlinedButton( onPressed: () => depositToL2(context), style: OutlinedButton.styleFrom( @@ -519,7 +522,7 @@ class BalanceRowWidget extends StatelessWidget { ), ), child: Container( - padding: EdgeInsets.symmetric(vertical: 12), + padding: const EdgeInsets.symmetric(vertical: 12), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -531,7 +534,7 @@ class BalanceRowWidget extends StatelessWidget { ), const SizedBox(width: 8), Text( - S.of(context).litecoin_mweb_pegin, + depositToL2Label, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onPrimary, fontWeight: FontWeight.w700, @@ -546,7 +549,7 @@ class BalanceRowWidget extends StatelessWidget { SizedBox(width: 16), Expanded( child: Semantics( - label: S.of(context).litecoin_mweb_pegout, + label: withdrawFromL2Label, child: OutlinedButton( onPressed: () => withdrawFromL2(context), style: OutlinedButton.styleFrom( @@ -573,7 +576,7 @@ class BalanceRowWidget extends StatelessWidget { ), const SizedBox(width: 8), Text( - S.of(context).litecoin_mweb_pegout, + withdrawFromL2Label, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context) .colorScheme @@ -591,7 +594,7 @@ class BalanceRowWidget extends StatelessWidget { ), ), ), - SizedBox(height: 16), + const SizedBox(height: 16), ], ), ), @@ -601,6 +604,14 @@ class BalanceRowWidget extends StatelessWidget { ); } + String get depositToL2Label => dashboardViewModel.type == WalletType.litecoin + ? S.current.litecoin_mweb_pegin + : S.current.bitcoin_lightning_deposit; + + String get withdrawFromL2Label => dashboardViewModel.type == WalletType.litecoin + ? S.current.litecoin_mweb_pegout + : S.current.bitcoin_lightning_withdraw; + Future depositToL2(BuildContext context) async { PaymentRequest? paymentRequest = null; @@ -653,6 +664,15 @@ class BalanceRowWidget extends StatelessWidget { ); } + void onPressedHelp() { + var helpUri = Uri.parse("https://docs.cakewallet.com/cryptos/bitcoin#lightning"); + if (dashboardViewModel.type == WalletType.litecoin) { + helpUri = Uri.parse("https://docs.cakewallet.com/cryptos/litecoin#mweb"); + } + + launchUrl(helpUri, mode: LaunchMode.externalApplication); + } + void _showBalanceDescription(BuildContext context, String content) { showPopUp(context: context, builder: (_) => InformationPage(information: content)); } diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 33709eb3e6..8af20ff642 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "امسح بصمة إصبعك للمصادقة", "bitcoin_dark_theme": "موضوع البيتكوين الظلام", "bitcoin_light_theme": "موضوع البيتكوين الخفيفة", + "bitcoin_lightning_deposit": "إيداع", + "bitcoin_lightning_withdraw": "ينسحب", "bitcoin_payments_require_1_confirmation": "تتطلب مدفوعات Bitcoin تأكيدًا واحدًا ، والذي قد يستغرق 20 دقيقة أو أكثر. شكرا لصبرك! سيتم إرسال بريد إلكتروني إليك عند تأكيد الدفع.", "block_height": "ارتفاع كتلة", "block_remaining": "1 كتلة متبقية", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 0484cc66fc..d8e4dcc266 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "Сканирайте своя пръстов отпечатък", "bitcoin_dark_theme": "Тъмна тема за биткойн", "bitcoin_light_theme": "Лека биткойн тема", + "bitcoin_lightning_deposit": "Депозит", + "bitcoin_lightning_withdraw": "Оттегляне", "bitcoin_payments_require_1_confirmation": "Плащанията с Bitcoin изискват потвърждение, което може да отнеме 20 минути или повече. Благодарим за търпението! Ще получите имейл, когато плащането е потвърдено.", "block_height": "Височина на блока", "block_remaining": "1 блок останал", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index e73b1f4bcc..1d5321a564 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "Naskenujte otisk prstu pro ověření", "bitcoin_dark_theme": "Tmavé téma bitcoinů", "bitcoin_light_theme": "Světlé téma bitcoinů", + "bitcoin_lightning_deposit": "Vklad", + "bitcoin_lightning_withdraw": "Odebrat", "bitcoin_payments_require_1_confirmation": "U plateb Bitcoinem je vyžadováno alespoň 1 potvrzení, což může trvat 20 minut i déle. Děkujeme za vaši trpělivost! Až bude platba potvrzena, budete informováni e-mailem.", "block_height": "Výška bloku", "block_remaining": "1 blok zbývající", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 5ed7982cda..98e3a75f2f 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "Scannen Sie Ihren Fingerabdruck zur Authentifizierung", "bitcoin_dark_theme": "Dunkles Bitcoin-Thema", "bitcoin_light_theme": "Bitcoin Light-Thema", + "bitcoin_lightning_deposit": "Einzahlen", + "bitcoin_lightning_withdraw": "Auszahlen", "bitcoin_payments_require_1_confirmation": "Bitcoin-Zahlungen erfordern 1 Bestätigung, was 20 Minuten oder länger dauern kann. Danke für Ihre Geduld! Sie erhalten eine E-Mail, wenn die Zahlung bestätigt ist.", "block_height": "Blockhöhe", "block_remaining": "1 Block verbleibend", @@ -1165,4 +1167,4 @@ "youCanGoBackToYourDapp": "Sie können jetzt zu Ihrem Dapp zurückkehren", "your": "Dein", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index bd6d22559b..a0eea1dcc3 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "Scan your fingerprint to authenticate", "bitcoin_dark_theme": "Bitcoin Dark Theme", "bitcoin_light_theme": "Bitcoin Light Theme", + "bitcoin_lightning_deposit": "Deposit", + "bitcoin_lightning_withdraw": "Withdraw", "bitcoin_payments_require_1_confirmation": "Bitcoin payments require 1 confirmation, which can take 20 minutes or longer. Thanks for your patience! You will be emailed when the payment is confirmed.", "block_height": "Block height", "block_remaining": "1 Block Remaining", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 0c28290b8f..314af00044 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "Escanee su huella dactilar para autenticarse", "bitcoin_dark_theme": "Tema oscuro de Bitcoin", "bitcoin_light_theme": "Tema claro de Bitcoin", + "bitcoin_lightning_deposit": "Depósito", + "bitcoin_lightning_withdraw": "Retirar", "bitcoin_payments_require_1_confirmation": "Los pagos de Bitcoin requieren 1 confirmación, que puede tardar 20 minutos o más. ¡Gracias por tu paciencia! Recibirás un correo electrónico cuando se confirme el pago.", "block_height": "Altura del bloque", "block_remaining": "1 bloque restante", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 94103b0e00..f5f82d0fd1 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "Scannez votre empreinte digitale pour vous authentifier", "bitcoin_dark_theme": "Thème sombre Bitcoin", "bitcoin_light_theme": "Thème clair Bitcoin", + "bitcoin_lightning_deposit": "Dépôt", + "bitcoin_lightning_withdraw": "Retirer", "bitcoin_payments_require_1_confirmation": "Les paiements Bitcoin nécessitent 1 confirmation, ce qui peut prendre 20 minutes ou plus. Merci pour votre patience ! Vous serez averti par e-mail lorsque le paiement sera confirmé.", "block_height": "Hauteur de bloc", "block_remaining": "1 bloc restant", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index bb26fed0e5..797a39124e 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "Duba hoton yatsa don tantancewa", "bitcoin_dark_theme": "Bitcoin Dark Jigo", "bitcoin_light_theme": "Jigon Hasken Bitcoin", + "bitcoin_lightning_deposit": "Yi ajiya", + "bitcoin_lightning_withdraw": "Janye", "bitcoin_payments_require_1_confirmation": "Akwatin Bitcoin na buɗe 1 sambumbu, da yake za ta samu mintuna 20 ko yawa. Ina kira ga sabuwar lafiya! Zaka sanarwa ta email lokacin da aka samu akwatin samun lambar waya.", "block_height": "Toshe tsawo", "block_remaining": "1 toshe ragowar", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 4c83c23a54..e260b9b4f6 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "प्रमाणित करने के लिए अपने फ़िंगरप्रिंट को स्कैन करें", "bitcoin_dark_theme": "बिटकॉइन डार्क थीम", "bitcoin_light_theme": "बिटकॉइन लाइट थीम", + "bitcoin_lightning_deposit": "जमा", + "bitcoin_lightning_withdraw": "निकालना", "bitcoin_payments_require_1_confirmation": "बिटकॉइन भुगतान के लिए 1 पुष्टिकरण की आवश्यकता होती है, जिसमें 20 मिनट या अधिक समय लग सकता है। आपके धैर्य के लिए धन्यवाद! भुगतान की पुष्टि होने पर आपको ईमेल किया जाएगा।", "block_height": "ब्लॉक ऊंचाई", "block_remaining": "1 ब्लॉक शेष", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index db714439e4..eb6a136329 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "Skenirajte svoj otisak prsta za autentifikaciju", "bitcoin_dark_theme": "Bitcoin Tamna tema", "bitcoin_light_theme": "Bitcoin Light Theme", + "bitcoin_lightning_deposit": "Polog", + "bitcoin_lightning_withdraw": "Povući", "bitcoin_payments_require_1_confirmation": "Bitcoin plaćanja zahtijevaju 1 potvrdu, što može potrajati 20 minuta ili dulje. Hvala na Vašem strpljenju! Dobit ćete e-poruku kada plaćanje bude potvrđeno.", "block_height": "Visina bloka", "block_remaining": "Preostalo 1 blok", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 694a01ddf8..cac4a9f079 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "Սկանավեք ձեր մատնահետքը նույնականացման համար", "bitcoin_dark_theme": "Bitcoin մութ տեսք", "bitcoin_light_theme": "Bitcoin պայծառ տեսք", + "bitcoin_lightning_deposit": "Ավանդ", + "bitcoin_lightning_withdraw": "Հանել", "bitcoin_payments_require_1_confirmation": "Bitcoin վճարումները պահանջում են 1 հաստատում, որը կարող է տևել 20 րոպե կամ ավելի: Շնորհակալություն ձեր համբերության համար: Դուք էլ. նամակ կստանաք, երբ վճարումը հաստատվի։", "block_height": "Բլոկի բարձրությունը", "block_remaining": "1 Բլոկ է մնացել", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index c6e197261d..75393c9696 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "Pindai sidik jari Anda untuk mengautentikasi", "bitcoin_dark_theme": "Tema Gelap Bitcoin", "bitcoin_light_theme": "Tema Cahaya Bitcoin", + "bitcoin_lightning_deposit": "Deposito", + "bitcoin_lightning_withdraw": "Menarik", "bitcoin_payments_require_1_confirmation": "Pembayaran Bitcoin memerlukan 1 konfirmasi, yang bisa memakan waktu 20 menit atau lebih. Terima kasih atas kesabaran Anda! Anda akan diemail saat pembayaran dikonfirmasi.", "block_height": "Tinggi blok", "block_remaining": "1 blok tersisa", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 603c862a08..a7a8047de4 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "Scansiona la tua impronta per autenticarti", "bitcoin_dark_theme": "Tema scuro Bitcoin", "bitcoin_light_theme": "Tema chiaro Bitcoin", + "bitcoin_lightning_deposit": "Depositare", + "bitcoin_lightning_withdraw": "Ritirare", "bitcoin_payments_require_1_confirmation": "I pagamenti in bitcoin richiedono 1 conferma, che può richiedere 20 minuti o più. Grazie per la vostra pazienza! Riceverai un'e-mail quando il pagamento sarà confermato.", "block_height": "Altezza del blocco", "block_remaining": "1 blocco rimanente", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 9b47ef5f6d..dfb3950c62 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "प指紋をスキャンして認証する", "bitcoin_dark_theme": "ビットコインダークテーマ", "bitcoin_light_theme": "ビットコインライトテーマ", + "bitcoin_lightning_deposit": "デポジット", + "bitcoin_lightning_withdraw": "撤回する", "bitcoin_payments_require_1_confirmation": "ビットコインの支払いには 1 回の確認が必要で、これには 20 分以上かかる場合があります。お待ち頂きまして、ありがとうございます!支払いが確認されると、メールが送信されます。", "block_height": "ブロックの高さ", "block_remaining": "残り1ブロック", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index be7f2106bd..22159aedbb 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "인증하려면 지문을 스캔하세요", "bitcoin_dark_theme": "비트코인 다크 테마", "bitcoin_light_theme": "비트코인 라이트 테마", + "bitcoin_lightning_deposit": "보증금", + "bitcoin_lightning_withdraw": "철회하다", "bitcoin_payments_require_1_confirmation": "비트코인 결제는 1번의 확인이 필요하며, 이는 20분 이상 소요될 수 있습니다. 기다려 주셔서 감사합니다! 결제가 확인되면 이메일로 알려드립니다.", "block_height": "블록 높이", "block_remaining": "1 블록 남음", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 2507dda1d4..f52f8b19b5 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "စစ်မှန်ကြောင်းအထောက်အထားပြရန် သင့်လက်ဗွေကို စကန်ဖတ်ပါ။", "bitcoin_dark_theme": "Bitcoin Dark Theme", "bitcoin_light_theme": "Bitcoin Light အပြင်အဆင်", + "bitcoin_lightning_deposit": "အပ်ငေှ", + "bitcoin_lightning_withdraw": "ဆုတ်ခွာ", "bitcoin_payments_require_1_confirmation": "Bitcoin ငွေပေးချေမှုများသည် မိနစ် 20 သို့မဟုတ် ထို့ထက်ပိုကြာနိုင်သည် 1 အတည်ပြုချက် လိုအပ်သည်။ မင်းရဲ့စိတ်ရှည်မှုအတွက် ကျေးဇူးတင်ပါတယ်။ ငွေပေးချေမှုကို အတည်ပြုပြီးသောအခါ သင့်ထံ အီးမေးလ်ပို့ပါမည်။", "block_height": "ပိတ်ပင်တားဆီးမှုအမြင့်", "block_remaining": "ကျန်ရှိနေသေးသော block", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 056908f1b0..9802b93c68 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "Scan uw vingerafdruk om te verifiëren", "bitcoin_dark_theme": "Bitcoin donker thema", "bitcoin_light_theme": "Bitcoin Light-thema", + "bitcoin_lightning_deposit": "Borg", + "bitcoin_lightning_withdraw": "Terugtrekken", "bitcoin_payments_require_1_confirmation": "Bitcoin-betalingen vereisen 1 bevestiging, wat 20 minuten of langer kan duren. Dank voor uw geduld! U ontvangt een e-mail wanneer de betaling is bevestigd.", "block_height": "Blokhoogte", "block_remaining": "1 blok resterend", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index c2509f821b..e3e43c3bb9 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "Zeskanuj odcisk palca, aby się uwierzytelnić", "bitcoin_dark_theme": "Ciemny motyw Bitcoin", "bitcoin_light_theme": "Jasny motyw Bitcoin", + "bitcoin_lightning_deposit": "Depozyt", + "bitcoin_lightning_withdraw": "Wycofać", "bitcoin_payments_require_1_confirmation": "Płatności Bitcoin wymagają jednego potwierdzenia, co może zająć 20 minut lub dłużej. Dziękujemy za cierpliwość! Otrzymasz e‑mail, gdy płatność zostanie potwierdzona.", "block_height": "Wysokość bloku", "block_remaining": "Pozostał 1 blok", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index e1b7d4c5a9..dcbf08f198 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "Digitalize sua impressão digital para autenticar", "bitcoin_dark_theme": "Tema escuro Bitcoin", "bitcoin_light_theme": "Tema claro de bitcoin", + "bitcoin_lightning_deposit": "Depósito", + "bitcoin_lightning_withdraw": "Retirar", "bitcoin_payments_require_1_confirmation": "Os pagamentos em Bitcoin exigem 1 confirmação, o que pode levar 20 minutos ou mais. Obrigado pela sua paciência! Você receberá um e-mail quando o pagamento for confirmado.", "block_height": "Altura do bloco", "block_remaining": "1 bloco restante", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 5993fd530a..426dd63116 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "Отсканируйте свой отпечаток пальца для аутентификации", "bitcoin_dark_theme": "Биткойн Темная тема", "bitcoin_light_theme": "Светлая биткойн-тема", + "bitcoin_lightning_deposit": "Депозит", + "bitcoin_lightning_withdraw": "Отзывать", "bitcoin_payments_require_1_confirmation": "Биткойн-платежи требуют 1 подтверждения, что может занять 20 минут или дольше. Спасибо тебе за твое терпение! Вы получите электронное письмо, когда платеж будет подтвержден.", "block_height": "Высота блока", "block_remaining": "1 Блок остался", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index d5887fd734..7f161832cb 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "สแกนลายนิ้วมือของคุณเพื่อยืนยันตัวตน", "bitcoin_dark_theme": "ธีมมืด Bitcoin", "bitcoin_light_theme": "ธีมแสง Bitcoin", + "bitcoin_lightning_deposit": "เงินฝาก", + "bitcoin_lightning_withdraw": "ถอน", "bitcoin_payments_require_1_confirmation": "การชำระเงินด้วย Bitcoin ต้องการการยืนยัน 1 ครั้ง ซึ่งอาจใช้เวลา 20 นาทีหรือนานกว่านั้น ขอบคุณสำหรับความอดทนของคุณ! คุณจะได้รับอีเมลเมื่อการชำระเงินได้รับการยืนยัน", "block_height": "ความสูงของบล็อก", "block_remaining": "เหลือ 1 บล็อก", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 97712fdba3..eee24ac822 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "I-scan ang iyong fingerprint para ma-authenticate", "bitcoin_dark_theme": "Bitcoin Dark Theme", "bitcoin_light_theme": "Bitcoin Light Theme", + "bitcoin_lightning_deposit": "Deposito", + "bitcoin_lightning_withdraw": "Umatras", "bitcoin_payments_require_1_confirmation": "Ang mga pagbabayad sa Bitcoin ay nangangailangan ng 1 kumpirmasyon, na maaaring tumagal ng 20 minuto o mas mahaba. Salamat sa iyong pasensya! Mag-email ka kapag nakumpirma ang pagbabayad.", "block_height": "I -block ang taas", "block_remaining": "1 Bloke ang Natitira", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index be2dd8fd7d..26acfb0d2e 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "Kimlik doğrulaması için parmak izini okutun", "bitcoin_dark_theme": "Bitcoin Karanlık Teması", "bitcoin_light_theme": "Bitcoin Hafif Tema", + "bitcoin_lightning_deposit": "Mevduat", + "bitcoin_lightning_withdraw": "Geri çekilmek", "bitcoin_payments_require_1_confirmation": "Bitcoin ödemeleri, 20 dakika veya daha uzun sürebilen 1 onay gerektirir. Sabrınız için teşekkürler! Ödeme onaylandığında e-posta ile bilgilendirileceksiniz.", "block_height": "Blok yüksekliği", "block_remaining": "Kalan 1 blok", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index e64086e271..33ba350e25 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "Відскануйте свій відбиток пальця для аутентифікації", "bitcoin_dark_theme": "Темна тема Bitcoin", "bitcoin_light_theme": "Світла тема Bitcoin", + "bitcoin_lightning_deposit": "депозит", + "bitcoin_lightning_withdraw": "Вилучити", "bitcoin_payments_require_1_confirmation": "Платежі Bitcoin потребують 1 підтвердження, яке може зайняти 20 хвилин або більше. Дякую за Ваше терпіння! Ви отримаєте електронний лист, коли платіж буде підтверджено.", "block_height": "Висота блоку", "block_remaining": "1 блок, що залишився", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 5190195d15..9f180ae66f 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "تصدیق کرنے کے لیے اپنے فنگر پرنٹ کو اسکین کریں۔", "bitcoin_dark_theme": "بٹ کوائن ڈارک تھیم", "bitcoin_light_theme": "بٹ کوائن لائٹ تھیم", + "bitcoin_lightning_deposit": "جمع کروائیں", + "bitcoin_lightning_withdraw": "واپس لے لو", "bitcoin_payments_require_1_confirmation": "بٹ کوائن کی ادائیگی میں 1 تصدیق کی ضرورت ہوتی ہے ، جس میں 20 منٹ یا اس سے زیادہ وقت لگ سکتا ہے۔ آپ کے صبر کا شکریہ! ادائیگی کی تصدیق ہونے پر آپ کو ای میل کیا جائے گا۔", "block_height": "اونچائی کو بلاک کریں", "block_remaining": "1 بلاک باقی", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 4f3abc8de6..7961970d2d 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "Quét vân tay để xác thực", "bitcoin_dark_theme": "Chủ đề Bitcoin tối", "bitcoin_light_theme": "Chủ đề Bitcoin sáng", + "bitcoin_lightning_deposit": "Tiền gửi", + "bitcoin_lightning_withdraw": "Rút", "bitcoin_payments_require_1_confirmation": "Các khoản thanh toán Bitcoin yêu cầu 1 xác nhận, có thể mất 20 phút hoặc lâu hơn. Cảm ơn bạn đã kiên nhẫn! Bạn sẽ nhận được email khi thanh toán được xác nhận.", "block_height": "Chiều cao khối", "block_remaining": "1 khối còn lại", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index ef80e77a0a..c8351ba770 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "Ya ìka ọwọ́ yín láti ṣe ìfẹ̀rílàdí", "bitcoin_dark_theme": "Bitcoin Dark Akori", "bitcoin_light_theme": "Bitcoin Light Akori", + "bitcoin_lightning_deposit": "Owo ifipamọ", + "bitcoin_lightning_withdraw": "Yọkuro", "bitcoin_payments_require_1_confirmation": "Àwọn àránṣẹ́ Bitcoin nílò ìjẹ́rìísí kan. Ó lè lo ìṣéjú ogun tàbí ìṣéjú jù. A dúpẹ́ fún sùúrù yín! Ẹ máa gba ímeèlì t'ó bá jẹ́rìísí àránṣẹ́ náà.", "block_height": "Dènà giga", "block_remaining": "1 bulọọki to ku", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index ba602b0241..cd7a33578f 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -96,6 +96,8 @@ "biometric_auth_reason": "扫描指纹进行身份认证", "bitcoin_dark_theme": "比特币黑暗主题", "bitcoin_light_theme": "比特币浅色主题", + "bitcoin_lightning_deposit": "订金", + "bitcoin_lightning_withdraw": "提取", "bitcoin_payments_require_1_confirmation": "比特币支付需要 1 次确认,这可能需要 20 分钟或更长时间。谢谢你的耐心!确认付款后,您将收到电子邮件。", "block_height": "块高度", "block_remaining": "剩下1个块", From 5ac24a9afe1e7e6d5c618f281920c37b05051553 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Wed, 5 Nov 2025 16:16:37 +0100 Subject: [PATCH 39/68] feat: reload balance and tx history after sending a lightning transaction --- assets/images/lightning-icon.svg | 46 +++++++++++++++++++ .../lib/lightning/lightning_wallet.dart | 8 ++-- .../pending_lightning_transaction.dart | 8 +++- cw_bitcoin/pubspec.yaml | 2 +- lib/view_model/send/send_view_model.dart | 5 +- 5 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 assets/images/lightning-icon.svg diff --git a/assets/images/lightning-icon.svg b/assets/images/lightning-icon.svg new file mode 100644 index 0000000000..aa4d3a9225 --- /dev/null +++ b/assets/images/lightning-icon.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cw_bitcoin/lib/lightning/lightning_wallet.dart b/cw_bitcoin/lib/lightning/lightning_wallet.dart index 4d44dc3b58..087bfa1079 100644 --- a/cw_bitcoin/lib/lightning/lightning_wallet.dart +++ b/cw_bitcoin/lib/lightning/lightning_wallet.dart @@ -2,7 +2,6 @@ import 'package:breez_sdk_spark_flutter/breez_sdk_spark.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/lightning/pending_lightning_transaction.dart'; -import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; @@ -81,7 +80,7 @@ class LightningWallet { } } - Future createTransaction( + Future createTransaction( String address, BigInt? amountSats, BitcoinTransactionPriority? priority) async { final inputType = await sdk.parse(input: address); @@ -120,10 +119,11 @@ class LightningWallet { return PendingLightningTransaction( id: prepareResponse.invoiceDetails.paymentHash, - amount: prepareResponse.invoiceDetails.amountMsat?.toInt() ?? 0, + amount: ((prepareResponse.invoiceDetails.amountMsat?.toInt() ?? 0) / 1000).round(), fee: feeSats.toInt(), commitOverride: () async { - await sdk.lnurlPay(request: LnurlPayRequest(prepareResponse: prepareResponse)); + final res = await sdk.lnurlPay(request: LnurlPayRequest(prepareResponse: prepareResponse)); + printV(res.payment.status.name); }, ); } else if (inputType is InputType_BitcoinAddress) { diff --git a/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart b/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart index 72b4423783..cb75b7d2b3 100644 --- a/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart +++ b/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart @@ -14,6 +14,7 @@ class PendingLightningTransaction with PendingTransaction { final int fee; final bool isSendAll; Future Function() commitOverride; + final List _listeners =[]; @override final String id; @@ -34,11 +35,16 @@ class PendingLightningTransaction with PendingTransaction { int? get outputCount => 1; @override - Future commit() => commitOverride.call(); + Future commit() async { + await commitOverride.call(); + _listeners.forEach((e) => e.call()); + } @override bool shouldCommitUR() => false; @override Future> commitUR() => throw UnimplementedError(); + + void addListener(void Function() listener) => _listeners.add(listener); } diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 439492bda0..ca6f75f0fa 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -75,7 +75,7 @@ dependencies: breez_sdk_spark_flutter: git: url: https://github.com/breez/breez-sdk-spark-flutter - ref: 92f62dc2037cf08003e418aadda58f451c021f42 + ref: bca05bc9085f778e95916d55e9a75133c27755a2 dev_dependencies: flutter_test: diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 4090cb59e9..217d7cf45d 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -57,7 +57,6 @@ import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; @@ -807,9 +806,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor // Immediate transaction update for EVM chains, Solana, Tron, and Nano if (isEVMWallet || - [WalletType.solana, WalletType.tron, WalletType.nano].contains(walletType)) { + [WalletType.bitcoin, WalletType.solana, WalletType.tron, WalletType.nano] + .contains(walletType)) { Future.delayed(Duration(seconds: 4), () async { try { + await wallet.updateBalance(); await wallet.updateTransactionsHistory(); } catch (e) { printV('Failed to update transactions after send: $e'); From b78ee5b5bc7872815eada6e8c29c53c2d09508d5 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Thu, 6 Nov 2025 09:53:07 +0100 Subject: [PATCH 40/68] feat: improve address formatting for human-readable addresses and update the default LNURL domain --- assets/images/btc_chain_qr_lightning.svg | 5 +++++ cw_bitcoin/lib/bitcoin_wallet.dart | 2 +- cw_bitcoin/lib/electrum_wallet_addresses.dart | 12 ++++++++++-- lib/utils/address_formatter.dart | 7 ++++++- .../wallet_address_list_view_model.dart | 5 ++++- 5 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 assets/images/btc_chain_qr_lightning.svg diff --git a/assets/images/btc_chain_qr_lightning.svg b/assets/images/btc_chain_qr_lightning.svg new file mode 100644 index 0000000000..b18ac0b9f8 --- /dev/null +++ b/assets/images/btc_chain_qr_lightning.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 304618e714..3b903897c1 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -99,7 +99,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { lightningWallet = LightningWallet( mnemonic: mnemonic, apiKey: secrets.breezApiKey, - lnurlDomain: "breez.tips", + lnurlDomain: "cake.cash", ); } else { lightningWallet = null; diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 7ef455793f..03b32bf4e7 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -1,4 +1,5 @@ import 'dart:io' show Platform; +import 'dart:math'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; @@ -761,8 +762,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { lightningAddress = await lightningWallet!.getAddress(); if (lightningAddress == null) { - lightningAddress = - await lightningWallet!.registerAddress(walletName.replaceAll(" ", "").toLowerCase()); + final randomNumber = Random.secure().nextInt(9999); + final username = "${walletName.replaceAll(" ", "")}$randomNumber".toLowerCase(); + try { + lightningAddress = await lightningWallet!.registerAddress(username); + } catch (e) { + printV(e); + printV(username); + rethrow; + } } } } diff --git a/lib/utils/address_formatter.dart b/lib/utils/address_formatter.dart index f2083c7724..bd46985828 100644 --- a/lib/utils/address_formatter.dart +++ b/lib/utils/address_formatter.dart @@ -15,6 +15,11 @@ class AddressFormatter { final cleanAddress = address.replaceAll('bitcoincash:', ''); final isMWEB = address.startsWith('ltcmweb'); final chunkSize = walletType != null ? _getChunkSize(walletType) : 4; + final isHumanReadable = address.contains("@"); + + if (isHumanReadable) { + return Text(address, style: evenTextStyle, textAlign: textAlign ?? TextAlign.start); + } if (shouldTruncate) { return _buildTruncatedAddress( @@ -158,4 +163,4 @@ class AddressFormatter { return 4; } } -} \ No newline at end of file +} diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index db7111017a..d4d2a69add 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -435,7 +435,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo } @computed - String get qrImage => getQrImage(type); + String get qrImage { + if (uri is LightningPaymentRequest) return 'assets/images/btc_chain_qr_lightning.svg'; + return getQrImage(type); + } @computed String get monoImage => getChainMonoImage(type); From f40f95025a7cdb4a546c1c3bbf323b96dd6ee270 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Thu, 6 Nov 2025 10:34:46 +0100 Subject: [PATCH 41/68] fix: merge conflicts --- cw_bitcoin/pubspec.lock | 6 +++--- lib/view_model/dashboard/transaction_list_item.dart | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index e281c7f7b4..4bbd0c3dea 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -134,11 +134,11 @@ packages: dependency: "direct main" description: path: "." - ref: "92f62dc2037cf08003e418aadda58f451c021f42" - resolved-ref: "92f62dc2037cf08003e418aadda58f451c021f42" + ref: bca05bc9085f778e95916d55e9a75133c27755a2 + resolved-ref: bca05bc9085f778e95916d55e9a75133c27755a2 url: "https://github.com/breez/breez-sdk-spark-flutter" source: git - version: "0.3.4" + version: "0.3.5-rc1" bs58check: dependency: transitive description: diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 8336a2374c..27c2ed1ab1 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -206,7 +206,7 @@ class TransactionListItem extends ActionListItem with Keyable { break; case WalletType.arbitrum: final asset = arbitrum!.assetOfTransaction(balanceViewModel.wallet, transaction); - final price = balanceViewModel.fiatConvertationStore.prices[asset]; + final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( cryptoAmount: arbitrum!.formatterArbitrumAmountToDouble(transaction: transaction), price: price); From 5357efb77f3b6a38ba1efd4e383b82cd561355a8 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Fri, 7 Nov 2025 14:42:52 +0100 Subject: [PATCH 42/68] feat: add error handling for LightningWallet initialization and adjust transaction direction logic --- cw_bitcoin/lib/bitcoin_wallet.dart | 15 ++++++++++----- cw_bitcoin/lib/lightning/lightning_wallet.dart | 6 ++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 3b903897c1..b1f7861f1e 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -31,6 +31,7 @@ import 'package:cw_core/payjoin_session.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/utils/zpub.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; @@ -96,11 +97,15 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { // final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType); if (mnemonic != null) { - lightningWallet = LightningWallet( - mnemonic: mnemonic, - apiKey: secrets.breezApiKey, - lnurlDomain: "cake.cash", - ); + try { + lightningWallet = LightningWallet( + mnemonic: mnemonic, + apiKey: secrets.breezApiKey, + lnurlDomain: "cake.cash", + ); + } catch (e) { + printV(e); + } } else { lightningWallet = null; } diff --git a/cw_bitcoin/lib/lightning/lightning_wallet.dart b/cw_bitcoin/lib/lightning/lightning_wallet.dart index 087bfa1079..315d7406dd 100644 --- a/cw_bitcoin/lib/lightning/lightning_wallet.dart +++ b/cw_bitcoin/lib/lightning/lightning_wallet.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:breez_sdk_spark_flutter/breez_sdk_spark.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; @@ -187,6 +189,10 @@ class LightningWallet { for (final payment in payments) { TransactionDirection direction = TransactionDirection.outgoing; + if (payment.paymentType == PaymentType.receive) { + direction = TransactionDirection.incoming; + } + if (payment.method == PaymentMethod.deposit) { direction = TransactionDirection.incoming; } From 14e7ed35a8763f63895f06573f6e374cbb684e15 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Sun, 16 Nov 2025 11:34:10 +0100 Subject: [PATCH 43/68] integrate homepage from new ui mockup --- .../WixMadeforText-VariableFont_wght.ttf | Bin 0 -> 152580 bytes assets/new-ui/3dots.svg | 3 + assets/new-ui/Apps.svg | 21 ++ assets/new-ui/Charts.svg | 10 + assets/new-ui/Contacts.svg | 3 + assets/new-ui/Home.svg | 16 + assets/new-ui/Wallets.svg | 3 + assets/new-ui/addr-book.svg | 3 + assets/new-ui/bitcoin.svg | 13 + assets/new-ui/btcqr.png | Bin 0 -> 188035 bytes assets/new-ui/copy-icon.svg | 11 + assets/new-ui/exchange.svg | 11 + assets/new-ui/history-received.svg | 3 + assets/new-ui/history-receiving.svg | 3 + assets/new-ui/history-sending.svg | 3 + assets/new-ui/history-sent.svg | 3 + assets/new-ui/lightning.svg | 14 + assets/new-ui/receive.svg | 10 + assets/new-ui/scan.svg | 10 + assets/new-ui/send.svg | 3 + assets/new-ui/settings.png | Bin 0 -> 1249 bytes assets/new-ui/switcher-bitcoin-off.svg | 3 + assets/new-ui/switcher-bitcoin.svg | 4 + assets/new-ui/switcher-lightning-off.svg | 3 + assets/new-ui/switcher-lightning.svg | 4 + assets/new-ui/top-settings.svg | 5 + assets/new-ui/wallet-trezor.svg | 10 + cw_core/lib/payment_uris.dart | 8 +- lib/di.dart | 19 +- lib/entities/new_main_actions.dart | 11 +- lib/new-ui/new_dashboard.dart | 100 ++++++ lib/new-ui/pages/receive_page.dart | 66 ++++ lib/new-ui/pages/scan_page.dart | 10 + lib/new-ui/pages/send_page.dart | 10 + .../action_row/coin_action_button.dart | 56 ++++ .../action_row/coin_action_row.dart | 65 ++++ .../coins_page/assets_history/asset_tile.dart | 74 +++++ .../assets_history/assets_section.dart | 23 ++ .../assets_history/assets_top_bar.dart | 64 ++++ .../assets_history/history_section.dart | 55 ++++ .../assets_history/history_tile.dart | 109 +++++++ .../assets_history/lightning_assets.dart | 39 +++ .../coins_page/cards/balance_card.dart | 127 ++++++++ .../widgets/coins_page/cards/cards_view.dart | 138 +++++++++ lib/new-ui/widgets/coins_page/top_bar.dart | 78 +++++ .../widgets/coins_page/wallet_info.dart | 50 +++ lib/new-ui/widgets/line_tab_switcher.dart | 134 ++++++++ lib/new-ui/widgets/modern_button.dart | 62 ++++ lib/new-ui/widgets/navbar/navbar.dart | 68 +++++ lib/new-ui/widgets/navbar/navbar_button.dart | 67 ++++ .../receive_page/receive_amount_input.dart | 90 ++++++ .../receive_page/receive_bottom_buttons.dart | 97 ++++++ .../widgets/receive_page/receive_qr_code.dart | 43 +++ .../receive_seed_type_selector.dart | 51 ++++ .../receive_page/receive_seed_widget.dart | 40 +++ .../widgets/receive_page/receive_top_bar.dart | 27 ++ lib/router.dart | 264 ++++++++++------ .../widgets/new_main_navbar_widget.dart | 286 ++++++++---------- lib/typography.dart | 4 +- lib/utils/feature_flag.dart | 4 +- .../exchange/exchange_trade_view_model.dart | 11 - lib/view_model/send/send_view_model.dart | 7 +- 62 files changed, 2237 insertions(+), 292 deletions(-) create mode 100644 assets/fonts/WixMadeforText-VariableFont_wght.ttf create mode 100644 assets/new-ui/3dots.svg create mode 100644 assets/new-ui/Apps.svg create mode 100644 assets/new-ui/Charts.svg create mode 100644 assets/new-ui/Contacts.svg create mode 100644 assets/new-ui/Home.svg create mode 100644 assets/new-ui/Wallets.svg create mode 100644 assets/new-ui/addr-book.svg create mode 100644 assets/new-ui/bitcoin.svg create mode 100644 assets/new-ui/btcqr.png create mode 100644 assets/new-ui/copy-icon.svg create mode 100644 assets/new-ui/exchange.svg create mode 100644 assets/new-ui/history-received.svg create mode 100644 assets/new-ui/history-receiving.svg create mode 100644 assets/new-ui/history-sending.svg create mode 100644 assets/new-ui/history-sent.svg create mode 100644 assets/new-ui/lightning.svg create mode 100644 assets/new-ui/receive.svg create mode 100644 assets/new-ui/scan.svg create mode 100644 assets/new-ui/send.svg create mode 100644 assets/new-ui/settings.png create mode 100644 assets/new-ui/switcher-bitcoin-off.svg create mode 100644 assets/new-ui/switcher-bitcoin.svg create mode 100644 assets/new-ui/switcher-lightning-off.svg create mode 100644 assets/new-ui/switcher-lightning.svg create mode 100644 assets/new-ui/top-settings.svg create mode 100644 assets/new-ui/wallet-trezor.svg create mode 100644 lib/new-ui/new_dashboard.dart create mode 100644 lib/new-ui/pages/receive_page.dart create mode 100644 lib/new-ui/pages/scan_page.dart create mode 100644 lib/new-ui/pages/send_page.dart create mode 100644 lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart create mode 100644 lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart create mode 100644 lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart create mode 100644 lib/new-ui/widgets/coins_page/assets_history/assets_section.dart create mode 100644 lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart create mode 100644 lib/new-ui/widgets/coins_page/assets_history/history_section.dart create mode 100644 lib/new-ui/widgets/coins_page/assets_history/history_tile.dart create mode 100644 lib/new-ui/widgets/coins_page/assets_history/lightning_assets.dart create mode 100644 lib/new-ui/widgets/coins_page/cards/balance_card.dart create mode 100644 lib/new-ui/widgets/coins_page/cards/cards_view.dart create mode 100644 lib/new-ui/widgets/coins_page/top_bar.dart create mode 100644 lib/new-ui/widgets/coins_page/wallet_info.dart create mode 100644 lib/new-ui/widgets/line_tab_switcher.dart create mode 100644 lib/new-ui/widgets/modern_button.dart create mode 100644 lib/new-ui/widgets/navbar/navbar.dart create mode 100644 lib/new-ui/widgets/navbar/navbar_button.dart create mode 100644 lib/new-ui/widgets/receive_page/receive_amount_input.dart create mode 100644 lib/new-ui/widgets/receive_page/receive_bottom_buttons.dart create mode 100644 lib/new-ui/widgets/receive_page/receive_qr_code.dart create mode 100644 lib/new-ui/widgets/receive_page/receive_seed_type_selector.dart create mode 100644 lib/new-ui/widgets/receive_page/receive_seed_widget.dart create mode 100644 lib/new-ui/widgets/receive_page/receive_top_bar.dart diff --git a/assets/fonts/WixMadeforText-VariableFont_wght.ttf b/assets/fonts/WixMadeforText-VariableFont_wght.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5994d24be65d6829efef10c85d95cf116e221782 GIT binary patch literal 152580 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 + + \ No newline at end of file diff --git a/assets/new-ui/Apps.svg b/assets/new-ui/Apps.svg new file mode 100644 index 0000000000..334d800349 --- /dev/null +++ b/assets/new-ui/Apps.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/Charts.svg b/assets/new-ui/Charts.svg new file mode 100644 index 0000000000..cc86078e64 --- /dev/null +++ b/assets/new-ui/Charts.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/new-ui/Contacts.svg b/assets/new-ui/Contacts.svg new file mode 100644 index 0000000000..a0b0f28945 --- /dev/null +++ b/assets/new-ui/Contacts.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/Home.svg b/assets/new-ui/Home.svg new file mode 100644 index 0000000000..31a55b115b --- /dev/null +++ b/assets/new-ui/Home.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/Wallets.svg b/assets/new-ui/Wallets.svg new file mode 100644 index 0000000000..04c91d0e87 --- /dev/null +++ b/assets/new-ui/Wallets.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/addr-book.svg b/assets/new-ui/addr-book.svg new file mode 100644 index 0000000000..c25d3b7dbc --- /dev/null +++ b/assets/new-ui/addr-book.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/bitcoin.svg b/assets/new-ui/bitcoin.svg new file mode 100644 index 0000000000..391f3c289e --- /dev/null +++ b/assets/new-ui/bitcoin.svg @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/assets/new-ui/btcqr.png b/assets/new-ui/btcqr.png new file mode 100644 index 0000000000000000000000000000000000000000..a5c34ed1f5271059157fcf2f59bd435c077bd482 GIT binary patch literal 188035 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 literal 0 HcmV?d00001 diff --git a/assets/new-ui/copy-icon.svg b/assets/new-ui/copy-icon.svg new file mode 100644 index 0000000000..3cc98258d7 --- /dev/null +++ b/assets/new-ui/copy-icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/new-ui/exchange.svg b/assets/new-ui/exchange.svg new file mode 100644 index 0000000000..0bf048de8e --- /dev/null +++ b/assets/new-ui/exchange.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/new-ui/history-received.svg b/assets/new-ui/history-received.svg new file mode 100644 index 0000000000..3dab9e7312 --- /dev/null +++ b/assets/new-ui/history-received.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/history-receiving.svg b/assets/new-ui/history-receiving.svg new file mode 100644 index 0000000000..cec2958dc6 --- /dev/null +++ b/assets/new-ui/history-receiving.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/history-sending.svg b/assets/new-ui/history-sending.svg new file mode 100644 index 0000000000..f40ec89a0c --- /dev/null +++ b/assets/new-ui/history-sending.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/history-sent.svg b/assets/new-ui/history-sent.svg new file mode 100644 index 0000000000..649e03d087 --- /dev/null +++ b/assets/new-ui/history-sent.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/lightning.svg b/assets/new-ui/lightning.svg new file mode 100644 index 0000000000..9788dcc17e --- /dev/null +++ b/assets/new-ui/lightning.svg @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/assets/new-ui/receive.svg b/assets/new-ui/receive.svg new file mode 100644 index 0000000000..fc420ad694 --- /dev/null +++ b/assets/new-ui/receive.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/new-ui/scan.svg b/assets/new-ui/scan.svg new file mode 100644 index 0000000000..b3e6a3258b --- /dev/null +++ b/assets/new-ui/scan.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/new-ui/send.svg b/assets/new-ui/send.svg new file mode 100644 index 0000000000..cde7609f63 --- /dev/null +++ b/assets/new-ui/send.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/settings.png b/assets/new-ui/settings.png new file mode 100644 index 0000000000000000000000000000000000000000..b6cfb5b0884b7eea4d95b17850aa9c71f52da7b7 GIT binary patch literal 1249 zcmV<71Rnc|P)V$O*f<+AU@x+=XO*e|vAuHHn;D-V;}dwYa@iZ? z6Tl5JB&iC_kDBiZK?$bS-D;SaXJ9Oq35cKuNGTtLkSVn$1%wh}A%sjB2nvM#?#sF9X!N&>Y}9DnYOmcL zQUKI!0{cf7x2I9m#m_mZ)WMUDrBj{UAG*%i=JYSqqym8I2xemWK(^>CVQ| zo^38+Q$CUMToK*QUG7Io(r#}qzs@h5j7D9^HOT=&sU9|?lb_@H@nsXr?C00Q?~cs~ zQBrxzPmY^o{N2ZC3+HoG5^M;3;tb(+t0;Qej1WG`e`}bG(7v=)Ql5cb-hFm_xu+YO zF<}4XYN1NX=kQQ>amIl5`eF*st|ZX|7kQ1bz@osb~cv>IP|F^ zfDi0-3KCyoA{!&t*QH#cvf)vwfk4%Xq!uS4ij#N$->TK?Hx?oXkVt06GFx%24WS<$#T_Y2{)``Gc` ze~rdnoAL~nK}?^A0SDv&Da49t+?a`{QobS!Sdph2OKtK1%7-2mgvT6)DE{V)ioY2H z(t--b7V7pbTW!Oy4eus-BCTM@WA5N=C3asW$zkorjrdDLQUkb4;zlE_)lD7M7vRV1 z^4gfdYs2Bsop0a(p;VkC$r(pWVeDZI_}bmi!&e73Mta>y#hK9u*wF#`0J?j(lpO0R z5b!jw$rIiNOvd9k6bNXLV)BHy0rH0*o%&xVL{aHx=#w7g2{jo7V;3gV>9;pP2Bb&e zPj|=@l02PJSyFsCEo6&4pg#2Ep9-v|;EY&8F=WTptn~Iq=|qN*i_MEsIQBV-&Xf|n zNbfht<2#8`T!orx5}v4DU;P@A3Kj1wYYWRXjFi1M_4;*3S4!xRF>PGTnH>z3C>Uq2 z4h|Xk0LpFIpjE z{SyfM87KsplF?n)8lVhL^Sw}tTHM}P>Wd1|i_FX+&vbNSg--l*bGc_U0yT-`u!u}d zei-6OAK2zFDW8p-kvg=L-H+!w2JSa-NQm_uwVXaN0sVg)jkSDT!*kS-NAirFP5Dg5 zjx;AAb8;WdEU}JgEmKvR=$PNJVa!QoCB&g^E++$A@V!2>2K&&RD&*1}!FNI_&j|XZ zo*6T%40;F(h@b{A-Di`50YN{U1oMH9fWyDULErG>^?_rqQb4>1zMu?PHargg00000 LNkvXXu0mjfc7{ks literal 0 HcmV?d00001 diff --git a/assets/new-ui/switcher-bitcoin-off.svg b/assets/new-ui/switcher-bitcoin-off.svg new file mode 100644 index 0000000000..d529c77e28 --- /dev/null +++ b/assets/new-ui/switcher-bitcoin-off.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/switcher-bitcoin.svg b/assets/new-ui/switcher-bitcoin.svg new file mode 100644 index 0000000000..1fadb6eb10 --- /dev/null +++ b/assets/new-ui/switcher-bitcoin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/new-ui/switcher-lightning-off.svg b/assets/new-ui/switcher-lightning-off.svg new file mode 100644 index 0000000000..d72c681b09 --- /dev/null +++ b/assets/new-ui/switcher-lightning-off.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/switcher-lightning.svg b/assets/new-ui/switcher-lightning.svg new file mode 100644 index 0000000000..f9c5f40a1f --- /dev/null +++ b/assets/new-ui/switcher-lightning.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/new-ui/top-settings.svg b/assets/new-ui/top-settings.svg new file mode 100644 index 0000000000..ba716f8d5f --- /dev/null +++ b/assets/new-ui/top-settings.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/new-ui/wallet-trezor.svg b/assets/new-ui/wallet-trezor.svg new file mode 100644 index 0000000000..d0747c4444 --- /dev/null +++ b/assets/new-ui/wallet-trezor.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/cw_core/lib/payment_uris.dart b/cw_core/lib/payment_uris.dart index ed172ca044..6a6a60b362 100644 --- a/cw_core/lib/payment_uris.dart +++ b/cw_core/lib/payment_uris.dart @@ -67,7 +67,7 @@ class LightningPaymentRequest extends PaymentURI { } class LitecoinURI extends PaymentURI { - LitecoinURI({required super.amount, required super.address}); + LitecoinURI({required super.amount, required super.address, required super.scheme}); @override String toString() { @@ -79,7 +79,7 @@ class LitecoinURI extends PaymentURI { } class EthereumURI extends PaymentURI { - EthereumURI({required super.amount, required super.address}); + EthereumURI({required super.amount, required super.address, required super.scheme}); @override String toString() { @@ -91,7 +91,7 @@ class EthereumURI extends PaymentURI { } class BaseURI extends PaymentURI { - BaseURI({required super.amount, required super.address}); + BaseURI({required super.amount, required super.address, required super.scheme}); @override String toString() { @@ -103,7 +103,7 @@ class BaseURI extends PaymentURI { } class ArbitrumURI extends PaymentURI { - ArbitrumURI({required super.amount, required super.address}); + ArbitrumURI({required super.amount, required super.address, required super.scheme}); @override String toString() { diff --git a/lib/di.dart b/lib/di.dart index 4138842595..5d970b06bb 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -11,6 +11,7 @@ import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart'; import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; +import 'package:cake_wallet/new-ui/new_dashboard.dart'; import 'package:cake_wallet/order/order.dart'; import 'package:cake_wallet/core/backup_service_v3.dart'; import 'package:cake_wallet/core/new_wallet_arguments.dart'; @@ -748,6 +749,10 @@ Future setup({ addressListViewModel: getIt.get(), )); + getIt.registerFactory(() => NewDashboard( + dashboardViewModel: getIt.get(), + )); + getIt.registerFactory(() { final GlobalKey _navigatorKey = GlobalKey(); return DesktopSidebarWrapper( @@ -1015,7 +1020,7 @@ Future setup({ getIt.registerFactory(() => WalletKeysViewModel(getIt.get())); getIt.registerFactory(() => WalletKeysPage(getIt.get())); - + getIt.registerFactory(() => AnimatedURModel(getIt.get())); getIt.registerFactoryParam, void>((Map urQr, _) => @@ -1581,24 +1586,24 @@ Future setup({ getIt.registerFactory(() => DevSharedPreferencesPage(getIt.get())); getIt.registerFactory(() => DevSecurePreferencesPage(getIt.get())); - + getIt.registerFactory(() => BackgroundSyncLogsViewModel()); - + getIt.registerFactory(() => DevBackgroundSyncLogsPage(getIt.get())); - + getIt.registerFactory(() => SocketHealthLogsViewModel()); getIt.registerFactory(() => DevSocketHealthLogsPage(getIt.get())); - + getIt.registerFactory(() => DevNetworkRequests()); - + getIt.registerFactory(() => DevQRToolsPage()); getIt.registerFactory(() => ExchangeProviderLogsViewModel()); getIt.registerFactory(() => DevExchangeProviderLogsPage(getIt.get())); getIt.registerFactory(() => StartTorPage(StartTorViewModel(),)); - + getIt.registerFactory(() => DEuroViewModel( getIt(), getIt(), diff --git a/lib/entities/new_main_actions.dart b/lib/entities/new_main_actions.dart index 0f904ae2a6..609b04cf4d 100644 --- a/lib/entities/new_main_actions.dart +++ b/lib/entities/new_main_actions.dart @@ -1,5 +1,4 @@ import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:flutter/material.dart'; @@ -31,14 +30,14 @@ class NewMainActions { static NewMainActions homeAction = NewMainActions._( name: (context) => 'Home', //TODO S.of(context).home, - image: 'assets/images/main_actions/home.svg', + image: 'assets/new-ui/Home.svg', key: ValueKey('dashboard_page_home_action_button_key'), onTap: () {}, ); static NewMainActions walletsAction = NewMainActions._( name: (context) => S.of(context).wallets, - image: 'assets/images/main_actions/wallets.svg', + image: 'assets/new-ui/Wallets.svg', key: ValueKey('dashboard_page_wallets_action_button_key'), onTap: () {}, ); @@ -46,21 +45,21 @@ class NewMainActions { static NewMainActions contactsAction = NewMainActions._( name: (context) => 'Contacts', //TODO S.of(context).contacts, - image: 'assets/images/main_actions/contacts.svg', + image: 'assets/new-ui/Contacts.svg', key: ValueKey('dashboard_page_contacts_action_button_key'), onTap: () {}, ); static NewMainActions appsAction = NewMainActions._( name: (context) => 'Apps', //TODO S.of(context).apps, - image: 'assets/images/main_actions/apps.svg', + image: 'assets/new-ui/Apps.svg', key: ValueKey('dashboard_page_apps_action_button_key'), onTap: () {}, ); static NewMainActions chartsAction = NewMainActions._( name: (context) => 'Charts', //TODO S.of(context).charts, - image: 'assets/images/main_actions/charts.svg', + image: 'assets/new-ui/Charts.svg', key: ValueKey('dashboard_page_charts_action_button_key'), onTap: () {}, ); diff --git a/lib/new-ui/new_dashboard.dart b/lib/new-ui/new_dashboard.dart new file mode 100644 index 0000000000..ba8b88fd12 --- /dev/null +++ b/lib/new-ui/new_dashboard.dart @@ -0,0 +1,100 @@ +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/lightning_assets.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/new_main_navbar_widget.dart'; +import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart'; +import 'package:flutter/material.dart'; +import '../view_model/dashboard/dashboard_view_model.dart'; +import 'widgets/coins_page/cards/cards_view.dart'; +import 'widgets/coins_page/action_row/coin_action_row.dart'; +import 'widgets/coins_page/assets_history/history_section.dart'; +import 'widgets/coins_page/top_bar.dart'; +import 'widgets/coins_page/wallet_info.dart'; + +class NewDashboard extends StatefulWidget { + NewDashboard({super.key, required this.dashboardViewModel}) { + this.accountListViewModel = + dashboardViewModel.balanceViewModel.hasAccounts ? getIt.get() : null; + } + + final DashboardViewModel dashboardViewModel; + late final MoneroAccountListViewModel? accountListViewModel; + + + @override + State createState() => _NewDashboardState(); +} + +class _NewDashboardState extends State { + bool _lightningMode = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + SafeArea( + child: Container( + height: MediaQuery.of(context).size.height, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Theme.of(context).colorScheme.surfaceBright, + Theme.of(context).colorScheme.surface, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: SingleChildScrollView( + physics: BouncingScrollPhysics(), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + TopBar( + dashboardViewModel: widget.dashboardViewModel, + lightningMode: _lightningMode, + onLightningSwitchPress: () { + setState(() { + _lightningMode = !_lightningMode; + }); + }, + ), + WalletInfo(lightningMode: _lightningMode, usesHardwareWallet: + widget.dashboardViewModel.wallet.isHardwareWallet, + name: widget.dashboardViewModel.wallet.name + ), + CardsView(dashboardViewModel: widget.dashboardViewModel, + accountListViewModel: widget.accountListViewModel, + lightningMode: _lightningMode, + ), + CoinActionRow(), + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + transitionBuilder: (child, animation) { + return FadeTransition(opacity: animation, child: child); + }, + layoutBuilder: (currentChild, previousChildren) { + return Stack( + alignment: Alignment.topCenter, + children: [ + ...previousChildren, + if (currentChild != null) currentChild, + ], + ); + }, + child: _lightningMode + ? LightningAssets(dashboardViewModel: widget.dashboardViewModel,) + : HistorySection(dashboardViewModel: widget.dashboardViewModel,), + ), + ], + ), + ), + ), + ), + NewMainNavBar(dashboardViewModel: widget.dashboardViewModel) + ], + ), + ); + } +} diff --git a/lib/new-ui/pages/receive_page.dart b/lib/new-ui/pages/receive_page.dart new file mode 100644 index 0000000000..a6a17cb7b6 --- /dev/null +++ b/lib/new-ui/pages/receive_page.dart @@ -0,0 +1,66 @@ +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_amount_input.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_bottom_buttons.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_qr_code.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_seed_type_selector.dart'; +import 'package:flutter/material.dart'; + +import '../widgets/receive_page/receive_seed_widget.dart'; +import '../widgets/receive_page/receive_top_bar.dart'; + +class ReceivePage extends StatefulWidget { + const ReceivePage({super.key}); + + @override + State createState() => _ReceivePageState(); +} + +class _ReceivePageState extends State { + bool _largeQrMode = false; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Theme.of(context).colorScheme.surfaceBright, + Theme.of(context).colorScheme.surface, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + borderRadius: BorderRadius.circular(30), + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox(height: 12), + ReceiveTopBar(), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + mainAxisSize: MainAxisSize.max, + children: [ + ReceiveQrCode( + onTap: () { + setState(() { + _largeQrMode = !_largeQrMode; + }); + }, + largeQrMode: _largeQrMode, + ), + ReceiveSeedTypeSelector(), + ReceiveSeedWidget(), + ReceiveAmountInput(largeQrMode: _largeQrMode), + ReceiveBottomButtons(largeQrMode: _largeQrMode), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/new-ui/pages/scan_page.dart b/lib/new-ui/pages/scan_page.dart new file mode 100644 index 0000000000..75bbddaa37 --- /dev/null +++ b/lib/new-ui/pages/scan_page.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class ScanPage extends StatelessWidget { + const ScanPage({super.key}); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/lib/new-ui/pages/send_page.dart b/lib/new-ui/pages/send_page.dart new file mode 100644 index 0000000000..c53515f2e1 --- /dev/null +++ b/lib/new-ui/pages/send_page.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class SendPage extends StatelessWidget { + const SendPage({super.key}); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart b/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart new file mode 100644 index 0000000000..d925cf8759 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class CoinActionButton extends StatelessWidget { + const CoinActionButton({ + super.key, + required this.icon, + required this.label, + required this.action, + }); + + final SvgPicture icon; + final String label; + final VoidCallback action; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [Color(0xFF2B3A67), Color(0xFF1C2A4F)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + border: Border.all( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + width: 1, + ), + ), + child: IconButton( + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + onPressed: action, + icon: icon, + color: Theme.of(context).colorScheme.primary, + ), + ), + Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + style: TextStyle( + fontSize: 15, + color: Theme.of(context).colorScheme.onSurface, + ), + label, + ), + ), + ], + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart new file mode 100644 index 0000000000..5ac17a11d8 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart @@ -0,0 +1,65 @@ +import 'package:cake_wallet/new-ui/pages/send_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +import '../../../pages/receive_page.dart'; +import '../../../pages/scan_page.dart'; +import 'coin_action_button.dart'; + +class CoinActionRow extends StatelessWidget { + const CoinActionRow({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + spacing: 24.0, + children: [ + CoinActionButton( + icon: SvgPicture.asset("assets/new-ui/send.svg"), + label: "Send", + action: () { + showModalBottomSheet( + context: context, + builder: (context) => SendPage(), + ); + }, + ), + CoinActionButton( + icon: SvgPicture.asset("assets/new-ui/receive.svg"), + label: "Receive", + action: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => FractionallySizedBox( + heightFactor: 0.9, + child: ReceivePage(), + ), + ); + }, + ), + CoinActionButton( + icon: SvgPicture.asset("assets/new-ui/exchange.svg"), + label: "Swap", + action: () {}, + ), + CoinActionButton( + icon: SvgPicture.asset("assets/new-ui/scan.svg"), + label: "Scan", + action: () { + showModalBottomSheet( + context: context, + + builder: (context) => ScanPage(), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart b/lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart new file mode 100644 index 0000000000..b811604fb6 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart @@ -0,0 +1,74 @@ +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:flutter/material.dart'; + +class AssetTile extends StatelessWidget { + const AssetTile({super.key, required this.dashboardViewModel}); + + final DashboardViewModel dashboardViewModel; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 6.0), + child: Container( + width: double.infinity, + height: 80, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Theme.of(context).colorScheme.surfaceContainerHigh, + Theme.of(context).colorScheme.surfaceContainer, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + borderRadius: BorderRadius.all(Radius.circular(20)), + border: Border.all( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + width: 1, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container(width: 45, height: 45, child: Image.asset("assets/images/crypto/tether.webp")), + SizedBox(width: 8.0), + Column( + spacing: 4.0, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "DummyCoin", + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text( + "0.000 DMC", + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ], + ), + + Text( + "\$0.00", + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/assets_section.dart b/lib/new-ui/widgets/coins_page/assets_history/assets_section.dart new file mode 100644 index 0000000000..44eff375b9 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/assets_section.dart @@ -0,0 +1,23 @@ +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:flutter/material.dart'; + + +import 'asset_tile.dart'; + +class AssetsSection extends StatelessWidget { + const AssetsSection({super.key, required this.dashboardViewModel}); + + final DashboardViewModel dashboardViewModel; + + @override + Widget build(BuildContext context) { + return ListView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: 1, + itemBuilder: (context, index) { + return AssetTile(dashboardViewModel: dashboardViewModel,); + }, + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart b/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart new file mode 100644 index 0000000000..2a282ef846 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart @@ -0,0 +1,64 @@ +import 'package:cake_wallet/new-ui/widgets/line_tab_switcher.dart'; +import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; +import 'package:flutter/material.dart'; + +class AssetsTopBar extends StatelessWidget { + const AssetsTopBar({ + super.key, + required this.onTabChange, + required this.selectedTab, + }); + + final void Function(int) onTabChange; + final int selectedTab; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LineTabSwitcher( + tabs: const ["Assets", "History"], + onTabChange: onTabChange, + selectedTab: selectedTab, + ), + Row( + spacing: 8.0, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 48, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(99999), + ), + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(999999), + ), + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainer, + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + spacing: 4.0, + children: [Icon(Icons.settings, color: Theme.of(context).colorScheme.primary), Text("Tokens", style: TextStyle(color: Theme.of(context).colorScheme.primary),)], + ), + ), + ), + ), + ModernButton(size: 48, onPressed:(){}, icon: Icon(Icons.question_mark)), + ], + ), + ], + ), + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_section.dart b/lib/new-ui/widgets/coins_page/assets_history/history_section.dart new file mode 100644 index 0000000000..745517cbbc --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/history_section.dart @@ -0,0 +1,55 @@ +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_tile.dart'; +import 'package:cake_wallet/utils/date_formatter.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/date_section_item.dart'; +import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart'; +import 'package:flutter/material.dart'; + + +class HistorySection extends StatelessWidget { + const HistorySection({super.key, required this.dashboardViewModel}); + + final DashboardViewModel dashboardViewModel; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 8.0), + child: ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: dashboardViewModel.items.length, + itemBuilder: (context, index) { + final prevItem = index == 0 ? null : dashboardViewModel.items[index - 1]; + final item = dashboardViewModel.items[index]; + final nextItem = index == dashboardViewModel.items.length - 1 ? null : dashboardViewModel.items[index + 1]; + + + if(item is TransactionListItem) { + final transaction = item.transaction; + final transactionType = + dashboardViewModel.getTransactionType(transaction); + + return HistoryTile( + title: item.formattedTitle + item.formattedStatus + transactionType, + date: DateFormatter.convertDateTimeToReadableString(item.date), + amount: item.formattedCryptoAmount, + amountFiat: item.formattedFiatAmount, + roundedBottom: !(nextItem is TransactionListItem), + roundedTop: !(prevItem is TransactionListItem), + bottomSeparator: nextItem is TransactionListItem, + direction: item.transaction.direction, + pending: item.transaction.isPending + ); + + + } else if(item is DateSectionItem){ + return Text(DateFormatter.convertDateTimeToReadableString(item.date)); + } + + else return Text(item.runtimeType.toString()); + }, + ), + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_tile.dart b/lib/new-ui/widgets/coins_page/assets_history/history_tile.dart new file mode 100644 index 0000000000..185d753adc --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/history_tile.dart @@ -0,0 +1,109 @@ +import 'package:cw_core/transaction_direction.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class HistoryTile extends StatelessWidget { + const HistoryTile( + {super.key, + required this.title, + required this.date, + required this.amount, + required this.amountFiat, + required this.roundedTop, + required this.roundedBottom, + required this.direction, + required this.pending, + required this.bottomSeparator}); + + final String title; + final String date; + final String amount; + final String amountFiat; + final bool roundedTop; + final bool roundedBottom; + final bool bottomSeparator; + final TransactionDirection direction; + final bool pending; + + String _getDirectionIcon() { + if (pending) { + return direction == TransactionDirection.incoming + ? 'assets/new-ui/history-receiving.svg' + : 'assets/new-ui/history-sending.svg'; + } else { + return direction == TransactionDirection.incoming + ? 'assets/new-ui/history-received.svg' + : 'assets/new-ui/history-sent.svg'; + } + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(roundedTop ? 12.0 : 0.0), + topRight: Radius.circular(roundedTop ? 12.0 : 0.0), + bottomLeft: Radius.circular(roundedBottom ? 12.0 : 0.0), + bottomRight: Radius.circular(roundedBottom ? 12.0 : 0.0), + )), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12.0, + horizontal: 12.0, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 0.0), + child: SizedBox( + height: 50, + width: 50, + child: SvgPicture.asset(_getDirectionIcon()), + ), + ), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title), + Text(date), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(amount), + Text(amountFiat), + ], + ), + ], + ), + ), + ], + ), + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.0), + child: SizedBox( + height: 1, + child: DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/lightning_assets.dart b/lib/new-ui/widgets/coins_page/assets_history/lightning_assets.dart new file mode 100644 index 0000000000..3100d4331f --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/lightning_assets.dart @@ -0,0 +1,39 @@ +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:flutter/material.dart'; +import 'assets_section.dart'; +import 'history_section.dart'; + +class LightningAssets extends StatefulWidget { + const LightningAssets({super.key, required this.dashboardViewModel}); + + static const List tabs = ["Assets", "History"]; + final DashboardViewModel dashboardViewModel; + + @override + State createState() => _LightningAssetsState(); +} + +class _LightningAssetsState extends State { + int _selectedTab = 0; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + AssetsTopBar( + onTabChange: (index) { + setState(() { + _selectedTab = index; + }); + }, + selectedTab: _selectedTab, + ), + [ + AssetsSection(dashboardViewModel: widget.dashboardViewModel,), + HistorySection(dashboardViewModel: widget.dashboardViewModel,), + ][_selectedTab], + ], + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/cards/balance_card.dart b/lib/new-ui/widgets/coins_page/cards/balance_card.dart new file mode 100644 index 0000000000..bcdf43bd89 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/cards/balance_card.dart @@ -0,0 +1,127 @@ +import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class BalanceCard extends StatelessWidget { + const BalanceCard({ + super.key, + required this.width, + required this.balanceRecord, + required this.selected, required this.accountName, required this.accountBalance, + }); + + final double width; + final String accountBalance; + final String accountName; + final BalanceRecord balanceRecord; + final bool selected; + + @override + Widget build(BuildContext context) { + final Duration textFadeDuration = Duration(milliseconds: 80); + + return Container( + width: width, + height: width * 2.0 / 3, + decoration: BoxDecoration( + border: Border.all(color: Color(0x77FFFFFF), width: 1), + gradient: LinearGradient( + colors: [Colors.lightBlueAccent, Colors.blue], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + borderRadius: BorderRadius.circular(20), + ), + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + accountName, + style: TextStyle(color: Colors.black, fontSize: 20), + ), + + AnimatedOpacity( + opacity: selected ? 0 : 1, + duration: textFadeDuration, + child: Text( + accountBalance, + style: TextStyle(color: Colors.black, fontSize: 14), + ), + ), + ], + ), + AnimatedOpacity( + opacity: selected ? 1 : 0, + duration: textFadeDuration, + child: Row( + spacing: 8.0, + children: [ + Text( + balanceRecord.availableBalance, + style: TextStyle(color: Colors.black, fontSize: 28), + ), + Text( + balanceRecord.asset.name.toUpperCase(), + style: TextStyle(color: Colors.black45, fontSize: 28), + ), + ], + ), + ), + Text( + balanceRecord.fiatAvailableBalance, + style: TextStyle(color: Colors.black45, fontSize: 20), + ), + ], + ), + + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + decoration: BoxDecoration( + color: Color(0x44FFFFFF), + borderRadius: BorderRadius.circular(10000000), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(6.0), + child: Text( + "Buy", + style: TextStyle(color: Colors.black, fontSize: 16), + ), + ), + Icon(Icons.arrow_forward, color: Colors.black45), + ], + ), + ), + SvgPicture.asset( + "assets/new-ui/switcher-bitcoin.svg", + height: 50, + width: 50, + colorFilter: const ColorFilter.mode( + Color(0x44FFFFFF), + BlendMode.srcIn, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/cards/cards_view.dart b/lib/new-ui/widgets/coins_page/cards/cards_view.dart new file mode 100644 index 0000000000..b4b517e5c4 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/cards/cards_view.dart @@ -0,0 +1,138 @@ +import 'dart:math'; + +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +import 'balance_card.dart'; + +class CardsView extends StatefulWidget { + const CardsView({super.key, required this.dashboardViewModel, required this.accountListViewModel, required this.lightningMode}); + + final DashboardViewModel dashboardViewModel; + final MoneroAccountListViewModel? accountListViewModel; + final bool lightningMode; + + + @override + _CardsViewState createState() => _CardsViewState(); +} + +class _CardsViewState extends State { + int? _selectedIndex = 0; + + static const Duration animDuration = Duration(milliseconds: 200); + static const double overlapAmount = 60.0; + late final double cardWidth = MediaQuery.of(context).size.width * 0.85; + late final int numCards; + + @override + void initState() { + super.initState(); + numCards = widget.accountListViewModel?.accounts.length ?? 1; + } + + Widget _buildCard(int index, double parentWidth) { + final int numCards = widget.accountListViewModel?.accounts.length ?? 1; + final double baseTop = overlapAmount * (numCards - 1); + final double scaleFactor = 0.96; + + final int howFarBehind = (_selectedIndex! - index + numCards) % numCards; + final double scale = pow(scaleFactor, howFarBehind).toDouble(); + + final double top = baseTop - (howFarBehind * overlapAmount); + + final double left = (parentWidth - cardWidth) / 2.0; + + return AnimatedPositioned( + key: ValueKey('box_$index'), + duration: animDuration, + curve: Curves.easeOut, + top: top, + left: left, + child: AnimatedScale( + duration: animDuration, + curve: Curves.easeOut, + scale: scale, + child: GestureDetector( + onTap: () { + setState(() { + if(widget.accountListViewModel != null) + widget.accountListViewModel!.select(widget.accountListViewModel!.accounts[index]); + _selectedIndex = index; + }); + }, + child: Observer( + builder: (_){return BalanceCard( + width: cardWidth, + accountName: (widget.accountListViewModel?.accounts[index].label) ?? "Primary account", + accountBalance: widget.accountListViewModel?.accounts[index].balance ?? "", + balanceRecord: widget.dashboardViewModel.balanceViewModel.formattedBalances.elementAt(0), + selected: _selectedIndex == index, + );} + ), + ), + ), + ); + } + + double _getBoxHeight() { + return + /* height of initial card */ + (2 / 3) * (cardWidth) + + /* height of bg card * amount of bg cards */ + overlapAmount * ((widget.accountListViewModel?.accounts.length ??1) - 1); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final double parentWidth = constraints.maxWidth; + List children = []; + + if (_selectedIndex! >= (widget.accountListViewModel?.accounts.length ?? 1)) { + _selectedIndex = 0; + } + + for ( + int i = _selectedIndex!; + i < (widget.accountListViewModel?.accounts.length ?? 1) + _selectedIndex!; + i++ + ) { + if (i != _selectedIndex) { + children.add(_buildCard(i % (widget.accountListViewModel?.accounts.length ?? 1), parentWidth)); + } + } + + if (_selectedIndex != null) { + children.add(_buildCard(_selectedIndex!, parentWidth)); + } + + return Observer( + builder: (_){return Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: AnimatedContainer( + duration: Duration(milliseconds: 200), + curve: Curves.easeOut, + width: double.infinity, + height: _getBoxHeight(), + child: AnimatedSwitcher( + duration: Duration(milliseconds: 200), + transitionBuilder: (child, animation) => + FadeTransition(opacity: animation, child: child), + child: SizedBox( + key: ValueKey(_getBoxHeight()), + width: double.infinity, + height: _getBoxHeight(), + child: Stack(alignment: Alignment.center, children: children), + ), + ), + ), + );} + ); + }, + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/top_bar.dart b/lib/new-ui/widgets/coins_page/top_bar.dart new file mode 100644 index 0000000000..0c7e0ce15f --- /dev/null +++ b/lib/new-ui/widgets/coins_page/top_bar.dart @@ -0,0 +1,78 @@ +import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class TopBar extends StatelessWidget { + const TopBar({ + super.key, + required this.lightningMode, + required this.onLightningSwitchPress, required this.dashboardViewModel, + }); + + final bool lightningMode; + final VoidCallback onLightningSwitchPress; + final DashboardViewModel dashboardViewModel; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(18.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if(dashboardViewModel.balanceViewModel.hasSecondAdditionalBalance || + dashboardViewModel.balanceViewModel.hasSecondAvailableBalance) + SizedBox( + child: AnimatedSwitcher( + duration: Duration(milliseconds: 200), + transitionBuilder: (child, animation) => + FadeTransition(opacity: animation, child: child), + child: ElevatedButton( + key: ValueKey(lightningMode), + style: ElevatedButton.styleFrom( + padding: EdgeInsets.all(4), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(900.0)), + ), + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainer, + ), + onPressed: onLightningSwitchPress, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SvgPicture.asset( + lightningMode + ? 'assets/new-ui/switcher-lightning.svg' + : 'assets/new-ui/switcher-bitcoin.svg', + width: 40, + height: 40, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + SvgPicture.asset( + lightningMode + ? 'assets/new-ui/switcher-bitcoin-off.svg' + : 'assets/new-ui/switcher-lightning-off.svg', + width: 40, + height: 40, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + ], + ), + ), + ), + ), + ModernButton.svg(size: 44, onPressed: (){}, svgPath: "assets/new-ui/top-settings.svg",), + ], + ), + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/wallet_info.dart b/lib/new-ui/widgets/coins_page/wallet_info.dart new file mode 100644 index 0000000000..68adc8b130 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/wallet_info.dart @@ -0,0 +1,50 @@ +import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class WalletInfo extends StatelessWidget { + const WalletInfo({super.key, required this.lightningMode, required this.name, required this.usesHardwareWallet}); + + final bool lightningMode; + final String name; + final bool usesHardwareWallet; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + + children: [ + AnimatedSwitcher( + duration: Duration(milliseconds: 150), + transitionBuilder: (child, animation) { + return SizeTransition( + axis: Axis.horizontal, + sizeFactor: animation, + child: FadeTransition(opacity: animation, child: child), + ); + }, + child: !usesHardwareWallet + ? SizedBox.shrink(key: ValueKey("empty")) + : Padding( + padding: const EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 0.0), + child: SvgPicture.asset( + "assets/new-ui/wallet-trezor.svg", + key: ValueKey("wallet"), + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, + BlendMode.srcIn, + ), + ), + ), + ), + Text(name, style: TextStyle(fontSize: 20)), + SizedBox(width: 8), + ModernButton.svg(size: 20, onPressed: (){}, svgPath: "assets/new-ui/3dots.svg",) + ], + ); + } +} diff --git a/lib/new-ui/widgets/line_tab_switcher.dart b/lib/new-ui/widgets/line_tab_switcher.dart new file mode 100644 index 0000000000..6fdb533d02 --- /dev/null +++ b/lib/new-ui/widgets/line_tab_switcher.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +class LineTabSwitcher extends StatefulWidget { + const LineTabSwitcher({ + super.key, + required this.tabs, + required this.onTabChange, + required this.selectedTab, + }); + + final List tabs; + final void Function(int index) onTabChange; + final int selectedTab; + + @override + State createState() => _LineTabSwitcherState(); +} + +class _LineTabSwitcherState extends State { + List textWidgetKeys = []; + List textWidgetSizes = []; + bool textWidgetsMeasured = false; + + double _calcBarLeft() { + double left = 0; + + if (textWidgetKeys.isEmpty || textWidgetSizes.isEmpty) { + return 0; + } + + for (int i = 0; i < widget.selectedTab; i++) { + left += textWidgetSizes[i].width + 16.0; + } + + left += 8.0; + + return left; + } + + @override + void initState() { + super.initState(); + textWidgetKeys = List.generate(widget.tabs.length, (index) => GlobalKey()); + WidgetsBinding.instance.addPostFrameCallback((_) => measure()); + } + + void measure() { + setState(() { + textWidgetSizes = textWidgetKeys + .map((k) => k.currentContext!.size) + .whereType() + .toList(); + textWidgetsMeasured = true; + }); + } + + @override + Widget build(BuildContext context) { + if (!textWidgetsMeasured) { + WidgetsBinding.instance.addPostFrameCallback((_) => measure()); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + width: 200, + height: 40, + child: ListView.builder( + physics: NeverScrollableScrollPhysics(), + scrollDirection: Axis.horizontal, + itemCount: widget.tabs.length, + itemBuilder: (context, index) { + return GestureDetector( + onTap: () { + widget.onTabChange(index); + }, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedDefaultTextStyle( + duration: Duration(milliseconds: 150), + style: DefaultTextStyle.of(context).style.copyWith( + inherit: true, + fontSize: 22, + color: widget.selectedTab == index + ? Theme.of(context).colorScheme.onSurface + : Theme.of(context).colorScheme.onSurfaceVariant, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + widget.tabs[index], + key: textWidgetKeys[index], + ), + ), + ), + ], + ), + ); + }, + ), + ), + Container( + width: 200, + height: 2, + child: Stack( + children: [ + AnimatedPositioned( + curve: Curves.easeOut, + left: _calcBarLeft(), + bottom: 0, + duration: Duration(milliseconds: 150), + child: AnimatedSize( + duration: Duration(milliseconds: 150), + child: Container( + height: 2, + width: textWidgetSizes.isEmpty + ? 0 + : textWidgetSizes[widget.selectedTab].width, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/new-ui/widgets/modern_button.dart b/lib/new-ui/widgets/modern_button.dart new file mode 100644 index 0000000000..2c0db7f12a --- /dev/null +++ b/lib/new-ui/widgets/modern_button.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class ModernButton extends StatelessWidget { + final double size; + final String? svgPath; + final Widget? icon; + final VoidCallback onPressed; + final Color? color; + + static const iconSvgSizeRatio = 2/3; + + + const ModernButton({ + super.key, + required this.size, + required this.icon, + required this.onPressed, + this.color + }) : svgPath = null; + + const ModernButton.svg({ + super.key, + required this.size, + required this.svgPath, + required this.onPressed, + this.color + }) : icon = null; + + @override + Widget build(BuildContext context) { + final color = Theme.of(context).colorScheme.primary; + final Widget resolvedIcon = svgPath != null + ? SvgPicture.asset( + svgPath!, + width: size, + height: size, + fit: BoxFit.contain, + alignment: Alignment.center, + allowDrawingOutsideViewBox: true, + colorFilter: ColorFilter.mode(color, BlendMode.srcIn), + ) + : IconTheme( + data: IconThemeData(color: color, size: size*iconSvgSizeRatio), + child: icon!, + ); + + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(size), + ), + width: size, + height: size, + child: IconButton( + padding: EdgeInsets.zero, + onPressed: onPressed, + icon: resolvedIcon, + ), + ); + } +} diff --git a/lib/new-ui/widgets/navbar/navbar.dart b/lib/new-ui/widgets/navbar/navbar.dart new file mode 100644 index 0000000000..f19f6e69b6 --- /dev/null +++ b/lib/new-ui/widgets/navbar/navbar.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +import 'navbar_button.dart'; + +class Navbar extends StatefulWidget { + const Navbar({super.key}); + + @override + State createState() => _NavbarState(); +} + +class NavbarItemData { + final String iconPath; + final String text; + + NavbarItemData(this.iconPath, this.text); +} + +class _NavbarState extends State { + int _selectedIndex = 0; + + final List _items = [ + NavbarItemData("assets/Home.svg", "Home"), + NavbarItemData("assets/Wallets.svg", "Wallets"), + NavbarItemData("assets/Contacts.svg", "Contacts"), + NavbarItemData("assets/Apps.svg", "Apps"), + NavbarItemData("assets/Charts.svg", "Charts"), + ]; + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.all(32.0), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(99999), + color: Theme.of( + context, + ).colorScheme.surfaceContainerHighest.withAlpha(170), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 12.0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: List.generate(_items.length, (index) { + return NavbarButton( + data: _items[index], + onPressed: () { + setState(() { + _selectedIndex = index; + }); + }, + selected: _selectedIndex == index, + ); + }), + ), + ), + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/navbar/navbar_button.dart b/lib/new-ui/widgets/navbar/navbar_button.dart new file mode 100644 index 0000000000..79c3af9eac --- /dev/null +++ b/lib/new-ui/widgets/navbar/navbar_button.dart @@ -0,0 +1,67 @@ +import 'package:cake_wallet/new-ui/widgets/navbar/navbar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class NavbarButton extends StatelessWidget { + const NavbarButton({ + super.key, + required this.data, + required this.selected, + required this.onPressed, + }); + + final NavbarItemData data; + final VoidCallback onPressed; + final bool selected; + + @override + Widget build(BuildContext context) { + return AnimatedSize( + curve: Curves.easeOut, + duration: Duration(milliseconds: 100), + child: AnimatedContainer( + curve: Curves.easeOut, + duration: Duration(milliseconds: 100), + decoration: BoxDecoration( + color: selected + ? Color(0x79BDCFFF) + : Theme.of( + context, + ).colorScheme.surfaceContainerHighest.withAlpha(0), + borderRadius: BorderRadius.circular(1242357), + ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + constraints: BoxConstraints(), + padding: EdgeInsets.zero, + icon: SvgPicture.asset( + data.iconPath, + width: selected ? 24 : 36, + height: selected ? 24 : 36, + colorFilter: ColorFilter.mode( + selected + ? Theme.of(context).colorScheme.onSurface + : Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + onPressed: onPressed, + ), + if (selected) + Padding( + padding: const EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 0.0), + child: Text( + data.text, + style: TextStyle(fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/receive_page/receive_amount_input.dart b/lib/new-ui/widgets/receive_page/receive_amount_input.dart new file mode 100644 index 0000000000..1a793d015c --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_amount_input.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; + +class ReceiveAmountInput extends StatelessWidget { + const ReceiveAmountInput({super.key, required this.largeQrMode}); + + final bool largeQrMode; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedContainer( + duration: Duration(milliseconds: 300), + height: 56, + width: largeQrMode ? 250 : 160, + decoration: BoxDecoration( + // color: largeQrMode + // ? Theme.of(context).colorScheme.surface + // // no it can't just be transparent. might be framework bug actually + // : Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10), + bottomLeft: Radius.circular(10), + topRight: Radius.circular(0), + bottomRight: Radius.circular(0), + ), + border: Border.all( + color: Theme.of(context).colorScheme.surfaceContainer, + width: 2, + ), + ), + child: AnimatedScale( + duration: Duration(milliseconds: 500), + scale: largeQrMode ? 1.3 : 1, + curve: Curves.easeOut, + child: TextField( + enabled: !largeQrMode, + textAlign: TextAlign.center, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + hint: Text( + "0.00000000", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.5), + ), + ), + border: InputBorder.none, + ), + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + ), + ), + ), + Container( + height: 56, + width: 74, + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(0), + bottomLeft: Radius.circular(0), + topRight: Radius.circular(10), + bottomRight: Radius.circular(10), + ), + color: Theme.of(context).colorScheme.surfaceContainer, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 4.0, + children: [ + Text( + "BTC", + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + Icon( + Icons.keyboard_arrow_down, + color: Theme.of(context).colorScheme.primary, + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/new-ui/widgets/receive_page/receive_bottom_buttons.dart b/lib/new-ui/widgets/receive_page/receive_bottom_buttons.dart new file mode 100644 index 0000000000..5df32e216b --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_bottom_buttons.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; + +class ReceiveBottomButtons extends StatelessWidget { + final bool largeQrMode; + const ReceiveBottomButtons({super.key, required this.largeQrMode}); + + @override + Widget build(BuildContext context) { + final double targetHeight = largeQrMode ? 0 : 150; + final double targetOpacity = largeQrMode ? 0 : 1; + + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + height: targetHeight, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: targetOpacity, + curve: Curves.easeOut, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainer, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric(vertical: 16), + ), + onPressed: () {}, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.book_outlined, + size: 20, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 10), + Text( + 'Accounts & Addresses', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric(vertical: 16), + ), + onPressed: () {}, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Copy Address', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + const SizedBox(width: 10), + Icon( + Icons.copy_all_outlined, + size: 20, + color: Theme.of(context).colorScheme.onPrimary, + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/receive_page/receive_qr_code.dart b/lib/new-ui/widgets/receive_page/receive_qr_code.dart new file mode 100644 index 0000000000..055567d73e --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_qr_code.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +class ReceiveQrCode extends StatelessWidget { + const ReceiveQrCode({ + super.key, + required this.onTap, + required this.largeQrMode, + }); + + final VoidCallback onTap; + final bool largeQrMode; + + @override + Widget build(BuildContext context) { + final double targetY = largeQrMode ? 40 : 0; + + return GestureDetector( + onTap: onTap, + child: TweenAnimationBuilder( + tween: Tween(begin: 0, end: targetY), + duration: const Duration(milliseconds: 500), + curve: Curves.easeOut, + builder: (context, value, child) { + return Transform.translate( + offset: Offset(0, value), + child: AnimatedContainer( + duration: const Duration(milliseconds: 500), + curve: Curves.easeOutCubic, + width: largeQrMode ? 400 : 250, + height: largeQrMode ? 400 : 250, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Colors.white, + ), + padding: const EdgeInsets.all(8.0), + child: Image.asset("assets/btcqr.png"), + ), + ); + }, + ), + ); + } +} diff --git a/lib/new-ui/widgets/receive_page/receive_seed_type_selector.dart b/lib/new-ui/widgets/receive_page/receive_seed_type_selector.dart new file mode 100644 index 0000000000..d136604b4d --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_seed_type_selector.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class ReceiveSeedTypeSelector extends StatelessWidget { + const ReceiveSeedTypeSelector({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + spacing: 12.0, + children: [ + SvgPicture.asset( + width: 32, + height: 32, + "assets/new-ui/switcher-bitcoin-off.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + Text( + "Standard", + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.primary, + ), + ), + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(999999), + ), + child: IconButton( + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + onPressed: () {}, + icon: (Icon( + color: Theme.of(context).colorScheme.primary, + size: 20, + Icons.keyboard_arrow_down, + )), + ), + ), + ], + ); + } +} diff --git a/lib/new-ui/widgets/receive_page/receive_seed_widget.dart b/lib/new-ui/widgets/receive_page/receive_seed_widget.dart new file mode 100644 index 0000000000..a95fcbb1b7 --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_seed_widget.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +class ReceiveSeedWidget extends StatelessWidget { + const ReceiveSeedWidget({super.key}); + + static const List dummyWalletStrings = [ + 'bc1q', + 'xy2k', + 'gdyg', + 'jrsq', + 'tzq2', + 'n0yr', + 'f249', + '3p83', + 'kkfj', + 'hx0wlh', + ]; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 80.0), + child: Wrap( + alignment: WrapAlignment.center, + spacing: 8.0, + runSpacing: 4.0, + children: List.generate( + dummyWalletStrings.length, + (index) => Text( + dummyWalletStrings[index], + style: TextStyle( + fontSize: 16, + color: index % 2 != 0 ? Colors.grey : Colors.white, + ), + ), + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/receive_page/receive_top_bar.dart b/lib/new-ui/widgets/receive_page/receive_top_bar.dart new file mode 100644 index 0000000000..cbd80167b2 --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_top_bar.dart @@ -0,0 +1,27 @@ +import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; +import 'package:flutter/material.dart'; + +class ReceiveTopBar extends StatelessWidget { + const ReceiveTopBar({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ModernButton(size: 52, onPressed: () { + Navigator.of(context).pop(); + }, icon: Icon(Icons.close)), + + Text("Receive", style: TextStyle(fontSize: 22)), + ModernButton(size: 52, onPressed: () { + Navigator.of(context).pop(); + }, icon: Icon(Icons.share)), + ], + ), + ); + } +} diff --git a/lib/router.dart b/lib/router.dart index 02fe8275df..ee0a77c20b 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/core/new_wallet_arguments.dart'; +import 'package:cake_wallet/new-ui/new_dashboard.dart'; import 'package:cake_wallet/order/order.dart'; import 'package:cake_wallet/core/new_wallet_type_arguments.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; @@ -40,6 +41,7 @@ import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart'; import 'package:cake_wallet/src/screens/dev/moneroc_cache_debug.dart'; import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart'; import 'package:cake_wallet/src/screens/dev/network_requests.dart'; +import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cake_wallet/src/screens/dev/qr_tools_page.dart'; import 'package:cake_wallet/src/screens/dev/secure_preferences_page.dart'; import 'package:cake_wallet/src/screens/dev/shared_preferences_page.dart'; @@ -148,6 +150,7 @@ import 'package:cw_core/nano_account.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/unspent_coin_type.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/cupertino.dart'; @@ -163,15 +166,19 @@ Route handleRouteWithPlatformAwareness( bool fullscreenDialog = false, }) { if (Platform.isIOS) { - return CupertinoPageRoute(builder: builder, fullscreenDialog: fullscreenDialog); + return CupertinoPageRoute( + builder: builder, fullscreenDialog: fullscreenDialog); } else { - return MaterialPageRoute(builder: builder, fullscreenDialog: fullscreenDialog); + return MaterialPageRoute( + builder: builder, fullscreenDialog: fullscreenDialog); } } Route createRoute(RouteSettings settings) { currentRouteSettings = settings; + printV(settings.name); + switch (settings.name) { case Routes.welcome: return MaterialPageRoute( @@ -222,7 +229,8 @@ Route createRoute(RouteSettings settings) { case Routes.walletGroupsDisplayPage: final type = settings.arguments as WalletType; - final walletGroupsDisplayVM = getIt.get(param1: type); + final walletGroupsDisplayVM = + getIt.get(param1: type); return handleRouteWithPlatformAwareness( (_) => WalletGroupsDisplayPage( @@ -247,21 +255,25 @@ Route createRoute(RouteSettings settings) { case Routes.chooseHardwareWalletAccount: final arguments = settings.arguments as List; final type = arguments[0] as WalletType; - final hardwareWallet = arguments [1] as HardwareWalletType; + final hardwareWallet = arguments[1] as HardwareWalletType; final walletVM = getIt.get( - param1: type, param2: getIt(param1: hardwareWallet)); + param1: type, + param2: getIt(param1: hardwareWallet)); if (type == WalletType.monero) - return handleRouteWithPlatformAwareness((_) => MoneroHardwareWalletOptionsPage(walletVM)); + return handleRouteWithPlatformAwareness( + (_) => MoneroHardwareWalletOptionsPage(walletVM)); - return handleRouteWithPlatformAwareness((_) => SelectHardwareWalletAccountPage(walletVM)); + return handleRouteWithPlatformAwareness( + (_) => SelectHardwareWalletAccountPage(walletVM)); case Routes.setupPin: Function(PinCodeState, String)? callback; if (settings.arguments is Function(PinCodeState, String)) { - callback = settings.arguments as Function(PinCodeState, String); + callback = + settings.arguments as Function(PinCodeState, String); } return handleRouteWithPlatformAwareness( @@ -274,7 +286,8 @@ Route createRoute(RouteSettings settings) { param1: NewWalletTypeArguments( onTypeSelected: (BuildContext context, WalletType type) { final arg = {'walletType': type}; - Navigator.of(context).pushNamed(Routes.restoreWallet, arguments: arg); + Navigator.of(context) + .pushNamed(Routes.restoreWallet, arguments: arg); }, isCreate: false, ), @@ -294,7 +307,8 @@ Route createRoute(RouteSettings settings) { case Routes.restoreWalletFromSeedKeys: if (isSingleCoin) { return handleRouteWithPlatformAwareness( - (context) => getIt.get(param1: availableWalletTypes.first), + (context) => + getIt.get(param1: availableWalletTypes.first), ); } return handleRouteWithPlatformAwareness( @@ -302,7 +316,8 @@ Route createRoute(RouteSettings settings) { param1: NewWalletTypeArguments( onTypeSelected: (BuildContext context, WalletType type) { final arg = {'walletType': type}; - Navigator.of(context).pushNamed(Routes.restoreWallet, arguments: arg); + Navigator.of(context) + .pushNamed(Routes.restoreWallet, arguments: arg); }, isCreate: false, ), @@ -312,19 +327,23 @@ Route createRoute(RouteSettings settings) { case Routes.restoreWalletFromHardwareWallet: final arguments = settings.arguments as Map?; final showUnavailable = (arguments?['showUnavailable'] as bool?) ?? true; - final onSelect = arguments?['onSelect'] as void Function(BuildContext, HardwareWalletType)?; + final onSelect = arguments?['onSelect'] as void Function( + BuildContext, HardwareWalletType)?; final availableHardwareWalletTypes = - arguments?['availableHardwareWalletTypes'] as List?; + arguments?['availableHardwareWalletTypes'] + as List?; - return handleRouteWithPlatformAwareness((_) => SelectDeviceManufacturerPage( - showUnavailable: showUnavailable, - onSelect: onSelect, - availableHardwareWalletTypes: availableHardwareWalletTypes, - )); + return handleRouteWithPlatformAwareness( + (_) => SelectDeviceManufacturerPage( + showUnavailable: showUnavailable, + onSelect: onSelect, + availableHardwareWalletTypes: availableHardwareWalletTypes, + )); case Routes.connectHardwareWallet: final arguments = settings.arguments as List; - final hardwareWalletType = (arguments[0] as HardwareWalletType?) ?? HardwareWalletType.ledger; + final hardwareWalletType = + (arguments[0] as HardwareWalletType?) ?? HardwareWalletType.ledger; if (isSingleCoin) { return handleRouteWithPlatformAwareness( @@ -332,9 +351,13 @@ Route createRoute(RouteSettings settings) { ConnectDevicePageParams( walletType: availableWalletTypes.first, hardwareWalletType: hardwareWalletType, - onConnectDevice: (BuildContext context, _) => Navigator.of(context).pushNamed( - Routes.chooseHardwareWalletAccount, - arguments: [availableWalletTypes.first, hardwareWalletType]), + onConnectDevice: (BuildContext context, _) => + Navigator.of(context).pushNamed( + Routes.chooseHardwareWalletAccount, + arguments: [ + availableWalletTypes.first, + hardwareWalletType + ]), isReconnect: false, ), getIt.get(), @@ -346,7 +369,8 @@ Route createRoute(RouteSettings settings) { param1: NewWalletTypeArguments( onTypeSelected: (BuildContext context, WalletType type) { if (hardwareWalletType == HardwareWalletType.trezor) { - Navigator.of(context).pushNamed(Routes.chooseHardwareWalletAccount, + Navigator.of(context).pushNamed( + Routes.chooseHardwareWalletAccount, arguments: [type, hardwareWalletType]); return; } @@ -354,13 +378,15 @@ Route createRoute(RouteSettings settings) { final arguments = ConnectDevicePageParams( walletType: type, hardwareWalletType: hardwareWalletType, - onConnectDevice: (BuildContext context, _) => Navigator.of(context).pushNamed( - Routes.chooseHardwareWalletAccount, - arguments: [type, hardwareWalletType]), + onConnectDevice: (BuildContext context, _) => + Navigator.of(context).pushNamed( + Routes.chooseHardwareWalletAccount, + arguments: [type, hardwareWalletType]), isReconnect: false, ); - Navigator.of(context).pushNamed(Routes.connectDevices, arguments: arguments); + Navigator.of(context) + .pushNamed(Routes.connectDevices, arguments: arguments); }, isCreate: false, hardwareWalletType: hardwareWalletType, @@ -381,14 +407,16 @@ Route createRoute(RouteSettings settings) { case Routes.seed: return handleRouteWithPlatformAwareness( - (context) => getIt.get(param1: settings.arguments as bool), + (context) => + getIt.get(param1: settings.arguments as bool), ); case Routes.restoreWallet: final args = settings.arguments as Map?; final walletType = args?['walletType'] as WalletType; return MaterialPageRoute( - builder: (_) => getIt.get(param1: walletType, param2: args)); + builder: (_) => + getIt.get(param1: walletType, param2: args)); case Routes.restoreWalletChooseDerivation: return MaterialPageRoute( @@ -396,16 +424,21 @@ Route createRoute(RouteSettings settings) { param1: settings.arguments as List)); case Routes.sweepingWalletPage: - return CupertinoPageRoute(builder: (_) => getIt.get()); + return CupertinoPageRoute( + builder: (_) => getIt.get()); case Routes.dashboard: return CupertinoPageRoute( - settings: settings, builder: (_) => getIt.get()); + settings: settings, builder: (_) => + FeatureFlag.hasNewUi? + getIt.get(): + getIt.get()); case Routes.send: final args = settings.arguments as Map?; final initialPaymentRequest = args?['paymentRequest'] as PaymentRequest?; - final coinTypeToSpendFrom = args?['coinTypeToSpendFrom'] as UnspentCoinType?; + final coinTypeToSpendFrom = + args?['coinTypeToSpendFrom'] as UnspentCoinType?; return handleRouteWithPlatformAwareness( (context) => getIt.get( @@ -416,10 +449,12 @@ Route createRoute(RouteSettings settings) { case Routes.sendTemplate: return CupertinoPageRoute( - fullscreenDialog: true, builder: (_) => getIt.get()); + fullscreenDialog: true, + builder: (_) => getIt.get()); case Routes.receive: - return CupertinoPageRoute(builder: (context) => getIt.get()); + return CupertinoPageRoute( + builder: (context) => getIt.get()); case Routes.addressPage: return handleRouteWithPlatformAwareness( @@ -429,29 +464,34 @@ Route createRoute(RouteSettings settings) { case Routes.transactionDetails: return CupertinoPageRoute( fullscreenDialog: true, - builder: (_) => - getIt.get(param1: settings.arguments as TransactionInfo)); + builder: (_) => getIt.get( + param1: settings.arguments as TransactionInfo)); case Routes.bumpFeePage: return CupertinoPageRoute( fullscreenDialog: true, - builder: (_) => getIt.get(param1: settings.arguments as List)); + builder: (_) => getIt.get( + param1: settings.arguments as List)); case Routes.newSubaddress: return CupertinoPageRoute( - builder: (_) => getIt.get(param1: settings.arguments)); + builder: (_) => + getIt.get(param1: settings.arguments)); case Routes.disclaimer: return CupertinoPageRoute(builder: (_) => DisclaimerPage()); case Routes.readDisclaimer: - return CupertinoPageRoute(builder: (_) => DisclaimerPage(isReadOnly: true)); + return CupertinoPageRoute( + builder: (_) => DisclaimerPage(isReadOnly: true)); case Routes.readThirdPartyDisclaimer: - return CupertinoPageRoute(builder: (_) => ThirdPartyDisclaimerPage()); + return CupertinoPageRoute( + builder: (_) => ThirdPartyDisclaimerPage()); case Routes.changeRep: - return CupertinoPageRoute(builder: (_) => getIt.get()); + return CupertinoPageRoute( + builder: (_) => getIt.get()); case Routes.walletList: final onWalletLoaded = settings.arguments as Function(BuildContext)?; @@ -463,8 +503,8 @@ Route createRoute(RouteSettings settings) { case Routes.walletEdit: return MaterialPageRoute( fullscreenDialog: true, - builder: (_) => - getIt.get(param1: settings.arguments as WalletEditPageArguments), + builder: (_) => getIt.get( + param1: settings.arguments as WalletEditPageArguments), ); case Routes.auth: @@ -477,7 +517,8 @@ Route createRoute(RouteSettings settings) { instanceName: 'wallet_unlock_verifiable', param2: true) : getIt.get( - param1: settings.arguments as OnAuthenticationFinished, param2: true)); + param1: settings.arguments as OnAuthenticationFinished, + param2: true)); case Routes.totpAuthCodePage: final args = settings.arguments as TotpAuthArgumentsModel; @@ -503,13 +544,15 @@ Route createRoute(RouteSettings settings) { ? WillPopScope( child: getIt.get( param1: WalletUnlockArguments( - callback: settings.arguments as OnAuthenticationFinished), + callback: + settings.arguments as OnAuthenticationFinished), param2: false, instanceName: 'wallet_unlock_verifiable'), onWillPop: () async => false) : WillPopScope( child: getIt.get( - param1: settings.arguments as OnAuthenticationFinished, param2: false), + param1: settings.arguments as OnAuthenticationFinished, + param2: false), onWillPop: () async => false)); case Routes.silentPaymentsSettings: @@ -554,11 +597,13 @@ Route createRoute(RouteSettings settings) { case Routes.trocadorProvidersPage: return CupertinoPageRoute( - fullscreenDialog: true, builder: (_) => getIt.get()); + fullscreenDialog: true, + builder: (_) => getIt.get()); case Routes.domainLookupsPage: return CupertinoPageRoute( - fullscreenDialog: true, builder: (_) => getIt.get()); + fullscreenDialog: true, + builder: (_) => getIt.get()); case Routes.displaySettingsPage: return handleRouteWithPlatformAwareness( @@ -574,17 +619,20 @@ Route createRoute(RouteSettings settings) { final args = settings.arguments as Map?; return CupertinoPageRoute( builder: (_) => getIt.get( - param1: args?['editingNode'] as Node?, param2: args?['isSelected'] as bool?)); + param1: args?['editingNode'] as Node?, + param2: args?['isSelected'] as bool?)); case Routes.login: return CupertinoPageRoute( builder: (context) => WillPopScope( child: SettingsStoreBase.walletPasswordDirectInput - ? getIt.get(instanceName: 'wallet_password_login') + ? getIt.get( + instanceName: 'wallet_password_login') : getIt.get(instanceName: 'login'), onWillPop: () async => // FIX-ME: Additional check does it works correctly - (await SystemChannels.platform.invokeMethod('SystemNavigator.pop') ?? + (await SystemChannels.platform + .invokeMethod('SystemNavigator.pop') ?? false)), fullscreenDialog: true); @@ -592,7 +640,8 @@ Route createRoute(RouteSettings settings) { final args = settings.arguments as Map?; return CupertinoPageRoute( builder: (_) => getIt.get( - param1: args?['editingNode'] as Node?, param2: args?['isSelected'] as bool?)); + param1: args?['editingNode'] as Node?, + param2: args?['isSelected'] as bool?)); case Routes.accountCreation: return CupertinoPageRoute( @@ -601,8 +650,8 @@ Route createRoute(RouteSettings settings) { case Routes.nanoAccountCreation: return CupertinoPageRoute( - builder: (_) => - getIt.get(param1: settings.arguments as NanoAccount?)); + builder: (_) => getIt.get( + param1: settings.arguments as NanoAccount?)); case Routes.addressBook: return handleRouteWithPlatformAwareness( @@ -615,11 +664,13 @@ Route createRoute(RouteSettings settings) { builder: (_) => getIt.get(param1: selectedCurrency)); case Routes.pickerWalletAddress: - return MaterialPageRoute(builder: (_) => getIt.get()); + return MaterialPageRoute( + builder: (_) => getIt.get()); case Routes.addressBookAddContact: return handleRouteWithPlatformAwareness( - (context) => getIt.get(param1: settings.arguments as ContactRecord?), + (context) => getIt.get( + param1: settings.arguments as ContactRecord?), ); case Routes.showKeys: @@ -628,19 +679,23 @@ Route createRoute(RouteSettings settings) { ); case Routes.exchangeTrade: - return CupertinoPageRoute(builder: (_) => getIt.get()); + return CupertinoPageRoute( + builder: (_) => getIt.get()); case Routes.exchangeConfirm: - return MaterialPageRoute(builder: (_) => getIt.get()); + return MaterialPageRoute( + builder: (_) => getIt.get()); case Routes.tradeDetails: return MaterialPageRoute( fullscreenDialog: true, - builder: (_) => getIt.get(param1: settings.arguments as Trade)); + builder: (_) => + getIt.get(param1: settings.arguments as Trade)); case Routes.orderDetails: return MaterialPageRoute( - builder: (_) => getIt.get(param1: settings.arguments as Order)); + builder: (_) => + getIt.get(param1: settings.arguments as Order)); case Routes.buySellPage: final args = settings.arguments as bool; @@ -650,7 +705,8 @@ Route createRoute(RouteSettings settings) { case Routes.buyOptionsPage: final args = settings.arguments as List; - return MaterialPageRoute(builder: (_) => getIt.get(param1: args)); + return MaterialPageRoute( + builder: (_) => getIt.get(param1: args)); case Routes.paymentMethodOptionsPage: final args = settings.arguments as List; @@ -661,15 +717,18 @@ Route createRoute(RouteSettings settings) { final args = settings.arguments as List; return MaterialPageRoute( - fullscreenDialog: true, builder: (_) => getIt.get(param1: args)); + fullscreenDialog: true, + builder: (_) => getIt.get(param1: args)); case Routes.exchange: return handleRouteWithPlatformAwareness( - (context) => getIt.get(param1: settings.arguments as PaymentRequest?), + (context) => getIt.get( + param1: settings.arguments as PaymentRequest?), ); case Routes.exchangeTemplate: - return CupertinoPageRoute(builder: (_) => getIt.get()); + return CupertinoPageRoute( + builder: (_) => getIt.get()); case Routes.rescan: return MaterialPageRoute(builder: (_) => getIt.get()); @@ -681,11 +740,13 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute(builder: (_) => getIt.get()); case Routes.walletGroupExistingSeedDescriptionPage: - return MaterialPageRoute(builder: (_) => WalletGroupExistingSeedDescriptionPage()); + return MaterialPageRoute( + builder: (_) => WalletGroupExistingSeedDescriptionPage()); case Routes.transactionSuccessPage: return MaterialPageRoute( - builder: (_) => getIt.get(param1: settings.arguments as String)); + builder: (_) => getIt.get( + param1: settings.arguments as String)); case Routes.backup: return handleRouteWithPlatformAwareness( @@ -693,11 +754,13 @@ Route createRoute(RouteSettings settings) { ); case Routes.editBackupPassword: - return CupertinoPageRoute(builder: (_) => getIt.get()); + return CupertinoPageRoute( + builder: (_) => getIt.get()); case Routes.restoreFromBackup: return CupertinoPageRoute( - fullscreenDialog: true, builder: (_) => getIt.get()); + fullscreenDialog: true, + builder: (_) => getIt.get()); case Routes.support: return handleRouteWithPlatformAwareness( @@ -705,7 +768,8 @@ Route createRoute(RouteSettings settings) { ); case Routes.supportLiveChat: - return CupertinoPageRoute(builder: (_) => getIt.get()); + return CupertinoPageRoute( + builder: (_) => getIt.get()); case Routes.supportOtherLinks: return handleRouteWithPlatformAwareness( @@ -715,7 +779,8 @@ Route createRoute(RouteSettings settings) { case Routes.unspentCoinsList: final coinTypeToSpendFrom = settings.arguments as UnspentCoinType?; return handleRouteWithPlatformAwareness( - (context) => getIt.get(param1: coinTypeToSpendFrom), + (context) => + getIt.get(param1: coinTypeToSpendFrom), ); case Routes.unspentCoinsDetails: @@ -752,7 +817,6 @@ Route createRoute(RouteSettings settings) { (context) => getIt.get(param1: args), ); - case Routes.cakePayAccountPage: return handleRouteWithPlatformAwareness( (context) => getIt.get(), @@ -782,41 +846,47 @@ Route createRoute(RouteSettings settings) { toggleUseTestnet: toggleTestnet, advancedPrivacySettingsViewModel: getIt.get(param1: type), - nodeViewModel: getIt.get(param1: type, param2: false), + nodeViewModel: + getIt.get(param1: type, param2: false), seedSettingsViewModel: getIt.get(), ), ); case Routes.anonPayInvoicePage: final args = settings.arguments as List; - return CupertinoPageRoute(builder: (_) => getIt.get(param1: args)); + return CupertinoPageRoute( + builder: (_) => getIt.get(param1: args)); case Routes.anonPayReceivePage: final anonReceivePageArgs = settings.arguments as AnonPayReceivePageArgs; return CupertinoPageRoute( - builder: (_) => getIt.get(param1: anonReceivePageArgs)); + builder: (_) => + getIt.get(param1: anonReceivePageArgs)); case Routes.anonPayDetailsPage: final anonInvoiceViewData = settings.arguments as AnonpayInvoiceInfo; return CupertinoPageRoute( - builder: (_) => getIt.get(param1: anonInvoiceViewData)); + builder: (_) => + getIt.get(param1: anonInvoiceViewData)); case Routes.payjoinDetails: final arguments = settings.arguments as List; final sessionId = arguments.first as String; final transactionInfo = arguments[1] as TransactionInfo?; return CupertinoPageRoute( - builder: (_) => - getIt.get(param1: sessionId, param2: transactionInfo)); + builder: (_) => getIt.get( + param1: sessionId, param2: transactionInfo)); case Routes.desktop_actions: return PageRouteBuilder( opaque: false, - pageBuilder: (_, __, ___) => DesktopDashboardActions(getIt()), + pageBuilder: (_, __, ___) => + DesktopDashboardActions(getIt()), ); case Routes.desktop_settings_page: - return CupertinoPageRoute(builder: (_) => getIt.get()); + return CupertinoPageRoute( + builder: (_) => getIt.get()); case Routes.empty_no_route: return MaterialPageRoute(builder: (_) => SizedBox.shrink()); @@ -831,17 +901,21 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute(builder: (_) => getIt.get()); case Routes.setup_2faQRPage: - return MaterialPageRoute(builder: (_) => getIt.get()); + return MaterialPageRoute( + builder: (_) => getIt.get()); case Routes.modify2FAPage: - return MaterialPageRoute(builder: (_) => getIt.get()); + return MaterialPageRoute( + builder: (_) => getIt.get()); case Routes.setup2faInfoPage: - return MaterialPageRoute(builder: (_) => getIt.get()); + return MaterialPageRoute( + builder: (_) => getIt.get()); case Routes.urqrAnimatedPage: return MaterialPageRoute( - builder: (_) => getIt.get(param1: settings.arguments)); + builder: (_) => + getIt.get(param1: settings.arguments)); case Routes.homeSettings: return CupertinoPageRoute( @@ -863,10 +937,12 @@ Route createRoute(RouteSettings settings) { ); case Routes.manageNodes: - return MaterialPageRoute(builder: (_) => getIt.get(param1: false)); + return MaterialPageRoute( + builder: (_) => getIt.get(param1: false)); case Routes.managePowNodes: - return MaterialPageRoute(builder: (_) => getIt.get(param1: true)); + return MaterialPageRoute( + builder: (_) => getIt.get(param1: true)); case Routes.walletConnectConnectionsListing: return MaterialPageRoute( @@ -902,7 +978,9 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute( builder: (_) => ConnectDevicePage( - params, getIt.get(param1: params.hardwareWalletType))); + params, + getIt.get( + param1: params.hardwareWalletType))); case Routes.walletGroupDescription: final walletType = settings.arguments as WalletType; @@ -942,12 +1020,12 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute( builder: (_) => getIt.get(), ); - + case Routes.devSocketHealthLogs: return CupertinoPageRoute( builder: (_) => getIt.get(), ); - + case Routes.devQRTools: return MaterialPageRoute( builder: (_) => getIt.get(), @@ -957,12 +1035,12 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute( builder: (_) => getIt.get(), ); - + case Routes.devExchangeProviderLogs: return MaterialPageRoute( builder: (_) => getIt.get(), ); - + case Routes.devMoneroCallProfiler: return MaterialPageRoute( builder: (_) => getIt.get(), @@ -977,7 +1055,7 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute( builder: (_) => getIt.get(), ); - + case Routes.startTor: return MaterialPageRoute( builder: (_) => getIt.get(), @@ -991,6 +1069,8 @@ Route createRoute(RouteSettings settings) { default: return MaterialPageRoute( builder: (_) => Scaffold( - body: Center(child: Text(S.current.router_no_route(settings.name ?? 'No route'))))); + body: Center( + child: Text(S.current + .router_no_route(settings.name ?? 'No route'))))); } } diff --git a/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart index 2c1dc74e96..2a708e4869 100644 --- a/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart +++ b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart @@ -1,4 +1,3 @@ -import 'dart:math' as math; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -20,36 +19,38 @@ class NewMainNavBar extends StatefulWidget { } class _NEWNewMainNavBarState extends State { - static const kBarFlex = 0.85; static const barHeight = 64.0; static const barBottomPadding = 32.0; static const iconWidth = 28.0; static const iconHeight = 28.0; + static const iconHorizontalPadding = 12.0; static const pillIconWidth = 20.0; static const pillIconHeight = 20.0; - static const pillIconSpacing = 8.0; - static const pillHorizontalPadding = 14.0; + static const pillIconSpacing = 4.0; + static const pillHorizontalPadding = 16.0; static const barBorderRadius = 50.0; static const pillBorderRadius = 50.0; - static const barResizeDuration = Duration(milliseconds: 400); + static const barHorizontalPadding = 12.0; + + static const barResizeDuration = Duration(milliseconds: 100); static const inactiveIconMoveDuration = Duration(milliseconds: 150); static const inactiveIconFadeDuration = Duration(milliseconds: 100); static const inactiveIconAppearDuration = Duration(milliseconds: 250); - static const pillMoveDuration = Duration(milliseconds: 300); - static const pillResizeDuration = Duration(milliseconds: 200); + static const pillMoveDuration = Duration(milliseconds: 150); + static const pillResizeDuration = Duration(milliseconds: 100); static const pillTextStyle = TextStyle( fontSize: 16, fontWeight: FontWeight.w500, ); - late int selectedIndex; - bool _fadeSelected = true; + int selectedIndex = 0; + bool _fadeSelected = false; bool _firstFrame = true; @override @@ -68,11 +69,11 @@ class _NEWNewMainNavBarState extends State { setState(() { selectedIndex = index; - _fadeSelected = false; + _fadeSelected = true; }); // delay fade (tweak duration) - Future.delayed(const Duration(milliseconds: 50), () { + Future.delayed(const Duration(milliseconds: 00), () { if (!mounted) return; if (index == selectedIndex) { setState(() => _fadeSelected = true); @@ -98,7 +99,21 @@ class _NEWNewMainNavBarState extends State { return pillIconWidth + pillIconSpacing + textPainter.width + - pillHorizontalPadding * 2; + pillHorizontalPadding; + } + + double calcLeft(int index, double pillWidth) { + final double baseOffset = (iconWidth+iconHorizontalPadding) * index; + + double additionalSpacing; + if (index > selectedIndex) additionalSpacing = pillWidth-iconWidth; + else additionalSpacing = 0; + + return baseOffset + additionalSpacing; + } + + double calcBarWidth(double pillWidth) { + return (iconWidth+iconHorizontalPadding)*NewMainActions.all.length+(pillWidth-iconWidth)+barHorizontalPadding; } @override @@ -115,56 +130,12 @@ class _NEWNewMainNavBarState extends State { (action) => action.canShow?.call(widget.dashboardViewModel) ?? true) .toList(); - final screenWidth = MediaQuery.of(context).size.width; final pillWidth = _estimatePillWidthForAction( context, visibleActions[selectedIndex], color: activeColor); - final baseWidth = screenWidth * 0.65; + final barWidth = calcBarWidth(pillWidth); - final double baselinePillWidth = - pillIconWidth + pillIconSpacing + (pillHorizontalPadding * 2) + 8; - - // Dynamic bar width - final barWidth = math.max( - baseWidth, - baseWidth + (pillWidth - baselinePillWidth) * kBarFlex, - ); - - final int itemCount = visibleActions.length; - const double edgePadding = 10.0; - final double firstItemLeft = edgePadding; - final double lastItemLeft = barWidth - pillWidth - edgePadding; - - // Center alignment for middle (3rd) icon - final double centerOfBar = barWidth / 2; - final double halfPill = pillWidth / 2; - final double centerItemLeft = centerOfBar - halfPill; - - // Base even spacing between first → center → last - final double secondItemLeft = - firstItemLeft + (centerItemLeft - firstItemLeft) / 2; - final double fourthItemLeft = - centerItemLeft + (lastItemLeft - centerItemLeft) / 2; - - // Spacing correction function - double spacingCorrection(int index) { - const double maxCorrection = 6.0; - final double factor = - (index - (itemCount - 1) / 2).abs() / ((itemCount - 1) / 2); - return maxCorrection * factor; - } - - // Apply correction: shift outer icons inward slightly - final List positions = [ - firstItemLeft + spacingCorrection(0), - secondItemLeft + spacingCorrection(1) / 2, - centerItemLeft, - fourthItemLeft - spacingCorrection(3) / 2, - lastItemLeft - spacingCorrection(4), - ]; - - final double left = positions[selectedIndex]; final currentAction = visibleActions[selectedIndex]; return Align( @@ -187,70 +158,67 @@ class _NEWNewMainNavBarState extends State { color: backgroundColor, borderRadius: BorderRadius.circular(barBorderRadius), ), - child: Stack( - alignment: Alignment.center, - children: [ - AnimatedPill( - left: left, - pillColor: pillColor, - currentAction: currentAction, - pillIconHeight: pillIconHeight, - pillIconWidth: pillIconWidth, - pillIconSpacing: pillIconSpacing, - pillBorderRadius: pillBorderRadius, - contentColor: activeColor, - estimateWidthForAction: pillWidth, - pillTextStyle: pillTextStyle, - pillMoveDuration: pillMoveDuration, - pillResizeDuration: pillResizeDuration, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - for (int i = 0; i < visibleActions.length; i++) - GestureDetector( - onTap: () => _onItemTap(i), - child: AnimatedContainer( - duration: _firstFrame - ? Duration.zero - : inactiveIconMoveDuration, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: barHorizontalPadding), + child: Stack( + alignment: Alignment.center, + children: [ + AnimatedPill( + left: calcLeft(selectedIndex, pillWidth), + pillColor: pillColor, + currentAction: currentAction, + pillIconHeight: pillIconHeight, + pillIconWidth: pillIconWidth, + pillIconSpacing: pillIconSpacing, + pillBorderRadius: pillBorderRadius, + contentColor: activeColor, + estimateWidthForAction: pillWidth, + pillTextStyle: pillTextStyle, + pillMoveDuration: pillMoveDuration, + pillResizeDuration: pillResizeDuration, + ), + for (int i = 0; i < visibleActions.length; i++) + AnimatedPositioned( + duration: pillResizeDuration, + left: calcLeft(i, pillWidth), + curve: Curves.easeOutCubic, + child: GestureDetector( + onTap: () => _onItemTap(i), + child: AnimatedContainer( + duration: _firstFrame + ? Duration.zero + : inactiveIconMoveDuration, + curve: Curves.easeOutCubic, + width: + i == selectedIndex ? pillWidth : iconWidth, + height: iconHeight, + alignment: Alignment.center, + child: AnimatedOpacity( + duration: inactiveIconFadeDuration, curve: Curves.easeOutCubic, - width: i == selectedIndex - ? pillWidth - : iconWidth, - height: iconHeight, - alignment: Alignment.center, - child: AnimatedOpacity( - duration: inactiveIconFadeDuration, + opacity: (i == selectedIndex && _fadeSelected) + ? 0.0 + : 1.0, + child: AnimatedScale( + duration: inactiveIconAppearDuration, curve: Curves.easeOutCubic, - opacity: - (i == selectedIndex && _fadeSelected) - ? 0.0 - : 1.0, - child: AnimatedScale( - duration: inactiveIconAppearDuration, - curve: Curves.easeOutCubic, - scale: - (i == selectedIndex) ? 0.95 : 1.0, - child: SvgPicture.asset( - visibleActions[i].image, - width: iconWidth, - height: iconHeight, - colorFilter: ColorFilter.mode( - inactiveColor, - BlendMode.srcIn, - ), + scale: (i == selectedIndex) ? 0.95 : 1.0, + child: SvgPicture.asset( + visibleActions[i].image, + width: iconWidth, + height: iconHeight, + colorFilter: ColorFilter.mode( + inactiveColor, + BlendMode.srcIn, ), ), ), ), ), - ], - ), - ) - ], + ), + ), + ], + ), )), ), ), @@ -294,60 +262,46 @@ class AnimatedPill extends StatelessWidget { @override Widget build(BuildContext context) { return AnimatedPositioned( - duration: pillMoveDuration, - curve: Curves.easeOutCubic, - left: left, - top: 12, - bottom: 12, - child: TweenAnimationBuilder( - tween: Tween( - begin: estimateWidthForAction, - end: estimateWidthForAction, - ), - duration: pillResizeDuration, + duration: pillMoveDuration, curve: Curves.easeOutCubic, - builder: (context, width, child) { - return AnimatedContainer( - duration: pillResizeDuration, - curve: Curves.easeOutCubic, - width: width + 4, - decoration: BoxDecoration( - color: pillColor, - borderRadius: BorderRadius.circular(pillBorderRadius), - ), - clipBehavior: Clip.hardEdge, - alignment: Alignment.center, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 2), - child: FittedBox( - fit: BoxFit.scaleDown, - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset( - currentAction.image, - width: pillIconWidth, - height: pillIconHeight, - colorFilter: ColorFilter.mode( - contentColor, - BlendMode.srcIn, - ), - ), - SizedBox(width: pillIconSpacing), - Text( - currentAction.name(context), - style: pillTextStyle.copyWith(color: contentColor), - overflow: TextOverflow.fade, - softWrap: false, - ), - ], + left: left, + top: 12, + bottom: 12, + child: AnimatedContainer( + duration: pillResizeDuration, + curve: Curves.easeOutCubic, + width: estimateWidthForAction, + decoration: BoxDecoration( + color: pillColor, + borderRadius: BorderRadius.circular(pillBorderRadius), + ), + clipBehavior: Clip.hardEdge, + alignment: Alignment.center, + child: FittedBox( + fit: BoxFit.scaleDown, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + currentAction.image, + width: pillIconWidth, + height: pillIconHeight, + colorFilter: ColorFilter.mode( + contentColor, + BlendMode.srcIn, + ), ), - ), + SizedBox(width: pillIconSpacing), + Text( + currentAction.name(context), + style: pillTextStyle.copyWith(color: contentColor), + overflow: TextOverflow.fade, + softWrap: false, + ), + ], ), - ); - }, - ), - ); + ), + )); } } diff --git a/lib/typography.dart b/lib/typography.dart index 816f116b41..c08a98e168 100644 --- a/lib/typography.dart +++ b/lib/typography.dart @@ -1,6 +1,8 @@ +import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:flutter/material.dart'; const latoFont = "Lato"; +const wixFont = "Wix Madefor Text"; TextStyle textXxSmall({Color? color}) => _cakeRegular(10, color); @@ -54,7 +56,7 @@ TextStyle _textStyle({ Color? color, }) => TextStyle( - fontFamily: latoFont, + fontFamily: FeatureFlag.hasNewUi ? wixFont : latoFont, fontSize: size, fontWeight: fontWeight, color: color ?? Colors.white, diff --git a/lib/utils/feature_flag.dart b/lib/utils/feature_flag.dart index 661595a414..4591aeffdf 100644 --- a/lib/utils/feature_flag.dart +++ b/lib/utils/feature_flag.dart @@ -10,7 +10,9 @@ class FeatureFlag { static const bool isBackgroundSyncEnabled = true; static bool get isInAppTorEnabled => CakeTor.instance is! CakeTorDisabled; static const int verificationWordsCount = kDebugMode ? 0 : 2; - static const bool hasDevOptions = bool.fromEnvironment('hasDevOptions', defaultValue: kDebugMode); + static const bool hasDevOptions = + bool.fromEnvironment('hasDevOptions', defaultValue: kDebugMode); static const bool hasBitcoinViewOnly = true; static const bool customBackgroundEnabled = false; + static const bool hasNewUi = true; } diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index a2cf0d738e..40f0fe0057 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -18,7 +18,6 @@ import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_item.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; @@ -422,16 +421,6 @@ abstract class ExchangeTradeViewModelBase with Store { case WalletType.ethereum: return _createERC681URI(fromCurrency, inputAddress, amount); // TODO: Expand ERC681URI support to Polygon(modify decoding flow for QRs, pay anything, and deep link handling) - case WalletType.polygon: - return PolygonURI(amount: amount, address: inputAddress); - case WalletType.base: - return BaseURI(amount: amount, address: inputAddress); - case WalletType.arbitrum: - return ArbitrumURI(amount: amount, address: inputAddress); - case WalletType.solana: - return SolanaURI(amount: amount, address: inputAddress); - case WalletType.tron: - return TronURI(amount: amount, address: inputAddress); case WalletType.monero: return MoneroURI(address: inputAddress, amount: amount); case WalletType.wownero: diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 217d7cf45d..aa7fe20fc5 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -100,8 +100,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor this.transactionDescriptionBox, this.hardwareWalletViewModel, this.unspentCoinsListViewModel, - this.feesViewModel, - this.walletInfoSource, { + this.feesViewModel, { this.coinTypeToSpendFrom = UnspentCoinType.nonMweb, }) : state = InitialExecutionState(), currencies = appStore.wallet!.balance.keys.toList(), @@ -873,7 +872,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor WalletType.banano, WalletType.solana, WalletType.tron, - WalletType.arbitrium + WalletType.arbitrum ].contains(wallet.type)) { throw Exception('Priority is null for wallet type: ${wallet.type}'); } @@ -1053,7 +1052,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor WalletType.polygon, WalletType.base, WalletType.haven, - WalletType.arbitrium + WalletType.arbitrum ].contains(walletType)) { if (errorMessage.contains('gas required exceeds allowance')) { return S.current.gas_exceeds_allowance; From b60abec0ca65e1dfd113b4e0d8585eee8fce4bf2 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Sun, 16 Nov 2025 11:50:31 +0100 Subject: [PATCH 44/68] fix import --- lib/src/screens/send/send_page.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index cd3e695aa3..d6547b3cfe 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -21,6 +21,7 @@ import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.da import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; +import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/simple_checkbox.dart'; From dbd0e05b10ac16c6d89ce3b84440e8574ae5a736 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Sun, 16 Nov 2025 21:07:17 +0100 Subject: [PATCH 45/68] add new-ui dir to pubspec_base --- pubspec_base.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 274a97201e..79f4acc696 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -242,6 +242,7 @@ flutter: - assets/text/ - assets/faq/ - assets/animation/ + - assets/new-ui/ fonts: - family: Lato From 564f1932a943150bff4798cf4e892baeb8a2a8e2 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Sun, 16 Nov 2025 21:31:42 +0100 Subject: [PATCH 46/68] minor layout fixes --- .../widgets/coins_page/action_row/coin_action_button.dart | 7 +++++-- .../widgets/coins_page/action_row/coin_action_row.dart | 2 +- lib/new-ui/widgets/coins_page/top_bar.dart | 6 +++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart b/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart index d925cf8759..a3ae3f4431 100644 --- a/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart +++ b/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart @@ -13,13 +13,16 @@ class CoinActionButton extends StatelessWidget { final String label; final VoidCallback action; + static const sizeFactor = 0.18; + @override Widget build(BuildContext context) { + final double size = MediaQuery.of(context).size.width*sizeFactor; return Column( children: [ Container( - width: 80, - height: 80, + width: size, + height: size, decoration: BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( diff --git a/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart index 5ac17a11d8..5c4d4204d5 100644 --- a/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart +++ b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart @@ -16,7 +16,7 @@ class CoinActionRow extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, - spacing: 24.0, + spacing: MediaQuery.of(context).size.width*0.05, children: [ CoinActionButton( icon: SvgPicture.asset("assets/new-ui/send.svg"), diff --git a/lib/new-ui/widgets/coins_page/top_bar.dart b/lib/new-ui/widgets/coins_page/top_bar.dart index 0c7e0ce15f..5e970dd7e3 100644 --- a/lib/new-ui/widgets/coins_page/top_bar.dart +++ b/lib/new-ui/widgets/coins_page/top_bar.dart @@ -21,8 +21,8 @@ class TopBar extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - if(dashboardViewModel.balanceViewModel.hasSecondAdditionalBalance || - dashboardViewModel.balanceViewModel.hasSecondAvailableBalance) + (dashboardViewModel.balanceViewModel.hasSecondAdditionalBalance || + dashboardViewModel.balanceViewModel.hasSecondAvailableBalance) ? SizedBox( child: AnimatedSwitcher( duration: Duration(milliseconds: 200), @@ -69,7 +69,7 @@ class TopBar extends StatelessWidget { ), ), ), - ), + ) : Container(), ModernButton.svg(size: 44, onPressed: (){}, svgPath: "assets/new-ui/top-settings.svg",), ], ), From 104119902ab4a85a9d6f44d589984b2fca250ab9 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Sun, 16 Nov 2025 21:37:20 +0100 Subject: [PATCH 47/68] cleanup navbar logic --- .../dashboard/widgets/new_main_navbar_widget.dart | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart index 2a708e4869..53bc21ab25 100644 --- a/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart +++ b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart @@ -50,7 +50,6 @@ class _NEWNewMainNavBarState extends State { ); int selectedIndex = 0; - bool _fadeSelected = false; bool _firstFrame = true; @override @@ -69,15 +68,6 @@ class _NEWNewMainNavBarState extends State { setState(() { selectedIndex = index; - _fadeSelected = true; - }); - - // delay fade (tweak duration) - Future.delayed(const Duration(milliseconds: 00), () { - if (!mounted) return; - if (index == selectedIndex) { - setState(() => _fadeSelected = true); - } }); NewMainActions.all[index].onTap.call(); @@ -196,7 +186,7 @@ class _NEWNewMainNavBarState extends State { child: AnimatedOpacity( duration: inactiveIconFadeDuration, curve: Curves.easeOutCubic, - opacity: (i == selectedIndex && _fadeSelected) + opacity: (i == selectedIndex) ? 0.0 : 1.0, child: AnimatedScale( From cd8d43af7bc852c0dc696ebbe6d075eb75a9ab97 Mon Sep 17 00:00:00 2001 From: tuxsudo Date: Mon, 17 Nov 2025 11:36:17 -0500 Subject: [PATCH 48/68] Modify navbar behaviour --- .../widgets/new_main_navbar_widget.dart | 44 ++++++++----------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart index 53bc21ab25..a6b760865c 100644 --- a/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart +++ b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart @@ -37,12 +37,12 @@ class _NEWNewMainNavBarState extends State { static const barHorizontalPadding = 12.0; - static const barResizeDuration = Duration(milliseconds: 100); - static const inactiveIconMoveDuration = Duration(milliseconds: 150); - static const inactiveIconFadeDuration = Duration(milliseconds: 100); - static const inactiveIconAppearDuration = Duration(milliseconds: 250); - static const pillMoveDuration = Duration(milliseconds: 150); - static const pillResizeDuration = Duration(milliseconds: 100); + static const barResizeDuration = Duration(milliseconds: 300); + static const inactiveIconMoveDuration = Duration(milliseconds: 300); + static const inactiveIconFadeDuration = Duration(milliseconds: 300); + static const inactiveIconAppearDuration = Duration(milliseconds: 300); + static const pillMoveDuration = Duration(milliseconds: 250); + static const pillResizeDuration = Duration(milliseconds: 250); static const pillTextStyle = TextStyle( fontSize: 16, @@ -50,6 +50,7 @@ class _NEWNewMainNavBarState extends State { ); int selectedIndex = 0; + bool _fadeSelected = false; bool _firstFrame = true; @override @@ -183,16 +184,14 @@ class _NEWNewMainNavBarState extends State { i == selectedIndex ? pillWidth : iconWidth, height: iconHeight, alignment: Alignment.center, - child: AnimatedOpacity( + child: AnimatedAlign( duration: inactiveIconFadeDuration, curve: Curves.easeOutCubic, - opacity: (i == selectedIndex) - ? 0.0 - : 1.0, + alignment: Alignment.centerLeft, child: AnimatedScale( duration: inactiveIconAppearDuration, curve: Curves.easeOutCubic, - scale: (i == selectedIndex) ? 0.95 : 1.0, + scale: (i == selectedIndex) ? 0.8 : 1.0, child: SvgPicture.asset( visibleActions[i].image, width: iconWidth, @@ -273,25 +272,18 @@ class AnimatedPill extends StatelessWidget { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - SvgPicture.asset( - currentAction.image, - width: pillIconWidth, - height: pillIconHeight, - colorFilter: ColorFilter.mode( - contentColor, - BlendMode.srcIn, - ), - ), SizedBox(width: pillIconSpacing), - Text( - currentAction.name(context), - style: pillTextStyle.copyWith(color: contentColor), - overflow: TextOverflow.fade, - softWrap: false, + Padding(padding: EdgeInsets.only(left: pillIconWidth), + child: Text( + currentAction.name(context), + style: pillTextStyle.copyWith(color: contentColor), + overflow: TextOverflow.fade, + softWrap: false, + ), ), ], ), ), )); } -} +} \ No newline at end of file From fd25cabad61d0c5c2db916162656b844ab10a74e Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Mon, 17 Nov 2025 18:44:40 +0100 Subject: [PATCH 49/68] smooth color change when selecting navbar options --- .../widgets/new_main_navbar_widget.dart | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart index a6b760865c..79447b5b3e 100644 --- a/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart +++ b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart @@ -43,6 +43,7 @@ class _NEWNewMainNavBarState extends State { static const inactiveIconAppearDuration = Duration(milliseconds: 300); static const pillMoveDuration = Duration(milliseconds: 250); static const pillResizeDuration = Duration(milliseconds: 250); + static const iconColorChangeDuration = Duration(milliseconds: 200); static const pillTextStyle = TextStyle( fontSize: 16, @@ -192,14 +193,23 @@ class _NEWNewMainNavBarState extends State { duration: inactiveIconAppearDuration, curve: Curves.easeOutCubic, scale: (i == selectedIndex) ? 0.8 : 1.0, - child: SvgPicture.asset( - visibleActions[i].image, - width: iconWidth, - height: iconHeight, - colorFilter: ColorFilter.mode( - inactiveColor, - BlendMode.srcIn, - ), + child: TweenAnimationBuilder( + tween: ColorTween( + begin: (i == selectedIndex) ? inactiveColor : activeColor, + end: (i==selectedIndex) ? activeColor : inactiveColor, + ), + duration: iconColorChangeDuration, + builder: (context, value, child) { + return SvgPicture.asset( + visibleActions[i].image, + width: iconWidth, + height: iconHeight, + colorFilter: ColorFilter.mode( + value ?? inactiveColor, + BlendMode.srcIn, + ), + ); + } ), ), ), From 37b08b61b28c933a4b2d55edcd9826166a2e0278 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Wed, 19 Nov 2025 13:26:52 +0100 Subject: [PATCH 50/68] open old ui pages with new ui action buttons --- lib/new-ui/pages/swap_page.dart | 10 +++++ .../action_row/coin_action_row.dart | 45 +++++++++++++++---- lib/utils/feature_flag.dart | 1 + 3 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 lib/new-ui/pages/swap_page.dart diff --git a/lib/new-ui/pages/swap_page.dart b/lib/new-ui/pages/swap_page.dart new file mode 100644 index 0000000000..075608983f --- /dev/null +++ b/lib/new-ui/pages/swap_page.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class SwapPage extends StatelessWidget { + const SwapPage({super.key}); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart index 5c4d4204d5..18ff9bb51a 100644 --- a/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart +++ b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart @@ -1,4 +1,9 @@ +import 'package:cake_wallet/entities/qr_scanner.dart'; +import 'package:cake_wallet/main.dart'; import 'package:cake_wallet/new-ui/pages/send_page.dart'; +import 'package:cake_wallet/new-ui/pages/swap_page.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; @@ -22,16 +27,18 @@ class CoinActionRow extends StatelessWidget { icon: SvgPicture.asset("assets/new-ui/send.svg"), label: "Send", action: () { + if(FeatureFlag.hasNewUiExtraPages) showModalBottomSheet( context: context, builder: (context) => SendPage(), - ); + ); else Navigator.of(context).pushNamed(Routes.send); }, ), CoinActionButton( icon: SvgPicture.asset("assets/new-ui/receive.svg"), label: "Receive", action: () { + if(FeatureFlag.hasNewUiExtraPages) showModalBottomSheet( context: context, isScrollControlled: true, @@ -39,23 +46,45 @@ class CoinActionRow extends StatelessWidget { heightFactor: 0.9, child: ReceivePage(), ), - ); + ); else Navigator.of(context).pushNamed(Routes.receive); }, ), CoinActionButton( icon: SvgPicture.asset("assets/new-ui/exchange.svg"), label: "Swap", - action: () {}, + action: () { + if(FeatureFlag.hasNewUiExtraPages) + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => FractionallySizedBox( + heightFactor: 0.9, + child: SwapPage(), + ), + ); else Navigator.of(context).pushNamed(Routes.exchange); + + }, ), CoinActionButton( icon: SvgPicture.asset("assets/new-ui/scan.svg"), label: "Scan", - action: () { - showModalBottomSheet( - context: context, + action: () async { + if(FeatureFlag.hasNewUiExtraPages) + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => FractionallySizedBox( + heightFactor: 0.9, + child: ScanPage(), + ), + ); else { + final code = await presentQRScanner(context); - builder: (context) => ScanPage(), - ); + if (code == null) return; + if (code.isEmpty) return; + final uri = Uri.parse(code); + rootKey.currentState?.handleDeepLinking(uri); + }; }, ), ], diff --git a/lib/utils/feature_flag.dart b/lib/utils/feature_flag.dart index 4591aeffdf..6b3e78fc0c 100644 --- a/lib/utils/feature_flag.dart +++ b/lib/utils/feature_flag.dart @@ -15,4 +15,5 @@ class FeatureFlag { static const bool hasBitcoinViewOnly = true; static const bool customBackgroundEnabled = false; static const bool hasNewUi = true; + static const bool hasNewUiExtraPages = false; } From ac1e0e68cb6d7c56f47688900e7debc1779c4d7c Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Wed, 19 Nov 2025 19:24:04 +0100 Subject: [PATCH 51/68] feat: working navbar in new ui --- lib/di.dart | 9 ++ lib/new-ui/new_dashboard.dart | 97 +++--------- lib/new-ui/pages/home_page.dart | 92 ++++++++++++ lib/src/screens/dashboard/dashboard_page.dart | 3 +- .../dashboard/pages/cake_features_page.dart | 142 +++++++++--------- .../widgets/new_main_navbar_widget.dart | 35 ++--- 6 files changed, 215 insertions(+), 163 deletions(-) create mode 100644 lib/new-ui/pages/home_page.dart diff --git a/lib/di.dart b/lib/di.dart index 5d970b06bb..7b056ea679 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -12,6 +12,7 @@ import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart'; import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; import 'package:cake_wallet/new-ui/new_dashboard.dart'; +import 'package:cake_wallet/new-ui/pages/home_page.dart'; import 'package:cake_wallet/order/order.dart'; import 'package:cake_wallet/core/backup_service_v3.dart'; import 'package:cake_wallet/core/new_wallet_arguments.dart'; @@ -34,6 +35,7 @@ import 'package:cake_wallet/entities/hardware_wallet/require_hardware_wallet_con import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; import 'package:cake_wallet/haven/cw_haven.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/cake_features_page.dart'; import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart'; import 'package:cake_wallet/src/screens/dev/moneroc_cache_debug.dart'; import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart'; @@ -753,6 +755,8 @@ Future setup({ dashboardViewModel: getIt.get(), )); + getIt.registerFactory(()=>NewHomePage(dashboardViewModel: getIt.get())); + getIt.registerFactory(() { final GlobalKey _navigatorKey = GlobalKey(); return DesktopSidebarWrapper( @@ -1340,6 +1344,11 @@ Future setup({ getIt.registerFactory(() => CakeFeaturesViewModel(getIt.get())); + + getIt.registerFactory(() => CakeFeaturesPage( + dashboardViewModel: getIt.get(), + cakeFeaturesViewModel: getIt.get())); + getIt.registerFactory(() => BackupServiceV3(getIt.get(), _transactionDescriptionBox, getIt.get(), getIt.get())); diff --git a/lib/new-ui/new_dashboard.dart b/lib/new-ui/new_dashboard.dart index ba8b88fd12..f1eaf15fb7 100644 --- a/lib/new-ui/new_dashboard.dart +++ b/lib/new-ui/new_dashboard.dart @@ -1,98 +1,45 @@ import 'package:cake_wallet/di.dart'; -import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/lightning_assets.dart'; +import 'package:cake_wallet/new-ui/pages/home_page.dart'; +import 'package:cake_wallet/src/screens/contact/contact_list_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/cake_features_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/new_main_navbar_widget.dart'; -import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart'; +import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart'; import 'package:flutter/material.dart'; import '../view_model/dashboard/dashboard_view_model.dart'; -import 'widgets/coins_page/cards/cards_view.dart'; -import 'widgets/coins_page/action_row/coin_action_row.dart'; -import 'widgets/coins_page/assets_history/history_section.dart'; -import 'widgets/coins_page/top_bar.dart'; -import 'widgets/coins_page/wallet_info.dart'; class NewDashboard extends StatefulWidget { - NewDashboard({super.key, required this.dashboardViewModel}) { - this.accountListViewModel = - dashboardViewModel.balanceViewModel.hasAccounts ? getIt.get() : null; - } + NewDashboard({super.key, required this.dashboardViewModel}); final DashboardViewModel dashboardViewModel; - late final MoneroAccountListViewModel? accountListViewModel; - @override State createState() => _NewDashboardState(); } class _NewDashboardState extends State { - bool _lightningMode = false; + int _selectedPage = 0; @override Widget build(BuildContext context) { return Scaffold( body: Stack( children: [ - SafeArea( - child: Container( - height: MediaQuery.of(context).size.height, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Theme.of(context).colorScheme.surfaceBright, - Theme.of(context).colorScheme.surface, - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: SingleChildScrollView( - physics: BouncingScrollPhysics(), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - TopBar( - dashboardViewModel: widget.dashboardViewModel, - lightningMode: _lightningMode, - onLightningSwitchPress: () { - setState(() { - _lightningMode = !_lightningMode; - }); - }, - ), - WalletInfo(lightningMode: _lightningMode, usesHardwareWallet: - widget.dashboardViewModel.wallet.isHardwareWallet, - name: widget.dashboardViewModel.wallet.name - ), - CardsView(dashboardViewModel: widget.dashboardViewModel, - accountListViewModel: widget.accountListViewModel, - lightningMode: _lightningMode, - ), - CoinActionRow(), - AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - transitionBuilder: (child, animation) { - return FadeTransition(opacity: animation, child: child); - }, - layoutBuilder: (currentChild, previousChildren) { - return Stack( - alignment: Alignment.topCenter, - children: [ - ...previousChildren, - if (currentChild != null) currentChild, - ], - ); - }, - child: _lightningMode - ? LightningAssets(dashboardViewModel: widget.dashboardViewModel,) - : HistorySection(dashboardViewModel: widget.dashboardViewModel,), - ), - ], - ), - ), - ), - ), - NewMainNavBar(dashboardViewModel: widget.dashboardViewModel) + [ + getIt.get(), + getIt.get(), + getIt.get(), + getIt.get(), + Placeholder(), + ][_selectedPage], + NewMainNavBar( + dashboardViewModel: widget.dashboardViewModel, + selectedIndex: _selectedPage, + onItemTap: (index) { + setState(() { + _selectedPage = index; + }); + }, + ) ], ), ); diff --git a/lib/new-ui/pages/home_page.dart b/lib/new-ui/pages/home_page.dart new file mode 100644 index 0000000000..21eda8140f --- /dev/null +++ b/lib/new-ui/pages/home_page.dart @@ -0,0 +1,92 @@ +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/action_row/coin_action_row.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_section.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/lightning_assets.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/cards/cards_view.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/wallet_info.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart'; +import 'package:flutter/material.dart'; + + +class NewHomePage extends StatefulWidget { + NewHomePage({super.key, required this.dashboardViewModel}) { + this.accountListViewModel = + dashboardViewModel.balanceViewModel.hasAccounts ? getIt.get() : null; + } + + final DashboardViewModel dashboardViewModel; + late final MoneroAccountListViewModel? accountListViewModel; + + @override + State createState() => _NewHomePageState(); +} + +class _NewHomePageState extends State { + bool _lightningMode = false; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Container( + height: MediaQuery.of(context).size.height, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Theme.of(context).colorScheme.surfaceBright, + Theme.of(context).colorScheme.surface, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: SingleChildScrollView( + physics: BouncingScrollPhysics(), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + TopBar( + dashboardViewModel: widget.dashboardViewModel, + lightningMode: _lightningMode, + onLightningSwitchPress: () { + setState(() { + _lightningMode = !_lightningMode; + }); + }, + ), + WalletInfo(lightningMode: _lightningMode, usesHardwareWallet: + widget.dashboardViewModel.wallet.isHardwareWallet, + name: widget.dashboardViewModel.wallet.name + ), + CardsView(dashboardViewModel: widget.dashboardViewModel, + accountListViewModel: widget.accountListViewModel, + lightningMode: _lightningMode, + ), + CoinActionRow(), + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + transitionBuilder: (child, animation) { + return FadeTransition(opacity: animation, child: child); + }, + layoutBuilder: (currentChild, previousChildren) { + return Stack( + alignment: Alignment.topCenter, + children: [ + ...previousChildren, + if (currentChild != null) currentChild, + ], + ); + }, + child: _lightningMode + ? LightningAssets(dashboardViewModel: widget.dashboardViewModel,) + : HistorySection(dashboardViewModel: widget.dashboardViewModel,), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index b007881cc4..b4cb67e4d4 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -31,7 +31,6 @@ import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:smooth_page_indicator/smooth_page_indicator.dart'; import 'package:cake_wallet/main.dart'; import 'package:cake_wallet/src/screens/release_notes/release_notes_screen.dart'; @@ -252,6 +251,8 @@ class _DashboardPageView extends BasePage { ), NewMainNavBar( dashboardViewModel: dashboardViewModel, + selectedIndex: 0, + onItemTap: (index) {} ) ], ), diff --git a/lib/src/screens/dashboard/pages/cake_features_page.dart b/lib/src/screens/dashboard/pages/cake_features_page.dart index bd381a9687..e03b6a8893 100644 --- a/lib/src/screens/dashboard/pages/cake_features_page.dart +++ b/lib/src/screens/dashboard/pages/cake_features_page.dart @@ -21,80 +21,82 @@ class CakeFeaturesPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.only(left: 24, top: 16), - child: Text( - S.of(context).apps, - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurface, - ), + return SafeArea( + child: Container( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(left: 24, top: 16), + child: Text( + S.of(context).apps, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface, + ), + ), ), - ), - Expanded( - child: ListView( - children: [ - SizedBox(height: 2), - DashBoardRoundedCardWidget( - shadowBlur: dashboardViewModel.getShadowBlur(), - shadowSpread: dashboardViewModel.getShadowSpread(), - onTap: () { - if (Platform.isMacOS) { - _launchUrl("buy.cakepay.com"); - } else { - _navigatorToGiftCardsPage(context); - } - }, - title: 'Cake Pay', - subTitle: S.of(context).cake_pay_subtitle, - image: Image.asset( - 'assets/images/cakepay.png', - height: 74, - width: 70, - fit: BoxFit.cover, + Expanded( + child: ListView( + children: [ + SizedBox(height: 2), + DashBoardRoundedCardWidget( + shadowBlur: dashboardViewModel.getShadowBlur(), + shadowSpread: dashboardViewModel.getShadowSpread(), + onTap: () { + if (Platform.isMacOS) { + _launchUrl("buy.cakepay.com"); + } else { + _navigatorToGiftCardsPage(context); + } + }, + title: 'Cake Pay', + subTitle: S.of(context).cake_pay_subtitle, + image: Image.asset( + 'assets/images/cakepay.png', + height: 74, + width: 70, + fit: BoxFit.cover, + ), ), - ), - Observer(builder: (_) { - if (dashboardViewModel.type == WalletType.ethereum) { - return DashBoardRoundedCardWidget( - shadowBlur: dashboardViewModel.getShadowBlur(), - shadowSpread: dashboardViewModel.getShadowSpread(), - onTap: () => Navigator.of(context).pushNamed(Routes.dEuroSavings), - title: S.of(context).deuro_savings, - subTitle: S.of(context).deuro_savings_subtitle, - image: Image.asset( - 'assets/images/deuro_icon.png', - height: 80, - width: 80, - fit: BoxFit.cover, - ), - ); - } - - return const SizedBox(); - }), - DashBoardRoundedCardWidget( - shadowBlur: dashboardViewModel.getShadowBlur(), - shadowSpread: dashboardViewModel.getShadowSpread(), - onTap: () => _launchUrl("cake.nano-gpt.com"), - title: "NanoGPT", - subTitle: S.of(context).nanogpt_subtitle, - image: Image.asset( - 'assets/images/nanogpt.png', - height: 80, - width: 80, - fit: BoxFit.cover, + Observer(builder: (_) { + if (dashboardViewModel.type == WalletType.ethereum) { + return DashBoardRoundedCardWidget( + shadowBlur: dashboardViewModel.getShadowBlur(), + shadowSpread: dashboardViewModel.getShadowSpread(), + onTap: () => Navigator.of(context).pushNamed(Routes.dEuroSavings), + title: S.of(context).deuro_savings, + subTitle: S.of(context).deuro_savings_subtitle, + image: Image.asset( + 'assets/images/deuro_icon.png', + height: 80, + width: 80, + fit: BoxFit.cover, + ), + ); + } + + return const SizedBox(); + }), + DashBoardRoundedCardWidget( + shadowBlur: dashboardViewModel.getShadowBlur(), + shadowSpread: dashboardViewModel.getShadowSpread(), + onTap: () => _launchUrl("cake.nano-gpt.com"), + title: "NanoGPT", + subTitle: S.of(context).nanogpt_subtitle, + image: Image.asset( + 'assets/images/nanogpt.png', + height: 80, + width: 80, + fit: BoxFit.cover, + ), ), - ), - SizedBox(height: 125), - ], + SizedBox(height: 125), + ], + ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart index 79447b5b3e..824149e8dc 100644 --- a/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart +++ b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart @@ -8,11 +8,13 @@ class NewMainNavBar extends StatefulWidget { const NewMainNavBar({ super.key, required this.dashboardViewModel, - this.initialIndex = 0, + required this.selectedIndex, + required this.onItemTap, }); final DashboardViewModel dashboardViewModel; - final int initialIndex; + final int selectedIndex; + final Function(int index) onItemTap; @override State createState() => _NEWNewMainNavBarState(); @@ -50,14 +52,11 @@ class _NEWNewMainNavBarState extends State { fontWeight: FontWeight.w500, ); - int selectedIndex = 0; - bool _fadeSelected = false; bool _firstFrame = true; @override void initState() { super.initState(); - selectedIndex = widget.initialIndex; WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; @@ -66,11 +65,13 @@ class _NEWNewMainNavBarState extends State { } void _onItemTap(int index) { - if (index == selectedIndex) return; + // if (index == widget.selectedIndex) return; + // + // setState(() { + // widget.selectedIndex = index; + // }); - setState(() { - selectedIndex = index; - }); + widget.onItemTap(index); NewMainActions.all[index].onTap.call(); } @@ -98,7 +99,7 @@ class _NEWNewMainNavBarState extends State { final double baseOffset = (iconWidth+iconHorizontalPadding) * index; double additionalSpacing; - if (index > selectedIndex) additionalSpacing = pillWidth-iconWidth; + if (index > widget.selectedIndex) additionalSpacing = pillWidth-iconWidth; else additionalSpacing = 0; return baseOffset + additionalSpacing; @@ -123,12 +124,12 @@ class _NEWNewMainNavBarState extends State { .toList(); final pillWidth = _estimatePillWidthForAction( - context, visibleActions[selectedIndex], + context, visibleActions[widget.selectedIndex], color: activeColor); final barWidth = calcBarWidth(pillWidth); - final currentAction = visibleActions[selectedIndex]; + final currentAction = visibleActions[widget.selectedIndex]; return Align( alignment: Alignment.bottomCenter, @@ -156,7 +157,7 @@ class _NEWNewMainNavBarState extends State { alignment: Alignment.center, children: [ AnimatedPill( - left: calcLeft(selectedIndex, pillWidth), + left: calcLeft(widget.selectedIndex, pillWidth), pillColor: pillColor, currentAction: currentAction, pillIconHeight: pillIconHeight, @@ -182,7 +183,7 @@ class _NEWNewMainNavBarState extends State { : inactiveIconMoveDuration, curve: Curves.easeOutCubic, width: - i == selectedIndex ? pillWidth : iconWidth, + i == widget.selectedIndex ? pillWidth : iconWidth, height: iconHeight, alignment: Alignment.center, child: AnimatedAlign( @@ -192,11 +193,11 @@ class _NEWNewMainNavBarState extends State { child: AnimatedScale( duration: inactiveIconAppearDuration, curve: Curves.easeOutCubic, - scale: (i == selectedIndex) ? 0.8 : 1.0, + scale: (i == widget.selectedIndex) ? 0.8 : 1.0, child: TweenAnimationBuilder( tween: ColorTween( - begin: (i == selectedIndex) ? inactiveColor : activeColor, - end: (i==selectedIndex) ? activeColor : inactiveColor, + begin: (i == widget.selectedIndex) ? inactiveColor : activeColor, + end: (i==widget.selectedIndex) ? activeColor : inactiveColor, ), duration: iconColorChangeDuration, builder: (context, value, child) { From b94a8676c122446cee53d8e457a4d0c10741da12 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Thu, 20 Nov 2025 16:23:02 +0100 Subject: [PATCH 52/68] fix: minor sizing tweaks --- lib/new-ui/pages/home_page.dart | 1 + .../action_row/coin_action_button.dart | 6 ++--- .../action_row/coin_action_row.dart | 2 +- lib/new-ui/widgets/coins_page/top_bar.dart | 2 +- .../widgets/coins_page/wallet_info.dart | 4 +-- .../widgets/new_main_navbar_widget.dart | 25 ++++++++++--------- 6 files changed, 21 insertions(+), 19 deletions(-) diff --git a/lib/new-ui/pages/home_page.dart b/lib/new-ui/pages/home_page.dart index 21eda8140f..94539cde10 100644 --- a/lib/new-ui/pages/home_page.dart +++ b/lib/new-ui/pages/home_page.dart @@ -46,6 +46,7 @@ class _NewHomePageState extends State { child: Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, + spacing: 24.0, children: [ TopBar( dashboardViewModel: widget.dashboardViewModel, diff --git a/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart b/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart index a3ae3f4431..80db0ce091 100644 --- a/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart +++ b/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart @@ -13,7 +13,7 @@ class CoinActionButton extends StatelessWidget { final String label; final VoidCallback action; - static const sizeFactor = 0.18; + static const sizeFactor = 0.16; @override Widget build(BuildContext context) { @@ -44,10 +44,10 @@ class CoinActionButton extends StatelessWidget { ), ), Padding( - padding: const EdgeInsets.all(10.0), + padding: const EdgeInsets.only(top:8.0), child: Text( style: TextStyle( - fontSize: 15, + fontSize: 12, color: Theme.of(context).colorScheme.onSurface, ), label, diff --git a/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart index 18ff9bb51a..acb5a4eb35 100644 --- a/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart +++ b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart @@ -17,7 +17,7 @@ class CoinActionRow extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.symmetric(vertical: 12.0), + padding: const EdgeInsets.symmetric(horizontal:18.0), child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/new-ui/widgets/coins_page/top_bar.dart b/lib/new-ui/widgets/coins_page/top_bar.dart index 5e970dd7e3..bff5b0987a 100644 --- a/lib/new-ui/widgets/coins_page/top_bar.dart +++ b/lib/new-ui/widgets/coins_page/top_bar.dart @@ -17,7 +17,7 @@ class TopBar extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.all(18.0), + padding: const EdgeInsets.only(left: 18.0, right: 18.0, top: 10.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/lib/new-ui/widgets/coins_page/wallet_info.dart b/lib/new-ui/widgets/coins_page/wallet_info.dart index 68adc8b130..633b6cefad 100644 --- a/lib/new-ui/widgets/coins_page/wallet_info.dart +++ b/lib/new-ui/widgets/coins_page/wallet_info.dart @@ -41,9 +41,9 @@ class WalletInfo extends StatelessWidget { ), ), ), - Text(name, style: TextStyle(fontSize: 20)), + Text(name, style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500)), SizedBox(width: 8), - ModernButton.svg(size: 20, onPressed: (){}, svgPath: "assets/new-ui/3dots.svg",) + ModernButton.svg(size: 24, onPressed: (){}, svgPath: "assets/new-ui/3dots.svg",) ], ); } diff --git a/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart index 824149e8dc..deecd97b08 100644 --- a/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart +++ b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart @@ -29,10 +29,10 @@ class _NEWNewMainNavBarState extends State { static const iconHeight = 28.0; static const iconHorizontalPadding = 12.0; - static const pillIconWidth = 20.0; - static const pillIconHeight = 20.0; + static const pillIconWidth = 24.0; + static const pillIconHeight = 24.0; static const pillIconSpacing = 4.0; - static const pillHorizontalPadding = 16.0; + static const pillHorizontalPadding = 20.0; static const barBorderRadius = 50.0; static const pillBorderRadius = 50.0; @@ -99,22 +99,22 @@ class _NEWNewMainNavBarState extends State { final double baseOffset = (iconWidth+iconHorizontalPadding) * index; double additionalSpacing; - if (index > widget.selectedIndex) additionalSpacing = pillWidth-iconWidth; + if (index > widget.selectedIndex) additionalSpacing = pillWidth-iconWidth-iconHorizontalPadding/2; else additionalSpacing = 0; return baseOffset + additionalSpacing; } double calcBarWidth(double pillWidth) { - return (iconWidth+iconHorizontalPadding)*NewMainActions.all.length+(pillWidth-iconWidth)+barHorizontalPadding; + return (iconWidth+iconHorizontalPadding)*(NewMainActions.all.length)+(pillWidth-(iconWidth))+barHorizontalPadding+pillIconSpacing/2; } @override Widget build(BuildContext context) { final theme = Theme.of(context); final backgroundColor = - theme.colorScheme.surfaceContainerHighest.withAlpha(85); - final pillColor = theme.colorScheme.onSurfaceVariant.withAlpha(85); + theme.colorScheme.surfaceContainer.withAlpha(127); + final pillColor = theme.colorScheme.onSurface.withAlpha(25); final activeColor = theme.colorScheme.onSurface; final inactiveColor = theme.colorScheme.primary; @@ -149,6 +149,7 @@ class _NEWNewMainNavBarState extends State { height: barHeight, decoration: BoxDecoration( color: backgroundColor, + border: Border.all(color: Color(0x14FFFFFF), width: 1), borderRadius: BorderRadius.circular(barBorderRadius), ), child: Padding( @@ -173,7 +174,7 @@ class _NEWNewMainNavBarState extends State { for (int i = 0; i < visibleActions.length; i++) AnimatedPositioned( duration: pillResizeDuration, - left: calcLeft(i, pillWidth), + left: calcLeft(i, pillWidth)+((i == widget.selectedIndex) ? iconHorizontalPadding/2 : 0), curve: Curves.easeOutCubic, child: GestureDetector( onTap: () => _onItemTap(i), @@ -193,7 +194,7 @@ class _NEWNewMainNavBarState extends State { child: AnimatedScale( duration: inactiveIconAppearDuration, curve: Curves.easeOutCubic, - scale: (i == widget.selectedIndex) ? 0.8 : 1.0, + scale: (i == widget.selectedIndex) ? 0.857 : 1.0, child: TweenAnimationBuilder( tween: ColorTween( begin: (i == widget.selectedIndex) ? inactiveColor : activeColor, @@ -265,8 +266,8 @@ class AnimatedPill extends StatelessWidget { duration: pillMoveDuration, curve: Curves.easeOutCubic, left: left, - top: 12, - bottom: 12, + top: 8, + bottom: 8, child: AnimatedContainer( duration: pillResizeDuration, curve: Curves.easeOutCubic, @@ -283,7 +284,7 @@ class AnimatedPill extends StatelessWidget { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - SizedBox(width: pillIconSpacing), + // SizedBox(width: pillIconSpacing*10), Padding(padding: EdgeInsets.only(left: pillIconWidth), child: Text( currentAction.name(context), From e033f06d4a799a3e023cc62676b9c719d0eee15c Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Thu, 20 Nov 2025 21:47:25 +0100 Subject: [PATCH 53/68] CW-1266-integrate-bitcoin-lightning-through-spark-sdk (#2623) * feat: add Lightning Network support for Bitcoin wallets * refactor: rename `fiatConvertationStore` to `fiatConversionStore` for consistency and update related occurrences across codebase * feat: enhance address validation with Lightning Network invoice support for BTC & refactor wallet type/token checks in view model * feat: add support for Lightning invoice detection, refactor MWEB deposit/withdraw actions, and integrate Lightning transaction creation with updated priority handling * feat: add method to retrieve unused Spark deposit address for Bitcoin wallets * feat: add Breez API key support and update secrets handling for Bitcoin Lightning wallet integration in workflows * chore: update Breez SDK dependency to version 0.3.4 in pubspec files * Add bitcoin secrets config [skip ci] * feat: extend Lightning wallet functionality with transaction history fetching * feat: add LNURL-pay address detection and support in address parsing flow for Bitcoin Lightning integration * refactor: simplify `ReceivePageOption` logic * refactor: centralize `PaymentURI` generation logic across wallet types * feat: enhance `PaymentURI` handling with asynchronous support and Lightning-specific functionality * refactor: streamline `PaymentURI` logic and remove redundant URI implementations across wallet types * refactor: remove redundant debug print statement from `bitcoin_wallet_addresses.dart` * refactor: improve consistency in widget styling and centralized label logic, add Bitcoin Lightning deposit/withdraw support * feat: reload balance and tx history after sending a lightning transaction * feat: improve address formatting for human-readable addresses and update the default LNURL domain * fix: merge conflicts * feat: add error handling for LightningWallet initialization and adjust transaction direction logic * feat: enable private transactions by default in LightningWallet and update Breez SDK version to 0.4.2 * minor fixes [skip ci] * chore: fix some minor issues in comments (#2654) Signed-off-by: black5box * fix: handle send-all functionality for LightningWallet transactions and adjust amount calculation logic * fix-german (#2659) * chore: update German localization strings for consistency and accuracy * chore: update German localization strings for consistency and accuracy [skip-ci] * feat: add LNURL support for address validation and LightningWallet compatibility, enhance error handling for OpenCryptoPay * fix: adjust LightningWallet amount parsing from 9 to 8 decimal places * feat: add LNURL support in LightningPaymentRequest and LightningWallet * Fix navigation gradient (#2657) * Fix navigation gradient * Fix CONFIG_ARGS formatting in app_config.sh (#2660) * fix: block wrongly parsed addresses (#2656) * fix: block wrongly parsed addresses * fix: move parsed address check into handlePaymentFlow method * fix: Handle QR URLs separately for pay anything flow * feat: add electrum seed support for Lightning * refactor: improve `parseFixed` logic and add comprehensive unit tests for edge cases (#2661) * Update cw_bitcoin/lib/bitcoin_wallet.dart [skip ci] * resolve conflict issue --------- Signed-off-by: black5box Co-authored-by: Omar Hatem Co-authored-by: black5box Co-authored-by: tuxsudo Co-authored-by: cyan Co-authored-by: David Adegoke <64401859+Blazebrain@users.noreply.github.com> --- README.md | 2 +- cw_bitcoin/lib/bitcoin_wallet.dart | 33 ++++++---- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 11 +++- .../lib/lightning/lightning_addres_type.dart | 1 + .../lib/lightning/lightning_wallet.dart | 43 +++++++++--- cw_bitcoin/lib/lightning/utils.dart | 9 +++ cw_bitcoin/pubspec.lock | 8 +-- cw_bitcoin/pubspec.yaml | 5 +- .../lib}/lnurl.dart | 5 ++ cw_core/lib/parse_fixed.dart | 10 ++- cw_core/lib/payment_uris.dart | 17 +++-- cw_core/pubspec.lock | 9 +++ cw_core/pubspec.yaml | 3 + cw_core/test/lnurl_test.dart | 18 +++++ cw_core/test/parse_fixed_test.dart | 38 +++++++++++ cw_decred/pubspec.lock | 9 +++ cw_monero/pubspec.lock | 9 +++ cw_nano/pubspec.lock | 9 +++ cw_wownero/pubspec.lock | 9 +++ cw_zano/pubspec.lock | 9 +++ lib/core/address_validator.dart | 5 +- .../open_cryptopay_service.dart | 2 +- lib/entities/lnurlpay_record.dart | 11 ++-- .../dashboard/pages/navigation_dock.dart | 26 -------- lib/src/screens/send/widgets/send_card.dart | 4 +- .../settings/background_sync_page.dart | 4 +- lib/utils/tor.dart | 3 +- .../payment/payment_view_model.dart | 2 +- lib/view_model/send/send_view_model.dart | 25 +++---- res/values/strings_de.arb | 66 +++++++++---------- res/values/strings_en.arb | 4 +- scripts/ios/app_config.sh | 2 +- 32 files changed, 275 insertions(+), 136 deletions(-) create mode 100644 cw_bitcoin/lib/lightning/utils.dart rename {lib/core/open_crypto_pay => cw_core/lib}/lnurl.dart (94%) create mode 100644 cw_core/test/lnurl_test.dart create mode 100644 cw_core/test/parse_fixed_test.dart diff --git a/README.md b/README.md index 43a72e0ad7..da155cb846 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Cake Wallet includes support for several cryptocurrencies, including: ### Ethereum Specific Features -* Store ETH and all ERc-20 tokens +* Store ETH and all ERC-20 tokens * Add custom tokens by contract address * Enable or disable Etherscan for transaction history diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index b1f7861f1e..a3e601b175 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -85,9 +85,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialBalance: initialBalance, seedBytes: seedBytes, encryptionFileUtils: encryptionFileUtils, - currency: networkParam == BitcoinNetwork.testnet - ? CryptoCurrency.tbtc - : CryptoCurrency.btc, + currency: + networkParam == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc, alwaysScan: alwaysScan, ) { // in a standard BIP44 wallet, mainHd derivation path = m/84'/0'/0'/0 (account 0, index unspecified here) @@ -100,11 +99,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { try { lightningWallet = LightningWallet( mnemonic: mnemonic, + passphrase: passphrase, + seedBytes: seedBytes, apiKey: secrets.breezApiKey, lnurlDomain: "cake.cash", ); } catch (e) { printV(e); + lightningWallet = null; } } else { lightningWallet = null; @@ -241,10 +243,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final derivationInfo = await walletInfo.getDerivationInfo(); // set the default if not present: - derivationInfo.derivationPath ??= - snp?.derivationPath ?? electrum_path; - derivationInfo.derivationType ??= - snp?.derivationType ?? DerivationType.electrum; + derivationInfo.derivationPath ??= snp?.derivationPath ?? electrum_path; + derivationInfo.derivationType ??= snp?.derivationType ?? DerivationType.electrum; await derivationInfo.save(); Uint8List? seedBytes = null; @@ -405,11 +405,17 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final isLNCompatible = await lightningWallet?.isCompatible(credentials.outputs.first.address); if ((credentials.coinTypeToSpendFrom == UnspentCoinType.lightning && lightningWallet != null) || isLNCompatible == true) { - final amount = parseFixed( - credentials.outputs.first.cryptoAmount?.isNotEmpty == true - ? credentials.outputs.first.cryptoAmount! - : "0", - 9); + + BigInt amount; + if (credentials.outputs.first.sendAll) { + amount = (await lightningWallet!.getBalance()) - BigInt.from(10); + } else { + amount = parseFixed( + credentials.outputs.first.cryptoAmount?.isNotEmpty == true + ? credentials.outputs.first.cryptoAmount! + : "0", + 8); + } return lightningWallet!.createTransaction(credentials.outputs.first.address, amount > BigInt.zero ? amount : null, credentials.priority); @@ -549,8 +555,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final isChange = addressEntry?.isHidden == true ? 1 : 0; final derivationInfo = await walletInfo.getDerivationInfo(); final accountPath = derivationInfo.derivationPath; - final derivationPath = - accountPath != null ? "$accountPath/$isChange/$index" : null; + final derivationPath = accountPath != null ? "$accountPath/$isChange/$index" : null; final signature = await hardwareWalletService! .signMessage(message: ascii.encode(message), derivationPath: derivationPath); diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index c14ab11849..c00ccac296 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -14,6 +14,8 @@ import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; import 'package:payjoin_flutter/receive.dart' as payjoin; +import 'lightning/utils.dart'; + part 'bitcoin_wallet_addresses.g.dart'; class BitcoinWalletAddresses = BitcoinWalletAddressesBase with _$BitcoinWalletAddresses; @@ -115,9 +117,14 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S Future getPaymentRequestUri(String amount) async { if (addressPageType is LightningAddressType && lightningWallet != null) { - final amountSats = amount.isNotEmpty ? parseFixed(amount, 9) : null; + final amountSats = amount.isNotEmpty ? parseFixed(amount, 8) : null; + final lnUrl = getLnurlOfLightningAddress(address); + if (amountSats == null) { + return LightningPaymentRequest(address: address, lnURL: lnUrl, amount: amount); + } final invoice = await lightningWallet!.getBolt11Invoice(amountSats, "Send to Cake Wallet"); - return LightningPaymentRequest(address: address, amount: amount, bolt11Invoice: invoice); + return LightningPaymentRequest( + address: address, lnURL: lnUrl, amount: amount, bolt11Invoice: invoice); } return getPaymentUri(amount); } diff --git a/cw_bitcoin/lib/lightning/lightning_addres_type.dart b/cw_bitcoin/lib/lightning/lightning_addres_type.dart index f0b13fca18..733d9338bc 100644 --- a/cw_bitcoin/lib/lightning/lightning_addres_type.dart +++ b/cw_bitcoin/lib/lightning/lightning_addres_type.dart @@ -6,6 +6,7 @@ class LightningAddressType implements BitcoinAddressType { static const String Bolt11InvoiceMatcher = r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$'; static const String Bolt12OfferMatcher = r'^(lightning:)?(lno1)[a-z0-9]+$'; + static const String LNURLMatcher = r'^(lightning:)?(lnurl)[a-z0-9]+$'; @override bool get isP2sh => false; diff --git a/cw_bitcoin/lib/lightning/lightning_wallet.dart b/cw_bitcoin/lib/lightning/lightning_wallet.dart index 315d7406dd..8225a265b1 100644 --- a/cw_bitcoin/lib/lightning/lightning_wallet.dart +++ b/cw_bitcoin/lib/lightning/lightning_wallet.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:typed_data'; import 'package:breez_sdk_spark_flutter/breez_sdk_spark.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; @@ -12,6 +13,8 @@ bool _breezSdkSparkLibUninitialized = true; class LightningWallet { final String mnemonic; + final String? passphrase; + final Uint8List? seedBytes; final String apiKey; final String lnurlDomain; final Network network; @@ -19,6 +22,8 @@ class LightningWallet { LightningWallet({ required this.mnemonic, + this.passphrase, + this.seedBytes, required this.apiKey, required this.lnurlDomain, this.network = Network.mainnet, @@ -30,10 +35,13 @@ class LightningWallet { _breezSdkSparkLibUninitialized = false; } - final seed = Seed.mnemonic(mnemonic: mnemonic, passphrase: null); + final seed = seedBytes != null + ? Seed.entropy(seedBytes!) + : Seed.mnemonic(mnemonic: mnemonic, passphrase: passphrase); final config = defaultConfig(network: Network.mainnet).copyWith( lnurlDomain: lnurlDomain, apiKey: apiKey, + privateEnabledDefault: true, ); final connectRequest = ConnectRequest( @@ -47,6 +55,8 @@ class LightningWallet { Future getAddress() async => (await sdk.getLightningAddress())?.lightningAddress; + Future getLNURL() async => (await sdk.getLightningAddress())?.lnurl; + Future getDepositAddress() async => (await sdk.receivePayment( request: ReceivePaymentRequest(paymentMethod: ReceivePaymentMethod.bitcoinAddress()))) .paymentRequest; @@ -76,7 +86,9 @@ class LightningWallet { Future isCompatible(String input) async { try { final inputType = await sdk.parse(input: input); - return (inputType is InputType_Bolt11Invoice) || (inputType is InputType_LightningAddress); + return (inputType is InputType_Bolt11Invoice) || + (inputType is InputType_LightningAddress) || + (inputType is InputType_LnurlPay); } catch (_) { return false; } @@ -107,14 +119,24 @@ class LightningWallet { }, ); } - } else if (inputType is InputType_LightningAddress) { + } else if (inputType is InputType_LightningAddress || inputType is InputType_LnurlPay) { final optionalValidateSuccessActionUrl = true; - final request = PrepareLnurlPayRequest( - amountSats: amountSats!, - payRequest: inputType.field0.payRequest, - validateSuccessActionUrl: optionalValidateSuccessActionUrl, - ); + PrepareLnurlPayRequest request; + if (inputType is InputType_LightningAddress) { + request = PrepareLnurlPayRequest( + amountSats: amountSats!, + payRequest: inputType.field0.payRequest, + validateSuccessActionUrl: optionalValidateSuccessActionUrl, + ); + } else { + request = PrepareLnurlPayRequest( + amountSats: amountSats!, + payRequest: (inputType as InputType_LnurlPay).field0, + validateSuccessActionUrl: optionalValidateSuccessActionUrl, + ); + } + final prepareResponse = await sdk.prepareLnurlPay(request: request); final feeSats = prepareResponse.feeSats; @@ -124,7 +146,8 @@ class LightningWallet { amount: ((prepareResponse.invoiceDetails.amountMsat?.toInt() ?? 0) / 1000).round(), fee: feeSats.toInt(), commitOverride: () async { - final res = await sdk.lnurlPay(request: LnurlPayRequest(prepareResponse: prepareResponse)); + final res = + await sdk.lnurlPay(request: LnurlPayRequest(prepareResponse: prepareResponse)); printV(res.payment.status.name); }, ); @@ -221,6 +244,7 @@ extension _ConfigCopyWith on Config { Fee? maxDepositClaimFee, bool? preferSparkOverLightning, bool? useDefaultExternalInputParsers, + bool? privateEnabledDefault, }) => Config( lnurlDomain: lnurlDomain ?? this.lnurlDomain, @@ -231,5 +255,6 @@ extension _ConfigCopyWith on Config { preferSparkOverLightning: preferSparkOverLightning ?? this.preferSparkOverLightning, useDefaultExternalInputParsers: useDefaultExternalInputParsers ?? this.useDefaultExternalInputParsers, + privateEnabledDefault: privateEnabledDefault ?? this.privateEnabledDefault, ); } diff --git a/cw_bitcoin/lib/lightning/utils.dart b/cw_bitcoin/lib/lightning/utils.dart new file mode 100644 index 0000000000..f5267e5b0a --- /dev/null +++ b/cw_bitcoin/lib/lightning/utils.dart @@ -0,0 +1,9 @@ +import 'package:cw_core/lnurl.dart'; + +String getLnurlOfLightningAddress(String lightningAddress) { + final parts = lightningAddress.split("@"); + + final name = parts.first; + final domain = parts.last; + return encodeLNURL("https://$domain/.well-known/lnurlp/$name"); +} diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 4bbd0c3dea..81f928606a 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -56,7 +56,7 @@ packages: source: git version: "1.0.0" bech32: - dependency: "direct main" + dependency: transitive description: path: "." ref: HEAD @@ -134,11 +134,11 @@ packages: dependency: "direct main" description: path: "." - ref: bca05bc9085f778e95916d55e9a75133c27755a2 - resolved-ref: bca05bc9085f778e95916d55e9a75133c27755a2 + ref: "9baaad9bdcef32bd0572a9bacaa67c7923c4e0a5" + resolved-ref: "9baaad9bdcef32bd0572a9bacaa67c7923c4e0a5" url: "https://github.com/breez/breez-sdk-spark-flutter" source: git - version: "0.3.5-rc1" + version: "0.4.2" bs58check: dependency: transitive description: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index ca6f75f0fa..48f954b1fc 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -37,9 +37,6 @@ dependencies: git: url: https://github.com/cake-tech/sp_scanner ref: sp_v4.0.1 - bech32: - git: - url: https://github.com/cake-tech/bech32.git payjoin_flutter: git: url: https://github.com/konstantinullrich/payjoin-flutter @@ -75,7 +72,7 @@ dependencies: breez_sdk_spark_flutter: git: url: https://github.com/breez/breez-sdk-spark-flutter - ref: bca05bc9085f778e95916d55e9a75133c27755a2 + ref: 9baaad9bdcef32bd0572a9bacaa67c7923c4e0a5 dev_dependencies: flutter_test: diff --git a/lib/core/open_crypto_pay/lnurl.dart b/cw_core/lib/lnurl.dart similarity index 94% rename from lib/core/open_crypto_pay/lnurl.dart rename to cw_core/lib/lnurl.dart index 0087bab512..ba77478264 100644 --- a/lib/core/open_crypto_pay/lnurl.dart +++ b/cw_core/lib/lnurl.dart @@ -2,6 +2,11 @@ import 'dart:convert'; import 'package:bech32/bech32.dart'; +String encodeLNURL(String url) { + final raw = _convert(utf8.encode(url), 8, 5, true); + return const Bech32Codec().encode(Bech32('lnurl', raw), 255); +} + Uri decodeLNURL(String encodedUrl) { Uri decodedUri; diff --git a/cw_core/lib/parse_fixed.dart b/cw_core/lib/parse_fixed.dart index 2b8a7ec17b..321320ef2c 100644 --- a/cw_core/lib/parse_fixed.dart +++ b/cw_core/lib/parse_fixed.dart @@ -1,14 +1,13 @@ -BigInt parseFixed(String value, int? decimals) { - decimals ??= 0; +BigInt parseFixed(String value, int decimals) { final multiplier = getMultiplier(decimals); -// Is it negative? - final negative = (value.substring(0, 1) == "-"); + final negative = value.startsWith("-"); if (negative) value = value.substring(1); if (value == ".") throw Exception("missing value, value, $value"); -// Split it into a whole and fractional part + if (value.startsWith(".")) value = "0$value"; + final comps = value.split("."); if (comps.length > 2) { throw Exception("too many decimal points, value, $value"); @@ -17,7 +16,6 @@ BigInt parseFixed(String value, int? decimals) { var whole = comps.isNotEmpty ? comps[0] : "0"; var fraction = (comps.length == 2 ? comps[1] : "0").padRight(decimals, "0"); - // Check the fraction doesn't exceed our decimals size if (fraction.length > multiplier.length - 1) { throw Exception( "fractional component exceeds decimals, underflow, parseFixed"); diff --git a/cw_core/lib/payment_uris.dart b/cw_core/lib/payment_uris.dart index 6a6a60b362..ced3d9fe6c 100644 --- a/cw_core/lib/payment_uris.dart +++ b/cw_core/lib/payment_uris.dart @@ -54,16 +54,19 @@ class BitcoinURI extends PaymentURI { } class LightningPaymentRequest extends PaymentURI { - const LightningPaymentRequest( - {required super.address, - required super.amount, - required this.bolt11Invoice, - super.scheme = "lightning"}); + const LightningPaymentRequest({ + required super.address, + required super.amount, + required this.lnURL, + this.bolt11Invoice, + super.scheme = "lightning", + }); - final String bolt11Invoice; + final String lnURL; + final String? bolt11Invoice; @override - String toString() => bolt11Invoice; + String toString() => bolt11Invoice ?? lnURL; } class LitecoinURI extends PaymentURI { diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index 918a81a0b5..bb70a374db 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -46,6 +46,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + bech32: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "05755063b593aa6cca0a4820a318e0ce17de6192" + url: "https://github.com/cake-tech/bech32.git" + source: git + version: "0.2.2" blockchain_utils: dependency: "direct main" description: diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml index 49188f5177..c2b4970db5 100644 --- a/cw_core/pubspec.yaml +++ b/cw_core/pubspec.yaml @@ -45,6 +45,9 @@ dependencies: ref: cake-update-v2 sqflite: ^2.4.1 sqflite_common_ffi: ^2.3.4+4 + bech32: + git: + url: https://github.com/cake-tech/bech32.git dev_dependencies: flutter_test: diff --git a/cw_core/test/lnurl_test.dart b/cw_core/test/lnurl_test.dart new file mode 100644 index 0000000000..6b05506247 --- /dev/null +++ b/cw_core/test/lnurl_test.dart @@ -0,0 +1,18 @@ +import 'package:cw_core/lnurl.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('lnurl', () { + test('decode lnurl', () { + final content = decodeLNURL( + "lnurl1dp68gurn8ghj7cmpddjjucmpwd5z7tnhv4kxctttdehhwm30d3h82unvwqhkkmmwwd6xj9vpzq4"); + expect(content, Uri.parse("https://cake.cash/.well-known/lnurlp/konsti")); + }); + + test('encode lnurl', () { + final content = encodeLNURL("https://cake.cash/.well-known/lnurlp/konsti"); + expect(content, + "lnurl1dp68gurn8ghj7cmpddjjucmpwd5z7tnhv4kxctttdehhwm30d3h82unvwqhkkmmwwd6xj9vpzq4"); + }); + }); +} diff --git a/cw_core/test/parse_fixed_test.dart b/cw_core/test/parse_fixed_test.dart new file mode 100644 index 0000000000..5ce5201665 --- /dev/null +++ b/cw_core/test/parse_fixed_test.dart @@ -0,0 +1,38 @@ +import 'package:cw_core/parse_fixed.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('parseFixed', () { + group('parseFixed, positive', () { + test('should parse 1.000001 as 1000001', + () => expect(parseFixed("1.000001", 6), BigInt.from(1000001))); + + test('should parse 1 as 1000000', () => expect(parseFixed("1", 6), BigInt.from(1000000))); + + test('should parse 1. as 1000000', () => expect(parseFixed("1.", 6), BigInt.from(1000000))); + + test('should parse 1.1 as 1100000', () => expect(parseFixed("1.1", 6), BigInt.from(1100000))); + + test('should parse 01.1 as 1100000', + () => expect(parseFixed("01.1", 6), BigInt.from(1100000))); + + test('should parse 1100000 as 11000000', + () => expect(parseFixed("1100000", 1), BigInt.from(11000000))); + }); + + group('parseFixed, negative', () { + test('should parse -1.000001 as -1000001', + () => expect(parseFixed("-1.000001", 6), BigInt.from(-1000001))); + + test('should parse -1 as 1000000', () => expect(parseFixed("-1", 6), BigInt.from(-1000000))); + }); + + group('parseFixed, no leading 0', () { + test('should parse .000001 as 1', () => expect(parseFixed(".000001", 6), BigInt.from(1))); + + test('should parse .00002 as 20', () => expect(parseFixed(".00002", 6), BigInt.from(20))); + + test('should parse -.00002 as -20', () => expect(parseFixed("-.00002", 6), BigInt.from(-20))); + }); + }); +} diff --git a/cw_decred/pubspec.lock b/cw_decred/pubspec.lock index 8558572154..811ebbac61 100644 --- a/cw_decred/pubspec.lock +++ b/cw_decred/pubspec.lock @@ -46,6 +46,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + bech32: + dependency: transitive + description: + path: "." + ref: HEAD + resolved-ref: "05755063b593aa6cca0a4820a318e0ce17de6192" + url: "https://github.com/cake-tech/bech32.git" + source: git + version: "0.2.2" blockchain_utils: dependency: transitive description: diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index ae521ab8dd..5d00f8bac1 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -46,6 +46,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + bech32: + dependency: transitive + description: + path: "." + ref: HEAD + resolved-ref: "05755063b593aa6cca0a4820a318e0ce17de6192" + url: "https://github.com/cake-tech/bech32.git" + source: git + version: "0.2.2" bip32: dependency: "direct main" description: diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock index c009b65024..034656704f 100644 --- a/cw_nano/pubspec.lock +++ b/cw_nano/pubspec.lock @@ -46,6 +46,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + bech32: + dependency: transitive + description: + path: "." + ref: HEAD + resolved-ref: "05755063b593aa6cca0a4820a318e0ce17de6192" + url: "https://github.com/cake-tech/bech32.git" + source: git + version: "0.2.2" bip32: dependency: "direct main" description: diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index 2e1b3fb093..a49a5f8c46 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -46,6 +46,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + bech32: + dependency: transitive + description: + path: "." + ref: HEAD + resolved-ref: "05755063b593aa6cca0a4820a318e0ce17de6192" + url: "https://github.com/cake-tech/bech32.git" + source: git + version: "0.2.2" blockchain_utils: dependency: transitive description: diff --git a/cw_zano/pubspec.lock b/cw_zano/pubspec.lock index 0c4a57658d..ac19ce2245 100644 --- a/cw_zano/pubspec.lock +++ b/cw_zano/pubspec.lock @@ -46,6 +46,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + bech32: + dependency: transitive + description: + path: "." + ref: HEAD + resolved-ref: "05755063b593aa6cca0a4820a318e0ce17de6192" + url: "https://github.com/cake-tech/bech32.git" + source: git + version: "0.2.2" blockchain_utils: dependency: transitive description: diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index c700835341..36e9d373f7 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -16,10 +16,9 @@ class AddressValidator extends TextValidator { useAdditionalValidation: [CryptoCurrency.btc, CryptoCurrency.ltc].contains(type) ? (String txt) { final RegExp lightningInvoiceRegex = RegExp( - r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$', + r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt|lnurl)[a-z0-9]+$', caseSensitive: false); if (lightningInvoiceRegex.hasMatch(txt)) return true; - if (txt.contains("@")) return true; return BitcoinAddressUtils.validateAddress( address: txt, @@ -62,7 +61,7 @@ class AddressValidator extends TextValidator { '|(bc1q[ac-hj-np-z02-9]{25,39})' '|(bc1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))' '|(bc1q[ac-hj-np-z02-9]{40,80})' - '|(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]' + '|(lightning:)?(lnbc|lntb|lnbs|lnbcrt|lnurl)[a-z0-9]+' '|(${silentPaymentAddressPatternMainnet})(\$|\s)'; } case CryptoCurrency.ltc: diff --git a/lib/core/open_crypto_pay/open_cryptopay_service.dart b/lib/core/open_crypto_pay/open_cryptopay_service.dart index cc94740f03..391f6ec5a0 100644 --- a/lib/core/open_crypto_pay/open_cryptopay_service.dart +++ b/lib/core/open_crypto_pay/open_cryptopay_service.dart @@ -2,9 +2,9 @@ import 'dart:convert'; import 'dart:developer'; import 'package:cake_wallet/core/open_crypto_pay/exceptions.dart'; -import 'package:cake_wallet/core/open_crypto_pay/lnurl.dart'; import 'package:cake_wallet/core/open_crypto_pay/models.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/lnurl.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; class OpenCryptoPayService { diff --git a/lib/entities/lnurlpay_record.dart b/lib/entities/lnurlpay_record.dart index 3fbb01bc3a..f62477d1ff 100644 --- a/lib/entities/lnurlpay_record.dart +++ b/lib/entities/lnurlpay_record.dart @@ -1,6 +1,5 @@ -import 'dart:convert'; - import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/lnurl.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; @@ -31,14 +30,14 @@ class LNUrlPayRecord { name = "_"; } - // lookup domain/.well-known/nano-currency.json and check if it has a nano address: + final expectedUrl = "https://$domain/.well-known/lnurlp/$name"; final response = await ProxyWrapper().get( - clearnetUri: Uri.parse("https://$domain/.well-known/lnurlp/$name"), + clearnetUri: Uri.parse(expectedUrl), headers: {"Accept": "application/json"}, ); if (response.statusCode == 200) { - return username; + return encodeLNURL(expectedUrl); } } catch (e) { printV("error checking well-known username: $e"); @@ -60,7 +59,7 @@ class LNUrlPayRecord { required String formattedName, required CryptoCurrency currency, }) async { - String name = formattedName; + final name = formattedName; printV("formattedName: $formattedName"); diff --git a/lib/src/screens/dashboard/pages/navigation_dock.dart b/lib/src/screens/dashboard/pages/navigation_dock.dart index c4ee6b09bf..12fd55c224 100644 --- a/lib/src/screens/dashboard/pages/navigation_dock.dart +++ b/lib/src/screens/dashboard/pages/navigation_dock.dart @@ -21,23 +21,9 @@ class NavigationDock extends StatelessWidget { return Container( height: 84, alignment: Alignment.bottomCenter, - decoration: dashboardViewModel.settingsStore.backgroundImage.isEmpty - ? BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: _getColors(context), - ), - ) - : null, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(50), - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: _getColors(context), - ), ), margin: const EdgeInsets.only(left: 8, right: 8, bottom: 16), child: ClipRRect( @@ -105,16 +91,4 @@ class NavigationDock extends StatelessWidget { ), ); } - - List _getColors(BuildContext context) { - return [ - context.customColors.backgroundGradientColor.withAlpha(5), - context.customColors.backgroundGradientColor.withAlpha(50), - context.customColors.backgroundGradientColor.withAlpha(125), - context.customColors.backgroundGradientColor.withAlpha(150), - context.customColors.backgroundGradientColor.withAlpha(200), - context.customColors.backgroundGradientColor, - context.customColors.backgroundGradientColor, - ]; - } } diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 20e3b2ceb3..2187b29216 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -149,6 +149,8 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin _handlePaymentFlow(String uri, PaymentRequest paymentRequest) async { + if (uri.contains('@') || paymentRequest.address.contains('@')) return; + try { final result = await paymentViewModel.processAddress(uri); @@ -446,8 +448,6 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin ensureTorStopped({required BuildContext? context}) async { - if (!didTorStart) { + if (!didTorStart || CakeTor.instance is CakeTorDisabled) { printV("Tor hasn't been initialized yet, so it can't be stopped."); return; } diff --git a/lib/view_model/payment/payment_view_model.dart b/lib/view_model/payment/payment_view_model.dart index 9e8bf3136c..1095a90117 100644 --- a/lib/view_model/payment/payment_view_model.dart +++ b/lib/view_model/payment/payment_view_model.dart @@ -45,7 +45,7 @@ abstract class PaymentViewModelBase with Store { return PaymentFlowResult.incompatible('Unable to detect address type'); } - if (_isEVMAddress(detectionResult.address)) { + if (!addressData.contains(':') && _isEVMAddress(detectionResult.address)) { return PaymentFlowResult.evmNetworkSelection(detectionResult); } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index aa7fe20fc5..124a415a68 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/amount_validator.dart'; import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/core/open_crypto_pay/exceptions.dart'; import 'package:cake_wallet/core/open_crypto_pay/models.dart'; import 'package:cake_wallet/core/open_crypto_pay/open_cryptopay_service.dart'; import 'package:cake_wallet/core/validator.dart'; @@ -74,9 +75,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor currencies = wallet.balance.keys.toList(); selectedCryptoCurrency = wallet.currency; hasMultipleTokens = isEVMCompatibleChain(wallet.type) || - wallet.type == WalletType.solana || - wallet.type == WalletType.tron || - wallet.type == WalletType.zano; + [WalletType.solana, WalletType.tron, WalletType.zano].contains(wallet.type); for (final output in outputs) { output.updateWallet(wallet); @@ -106,9 +105,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor currencies = appStore.wallet!.balance.keys.toList(), selectedCryptoCurrency = appStore.wallet!.currency, hasMultipleTokens = isEVMCompatibleChain(appStore.wallet!.type) || - appStore.wallet!.type == WalletType.solana || - appStore.wallet!.type == WalletType.tron || - appStore.wallet!.type == WalletType.zano, + [WalletType.solana, WalletType.tron, WalletType.zano].contains(appStore.wallet!.type), outputs = ObservableList(), _settingsStore = appStore.settingsStore, fiatFromSettings = appStore.settingsStore.fiatCurrency, @@ -467,11 +464,18 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor outputs.first.note = ocpRequest!.receiverName; return createTransaction(); + } on OpenCryptoPayNotSupportedException catch (e) { + printV(e.message); + if (walletType == WalletType.bitcoin) { + state = InitialExecutionState(); + } else { + state = FailureState(translateErrorMessage(e, walletType, currency)); + } } catch (e) { printV(e); state = FailureState(translateErrorMessage(e, walletType, currency)); - return null; } + return null; } bool isLightningInvoice(String txt) { @@ -867,12 +871,12 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor final priority = _settingsStore.priority[wallet.type]; if (priority == null && - [ + ![ WalletType.nano, WalletType.banano, WalletType.solana, WalletType.tron, - WalletType.arbitrum + WalletType.arbitrum, ].contains(wallet.type)) { throw Exception('Priority is null for wallet type: ${wallet.type}'); } @@ -1051,8 +1055,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor WalletType.ethereum, WalletType.polygon, WalletType.base, - WalletType.haven, - WalletType.arbitrum + WalletType.arbitrum, ].contains(walletType)) { if (errorMessage.contains('gas required exceeds allowance')) { return S.current.gas_exceeds_allowance; diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 98e3a75f2f..ecca6a03c7 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -11,13 +11,13 @@ "add": "Hinzufügen", "add_contact": "Kontakt hinzufügen", "add_contact_to_address_book": "Möchten Sie diesen Kontakt zu Ihrem Adressbuch hinzufügen?", - "add_custom_node": "Neuen benutzerdefinierten Knoten hinzufügen", + "add_custom_node": "Neuen benutzerdefinierten Nodes hinzufügen", "add_custom_redemption": "Benutzerdefinierte Einlösung hinzufügen", "add_fund_to_card": "Prepaid-Guthaben zu den Karten hinzufügen (bis zu ${value})", - "add_new_node": "Neuen Knoten hinzufügen", + "add_new_node": "Neuen Nodes hinzufügen", "add_new_word": "Neues Wort hinzufügen", "add_passphrase": "Fügen Sie Passphrase hinzu", - "add_passphrase_warning_text": "Geben Sie nur eine Passphrase ein, wenn Sie in der Vergangenheit eine für diese Wallet verwendet haben. Wenn Sie die falsche Passphrase eingeben oder in dieser Wallet noch keine Passphrase verwendet haben, werden Sie keine vorhandenen Gelder oder Geschichte sehen.", + "add_passphrase_warning_text": "Geben Sie nur eine Passphrase ein, wenn Sie in der Vergangenheit eine für diese Wallet verwendet haben. Wenn Sie die falsche Passphrase eingeben oder in dieser Wallet noch keine Passphrase verwendet haben, werden Sie keine vorhandenen Gelder oder Verlauf sehen.", "add_receiver": "Fügen Sie einen weiteren Empfänger hinzu (optional)", "add_secret_code": "Oder fügen Sie diesen Geheimcode einer Authentifizierungs-App hinzu", "add_tip": "Tipp hinzufügen", @@ -61,7 +61,7 @@ "approve_request": "Anfrage genehmigen", "approve_tokens": "Token genehmigen", "apps": "Apps", - "arbiscan_history": "ArbiScan-Geschichte", + "arbiscan_history": "ArbiScan-Verlauf", "arrive_in_this_address": "${currency} ${tag} wird an dieser Adresse ankommen", "ascending": "Aufsteigend", "ask_each_time": "Jedes Mal fragen", @@ -143,8 +143,8 @@ "change": "Ändern", "change_backup_password_alert": "Ihre vorherigen Sicherungsdateien können nicht mit einem neuen Sicherungskennwort importiert werden. Das neue Sicherungskennwort wird nur für neue Sicherungsdateien verwendet. Sind Sie sicher, dass Sie das Sicherungskennwort ändern möchten?", "change_currency": "Währung ändern", - "change_current_node": "Möchten Sie den aktuellen Knoten wirklich zu ${node}? ändern?", - "change_current_node_title": "Aktuellen Knoten ändern", + "change_current_node": "Möchten Sie den aktuellen Node wirklich zu ${node}? ändern?", + "change_current_node_title": "Aktuellen Node ändern", "change_exchange_provider": "Swap-Anbieter ändern", "change_language": "Sprache ändern", "change_language_to": "Sprache zu ${language} ändern?", @@ -187,7 +187,7 @@ "confirm_fee_deduction_content": "Stimmen Sie zu, die Gebühr von der Ausgabe abzuziehen?", "confirm_passphrase": "Passphrase bestätigen", "confirm_sending": "Senden bestätigen", - "confirm_silent_payments_switch_node": "Ihr aktueller Knoten unterstützt keine Silent Payments.\\n\\nCake Wallet wechselt zu einem kompatiblen Knoten, nur zum Scannen", + "confirm_silent_payments_switch_node": "Ihr aktueller Node unterstützt keine Silent Payments.\\n\\nCake Wallet wechselt zu einem kompatiblen Node, nur zum Scannen", "confirm_transaction": "Transaktion bestätigen", "confirmations": "Bestätigungen", "confirmed": "Bestätigter Saldo", @@ -315,12 +315,12 @@ "e_sign_consent": "E-Sign-Zustimmung", "edit": "Bearbeiten", "edit_backup_password": "Sicherungskennwort bearbeiten", - "edit_node": "Knoten bearbeiten", + "edit_node": "Node bearbeiten", "edit_token": "Token bearbeiten", "electrum_address_disclaimer": "Wir generieren jedes Mal neue Adressen, wenn Sie eine verwenden, aber vorherige Adressen funktionieren weiterhin", "email_address": "E-Mail-Adresse", "enable": "Aktivieren", - "enable_auto_node_switching": "Aktivieren Sie die automatische Knotenschaltung", + "enable_auto_node_switching": "Aktivieren Sie die automatische Nodewechsel", "enable_builtin_tor": "Aktivieren Sie den bau-in tor", "enable_for_auto_switching": "Aktivieren Sie die automatische Schaltung", "enable_mempool_api": "Mempool-API für genaue Gebühren und Daten", @@ -353,7 +353,7 @@ "error_text_maximum_limit": "Handel für ${provider} wird nicht erstellt. Menge ist über dem Maximum: ${max} ${currency}", "error_text_minimal_limit": "Handel für ${provider} wird nicht erstellt. Menge ist unter dem Minimum: ${min} ${currency}", "error_text_node_address": "Bitte geben Sie eine iPv4-Adresse ein", - "error_text_node_port": "Der Knotenport darf nur Nummern zwischen 0 und 65535 enthalten", + "error_text_node_port": "Der Port darf nur Nummern zwischen 0 und 65535 enthalten", "error_text_node_proxy_address": "Bitte geben Sie : ein, zum Beispiel 127.0.0.1:9050", "error_text_payment_id": "Die Zahlungs-ID darf nur 16 bis 64 hexadezimale Zeichen enthalten", "error_text_subaddress_name": "Der Name der Unteradresse darf nicht die Zeichen ` , ' \" enthalten\nund muss zwischen 1 und 20 Zeichen lang sein", @@ -443,8 +443,8 @@ "hide": "Verstecken", "hide_details": "Details ausblenden", "high_contrast_theme": "Kontrastreiches Thema", - "history": "Geschichte", - "home": "Heim", + "history": "Verlauf", + "home": "Home", "home_screen_settings": "Einstellungen für den Startbildschirm", "how_to_connect": "Anleitung", "how_to_use": "Wie benutzt man", @@ -490,7 +490,7 @@ "litecoin_mweb_enable": "Aktivieren Sie MWeb", "litecoin_mweb_enable_later": "Sie können MWEB unter Anzeigeeinstellungen erneut aktivieren.", "litecoin_mweb_logs": "MWEB-Protokolle", - "litecoin_mweb_node": "MWEB-Knoten", + "litecoin_mweb_node": "MWEB-Node", "litecoin_mweb_pegin": "Peg in", "litecoin_mweb_pegout": "Peg out", "litecoin_mweb_scanning": "MWEB Scanning", @@ -505,8 +505,8 @@ "low_fee": "Niedrige Gebühr", "low_fee_alert": "Sie verwenden derzeit eine niedrige Netzwerkgebührenpriorität. Dies kann zu langen Wartezeiten, unterschiedlichen Kursen oder stornierten Trades führen. Wir empfehlen, für ein besseres Erlebnis eine höhere Gebühr festzulegen.", "made_easy": "leicht gemacht", - "manage_nodes": "Knoten verwalten", - "manage_pow_nodes": "PoW-Knoten verwalten", + "manage_nodes": "Node verwalten", + "manage_pow_nodes": "PoW-Node verwalten", "manage_yats": "Yats verwalten", "mark_as_redeemed": "Als eingelöst markieren", "market_place": "Marktplatz", @@ -542,7 +542,7 @@ "nanogpt_subtitle": "Alle neuesten Modelle (GPT-4, Claude).", "narrow": "Eng", "new_first_wallet_text": "Wenn Sie Ihren Krypto schützen, ist ein Kinderspiel", - "new_node_testing": "Neuen Knoten testen", + "new_node_testing": "Neuen Node testen", "new_subaddress_create": "Erstellen", "new_subaddress_label_name": "Bezeichnung", "new_subaddress_title": "Neue Adresse", @@ -559,14 +559,14 @@ "no_relay_on_domain": "Es gibt kein Relay für die Domäne des Benutzers oder das Relay ist nicht verfügbar. Bitte wählen Sie ein zu verwendendes Relais aus.", "no_relays": "Keine Relais", "no_relays_message": "Wir haben einen Nostr NIP-05-Eintrag für diesen Benutzer gefunden, der jedoch keine Relays enthält. Bitte weisen Sie den Empfänger an, Relays zu seinem Nostr-Datensatz hinzuzufügen.", - "node_address": "Knotenadresse", + "node_address": "Nodeadresse", "node_connection_failed": "Verbindung fehlgeschlagen", "node_connection_successful": "Die Verbindung war erfolgreich", - "node_new": "Neuer Knoten", - "node_port": "Knotenport", + "node_new": "Neuer node", + "node_port": "Port", "node_reset_settings_title": "Einstellungen zurücksetzen", "node_test": "Test", - "nodes": "Knoten", + "nodes": "Nodes", "nodes_list_reset_to_default_message": "Möchten Sie wirklich die Standardeinstellungen wiederherstellen?", "none_of_selected_providers_can_exchange": "Keiner der ausgewählten Anbieter kann diesen Tausch machen", "noNFTYet": "Noch keine NFTs", @@ -579,7 +579,7 @@ "offline": "offline", "ok": "OK", "old_fee": "Alte Gebühr", - "oled_mode": "OLED -Modus", + "oled_mode": "OLED-Modus", "onion_link": "Onion-Link", "online": "online", "onramper_option_description": "Kaufen Sie schnell Krypto mit vielen Zahlungsmethoden. In den meisten Ländern erhältlich. Spreads und Gebühren variieren.", @@ -589,7 +589,7 @@ "optional_email_hint": "Optionale Benachrichtigungs-E-Mail für den Zahlungsempfänger", "optional_name": "Optionaler Empfängername", "optionally_order_card": "Optional eine physische Karte bestellen.", - "orbot_running_alert": "Bitte stellen Sie sicher, dass Orbot läuft, bevor Sie sich mit diesem Knoten verbinden.", + "orbot_running_alert": "Bitte stellen Sie sicher, dass Orbot läuft, bevor Sie sich mit diesem Node verbinden.", "order_by": "Sortieren nach", "order_id": "Bestell-ID", "order_physical_card": "Physische Karte bestellen", @@ -634,7 +634,7 @@ "Please_reference_document": "Weitere Informationen finden Sie in den Dokumenten unten.", "please_select": "Bitte auswählen:", "please_select_backup_file": "Bitte wählen Sie die Sicherungsdatei und geben Sie das Sicherungskennwort ein.", - "please_try_to_connect_to_another_node": "Bitte versuchen Sie, sich mit einem anderen Knoten zu verbinden", + "please_try_to_connect_to_another_node": "Bitte versuchen Sie, sich mit einem anderen Node zu verbinden", "please_wait": "Warten Sie mal", "polygonscan_history": "PolygonScan-Verlauf", "potential_scam": "Potenzieller Betrug", @@ -683,8 +683,8 @@ "reject": "Ablehnen", "remaining": "Rest", "remove": "Entfernen", - "remove_node": "Knoten entfernen", - "remove_node_message": "Möchten Sie den ausgewählten Knoten wirklich entfernen?", + "remove_node": "Node entfernen", + "remove_node_message": "Möchten Sie den ausgewählten Node wirklich entfernen?", "rename": "Umbenennen", "rep_warning": "Repräsentative Warnung", "rep_warning_sub": "Ihr Vertreter scheint nicht gut zu sein. Tippen Sie hier, um eine neue auszuwählen", @@ -790,11 +790,11 @@ "seed_share": "Seed teilen", "seed_title": "Seed", "seed_verified": "Seed verifiziert", - "seed_verified_subtext": "Sie können Ihre Wallet mit dem gespeicherten Seed wiederherstelle, selbst wenn sie ihr Gerät verloren haben.", + "seed_verified_subtext": "Sie können Ihre Wallet mit dem gespeicherten Seed wiederherstellen, selbst wenn sie ihr Gerät verloren haben.", "seedtype": "Seedtyp", "seedtype_alert_content": "Das Teilen von Seeds mit anderen Wallet ist nur mit bip39 Seedype möglich.", "seedtype_alert_title": "Seedype-Alarm", - "select_a_wallet": "Wählen Sie eine Brieftasche", + "select_a_wallet": "Wählen Sie eine Wallet", "select_backup_file": "Sicherungsdatei auswählen", "select_buy_provider_notice": "Wählen Sie oben einen Anbieter kaufen. Sie können diese Seite überspringen, indem Sie Ihren Standard-Kaufanbieter in den App-Einstellungen festlegen.", "select_destination": "Bitte wählen Sie das Ziel für die Sicherungsdatei aus.", @@ -844,12 +844,12 @@ "settings_change_language": "Sprache ändern", "settings_change_pin": "PIN ändern", "settings_currency": "Währung", - "settings_current_node": "Aktueller Knoten", + "settings_current_node": "Aktueller Node", "settings_dark_mode": "Dunkler Modus", "settings_display_balance": "Kontostand anzeigen", "settings_display_on_dashboard_list": "Anzeige in der Dashboard-Liste", "settings_fee_priority": "Gebührenpriorität", - "settings_nodes": "Knoten", + "settings_nodes": "Nodes", "settings_none": "Keiner", "settings_only_trades": "Nur Handel", "settings_only_transactions": "Nur Transaktionen", @@ -883,9 +883,9 @@ "sign_message": "Nachricht unterschreiben", "sign_one": "Unterschreiben", "sign_up": "Anmelden", - "sign_verify_message": "Zeichen / überprüfen", + "sign_verify_message": "Unterschreiben / überprüfen", "sign_verify_message_sub": "Unterschreiben oder überprüfen Sie eine Nachricht mit Ihrem privaten Schlüssel", - "sign_verify_title": "Zeichen / überprüfen", + "sign_verify_title": "Unterschreiben / überprüfen", "signature": "Signatur", "signature_invalid_error": "Die Signatur gilt nicht für die angegebene Nachricht", "signTransaction": "Transaktion unterzeichnen", @@ -931,7 +931,7 @@ "swap": "Tauschen", "sweeping_wallet": "Wallet leeren", "sweeping_wallet_alert": "Das sollte nicht lange dauern. VERLASSEN SIE DIESEN BILDSCHIRM NICHT, ANDERNFALLS KÖNNEN DIE GELDER VERLOREN GEHEN", - "switch_wallet": "Brieftasche schalten", + "switch_wallet": "Wallet wechseln", "switchToETHWallet": "Bitte wechseln Sie zu einem Ethereum-Wallet und versuchen Sie es erneut", "switchToEVMCompatibleWallet": "Bitte wechseln Sie zu einem EVM-kompatiblen Wallet und versuchen Sie es erneut (Ethereum, Polygon)", "symbol": "Symbol", @@ -950,7 +950,7 @@ "sync_status_syncronized": "SYNCHRONISIERT", "sync_status_syncronizing": "SYNCHRONISIERE", "sync_status_timed_out": "Zeitlich abgestimmt", - "sync_status_unsupported": "Nicht unterstützter Knoten", + "sync_status_unsupported": "Nicht unterstützter Node", "synchronizing": "Synchronisierung", "syncing_wallet_alert_content": "Ihr Kontostand und Ihre Transaktionsliste sind möglicherweise erst vollständig, wenn oben „SYNCHRONISIERT“ steht. Klicken/tippen Sie, um mehr zu erfahren.", "syncing_wallet_alert_title": "Ihr Wallet wird synchronisiert", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index a0eea1dcc3..5bfdc8be48 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -360,9 +360,9 @@ "error_text_template": "Template name and address can't contain ` , ' \" symbols\nand must be between 1 and 106 characters long", "error_text_wallet_name": "Wallet name can only contain letters, numbers, _ - symbols \nand must be between 1 and 33 characters long", "error_text_xmr": "XMR value can't exceed available balance.\nThe number of fraction digits must be less or equal to 12", - "error_while_processing": "An error occurred while proceessing", + "error_while_processing": "An error occurred while processing", "errorGettingCredentials": "Failed: Error while getting credentials", - "errorSigningTransaction": "An error has occured while signing transaction", + "errorSigningTransaction": "An error has occurred while signing transaction", "establishing_tor_connection": "Establishing Tor connection", "estimated": "Estimated", "estimated_new_fee": "Estimated new fee", diff --git a/scripts/ios/app_config.sh b/scripts/ios/app_config.sh index 9c2f99fdb6..d22b5ab3b6 100755 --- a/scripts/ios/app_config.sh +++ b/scripts/ios/app_config.sh @@ -31,7 +31,7 @@ case $APP_IOS_TYPE in ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred --dogecoin --base"# --arbitrum + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred --dogecoin --base" # --arbitrum ;; esac From bdfbbc4e19c6b741329bc67dd60d1856d7c6e5e2 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Thu, 20 Nov 2025 23:29:10 +0100 Subject: [PATCH 54/68] remove old code [skip ci] --- lib/src/screens/send/send_page.dart | 26 ------------------------ lib/view_model/send/send_view_model.dart | 2 -- 2 files changed, 28 deletions(-) diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index d6547b3cfe..6e220fc845 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -21,7 +21,6 @@ import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.da import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; -import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/simple_checkbox.dart'; @@ -374,19 +373,6 @@ class SendPage extends BasePage { bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), bottomSection: Column( children: [ - if (sendViewModel.hasCurrencyChanger) - Observer( - builder: (_) => Padding( - padding: EdgeInsets.only(bottom: 12), - child: PrimaryButton( - key: ValueKey('send_page_change_asset_button_key'), - onPressed: () => presentCurrencyPicker(context), - text: 'Change your asset (${sendViewModel.selectedCryptoCurrency})', - color: Colors.transparent, - textColor: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ), if (sendViewModel.sendTemplateViewModel.hasMultiRecipient) Padding( padding: EdgeInsets.only(bottom: 12), @@ -791,18 +777,6 @@ class SendPage extends BasePage { ), ); - void presentCurrencyPicker(BuildContext context) => showPopUp( - builder: (_) => Picker( - items: sendViewModel.currencies, - displayItem: (item) => item.toString(), - selectedAtIndex: sendViewModel.currencies.indexOf(sendViewModel.selectedCryptoCurrency), - title: S.of(context).please_select, - mainAxisAlignment: MainAxisAlignment.center, - onItemSelected: (cur) => sendViewModel.selectedCryptoCurrency = cur, - ), - context: context, - ); - bool isRegularElectrumAddress(String address) { final supportedTypes = [CryptoCurrency.btc, CryptoCurrency.ltc, CryptoCurrency.bch]; final excludedPatterns = [ diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 124a415a68..756f9ffcf7 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -353,8 +353,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor String? get walletCurrencyName => wallet.currency.fullName?.toLowerCase() ?? wallet.currency.name; - bool get hasCurrencyChanger => walletType == WalletType.haven; - @computed FiatCurrency get fiatCurrency => _settingsStore.fiatCurrency; From 102ca90400987fb41e1c885ec9bd4c9087a7d1a3 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Fri, 21 Nov 2025 09:41:50 +0100 Subject: [PATCH 55/68] fix: page widgets rebuilt every time --- lib/new-ui/new_dashboard.dart | 16 ++++++++------ .../assets_history/lightning_assets.dart | 22 ++++++++++++++----- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/lib/new-ui/new_dashboard.dart b/lib/new-ui/new_dashboard.dart index f1eaf15fb7..7bb008fb7e 100644 --- a/lib/new-ui/new_dashboard.dart +++ b/lib/new-ui/new_dashboard.dart @@ -12,6 +12,14 @@ class NewDashboard extends StatefulWidget { final DashboardViewModel dashboardViewModel; + final List dashboardPageWidgets = [ + getIt.get(), + getIt.get(), + getIt.get(), + getIt.get(), + Placeholder(), + ]; + @override State createState() => _NewDashboardState(); } @@ -24,13 +32,7 @@ class _NewDashboardState extends State { return Scaffold( body: Stack( children: [ - [ - getIt.get(), - getIt.get(), - getIt.get(), - getIt.get(), - Placeholder(), - ][_selectedPage], + widget.dashboardPageWidgets[_selectedPage], NewMainNavBar( dashboardViewModel: widget.dashboardViewModel, selectedIndex: _selectedPage, diff --git a/lib/new-ui/widgets/coins_page/assets_history/lightning_assets.dart b/lib/new-ui/widgets/coins_page/assets_history/lightning_assets.dart index 3100d4331f..dbbc479d47 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/lightning_assets.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/lightning_assets.dart @@ -5,9 +5,8 @@ import 'assets_section.dart'; import 'history_section.dart'; class LightningAssets extends StatefulWidget { - const LightningAssets({super.key, required this.dashboardViewModel}); + LightningAssets({super.key, required this.dashboardViewModel}); - static const List tabs = ["Assets", "History"]; final DashboardViewModel dashboardViewModel; @override @@ -15,8 +14,22 @@ class LightningAssets extends StatefulWidget { } class _LightningAssetsState extends State { + late final List lightningTabs; int _selectedTab = 0; + @override + void initState() { + super.initState(); + lightningTabs = [ + AssetsSection( + dashboardViewModel: widget.dashboardViewModel, + ), + HistorySection( + dashboardViewModel: widget.dashboardViewModel, + ), + ]; + } + @override Widget build(BuildContext context) { return Column( @@ -29,10 +42,7 @@ class _LightningAssetsState extends State { }, selectedTab: _selectedTab, ), - [ - AssetsSection(dashboardViewModel: widget.dashboardViewModel,), - HistorySection(dashboardViewModel: widget.dashboardViewModel,), - ][_selectedTab], + lightningTabs[_selectedTab], ], ); } From b1c977398745386a1312b234b762b6d101669db9 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Fri, 21 Nov 2025 09:43:03 +0100 Subject: [PATCH 56/68] chore: formatting --- .../action_row/coin_action_row.dart | 53 ++-- .../assets_history/assets_top_bar.dart | 10 +- .../widgets/coins_page/cards/cards_view.dart | 86 +++--- lib/router.dart | 248 ++++++------------ 4 files changed, 170 insertions(+), 227 deletions(-) diff --git a/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart index acb5a4eb35..9a52178ed3 100644 --- a/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart +++ b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart @@ -17,43 +17,49 @@ class CoinActionRow extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.symmetric(horizontal:18.0), + padding: const EdgeInsets.symmetric(horizontal: 18.0), child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, - spacing: MediaQuery.of(context).size.width*0.05, + spacing: MediaQuery.of(context).size.width * 0.05, children: [ CoinActionButton( icon: SvgPicture.asset("assets/new-ui/send.svg"), label: "Send", action: () { - if(FeatureFlag.hasNewUiExtraPages) - showModalBottomSheet( - context: context, - builder: (context) => SendPage(), - ); else Navigator.of(context).pushNamed(Routes.send); + if (FeatureFlag.hasNewUiExtraPages) { + showModalBottomSheet( + context: context, + builder: (context) => SendPage(), + ); + } else { + Navigator.of(context).pushNamed(Routes.send); + } }, ), CoinActionButton( icon: SvgPicture.asset("assets/new-ui/receive.svg"), label: "Receive", action: () { - if(FeatureFlag.hasNewUiExtraPages) - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (context) => FractionallySizedBox( - heightFactor: 0.9, - child: ReceivePage(), - ), - ); else Navigator.of(context).pushNamed(Routes.receive); + if (FeatureFlag.hasNewUiExtraPages) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => FractionallySizedBox( + heightFactor: 0.9, + child: ReceivePage(), + ), + ); + } else { + Navigator.of(context).pushNamed(Routes.receive); + } }, ), CoinActionButton( icon: SvgPicture.asset("assets/new-ui/exchange.svg"), label: "Swap", action: () { - if(FeatureFlag.hasNewUiExtraPages) + if (FeatureFlag.hasNewUiExtraPages) { showModalBottomSheet( context: context, isScrollControlled: true, @@ -61,15 +67,17 @@ class CoinActionRow extends StatelessWidget { heightFactor: 0.9, child: SwapPage(), ), - ); else Navigator.of(context).pushNamed(Routes.exchange); - + ); + } else { + Navigator.of(context).pushNamed(Routes.exchange); + } }, ), CoinActionButton( icon: SvgPicture.asset("assets/new-ui/scan.svg"), label: "Scan", action: () async { - if(FeatureFlag.hasNewUiExtraPages) + if (FeatureFlag.hasNewUiExtraPages) { showModalBottomSheet( context: context, isScrollControlled: true, @@ -77,12 +85,13 @@ class CoinActionRow extends StatelessWidget { heightFactor: 0.9, child: ScanPage(), ), - ); else { + ); + } else { final code = await presentQRScanner(context); if (code == null) return; if (code.isEmpty) return; - final uri = Uri.parse(code); + final uri = Uri.tryParse(code); rootKey.currentState?.handleDeepLinking(uri); }; }, diff --git a/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart b/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart index 2a282ef846..bde605d667 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart @@ -49,12 +49,18 @@ class AssetsTopBar extends StatelessWidget { padding: const EdgeInsets.all(12.0), child: Row( spacing: 4.0, - children: [Icon(Icons.settings, color: Theme.of(context).colorScheme.primary), Text("Tokens", style: TextStyle(color: Theme.of(context).colorScheme.primary),)], + children: [ + Icon(Icons.settings, color: Theme.of(context).colorScheme.primary), + Text( + "Tokens", + style: TextStyle(color: Theme.of(context).colorScheme.primary), + ) + ], ), ), ), ), - ModernButton(size: 48, onPressed:(){}, icon: Icon(Icons.question_mark)), + ModernButton(size: 48, onPressed: () {}, icon: Icon(Icons.question_mark)), ], ), ], diff --git a/lib/new-ui/widgets/coins_page/cards/cards_view.dart b/lib/new-ui/widgets/coins_page/cards/cards_view.dart index b4b517e5c4..22d0bce9a8 100644 --- a/lib/new-ui/widgets/coins_page/cards/cards_view.dart +++ b/lib/new-ui/widgets/coins_page/cards/cards_view.dart @@ -8,13 +8,16 @@ import 'package:flutter_mobx/flutter_mobx.dart'; import 'balance_card.dart'; class CardsView extends StatefulWidget { - const CardsView({super.key, required this.dashboardViewModel, required this.accountListViewModel, required this.lightningMode}); + const CardsView( + {super.key, + required this.dashboardViewModel, + required this.accountListViewModel, + required this.lightningMode}); final DashboardViewModel dashboardViewModel; final MoneroAccountListViewModel? accountListViewModel; final bool lightningMode; - @override _CardsViewState createState() => _CardsViewState(); } @@ -58,20 +61,22 @@ class _CardsViewState extends State { child: GestureDetector( onTap: () { setState(() { - if(widget.accountListViewModel != null) - widget.accountListViewModel!.select(widget.accountListViewModel!.accounts[index]); + if (widget.accountListViewModel != null) + widget.accountListViewModel!.select(widget.accountListViewModel!.accounts[index]); _selectedIndex = index; }); }, - child: Observer( - builder: (_){return BalanceCard( + child: Observer(builder: (_) { + return BalanceCard( width: cardWidth, - accountName: (widget.accountListViewModel?.accounts[index].label) ?? "Primary account", + accountName: + (widget.accountListViewModel?.accounts[index].label) ?? "Primary account", accountBalance: widget.accountListViewModel?.accounts[index].balance ?? "", - balanceRecord: widget.dashboardViewModel.balanceViewModel.formattedBalances.elementAt(0), + balanceRecord: + widget.dashboardViewModel.balanceViewModel.formattedBalances.elementAt(0), selected: _selectedIndex == index, - );} - ), + ); + }), ), ), ); @@ -79,10 +84,10 @@ class _CardsViewState extends State { double _getBoxHeight() { return - /* height of initial card */ - (2 / 3) * (cardWidth) + - /* height of bg card * amount of bg cards */ - overlapAmount * ((widget.accountListViewModel?.accounts.length ??1) - 1); + /* height of initial card */ + (2 / 3) * (cardWidth) + + /* height of bg card * amount of bg cards */ + overlapAmount * ((widget.accountListViewModel?.accounts.length ?? 1) - 1); } @override @@ -96,13 +101,12 @@ class _CardsViewState extends State { _selectedIndex = 0; } - for ( - int i = _selectedIndex!; - i < (widget.accountListViewModel?.accounts.length ?? 1) + _selectedIndex!; - i++ - ) { + for (int i = _selectedIndex!; + i < (widget.accountListViewModel?.accounts.length ?? 1) + _selectedIndex!; + i++) { if (i != _selectedIndex) { - children.add(_buildCard(i % (widget.accountListViewModel?.accounts.length ?? 1), parentWidth)); + children.add( + _buildCard(i % (widget.accountListViewModel?.accounts.length ?? 1), parentWidth)); } } @@ -110,28 +114,26 @@ class _CardsViewState extends State { children.add(_buildCard(_selectedIndex!, parentWidth)); } - return Observer( - builder: (_){return Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: AnimatedContainer( - duration: Duration(milliseconds: 200), - curve: Curves.easeOut, - width: double.infinity, - height: _getBoxHeight(), - child: AnimatedSwitcher( - duration: Duration(milliseconds: 200), - transitionBuilder: (child, animation) => - FadeTransition(opacity: animation, child: child), - child: SizedBox( - key: ValueKey(_getBoxHeight()), - width: double.infinity, - height: _getBoxHeight(), - child: Stack(alignment: Alignment.center, children: children), - ), - ), - ), - );} - ); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: AnimatedContainer( + duration: Duration(milliseconds: 200), + curve: Curves.easeOut, + width: double.infinity, + height: _getBoxHeight(), + child: AnimatedSwitcher( + duration: Duration(milliseconds: 200), + transitionBuilder: (child, animation) => + FadeTransition(opacity: animation, child: child), + child: SizedBox( + key: ValueKey(_getBoxHeight()), + width: double.infinity, + height: _getBoxHeight(), + child: Stack(alignment: Alignment.center, children: children), + ), + ), + ), + ); }, ); } diff --git a/lib/router.dart b/lib/router.dart index ee0a77c20b..4897460db5 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -166,11 +166,9 @@ Route handleRouteWithPlatformAwareness( bool fullscreenDialog = false, }) { if (Platform.isIOS) { - return CupertinoPageRoute( - builder: builder, fullscreenDialog: fullscreenDialog); + return CupertinoPageRoute(builder: builder, fullscreenDialog: fullscreenDialog); } else { - return MaterialPageRoute( - builder: builder, fullscreenDialog: fullscreenDialog); + return MaterialPageRoute(builder: builder, fullscreenDialog: fullscreenDialog); } } @@ -229,8 +227,7 @@ Route createRoute(RouteSettings settings) { case Routes.walletGroupsDisplayPage: final type = settings.arguments as WalletType; - final walletGroupsDisplayVM = - getIt.get(param1: type); + final walletGroupsDisplayVM = getIt.get(param1: type); return handleRouteWithPlatformAwareness( (_) => WalletGroupsDisplayPage( @@ -258,22 +255,18 @@ Route createRoute(RouteSettings settings) { final hardwareWallet = arguments[1] as HardwareWalletType; final walletVM = getIt.get( - param1: type, - param2: getIt(param1: hardwareWallet)); + param1: type, param2: getIt(param1: hardwareWallet)); if (type == WalletType.monero) - return handleRouteWithPlatformAwareness( - (_) => MoneroHardwareWalletOptionsPage(walletVM)); + return handleRouteWithPlatformAwareness((_) => MoneroHardwareWalletOptionsPage(walletVM)); - return handleRouteWithPlatformAwareness( - (_) => SelectHardwareWalletAccountPage(walletVM)); + return handleRouteWithPlatformAwareness((_) => SelectHardwareWalletAccountPage(walletVM)); case Routes.setupPin: Function(PinCodeState, String)? callback; if (settings.arguments is Function(PinCodeState, String)) { - callback = - settings.arguments as Function(PinCodeState, String); + callback = settings.arguments as Function(PinCodeState, String); } return handleRouteWithPlatformAwareness( @@ -286,8 +279,7 @@ Route createRoute(RouteSettings settings) { param1: NewWalletTypeArguments( onTypeSelected: (BuildContext context, WalletType type) { final arg = {'walletType': type}; - Navigator.of(context) - .pushNamed(Routes.restoreWallet, arguments: arg); + Navigator.of(context).pushNamed(Routes.restoreWallet, arguments: arg); }, isCreate: false, ), @@ -307,8 +299,7 @@ Route createRoute(RouteSettings settings) { case Routes.restoreWalletFromSeedKeys: if (isSingleCoin) { return handleRouteWithPlatformAwareness( - (context) => - getIt.get(param1: availableWalletTypes.first), + (context) => getIt.get(param1: availableWalletTypes.first), ); } return handleRouteWithPlatformAwareness( @@ -316,8 +307,7 @@ Route createRoute(RouteSettings settings) { param1: NewWalletTypeArguments( onTypeSelected: (BuildContext context, WalletType type) { final arg = {'walletType': type}; - Navigator.of(context) - .pushNamed(Routes.restoreWallet, arguments: arg); + Navigator.of(context).pushNamed(Routes.restoreWallet, arguments: arg); }, isCreate: false, ), @@ -327,23 +317,19 @@ Route createRoute(RouteSettings settings) { case Routes.restoreWalletFromHardwareWallet: final arguments = settings.arguments as Map?; final showUnavailable = (arguments?['showUnavailable'] as bool?) ?? true; - final onSelect = arguments?['onSelect'] as void Function( - BuildContext, HardwareWalletType)?; + final onSelect = arguments?['onSelect'] as void Function(BuildContext, HardwareWalletType)?; final availableHardwareWalletTypes = - arguments?['availableHardwareWalletTypes'] - as List?; + arguments?['availableHardwareWalletTypes'] as List?; - return handleRouteWithPlatformAwareness( - (_) => SelectDeviceManufacturerPage( - showUnavailable: showUnavailable, - onSelect: onSelect, - availableHardwareWalletTypes: availableHardwareWalletTypes, - )); + return handleRouteWithPlatformAwareness((_) => SelectDeviceManufacturerPage( + showUnavailable: showUnavailable, + onSelect: onSelect, + availableHardwareWalletTypes: availableHardwareWalletTypes, + )); case Routes.connectHardwareWallet: final arguments = settings.arguments as List; - final hardwareWalletType = - (arguments[0] as HardwareWalletType?) ?? HardwareWalletType.ledger; + final hardwareWalletType = (arguments[0] as HardwareWalletType?) ?? HardwareWalletType.ledger; if (isSingleCoin) { return handleRouteWithPlatformAwareness( @@ -351,13 +337,9 @@ Route createRoute(RouteSettings settings) { ConnectDevicePageParams( walletType: availableWalletTypes.first, hardwareWalletType: hardwareWalletType, - onConnectDevice: (BuildContext context, _) => - Navigator.of(context).pushNamed( - Routes.chooseHardwareWalletAccount, - arguments: [ - availableWalletTypes.first, - hardwareWalletType - ]), + onConnectDevice: (BuildContext context, _) => Navigator.of(context).pushNamed( + Routes.chooseHardwareWalletAccount, + arguments: [availableWalletTypes.first, hardwareWalletType]), isReconnect: false, ), getIt.get(), @@ -369,8 +351,7 @@ Route createRoute(RouteSettings settings) { param1: NewWalletTypeArguments( onTypeSelected: (BuildContext context, WalletType type) { if (hardwareWalletType == HardwareWalletType.trezor) { - Navigator.of(context).pushNamed( - Routes.chooseHardwareWalletAccount, + Navigator.of(context).pushNamed(Routes.chooseHardwareWalletAccount, arguments: [type, hardwareWalletType]); return; } @@ -378,15 +359,13 @@ Route createRoute(RouteSettings settings) { final arguments = ConnectDevicePageParams( walletType: type, hardwareWalletType: hardwareWalletType, - onConnectDevice: (BuildContext context, _) => - Navigator.of(context).pushNamed( - Routes.chooseHardwareWalletAccount, - arguments: [type, hardwareWalletType]), + onConnectDevice: (BuildContext context, _) => Navigator.of(context).pushNamed( + Routes.chooseHardwareWalletAccount, + arguments: [type, hardwareWalletType]), isReconnect: false, ); - Navigator.of(context) - .pushNamed(Routes.connectDevices, arguments: arguments); + Navigator.of(context).pushNamed(Routes.connectDevices, arguments: arguments); }, isCreate: false, hardwareWalletType: hardwareWalletType, @@ -407,16 +386,14 @@ Route createRoute(RouteSettings settings) { case Routes.seed: return handleRouteWithPlatformAwareness( - (context) => - getIt.get(param1: settings.arguments as bool), + (context) => getIt.get(param1: settings.arguments as bool), ); case Routes.restoreWallet: final args = settings.arguments as Map?; final walletType = args?['walletType'] as WalletType; return MaterialPageRoute( - builder: (_) => - getIt.get(param1: walletType, param2: args)); + builder: (_) => getIt.get(param1: walletType, param2: args)); case Routes.restoreWalletChooseDerivation: return MaterialPageRoute( @@ -424,21 +401,18 @@ Route createRoute(RouteSettings settings) { param1: settings.arguments as List)); case Routes.sweepingWalletPage: - return CupertinoPageRoute( - builder: (_) => getIt.get()); + return CupertinoPageRoute(builder: (_) => getIt.get()); case Routes.dashboard: return CupertinoPageRoute( - settings: settings, builder: (_) => - FeatureFlag.hasNewUi? - getIt.get(): - getIt.get()); + settings: settings, + builder: (_) => + FeatureFlag.hasNewUi ? getIt.get() : getIt.get()); case Routes.send: final args = settings.arguments as Map?; final initialPaymentRequest = args?['paymentRequest'] as PaymentRequest?; - final coinTypeToSpendFrom = - args?['coinTypeToSpendFrom'] as UnspentCoinType?; + final coinTypeToSpendFrom = args?['coinTypeToSpendFrom'] as UnspentCoinType?; return handleRouteWithPlatformAwareness( (context) => getIt.get( @@ -449,12 +423,10 @@ Route createRoute(RouteSettings settings) { case Routes.sendTemplate: return CupertinoPageRoute( - fullscreenDialog: true, - builder: (_) => getIt.get()); + fullscreenDialog: true, builder: (_) => getIt.get()); case Routes.receive: - return CupertinoPageRoute( - builder: (context) => getIt.get()); + return CupertinoPageRoute(builder: (context) => getIt.get()); case Routes.addressPage: return handleRouteWithPlatformAwareness( @@ -464,34 +436,29 @@ Route createRoute(RouteSettings settings) { case Routes.transactionDetails: return CupertinoPageRoute( fullscreenDialog: true, - builder: (_) => getIt.get( - param1: settings.arguments as TransactionInfo)); + builder: (_) => + getIt.get(param1: settings.arguments as TransactionInfo)); case Routes.bumpFeePage: return CupertinoPageRoute( fullscreenDialog: true, - builder: (_) => getIt.get( - param1: settings.arguments as List)); + builder: (_) => getIt.get(param1: settings.arguments as List)); case Routes.newSubaddress: return CupertinoPageRoute( - builder: (_) => - getIt.get(param1: settings.arguments)); + builder: (_) => getIt.get(param1: settings.arguments)); case Routes.disclaimer: return CupertinoPageRoute(builder: (_) => DisclaimerPage()); case Routes.readDisclaimer: - return CupertinoPageRoute( - builder: (_) => DisclaimerPage(isReadOnly: true)); + return CupertinoPageRoute(builder: (_) => DisclaimerPage(isReadOnly: true)); case Routes.readThirdPartyDisclaimer: - return CupertinoPageRoute( - builder: (_) => ThirdPartyDisclaimerPage()); + return CupertinoPageRoute(builder: (_) => ThirdPartyDisclaimerPage()); case Routes.changeRep: - return CupertinoPageRoute( - builder: (_) => getIt.get()); + return CupertinoPageRoute(builder: (_) => getIt.get()); case Routes.walletList: final onWalletLoaded = settings.arguments as Function(BuildContext)?; @@ -503,8 +470,8 @@ Route createRoute(RouteSettings settings) { case Routes.walletEdit: return MaterialPageRoute( fullscreenDialog: true, - builder: (_) => getIt.get( - param1: settings.arguments as WalletEditPageArguments), + builder: (_) => + getIt.get(param1: settings.arguments as WalletEditPageArguments), ); case Routes.auth: @@ -517,8 +484,7 @@ Route createRoute(RouteSettings settings) { instanceName: 'wallet_unlock_verifiable', param2: true) : getIt.get( - param1: settings.arguments as OnAuthenticationFinished, - param2: true)); + param1: settings.arguments as OnAuthenticationFinished, param2: true)); case Routes.totpAuthCodePage: final args = settings.arguments as TotpAuthArgumentsModel; @@ -544,15 +510,13 @@ Route createRoute(RouteSettings settings) { ? WillPopScope( child: getIt.get( param1: WalletUnlockArguments( - callback: - settings.arguments as OnAuthenticationFinished), + callback: settings.arguments as OnAuthenticationFinished), param2: false, instanceName: 'wallet_unlock_verifiable'), onWillPop: () async => false) : WillPopScope( child: getIt.get( - param1: settings.arguments as OnAuthenticationFinished, - param2: false), + param1: settings.arguments as OnAuthenticationFinished, param2: false), onWillPop: () async => false)); case Routes.silentPaymentsSettings: @@ -597,13 +561,11 @@ Route createRoute(RouteSettings settings) { case Routes.trocadorProvidersPage: return CupertinoPageRoute( - fullscreenDialog: true, - builder: (_) => getIt.get()); + fullscreenDialog: true, builder: (_) => getIt.get()); case Routes.domainLookupsPage: return CupertinoPageRoute( - fullscreenDialog: true, - builder: (_) => getIt.get()); + fullscreenDialog: true, builder: (_) => getIt.get()); case Routes.displaySettingsPage: return handleRouteWithPlatformAwareness( @@ -619,20 +581,17 @@ Route createRoute(RouteSettings settings) { final args = settings.arguments as Map?; return CupertinoPageRoute( builder: (_) => getIt.get( - param1: args?['editingNode'] as Node?, - param2: args?['isSelected'] as bool?)); + param1: args?['editingNode'] as Node?, param2: args?['isSelected'] as bool?)); case Routes.login: return CupertinoPageRoute( builder: (context) => WillPopScope( child: SettingsStoreBase.walletPasswordDirectInput - ? getIt.get( - instanceName: 'wallet_password_login') + ? getIt.get(instanceName: 'wallet_password_login') : getIt.get(instanceName: 'login'), onWillPop: () async => // FIX-ME: Additional check does it works correctly - (await SystemChannels.platform - .invokeMethod('SystemNavigator.pop') ?? + (await SystemChannels.platform.invokeMethod('SystemNavigator.pop') ?? false)), fullscreenDialog: true); @@ -640,8 +599,7 @@ Route createRoute(RouteSettings settings) { final args = settings.arguments as Map?; return CupertinoPageRoute( builder: (_) => getIt.get( - param1: args?['editingNode'] as Node?, - param2: args?['isSelected'] as bool?)); + param1: args?['editingNode'] as Node?, param2: args?['isSelected'] as bool?)); case Routes.accountCreation: return CupertinoPageRoute( @@ -650,8 +608,8 @@ Route createRoute(RouteSettings settings) { case Routes.nanoAccountCreation: return CupertinoPageRoute( - builder: (_) => getIt.get( - param1: settings.arguments as NanoAccount?)); + builder: (_) => + getIt.get(param1: settings.arguments as NanoAccount?)); case Routes.addressBook: return handleRouteWithPlatformAwareness( @@ -664,13 +622,11 @@ Route createRoute(RouteSettings settings) { builder: (_) => getIt.get(param1: selectedCurrency)); case Routes.pickerWalletAddress: - return MaterialPageRoute( - builder: (_) => getIt.get()); + return MaterialPageRoute(builder: (_) => getIt.get()); case Routes.addressBookAddContact: return handleRouteWithPlatformAwareness( - (context) => getIt.get( - param1: settings.arguments as ContactRecord?), + (context) => getIt.get(param1: settings.arguments as ContactRecord?), ); case Routes.showKeys: @@ -679,23 +635,19 @@ Route createRoute(RouteSettings settings) { ); case Routes.exchangeTrade: - return CupertinoPageRoute( - builder: (_) => getIt.get()); + return CupertinoPageRoute(builder: (_) => getIt.get()); case Routes.exchangeConfirm: - return MaterialPageRoute( - builder: (_) => getIt.get()); + return MaterialPageRoute(builder: (_) => getIt.get()); case Routes.tradeDetails: return MaterialPageRoute( fullscreenDialog: true, - builder: (_) => - getIt.get(param1: settings.arguments as Trade)); + builder: (_) => getIt.get(param1: settings.arguments as Trade)); case Routes.orderDetails: return MaterialPageRoute( - builder: (_) => - getIt.get(param1: settings.arguments as Order)); + builder: (_) => getIt.get(param1: settings.arguments as Order)); case Routes.buySellPage: final args = settings.arguments as bool; @@ -705,8 +657,7 @@ Route createRoute(RouteSettings settings) { case Routes.buyOptionsPage: final args = settings.arguments as List; - return MaterialPageRoute( - builder: (_) => getIt.get(param1: args)); + return MaterialPageRoute(builder: (_) => getIt.get(param1: args)); case Routes.paymentMethodOptionsPage: final args = settings.arguments as List; @@ -717,18 +668,15 @@ Route createRoute(RouteSettings settings) { final args = settings.arguments as List; return MaterialPageRoute( - fullscreenDialog: true, - builder: (_) => getIt.get(param1: args)); + fullscreenDialog: true, builder: (_) => getIt.get(param1: args)); case Routes.exchange: return handleRouteWithPlatformAwareness( - (context) => getIt.get( - param1: settings.arguments as PaymentRequest?), + (context) => getIt.get(param1: settings.arguments as PaymentRequest?), ); case Routes.exchangeTemplate: - return CupertinoPageRoute( - builder: (_) => getIt.get()); + return CupertinoPageRoute(builder: (_) => getIt.get()); case Routes.rescan: return MaterialPageRoute(builder: (_) => getIt.get()); @@ -740,13 +688,11 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute(builder: (_) => getIt.get()); case Routes.walletGroupExistingSeedDescriptionPage: - return MaterialPageRoute( - builder: (_) => WalletGroupExistingSeedDescriptionPage()); + return MaterialPageRoute(builder: (_) => WalletGroupExistingSeedDescriptionPage()); case Routes.transactionSuccessPage: return MaterialPageRoute( - builder: (_) => getIt.get( - param1: settings.arguments as String)); + builder: (_) => getIt.get(param1: settings.arguments as String)); case Routes.backup: return handleRouteWithPlatformAwareness( @@ -754,13 +700,11 @@ Route createRoute(RouteSettings settings) { ); case Routes.editBackupPassword: - return CupertinoPageRoute( - builder: (_) => getIt.get()); + return CupertinoPageRoute(builder: (_) => getIt.get()); case Routes.restoreFromBackup: return CupertinoPageRoute( - fullscreenDialog: true, - builder: (_) => getIt.get()); + fullscreenDialog: true, builder: (_) => getIt.get()); case Routes.support: return handleRouteWithPlatformAwareness( @@ -768,8 +712,7 @@ Route createRoute(RouteSettings settings) { ); case Routes.supportLiveChat: - return CupertinoPageRoute( - builder: (_) => getIt.get()); + return CupertinoPageRoute(builder: (_) => getIt.get()); case Routes.supportOtherLinks: return handleRouteWithPlatformAwareness( @@ -779,8 +722,7 @@ Route createRoute(RouteSettings settings) { case Routes.unspentCoinsList: final coinTypeToSpendFrom = settings.arguments as UnspentCoinType?; return handleRouteWithPlatformAwareness( - (context) => - getIt.get(param1: coinTypeToSpendFrom), + (context) => getIt.get(param1: coinTypeToSpendFrom), ); case Routes.unspentCoinsDetails: @@ -846,47 +788,41 @@ Route createRoute(RouteSettings settings) { toggleUseTestnet: toggleTestnet, advancedPrivacySettingsViewModel: getIt.get(param1: type), - nodeViewModel: - getIt.get(param1: type, param2: false), + nodeViewModel: getIt.get(param1: type, param2: false), seedSettingsViewModel: getIt.get(), ), ); case Routes.anonPayInvoicePage: final args = settings.arguments as List; - return CupertinoPageRoute( - builder: (_) => getIt.get(param1: args)); + return CupertinoPageRoute(builder: (_) => getIt.get(param1: args)); case Routes.anonPayReceivePage: final anonReceivePageArgs = settings.arguments as AnonPayReceivePageArgs; return CupertinoPageRoute( - builder: (_) => - getIt.get(param1: anonReceivePageArgs)); + builder: (_) => getIt.get(param1: anonReceivePageArgs)); case Routes.anonPayDetailsPage: final anonInvoiceViewData = settings.arguments as AnonpayInvoiceInfo; return CupertinoPageRoute( - builder: (_) => - getIt.get(param1: anonInvoiceViewData)); + builder: (_) => getIt.get(param1: anonInvoiceViewData)); case Routes.payjoinDetails: final arguments = settings.arguments as List; final sessionId = arguments.first as String; final transactionInfo = arguments[1] as TransactionInfo?; return CupertinoPageRoute( - builder: (_) => getIt.get( - param1: sessionId, param2: transactionInfo)); + builder: (_) => + getIt.get(param1: sessionId, param2: transactionInfo)); case Routes.desktop_actions: return PageRouteBuilder( opaque: false, - pageBuilder: (_, __, ___) => - DesktopDashboardActions(getIt()), + pageBuilder: (_, __, ___) => DesktopDashboardActions(getIt()), ); case Routes.desktop_settings_page: - return CupertinoPageRoute( - builder: (_) => getIt.get()); + return CupertinoPageRoute(builder: (_) => getIt.get()); case Routes.empty_no_route: return MaterialPageRoute(builder: (_) => SizedBox.shrink()); @@ -901,21 +837,17 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute(builder: (_) => getIt.get()); case Routes.setup_2faQRPage: - return MaterialPageRoute( - builder: (_) => getIt.get()); + return MaterialPageRoute(builder: (_) => getIt.get()); case Routes.modify2FAPage: - return MaterialPageRoute( - builder: (_) => getIt.get()); + return MaterialPageRoute(builder: (_) => getIt.get()); case Routes.setup2faInfoPage: - return MaterialPageRoute( - builder: (_) => getIt.get()); + return MaterialPageRoute(builder: (_) => getIt.get()); case Routes.urqrAnimatedPage: return MaterialPageRoute( - builder: (_) => - getIt.get(param1: settings.arguments)); + builder: (_) => getIt.get(param1: settings.arguments)); case Routes.homeSettings: return CupertinoPageRoute( @@ -937,12 +869,10 @@ Route createRoute(RouteSettings settings) { ); case Routes.manageNodes: - return MaterialPageRoute( - builder: (_) => getIt.get(param1: false)); + return MaterialPageRoute(builder: (_) => getIt.get(param1: false)); case Routes.managePowNodes: - return MaterialPageRoute( - builder: (_) => getIt.get(param1: true)); + return MaterialPageRoute(builder: (_) => getIt.get(param1: true)); case Routes.walletConnectConnectionsListing: return MaterialPageRoute( @@ -978,9 +908,7 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute( builder: (_) => ConnectDevicePage( - params, - getIt.get( - param1: params.hardwareWalletType))); + params, getIt.get(param1: params.hardwareWalletType))); case Routes.walletGroupDescription: final walletType = settings.arguments as WalletType; @@ -1069,8 +997,6 @@ Route createRoute(RouteSettings settings) { default: return MaterialPageRoute( builder: (_) => Scaffold( - body: Center( - child: Text(S.current - .router_no_route(settings.name ?? 'No route'))))); + body: Center(child: Text(S.current.router_no_route(settings.name ?? 'No route'))))); } } From f8e78e43afe72ff6e0cad2c599fcacf9d1664547 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Fri, 21 Nov 2025 10:25:21 +0100 Subject: [PATCH 57/68] remove unused navbar --- lib/new-ui/widgets/navbar/navbar.dart | 68 -------------------- lib/new-ui/widgets/navbar/navbar_button.dart | 67 ------------------- 2 files changed, 135 deletions(-) delete mode 100644 lib/new-ui/widgets/navbar/navbar.dart delete mode 100644 lib/new-ui/widgets/navbar/navbar_button.dart diff --git a/lib/new-ui/widgets/navbar/navbar.dart b/lib/new-ui/widgets/navbar/navbar.dart deleted file mode 100644 index f19f6e69b6..0000000000 --- a/lib/new-ui/widgets/navbar/navbar.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'navbar_button.dart'; - -class Navbar extends StatefulWidget { - const Navbar({super.key}); - - @override - State createState() => _NavbarState(); -} - -class NavbarItemData { - final String iconPath; - final String text; - - NavbarItemData(this.iconPath, this.text); -} - -class _NavbarState extends State { - int _selectedIndex = 0; - - final List _items = [ - NavbarItemData("assets/Home.svg", "Home"), - NavbarItemData("assets/Wallets.svg", "Wallets"), - NavbarItemData("assets/Contacts.svg", "Contacts"), - NavbarItemData("assets/Apps.svg", "Apps"), - NavbarItemData("assets/Charts.svg", "Charts"), - ]; - - @override - Widget build(BuildContext context) { - return Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: const EdgeInsets.all(32.0), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(99999), - color: Theme.of( - context, - ).colorScheme.surfaceContainerHighest.withAlpha(170), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 12.0, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: List.generate(_items.length, (index) { - return NavbarButton( - data: _items[index], - onPressed: () { - setState(() { - _selectedIndex = index; - }); - }, - selected: _selectedIndex == index, - ); - }), - ), - ), - ), - ), - ); - } -} diff --git a/lib/new-ui/widgets/navbar/navbar_button.dart b/lib/new-ui/widgets/navbar/navbar_button.dart deleted file mode 100644 index 79c3af9eac..0000000000 --- a/lib/new-ui/widgets/navbar/navbar_button.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:cake_wallet/new-ui/widgets/navbar/navbar.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; - -class NavbarButton extends StatelessWidget { - const NavbarButton({ - super.key, - required this.data, - required this.selected, - required this.onPressed, - }); - - final NavbarItemData data; - final VoidCallback onPressed; - final bool selected; - - @override - Widget build(BuildContext context) { - return AnimatedSize( - curve: Curves.easeOut, - duration: Duration(milliseconds: 100), - child: AnimatedContainer( - curve: Curves.easeOut, - duration: Duration(milliseconds: 100), - decoration: BoxDecoration( - color: selected - ? Color(0x79BDCFFF) - : Theme.of( - context, - ).colorScheme.surfaceContainerHighest.withAlpha(0), - borderRadius: BorderRadius.circular(1242357), - ), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - constraints: BoxConstraints(), - padding: EdgeInsets.zero, - icon: SvgPicture.asset( - data.iconPath, - width: selected ? 24 : 36, - height: selected ? 24 : 36, - colorFilter: ColorFilter.mode( - selected - ? Theme.of(context).colorScheme.onSurface - : Theme.of(context).colorScheme.primary, - BlendMode.srcIn, - ), - ), - onPressed: onPressed, - ), - if (selected) - Padding( - padding: const EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 0.0), - child: Text( - data.text, - style: TextStyle(fontWeight: FontWeight.bold), - textAlign: TextAlign.center, - ), - ), - ], - ), - ), - ); - } -} From d97b0db9d75968bfd572137c06ffc2312c44feb4 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Fri, 21 Nov 2025 11:53:02 +0100 Subject: [PATCH 58/68] fix: lightning balance on balance card in lightning mode --- .../coins_page/cards/balance_card.dart | 28 +++++++++++-------- .../widgets/coins_page/cards/cards_view.dart | 7 +++++ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/lib/new-ui/widgets/coins_page/cards/balance_card.dart b/lib/new-ui/widgets/coins_page/cards/balance_card.dart index bcdf43bd89..f05cbbca78 100644 --- a/lib/new-ui/widgets/coins_page/cards/balance_card.dart +++ b/lib/new-ui/widgets/coins_page/cards/balance_card.dart @@ -7,14 +7,22 @@ class BalanceCard extends StatelessWidget { super.key, required this.width, required this.balanceRecord, - required this.selected, required this.accountName, required this.accountBalance, + required this.selected, + required this.accountName, + required this.accountBalance, + required this.gradient, + required this.svgPath, + required this.lightningMode, }); final double width; final String accountBalance; final String accountName; + final Gradient gradient; + final String svgPath; final BalanceRecord balanceRecord; final bool selected; + final bool lightningMode; @override Widget build(BuildContext context) { @@ -25,11 +33,7 @@ class BalanceCard extends StatelessWidget { height: width * 2.0 / 3, decoration: BoxDecoration( border: Border.all(color: Color(0x77FFFFFF), width: 1), - gradient: LinearGradient( - colors: [Colors.lightBlueAccent, Colors.blue], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), + gradient: gradient, borderRadius: BorderRadius.circular(20), ), child: Padding( @@ -49,7 +53,6 @@ class BalanceCard extends StatelessWidget { accountName, style: TextStyle(color: Colors.black, fontSize: 20), ), - AnimatedOpacity( opacity: selected ? 0 : 1, duration: textFadeDuration, @@ -67,7 +70,9 @@ class BalanceCard extends StatelessWidget { spacing: 8.0, children: [ Text( - balanceRecord.availableBalance, + lightningMode + ? balanceRecord.secondAvailableBalance + : balanceRecord.availableBalance, style: TextStyle(color: Colors.black, fontSize: 28), ), Text( @@ -78,12 +83,13 @@ class BalanceCard extends StatelessWidget { ), ), Text( - balanceRecord.fiatAvailableBalance, + lightningMode + ? balanceRecord.fiatSecondAdditionalBalance + : balanceRecord.fiatAvailableBalance, style: TextStyle(color: Colors.black45, fontSize: 20), ), ], ), - Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -109,7 +115,7 @@ class BalanceCard extends StatelessWidget { ), ), SvgPicture.asset( - "assets/new-ui/switcher-bitcoin.svg", + svgPath, height: 50, width: 50, colorFilter: const ColorFilter.mode( diff --git a/lib/new-ui/widgets/coins_page/cards/cards_view.dart b/lib/new-ui/widgets/coins_page/cards/cards_view.dart index 22d0bce9a8..c1c4f37733 100644 --- a/lib/new-ui/widgets/coins_page/cards/cards_view.dart +++ b/lib/new-ui/widgets/coins_page/cards/cards_view.dart @@ -75,6 +75,13 @@ class _CardsViewState extends State { balanceRecord: widget.dashboardViewModel.balanceViewModel.formattedBalances.elementAt(0), selected: _selectedIndex == index, + gradient: LinearGradient( + colors: [Colors.lightBlue, Colors.blue], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + svgPath: widget.dashboardViewModel.balanceViewModel.balances.keys.elementAt(0).iconPath!, + lightningMode: widget.lightningMode, ); }), ), From 2c5f6cfb872f51c862f7e79b5d6bdf2e616f8168 Mon Sep 17 00:00:00 2001 From: malik1004x Date: Fri, 21 Nov 2025 12:55:08 +0100 Subject: [PATCH 59/68] fix: return if uri == null --- lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart index 9a52178ed3..2cb9d4d36a 100644 --- a/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart +++ b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart @@ -92,6 +92,7 @@ class CoinActionRow extends StatelessWidget { if (code == null) return; if (code.isEmpty) return; final uri = Uri.tryParse(code); + if (uri == null) return; rootKey.currentState?.handleDeepLinking(uri); }; }, From 8ca44cd6203d85d2f52c6d3c47e4eb1ddc02b527 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Mon, 24 Nov 2025 13:03:43 +0100 Subject: [PATCH 60/68] feat: trade history in new ui --- .../assets_history/history_section.dart | 57 +++++--- .../assets_history/history_trade_tile.dart | 138 ++++++++++++++++++ 2 files changed, 175 insertions(+), 20 deletions(-) create mode 100644 lib/new-ui/widgets/coins_page/assets_history/history_trade_tile.dart diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_section.dart b/lib/new-ui/widgets/coins_page/assets_history/history_section.dart index 745517cbbc..cc68de3456 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/history_section.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/history_section.dart @@ -1,11 +1,12 @@ import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_tile.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_trade_tile.dart'; import 'package:cake_wallet/utils/date_formatter.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/date_section_item.dart'; +import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart'; import 'package:flutter/material.dart'; - class HistorySection extends StatelessWidget { const HistorySection({super.key, required this.dashboardViewModel}); @@ -22,32 +23,48 @@ class HistorySection extends StatelessWidget { itemBuilder: (context, index) { final prevItem = index == 0 ? null : dashboardViewModel.items[index - 1]; final item = dashboardViewModel.items[index]; - final nextItem = index == dashboardViewModel.items.length - 1 ? null : dashboardViewModel.items[index + 1]; - + final nextItem = index == dashboardViewModel.items.length - 1 + ? null + : dashboardViewModel.items[index + 1]; - if(item is TransactionListItem) { + if (item is TransactionListItem) { final transaction = item.transaction; - final transactionType = - dashboardViewModel.getTransactionType(transaction); + final transactionType = dashboardViewModel.getTransactionType(transaction); return HistoryTile( - title: item.formattedTitle + item.formattedStatus + transactionType, - date: DateFormatter.convertDateTimeToReadableString(item.date), - amount: item.formattedCryptoAmount, - amountFiat: item.formattedFiatAmount, - roundedBottom: !(nextItem is TransactionListItem), - roundedTop: !(prevItem is TransactionListItem), - bottomSeparator: nextItem is TransactionListItem, - direction: item.transaction.direction, - pending: item.transaction.isPending - ); + title: item.formattedTitle + item.formattedStatus + transactionType, + date: DateFormatter.convertDateTimeToReadableString(item.date), + amount: item.formattedCryptoAmount, + amountFiat: item.formattedFiatAmount, + roundedBottom: !(nextItem is TransactionListItem || nextItem is TradeListItem), + roundedTop: !(prevItem is TransactionListItem || prevItem is TradeListItem), + bottomSeparator: nextItem is TransactionListItem || nextItem is TradeListItem, + direction: item.transaction.direction, + pending: item.transaction.isPending); + } else if (item is TradeListItem) { + final trade = item.trade; + final tradeFrom = trade.fromRaw >= 0 ? trade.from : trade.userCurrencyFrom; - } else if(item is DateSectionItem){ - return Text(DateFormatter.convertDateTimeToReadableString(item.date)); - } + final tradeTo = trade.toRaw >= 0 ? trade.to : trade.userCurrencyTo; - else return Text(item.runtimeType.toString()); + return HistoryTradeTile( + from: tradeFrom!, + to: tradeTo!, + date: DateFormatter.convertDateTimeToReadableString(item.date), + amount: trade.amountFormatted(), + receiveAmount: trade.receiveAmountFormatted(), + roundedBottom: !(nextItem is TransactionListItem || nextItem is TradeListItem), + roundedTop: !(prevItem is TransactionListItem || prevItem is TradeListItem), + bottomSeparator: nextItem is TransactionListItem || nextItem is TradeListItem, + swapState: trade.state, + ); + } else if (item is DateSectionItem) { + return Padding( + padding: EdgeInsets.only(left: 8.0, bottom: 8.0), + child: Text(DateFormatter.convertDateTimeToReadableString(item.date))); + } else + return Text(item.runtimeType.toString()); }, ), ); diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_trade_tile.dart b/lib/new-ui/widgets/coins_page/assets_history/history_trade_tile.dart new file mode 100644 index 0000000000..5c746081c8 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/history_trade_tile.dart @@ -0,0 +1,138 @@ +import 'package:cake_wallet/exchange/trade_state.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:flutter/material.dart'; + +class HistoryTradeTile extends StatelessWidget { + const HistoryTradeTile( + {super.key, + required this.date, + required this.amount, + required this.receiveAmount, + required this.roundedTop, + required this.roundedBottom, + required this.bottomSeparator, + required this.from, + required this.to, + required this.swapState}); + + final CryptoCurrency from; + final CryptoCurrency to; + final String date; + final String amount; + final String receiveAmount; + final bool roundedTop; + final bool roundedBottom; + final bool bottomSeparator; + final TradeState swapState; + + @override + Widget build(BuildContext context) { + double currencyIconSize = 30.0; + + return Column( + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(roundedTop ? 12.0 : 0.0), + topRight: Radius.circular(roundedTop ? 12.0 : 0.0), + bottomLeft: Radius.circular(roundedBottom ? 12.0 : 0.0), + bottomRight: Radius.circular(roundedBottom ? 12.0 : 0.0), + )), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12.0, + horizontal: 12.0, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 0.0), + child: SizedBox( + height: 50, + width: 50, + child: Stack( + children: [ + Image.asset(_getIconPath(from), + width: currencyIconSize, height: currencyIconSize), + Positioned( + top: currencyIconSize / 2, + left: currencyIconSize / 2, + child: Container( + decoration: BoxDecoration( + border: Border.all( + width: 1, + color: Theme.of(context).colorScheme.surfaceContainer), + shape: BoxShape.circle), + child: Image.asset(_getIconPath(to), + width: currencyIconSize, height: currencyIconSize))), + ], + ), + ), + ), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("${from.toString()} → ${to.toString()}"), + Text(date), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text("$amount ${from.toString()}"), + Text("$receiveAmount ${to.toString()}"), + ], + ), + ], + ), + ), + ], + ), + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.0), + child: SizedBox( + height: 1, + child: DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + ), + ), + ), + ], + ); + } + + String _getIconPath(CryptoCurrency currency) { + if (currency.iconPath != null) { + return currency.iconPath!; + } + + if (currency.name.isNotEmpty) { + final currencyFromName = CryptoCurrency.fromString(currency.name); + if (currencyFromName.iconPath != null) { + return currencyFromName.iconPath!; + } + } + + if (currency.title.isNotEmpty) { + final currencyFromTitle = CryptoCurrency.fromString(currency.title); + if (currencyFromTitle.iconPath != null) { + return currencyFromTitle.iconPath!; + } + } + + //TODO approporiate fallback + return ""; + } +} From ae32b14f3026d063b04072f04321ddbd62bcdab8 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Tue, 25 Nov 2025 11:19:57 +0100 Subject: [PATCH 61/68] wip: new wallets page w balance display --- lib/di.dart | 5 + lib/new-ui/new_dashboard.dart | 6 +- lib/new-ui/pages/wallets_page.dart | 25 +++ .../assets_history => }/asset_tile.dart | 23 ++- .../assets_history/assets_section.dart | 10 +- .../screens/wallet_list/wallet_list_page.dart | 166 ++++++++++-------- lib/utils/feature_flag.dart | 2 +- .../wallet_list/wallet_list_item.dart | 2 + .../wallet_list/wallet_list_view_model.dart | 17 +- .../xcshareddata/xcschemes/Runner.xcscheme | 1 + 10 files changed, 158 insertions(+), 99 deletions(-) create mode 100644 lib/new-ui/pages/wallets_page.dart rename lib/new-ui/widgets/{coins_page/assets_history => }/asset_tile.dart (82%) diff --git a/lib/di.dart b/lib/di.dart index 7b056ea679..ed93b65587 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -13,6 +13,7 @@ import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; import 'package:cake_wallet/new-ui/new_dashboard.dart'; import 'package:cake_wallet/new-ui/pages/home_page.dart'; +import 'package:cake_wallet/new-ui/pages/wallets_page.dart'; import 'package:cake_wallet/order/order.dart'; import 'package:cake_wallet/core/backup_service_v3.dart'; import 'package:cake_wallet/core/new_wallet_arguments.dart'; @@ -894,6 +895,10 @@ Future setup({ onWalletLoaded: onWalletLoaded as Future Function(BuildContext)?, )); + getIt.registerFactory(() => NewWalletListPage( + walletListViewModel: getIt.get(), + )); + getIt.registerFactoryParam( (WalletListViewModel walletListViewModel, _) => WalletEditViewModel( walletListViewModel, diff --git a/lib/new-ui/new_dashboard.dart b/lib/new-ui/new_dashboard.dart index 7bb008fb7e..728b2fd25e 100644 --- a/lib/new-ui/new_dashboard.dart +++ b/lib/new-ui/new_dashboard.dart @@ -1,9 +1,11 @@ import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/new-ui/pages/home_page.dart'; +import 'package:cake_wallet/new-ui/pages/wallets_page.dart'; import 'package:cake_wallet/src/screens/contact/contact_list_page.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/cake_features_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/new_main_navbar_widget.dart'; import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart'; +import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:flutter/material.dart'; import '../view_model/dashboard/dashboard_view_model.dart'; @@ -14,7 +16,7 @@ class NewDashboard extends StatefulWidget { final List dashboardPageWidgets = [ getIt.get(), - getIt.get(), + FeatureFlag.hasNewUiExtraPages ? getIt.get() : getIt.get(), getIt.get(), getIt.get(), Placeholder(), @@ -32,7 +34,7 @@ class _NewDashboardState extends State { return Scaffold( body: Stack( children: [ - widget.dashboardPageWidgets[_selectedPage], + widget.dashboardPageWidgets[_selectedPage], NewMainNavBar( dashboardViewModel: widget.dashboardViewModel, selectedIndex: _selectedPage, diff --git a/lib/new-ui/pages/wallets_page.dart b/lib/new-ui/pages/wallets_page.dart new file mode 100644 index 0000000000..36b56e6cbb --- /dev/null +++ b/lib/new-ui/pages/wallets_page.dart @@ -0,0 +1,25 @@ +import 'package:cake_wallet/new-ui/widgets/asset_tile.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; +import 'package:cw_core/currency_for_wallet_type.dart'; +import 'package:flutter/material.dart'; + +class NewWalletListPage extends StatelessWidget { + const NewWalletListPage({super.key, required this.walletListViewModel}); + + final WalletListViewModel walletListViewModel; + + + @override + Widget build(BuildContext context) { + return SafeArea( + child: ListView.builder( + itemCount: walletListViewModel.wallets.length, + itemBuilder: (context, index){ + final wallet = walletListViewModel.wallets[index]; + + return AssetTile(iconPath: walletTypeToCryptoCurrency(wallet.type).iconPath!, name: wallet.name, amount: "123 BTC", amountFiat: "123 USD"); + }, + ) + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart b/lib/new-ui/widgets/asset_tile.dart similarity index 82% rename from lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart rename to lib/new-ui/widgets/asset_tile.dart index b811604fb6..914a2d8bca 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart +++ b/lib/new-ui/widgets/asset_tile.dart @@ -2,9 +2,17 @@ import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:flutter/material.dart'; class AssetTile extends StatelessWidget { - const AssetTile({super.key, required this.dashboardViewModel}); + const AssetTile( + {super.key, + required this.iconPath, + required this.name, + required this.amount, + required this.amountFiat}); - final DashboardViewModel dashboardViewModel; + final String iconPath; + final String name; + final String amount; + final String amountFiat; @override Widget build(BuildContext context) { @@ -37,19 +45,19 @@ class AssetTile extends StatelessWidget { Row( mainAxisSize: MainAxisSize.min, children: [ - Container(width: 45, height: 45, child: Image.asset("assets/images/crypto/tether.webp")), - SizedBox(width: 8.0), + Container(width: 36, height: 36, child: Image.asset(iconPath)), + SizedBox(width: 12.0), Column( spacing: 4.0, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "DummyCoin", + name, style: TextStyle(fontWeight: FontWeight.bold), ), Text( - "0.000 DMC", + amount, style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant, ), @@ -58,9 +66,8 @@ class AssetTile extends StatelessWidget { ), ], ), - Text( - "\$0.00", + amountFiat, style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant, ), diff --git a/lib/new-ui/widgets/coins_page/assets_history/assets_section.dart b/lib/new-ui/widgets/coins_page/assets_history/assets_section.dart index 44eff375b9..cabe08986f 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/assets_section.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/assets_section.dart @@ -1,8 +1,7 @@ import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:flutter/material.dart'; - -import 'asset_tile.dart'; +import '../../asset_tile.dart'; class AssetsSection extends StatelessWidget { const AssetsSection({super.key, required this.dashboardViewModel}); @@ -16,7 +15,12 @@ class AssetsSection extends StatelessWidget { physics: NeverScrollableScrollPhysics(), itemCount: 1, itemBuilder: (context, index) { - return AssetTile(dashboardViewModel: dashboardViewModel,); + return AssetTile( + iconPath: "assets/images/crypto/tether.webp", + name: "DummyCoin", + amount: "0.000 DMC", + amountFiat: "\$ 0.00", + ); }, ); } diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index bf882a4e6f..48a81b6e8c 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -49,20 +49,19 @@ class WalletListPage extends BasePage { String get title => S.current.wallets; @override - Widget body(BuildContext context) => Observer( - builder: (_) { - if (walletListViewModel.singleWalletsList.isEmpty && walletListViewModel.multiWalletGroups.isEmpty) { - return Center( - child: CircularProgressIndicator(), + Widget body(BuildContext context) => Observer(builder: (_) { + if (walletListViewModel.singleWalletsList.isEmpty && + walletListViewModel.multiWalletGroups.isEmpty) { + return Center( + child: CircularProgressIndicator(), + ); + } + return WalletListBody( + walletListViewModel: walletListViewModel, + authService: authService, + onWalletLoaded: onWalletLoaded ?? (context) => Navigator.of(context).pop(), ); - } - return WalletListBody( - walletListViewModel: walletListViewModel, - authService: authService, - onWalletLoaded: onWalletLoaded ?? (context) => Navigator.of(context).pop(), - ); - } - ); + }); @override Widget trailing(BuildContext context) { @@ -166,76 +165,91 @@ class WalletListBodyState extends State { padding: EdgeInsets.only(left: 20, right: 20), child: Observer( builder: (_) => FilteredList( - shrinkWrap: true, - list: widget.walletListViewModel.multiWalletGroups, - updateFunction: widget.walletListViewModel.reorderAccordingToWalletList, - itemBuilder: (context, index) { - final group = widget.walletListViewModel.multiWalletGroups[index]; - final groupName = - group.groupName ?? '${S.current.wallet_group} ${index + 1}'; + shrinkWrap: true, + list: widget.walletListViewModel.multiWalletGroups, + updateFunction: widget.walletListViewModel.reorderAccordingToWalletList, + itemBuilder: (context, index) { + final group = widget.walletListViewModel.multiWalletGroups[index]; + final groupName = + group.groupName ?? '${S.current.wallet_group} ${index + 1}'; - widget.walletListViewModel.updateTileState( - index, - widget.walletListViewModel.expansionTileStateTrack[index] ?? false, - ); + widget.walletListViewModel.updateTileState( + index, + widget.walletListViewModel.expansionTileStateTrack[index] ?? false, + ); - return GroupedWalletExpansionTile( - onExpansionChanged: (value) { - widget.walletListViewModel.updateTileState(index, value); - setState(() {}); - }, - shouldShowCurrentWalletPointer: true, - borderRadius: BorderRadius.all(Radius.circular(16)), - title: groupName, - tileKey: ValueKey('group_wallets_expansion_tile_widget_$index'), - leadingWidget: Icon( - Icons.account_balance_wallet_outlined, - size: 28, - ), - trailingWidget: EditWalletButtonWidget( - width: 88, - isGroup: true, - isExpanded: - widget.walletListViewModel.expansionTileStateTrack[index]!, - onTap: () { - final wallet = widget.walletListViewModel - .convertWalletInfoToWalletListItem(group.wallets.first); - Navigator.of(context).pushNamed( - Routes.walletEdit, - arguments: WalletEditPageArguments( - walletListViewModel: widget.walletListViewModel, - editingWallet: wallet, - isWalletGroup: true, - groupName: groupName, - walletGroupKey: group.groupKey, + return FutureBuilder>( + future: Future.wait( + group.wallets.map((w) { + return widget.walletListViewModel + .convertWalletInfoToWalletListItem(w); + }), + ), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return SizedBox( + height: 60, + child: Center(child: CircularProgressIndicator()), + ); + } + + final childWallets = snapshot.data!; + + return GroupedWalletExpansionTile( + onExpansionChanged: (value) { + widget.walletListViewModel.updateTileState(index, value); + setState(() {}); + }, + shouldShowCurrentWalletPointer: true, + borderRadius: BorderRadius.all(Radius.circular(16)), + title: groupName, + tileKey: ValueKey('group_wallets_expansion_tile_widget_$index'), + leadingWidget: Icon( + Icons.account_balance_wallet_outlined, + size: 28, ), - ); - }, - ), - childWallets: group.wallets.map((walletInfo) { - return widget.walletListViewModel - .convertWalletInfoToWalletListItem(walletInfo); - }).toList(), - isSelected: false, - onChildItemTapped: (wallet) => - wallet.isCurrent ? null : _loadWallet(wallet), - childTrailingWidget: (item) { - return item.isCurrent - ? SizedBox.shrink() - : EditWalletButtonWidget( - width: 60, - onTap: () => Navigator.of(context).pushNamed( + trailingWidget: EditWalletButtonWidget( + width: 88, + isGroup: true, + isExpanded: widget + .walletListViewModel.expansionTileStateTrack[index]!, + onTap: () async { + final wallet = await widget.walletListViewModel + .convertWalletInfoToWalletListItem(group.wallets.first); + Navigator.of(context).pushNamed( Routes.walletEdit, arguments: WalletEditPageArguments( walletListViewModel: widget.walletListViewModel, - editingWallet: item, + editingWallet: wallet, + isWalletGroup: true, + groupName: groupName, + walletGroupKey: group.groupKey, ), - ), - ); - }, - ); - }, - ), + ); + }, + ), + childWallets: childWallets, + isSelected: false, + onChildItemTapped: (wallet) => + wallet.isCurrent ? null : _loadWallet(wallet), + childTrailingWidget: (item) { + return item.isCurrent + ? SizedBox.shrink() + : EditWalletButtonWidget( + width: 60, + onTap: () => Navigator.of(context).pushNamed( + Routes.walletEdit, + arguments: WalletEditPageArguments( + walletListViewModel: widget.walletListViewModel, + editingWallet: item, + ), + ), + ); + }, + ); + }, + ); + }), ), ), ), diff --git a/lib/utils/feature_flag.dart b/lib/utils/feature_flag.dart index 6b3e78fc0c..2f3374aa75 100644 --- a/lib/utils/feature_flag.dart +++ b/lib/utils/feature_flag.dart @@ -15,5 +15,5 @@ class FeatureFlag { static const bool hasBitcoinViewOnly = true; static const bool customBackgroundEnabled = false; static const bool hasNewUi = true; - static const bool hasNewUiExtraPages = false; + static const bool hasNewUiExtraPages = true; } diff --git a/lib/view_model/wallet_list/wallet_list_item.dart b/lib/view_model/wallet_list/wallet_list_item.dart index 8f8c58ea9f..46e814f073 100644 --- a/lib/view_model/wallet_list/wallet_list_item.dart +++ b/lib/view_model/wallet_list/wallet_list_item.dart @@ -6,6 +6,7 @@ class WalletListItem { required this.type, required this.key, required this.isHardware, + this.balance, this.isCurrent = false, this.isEnabled = true, this.isTestnet = false, @@ -15,6 +16,7 @@ class WalletListItem { final WalletType type; final bool isCurrent; final dynamic key; + final String? balance; final bool isEnabled; final bool isTestnet; final bool isHardware; diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index 46d6d38c47..1915842403 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -66,8 +66,7 @@ abstract class WalletListViewModelBase with Store { WalletType get currentWalletType => _appStore.wallet!.type; Future requireHardwareWalletConnection(WalletListItem walletItem) async => - _walletLoadingService.requireHardwareWalletConnection( - walletItem.type, walletItem.name); + _walletLoadingService.requireHardwareWalletConnection(walletItem.type, walletItem.name); @action Future loadWallet(WalletListItem walletItem) async { @@ -84,8 +83,8 @@ abstract class WalletListViewModelBase with Store { bool get ascending => _appStore.settingsStore.walletListAscending; - bool isUpdating = false; + @action Future updateList() async { if (isUpdating) { @@ -100,7 +99,7 @@ abstract class WalletListViewModelBase with Store { final list = await WalletInfo.getAll(); for (var info in list) { - wallets.add(convertWalletInfoToWalletListItem(info)); + wallets.add(await convertWalletInfoToWalletListItem(info)); } //========== Split into shared seed groups and single wallets list @@ -110,7 +109,7 @@ abstract class WalletListViewModelBase with Store { for (var group in walletGroupsFromManager) { if (group.wallets.length == 1) { - singleWalletsList.add(convertWalletInfoToWalletListItem(group.wallets.first)); + singleWalletsList.add(await convertWalletInfoToWalletListItem(group.wallets.first)); continue; } @@ -150,7 +149,7 @@ abstract class WalletListViewModelBase with Store { for (WalletInfo walletInfo in group.wallets) { for (int i = 0; i < wiList.length; i++) { if (wiList[i].name == walletInfo.name) { - wiList[i].sortOrder = i+oldI; + wiList[i].sortOrder = i + oldI; await wiList[i].save(); wiList.removeAt(i); break; @@ -234,13 +233,13 @@ abstract class WalletListViewModelBase with Store { } } - WalletListItem convertWalletInfoToWalletListItem(WalletInfo info) { + Future convertWalletInfoToWalletListItem(WalletInfo info) async { return WalletListItem( name: info.name, type: info.type, key: info.id, - isCurrent: info.name == _appStore.wallet?.name && - info.type == _appStore.wallet?.type, + balance: derivationInfoItem.balance, + isCurrent: info.name == _appStore.wallet?.name && info.type == _appStore.wallet?.type, isEnabled: availableWalletTypes.contains(info.type), isTestnet: info.network?.toLowerCase().contains('testnet') ?? false, isHardware: info.isHardwareWallet, diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index f28097b88c..e5dd4c50fe 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -48,6 +48,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> From d78ddc7182d1b0f0b384df285dbc34c57611f6a3 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Sat, 29 Nov 2025 13:47:36 +0100 Subject: [PATCH 62/68] feat: cached balance for wallet list page --- cw_bitcoin/lib/electrum_wallet.dart | 16 ++++++ cw_core/lib/wallet_info.dart | 54 +++++++++++++++--- cw_evm/lib/evm_chain_wallet.dart | 24 +++++++- cw_monero/lib/monero_wallet.dart | 16 ++++++ cw_nano/lib/nano_wallet.dart | 16 ++++++ cw_solana/lib/solana_wallet.dart | 16 ++++++ cw_tron/lib/tron_wallet.dart | 20 ++++++- cw_wownero/lib/wownero_wallet.dart | 18 ++++++ cw_zano/lib/zano_wallet.dart | 16 ++++++ lib/di.dart | 2 + lib/entities/default_settings_migration.dart | 13 +++++ lib/main.dart | 2 +- lib/new-ui/pages/wallets_page.dart | 24 +++++--- .../wallet_list/wallet_list_item.dart | 2 - .../wallet_list/wallet_list_view_model.dart | 56 ++++++++++++++++++- 15 files changed, 268 insertions(+), 27 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index a38879a8bf..33e1c04ed4 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -114,6 +114,19 @@ abstract class ElectrumWalletBase reaction((_) => syncStatus, _syncStatusReaction); sharedPrefs.complete(SharedPreferences.getInstance()); + + _onBalanceChangeReaction = reaction( + (_) => balance.entries.map((e) => e.value).toList(), + (_) { + for (final bal in balance.keys) { + if (balance[bal]?.formattedAvailableBalance != null) { + BalanceCache(bal.title, bal.tag ?? "", walletInfo.internalId, + balance[bal]!.formattedAvailableBalance) + .save(); + } + } + }, + ); } static Bip32Slip10Secp256k1 getAccountHDWallet( @@ -184,6 +197,8 @@ abstract class ElectrumWalletBase final EncryptionFileUtils encryptionFileUtils; + late final ReactionDisposer _onBalanceChangeReaction; + @override final String? passphrase; @@ -1583,6 +1598,7 @@ abstract class ElectrumWalletBase } catch (_) {} _autoSaveTimer?.cancel(); _updateFeeRateTimer?.cancel(); + _onBalanceChangeReaction.reaction.dispose(); } @action diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index 2fd18016b1..7101b3ee51 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -70,7 +70,7 @@ class WalletInfoAddressInfo { String address; String label; - static String get tableName => 'walletInfoAddressInfo'; + static String get tableName => 'walletInfoAddressInfo'; static String get selfIdColumn => "${tableName}Id"; static Future> selectList(int walletInfoId) async { @@ -134,8 +134,8 @@ class WalletInfoAddressMap { String addressKey; String addressValue; - static String get tableName => 'walletInfoAddressMap'; - static String get selfIdColumn => "${tableName}Id"; + static String get tableName => 'walletInfoAddressMap'; + static String get selfIdColumn => "${tableName}Id"; static Future> selectList(int walletInfoId) async { final query = await db.query(tableName, where: 'walletInfoId = ?', whereArgs: [walletInfoId]); @@ -184,7 +184,7 @@ class WalletInfoAddress { WalletInfoAddressType type; String address; - static String get tableName => 'walletInfoAddress'; + static String get tableName => 'walletInfoAddress'; static String get selfIdColumn => "${tableName}Id"; static Future> selectList(int walletInfoId, WalletInfoAddressType type) async { @@ -245,7 +245,7 @@ class DerivationInfo { int id; - static String get tableName => 'walletInfoDerivationInfo'; + static String get tableName => 'walletInfoDerivationInfo'; static String get selfIdColumn => "${tableName}Id"; String address; @@ -262,7 +262,7 @@ class DerivationInfo { columns: [ selfIdColumn, 'address', - 'balance', + 'balance', 'transactionsCount', 'derivationType', 'derivationPath', @@ -377,7 +377,7 @@ class WalletInfo { ); } - static String get tableName => 'walletInfo'; + static String get tableName => 'walletInfo'; static String get selfIdColumn => "${tableName}Id"; int internalId; @@ -495,7 +495,7 @@ class WalletInfo { String? parentAddress; String? hashedWalletIdentifier; bool isNonSeedWallet; - + int sortOrder; String get yatLastUsedAddress => yatLastUsedAddressRaw ?? ''; @@ -615,3 +615,41 @@ class WalletInfo { await save(); } } + +class BalanceCache { + final String title; + final String tag; + final int walletInfoId; + final String cachedBalance; + + static String get tableName => "BalanceCache"; + + const BalanceCache(this.title, this.tag, this.walletInfoId, this.cachedBalance); + + BalanceCache.fromJson(Map json) + : title = json['title'] as String, + tag = json['tag'] as String, + walletInfoId = json['walletInfoId'] as int, + cachedBalance = json['cachedBalance'] as String; + + Map toJson() => { + 'title': title, + 'tag': tag, + 'walletInfoId': walletInfoId, + 'cachedBalance': cachedBalance, + }; + + static Future> fromWalletId(int walletInfoId) async { + final list = await db.query( + tableName, + where: 'walletInfoId = ?', + whereArgs: [walletInfoId], + ); + return List.generate(list.length, (index) => BalanceCache.fromJson(list[index])); + } + + Future save() async { + final json = toJson(); + await db.insert(tableName, json, conflictAlgorithm: ConflictAlgorithm.replace); + } +} diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index 0db8781c7a..8ea2f84b74 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -98,6 +98,19 @@ abstract class EVMChainWalletBase } sharedPrefs.complete(SharedPreferences.getInstance()); + + _onBalanceChangeReaction = reaction( + (_) => balance.entries.map((e) => e.value).toList(), + (_) { + for (final bal in balance.keys) { + if (balance[bal]?.formattedAvailableBalance != null) { + BalanceCache(bal.title, bal.tag ?? "", walletInfo.internalId, + balance[bal]!.formattedAvailableBalance) + .save(); + } + } + }, + ); } final String? _mnemonic; @@ -105,6 +118,8 @@ abstract class EVMChainWalletBase final String _password; final EncryptionFileUtils encryptionFileUtils; + late final ReactionDisposer _onBalanceChangeReaction; + late final Box erc20TokensBox; late final Box evmChainErc20TokensBox; @@ -334,7 +349,8 @@ abstract class EVMChainWalletBase final gasUnits = await _client.getEstimatedGasUnitsForTransaction( senderAddress: evmChainPrivateKey.address, toAddress: evmChainPrivateKey.address, - contractAddress: _getUSDCContractAddress(), // Using USDC for default estimation + contractAddress: _getUSDCContractAddress(), + // Using USDC for default estimation gasPrice: EtherAmount.fromInt(EtherUnit.wei, gasPrice), value: EtherAmount.fromBigInt(EtherUnit.wei, BigInt.from(0.0000000001)), ); @@ -413,6 +429,7 @@ abstract class EVMChainWalletBase Future close({bool shouldCleanup = false}) async { _client.stop(); _transactionsUpdateTimer?.cancel(); + _onBalanceChangeReaction.reaction.dispose(); } @action @@ -609,7 +626,8 @@ abstract class EVMChainWalletBase ) async { // Estimate gas with the SAME call (sender, to, value, data) final gas = await calculateActualEstimatedFeeForCreateTransaction( - amount: valueWei, // native value (usually 0 for ERC20 transfer) + amount: valueWei, + // native value (usually 0 for ERC20 transfer) receivingAddressHex: to, priority: priority, contractAddress: null, @@ -806,7 +824,6 @@ abstract class EVMChainWalletBase Future _updateBalance() async { balance[currency] = await _fetchEVMChainBalance(); - await _fetchErc20Balances(); await save(); } @@ -882,6 +899,7 @@ abstract class EVMChainWalletBase @override Future? updateBalance() async => await _updateBalance(); + @override Future updateTransactionsHistory() async => await _updateTransactions(); diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 9f5986e267..db8bd490d2 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -96,6 +96,19 @@ abstract class MoneroWalletBase extends WalletBase transactionHistory, (__) { _updateSubAddress(isEnabledAutoGenerateSubaddress, account: walletAddresses.account); }); + + _onBalanceChangeReaction = reaction( + (_) => balance.entries.map((e) => e.value).toList(), + (_) { + for (final bal in balance.keys) { + if (balance[bal]?.formattedAvailableBalance != null) { + BalanceCache(bal.title, bal.tag ?? "", walletInfo.internalId, + balance[bal]!.formattedAvailableBalance) + .save(); + } + } + }, + ); } static const int _autoSaveInterval = 30; @@ -147,6 +160,8 @@ abstract class MoneroWalletBase extends WalletBase balance.entries.map((e) => e.value).toList(), + (_) { + for (final bal in balance.keys) { + if (balance[bal]?.formattedAvailableBalance != null) { + BalanceCache(bal.title, bal.tag ?? "", walletInfo.internalId, + balance[bal]!.formattedAvailableBalance) + .save(); + } + } + }, + ); } String _mnemonic; @@ -75,6 +88,8 @@ abstract class NanoWalletBase final EncryptionFileUtils _encryptionFileUtils; + late final ReactionDisposer _onBalanceChangeReaction; + String? _privateKey; String? _publicAddress; String? _hexSeed; @@ -154,6 +169,7 @@ abstract class NanoWalletBase Future close({bool shouldCleanup = false}) async { _client.stop(); _receiveTimer?.cancel(); + _onBalanceChangeReaction.reaction.dispose(); } @action diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index 49af6deca0..366b538f7d 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -70,6 +70,19 @@ abstract class SolanaWalletBase CakeHive.registerAdapter(SPLTokenAdapter()); } + _onBalanceChangeReaction = reaction( + (_) => balance.entries.map((e) => e.value).toList(), + (_) { + for (final bal in balance.keys) { + if (balance[bal]?.formattedAvailableBalance != null) { + BalanceCache(bal.title, bal.tag ?? "", walletInfo.internalId, + balance[bal]!.formattedAvailableBalance) + .save(); + } + } + }, + ); + _sharedPrefs.complete(SharedPreferences.getInstance()); } @@ -78,6 +91,8 @@ abstract class SolanaWalletBase final String? _hexPrivateKey; final EncryptionFileUtils encryptionFileUtils; + late final ReactionDisposer _onBalanceChangeReaction; + late final SolanaWalletClient _client; @observable @@ -185,6 +200,7 @@ abstract class SolanaWalletBase Future close({bool shouldCleanup = false}) async { _client.stop(); _transactionsUpdateTimer?.cancel(); + _onBalanceChangeReaction.reaction.dispose(); } @action diff --git a/cw_tron/lib/tron_wallet.dart b/cw_tron/lib/tron_wallet.dart index 80afb2f8ee..bf3a108248 100644 --- a/cw_tron/lib/tron_wallet.dart +++ b/cw_tron/lib/tron_wallet.dart @@ -66,6 +66,19 @@ abstract class TronWalletBase if (!CakeHive.isAdapterRegistered(TronToken.typeId)) { CakeHive.registerAdapter(TronTokenAdapter()); } + + _onBalanceChangeReaction = reaction( + (_) => balance.entries.map((e) => e.value).toList(), + (_) { + for (final bal in balance.keys) { + if (balance[bal]?.formattedAvailableBalance != null) { + BalanceCache(bal.title, bal.tag ?? "", walletInfo.internalId, + balance[bal]!.formattedAvailableBalance) + .save(); + } + } + }, + ); } final String? _mnemonic; @@ -73,6 +86,8 @@ abstract class TronWalletBase final String _password; final EncryptionFileUtils encryptionFileUtils; + late final ReactionDisposer _onBalanceChangeReaction; + late final Box tronTokensBox; late final TronPrivateKey _tronPrivateKey; @@ -228,7 +243,10 @@ abstract class TronWalletBase Future changePassword(String password) => throw UnimplementedError("changePassword"); @override - Future close({bool shouldCleanup = false}) async => _transactionsUpdateTimer?.cancel(); + Future close({bool shouldCleanup = false}) async { + _transactionsUpdateTimer?.cancel(); + _onBalanceChangeReaction.reaction.dispose(); + } @action @override diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index 08b7e65416..54223a81c4 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -89,12 +89,27 @@ abstract class WowneroWalletBase _onTxHistoryChangeReaction = reaction((_) => transactionHistory, (__) { _updateSubAddress(isEnabledAutoGenerateSubaddress, account: walletAddresses.account); }); + + _onBalanceChangeReaction = reaction( + (_) => balance.entries.map((e) => e.value).toList(), + (_) { + for (final bal in balance.keys) { + if (balance[bal]?.formattedAvailableBalance != null) { + BalanceCache(bal.title, bal.tag ?? "", walletInfo.internalId, + balance[bal]!.formattedAvailableBalance) + .save(); + } + } + }, + ); } static const int _autoSaveInterval = 30; Box unspentCoinsInfo; + late final ReactionDisposer _onBalanceChangeReaction; + void Function(FlutterErrorDetails)? onError; @override @@ -198,6 +213,7 @@ abstract class WowneroWalletBase _onAccountChangeReaction?.reaction.dispose(); _onTxHistoryChangeReaction?.reaction.dispose(); _autoSaveTimer?.cancel(); + _onBalanceChangeReaction.reaction.dispose(); } @override @@ -802,4 +818,6 @@ abstract class WowneroWalletBase String formatCryptoAmount(String amount) { return wowneroAmountToString(amount: int.parse(amount)); } + + } diff --git a/cw_zano/lib/zano_wallet.dart b/cw_zano/lib/zano_wallet.dart index 913bc083ad..06b88fa04f 100644 --- a/cw_zano/lib/zano_wallet.dart +++ b/cw_zano/lib/zano_wallet.dart @@ -78,6 +78,8 @@ abstract class ZanoWalletBase @observable ObservableMap balance; + late final ReactionDisposer _onBalanceChangeReaction; + @override String seed = ''; @@ -117,6 +119,19 @@ abstract class ZanoWalletBase if (!CakeHive.isAdapterRegistered(ZanoAsset.typeId)) { CakeHive.registerAdapter(ZanoAssetAdapter()); } + + _onBalanceChangeReaction = reaction( + (_) => balance.entries.map((e) => e.value).toList(), + (_) { + for (final bal in balance.keys) { + if (balance[bal]?.formattedAvailableBalance != null) { + BalanceCache(bal.title, bal.tag ?? "", walletInfo.internalId, + balance[bal]!.formattedAvailableBalance) + .save(); + } + } + }, + ); } @override @@ -217,6 +232,7 @@ abstract class ZanoWalletBase closeWallet(null); _updateSyncInfoTimer?.cancel(); _autoSaveTimer?.cancel(); + _onBalanceChangeReaction.reaction.dispose(); } @override diff --git a/lib/di.dart b/lib/di.dart index ed93b65587..f232d97283 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -874,6 +874,7 @@ Future setup({ getIt.get(), getIt.get(), getIt.get(), + getIt.get(), ), ); } else { @@ -884,6 +885,7 @@ Future setup({ getIt.get(), getIt.get(), getIt.get(), + getIt.get() ), ); } diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 9f2ac0bcea..0ec50fdf8f 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/haven_seed_store.dart'; import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/db/sqlite.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cake_wallet/entities/secret_store_key.dart'; import 'package:cw_core/root_dir.dart'; @@ -555,6 +556,18 @@ Future defaultSettingsMigration( currentNodePreferenceKey: PreferencesKey.currentArbitrumNodeIdKey, ); break; + case 54: + await db.execute(''' +CREATE TABLE BalanceCache ( + title TEXT NOT NULL, + tag TEXT DEFAULT "", + walletInfoId INTEGER NOT NULL, + cachedBalance TEXT DEFAULT "", + PRIMARY KEY (walletInfoId, title, tag), + FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId) +); + '''); + break; default: break; diff --git a/lib/main.dart b/lib/main.dart index 77430f08fe..6d96e67877 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -266,7 +266,7 @@ Future initializeAppConfigs({bool loadWallet = true}) async { payjoinSessionSource: payjoinSessionSource, anonpayInvoiceInfo: anonpayInvoiceInfo, havenSeedStore: havenSeedStore, - initialMigrationVersion: 53, + initialMigrationVersion: 54, ); } diff --git a/lib/new-ui/pages/wallets_page.dart b/lib/new-ui/pages/wallets_page.dart index 36b56e6cbb..7efc7b8b5a 100644 --- a/lib/new-ui/pages/wallets_page.dart +++ b/lib/new-ui/pages/wallets_page.dart @@ -8,18 +8,24 @@ class NewWalletListPage extends StatelessWidget { final WalletListViewModel walletListViewModel; - @override Widget build(BuildContext context) { return SafeArea( - child: ListView.builder( - itemCount: walletListViewModel.wallets.length, - itemBuilder: (context, index){ - final wallet = walletListViewModel.wallets[index]; + child: ListView.builder( + itemCount: walletListViewModel.wallets.length, + itemBuilder: (context, index) { + final wallet = walletListViewModel.wallets[index]; + final balance = + walletListViewModel.cachedBalanceFor(walletTypeToCryptoCurrency(wallet.type)); + final fiatBalance = + walletListViewModel.fiatCachedBalanceFor(walletTypeToCryptoCurrency(wallet.type)); - return AssetTile(iconPath: walletTypeToCryptoCurrency(wallet.type).iconPath!, name: wallet.name, amount: "123 BTC", amountFiat: "123 USD"); - }, - ) - ); + return AssetTile( + iconPath: walletTypeToCryptoCurrency(wallet.type).iconPath!, + name: wallet.name, + amount: balance, + amountFiat: fiatBalance); + }, + )); } } diff --git a/lib/view_model/wallet_list/wallet_list_item.dart b/lib/view_model/wallet_list/wallet_list_item.dart index 46e814f073..8f8c58ea9f 100644 --- a/lib/view_model/wallet_list/wallet_list_item.dart +++ b/lib/view_model/wallet_list/wallet_list_item.dart @@ -6,7 +6,6 @@ class WalletListItem { required this.type, required this.key, required this.isHardware, - this.balance, this.isCurrent = false, this.isEnabled = true, this.isTestnet = false, @@ -16,7 +15,6 @@ class WalletListItem { final WalletType type; final bool isCurrent; final dynamic key; - final String? balance; final bool isEnabled; final bool isTestnet; final bool isHardware; diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index 1915842403..49c9ce83cc 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -1,13 +1,19 @@ +import 'dart:async'; +import 'package:cake_wallet/core/fiat_conversion_service.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; +import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; +import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/wallet_group.dart'; import 'package:cake_wallet/entities/wallet_list_order_types.dart'; import 'package:cake_wallet/entities/wallet_manager.dart'; +import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/currency_for_wallet_type.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cw_core/utils/print_verbose.dart'; import 'package:cake_wallet/wallet_types.g.dart'; part 'wallet_list_view_model.g.dart'; @@ -19,17 +25,30 @@ abstract class WalletListViewModelBase with Store { this._appStore, this._walletLoadingService, this._walletManager, + this.fiatConversionStore, ) : wallets = ObservableList(), multiWalletGroups = ObservableList(), singleWalletsList = ObservableList(), - expansionTileStateTrack = ObservableMap() { + expansionTileStateTrack = ObservableMap(), + cachedBalances = ObservableList() { setOrderType(_appStore.settingsStore.walletListOrder); updateList(); + + _updateFiatStore(); + Timer.periodic( + Duration(seconds: 5), + (timer) => _updateFiatStore(), + ); } + final FiatConversionStore fiatConversionStore; + @observable ObservableList wallets; + @observable + ObservableList cachedBalances; + // @observable // ObservableList walletGroups; @@ -51,6 +70,37 @@ abstract class WalletListViewModelBase with Store { } } + String cachedBalanceFor(CryptoCurrency currency) => cachedBalances + .where((element) => + (element.tag == currency.tag || element.tag == "" && currency.tag == null) && + element.title == currency.title) + .first + .cachedBalance; + + Future _updateFiatStoreForCurrency(CryptoCurrency currency) async { + fiatConversionStore.prices[currency] = await FiatConversionService.fetchPrice( + crypto: currency, + fiat: _appStore.settingsStore.fiatCurrency, + torOnly: _appStore.settingsStore.fiatApiMode == FiatApiMode.torOnly); + } + + + Future _updateFiatStore() async { + for (final wallet in wallets) { + final currency = walletTypeToCryptoCurrency(wallet.type); + _updateFiatStoreForCurrency(currency); + } + } + + String fiatCachedBalanceFor(CryptoCurrency currency) { + if (fiatConversionStore.prices[currency] == null) { + _updateFiatStoreForCurrency(currency); + } + + final price = fiatConversionStore.prices[currency]; + return calculateFiatAmount(cryptoAmount: cachedBalanceFor(currency), price: price); + } + @computed bool get shouldRequireTOTP2FAForAccessingWallet => _appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet; @@ -100,6 +150,7 @@ abstract class WalletListViewModelBase with Store { for (var info in list) { wallets.add(await convertWalletInfoToWalletListItem(info)); + cachedBalances.addAll(await BalanceCache.fromWalletId(info.internalId)); } //========== Split into shared seed groups and single wallets list @@ -238,7 +289,6 @@ abstract class WalletListViewModelBase with Store { name: info.name, type: info.type, key: info.id, - balance: derivationInfoItem.balance, isCurrent: info.name == _appStore.wallet?.name && info.type == _appStore.wallet?.type, isEnabled: availableWalletTypes.contains(info.type), isTestnet: info.network?.toLowerCase().contains('testnet') ?? false, From 80a1928aefdc0ff34937ee1b51cdf9a7dbc60bcb Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Sun, 30 Nov 2025 16:30:17 +0100 Subject: [PATCH 63/68] feat: sync all wallets from wallets page to update balance cache --- lib/new-ui/pages/wallets_page.dart | 53 +++++++++++++------ lib/new-ui/widgets/asset_tile.dart | 19 ++++--- .../assets_history/assets_section.dart | 3 ++ .../widgets/wallets_page/total_balance.dart | 24 +++++++++ .../wallet_list/wallet_list_view_model.dart | 39 +++++++++++++- 5 files changed, 113 insertions(+), 25 deletions(-) create mode 100644 lib/new-ui/widgets/wallets_page/total_balance.dart diff --git a/lib/new-ui/pages/wallets_page.dart b/lib/new-ui/pages/wallets_page.dart index 7efc7b8b5a..a9133646ca 100644 --- a/lib/new-ui/pages/wallets_page.dart +++ b/lib/new-ui/pages/wallets_page.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/new-ui/widgets/asset_tile.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; class NewWalletListPage extends StatelessWidget { const NewWalletListPage({super.key, required this.walletListViewModel}); @@ -10,22 +11,44 @@ class NewWalletListPage extends StatelessWidget { @override Widget build(BuildContext context) { + final fiatCurrency = walletListViewModel.fiatCurrency; + return SafeArea( - child: ListView.builder( - itemCount: walletListViewModel.wallets.length, - itemBuilder: (context, index) { - final wallet = walletListViewModel.wallets[index]; - final balance = - walletListViewModel.cachedBalanceFor(walletTypeToCryptoCurrency(wallet.type)); - final fiatBalance = - walletListViewModel.fiatCachedBalanceFor(walletTypeToCryptoCurrency(wallet.type)); + child: Column( + children: [ + Expanded( + child: RefreshIndicator( + onRefresh: () async { + walletListViewModel.refreshCachedBalances(); + }, + child: Observer( + builder: (_) => ListView.builder( + itemCount: walletListViewModel.wallets.length, + itemBuilder: (context, index) { + return Observer( + builder: (_) { + final wallet = walletListViewModel.wallets[index]; + final currency = walletTypeToCryptoCurrency(wallet.type); + final balance = walletListViewModel.cachedBalanceFor(currency); + final fiatBalance = walletListViewModel.fiatCachedBalanceFor(currency); + final cacheUpdateStatus = walletListViewModel.cacheUpdateStatuses[index]; - return AssetTile( - iconPath: walletTypeToCryptoCurrency(wallet.type).iconPath!, - name: wallet.name, - amount: balance, - amountFiat: fiatBalance); - }, - )); + return AssetTile( + iconPath: currency.iconPath!, + name: wallet.name, + amount: "$balance ${currency.name.toUpperCase()}", + amountFiat: "$fiatBalance $fiatCurrency", + showLoading: !cacheUpdateStatus, + ); + }, + ); + }, + ), + ), + ), + ), + ], + ), + ); } } diff --git a/lib/new-ui/widgets/asset_tile.dart b/lib/new-ui/widgets/asset_tile.dart index 914a2d8bca..3eeebe609a 100644 --- a/lib/new-ui/widgets/asset_tile.dart +++ b/lib/new-ui/widgets/asset_tile.dart @@ -1,4 +1,3 @@ -import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:flutter/material.dart'; class AssetTile extends StatelessWidget { @@ -7,12 +6,14 @@ class AssetTile extends StatelessWidget { required this.iconPath, required this.name, required this.amount, - required this.amountFiat}); + required this.amountFiat, + required this.showLoading}); final String iconPath; final String name; final String amount; final String amountFiat; + final bool showLoading; @override Widget build(BuildContext context) { @@ -66,12 +67,14 @@ class AssetTile extends StatelessWidget { ), ], ), - Text( - amountFiat, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), + showLoading + ? CircularProgressIndicator() + : Text( + amountFiat, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), ], ), ), diff --git a/lib/new-ui/widgets/coins_page/assets_history/assets_section.dart b/lib/new-ui/widgets/coins_page/assets_history/assets_section.dart index cabe08986f..7de6df6b86 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/assets_section.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/assets_section.dart @@ -20,6 +20,9 @@ class AssetsSection extends StatelessWidget { name: "DummyCoin", amount: "0.000 DMC", amountFiat: "\$ 0.00", + // don't worry about this, it's mostly for wallets page + // unless you load each asset's balance separately for some reason? + showLoading: false, ); }, ); diff --git a/lib/new-ui/widgets/wallets_page/total_balance.dart b/lib/new-ui/widgets/wallets_page/total_balance.dart new file mode 100644 index 0000000000..3cb08efdc0 --- /dev/null +++ b/lib/new-ui/widgets/wallets_page/total_balance.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class TotalBalanceWidget extends StatelessWidget { + const TotalBalanceWidget({super.key, required this.totalBalance, required this.currency}); + + final String totalBalance; + final String currency; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Column( + children: [ + Text("Total assets"), + Row( + children: [Text(totalBalance), Text(currency)], + ) + ], + ) + ], + ); + } +} diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index 49c9ce83cc..6c589d52f2 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -3,6 +3,7 @@ import 'package:cake_wallet/core/fiat_conversion_service.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/entities/wallet_group.dart'; import 'package:cake_wallet/entities/wallet_list_order_types.dart'; import 'package:cake_wallet/entities/wallet_manager.dart'; @@ -30,7 +31,8 @@ abstract class WalletListViewModelBase with Store { multiWalletGroups = ObservableList(), singleWalletsList = ObservableList(), expansionTileStateTrack = ObservableMap(), - cachedBalances = ObservableList() { + cachedBalances = ObservableList(), + cacheUpdateStatuses = ObservableList() { setOrderType(_appStore.settingsStore.walletListOrder); updateList(); @@ -49,6 +51,9 @@ abstract class WalletListViewModelBase with Store { @observable ObservableList cachedBalances; + @observable + ObservableList cacheUpdateStatuses; + // @observable // ObservableList walletGroups; @@ -84,7 +89,6 @@ abstract class WalletListViewModelBase with Store { torOnly: _appStore.settingsStore.fiatApiMode == FiatApiMode.torOnly); } - Future _updateFiatStore() async { for (final wallet in wallets) { final currency = walletTypeToCryptoCurrency(wallet.type); @@ -101,6 +105,16 @@ abstract class WalletListViewModelBase with Store { return calculateFiatAmount(cryptoAmount: cachedBalanceFor(currency), price: price); } + String totalFiatBalance() { + double ret = 0; + + for (final wallet in wallets) { + ret += double.parse(fiatCachedBalanceFor(walletTypeToCryptoCurrency(wallet.type))); + } + + return ret.toString(); + } + @computed bool get shouldRequireTOTP2FAForAccessingWallet => _appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet; @@ -109,6 +123,9 @@ abstract class WalletListViewModelBase with Store { bool get shouldRequireTOTP2FAForCreatingNewWallets => _appStore.settingsStore.shouldRequireTOTP2FAForCreatingNewWallets; + @computed + FiatCurrency get fiatCurrency => _appStore.settingsStore.fiatCurrency; + final AppStore _appStore; final WalletManager _walletManager; final WalletLoadingService _walletLoadingService; @@ -145,12 +162,14 @@ abstract class WalletListViewModelBase with Store { wallets.clear(); multiWalletGroups.clear(); singleWalletsList.clear(); + cacheUpdateStatuses.clear(); final list = await WalletInfo.getAll(); for (var info in list) { wallets.add(await convertWalletInfoToWalletListItem(info)); cachedBalances.addAll(await BalanceCache.fromWalletId(info.internalId)); + cacheUpdateStatuses.add(true); } //========== Split into shared seed groups and single wallets list @@ -171,6 +190,22 @@ abstract class WalletListViewModelBase with Store { } } + @action + Future refreshCachedBalances() async { + for (final wallet in wallets) { + cacheUpdateStatuses[wallets.indexOf(wallet)] = false; + + final tmpWallet = await _walletLoadingService.load(wallet.type, wallet.name); + await tmpWallet.startSync(); + while (tmpWallet.syncStatus.progress() < 1.0) { + await Future.delayed(Duration(milliseconds: 100)); + } + await tmpWallet.close(); + + cacheUpdateStatuses[wallets.indexOf(wallet)] = true; + } + } + Future reorderAccordingToWalletList() async { if (wallets.isEmpty) { await updateList(); From 09eeee5355910ef25c4592e5623830db6f12563b Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Sun, 30 Nov 2025 18:18:47 +0100 Subject: [PATCH 64/68] fix: clearer name for sync status bool list --- lib/new-ui/pages/wallets_page.dart | 2 +- .../wallet_list/wallet_list_view_model.dart | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/new-ui/pages/wallets_page.dart b/lib/new-ui/pages/wallets_page.dart index a9133646ca..fa63ccbc37 100644 --- a/lib/new-ui/pages/wallets_page.dart +++ b/lib/new-ui/pages/wallets_page.dart @@ -31,7 +31,7 @@ class NewWalletListPage extends StatelessWidget { final currency = walletTypeToCryptoCurrency(wallet.type); final balance = walletListViewModel.cachedBalanceFor(currency); final fiatBalance = walletListViewModel.fiatCachedBalanceFor(currency); - final cacheUpdateStatus = walletListViewModel.cacheUpdateStatuses[index]; + final cacheUpdateStatus = walletListViewModel.isBalanceCacheSynced[index]; return AssetTile( iconPath: currency.iconPath!, diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index 6c589d52f2..571d0d3430 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -32,7 +32,7 @@ abstract class WalletListViewModelBase with Store { singleWalletsList = ObservableList(), expansionTileStateTrack = ObservableMap(), cachedBalances = ObservableList(), - cacheUpdateStatuses = ObservableList() { + isBalanceCacheSynced = ObservableList() { setOrderType(_appStore.settingsStore.walletListOrder); updateList(); @@ -52,7 +52,7 @@ abstract class WalletListViewModelBase with Store { ObservableList cachedBalances; @observable - ObservableList cacheUpdateStatuses; + ObservableList isBalanceCacheSynced; // @observable // ObservableList walletGroups; @@ -162,14 +162,14 @@ abstract class WalletListViewModelBase with Store { wallets.clear(); multiWalletGroups.clear(); singleWalletsList.clear(); - cacheUpdateStatuses.clear(); + isBalanceCacheSynced.clear(); final list = await WalletInfo.getAll(); for (var info in list) { wallets.add(await convertWalletInfoToWalletListItem(info)); cachedBalances.addAll(await BalanceCache.fromWalletId(info.internalId)); - cacheUpdateStatuses.add(true); + isBalanceCacheSynced.add(true); } //========== Split into shared seed groups and single wallets list @@ -193,7 +193,7 @@ abstract class WalletListViewModelBase with Store { @action Future refreshCachedBalances() async { for (final wallet in wallets) { - cacheUpdateStatuses[wallets.indexOf(wallet)] = false; + isBalanceCacheSynced[wallets.indexOf(wallet)] = false; final tmpWallet = await _walletLoadingService.load(wallet.type, wallet.name); await tmpWallet.startSync(); @@ -202,7 +202,7 @@ abstract class WalletListViewModelBase with Store { } await tmpWallet.close(); - cacheUpdateStatuses[wallets.indexOf(wallet)] = true; + isBalanceCacheSynced[wallets.indexOf(wallet)] = true; } } From 8b9c8cecc5a2a78180bc860f6603e7fb0227dc41 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Sun, 30 Nov 2025 18:22:10 +0100 Subject: [PATCH 65/68] fix: restore previous behavior of wallet list page --- .../screens/wallet_list/wallet_list_page.dart | 217 ++++++++---------- .../wallet_list/wallet_list_view_model.dart | 2 +- 2 files changed, 102 insertions(+), 117 deletions(-) diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index 48a81b6e8c..ec0a54664a 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -29,7 +29,6 @@ import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -49,9 +48,9 @@ class WalletListPage extends BasePage { String get title => S.current.wallets; @override - Widget body(BuildContext context) => Observer(builder: (_) { - if (walletListViewModel.singleWalletsList.isEmpty && - walletListViewModel.multiWalletGroups.isEmpty) { + Widget body(BuildContext context) => Observer( + builder: (_) { + if (walletListViewModel.singleWalletsList.isEmpty && walletListViewModel.multiWalletGroups.isEmpty) { return Center( child: CircularProgressIndicator(), ); @@ -61,7 +60,8 @@ class WalletListPage extends BasePage { authService: authService, onWalletLoaded: onWalletLoaded ?? (context) => Navigator.of(context).pop(), ); - }); + } + ); @override Widget trailing(BuildContext context) { @@ -153,10 +153,10 @@ class WalletListBodyState extends State { child: Text( S.current.shared_seed_wallet_groups, style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 18, - fontWeight: FontWeight.w700, - color: Theme.of(context).colorScheme.onSurface, - ), + fontSize: 18, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.onSurface, + ), ), ), SizedBox(height: 16), @@ -165,91 +165,76 @@ class WalletListBodyState extends State { padding: EdgeInsets.only(left: 20, right: 20), child: Observer( builder: (_) => FilteredList( - shrinkWrap: true, - list: widget.walletListViewModel.multiWalletGroups, - updateFunction: widget.walletListViewModel.reorderAccordingToWalletList, - itemBuilder: (context, index) { - final group = widget.walletListViewModel.multiWalletGroups[index]; - final groupName = - group.groupName ?? '${S.current.wallet_group} ${index + 1}'; - - widget.walletListViewModel.updateTileState( - index, - widget.walletListViewModel.expansionTileStateTrack[index] ?? false, - ); - - return FutureBuilder>( - future: Future.wait( - group.wallets.map((w) { - return widget.walletListViewModel - .convertWalletInfoToWalletListItem(w); - }), - ), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return SizedBox( - height: 60, - child: Center(child: CircularProgressIndicator()), - ); - } + shrinkWrap: true, + list: widget.walletListViewModel.multiWalletGroups, + updateFunction: widget.walletListViewModel.reorderAccordingToWalletList, + itemBuilder: (context, index) { + final group = widget.walletListViewModel.multiWalletGroups[index]; + final groupName = + group.groupName ?? '${S.current.wallet_group} ${index + 1}'; - final childWallets = snapshot.data!; + widget.walletListViewModel.updateTileState( + index, + widget.walletListViewModel.expansionTileStateTrack[index] ?? false, + ); - return GroupedWalletExpansionTile( - onExpansionChanged: (value) { - widget.walletListViewModel.updateTileState(index, value); - setState(() {}); - }, - shouldShowCurrentWalletPointer: true, - borderRadius: BorderRadius.all(Radius.circular(16)), - title: groupName, - tileKey: ValueKey('group_wallets_expansion_tile_widget_$index'), - leadingWidget: Icon( - Icons.account_balance_wallet_outlined, - size: 28, - ), - trailingWidget: EditWalletButtonWidget( - width: 88, - isGroup: true, - isExpanded: widget - .walletListViewModel.expansionTileStateTrack[index]!, - onTap: () async { - final wallet = await widget.walletListViewModel - .convertWalletInfoToWalletListItem(group.wallets.first); - Navigator.of(context).pushNamed( - Routes.walletEdit, - arguments: WalletEditPageArguments( - walletListViewModel: widget.walletListViewModel, - editingWallet: wallet, - isWalletGroup: true, - groupName: groupName, - walletGroupKey: group.groupKey, - ), - ); - }, + return GroupedWalletExpansionTile( + onExpansionChanged: (value) { + widget.walletListViewModel.updateTileState(index, value); + setState(() {}); + }, + shouldShowCurrentWalletPointer: true, + borderRadius: BorderRadius.all(Radius.circular(16)), + title: groupName, + tileKey: ValueKey('group_wallets_expansion_tile_widget_$index'), + leadingWidget: Icon( + Icons.account_balance_wallet_outlined, + size: 28, + ), + trailingWidget: EditWalletButtonWidget( + width: 88, + isGroup: true, + isExpanded: + widget.walletListViewModel.expansionTileStateTrack[index]!, + onTap: () { + final wallet = widget.walletListViewModel + .convertWalletInfoToWalletListItem(group.wallets.first); + Navigator.of(context).pushNamed( + Routes.walletEdit, + arguments: WalletEditPageArguments( + walletListViewModel: widget.walletListViewModel, + editingWallet: wallet, + isWalletGroup: true, + groupName: groupName, + walletGroupKey: group.groupKey, ), - childWallets: childWallets, - isSelected: false, - onChildItemTapped: (wallet) => - wallet.isCurrent ? null : _loadWallet(wallet), - childTrailingWidget: (item) { - return item.isCurrent - ? SizedBox.shrink() - : EditWalletButtonWidget( - width: 60, - onTap: () => Navigator.of(context).pushNamed( - Routes.walletEdit, - arguments: WalletEditPageArguments( - walletListViewModel: widget.walletListViewModel, - editingWallet: item, - ), - ), - ); - }, ); }, - ); - }), + ), + childWallets: group.wallets.map((walletInfo) { + return widget.walletListViewModel + .convertWalletInfoToWalletListItem(walletInfo); + }).toList(), + isSelected: false, + onChildItemTapped: (wallet) => + wallet.isCurrent ? null : _loadWallet(wallet), + childTrailingWidget: (item) { + return item.isCurrent + ? SizedBox.shrink() + : EditWalletButtonWidget( + width: 60, + onTap: () => Navigator.of(context).pushNamed( + Routes.walletEdit, + arguments: WalletEditPageArguments( + walletListViewModel: widget.walletListViewModel, + editingWallet: item, + ), + ), + ); + }, + ); + }, + ), ), ), ), @@ -261,10 +246,10 @@ class WalletListBodyState extends State { child: Text( S.current.single_seed_wallets_group, style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 18, - fontWeight: FontWeight.w700, - color: Theme.of(context).colorScheme.onSurface, - ), + fontSize: 18, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.onSurface, + ), ), ), SizedBox(height: 16), @@ -290,17 +275,17 @@ class WalletListBodyState extends State { children: [ wallet.isCurrent ? Container( - height: 35, - width: 6, - margin: EdgeInsets.only(right: 16), - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topRight: Radius.circular(16), - bottomRight: Radius.circular(16), - ), - color: currentColor, - ), - ) + height: 35, + width: 6, + margin: EdgeInsets.only(right: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topRight: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + color: currentColor, + ), + ) : SizedBox(width: 6), Image.asset( walletTypeToCryptoCurrency(wallet.type).iconPath!, @@ -318,17 +303,17 @@ class WalletListBodyState extends State { trailingWidget: wallet.isCurrent ? null : EditWalletButtonWidget( - width: 64, - onTap: () { - Navigator.of(context).pushNamed( - Routes.walletEdit, - arguments: WalletEditPageArguments( - walletListViewModel: widget.walletListViewModel, - editingWallet: wallet, - ), - ); - }, + width: 64, + onTap: () { + Navigator.of(context).pushNamed( + Routes.walletEdit, + arguments: WalletEditPageArguments( + walletListViewModel: widget.walletListViewModel, + editingWallet: wallet, ), + ); + }, + ), ); }, ), @@ -489,7 +474,7 @@ class WalletListBodyState extends State { try { final requireHardwareWalletConnection = - await widget.walletListViewModel.requireHardwareWalletConnection(wallet); + await widget.walletListViewModel.requireHardwareWalletConnection(wallet); if (requireHardwareWalletConnection) { bool didConnect = false; await Navigator.of(context).pushNamed( @@ -545,7 +530,7 @@ class WalletListBodyState extends State { } }, conditionToDetermineIfToUse2FA: - widget.walletListViewModel.shouldRequireTOTP2FAForAccessingWallet, + widget.walletListViewModel.shouldRequireTOTP2FAForAccessingWallet, ); } diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index 571d0d3430..9dcb7c4b92 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -319,7 +319,7 @@ abstract class WalletListViewModelBase with Store { } } - Future convertWalletInfoToWalletListItem(WalletInfo info) async { + WalletListItem convertWalletInfoToWalletListItem(WalletInfo info) { return WalletListItem( name: info.name, type: info.type, From 1e10edb04047f4f45c3a21158d733d9fce6f619d Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Sun, 30 Nov 2025 18:22:10 +0100 Subject: [PATCH 66/68] fix: restore previous behavior of wallet list page --- .../screens/wallet_list/wallet_list_page.dart | 217 ++++++++---------- .../wallet_list/wallet_list_view_model.dart | 13 +- 2 files changed, 108 insertions(+), 122 deletions(-) diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index 48a81b6e8c..ec0a54664a 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -29,7 +29,6 @@ import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -49,9 +48,9 @@ class WalletListPage extends BasePage { String get title => S.current.wallets; @override - Widget body(BuildContext context) => Observer(builder: (_) { - if (walletListViewModel.singleWalletsList.isEmpty && - walletListViewModel.multiWalletGroups.isEmpty) { + Widget body(BuildContext context) => Observer( + builder: (_) { + if (walletListViewModel.singleWalletsList.isEmpty && walletListViewModel.multiWalletGroups.isEmpty) { return Center( child: CircularProgressIndicator(), ); @@ -61,7 +60,8 @@ class WalletListPage extends BasePage { authService: authService, onWalletLoaded: onWalletLoaded ?? (context) => Navigator.of(context).pop(), ); - }); + } + ); @override Widget trailing(BuildContext context) { @@ -153,10 +153,10 @@ class WalletListBodyState extends State { child: Text( S.current.shared_seed_wallet_groups, style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 18, - fontWeight: FontWeight.w700, - color: Theme.of(context).colorScheme.onSurface, - ), + fontSize: 18, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.onSurface, + ), ), ), SizedBox(height: 16), @@ -165,91 +165,76 @@ class WalletListBodyState extends State { padding: EdgeInsets.only(left: 20, right: 20), child: Observer( builder: (_) => FilteredList( - shrinkWrap: true, - list: widget.walletListViewModel.multiWalletGroups, - updateFunction: widget.walletListViewModel.reorderAccordingToWalletList, - itemBuilder: (context, index) { - final group = widget.walletListViewModel.multiWalletGroups[index]; - final groupName = - group.groupName ?? '${S.current.wallet_group} ${index + 1}'; - - widget.walletListViewModel.updateTileState( - index, - widget.walletListViewModel.expansionTileStateTrack[index] ?? false, - ); - - return FutureBuilder>( - future: Future.wait( - group.wallets.map((w) { - return widget.walletListViewModel - .convertWalletInfoToWalletListItem(w); - }), - ), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return SizedBox( - height: 60, - child: Center(child: CircularProgressIndicator()), - ); - } + shrinkWrap: true, + list: widget.walletListViewModel.multiWalletGroups, + updateFunction: widget.walletListViewModel.reorderAccordingToWalletList, + itemBuilder: (context, index) { + final group = widget.walletListViewModel.multiWalletGroups[index]; + final groupName = + group.groupName ?? '${S.current.wallet_group} ${index + 1}'; - final childWallets = snapshot.data!; + widget.walletListViewModel.updateTileState( + index, + widget.walletListViewModel.expansionTileStateTrack[index] ?? false, + ); - return GroupedWalletExpansionTile( - onExpansionChanged: (value) { - widget.walletListViewModel.updateTileState(index, value); - setState(() {}); - }, - shouldShowCurrentWalletPointer: true, - borderRadius: BorderRadius.all(Radius.circular(16)), - title: groupName, - tileKey: ValueKey('group_wallets_expansion_tile_widget_$index'), - leadingWidget: Icon( - Icons.account_balance_wallet_outlined, - size: 28, - ), - trailingWidget: EditWalletButtonWidget( - width: 88, - isGroup: true, - isExpanded: widget - .walletListViewModel.expansionTileStateTrack[index]!, - onTap: () async { - final wallet = await widget.walletListViewModel - .convertWalletInfoToWalletListItem(group.wallets.first); - Navigator.of(context).pushNamed( - Routes.walletEdit, - arguments: WalletEditPageArguments( - walletListViewModel: widget.walletListViewModel, - editingWallet: wallet, - isWalletGroup: true, - groupName: groupName, - walletGroupKey: group.groupKey, - ), - ); - }, + return GroupedWalletExpansionTile( + onExpansionChanged: (value) { + widget.walletListViewModel.updateTileState(index, value); + setState(() {}); + }, + shouldShowCurrentWalletPointer: true, + borderRadius: BorderRadius.all(Radius.circular(16)), + title: groupName, + tileKey: ValueKey('group_wallets_expansion_tile_widget_$index'), + leadingWidget: Icon( + Icons.account_balance_wallet_outlined, + size: 28, + ), + trailingWidget: EditWalletButtonWidget( + width: 88, + isGroup: true, + isExpanded: + widget.walletListViewModel.expansionTileStateTrack[index]!, + onTap: () { + final wallet = widget.walletListViewModel + .convertWalletInfoToWalletListItem(group.wallets.first); + Navigator.of(context).pushNamed( + Routes.walletEdit, + arguments: WalletEditPageArguments( + walletListViewModel: widget.walletListViewModel, + editingWallet: wallet, + isWalletGroup: true, + groupName: groupName, + walletGroupKey: group.groupKey, ), - childWallets: childWallets, - isSelected: false, - onChildItemTapped: (wallet) => - wallet.isCurrent ? null : _loadWallet(wallet), - childTrailingWidget: (item) { - return item.isCurrent - ? SizedBox.shrink() - : EditWalletButtonWidget( - width: 60, - onTap: () => Navigator.of(context).pushNamed( - Routes.walletEdit, - arguments: WalletEditPageArguments( - walletListViewModel: widget.walletListViewModel, - editingWallet: item, - ), - ), - ); - }, ); }, - ); - }), + ), + childWallets: group.wallets.map((walletInfo) { + return widget.walletListViewModel + .convertWalletInfoToWalletListItem(walletInfo); + }).toList(), + isSelected: false, + onChildItemTapped: (wallet) => + wallet.isCurrent ? null : _loadWallet(wallet), + childTrailingWidget: (item) { + return item.isCurrent + ? SizedBox.shrink() + : EditWalletButtonWidget( + width: 60, + onTap: () => Navigator.of(context).pushNamed( + Routes.walletEdit, + arguments: WalletEditPageArguments( + walletListViewModel: widget.walletListViewModel, + editingWallet: item, + ), + ), + ); + }, + ); + }, + ), ), ), ), @@ -261,10 +246,10 @@ class WalletListBodyState extends State { child: Text( S.current.single_seed_wallets_group, style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 18, - fontWeight: FontWeight.w700, - color: Theme.of(context).colorScheme.onSurface, - ), + fontSize: 18, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.onSurface, + ), ), ), SizedBox(height: 16), @@ -290,17 +275,17 @@ class WalletListBodyState extends State { children: [ wallet.isCurrent ? Container( - height: 35, - width: 6, - margin: EdgeInsets.only(right: 16), - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topRight: Radius.circular(16), - bottomRight: Radius.circular(16), - ), - color: currentColor, - ), - ) + height: 35, + width: 6, + margin: EdgeInsets.only(right: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topRight: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + color: currentColor, + ), + ) : SizedBox(width: 6), Image.asset( walletTypeToCryptoCurrency(wallet.type).iconPath!, @@ -318,17 +303,17 @@ class WalletListBodyState extends State { trailingWidget: wallet.isCurrent ? null : EditWalletButtonWidget( - width: 64, - onTap: () { - Navigator.of(context).pushNamed( - Routes.walletEdit, - arguments: WalletEditPageArguments( - walletListViewModel: widget.walletListViewModel, - editingWallet: wallet, - ), - ); - }, + width: 64, + onTap: () { + Navigator.of(context).pushNamed( + Routes.walletEdit, + arguments: WalletEditPageArguments( + walletListViewModel: widget.walletListViewModel, + editingWallet: wallet, ), + ); + }, + ), ); }, ), @@ -489,7 +474,7 @@ class WalletListBodyState extends State { try { final requireHardwareWalletConnection = - await widget.walletListViewModel.requireHardwareWalletConnection(wallet); + await widget.walletListViewModel.requireHardwareWalletConnection(wallet); if (requireHardwareWalletConnection) { bool didConnect = false; await Navigator.of(context).pushNamed( @@ -545,7 +530,7 @@ class WalletListBodyState extends State { } }, conditionToDetermineIfToUse2FA: - widget.walletListViewModel.shouldRequireTOTP2FAForAccessingWallet, + widget.walletListViewModel.shouldRequireTOTP2FAForAccessingWallet, ); } diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index 571d0d3430..9ceb176f08 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -1,4 +1,5 @@ import 'dart:async'; + import 'package:cake_wallet/core/fiat_conversion_service.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; @@ -7,15 +8,15 @@ import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/entities/wallet_group.dart'; import 'package:cake_wallet/entities/wallet_list_order_types.dart'; import 'package:cake_wallet/entities/wallet_manager.dart'; +import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; +import 'package:cake_wallet/wallet_types.g.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/store/app_store.dart'; -import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/wallet_types.g.dart'; +import 'package:mobx/mobx.dart'; part 'wallet_list_view_model.g.dart'; @@ -179,7 +180,7 @@ abstract class WalletListViewModelBase with Store { for (var group in walletGroupsFromManager) { if (group.wallets.length == 1) { - singleWalletsList.add(await convertWalletInfoToWalletListItem(group.wallets.first)); + singleWalletsList.add(convertWalletInfoToWalletListItem(group.wallets.first)); continue; } @@ -319,7 +320,7 @@ abstract class WalletListViewModelBase with Store { } } - Future convertWalletInfoToWalletListItem(WalletInfo info) async { + WalletListItem convertWalletInfoToWalletListItem(WalletInfo info) { return WalletListItem( name: info.name, type: info.type, From ea979cf819c7d6814905a874cd0c392d86fc95a6 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Wed, 3 Dec 2025 13:46:13 +0100 Subject: [PATCH 67/68] fix: connect to node on balance cache refresh --- lib/view_model/wallet_list/wallet_list_view_model.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index 9ceb176f08..0e09b53384 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -197,6 +197,7 @@ abstract class WalletListViewModelBase with Store { isBalanceCacheSynced[wallets.indexOf(wallet)] = false; final tmpWallet = await _walletLoadingService.load(wallet.type, wallet.name); + await tmpWallet.connectToNode(node: _appStore.settingsStore.getCurrentNode(tmpWallet.type)); await tmpWallet.startSync(); while (tmpWallet.syncStatus.progress() < 1.0) { await Future.delayed(Duration(milliseconds: 100)); From 193a0c22f2bdc48143a79d6a556f4be387d47b92 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Wed, 3 Dec 2025 13:46:49 +0100 Subject: [PATCH 68/68] skeletonizer-based wallet list refresh ui --- lib/new-ui/widgets/asset_tile.dart | 29 +++++++++++-------- .../wallet_list/wallet_list_view_model.dart | 2 ++ pubspec_base.yaml | 1 + 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/lib/new-ui/widgets/asset_tile.dart b/lib/new-ui/widgets/asset_tile.dart index 3eeebe609a..44451bbf11 100644 --- a/lib/new-ui/widgets/asset_tile.dart +++ b/lib/new-ui/widgets/asset_tile.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:skeletonizer/skeletonizer.dart'; class AssetTile extends StatelessWidget { const AssetTile( @@ -57,24 +58,28 @@ class AssetTile extends StatelessWidget { name, style: TextStyle(fontWeight: FontWeight.bold), ), - Text( - amount, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, + Skeletonizer( + enabled: showLoading, + child: Text( + amount, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ), ], ), ], ), - showLoading - ? CircularProgressIndicator() - : Text( - amountFiat, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), + Skeletonizer( + enabled: showLoading, + child: Text( + amountFiat, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), ], ), ), diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index 0e09b53384..bdbd464118 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -195,7 +195,9 @@ abstract class WalletListViewModelBase with Store { Future refreshCachedBalances() async { for (final wallet in wallets) { isBalanceCacheSynced[wallets.indexOf(wallet)] = false; + } + for (final wallet in wallets) { final tmpWallet = await _walletLoadingService.load(wallet.type, wallet.name); await tmpWallet.connectToNode(node: _appStore.settingsStore.getCurrentNode(tmpWallet.type)); await tmpWallet.startSync(); diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 79f4acc696..9a39b8260d 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -59,6 +59,7 @@ dependencies: another_flushbar: ^1.12.29 archive: ^4.0.3 cryptography: ^2.0.5 + skeletonizer: ^2.1.1 file_picker: git: url: https://github.com/cake-tech/flutter_file_picker.git