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/.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/assets/fonts/WixMadeforText-VariableFont_wght.ttf b/assets/fonts/WixMadeforText-VariableFont_wght.ttf new file mode 100644 index 0000000000..5994d24be6 Binary files /dev/null and b/assets/fonts/WixMadeforText-VariableFont_wght.ttf differ 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/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/assets/new-ui/3dots.svg b/assets/new-ui/3dots.svg new file mode 100644 index 0000000000..5b8896d066 --- /dev/null +++ b/assets/new-ui/3dots.svg @@ -0,0 +1,3 @@ + + + \ 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/balance_card_icons/bitcoin.svg b/assets/new-ui/balance_card_icons/bitcoin.svg new file mode 100644 index 0000000000..7060f10467 --- /dev/null +++ b/assets/new-ui/balance_card_icons/bitcoin.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/new-ui/balance_card_icons/lightning.svg b/assets/new-ui/balance_card_icons/lightning.svg new file mode 100644 index 0000000000..3548851d19 --- /dev/null +++ b/assets/new-ui/balance_card_icons/lightning.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/blank.svg b/assets/new-ui/blank.svg new file mode 100644 index 0000000000..cb30dd8701 --- /dev/null +++ b/assets/new-ui/blank.svg @@ -0,0 +1,3 @@ + + + \ 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 0000000000..a5c34ed1f5 Binary files /dev/null and b/assets/new-ui/btcqr.png differ 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 0000000000..b6cfb5b088 Binary files /dev/null and b/assets/new-ui/settings.png differ 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_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..a3e601b175 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'; @@ -10,9 +11,11 @@ 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'; +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,17 +26,18 @@ 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_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'; 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'; @@ -81,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) @@ -92,24 +95,45 @@ 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) { + try { + lightningWallet = LightningWallet( + mnemonic: mnemonic, + passphrase: passphrase, + seedBytes: seedBytes, + apiKey: secrets.breezApiKey, + lnurlDomain: "cake.cash", + ); + } catch (e) { + printV(e); + lightningWallet = null; + } + } else { + lightningWallet = null; + } + 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 +170,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; } @@ -220,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; @@ -233,8 +254,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 +294,40 @@ 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(), + ); + } + + @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; + 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 +345,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 +402,26 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Future createTransaction(Object credentials) async { credentials = credentials as BitcoinTransactionCredentials; - final tx = (await super.createTransaction(credentials)) - as PendingBitcoinTransaction; + final isLNCompatible = await lightningWallet?.isCompatible(credentials.outputs.first.address); + if ((credentials.coinTypeToSpendFrom == UnspentCoinType.lightning && lightningWallet != null) || + isLNCompatible == true) { + + 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); + } + + final tx = (await super.createTransaction(credentials)) as PendingBitcoinTransaction; final payjoinUri = credentials.payjoinUri; if (payjoinUri == null && !tx.shouldCommitUR()) return tx; @@ -381,8 +446,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 +471,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 +486,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,15 +549,13 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Future signMessage(String message, {String? address = null}) async { if (walletInfo.isHardwareWallet) { final addressEntry = address != null - ? walletAddresses.allAddresses - .firstWhere((element) => element.address == address) + ? walletAddresses.allAddresses.firstWhere((element) => element.address == address) : null; final index = addressEntry?.index ?? 0; final isChange = addressEntry?.isHidden == true ? 1 : 0; final 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 fcd0b7d8cc..c00ccac296 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -1,14 +1,21 @@ 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'; import 'package:cw_core/utils/print_verbose.dart'; 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; @@ -27,6 +34,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S super.initialSilentAddresses, super.initialSilentAddressIndex = 0, super.masterHd, + super.lightningWallet, }) : super(walletInfo); final PayjoinManager payjoinManager; @@ -88,4 +96,36 @@ 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) + ]; + } + + @override + PaymentURI getPaymentUri(String amount) => + BitcoinURI(address: address, amount: amount, pjUri: payjoinEndpoint ?? ''); + + Future getPaymentRequestUri(String amount) async { + if (addressPageType is LightningAddressType && lightningWallet != 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, lnURL: lnUrl, amount: amount, bolt11Invoice: invoice); + } + return getPaymentUri(amount); + } } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index dc611d5007..a38879a8bf 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -715,6 +715,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/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 18d2898b4d..03b32bf4e7 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -1,11 +1,15 @@ import 'dart:io' show Platform; +import 'dart:math'; 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 +55,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 +69,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 +110,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { )); } } - updateAddressesByMatch(); } @@ -115,14 +121,17 @@ 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; final Bip32Slip10Secp256k1 mainHd; final Bip32Slip10Secp256k1 sideHd; final bool isHardwareWallet; + final LightningWallet? lightningWallet; @observable ObservableMap lockedReceiveAddressByType; @@ -139,6 +148,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @observable String? activeSilentAddress; + @observable + String? lightningAddress; + @computed List get allAddresses => _addresses; @@ -153,6 +165,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 +236,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { addressRecord.type == addressPageType) { lockedReceiveAddressByType[addressPageType] = addr; } - } catch (e) { printV("ElectrumWalletAddressBase: set address ($addr): $e"); } @@ -736,4 +751,26 @@ 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!.getAddress(); + + if (lightningAddress == null) { + 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/cw_bitcoin/lib/lightning/lightning_addres_type.dart b/cw_bitcoin/lib/lightning/lightning_addres_type.dart new file mode 100644 index 0000000000..733d9338bc --- /dev/null +++ b/cw_bitcoin/lib/lightning/lightning_addres_type.dart @@ -0,0 +1,26 @@ +import 'package:bitcoin_base/src/bitcoin/address/address.dart'; + +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]+$'; + 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; + @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..8225a265b1 --- /dev/null +++ b/cw_bitcoin/lib/lightning/lightning_wallet.dart @@ -0,0 +1,260 @@ +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'; +import 'package:cw_bitcoin/electrum_transaction_info.dart'; +import 'package:cw_bitcoin/lightning/pending_lightning_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; + +class LightningWallet { + final String mnemonic; + final String? passphrase; + final Uint8List? seedBytes; + final String apiKey; + final String lnurlDomain; + final Network network; + late BreezSdk sdk; + + LightningWallet({ + required this.mnemonic, + this.passphrase, + this.seedBytes, + required this.apiKey, + required this.lnurlDomain, + this.network = Network.mainnet, + }); + + Future init(String appPath) async { + if (_breezSdkSparkLibUninitialized) { + await BreezSdkSparkLib.init(); + _breezSdkSparkLibUninitialized = false; + } + + 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( + config: config, + seed: seed, + storageDir: "$appPath/", + ); + + sdk = await connect(request: connectRequest); + } + + 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; + + Future getBalance() async => + (await sdk.getInfo(request: GetInfoRequest(ensureSynced: true))).balanceSats; + + Future registerAddress(String username) async { + return (await sdk.registerLightningAddress( + request: RegisterLightningAddressRequest(username: username))) + .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); + return (inputType is InputType_Bolt11Invoice) || + (inputType is InputType_LightningAddress) || + (inputType is InputType_LnurlPay); + } catch (_) { + return false; + } + } + + Future createTransaction( + String address, BigInt? amountSats, BitcoinTransactionPriority? priority) 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) { + final lightningFeeSats = paymentMethod.lightningFeeSats; + 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: () async { + final res = await sdk.sendPayment( + request: SendPaymentRequest(prepareResponse: prepareResponse)); + printV(res.payment.status.name); + }, + ); + } + } else if (inputType is InputType_LightningAddress || inputType is InputType_LnurlPay) { + final optionalValidateSuccessActionUrl = true; + + 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; + + return PendingLightningTransaction( + id: prepareResponse.invoiceDetails.paymentHash, + amount: ((prepareResponse.invoiceDetails.amountMsat?.toInt() ?? 0) / 1000).round(), + fee: feeSats.toInt(), + commitOverride: () async { + final res = + await sdk.lnurlPay(request: LnurlPayRequest(prepareResponse: prepareResponse)); + printV(res.payment.status.name); + }, + ); + } 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 + 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.paymentType == PaymentType.receive) { + direction = TransactionDirection.incoming; + } + + 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 { + Config copyWith({ + String? apiKey, + String? lnurlDomain, + Network? network, + int? syncIntervalSecs, + Fee? maxDepositClaimFee, + bool? preferSparkOverLightning, + bool? useDefaultExternalInputParsers, + bool? privateEnabledDefault, + }) => + 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, + privateEnabledDefault: privateEnabledDefault ?? this.privateEnabledDefault, + ); +} 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..cb75b7d2b3 --- /dev/null +++ b/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart @@ -0,0 +1,50 @@ +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; + final List _listeners =[]; + + @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() 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/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/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/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index c4b45c7762..572af00b9b 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 @@ -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: "9baaad9bdcef32bd0572a9bacaa67c7923c4e0a5" + resolved-ref: "9baaad9bdcef32bd0572a9bacaa67c7923c4e0a5" + url: "https://github.com/breez/breez-sdk-spark-flutter" + source: git + version: "0.4.2" bs58check: dependency: transitive description: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 942a5069a2..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 @@ -72,6 +69,10 @@ dependencies: git: url: https://github.com/mrcyjanek/bbqrdart ref: e867e3d0156d0b29858100f30adc2625b9dae586 + breez_sdk_spark_flutter: + git: + url: https://github.com/breez/breez-sdk-spark-flutter + ref: 9baaad9bdcef32bd0572a9bacaa67c7923c4e0a5 dev_dependencies: flutter_test: 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/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart index fe0ebc8284..25c11b7639 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(address: address, amount: amount); } diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 0857c0072d..2f55c7a651 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -1,5 +1,8 @@ +import 'dart:ui'; + import 'package:cw_core/currency.dart'; import 'package:cw_core/enumerable_item.dart'; +import 'package:flutter/material.dart'; class CryptoCurrency extends EnumerableItem with Serializable implements Currency { const CryptoCurrency({ @@ -12,13 +15,19 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen this.tag, this.enabled = false, this.isPotentialScam = false, - }) - : super(title: title, raw: raw); + this.flatIconPath, + this.gradientStartColor = Colors.lightBlue, + this.gradientEndColor = Colors.blue, + }) : super(title: title, raw: raw); final String name; final String? tag; final String? fullName; final String? iconPath; + final String? flatIconPath; + final Color gradientStartColor; + final Color gradientEndColor; + @override final int decimals; final bool enabled; final bool isPotentialScam; @@ -141,7 +150,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const ada = CryptoCurrency(title: 'ADA', fullName: 'Cardano', raw: 1, name: 'ada', iconPath: 'assets/images/ada_icon.png', decimals: 6); static const bch = CryptoCurrency(title: 'BCH', fullName: 'Bitcoin Cash', raw: 2, name: 'bch', iconPath: 'assets/images/crypto/bitcoin-cash.webp', decimals: 8); static const bnb = CryptoCurrency(title: 'BNB', tag: 'BSC', fullName: 'Binance Coin', raw: 3, name: 'bnb', iconPath: 'assets/images/bnb_icon.png', decimals: 8); - static const btc = CryptoCurrency(title: 'BTC', fullName: 'Bitcoin', raw: 4, name: 'btc', iconPath: 'assets/images/crypto/bitcoin.webp', decimals: 8); + static const btc = CryptoCurrency(title: 'BTC', fullName: 'Bitcoin', raw: 4, name: 'btc', iconPath: 'assets/images/crypto/bitcoin.webp', decimals: 8, gradientStartColor: Color(0xFFFFD000), gradientEndColor: Color(0xFFFFAA00), flatIconPath: "assets/new-ui/balance_card_icons/bitcoin.svg"); static const dai = CryptoCurrency(title: 'DAI', tag: 'ETH', fullName: 'Dai', raw: 5, name: 'dai', iconPath: 'assets/images/crypto/dai.webp', decimals: 18); static const dash = CryptoCurrency(title: 'DASH', fullName: 'Dash', raw: 6, name: 'dash', iconPath: 'assets/images/dash_icon.png', decimals: 8); static const eos = CryptoCurrency(title: 'EOS', fullName: 'EOS', raw: 7, name: 'eos', iconPath: 'assets/images/eos_icon.png', decimals: 4); @@ -202,7 +211,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const scrt = CryptoCurrency(title: 'SCRT', fullName: 'Secret Network', raw: 59, name: 'scrt', iconPath: 'assets/images/scrt_icon.png', decimals: 6); static const uni = CryptoCurrency(title: 'UNI', tag: 'ETH', fullName: 'Uniswap', raw: 60, name: 'uni', iconPath: 'assets/images/uni_icon.png', decimals: 18); static const stx = CryptoCurrency(title: 'STX', fullName: 'Stacks', raw: 61, name: 'stx', iconPath: 'assets/images/stx_icon.png', decimals: 8); - static const btcln = CryptoCurrency(title: 'BTC', tag: 'LN', fullName: 'Bitcoin Lightning Network', raw: 62, name: 'btcln', iconPath: 'assets/images/crypto/bitcoin.webp', decimals: 8); + static const btcln = CryptoCurrency(title: 'BTC', tag: 'LN', fullName: 'Bitcoin Lightning Network', raw: 62, name: 'btcln', iconPath: 'assets/images/crypto/bitcoin.webp', decimals: 8, gradientStartColor: Color(0xFFE0E8FF), gradientEndColor: Color(0xFF6D8ADE), flatIconPath: "assets/new-ui/balance_card_icons/lightning.svg"); static const shib = CryptoCurrency(title: 'SHIB', tag: 'ETH', fullName: 'Shiba Inu', raw: 63, name: 'shib', iconPath: 'assets/images/shib_icon.png', decimals: 18); static const aave = CryptoCurrency(title: 'AAVE', tag: 'ETH', fullName: 'Aave', raw: 64, name: 'aave', iconPath: 'assets/images/aave_icon.png', decimals: 18); static const arb = CryptoCurrency(title: 'ARB', fullName: 'Arbitrum', raw: 65, name: 'arb', iconPath: 'assets/images/arb_icon.png', decimals: 18); @@ -246,7 +255,8 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const cbbtc = CryptoCurrency(title: 'CBBTC', tag: 'ETH', fullName: 'Coinbase Wrapped BTC', raw: 103, name: 'cbbtc', iconPath: 'assets/images/cbbtc_icon.png', decimals: 8); static const baseEth = CryptoCurrency(title: 'ETH', tag: 'BASE', fullName: 'Ethereum', raw: 104, name: 'baseth', iconPath: 'assets/images/crypto/base_icon.webp', decimals: 18); static const usde = CryptoCurrency(title: 'USDE', tag: 'BASE', fullName: 'Ethena USDE', raw: 105, name: 'usde', iconPath: 'assets/images/crypto/ethena-usde-logo.png', decimals: 18); - static const arbEth = CryptoCurrency(title: 'ETH', tag: 'ARB', fullName: 'Arbitrum', raw: 106, name: 'arbeth', iconPath: 'assets/images/crypto/arbitrum.webp', decimals: 18); + static const arbEth = CryptoCurrency(title: 'ETH', tag: 'ARB', fullName: 'Arbitrum', raw: 106, name: 'arbeth', iconPath: 'assets/images/crypto/arbitrum.webp', decimals: 18); + static const ltcmweb = CryptoCurrency(title: 'LTC', fullName: 'Litecoin MWeb', raw: 107, name: 'ltcmweb', iconPath: 'assets/images/crypto/litecoin.webp', decimals: 8); static final Map _rawCurrencyMap = [...all, ...havenCurrencies].fold>({}, (acc, item) { 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/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/payment_uris.dart b/cw_core/lib/payment_uris.dart new file mode 100644 index 0000000000..ced3d9fe6c --- /dev/null +++ b/cw_core/lib/payment_uris.dart @@ -0,0 +1,303 @@ +import "package:cw_core/format_fixed.dart"; + +class PaymentURI { + const PaymentURI({required this.scheme, required this.address, required this.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 { + const MoneroURI({required super.address, required super.amount, super.scheme = "monero"}); + + @override + String toString() { + final queryParameters = {}; + + if (amount.isNotEmpty) queryParameters["tx_amount"] = amount.replaceAll(",", "."); + + return Uri(scheme: scheme, path: address, queryParameters: queryParameters).toString(); + } +} + +class BitcoinURI extends PaymentURI { + const BitcoinURI({ + required super.address, + required super.amount, + this.pjUri = "", + super.scheme = "bitcoin", + }); + + final String pjUri; + + @override + String toString() { + final qp = {}; + + if (amount.isNotEmpty) qp["amount"] = amount.replaceAll(",", "."); + if (pjUri.isNotEmpty && !address.startsWith("sp")) { + qp["pjos"] = "0"; + qp["pj"] = pjUri; + } + + return Uri(scheme: "bitcoin", path: address, queryParameters: qp).toString(); + } +} + +class LightningPaymentRequest extends PaymentURI { + const LightningPaymentRequest({ + required super.address, + required super.amount, + required this.lnURL, + this.bolt11Invoice, + super.scheme = "lightning", + }); + + final String lnURL; + final String? bolt11Invoice; + + @override + String toString() => bolt11Invoice ?? lnURL; +} + +class LitecoinURI extends PaymentURI { + LitecoinURI({required super.amount, required super.address, required super.scheme}); + + @override + String toString() { + var base = 'litecoin:$address'; + if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; + + return base; + } +} + +class EthereumURI extends PaymentURI { + EthereumURI({required super.amount, required super.address, required super.scheme}); + + @override + String toString() { + var base = 'ethereum:$address'; + if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; + + return base; + } +} + +class BaseURI extends PaymentURI { + BaseURI({required super.amount, required super.address, required super.scheme}); + + @override + String toString() { + var base = 'base:$address'; + if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; + + return base; + } +} + +class ArbitrumURI extends PaymentURI { + ArbitrumURI({required super.amount, required super.address, required super.scheme}); + + @override + String toString() { + var base = 'arbitrum:$address'; + + if (amount.isNotEmpty) { + base += '?amount=${amount.replaceAll(',', '.')}'; + } + + return base; + } +} + +class BitcoinCashURI extends PaymentURI { + const BitcoinCashURI({required super.address, required super.amount, super.scheme = ""}); + + @override + String toString() { + var base = address; + if (amount.isNotEmpty) base += '?amount=${amount.replaceAll(',', '.')}'; + + return base; + } +} + +class ERC681URI extends PaymentURI { + final int chainId; + final String? contractAddress; + + const ERC681URI({ + required this.chainId, + required super.address, + required super.amount, + required this.contractAddress, + super.scheme = "ethereum", + }); + + @override + String toString() { + var uri = '$scheme:'; + + final targetAddress = contractAddress ?? address; + uri += targetAddress; + + if (chainId != 1) uri += "@$chainId"; + + if (contractAddress != null) uri += "/transfer"; + + final params = {}; + + if (contractAddress != null) { + params["address"] = address; + if (amount.isNotEmpty) params["uint256"] = _formatAmountForERC20(amount); + } else { + if (amount.isNotEmpty) params["value"] = _formatAmountForNative(amount); + } + + if (params.isNotEmpty) { + uri += "?${params.entries.map((e) => "${e.key}=${e.value}").join("&")}"; + } + + return uri; + } + + /// Formats amount for ERC-20 token transfers (in atomic units) + String _formatAmountForERC20(String amount) { + try { + // Convert decimal amount to BigInt (assuming 18 decimals) + final amountDouble = double.parse(amount.replaceAll(",", ".")); + final amountBigInt = BigInt.from(amountDouble * 1e18); + return amountBigInt.toString(); + } catch (e) { + return amount.replaceAll(",", "."); + } + } + + /// Formats amount for native ETH payments (in wei using scientific notation) + String _formatAmountForNative(String amount) { + try { + // Convert decimal amount to double for scientific notation + final amountDouble = double.parse(amount.replaceAll(",", ".")); + + // Use scientific notation as recommended by ERC-681 + return "${amountDouble}e18"; + } catch (e) { + return amount.replaceAll(",", "."); + } + } + + factory ERC681URI.fromUri(Uri uri) { + final (isContract, targetAddress) = _getTargetAddress(uri.path); + final chainId = _getChainID(uri.path); + + final address = isContract ? uri.queryParameters["address"] ?? "" : targetAddress; + final amountParam = isContract ? uri.queryParameters["uint256"] : uri.queryParameters["value"]; + + var formatedAmount = ""; + + if (amountParam != null) { + final normalized = _normalizeToIntegerWei(amountParam); + formatedAmount = formatFixed(BigInt.parse(normalized), 18); + } else { + formatedAmount = uri.queryParameters["amount"] ?? ""; + } + + return ERC681URI( + chainId: chainId, + address: address, + amount: formatedAmount, + contractAddress: isContract ? targetAddress : null, + ); + } + + static int _getChainID(String path) { + 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)!; + return (path.contains("/"), targetAddress); + } + + /// Normalize an input amount into an integer wei string. + /// + /// Accepts the following forms: + /// - Integer string: "123000000000000000" → unchanged + /// - 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(); + + // 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"^\+"), ""); + + // Then we check if it's a scientific notation + 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]); + 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(".")) { + return _expandDecimal(raw, 18); + } + return raw; + } catch (e) { + return raw; + } + } + + /// 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" + static String _expandDecimal(String decimalStr, int expShift) { + var s = decimalStr.trim(); + 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 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 res = head + zeros; + 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"; + + final res = digits.substring(0, cut); + return sign + (res.isEmpty ? "0" : res); + } + } +} 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/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index 4d4d2c0a5a..44fa821789 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -1,9 +1,11 @@ +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'; import 'package:cw_core/wallet_type.dart'; abstract class WalletAddresses { - WalletAddresses(this.walletInfo) + WalletAddresses(this.walletInfo, [this.isTestnet = false]) : addressesMap = {}, allAddressesMap = {}, addressInfos = {}, @@ -17,11 +19,13 @@ abstract class WalletAddresses { final WalletInfo walletInfo; + final bool isTestnet; + 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; @@ -79,4 +83,18 @@ abstract class WalletAddresses { bool containsAddress(String address) => addressesMap.containsKey(address) || allAddressesMap.containsKey(address); + + List get receivePageOptions => ReceivePageOptions; + + /// Get a [PaymentURI] for the current [address] + /// e.g. ethereum:0x0 + 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 => getPaymentUri(amount); } diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index dd5dbaacf3..f6ef726a7b 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 4dc27a062a..d459f3a53d 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: @@ -46,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 sqlite3_flutter_libs: 0.5.40 dev_dependencies: 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_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..273a8e0510 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,11 +12,10 @@ 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, this._libwallet, super.isTestnet); + final Libwallet _libwallet; - String currentAddr = ''; + String _currentAddr = ''; @observable bool isEnabledAutoGenerateSubaddress = true; @@ -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 { @@ -47,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( @@ -70,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); @@ -115,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); } } @@ -126,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] ??= []; @@ -145,11 +131,19 @@ abstract class DecredWalletAddressesBase extends WalletAddresses with Store { selectedAddr = addr; await saveAddressesInBox(); } + + @override + 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_decred/pubspec.lock b/cw_decred/pubspec.lock index ab2826fa22..ea38cb172e 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_dogecoin/lib/src/dogecoin_wallet_addresses.dart b/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart index 75d06c1484..8f12dcc1ca 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'; @@ -10,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, @@ -18,12 +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) => + PaymentURI(scheme: "doge", address: address, amount: amount); } diff --git a/cw_monero/lib/monero_wallet_addresses.dart b/cw_monero/lib/monero_wallet_addresses.dart index 0b38ac5fd6..9a7264c035 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(address: address, amount: amount); } diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index f9ef7a82a4..37ed644815 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/lib/nano_wallet_addresses.dart b/cw_nano/lib/nano_wallet_addresses.dart index f1ff14a854..d29433e39e 100644 --- a/cw_nano/lib/nano_wallet_addresses.dart +++ b/cw_nano/lib/nano_wallet_addresses.dart @@ -15,6 +15,7 @@ abstract class NanoWalletAddressesBase extends WalletAddresses with Store { : accountList = NanoAccountList(walletInfo.address), address = '', super(walletInfo); + @override @observable String address; diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock index 42df7b8d69..9f8665358f 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/lib/wownero_wallet_addresses.dart b/cw_wownero/lib/wownero_wallet_addresses.dart index 936c187247..ef17237f48 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'; @@ -63,7 +62,7 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { WowneroSubaddressList subaddressList; WowneroAccountList accountList; - + @override Set usedAddresses = Set(); @@ -151,4 +150,8 @@ 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) => + MoneroURI(scheme: "wownero", address: address, amount: amount); } diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index ce8192f846..8ded80dc2b 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/lib/zano_wallet_addresses.dart b/cw_zano/lib/zano_wallet_addresses.dart index 39e61be7f0..be25c9383e 100644 --- a/cw_zano/lib/zano_wallet_addresses.dart +++ b/cw_zano/lib/zano_wallet_addresses.dart @@ -1,7 +1,6 @@ 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'; diff --git a/cw_zano/pubspec.lock b/cw_zano/pubspec.lock index 826e385b85..cd98e8a3cd 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/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..d26bc20e08 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; } @@ -303,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) { @@ -687,6 +664,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) { @@ -764,6 +743,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/core/address_validator.dart b/lib/core/address_validator.dart index 059a92c448..36e9d373f7 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|lnurl)[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|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/core/payment_uris.dart b/lib/core/payment_uris.dart deleted file mode 100644 index 38f4f6d2ef..0000000000 --- a/lib/core/payment_uris.dart +++ /dev/null @@ -1,440 +0,0 @@ -import 'package:cw_core/format_fixed.dart'; - -abstract class PaymentURI { - PaymentURI({required this.amount, required this.address}); - - final String amount; - final String address; -} - -class MoneroURI extends PaymentURI { - MoneroURI({required super.amount, required super.address}); - - @override - String toString() { - var base = 'monero:$address'; - - if (amount.isNotEmpty) { - base += '?tx_amount=${amount.replaceAll(',', '.')}'; - } - - return base; - } -} - -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 = ''}); - - final String pjUri; - - @override - String toString() { - final qp = {}; - - if (amount.isNotEmpty) qp['amount'] = amount.replaceAll(',', '.'); - if (pjUri.isNotEmpty && !address.startsWith("sp")) { - qp['pjos'] = '0'; - qp['pj'] = pjUri; - } - - return Uri(scheme: 'bitcoin', path: address, queryParameters: qp).toString(); - } -} - -class LitecoinURI extends PaymentURI { - LitecoinURI({required super.amount, required super.address}); - - @override - String toString() { - var base = 'litecoin:$address'; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } - - return base; - } -} - -class EthereumURI extends PaymentURI { - EthereumURI({required super.amount, required super.address}); - - @override - String toString() { - var base = 'ethereum:$address'; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } - - return base; - } -} - -class BaseURI extends PaymentURI { - BaseURI({required super.amount, required super.address}); - - @override - String toString() { - var base = 'base:$address'; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } - - return base; - } -} - -class ArbitrumURI extends PaymentURI { - ArbitrumURI({required super.amount, required super.address}); - - @override - String toString() { - var base = 'arbitrum:$address'; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } - - return base; - } -} - -class BitcoinCashURI extends PaymentURI { - BitcoinCashURI({required super.amount, required super.address}); - - @override - String toString() { - var base = address; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } - - return base; - } -} - -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({ - required this.chainId, - required super.address, - required super.amount, - required this.contractAddress, - }); - - @override - String toString() { - var uri = 'ethereum:'; - - final targetAddress = contractAddress ?? address; - uri += targetAddress; - - if (chainId != 1) { - uri += '@$chainId'; - } - - if (contractAddress != null) { - uri += '/transfer'; - } - - final params = {}; - - if (contractAddress != null) { - params['address'] = address; - if (amount.isNotEmpty) { - params['uint256'] = _formatAmountForERC20(amount); - } - } else { - if (amount.isNotEmpty) { - params['value'] = _formatAmountForNative(amount); - } - } - - if (params.isNotEmpty) { - uri += '?'; - uri += params.entries.map((e) => '${e.key}=${e.value}').join('&'); - } - - return uri; - } - - /// Formats amount for ERC-20 token transfers (in atomic units) - String _formatAmountForERC20(String amount) { - try { - // Convert decimal amount to BigInt (assuming 18 decimals) - 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(',', '.'); - } - } - - /// Formats amount for native ETH payments (in wei using scientific notation) - String _formatAmountForNative(String amount) { - try { - // Convert decimal amount to double for scientific notation - final amountDouble = double.parse(amount.replaceAll(',', '.')); - - // Use scientific notation as recommended by ERC-681 - return '${amountDouble}e18'; - } catch (e) { - // Fallback to original amount if parsing fails - return amount.replaceAll(',', '.'); - } - } - - factory ERC681URI.fromUri(Uri uri) { - final (isContract, targetAddress) = _getTargetAddress(uri.path); - final chainId = _getChainID(uri.path); - - final address = isContract ? uri.queryParameters["address"] ?? '' : targetAddress; - final amountParam = isContract ? uri.queryParameters["uint256"] : uri.queryParameters["value"]; - - var formatedAmount = ""; - - if (amountParam != null) { - final normalized = _normalizeToIntegerWei(amountParam); - formatedAmount = formatFixed(BigInt.parse(normalized), 18); - } else { - formatedAmount = uri.queryParameters["amount"] ?? ""; - } - - return ERC681URI( - chainId: chainId, - address: address, - amount: formatedAmount, - contractAddress: isContract ? targetAddress : null, - ); - } - - static int _getChainID(String path) { - 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)!; - return (path.contains("/"), targetAddress); - } - - /// Normalize an input amount into an integer wei string. - /// - /// Accepts the following forms: - /// - Integer string: "123000000000000000" → unchanged - /// - 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(); - - // 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'^\+'), ''); - - // Then we check if it's a scientific notation - 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]); - 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('.')) { - 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' - static String _expandDecimal(String decimalStr, int expShift) { - var s = decimalStr.trim(); - 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 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 res = head + zeros; - 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'; - } - final res = digits.substring(0, cut); - return sign + (res.isEmpty ? '0' : res); - } - } -} diff --git a/lib/di.dart b/lib/di.dart index fde64bd72d..7b056ea679 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -11,6 +11,8 @@ 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/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'; @@ -33,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'; @@ -543,7 +546,7 @@ Future setup({ getIt.registerFactory(() => BalanceViewModel( appStore: getIt.get(), settingsStore: getIt.get(), - fiatConvertationStore: getIt.get())); + fiatConversionStore: getIt.get())); getIt.registerFactory( () => ExchangeViewModel( @@ -748,6 +751,12 @@ Future setup({ addressListViewModel: getIt.get(), )); + getIt.registerFactory(() => NewDashboard( + dashboardViewModel: getIt.get(), + )); + + getIt.registerFactory(()=>NewHomePage(dashboardViewModel: getIt.get())); + getIt.registerFactory(() { final GlobalKey _navigatorKey = GlobalKey(); return DesktopSidebarWrapper( @@ -1015,7 +1024,7 @@ Future setup({ getIt.registerFactory(() => WalletKeysViewModel(getIt.get())); getIt.registerFactory(() => WalletKeysPage(getIt.get())); - + getIt.registerFactory(() => AnimatedURModel(getIt.get())); getIt.registerFactoryParam, void>((Map urQr, _) => @@ -1335,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())); @@ -1581,24 +1595,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/lnurlpay_record.dart b/lib/entities/lnurlpay_record.dart new file mode 100644 index 0000000000..f62477d1ff --- /dev/null +++ b/lib/entities/lnurlpay_record.dart @@ -0,0 +1,74 @@ +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'; + +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 = "_"; + } + + final expectedUrl = "https://$domain/.well-known/lnurlp/$name"; + final response = await ProxyWrapper().get( + clearnetUri: Uri.parse(expectedUrl), + headers: {"Accept": "application/json"}, + ); + + if (response.statusCode == 200) { + return encodeLNURL(expectedUrl); + } + } 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 { + final 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/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/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/new-ui/new_dashboard.dart b/lib/new-ui/new_dashboard.dart new file mode 100644 index 0000000000..7bb008fb7e --- /dev/null +++ b/lib/new-ui/new_dashboard.dart @@ -0,0 +1,49 @@ +import 'package:cake_wallet/di.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/src/screens/wallet_list/wallet_list_page.dart'; +import 'package:flutter/material.dart'; +import '../view_model/dashboard/dashboard_view_model.dart'; + +class NewDashboard extends StatefulWidget { + NewDashboard({super.key, required this.dashboardViewModel}); + + final DashboardViewModel dashboardViewModel; + + final List dashboardPageWidgets = [ + getIt.get(), + getIt.get(), + getIt.get(), + getIt.get(), + Placeholder(), + ]; + + @override + State createState() => _NewDashboardState(); +} + +class _NewDashboardState extends State { + int _selectedPage = 0; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + widget.dashboardPageWidgets[_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..94539cde10 --- /dev/null +++ b/lib/new-ui/pages/home_page.dart @@ -0,0 +1,93 @@ +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, + spacing: 24.0, + 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/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/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_button.dart b/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart new file mode 100644 index 0000000000..80db0ce091 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart @@ -0,0 +1,59 @@ +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; + + static const sizeFactor = 0.16; + + @override + Widget build(BuildContext context) { + final double size = MediaQuery.of(context).size.width*sizeFactor; + return Column( + children: [ + Container( + width: size, + height: size, + 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.only(top:8.0), + child: Text( + style: TextStyle( + 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 new file mode 100644 index 0000000000..2cb9d4d36a --- /dev/null +++ b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart @@ -0,0 +1,104 @@ +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'; + +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(horizontal: 18.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + 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); + } + }, + ), + 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); + } + }, + ), + CoinActionButton( + icon: SvgPicture.asset("assets/new-ui/exchange.svg"), + label: "Swap", + 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: () async { + if (FeatureFlag.hasNewUiExtraPages) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => FractionallySizedBox( + heightFactor: 0.9, + child: ScanPage(), + ), + ); + } else { + final code = await presentQRScanner(context); + + if (code == null) return; + if (code.isEmpty) return; + final uri = Uri.tryParse(code); + if (uri == null) return; + rootKey.currentState?.handleDeepLinking(uri); + }; + }, + ), + ], + ), + ); + } +} 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..bde605d667 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart @@ -0,0 +1,70 @@ +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..cc68de3456 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/history_section.dart @@ -0,0 +1,72 @@ +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}); + + 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 || 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; + + final tradeTo = trade.toRaw >= 0 ? trade.to : trade.userCurrencyTo; + + 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_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/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 ""; + } +} 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..dbbc479d47 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/lightning_assets.dart @@ -0,0 +1,49 @@ +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 { + LightningAssets({super.key, required this.dashboardViewModel}); + + final DashboardViewModel dashboardViewModel; + + @override + State createState() => _LightningAssetsState(); +} + +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( + children: [ + AssetsTopBar( + onTabChange: (index) { + setState(() { + _selectedTab = index; + }); + }, + selectedTab: _selectedTab, + ), + lightningTabs[_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..6ec16a75ef --- /dev/null +++ b/lib/new-ui/widgets/coins_page/cards/balance_card.dart @@ -0,0 +1,133 @@ +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, + 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) { + 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: gradient, + 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( + lightningMode + ? balanceRecord.secondAvailableBalance + : balanceRecord.availableBalance, + style: TextStyle(color: Colors.black, fontSize: 28), + ), + Text( + balanceRecord.asset.name.toUpperCase(), + style: TextStyle(color: Colors.black45, fontSize: 28), + ), + ], + ), + ), + Text( + lightningMode + ? balanceRecord.fiatSecondAdditionalBalance + : 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( + svgPath, + height: 50, + width: 50, + colorFilter: const ColorFilter.mode( + Color(0xBBFFFFFF), + 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..51bcd5e802 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/cards/cards_view.dart @@ -0,0 +1,164 @@ +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: (_) { + final account = widget.accountListViewModel?.accounts[index]; + + final walletBalance = + widget.dashboardViewModel.balanceViewModel.formattedBalances.elementAt(0); + final walletCurrency = + widget.lightningMode ? walletBalance.secondAsset : walletBalance.asset; + + late final String accountName; + late final String accountBalance; + if (account == null) { + accountName = walletCurrency.fullName ?? walletCurrency.title; + accountBalance = ""; + } else { + accountName = account.label; + accountBalance = account.balance ?? "0.00"; + } + + // TODO get user-selected custom gradient if set, fallback to CryptoCurrency one if null + final gradient = LinearGradient(colors: [ + walletCurrency.gradientStartColor, + walletCurrency.gradientEndColor, + ], begin: Alignment.topCenter, end: Alignment.bottomCenter); + + return BalanceCard( + width: cardWidth, + accountName: accountName, + accountBalance: accountBalance, + balanceRecord: walletBalance, + selected: _selectedIndex == index, + gradient: gradient, + svgPath: walletCurrency.flatIconPath ?? "assets/new-ui/blank.svg", + lightningMode: widget.lightningMode, + ); + }), + ), + ), + ); + } + + 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 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..bff5b0987a --- /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.only(left: 18.0, right: 18.0, top: 10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + (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, + ), + ), + ], + ), + ), + ), + ) : Container(), + 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..633b6cefad --- /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, fontWeight: FontWeight.w500)), + SizedBox(width: 8), + ModernButton.svg(size: 24, 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/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..4897460db5 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'; @@ -172,6 +175,8 @@ Route handleRouteWithPlatformAwareness( Route createRoute(RouteSettings settings) { currentRouteSettings = settings; + printV(settings.name); + switch (settings.name) { case Routes.welcome: return MaterialPageRoute( @@ -247,7 +252,7 @@ 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)); @@ -400,7 +405,9 @@ Route createRoute(RouteSettings settings) { 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?; @@ -752,7 +759,6 @@ Route createRoute(RouteSettings settings) { (context) => getIt.get(param1: args), ); - case Routes.cakePayAccountPage: return handleRouteWithPlatformAwareness( (context) => getIt.get(), @@ -942,12 +948,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 +963,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 +983,7 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute( builder: (_) => getIt.get(), ); - + case Routes.startTor: return MaterialPageRoute( builder: (_) => getIt.get(), diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index 3d7a90b4bc..7beae219a9 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'; import 'package:cake_wallet/themes/core/theme_extension.dart'; @@ -275,7 +274,9 @@ class _DashboardPageView extends BasePage { ), NewMainNavBar( dashboardViewModel: dashboardViewModel, - ) + selectedIndex: 0, + onItemTap: (index) {} + ), ], ), ], 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/dashboard/pages/balance/balance_row_widget.dart b/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart index 766914f769..68d2ca2782 100644 --- a/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart +++ b/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart @@ -12,7 +12,9 @@ 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:flutter_svg/flutter_svg.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -147,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( @@ -215,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), @@ -243,7 +243,7 @@ class BalanceRowWidget extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 26), + const SizedBox(height: 26), Row( children: [ Text( @@ -256,7 +256,7 @@ class BalanceRowWidget extends StatelessWidget { ), ], ), - SizedBox(height: 8), + const SizedBox(height: 8), AutoSizeText( frozenBalance, style: Theme.of(context).textTheme.bodyLarge!.copyWith( @@ -267,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), ), ], ), @@ -282,7 +280,7 @@ class BalanceRowWidget extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 24), + const SizedBox(height: 24), Text( '${additionalBalanceLabel}', textAlign: TextAlign.center, @@ -291,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( @@ -302,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), ), ], ), @@ -332,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, @@ -359,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, + ), + ), + ], ), ], ), @@ -391,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: @@ -433,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, @@ -461,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, @@ -497,31 +501,15 @@ 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: () { - 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( @@ -534,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: [ @@ -546,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, @@ -561,25 +549,9 @@ class BalanceRowWidget extends StatelessWidget { SizedBox(width: 16), Expanded( child: Semantics( - label: S.of(context).litecoin_mweb_pegout, + label: withdrawFromL2Label, 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( @@ -604,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 @@ -622,7 +594,7 @@ class BalanceRowWidget extends StatelessWidget { ), ), ), - SizedBox(height: 16), + const SizedBox(height: 16), ], ), ), @@ -632,20 +604,74 @@ 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; - // } + 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; + + 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 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/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 2c1dc74e96..deecd97b08 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'; @@ -9,53 +8,55 @@ 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(); } 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 pillIconWidth = 24.0; + static const pillIconHeight = 24.0; + static const pillIconSpacing = 4.0; + static const pillHorizontalPadding = 20.0; static const barBorderRadius = 50.0; static const pillBorderRadius = 50.0; - static const barResizeDuration = Duration(milliseconds: 400); - 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 barHorizontalPadding = 12.0; + + 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 iconColorChangeDuration = Duration(milliseconds: 200); static const pillTextStyle = TextStyle( fontSize: 16, fontWeight: FontWeight.w500, ); - late int selectedIndex; - bool _fadeSelected = true; bool _firstFrame = true; @override void initState() { super.initState(); - selectedIndex = widget.initialIndex; WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; @@ -64,20 +65,13 @@ class _NEWNewMainNavBarState extends State { } void _onItemTap(int index) { - if (index == selectedIndex) return; - - setState(() { - selectedIndex = index; - _fadeSelected = false; - }); + // if (index == widget.selectedIndex) return; + // + // setState(() { + // widget.selectedIndex = index; + // }); - // delay fade (tweak duration) - Future.delayed(const Duration(milliseconds: 50), () { - if (!mounted) return; - if (index == selectedIndex) { - setState(() => _fadeSelected = true); - } - }); + widget.onItemTap(index); NewMainActions.all[index].onTap.call(); } @@ -98,15 +92,29 @@ 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 > 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+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; @@ -115,57 +123,13 @@ 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], + context, visibleActions[widget.selectedIndex], color: activeColor); - final baseWidth = screenWidth * 0.65; - - 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; + final barWidth = calcBarWidth(pillWidth); - // 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]; + final currentAction = visibleActions[widget.selectedIndex]; return Align( alignment: Alignment.bottomCenter, @@ -185,72 +149,77 @@ class _NEWNewMainNavBarState extends State { height: barHeight, decoration: BoxDecoration( color: backgroundColor, + border: Border.all(color: Color(0x14FFFFFF), width: 1), 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(widget.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)+((i == widget.selectedIndex) ? iconHorizontalPadding/2 : 0), + curve: Curves.easeOutCubic, + child: GestureDetector( + onTap: () => _onItemTap(i), + child: AnimatedContainer( + duration: _firstFrame + ? Duration.zero + : inactiveIconMoveDuration, + curve: Curves.easeOutCubic, + width: + i == widget.selectedIndex ? pillWidth : iconWidth, + height: iconHeight, + alignment: Alignment.center, + child: AnimatedAlign( + duration: inactiveIconFadeDuration, curve: Curves.easeOutCubic, - width: i == selectedIndex - ? pillWidth - : iconWidth, - height: iconHeight, - alignment: Alignment.center, - child: AnimatedOpacity( - duration: inactiveIconFadeDuration, + alignment: Alignment.centerLeft, + 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 == widget.selectedIndex) ? 0.857 : 1.0, + child: TweenAnimationBuilder( + tween: ColorTween( + begin: (i == widget.selectedIndex) ? inactiveColor : activeColor, + end: (i==widget.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, + ), + ); + } ), ), ), ), - ], - ), - ) - ], + ), + ), + ], + ), )), ), ), @@ -294,60 +263,39 @@ 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: 8, + bottom: 8, + 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: [ + // SizedBox(width: pillIconSpacing*10), + 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 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/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index f5b151482b..6e220fc845 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, @@ -474,7 +471,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 +489,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 +511,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 +537,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 +591,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 +762,20 @@ 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(), + ), + ); bool isRegularElectrumAddress(String address) { final supportedTypes = [CryptoCurrency.btc, CryptoCurrency.ltc, CryptoCurrency.bch]; @@ -800,7 +788,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 +799,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/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; diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 76eec7dc1d..2187b29216 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 _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/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/utils/feature_flag.dart b/lib/utils/feature_flag.dart index 661595a414..6b3e78fc0c 100644 --- a/lib/utils/feature_flag.dart +++ b/lib/utils/feature_flag.dart @@ -10,7 +10,10 @@ 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; + static const bool hasNewUiExtraPages = false; } 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/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 552d318e4a..0bf27b3417 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -1,38 +1,39 @@ 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.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.secondAsset, + required this.formattedAssetTitle, + }); final String fiatAdditionalBalance; final String fiatAvailableBalance; @@ -45,6 +46,7 @@ class BalanceRecord { final String fiatSecondAdditionalBalance; final String fiatSecondAvailableBalance; final CryptoCurrency asset; + final CryptoCurrency secondAsset; final String formattedAssetTitle; } @@ -52,7 +54,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! { @@ -63,9 +65,7 @@ abstract class BalanceViewModelBase with Store { _checkMweb(); - reaction((_) => settingsStore.mwebAlwaysScan, (bool value) { - _checkMweb(); - }); + reaction((_) => settingsStore.mwebAlwaysScan, (_) => _checkMweb()); } void _checkMweb() { @@ -76,9 +76,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; @@ -86,17 +84,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; } @@ -110,12 +103,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; @@ -150,18 +141,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: @@ -202,9 +190,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; } @@ -212,6 +198,14 @@ abstract class BalanceViewModelBase with Store { @computed Map get balances { return wallet.balance.map((key, value) { + + var secondAsset = key; + if (key == CryptoCurrency.btc) { + secondAsset = CryptoCurrency.btcln; + } else if (key == CryptoCurrency.ltc) { + secondAsset = CryptoCurrency.ltcmweb; + } + if (displayMode == BalanceDisplayMode.hiddenBalance) { final fiatCurrency = settingsStore.fiatCurrency; return MapEntry( @@ -225,13 +219,16 @@ 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, + secondAsset: secondAsset, 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'); @@ -281,6 +278,7 @@ abstract class BalanceViewModelBase with Store { fiatSecondAvailableBalance: secondAvailableFiatBalance, fiatSecondAdditionalBalance: secondAdditionalFiatBalance, asset: key, + secondAsset: secondAsset, formattedAssetTitle: _formatterAsset(key))); }); } @@ -289,47 +287,36 @@ 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 => - mwebEnabled && _hasSecondAvailableBalanceForWalletType(wallet.type); - - bool _hasAdditionalBalanceForWalletType(WalletType type) { - switch (type) { - case WalletType.monero: - case WalletType.wownero: - case WalletType.zano: - case WalletType.decred: + bool get hasSecondAvailableBalance { + switch (wallet.type) { + case WalletType.bitcoin: return true; + case WalletType.litecoin: + return mwebEnabled; default: return false; } } - 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 false; - } + bool _hasAdditionalBalanceForWalletType(WalletType type) => + [WalletType.monero, WalletType.wownero, WalletType.zano, WalletType.decred].contains(type); @computed List get formattedBalances { @@ -337,25 +324,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; } @@ -368,9 +345,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); @@ -388,14 +365,11 @@ 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; } - @observable bool isShowCard; @@ -404,9 +378,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(); @@ -439,18 +411,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/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/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 33df3ea241..27c2ed1ab1 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -185,28 +185,28 @@ 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); 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); @@ -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); diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index fec47f3f35..40f0fe0057 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'; @@ -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'; @@ -414,40 +413,26 @@ 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) - 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(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.haven: - return HavenURI(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/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 1b446e0203..756f9ffcf7 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, @@ -138,21 +135,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 +176,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 +299,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,9 +349,6 @@ 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; @@ -393,19 +379,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; @@ -483,11 +462,25 @@ 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) { + final RegExp lightningInvoiceRegex = RegExp( + r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$', + caseSensitive: false); + return lightningInvoiceRegex.hasMatch(txt); } Timer? _ledgerTxStateTimer; @@ -499,13 +492,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 @@ -813,9 +807,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'); @@ -873,11 +869,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.arbitrum, + ].contains(wallet.type)) { throw Exception('Priority is null for wallet type: ${wallet.type}'); } @@ -962,24 +960,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 +1049,12 @@ 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.arbitrum, + ].contains(walletType)) { if (errorMessage.contains('gas required exceeds allowance')) { return S.current.gas_exceeds_allowance; } 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..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 @@ -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:cake_wallet/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,12 @@ 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 { + uri = await wallet.walletAddresses.getPaymentRequestUri(amount); } @computed @@ -471,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); @@ -518,6 +485,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo void _init() { _baseItems = []; + uri = wallet.walletAddresses.getPaymentUri(amount); if (wallet.walletAddresses.hiddenAddresses.isNotEmpty) { _baseItems.add(WalletAddressHiddenListHeader()); @@ -537,6 +505,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo if (wallet.isEnabledAutoGenerateSubaddress) { wallet.walletAddresses.address = wallet.walletAddresses.latestAddress; } + + reaction((_) => amount, (_) => refreshUri()); + reaction((_) => address, (_) => refreshUri()); } @action diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 274a97201e..826f316c18 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -242,6 +242,8 @@ flutter: - assets/text/ - assets/faq/ - assets/animation/ + - assets/new-ui/ + - assets/new-ui/balance_card_icons/ fonts: - family: Lato 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 fb5ff2f8b6..ecca6a03c7 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", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 2cd95ea2ea..5bfdc8be48 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个块", 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/configure.dart b/tool/configure.dart index 47b3379406..7534457aca 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'; @@ -227,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); @@ -268,6 +267,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); 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; }