diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 879ea247f..467743048 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -159,11 +159,6 @@ jobs: - name: Generate localization run: | dart run tool/generate_localization.dart - - name: Workaround zcash - run: | - mkdir -p scripts/zcash_lib/assets/ - touch scripts/zcash_lib/assets/sapling-spend.params - touch scripts/zcash_lib/assets/sapling-output.params - name: Build linux run: | flutter build linux --dart-define=hasDevOptions=true --release diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml index c6082d4e0..269619cf3 100644 --- a/.github/workflows/reusable-build.yml +++ b/.github/workflows/reusable-build.yml @@ -176,16 +176,18 @@ jobs: - name: Rename apk file run: | + sanitized_branch_name=$(grep '^id=' android/app.properties | cut -d'=' -f2 | sed 's/com\.cakewallet\.test_//') cd build/app/outputs/flutter-apk mkdir test-apk - cp app-arm64-v8a-release.apk test-apk/${BRANCH_NAME}.apk - cp app-x86_64-release.apk test-apk/${BRANCH_NAME}_x86.apk + cp app-arm64-v8a-release.apk test-apk/${sanitized_branch_name}.apk + cp app-x86_64-release.apk test-apk/${sanitized_branch_name}_x86.apk - name: Find APK file id: find_apk run: | set -x - apk_file=$(ls build/app/outputs/flutter-apk/test-apk/${BRANCH_NAME}.apk || exit 1) + sanitized_branch_name=$(grep '^id=' android/app.properties | cut -d'=' -f2 | sed 's/com\.cakewallet\.test_//') + apk_file=$(ls build/app/outputs/flutter-apk/test-apk/${sanitized_branch_name}.apk || exit 1) echo "APK_FILE=$apk_file" >> $GITHUB_ENV - name: Write BRANCH_NAME to env run: echo "BRANCH_NAME=${BRANCH_NAME}" >> $GITHUB_ENV diff --git a/.gitignore b/.gitignore index 268167bc4..cd064fc15 100644 --- a/.gitignore +++ b/.gitignore @@ -226,4 +226,5 @@ scripts/android/yttrium scripts/bitbox_flutter scripts/reown_flutter -*.salive \ No newline at end of file +*.salive +android/.kotlin diff --git a/README.md b/README.md index 0852d8552..fa98091c6 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,6 @@ Cake Wallet includes support for several cryptocurrencies, including: * Nano (XNO) * Zano (ZANO) * Decred (DCR) -* Wownero (WOW) ## Features diff --git a/assets/images/tbb_icon.png b/assets/images/tbb_icon.png new file mode 100644 index 000000000..744b9e3e1 Binary files /dev/null and b/assets/images/tbb_icon.png differ diff --git a/assets/tron_node_list.yml b/assets/tron_node_list.yml index 3837ec4ee..ae30489d9 100644 --- a/assets/tron_node_list.yml +++ b/assets/tron_node_list.yml @@ -1,6 +1,5 @@ - uri: api.trongrid.io - isDefault: true useSSL: true isEnabledForAutoSwitching: true label: "TronGrid" @@ -10,6 +9,7 @@ label: "PublicNode" - uri: trx.nownodes.io + isDefault: true useSSL: true isEnabledForAutoSwitching: true label: "NOWNodes" \ No newline at end of file diff --git a/assets/zcash_node_list.yml b/assets/zcash_node_list.yml index afdd49599..148e785ed 100644 --- a/assets/zcash_node_list.yml +++ b/assets/zcash_node_list.yml @@ -5,8 +5,8 @@ useSSL: true isEnabledForAutoSwitching: true label: "Cake Wallet" -- - uri: zec.rocks:443 +- uri: zec.rocks:443 + is_default: true useSSL: true isEnabledForAutoSwitching: true label: "Zec.rocks" diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 7e76132a8..b8e53d11b 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -17,6 +17,7 @@ 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/hardware/bitcoin_ledger_service.dart'; +import 'package:cw_bitcoin/output_ordering.dart'; import 'package:cw_bitcoin/payjoin/manager.dart'; import 'package:cw_bitcoin/payjoin/storage.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; @@ -467,8 +468,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final masterFingerprint = await (hardwareWalletService as BitcoinHardwareWalletService).getMasterFingerprint(); + final orderedOutputs = orderOutputs(outputs, outputOrdering); + final psbt = await buildPsbt( - outputs: outputs, + outputs: orderedOutputs, fee: fee, network: network, utxos: utxos, @@ -478,7 +481,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { memo: memo, enableRBF: enableRBF, inputOrdering: inputOrdering, - outputOrdering: outputOrdering, + // Already applied above; don't reorder again. + outputOrdering: BitcoinOrdering.none, ); final psbtStr = base64Encode(psbt.serialize()); diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 0bc7f8c9c..c4c41f913 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -124,7 +124,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S Future getPaymentRequestUri(String amount) async { if (addressPageType is LightningAddressType && lightningWallet != null) { - final amountSats = amount.isNotEmpty ? parseFixed(amount, 8) : null; + final amountSats = amount.isNotEmpty ? tryParseFixed(amount, 8) : null; final lnUrl = getLnurlOfLightningAddress(address); if (amountSats == null) { return LightningPaymentRequest(address: address, lnURL: lnUrl, amount: amount); diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index bc7d45ec4..ceba0eccd 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -18,7 +18,6 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; -import 'package:cw_bitcoin/bitcoin_wallet.dart'; import 'package:cw_bitcoin/bitcoin_wallet_keys.dart'; import 'package:cw_bitcoin/electrum.dart' as electrum; import 'package:cw_bitcoin/electrum_balance.dart'; @@ -27,25 +26,20 @@ import 'package:cw_bitcoin/electrum_transaction_history.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/exceptions.dart'; -import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/get_height_by_date.dart'; -import 'package:cw_core/hardware/hardware_wallet_service.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/output_info.dart'; import 'package:cw_core/pending_transaction.dart'; -import 'package:cw_core/root_dir.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/unspent_coins_info.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/utils/socket_health_logger.dart'; import 'package:cw_core/utils/tor/abstract.dart'; import 'package:cw_core/wallet_base.dart'; @@ -57,7 +51,6 @@ import 'package:hex/hex.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:rxdart/subjects.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:sp_scanner/sp_scanner.dart'; part 'electrum_wallet.g.dart'; @@ -1492,7 +1485,9 @@ abstract class ElectrumWalletBase fee: estimatedTx.fee.amount, network: network, memo: estimatedTx.memo, - outputOrdering: BitcoinOrdering.none, + // Shuffle so the change output isn't placed deterministically last + // (privacy fingerprint). Applied by orderOutputs in the builder. + outputOrdering: BitcoinOrdering.shuffle, enableRBF: true, cwOutputs: transactionCredentials.outputs, ); @@ -1525,7 +1520,10 @@ abstract class ElectrumWalletBase fee: estimatedTx.fee.amount, network: network, memo: estimatedTx.memo, - outputOrdering: BitcoinOrdering.none, + // Shuffle so the change output isn't placed deterministically last + // (privacy fingerprint). Change is found by isChange, not position. + inputOrdering: BitcoinOrdering.shuffle, + outputOrdering: BitcoinOrdering.shuffle, enableRBF: !estimatedTx.spendsUnconfirmedTX, ); } else { @@ -1536,7 +1534,7 @@ abstract class ElectrumWalletBase network: network, memo: estimatedTx.memo, inputOrdering: BitcoinOrdering.shuffle, - outputOrdering: BitcoinOrdering.none, + outputOrdering: BitcoinOrdering.shuffle, enableRBF: !estimatedTx.spendsUnconfirmedTX, ); } @@ -1594,6 +1592,7 @@ abstract class ElectrumWalletBase isSendAll: estimatedTx.isSendAll, hasTaprootInputs: hasTaprootInputs, utxos: estimatedTx.utxos, + derivedOutputs: updatedOutputs, publicKeys: estimatedTx.publicKeys, isViewOnly: keys.privateKey.isEmpty, )..addListener((transaction) async { @@ -1983,8 +1982,12 @@ abstract class ElectrumWalletBase @action Future addCoinInfo(BitcoinUnspent coin) async { // Check if the coin is already in the unspentCoinsInfo for the wallet - final existingCoinInfo = unspentCoinsInfo.values - .firstWhereOrNull((element) => element.walletId == walletInfo.id && element == coin); + final existingCoinInfo = unspentCoinsInfo.values.firstWhereOrNull( + (element) => + element.walletId == walletInfo.id && + element.hash == coin.hash && + element.vout == coin.vout, + ); if (existingCoinInfo == null) { final newInfo = UnspentCoinsInfo( @@ -2297,7 +2300,7 @@ abstract class ElectrumWalletBase network: network, memo: memo, inputOrdering: BitcoinOrdering.shuffle, - outputOrdering: BitcoinOrdering.none, + outputOrdering: BitcoinOrdering.shuffle, enableRBF: true, ); @@ -2509,10 +2512,16 @@ abstract class ElectrumWalletBase Map historiesWithDetails, BitcoinAddressType type, ) async { - final addressesByType = walletAddresses.allAddresses.where((addr) => addr.type == type); - final hiddenAddresses = addressesByType.where((addr) => addr.isHidden == true); - final receiveAddresses = addressesByType.where((addr) => addr.isHidden == false); - walletAddresses.hiddenAddresses.addAll(hiddenAddresses.map((e) => e.address)); + + final addressesByType = + walletAddresses.allAddresses.where((addr) => addr.type == type).toList(); + + final receiveStandard = getAddressBranchByType(hidden: false, legacy: false, type: type); + final changeStandard = getAddressBranchByType(hidden: true, legacy: false, type: type); + final receiveLegacy = getAddressBranchByType(hidden: false, legacy: true, type: type); + final changeLegacy = getAddressBranchByType(hidden: true, legacy: true, type: type); + + walletAddresses.hiddenAddresses.addAll([...changeStandard, ...changeLegacy].map((e) => e.address)); await walletAddresses.saveAddressesInBox(); await Future.wait(addressesByType.map((addressRecord) async { final history = await _fetchAddressHistory(addressRecord, await getCurrentChainTip()); @@ -2521,7 +2530,9 @@ abstract class ElectrumWalletBase addressRecord.txCount = history.length; historiesWithDetails.addAll(history); - final matchedAddresses = addressRecord.isHidden ? hiddenAddresses : receiveAddresses; + final matchedAddresses = addressRecord.isHidden + ? (addressRecord.isLegacyDerivation ? changeLegacy : changeStandard) + : (addressRecord.isLegacyDerivation ? receiveLegacy : receiveStandard); final isUsedAddressAboveGap = matchedAddresses.toList().indexOf(addressRecord) >= matchedAddresses.length - (addressRecord.isHidden @@ -2625,18 +2636,19 @@ abstract class ElectrumWalletBase Future fetchTransactionsForAddressTypeBatch( Map historiesWithDetails, BitcoinAddressType type) async { - final addressesByType = - walletAddresses.allAddresses.where((addr) => addr.type == type).toList(); - final receiveAddresses = addressesByType.where((addr) => !addr.isHidden).toList(); - final hiddenAddresses = addressesByType.where((addr) => addr.isHidden).toList(); - walletAddresses.hiddenAddresses.addAll(hiddenAddresses.map((e) => e.address)); + final receiveStandard = getAddressBranchByType(hidden: false, legacy: false, type: type); + final changeStandard = getAddressBranchByType(hidden: true, legacy: false, type: type); + final receiveLegacy = getAddressBranchByType(hidden: false, legacy: true, type: type); + final changeLegacy = getAddressBranchByType(hidden: true, legacy: true, type: type); + + walletAddresses.hiddenAddresses.addAll([...changeStandard, ...changeLegacy].map((e) => e.address)); await walletAddresses.saveAddressesInBox(); await fetchTransactionsForAddressesBranchBatch( historiesWithDetails, type, - receiveAddresses, + receiveStandard, isHidden: false, isLegacyDerivation: false, ); @@ -2644,7 +2656,7 @@ abstract class ElectrumWalletBase await fetchTransactionsForAddressesBranchBatch( historiesWithDetails, type, - hiddenAddresses, + changeStandard, isHidden: true, isLegacyDerivation: false, ); @@ -2652,7 +2664,7 @@ abstract class ElectrumWalletBase await fetchTransactionsForAddressesBranchBatch( historiesWithDetails, type, - receiveAddresses, + receiveLegacy, isHidden: false, isLegacyDerivation: true, ); @@ -2660,7 +2672,7 @@ abstract class ElectrumWalletBase await fetchTransactionsForAddressesBranchBatch( historiesWithDetails, type, - hiddenAddresses, + changeLegacy, isHidden: true, isLegacyDerivation: true, ); @@ -2736,6 +2748,11 @@ abstract class ElectrumWalletBase } } + List getAddressBranchByType({required bool hidden, required bool legacy, required BitcoinAddressType + type}) => walletAddresses.allAddresses.where((addr) => addr.type == type && addr.isHidden == hidden && addr.isLegacyDerivation == legacy) + .toList() + ..sort((a, b) => a.index.compareTo(b.index)); + int _highestUsedIndex(List addresses) { for (int i = addresses.length - 1; i >= 0; i--) { if (addresses[i].isUsed) return i; @@ -3680,7 +3697,6 @@ abstract class ElectrumWalletBase syncStatus = ConnectingSyncStatus(); } break; - default: } } diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 23959374c..aa8fcd330 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -30,6 +30,7 @@ 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/litecoin_wallet_addresses.dart'; +import 'package:cw_bitcoin/output_ordering.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/psbt/transaction_builder.dart'; import 'package:cw_bitcoin/utils.dart'; @@ -1584,8 +1585,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { )); } + final orderedOutputs = orderOutputs(outputs, outputOrdering); + final rawHex = await (hardwareWalletService as LitecoinHardwareWalletService) - .signLitecoinTransaction(outputs: outputs, inputs: readyInputs, publicKeys: publicKeys); + .signLitecoinTransaction( + outputs: orderedOutputs, inputs: readyInputs, publicKeys: publicKeys); return BtcTransaction.fromRaw(rawHex); } diff --git a/cw_bitcoin/lib/output_ordering.dart b/cw_bitcoin/lib/output_ordering.dart new file mode 100644 index 000000000..36a980e93 --- /dev/null +++ b/cw_bitcoin/lib/output_ordering.dart @@ -0,0 +1,21 @@ +import 'dart:math'; + +import 'package:bitcoin_base/bitcoin_base.dart'; + +/// Applies [ordering] to a transaction's [outputs] before building. +/// +/// Returns a new list; the input is never mutated. Only [BitcoinOrdering.shuffle] +/// reorders (using a secure RNG by default) so the change output is no longer +/// placed deterministically last. Any other value preserves the given order, +/// matching the pre-existing hardware-wallet behavior. +List orderOutputs( + List outputs, + BitcoinOrdering ordering, { + Random? rng, +}) { + final result = List.of(outputs); + if (ordering == BitcoinOrdering.shuffle) { + result.shuffle(rng ?? Random.secure()); + } + return result; +} diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 565234291..c92e80780 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -31,6 +31,7 @@ class PendingBitcoinTransaction with PendingTransaction { this.hasTaprootInputs = false, this.isMweb = false, this.utxos = const [], + this.derivedOutputs = const [], this.publicKeys, this.commitOverride, this.unsignedPsbt, @@ -47,6 +48,9 @@ class PendingBitcoinTransaction with PendingTransaction { final bool hasTaprootInputs; final bool isViewOnly; List utxos; + + final List derivedOutputs; + bool isMweb; String? changeAddressOverride; String? idOverride; @@ -79,6 +83,13 @@ class PendingBitcoinTransaction with PendingTransaction { bool get hasSilentPayment => _tx.hasSilentPayment; + List get stealthAddresses => derivedOutputs + .where((out) => !out.isChange) + .map((out) => out.isSilentPayment + ? out.address.toAddress(network ?? BitcoinNetwork.mainnet) + : null) + .toList(); + PendingChange? get change { try { final change = _tx.outputs.firstWhere((out) => out.isChange); diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 6c15a9ad1..b35b2bf09 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -634,10 +634,10 @@ packages: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.0" mime: dependency: transitive description: @@ -1100,10 +1100,10 @@ packages: dependency: transitive description: name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.10" torch_dart: dependency: transitive description: @@ -1322,5 +1322,5 @@ packages: source: hosted version: "2.2.2" sdks: - dart: ">=3.10.0-0 <4.0.0" + dart: ">=3.9.0 <4.0.0" flutter: ">=3.29.0" diff --git a/cw_bitcoin/test/output_ordering_test.dart b/cw_bitcoin/test/output_ordering_test.dart new file mode 100644 index 000000000..d0fc7de05 --- /dev/null +++ b/cw_bitcoin/test/output_ordering_test.dart @@ -0,0 +1,52 @@ +import 'dart:math'; + +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/output_ordering.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('orderOutputs', () { + test('none preserves the given order', () { + final outputs = [0, 1, 2, 3, 4]; + expect(orderOutputs(outputs, BitcoinOrdering.none), outputs); + }); + + test('does not mutate the input list', () { + final outputs = [0, 1, 2, 3, 4]; + orderOutputs(outputs, BitcoinOrdering.shuffle, rng: Random(1)); + expect(outputs, [0, 1, 2, 3, 4]); + }); + + test('returns a new list instance', () { + final outputs = [0, 1, 2]; + expect(identical(orderOutputs(outputs, BitcoinOrdering.none), outputs), isFalse); + }); + + test('shuffle preserves the multiset of outputs', () { + final outputs = [0, 1, 2, 3, 4, 5, 6, 7]; + final shuffled = orderOutputs(outputs, BitcoinOrdering.shuffle, rng: Random(7)); + expect(shuffled.length, outputs.length); + expect(shuffled.toSet(), outputs.toSet()); + }); + + test('shuffle is deterministic for a given seed', () { + final outputs = [0, 1, 2, 3, 4, 5]; + final a = orderOutputs(outputs, BitcoinOrdering.shuffle, rng: Random(42)); + final b = orderOutputs(outputs, BitcoinOrdering.shuffle, rng: Random(42)); + expect(a, b); + }); + + test('shuffle does not keep the change output deterministically last', () { + // Last element (99) models the change output, appended last today. + final outputs = [0, 1, 2, 3, 99]; + final changePositions = {}; + for (var seed = 0; seed < 50; seed++) { + final shuffled = orderOutputs(outputs, BitcoinOrdering.shuffle, rng: Random(seed)); + changePositions.add(shuffled.indexOf(99)); + } + // Across seeds the change lands in more than one position, and not always last. + expect(changePositions.length, greaterThan(1)); + expect(changePositions, isNot(equals({outputs.length - 1}))); + }); + }); +} diff --git a/cw_core/lib/amount/money.dart b/cw_core/lib/amount/money.dart index 9c2332310..20857e092 100644 --- a/cw_core/lib/amount/money.dart +++ b/cw_core/lib/amount/money.dart @@ -1,13 +1,12 @@ -import 'package:cw_core/crypto_amount_format.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/currency.dart'; -import 'package:cw_core/format_fixed.dart'; -import 'package:cw_core/parse_fixed.dart'; +import "package:cw_core/crypto_amount_format.dart"; +import "package:cw_core/crypto_currency.dart"; +import "package:cw_core/currency.dart"; +import "package:cw_core/format_fixed.dart"; +import "package:cw_core/parse_fixed.dart"; + +export "money_local.dart"; class Money implements Comparable { - final BigInt amount; - final Currency currency; - const Money(this.amount, this.currency); factory Money.zero(Currency currency) => Money(BigInt.zero, currency); @@ -38,6 +37,9 @@ class Money implements Comparable { return amount != null ? Money(amount, currency) : null; } + final BigInt amount; + final Currency currency; + /// Returns the sign of this [BigInt] amount. /// Returns 0 for zero, -1 for values less than zero and +1 for values /// greater than zero. @@ -139,7 +141,9 @@ class Money implements Comparable { /// /// The result is again [Money]. Money operator /(BigInt other) { - if (other == BigInt.zero) throw Exception('Division by zero.'); + if (other == BigInt.zero) { + throw Exception("Division by zero."); + } final neg = (amount.isNegative) ^ (other.isNegative); final A = amount.abs(); @@ -163,7 +167,9 @@ class Money implements Comparable { Money copyWith({BigInt? amount, Currency? currency}) { if (currency != null && amount == null && currency.decimals != this.currency.decimals) { return Money( - _transformAmount(this.amount, this.currency.decimals, currency.decimals), currency); + _transformAmount(this.amount, this.currency.decimals, currency.decimals), + currency, + ); } return Money(amount ?? this.amount, currency ?? this.currency); @@ -173,16 +179,21 @@ class Money implements Comparable { Money _withAmount(BigInt amount) => Money(amount, currency); void _assertSameCurrency(Money other, [String? message]) { - if (currency != other.currency) + if (currency != other.currency) { throw ArgumentError(message ?? "Cannot operate with money values in different currencies."); + } } BigInt _transformAmount(BigInt source, int sourceDecimals, int targetDecimals) { - if (sourceDecimals == targetDecimals) return source; + if (sourceDecimals == targetDecimals) { + return source; + } if (sourceDecimals > targetDecimals) { return parseFixed( - formatFixed(source, sourceDecimals).withMaxDecimals(targetDecimals), targetDecimals); + formatFixed(source, sourceDecimals).withMaxDecimals(targetDecimals), + targetDecimals, + ); } else { return parseFixed(formatFixed(source, sourceDecimals), targetDecimals); } @@ -198,18 +209,39 @@ class Money implements Comparable { @override String toString() => formatFixed(amount, currency.decimals); - String toStringWithSymbol( - {int? fractionalDigits, bool trimZeros = true, bool useBaseUnit = false}) => - "${toStringWithPrecision(fractionalDigits: fractionalDigits, trimZeros: trimZeros, useBaseUnit: useBaseUnit)} ${_getSymbol(useBaseUnit)}"; + String toStringWithSymbol({ + int? fractionalDigits, + bool trimZeros = true, + bool useBaseUnit = false, + bool withSymbolPrefix = false, + }) { + final amount = toStringWithPrecision( + fractionalDigits: fractionalDigits, + trimZeros: trimZeros, + useBaseUnit: useBaseUnit, + ); + final symbol = getSymbol(useBaseUnit: useBaseUnit); - String toStringWithPrecision( - {int? fractionalDigits, bool trimZeros = true, bool useBaseUnit = false}) => - formatFixed(amount, useBaseUnit ? 0 : currency.decimals, - fractionalDigits: fractionalDigits, trimZeros: trimZeros); + return withSymbolPrefix ? "$symbol $amount" : "$amount $symbol"; + } + + String toStringWithPrecision({ + int? fractionalDigits, + bool trimZeros = true, + bool useBaseUnit = false, + }) => + formatFixed( + amount, + useBaseUnit ? 0 : currency.decimals, + fractionalDigits: fractionalDigits, + trimZeros: trimZeros, + ); // To Override the symbol with the ticker of the base unit - String _getSymbol(bool useBaseUnit) { - if (useBaseUnit && [CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency)) return "sats"; + String getSymbol({required bool useBaseUnit}) { + if (useBaseUnit && [CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency)) { + return "sats"; + } return currency.symbol; } } diff --git a/cw_core/lib/amount/money_double.dart b/cw_core/lib/amount/money_double.dart index b3331c44b..8dc44c4e3 100644 --- a/cw_core/lib/amount/money_double.dart +++ b/cw_core/lib/amount/money_double.dart @@ -1,6 +1,6 @@ -import 'dart:math'; -import 'package:cw_core/amount/money.dart'; -import 'package:cw_core/currency.dart'; +import "dart:math"; +import "package:cw_core/amount/money.dart"; +import "package:cw_core/currency.dart"; extension ToMoney on double { /// Turn a double representation of a currency amount to a proper Money representation diff --git a/cw_core/lib/amount/money_local.dart b/cw_core/lib/amount/money_local.dart new file mode 100644 index 000000000..c5a3c0ea3 --- /dev/null +++ b/cw_core/lib/amount/money_local.dart @@ -0,0 +1,47 @@ +import "package:cw_core/amount/money.dart"; +import "package:intl/intl.dart"; + +extension WithLocalSeparator on Money { + String toLocalStringWithSymbol({ + int? fractionalDigits, + bool trimZeros = true, + bool useBaseUnit = false, + bool withSymbolPrefix = false, + String? locale, + }) { + final amount = toLocalStringWithPrecision( + fractionalDigits: fractionalDigits, + trimZeros: trimZeros, + useBaseUnit: useBaseUnit, + locale: locale, + ); + final symbol = getSymbol(useBaseUnit: useBaseUnit); + + return withSymbolPrefix ? "$symbol $amount" : "$amount $symbol"; + } + + String toLocalStringWithPrecision({ + int? fractionalDigits, + bool trimZeros = true, + bool useBaseUnit = false, + String? locale, + }) => + _withLocalSeparator( + toStringWithPrecision( + fractionalDigits: fractionalDigits, + trimZeros: trimZeros, + useBaseUnit: useBaseUnit, + ), + locale: locale, + ); + + String _withLocalSeparator(String amount, {String? locale}) { + final isNegative = amount.startsWith("-"); + final formater = NumberFormat("#,###", locale); + final parts = (isNegative ? amount.substring(1) : amount).split("."); + final formatted = [formater.format(int.tryParse(parts.first) ?? 0), ...parts.sublist(1)] + .join(formater.symbols.DECIMAL_SEP); + + return isNegative ? "-$formatted" : formatted; + } +} diff --git a/cw_core/lib/get_height_by_date_zec.dart b/cw_core/lib/get_height_by_date_zec.dart new file mode 100644 index 000000000..2176e8bfc --- /dev/null +++ b/cw_core/lib/get_height_by_date_zec.dart @@ -0,0 +1,416 @@ +class ZcashHeight { + // for i in $(seq 1 331); do echo "_Checkpoint($(date -j -f "%Y-%m-%d %H:%M:%S" "$(curl -s "https://mainnet.zcashexplorer.app/blocks/${i}0000" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}')" "+%s"), ${i}0000),"; done > points.txt + static final List<_Checkpoint> _points = [ + _Checkpoint(1479114667, 10000), + _Checkpoint(1480618460, 20000), + _Checkpoint(1482129143, 30000), + _Checkpoint(1483635610, 40000), + _Checkpoint(1485145514, 50000), + _Checkpoint(1486655742, 60000), + _Checkpoint(1488163642, 70000), + _Checkpoint(1489668244, 80000), + _Checkpoint(1491175107, 90000), + _Checkpoint(1492681094, 100000), + _Checkpoint(1494185626, 110000), + _Checkpoint(1495691260, 120000), + _Checkpoint(1497196465, 130000), + _Checkpoint(1498702428, 140000), + _Checkpoint(1500209116, 150000), + _Checkpoint(1501716823, 160000), + _Checkpoint(1503222893, 170000), + _Checkpoint(1504730866, 180000), + _Checkpoint(1506236963, 190000), + _Checkpoint(1507744233, 200000), + _Checkpoint(1509254578, 210000), + _Checkpoint(1510768449, 220000), + _Checkpoint(1512275502, 230000), + _Checkpoint(1513783951, 240000), + _Checkpoint(1515292290, 250000), + _Checkpoint(1516799219, 260000), + _Checkpoint(1518306180, 270000), + _Checkpoint(1519813817, 280000), + _Checkpoint(1521319150, 290000), + _Checkpoint(1522828107, 300000), + _Checkpoint(1524336223, 310000), + _Checkpoint(1525844954, 320000), + _Checkpoint(1527354342, 330000), + _Checkpoint(1528860301, 340000), + _Checkpoint(1530365206, 350000), + _Checkpoint(1531873693, 360000), + _Checkpoint(1533380392, 370000), + _Checkpoint(1534888873, 380000), + _Checkpoint(1536393381, 390000), + _Checkpoint(1537899117, 400000), + _Checkpoint(1539406330, 410000), + _Checkpoint(1540912214, 420000), + _Checkpoint(1542422891, 430000), + _Checkpoint(1543931111, 440000), + _Checkpoint(1545437758, 450000), + _Checkpoint(1546945529, 460000), + _Checkpoint(1548450107, 470000), + _Checkpoint(1549957767, 480000), + _Checkpoint(1551466960, 490000), + _Checkpoint(1552969806, 500000), + _Checkpoint(1554477648, 510000), + _Checkpoint(1555981451, 520000), + _Checkpoint(1557489241, 530000), + _Checkpoint(1558997282, 540000), + _Checkpoint(1560503840, 550000), + _Checkpoint(1562011717, 560000), + _Checkpoint(1563518091, 570000), + _Checkpoint(1565027136, 580000), + _Checkpoint(1566532760, 590000), + _Checkpoint(1568039395, 600000), + _Checkpoint(1569547911, 610000), + _Checkpoint(1571051853, 620000), + _Checkpoint(1572559831, 630000), + _Checkpoint(1574069743, 640000), + _Checkpoint(1575576895, 650000), + _Checkpoint(1576603014, 660000), + _Checkpoint(1577356346, 670000), + _Checkpoint(1578109375, 680000), + _Checkpoint(1578863180, 690000), + _Checkpoint(1579616443, 700000), + _Checkpoint(1580369806, 710000), + _Checkpoint(1581122786, 720000), + _Checkpoint(1581876634, 730000), + _Checkpoint(1582630471, 740000), + _Checkpoint(1583383678, 750000), + _Checkpoint(1584133944, 760000), + _Checkpoint(1584887940, 770000), + _Checkpoint(1585641057, 780000), + _Checkpoint(1586395543, 790000), + _Checkpoint(1587148500, 800000), + _Checkpoint(1587901493, 810000), + _Checkpoint(1588655183, 820000), + _Checkpoint(1589408476, 830000), + _Checkpoint(1590163464, 840000), + _Checkpoint(1590916379, 850000), + _Checkpoint(1591669786, 860000), + _Checkpoint(1592423887, 870000), + _Checkpoint(1593176771, 880000), + _Checkpoint(1593930930, 890000), + _Checkpoint(1594684380, 900000), + _Checkpoint(1595439209, 910000), + _Checkpoint(1596192463, 920000), + _Checkpoint(1596946009, 930000), + _Checkpoint(1597700256, 940000), + _Checkpoint(1598453538, 950000), + _Checkpoint(1599207483, 960000), + _Checkpoint(1599960598, 970000), + _Checkpoint(1600714563, 980000), + _Checkpoint(1601467884, 990000), + _Checkpoint(1602220941, 1000000), + _Checkpoint(1602974722, 1010000), + _Checkpoint(1603728848, 1020000), + _Checkpoint(1604485637, 1030000), + _Checkpoint(1605238381, 1040000), + _Checkpoint(1605992807, 1050000), + _Checkpoint(1606747750, 1060000), + _Checkpoint(1607500606, 1070000), + _Checkpoint(1608255278, 1080000), + _Checkpoint(1609008684, 1090000), + _Checkpoint(1609763782, 1100000), + _Checkpoint(1610517871, 1110000), + _Checkpoint(1611271575, 1120000), + _Checkpoint(1612025546, 1130000), + _Checkpoint(1612779940, 1140000), + _Checkpoint(1613533993, 1150000), + _Checkpoint(1614287669, 1160000), + _Checkpoint(1615042297, 1170000), + _Checkpoint(1615792993, 1180000), + _Checkpoint(1616546145, 1190000), + _Checkpoint(1617299867, 1200000), + _Checkpoint(1618054038, 1210000), + _Checkpoint(1618808513, 1220000), + _Checkpoint(1619563243, 1230000), + _Checkpoint(1620318159, 1240000), + _Checkpoint(1621071298, 1250000), + _Checkpoint(1621826156, 1260000), + _Checkpoint(1622580237, 1270000), + _Checkpoint(1623335553, 1280000), + _Checkpoint(1624090141, 1290000), + _Checkpoint(1624844712, 1300000), + _Checkpoint(1625597720, 1310000), + _Checkpoint(1626351652, 1320000), + _Checkpoint(1627106054, 1330000), + _Checkpoint(1627860648, 1340000), + _Checkpoint(1628614015, 1350000), + _Checkpoint(1629368398, 1360000), + _Checkpoint(1630122679, 1370000), + _Checkpoint(1630877190, 1380000), + _Checkpoint(1631631884, 1390000), + _Checkpoint(1632385851, 1400000), + _Checkpoint(1633141006, 1410000), + _Checkpoint(1633895592, 1420000), + _Checkpoint(1634649550, 1430000), + _Checkpoint(1635403121, 1440000), + _Checkpoint(1636157669, 1450000), + _Checkpoint(1636915133, 1460000), + _Checkpoint(1637670036, 1470000), + _Checkpoint(1638424121, 1480000), + _Checkpoint(1639177413, 1490000), + _Checkpoint(1639931234, 1500000), + _Checkpoint(1640684476, 1510000), + _Checkpoint(1641438827, 1520000), + _Checkpoint(1642192402, 1530000), + _Checkpoint(1642946383, 1540000), + _Checkpoint(1643700901, 1550000), + _Checkpoint(1644455522, 1560000), + _Checkpoint(1645208504, 1570000), + _Checkpoint(1645962081, 1580000), + _Checkpoint(1646715150, 1590000), + _Checkpoint(1647465727, 1600000), + _Checkpoint(1648219045, 1610000), + _Checkpoint(1648973131, 1620000), + _Checkpoint(1649727422, 1630000), + _Checkpoint(1650481408, 1640000), + _Checkpoint(1651234287, 1650000), + _Checkpoint(1651989865, 1660000), + _Checkpoint(1652743327, 1670000), + _Checkpoint(1653497455, 1680000), + _Checkpoint(1654252094, 1690000), + _Checkpoint(1655005944, 1700000), + _Checkpoint(1655759708, 1710000), + _Checkpoint(1656513765, 1720000), + _Checkpoint(1657268358, 1730000), + _Checkpoint(1658022453, 1740000), + _Checkpoint(1658777019, 1750000), + _Checkpoint(1659531133, 1760000), + _Checkpoint(1660283907, 1770000), + _Checkpoint(1661037645, 1780000), + _Checkpoint(1661791977, 1790000), + _Checkpoint(1662544394, 1800000), + _Checkpoint(1663298018, 1810000), + _Checkpoint(1664051450, 1820000), + _Checkpoint(1664805547, 1830000), + _Checkpoint(1665558994, 1840000), + _Checkpoint(1666312456, 1850000), + _Checkpoint(1667066473, 1860000), + _Checkpoint(1667823393, 1870000), + _Checkpoint(1668576623, 1880000), + _Checkpoint(1669330657, 1890000), + _Checkpoint(1670084156, 1900000), + _Checkpoint(1670837831, 1910000), + _Checkpoint(1671591094, 1920000), + _Checkpoint(1672343975, 1930000), + _Checkpoint(1673097703, 1940000), + _Checkpoint(1673852016, 1950000), + _Checkpoint(1674605810, 1960000), + _Checkpoint(1675360050, 1970000), + _Checkpoint(1676113710, 1980000), + _Checkpoint(1676867174, 1990000), + _Checkpoint(1677620242, 2000000), + _Checkpoint(1678374852, 2010000), + _Checkpoint(1679125063, 2020000), + _Checkpoint(1679878121, 2030000), + _Checkpoint(1680632254, 2040000), + _Checkpoint(1681386518, 2050000), + _Checkpoint(1682140381, 2060000), + _Checkpoint(1682894564, 2070000), + _Checkpoint(1683648178, 2080000), + _Checkpoint(1684401932, 2090000), + _Checkpoint(1685156464, 2100000), + _Checkpoint(1685910040, 2110000), + _Checkpoint(1686663406, 2120000), + _Checkpoint(1687417875, 2130000), + _Checkpoint(1688172069, 2140000), + _Checkpoint(1688925610, 2150000), + _Checkpoint(1689679500, 2160000), + _Checkpoint(1690432691, 2170000), + _Checkpoint(1691186349, 2180000), + _Checkpoint(1691940901, 2190000), + _Checkpoint(1692694780, 2200000), + _Checkpoint(1693449743, 2210000), + _Checkpoint(1694203665, 2220000), + _Checkpoint(1694957643, 2230000), + _Checkpoint(1695710712, 2240000), + _Checkpoint(1696465787, 2250000), + _Checkpoint(1697218701, 2260000), + _Checkpoint(1697973031, 2270000), + _Checkpoint(1698726864, 2280000), + _Checkpoint(1699483348, 2290000), + _Checkpoint(1700237650, 2300000), + _Checkpoint(1700991210, 2310000), + _Checkpoint(1701745742, 2320000), + _Checkpoint(1702500723, 2330000), + _Checkpoint(1703253121, 2340000), + _Checkpoint(1704007238, 2350000), + _Checkpoint(1704760935, 2360000), + _Checkpoint(1705514669, 2370000), + _Checkpoint(1706268087, 2380000), + _Checkpoint(1707021423, 2390000), + _Checkpoint(1707776257, 2400000), + _Checkpoint(1708529542, 2410000), + _Checkpoint(1709283065, 2420000), + _Checkpoint(1710037165, 2430000), + _Checkpoint(1710787780, 2440000), + _Checkpoint(1711541488, 2450000), + _Checkpoint(1712294996, 2460000), + _Checkpoint(1713048843, 2470000), + _Checkpoint(1713803486, 2480000), + _Checkpoint(1714557127, 2490000), + _Checkpoint(1715311181, 2500000), + _Checkpoint(1716064271, 2510000), + _Checkpoint(1716818792, 2520000), + _Checkpoint(1717572765, 2530000), + _Checkpoint(1718326333, 2540000), + _Checkpoint(1719080275, 2550000), + _Checkpoint(1719834297, 2560000), + _Checkpoint(1720588042, 2570000), + _Checkpoint(1721341832, 2580000), + _Checkpoint(1722095122, 2590000), + _Checkpoint(1722848604, 2600000), + _Checkpoint(1723602357, 2610000), + _Checkpoint(1724355843, 2620000), + _Checkpoint(1725110085, 2630000), + _Checkpoint(1725863681, 2640000), + _Checkpoint(1726616892, 2650000), + _Checkpoint(1727371443, 2660000), + _Checkpoint(1728124893, 2670000), + _Checkpoint(1728878141, 2680000), + _Checkpoint(1729631572, 2690000), + _Checkpoint(1730385888, 2700000), + _Checkpoint(1731142437, 2710000), + _Checkpoint(1731896266, 2720000), + _Checkpoint(1732650193, 2730000), + _Checkpoint(1733403626, 2740000), + _Checkpoint(1734157390, 2750000), + _Checkpoint(1734911442, 2760000), + _Checkpoint(1735665720, 2770000), + _Checkpoint(1736419845, 2780000), + _Checkpoint(1737173511, 2790000), + _Checkpoint(1737926795, 2800000), + _Checkpoint(1738681511, 2810000), + _Checkpoint(1739434236, 2820000), + _Checkpoint(1740188214, 2830000), + _Checkpoint(1740941352, 2840000), + _Checkpoint(1741692338, 2850000), + _Checkpoint(1742445247, 2860000), + _Checkpoint(1743198927, 2870000), + _Checkpoint(1743952947, 2880000), + _Checkpoint(1744706370, 2890000), + _Checkpoint(1745461377, 2900000), + _Checkpoint(1746214735, 2910000), + _Checkpoint(1746967908, 2920000), + _Checkpoint(1747722271, 2930000), + _Checkpoint(1748474582, 2940000), + _Checkpoint(1749228582, 2950000), + _Checkpoint(1749983372, 2960000), + _Checkpoint(1750737278, 2970000), + _Checkpoint(1751490762, 2980000), + _Checkpoint(1752243931, 2990000), + _Checkpoint(1752997873, 3000000), + _Checkpoint(1753751756, 3010000), + _Checkpoint(1754504935, 3020000), + _Checkpoint(1755259449, 3030000), + _Checkpoint(1756012904, 3040000), + _Checkpoint(1756765335, 3050000), + _Checkpoint(1757519206, 3060000), + _Checkpoint(1758272484, 3070000), + _Checkpoint(1759025597, 3080000), + _Checkpoint(1759778957, 3090000), + _Checkpoint(1760532780, 3100000), + _Checkpoint(1761286210, 3110000), + _Checkpoint(1762039783, 3120000), + _Checkpoint(1762796627, 3130000), + _Checkpoint(1763550532, 3140000), + _Checkpoint(1764303860, 3150000), + _Checkpoint(1765057381, 3160000), + _Checkpoint(1765811222, 3170000), + _Checkpoint(1766564356, 3180000), + _Checkpoint(1767318326, 3190000), + _Checkpoint(1768071956, 3200000), + _Checkpoint(1768824957, 3210000), + _Checkpoint(1769578781, 3220000), + _Checkpoint(1770332206, 3230000), + _Checkpoint(1771086265, 3240000), + _Checkpoint(1771840180, 3250000), + _Checkpoint(1772592783, 3260000), + _Checkpoint(1773343947, 3270000), + _Checkpoint(1774097085, 3280000), + _Checkpoint(1774850161, 3290000), + _Checkpoint(1775603525, 3300000), + _Checkpoint(1776358117, 3310000), + ]; + + static int getBlockHeightByTime(DateTime time) { + final t = time.toUtc().millisecondsSinceEpoch; + + if (_points.isEmpty) return 0; + + if (t <= _points.first.timeMs) { + return 1; + } + + for (int i = 0; i < _points.length - 1; i++) { + final p0 = _points[i]; + final p1 = _points[i + 1]; + + if (t >= p0.timeMs && t < p1.timeMs) { + return _interpolate( + p0.height, + p1.height, + p0.timeMs, + p1.timeMs, + t, + ); + } + } + + final last = _points[_points.length - 1]; + final prev = _points[_points.length - 2]; + + final avgBlockTimeMs = (last.timeMs - prev.timeMs) / (last.height - prev.height); + + final blocksSinceLast = ((t - last.timeMs) / avgBlockTimeMs).floor(); + + return last.height + blocksSinceLast; + } + + static DateTime getTimeByBlockHeight(int height) { + if (_points.isEmpty) return DateTime.fromMillisecondsSinceEpoch(0, isUtc: true); + if (height <= 1) { + return DateTime.fromMillisecondsSinceEpoch(_points.first.timeMs, isUtc: true); + } + + for (int i = 0; i < _points.length - 1; i++) { + final p0 = _points[i]; + final p1 = _points[i + 1]; + if (height >= p0.height && height < p1.height) { + final t = _interpolate( + p0.timeMs, + p1.timeMs, + p0.height, + p1.height, + height, + ); + return DateTime.fromMillisecondsSinceEpoch(t, isUtc: true); + } + } + + final last = _points[_points.length - 1]; + final prev = _points[_points.length - 2]; + final avgBlockTimeMs = (last.timeMs - prev.timeMs) / (last.height - prev.height); + final msSinceLast = ((height - last.height) * avgBlockTimeMs).floor(); + return DateTime.fromMillisecondsSinceEpoch(last.timeMs + msSinceLast, isUtc: true); + } + + static int _interpolate( + int h0, + int h1, + int t0, + int t1, + int t, + ) { + final ratio = (t - t0) / (t1 - t0); + return h0 + ((h1 - h0) * ratio).floor(); + } +} + +class _Checkpoint { + final int timeMs; + final int height; + + _Checkpoint(int timeSec, this.height) : timeMs = timeSec * 1000; +} diff --git a/cw_core/lib/lnurl.dart b/cw_core/lib/lnurl.dart index 8ea33b99d..e0d17685a 100644 --- a/cw_core/lib/lnurl.dart +++ b/cw_core/lib/lnurl.dart @@ -1,19 +1,20 @@ -import 'dart:convert'; +import "dart:convert"; -import 'package:bech32/bech32.dart'; -import 'package:cw_core/amount/money.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; +import "package:bech32/bech32.dart"; +import "package:cw_core/amount/money.dart"; +import "package:cw_core/crypto_currency.dart"; +import "package:cw_core/utils/proxy_wrapper.dart"; const _BOLT_PREFIXES = ["lnbcrt", "lntbs", "lnbc", "lntb"]; -const _LUD17_PREFIXES = ['lnurlw', 'lnurlc', 'lnurlp', 'keyauth']; +const _LUD17_PREFIXES = ["lnurlw", "lnurlc", "lnurlp", "keyauth"]; bool isBolt11ZeroInvoice(String invoice) { try { - final request = Bech32Codec().decode(invoice.replaceFirst("lightning:", ""), invoice.length); + final request = + const Bech32Codec().decode(invoice.replaceFirst("lightning:", ""), invoice.length); final prefix = - _BOLT_PREFIXES.firstWhere((prefix) => request.hrp.startsWith(prefix), orElse: () => ""); + _BOLT_PREFIXES.firstWhere(request.hrp.startsWith, orElse: () => ""); return request.hrp.length == prefix.length; } catch (e) { @@ -23,48 +24,56 @@ bool isBolt11ZeroInvoice(String invoice) { /// Get the amount of a Bolt 11 Invoice in int? _getAmountBolt11Msat(String invoice) { - final request = Bech32Codec().decode(invoice.replaceFirst("lightning:", ""), invoice.length); + final request = + const Bech32Codec().decode(invoice.replaceFirst("lightning:", ""), invoice.length); final prefix = _BOLT_PREFIXES.firstWhere( - (prefix) => request.hrp.startsWith(prefix), - orElse: () => throw FormatException('Invalid BOLT11 invoice: unknown HRP prefix.'), + request.hrp.startsWith, + orElse: () => throw const FormatException("Invalid BOLT11 invoice: unknown HRP prefix."), ); final amountPart = request.hrp.substring(prefix.length); - if (amountPart.isEmpty) return null; // zero-amount invoice + if (amountPart.isEmpty) { + return null; // zero-amount invoice + } - final hasMultiplier = RegExp(r'[munp]$').hasMatch(amountPart); - final multiplier = hasMultiplier ? amountPart[amountPart.length - 1] : ''; + final hasMultiplier = RegExp(r"[munp]$").hasMatch(amountPart); + final multiplier = hasMultiplier ? amountPart[amountPart.length - 1] : ""; final numberStr = hasMultiplier ? amountPart.substring(0, amountPart.length - 1) : amountPart; - if (numberStr.isEmpty || !RegExp(r'^\d+$').hasMatch(numberStr)) { - throw FormatException('Invalid BOLT11 invoice: invalid amount number in HRP.'); + if (numberStr.isEmpty || !RegExp(r"^\d+$").hasMatch(numberStr)) { + throw const FormatException("Invalid BOLT11 invoice: invalid amount number in HRP."); } final amount = int.parse(numberStr); switch (multiplier) { - case '': // bitcoin + case "": // bitcoin return amount * 100000000000; - case 'm': // milli-bitcoin + case "m": // milli-bitcoin return amount * 100000000; - case 'u': // micro-bitcoin + case "u": // micro-bitcoin return amount * 100000; - case 'n': // nano-bitcoin + case "n": // nano-bitcoin return amount * 100; - case 'p': + case "p": if (amount % 10 != 0) { - throw FormatException('Invalid BOLT11 invoice: amount not representable in whole msat.'); + throw const FormatException( + "Invalid BOLT11 invoice: amount not representable in whole msat.", + ); } return amount ~/ 10; default: - throw FormatException('Invalid BOLT11 invoice: unknown amount multiplier.'); + throw const FormatException("Invalid BOLT11 invoice: unknown amount multiplier."); } } Money? getBolt11Amount(String invoice) { final msat = _getAmountBolt11Msat(invoice); - if (msat == null || msat % 1000 != 0) return null; + if (msat == null || msat % 1000 != 0) { + return null; + } + return Money.fromInt(msat ~/ 1000, CryptoCurrency.btcln); } @@ -73,30 +82,99 @@ class LNURL { final url = decode(lnurl); final response = await ProxyWrapper().get(clearnetUri: url); - if (response.statusCode != 200) return null; + if (response.statusCode != 200) { + return null; + } try { final body = jsonDecode(response.body) as Map; final tag = body["tag"] as String?; - if (tag != "payRequest") return null; + if (tag != "payRequest") { + return null; + } final msat = body["minSendable"] as int?; final maxSendable = body["maxSendable"] as int?; // if minSendable and maxSendable are the same we assume a specific payment request - if (msat != maxSendable) return null; + if (msat != maxSendable) { + return null; + } - if (msat == null || msat % 1000 != 0) return null; + if (msat == null || msat % 1000 != 0) { + return null; + } return Money.fromInt(msat ~/ 1000, CryptoCurrency.btcln); } catch (_) { return null; } } + static Future getWithdrawRequest(String lnurl) async { + final url = decode(lnurl); + final response = await ProxyWrapper().get(clearnetUri: url); + + if (response.statusCode != 200) { + return null; + } + + try { + final body = jsonDecode(response.body) as Map; + final tag = body["tag"] as String?; + + if (tag != "withdrawRequest") { + return null; + } + + final minWithdrawable = body["minWithdrawable"] as int?; + final maxWithdrawable = body["maxWithdrawable"] as int?; + + if ((minWithdrawable == null || minWithdrawable % 1000 != 0) || + (maxWithdrawable == null || maxWithdrawable % 1000 != 0)) { + return null; + } + + return LNURLWithdrawRequest( + callback: body["callback"] as String, + k1: body["k1"] as String, + description: body["defaultDescription"] as String, + minAmount: Money.fromInt(minWithdrawable ~/ 1000, CryptoCurrency.btcln), + maxAmount: Money.fromInt(maxWithdrawable ~/ 1000, CryptoCurrency.btcln), + ); + } catch (_) { + return null; + } + } + + Future commitWithdrawRequest(LNURLWithdrawRequest request, String invoice) async { + try { + final url = Uri.parse(request.callback); + url.queryParameters["k1"] = request.k1; + url.queryParameters["pr"] = invoice; + + final response = await ProxyWrapper().get(clearnetUri: url); + + if (response.statusCode != 200) { + return false; + } + + final body = jsonDecode(response.body) as Map; + final status = body["status"] as String?; + + if (status == "OK") { + return true; + } + + return false; + } catch (_) { + return false; + } + } + static String encode(String url) { final raw = _convert(utf8.encode(url), 8, 5, true); - return const Bech32Codec().encode(Bech32('lnurl', raw), 999); + return const Bech32Codec().encode(Bech32("lnurl", raw), 999); } static Uri decode(String encodedUrl) { @@ -114,9 +192,10 @@ class LNURL { if (_LUD17_PREFIXES.contains(decodedUri.scheme)) { /// If the non-bech32 LNURL is a Tor address, the port has to be http instead of https for the clearnet LNURL so check if the host ends with '.onion' or '.onion.' decodedUri = decodedUri.replace( - scheme: decodedUri.host.endsWith('onion') || decodedUri.host.endsWith('onion.') - ? 'http' - : 'https'); + scheme: decodedUri.host.endsWith("onion") || decodedUri.host.endsWith("onion.") + ? "http" + : "https", + ); } else { /// Try to parse the input as a lnUrl. Will throw an error if it fails. final lnUrl = _findLnUrl(encodedUrl); @@ -129,12 +208,30 @@ class LNURL { } } +class LNURLWithdrawRequest { + const LNURLWithdrawRequest({ + required this.callback, + required this.k1, + required this.description, + required this.minAmount, + required this.maxAmount, + }); + + final String callback; + final String k1; + final String description; + final Money minAmount; + final Money maxAmount; +} + /// Parse and return a given lnurl string if it's valid. Will remove /// `lightning:` from the beginning of it if present. String _findLnUrl(String input) { - final res = RegExp(r',*?((lnurl)([0-9]+[a-z0-9]+))').allMatches(input.toLowerCase()); + final res = RegExp(r",*?((lnurl)([0-9]+[a-z0-9]+))").allMatches(input.toLowerCase()); - if (res.length != 1) throw ArgumentError('Not a valid lnurl string'); + if (res.length != 1) { + throw ArgumentError("Not a valid lnurl string"); + } return res.first.group(0)!; } @@ -156,10 +253,16 @@ List _convert(List data, int inBits, int outBits, bool pad) { } if (pad) { - if (bits > 0) result.add((value << (outBits - bits)) & maxV); + if (bits > 0) { + result.add((value << (outBits - bits)) & maxV); + } } else { - if (bits >= inBits) throw Exception('[BECH32] Excess padding'); - if ((value << (outBits - bits)) & maxV > 0) throw Exception('[BECH32] Non-zero padding'); + if (bits >= inBits) { + throw Exception("[BECH32] Excess padding"); + } + if ((value << (outBits - bits)) & maxV > 0) { + throw Exception("[BECH32] Non-zero padding"); + } } return result; diff --git a/cw_core/lib/payment_uris.dart b/cw_core/lib/payment_uris.dart index 2703d5e21..43d452f8d 100644 --- a/cw_core/lib/payment_uris.dart +++ b/cw_core/lib/payment_uris.dart @@ -1,4 +1,4 @@ -import 'package:cw_core/format_fixed.dart'; +import "package:cw_core/format_fixed.dart"; abstract class PaymentURI { PaymentURI({required this.amount, required this.address}); @@ -19,7 +19,7 @@ class MoneroURI extends PaymentURI { @override String toString() { - var base = 'monero:$address'; + var base = "monero:$address"; if (amount.isNotEmpty) { base += '?tx_amount=${amount.replaceAll(',', '.')}'; @@ -34,7 +34,7 @@ class HavenURI extends PaymentURI { @override String toString() { - var base = 'haven:$address'; + var base = "haven:$address"; if (amount.isNotEmpty) { base += '?tx_amount=${amount.replaceAll(',', '.')}'; @@ -45,7 +45,7 @@ class HavenURI extends PaymentURI { } class BitcoinURI extends PaymentURI { - BitcoinURI({required super.amount, required super.address, this.pjUri = ''}); + BitcoinURI({required super.amount, required super.address, this.pjUri = ""}); final String pjUri; @@ -53,13 +53,16 @@ class BitcoinURI extends PaymentURI { String toString() { final qp = {}; - if (amount.isNotEmpty) qp['amount'] = amount.replaceAll(',', '.'); - if (pjUri.isNotEmpty && !address.startsWith("sp")) { - qp['pjos'] = '0'; - qp['pj'] = pjUri; + if (amount.isNotEmpty) { + qp["amount"] = amount.replaceAll(",", "."); } - return Uri(scheme: 'bitcoin', path: address, queryParameters: qp).toString(); + if (pjUri.isNotEmpty && !address.startsWith("sp")) { + qp["pjos"] = "0"; + qp["pj"] = pjUri; + } + + return Uri(scheme: "bitcoin", path: address, queryParameters: qp).toString(); } } @@ -68,7 +71,7 @@ class LitecoinURI extends PaymentURI { @override String toString() { - var base = 'litecoin:$address'; + var base = "litecoin:$address"; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; @@ -83,52 +86,7 @@ class EthereumURI extends PaymentURI { @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 BSCURI extends PaymentURI { - BSCURI({required super.amount, required super.address}); - - @override - String toString() { - var base = 'bsc:$address'; + var base = "ethereum:$address"; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; @@ -158,22 +116,7 @@ class NanoURI extends PaymentURI { @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'; - + var base = "nano:$address"; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; } @@ -189,14 +132,14 @@ class SolanaURI extends PaymentURI { @override String toString() { - var base = 'solana:$address'; + var base = "solana:$address"; final params = []; if (amount.isNotEmpty) { params.add('amount=${amount.replaceAll(',', '.')}'); } if (contractAddress != null && contractAddress!.isNotEmpty) { - params.add('spl-token=$contractAddress'); + params.add("spl-token=$contractAddress"); } if (params.isNotEmpty) { base += '?${params.join('&')}'; @@ -213,14 +156,14 @@ class TronURI extends PaymentURI { @override String toString() { - var base = 'tron:$address'; + var base = "tron:$address"; final params = []; if (amount.isNotEmpty) { params.add('amount=${amount.replaceAll(',', '.')}'); } if (contractAddress != null && contractAddress!.isNotEmpty) { - params.add('token=$contractAddress'); + params.add("token=$contractAddress"); } if (params.isNotEmpty) { base += '?${params.join('&')}'; @@ -235,7 +178,7 @@ class WowneroURI extends PaymentURI { @override String toString() { - var base = 'wownero:$address'; + var base = "wownero:$address"; if (amount.isNotEmpty) { base += '?tx_amount=${amount.replaceAll(',', '.')}'; @@ -246,12 +189,11 @@ class WowneroURI extends PaymentURI { } class ZanoURI extends PaymentURI { - ZanoURI({required String amount, required String address}) - : super(amount: amount, address: address); + ZanoURI({required super.amount, required super.address}); @override String toString() { - var base = 'zano:' + address; + var base = "zano:$address"; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; @@ -262,12 +204,11 @@ class ZanoURI extends PaymentURI { } class DecredURI extends PaymentURI { - DecredURI({required String amount, required String address}) - : super(amount: amount, address: address); + DecredURI({required super.amount, required super.address}); @override String toString() { - var base = 'decred:' + address; + var base = "decred:$address"; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; @@ -278,12 +219,11 @@ class DecredURI extends PaymentURI { } class DogeURI extends PaymentURI { - DogeURI({required String amount, required String address}) - : super(amount: amount, address: address); + DogeURI({required super.amount, required super.address}); @override String toString() { - var base = 'doge:' + address; + var base = "doge:$address"; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; @@ -298,7 +238,7 @@ class ZcashURI extends PaymentURI { @override String toString() { - var base = 'zcash:$address'; + var base = "zcash:$address"; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; @@ -309,9 +249,6 @@ class ZcashURI extends PaymentURI { } class ERC681URI extends PaymentURI { - final int chainId; - final String? contractAddress; - ERC681URI({ required this.chainId, required super.address, @@ -319,74 +256,11 @@ class ERC681URI extends PaymentURI { 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 address = isContract ? uri.queryParameters["address"] ?? "" : targetAddress; final amountParam = isContract ? uri.queryParameters["uint256"] : uri.queryParameters["value"]; var formatedAmount = ""; @@ -406,16 +280,78 @@ class ERC681URI extends PaymentURI { ); } - static int _getChainID(String path) { - return int.parse(RegExp( - r'@\d*', - ).firstMatch(path)?.group(0)?.replaceAll("@", "") ?? - "1"); + final int chainId; + final String? 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 += "?${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(",", "."); + } + } + + static int _getChainID(String path) => int.parse( + RegExp(r"@\d*").firstMatch(path)?.group(0)?.replaceAll("@", "") ?? "1", + ); + static (bool, String) _getTargetAddress(String path) { final targetAddress = - RegExp(r'^(0x)?[0-9a-f]{40}', caseSensitive: false).firstMatch(path)!.group(0)!; + RegExp(r"^(0x)?[0-9a-f]{40}", caseSensitive: false).firstMatch(path)!.group(0)!; return (path.contains("/"), targetAddress); } @@ -426,25 +362,27 @@ class ERC681URI extends PaymentURI { /// - Scientific notation: "0.123e18", "1e6" → expanded to integer /// - Decimal ETH: "0.123456" → shifted by 18 decimals static String _normalizeToIntegerWei(String input) { - final raw = input.replaceAll(',', '.').trim(); + final raw = input.replaceAll(",", ".").trim(); // First we check if it's already a plain integer (basically just a number with no dot, no exponent) try { - final isPlainInteger = RegExp(r'^[+-]?\d+$').hasMatch(raw) && - !raw.contains('.') && - !raw.toLowerCase().contains('e'); - if (isPlainInteger) return raw.replaceFirst(RegExp(r'^\+'), ''); + final isPlainInteger = RegExp(r"^[+-]?\d+$").hasMatch(raw) && + !raw.contains(".") && + !raw.toLowerCase().contains("e"); + if (isPlainInteger) { + return raw.replaceFirst(RegExp(r"^\+"), ""); + } // Then we check if it's a scientific notation - final sci = RegExp(r'^[+-]?(\d+\.?\d*|\d*\.?\d+)[eE][+-]?\d+$'); + final sci = RegExp(r"^[+-]?(\d+\.?\d*|\d*\.?\d+)[eE][+-]?\d+$"); if (sci.hasMatch(raw)) { - final mantissaStr = raw.toLowerCase().split('e')[0]; - final exp = int.parse(raw.toLowerCase().split('e')[1]); + final mantissaStr = raw.toLowerCase().split("e")[0]; + final exp = int.parse(raw.toLowerCase().split("e")[1]); return _expandDecimal(mantissaStr, exp); } // Lastly, we check if it's a fixed decimal ETH amount, here we shift by 18 to get wei for the amount - if (raw.contains('.')) { + if (raw.contains(".")) { return _expandDecimal(raw, 18); } return raw; @@ -462,34 +400,34 @@ class ERC681URI extends PaymentURI { /// _expandDecimal('1.2', 3) -> '1200' static String _expandDecimal(String decimalStr, int expShift) { var s = decimalStr.trim(); - var sign = ''; - if (s.startsWith('-') || s.startsWith('+')) { - sign = s[0] == '-' ? '-' : ''; + var sign = ""; + if (s.startsWith("-") || s.startsWith("+")) { + sign = s[0] == "-" ? "-" : ""; s = s.substring(1); } // First we split the integer and fractional parts - final parts = s.split('.'); - final intPart = parts[0].isEmpty ? '0' : parts[0]; - final fracPart = parts.length > 1 ? parts[1] : ''; - final digits = (intPart + fracPart).replaceFirst(RegExp(r'^0+'), ''); + final parts = s.split("."); + final intPart = parts[0].isEmpty ? "0" : parts[0]; + final fracPart = parts.length > 1 ? parts[1] : ""; + final digits = (intPart + fracPart).replaceFirst(RegExp(r"^0+"), ""); final fracLen = fracPart.length; // Then we calculate the effective shift = desired shift minus existing fractional digits final shift = expShift - fracLen; if (shift >= 0) { - final head = digits.isEmpty ? '0' : digits; - final zeros = List.filled(shift, '0').join(); + final head = digits.isEmpty ? "0" : digits; + final zeros = List.filled(shift, "0").join(); final res = head + zeros; - return sign + (res.isEmpty ? '0' : res); + return sign + (res.isEmpty ? "0" : res); } else { // Need to insert a decimal point within digits; return integer by truncating final cut = digits.length + shift; if (cut <= 0) { - return '0'; + return "0"; } final res = digits.substring(0, cut); - return sign + (res.isEmpty ? '0' : res); + return sign + (res.isEmpty ? "0" : res); } } } diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index aa4b258c2..585ce3b3a 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -586,11 +586,12 @@ class WalletInfo { "addressPageType": addressPageType, "receiveInfoboxDismissed": receiveInfoboxDismissed ? 1 : 0, "showCombinedBalance": showCombinedBalance ? 1 : 0, - "favoriteTokenAddress": favoriteTokenAddress + "favoriteTokenAddress": favoriteTokenAddress, + "network": network, }; factory WalletInfo.fromJson(Map json) { - return WalletInfo( + final info = WalletInfo( json[selfIdColumn] as int, json['id'] as String, json['name'] as String, @@ -616,6 +617,8 @@ class WalletInfo { json['receiveInfoboxDismissed'] != 0, json["showCombinedBalance"] != 0, json["favoriteTokenAddress"] as String? ?? null); + info.network = json['network'] as String?; + return info; } Future save() async { diff --git a/cw_core/lib/wallet_info_legacy.dart b/cw_core/lib/wallet_info_legacy.dart index 35a710ee9..2dadbf2ab 100644 --- a/cw_core/lib/wallet_info_legacy.dart +++ b/cw_core/lib/wallet_info_legacy.dart @@ -274,7 +274,8 @@ class WalletInfo extends HiveObject { addressPageType, false, true, - null); + null) + ..network = network; final wiId = await walletInfo.save(); for (final address in usedAddresses ?? []) { await newWi.WalletInfoAddress.insert(wiId, newWi.WalletInfoAddressType.used, address); diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index 908829bcb..8eff1d338 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -394,10 +394,10 @@ packages: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.0" mime: dependency: transitive description: @@ -738,10 +738,10 @@ packages: dependency: transitive description: name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.10" torch_dart: dependency: "direct main" description: @@ -838,5 +838,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.10.0-0 <4.0.0" + dart: ">=3.9.0 <4.0.0" flutter: ">=3.27.0" diff --git a/cw_core/test/amount/money_local_test.dart b/cw_core/test/amount/money_local_test.dart new file mode 100644 index 000000000..24a4fffac --- /dev/null +++ b/cw_core/test/amount/money_local_test.dart @@ -0,0 +1,128 @@ +import "package:cw_core/amount/money.dart"; +import "package:cw_core/amount/money_local.dart"; +import "package:cw_core/crypto_currency.dart"; +import "package:flutter_test/flutter_test.dart"; +import "package:intl/intl.dart"; + +final _btc012 = Money.fromInt(12345678, CryptoCurrency.btc); // 0.12345678 BTC +final _btcTrailing = Money.fromInt(12000000, CryptoCurrency.btc); // 0.12 BTC +final _btcGrouping = Money.fromInt(123450000000, CryptoCurrency.btc); // 1234.5 BTC +final _xmrHalf = Money.fromInt(500000000000, CryptoCurrency.xmr); // 0.5 XMR + +void main() { + group("toLocalStringWithPrecision", () { + test("localizes grouping (en_US)", () { + expect(_btcGrouping.toLocalStringWithPrecision(locale: "en_US"), "1,234.5"); + }); + + test("no grouping for sub-1 values", () { + expect(_btc012.toLocalStringWithPrecision(locale: "en_US"), "0.12345678"); + }); + + test("swaps grouping/decimal separators (de_DE)", () { + expect(_btcGrouping.toLocalStringWithPrecision(locale: "de_DE"), "1.234,5"); + }); + + test("base unit renders integer sats with grouping", () { + expect( + _btc012.toLocalStringWithPrecision(useBaseUnit: true, locale: "en_US"), + "12,345,678", + ); + }); + + test("fractionalDigits truncates (does not round)", () { + // 0.12345678 -> 0.12 (NOT 0.13). + expect(_btc012.toLocalStringWithPrecision(fractionalDigits: 2, locale: "en_US"), "0.12"); + }); + + test("trimZeros:false keeps trailing zeros", () { + expect( + _btcTrailing.toLocalStringWithPrecision(trimZeros: false, locale: "en_US"), + "0.12000000", + ); + }); + + test("respects a higher-precision currency (XMR)", () { + expect(_xmrHalf.toLocalStringWithPrecision(locale: "en_US"), "0.5"); + }); + + test("falls back to the ambient Intl locale when locale is null", () { + final previous = Intl.defaultLocale; + addTearDown(() => Intl.defaultLocale = previous); + Intl.defaultLocale = "de_DE"; + + // Proves the extension passes null through to NumberFormat rather than + // hardcoding a locale. + expect(_btcGrouping.toLocalStringWithPrecision(), "1.234,5"); + }); + }); + + group("toLocalStringWithSymbol", () { + test("suffixes the symbol by default", () { + expect(_btcGrouping.toLocalStringWithSymbol(locale: "en_US"), "1,234.5 BTC"); + }); + + test("prefixes the symbol when withSymbolPrefix is true", () { + expect( + _btcGrouping.toLocalStringWithSymbol(withSymbolPrefix: true, locale: "en_US"), + "BTC 1,234.5", + ); + }); + + test("suffixes the base-unit ticker", () { + expect( + _btc012.toLocalStringWithSymbol(useBaseUnit: true, locale: "en_US"), + "12,345,678 sats", + ); + }); + + test("prefixes the base-unit ticker", () { + expect( + _btc012.toLocalStringWithSymbol( + useBaseUnit: true, + withSymbolPrefix: true, + locale: "en_US", + ), + "sats 12,345,678", + ); + }); + + test("prefix respects de_DE separators", () { + expect( + _btcGrouping.toLocalStringWithSymbol(withSymbolPrefix: true, locale: "de_DE"), + "BTC 1.234,5", + ); + }); + + test("uses the currency symbol for non-BTC", () { + expect(_xmrHalf.toLocalStringWithSymbol(locale: "en_US"), "0.5 XMR"); + }); + + test("forwards fractionalDigits to the precision string", () { + expect(_btc012.toLocalStringWithSymbol(fractionalDigits: 2, locale: "en_US"), "0.12 BTC"); + }); + + test("forwards trimZeros to the precision string", () { + expect( + _btcTrailing.toLocalStringWithSymbol(trimZeros: false, locale: "en_US"), + "0.12000000 BTC", + ); + }); + }); + + group("negative amounts", () { + test("keeps the sign when the integer part is non-zero", () { + expect((-_btcGrouping).toLocalStringWithSymbol(locale: "en_US"), "-1,234.5 BTC"); + }); + + // NOTE: currently fails. For |amount| < 1 the sign is dropped: + // formatFixed yields "-0.5", but _withLocalSeparator re-parses the integer + // part and int.tryParse("-0") == 0, so the "-" is lost -> "0.5 XMR". + // Fix in _withLocalSeparator by capturing the sign before parsing, e.g. + // final negative = amount.startsWith("-"); + // ... then re-apply it to the formatted result. + test("keeps the sign for sub-1 amounts", () { + expect((-_xmrHalf).toLocalStringWithSymbol(locale: "en_US"), "-0.5 XMR"); + }); + }); +} diff --git a/cw_core/test/amount/money_test.dart b/cw_core/test/amount/money_test.dart index 45e43e106..8f045bdcb 100644 --- a/cw_core/test/amount/money_test.dart +++ b/cw_core/test/amount/money_test.dart @@ -278,10 +278,14 @@ void main() { }); test("handles exact division perfectly", () { - expect((Money(BigInt.from(100), CryptoCurrency.btc) / BigInt.from(20)).amount, - BigInt.from(5)); - expect((Money(BigInt.from(-100), CryptoCurrency.btc) / BigInt.from(20)).amount, - BigInt.from(-5)); + expect( + (Money(BigInt.from(100), CryptoCurrency.btc) / BigInt.from(20)).amount, + BigInt.from(5), + ); + expect( + (Money(BigInt.from(-100), CryptoCurrency.btc) / BigInt.from(20)).amount, + BigInt.from(-5), + ); }); }); }); @@ -346,6 +350,28 @@ void main() { expect(money.currency.decimals, equals(8)); expect(money.toStringWithSymbol(useBaseUnit: true), "100000000 sats"); }); + + group("withSymbolPrefix", () { + test("with fractionalDigits and padded with zeros", () { + final money = Money.parse("1", CryptoCurrency.btc); + expect(money.amount, equals(BigInt.parse("100000000"))); + expect(money.currency.decimals, equals(8)); + expect( + money.toStringWithSymbol(fractionalDigits: 5, trimZeros: false, withSymbolPrefix: true), + "BTC 1.00000", + ); + }); + + test("using base unit sats", () { + final money = Money.parse("1", CryptoCurrency.btc); + expect(money.amount, equals(BigInt.parse("100000000"))); + expect(money.currency.decimals, equals(8)); + expect( + money.toStringWithSymbol(useBaseUnit: true, withSymbolPrefix: true), + "sats 100000000", + ); + }); + }); }); }); } diff --git a/cw_custom_lints/lib/main.dart b/cw_custom_lints/lib/main.dart index 39e07ba30..e57e22525 100644 --- a/cw_custom_lints/lib/main.dart +++ b/cw_custom_lints/lib/main.dart @@ -1,6 +1,8 @@ import "package:analysis_server_plugin/plugin.dart"; import "package:analysis_server_plugin/registry.dart"; import "package:cw_custom_lints/http_force_proxy/http_force_proxy_rule.dart"; +import "package:cw_custom_lints/modal_top_bar_semantics/modal_top_bar_semantics_rule.dart"; +import "package:cw_custom_lints/modern_button_semantics/modern_button_semantics_rule.dart"; import "package:cw_custom_lints/print_verbose/print_verbose_fix.dart"; import "package:cw_custom_lints/print_verbose/print_verbose_rule.dart"; import "package:cw_custom_lints/restricted_imports/restricted_imports_rule.dart"; @@ -16,6 +18,8 @@ class CwCustomLintsPlugin extends Plugin { registry.registerWarningRule(PrintVerboseRule()); registry.registerWarningRule(RestrictedImportsRule()); registry.registerWarningRule(HttpForceProxyRule()); + registry.registerWarningRule(ModernButtonSemanticsRule()); + registry.registerWarningRule(ModalTopBarSemanticsRule()); registry.registerFixForRule(PrintVerboseRule.code, ReplaceWithPrintV.new); } diff --git a/cw_custom_lints/lib/modal_top_bar_semantics/modal_top_bar_semantics_rule.dart b/cw_custom_lints/lib/modal_top_bar_semantics/modal_top_bar_semantics_rule.dart new file mode 100644 index 000000000..69eccaffe --- /dev/null +++ b/cw_custom_lints/lib/modal_top_bar_semantics/modal_top_bar_semantics_rule.dart @@ -0,0 +1,69 @@ +import "package:analyzer/analysis_rule/analysis_rule.dart"; +import "package:analyzer/analysis_rule/rule_context.dart"; +import "package:analyzer/analysis_rule/rule_visitor_registry.dart"; +import "package:analyzer/dart/ast/ast.dart"; +import "package:analyzer/dart/ast/visitor.dart"; +import "package:analyzer/error/error.dart"; +import "package:cw_custom_lints/utils/widget_arguments.dart"; + +class ModalTopBarSemanticsRule extends AnalysisRule { + ModalTopBarSemanticsRule() + : super( + name: "require_modal_top_bar_semantics", + description: + "ModalTopBar icons need a semantic label, because the icon alone does not say whether it closes the modal, goes back or does something else.", + ); + + static const LintCode code = LintCode( + "require_modal_top_bar_semantics", + "ModalTopBar was given a {0} without a {1}, so screen readers have nothing to announce.", + correctionMessage: "Pass a localized {1} next to the {0}.", + severity: DiagnosticSeverity.WARNING, + ); + + static const _iconSemanticLabels = { + "leadingIcon": "leadingSemanticLabel", + "trailingIcon": "trailingSemanticLabel", + }; + + @override + LintCode get diagnosticCode => code; + + @override + void registerNodeProcessors(RuleVisitorRegistry registry, RuleContext context) => + registry.addInstanceCreationExpression(this, _Visitor(this)); +} + +class _Visitor extends SimpleAstVisitor { + _Visitor(this.rule); + + final AnalysisRule rule; + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + if (!isCakeWalletWidget(node.constructorName, "ModalTopBar")) { + return; + } + + for (final slot in ModalTopBarSemanticsRule._iconSemanticLabels.entries) { + _checkSlot(node.argumentList, iconName: slot.key, semanticLabelName: slot.value); + } + } + + void _checkSlot( + ArgumentList argumentList, { + required String iconName, + required String semanticLabelName, + }) { + final icon = namedArgument(argumentList, iconName); + if (icon == null || icon.argumentExpression is NullLiteral) { + return; + } + + if (!isMissingOrEmptyText(namedArgumentExpression(argumentList, semanticLabelName))) { + return; + } + + rule.reportAtToken(icon.name, arguments: [iconName, semanticLabelName]); + } +} diff --git a/cw_custom_lints/lib/modern_button_semantics/modern_button_semantics_rule.dart b/cw_custom_lints/lib/modern_button_semantics/modern_button_semantics_rule.dart new file mode 100644 index 000000000..7824fced4 --- /dev/null +++ b/cw_custom_lints/lib/modern_button_semantics/modern_button_semantics_rule.dart @@ -0,0 +1,56 @@ +import "package:analyzer/analysis_rule/analysis_rule.dart"; +import "package:analyzer/analysis_rule/rule_context.dart"; +import "package:analyzer/analysis_rule/rule_visitor_registry.dart"; +import "package:analyzer/dart/ast/ast.dart"; +import "package:analyzer/dart/ast/visitor.dart"; +import "package:analyzer/error/error.dart"; +import "package:cw_custom_lints/utils/widget_arguments.dart"; + +class ModernButtonSemanticsRule extends AnalysisRule { + ModernButtonSemanticsRule() + : super( + name: "require_modern_button_semantics", + description: + "ModernButton needs a semanticLabel when it has no visible label, otherwise screen readers announce an unnamed button.", + ); + + static const LintCode code = LintCode( + "require_modern_button_semantics", + "ModernButton has no visible label, so screen readers have nothing to announce.", + correctionMessage: "Pass a localized semanticLabel, or a non-empty label.", + severity: DiagnosticSeverity.WARNING, + ); + + @override + LintCode get diagnosticCode => code; + + @override + void registerNodeProcessors(RuleVisitorRegistry registry, RuleContext context) => + registry.addInstanceCreationExpression(this, _Visitor(this)); +} + +class _Visitor extends SimpleAstVisitor { + _Visitor(this.rule); + + final AnalysisRule rule; + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + final constructorName = node.constructorName; + if (!isCakeWalletWidget(constructorName, "ModernButton")) { + return; + } + + final argumentList = node.argumentList; + final hasLabel = !isMissingOrEmptyText(namedArgumentExpression(argumentList, "label")); + final hasSemanticLabel = !isMissingOrEmptyText( + namedArgumentExpression(argumentList, "semanticLabel"), + ); + + if (hasLabel || hasSemanticLabel) { + return; + } + + rule.reportAtNode(constructorName); + } +} diff --git a/cw_custom_lints/lib/no_bare_catch/no_bare_catch_rule.dart b/cw_custom_lints/lib/no_bare_catch/no_bare_catch_rule.dart new file mode 100644 index 000000000..611ca6bfe --- /dev/null +++ b/cw_custom_lints/lib/no_bare_catch/no_bare_catch_rule.dart @@ -0,0 +1,51 @@ +import "package:analyzer/analysis_rule/analysis_rule.dart"; +import "package:analyzer/analysis_rule/rule_context.dart"; +import "package:analyzer/analysis_rule/rule_visitor_registry.dart"; +import "package:analyzer/dart/ast/ast.dart"; +import "package:analyzer/dart/ast/visitor.dart"; +import "package:analyzer/error/error.dart"; + +class NoBareCatchRule extends AnalysisRule { + NoBareCatchRule() + : super( + name: "no_bare_catch", + description: "please specify the exception class to catch (on SomeException catch (e))", + ); + + static const LintCode code = LintCode( + "no_bare_catch", + "please specify the exception class to catch (on SomeException catch (e))", + severity: DiagnosticSeverity.WARNING, + ); + + @override + LintCode get diagnosticCode => code; + + @override + void registerNodeProcessors(RuleVisitorRegistry registry, RuleContext context) { + registry.addCatchClause(this, _Visitor(this)); + } +} + +class _Visitor extends SimpleAstVisitor { + _Visitor(this.rule); + + final AnalysisRule rule; + + @override + void visitCatchClause(CatchClause node) { + if (node.exceptionType != null) { + return; + } + + final tryStatement = node.parent; + if (tryStatement is TryStatement) { + final precedingClauses = tryStatement.catchClauses.takeWhile((clause) => clause != node); + if (precedingClauses.any((clause) => clause.exceptionType != null)) { + return; + } + } + + rule.reportAtNode(node); + } +} diff --git a/cw_custom_lints/lib/utils/widget_arguments.dart b/cw_custom_lints/lib/utils/widget_arguments.dart new file mode 100644 index 000000000..cd6bb22d3 --- /dev/null +++ b/cw_custom_lints/lib/utils/widget_arguments.dart @@ -0,0 +1,39 @@ +import "package:analyzer/dart/ast/ast.dart"; + +bool isCakeWalletWidget(ConstructorName constructorName, String className) { + final element = constructorName.type.element; + if (element == null || element.name != className) { + return false; + } + + final libraryUri = element.library?.uri; + if (libraryUri == null || libraryUri.scheme != "package") { + return false; + } + + final segments = libraryUri.pathSegments; + return segments.isNotEmpty && segments.first == "cake_wallet"; +} + +NamedArgument? namedArgument(ArgumentList argumentList, String name) { + for (final argument in argumentList.arguments) { + if (argument is NamedArgument && argument.name.lexeme == name) { + return argument; + } + } + + return null; +} + +Expression? namedArgumentExpression(ArgumentList argumentList, String name) => + namedArgument(argumentList, name)?.argumentExpression; + +// passed null or "" +// does NOT work if a runtime expression evaluates to an empty string or null +bool isMissingOrEmptyText(Expression? expression) { + if (expression == null || expression is NullLiteral) { + return true; + } + + return expression is StringLiteral && (expression.stringValue?.isEmpty ?? false); +} diff --git a/cw_custom_lints/pubspec.lock b/cw_custom_lints/pubspec.lock new file mode 100644 index 000000000..cd831e497 --- /dev/null +++ b/cw_custom_lints/pubspec.lock @@ -0,0 +1,429 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: cd6add6f846f35fb79f3c315296703c1a24f3cfd7f4739d91a74961c1c7e9f1b + url: "https://pub.dev" + source: hosted + version: "100.0.0" + analysis_server_plugin: + dependency: "direct main" + description: + name: analysis_server_plugin + sha256: c4021359e7a673ecd9c6db5f11fea12fabb55e0ba96fa08c78920ae81936f1d1 + url: "https://pub.dev" + source: hosted + version: "0.3.15" + analyzer: + dependency: "direct main" + description: + name: analyzer + sha256: "6ba98576948803398b69e3a444df24eacdbe12ed699c7014e120ea38552debbf" + url: "https://pub.dev" + source: hosted + version: "13.0.0" + analyzer_plugin: + dependency: "direct main" + description: + name: analyzer_plugin + sha256: "9a81dc36f7f71890d7cf83960a3a7df1e6c9a24035f98eb848e91ad48b1d21c6" + url: "https://pub.dev" + source: hosted + version: "0.14.9" + analyzer_testing: + dependency: "direct dev" + description: + name: analyzer_testing + sha256: da27ae9bd4bf3ce4ebab6295cf922ca21a2b14ea8fcacc859342aca796a48a2d + url: "https://pub.dev" + source: hosted + version: "0.2.6" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d" + url: "https://pub.dev" + source: hosted + version: "1.15.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "59d53ef8eaed9d288ed9767618e2b31c4fa0383a127db59d5eb2e737a7638a60" + url: "https://pub.dev" + source: hosted + version: "3.1.9" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" + url: "https://pub.dev" + source: hosted + version: "0.12.20" + meta: + dependency: transitive + description: + name: meta + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" + url: "https://pub.dev" + source: hosted + version: "1.19.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test: + dependency: "direct dev" + description: + name: test + sha256: "0d5ba5602ec3baa28c8ce365e1efc5575969c765f45c554a3e167dc7945b9c30" + url: "https://pub.dev" + source: hosted + version: "1.31.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "475610b2aa23c19687cce2961e44b0cc57cafe220f67c2b80201231b2a07fbe7" + url: "https://pub.dev" + source: hosted + version: "0.7.13" + test_core: + dependency: transitive + description: + name: test_core + sha256: a39c204a4fc7a7ccb04a2b985e359fda3cc37e45e0b8ac61c3fb1a05aa832132 + url: "https://pub.dev" + source: hosted + version: "0.6.19" + test_reflective_loader: + dependency: "direct dev" + description: + name: test_reflective_loader + sha256: d828d5ca15179aaac2aaf8f510cf0a52ec28e0031681b044ec5e581a4b8002e7 + url: "https://pub.dev" + source: hosted + version: "0.4.0" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + sha256: "07c9e63ba42519745182b88ca12264a7ba2484d8239958778dfe4d44fe760488" + url: "https://pub.dev" + source: hosted + version: "2.2.4" +sdks: + dart: ">=3.11.0 <4.0.0" diff --git a/cw_decred/pubspec.lock b/cw_decred/pubspec.lock index 0b460e9f9..9a568daa7 100644 --- a/cw_decred/pubspec.lock +++ b/cw_decred/pubspec.lock @@ -417,10 +417,10 @@ packages: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.0" mime: dependency: transitive description: @@ -769,10 +769,10 @@ packages: dependency: transitive description: name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.10" torch_dart: dependency: transitive description: diff --git a/cw_evm/lib/evm_chain_wallet_addresses.dart b/cw_evm/lib/evm_chain_wallet_addresses.dart index f7cef7513..fad6eb688 100644 --- a/cw_evm/lib/evm_chain_wallet_addresses.dart +++ b/cw_evm/lib/evm_chain_wallet_addresses.dart @@ -1,18 +1,15 @@ -import 'dart:developer'; +import "dart:developer"; -import 'package:cw_core/payment_uris.dart'; -import 'package:cw_core/wallet_addresses.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:mobx/mobx.dart'; +import "package:cw_core/payment_uris.dart"; +import "package:cw_core/wallet_addresses.dart"; +import "package:mobx/mobx.dart"; -part 'evm_chain_wallet_addresses.g.dart'; +part "evm_chain_wallet_addresses.g.dart"; class EVMChainWalletAddresses = EVMChainWalletAddressesBase with _$EVMChainWalletAddresses; abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store { - EVMChainWalletAddressesBase(WalletInfo walletInfo, this._selectedChainId) - : address = '', - super(walletInfo); + EVMChainWalletAddressesBase(super.walletInfo, this._selectedChainId) : address = ""; @override @observable @@ -33,7 +30,7 @@ abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store { Future updateAddressesInBox() async { try { addressesMap.clear(); - addressesMap[address] = ''; + addressesMap[address] = ""; await saveAddressesInBox(); } catch (e) { log(e.toString()); @@ -41,20 +38,10 @@ abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store { } @override - PaymentURI getPaymentUri(String amount) { - switch (_selectedChainId) { - case 1: - return EthereumURI(amount: amount, address: address); - case 137: - return PolygonURI(amount: amount, address: address); - case 8453: - return BaseURI(amount: amount, address: address); - case 42161: - return ArbitrumURI(amount: amount, address: address); - case 56: - return BSCURI(amount: amount, address: address); - default: - return EthereumURI(amount: amount, address: address); - } - } + PaymentURI getPaymentUri(String amount) => ERC681URI( + address: address, + amount: amount, + chainId: _selectedChainId, + contractAddress: null, + ); } diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index 80924bb0d..787933217 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -257,6 +257,10 @@ void setRecoveringFromSeed({required bool isRecovery}) { final storeMutex = Mutex(); +void refresh() { + currentWallet!.refresh(); +} + int lastStorePointer = 0; int lastStoreHeight = 0; void storeSync({bool force = false}) async { diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart index 2bdcf0bac..85bc77291 100644 --- a/cw_monero/lib/api/wallet_manager.dart +++ b/cw_monero/lib/api/wallet_manager.dart @@ -150,6 +150,11 @@ void restoreWalletFromSeedSync( _lastOpenedWallet = path; } +void storePassphrase({required String path, required String passphrase}) { + currentWallet!.setCacheAttribute(key: "cakewallet.passphrase", value: passphrase); + currentWallet!.store(path: path); +} + void restoreWalletFromKeys( {required String path, required String password, diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 16ab89ad6..afd9284be 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -370,6 +370,8 @@ abstract class MoneroWalletBase }; } + bool hasUnknownKeyImages() => currentWallet!.hasUnknownKeyImages(); + bool needExportOutputs(Money amount) { if (int.tryParse(currentWallet!.secretSpendKey()) != 0) { return false; @@ -382,22 +384,30 @@ abstract class MoneroWalletBase } MoneroTrezorService? trezorService; + + Future _getTrezor() async { + if (trezorService == null) throw Exception("Trezor not connected"); + + final trezor = Trezor(trezorService!); + await trezor.newPassphraseSession(passphrase); + return trezor; + } + Future syncTrezor() async { if (trezorService == null) throw Exception("Trezor not connected"); final ptr = Pointer.fromAddress(currentWallet!.ffiAddress()); final tdis = monero.Wallet_exportTrezorTdis(ptr); - - final response = await Trezor(trezorService!).keyImageSync(tdis); - + final trezor = await _getTrezor(); + final response = await trezor.keyImageSync(tdis); final success = monero.Wallet_importTrezorEncryptedKeyImagesJson(ptr, response); if (!success) throw Exception(monero.Wallet_errorString(ptr)); } Future signTrezorTransaction(String json) async { - if (trezorService == null) throw Exception("Trezor not connected"); - return Trezor(trezorService!).signTransaction(json); + final trezor = await _getTrezor(); + return trezor.signTransaction(json); } @override diff --git a/cw_monero/lib/monero_wallet_addresses.dart b/cw_monero/lib/monero_wallet_addresses.dart index ee211ef90..01f711bed 100644 --- a/cw_monero/lib/monero_wallet_addresses.dart +++ b/cw_monero/lib/monero_wallet_addresses.dart @@ -90,7 +90,7 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { addressesMap.clear(); addressInfos.clear(); - accountList.accounts.forEach((account) async { + for (final account in accountList.accounts) { await _subaddressList.update(accountIndex: account.id); _subaddressList.subaddresses.forEach((subaddress) { addressesMap[subaddress.address] = subaddress.label; @@ -102,7 +102,7 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { address: subaddress.address, label: subaddress.label)); }); - }); + } await saveAddressesInBox(); } catch (e) { diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index cb549a861..e774b47ba 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -44,10 +44,15 @@ class MoneroNewWalletCredentials extends WalletCredentials { } class MoneroRestoreWalletFromHardwareCredentials extends WalletCredentials { - MoneroRestoreWalletFromHardwareCredentials( - {required String name, required this.hardwareWalletService, int height = 0, String? password}) - : super(name: name, password: password, height: height); - HardwareWalletService hardwareWalletService; + MoneroRestoreWalletFromHardwareCredentials({ + required String name, + required this.hardwareWalletService, + int height = 0, + String? password, + String? passphrase, + }) : super(name: name, password: password, height: height, passphrase: passphrase); + + final HardwareWalletService hardwareWalletService; } class MoneroRestoreWalletFromSeedCredentials extends WalletCredentials { @@ -306,7 +311,13 @@ class MoneroWalletService extends WalletService< deviceName: 'Ledger', ); } else if (credentials.hardwareWalletService case MoneroTrezorService service) { - final watchCredentials = await Trezor(service).getWatchCredentials(); + final trezor = Trezor(service); + + if (credentials.passphrase != null) { + await trezor.newPassphraseSession(credentials.passphrase); + } + + final watchCredentials = await trezor.getWatchCredentials(); monero_wallet_manager.restoreWalletFromKeys( path: path, @@ -317,6 +328,10 @@ class MoneroWalletService extends WalletService< viewKey: watchCredentials.watchKey, spendKey: "", ); + + if (credentials.passphrase != null) { + monero_wallet_manager.storePassphrase(path: path, passphrase: credentials.passphrase!); + } } final wallet = MoneroWallet( diff --git a/cw_monero/lib/trezor.dart b/cw_monero/lib/trezor.dart index eff953d1b..b851859cb 100644 --- a/cw_monero/lib/trezor.dart +++ b/cw_monero/lib/trezor.dart @@ -1,25 +1,35 @@ -import 'dart:convert'; +import "dart:convert"; -import 'package:cw_core/hardware/hardware_wallet_service.dart'; -import 'package:trezor_flutter/trezor_flutter.dart'; +import "package:cw_core/hardware/hardware_wallet_service.dart"; +import "package:trezor_flutter/trezor_flutter.dart"; class MoneroTrezorService extends HardwareWalletService { - final TrezorClient client; - MoneroTrezorService(this.client); + + final TrezorClient client; } class MoneroTrezorWatchCredentials { + const MoneroTrezorWatchCredentials(this.watchKey, this.address); + final String watchKey; final String address; - - const MoneroTrezorWatchCredentials(this.watchKey, this.address); } class Trezor { + Trezor(this.service); + final MoneroTrezorService service; - Trezor(this.service); + String? _sessionPassphrase; + + Future newPassphraseSession(String? passphrase) async { + if (passphrase == null) return; + + if (_sessionPassphrase == passphrase) return; + _sessionPassphrase = passphrase; + return service.client.createChannel(passphrase: _sessionPassphrase); + } Future getWatchCredentials() async { final credentials = await TrezorMonero(service.client).getWatchCredentials(); @@ -33,21 +43,23 @@ class Trezor { final txIds = []; for (final tdi in tdisList) { - txIds.add(MoneroKeyImageTxData( - outKey: tdi["out_key"], - txPubKey: tdi["tx_pub_key"], - internalOutputIndex: tdi["internal_output_index"], - subAddrMajor: tdi["sub_addr_major"], - subAddrMinor: tdi["sub_addr_minor"], + txIds.add( + MoneroKeyImageTxData( + outKey: tdi["out_key"] as String, + txPubKey: tdi["tx_pub_key"] as String, + internalOutputIndex: tdi["internal_output_index"] as int, + subAddrMajor: tdi["sub_addr_major"] as int, + subAddrMinor: tdi["sub_addr_minor"] as int, additionalTxPubKeys: - (tdi["additional_tx_pub_keys"] as List?)?.map((e) => e as String).toList() ?? [])); + (tdi["additional_tx_pub_keys"] as List?)?.map((e) => e as String).toList() ?? [], + ), + ); } final keyImages = await TrezorMonero(service.client).syncKeyImages(txIds); return jsonEncode(keyImages.toMap()); } - Future signTransaction(String json) async { - return TrezorMonero(service.client).signTransaction(jsonDecode(json)); - } + Future signTransaction(String json) => + TrezorMonero(service.client).signTransaction(jsonDecode(json) as Map); } diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index b32f56c5b..0cc1a9e19 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -505,10 +505,10 @@ packages: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.0" mime: dependency: transitive description: @@ -906,10 +906,10 @@ packages: dependency: transitive description: name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.10" torch_dart: dependency: transitive description: @@ -921,8 +921,8 @@ packages: dependency: "direct main" description: path: trezor-flutter - ref: d1289dfa4247a93ee57342dfd249957b339915ef - resolved-ref: d1289dfa4247a93ee57342dfd249957b339915ef + ref: "7a736320f6d53ad5c763734ab832d6bea560a327" + resolved-ref: "7a736320f6d53ad5c763734ab832d6bea560a327" url: "https://github.com/cake-tech/trezor-flutter.git" source: git version: "0.1.0" @@ -930,8 +930,8 @@ packages: dependency: transitive description: path: trezor_usb_transport - ref: d1289dfa4247a93ee57342dfd249957b339915ef - resolved-ref: d1289dfa4247a93ee57342dfd249957b339915ef + ref: "7a736320f6d53ad5c763734ab832d6bea560a327" + resolved-ref: "7a736320f6d53ad5c763734ab832d6bea560a327" url: "https://github.com/cake-tech/trezor-flutter.git" source: git version: "0.0.1" @@ -1048,5 +1048,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.10.0-0 <4.0.0" + dart: ">=3.9.0 <4.0.0" flutter: ">=3.24.0" diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index c84f08b04..3f1c10f7b 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -33,7 +33,7 @@ dependencies: trezor_flutter: git: url: https://github.com/cake-tech/trezor-flutter.git - ref: d1289dfa4247a93ee57342dfd249957b339915ef + ref: 7d27e19b7bcdcd6241df35953f9a6ec2df0c7a97 path: trezor-flutter dev_dependencies: diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock index 2dd436985..509d74498 100644 --- a/cw_nano/pubspec.lock +++ b/cw_nano/pubspec.lock @@ -462,10 +462,10 @@ packages: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.0" mime: dependency: transitive description: @@ -887,10 +887,10 @@ packages: dependency: transitive description: name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.10" torch_dart: dependency: transitive description: diff --git a/cw_solana/lib/default_spl_tokens.dart b/cw_solana/lib/default_spl_tokens.dart index 324502fd9..c9dd6fac0 100644 --- a/cw_solana/lib/default_spl_tokens.dart +++ b/cw_solana/lib/default_spl_tokens.dart @@ -100,6 +100,15 @@ class DefaultSPLTokens { iconPath: 'assets/images/avdo_icon.png', enabled: false, ), + SPLToken( + name: "The Bitcoin Bull", + symbol: "TBB", + mintAddress: "42cXQvAAr7hcPBPWAS4ocVtDyeJ4Fa6gRR2uG4gppump", + decimal: 6, + mint: "tbb", + iconPath: "assets/images/tbb_icon.png", + enabled: false, + ), SPLToken( name: "Tether Gold", symbol: "XAUT0", diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart index aa79789dc..bed7b8a02 100644 --- a/cw_solana/lib/solana_client.dart +++ b/cw_solana/lib/solana_client.dart @@ -52,6 +52,8 @@ class SolanaWalletClient { SolanaRPC? _provider; bool _isStopped = false; + final Map _jupiterVerificationCache = {}; + bool connect(Node node) { try { _isStopped = false; @@ -1906,6 +1908,48 @@ class SolanaWalletClient { return []; } } + + Future isTokenVerifiedOnJupiter(String mintAddress) async { + if (_jupiterVerificationCache.containsKey(mintAddress)) { + return _jupiterVerificationCache[mintAddress]; + } + + try { + final uri = Uri.https( + "lite-api.jup.ag", + "/tokens/v2/search", + {"query": mintAddress}, + ); + + final response = await client.get( + uri, + headers: {"Accept": "application/json"}, + ); + + if (response.statusCode < 200 || response.statusCode >= 300) { + printV("Jupiter token API returned status: ${response.statusCode}"); + return null; + } + + final decodedResponse = jsonDecode(response.body) as List; + + for (final item in decodedResponse) { + final tokenData = item as Map; + + if (tokenData["id"] == mintAddress) { + final isVerified = tokenData["isVerified"] as bool? ?? false; + _jupiterVerificationCache[mintAddress] = isVerified; + return isVerified; + } + } + + // this means Jupiter doesn't index this mint at all, so we can't say either + return null; + } catch (e) { + printV("Error checking Jupiter verification: ${e.toString()}"); + return null; + } + } } class MoralisSolanaTokenBalance { diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index 2305f27fb..31a1bc3d2 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -902,6 +902,9 @@ abstract class SolanaWalletBase } } + Future isTokenVerifiedOnJupiter(String mintAddress) => + _client.isTokenVerifiedOnJupiter(mintAddress); + void _setTransactionUpdateTimer() { if (_transactionsUpdateTimer?.isActive ?? false) { _transactionsUpdateTimer!.cancel(); diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index 7fd7eac2d..4b622b350 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -417,10 +417,10 @@ packages: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.0" mime: dependency: transitive description: @@ -786,10 +786,10 @@ packages: dependency: transitive description: name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.10" torch_dart: dependency: transitive description: diff --git a/cw_zano/pubspec.lock b/cw_zano/pubspec.lock index a4a6c5381..c4602f532 100644 --- a/cw_zano/pubspec.lock +++ b/cw_zano/pubspec.lock @@ -422,10 +422,10 @@ packages: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.0" mime: dependency: transitive description: @@ -783,10 +783,10 @@ packages: dependency: transitive description: name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.10" torch_dart: dependency: transitive description: diff --git a/cw_zcash/lib/cw_zcash.dart b/cw_zcash/lib/cw_zcash.dart index c312ddb82..6df1d9b37 100644 --- a/cw_zcash/lib/cw_zcash.dart +++ b/cw_zcash/lib/cw_zcash.dart @@ -4,7 +4,6 @@ library; export 'src/zcash_balance.dart'; -export 'src/zcash_mnemonic_is_incorrect_exception.dart'; export 'src/zcash_transaction_info.dart'; export 'src/zcash_transactions.dart'; export 'src/zcash_wallet.dart'; @@ -12,4 +11,6 @@ export 'src/zcash_wallet_service.dart'; export 'src/models.dart'; export 'src/zcash_transaction_credentials.dart'; export 'src/pending_zcash_transaction.dart'; +export 'src/zcash_network.dart'; export 'src/zcash_receive_page_options.dart'; +export 'src/zcash_wallet_addresses.dart'; diff --git a/cw_zcash/lib/src/legacy/zkool_sweep.dart b/cw_zcash/lib/src/legacy/zkool_sweep.dart deleted file mode 100644 index 20f04daa3..000000000 --- a/cw_zcash/lib/src/legacy/zkool_sweep.dart +++ /dev/null @@ -1,196 +0,0 @@ -import 'dart:io'; - -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:path/path.dart' as p; -import 'package:zkool/src/rust/api/account.dart' as zkool_account; -import 'package:zkool/src/rust/api/coin.dart' as zkool_coin; -import 'package:zkool/src/rust/api/sync.dart' as zkool_sync; -import 'package:zkool/src/rust/api/pay.dart' as zkool_pay; -import 'package:zkool/src/rust/api/key.dart' as zkool_key; -import 'package:zkool/src/rust/api/network.dart' as zkool_network; -import 'package:zkool/src/rust/pay.dart' as zkool_paydart; -import 'package:zkool/src/rust/frb_generated.dart' as zkool_frb; - -class ZkoolSweep { - ZkoolSweep({ - required this.currentBalance, - required this.cacheDir, - required this.seed, - required this.passphrase, - required this.address, - required this.url, - required this.height, - }); - final int currentBalance; - final String seed; - final String cacheDir; - final String passphrase; - final String address; - final String url; - final int height; - static SweepUpdate msg = SweepUpdate(message: 'Idling', currentHeight: 0, networkHeight: 0); - Future start() async { - try { - await _init(); - if (!zkool_key.isValidAddress(address: address)) { - msg.message = "$address is not valid"; - return; - } - - msg = SweepUpdate(message: 'Starting import', currentHeight: 0, networkHeight: 0); - await _newAccount(); - msg.message = "Starting sync"; - await warpSync(); - msg.message = "Waiting"; - while (msg.blocksLeft != 0 && msg.networkHeight != 0) { - await Future.delayed(Duration(seconds: 1)); - } - await Future.delayed(Duration(seconds: 5)); - final balance = await getBalance(); - if (balance.toInt() == balance) { - msg.message = "No need to sweep, balance is the same"; - return; - } - msg.message = "Sweeping"; - await sweep(balance); - } catch (e) { - msg.message = e.toString(); - printV("zkool_sweep: $e"); - rethrow; - } - } - - Future getBalance() async { - final b = await zkool_sync.balance(c: c); - return b.field0.reduce((final a, final b) => a + b); - } - - Future sweep(final BigInt amount) async { - msg.message = "prepare"; - final tx = await zkool_pay.prepare( - recipients: [zkool_paydart.Recipient(address: address, amount: amount)], - options: zkool_pay.PaymentOptions( - srcPools: 7, - recipientPaysFee: true, - smartTransparent: false, - ), - c: c, - ); - msg.message = "getLatestHeight"; - final height = await getLatestHeight(); - msg.message = "signTransaction"; - final signTx = await zkool_pay.signTransaction(pczt: tx, c: c); - msg.message = "extractTransaction"; - final txBytes = await zkool_pay.extractTransaction(package: signTx); - msg.message = "broadcastTransaction"; - final result = await zkool_pay.broadcastTransaction(height: height, txBytes: txBytes, c: c); - msg.message = "result"; - if (result.isEmpty) { - msg.message = "Unknown error"; - throw Exception(result); - } - } - - static var c = zkool_coin.Coin(); - - static bool _isInit = false; - - File get dbFile => File(p.join(cacheDir, "zkool-tmp-mirror.db")); - - Future warpSync() async { - final currentHeight = await getLatestHeight(); - msg.networkHeight = currentHeight; - final sync = zkool_sync.synchronize( - accounts: [currentAccount], - currentHeight: currentHeight, - actionsPerSync: 10000, - transparentLimit: 100, - checkpointAge: 200, - c: c, - ); - bool success = false; - await sync.listen( - (final syncProgress) { - msg.currentHeight = syncProgress.height; - msg.message = "Sync ${syncProgress.height} / ${syncProgress.time}"; - }, - onError: (final e) { - msg.message = e.toString(); - }, - onDone: () { - msg.message = "Sync done!"; - success = true; - }, - ); - - if (msg.currentHeight != currentHeight && msg.currentHeight != 0) { - msg.message = "Canceling sync"; - await zkool_sync.cancelSync(); - await Future.delayed(Duration(seconds: 1)); - return warpSync(); - } - return 0; - } - - Future getLatestHeight() async { - try { - return await zkool_network.getCurrentHeight(c: c); - } catch (e) { - printV("getLatestHeight: $e"); - } - return 0; - } - - Future _newAccount() async { - final id = await zkool_account.newAccount( - na: zkool_account.NewAccount( - name: 'restoretmp' + DateTime.now().microsecondsSinceEpoch.toString(), - restore: true, - passphrase: "", - key: seed, - aindex: 0, - birth: height, - folder: '', - useInternal: true, - internal: false, - ledger: false, - ), - c: c, - ); - currentAccount = id; - c = await c.setAccount(account: id); - } - - int currentAccount = 0; - - Future _init() async { - if (_isInit) return; - _isInit = true; - printV(".init()"); - await zkool_frb.RustLib.init(); - await zkool_network.initDatadir(directory: dbFile.parent.path); - c = await c.openDatabase(dbFilepath: dbFile.path, password: 'cw_zcash_migration'); - c = c.setLwd(url: url, serverType: 0); - printV(".init(): Done"); - } -} - -class SweepUpdate { - SweepUpdate({required this.message, required this.currentHeight, required this.networkHeight}); - String message; - int currentHeight; - int networkHeight; - double get progress { - if (networkHeight == 0) return 0.01; - return currentHeight / networkHeight; - } - - int get blocksLeft => networkHeight - currentHeight; - - Map toJson() => { - "message": message, - "currentHeight": currentHeight, - "networkHeight": networkHeight, - "progress": progress, - }; -} diff --git a/cw_zcash/lib/src/migrate.dart b/cw_zcash/lib/src/migrate.dart new file mode 100644 index 000000000..492376dbb --- /dev/null +++ b/cw_zcash/lib/src/migrate.dart @@ -0,0 +1,96 @@ +import 'dart:math'; + +import 'package:cw_core/get_height_by_date_zec.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_zcash/cw_zcash.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'package:path/path.dart' as p; + +class ZkoolSeed { + ZkoolSeed({required this.seed, required this.passphrase}); + + factory ZkoolSeed.fromYwalletSeed({required final String ywalletSeed}) { + final splSeed = ywalletSeed.split(" "); + String seed = ""; + ; + String passphrase = ""; + if ([13, 25].contains(splSeed.length)) { + seed = splSeed.take(splSeed.length - 1).join(" "); + passphrase = ywalletSeed.replaceFirst("${seed} ", ""); + } else { + seed = ywalletSeed; + } + return ZkoolSeed(seed: seed, passphrase: passphrase); + } + + String seed; + String passphrase; +} + +class YwalletAccountInfo { + YwalletAccountInfo({ + required this.idAccount, + required this.name, + required this.seed, + required this.aindex, + required this.sk, + required this.ivk, + required this.address, + }); + final int idAccount; + final String name; + final String seed; + late final zkoolSeed = ZkoolSeed.fromYwalletSeed(ywalletSeed: seed); + final int aindex; + final String sk; + final String ivk; + final String address; + static YwalletAccountInfo fromJson(final Map j) => YwalletAccountInfo( + idAccount: j['id_account'], + name: j['name'], + seed: j['seed'], + aindex: j['aindex'], + sk: j['sk'], + ivk: j['ivk'], + address: j['address'], + ); +} + +Future migrateOldSqliteToZkool2({required final String walletName}) async { + final dbFile = p.join(await pathForWalletTypeDir(type: WalletType.zcash), "zec.db"); + final migrateDb = await openDatabase(dbFile); + final outputRaw = await migrateDb.rawQuery( + "SELECT `id_account`, `name`, `seed`, `aindex`, `sk`, `ivk`, `address` FROM `accounts`", + ); + final output = outputRaw.map(YwalletAccountInfo.fromJson); + + final walletId = await ZcashWalletBase.getLegacyZcashAccountIdForName(walletName); + for (final ya in output) { + if (ya.idAccount != walletId) { + continue; + } + printV("migrating account: ${ya.name} - ${walletName}"); + int birthHeight = await ZcashHeight.getBlockHeightByTime(DateTime(2026, 1, 1)); + + final accTxs = await migrateDb.rawQuery( + "SELECT height FROM transactions WHERE account = ${ya.idAccount}", + ); + for (final tx in accTxs) { + final txHeight = int.tryParse(tx['height'].toString()) ?? birthHeight; + birthHeight = min(birthHeight, txHeight); + } + + final accountId = await ZcashWalletBase.restoreZcashWalletFromSeed( + name: walletName, + seed: ya.zkoolSeed.seed, + passphrase: ya.zkoolSeed.passphrase, + birthHeight: birthHeight, + ); + await ZcashWalletBase.saveAccountId(walletName, accountId); + return; + } + + throw Exception("migration not finished (wallet name: $walletName not found)"); +} diff --git a/cw_zcash/lib/src/models.dart b/cw_zcash/lib/src/models.dart index e6886b189..d6bae3c1f 100644 --- a/cw_zcash/lib/src/models.dart +++ b/cw_zcash/lib/src/models.dart @@ -7,6 +7,7 @@ class ZcashNewWalletCredentials extends WalletCredentials { required final String? passphrase, final String? mnemonic, final int? seedPhraseLength, + this.network = 0, }) : super( name: name, password: password, @@ -17,6 +18,7 @@ class ZcashNewWalletCredentials extends WalletCredentials { } String? mnemonic; + int network; } class ZcashFromSeedWalletCredentials extends WalletCredentials { @@ -26,8 +28,10 @@ class ZcashFromSeedWalletCredentials extends WalletCredentials { required final String? passphrase, required this.seed, required super.height, + this.network = 0, }) : super(name: name, password: password, passphrase: passphrase); final String? seed; + int network; } class ZcashFromKeysWalletCredentials extends WalletCredentials { @@ -36,6 +40,8 @@ class ZcashFromKeysWalletCredentials extends WalletCredentials { final String? password, required final int? height, required this.privateKey, + this.network = 0, }) : super(name: name, password: password, height: height); final String? privateKey; + int network; } diff --git a/cw_zcash/lib/src/pending_zcash_transaction.dart b/cw_zcash/lib/src/pending_zcash_transaction.dart index db4f59f7e..3aba2404b 100644 --- a/cw_zcash/lib/src/pending_zcash_transaction.dart +++ b/cw_zcash/lib/src/pending_zcash_transaction.dart @@ -1,14 +1,10 @@ -import 'dart:convert'; -import 'dart:math'; - import 'package:cw_core/amount/money.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/output_info.dart'; +import 'package:cw_core/exceptions.dart'; import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_zcash/cw_zcash.dart'; -import 'package:cw_zcash/src/zcash_taddress_rotation.dart'; -import 'package:warp_api/data_fb_generated.dart'; -import 'package:warp_api/warp_api.dart'; +import 'package:zkool/src/rust/api/pay.dart' as zkool_pay; +import 'package:zkool/src/rust/api/network.dart' as zkool_network; class PendingZcashTransaction with PendingTransaction { PendingZcashTransaction({ @@ -21,7 +17,7 @@ class PendingZcashTransaction with PendingTransaction { final ZcashWallet zcashWallet; final ZcashTransactionCredentials credentials; - final String txPlan; + final zkool_pay.PcztPackage txPlan; String? _txId; final Money availableBalance; @@ -32,56 +28,43 @@ class PendingZcashTransaction with PendingTransaction { String get hex => ''; @override - Money get amount => Money.fromInt(_totalAmount, CryptoCurrency.zec); + Money get amount { + final isAll = credentials.outputs.fold(false, (final a, final b) => a || (b.sendAll)); + if (isAll) { + return availableBalance - fee; + } + return credentials.outputs + .map((final output) => output.cryptoAmount) + .reduce((final a, final b) => a + b); + } @override String get amountFormatted => amount.toString(); - int get _totalAmount { - final isAll = credentials.outputs.fold(false, (final a, final b) => a || (b.sendAll)); - if (isAll) { - return availableBalance.amount.toInt(); - } - return credentials.outputs.fold( - 0, - (final a, final b) => a + b.cryptoAmount.amount.toInt(), - ); - } - @override final Money fee; @override Future commit() async { - _txId = await ZcashWalletService.runInDbMutex( - () => WarpApi.signAndBroadcast(ZcashWalletBase.coin, zcashWallet.accountId, txPlan), - ); - ZcashWalletBase.temporarySentTx[zcashWallet.accountId] ??= []; - ZcashWalletBase.temporarySentTx[zcashWallet.accountId]?.add( - ShieldedTx( - base64.decode( - ZcashTaddressRotation.flatBuffersPack( - ShieldedTxT( - id: Random().nextInt(pow(2, 32).toInt()), - txId: _txId, - height: 0, - timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, - value: -_totalAmount, - ).pack, - ), - ), - ), - ); - await ZcashTransactionInfo.addCachedDestinationAddress( - _txId ?? '', - credentials.outputs.reduce((final o1, final o2) { - return OutputInfo( - cryptoAmount: Money.zero(CryptoCurrency.zec), - address: "${o1.address},${o2.address}", - sendAll: false, - isParsedAddress: false, + await ZcashWalletBase.runWithCoin( + accountId: zcashWallet.accountId, + func: (coin) async { + final signTx = await zkool_pay.signTransaction(pczt: txPlan, c: coin); + final txBytes = await zkool_pay.extractTransaction(package: signTx); + final currentHeight = await zkool_network.getCurrentHeight(c: coin); + final result = await zkool_pay.broadcastTransaction( + height: currentHeight, + txBytes: txBytes, + c: coin, ); - }).address, + printV("result: $result"); + final txId = ZcashWalletService.normalizeTxId(result); + if (txId.length != 64) { + throw TransactionCommitFailed(errorMessage: result); + } + _txId = txId; + zcashWallet.rememberPendingOutgoingAmount(txId, amount); + }, ); await zcashWallet.updateTransactions(); await zcashWallet.updateBalance(); diff --git a/cw_zcash/lib/src/util/hex.dart b/cw_zcash/lib/src/util/hex.dart new file mode 100644 index 000000000..1cd6a1253 --- /dev/null +++ b/cw_zcash/lib/src/util/hex.dart @@ -0,0 +1,5 @@ +import 'package:flutter/services.dart'; + +String uint8ListToHex(final Uint8List bytes) { + return bytes.map((final b) => b.toRadixString(16).padLeft(2, '0')).join(); +} diff --git a/cw_zcash/lib/src/zcash_mempool.dart b/cw_zcash/lib/src/zcash_mempool.dart new file mode 100644 index 000000000..97bdf87c5 --- /dev/null +++ b/cw_zcash/lib/src/zcash_mempool.dart @@ -0,0 +1,135 @@ +import 'dart:async'; + +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_zcash/src/zcash_wallet_service.dart'; +import 'package:zkool/src/rust/api/coin.dart' as zkool_coin; +import 'package:zkool/src/rust/api/mempool.dart' as zkool_mempool; + +typedef ZcashMempoolRefreshCallback = void Function(Set accountIds); + +class ZcashMempoolService { + ZcashMempoolService._(); + + static final ZcashMempoolService instance = ZcashMempoolService._(); + + zkool_mempool.Mempool? _mempool; + final Map _txsById = {}; + + zkool_mempool.Mempool get _mempoolInstance => _mempool ??= zkool_mempool.Mempool(); + + ZcashMempoolRefreshCallback? onAccountsUpdated; + + bool _loopRunning = false; + String? _nodeUrl; + int? _lastBlockHeight; + StreamSubscription? _subscription; + + void removeTx(final String txId) { + _txsById.remove(ZcashWalletService.normalizeTxId(txId)); + } + + void removeKnownTxs(final Iterable txIds) { + for (final txId in txIds) { + removeTx(txId); + } + } + + List txsForAccount(final int accountId) => _txsById.values + .where( + (final tx) => + tx.notes.any((final n) => n.account == accountId) || + tx.amounts.any((final a) => a.account == accountId), + ) + .toList(); + + Future ensureRunning(final zkool_coin.Coin c) async { + if (c.url.isEmpty) { + return; + } + if (_loopRunning && _nodeUrl == c.url) { + return; + } + await stop(); + _nodeUrl = c.url; + _loopRunning = true; + unawaited(_runLoop(c)); + } + + Future stop() async { + _loopRunning = false; + await _subscription?.cancel(); + _subscription = null; + _txsById.clear(); + _lastBlockHeight = null; + try { + await _mempool?.cancel(); + } catch (e) { + printV("mempool cancel: $e"); + } + _mempool = null; + } + + Future _runLoop(final zkool_coin.Coin c) async { + while (_loopRunning && _nodeUrl == c.url) { + try { + final completer = Completer(); + _subscription = _mempoolInstance + .run(c: c) + .listen( + (final msg) { + if (!_loopRunning) { + return; + } + switch (msg) { + case zkool_mempool.MempoolMsg_BlockHeight(:final field0): + _onBlockHeight(field0); + case zkool_mempool.MempoolMsg_TxId(:final field0): + _onMempoolTx(field0); + } + }, + onDone: () { + if (!completer.isCompleted) { + completer.complete(); + } + }, + onError: (final e) { + printV("mempool stream error: $e"); + if (!completer.isCompleted) { + completer.complete(); + } + }, + ); + await completer.future; + } catch (e) { + printV("mempool loop error: $e"); + } + if (_loopRunning) { + await Future.delayed(const Duration(seconds: 5)); + } + } + } + + void _onBlockHeight(final int height) { + final heightChanged = _lastBlockHeight != null && height > _lastBlockHeight!; + _lastBlockHeight = height; + if (!heightChanged) { + return; + } + final affected = _allAffectedAccounts(); + _txsById.clear(); + onAccountsUpdated?.call(affected); + } + + void _onMempoolTx(final zkool_mempool.MempoolTx tx) { + final normalized = ZcashWalletService.normalizeTxId(tx.txid); + _txsById[normalized] = tx; + onAccountsUpdated?.call(_affectedAccountsForTx(tx)); + } + + Set _allAffectedAccounts() => _txsById.values.expand(_affectedAccountsForTx).toSet(); + + Set _affectedAccountsForTx(final zkool_mempool.MempoolTx tx) => { + ...tx.notes.map((final n) => n.account), + ...tx.amounts.map((final a) => a.account), + }; +} diff --git a/cw_zcash/lib/src/zcash_mnemonic_is_incorrect_exception.dart b/cw_zcash/lib/src/zcash_mnemonic_is_incorrect_exception.dart deleted file mode 100644 index 4986837c5..000000000 --- a/cw_zcash/lib/src/zcash_mnemonic_is_incorrect_exception.dart +++ /dev/null @@ -1,5 +0,0 @@ -class ZcashMnemonicIsIncorrectException implements Exception { - @override - String toString() => - 'Zcash mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; -} diff --git a/cw_zcash/lib/src/zcash_network.dart b/cw_zcash/lib/src/zcash_network.dart new file mode 100644 index 000000000..d69254414 --- /dev/null +++ b/cw_zcash/lib/src/zcash_network.dart @@ -0,0 +1,49 @@ +enum ZcashNetwork { + mainnet, + testnet, + regtest; + + /// NU6.3 / Ironwood activation height on zkool regtest (see zkool2/misc/zebra.toml). + static const int regtestNu63Height = 250; + + String get value => name; + + String get label => switch (this) { + ZcashNetwork.mainnet => 'Mainnet', + ZcashNetwork.testnet => 'Testnet', + ZcashNetwork.regtest => 'Regtest', + }; + + String get dbFileName => switch (this) { + ZcashNetwork.mainnet => 'zec.v2.db', + ZcashNetwork.testnet => 'zec.testnet.v2.db', + ZcashNetwork.regtest => 'zec.regtest.v2.db', + }; + + /// Default lightwalletd endpoint for dev networks. + String get defaultNodeUri => switch (this) { + ZcashNetwork.mainnet => 'zec.rocks:443', + ZcashNetwork.testnet => 'testnet.lightwalletd.com:9067', + ZcashNetwork.regtest => '10.0.2.2:9067', // 10.0.2.2 - AVD host bridge + }; + + bool get useSsl => this == ZcashNetwork.mainnet; + + static ZcashNetwork fromName(final String? name) => switch (name) { + 'testnet' => ZcashNetwork.testnet, + 'regtest' => ZcashNetwork.regtest, + _ => ZcashNetwork.mainnet, + }; + + static ZcashNetwork fromIndex(final int index) => switch (index) { + 1 => ZcashNetwork.testnet, + 2 => ZcashNetwork.regtest, + _ => ZcashNetwork.mainnet, + }; + + int get networkIndex => switch (this) { + ZcashNetwork.testnet => 1, + ZcashNetwork.regtest => 2, + ZcashNetwork.mainnet => 0, + }; +} diff --git a/cw_zcash/lib/src/zcash_receive_page_options.dart b/cw_zcash/lib/src/zcash_receive_page_options.dart index 8a9d05766..31c03ca00 100644 --- a/cw_zcash/lib/src/zcash_receive_page_options.dart +++ b/cw_zcash/lib/src/zcash_receive_page_options.dart @@ -19,7 +19,7 @@ class ZcashReceivePageOption implements ReceivePageOption { case ZcashAddressType.shieldedSapling: return shieldedSapling; case ZcashAddressType.shieldedOrchard: - return shieldedOrchard; + return shieldedOrchard(); case ZcashAddressType.unifiedType: return unified; } @@ -57,13 +57,18 @@ class ZcashReceivePageOption implements ReceivePageOption { description: "Sapling", iconPath: "assets/new-ui/address-type-picker-icons/zec/sapling.svg", ); - static const shieldedOrchard = ZcashReceivePageOption._( - ZcashAddressType.shieldedOrchard, - "Shielded", - description: "Default (Orchard)", - iconPath: "assets/new-ui/address-type-picker-icons/zec/shielded.svg", - isCommon: true, - ); + static const _shieldedOrchardIcon = + "assets/new-ui/address-type-picker-icons/zec/shielded.svg"; + + static ZcashReceivePageOption shieldedOrchard({final bool ironwood = false}) { + return ZcashReceivePageOption._( + ZcashAddressType.shieldedOrchard, + "Shielded", + description: ironwood ? "Default (Ironwood)" : "Default (Orchard)", + iconPath: _shieldedOrchardIcon, + isCommon: true, + ); + } static const unified = ZcashReceivePageOption._( ZcashAddressType.unifiedType, "Unified", @@ -78,13 +83,15 @@ class ZcashReceivePageOption implements ReceivePageOption { return value; } - static const all = [ - ZcashReceivePageOption.shieldedOrchard, - ZcashReceivePageOption.shieldedSapling, - ZcashReceivePageOption.unified, - ZcashReceivePageOption.transparentRotated, - ZcashReceivePageOption.transparent, - ]; + static List allOptionsFor({final bool ironwood = false}) => [ + shieldedOrchard(ironwood: ironwood), + shieldedSapling, + unified, + transparentRotated, + transparent, + ]; + + static List get allOptions => allOptionsFor(); ZcashAddressType toType() { return type; diff --git a/cw_zcash/lib/src/zcash_taddress_rotation.dart b/cw_zcash/lib/src/zcash_taddress_rotation.dart index 1c2ef1f20..e0b344e8c 100644 --- a/cw_zcash/lib/src/zcash_taddress_rotation.dart +++ b/cw_zcash/lib/src/zcash_taddress_rotation.dart @@ -12,368 +12,509 @@ // turned off and replaced with proper solution. import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; +import 'dart:math' show max; -import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.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:cw_zcash/src/util/crc32.dart'; import 'package:cw_zcash/src/zcash_wallet.dart'; import 'package:cw_zcash/src/zcash_wallet_service.dart'; -import 'package:flutter/foundation.dart'; -import 'package:warp_api/data_fb_generated.dart'; -import 'package:warp_api/warp_api.dart'; -import 'package:flat_buffers/flat_buffers.dart' as fb; -import 'package:path/path.dart' as p; +import 'package:cw_zcash/src/zkool_compat.dart'; +import 'package:cw_zcash/src/zkooltx.dart'; +import 'package:zkool/src/rust/api/account.dart' as zkool_account; +import 'package:zkool/src/rust/api/coin.dart' as zkool_coin; +import 'package:zkool/src/rust/api/sync.dart' as zkool_sync; +import 'package:zkool/src/rust/api/pay.dart' as zkool_pay; +import 'package:zkool/src/rust/api/network.dart' as zkool_network; +import 'package:zkool/src/rust/api/sweep.dart' as zkool_sweep; +import 'package:zkool/src/rust/pay.dart' as zkool_paydart; class ZcashTaddressRotation { - ZcashTaddressRotation(this.accountIndex); - final int accountIndex; - static bool _isStarted = false; + static zkool_coin.Coin get c => ZcashWalletBase.c; + static const int _sweepThreshold = 30000; + static const int minSpendableNote = 5000; + static const int _lookahead = 5; + // Matches transparentLimit in ZcashWalletBase._oneshotSync. + static const int _transparentSyncLimit = 100; - static const coin = ZcashWalletBase.coin; - static String seedForOffset(final String seed) { - final seedWords = seed.split(" "); + static zkool_account.Seed seedForOffset(final zkool_account.Seed seed) { + final seedStr = "${seed.mnemonic} ${seed.phrase.replaceAll(" ", "_")}".trim(); + final seedWords = seedStr.split(" "); if ([12, 24].contains(seedWords.length)) { seedWords.add(""); } final lastI = seedWords.length - 1; - final crc = CRC32.compute(seed); - seedWords[lastI] += ":tgen:${crc}"; - return seedWords.join(" "); + final crc = CRC32.compute(seedStr); + seedWords[lastI] += ":tgen:$crc"; + final phrase = seedWords.removeLast(); + return zkool_account.Seed(mnemonic: seedWords.join(" "), phrase: phrase, aindex: 0); } - static bool isSeedForWallet(final String? mainWallet, final String? subWallet) { + static bool zkoolAccountSeedIsEqual(final zkool_account.Seed a, final zkool_account.Seed b) { + return a.mnemonic == b.mnemonic && a.phrase == b.phrase; + } + + static bool isSeedForWallet( + final zkool_account.Seed? mainWallet, + final zkool_account.Seed? subWallet, + ) { if (mainWallet == null || subWallet == null) return false; - return seedForOffset(mainWallet.trim()) == subWallet; + return zkoolAccountSeedIsEqual(seedForOffset(mainWallet), subWallet); + } + + static Map> shieldedAccountsTx = {}; + static final Map _rotationAccountByMain = {}; + static final Set _resyncedRotationAccounts = {}; + static final Set _poolRescanAttempted = {}; + static bool _sweepJobRunning = false; + static final Map _lastSweepBroadcastAt = {}; + + static bool _poolNeedsRescan(final List slots) { + if (slots.isEmpty) { + return false; + } + final unused = slots.where((final e) => e.txCount == 0); + if (unused.isEmpty) { + return false; + } + final head = unused.first; + final tail = slots.last.dindex; + if (slots.any((final e) => e.dindex > head.dindex && e.txCount > 0)) { + return true; + } + return head.dindex + _transparentSyncLimit <= tail; + } + + static Future _scanTransparentChain(final int rotationAccountId) async { + await ZcashWalletBase.runWithCoin( + accountId: rotationAccountId, + func: (final coin) async { + final all = await zkool_account.fetchAddressTxCount( + c: coin, + aggregate: false, + poolFilter: 1, + ); + final external = all.where((final e) => e.scope == 0).toList(); + if (external.isEmpty) { + return; + } + final maxD = external.map((final e) => e.dindex).reduce(max); + final scanner = await zkool_sweep.TransparentScanner.newInstance(); + final height = await zkool_network.getCurrentHeight(c: coin); + final completer = Completer(); + final sub = scanner + .run(endHeight: height, gapLimit: maxD + _lookahead + 1, c: coin) + .listen( + (_) {}, + onError: (final e) { + printV("transparent chain scan: $e"); + if (!completer.isCompleted) { + completer.complete(); + } + }, + onDone: () { + if (!completer.isCompleted) { + completer.complete(); + } + }, + ); + await Future.any([ + completer.future, + Future.delayed(Duration(seconds: (maxD + 2).clamp(8, 45))), + ]); + await sub.cancel(); + }, + ); + } + + static Future _resyncRotationPool( + final int rotationAccountId, + final int transparentLimit, + ) async { + await ZcashWalletBase.runWithCoin( + accountId: rotationAccountId, + func: (final coin) async { + await zkool_sync.cancelSync(); + await zkool_account.resetSync(id: rotationAccountId, c: coin); + final height = await zkool_network.getCurrentHeight(c: coin); + final sync = zkool_sync.synchronize( + accounts: [rotationAccountId], + currentHeight: height, + actionsPerSync: 10000, + transparentLimit: transparentLimit, + checkpointAge: 200, + c: coin, + fast: false, + ); + final completer = Completer(); + sync.listen( + (_) {}, + onError: (final e) { + printV("rotation pool resync error: $e"); + if (!completer.isCompleted) { + completer.complete(); + } + }, + onDone: () { + if (!completer.isCompleted) { + completer.complete(); + } + }, + ); + await completer.future; + }, + ); + } + + static Future _ensurePoolScanned(final int rotationAccountId) async { + var slots = await _transparentSlots(rotationAccountId); + final firstSession = _resyncedRotationAccounts.add(rotationAccountId); + if (!firstSession && !_poolNeedsRescan(slots)) { + return; + } + if (!firstSession) { + final head = slots.where((final e) => e.txCount == 0).firstOrNull?.dindex; + final key = '$rotationAccountId:$head'; + if (_poolRescanAttempted.contains(key)) { + return; + } + _poolRescanAttempted.add(key); + } + await _ensureLookahead(rotationAccountId); + await _scanTransparentChain(rotationAccountId); + slots = await _transparentSlots(rotationAccountId); + final limit = slots.isEmpty ? _transparentSyncLimit : slots.last.dindex + 1; + printV("rotation pool resync account $rotationAccountId limit $limit"); + await _resyncRotationPool(rotationAccountId, limit); } - static Map> rotationAccounts = {}; - static Map> rotationAccountsUsable = {}; - static Map> shieldedAccountsTx = {}; static Future init() async { - printV("Deserializing previous state"); if (_isStarted) { return; } _isStarted = true; - final pfw = await pathForWalletTypeDir(type: WalletType.zcash); - try { - final f = File(p.join(pfw, "zec-taddr.json")); - if (!f.existsSync()) { - f.writeAsBytesSync(serialize()); - } - deserialize(f.readAsBytesSync()); - } catch (e) { - printV("Failed to deserialize T address DB for zcash, it doesn't really matter: $e"); - } - unawaited( (() async { - await Future.delayed(Duration(seconds: 5)); + await Future.delayed(const Duration(seconds: 2)); return _jobRunner(); })(), ); } - static Future serializeToFile() async { - final pfw = await pathForWalletTypeDir(type: WalletType.zcash); - final f = File(p.join(pfw, "zec-taddr.json")); - f.writeAsBytesSync(serialize()); - } - - static Uint8List serialize() { - final data = { - "rotationAccounts": rotationAccounts.map( - (final k, final v) => MapEntry( - k.toRadixString(16), - v.map((final a) => flatBuffersPack(a.unpack().pack)).toList(), - ), - ), - if (kDebugMode) - "_rotationAccounts": rotationAccounts.map( - (final k, final v) => - MapEntry(k.toRadixString(16), v.map((final a) => a.toString()).toList()), - ), - "rotationAccountsUsable": rotationAccountsUsable.map( - (final k, final v) => MapEntry( - k.toRadixString(16), - v.map((final a) => flatBuffersPack(a.unpack().pack)).toList(), - ), - ), - if (kDebugMode) - "_rotationAccountsUsable": rotationAccountsUsable.map( - (final k, final v) => - MapEntry(k.toRadixString(16), v.map((final a) => a.toString()).toList()), - ), - "shieldedAccountsTx": shieldedAccountsTx.map( - (final k, final v) => MapEntry( - k.toRadixString(16), - v.map((final a) => flatBuffersPack(a.unpack().pack)).toList(), - ), - ), - if (kDebugMode) - "_shieldedAccountsTx": shieldedAccountsTx.map( - (final k, final v) => - MapEntry(k.toRadixString(16), v.map((final a) => a.toString()).toList()), - ), - }; - - if (kDebugMode) { - return utf8.encode(JsonEncoder.withIndent(" ").convert(data)); + static Future getRotationAccountForMainAccount(final int mainAccountId) async { + final cached = _rotationAccountByMain[mainAccountId]; + if (cached != null) { + return cached; } - return utf8.encode(jsonEncode(data)); - } - - static String flatBuffersPack(final int Function(fb.Builder fbBuilder) pack) { - final fbBuilder = fb.Builder(); - final offset = pack(fbBuilder); - fbBuilder.finish(offset); - return base64.encode(fbBuilder.buffer); - } - - static void deserialize(final Uint8List bytes) { - // todo: replace keys with sums and reverse in serialize - // final List seeds = []; - // for (int i = 0; i < accounts.length; i++) { - // final acc = accounts[i]; - // final backup = WarpApi.getBackup(coin, acc.id); - // await WarpApi.transparentSync(coin, acc.id, syncHeight); - // seeds.add(backup.seed!); - // } try { - final Map data = jsonDecode(utf8.decode(bytes)); - rotationAccounts = (data["rotationAccounts"] as Map).map( - (final k, final v) => MapEntry( - int.parse(k, radix: 16), - (v as List).map((final a) => Account(atob(a))).toList(), - ), + await ZcashWalletBase.runWithCoinMutex.acquire(); + final wSeed = await zkool_account.getAccountSeed(c: c, account: mainAccountId); + if (wSeed == null) { + printV("Not running Taddr rotation - seed not found"); + return null; + } + final seed = seedForOffset(wSeed); + final accs = await zkool_account.listAccounts(c: c); + for (int i = 0; i < accs.length; i++) { + final accSeed = await zkool_account.getAccountSeed(c: c, account: accs[i].id); + if (accSeed?.mnemonic == seed.mnemonic && + accSeed?.phrase == seed.phrase && + accSeed?.aindex == seed.aindex) { + _rotationAccountByMain[mainAccountId] = accs[i].id; + return accs[i].id; + } + } + + zkool_account.Account? acc; + for (final a in accs) { + if (a.id == mainAccountId) { + acc = a; + break; + } + } + if (acc == null) { + printV("Main account $mainAccountId not found for Taddr rotation"); + return null; + } + + final name = CRC32.compute(wSeed.mnemonic).toString(); + final id = await newAccount( + name: name, + seed: seed.mnemonic, + passphrase: seed.phrase, + height: acc.birth, + aindex: seed.aindex, ); - rotationAccountsUsable = (data["rotationAccountsUsable"] as Map).map( - (final k, final v) => MapEntry( - int.parse(k, radix: 16), - (v as List).map((final a) => Account(atob(a))).toList(), - ), - ); - shieldedAccountsTx = (data["shieldedAccountsTx"] as Map).map( - (final k, final v) => MapEntry( - int.parse(k, radix: 16), - (v as List).map((final a) => ShieldedTx(atob(a))).toList(), - ), - ); - } catch (e) { - rethrow; + _rotationAccountByMain[mainAccountId] = id; + return id; + } finally { + ZcashWalletBase.runWithCoinMutex.release(); } } - static Uint8List atob(final String value) => - Uint8List.fromList(List.from(base64.decode(value))); + // External (scope 0) transparent slots for the rotation account, ordered by + // dindex. zkool derives every slot in 0..=dindex, so unused ones (txCount==0) + // form the disposable look-ahead pool. + static Future> _transparentSlots( + final int rotationAccountId, + ) async { + final all = await ZcashWalletBase.runWithCoin( + accountId: rotationAccountId, + func: (final coin) => + zkool_account.fetchAddressTxCount(c: coin, aggregate: false, poolFilter: 1), + ); + return all.where((final e) => e.scope == 0).toList() + ..sort((final a, final b) => a.dindex.compareTo(b.dindex)); + } + + // Keep _lookahead unused disposable addresses ready, generating more whenever + // the pool runs low. Newly generated addresses are scanned by ongoing sync + // (the latest addresses are always in the scan window), so no resync needed. + static Future _ensureLookahead(final int rotationAccountId) async { + final slots = await _transparentSlots(rotationAccountId); + final unused = slots.where((final e) => e.txCount == 0).length; + final toGenerate = _lookahead - unused; + for (var i = 0; i < toGenerate; i++) { + await ZcashWalletBase.runWithCoin( + accountId: rotationAccountId, + func: (final coin) => zkool_account.generateNextDindex(c: coin), + ); + } + if (toGenerate > 0) { + printV("topped up disposable taddr pool by $toGenerate (had $unused)"); + } + } + + static bool _isWalletSynced(final int mainAccountId) { + final wallet = ZcashWalletBase.walletsByAccountId[mainAccountId]; + return wallet != null && wallet.syncStatus is SyncedSyncStatus; + } + + static Future _rotationTransparentBalance(final int rotationAccount) async { + return ZcashWalletBase.runWithCoin( + accountId: rotationAccount, + func: (final coin) async { + final b = await zkool_sync.balance(c: coin); + return b.field0.elementAt(0); + }, + ); + } + + static Future> _loadTxHistory(final int accountId) async { + return ZcashWalletBase.runWithCoin( + accountId: accountId, + func: (final coin) async { + final txsI = await zkool_account.listTxHistory(c: coin); + final txs = []; + for (int i = 0; i < txsI.length; i++) { + final tx = txsI[i]; + try { + final details = await zkool_account.getTxDetails(idTx: tx.id, c: coin); + txs.add(ZkoolTx(tx, details)); + } catch (e) { + printV("getTxDetails skipped for tx ${tx.id}: $e"); + } + } + return txs; + }, + ); + } + + static bool _canBroadcastSweep(final int mainAccountId) { + final lastSweep = _lastSweepBroadcastAt[mainAccountId]; + if (lastSweep == null) { + return true; + } + return !lastSweep.isAfter(DateTime.now().subtract(const Duration(seconds: 120))); + } static Future createAndSweepTAddresses() async { - int chainHeight = 0; - try { - chainHeight = await WarpApi.getLatestHeight(coin); - } catch (e) { - printV("Error getting latest height: $e"); - } - final dbHeight = WarpApi.getDbHeight(coin); - final height = dbHeight.unpack(); - final syncHeight = height.height; - if (chainHeight != syncHeight) { - printV( - "Waiting for sync to finish: chainHeight(${chainHeight}) != syncHeight(${syncHeight})", - ); + if (_sweepJobRunning) { return; } - final Map> newShieldedAccountsTx = {}; - final accounts = WarpApi.getAccountList(coin); - if (accounts.isEmpty) return; - final Map seeds = {}; - for (int i = 0; i < accounts.length; i++) { - final acc = accounts[i]; - final backup = WarpApi.getBackup(coin, acc.id); - await WarpApi.transparentSync(coin, acc.id, syncHeight); - if (backup.seed == null) continue; - seeds[acc.id] = backup.seed!; - } - for (int i = 0; i < accounts.length; i++) { - final seed = seeds[accounts[i].id]!; - if ([12, 13, 24, 25].contains(seed.split(" ").length)) { - if (seed.split(" ").last.contains(":tgen:")) continue; - rotationAccounts[accounts[i].id] = []; + _sweepJobRunning = true; + try { + final mainAccountIds = ZcashWalletBase.walletsByAccountId.keys.toList(); + if (mainAccountIds.isEmpty) { + await createAndSweepTAddressesForAccount(c.account); + return; } - } - final raKeys = rotationAccounts.keys.toList(); - seeds.removeWhere((_, final val) => raKeys.contains(val)); - printV("raKeys: ${raKeys.length}"); - for (int i = 0; i < raKeys.length; i++) { - final accs = accounts.where((final a) { - final seed = WarpApi.getBackup(coin, a.id).seed; - return isSeedForWallet(seeds[raKeys[i]], seed); - }).toList(); - rotationAccounts[raKeys[i]]!.addAll(accs); - final acc = accountForSeed(seeds[raKeys[i]]!)!; - final txs = WarpApi.getTxsSync(coin, acc.id); - newShieldedAccountsTx[acc.id] ??= []; - newShieldedAccountsTx[acc.id]!.addAll(txs); - } - rotationAccountsUsable = rotationAccounts.map((final k, final v) => MapEntry(k, v.toList())); - printV("rotationAccounts: ${rotationAccounts.length}"); - - for (int i = 0; i < raKeys.length; i++) { - final acc = accountForSeed(seeds[raKeys[i]]!)!; - newShieldedAccountsTx[acc.id] = []; - for (int j = 0; j < (rotationAccounts[raKeys[i]]?.length ?? 0); j++) { - final txs = WarpApi.getTxsSync(coin, rotationAccounts[raKeys[i]]![j].id); - rotationAccounts[raKeys[i]]![j]; - newShieldedAccountsTx[acc.id]!.addAll(txs); - } - rotationAccountsUsable[raKeys[i]]!.removeWhere((final a) { - final txs = WarpApi.getTxsSync(coin, a.id); - return txs.isNotEmpty; - }); - - // remove hidden addresses - final wis = await WalletInfo.selectList('type = ?', [WalletType.zcash.index]); - - final List hiddenAddresses_ = []; - for (int k = 0; k < wis.length; k++) { - final addrs = await wis[k].getHiddenAddresses(); - final addr2 = await wis[k].getUsedAddresses(); - hiddenAddresses_.addAll(addrs); - hiddenAddresses_.addAll(addr2); - } - final Set hiddenAddresses = hiddenAddresses_.toSet(); - rotationAccountsUsable[raKeys[i]]!.removeWhere((final a) { - return hiddenAddresses.contains(WarpApi.getTAddr(coin, a.id)); - }); - } - printV("rotationAccountsUsable: ${rotationAccountsUsable.length}"); - - bool didAddNewAccount = false; - for (int i = 0; i < raKeys.length; i++) { - if (rotationAccountsUsable[raKeys[i]]!.length < 5) { - final seed = seedForOffset(seeds[raKeys[i]]!); - final name = CRC32.compute(raKeys[i].toString()).toString(); - final id = await ZcashWalletService.runInDbMutex( - () => WarpApi.newAccount(coin, name, seed, rotationAccounts[raKeys[i]]!.length), - ); - printV("new id: $id / $seed"); - printV("${rotationAccounts[raKeys[i]]}"); - printV(raKeys[i]); - rotationAccounts[raKeys[i]]!.forEach((final a) { - final b = WarpApi.getBackup(coin, a.id); - printV("${a.id}"); - }); - didAddNewAccount = true; - } - } - printV("didAddNewAccount: ${didAddNewAccount}"); - if (didAddNewAccount) { - return createAndSweepTAddresses(); - } - - for (var i = 0; i < raKeys.length; i++) { - final accs = rotationAccounts[raKeys[i]]!; - inner: - for (var j = 0; j < accs.length; j++) { - int bal = 0; + for (final mainAccountId in mainAccountIds) { try { - bal += await WarpApi.getTBalance(coin, accs[j].id); - } catch (e) { - printV("getTBalance: $e"); - return; + await createAndSweepTAddressesForAccount(mainAccountId); + } catch (e, s) { + printV("createAndSweepTAddresses for $mainAccountId error: $e"); + s.toString().split("\n").forEach(printV); } - if (bal < 30000) continue inner; - final to = accountForSeed(seeds[raKeys[i]]!)!; - // final toAddress = WarpApi.getTAddr(coin, to.id); - // final fromSeed = WarpApi.getBackup(coin, to.id); - // final txId = await ZcashWalletService.runInDbMutex( - // () => WarpApi.sweepTransparentSeed( - // to.coin, - // to.id, - // chainHeight, - // fromSeed.seed!, - // 7, - // toAddress, - // 0, - // 1, - // FeeT(fee: 10000, minFee: 0, maxFee: 0, scheme: 0), - // ), - // ); - final recipientBuilder = RecipientObjectBuilder( - address: WarpApi.getAddress(coin, to.id, 4), - pools: 4, - feeIncluded: true, - amount: bal, - ); + } + } finally { + _sweepJobRunning = false; + } + } - final recipient = Recipient(recipientBuilder.toBytes()); - final fee = FeeT(fee: 10000, minFee: 0, maxFee: 0, scheme: 0); - await ZcashWalletBase.loadProver(); - final txPlan = await ZcashWalletService.runInDbMutex( - () => WarpApi.prepareTx( - coin, - accs[j].id, - [recipient], - 3, // pools: (Transparent + Sapling) - 1, - 0, // anchorOffset - fee, + static Future createAndSweepTAddressesForAccount(final int cId) async { + if (!_isWalletSynced(cId)) { + printV("rotation sweep waiting for wallet sync (account $cId)"); + return; + } + + printV("createAndSweepTAddresses for account $cId"); + + final rotationAccount = await getRotationAccountForMainAccount(cId); + if (rotationAccount == null) { + printV("rotation account is null, bailing out"); + return; + } + + await _ensureLookahead(rotationAccount); + await _ensurePoolScanned(rotationAccount); + await _refreshWalletAddresses(cId); + + BigInt transparentBal = BigInt.zero; + try { + transparentBal = await _rotationTransparentBalance(rotationAccount); + printV("rotation account $rotationAccount transparent balance: $transparentBal"); + } catch (e) { + printV("getTBalance: $e"); + return; + } + + if (transparentBal < BigInt.from(_sweepThreshold)) { + await updateCache(mainAccountId: cId); + await _refreshWalletAfterRotationChange(cId); + return; + } + + if (!_canBroadcastSweep(cId)) { + printV("rotation sweep rate-limited for account $cId"); + await updateCache(mainAccountId: cId); + await _refreshWalletAfterRotationChange(cId); + return; + } + + final toAddress = await ZcashWalletBase.runWithCoin( + accountId: cId, + func: (final coin) async { + final addrs = await zkool_account.getAddresses(c: coin, uaPools: 7); + final orchard = addrs.oaddr; + if (orchard == null || orchard.isEmpty) { + throw Exception('Orchard address unavailable for rotation sweep'); + } + return orchard; + }, + ); + final result = await ZcashWalletBase.runWithCoin( + accountId: rotationAccount, + func: (final coin) async { + final height = await zkool_network.getCurrentHeight(c: coin); + final notes = await zkool_account.listNotes(c: coin); + var amount = BigInt.zero; + for (final note in notes) { + if (note.pool != NotePool.transparent.index || note.locked) { + continue; + } + if (note.value < BigInt.from(minSpendableNote)) { + continue; + } + if (note.height > height) { + continue; + } + amount += note.value; + } + if (amount < BigInt.from(_sweepThreshold)) { + throw Exception('rotation sweep: insufficient spendable transparent notes'); + } + final ironwood = await zkool_network.isIronwoodActive(c: coin); + final tx = await zkool_pay.prepare( + recipients: [ + zkool_paydart.Recipient( + assetBase: zecBase, + address: toAddress, + amount: amount, + pools: ironwood ? ironwoodPoolMask : null, + ), + ], + options: zkool_pay.PaymentOptions( + srcPools: 1, + recipientPaysFee: true, + smartTransparent: false, + mode: 0, ), + c: coin, ); - final _txId = await ZcashWalletService.runInDbMutex( - () => WarpApi.signAndBroadcast(ZcashWalletBase.coin, accs[j].id, txPlan), - ); - await ZcashWalletService.addShieldedTx(_txId); - printV("shielded: $_txId"); + final signTx = await zkool_pay.signTransaction(pczt: tx, c: coin); + final txBytes = await zkool_pay.extractTransaction(package: signTx); + return zkool_pay.broadcastTransaction(height: height, txBytes: txBytes, c: coin); + }, + ); + printV("rotation sweep broadcast: $result"); + if (result.isEmpty) { + throw Exception("rotation sweep broadcast failed"); + } - await Future.delayed(Duration(seconds: 120)); // let it be - return createAndSweepTAddresses(); - } - } - final nsatKeys = newShieldedAccountsTx.keys.toList(); - for (int i = 0; i < nsatKeys.length; i++) { - inner: - for (int j = 0; j < newShieldedAccountsTx[nsatKeys[i]]!.length; j++) { - final tx = newShieldedAccountsTx[nsatKeys[i]]?[j]; - if (tx == null) { - continue inner; - } - final _txId = tx.txId; - if (_txId == null) { - continue inner; - } - if (tx.value < 0) { - await ZcashWalletService.addShieldedTx(_txId); - // continue inner; - } - if (ZcashWalletService.autoshieldTx.contains(_txId)) { - continue inner; - } - } - } - shieldedAccountsTx = newShieldedAccountsTx.map((final k, final v) => MapEntry(k, v.toList())); - await serializeToFile(); - return; + await ZcashWalletService.addShieldedTx(result); + _lastSweepBroadcastAt[cId] = DateTime.now(); + await updateCache(mainAccountId: cId); + await _refreshWalletAfterRotationChange(cId); } - static ShieldedTx reverseAmount(final ShieldedTx t) { - final tt = t.unpack(); - tt.value = -tt.value; - return ShieldedTx(base64.decode(flatBuffersPack(tt.pack))); + static Future _refreshWalletAfterRotationChange(final int mainAccountId) async { + final wallet = ZcashWalletBase.walletsByAccountId[mainAccountId]; + if (wallet == null) { + return; + } + unawaited(wallet.updateTransactions()); + unawaited(wallet.updateBalance()); } - static Account? accountForSeed(final String seed) { - final accounts = WarpApi.getAccountList(coin); + static Future updateCache({required final int mainAccountId}) async { + final rotationAccount = await getRotationAccountForMainAccount(mainAccountId); + if (rotationAccount == null) { + printV("rotationAccount is null"); + return; + } + await _ensureLookahead(rotationAccount); + await _ensurePoolScanned(rotationAccount); + final txs = await _loadTxHistory(rotationAccount); + shieldedAccountsTx[mainAccountId] = txs; + if (mainAccountId != rotationAccount && shieldedAccountsTx.containsKey(rotationAccount)) { + shieldedAccountsTx.remove(rotationAccount); + } + + for (final tx in shieldedAccountsTx[mainAccountId]!) { + if (tx.direction == TransactionDirection.outgoing) { + await ZcashWalletService.addShieldedTx(tx.txHash); + } + } + await _refreshWalletAddresses(mainAccountId); + await _refreshWalletAfterRotationChange(mainAccountId); + } + + static Future _refreshWalletAddresses(final int mainAccountId) async { + final wallet = ZcashWalletBase.walletsByAccountId[mainAccountId]; + if (wallet == null) { + return; + } + try { + await wallet.walletAddresses.refreshRotationAddresses(); + } catch (e) { + printV("rotation address refresh: $e"); + } + } + + static List rotationTxsForMainAccount(final int mainAccountId) { + return shieldedAccountsTx[mainAccountId] ?? const []; + } + + static Future accountForSeed(final zkool_account.Seed seed) async { + final accounts = await zkool_account.listAccounts(c: c); if (accounts.isEmpty) return null; for (int i = 0; i < accounts.length; i++) { final acc = accounts[i]; - final backup = WarpApi.getBackup(coin, acc.id); - if (backup.seed == seed) { + final backup = await zkool_account.getAccountSeed(c: c, account: acc.id); + if (backup?.mnemonic == seed.mnemonic && backup?.phrase == seed.phrase) { return acc; } } @@ -383,63 +524,76 @@ class ZcashTaddressRotation { static Future _jobRunner() async { for (;;) { try { - await Future.delayed(Duration(seconds: 5)); + await Future.delayed(const Duration(seconds: 5)); await createAndSweepTAddresses(); } catch (e, s) { - printV(e); + printV("createAndSweepTAddresses error: $e"); s.toString().split("\n").forEach(printV); - rethrow; } finally { - await Future.delayed(Duration(seconds: 30)); + await Future.delayed(const Duration(seconds: 30)); } } } - static String? addressForAccount(final int accountId, final Set? hiddenAddresses) { - final accs = rotationAccountsUsable[accountId] ?? []; - for (int i = 0; i < accs.length; i++) { - final addr = WarpApi.getTAddr(coin, accs[i].id); - if (hiddenAddresses?.contains(addr) == true) { - continue; - } - return addr; - } - return null; - } - - static List? allAddressesForAccount(final int accountId) { - final acc = rotationAccounts[accountId]; - if (acc == null) { - printV("Nothing found"); + // Current disposable address = first unused slot (lowest dindex). + static Future addressForAccount(final int accountId) async { + final rotationAccount = await getRotationAccountForMainAccount(accountId); + if (rotationAccount == null) { return null; } - for (int i = 0; i < acc.length; i++) { - final b = WarpApi.getBackup(coin, acc[i].id); - printV("$i. ${b.seed?.split(" ").last}, ${b.index}, ${WarpApi.getTAddr(coin, acc[i].id)}"); + final slots = await _transparentSlots(rotationAccount); + final unused = slots.where((final e) => e.txCount == 0); + if (unused.isNotEmpty) { + return unused.first.address; } - return acc.map((final a) => WarpApi.getTAddr(coin, a.id)).toList(); + return slots.isNotEmpty ? slots.last.address : null; } - static List? allUsedAddressesForAccount(final int accountId) { - final seed = WarpApi.getBackup(coin, accountId).seed; - if (seed == null) return []; - final acc = rotationAccounts[seed]?.toList(); - if (acc == null) { - printV("Nothing found"); - return null; + static Future> usedAddressesForAccount(final int accountId) async { + final rotationAccount = await getRotationAccountForMainAccount(accountId); + if (rotationAccount == null) { + return const []; } - acc.removeWhere((final a1) { - for (int i = 0; i < (rotationAccountsUsable[seed]?.length ?? 0); i++) { - if (rotationAccountsUsable[seed]?[i].id == a1.id) { - return true; - } - } - return false; - }); - for (int i = 0; i < acc.length; i++) { - final b = WarpApi.getBackup(coin, acc[i].id); - printV("$i. ${b.seed?.split(" ").last}, ${b.index}, ${WarpApi.getTAddr(coin, acc[i].id)}"); + final slots = await _transparentSlots(rotationAccount); + return [ + for (final entry in slots) + if (entry.txCount > 0) entry.address, + ]; + } + + // The full disposable pool: used slots plus the unused look-ahead window. + static Future> allAddressesForAccount(final int accountId) async { + final rotationAccount = await getRotationAccountForMainAccount(accountId); + if (rotationAccount == null) { + return const []; } - return acc.map((final a) => WarpApi.getTAddr(coin, a.id)).toList(); + final slots = await _transparentSlots(rotationAccount); + return slots.map((final e) => e.address).toList(); + } + + static Future newAccount({ + required final String name, + required final String seed, + required final String passphrase, + required final int height, + required final int aindex, + }) async { + final id = await zkool_account.newAccount( + na: zkool_account.NewAccount( + name: name, + restore: true, + passphrase: passphrase, + key: seed, + aindex: aindex, + birth: height, + folder: '', + pools: 1, + useInternal: true, + internal: false, + ledger: false, + ), + c: c, + ); + return id; } } diff --git a/cw_zcash/lib/src/zcash_transaction_info.dart b/cw_zcash/lib/src/zcash_transaction_info.dart index d307cafa7..46494111b 100644 --- a/cw_zcash/lib/src/zcash_transaction_info.dart +++ b/cw_zcash/lib/src/zcash_transaction_info.dart @@ -1,15 +1,9 @@ -import 'dart:convert'; -import 'dart:io'; - import 'package:cw_core/amount/money.dart'; -import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/format_amount.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:cw_zcash/cw_zcash.dart'; -import 'package:path/path.dart' as p; +import 'package:cw_zcash/src/zkooltx.dart'; class ZcashTransactionInfo extends TransactionInfo { ZcashTransactionInfo({ @@ -23,6 +17,10 @@ class ZcashTransactionInfo extends TransactionInfo { required final int confirmations, required final String to, final String? memo, + final TxType? txType, + final bool isRotationReceive = false, + final bool isShieldAction = false, + final bool isIronwoodMigration = false, }) { this.id = id; this.amount = amount; @@ -32,23 +30,22 @@ class ZcashTransactionInfo extends TransactionInfo { this.date = date; this.isPending = isPending; this.confirmations = confirmations; - this.to = getCachedDestinationAddress(id); + this.to = to; if (memo != null && memo.isNotEmpty) { additionalInfo['memo'] = memo; } - additionalInfo['autoShield'] = false; - // note: this won't work yet, fee is not in zcash_lib metadata, - // leaving here so it can start working automagically in future - if (amount == fee && to.startsWith("u")) { - additionalInfo['autoShield'] = true; + if (txType != null) { + additionalInfo['txType'] = txType.name; } - // remove below when above starts working - additionalInfo['autoShield'] = ZcashWalletService.autoshieldTx.contains(txHash); - // --- == === cut here === == --- - if (additionalInfo['autoShield'] == true) { + additionalInfo['isRotationReceive'] = isRotationReceive; + additionalInfo['isAutoShield'] = isShieldAction || ZcashWalletService.isAutoshieldTx(txHash); + additionalInfo['isIronwoodMigration'] = isIronwoodMigration; + + if (additionalInfo['isAutoShield'] == true) { additionalInfo['memo'] ??= ''; - additionalInfo['memo'] += '\This is an auto-shielding transaction. Enjoy default privacy!'; - additionalInfo['memo'] = additionalInfo['memo'].trim(); + additionalInfo['memo'] = + '${additionalInfo['memo']}\nThis is an auto-shielding transaction. Enjoy default privacy!.' + .trim(); } } @@ -61,35 +58,4 @@ class ZcashTransactionInfo extends TransactionInfo { void changeFiatAmount(final String amount) => _fiatAmount = formatAmount(amount); String? get memo => additionalInfo['memo'] as String?; - - static final Map _destinationAddressMap = {}; - - static String? getCachedDestinationAddress(final String txId) { - return _destinationAddressMap[txId] ?? - _destinationAddressMap['"$txId"'] ?? - _destinationAddressMap[txId.replaceAll('"', '')]; - } - - static Future addCachedDestinationAddress(final String txId, final String address) async { - _destinationAddressMap[txId] = address; - final pfwt = await pathForWalletTypeDir(type: WalletType.zcash); - final f = File(p.join(pfwt, "sent-tx-map.json")); - f.writeAsStringSync(json.encode(_destinationAddressMap)); - } - - static Future init() async { - try { - final pfwt = await pathForWalletTypeDir(type: WalletType.zcash); - final f = File(p.join(pfwt, "sent-tx-map.json")); - if (!f.existsSync()) { - f.writeAsStringSync('{}'); - } - final tmpMap = json.decode(f.readAsStringSync()); - tmpMap.forEach((final k, final v) { - _destinationAddressMap[k.toString()] = v.toString(); - }); - } catch (e, s) { - printV("failed to deserialize: $e, $s"); - } - } } diff --git a/cw_zcash/lib/src/zcash_wallet.dart b/cw_zcash/lib/src/zcash_wallet.dart index d255ae7fb..98d4a92b1 100644 --- a/cw_zcash/lib/src/zcash_wallet.dart +++ b/cw_zcash/lib/src/zcash_wallet.dart @@ -1,9 +1,10 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; +import 'dart:math'; import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/get_height_by_date_zec.dart'; import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/pathForWallet.dart'; @@ -12,26 +13,30 @@ import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; -import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:bip39/bip39.dart' as bip39; import 'package:cw_zcash/cw_zcash.dart'; -import 'package:cw_zcash/src/legacy/zkool_sweep.dart'; import 'package:cw_zcash/src/util/crc32.dart'; +import 'package:cw_zcash/src/zcash_mempool.dart'; import 'package:cw_zcash/src/zcash_taddress_rotation.dart'; import 'package:cw_zcash/src/zcash_wallet_addresses.dart'; -import 'package:flutter/foundation.dart'; +import 'package:cw_zcash/src/zcash_network.dart'; +import 'package:cw_zcash/src/zkool_compat.dart'; +import 'package:cw_zcash/src/zkooltx.dart'; import 'package:mobx/mobx.dart'; -import 'package:warp_api/warp_api.dart'; -import 'package:warp_api/data_fb_generated.dart'; -import 'package:flutter/services.dart'; import 'package:mutex/mutex.dart'; -import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart'; +import 'package:zkool/src/rust/api/account.dart' as zkool_account; +import 'package:zkool/src/rust/api/coin.dart' as zkool_coin; +import 'package:zkool/src/rust/api/mempool.dart' as zkool_mempool; +import 'package:zkool/src/rust/api/sync.dart' as zkool_sync; +import 'package:zkool/src/rust/api/pay.dart' as zkool_pay; +import 'package:zkool/src/rust/api/migrate.dart' as zkool_migrate; +import 'package:zkool/src/rust/api/network.dart' as zkool_network; +import 'package:zkool/src/rust/pay.dart' as zkool_paydart; +import 'package:zkool/src/rust/frb_generated.dart' as zkool_frb; part 'zcash_wallet.g.dart'; @@ -42,24 +47,66 @@ abstract class ZcashWalletBase with Store { ZcashWalletBase(super.walletInfo, super.derivationInfo, {required this.accountId}) { transactionHistory = ZcashTransactionHistory(); + walletsByAccountId[accountId] = this; + } + + static final Map walletsByAccountId = {}; + + static Future refreshWalletForAccount(final int accountId) async { + final wallet = walletsByAccountId[accountId]; + if (wallet == null) { + return; + } + await wallet.updateTransactions(); + await wallet.updateBalance(); + } + + int accountId; + + final Map _pendingOutgoingAmounts = {}; + + void rememberPendingOutgoingAmount(final String txId, final Money amount) { + if (amount.isZero) { + return; + } + _pendingOutgoingAmounts[ZcashWalletService.normalizeTxId(txId)] = amount.amount; } - final int accountId; - static const int coin = 0; // Zcash mainnet coin ID in warp_api @override @observable SyncStatus syncStatus = NotConnectedSyncStatus(); - Timer? _syncStatusTimer; - Timer? _periodicSyncTimer; - int _initialSyncHeight = 0; - int _lastKnownBlockHeight = 0; - @override ObservableMap balance = ObservableMap.of({ CryptoCurrency.zec: ZcashBalance.zero(), }); + static const int _autoShieldMinSweep = 30000; + + // zkool's migrate::MIN_SD. + static const int _ironwoodMigrateMinNote = 500000; + + static int _minSweepThreshold({required final bool ironwood}) => + ironwood ? _ironwoodMigrateMinNote : _autoShieldMinSweep; + + Money _feeFromTxPlan( + final zkool_pay.PcztPackage txPlan, + final TransactionPriority priority, + final int tryReduceFeeAmount, { + final zkool_coin.Coin? coin, + }) { + try { + return Money(zkool_pay.toPlan(package: txPlan, c: coin ?? c).fee, currency); + } catch (_) { + return Money.fromInt( + tryReduceFeeAmount != 0 + ? tryReduceFeeAmount + : internalCalculateEstimatedFee(priority, null), + currency, + ); + } + } + static int internalCalculateEstimatedFee(final TransactionPriority priority, final int? amount) { const baseFee = 10000; switch (priority) { @@ -96,9 +143,8 @@ abstract class ZcashWalletBase @override Future close({final bool shouldCleanup = false}) async { - _stopSyncStatusUpdates(); - _stopPeriodicSync(); - await ZcashWalletService.runInDbMutex(() async => WarpApi.cancelSync()); + _syncLoopRunning = false; + walletsByAccountId.remove(accountId); } Node? lastNode; @@ -115,14 +161,13 @@ abstract class ZcashWalletBase lwdUrl = '$protocol$lwdUrl'; } printV("Setting LWD URL to: $lwdUrl"); - WarpApi.updateLWD(coin, lwdUrl); + c = c.setLwd(url: lwdUrl, serverType: 0); syncStatus = ConnectedSyncStatus(); - try { - await updateBalance(); - await updateTransactions(); - } catch (e) { - printV("Error updating balance/transactions after connect: $e"); - } + unawaited(ZcashMempoolService.instance.ensureRunning(c)); + unawaited(_updateIronwoodActive()); + _ensureSyncLoopRunning(); + unawaited(_refreshSyncStatus()); + unawaited(_oneshotSync()); } catch (e) { printV("Connection error: $e"); syncStatus = FailedSyncStatus(error: e.toString()); @@ -130,343 +175,964 @@ abstract class ZcashWalletBase } } + bool _syncLoopRunning = false; + + void _ensureSyncLoopRunning() { + if (_syncLoopRunning) { + return; + } + _syncLoopRunning = true; + unawaited(_runSyncLoop()); + } + + static const _syncedPollInterval = Duration(seconds: 5); + static const _activePollInterval = Duration(seconds: 1); + + Future _runSyncLoop() async { + var pollInterval = _activePollInterval; + while (_syncLoopRunning) { + await Future.delayed(pollInterval); + try { + final alreadySynced = await _oneshotSync(); + pollInterval = alreadySynced ? _syncedPollInterval : _activePollInterval; + } catch (e) { + printV("zcash sync failed: $e"); + pollInterval = _activePollInterval; + } + } + } + + int _syncCheckpointHeight = 0; + + bool get isSyncing { + return _isSyncing; + } + + set isSyncing(final bool value) { + _isSyncing = value; + } + + bool _isSyncing = false; + + static int oneshotSyncCount = 0; + + Future _getLowestSyncHeight() async { + final accounts = await zkool_account.listAccounts(c: c); + var lowest = await _getWalletDbHeight(); + for (final acc in accounts) { + if (acc.id == accountId) continue; + c = await c.setAccount(account: acc.id); + lowest = min(lowest, await _getWalletDbHeight()); + } + c = await c.setAccount(account: accountId); + return lowest; + } + + Future _anyAccountNeedsSync(final int currentHeight) => + withSharedCoinLock(() => _anyAccountNeedsSyncUnlocked(currentHeight)); + + Future _anyAccountNeedsSyncUnlocked(final int currentHeight) async { + final accounts = await zkool_account.listAccounts(c: c); + for (final acc in accounts) { + c = await c.setAccount(account: acc.id); + if (currentHeight > await _getWalletDbHeight()) { + c = await c.setAccount(account: accountId); + return true; + } + } + c = await c.setAccount(account: accountId); + return false; + } + + @action + void _applySyncProgress(final int currentHeight, final int walletHeight) { + final blocksLeft = (currentHeight - walletHeight).clamp(0, currentHeight); + _syncCheckpointHeight = walletHeight; + if (blocksLeft <= 0) { + syncStatus = _isSyncing ? SyncingSyncStatus(1, 0.999) : SyncedSyncStatus(); + return; + } + final ptc = currentHeight > 0 ? (walletHeight / currentHeight).clamp(0.0, 1.0) : 0.0; + syncStatus = SyncingSyncStatus(blocksLeft, ptc); + } + + void _broadcastSyncProgress(final int currentHeight, final int walletHeight) { + runInAction(() { + for (final wallet in walletsByAccountId.values) { + wallet._applySyncProgress(currentHeight, walletHeight); + } + }); + } + + Future _getWalletDbHeight() async { + try { + return (await zkool_sync.getDbHeight(c: c)).height; + } catch (_) { + final accounts = await zkool_account.listAccounts(c: c); + final account = accounts.where((final a) => a.id == accountId).firstOrNull; + if (account != null) { + return account.height; + } + rethrow; + } + } + + void _onSyncCheckpoint(final int currentHeight, final int checkpointHeight) { + final height = checkpointHeight > _syncCheckpointHeight + ? checkpointHeight + : _syncCheckpointHeight; + _broadcastSyncProgress(currentHeight, height); + } + + @action + Future _refreshSyncStatus() async { + try { + await withSharedCoinLock(() async { + c = await c.setAccount(account: accountId); + final currentHeight = await zkool_network.getCurrentHeight(c: c); + final walletDbHeight = await _getWalletDbHeight(); + _broadcastSyncProgress(currentHeight, walletDbHeight); + }); + } catch (e) { + printV("refresh sync status: $e"); + } + } + + @action + Future _oneshotSync() async { + try { + if (isSyncing) { + return syncStatus is SyncedSyncStatus; + } + isSyncing = true; + late final int currentHeight; + late final int walletDbHeight; + await withSharedCoinLock(() async { + c = await c.setAccount(account: accountId); + currentHeight = await zkool_network.getCurrentHeight(c: c); + walletDbHeight = await _getWalletDbHeight(); + }); + if (!await _anyAccountNeedsSync(currentHeight)) { + _syncCheckpointHeight = walletDbHeight; + if (syncStatus is! SyncedSyncStatus) { + syncStatus = SyncedSyncStatus(); + } + isSyncing = false; + return true; + } + await zkool_sync.cancelSync(); + late final List accountList; + late final int lagHeight; + await withSharedCoinLock(() async { + final accounts = await zkool_account.listAccounts(c: c); + accountList = accounts.map((final a) => a.id).toList() + ..removeWhere((final a) => a == c.account); + c = await c.setAccount(account: accountId); + lagHeight = await _getLowestSyncHeight(); + }); + _broadcastSyncProgress(currentHeight, lagHeight); + final sync = zkool_sync.synchronize( + accounts: [c.account, ...accountList], + currentHeight: currentHeight, + actionsPerSync: 10000, + transparentLimit: 100, + checkpointAge: 200, + c: c, + fast: false, + ); + await withSharedCoinLock(() async { + c = await c.setAccount(account: accountId); + }); + final randInt = CRC32.compute("${DateTime.now().microsecondsSinceEpoch}").toRadixString(16); + oneshotSyncCount++; + final completer = Completer(); + var lastLoggedHeight = walletDbHeight; + var chainTip = currentHeight; + late final StreamSubscription subscription; + subscription = sync.listen( + (final syncProgress) { + if (syncProgress.height > chainTip) { + chainTip = syncProgress.height; + } + if (syncProgress.height >= lastLoggedHeight + 5000) { + lastLoggedHeight = syncProgress.height; + printV( + "[${c.account} ($accountList)] [$oneshotSyncCount/$randInt] sync: ${syncProgress.height}", + ); + unawaited( + zkool_network.getCurrentHeight(c: c).then((final tip) { + if (tip > chainTip) { + chainTip = tip; + _onSyncCheckpoint(chainTip, syncProgress.height); + } + }), + ); + } + _onSyncCheckpoint(chainTip, syncProgress.height); + }, + onError: (final e) { + printV("[${c.account} ($accountList)] [$oneshotSyncCount/$randInt] error syncing: $e"); + runInAction(() { + syncStatus = FailedSyncStatus( + error: + e.toString().replaceAll("AnyhowException(", "").split("\n").firstOrNull ?? + "Unknown error", + ); + }); + isSyncing = false; + if (!completer.isCompleted) { + completer.complete(); + } + }, + onDone: () async { + printV("[${c.account} ($accountList)] [$oneshotSyncCount/$randInt] synchronized"); + oneshotSyncCount--; + isSyncing = false; + try { + await withSharedCoinLock(() async { + c = await c.setAccount(account: accountId); + if (await _anyAccountNeedsSyncUnlocked(currentHeight)) { + final lagHeight = await _getLowestSyncHeight(); + _broadcastSyncProgress(currentHeight, lagHeight); + return; + } + runInAction(() { + for (final wallet in walletsByAccountId.values) { + wallet.syncStatus = SyncedSyncStatus(); + } + }); + for (final wallet in walletsByAccountId.values) { + unawaited(wallet.updateBalance()); + unawaited(wallet.updateTransactions()); + unawaited( + ZcashTaddressRotation.updateCache(mainAccountId: wallet.accountId) + .catchError((final e) { + printV("rotation cache refresh: $e"); + }), + ); + } + }); + } catch (e) { + printV("sync done height refresh: $e"); + } + if (!completer.isCompleted) { + completer.complete(); + } + }, + ); + await completer.future; + await subscription.cancel(); + return syncStatus is SyncedSyncStatus; + } catch (e) { + syncStatus = FailedSyncStatus(error: e.toString()); + isSyncing = false; + printV("error syncing: $e"); + return false; + } + } + @override - Future createTransaction(final Object credentials) async { + Future createTransaction(final Object credentials) => + _createTransaction(credentials); + + Future _createTransaction( + final Object credentials, { + final int tryReduceFeeAmount = 0, + }) async { final creds = credentials as ZcashTransactionCredentials; await updateBalance(); final zcashBalance = balance[CryptoCurrency.zec]; final availableBalance = zcashBalance?.available ?? Money.zero(currency); - final recipients = []; - var totalAmount = Money.zero(currency); + final recipients = []; + + bool receipientPaysFee = false; for (final output in creds.outputs) { - Money amount; + receipientPaysFee = receipientPaysFee || output.sendAll; + var amount = output.cryptoAmount; if (output.sendAll) { - amount = availableBalance; - } else { - amount = output.cryptoAmount; - - if (amount <= Money.zero(currency)) { - throw Exception( - 'Invalid amount for output. Amount: ${output.cryptoAmount}, Formatted: ${output.cryptoAmount}', - ); - } + amount = availableBalance - Money.fromInt(tryReduceFeeAmount, currency); } - - totalAmount += amount; - - var address = (output.isParsedAddress ? output.extractedAddress! : output.address).trim(); - - if (address.isEmpty) { - throw Exception('Empty address for output'); - } - - final paymentUri = WarpApi.decodePaymentURI(coin, address); - String memo = output.memo ?? ''; - // String memo = ''; - if (paymentUri != null && paymentUri.address != null) { - address = paymentUri.address!; - if (memo.isEmpty && paymentUri.memo != null) { - memo = paymentUri.memo!; - } - } - - if (!WarpApi.validAddress(coin, address)) { - throw Exception('Invalid Zcash address: $address'); - } - - int recipientPools = 7; - - if (address.startsWith('t1') || address.startsWith('t3')) { - recipientPools = 1; // Transparent only - } else if (address.startsWith('zs')) { - recipientPools = 2; // Sapling only - } - // For unified addresses (u1...) and other types, use 7 (all pools) - - final builder = RecipientObjectBuilder( - address: address, - pools: recipientPools, - amount: amount.amount.toInt(), - feeIncluded: output.sendAll, - replyTo: false, - memo: memo.isNotEmpty ? memo : null, + final recipientAddress = output.isParsedAddress ? output.extractedAddress! : output.address; + recipients.add( + zkool_paydart.Recipient( + assetBase: zecBase, + address: recipientAddress, + amount: amount.amount, + userMemo: output.memo, + ), ); - - recipients.add(Recipient(builder.toBytes())); } - if (totalAmount > availableBalance) { - throw Exception('Insufficient balance'); - } + final sendAmount = creds.outputs + .map((final out) => out.cryptoAmount) + .reduce((final a, final b) => a + b); - final fee = FeeT( - fee: internalCalculateEstimatedFee(creds.priority, null), - minFee: 0, - maxFee: 0, - scheme: 0, // Fixed fee scheme - ); + if (availableBalance == sendAmount) { + receipientPaysFee = true; + } // pools parameter: bitmask for which pools to use for sending - // 1=Transparent, 2=Sapling, 4=Orchard, 7=All pools - await ZcashWalletBase.loadProver(); - // Using 7 (all pools) allows spending from any pool type - final txPlan = await ZcashWalletService.runInDbMutex( - () => WarpApi.prepareTx( - coin, - accountId, - recipients, - 7, // pools: All pools (Transparent + Sapling + Orchard) - allows spending from any pool - 1, // senderUAType: 0 = unified address - 0, // anchorOffset - fee, - ), - ); - - return PendingZcashTransaction( - zcashWallet: this as ZcashWallet, - credentials: creds, - txPlan: txPlan, - fee: Money.fromInt(internalCalculateEstimatedFee(creds.priority, null), currency), - availableBalance: availableBalance, - ); + // 1=Transparent, 2=Sapling, 4=Orchard, 8=Ironwood + try { + return await runWithCoin( + accountId: accountId, + func: (coin) async { + final ironwood = await zkool_network.isIronwoodActive(c: coin); + final txPlan = await zkool_pay.prepare( + recipients: recipients, + options: zkool_pay.PaymentOptions( + srcPools: ironwood ? 8 : 4, + recipientPaysFee: receipientPaysFee, + smartTransparent: false, + mode: 0, + ), + c: coin, + ); + final txFee = _feeFromTxPlan(txPlan, creds.priority, tryReduceFeeAmount, coin: coin); + return PendingZcashTransaction( + zcashWallet: this as ZcashWallet, + credentials: creds, + txPlan: txPlan, + fee: txFee, + availableBalance: availableBalance, + ); + }, + ); + } catch (e) { + if (tryReduceFeeAmount != 0) rethrow; + final estr = e.toString(); + const prefix = "Not enough funds, "; + const suffix = " more ZEC required"; + if (estr.contains(prefix) && estr.contains(suffix)) { + final start = estr.indexOf(prefix) + prefix.length; + final end = estr.indexOf(suffix, start); + final amtStr = estr.substring(start, end); + final amt = double.tryParse(amtStr); + if (amt == null) rethrow; + final feeInt = (amt * 100000000).ceil(); + return _createTransaction(credentials, tryReduceFeeAmount: feeInt); + } + rethrow; + } } static const _dispPhrase = "Received to disposable address"; - Future> getShieldTxForUi() async { - final tx = (ZcashTaddressRotation.shieldedAccountsTx[accountId] ?? []) - .map((final v) { - final unpacked = v.unpack(); - unpacked.memo ??= ""; - unpacked.memo = "${unpacked.memo}\n$_dispPhrase".trim(); - final List buff = base64.decode( - ZcashTaddressRotation.flatBuffersPack(unpacked.pack), - ); - return ShieldedTx(buff); - }) - .where((final t) => t.value > 0); - return tx.toList(); + bool _hasExternalOutputs(final ZkoolTx tx) => + tx.outputsWithAddress.any((final o) => !_addressBelongsToWallet(o.address)); + + bool _isIronwoodMigrationTx(final ZkoolTx tx) { + // zkool classifies migration as selfTransfer with fee-sized net value. + if (tx.type != TxType.selfTransfer) { + return false; + } + final orchardSpent = tx.orchardSpent; + final spentFromOrchard = orchardSpent > BigInt.zero || + tx.spendPools.contains(NotePool.orchard.index); + if (!spentFromOrchard) { + return false; + } + if (tx.ironwoodReceived > BigInt.zero || + tx.notePools.contains(NotePool.ironwood.index)) { + return true; + } + // Orchard → Orchard split step (SD notes created, all outputs are ours). + if (tx.orchardReceived > BigInt.zero && !_hasExternalOutputs(tx)) { + return true; + } + // Orchard → Ironwood before the IW note is attached to tx details. + return orchardSpent > tx.value; } - static Map> temporarySentTx = {}; + BigInt _migrationDisplayAmount(final ZkoolTx tx) { + if (tx.ironwoodReceived > BigInt.zero) { + return tx.ironwoodReceived; + } + if (tx.orchardReceived > BigInt.zero) { + return tx.orchardReceived; + } + return tx.orchardSpent; + } - static String txChecksumKey(final ShieldedTx tx) { - final direction = tx.value > 0 ? TransactionDirection.incoming : TransactionDirection.outgoing; - return 'tx${direction}_${tx.id}_${tx.timestamp}_${CRC32.compute(tx.toString())}'; + ZcashTransactionInfo _zcashInfoFromMempoolTx( + final zkool_mempool.MempoolTx tx, + final int accountId, + ) { + final accountNotes = tx.notes.where((final n) => n.account == accountId); + final txHash = ZcashWalletService.normalizeTxId(tx.txid); + final pendingAmount = _pendingOutgoingAmounts[txHash]; + final netValue = accountNotes.fold( + BigInt.zero, + (final sum, final note) => sum + BigInt.from(note.value), + ); + final direction = pendingAmount != null || netValue < BigInt.zero + ? TransactionDirection.outgoing + : TransactionDirection.incoming; + final displayAmount = pendingAmount ?? netValue.abs(); + final memo = accountNotes + .map((final n) => n.memo) + .whereType() + .where((final m) => m.isNotEmpty) + .firstOrNull; + final recipientAddresses = _recipientAddresses( + accountNotes.map((final n) => n.address).whereType(), + direction, + ); + + final info = ZcashTransactionInfo( + id: txHash, + amount: Money(displayAmount, currency), + fee: Money.zero(currency), + direction: direction, + isPending: true, + date: DateTime.now(), + height: 0, + confirmations: 0, + to: recipientAddresses.isEmpty ? '' : recipientAddresses.first, + memo: memo, + ); + if (recipientAddresses.isNotEmpty) { + info.outputAddresses = recipientAddresses; + } + return info; + } + + ZcashTransactionInfo _zcashInfoFromZkoolTx( + final ZkoolTx tx, + final int currentHeight, { + final String? extraMemo, + final bool isRotationReceive = false, + final bool isShieldAction = false, + final TransactionDirection? directionOverride, + final BigInt? amountOverride, + }) { + final confirmations = tx.height > 0 && currentHeight >= tx.height + ? currentHeight - tx.height + 1 + : 0; + final memo = extraMemo != null ? "${tx.memo ?? ''}\n$extraMemo".trim() : tx.memo; + final isMigration = directionOverride == null && _isIronwoodMigrationTx(tx); + final direction = directionOverride ?? + (isMigration ? TransactionDirection.outgoing : tx.direction); + final recipientAddresses = _recipientAddresses(_paymentOutputAddresses(tx), direction); + final amount = amountOverride ?? + (isMigration ? _migrationDisplayAmount(tx) : tx.value); + final info = ZcashTransactionInfo( + id: tx.txHash, + amount: Money(amount, currency), + fee: Money.zero(currency), + direction: direction, + isPending: tx.height == 0, + date: tx.time, + height: tx.height, + confirmations: confirmations, + to: recipientAddresses.isEmpty ? '' : recipientAddresses.first, + memo: memo?.isNotEmpty == true ? memo : null, + txType: tx.type, + isRotationReceive: isRotationReceive, + isShieldAction: isShieldAction, + isIronwoodMigration: isMigration, + ); + if (recipientAddresses.isNotEmpty) { + info.outputAddresses = recipientAddresses; + } + return info; + } + + bool _isShieldActionTx(final ZkoolTx tx, {required final Set rotationSweepHashes}) { + if (ZcashWalletService.isAutoshieldTx(tx.txHash)) { + return true; + } + if (rotationSweepHashes.contains(tx.txHash)) { + return true; + } + if (_isPayToSelfAutoshield(tx)) { + return true; + } + if (tx.direction == TransactionDirection.outgoing && + (tx.type == TxType.shield || tx.type == TxType.transparentSelfTransfer)) { + return true; + } + return false; + } + + bool _isPayToSelfAutoshield(final ZkoolTx tx) { + if (tx.type != TxType.shield && tx.type != TxType.transparentSelfTransfer) { + return false; + } + if (tx.transparentOrSaplingSpent <= BigInt.zero) { + return false; + } + if (tx.orchardReceived <= BigInt.zero) { + return false; + } + for (final dest in tx.outputAddresses) { + if (_addressBelongsToWallet(dest)) { + return true; + } + } + return tx.orchardReceived > BigInt.zero; + } + + bool _shouldSplitAutoshieldTx(final ZkoolTx tx, {required final bool isShield}) { + if (!isShield) { + return false; + } + if (ZcashWalletService.isAutoshieldTx(tx.txHash) || _isPayToSelfAutoshield(tx)) { + return tx.transparentOrSaplingSpent > BigInt.zero && tx.orchardReceived > BigInt.zero; + } + return false; + } + + static String _txResultKey(final String txHash, {final String suffix = ''}) => + 'tx_$txHash$suffix'; + + static int _txDisplayPriority(final ZcashTransactionInfo info) { + if (info.additionalInfo['isIronwoodMigration'] == true) { + return 4; + } + if (info.additionalInfo['isAutoShield'] == true) { + return 3; + } + if (info.additionalInfo['isRotationReceive'] == true) { + return 2; + } + return 1; + } + + void _offerTx(final Map byHash, final ZcashTransactionInfo info) { + final hash = info.txHash; + final existing = byHash[hash]; + if (existing == null) { + byHash[hash] = info; + return; + } + final infoPriority = _txDisplayPriority(info); + final existingPriority = _txDisplayPriority(existing); + if (infoPriority > existingPriority) { + byHash[hash] = info; + return; + } + if (infoPriority == existingPriority && + (info.additionalInfo['isAutoShield'] == true || + info.additionalInfo['isIronwoodMigration'] == true) && + info.direction == TransactionDirection.outgoing && + existing.direction == TransactionDirection.incoming) { + byHash[hash] = info; + } + } + + bool _addressBelongsToWallet(final String addr) { + final addrs = walletAddresses; + if (addrs.containsAddress(addr) || + addrs.hiddenAddresses.contains(addr) || + addrs.usedAddresses.contains(addr)) { + return true; + } + for (final infos in addrs.addressInfos.values) { + for (final info in infos) { + if (info.address == addr) { + return true; + } + } + } + for (final own in [ + addrs.orchardAddress, + addrs.unifiedAddress, + addrs.saplingAddress, + addrs.transparentAddress, + addrs.address, + ]) { + if (own == null || own.isEmpty || own.startsWith('unknown ')) { + continue; + } + if (addr == own || addr.startsWith(own) || own.startsWith(addr)) { + return true; + } + } + return false; + } + + List _paymentOutputAddresses(final ZkoolTx tx) { + final all = tx.outputsWithAddress.toList(); + final external = all.where((final o) => !_addressBelongsToWallet(o.address)).toList(); + final outputs = external.isNotEmpty ? external : all; + final transparent = outputs.where((final o) => o.pool == NotePool.transparent.index).toList(); + if (transparent.length >= 2) { + return transparent.map((final o) => o.address).toList(); + } + return outputs.map((final o) => o.address).toList(); + } + + List _recipientAddresses( + final Iterable raw, + final TransactionDirection direction, + ) { + final seen = {}; + final addresses = [ + for (final address in raw) + if (address.trim().isNotEmpty && seen.add(address.trim())) address.trim(), + ]; + if (addresses.isEmpty) { + return []; + } + + final external = direction == TransactionDirection.incoming + ? addresses + : [ + for (final address in addresses) + if (!_addressBelongsToWallet(address)) address, + ]; + final list = external.isNotEmpty ? external : addresses; + if (list.isEmpty) { + return []; + } + + final embedded = { + for (final address in list) + if (address.startsWith('u')) ..._uaReceivers(address), + }; + var result = embedded.isEmpty + ? list + : [ + for (final address in list) + if (address.startsWith('u') || !embedded.contains(address)) address, + ]; + if (result.isEmpty) { + result = list; + } + + final standalone = [ + for (final address in result) + if (!address.startsWith('u') && !embedded.contains(address)) address, + ]; + if (standalone.length >= 2) { + result = standalone; + } + + if (direction == TransactionDirection.incoming) { + return [ + result.firstWhere( + (final a) => a.startsWith('u'), + orElse: () => + result.firstWhere((final a) => a.startsWith('z'), orElse: () => result.first), + ), + ]; + } + return result; + } + + Set _uaReceivers(final String ua) { + try { + final receivers = zkool_account.receiversFromUa(ua: ua, c: ZcashWalletBase.c); + return { + for (final address in [receivers.taddr, receivers.saddr, receivers.oaddr]) + if (address != null && address.isNotEmpty) address, + }; + } catch (_) { + return {}; + } } @override Future> fetchTransactions() async { await ZcashWalletService.loadShieldTxs(); - final txs = (await ZcashWalletService.runInDbMutex( - () => WarpApi.getTxs(coin, accountId), - )).toList(); - // ShieldedTx{id: 26, txId: 4d1be06ce2c2debec8d98ce4e9434c8aac27c980488b459017d423fdcab37f93, height: 3195705, shortTxId: 4d1be06c, timestamp: 1767730944, name: null, value: 1000000, address: null, memo: , messages: MemoVec{memos: null}} - - final shieldTx = await getShieldTxForUi(); - - txs.addAll(shieldTx); - final txIds = txs.map((final tx) => tx.txId!.replaceAll('"', '')).toSet(); - temporarySentTx[accountId]?.removeWhere( - (final ttx) => txIds.contains(ttx.txId!.replaceAll('"', '')), + final (txs, currentHeight) = await runWithCoin( + accountId: accountId, + func: (coin) async { + final txsI = await zkool_account.listTxHistory(c: coin); + final txsA = await Future.wait( + txsI.map((final tx) => zkool_account.getTxDetails(idTx: tx.id, c: coin)), + ); + final txs = []; + for (int i = 0; i < txsI.length; i++) { + txs.add(ZkoolTx(txsI[i], txsA[i])); + } + txs.sort((final a, final b) => a.height.compareTo(b.height)); + var currentHeight = 1; + try { + currentHeight = await zkool_network.getCurrentHeight(c: coin); + } catch (e) { + printV("failed to get height: $e"); + } + return (txs, currentHeight); + }, ); - txs.addAll(temporarySentTx[accountId] ?? []); + final Map byHash = {}; + final rotationTxs = ZcashTaddressRotation.rotationTxsForMainAccount(accountId); + final rotationSweepHashes = { + for (final tx in rotationTxs) + if (tx.direction == TransactionDirection.outgoing) tx.txHash, + }; - txs.sort((final a, final b) => a.height.compareTo(b.height)); - final Map result = {}; - int currentHeight = 0; - try { - currentHeight = await WarpApi.getLatestHeight(coin); - } catch (e) { - printV("Error getting latest height: $e"); + for (final tx in rotationTxs) { + if (tx.direction == TransactionDirection.incoming) { + _offerTx( + byHash, + _zcashInfoFromZkoolTx(tx, currentHeight, extraMemo: _dispPhrase, isRotationReceive: true), + ); + continue; + } + _offerTx(byHash, _zcashInfoFromZkoolTx(tx, currentHeight, isShieldAction: true)); } + final Map splitEntries = {}; for (final tx in txs) { - final direction = tx.value > 0 - ? TransactionDirection.incoming - : TransactionDirection.outgoing; - - final confirmations = tx.height > 0 && currentHeight > 0 ? currentHeight - tx.height + 1 : 0; - - final txChecksum = txChecksumKey(tx); - final txId = tx.txId ?? tx.shortTxId ?? txChecksum; - - final txInfo = ZcashTransactionInfo( - id: txId.trim().replaceAll('"', ''), - amount: Money.fromInt(tx.value.abs(), currency), - fee: Money.zero(currency), - direction: direction, - isPending: tx.height == 0, - date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), - height: tx.height, - confirmations: confirmations, - to: tx.address ?? '', - memo: tx.memo, - ); - // if (txInfo.additionalInfo['autoShield'] == true) { - // continue; - // } - result[txChecksum] = txInfo; + _pendingOutgoingAmounts.remove(ZcashWalletService.normalizeTxId(tx.txHash)); + if (tx.height > 0) { + ZcashMempoolService.instance.removeTx(tx.txHash); + } + final isShield = _isShieldActionTx(tx, rotationSweepHashes: rotationSweepHashes); + if (_shouldSplitAutoshieldTx(tx, isShield: isShield)) { + byHash.remove(tx.txHash); + splitEntries[_txResultKey(tx.txHash, suffix: '_shield')] = _zcashInfoFromZkoolTx( + tx, + currentHeight, + isShieldAction: true, + directionOverride: TransactionDirection.outgoing, + amountOverride: tx.transparentOrSaplingSpent, + ); + splitEntries[_txResultKey(tx.txHash, suffix: '_recv')] = _zcashInfoFromZkoolTx( + tx, + currentHeight, + directionOverride: TransactionDirection.incoming, + amountOverride: tx.orchardReceived, + ); + continue; + } + _offerTx(byHash, _zcashInfoFromZkoolTx(tx, currentHeight, isShieldAction: isShield)); } - return result; + final knownHashes = { + for (final tx in txs) ZcashWalletService.normalizeTxId(tx.txHash), + for (final hash in byHash.keys) ZcashWalletService.normalizeTxId(hash), + }; + for (final mempoolTx in ZcashMempoolService.instance.txsForAccount(accountId)) { + final hash = ZcashWalletService.normalizeTxId(mempoolTx.txid); + if (knownHashes.contains(hash)) { + ZcashMempoolService.instance.removeTx(hash); + _pendingOutgoingAmounts.remove(hash); + continue; + } + final info = _zcashInfoFromMempoolTx(mempoolTx, accountId); + if (info.amount.isZero) { + continue; + } + _offerTx(byHash, info); + } + + return { + for (final entry in byHash.entries) _txResultKey(entry.key): entry.value, + ...splitEntries, + }; + } + + Future _initKeys() async { + try { + c = await c.setAccount(account: accountId); + final ufvk = await zkool_account.getAccountUfvk(account: accountId, c: c, pools: 7); + + keys = { + "privateViewKey": ufvk, + if (lastKnownRestoreHeight != null) "restoreHeight": lastKnownRestoreHeight.toString(), + }; + } catch (e) { + keys = {"privateViewKey": e.toString()}; + } + try { + c = await c.setAccount(account: accountId); + final s = (await zkool_account.getAccountSeed(account: accountId, c: c)); + + if (s == null) { + throw Exception("seed not found"); + } + final seedPhrase = s.mnemonic.split(" "); + if ([13, 25].contains(seedPhrase.length)) { + passphrase = seedPhrase.removeLast(); + } else { + passphrase = s.phrase; + } + seed = s.mnemonic.trim(); + } catch (e) { + seed = e.toString(); + } } @override - Object get keys { - final backup = WarpApi.getBackup(coin, accountId); - return { - // "seed": backup.seed, - // "index": backup.index, - "privateSpendKey": backup.sk, - "privateViewKey": backup.fvk, - "uvk": backup.uvk, - "tsk": backup.tsk, - if (lastKnownRestoreHeight != null) "restoreHeight": lastKnownRestoreHeight.toString(), - }; - } + Object keys = {}; @override String get password => _password!; @override Future renameWalletFiles(final String newWalletName) async { - final currentWalletPath = await pathForWallet(name: name, type: type); - final currentCacheFile = File(currentWalletPath); - final newWalletPath = await pathForWallet(name: newWalletName, type: type); - if (currentCacheFile.existsSync()) { - await currentCacheFile.copy(newWalletPath); + await renameWalletFilesForName(fromName: name, toName: newWalletName); + } + + static Future renameWalletFilesForName({ + required final String fromName, + required final String toName, + }) async { + if (fromName == toName) { + return; + } + final currentWalletDir = Directory(await pathForWalletDir(name: fromName, type: _type)); + if (!currentWalletDir.existsSync()) { + throw Exception('Wallet directory not found: $fromName'); + } + final newWalletDirPath = '${await pathForWalletTypeDir(type: _type)}/$toName'; + if (Directory(newWalletDirPath).existsSync()) { + throw Exception('Cannot rename wallet: "$toName" already exists'); + } + await currentWalletDir.rename(newWalletDirPath); + for (final suffix in const ['', '.v2']) { + final oldFile = File('$newWalletDirPath/$fromName$suffix'); + if (oldFile.existsSync()) { + await oldFile.rename('$newWalletDirPath/$toName$suffix'); + } } - Directory(currentWalletPath).deleteSync(recursive: true); } @override bool get hasRescan => true; - static Future storeZcashHeight(final int height) async { - lastKnownRestoreHeight = height; - final zcashDir = await pathForWalletTypeDir(type: WalletType.zcash); - final zcashInitialSync = File(p.join(zcashDir, ".initial-sync-marker")); - zcashInitialSync.writeAsBytesSync([0x00]); - zcashInitialSync.writeAsStringSync(height.toString(), mode: FileMode.writeOnlyAppend); - } - static int? lastKnownRestoreHeight = null; - static Future loadZcashHeight() async { - final zcashDir = await pathForWalletTypeDir(type: WalletType.zcash); - final zcashInitialSync = File(p.join(zcashDir, ".initial-sync-marker")); - if (!await zcashInitialSync.exists()) { - return null; - } - final bytes = await zcashInitialSync.readAsBytes(); - if (bytes.isEmpty) { - return null; - } - final heightString = String.fromCharCodes(bytes.skip(1)); - lastKnownRestoreHeight = int.tryParse(heightString); - return lastKnownRestoreHeight; - } static int zashiAnnouncedBlockHeight = 2419420; + Future _getAddressesForAccount(final int id) async { + return runWithCoin( + accountId: id, + func: (final coin) => zkool_account.getAddresses(c: coin, uaPools: 7), + ); + } + + bool _addressesMatch(final dynamic old, final dynamic new_) { + return old.ua == new_.ua && + old.oaddr == new_.oaddr && + old.saddr == new_.saddr && + old.taddr == new_.taddr; + } + + Future _switchToAccount(final int newAccountId, final int height) async { + walletsByAccountId.remove(accountId); + accountId = newAccountId; + walletsByAccountId[newAccountId] = this; + walletAddresses.accountId = newAccountId; + c = await c.setAccount(account: newAccountId); + lastKnownRestoreHeight = height; + await walletAddresses.init(); + await _initKeys(); + } + @override @action Future rescan({required final int height}) async { - try { - syncStatus = StartingScanSyncStatus(height); - printV("rescanning from: $height"); - await storeZcashHeight(height); - await ZcashWalletService.runInDbMutex(() async => WarpApi.rescanFrom(coin, height)); - await startSync(); - } catch (e) { - printV("Rescan error: $e"); - syncStatus = FailedSyncStatus(error: e.toString()); - } + await zkool_sync.rewindSync(height: height, account: accountId, c: c); + // try { + // syncStatus = StartingScanSyncStatus(height); + // printV("rescanning from: $height"); + // await zkool_sync.cancelSync(); + // isSyncing = false; + + // await runWithCoinMutex.acquire(); + // try { + // final oldAddresses = await _getAddressesForAccount(accountId); + + // c = await c.setAccount(account: accountId); + // final accountSeed = await zkool_account.getAccountSeed(account: accountId, c: c); + // if (accountSeed == null) { + // throw Exception('Cannot rescan: seed not available'); + // } + + // final newAccountId = await restoreZcashWalletFromSeed( + // name: name, + // seed: accountSeed.mnemonic, + // passphrase: accountSeed.phrase, + // birthHeight: height, + // ); + + // final newAddresses = await _getAddressesForAccount(newAccountId); + // if (!_addressesMatch(oldAddresses, newAddresses)) { + // throw Exception('Rescan address verification failed'); + // } + + // await saveAccountId(name, newAccountId); + // await _switchToAccount(newAccountId, height); + // } finally { + // runWithCoinMutex.release(); + // } + + // syncStatus = ConnectedSyncStatus(); + // } catch (e) { + // printV("Rescan error: $e"); + // syncStatus = FailedSyncStatus(error: e.toString()); + // rethrow; + // } } bool _isTransactionUpdating = false; + bool _transactionUpdateQueued = false; Future updateTransactions() async { + if (_isTransactionUpdating) { + _transactionUpdateQueued = true; + return; + } + + _isTransactionUpdating = true; try { - if (_isTransactionUpdating) { - return; - } + do { + _transactionUpdateQueued = false; + final transactions = await fetchTransactions(); - _isTransactionUpdating = true; - final transactions = await fetchTransactions(); + final currentIds = transactionHistory.transactions.keys.toSet(); + final newIds = transactions.keys.toSet(); - final currentIds = transactionHistory.transactions.keys.toSet(); - final newIds = transactions.keys.toSet(); + currentIds + .difference(newIds) + .forEach((final id) => transactionHistory.transactions.remove(id)); - currentIds - .difference(newIds) - .forEach((final id) => transactionHistory.transactions.remove(id)); - - transactions.forEach((final key, final tx) { - transactionHistory.transactions[key] = tx; - }); - await transactionHistory.save(); - _isTransactionUpdating = false; + transactions.forEach((final key, final tx) { + transactionHistory.transactions[key] = tx; + }); + await transactionHistory.save(); + } while (_transactionUpdateQueued); } catch (e, stackTrace) { printV("Update transactions error: $e"); printV("Stack trace: $stackTrace"); + } finally { _isTransactionUpdating = false; } } - Future updateTransactionsHistory() => updateTransactions(); - @override Future save() async {} Future init() async { try { + await ZcashTaddressRotation.init(); await walletAddresses.init(); await updateBalance(); await updateTransactions(); + unawaited( + ZcashTaddressRotation.updateCache(mainAccountId: accountId) + .catchError((final e) => printV("rotation cache refresh: $e")), + ); + await _initKeys(); } catch (e) { printV("Wallet init error: $e"); } } @override - String? get seed { - try { - final backup = WarpApi.getBackup(coin, accountId); - final seed = backup.seed?.split(" "); - if (seed == null) { - return null; - } - if ([13, 25].contains(seed.length)) { - seed.removeLast(); - } - return seed.join(" ").trim(); - } catch (e) { - return null; - } - } + String? seed = ""; @override - String? get passphrase { - try { - final backup = WarpApi.getBackup(coin, accountId); - final seed = backup.seed?.split(" "); - if (seed == null) { - return null; - } - if ([13, 25].contains(seed.length)) { - final passphrase = seed.removeLast(); - return passphrase; - } - return null; - } catch (e) { - return null; - } - } + String? passphrase = ""; @override Future signMessage(final String message, {final String? address = null}) { @@ -476,328 +1142,279 @@ abstract class ZcashWalletBase @override @action Future startSync() async { + if (syncStatus is AttemptingSyncStatus || + syncStatus is SyncronizingSyncStatus || + syncStatus is SyncingSyncStatus) { + return; + } try { - syncStatus = AttemptingSyncStatus(); - - _initialSyncHeight = 0; - _lastKnownBlockHeight = 0; - - _startSyncStatusUpdates(); - - syncStatus = SyncronizingSyncStatus(); - - unawaited( - _runWarpSync().catchError((final e) { - isNodeWorking = false; - printV("WarpSync error in startSync: $e"); - syncStatus = FailedSyncStatus(error: e.toString()); - _stopSyncStatusUpdates(); - }), - ); + _ensureSyncLoopRunning(); + unawaited(_oneshotSync()); } catch (e) { isNodeWorking = false; printV("Sync error: $e"); syncStatus = FailedSyncStatus(error: e.toString()); - _stopSyncStatusUpdates(); rethrow; } } static Mutex warpSyncMutex = Mutex(); - static Future initialSyncCheck() async { - final zcashDir = await pathForWalletTypeDir(type: WalletType.zcash); - final zcashInitialSync = File(p.join(zcashDir, ".initial-sync-marker")); - if (!zcashInitialSync.existsSync()) { - int chainHeight = 3000000; // fallback if node is offline - try { - chainHeight = await WarpApi.getLatestHeight(coin); - } catch (e) { - printV("Error getting latest height: $e"); - } - await ZcashWalletService.runInDbMutex( - () async => await WarpApi.rescanFrom(coin, chainHeight - 150000), - ); - await storeZcashHeight(chainHeight); - } - } - - @action - Future _runWarpSync() async { - Timer? _t; - try { - await warpSyncMutex.acquire(); - await initialSyncCheck(); - isNodeWorking = true; - printV("Starting warpSync for coin $coin, account $accountId"); - int? initialQueue = null; - void _cancelSyncIfShould(final Timer t) { - initialQueue ??= ZcashWalletService.dbMutexQueue + 2; - if (ZcashWalletService.dbMutexQueue <= initialQueue!) { - initialQueue = ZcashWalletService.dbMutexQueue; - return; - } - printV( - "Canceling sync! (ZcashWalletService.dbMutexQueue: ${ZcashWalletService.dbMutexQueue} > initialQueue: ${initialQueue})", - ); - WarpApi.cancelSync(); - t.cancel(); - _t = null; - } - - unawaited( - Future.delayed(Duration(seconds: 2)).then((_) { - _t = Timer.periodic(Duration(milliseconds: 100), _cancelSyncIfShould); - }), - ); - final result = await ZcashWalletService.runInDbMutex( - () => WarpApi.warpSync(coin, accountId, true, 0, 1000000, 0), - ); - printV("warpSync completed with result: $result"); - - await _updateSyncStatus(); - } catch (e) { - syncStatus = FailedSyncStatus(error: e.toString()); - unawaited(Future.delayed(Duration(seconds: 1)).then((_) => unawaited(_runWarpSync()))); - _stopSyncStatusUpdates(); - } finally { - isNodeWorking = false; - warpSyncMutex.release(); - _t?.cancel(); - } - } - - void _startSyncStatusUpdates() { - _stopSyncStatusUpdates(); - _updateSyncStatus(); - _syncStatusTimer = Timer.periodic(const Duration(milliseconds: 5000), (_) { - _updateSyncStatus().catchError((final e) { - printV("Error in sync status update timer: $e"); - }); - }); - } - - void _stopSyncStatusUpdates() { - _syncStatusTimer?.cancel(); - _syncStatusTimer = null; - } - - void _startPeriodicSync() { - printV("_startPeriodicSync"); - _stopPeriodicSync(); - _periodicSyncTimer = Timer.periodic(const Duration(seconds: 5), (_) async { - try { - final chainHeight = await WarpApi.getLatestHeight(coin); - final dbHeight = WarpApi.getDbHeight(coin); - final height = dbHeight.unpack(); - final syncHeight = height.height; - - if (syncHeight < chainHeight) { - printV("Periodic sync: chainHeight=$chainHeight, syncHeight=$syncHeight, starting sync"); - await _runWarpSync(); - } else { - await updateBalance(); - await updateTransactions(); - } - } catch (e) { - printV("Periodic sync error: $e"); - } - }); - } - - void _stopPeriodicSync() { - _periodicSyncTimer?.cancel(); - _periodicSyncTimer = null; - } - - @action - Future _updateSyncStatus() async { - try { - final dbHeight = WarpApi.getDbHeight(coin); - final height = dbHeight.unpack(); - final syncHeight = height.height; - - final chainHeight = await WarpApi.getLatestHeight(coin); - - if (_initialSyncHeight <= 0 && syncHeight > 0) { - _initialSyncHeight = syncHeight; - printV("Initialized sync height to: $_initialSyncHeight"); - if (syncHeight - 10 > dbHeight.height) {} - } - - if (chainHeight <= 0) { - if (syncStatus is! ConnectedSyncStatus && syncStatus is! ConnectingSyncStatus) { - syncStatus = ConnectedSyncStatus(); - } - try { - await updateBalance(); - await updateTransactions(); - } catch (e) { - printV("Error updating balance/transactions: $e"); - } - return; - } - - if (syncHeight <= 0) { - if (syncStatus is! ConnectedSyncStatus && - syncStatus is! ConnectingSyncStatus && - syncStatus is! AttemptingSyncStatus) { - syncStatus = ConnectedSyncStatus(); - } - try { - await updateBalance(); - await updateTransactions(); - } catch (e) { - printV("Error updating balance/transactions: $e"); - } - return; - } - - if (syncHeight >= chainHeight && syncHeight > 0) { - syncStatus = SyncedSyncStatus(); - _stopSyncStatusUpdates(); - await updateBalance(); - await updateTransactions(); - _startPeriodicSync(); - return; - } - - if (_lastKnownBlockHeight != syncHeight) { - _lastKnownBlockHeight = syncHeight; - } - - if (syncHeight < 0 || chainHeight < syncHeight) { - return; - } - - final blocksLeft = chainHeight - syncHeight; - if (blocksLeft <= 0) { - syncStatus = SyncedSyncStatus(); - _stopSyncStatusUpdates(); - await updateBalance(); - await updateTransactions(); - _startPeriodicSync(); - return; - } - - double ptc = 0.0; - if (_initialSyncHeight > 0) { - final track = chainHeight - _initialSyncHeight; - final diff = track > 0 ? track - (chainHeight - syncHeight) : 0; - ptc = track > 0 && diff >= 0 ? diff / track : 0.0; - } else { - ptc = syncHeight / chainHeight; - } - - syncStatus = SyncingSyncStatus(blocksLeft, ptc.clamp(0.0, 1.0)); - - await updateBalance(); - await updateTransactions(); - } catch (e) { - printV("Sync status update error: $e"); - } - } - - String getDiversifiedAddress(final int uaType, {final DateTime? time}) { - try { - final timestamp = (time ?? DateTime.now()).millisecondsSinceEpoch ~/ 1000; - return WarpApi.getDiversifiedAddress(coin, accountId, uaType, timestamp); - } catch (e) { - printV("Error getting diversified address: $e"); - return ""; - } - } - static final autoShieldMutex = Mutex(); + static DateTime? _lastAutoShieldAt; + static final ironwoodMigrateMutex = Mutex(); + static DateTime? _lastIronwoodMigrateAt; Future _autoShield() async { + if (_lastAutoShieldAt != null && + _lastAutoShieldAt!.isAfter(DateTime.now().subtract(const Duration(seconds: 75)))) { + return; + } try { await autoShieldMutex.acquire(); await _$autoShield(); - } catch (e) { + } catch (e, s) { printV("shielding failed: $e"); - await Future.delayed(Duration(seconds: 30)); + s.toString().split("\n").forEach(printV); } finally { autoShieldMutex.release(); } } - Future _$autoShield() async { - final chainHeight = await WarpApi.getLatestHeight(coin); - final dbHeight = WarpApi.getDbHeight(coin); - final height = dbHeight.unpack(); - final syncHeight = height.height; - if (chainHeight != syncHeight) { - printV("Not autoshielding: chainHeight(${chainHeight}) != syncHeight(${syncHeight})"); - return; + /// Total of transparent + sapling notes at or above the per-note spendable floor. + static Future _sweepableTotal(final zkool_coin.Coin coin) async { + final notes = await zkool_account.listNotes(c: coin); + BigInt sweepable = BigInt.zero; + for (int i = 0; i < notes.length; i++) { + final note = notes[i]; + if (note.pool < 0 || note.pool >= NotePool.values.length) { + continue; + } + final noteType = NotePool.values[note.pool]; + if ((noteType == NotePool.transparent || noteType == NotePool.sapling) && + note.value >= BigInt.from(ZcashTaddressRotation.minSpendableNote)) { + sweepable += note.value; + } } - final bpConfirmed = WarpApi.getPoolBalances(coin, accountId, 0, false); - if (bpConfirmed.transparent + bpConfirmed.sapling <= 20000) { - return; - } - - final recipientBuilder = RecipientObjectBuilder( - address: (walletAddresses as ZcashWalletAddresses).orchardAddress, - pools: 4, - feeIncluded: true, - amount: bpConfirmed.transparent + bpConfirmed.sapling, - ); - - final recipient = Recipient(recipientBuilder.toBytes()); - final fee = FeeT(fee: 10000, minFee: 0, maxFee: 0, scheme: 0); - await ZcashWalletBase.loadProver(); - final txPlan = await ZcashWalletService.runInDbMutex( - () => WarpApi.prepareTx( - coin, - accountId, - [recipient], - 3, // pools: (Transparent + Sapling) - 1, - 0, // anchorOffset - fee, - ), - ); - final _txId = await ZcashWalletService.runInDbMutex( - () => WarpApi.signAndBroadcast(ZcashWalletBase.coin, accountId, txPlan), - ); - await ZcashWalletService.addShieldedTx(_txId); - printV("shielded: $_txId"); - await updateTransactions(); - await updateBalance(); - await Future.delayed(Duration(seconds: 75 * 5)); // do not re-try doing that + return sweepable; } - @override - @action - Future updateBalance() async { + /// Orchard notes that migration will actually split or move to Ironwood. + static Future _migratableOrchardTotal(final zkool_coin.Coin coin) async { + final notes = await zkool_account.listNotes(c: coin); + BigInt migratable = BigInt.zero; + for (int i = 0; i < notes.length; i++) { + final note = notes[i]; + if (note.pool != NotePool.orchard.index || note.locked) { + continue; + } + if (note.value >= BigInt.from(_ironwoodMigrateMinNote)) { + migratable += note.value; + } + } + return migratable; + } + + Future hasOrchardMigratableBalance() async { + + final (active, migratableOrchard) = await runWithCoin( + accountId: accountId, + func: (final coin) async => ( + await zkool_network.isIronwoodActive(c: coin), + await _migratableOrchardTotal(coin), + ), + ); + + if(!active) { + return false; + } + + return migratableOrchard > BigInt.zero; + } + + Future _$autoShield() async { + if (syncStatus is! SyncedSyncStatus) { + return; + } + final txId = await runWithCoin( + accountId: accountId, + func: (coin) async { + final sweepable = await _sweepableTotal(coin); + final ironwood = await zkool_network.isIronwoodActive(c: coin); + + if (sweepable <= BigInt.from(_minSweepThreshold(ironwood: ironwood))) { + return null; + } + final txPlan = await zkool_pay.prepare( + recipients: [ + zkool_paydart.Recipient( + assetBase: zecBase, + address: walletAddresses.orchardAddress!, + amount: sweepable, + pools: ironwood ? ironwoodPoolMask : null, + ), + ], + options: zkool_pay.PaymentOptions( + srcPools: 3, + recipientPaysFee: true, + smartTransparent: false, + mode: 0, + ), + c: coin, + ); + + final signTx = await zkool_pay.signTransaction(pczt: txPlan, c: coin); + final txBytes = await zkool_pay.extractTransaction(package: signTx); + final currentHeight = await zkool_network.getCurrentHeight(c: coin); + return await zkool_pay.broadcastTransaction( + height: currentHeight, + txBytes: txBytes, + c: coin, + ); + }, + ); + if (txId == null) { + return; + } + + await ZcashWalletService.addShieldedTx(txId); + _lastAutoShieldAt = DateTime.now(); + printV("shielded: $txId"); + await updateTransactions(); + await _refreshBalance(runAutoShield: false, runIronwoodMigrate: false); + } + + Future _ironwoodMigrate() async { + if (_lastIronwoodMigrateAt != null && + _lastIronwoodMigrateAt!.isAfter(DateTime.now().subtract(const Duration(seconds: 75)))) { + return; + } try { - final poolBalances = WarpApi.getPoolBalances(coin, accountId, 0, true); - final balances = poolBalances.unpack(); - // final notes = WarpApi.getNotesSync(coin, accountId); - // int frozenBalance = 0; - // for (final note in notes) { - // if (note.excluded) { - // frozenBalance += note.value; - // } - // } - final total = balances.orchard + balances.sapling + balances.transparent; - final spendable = total - balances.transparent; + await ironwoodMigrateMutex.acquire(); + await _$ironwoodMigrate(); + } catch (e, s) { + printV("ironwood migration failed: $e"); + s.toString().split("\n").forEach(printV); + } finally { + ironwoodMigrateMutex.release(); + } + } - final confirmedPoolBalances = WarpApi.getPoolBalances(coin, accountId, 3, true); - final confirmedBalances = confirmedPoolBalances.unpack(); - final confirmedTotal = - confirmedBalances.orchard + confirmedBalances.sapling + confirmedBalances.transparent; + Future _$ironwoodMigrate() async { + if (syncStatus is! SyncedSyncStatus) { + return; + } + final event = await runWithCoin( + accountId: accountId, + func: (coin) async { + if (!await zkool_network.isIronwoodActive(c: coin)) { + return null; + } + final bal = await zkool_sync.balance(c: coin); + if (bal.field0.length <= 2 || bal.field0[2] <= BigInt.zero) { + return null; + } + return zkool_migrate.stepMigration(c: coin); + }, + ); + if (event == null) { + return; + } + switch (event) { + case zkool_migrate.MigrationEvent_Complete(): + case zkool_migrate.MigrationEvent_NothingToDo(): + return; + case zkool_migrate.MigrationEvent_SplitComplete(:final fee): + printV("ironwood split step complete, fee: $fee"); + case zkool_migrate.MigrationEvent_MigrateComplete(:final fee): + printV("ironwood migrate step complete, fee: $fee"); + case zkool_migrate.MigrationEvent_Error(:final message): + printV("ironwood migration error: $message"); + return; + } - int knownOutPending = 0; - ZcashWalletBase.temporarySentTx[accountId]?.forEach((final sTx) { - knownOutPending += sTx.value; // it's negative - }); - final confirmedSpendable = confirmedTotal - balances.transparent + knownOutPending; + _lastIronwoodMigrateAt = DateTime.now(); + await updateTransactions(); + await _refreshBalance(runAutoShield: false, runIronwoodMigrate: false); + } - unawaited(_autoShield()); + Future _updateIronwoodActive() async { + bool? active; + try { + active = await runWithCoin( + accountId: accountId, + func: (final coin) => zkool_network.isIronwoodActive(c: coin), + ); + } catch (e) { + printV("isIronwoodActive: $e"); + } - balance[CryptoCurrency.zec] = ZcashBalance( - Money.fromInt(confirmedSpendable, currency), - Money.fromInt(spendable - confirmedSpendable, currency), + if (active == null && networkFor(walletInfo) == ZcashNetwork.regtest) { + try { + final height = await runWithCoin( + accountId: accountId, + func: (final coin) => zkool_network.getCurrentHeight(c: coin), + ); + active = height >= ZcashNetwork.regtestNu63Height; + printV("regtest ironwood inferred from height $height: $active"); + } catch (e) { + printV("regtest height check failed: $e"); + } + } + + if (active == null) { + return; + } + + ironwoodActive = active; + runInAction(() => walletAddresses.setIronwoodActive(active!)); + printV("ironwoodActive=$active (account $accountId)"); + } + + Future _refreshBalance({ + required final bool runAutoShield, + final bool runIronwoodMigrate = true, + }) async { + try { + await _updateIronwoodActive(); + if (runAutoShield) { + await _autoShield(); + } + if (runIronwoodMigrate) { + await _ironwoodMigrate(); + } + + final (bal, sweepable, migratableOrchard) = await runWithCoin( + accountId: accountId, + func: (final coin) async => ( + await zkool_sync.balance(c: coin), + await _sweepableTotal(coin), + await _migratableOrchardTotal(coin), + ), + ); + + // 0 - transparent, 1 - sapling, 2 - orchard, 3 - ironwood + final orchard = bal.field0.length > 2 ? bal.field0[2] : BigInt.zero; + final ironwood = bal.field0.length > 3 ? bal.field0[3] : BigInt.zero; + + // After NU6.3, Orchard notes are migrated to Ironwood - show them as unconfirmed. + // Unavailable uses the same per-note totals and thresholds as auto-shield/migration guards. + final BigInt availableAmount; + final BigInt unavailableAmount; + if (ironwoodActive == true && orchard > BigInt.zero) { + final sweepableUnavailable = sweepable <= BigInt.from(_ironwoodMigrateMinNote) + ? BigInt.zero + : sweepable; + availableAmount = ironwood; + unavailableAmount = migratableOrchard + sweepableUnavailable; + } else { + final minSweep = _minSweepThreshold(ironwood: ironwoodActive == true); + availableAmount = orchard + ironwood; + unavailableAmount = sweepable <= BigInt.from(minSweep) ? BigInt.zero : sweepable; + } + + balance[currency] = ZcashBalance( + Money(availableAmount, currency), + Money(unavailableAmount, currency), frozen: Money.zero(currency), ); } catch (e, stackTrace) { @@ -806,6 +1423,12 @@ abstract class ZcashWalletBase } } + @override + @action + Future updateBalance() async { + await _refreshBalance(runAutoShield: true); + } + @override Future verifyMessage( final String message, @@ -815,11 +1438,13 @@ abstract class ZcashWalletBase throw UnimplementedError(); } - @observable - late WalletAddresses walletAddresses = ZcashWalletAddresses(accountId, walletInfo); + @override + late ZcashWalletAddresses walletAddresses = ZcashWalletAddresses(accountId, walletInfo); static Future create(final WalletCredentials credentials) async { - await _init(); + final network = networkForCredentials(credentials); + await $init(network: network); + credentials.walletInfo?.network = network.value; final newWalletCredentials = credentials as ZcashNewWalletCredentials; String mnemonic; @@ -830,57 +1455,54 @@ abstract class ZcashWalletBase mnemonic = bip39.generateMnemonic(strength: strength); } - final accountId = await _restoreZcashWalletFromSeed( + final birthHeight = await birthHeightForNetwork(network); + + final accountId = await restoreZcashWalletFromSeed( name: credentials.name, seed: mnemonic, passphrase: newWalletCredentials.passphrase, + birthHeight: birthHeight, ); - await _saveAccountId(credentials.name, accountId); + await saveAccountId(credentials.name, accountId); final wallet = await open( name: credentials.name, password: credentials.password!, walletInfo: credentials.walletInfo!, ); - await wallet.walletAddresses.saveAddressesInBox(); + await wallet.init(); return wallet; } static Future restore(final WalletCredentials credentials) async { - await _init(); + final network = networkForCredentials(credentials); + await $init(network: network); + credentials.walletInfo?.network = network.value; final fromSeedCredentials = credentials as ZcashFromSeedWalletCredentials; final String? seed = fromSeedCredentials.seed; if (seed == null || seed.isEmpty) { throw Exception('Seed phrase is required for wallet restoration'); } - final accountId = await _restoreZcashWalletFromSeed( + final accountId = await restoreZcashWalletFromSeed( name: credentials.name, seed: seed, passphrase: fromSeedCredentials.passphrase, + birthHeight: credentials.height!, ); - await _saveAccountId(credentials.name, accountId); + await saveAccountId(credentials.name, accountId); final wallet = await open( name: credentials.name, password: credentials.password!, walletInfo: credentials.walletInfo!, ); - await wallet.walletAddresses.saveAddressesInBox(); - printV("height: ${credentials.height}"); - if (credentials.height != null) { - await storeZcashHeight(credentials.height!); - unawaited( - Future.delayed(Duration(seconds: 2)).then( - (_) => ZcashWalletService.runInDbMutex( - () async => await WarpApi.rescanFrom(coin, credentials.height ?? 0), - ), - ), - ); - } + await wallet.init(); return wallet; } static Future restoreKeys(final WalletCredentials credentials) async { - await _init(); + final network = networkForCredentials(credentials); + await $init(network: network); + credentials.walletInfo?.network = network.value; final fromKeysCredentials = credentials as ZcashFromKeysWalletCredentials; final String? keys = fromKeysCredentials.privateKey; if (keys == null || keys.isEmpty) { @@ -892,29 +1514,20 @@ abstract class ZcashWalletBase throw Exception('Key is not in secret-extended-key-main1 format'); } - final accountId = await _restoreZcashWalletFromSeed( + final accountId = await restoreZcashWalletFromSeed( name: credentials.name, seed: keys, passphrase: fromKeysCredentials.passphrase, + birthHeight: credentials.height!, ); - await _saveAccountId(credentials.name, accountId); + await saveAccountId(credentials.name, accountId); final wallet = await open( name: credentials.name, password: credentials.password!, walletInfo: credentials.walletInfo!, ); - await wallet.walletAddresses.saveAddressesInBox(); + await wallet.init(); printV("height: ${credentials.height}"); - if (credentials.height != null) { - await storeZcashHeight(credentials.height!); - unawaited( - Future.delayed(Duration(seconds: 2)).then( - (_) => ZcashWalletService.runInDbMutex( - () async => await WarpApi.rescanFrom(coin, credentials.height ?? 0), - ), - ), - ); - } return wallet; } @@ -923,63 +1536,120 @@ abstract class ZcashWalletBase required final String password, required final WalletInfo walletInfo, }) async { - await _init(); - if (password.isNotEmpty) { - WarpApi.setDbPasswd(coin, password); - } + final network = networkFor(walletInfo); + await $init(network: network); + // if (password.isNotEmpty) { + // setDbPasswd(coin, password); + // } final accountId = await getZcashAccountIdForName(name); if (accountId == null) { - throw Exception("Wallet account not found for name: $name"); + throw Exception("accountId is null"); } + c = await c.setAccount(account: accountId); final wallet = ZcashWallet( walletInfo, await walletInfo.getDerivationInfo(), accountId: accountId, ); - await wallet.walletAddresses.init(); + await wallet._initKeys(); return wallet; } - static Future _restoreZcashWalletFromSeed({ + static Future restoreZcashWalletFromSeed({ required final String name, - required String seed, - required String? passphrase, + required final String seed, + required final String? passphrase, + required final int birthHeight, }) async { - if (passphrase?.isNotEmpty == true) { - passphrase = passphrase!.replaceAll(" ", "_"); - seed = "${seed} ${passphrase}"; - } - final accountId = await ZcashWalletService.runInDbMutex( - () => WarpApi.newAccount(coin, name, seed, 0), + // if (passphrase?.isNotEmpty == true) { + // passphrase = passphrase!.replaceAll(" ", "_"); + // seed = "${seed} ${passphrase}"; + // } + + final accountId = await newAccount( + name: name, + height: birthHeight, + seed: seed, + passphrase: passphrase ?? '', ); return accountId; } - static Future getZcashAccountIdForName(final String name) async { - final wPath = await pathForWallet(name: name, type: _type); + static Future getLegacyZcashAccountIdForName(final String name) async { + final wPath = (await pathForWallet(name: name, type: _type)); final f = File(wPath); if (!f.existsSync()) { - final accounts = WarpApi.getAccountList(coin); - for (final account in accounts) { - if (account.name == name) { - return account.id; + final accs = await zkool_account.listAccounts(c: c); + for (final acc in accs) { + if (acc.name == name) { + return acc.id; } } - return null; } final content = f.readAsStringSync(); return int.tryParse(content.trim()); } - static Future _saveAccountId(final String name, final int accountId) async { - final wPath = await pathForWallet(name: name, type: _type); + static Future getZcashAccountIdForName(final String name) async { + final wPath = (await pathForWallet(name: name, type: _type)) + ".v2"; + final f = File(wPath); + if (!f.existsSync()) { + final accs = await zkool_account.listAccounts(c: c); + for (final acc in accs) { + if (acc.name == name) { + return acc.id; + } + } + } + final content = f.readAsStringSync(); + return int.tryParse(content.trim()); + } + + static Future saveAccountId(final String name, final int accountId) async { + final wPath = (await pathForWallet(name: name, type: _type)) + ".v2"; + final dirName = Directory(wPath).parent.path; + if (!Directory(dirName).existsSync()) { + Directory(dirName).createSync(recursive: true); + } final f = File(wPath); f.writeAsStringSync(accountId.toString()); } static WalletType get _type => WalletType.zcash; - static Future getDbDataPath() async { + static ZcashNetwork networkFor(final WalletInfo? walletInfo) => + ZcashNetwork.fromName(walletInfo?.network ?? ZcashNetwork.mainnet.value); + + static ZcashNetwork networkForCredentials(final WalletCredentials credentials) { + if (credentials is ZcashNewWalletCredentials) { + return ZcashNetwork.fromIndex(credentials.network); + } + if (credentials is ZcashFromSeedWalletCredentials) { + return ZcashNetwork.fromIndex(credentials.network); + } + if (credentials is ZcashFromKeysWalletCredentials) { + return ZcashNetwork.fromIndex(credentials.network); + } + return ZcashNetwork.mainnet; + } + + static Future birthHeightForNetwork(final ZcashNetwork network) async { + if (network != ZcashNetwork.mainnet) { + return 1; + } + return ZcashHeight.getBlockHeightByTime(DateTime.now()); + } + + static Future getDbDataPath({final ZcashNetwork network = ZcashNetwork.mainnet}) async { + final pathForWalletType = await pathForWalletTypeDir(type: _type); + final dbDataPath = "${pathForWalletType}/${network.dbFileName}"; + if (!Directory(pathForWalletType).existsSync()) { + Directory(pathForWalletType).createSync(recursive: true); + } + return dbDataPath; + } + + static Future getDbDataPathLegacyYwallet() async { final pathForWalletType = await pathForWalletTypeDir(type: _type); final dbDataPath = "${pathForWalletType}/zec.db"; if (!Directory(pathForWalletType).existsSync()) { @@ -988,195 +1658,110 @@ abstract class ZcashWalletBase return dbDataPath; } - static Future getTorDir() async { - final pathForWalletType = await pathForWalletTypeDir(type: _type); - final torPath = "${pathForWalletType}/tor"; - if (!Directory(torPath).existsSync()) { - Directory(torPath).createSync(recursive: true); - } - return torPath; - } - - static Future getFsBlockCacheDir() async { - final pathForWalletType = await pathForWalletTypeDir(type: _type); - final fsBlockCacheDir = "${pathForWalletType}/blockCache"; - if (!Directory(pathForWalletType).existsSync()) { - Directory(pathForWalletType).createSync(recursive: true); - } - return fsBlockCacheDir; - } - - static String? dbDataPath; static bool _initialized = false; + static bool _rustInitialized = false; + static ZcashNetwork? _activeNetwork; static void unlockDatabase(final String password) { _password = password; } + static var c = zkool_coin.Coin(); + static String? _password; - static Future _init() async { - if (_initialized) return; - dbDataPath = await getDbDataPath(); - printV("WarpApi.initWallet"); + static Future $init({final ZcashNetwork network = ZcashNetwork.mainnet}) async { + if (!_rustInitialized) { + await zkool_frb.RustLib.init(); + _rustInitialized = true; + } + if (_initialized && _activeNetwork == network) { + return; + } + printV(r".$init($network)"); + ZcashMempoolService.instance.onAccountsUpdated = (final accountIds) { + for (final accountId in accountIds) { + unawaited(refreshWalletForAccount(accountId)); + } + }; + final dbFile = File(await getDbDataPath(network: network)); + final ywalletDbFile = File(await getDbDataPathLegacyYwallet()); + await zkool_network.initDatadir(directory: dbFile.parent.path); + c = await c.openDatabase(dbFilepath: dbFile.path, password: null); + printV("initWallet: ${dbFile.path}"); if (_password == null) { throw Exception("Zcash wallet locked! Please contact support"); } - if (!File(dbDataPath!).existsSync()) { + if (!dbFile.existsSync()) { //TODO(mrcyjanek): copy-encrypt } - // coin+1 = ycash - WarpApi.setDbPasswd(coin, ''); - WarpApi.setDbPasswd(coin + 1, ''); - WarpApi.initWallet(coin, dbDataPath!); - WarpApi.initWallet(coin + 1, dbDataPath!); - try { - WarpApi.migrateData(coin); - WarpApi.migrateData(coin + 1); - } catch (e) { - printV("zec init failed: $e"); - } // do not fail on network exception - await loadZcashHeight(); + if (!ywalletDbFile.existsSync()) { + //TODO(mrcyjanek): migrate to zkool + } - unawaited(loadProver()); - - await ZcashTaddressRotation.init(); - await ZcashTransactionInfo.init(); + _activeNetwork = network; _initialized = true; } - static bool isProverLoaded = false; - static Future loadProver() async { - Uint8List? spend; - Uint8List? output; - final cacheDir = await getApplicationCacheDirectory(); - try { - final spendBundle = await rootBundle.load('scripts/zcash_lib/assets/sapling-spend.params'); - final outputBundle = await rootBundle.load('scripts/zcash_lib/assets/sapling-output.params'); - spend = spendBundle.buffer.asUint8List(); - output = outputBundle.buffer.asUint8List(); - if (spend.length == 0 || output.length == 0) { - spend = await File(cacheDir.path + "/sapling-spend.params").readAsBytesSync(); - output = await File(cacheDir.path + "/sapling-output.params").readAsBytesSync(); - } - if (spend.length == 0 || output.length == 0) throw Exception("NUH UH"); - } catch (e) { - printV("$e. Fine, I'll download them."); - final spendResponse = await ProxyWrapper().get( - clearnetUri: Uri.parse("https://download.z.cash/downloads/sapling-spend.params"), - ); - final outputResponse = await ProxyWrapper().get( - clearnetUri: Uri.parse("https://download.z.cash/downloads/sapling-output.params"), - ); - spend = spendResponse.bodyBytes; - output = outputResponse.bodyBytes; - await File(cacheDir.path + "/sapling-spend.params").writeAsBytes(spend); - await File(cacheDir.path + "/sapling-output.params").writeAsBytes(output); - } - WarpApi.initProver(spend, output); - isProverLoaded = true; - } - - static Future getBlockHeightByTime(final DateTime time) async { - final genesisTime = DateTime.utc(2016, 10, 28); - const genesisHeight = 0; - - final firstHalvingTime = DateTime.utc(2020, 11, 18); - const firstHalvingHeight = 1046400; - - final secondHalvingTime = DateTime.utc(2024, 11, 23); - const secondHalvingHeight = 2726400; - - final t = time.toUtc().millisecondsSinceEpoch; - final t0 = genesisTime.millisecondsSinceEpoch; - final t1 = firstHalvingTime.millisecondsSinceEpoch; - final t2 = secondHalvingTime.millisecondsSinceEpoch; - - if (t <= t0) return genesisHeight; - - if (t < t1) { - return _interpolate(genesisHeight, firstHalvingHeight, t0, t1, t); - } - - if (t < t2) { - return _interpolate(firstHalvingHeight, secondHalvingHeight, t1, t2, t); - } - - final secondsSince2 = (t - t2) / 1000.0; - final blocksSince2 = (secondsSince2 / 76.0).floor(); - return secondHalvingHeight + blocksSince2; - } - - static int _interpolate( - final int hStart, - final int hEnd, - final int tStart, - final int tEnd, - final int t, - ) { - if (tEnd == tStart) return hStart; - final ratio = (t - tStart) / (tEnd - tStart); - return (hStart + (hEnd - hStart) * ratio).round(); - } - static Future getHeightByDate(final DateTime date) async { - int height = await getBlockHeightByTime(date); - try { - final h2 = await WarpApi.getBlockHeightByTime(coin, date); - height = h2; - } catch (e) { - printV("getHeightByDate: $e"); - } + final height = await ZcashHeight.getBlockHeightByTime(date); return height; } - bool couldBeZashiWallet() { - final backup = WarpApi.getBackup(coin, accountId); - final seed = backup.seed?.split(" "); - if (seed == null) return false; - if (zkoolSweep != null) return false; - if (!(syncStatus is SyncedSyncStatus)) return false; - return seed.length == 24; + static Future newAccount({ + required final String name, + required final int height, + required final String seed, + required final String passphrase, + }) async { + final id = await zkool_account.newAccount( + na: zkool_account.NewAccount( + name: name, + restore: true, + passphrase: passphrase, + key: seed, + aindex: 0, + birth: height, + folder: '', + useInternal: true, + internal: false, + ledger: false, + ), + c: c, + ); + return id; } - static ZkoolSweep? zkoolSweep; + static final runWithCoinMutex = Mutex(); + static int runWithCoinCount = 0; - static bool _didRunRescanInternalChange = false; - Future rescanInternalChange() async { - if (_didRunRescanInternalChange) { - return; + static Future withSharedCoinLock(final FutureOr Function() func) async { + await runWithCoinMutex.acquire(); + try { + return await func(); + } finally { + runWithCoinMutex.release(); + } + } + + static FutureOr runWithCoin({ + required final int accountId, + required final FutureOr Function(zkool_coin.Coin c) func, + }) async { + var newC = zkool_coin.Coin(); + newC = await newC.openDatabase(dbFilepath: c.dbFilepath); + newC = await newC.setAccount(account: accountId); + newC = await newC.setLwd(serverType: c.serverType, url: c.url); + newC = await newC.setUseTor(useTor: c.useTor); + + runWithCoinCount++; + printV("run with coin: $runWithCoinCount"); + await runWithCoinMutex.acquire(); + try { + newC = await newC.setAccount(account: accountId); + return await func(newC); + } finally { + runWithCoinMutex.release(); + runWithCoinCount--; } - _didRunRescanInternalChange = true; - final bal = - balance[CryptoCurrency.zec]!.available + - balance[CryptoCurrency.zec]!.unavailable + - (balance[CryptoCurrency.zec]!.frozen ?? Money.zero(CryptoCurrency.zec)); - final osCacheDir = await getApplicationCacheDirectory(); - final cacheDir = osCacheDir.createTempSync("zkool-import"); - zkoolSweep = ZkoolSweep( - currentBalance: bal.amount.toInt(), - cacheDir: cacheDir.path, - seed: seed ?? '', - passphrase: password, - address: (walletAddresses as ZcashWalletAddresses).orchardAddress, - url: (lastNode!.isSSL ? 'https://' : 'http://') + lastNode!.uriRaw, - height: (await loadZcashHeight()) ?? zashiAnnouncedBlockHeight, - ); - unawaited(zkoolSweep!.start()); - int count = 0; - Timer.periodic(Duration(milliseconds: 1000 ~/ 120), (final Timer t) { - final msg = ZkoolSweep.msg; - if (kDebugMode && (++count % (120 ~/ 10)) == 0) { - printV(msg.message); - } - if (msg.blocksLeft == 0 && msg.networkHeight != 0) { - zkoolSweep = null; - t.cancel(); - } - if (msg.blocksLeft == 0) { - syncStatus = ConnectedSyncStatus(); - return; - } - syncStatus = SyncingSyncStatus(msg.blocksLeft, msg.progress); - }); } } diff --git a/cw_zcash/lib/src/zcash_wallet_addresses.dart b/cw_zcash/lib/src/zcash_wallet_addresses.dart index a9fd207ae..295f2b096 100644 --- a/cw_zcash/lib/src/zcash_wallet_addresses.dart +++ b/cw_zcash/lib/src/zcash_wallet_addresses.dart @@ -6,7 +6,7 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_zcash/cw_zcash.dart'; import 'package:cw_zcash/src/zcash_taddress_rotation.dart'; import 'package:mobx/mobx.dart'; -import 'package:warp_api/warp_api.dart'; +import 'package:zkool/src/rust/api/account.dart' as zkool_account; part 'zcash_wallet_addresses.g.dart'; @@ -17,24 +17,50 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store { addressPageType = ZcashReceivePageOption.typeFromString(walletInfo.addressPageType ?? ""); } - final int accountId; + int accountId; static const int transparentType = 0; static const int shieldedSaplingType = 1; static const int shieldedOrchardType = 2; static const int unifiedType = 3; - String? _transparentAddress; - String? _saplingAddress; - String? _orchardAddress; - String? _unifiedAddress; + @observable + String? transparentAddress; + + @observable + String? _transparentObservableAddress; + + @observable + String? saplingAddress; + + @observable + String? orchardAddress; + + @observable + String? unifiedAddress; + + @observable + bool ironwoodActive = false; + + @action + void setIronwoodActive(final bool active) { + ironwoodActive = active; + } @override @computed + List get receivePageOptions { + return [ + ...ZcashReceivePageOption.allOptionsFor(ironwood: ironwoodActive), + ...ReceivePageOptions.where((final element) => element != ReceivePageOption.mainnet), + ]; + } + + @override String get latestAddress { switch (addressPageType) { case ZcashAddressType.transparent: - return transparentAddress; + return transparentAddress ?? "unknown transparentAddress"; case ZcashAddressType.transparentRotated: // just to display something, after the wallet is synced the address // will start rotating, and since we can't rotate address until the wallet syncs @@ -46,16 +72,15 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store { // 3) display first address from rotated pool which is also static until synced // 2 seems like an obvious winner simply because it offers little to no benefit over 3 // and is noticably less complex in implementation - return transparentAddressRotated ?? transparentAddress; + return _transparentObservableAddress ?? + transparentAddress ?? + "unknown transparentAddressRotated"; case ZcashAddressType.shieldedSapling: - return saplingAddress; + return saplingAddress ?? "unknown saplingAddress"; case ZcashAddressType.shieldedOrchard: - return orchardAddress; + return orchardAddress ?? "unknown orchardAddress"; case ZcashAddressType.unifiedType: - if (_unifiedAddress == null) { - _unifiedAddress = WarpApi.getAddress(ZcashWalletBase.coin, accountId, 6); - } - return _unifiedAddress ?? ""; + return unifiedAddress ?? "unknown unifiedAddress"; } } @@ -79,69 +104,27 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store { await walletInfo.save(); } - String get transparentAddress { - try { - if (_transparentAddress == null) { - _transparentAddress = WarpApi.getTAddr(ZcashWalletBase.coin, accountId); - } - return _transparentAddress ?? ""; - } catch (e) { - return ""; - } - } - + @override @computed - String? get transparentAddressRotated { - try { - return ZcashTaddressRotation.addressForAccount(accountId, hiddenAddresses); - } catch (e) { - return null; - } - } - - String get saplingAddress { - try { - if (_saplingAddress == null) { - _saplingAddress = WarpApi.getAddress(ZcashWalletBase.coin, accountId, 2); - } - return _saplingAddress ?? ""; - } catch (e) { - return ""; - } - } - - String get orchardAddress { - try { - if (_orchardAddress == null) { - _orchardAddress = WarpApi.getAddress(ZcashWalletBase.coin, accountId, 4); - } - return _orchardAddress ?? ""; - } catch (e) { - return ""; - } - } + String get addressForExchange => + _transparentObservableAddress ?? transparentAddress ?? "unknown addressForExchange"; @override @computed - String get addressForExchange => transparentAddressRotated ?? transparentAddress; - - @override - @computed - String get addressForBuy => transparentAddressRotated ?? transparentAddress; + String get addressForBuy => + _transparentObservableAddress ?? transparentAddress ?? "unknown addressForBuy"; @override bool containsAddress(final String address) { return this.address == address || addressesMap.values.contains(address); } - static int get coin => ZcashWalletBase.coin; - @override @observable Set hiddenAddresses = {}; Future _initAddresses() async { - addressesMap = await walletInfo.getAddresses(); + addressesMap = {}; addressInfos = await walletInfo.getAddressInfos(); usedAddresses = await walletInfo.getUsedAddresses(); manualAddresses = await walletInfo.getManualAddresses(); @@ -149,37 +132,105 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store { address = latestAddress; } + Future _syncRotationHiddenAddresses() async { + final all = (await ZcashTaddressRotation.allAddressesForAccount(accountId)).toSet(); + final used = (await ZcashTaddressRotation.usedAddressesForAccount(accountId)).toSet(); + hiddenAddresses.removeWhere(all.contains); + hiddenAddresses.addAll(used); + } + @override Future init() async { + try { + await _init(); + } catch (e) { + printV("init failed, retrying in 2 seconds"); + printV(e); + await Future.delayed(Duration(seconds: 2)); + await init(); + } + } + + static bool isPlaceholderAddress(final String? address) => + address == null || address.isEmpty || address.startsWith('unknown '); + + String _resolvedWalletInfoAddress() { + for (final candidate in [unifiedAddress, orchardAddress, saplingAddress, transparentAddress]) { + if (!isPlaceholderAddress(candidate)) { + return candidate!; + } + } + return latestAddress; + } + + Future _init() async { await _initAddresses(); - await ZcashTaddressRotation.init(); - int accountIndex = 0; - addressInfos = { - 0: - ZcashTaddressRotation.allAddressesForAccount(accountId)?.map((final v) { - return WalletInfoAddressInfo( - walletInfoId: walletInfo.internalId, - mapKey: ++accountIndex, - accountIndex: 0, - address: v, - label: "", - ); - }).toList() ?? - [], - }; - hiddenAddresses.addAll( - ZcashTaddressRotation.allUsedAddressesForAccount(accountId)?.toSet() ?? {}, + final addr = await ZcashWalletBase.runWithCoin( + accountId: accountId, + func: (final coin) => zkool_account.getAddresses(c: coin, uaPools: 7), ); - // addressInfos[0]?.removeWhere((final test) => hiddenAddresses.contains(test.address)); + transparentAddress = addr.taddr ?? 'unknown addr.taddr'; + saplingAddress = addr.saddr ?? 'unknown addr.saddr'; + orchardAddress = addr.oaddr ?? 'unknown addr.oaddr'; + unifiedAddress = addr.ua ?? 'unknown addr.ua'; + _transparentObservableAddress = await ZcashTaddressRotation.addressForAccount(accountId); + final rotationAddrs = await ZcashTaddressRotation.allAddressesForAccount(accountId); + addressInfos = { + 0: [ + for (int i = 0; i < rotationAddrs.length; i++) + WalletInfoAddressInfo( + walletInfoId: walletInfo.internalId, + mapKey: i + 1, + accountIndex: 0, + address: rotationAddrs[i], + label: "", + ), + ], + }; + await _syncRotationHiddenAddresses(); + await _applyAddressForCurrentType(); + + if (isPlaceholderAddress(walletInfo.address)) { + final resolved = _resolvedWalletInfoAddress(); + if (!isPlaceholderAddress(resolved)) { + walletInfo.address = resolved; + await walletInfo.save(); + } + } + + await saveAddressesInBox(); + } + + Future refreshRotationAddresses() async { + _transparentObservableAddress = await ZcashTaddressRotation.addressForAccount(accountId); + final rotationAddrs = await ZcashTaddressRotation.allAddressesForAccount(accountId); + addressInfos = { + 0: [ + for (int i = 0; i < rotationAddrs.length; i++) + WalletInfoAddressInfo( + walletInfoId: walletInfo.internalId, + mapKey: i + 1, + accountIndex: 0, + address: rotationAddrs[i], + label: "", + ), + ], + }; + await _syncRotationHiddenAddresses(); + await _applyAddressForCurrentType(); + } + + Future _applyAddressForCurrentType() async { if (_addressPageType == ZcashAddressType.transparentRotated) { - final addr = ZcashTaddressRotation.addressForAccount(accountId, hiddenAddresses); + final addr = await ZcashTaddressRotation.addressForAccount(accountId); if (addr != null) { address = addr; } + return; } - await saveAddressesInBox(); + address = latestAddress; } @observable @@ -198,12 +249,17 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store { @override Future saveAddressesInBox() async { try { - await walletInfo.setAddresses(addressesMap); + if (!isPlaceholderAddress(address)) { + walletInfo.address = address; + } + await walletInfo.setAddresses({}); await walletInfo.setAddressInfos(addressInfos); await walletInfo.setUsedAddresses(usedAddresses.toList()); await walletInfo.setHiddenAddresses(hiddenAddresses.toList()); await walletInfo.setManualAddresses(manualAddresses.toList()); + await walletInfo.save(); await _initAddresses(); + await _syncRotationHiddenAddresses(); } catch (e) { printV("Error saving addresses: $e"); } @@ -211,10 +267,7 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store { @override Future updateAddressesInBox() async { - _transparentAddress = null; - _saplingAddress = null; - _orchardAddress = null; - _unifiedAddress = null; + await _initAddresses(); } @override @@ -227,19 +280,7 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store { if (addressPageType != ZcashAddressType.transparentRotated) { return []; } - final List allInfos = []; - for (final entry in addressInfos.entries) { - allInfos.addAll(entry.value); - } - return allInfos; - } - - @override - List get receivePageOptions { - return [ - ...ZcashReceivePageOption.all, - ...ReceivePageOptions.where((final element) => element != ReceivePageOption.mainnet), - ]; + return addressInfos[0] ?? []; } @override diff --git a/cw_zcash/lib/src/zcash_wallet_service.dart b/cw_zcash/lib/src/zcash_wallet_service.dart index 0d79e7065..660724e44 100644 --- a/cw_zcash/lib/src/zcash_wallet_service.dart +++ b/cw_zcash/lib/src/zcash_wallet_service.dart @@ -9,6 +9,7 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_zcash/cw_zcash.dart'; +import 'package:cw_zcash/src/migrate.dart'; import 'package:mutex/mutex.dart'; import 'package:path/path.dart' as p; @@ -25,9 +26,9 @@ class ZcashWalletService static Mutex dbMutex = Mutex(); static int dbMutexQueue = 0; static Future runInDbMutex(final Future Function() call) async { + dbMutexQueue++; + await dbMutex.acquire(); try { - printV("dbMutexQueue: ${dbMutexQueue++}"); - await dbMutex.acquire(); return await call(); } finally { dbMutex.release(); @@ -37,10 +38,15 @@ class ZcashWalletService static Set autoshieldTx = {}; + static String normalizeTxId(final String txId) => + txId.replaceAll(RegExp(r'[^a-fA-F0-9]'), '').toLowerCase(); + + static bool isAutoshieldTx(final String txHash) => autoshieldTx.contains(normalizeTxId(txHash)); + static Future addShieldedTx(final String txId) async { final pathForWalletType = await pathForWalletTypeDir(type: type); - final shieldedListFile = p.join(pathForWalletType, "autoshield_list.txt"); - autoshieldTx.add(txId.replaceAll(RegExp(r'[^a-zA-Z0-9 ]'), '')); + final shieldedListFile = p.join(pathForWalletType, "autoshield_list.v2.txt"); + autoshieldTx.add(normalizeTxId(txId)); final shieldedList = File(shieldedListFile); shieldedList.writeAsStringSync(autoshieldTx.join("\n")); } @@ -48,12 +54,16 @@ class ZcashWalletService static Future loadShieldTxs() async { try { final pathForWalletType = await pathForWalletTypeDir(type: type); - final shieldedListFile = p.join(pathForWalletType, "autoshield_list.txt"); + final shieldedListFile = p.join(pathForWalletType, "autoshield_list.v2.txt"); final shieldedList = File(shieldedListFile); if (!shieldedList.existsSync()) { return; } - autoshieldTx = shieldedList.readAsLinesSync().toSet(); + autoshieldTx = shieldedList + .readAsLinesSync() + .map(normalizeTxId) + .where((final line) => line.isNotEmpty) + .toSet(); } catch (e) { printV("loadShieldTxs failed: $e"); } @@ -74,17 +84,26 @@ class ZcashWalletService @override Future isWalletExit(final String name) async { - final path = await pathForWallet(name: name, type: getType()); - return File(path).existsSync(); + final oldPath = (await pathForWallet(name: name, type: getType())); + final path = (await pathForWallet(name: name, type: getType())) + ".v2"; + return File(path).existsSync() || File(oldPath).existsSync(); } @override Future openWallet(final String name, final String password) async { - await loadShieldTxs(); final walletInfo = await WalletInfo.get(name, getType()); if (walletInfo == null) { throw Exception('Wallet not found'); } + await ZcashWalletBase.$init(network: ZcashWalletBase.networkFor(walletInfo)); + if (await isWalletExit(name)) { + final path = (await pathForWallet(name: name, type: getType())) + ".v2"; + if (!File(path).existsSync()) { + await migrateOldSqliteToZkool2(walletName: name); + } + } + + await loadShieldTxs(); try { final wallet = await ZcashWalletBase.open( name: name, @@ -108,7 +127,7 @@ class ZcashWalletService @override Future remove(final String wallet) async { - final path = await pathForWalletDir(name: wallet, type: getType()); + final path = (await pathForWalletDir(name: wallet, type: getType())); final file = Directory(path); final isExist = file.existsSync(); @@ -129,17 +148,11 @@ class ZcashWalletService if (currentWalletInfo == null) { throw Exception('Wallet not found'); } - final accountId = await ZcashWalletBase.getZcashAccountIdForName(currentName); - if (accountId == null) { - throw Exception('Wallet account not found for name: $currentName'); + if (!await isWalletExit(currentName)) { + throw Exception('Wallet not found'); } - final currentWallet = ZcashWallet( - currentWalletInfo, - await currentWalletInfo.getDerivationInfo(), - accountId: accountId, - ); - await currentWallet.renameWalletFiles(newName); + await ZcashWalletBase.renameWalletFilesForName(fromName: currentName, toName: newName); final newWalletInfo = currentWalletInfo; newWalletInfo.id = WalletBase.idFor(newName, getType()); @@ -162,11 +175,11 @@ class ZcashWalletService final bool? isTestnet, }) async { if (credentials.seed == null || credentials.seed!.isEmpty) { - throw ZcashMnemonicIsIncorrectException(); + throw Exception("Seed is missing"); } if (!bip39.validateMnemonic(credentials.seed!)) { - throw ZcashMnemonicIsIncorrectException(); + throw Exception("Seed is not valid bip39"); } return ZcashWalletBase.restore(credentials); diff --git a/cw_zcash/lib/src/zkool_compat.dart b/cw_zcash/lib/src/zkool_compat.dart new file mode 100644 index 000000000..60c296ee1 --- /dev/null +++ b/cw_zcash/lib/src/zkool_compat.dart @@ -0,0 +1,11 @@ +import 'dart:typed_data'; + +final zecBase = Uint8List(32); + +/// Whether NU6.3 (Ironwood) is active at the chain tip. Updated on balance refresh. +bool ironwoodActive = false; + +/// Pool bitmask for Ironwood (pool index 3). Orchard and Ironwood share the same address. +const ironwoodPoolMask = 8; + +int? get shieldedRecipientPools => ironwoodActive ? ironwoodPoolMask : null; diff --git a/cw_zcash/lib/src/zkooltx.dart b/cw_zcash/lib/src/zkooltx.dart new file mode 100644 index 000000000..6c7c64b3c --- /dev/null +++ b/cw_zcash/lib/src/zkooltx.dart @@ -0,0 +1,337 @@ +import 'dart:convert'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:cw_core/get_height_by_date_zec.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_zcash/src/util/hex.dart'; +import 'package:zkool/src/rust/api/account.dart' as zkool_account; + +enum TxType { + unknown, + selfTransfer, // 0 + receive, // 1 + sent, // 2 + unshield, // 4 + shield, // 8 + transparentSelfTransfer, // 12 +} + +enum NotePool { transparent, sapling, orchard, ironwood } + +class ZkoolTx { + ZkoolTx(final zkool_account.Tx tx, final zkool_account.TxAccount txAccount) + : this._tx = tx, + this._txAccount = txAccount; + final zkool_account.Tx _tx; + final zkool_account.TxAccount _txAccount; + + int get height => max( + _tx.height, + _txAccount.height, + ); // sometimes returns 0 - higher number would be correct in this case + + DateTime get time { + final ts = max(_tx.time, _txAccount.time) * 1000; + if (ts == 0) { + return ZcashHeight.getTimeByBlockHeight(height); + } + return DateTime.fromMillisecondsSinceEpoch(ts); + } + + String? get to => _txAccount.outputs.firstOrNull?.address; + Iterable get outputAddresses => + _txAccount.outputs.map((final o) => o.address).whereType(); + Iterable<({String address, int pool, BigInt value})> get outputsWithAddress => _txAccount.outputs + .where((final o) => o.address.isNotEmpty) + .map((final o) => (address: o.address, pool: o.pool, value: o.value)); + Iterable get spendPools => _txAccount.spends.map((final s) => s.pool); + Iterable get notePools => _txAccount.notes.map((final n) => n.pool); + String? get memo => _txAccount.memos.firstOrNull?.memo; + + BigInt get transparentOrSaplingSpent { + if (_txAccount.spends.isEmpty) { + return BigInt.zero; + } + return _txAccount.spends + .where( + (final s) => s.pool == NotePool.transparent.index || s.pool == NotePool.sapling.index, + ) + .fold(BigInt.zero, (final a, final s) => a + s.value); + } + + BigInt get orchardReceived { + final fromNotes = _txAccount.notes + .where((final n) => n.pool == NotePool.orchard.index) + .fold(BigInt.zero, (final a, final n) => a + n.value); + if (fromNotes > BigInt.zero) { + return fromNotes; + } + return _txAccount.outputs + .where((final o) => o.pool == NotePool.orchard.index) + .fold(BigInt.zero, (final a, final o) => a + o.value); + } + + BigInt get ironwoodReceived { + final fromNotes = _txAccount.notes + .where((final n) => n.pool == NotePool.ironwood.index) + .fold(BigInt.zero, (final a, final n) => a + n.value); + if (fromNotes > BigInt.zero) { + return fromNotes; + } + return _txAccount.outputs + .where((final o) => o.pool == NotePool.ironwood.index) + .fold(BigInt.zero, (final a, final o) => a + o.value); + } + + BigInt get orchardSpent => _txAccount.spends + .where((final s) => s.pool == NotePool.orchard.index) + .fold(BigInt.zero, (final a, final s) => a + s.value); + + String get txHash { + final reversed = Uint8List.fromList(_txAccount.txid.reversed.toList()); + final txId = uint8ListToHex(reversed); + return txId; + } + + BigInt get value => _value.abs(); + + BigInt get fee => value - _calcValue; + + BigInt get _value { + if (_tx.value != 0) { + return BigInt.from(_tx.value); + } + + return _calcValue; + } + + /////////// calc is short for calculator (calculated in this case) + BigInt get _calcValue { + final noteSum = _txAccount.notes.isEmpty + ? BigInt.from(0) + : _txAccount.notes.map((final note) => note.value).reduce((final a, final b) => a + b); + final outputSum = _txAccount.outputs.isEmpty + ? BigInt.from(0) + : _txAccount.outputs + .map((final output) => output.value) + .reduce((final a, final b) => a + b); + final spentSum = _txAccount.spends.isEmpty + ? BigInt.from(0) + : _txAccount.spends.map((final spent) => spent.value).reduce((final a, final b) => a + b); + + return noteSum - outputSum - spentSum; + } + + TransactionDirection get direction { + switch (type) { + case TxType.selfTransfer: + case TxType.receive: + case TxType.unshield: + case TxType.shield: + case TxType.transparentSelfTransfer: + return TransactionDirection.incoming; + case TxType.sent: + return TransactionDirection.outgoing; + case TxType.unknown: + return _value > BigInt.from(0) + ? TransactionDirection.incoming + : TransactionDirection.outgoing; + } + } + + TxType get type { + switch (_tx.tpe) { + case 0: + return TxType.selfTransfer; + case 1: + return TxType.receive; + case 2: + return TxType.sent; + case 4: + return TxType.unshield; + case 8: + return TxType.shield; + case 12: + return TxType.transparentSelfTransfer; + default: + return TxType.unknown; + } + } + + Map toJson() => { + "tx": { + "id": _tx.id, + "txid": base64.encode(_tx.txid), + "height": _tx.height, + "time": _tx.time, + "value": value.toInt(), + "tpe": _tx.tpe, + "zsaValue": _tx.zsaValue, + "assetDisplay": _tx.assetDisplay, + }, + "txaccount": { + "id": _txAccount.id, + "account": _txAccount.account, + "txid": base64.encode(_txAccount.txid), + "height": _txAccount.height, + "time": _txAccount.time, + "notes": _txAccount.notes + .map( + (final note) => { + "id": note.id, + "pool": note.pool, + "height": note.height, + "tx": note.tx, + "scope": note.scope, + "value": note.value.toString(), + "locked": note.locked, + }, + ) + .toList(), + "spends": _txAccount.spends + .map( + (final spend) => { + "id": spend.id, + "pool": spend.pool, + "height": spend.height, + "value": spend.value.toString(), + "assetDisplay": spend.assetDisplay, + }, + ) + .toList(), + "outputs": _txAccount.outputs + .map( + (final output) => { + "id": output.id, + "pool": output.pool, + "height": output.height, + "value": output.value.toString(), + "address": output.address, + }, + ) + .toList(), + "memos": _txAccount.memos + .map( + (final memo) => { + "note": memo.note, + "pool": memo.pool, + "output": memo.output, + "memo": memo.memo, + }, + ) + .toList(), + }, + }; + + static int _asInt(final dynamic value, {final int fallback = 0}) { + if (value == null) { + return fallback; + } + if (value is int) { + return value; + } + if (value is num) { + return value.toInt(); + } + return int.parse(value.toString()); + } + + static Uint8List _txidFromJson(final dynamic raw) { + return _bytesFromJson(raw, fieldName: "txid"); + } + + static Uint8List _bytesFromJson(final dynamic raw, {required final String fieldName}) { + if (raw == null) { + return Uint8List(0); + } + if (raw is Uint8List) { + return raw; + } + if (raw is List) { + return Uint8List.fromList(List.from(raw)); + } + if (raw is String) { + return base64.decode(raw); + } + throw FormatException("Invalid $fieldName in ZkoolTx json: $raw"); + } + + static List _listFromJson(final dynamic raw, final T Function(Map) parse) { + if (raw is! List) { + return const []; + } + return raw.map((final entry) => parse(Map.from(entry as Map))).toList(); + } + + static ZkoolTx fromJson(final Map json) { + final txJson = Map.from((json["tx"] as Map?)?.cast() ?? json); + final txAccountJson = Map.from( + (json["txaccount"] as Map?)?.cast() ?? json, + ); + + return ZkoolTx( + zkool_account.Tx( + id: _asInt(txJson["id"]), + txid: _txidFromJson(txJson["txid"]), + height: _asInt(txJson["height"]), + time: _asInt(txJson["time"]), + value: _asInt(txJson["value"]), + tpe: txJson["tpe"] == null ? null : _asInt(txJson["tpe"]), + zsaValue: _asInt(txJson["zsaValue"]), + assetDisplay: txJson["assetDisplay"] as String? ?? "", + isUserMemo: txJson["isUserMemo"] as bool? ?? false, + ), + zkool_account.TxAccount( + id: _asInt(txAccountJson["id"]), + account: _asInt(txAccountJson["account"]), + txid: _txidFromJson(txAccountJson["txid"]), + height: _asInt(txAccountJson["height"]), + time: _asInt(txAccountJson["time"]), + notes: _listFromJson( + txAccountJson["notes"], + (final a) => zkool_account.TxNote( + id: _asInt(a["id"]), + pool: _asInt(a["pool"]), + height: _asInt(a["height"]), + tx: _asInt(a["tx"]), + value: BigInt.parse(a["value"].toString()), + scope: _asInt(a["scope"]), + locked: a["locked"] as bool? ?? false, + assetDisplay: a["assetDisplay"] as String? ?? "", + ), + ), + spends: _listFromJson( + txAccountJson["spends"], + (final a) => zkool_account.TxSpend( + id: _asInt(a["id"]), + pool: _asInt(a["pool"]), + height: _asInt(a["height"]), + value: BigInt.parse(a["value"].toString()), + assetDisplay: a["assetDisplay"] as String? ?? "", + ), + ), + outputs: _listFromJson( + txAccountJson["outputs"], + (final a) => zkool_account.TxOutput( + id: _asInt(a["id"]), + pool: _asInt(a["pool"]), + height: _asInt(a["height"]), + value: BigInt.parse(a["value"].toString()), + address: a["address"] as String? ?? "", + ), + ), + memos: _listFromJson( + txAccountJson["memos"], + (final a) => zkool_account.TxMemo( + note: a["note"] == null ? null : _asInt(a["note"]), + pool: _asInt(a["pool"]), + output: a["output"] == null ? null : _asInt(a["output"]), + memo: a["memo"] as String?, + memoBytes: _bytesFromJson(a["memoBytes"], fieldName: "memoBytes"), + ), + ), + ), + ); + } +} diff --git a/cw_zcash/pubspec.yaml b/cw_zcash/pubspec.yaml index 72e6de2be..40fdd5019 100644 --- a/cw_zcash/pubspec.yaml +++ b/cw_zcash/pubspec.yaml @@ -9,8 +9,6 @@ environment: dependencies: flutter: sdk: flutter - warp_api: - path: ../scripts/zcash_lib/packages/warp_api_ffi cw_core: path: ../cw_core mobx: ^2.6.0 @@ -20,11 +18,13 @@ dependencies: path: any flat_buffers: any zkool: + # path: ../../zkool2 git: - url: https://github.com/cake-tech/zkool2 - ref: b1642f260da4fa00dd2f8d4dd689073d0b56960a + url: https://github.com/cake-tech/zkool2.git + ref: 6329d4aacb4568ee80ff84baba5ea93b07ae1013 path_provider: any + sqflite_common_ffi: any dependency_overrides: decimal: ^2.3.3 diff --git a/docs/SECURITY.md b/docs/SECURITY.md index e7c6baa02..b92493be5 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -1,12 +1,112 @@ # Security Policy -## Reporting a Vulnerability +Cake Wallet is developed by **Cake Labs LLC**. We take the security and privacy of +our users seriously and welcome reports from security researchers. -If you need to report a vulnerability, please either: +> **Cake Wallet is not affiliated with CAKE.com, Clockify, or security.cake.com.** +> Those services belong to unrelated companies. The only official vulnerability +> disclosure channels for Cake Wallet are the ones listed on this page and at +> https://cakewallet.com/security. -* Open a security advisory: https://github.com/cake-tech/cake_wallet/security/advisories/new -* Send an email to `dev@cakewallet.com` with details on the vulnerability +## Reporting a vulnerability -## Supported Versions +**Please do not open a public issue, pull request, or social-media post for a +security vulnerability.** Public disclosure before a fix is available puts users' +funds and privacy at risk. Use one of the private channels below and we will +coordinate a fix and disclosure with you. -As we don't maintain previous versions of the app, only the latest release for each platform is supported and any updates will bump the version number. +1. **GitHub private security advisory (preferred).** + [Report a vulnerability](https://github.com/cake-tech/cake_wallet/security/advisories/new). + This gives you a private, structured thread with the maintainers and is the + fastest way to reach us. +2. **Encrypted email.** Send details to **security@cakewallet.com**. For sensitive + reports, please encrypt with our PGP key: + - Key: https://cakewallet.com/.well-known/cakewallet-security.asc + - Fingerprint: `DC91 6520 0271 AC6A 0533 3D3C BFE7 D9A5 0E4D 3A0A` + +Both channels are monitored and automatically raise an alert in our internal +security channel, so reports will not be missed. + +### What to include + +- A clear description of the issue and its security impact. +- A working proof of concept is required. It must run directly against Cake + Wallet itself, using a release build or locally built version, and demonstrate + the reported behavior and security impact. Standalone Python code, + mathematical examples, or simulations that only reproduce the theory without + exercising Cake Wallet do not satisfy this requirement. +- Step-by-step reproduction instructions. +- Affected platforms (iOS, Android, macOS, Linux, Windows) and app version. +- Affected wallet types / chains, if applicable. +- Any relevant logs, addresses, or transaction IDs (for on-chain issues). + +If you used AI tooling to find or write up the report, please say so. + +### Communication expectations + +AI should not be used to generate comments when communicating with maintainers +and other contributors. Comments are expected to be written by humans. Comments +that are believed to be written by AI may be moderated. + +## Our commitment (safe harbor) + +We consider security research conducted in good faith under this policy to be +authorized. We will not pursue or support legal action against researchers who: + +- make a good-faith effort to avoid privacy violations, data destruction, and + interruption or degradation of our services; +- only interact with accounts they own or have explicit permission to access; and +- give us a reasonable opportunity to fix an issue before disclosing it publicly. + +If in doubt about whether an action is authorized, ask us first at +security@cakewallet.com. + +## What to expect + +- **Acknowledgement:** within **2 business days**. +- **Triage and initial assessment:** within **7 business days**. +- **Coordinated disclosure:** we aim to ship a fix and coordinate public + disclosure within **90 days** of the report. We will keep you updated on + progress and agree on a disclosure date with you. +- **Credit:** with your permission, we are happy to publicly credit you for the + report once a fix is released. + +## Scope + +**In scope:** the Cake Wallet and Monero.com applications and the code in this +repository and its sibling `cake-tech` repositories — anything that could lead to +loss of funds, exposure of keys or seeds, a privacy leak, or a +failed/incorrect transaction. + +**Out of scope:** issues in third-party services, exchange/swap providers, or +nodes we do not operate; reports generated solely by automated scanners without a +demonstrated impact; low-severity or informational issues on our marketing and +landing websites (for example reflected or self-XSS, missing security headers, +clickjacking on pages with no sensitive actions, or SPF/DMARC and cookie-flag +nitpicks) that do not affect the app or user funds; and social-engineering or +physical attacks. + +## Rewards + +At our **sole discretion**, we may offer a reward for a valid report. To be +eligible, a report must: + +- be submitted **privately** through one of the channels above (a GitHub private + security advisory or `security@cakewallet.com`) — anything disclosed publicly or + sent through other channels is not eligible; and +- identify a genuine vulnerability with real impact on users — typically loss of + funds, exposure of keys or seeds, a privacy leak, or a failed or incorrect + transaction. + +Trivial or low-impact findings are **not** eligible — for example, reflected XSS +or other low-severity issues on our marketing websites, missing security headers, +hardening or best-practice suggestions, automated-scanner output without a working +proof of concept, or already-known issues. There is no fixed bounty and no +guaranteed payout; whether a report qualifies, and any amount, are determined +solely by Cake Labs LLC. + +## Supported versions + +We do not maintain previous releases. Only the **latest release for each platform** +is supported; security fixes are delivered in new versions. Please keep Cake Wallet +up to date. diff --git a/ios/Podfile b/ios/Podfile index cf24cb605..60e07e6b4 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -43,7 +43,7 @@ post_install do |installer| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0' config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 29a2ad974..b7e67acbf 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -149,8 +149,6 @@ PODS: - Flutter - wakelock_plus (0.0.1): - Flutter - - warp_api (0.0.1): - - Flutter - YttriumWrapper (0.8.35) DEPENDENCIES: @@ -196,7 +194,6 @@ DEPENDENCIES: - universal_ble (from `.symlinks/plugins/universal_ble/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) - - warp_api (from `.symlinks/plugins/warp_api/ios`) SPEC REPOS: https://github.com/CocoaPods/Specs.git: @@ -292,61 +289,58 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/url_launcher_ios/ios" wakelock_plus: :path: ".symlinks/plugins/wakelock_plus/ios" - warp_api: - :path: ".symlinks/plugins/warp_api/ios" SPEC CHECKSUMS: bitbox_flutter: 9505732798041c413152669751beeaecc5fe400f breez_sdk_spark_flutter: ed3b6709b8ce9b40309d8728640eade5b19e8a0d connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd CryptoSwift: e64e11850ede528a02a0f3e768cec8e9d92ecb90 - cw_decred: 9c0e1df74745b51a1289ec5e91fb9e24b68fa14a - cw_mweb: 22cd01dfb8ad2d39b15332006f22046aaa8352a3 - device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 - device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 - devicelocale: 35ba84dc7f45f527c3001535d8c8d104edd5d926 + cw_decred: a02cf30175a46971c1e2fa22c48407534541edc6 + cw_mweb: 3aea2fb35b2bd04d8b2d21b83216f3b8fb768d85 + device_display_brightness: 04374ebd653619292c1d996f00f42877ea19f17f + device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89 + devicelocale: bd64aa714485a8afdaded0892c1e7d5b7f680cf8 DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 dnssec_proof: d461cac7bd3301eb7447f87936745a0c1ae0a67e - fast_scanner: 44c00940355a51258cd6c2085734193cd23d95bc - file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 + fast_scanner: 2cb1ad3e69e645e9980fb4961396ce5804caa3e3 + file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 - flutter_local_authentication: 1172a4dd88f6306dadce067454e2c4caf07977bb - flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f - flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83 - flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29 - flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be - fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f - image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b - in_app_review: 436034b18594851a7328d7f1c2ed5ec235b79cfc - integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 - local_auth_darwin: 63c73d6d28cc3e239be2b6aa460ea6e317cd5100 + flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 + flutter_local_authentication: 989278c681612f1ee0e36019e149137f114b9d7f + flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb + flutter_mailer: 3a8cd4f36c960fb04528d5471097270c19fec1c4 + flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf + flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418 + fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 + image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 + in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937 + integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e + local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 - package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 payjoin_flutter: d9d4c8aa16bd5dfedb9b21d0edc8199e0187d96e permission_handler_apple: 92d754bbaa7361d436db2d6c3c1c2a0fdcec462e - quick_actions_ios: 4b07fb49d8d8f3518d7565fbb7a91014067a7d82 + quick_actions_ios: 500fcc11711d9f646739093395c4ae8eec25f779 reown_yttrium: cee334ade64725b1d83f7b34c706a6aae2696d58 rive_common: dd421daaf9ae69f0125aa761dd96abd278399952 rlz: 3f4022361f540a5c26d9d2474fea1d2455b69d8d SDWebImage: 9f177d83116802728e122410fb25ad88f5c7608a - sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 - share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f - shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6 - sp_scanner: eaa617fa827396b967116b7f1f43549ca62e9a12 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + sensitive_clipboard: 161e9abc3d56b3131309d8a321eb4690a803c16b + share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + sp_scanner: b1bc9321690980bdb44bba7ec85d5543e716d1b5 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b - sqlite3_flutter_libs: 86f82662868ee26ff3451f73cac9c5fc2a1f57fa + sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 torch_dart: f4620705d10f05492fab047f2fa1c3a600e7d17d uni_links: ed8c961e47ed9ce42b6d91e1de8049e38a4b3152 - universal_ble: 45519b2aeafe62761e2c6309f8927edb5288b914 - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + universal_ble: a322ebabee64f0ec27a313e89a5dd6967f37a60f + url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 - warp_api: 997d8fb3e777669bc6260d0c12e56cd40b9db1d1 YttriumWrapper: 31e937fe9fbe0f1314d2ca6be9ce9b379a059966 -PODFILE CHECKSUM: 5296465b1c6d14d506230356756826012f65d97a +PODFILE CHECKSUM: b08d7f4c8d9c64f5648599a30caea878b0f7616a COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 2c2584e8e..e9d4c224f 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -26,7 +26,6 @@ A1B4A70C9CFA13AB71662216 /* LnurlPay.swift in Resources */ = {isa = PBXBuildFile; fileRef = 7D3364C03978A8A74B6D586E /* LnurlPay.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; A3D5E17CC53DF13FA740DEFA /* RedeemSwap.swift in Resources */ = {isa = PBXBuildFile; fileRef = 9D2F2C9F2555316C95EE7EA3 /* RedeemSwap.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; B6C6E59403ACDE44724C12F4 /* ServiceConfig.swift in Resources */ = {isa = PBXBuildFile; fileRef = B3D5E78267F5F18D882FDC3B /* ServiceConfig.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; - CE3D5B792F142305004D767D /* WarpApiFFI.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE3D5B782F142305004D767D /* WarpApiFFI.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CE918BF82D533ECE007F186E /* MoneroWallet.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE918BF72D533ECE007F186E /* MoneroWallet.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CE918BFA2D533ED4007F186E /* WowneroWallet.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE918BF92D533ED4007F186E /* WowneroWallet.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CE918BFC2D533ED8007F186E /* ZanoWallet.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE918BFB2D533ED8007F186E /* ZanoWallet.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -48,7 +47,6 @@ CE918BF82D533ECE007F186E /* MoneroWallet.xcframework in CopyFiles */, CE918BFA2D533ED4007F186E /* WowneroWallet.xcframework in CopyFiles */, CE918BFC2D533ED8007F186E /* ZanoWallet.xcframework in CopyFiles */, - CE3D5B792F142305004D767D /* WarpApiFFI.xcframework in CopyFiles */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -89,7 +87,6 @@ B3D5E78267F5F18D882FDC3B /* ServiceConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ServiceConfig.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/ServiceConfig.swift"; sourceTree = ""; }; B478EFCC3C0760ED97FFE561 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C58D93382C00FAC6004BCF69 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; }; - CE3D5B782F142305004D767D /* WarpApiFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = WarpApiFFI.xcframework; path = ../scripts/zcash_lib/WarpApiFFI.xcframework; sourceTree = ""; }; CE918BF72D533ECE007F186E /* MoneroWallet.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = MoneroWallet.xcframework; sourceTree = ""; }; CE918BF92D533ED4007F186E /* WowneroWallet.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = WowneroWallet.xcframework; sourceTree = ""; }; CE918BFB2D533ED8007F186E /* ZanoWallet.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = ZanoWallet.xcframework; sourceTree = ""; }; @@ -170,7 +167,6 @@ 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( - CE3D5B782F142305004D767D /* WarpApiFFI.xcframework */, 0C44A7182518EF4A00B570ED /* CakeWallet */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, @@ -483,7 +479,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -513,7 +509,7 @@ "$(PROJECT_DIR)", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -582,7 +578,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -632,7 +628,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -664,7 +660,7 @@ "$(PROJECT_DIR)", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -708,7 +704,7 @@ "$(PROJECT_DIR)", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 48dbfda84..ae1018319 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -687,28 +687,20 @@ class CWBitcoin extends Bitcoin { } List updateOutputs(PendingTransaction pendingTransaction, List outputs) { - if (pendingTransaction is PendingLightningTransaction) return outputs; - - final pendingTx = pendingTransaction as PendingBitcoinTransaction; - - if (!pendingTx.hasSilentPayment) { + if (pendingTransaction is! PendingBitcoinTransaction || !pendingTransaction.hasSilentPayment) { return outputs; } - final updatedOutputs = outputs.map((output) { - try { - final pendingOut = pendingTx.outputs[outputs.indexOf(output)]; - final updatedOutput = output; + final stealthAddresses = pendingTransaction.stealthAddresses; + if (stealthAddresses.length != outputs.length) { + printV("well, that shouldn't happen"); + return outputs; + } - updatedOutput.stealthAddress = P2trAddress.fromScriptPubkey(script: pendingOut.scriptPubKey) - .toAddress(BitcoinNetwork.mainnet); - return updatedOutput; - } catch (_) {} - - return output; - }).toList(); - - return updatedOutputs; + for (var i = 0; i < outputs.length; i++) { + outputs[i].stealthAddress = stealthAddresses[i]; + } + return outputs; } @override diff --git a/lib/buy/dfx/dfx_buy_provider.dart b/lib/buy/dfx/dfx_buy_provider.dart index 104d66325..282ed7698 100644 --- a/lib/buy/dfx/dfx_buy_provider.dart +++ b/lib/buy/dfx/dfx_buy_provider.dart @@ -336,14 +336,23 @@ class DFXBuyProvider extends BuyProvider { String? countryCode}) async { if (wallet.isHardwareWallet) { if (!hardwareWalletVM!.isConnected(wallet.walletInfo.type)) { - await Navigator.of(context).pushNamed(Routes.connectDevices, - arguments: ConnectDevicePageParams( - walletType: wallet.walletInfo.type, - hardwareWalletType: wallet.walletInfo.hardwareWalletType!, - onConnectDevice: (context, hwwVM) { - hwwVM.initWallet(wallet); - Navigator.of(context).pop(); - })); + await Navigator.of(context).pushNamed( + Routes.connectDevices, + arguments: ConnectDevicePageParams( + walletType: wallet.walletInfo.type, + hardwareWalletType: wallet.walletInfo.hardwareWalletType!, + onConnectDevice: (context, hwwVM) { + hwwVM.initWallet(wallet); + Navigator.of(context).pop(); + }, + isReconnect: false, + ), + ); + + // Recheck to handle tap-backs + if (!hardwareWalletVM!.isConnected(wallet.walletInfo.type)) { + return; + } } else { hardwareWalletVM!.initWallet(wallet); } diff --git a/lib/buy/robinhood/robinhood_buy_provider.dart b/lib/buy/robinhood/robinhood_buy_provider.dart index f56fc8c1b..651dc6b84 100644 --- a/lib/buy/robinhood/robinhood_buy_provider.dart +++ b/lib/buy/robinhood/robinhood_buy_provider.dart @@ -174,14 +174,21 @@ class RobinhoodBuyProvider extends BuyProvider { String? countryCode}) async { if (wallet.isHardwareWallet) { if (!hardwareWalletVM!.isConnected(wallet.walletInfo.type)) { - await Navigator.of(context).pushNamed(Routes.connectDevices, - arguments: ConnectDevicePageParams( - walletType: wallet.walletInfo.type, - hardwareWalletType: wallet.walletInfo.hardwareWalletType!, - onConnectDevice: (context, hwwVM) { - hwwVM.initWallet(wallet); - Navigator.of(context).pop(); - })); + await Navigator.of(context).pushNamed( + Routes.connectDevices, + arguments: ConnectDevicePageParams( + walletType: wallet.walletInfo.type, + hardwareWalletType: wallet.walletInfo.hardwareWalletType!, + onConnectDevice: (context, hwwVM) { + hwwVM.initWallet(wallet); + Navigator.of(context).pop(); + }, + isReconnect: false, + ), + ); + if (!hardwareWalletVM!.isConnected(wallet.walletInfo.type)) { + return; + } } else { hardwareWalletVM!.initWallet(wallet); } diff --git a/lib/cake_pay/src/widgets/rounded_overlay_cards_widget.dart b/lib/cake_pay/src/widgets/rounded_overlay_cards_widget.dart index 917b1ecbb..484792bf5 100644 --- a/lib/cake_pay/src/widgets/rounded_overlay_cards_widget.dart +++ b/lib/cake_pay/src/widgets/rounded_overlay_cards_widget.dart @@ -15,7 +15,7 @@ class RoundedOverlayCards extends StatelessWidget { return ClipRRect( borderRadius: - BorderRadius.only(bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)), + BorderRadius.only(bottomLeft: Radius.circular(25), bottomRight: Radius.circular(25)), child: Container( decoration: BoxDecoration( borderRadius: const BorderRadius.only( @@ -30,8 +30,8 @@ class RoundedOverlayCards extends StatelessWidget { Flexible( child: ClipRRect( borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(25.0), - bottomRight: Radius.circular(25.0), + bottomLeft: Radius.circular(25), + bottomRight: Radius.circular(25), ), child: Container( decoration: BoxDecoration( diff --git a/lib/core/address_resolver/yat/yat_address_provider.dart b/lib/core/address_resolver/yat/yat_address_provider.dart index d8ee3454b..73363fa1a 100644 --- a/lib/core/address_resolver/yat/yat_address_provider.dart +++ b/lib/core/address_resolver/yat/yat_address_provider.dart @@ -20,7 +20,7 @@ class YatAddressProvider extends AddressLookupProvider { bool canHandle(String q) => q.hasOnlyEmojis; // Yat handle example: 🐶🐾 @override - bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsWellKnown; + bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsYatService; @override Future> resolve({ diff --git a/lib/core/address_resolver/zcash/zcash_me_address_provider.dart b/lib/core/address_resolver/zcash/zcash_me_address_provider.dart index 6dd0968d9..3836ee46f 100644 --- a/lib/core/address_resolver/zcash/zcash_me_address_provider.dart +++ b/lib/core/address_resolver/zcash/zcash_me_address_provider.dart @@ -15,7 +15,7 @@ class ZcashMeAddressProvider extends AddressLookupProvider { List get supportedCurrencies => [CryptoCurrency.zec]; @override - bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsZcashNames; + bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsZcashAddress; @override bool canHandle(String query) { diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index e423dda2b..e5905bb82 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -3,6 +3,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/core/validator.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/zano/zano.dart'; +import 'package:cake_wallet/zcash/zcash_network_type.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; @@ -10,8 +11,11 @@ const BEFORE_REGEX = '(^|\\s)'; const AFTER_REGEX = '(\$|\\s)'; class AddressValidator extends TextValidator { - AddressValidator({required CryptoCurrency type, bool isTestnet = false}) - : super( + AddressValidator({ + required CryptoCurrency type, + bool isTestnet = false, + String? network, + }) : super( errorMessage: S.current.error_text_address, useAdditionalValidation: [CryptoCurrency.btc, CryptoCurrency.ltc].contains(type) ? (String txt) { @@ -32,7 +36,7 @@ class AddressValidator extends TextValidator { : type == CryptoCurrency.zano ? zano?.validateAddress : null, - pattern: getPattern(type, isTestnet: isTestnet), + pattern: getPattern(type, isTestnet: isTestnet, network: network), length: getLength(type), ); @@ -61,7 +65,7 @@ class AddressValidator extends TextValidator { CryptoCurrency.btcln, ]; - static String getPattern(CryptoCurrency type, {bool isTestnet = false}) { + static String? getPattern(CryptoCurrency type, {bool isTestnet = false, String? network}) { var pattern = ""; if (type is Erc20Token) { pattern = '0x[0-9a-zA-Z]+'; @@ -170,6 +174,9 @@ class AddressValidator extends TextValidator { case CryptoCurrency.hbar: pattern = '[0-9a-zA-Z.]+'; case CryptoCurrency.zec: + if (ZcashNetworkType.isDevNetwork(network)) { + return null; + } pattern = '(?:' 't1[0-9A-Za-z]{33}' '|t3[0-9A-Za-z]{33}' diff --git a/lib/core/background_sync.dart b/lib/core/background_sync.dart index a63618d0c..c2e270575 100644 --- a/lib/core/background_sync.dart +++ b/lib/core/background_sync.dart @@ -210,7 +210,11 @@ class BackgroundSync { } await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name), tx.date.add(Duration(minutes: 1)).toIso8601String()); - final action = tx.direction == TransactionDirection.incoming ? "Received" : "Sent"; + final action = tx.additionalInfo['isAutoShield'] == true + ? "Autoshield" + : tx.direction == TransactionDirection.incoming + ? "Received" + : "Sent"; if (sharedPreferences.getBool(PreferencesKey.backgroundSyncNotificationsEnabled) ?? false) { await showNotification( "$action ${wallet.currency.fullName} in ${wallet.name}", "${tx.amount.toString()}"); diff --git a/lib/core/lightning_invoice_service.dart b/lib/core/lightning_invoice_service.dart index 995c62d6b..8b29f6c5d 100644 --- a/lib/core/lightning_invoice_service.dart +++ b/lib/core/lightning_invoice_service.dart @@ -1,6 +1,6 @@ -import 'dart:convert'; +import "dart:convert"; -import 'package:cw_core/utils/proxy_wrapper.dart'; +import "package:cw_core/utils/proxy_wrapper.dart"; Future getBolt11FromLightingAddress(String lightningAddress, {int amount = 0}) async { try { @@ -31,28 +31,34 @@ Uri getURlOfLightningAddress(String lightningAddress) { } class _LNURLPResponseDTO { + const _LNURLPResponseDTO( + this.callback, + this.maxSendable, + this.minSendable, + this.tag, + this.metadata, + ); + + factory _LNURLPResponseDTO.fromJson(Map map) => _LNURLPResponseDTO( + map["callback"] as String, + map["maxSendable"] as int, + map["minSendable"] as int, + map["tag"] as String, + map["metadata"] as String, + ); + final String callback; final int maxSendable; final int minSendable; final String tag; final String metadata; - - const _LNURLPResponseDTO( - this.callback, this.maxSendable, this.minSendable, this.tag, this.metadata); - - static _LNURLPResponseDTO fromJson(Map map) => _LNURLPResponseDTO( - map["callback"] as String, - map["maxSendable"] as int, - map["minSendable"] as int, - map["tag"] as String, - map["metadata"] as String); } class _LNURLPCallbackResponseDTO { - final String pr; - const _LNURLPCallbackResponseDTO(this.pr); - static _LNURLPCallbackResponseDTO fromJson(Map map) => + factory _LNURLPCallbackResponseDTO.fromJson(Map map) => _LNURLPCallbackResponseDTO(map["pr"] as String); + + final String pr; } diff --git a/lib/di.dart b/lib/di.dart index 9f6b100c8..173b119f5 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -436,7 +436,9 @@ Future setup({ () => TrezorConnect("cakewallet://trezor_connect", appName: "Cake Wallet"), ); - getIt.registerLazySingleton(() => TrezorConnectViewModel(getIt())); + getIt.registerLazySingleton( + () => TrezorConnectViewModel(getIt(), getIt()), + ); getIt.registerFactory(() => KeyService(getIt.get())); diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 5a673b58d..3934b6330 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -10,7 +10,6 @@ import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/entities/fs_migration.dart'; import 'package:cake_wallet/entities/haven_seed_store.dart'; -import 'package:cake_wallet/entities/node_list.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/secret_store_key.dart'; import 'package:cake_wallet/monero/monero.dart'; @@ -46,7 +45,7 @@ const cakeWalletBitcoinCashDefaultNodeUri = 'bitcoincash.stackwallet.com:50002'; const nanoDefaultNodeUri = 'rpc.nano.to'; const nanoDefaultPowNodeUri = 'rpc.nano.to'; const solanaDefaultNodeUri = 'solana-mainnet.core.chainstack.com'; -const tronDefaultNodeUri = 'api.trongrid.io'; +const tronDefaultNodeUri = 'trx.nownodes.io'; const newCakeWalletBitcoinUri = 'btc-electrum.cakewallet.com:50002'; const wowneroDefaultNodeUri = 'node3.monerodevs.org:34568'; const zanoDefaultNodeUri = '37.27.100.59:10500'; @@ -56,7 +55,7 @@ const dogecoinDefaultNodeUri = 'dogecoin.stackwallet.com:50022'; const baseDefaultNodeUri = 'base-rpc.publicnode.com'; const arbitrumDefaultNodeUri = 'arbitrum.nownodes.io'; const bscDefaultNodeUri = 'bsc-dataseed.bnbchain.org'; -const zcashDefaultNodeUri = 'zec-node.cakewallet.com:443'; +const zcashDefaultNodeUri = 'zec.rocks:443'; Future defaultSettingsMigration( {required int version, @@ -607,6 +606,38 @@ Future defaultSettingsMigration( enabled: true, ); break; + case 68: + await _changeDefaultNode( + sharedPreferences: sharedPreferences, + type: WalletType.tron, + newDefaultUri: tronDefaultNodeUri, + currentNodePreferenceKey: PreferencesKey.currentTronNodeIdKey, + useSSL: true, + oldUri: [ + 'tron-rpc.publicnode.com:443', + 'api.trongrid.io', + ], + ); + await _changeDefaultNode( + sharedPreferences: sharedPreferences, + type: WalletType.monero, + newDefaultUri: newCakeWalletMoneroUri, + currentNodePreferenceKey: PreferencesKey.currentNodeIdKey, + useSSL: true, + trusted: true, + oldUri: ['nodes.hashvault.pro:18081'], + ); + break; + case 69: + _changeExchangeProviderAvailability( + sharedPreferences, + providerName: "Exolix", + enabled: false, + ); + break; + case 70: + await _addTbbTokenToExistingSolanaWallets(); + break; default: break; } @@ -663,7 +694,7 @@ Future _changeDefaultNode({ } else { final currentNode = nodes.firstWhereOrNull((node) => node.id == currentNodeId); shouldReplace = - currentNode == null || (oldUri?.any((e) => currentNode!.uriRaw.contains(e)) ?? true); + currentNode == null || (oldUri?.any((e) => currentNode.uriRaw.contains(e)) ?? true); } if (shouldReplace) { @@ -1297,3 +1328,40 @@ Future _addXaut0TokenToExistingSolanaWallets() async { printV('Error in XAUT0 migration: $e'); } } + +Future _addTbbTokenToExistingSolanaWallets() async { + try { + final tbbToken = SPLToken( + name: "The Bitcoin Bull", + symbol: "TBB", + mintAddress: "42cXQvAAr7hcPBPWAS4ocVtDyeJ4Fa6gRR2uG4gppump", + decimal: 6, + mint: "tbb", + enabled: false, + iconPath: "assets/images/tbb_icon.png", + ); + + final allWallets = await WalletInfo.getAll(); + + final solanaWallets = allWallets.where((wallet) => wallet.type == WalletType.solana).toList(); + + for (final walletInfo in solanaWallets) { + final sanitizedName = walletInfo.name.replaceAll(" ", "_"); + final boxName = "${sanitizedName}_${SPLToken.boxName}"; + + Box tokenBox; + if (CakeHive.isBoxOpen(boxName)) { + tokenBox = CakeHive.box(boxName); + } else { + tokenBox = await CakeHive.openBox(boxName); + } + + final tbbAddress = tbbToken.mintAddress; + if (!tokenBox.containsKey(tbbAddress)) { + await tokenBox.put(tbbAddress, tbbToken); + } + } + } catch (e) { + printV("Error in TBB migration: $e"); + } +} diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index b12c58265..63c94bdb7 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -145,4 +145,5 @@ class PreferencesKey { static const backgroundImage = 'background_image'; static const mwebAdDismissed = "mweb_ad_dismissed"; static const balanceHideCounter = "balance_hide_counter"; + static const zcashMigrationModalViewed = "zcash_migration_modal_viewed"; } diff --git a/lib/entities/qr_scanner.dart b/lib/entities/qr_scanner.dart index ad076cd8a..296d3c5e4 100644 --- a/lib/entities/qr_scanner.dart +++ b/lib/entities/qr_scanner.dart @@ -69,7 +69,7 @@ class _BarcodeScannerSimpleState extends State { return AlertWithOneAction( alertTitle: S.of(context).error, alertContent: S.of(context).error_dialog_content, - buttonText: 'ok', + buttonText: S.of(context).ok, buttonAction: () { Navigator.of(context).pop(); }, diff --git a/lib/exchange/provider/changenow_exchange_provider.dart b/lib/exchange/provider/changenow_exchange_provider.dart index 15913b29e..e6c94d559 100644 --- a/lib/exchange/provider/changenow_exchange_provider.dart +++ b/lib/exchange/provider/changenow_exchange_provider.dart @@ -325,6 +325,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider { return 'btc'; case CryptoCurrency.arb: return 'arbitrum'; + case CryptoCurrency.nano: + return 'nano'; default: return currency.tag != null ? _normalizeTag(currency.tag!) : currency.title.toLowerCase(); } @@ -337,6 +339,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider { switch (currency) { case CryptoCurrency.zec: return 'zec'; + case CryptoCurrency.maticpoly: + return 'matic'; default: return currency.title.toLowerCase(); } @@ -344,7 +348,7 @@ class ChangeNowExchangeProvider extends ExchangeProvider { String _normalizeTag(String tag) { switch (tag) { - case 'POLY': + case 'POL': return 'matic'; case 'LN': return 'lightning'; diff --git a/lib/exchange/provider/trocador_exchange_provider.dart b/lib/exchange/provider/trocador_exchange_provider.dart index a9d977dd6..8b027b323 100644 --- a/lib/exchange/provider/trocador_exchange_provider.dart +++ b/lib/exchange/provider/trocador_exchange_provider.dart @@ -539,7 +539,7 @@ class TrocadorExchangeProvider extends ExchangeProvider { } Future _getUri(String path, Map queryParams) async { - final uri = Uri.http(onionApiAuthority, path, queryParams); + final uri = Uri.https(onionApiAuthority, path, queryParams); if (useTorOnly) return uri; diff --git a/lib/main.dart b/lib/main.dart index b6f9714f6..2f248858e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -23,6 +23,7 @@ import 'package:cake_wallet/exchange/exchange_template.dart'; import 'package:cake_wallet/exchange/trade_legacy.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/locales/locale.dart'; +import "package:cake_wallet/new-ui/widgets/money/money_settings_provider.dart"; import 'package:cake_wallet/order/order.dart'; import 'package:cake_wallet/reactions/bootstrap.dart'; import 'package:cake_wallet/router.dart' as Router; @@ -315,7 +316,7 @@ Future initializeAppConfigs({bool loadWallet = true}) async { payjoinSessionSource: payjoinSessionSource, anonpayInvoiceInfo: anonpayInvoiceInfo, havenSeedStore: havenSeedStore, - initialMigrationVersion: 67, + initialMigrationVersion: 70, ); } @@ -425,8 +426,9 @@ class AppState extends State with SingleTickerProviderStateMixin { navigatorKey: navigatorKey, debugShowCheckedModeBanner: false, builder: (context, child) => MediaQuery( - data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling), - child: child!), + data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling), + child: MoneySettingsProvider(settingsStore: settingsStore, child: child!), + ), theme: theme, darkTheme: darkTheme, themeMode: themeMode, diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index 1551b95de..d1e421d30 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -236,12 +236,14 @@ class CWMonero extends Monero { required String password, required int height, required HardwareWalletService hardwareWalletService, + required String? passphrase, }) => MoneroRestoreWalletFromHardwareCredentials( name: name, password: password, height: height, hardwareWalletService: hardwareWalletService, + passphrase: passphrase, ); @override @@ -418,6 +420,12 @@ class CWMonero extends Monero { return moneroWallet.needExportOutputs(amount); } + @override + bool hasUnknownKeyImages(Object wallet) { + final moneroWallet = wallet as MoneroWallet; + return moneroWallet.hasUnknownKeyImages(); + } + @override void monerocCheck() => checkIfMoneroCIsFine(); diff --git a/lib/new-ui/new_dashboard.dart b/lib/new-ui/new_dashboard.dart index a285e9cac..9c9294648 100644 --- a/lib/new-ui/new_dashboard.dart +++ b/lib/new-ui/new_dashboard.dart @@ -90,14 +90,18 @@ class _NewDashboardState extends State { ), ), ), - SafeArea( - bottom: !(Platform.isIOS), - child: SizedBox( - width: double.infinity, - height: NewMainNavBar.barHeight + NewMainNavBar.barBottomPadding, - child: AbsorbPointer( - absorbing: true, - child: Container(color: Colors.transparent), + // Invisible pointer-absorbing strip behind the nav bar: it must not + // be reachable by screen-reader traversal either. + ExcludeSemantics( + child: SafeArea( + bottom: !(Platform.isIOS), + child: SizedBox( + width: double.infinity, + height: NewMainNavBar.barHeight + NewMainNavBar.barBottomPadding, + child: AbsorbPointer( + absorbing: true, + child: Container(color: Colors.transparent), + ), ), ), ), diff --git a/lib/new-ui/pages/about_page.dart b/lib/new-ui/pages/about_page.dart index 533184eac..766ae2399 100644 --- a/lib/new-ui/pages/about_page.dart +++ b/lib/new-ui/pages/about_page.dart @@ -56,6 +56,7 @@ class _AboutPageState extends State { topBar: ModalTopBar( title: S.of(context).about, leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: Navigator.of(context).pop, ), content: Container( diff --git a/lib/new-ui/pages/account_customizer.dart b/lib/new-ui/pages/account_customizer.dart index 15aa1f0c5..ff9d6f59c 100644 --- a/lib/new-ui/pages/account_customizer.dart +++ b/lib/new-ui/pages/account_customizer.dart @@ -128,8 +128,10 @@ class _AccountCustomizerState extends State { ModalTopBar( title: S.of(context).wallet_accounts, leadingIcon: Icon(Icons.close), + leadingSemanticLabel: S.of(context).close, onLeadingPressed: Navigator.of(context).maybePop, trailingIcon: Icon(Icons.refresh), + trailingSemanticLabel: S.of(context).reset, onTrailingPressed: showResetDialog, ), Padding( @@ -172,17 +174,29 @@ class _AccountCustomizerState extends State { itemCount: _items.length, itemBuilder: (BuildContext context, int index) { final card = _items[index].card; + // The stack is ordered bottom to top, so the last item + // is the account currently in front — the selected one. + final selectedItemIndex = _items.length - 1; return Container( key: ValueKey(index), - child: GestureDetector( - onTap: () { - reorder(index, _items.length); - }, - child: Align( - alignment: Alignment.topCenter, - heightFactor: _kStackVisibleFactor, - child: card, + // One labeled, selectable node per account; the card's own + // texts stay reachable underneath it. + child: Semantics( + button: true, + selected: selectedItemIndex == index, + label: _items[index].accountListItem.label, + onTap: () => reorder(index, _items.length), + child: GestureDetector( + excludeFromSemantics: true, + onTap: () { + reorder(index, _items.length); + }, + child: Align( + alignment: Alignment.topCenter, + heightFactor: _kStackVisibleFactor, + child: card, + ), ), ), ); @@ -197,31 +211,36 @@ class _AccountCustomizerState extends State { padding: const EdgeInsets.symmetric(horizontal: 24.0), child: Material( color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(999999), - onTap: _showAddAccountModal, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.circular(999999)), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 18.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 8, - children: [ - Icon( - Icons.add, - size: 28, - color: Theme.of(context).colorScheme.primary, - ), - Text( - S.of(context).add_account, - style: TextStyle( + child: MergeSemantics( + child: Semantics( + button: true, + child: InkWell( + borderRadius: BorderRadius.circular(999999), + onTap: _showAddAccountModal, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(999999)), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 18.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 8, + children: [ + Icon( + Icons.add, + size: 28, color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.w500), - ) - ], + ), + Text( + S.of(context).add_account, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.w500), + ) + ], + ), + ), ), ), ), @@ -404,6 +423,8 @@ class _AccountCreationModalState extends State { final TextEditingController _controller = TextEditingController(); bool _loading = false; + Future _generateAccountName() async => _controller.text = await generateName(); + @override Widget build(BuildContext context) { return Container( @@ -439,18 +460,23 @@ class _AccountCreationModalState extends State { ), Padding( padding: const EdgeInsets.all(12.0), - child: GestureDetector( - onTap: () async { - _controller.text = await generateName(); - }, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHigh, - borderRadius: BorderRadius.circular(5)), - child: CakeImageWidget( - imageUrl: "assets/new-ui/randomize.svg", - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.primary, BlendMode.srcIn), + child: Semantics( + button: true, + label: S.of(context).generate_name, + onTap: _generateAccountName, + child: ExcludeSemantics( + child: GestureDetector( + onTap: _generateAccountName, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(5)), + child: CakeImageWidget( + imageUrl: "assets/new-ui/randomize.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, BlendMode.srcIn), + ), + ), ), ), ), diff --git a/lib/new-ui/pages/addresses_page.dart b/lib/new-ui/pages/addresses_page.dart index 62075e23b..ee02aa88f 100644 --- a/lib/new-ui/pages/addresses_page.dart +++ b/lib/new-ui/pages/addresses_page.dart @@ -20,6 +20,7 @@ import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/card_design.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/utils/list_item.dart'; @@ -105,6 +106,7 @@ class _NewAddressesPageState extends State { ModalTopBar( title: widget.showHidden ? S.of(context).hidden_addresses : S.of(context).addresses, leadingIcon: Icon(Icons.arrow_back), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: Navigator.of(context).pop, onTrailingPressed: () {}), Expanded( @@ -212,24 +214,31 @@ class AddressSearchBox extends StatelessWidget { border: Border.all( color: Theme.of(context).colorScheme.surfaceContainerHighest, width: 1), borderRadius: BorderRadius.circular(99999)), - child: TextField( - controller: controller, - decoration: InputDecoration( - hintText: S.of(context).search, - hintStyle: TextStyle( - fontWeight: FontWeight.w600, + // The hint names the field only while it is empty, so the label is + // supplied once there is text to keep exactly one announcement. + child: MergeSemantics( + child: Semantics( + label: controller.text.isEmpty ? null : S.of(context).search, + child: TextField( + controller: controller, + decoration: InputDecoration( + hintText: S.of(context).search, + hintStyle: TextStyle( + fontWeight: FontWeight.w600, + ), + prefixIcon: ExcludeSemantics(child: Icon(Icons.search)), + filled: false, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(99999), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(99999), + borderSide: BorderSide.none, + ), + contentPadding: EdgeInsets.symmetric(vertical: 8), + ), ), - prefixIcon: Icon(Icons.search), - filled: false, - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(99999), - borderSide: BorderSide.none, - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(99999), - borderSide: BorderSide.none, - ), - contentPadding: EdgeInsets.symmetric(vertical: 8), ), ), ), @@ -333,116 +342,139 @@ class AddressRow extends StatelessWidget { @override Widget build(BuildContext context) { final hasLabel = item.name != null && item.name!.isNotEmpty; + final hideLabel = item.isHidden ? S.of(context).unhide_address : S.of(context).hide_address; - return GestureDetector( - onTap: onSelect, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: LongPressPopupBuilder( - popup: LongPressMenu( - items: [ - LongPressMenuItem( - label: S.of(context).set_label, - iconPath: "assets/new-ui/address_set_label.svg", - onSelected: () async { - Navigator.of(context, rootNavigator: true).pop(); - final res = await showPopUp( - context: context, - builder: (context) => getIt.get(param1: item)); + Future editLabel() async { + final res = await showPopUp( + context: context, builder: (context) => getIt.get(param1: item)); - if (res != null) { - onLabelChanged(); - } - }), - LongPressMenuItem( - label: item.isHidden ? S.of(context).unhide_address : S.of(context).hide_address, - iconPath: "assets/new-ui/address_hide.svg", - onSelected: () { - Navigator.of(context, rootNavigator: true).pop(); - onAddressHidden(); - }, - color: Theme.of(context).colorScheme.error), - LongPressMenuItem( - label: 'Info', - iconPath: "assets/images/info_icon.svg", - onSelected: () async { - Navigator.of(context, rootNavigator: true).pop(); - await showPopUp( - context: context, - builder: (context) => getIt.get(param1: item)); - }), - ], - ), - child: AnimatedContainer( - duration: Duration(milliseconds: 150), - decoration: BoxDecoration( - color: selected - ? Theme.of(context).colorScheme.surfaceContainerHigh - : Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.vertical( - top: Radius.circular(first ? 16 : 0), - bottom: Radius.circular(last ? 16 : 0), + if (res != null) { + onLabelChanged(); + } + } + + Future showInfo() async { + await showPopUp( + context: context, builder: (context) => getIt.get(param1: item)); + } + + // The row is a single control: selection on tap, and the actions that are + // otherwise reachable only by long press exposed as custom actions. + return MergeSemantics( + child: Semantics( + button: true, + selected: selected, + customSemanticsActions: { + CustomSemanticsAction(label: S.of(context).set_label): editLabel, + CustomSemanticsAction(label: hideLabel): onAddressHidden, + CustomSemanticsAction(label: S.of(context).info): showInfo, + }, + child: GestureDetector( + onTap: onSelect, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: LongPressPopupBuilder( + popup: LongPressMenu( + items: [ + LongPressMenuItem( + label: S.of(context).set_label, + iconPath: "assets/new-ui/address_set_label.svg", + onSelected: () { + Navigator.of(context, rootNavigator: true).pop(); + editLabel(); + }), + LongPressMenuItem( + label: hideLabel, + iconPath: "assets/new-ui/address_hide.svg", + onSelected: () { + Navigator.of(context, rootNavigator: true).pop(); + onAddressHidden(); + }, + color: Theme.of(context).colorScheme.error), + LongPressMenuItem( + label: S.of(context).info, + iconPath: "assets/images/info_icon.svg", + onSelected: () { + Navigator.of(context, rootNavigator: true).pop(); + showInfo(); + }), + ], ), - ), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - spacing: 8, - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - children: [ - Row( - spacing: 4, + child: AnimatedContainer( + duration: Duration(milliseconds: 150), + decoration: BoxDecoration( + color: selected + ? Theme.of(context).colorScheme.surfaceContainerHigh + : Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.vertical( + top: Radius.circular(first ? 16 : 0), + bottom: Radius.circular(last ? 16 : 0), + ), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + spacing: 8, + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( children: [ - if (hasLabel) - Text( - item.name!, - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.onSurface, - fontWeight: FontWeight.w500), - ), - Expanded( - child: AddressFormatter.buildSegmentedAddress( - address: item.address, - walletType: walletType, - textAlign: TextAlign.left, - evenTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( + Row( + spacing: 4, + children: [ + if (hasLabel) + Text( + item.name!, + style: TextStyle( fontSize: 12, - fontWeight: FontWeight.w400, - color: hasLabel - ? Theme.of(context).colorScheme.onSurfaceVariant - : Theme.of(context).colorScheme.onSurface, - ), - shouldTruncate: (hasLabel)), + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.w500), + ), + Expanded( + child: AddressFormatter.buildSegmentedAddress( + address: item.address, + walletType: walletType, + textAlign: TextAlign.left, + evenTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 12, + fontWeight: FontWeight.w400, + color: hasLabel + ? Theme.of(context).colorScheme.onSurfaceVariant + : Theme.of(context).colorScheme.onSurface, + ), + shouldTruncate: (hasLabel)), + ), + ], ), + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${S.of(context).transactions}: ${item.txCount}", + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + Text( + "${hasReceived ? S.of(context).received : S.of(context).balance}: ${item.balance}", + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant)), + ], + ) ], ), - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "${S.of(context).transactions}: ${item.txCount}", - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.onSurfaceVariant), - ), - Text( - "${hasReceived ? S.of(context).received : S.of(context).balance}: ${item.balance}", - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.onSurfaceVariant)), - ], - ) - ], - ), + ), + // The row's selected state carries this information. + if (selected) + ExcludeSemantics( + child: CakeImageWidget(imageUrl: "assets/new-ui/checkmark.svg")) + ], ), - if (selected) CakeImageWidget(imageUrl: "assets/new-ui/checkmark.svg") - ], + ), ), ), ), @@ -462,26 +494,34 @@ class ShowHiddenButton extends StatelessWidget { child: Column( children: [ Material( - child: InkWell( - onTap: () { - Navigator.of(context).pushNamed(Routes.receiveAddresses, arguments: true); - }, - child: Container( - height: 48, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(S.of(context).show_hidden_addresses), - RotatedBox( - quarterTurns: 1, - child: CakeImageWidget(imageUrl: "assets/new-ui/dropdown_arrow.svg")) - ], + child: MergeSemantics( + child: Semantics( + button: true, + child: InkWell( + onTap: () { + Navigator.of(context).pushNamed(Routes.receiveAddresses, arguments: true); + }, + child: Container( + height: 48, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(S.of(context).show_hidden_addresses), + ExcludeSemantics( + child: RotatedBox( + quarterTurns: 1, + child: + CakeImageWidget(imageUrl: "assets/new-ui/dropdown_arrow.svg")), + ) + ], + ), + ), ), ), ), diff --git a/lib/new-ui/pages/bridge/bridge_amount_page.dart b/lib/new-ui/pages/bridge/bridge_amount_page.dart index 757a9579e..d3255c1a9 100644 --- a/lib/new-ui/pages/bridge/bridge_amount_page.dart +++ b/lib/new-ui/pages/bridge/bridge_amount_page.dart @@ -116,8 +116,10 @@ class _BridgeAmountPageState extends State { ModalTopBar( title: S.of(context).enter_amount, leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: () => Navigator.of(context, rootNavigator: true).pop(), trailingIcon: Icon(Icons.history), + trailingSemanticLabel: S.of(context).history, onTrailingPressed: () { Navigator.pushNamed(context, Routes.bridgeHistoryPage, arguments: widget.bridgeHistoryViewModel); @@ -274,6 +276,7 @@ class _BridgeAmountPageState extends State { backgroundColor: theme.colorScheme.primary, iconColor: theme.colorScheme.onPrimary, icon: Icon(Icons.arrow_forward, size: 20), + semanticLabel: S.of(context).continue_text, onPressed: () { Navigator.pushNamed( context, diff --git a/lib/new-ui/pages/bridge/bridge_confirm_sheet.dart b/lib/new-ui/pages/bridge/bridge_confirm_sheet.dart index 290666d74..4ccbfb4e7 100644 --- a/lib/new-ui/pages/bridge/bridge_confirm_sheet.dart +++ b/lib/new-ui/pages/bridge/bridge_confirm_sheet.dart @@ -87,6 +87,7 @@ class _BridgeConfirmSheetState extends State { ], ), trailingIcon: const Icon(Icons.close), + trailingSemanticLabel: S.of(context).close, onTrailingPressed: () => Navigator.of(context).maybePop(), ); }, diff --git a/lib/new-ui/pages/bridge/bridge_detail_page.dart b/lib/new-ui/pages/bridge/bridge_detail_page.dart index 64cc457fd..d29602bc3 100644 --- a/lib/new-ui/pages/bridge/bridge_detail_page.dart +++ b/lib/new-ui/pages/bridge/bridge_detail_page.dart @@ -46,6 +46,7 @@ class BridgeDetailPage extends StatelessWidget { ModalTopBar( title: "Bridge Transfer Details", leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: () => Navigator.of(context).pop(), ), Expanded( diff --git a/lib/new-ui/pages/bridge/bridge_history_page.dart b/lib/new-ui/pages/bridge/bridge_history_page.dart index 5dff00f31..49594850d 100644 --- a/lib/new-ui/pages/bridge/bridge_history_page.dart +++ b/lib/new-ui/pages/bridge/bridge_history_page.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/new-ui/pages/bridge/bridge_detail_page.dart'; import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; import 'package:cake_wallet/view_model/bridge/bridge_history_view_model.dart'; @@ -25,6 +26,7 @@ class BridgeHistoryPage extends StatelessWidget { ModalTopBar( title: "Bridge history", leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: () => Navigator.of(context).pop(), ), Expanded( diff --git a/lib/new-ui/pages/bridge/bridge_network_page.dart b/lib/new-ui/pages/bridge/bridge_network_page.dart index 05f1c3440..b0df5270b 100644 --- a/lib/new-ui/pages/bridge/bridge_network_page.dart +++ b/lib/new-ui/pages/bridge/bridge_network_page.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart'; import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; import 'package:cake_wallet/routes.dart'; @@ -30,6 +31,7 @@ class BridgeNetworkPage extends StatelessWidget { ModalTopBar( title: 'Destination Network', leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: () => Navigator.of(context).pop(), ), SizedBox(height: 48), diff --git a/lib/new-ui/pages/bridge/bridge_receive_address_input_page.dart b/lib/new-ui/pages/bridge/bridge_receive_address_input_page.dart index dfecf82d6..ee21669a4 100644 --- a/lib/new-ui/pages/bridge/bridge_receive_address_input_page.dart +++ b/lib/new-ui/pages/bridge/bridge_receive_address_input_page.dart @@ -55,6 +55,7 @@ class _BridgeReceiveAddressInputPageState extends State Navigator.of(context).pop(), ), SizedBox(height: 48), diff --git a/lib/new-ui/pages/bridge/bridge_receiving_wallet_page.dart b/lib/new-ui/pages/bridge/bridge_receiving_wallet_page.dart index 16872b511..6b4f3f0e6 100644 --- a/lib/new-ui/pages/bridge/bridge_receiving_wallet_page.dart +++ b/lib/new-ui/pages/bridge/bridge_receiving_wallet_page.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/view_model/bridge/bridge_receiving_wallet_option.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/new-ui/pages/bridge/bridge_confirm_sheet.dart'; import 'package:cake_wallet/new-ui/pages/bridge/bridge_receive_address_input_page.dart'; import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart'; @@ -42,6 +43,7 @@ class _BridgeReceivingWalletPageState extends State { ModalTopBar( title: 'Receiving Wallet', leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: () => Navigator.of(context).pop(), ), const SizedBox(height: 48), diff --git a/lib/new-ui/pages/card_customizer.dart b/lib/new-ui/pages/card_customizer.dart index 9b2249d4f..9973ec265 100644 --- a/lib/new-ui/pages/card_customizer.dart +++ b/lib/new-ui/pages/card_customizer.dart @@ -84,6 +84,7 @@ class _CardCustomizerState extends State { ModalTopBar( title: editEnabled ? S.of(context).edit_account : S.of(context).edit_card, leadingIcon: Icon(Icons.close), + leadingSemanticLabel: S.of(context).close, // trailingIcon: editEnabled ? Icon(Icons.delete_forever) : null, onLeadingPressed: () => Navigator.of(context).maybePop(), // onTrailingPressed: () {}, diff --git a/lib/new-ui/pages/home_page.dart b/lib/new-ui/pages/home_page.dart index 8fd2e3e23..f62440952 100644 --- a/lib/new-ui/pages/home_page.dart +++ b/lib/new-ui/pages/home_page.dart @@ -13,10 +13,14 @@ import 'package:cake_wallet/new-ui/widgets/coins_page/mweb_ad.dart'; import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart'; import 'package:cake_wallet/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart'; import 'package:cake_wallet/new-ui/widgets/coins_page/wallet_info.dart'; +import "package:cake_wallet/new-ui/widgets/coins_page/zcash_migration_modal.dart"; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart'; +import "package:cw_core/amount/money.dart"; +import "package:cw_core/crypto_currency.dart"; +import "package:cw_core/transaction_direction.dart"; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -48,6 +52,24 @@ class _NewHomePageState extends State { _lightningMode = false; }); }); + + reaction((_) => widget.dashboardViewModel.isMigratingToIronwood, (val) { + if (val && !widget.dashboardViewModel.settingsStore.zcashMigrationModalViewed) { + if(!context.mounted) { + return; + } + widget.dashboardViewModel.settingsStore.zcashMigrationModalViewed = true; + showMaterialModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (context) => ZcashMigrationModal( + hasContinue: true, + balance: widget.dashboardViewModel.wallet.balance.values.first.unavailable + .toStringWithSymbol(), + ), + ); + } + }); } void _setAccountViewModel() { diff --git a/lib/new-ui/pages/lightning_username_page.dart b/lib/new-ui/pages/lightning_username_page.dart index 38c801443..c42c19f0b 100644 --- a/lib/new-ui/pages/lightning_username_page.dart +++ b/lib/new-ui/pages/lightning_username_page.dart @@ -68,6 +68,7 @@ class _LightningUsernamePageState extends State { ModalTopBar( title: "Lightning ${S.of(context).username}", leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: Navigator.of(context).pop, ), Expanded( diff --git a/lib/new-ui/pages/receive_page.dart b/lib/new-ui/pages/receive_page.dart index e1c6afd01..b5656e9de 100644 --- a/lib/new-ui/pages/receive_page.dart +++ b/lib/new-ui/pages/receive_page.dart @@ -1,54 +1,54 @@ -import 'package:cake_wallet/core/utilities.dart'; -import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; -import 'package:cake_wallet/new-ui/widgets/receive_page/payjoin_copy_modal.dart'; -import 'package:cake_wallet/new-ui/widgets/receive_page/receive_address_type.dart'; -import 'package:cake_wallet/new-ui/widgets/receive_page/receive_address_widget.dart'; -import 'package:cake_wallet/new-ui/widgets/receive_page/receive_amount_display.dart'; -import 'package:cake_wallet/new-ui/widgets/receive_page/receive_amount_modal.dart'; -import 'package:cake_wallet/new-ui/widgets/receive_page/receive_bottom_buttons.dart'; -import 'package:cake_wallet/new-ui/widgets/receive_page/receive_info_box.dart'; -import 'package:cake_wallet/new-ui/widgets/receive_page/receive_label_modal.dart'; -import 'package:cake_wallet/new-ui/widgets/receive_page/receive_label_widget.dart'; -import 'package:cake_wallet/new-ui/widgets/receive_page/receive_large_amount_preview.dart'; -import 'package:cake_wallet/new-ui/widgets/receive_page/receive_qr_code.dart'; -import 'package:cake_wallet/new-ui/widgets/receive_page/receive_token_display.dart'; -import 'package:cake_wallet/utils/share_util.dart'; -import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; -import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; -import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; -import 'package:cake_wallet/zcash/zcash.dart'; -import 'package:cw_core/crypto_currency.dart'; -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:flutter/cupertino.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:mobx/mobx.dart'; -import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; -import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/di.dart'; -import 'package:cake_wallet/anonpay/anonpay_donation_link_info.dart'; -import 'package:cake_wallet/entities/preferences_key.dart'; -import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import "package:cake_wallet/anonpay/anonpay_donation_link_info.dart"; +import "package:cake_wallet/bitcoin/bitcoin.dart"; +import "package:cake_wallet/core/utilities.dart"; +import "package:cake_wallet/di.dart"; +import "package:cake_wallet/entities/auto_generate_subaddress_status.dart"; +import "package:cake_wallet/entities/preferences_key.dart"; +import "package:cake_wallet/generated/i18n.dart"; +import "package:cake_wallet/new-ui/widgets/modern_button.dart"; +import "package:cake_wallet/new-ui/widgets/receive_page/payjoin_copy_modal.dart"; +import "package:cake_wallet/new-ui/widgets/receive_page/receive_address_type.dart"; +import "package:cake_wallet/new-ui/widgets/receive_page/receive_address_widget.dart"; +import "package:cake_wallet/new-ui/widgets/receive_page/receive_amount_display.dart"; +import "package:cake_wallet/new-ui/widgets/receive_page/receive_amount_modal.dart"; +import "package:cake_wallet/new-ui/widgets/receive_page/receive_bottom_buttons.dart"; +import "package:cake_wallet/new-ui/widgets/receive_page/receive_info_box.dart"; +import "package:cake_wallet/new-ui/widgets/receive_page/receive_label_modal.dart"; +import "package:cake_wallet/new-ui/widgets/receive_page/receive_label_widget.dart"; +import "package:cake_wallet/new-ui/widgets/receive_page/receive_large_amount_preview.dart"; +import "package:cake_wallet/new-ui/widgets/receive_page/receive_qr_code.dart"; +import "package:cake_wallet/new-ui/widgets/receive_page/receive_token_display.dart"; +import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart"; +import "package:cake_wallet/routes.dart"; +import "package:cake_wallet/src/screens/receive/anonpay_receive_page.dart"; +import "package:cake_wallet/utils/share_util.dart"; +import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart"; +import "package:cake_wallet/view_model/dashboard/receive_option_view_model.dart"; +import "package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart"; +import "package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart"; +import "package:cake_wallet/zcash/zcash.dart"; +import "package:cw_core/crypto_currency.dart"; +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_type.dart"; +import "package:flutter/cupertino.dart"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:flutter_mobx/flutter_mobx.dart"; +import "package:mobx/mobx.dart"; +import "package:modal_bottom_sheet/modal_bottom_sheet.dart"; +import "package:shared_preferences/shared_preferences.dart"; class NewReceivePage extends StatefulWidget { - NewReceivePage( - {super.key, - required this.addressListViewModel, - required this.receiveOptionViewModel, - required this.dashboardViewModel, - required this.lightningMode, - CryptoCurrency? initialCurrency}) { + NewReceivePage({ + required this.addressListViewModel, + required this.receiveOptionViewModel, + required this.dashboardViewModel, + required this.lightningMode, + super.key, + CryptoCurrency? initialCurrency, + }) { if (initialCurrency != null && initialCurrency != addressListViewModel.selectedCurrency) { addressListViewModel.setTokenCurrency(initialCurrency); } @@ -78,31 +78,33 @@ class _NewReceivePageState extends State { .firstWhereOrNull((item) => item.value.contains("Lightning")) ?? ReceivePageOption.mainnet, ); - widget.addressListViewModel.setTokenCurrency(CryptoCurrency.btcln); + widget.addressListViewModel.selectedCurrency = CryptoCurrency.btcln; } else if (widget.addressListViewModel.wallet.type == WalletType.bitcoin) { widget.receiveOptionViewModel.selectReceiveOption( widget.receiveOptionViewModel.options .firstWhereOrNull((item) => item.value.contains("Standard")) ?? ReceivePageOption.mainnet, ); + widget.addressListViewModel.selectedCurrency = CryptoCurrency.btc; } }); - reaction((_) => widget.addressListViewModel.uri, (newAddress) { - _reloadAddressWithLabel(newAddress); - }); + reaction((_) => widget.addressListViewModel.uri, _reloadAddressWithLabel); - _addressItemWithLabel = - widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull((item) { - return (item is WalletAddressListItem && - item.address == widget.addressListViewModel.uri.address); - }) as WalletAddressListItem?; + _addressItemWithLabel = widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull( + (item) => + item is WalletAddressListItem && item.address == widget.addressListViewModel.uri.address, + ) as WalletAddressListItem?; - reaction((_) => widget.receiveOptionViewModel.selectedReceiveOption, - (ReceivePageOption option) { + reaction((_) => widget.receiveOptionViewModel.selectedReceiveOption, (option) { if (widget.dashboardViewModel.type == WalletType.bitcoin && bitcoin!.isBitcoinReceivePageOption(option)) { widget.addressListViewModel.setAddressType(bitcoin!.getOptionToType(option)); + if (option.value.contains("Lightning")) { + widget.addressListViewModel.selectedCurrency = CryptoCurrency.btcln; + } else { + widget.addressListViewModel.selectedCurrency = CryptoCurrency.btc; + } return; } if (widget.dashboardViewModel.type == WalletType.zcash) { @@ -166,16 +168,18 @@ class _NewReceivePageState extends State { final hasAddressTypeSelector = widget.receiveOptionViewModel.options.length > 1; final hasLabel = _addressItemWithLabel?.name != null && _addressItemWithLabel!.name!.isNotEmpty; final infoboxDismissed = widget.addressListViewModel.wallet.walletInfo.receiveInfoboxDismissed; - final infobox = ReceiveInfoBox.forWalletType(widget.addressListViewModel.type, - supportedCurrencies: widget.addressListViewModel.tokenCurrencies - .whereType() - .toList(), onDismissed: () { - widget.addressListViewModel.dismissInfobox(); - setState(() {}); - }, - autoGenerateSubaddressStatus: widget.lightningMode - ? AutoGenerateSubaddressStatus.disabled - : widget.dashboardViewModel.settingsStore.autoGenerateSubaddressStatus); + final infobox = ReceiveInfoBox.forWalletType( + widget.addressListViewModel.type, + supportedCurrencies: + widget.addressListViewModel.tokenCurrencies.whereType().toList(), + onDismissed: () { + widget.addressListViewModel.dismissInfobox(); + setState(() {}); + }, + autoGenerateSubaddressStatus: widget.lightningMode + ? AutoGenerateSubaddressStatus.disabled + : widget.dashboardViewModel.settingsStore.autoGenerateSubaddressStatus, + ); return Container( decoration: BoxDecoration( @@ -187,9 +191,7 @@ class _NewReceivePageState extends State { begin: Alignment.topCenter, end: Alignment.bottomCenter, ), - borderRadius: BorderRadius.vertical( - top: Radius.circular(24), - ), + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), ), child: SafeArea( child: Column( @@ -198,13 +200,14 @@ class _NewReceivePageState extends State { children: [ ModalTopBar( title: _largeQrMode ? "" : S.of(context).receive, - leadingIcon: Icon(Icons.close), + leadingIcon: const Icon(Icons.close), + leadingSemanticLabel: S.of(context).close, onLeadingPressed: () { Navigator.of(context, rootNavigator: true).pop(); }, trailingWidget: Observer( builder: (_) => AnimatedSwitcher( - duration: Duration(milliseconds: 300), + duration: const Duration(milliseconds: 300), child: _largeQrMode || widget.addressListViewModel.hasAddressRotation /* TODO rotating is broken on mweb, disabling for now, fix after mvp*/ @@ -217,10 +220,13 @@ class _NewReceivePageState extends State { key: ValueKey(_largeQrMode), size: 36, icon: _largeQrMode - ? Icon(Icons.share) + ? const Icon(Icons.share) : widget.addressListViewModel.isRotatingAddress - ? CupertinoActivityIndicator() - : Icon(Icons.refresh), + ? const CupertinoActivityIndicator() + : const Icon(Icons.refresh), + semanticLabel: _largeQrMode + ? S.of(context).share_address + : S.of(context).rotate_address, onPressed: () { if (_largeQrMode) { ShareUtil.share( @@ -232,8 +238,9 @@ class _NewReceivePageState extends State { widget.addressListViewModel.rotateAddress(); } } - }) - : SizedBox.shrink(), + }, + ) + : const SizedBox.shrink(), ), ), ), @@ -268,12 +275,25 @@ class _NewReceivePageState extends State { ReceiveAddressWidget( addressListViewModel: widget.addressListViewModel, ), - GestureDetector( - onTap: _showLabelModal, - child: ReceiveLabelWidget( - name: _addressItemWithLabel?.name ?? "", - largeQrMode: _largeQrMode, - )), + // The label chip animates to zero height when there is no + // label (or in large QR mode); keep it out of the semantics + // tree entirely while it is collapsed. + ExcludeSemantics( + excluding: _largeQrMode || !hasLabel, + child: MergeSemantics( + child: Semantics( + button: true, + hint: S.of(context).set_label, + child: GestureDetector( + onTap: _showLabelModal, + child: ReceiveLabelWidget( + name: _addressItemWithLabel?.name ?? "", + largeQrMode: _largeQrMode, + ), + ), + ), + ), + ), Observer( builder: (_) => ReceiveBottomButtons( key: const ValueKey(0), @@ -285,14 +305,16 @@ class _NewReceivePageState extends State { : ClipboardData( text: widget.addressListViewModel.displayAmount.isEmpty ? widget.addressListViewModel.uri.address - : widget.addressListViewModel.uri.toString()), + : widget.addressListViewModel.uri.toString(), + ), onCopyButtonPressed: () { if (widget.addressListViewModel.hasPayjoin) { showModalBottomSheet( - isScrollControlled: true, - context: context, - builder: (context) => - PayjoinCopyModal(uri: widget.addressListViewModel.uri)); + isScrollControlled: true, + context: context, + builder: (context) => + PayjoinCopyModal(uri: widget.addressListViewModel.uri), + ); } }, onAmountButtonPressed: () { @@ -300,12 +322,10 @@ class _NewReceivePageState extends State { context: context, backgroundColor: Colors.transparent, barrierColor: Colors.black.withAlpha(80), - builder: (context) { - return ReceiveAmountModal( - walletAddressListViewModel: widget.addressListViewModel, - onSubmitted: (amount) {}, - ); - }, + builder: (context) => ReceiveAmountModal( + walletAddressListViewModel: widget.addressListViewModel, + onSubmitted: (amount) {}, + ), ); }, onLabelButtonPressed: _showLabelModal, @@ -318,22 +338,25 @@ class _NewReceivePageState extends State { ), ), ReceiveLargeAmountPreview( - amount: widget.addressListViewModel.displayAmount, - currency: widget.addressListViewModel.cryptoCurrencySymbol, - largeQrMode: _largeQrMode), + amount: widget.addressListViewModel.displayAmount, + currency: widget.addressListViewModel.cryptoCurrencySymbol, + largeQrMode: _largeQrMode, + ), if (infobox != null && !widget.addressListViewModel.isLightning) ClipRect( - child: AnimatedAlign( - duration: const Duration(milliseconds: 200), - curve: Curves.easeOutCubic, - heightFactor: infoboxDismissed ? 0 : 1, - alignment: Alignment.center, - child: AnimatedOpacity( + child: AnimatedAlign( + duration: const Duration(milliseconds: 200), + curve: Curves.easeOutCubic, + heightFactor: infoboxDismissed ? 0 : 1, + alignment: Alignment.center, + child: AnimatedOpacity( duration: const Duration(milliseconds: 200), opacity: infoboxDismissed ? 0 : 1, curve: Curves.easeOutCubic, - child: infobox), - )) + child: infobox, + ), + ), + ), ], ), ), @@ -345,12 +368,11 @@ class _NewReceivePageState extends State { void _showLabelModal() { showMaterialModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - barrierColor: Colors.black.withAlpha(80), - builder: (context) { - return getIt.get(param1: _addressItemWithLabel); - }).then((value) { + context: context, + backgroundColor: Colors.transparent, + barrierColor: Colors.black.withAlpha(80), + builder: (_) => getIt.get(param1: _addressItemWithLabel), + ).then((value) { _reloadAddressWithLabel(widget.addressListViewModel.uri); }); } @@ -359,8 +381,8 @@ class _NewReceivePageState extends State { // FIXME: viewmodel doesn't want to load address name here, so we make it. investigate why later setState(() { _addressItemWithLabel = widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull( - (item) => (item is WalletAddressListItem && item.address == newAddress.address)) - as WalletAddressListItem?; + (item) => item is WalletAddressListItem && item.address == newAddress.address, + ) as WalletAddressListItem?; }); } } diff --git a/lib/new-ui/pages/scan_page.dart b/lib/new-ui/pages/scan_page.dart index 16dcb38bd..65182b12f 100644 --- a/lib/new-ui/pages/scan_page.dart +++ b/lib/new-ui/pages/scan_page.dart @@ -76,46 +76,50 @@ class _ScanPageState extends State { controller: controller, onDetect: _handleBarcode, ), + // A full screen tap target would otherwise show up as one giant unlabeled + // node; leaving text mode is also possible with the back button. Positioned.fill( - child: GestureDetector( - onTap: () => setState(() { - _textInputMode = false; - }), - child: RepaintBoundary( - child: AnimatedSwitcher( - switchInCurve: Curves.easeOutCubic, - switchOutCurve: Curves.easeInQuad, - duration: textModeSwitchDuration, - child: _textInputMode - ? BackdropFilter( - key: ValueKey(1), - filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0), - child: Container( - color: _frontFlashMode ? Colors.white : Colors.black.withAlpha(153), - ), - ) - : TweenAnimationBuilder( - key: const ValueKey(0), - tween: Tween(begin: 0.0, end: targetRadius), - duration: const Duration(milliseconds: 500), - curve: Curves.easeOutCubic, - child: BackdropFilter( + child: ExcludeSemantics( + child: GestureDetector( + onTap: () => setState(() { + _textInputMode = false; + }), + child: RepaintBoundary( + child: AnimatedSwitcher( + switchInCurve: Curves.easeOutCubic, + switchOutCurve: Curves.easeInQuad, + duration: textModeSwitchDuration, + child: _textInputMode + ? BackdropFilter( + key: ValueKey(1), filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0), child: Container( color: _frontFlashMode ? Colors.white : Colors.black.withAlpha(153), ), - ), - builder: (context, radius, child) { - return ClipPath( - clipper: HoleClipper( - width: cutoutSize, - height: cutoutSize, - radius: radius, + ) + : TweenAnimationBuilder( + key: const ValueKey(0), + tween: Tween(begin: 0.0, end: targetRadius), + duration: const Duration(milliseconds: 500), + curve: Curves.easeOutCubic, + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0), + child: Container( + color: _frontFlashMode ? Colors.white : Colors.black.withAlpha(153), ), - child: child, - ); - }, - ), + ), + builder: (context, radius, child) { + return ClipPath( + clipper: HoleClipper( + width: cutoutSize, + height: cutoutSize, + radius: radius, + ), + child: child, + ); + }, + ), + ), ), ), ), @@ -163,6 +167,7 @@ class _ScanPageState extends State { }, iconColor: buttonIconColor, backgroundColor: buttonColor, + semanticLabel: S.of(context).switch_camera, ), ModernButton( size: 36, @@ -171,6 +176,10 @@ class _ScanPageState extends State { (controller.value.torchState == TorchState.on || _frontFlashMode) ? Icons.flash_off_outlined : Icons.flash_on), + semanticLabel: + (controller.value.torchState == TorchState.on || _frontFlashMode) + ? S.of(context).turn_flash_off + : S.of(context).turn_flash_on, onPressed: () { if (controller.value.cameraDirection == CameraFacing.front) { setState(() { @@ -194,6 +203,7 @@ class _ScanPageState extends State { size: 36, iconSize: 18, icon: Icon(Icons.arrow_back_ios_new), + semanticLabel: S.of(context).seed_alert_back, onPressed: () { if (_textInputMode) { setState(() { @@ -235,20 +245,25 @@ class _ScanPageState extends State { spacing: 10, children: [ Expanded( - child: TextField( - enabled: _textInputMode, - controller: textController, - focusNode: textFocusNode, - onSubmitted: (val) { - if (val.isNotEmpty) { - Navigator.of(context).pop(val); - } else { - setState(() { - _textInputMode = false; - }); - } - }, - decoration: InputDecoration(hintText: S.of(context).enter_code), + child: MergeSemantics( + child: Semantics( + label: S.of(context).enter_code, + child: TextField( + enabled: _textInputMode, + controller: textController, + focusNode: textFocusNode, + onSubmitted: (val) { + if (val.isNotEmpty) { + Navigator.of(context).pop(val); + } else { + setState(() { + _textInputMode = false; + }); + } + }, + decoration: InputDecoration(hintText: S.of(context).enter_code), + ), + ), ), ), FloatingIconButton( @@ -330,6 +345,7 @@ class _ScanPageState extends State { } }, icon: Icons.question_mark, + semanticsLabel: S.of(context).help, buttonColor: buttonColor, buttonIconColor: buttonIconColor) ], @@ -360,27 +376,39 @@ class _ScanPageState extends State { child: AnimatedOpacity( duration: Duration(milliseconds: 500), opacity: isScanningURQR ? 1 : 0, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "${decoder.processedPartsCount()}", - style: TextStyle( - fontSize: 45, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.primary), + // Three separate digits mean nothing on their own: announce the progress + // as one node, and only while a multi part code is actually being scanned. + child: ExcludeSemantics( + excluding: !isScanningURQR, + child: Semantics( + container: true, + liveRegion: true, + label: S.of(context).qr_parts_scanned( + "${decoder.processedPartsCount()}", "${decoder.expectedPartCount() ?? 0}"), + excludeSemantics: true, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "${decoder.processedPartsCount()}", + style: TextStyle( + fontSize: 45, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary), + ), + Text( + "/", + style: TextStyle( + fontSize: 45, color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + Text( + "${decoder.expectedPartCount()}", + style: TextStyle( + fontSize: 45, color: Theme.of(context).colorScheme.onSurface), + ), + ], ), - Text( - "/", - style: TextStyle( - fontSize: 45, color: Theme.of(context).colorScheme.onSurfaceVariant), - ), - Text( - "${decoder.expectedPartCount()}", - style: - TextStyle(fontSize: 45, color: Theme.of(context).colorScheme.onSurface), - ), - ], + ), ), ), ), @@ -400,7 +428,7 @@ class _ScanPageState extends State { return AlertWithOneAction( alertTitle: S.of(context).error, alertContent: S.of(context).error_dialog_content, - buttonText: 'ok', + buttonText: S.of(context).ok, buttonAction: () { Navigator.of(context).pop(); }, @@ -453,37 +481,51 @@ class ScanPageButton extends StatelessWidget { required this.onTap, required this.icon, this.label, + this.semanticsLabel, required this.buttonColor, required this.buttonIconColor}); final VoidCallback onTap; final IconData icon; final String? label; + + /// Name for the icon-only variant, which has no visible [label]. + final String? semanticsLabel; final Color buttonColor; final Color buttonIconColor; @override Widget build(BuildContext context) { - return GestureDetector( + return Semantics( + button: true, + enabled: true, + label: semanticsLabel ?? label, onTap: onTap, - behavior: HitTestBehavior.opaque, - child: Container( - decoration: BoxDecoration(color: buttonColor, borderRadius: BorderRadius.circular(99999)), - child: Padding( - padding: EdgeInsets.only( - top: 10, bottom: 10, left: label == null ? 10 : 16, right: label == null ? 10 : 20), - child: Row( - spacing: 10, - children: [ - Icon(icon, size: 28, color: buttonIconColor), - if (label != null) - Text(label!, - style: TextStyle( - fontSize: 16, fontWeight: FontWeight.w500, color: buttonIconColor)) - ], - ), - ), - )); + excludeSemantics: true, + child: GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: Container( + decoration: + BoxDecoration(color: buttonColor, borderRadius: BorderRadius.circular(99999)), + child: Padding( + padding: EdgeInsets.only( + top: 10, + bottom: 10, + left: label == null ? 10 : 16, + right: label == null ? 10 : 20), + child: Row( + spacing: 10, + children: [ + Icon(icon, size: 28, color: buttonIconColor), + if (label != null) + Text(label!, + style: TextStyle( + fontSize: 16, fontWeight: FontWeight.w500, color: buttonIconColor)) + ], + ), + ), + ))); } } diff --git a/lib/new-ui/pages/send_page.dart b/lib/new-ui/pages/send_page.dart index 994e13456..1b379df8e 100644 --- a/lib/new-ui/pages/send_page.dart +++ b/lib/new-ui/pages/send_page.dart @@ -1,85 +1,97 @@ -import 'package:cake_wallet/core/address_validator.dart'; -import 'package:cake_wallet/core/auth_service.dart'; -import 'package:cake_wallet/core/execution_state.dart'; -import 'package:cake_wallet/core/open_crypto_pay/open_cryptopay_service.dart'; -import 'package:cake_wallet/di.dart'; -import 'package:cake_wallet/entities/contact_record.dart'; -import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; -import 'package:cake_wallet/evm/evm.dart'; -import 'package:cake_wallet/exchange/trade.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/new-ui/modal_navigator.dart'; -import 'package:cake_wallet/new-ui/pages/coin_control_page.dart'; -import 'package:cake_wallet/new-ui/widgets/animated_dropdown.dart'; -import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart'; -import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart'; -import 'package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart'; -import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart'; -import 'package:cake_wallet/new-ui/widgets/picker.dart'; -import 'package:cake_wallet/new-ui/widgets/send_page/fiat_amount_bar.dart'; -import 'package:cake_wallet/new-ui/widgets/send_page/send_confirm_sheet.dart'; -import 'package:cake_wallet/new-ui/widgets/send_page/send_memo_input.dart'; -import 'package:cake_wallet/reactions/wallet_connect.dart'; -import 'package:cake_wallet/src/widgets/bottom_sheet/token_selection_bottom_sheet.dart'; -import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; -import 'package:cake_wallet/src/widgets/new_list_row/list_item_regular_row_widget.dart'; -import 'package:cake_wallet/src/widgets/standard_checkbox.dart'; -import 'package:cake_wallet/store/app_store.dart'; -import 'package:cw_core/amount/amount_sanitizer.dart'; -import 'package:cw_core/amount/money.dart'; -import 'package:cw_core/lnurl.dart'; -import 'package:cw_core/wallet_info.dart'; +import "package:cake_wallet/core/address_resolver/parsed_address.dart"; +import "package:cake_wallet/core/address_validator.dart"; +import "package:cake_wallet/core/auth_service.dart"; +import "package:cake_wallet/core/execution_state.dart"; +import "package:cake_wallet/core/open_crypto_pay/open_cryptopay_service.dart"; +import "package:cake_wallet/di.dart"; +import "package:cake_wallet/entities/contact_record.dart"; +import "package:cake_wallet/entities/priority_for_wallet_type.dart"; +import "package:cake_wallet/evm/evm.dart"; +import "package:cake_wallet/exchange/trade.dart"; +import "package:cake_wallet/generated/i18n.dart"; +import "package:cake_wallet/main.dart"; +import "package:cake_wallet/monero/monero.dart"; +import "package:cake_wallet/new-ui/modal_navigator.dart"; +import "package:cake_wallet/new-ui/pages/coin_control_page.dart"; +import "package:cake_wallet/new-ui/widgets/animated_dropdown.dart"; +import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart"; +import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart"; +import "package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart"; +import "package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart"; +import "package:cake_wallet/new-ui/widgets/modern_button.dart"; +import "package:cake_wallet/new-ui/widgets/new_future_primary_button.dart"; +import "package:cake_wallet/new-ui/widgets/new_primary_button.dart"; +import "package:cake_wallet/new-ui/widgets/picker.dart"; +import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart"; +import "package:cake_wallet/new-ui/widgets/send_page/directional_switcher.dart"; +import "package:cake_wallet/new-ui/widgets/send_page/fiat_amount_bar.dart"; +import "package:cake_wallet/new-ui/widgets/send_page/l2_action_wallet_selector.dart"; +import "package:cake_wallet/new-ui/widgets/send_page/recipient_dot_row.dart"; +import "package:cake_wallet/new-ui/widgets/send_page/send_address_input.dart"; +import "package:cake_wallet/new-ui/widgets/send_page/send_amount_input.dart"; +import "package:cake_wallet/new-ui/widgets/send_page/send_confirm_sheet.dart"; +import "package:cake_wallet/new-ui/widgets/send_page/send_memo_input.dart"; +import "package:cake_wallet/new-ui/widgets/send_page/send_syncing_indicator.dart"; +import "package:cake_wallet/reactions/wallet_connect.dart"; +import "package:cake_wallet/routes.dart" show Routes; +import "package:cake_wallet/src/screens/connect_device/connect_device_page.dart"; +import "package:cake_wallet/src/widgets/alert_with_one_action.dart"; +import "package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart"; +import "package:cake_wallet/src/widgets/bottom_sheet/payment_confirmation_bottom_sheet.dart"; +import "package:cake_wallet/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart"; +import "package:cake_wallet/src/widgets/bottom_sheet/token_selection_bottom_sheet.dart"; +import "package:cake_wallet/src/widgets/bottom_sheet/wallet_switcher_bottom_sheet.dart"; +import "package:cake_wallet/src/widgets/cake_image_widget.dart"; +import "package:cake_wallet/src/widgets/new_list_row/list_item_regular_row_widget.dart"; +import "package:cake_wallet/src/widgets/standard_checkbox.dart"; +import "package:cake_wallet/store/app_store.dart"; +import "package:cake_wallet/utils/payment_request.dart"; +import "package:cake_wallet/utils/show_pop_up.dart"; +import "package:cake_wallet/view_model/contact_list/contact_list_view_model.dart"; +import "package:cake_wallet/view_model/payment/payment_view_model.dart"; +import "package:cake_wallet/view_model/send/output.dart"; +import "package:cake_wallet/view_model/send/send_view_model.dart"; +import "package: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/amount/amount_sanitizer.dart"; +import "package:cw_core/amount/money.dart"; +import "package:cw_core/crypto_currency.dart"; +import "package:cw_core/lnurl.dart"; +import "package:cw_core/transaction_priority.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:cake_wallet/new-ui/widgets/modern_button.dart'; -import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; -import 'package:cake_wallet/new-ui/widgets/send_page/directional_switcher.dart'; -import 'package:cake_wallet/new-ui/widgets/send_page/recipient_dot_row.dart'; -import 'package:cake_wallet/new-ui/widgets/send_page/send_address_input.dart'; -import 'package:cake_wallet/new-ui/widgets/send_page/send_amount_input.dart'; -import 'package:cake_wallet/new-ui/widgets/send_page/send_syncing_indicator.dart'; -import 'package:cake_wallet/routes.dart' show Routes; -import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; -import 'package:cake_wallet/src/widgets/bottom_sheet/payment_confirmation_bottom_sheet.dart'; -import 'package:cake_wallet/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart'; -import 'package:cake_wallet/src/widgets/bottom_sheet/wallet_switcher_bottom_sheet.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/utils/payment_request.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/core/address_resolver/parsed_address.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/transaction_priority.dart'; -import 'package:cw_core/utils/print_verbose.dart'; - -import 'package:cake_wallet/main.dart'; -import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; -import 'package:cake_wallet/new-ui/widgets/send_page/l2_action_wallet_selector.dart'; -import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; -import 'package:cw_core/unspent_coin_type.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:mobx/mobx.dart'; -import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import "package:flutter/cupertino.dart"; +import "package:flutter/material.dart"; +import "package:flutter_mobx/flutter_mobx.dart"; +import "package:mobx/mobx.dart"; +import "package:modal_bottom_sheet/modal_bottom_sheet.dart"; class SendPageHelpContent { + const SendPageHelpContent({ + required this.title, + required this.imagePath, + required this.description, + this.disclaimer, + }); + final String imagePath; final String title; final String description; final String? disclaimer; - - const SendPageHelpContent( - {required this.title, required this.imagePath, required this.description, this.disclaimer}); } class SendPageModes { + const SendPageModes({ + required this.title, + required this.showAddressField, + this.description, + this.confirmSheetIconPath, + this.helpContent, + this.popOnConfirmation = true, + }); + final bool showAddressField; final String title; final String? description; @@ -87,87 +99,87 @@ class SendPageModes { final SendPageHelpContent? helpContent; final bool popOnConfirmation; - const SendPageModes( - {required this.title, - this.description, - required this.showAddressField, - this.confirmSheetIconPath, - this.helpContent, - this.popOnConfirmation = true}); - static final SendPageModes normal = SendPageModes(title: S.current.send, showAddressField: true); static final SendPageModes lightningDeposit = SendPageModes( + title: S.current.bitcoin_lightning_deposit, + description: S.current.to_lightning, + showAddressField: false, + helpContent: SendPageHelpContent( title: S.current.bitcoin_lightning_deposit, - description: S.current.to_lightning, - showAddressField: false, - helpContent: SendPageHelpContent( - title: S.current.bitcoin_lightning_deposit, - imagePath: "assets/new-ui/lightning_deposit_help.svg", - description: S.current.lightning_deposit_desc, - disclaimer: S.current.lightning_deposit_disclaimer), - popOnConfirmation: false); + imagePath: "assets/new-ui/lightning_deposit_help.svg", + description: S.current.lightning_deposit_desc, + disclaimer: S.current.lightning_deposit_disclaimer, + ), + popOnConfirmation: false, + ); static final SendPageModes lightningWithdrawal = SendPageModes( + title: S.current.bitcoin_lightning_withdraw, + description: S.current.to_on_chain, + showAddressField: false, + helpContent: SendPageHelpContent( title: S.current.bitcoin_lightning_withdraw, - description: S.current.to_on_chain, - showAddressField: false, - helpContent: SendPageHelpContent( - title: S.current.bitcoin_lightning_withdraw, - imagePath: "assets/new-ui/lightning_withdraw_help.svg", - description: S.current.lightning_withdraw_desc, - disclaimer: S.current.lightning_withdraw_disclaimer), - popOnConfirmation: false); + imagePath: "assets/new-ui/lightning_withdraw_help.svg", + description: S.current.lightning_withdraw_desc, + disclaimer: S.current.lightning_withdraw_disclaimer, + ), + popOnConfirmation: false, + ); static final SendPageModes mwebDeposit = SendPageModes( - title: "${S.current.mask} Litecoin", - showAddressField: false, - confirmSheetIconPath: "assets/new-ui/mask.svg", - helpContent: SendPageHelpContent( - title: S.current.about_litecoin_privacy, - imagePath: "assets/new-ui/mweb_help.svg", - description: "${S.current.mweb_help_desc_1}\n\n${S.current.mweb_help_desc_2}", - disclaimer: S.current.mweb_help_disclaimer), - popOnConfirmation: false); + title: "${S.current.mask} Litecoin", + showAddressField: false, + confirmSheetIconPath: "assets/new-ui/mask.svg", + helpContent: SendPageHelpContent( + title: S.current.about_litecoin_privacy, + imagePath: "assets/new-ui/mweb_help.svg", + description: "${S.current.mweb_help_desc_1}\n\n${S.current.mweb_help_desc_2}", + disclaimer: S.current.mweb_help_disclaimer, + ), + popOnConfirmation: false, + ); static final SendPageModes mwebWithdrawal = SendPageModes( - title: "${S.current.unmask} Litecoin", - showAddressField: false, - confirmSheetIconPath: "assets/new-ui/unmask.svg", - helpContent: SendPageHelpContent( - title: S.current.about_litecoin_privacy, - imagePath: "assets/new-ui/mweb_help.svg", - description: "${S.current.mweb_help_desc_1}\n\n${S.current.mweb_help_desc_2}", - disclaimer: S.current.mweb_help_disclaimer), - popOnConfirmation: false); + title: "${S.current.unmask} Litecoin", + showAddressField: false, + confirmSheetIconPath: "assets/new-ui/unmask.svg", + helpContent: SendPageHelpContent( + title: S.current.about_litecoin_privacy, + imagePath: "assets/new-ui/mweb_help.svg", + description: "${S.current.mweb_help_desc_1}\n\n${S.current.mweb_help_desc_2}", + disclaimer: S.current.mweb_help_disclaimer, + ), + popOnConfirmation: false, + ); static final all = [normal, lightningDeposit, lightningWithdrawal, mwebDeposit, mwebWithdrawal]; } class SendPageParams { - final PaymentRequest? initialPaymentRequest; - final SendPageModes mode; - final CryptoCurrency? initialCurrency; - final UnspentCoinType unspentCoinType; - SendPageParams({ this.initialPaymentRequest, SendPageModes? mode, this.unspentCoinType = UnspentCoinType.any, this.initialCurrency, }) : mode = mode ?? SendPageModes.normal; + + final PaymentRequest? initialPaymentRequest; + final SendPageModes mode; + final CryptoCurrency? initialCurrency; + final UnspentCoinType unspentCoinType; } class NewSendPage extends StatefulWidget { - NewSendPage( - {super.key, - required this.sendViewModel, - required this.paymentViewModel, - required this.walletSwitcherViewModel, - required this.contactListViewModel, - required this.authService, - required SendPageParams params}) - : initialPaymentRequest = params.initialPaymentRequest, + NewSendPage({ + required this.sendViewModel, + required this.paymentViewModel, + required this.walletSwitcherViewModel, + required this.contactListViewModel, + required this.authService, + required SendPageParams params, + super.key, + }) : initialPaymentRequest = params.initialPaymentRequest, mode = params.mode { if (params.initialCurrency != null) { sendViewModel.selectedCryptoCurrency = params.initialCurrency!; @@ -205,20 +217,20 @@ class _NewSendPageState extends State { super.initState(); _addInputControllers(); - reaction((_) => widget.sendViewModel.outputs[_selectedOutput].sendAll, ((bool all) { + reaction((_) => widget.sendViewModel.outputs[_selectedOutput].sendAll, (all) { if (all) { widget.sendViewModel.outputs[_selectedOutput].isFiatEntry = false; _amountControllers[_selectedOutput].text = S.current.all; } - })); + }); - reaction((_) => widget.sendViewModel.outputs[_selectedOutput].address, ((address) { + reaction((_) => widget.sendViewModel.outputs[_selectedOutput].address, (address) { if (_addressControllers[_selectedOutput].text != address) { _addressControllers[_selectedOutput].text = address; } - })); + }); - reaction((_) => widget.sendViewModel.outputs[_selectedOutput].memo, (String memo) { + reaction((_) => widget.sendViewModel.outputs[_selectedOutput].memo, (memo) { if (memo != _memoControllers[_selectedOutput].text) { _memoControllers[_selectedOutput].text = memo; } @@ -229,7 +241,9 @@ class _NewSendPageState extends State { widget.initialPaymentRequest!.scheme.toLowerCase()) { _addressControllers[0].text = widget.initialPaymentRequest!.address; _amountControllers[0].text = widget.initialPaymentRequest!.amount; - _memoControllers[0].text = widget.initialPaymentRequest!.note; + // _memoControllers[0].text = widget.initialPaymentRequest!.note; + _applyNote(widget.initialPaymentRequest!.note, 0); + final contractAddress = widget.initialPaymentRequest!.contractAddress; if (contractAddress != null && contractAddress.isNotEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -270,7 +284,9 @@ class _NewSendPageState extends State { Future _resolveAddressForOutput(Output output) async { final result = await widget.sendViewModel.resolveAddressForOutput(output); - if (result == null) return; + if (result == null || !mounted) { + return; + } final confirmed = await showParsedAddressConfirmationAlert(context, result); @@ -291,363 +307,399 @@ class _NewSendPageState extends State { } @override - Widget build(BuildContext context) { - return Observer(builder: (_) { - final output = widget.sendViewModel.outputs[_selectedOutput]; - return SafeArea( - bottom: false, - child: KeyboardHideOverlay( - unfocusOnTap: true, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.vertical(top: Radius.circular(24))), - child: SafeArea( - child: Column( - spacing: 12, - mainAxisSize: MainAxisSize.max, - children: [ - ModalTopBar( - title: widget.mode.title, - subtitle: widget.mode.description, - leadingIcon: Icon(Icons.close), - onLeadingPressed: Navigator.of(context, rootNavigator: true).pop, - trailingWidget: Observer( - builder: (_) => Row( - spacing: 8, - children: [ - if (widget.sendViewModel.outputs.length > 1) - ModernButton( - size: 36, - icon: CakeImageWidget( - imageUrl: "assets/new-ui/remove_recipient.svg", - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.primary, - BlendMode.srcIn, + Widget build(BuildContext context) => Observer( + builder: (_) { + final output = widget.sendViewModel.outputs[_selectedOutput]; + return SafeArea( + bottom: false, + child: KeyboardHideOverlay( + unfocusOnTap: true, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + ), + child: SafeArea( + child: Column( + spacing: 12, + mainAxisSize: MainAxisSize.max, + children: [ + ModalTopBar( + title: widget.mode.title, + subtitle: widget.mode.description, + leadingIcon: const Icon(Icons.close), + leadingSemanticLabel: S.of(context).close, + onLeadingPressed: Navigator.of(context, rootNavigator: true).pop, + trailingWidget: Observer( + builder: (_) => Row( + spacing: 8, + children: [ + if (widget.sendViewModel.outputs.length > 1) + ModernButton( + size: 36, + icon: CakeImageWidget( + imageUrl: "assets/new-ui/remove_recipient.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), ), + semanticLabel: S.of(context).remove, + onPressed: () { + final outputIndex = _selectedOutput; + if (_selectedOutput != 0) { + _setOutput(_selectedOutput - 1); + } else { + _setOutput(1); + } + _removeInputControllers(outputIndex); + widget.sendViewModel.removeOutput(output); + if (outputIndex == 0) { + _setOutput(0); + } + }, ), - onPressed: () { - final outputIndex = _selectedOutput; - if (_selectedOutput != 0) { - _setOutput(_selectedOutput - 1); - } else { - _setOutput(1); - } - _removeInputControllers(outputIndex); - widget.sendViewModel.removeOutput(output); - if (outputIndex == 0) _setOutput(0); - }), - if (widget.mode == SendPageModes.normal && - widget.sendViewModel.hasMultiRecipient) - ModernButton( - size: 36, - icon: Icon(Icons.add), - onPressed: () { - _addInputControllers(); - widget.sendViewModel.addOutput(); - _setOutput(widget.sendViewModel.outputs.length - 1); - }), - if (widget.mode.helpContent != null) - ModernButton( - size: 36, - icon: CakeImageWidget( - imageUrl: "assets/new-ui/help.svg", - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.primary, BlendMode.srcIn), + if (widget.mode == SendPageModes.normal && + widget.sendViewModel.hasMultiRecipient) + ModernButton( + size: 36, + icon: const Icon(Icons.add), + semanticLabel: S.of(context).add_receiver, + onPressed: () { + _addInputControllers(); + widget.sendViewModel.addOutput(); + _setOutput(widget.sendViewModel.outputs.length - 1); + }, ), - onPressed: () { - Navigator.of(context).push(CupertinoPageRoute( - builder: (context) => Material( - child: SendHelpPage(content: widget.mode.helpContent!)))); - }) - ], - ), - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.max, - children: [ - Form( - key: _formKey, - child: DirectionalAnimatedSwitcher( - duration: Duration(milliseconds: 300), - child: Column( - key: ValueKey(_selectedOutput), - spacing: 24, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.mode.showAddressField) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 12, - children: [ - Text(S.of(context).address_or_alias), - NewSendAddressInput( - displayName: output.displayName, - validator: output.isParsedAddress - ? widget.sendViewModel.textValidator - : widget.sendViewModel.addressValidator, - addressController: _addressControllers[_selectedOutput], - focusNode: _addressFocusNode, - onURIScanned: (uri) async { - output.resetParsedAddress(); - await _resolveAddressForOutput(output); - - // Process the payment through the new flow - await _handlePaymentFlow( - uri.toString(), - PaymentRequest.fromString(uri.toString()), - ); - }, - onEditingComplete: () { - _addressFocusNode.unfocus(); - // output.fetchParsedAddress(context).then((val){ - // if(_addressControllers[_selectedOutput].text != output.extractedAddress) { - // _addressControllers[_selectedOutput].text = output.extractedAddress; - // } - // }); - }, - onPushAddressBookButton: (context) async { - output.resetParsedAddress(); - }, - onSelectedContact: (contact) { - output.loadContact(contact); - }, - onPushPasteButton: (context) async { - if (_justHandledPasteButton) return; - _justHandledPasteButton = true; - try { - output.resetParsedAddress(); - await _resolveAddressForOutput(output); - - final address = output.isParsedAddress - ? output.extractedAddress - : output.address; - - await _handlePaymentFlow( - address, - PaymentRequest( - address, - _amountControllers[_selectedOutput].text, - _memoControllers[_selectedOutput].text, - "", - null, - ), - ); - } finally { - _justHandledPasteButton = false; - } - - _handleLightningInvoicePaste(); - }, - selectedCurrency: - widget.sendViewModel.selectedCryptoCurrency, + if (widget.mode.helpContent != null) + ModernButton( + size: 36, + icon: CakeImageWidget( + imageUrl: "assets/new-ui/help.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + semanticLabel: S.of(context).help, + onPressed: () { + Navigator.of(context).push( + CupertinoPageRoute( + builder: (context) => Material( + child: SendHelpPage(content: widget.mode.helpContent!), ), - ], - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 12, - children: [ - Text(S.of(context).amount), - NewSendAmountInput( - validator: output.sendAll - ? widget.sendViewModel.allAmountValidator - : widget.sendViewModel.amountValidator(output), - amountController: _amountControllers[_selectedOutput], - currency: output.isFiatEntry - ? widget.sendViewModel.fiatCurrency.title - : widget.sendViewModel.selectedCryptoCurrencySymbol, - currencyIconPath: output.isFiatEntry - ? "" - : widget.sendViewModel.selectedCryptoCurrency - .iconPath ?? - "", - hasPicker: (output.isFiatEntry || - widget.sendViewModel.hasMultipleTokens), - onPickerClicked: () => _presentCurrencyPicker(context), - maxDecimals: output.isFiatEntry - ? widget.sendViewModel.fiatCurrency.decimals - : widget.sendViewModel.useBaseUnits - ? 0 - : widget - .sendViewModel.selectedCryptoCurrency.decimals, ), - FiatAmountBar( - fiatInputMode: output.isFiatEntry, - onSwitchButtonPressed: () { - widget.sendViewModel.outputs[_selectedOutput] - .isFiatEntry = !output.isFiatEntry; - _amountControllers[_selectedOutput].text = - output.isFiatEntry - ? output.fiatAmount - : output.displayCryptoAmount; - }, - fiatAmount: _wrapAmount(output.roundedFiatAmount(6), 20), - cryptoAmount: - _wrapAmount(output.roundedCryptoAmount(6), 20), - allAmount: widget.sendViewModel.balance, - cryptoCurrencySymbol: - widget.sendViewModel.selectedCryptoCurrencySymbol, - fiatCurrencySymbol: - widget.sendViewModel.fiatCurrency.symbol, - onAllButtonPressed: () async { - output.setSendAll( - await widget.sendViewModel.sendingBalance); - await output.calculateEstimatedFee(); - }, - ), - ], - ), - if (widget.sendViewModel.isMwebAvailable && - widget.mode == SendPageModes.normal) - StandardCheckbox( - caption: S.of(context).litecoin_mweb_allow_coins, - captionColor: Theme.of(context).colorScheme.onSurface, - borderColor: Theme.of(context).colorScheme.primary, - iconColor: Theme.of(context).colorScheme.primary, - value: [UnspentCoinType.any, UnspentCoinType.mweb] - .contains(widget.sendViewModel.coinTypeToSpendFrom), - onChanged: (value) => - widget.sendViewModel.setAllowMwebCoins(value), - ), - if (widget.sendViewModel.hasMemos) - Observer( - builder: (_) => NewSendMemoInput( - memoController: _memoControllers[_selectedOutput], - maxMemoLength: widget.sendViewModel.maxMemoLength, - memoLength: output.memo.length, - )), - if (widget.sendViewModel.hasCoinControl || - widget.sendViewModel.hasFees) - AnimatedDropdown( - dropdownText: S.of(context).advanced_settings, - content: Column(children: [ - if (widget.sendViewModel.hasFees) - ListItemRegularRowWidget( - keyValue: "", - label: S.of(context).fees, - subtitle: - "~${output.estimatedFee} ${widget.sendViewModel.currencySymbol} (${output.estimatedFeeFiatAmount} ${widget.sendViewModel.fiatCurrency})", - onTap: () { - if (widget - .sendViewModel.feesViewModel.hasFeesPriority) - pickTransactionPriority(context, output); - }, - ), - if (widget.sendViewModel.hasCoinControl) - ListItemRegularRowWidget( - keyValue: "", - label: "Coin Control", - onTap: () { - showCupertinoModalBottomSheet( - enableDrag: false, - useRootNavigator: true, - isDismissible: false, - context: context, - builder: (context) { - return NewCoinControlPage( - unspentCoinsListViewModel: widget - .sendViewModel - .unspentCoinsListViewModel, - canEdit: true); - }); - }), - ]), - ) - ], - ), - ), - ), - Observer( - builder: (_) => Column( - spacing: 12, - children: [ - if (!widget.sendViewModel.isReadyForSend) - SendSyncingIndicator( - status: widget.sendViewModel.wallet.syncStatus), - if (widget.sendViewModel.outputs.length > 1) - RecipientDotRow( - numDots: widget.sendViewModel.outputs.length, - onSelected: _setOutput, - selectedDot: _selectedOutput, - ), - Observer( - builder: (_) { - return LoadingPrimaryButton( - key: ValueKey('send_page_send_button_key'), - onPressed: () async { - //Request dummy node to get the focus out of the text fields - FocusScope.of(context).requestFocus(FocusNode()); - - if (widget.sendViewModel.state is IsExecutingState) return; - - if (widget.mode == SendPageModes.normal) { - _handleSend(); - } else if (widget.mode == SendPageModes.lightningDeposit || - widget.mode == SendPageModes.mwebDeposit) { - Navigator.of(context).push(CupertinoPageRoute( - builder: (context) => Material( - child: L2ActionWalletSelector( - showOtherWallets: false, - action: l2actions.deposit, - sendViewModel: widget.sendViewModel, - contactListViewModel: - widget.contactListViewModel, - walletSwitcherViewModel: - widget.walletSwitcherViewModel, - onSendInitiated: _handleSend, - )))); - } else if (widget.mode == - SendPageModes.lightningWithdrawal || - widget.mode == SendPageModes.mwebWithdrawal) { - Navigator.of(context).push(CupertinoPageRoute( - builder: (context) => Material( - child: L2ActionWalletSelector( - showOtherWallets: false, - action: l2actions.withdraw, - sendViewModel: widget.sendViewModel, - contactListViewModel: - widget.contactListViewModel, - walletSwitcherViewModel: - widget.walletSwitcherViewModel, - onSendInitiated: _handleSend, - )))); - } - }, - text: S.of(context).continue_text, - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimary, - isLoading: widget.sendViewModel.state is IsExecutingState || - widget.sendViewModel.state is TransactionCommitting || - widget.sendViewModel.state - is IsAwaitingDeviceResponseState || - widget.sendViewModel.state - is LoadingTemplateExecutingState, - isDisabled: !widget.sendViewModel.isReadyForSend || - widget.sendViewModel.state is ExecutedSuccessfullyState, ); }, ), - SizedBox(), - ], - ), - ) - ], + ], + ), + ), ), - ), - ) - ], + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + Form( + key: _formKey, + child: DirectionalAnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: Column( + key: ValueKey(_selectedOutput), + spacing: 24, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.mode.showAddressField) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 12, + children: [ + // NewSendAddressInput merges this label onto its + // own text-field node, so announcing the caption + // as well would read it twice. + ExcludeSemantics( + child: Text(S.of(context).address_or_alias), + ), + NewSendAddressInput( + displayName: output.displayName, + validator: output.isParsedAddress + ? widget.sendViewModel.textValidator + : widget.sendViewModel.addressValidator, + addressController: + _addressControllers[_selectedOutput], + focusNode: _addressFocusNode, + onURIScanned: (uri) async { + output.resetParsedAddress(); + await _resolveAddressForOutput(output); + + // Process the payment through the new flow + await _handlePaymentFlow( + uri.toString(), + PaymentRequest.fromString(uri.toString()), + ); + }, + onEditingComplete: _addressFocusNode.unfocus, + onPushAddressBookButton: (_) { + output.resetParsedAddress(); + }, + onSelectedContact: (contact) { + output.loadContact(contact); + }, + onPushPasteButton: (context) async { + if (_justHandledPasteButton) { + return; + } + _justHandledPasteButton = true; + try { + output.resetParsedAddress(); + await _resolveAddressForOutput(output); + + final address = output.isParsedAddress + ? output.extractedAddress + : output.address; + + await _handlePaymentFlow( + address, + PaymentRequest( + address, + _amountControllers[_selectedOutput].text, + _memoControllers[_selectedOutput].text, + "", + null, + ), + ); + } finally { + _justHandledPasteButton = false; + } + + _handleLightningInvoicePaste(); + }, + selectedCurrency: + widget.sendViewModel.selectedCryptoCurrency, + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 12, + children: [ + // This caption is the amount field's accessible name + // and must stay in the semantics tree: + // NewSendAmountInput deliberately carries no label of + // its own, because labelling the field made Android + // announce the amount twice. Covered by + // test/new-ui/widgets/send_page/send_amount_input_test.dart. + Text(S.of(context).amount), + NewSendAmountInput( + validator: output.sendAll + ? widget.sendViewModel.allAmountValidator + : widget.sendViewModel.amountValidator(output), + amountController: _amountControllers[_selectedOutput], + currency: output.isFiatEntry + ? widget.sendViewModel.fiatCurrency.title + : widget.sendViewModel.selectedCryptoCurrencySymbol, + currencyIconPath: output.isFiatEntry + ? "" + : widget.sendViewModel.selectedCryptoCurrency + .iconPath ?? + "", + hasPicker: output.isFiatEntry || + widget.sendViewModel.hasMultipleTokens, + onPickerClicked: () => _presentCurrencyPicker(context), + maxDecimals: output.isFiatEntry + ? widget.sendViewModel.fiatCurrency.decimals + : widget.sendViewModel.useBaseUnits + ? 0 + : widget.sendViewModel.selectedCryptoCurrency + .decimals, + ), + FiatAmountBar( + fiatInputMode: output.isFiatEntry, + onSwitchButtonPressed: _onFiatSwitchPressed, + fiatAmount: + _wrapAmount(output.roundedFiatAmount(6), 20), + cryptoAmount: + _wrapAmount(output.roundedCryptoAmount(6), 20), + allAmount: widget.sendViewModel.balance, + cryptoCurrencySymbol: + widget.sendViewModel.selectedCryptoCurrencySymbol, + fiatCurrencySymbol: + widget.sendViewModel.fiatCurrency.symbol, + onAllButtonPressed: () async { + output.setSendAll( + await widget.sendViewModel.sendingBalance, + ); + await output.calculateEstimatedFee(); + }, + ), + ], + ), + if (widget.sendViewModel.isMwebAvailable && + widget.mode == SendPageModes.normal) + StandardCheckbox( + caption: S.of(context).litecoin_mweb_allow_coins, + captionColor: Theme.of(context).colorScheme.onSurface, + borderColor: Theme.of(context).colorScheme.primary, + iconColor: Theme.of(context).colorScheme.primary, + value: [UnspentCoinType.any, UnspentCoinType.mweb] + .contains(widget.sendViewModel.coinTypeToSpendFrom), + onChanged: (value) => + widget.sendViewModel.setAllowMwebCoins(value), + ), + if (widget.sendViewModel.hasMemos) + Observer( + builder: (_) => NewSendMemoInput( + memoController: _memoControllers[_selectedOutput], + maxMemoLength: widget.sendViewModel.maxMemoLength, + memoLength: output.memo.length, + ), + ), + if (widget.sendViewModel.hasCoinControl || + widget.sendViewModel.hasFees) + AnimatedDropdown( + dropdownText: S.of(context).advanced_settings, + content: Column( + children: [ + if (widget.sendViewModel.hasFees) + ListItemRegularRowWidget( + keyValue: "", + label: S.of(context).fees, + subtitle: + "~${output.estimatedFee} ${widget.sendViewModel.currencySymbol} (${output.estimatedFeeFiatAmount} ${widget.sendViewModel.fiatCurrency})", + // Without fee priorities the row does nothing, + // so it must not be announced as interactive. + onTap: widget.sendViewModel.feesViewModel + .hasFeesPriority + ? () => + pickTransactionPriority(context, output) + : null, + ), + if (widget.sendViewModel.hasCoinControl) + ListItemRegularRowWidget( + keyValue: "", + label: S.of(context).coin_control, + onTap: () { + showCupertinoModalBottomSheet( + enableDrag: false, + useRootNavigator: true, + isDismissible: false, + context: context, + builder: (context) => NewCoinControlPage( + unspentCoinsListViewModel: widget + .sendViewModel + .unspentCoinsListViewModel, + canEdit: true, + ), + ); + }, + ), + ], + ), + ), + ], + ), + ), + ), + Observer( + builder: (_) => Column( + spacing: 12, + children: [ + if (!widget.sendViewModel.isReadyForSend) + SendSyncingIndicator( + status: widget.sendViewModel.wallet.syncStatus, + ), + if (widget.sendViewModel.outputs.length > 1) + RecipientDotRow( + numDots: widget.sendViewModel.outputs.length, + onSelected: _setOutput, + selectedDot: _selectedOutput, + ), + Observer( + builder: (_) => NewFuturePrimaryButton( + key: const ValueKey("send_page_send_button_key"), + onPressed: () async { + //Request dummy node to get the focus out of the text fields + FocusScope.of(context).requestFocus(FocusNode()); + + if (widget.sendViewModel.state is IsExecutingState) { + return; + } + + if (widget.mode == SendPageModes.normal) { + await _handleSend(); + } else if (widget.mode == + SendPageModes.lightningDeposit || + widget.mode == SendPageModes.mwebDeposit) { + await Navigator.of(context).push( + CupertinoPageRoute( + builder: (context) => Material( + child: L2ActionWalletSelector( + showOtherWallets: false, + action: L2Actions.deposit, + sendViewModel: widget.sendViewModel, + contactListViewModel: + widget.contactListViewModel, + walletSwitcherViewModel: + widget.walletSwitcherViewModel, + onSendInitiated: _handleSend, + ), + ), + ), + ); + } else if (widget.mode == + SendPageModes.lightningWithdrawal || + widget.mode == SendPageModes.mwebWithdrawal) { + await Navigator.of(context).push( + CupertinoPageRoute( + builder: (context) => Material( + child: L2ActionWalletSelector( + showOtherWallets: false, + action: L2Actions.withdraw, + sendViewModel: widget.sendViewModel, + contactListViewModel: + widget.contactListViewModel, + walletSwitcherViewModel: + widget.walletSwitcherViewModel, + onSendInitiated: _handleSend, + ), + ), + ), + ); + } + }, + text: S.of(context).continue_text, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + disabled: !widget.sendViewModel.isReadyForSend || + widget.sendViewModel.state is ExecutedSuccessfullyState, + ), + ), + const SizedBox(), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ), ), ), - ), - ), + ); + }, ); - }); + + void _onFiatSwitchPressed() { + final output = widget.sendViewModel.outputs[_selectedOutput]; + widget.sendViewModel.outputs[_selectedOutput].isFiatEntry = !output.isFiatEntry; + + final amount = output.isFiatEntry ? output.fiatAmount : output.displayCryptoAmount; + _amountControllers[_selectedOutput].text = amount.startsWith("<") ? "0" : amount; } void _setOutput(int index) { @@ -667,7 +719,8 @@ class _NewSendPageState extends State { _amountControllers[_amountControllers.length - 1].addListener(() { if (_selectedOutput > widget.sendViewModel.outputs.length - 1) { printV( - "_selectedOutput > widget.sendViewModel.outputs.length - 1! this should NOT happen!"); + "_selectedOutput > widget.sendViewModel.outputs.length - 1! this should NOT happen!", + ); return; } @@ -685,10 +738,14 @@ class _NewSendPageState extends State { output.sendAll = false; } - if (S.current.all.contains(amount)) return; + if (S.current.all.contains(amount)) { + return; + } final cAmount = widget.sendViewModel.amountParsingProxy.getDisplayCryptoAmount( - output.cryptoAmount, widget.sendViewModel.selectedCryptoCurrency); + output.cryptoAmount, + widget.sendViewModel.selectedCryptoCurrency, + ); if (amount != cAmount) { final newAmount = widget.sendViewModel.amountParsingProxy .getCanonicalCryptoAmount(amount, widget.sendViewModel.selectedCryptoCurrency); @@ -700,7 +757,8 @@ class _NewSendPageState extends State { _addressControllers[_amountControllers.length - 1].addListener(() { if (_selectedOutput > widget.sendViewModel.outputs.length - 1) { printV( - "_selectedOutput > widget.sendViewModel.outputs.length - 1! this should NOT happen!"); + "_selectedOutput > widget.sendViewModel.outputs.length - 1! this should NOT happen!", + ); return; } @@ -716,7 +774,8 @@ class _NewSendPageState extends State { _memoControllers[_amountControllers.length - 1].addListener(() { if (_selectedOutput > widget.sendViewModel.outputs.length - 1) { printV( - "_selectedOutput > widget.sendViewModel.outputs.length - 1! this should NOT happen!"); + "_selectedOutput > widget.sendViewModel.outputs.length - 1! this should NOT happen!", + ); return; } final memo = _memoControllers[_selectedOutput].text; @@ -728,8 +787,8 @@ class _NewSendPageState extends State { }); } - void _handleSend() async { - //TODO refactor this action. code was copied over from old ui. i don't like it. + Future _handleSend() async { + //TODO(malik1004x): refactor this action. code was copied over from old ui. i don't like it. for (var i = 0; i < widget.sendViewModel.outputs.length; i++) { if (i < _amountControllers.length && !widget.sendViewModel.outputs[i].sendAll) { @@ -737,7 +796,9 @@ class _NewSendPageState extends State { widget.sendViewModel.outputs[i].setFiatAmount(_amountControllers[i].text); } else { final amount = widget.sendViewModel.amountParsingProxy.getCanonicalCryptoAmount( - _amountControllers[i].text.sanitized(), widget.sendViewModel.selectedCryptoCurrency); + _amountControllers[i].text.sanitized(), + widget.sendViewModel.selectedCryptoCurrency, + ); widget.sendViewModel.outputs[i].setCryptoAmount(amount); } } @@ -763,34 +824,40 @@ class _NewSendPageState extends State { if (widget.sendViewModel.wallet.isHardwareWallet) { if (!widget.sendViewModel.hardwareWalletViewModel! .isConnected(widget.sendViewModel.walletType)) { - await Navigator.of(context).pushNamed(Routes.connectDevices, - arguments: ConnectDevicePageParams( - walletType: widget.sendViewModel.walletType, - hardwareWalletType: widget.sendViewModel.wallet.walletInfo.hardwareWalletType!, - onConnectDevice: (_, __) { - widget.sendViewModel.hardwareWalletViewModel! - .initWallet(widget.sendViewModel.wallet); - Navigator.of(context).pop(); - }, - )); + await Navigator.of(context).pushNamed( + Routes.connectDevices, + arguments: ConnectDevicePageParams( + walletType: widget.sendViewModel.walletType, + hardwareWalletType: widget.sendViewModel.wallet.walletInfo.hardwareWalletType!, + onConnectDevice: (_, __) { + widget.sendViewModel.hardwareWalletViewModel!.initWallet(widget.sendViewModel.wallet); + Navigator.of(context).pop(); + }, + isReconnect: false, + ), + ); + + // Recheck to handle tap-backs + if (!widget.sendViewModel.hardwareWalletViewModel! + .isConnected(widget.sendViewModel.walletType)) { + return; + } } else { - widget.sendViewModel.hardwareWalletViewModel!.initWallet(widget.sendViewModel.wallet); + await widget.sendViewModel.hardwareWalletViewModel!.initWallet(widget.sendViewModel.wallet); } } if (widget.sendViewModel.wallet.type == WalletType.monero) { var amount = Money.zero(widget.sendViewModel.wallet.currency); - for (var item in widget.sendViewModel.outputs) { + for (final item in widget.sendViewModel.outputs) { amount += item.cryptoAmountMoney; } - if (monero!.needExportOutputs(widget.sendViewModel.wallet, amount)) { - if (widget.sendViewModel.wallet.hardwareWalletType == HardwareWalletType.trezor) { - await monero!.syncTrezor(widget.sendViewModel.wallet); - } else { - await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, - arguments: monero!.exportOutputsUR(widget.sendViewModel.wallet)); - } - await Future.delayed(Duration(seconds: 1)); // wait for monero to refresh the state + if (monero!.hasUnknownKeyImages(widget.sendViewModel.wallet) && mounted) { + await Navigator.of(context).pushNamed( + Routes.syncKeyImagesDevices, + arguments: monero!.exportOutputsUR(widget.sendViewModel.wallet), + ); + await Future.delayed(const Duration(seconds: 1)); // wait for monero to refresh the state } if (monero!.needExportOutputs(widget.sendViewModel.wallet, amount)) { return; @@ -807,21 +874,22 @@ class _NewSendPageState extends State { Navigator.of(context, rootNavigator: true).pop(); } showModalBottomSheet( - isScrollControlled: true, - isDismissible: false, - enableDrag: false, - context: navigatorKey.currentContext ?? context, - backgroundColor: Colors.transparent, - builder: (context) { - return SendConfirmSheet( - title: widget.mode.title, - iconPath: widget.mode.helpContent?.imagePath, - sendViewModel: widget.sendViewModel, - ); - }).then((value) async { + isScrollControlled: true, + isDismissible: false, + enableDrag: false, + context: navigatorKey.currentContext ?? context, + backgroundColor: Colors.transparent, + builder: (context) => SendConfirmSheet( + title: widget.mode.title, + iconPath: widget.mode.helpContent?.imagePath, + sendViewModel: widget.sendViewModel, + ), + ).then((value) { if (widget.sendViewModel.state is TransactionCommitted && widget.mode.popOnConfirmation) { - if (!mounted) return; + if (!mounted) { + return; + } Navigator.of(context, rootNavigator: true).pop(); } widget.sendViewModel.dismissTransaction(); @@ -842,7 +910,9 @@ class _NewSendPageState extends State { void _presentCurrencyPicker(BuildContext context) { final output = widget.sendViewModel.outputs[_selectedOutput]; - if (!output.isFiatEntry && !widget.sendViewModel.hasMultipleTokens) return; + if (!output.isFiatEntry && !widget.sendViewModel.hasMultipleTokens) { + return; + } if (output.isFiatEntry) { FiatCurrencyPickerSheet.show( @@ -860,8 +930,8 @@ class _NewSendPageState extends State { final balanceByAsset = { for (final r in widget.sendViewModel.balanceViewModel.formattedBalances) r.asset: CurrencyPickerBalance( - amount: '${r.availableBalance} ${r.asset.title}', - fiat: isFiatDisabled ? null : '${r.fiatAvailableBalanceRaw} ${r.fiatCurrency?.symbol}', + amount: "${r.availableBalance} ${r.asset.title}", + fiat: isFiatDisabled ? null : "${r.fiatAvailableBalanceRaw} ${r.fiatCurrency?.symbol}", fiatValue: isFiatDisabled ? null : double.tryParse(r.fiatAvailableBalanceRaw), ), }; @@ -894,12 +964,16 @@ class _NewSendPageState extends State { } Future _handlePaymentFlow(String uri, PaymentRequest paymentRequest) async { - if (uri.contains('@') || paymentRequest.address.contains('@')) return; + if (uri.contains("@") || paymentRequest.address.contains("@")) { + return; + } if (OpenCryptoPayService.isOpenCryptoPayQR(uri) && widget.sendViewModel.selectedCryptoCurrency != CryptoCurrency.btcln) { final request = await widget.sendViewModel.getOpenCryptoPayRequest(uri); - if (request == null) return; + if (request == null) { + return; + } _applyPaymentRequest(request); return; } @@ -960,7 +1034,7 @@ class _NewSendPageState extends State { break; } } catch (e) { - printV('Payment flow error: $e'); + printV("Payment flow error: $e"); _applyPaymentRequest(paymentRequest); } } @@ -979,34 +1053,32 @@ class _NewSendPageState extends State { context: context, isDismissible: true, isScrollControlled: true, - builder: (BuildContext context) { - return PaymentConfirmationBottomSheet( - paymentFlowResult: result, - paymentViewModel: paymentViewModel, - walletSwitcherViewModel: walletSwitcherViewModel, - paymentRequest: paymentRequest, - onSelectWallet: () => _handleSelectWallet( - paymentViewModel, - walletSwitcherViewModel, - paymentRequest, - result, - ), - onChangeWallet: () => _handleChangeWallet( - paymentViewModel, - walletSwitcherViewModel, - paymentRequest, - result, - ), - onSwap: (bottomSheetContext) => - _handleSwapFlow(paymentViewModel, result, bottomSheetContext), - onSwitchNetwork: () => _handleSwitchNetwork( - paymentViewModel, - walletSwitcherViewModel, - paymentRequest, - result, - ), - ); - }, + builder: (context) => PaymentConfirmationBottomSheet( + paymentFlowResult: result, + paymentViewModel: paymentViewModel, + walletSwitcherViewModel: walletSwitcherViewModel, + paymentRequest: paymentRequest, + onSelectWallet: () => _handleSelectWallet( + paymentViewModel, + walletSwitcherViewModel, + paymentRequest, + result, + ), + onChangeWallet: () => _handleChangeWallet( + paymentViewModel, + walletSwitcherViewModel, + paymentRequest, + result, + ), + onSwap: (bottomSheetContext) => + _handleSwapFlow(paymentViewModel, result, bottomSheetContext), + onSwitchNetwork: () => _handleSwitchNetwork( + paymentViewModel, + walletSwitcherViewModel, + paymentRequest, + result, + ), + ), ); } @@ -1024,32 +1096,30 @@ class _NewSendPageState extends State { context: context, isDismissible: true, isScrollControlled: true, - builder: (BuildContext context) { - return TokenSelectionBottomSheet( - paymentViewModel: paymentViewModel, - paymentRequest: paymentRequest, - fixedNetwork: fixedNetwork, - onNext: (PaymentFlowResult newResult) { - final selectedChainId = newResult.chainId; - final isCompatible = - selectedChainId == evm?.getSelectedChainId(widget.sendViewModel.wallet); + builder: (context) => TokenSelectionBottomSheet( + paymentViewModel: paymentViewModel, + paymentRequest: paymentRequest, + fixedNetwork: fixedNetwork, + onNext: (newResult) { + final selectedChainId = newResult.chainId; + final isCompatible = + selectedChainId == evm?.getSelectedChainId(widget.sendViewModel.wallet); - if (isCompatible) { - widget.sendViewModel.setSelectedCryptoCurrency( - newResult.addressDetectionResult!.detectedCurrency!.title, - ); - _applyPaymentRequest(paymentRequest); - } else { - _showPaymentConfirmation( - paymentViewModel, - walletSwitcherViewModel, - paymentRequest, - newResult, - ); - } - }, - ); - }, + if (isCompatible) { + widget.sendViewModel.setSelectedCryptoCurrency( + newResult.addressDetectionResult!.detectedCurrency!.title, + ); + _applyPaymentRequest(paymentRequest); + } else { + _showPaymentConfirmation( + paymentViewModel, + walletSwitcherViewModel, + paymentRequest, + newResult, + ); + } + }, + ), ); } @@ -1065,12 +1135,10 @@ class _NewSendPageState extends State { context: context, isDismissible: true, isScrollControlled: true, - builder: (BuildContext dialogContext) { - return WalletSwitcherBottomSheet( - viewModel: walletSwitcherViewModel, - filterWalletType: paymentViewModel.detectedWalletType, - ); - }, + builder: (dialogContext) => WalletSwitcherBottomSheet( + viewModel: walletSwitcherViewModel, + filterWalletType: paymentViewModel.detectedWalletType, + ), ); final success = await walletSwitcherViewModel.switchToSelectedWallet(); @@ -1115,7 +1183,7 @@ class _NewSendPageState extends State { showModalBottomSheet( context: context, isDismissible: false, - builder: (BuildContext context) { + builder: (context) { loadingBottomSheetContext = context; return LoadingBottomSheet( titleText: S.of(context).loading_your_wallet, @@ -1156,7 +1224,7 @@ class _NewSendPageState extends State { showModalBottomSheet( context: context, isDismissible: false, - builder: (BuildContext context) { + builder: (context) { loadingBottomSheetContext = context; return LoadingBottomSheet( titleText: S.of(context).loading_your_wallet, @@ -1195,7 +1263,9 @@ class _NewSendPageState extends State { PaymentRequest paymentRequest, PaymentFlowResult result, ) async { - if (result.type != PaymentFlowType.evmNetworkSelection || result.wallet == null) return; + if (result.type != PaymentFlowType.evmNetworkSelection || result.wallet == null) { + return; + } if (context.mounted && Navigator.of(context).canPop()) { Navigator.of(context).pop(); @@ -1207,11 +1277,9 @@ class _NewSendPageState extends State { showModalBottomSheet( context: context, isDismissible: false, - builder: (BuildContext context) { + builder: (context) { loadingBottomSheetContext = context; - return LoadingBottomSheet( - titleText: S.of(context).loading_your_wallet, - ); + return LoadingBottomSheet(titleText: S.of(context).loading_your_wallet); }, ); } @@ -1234,7 +1302,7 @@ class _NewSendPageState extends State { if (loadingBottomSheetContext != null && loadingBottomSheetContext!.mounted) { Navigator.of(loadingBottomSheetContext!).pop(); } - printV('Switch network error: $e'); + printV("Switch network error: $e"); } } @@ -1246,12 +1314,16 @@ class _NewSendPageState extends State { _addressControllers[_selectedOutput].text = paymentRequest.address; if (paymentRequest.amount.isNotEmpty) { try { - _amountControllers[_selectedOutput].text = widget.sendViewModel.amountParsingProxy - .getDisplayCryptoAmount( - paymentRequest.amount, widget.sendViewModel.selectedCryptoCurrency); + _amountControllers[_selectedOutput].text = + widget.sendViewModel.amountParsingProxy.getDisplayCryptoAmount( + paymentRequest.amount, + widget.sendViewModel.selectedCryptoCurrency, + ); } catch (e) {} } - _memoControllers[_selectedOutput].text = paymentRequest.note; + // _memoControllers[_selectedOutput].text = paymentRequest.note; + + _applyNote(paymentRequest.note, _selectedOutput); } Future _handleSwapFlow( @@ -1263,14 +1335,16 @@ class _NewSendPageState extends State { await Future.delayed(const Duration(milliseconds: 100)); - if (!mounted) return; + if (!mounted) { + return; + } final bottomSheet = getIt.get(param1: result); await showModalBottomSheet( context: context, isDismissible: true, isScrollControlled: true, - builder: (BuildContext context) => bottomSheet, + builder: (context) => bottomSheet, ); } @@ -1314,7 +1388,7 @@ class _NewSendPageState extends State { for (final type in supportedTypes) { final addressPattern = AddressValidator.getAddressFromStringPattern(type); if (addressPattern != null) { - final regex = RegExp('^$addressPattern\$'); + final regex = RegExp("^$addressPattern\$"); if (regex.hasMatch(trimmed)) { isValid = true; break; @@ -1323,7 +1397,9 @@ class _NewSendPageState extends State { } for (final pattern in excludedPatterns) { - if (pattern.hasMatch(trimmed)) return false; + if (pattern.hasMatch(trimmed)) { + return false; + } } return isValid; @@ -1341,116 +1417,133 @@ class _NewSendPageState extends State { await showCupertinoModalBottomSheet( context: pageContext, expand: false, - builder: (BuildContext modalContext) { + builder: (modalContext) { int selectedIdx = selectedItem; - return Observer(builder: (context) { - double? customFeeRate = isBitcoinWallet - ? widget.sendViewModel.feesViewModel.customBitcoinFeeRate.toDouble() - : null; - return StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - return IntrinsicHeight( + return Observer( + builder: (context) { + final customFeeRate = isBitcoinWallet + ? widget.sendViewModel.feesViewModel.customBitcoinFeeRate.toDouble() + : null; + return StatefulBuilder( + builder: (context, setState) => IntrinsicHeight( // height: MediaQuery.of(context).size.height*0.4, child: ModalNavigator( - parentContext: modalContext, - heightMode: ModalHeightModes.natural, - rootPage: Material( - child: NewPicker( - title: S.of(context).set_fees, - description: S.of(context).set_fees_desc, - sliderPageTitle: S.of(context).custom_fee, - sliderInitialValue: customFeeRate, - sliderMaxValue: maxCustomFeeRate, - sliderValueDescription: "sat/byte", - items: items - .map((item) => PickerItem( - title: item.title, - subtitle: item.description, - hint: item.hint, - value: item, - isSliderItem: items.indexOf(item) == customItemIndex, - )) - .toList(), - onItemSelected: (TransactionPriority priority) async { - widget.sendViewModel.feesViewModel.setTransactionPriority(priority); - setState(() => selectedIdx = items.indexOf(priority)); - await output.calculateEstimatedFee(); - }, - onSliderChanged: (double value) { - widget.sendViewModel.feesViewModel.customBitcoinFeeRate = value.round(); - }, - selectedIndex: selectedIdx), - )), - ); - }, - ); - }); + parentContext: modalContext, + heightMode: ModalHeightModes.natural, + rootPage: Material( + child: NewPicker( + title: S.of(context).set_fees, + description: S.of(context).set_fees_desc, + sliderPageTitle: S.of(context).custom_fee, + sliderInitialValue: customFeeRate, + sliderMaxValue: maxCustomFeeRate, + sliderValueDescription: "sat/byte", + items: items + .map( + (item) => PickerItem( + title: item.title, + subtitle: item.description, + hint: item.hint, + value: item, + isSliderItem: items.indexOf(item) == customItemIndex, + ), + ) + .toList(), + onItemSelected: (priority) async { + widget.sendViewModel.feesViewModel.setTransactionPriority(priority); + setState(() => selectedIdx = items.indexOf(priority)); + await output.calculateEstimatedFee(); + }, + onSliderChanged: (value) { + widget.sendViewModel.feesViewModel.customBitcoinFeeRate = value.round(); + }, + selectedIndex: selectedIdx, + ), + ), + ), + ), + ); + }, + ); }, ); } - String _wrapAmount(String amount, int maxChars) { - return amount.length <= maxChars ? amount : "${amount.substring(0, maxChars - 3)}..."; + String _wrapAmount(String amount, int maxChars) => + amount.length <= maxChars ? amount : "${amount.substring(0, maxChars - 3)}..."; + + // TODO: make a separate variable for memo in payment request model + void _applyNote(String note, int selectedOutput) { + if (widget.sendViewModel.hasMemos && note.length <= widget.sendViewModel.maxMemoLength) { + widget.sendViewModel.outputs[selectedOutput].memo = note; + } else { + widget.sendViewModel.outputs[selectedOutput].note = note; + } } } class SendHelpPage extends StatelessWidget { - const SendHelpPage({super.key, required this.content}); + const SendHelpPage({required this.content, super.key}); final SendPageHelpContent content; @override - Widget build(BuildContext context) { - return SafeArea( - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ModalTopBar( - title: content.title, - leadingIcon: Icon(Icons.arrow_back_ios_new), - onLeadingPressed: Navigator.of(context).pop, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 18.0), - child: Column( - spacing: 12, - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CakeImageWidget(imageUrl: content.imagePath), - Text( - content.description, - textAlign: TextAlign.center, - style: TextStyle( + Widget build(BuildContext context) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ModalTopBar( + title: content.title, + leadingIcon: const Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.of(context).seed_alert_back, + onLeadingPressed: Navigator.of(context).pop, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18), + child: Column( + spacing: 12, + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CakeImageWidget(imageUrl: content.imagePath), + Text( + content.description, + textAlign: TextAlign.center, + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface), - ), - if (content.disclaimer != null) ...[ - SizedBox(), - SizedBox(), - Text(content.disclaimer!, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + if (content.disclaimer != null) ...[ + const SizedBox(), + const SizedBox(), + Text( + content.disclaimer!, textAlign: TextAlign.center, style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurfaceVariant)), - ] - ], + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ], + ), ), - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 18), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18), child: NewPrimaryButton( - onPressed: Navigator.of(context).pop, - text: S.of(context).i_understand, - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimary)) - ], - ), - ); - } + onPressed: Navigator.of(context).pop, + text: S.of(context).i_understand, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + ), + ), + ], + ), + ); } Future showParsedAddressConfirmationAlert( @@ -1459,20 +1552,18 @@ Future showParsedAddressConfirmationAlert( ) async { final confirmed = await showPopUp( context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.of(context).address_detected, - headerTitleText: parsedAddress.profileName.isEmpty ? null : parsedAddress.profileName, - headerImageProfileUrl: parsedAddress.profileImageUrl.isEmpty - ? parsedAddress.addressSource.iconPath - : parsedAddress.profileImageUrl, - alertContent: S.of(context).extracted_address_content( - '${parsedAddress.handle} (${parsedAddress.addressSource.label})', - ), - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop(true), - ); - }, + builder: (context) => AlertWithOneAction( + alertTitle: S.of(context).address_detected, + headerTitleText: parsedAddress.profileName.isEmpty ? null : parsedAddress.profileName, + headerImageProfileUrl: parsedAddress.profileImageUrl.isEmpty + ? parsedAddress.addressSource.iconPath + : parsedAddress.profileImageUrl, + alertContent: S.of(context).extracted_address_content( + "${parsedAddress.handle} (${parsedAddress.addressSource.label})", + ), + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(true), + ), ); return confirmed ?? false; diff --git a/lib/new-ui/pages/settings_page.dart b/lib/new-ui/pages/settings_page.dart index f7b8d4c79..8aaec520d 100644 --- a/lib/new-ui/pages/settings_page.dart +++ b/lib/new-ui/pages/settings_page.dart @@ -9,10 +9,9 @@ import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import "package:cw_core/wallet_type.dart"; +import 'package:flutter/material.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; bool _trueFunc(DashboardViewModel _) => true; @@ -26,10 +25,21 @@ bool _hasMweb(DashboardViewModel vm) => vm.hasMweb; bool _hasWalletConnect(DashboardViewModel vm) => vm.hasWalletConnect; -bool _isCupcake(DashboardViewModel vm) => - vm.wallet.hardwareWalletType == HardwareWalletType.cupcake; +bool _requiresKeyImageSync(DashboardViewModel vm) => + vm.wallet.type == WalletType.monero && + [HardwareWalletType.cupcake, HardwareWalletType.trezor].contains(vm.wallet.hardwareWalletType); class SettingsListItem { + const SettingsListItem( + this.iconPath, + this.title, + this.route, { + this.requireAuth = false, + this.use2fa = _falseFunc, + this.condition = _trueFunc, + this.routeArgs, + }); + final String iconPath; final String title; final String route; @@ -37,21 +47,15 @@ class SettingsListItem { final bool requireAuth; final bool Function(DashboardViewModel) use2fa; final bool Function(DashboardViewModel) condition; - - const SettingsListItem(this.iconPath, this.title, this.route, - {this.routeArgs = null, - this.requireAuth = false, - this.use2fa = _falseFunc, - this.condition = _trueFunc}); } class SettingsSectionData { + const SettingsSectionData(this.title, this.titleIconPath, this.items); + final String title; final String titleIconPath; final List items; - const SettingsSectionData(this.title, this.titleIconPath, this.items); - static SettingsSectionData walletSettings = SettingsSectionData(S.current.wallet_settings, "assets/new-ui/wallet-setting.svg", [ SettingsListItem( @@ -71,9 +75,13 @@ class SettingsSectionData { condition: _hasWalletConnect), //SettingsListItem("assets/new-ui/settings_row_icons/silent-payments.svg", S.current.silent_payments_settings, Routes.silentPaymentsSettings, condition: _isBtc), //SettingsListItem("assets/new-ui/settings_row_icons/mweb.svg", S.current.litecoin_mweb_settings, Routes.mwebSettings, condition: _hasMweb), - SettingsListItem("assets/new-ui/settings_row_icons/cupcake.svg", S.current.export_outputs, - Routes.urqrAnimatedPage, - routeArgs: {'export-outputs': 'export-outputs'}, condition: _isCupcake), + SettingsListItem( + "assets/new-ui/settings_row_icons/sync-balance.svg", + S.current.resync_device, + Routes.syncKeyImagesDevices, + routeArgs: {'export-outputs': 'export-outputs'}, + condition: _requiresKeyImageSync, + ), SettingsListItem( "assets/new-ui/settings_row_icons/other.svg", S.current.other, Routes.otherSettingsPage), ]); @@ -152,8 +160,9 @@ class SettingsMainPage extends StatelessWidget { color: Theme.of(context).colorScheme.surface, child: Column(children: [ ModalTopBar( - title: "Settings", + title: S.of(context).settings_title, leadingIcon: Icon(Icons.close), + leadingSemanticLabel: S.of(context).close, onLeadingPressed: Navigator.of(context, rootNavigator: true).pop, onTrailingPressed: () {}, ), diff --git a/lib/new-ui/pages/swap_page.dart b/lib/new-ui/pages/swap_page.dart index b4719a85a..6d3d7049a 100644 --- a/lib/new-ui/pages/swap_page.dart +++ b/lib/new-ui/pages/swap_page.dart @@ -429,8 +429,8 @@ class _NewSwapPageState extends State { final parsedAddress = await widget.adrResService .resolve(query: domain, wallet: widget.exchangeViewModel.wallet, currency: currency); return parsedAddress.isNotEmpty - ? parsedAddress.first.parsedAddressByCurrencyMap[currency] ?? '' - : ''; + ? (parsedAddress.first.parsedAddressByCurrencyMap[currency] ?? domain) + : domain; } void _showFeeAlert(BuildContext context) async { @@ -472,12 +472,14 @@ class _NewSwapPageState extends State { ModalTopBar( title: S.of(context).swap, leadingIcon: Icon(Icons.close), + leadingSemanticLabel: S.of(context).close, onLeadingPressed: Navigator.of(context).maybePop, trailingIcon: CakeImageWidget( imageUrl: "assets/new-ui/options.svg", colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn), ), + trailingSemanticLabel: S.of(context).configure, onTrailingPressed: () { Navigator.of(context).push(CupertinoPageRoute( builder: (context) => Material( @@ -576,6 +578,7 @@ class _NewSwapPageState extends State { iconSize: 24, svgPath: "assets/new-ui/swap_amounts.svg", onPressed: widget.exchangeViewModel.reverseSwapDirection, + semanticLabel: S.of(context).swap_reverse_direction, ), ], ), @@ -1174,6 +1177,7 @@ class SwapAmountBoxState extends State { size: 36, iconSize: 18, backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, + semanticLabel: S.of(context).refund_address, ), if (widget.isReceiverCard && widget.exchangeViewModel.receiveAddress.isEmpty) ...[ @@ -1183,6 +1187,7 @@ class SwapAmountBoxState extends State { size: 36, iconSize: 20, backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, + semanticLabel: S.of(context).paste, ), ModernButton.svg( svgPath: "assets/new-ui/scan.svg", @@ -1190,6 +1195,7 @@ class SwapAmountBoxState extends State { size: 36, iconSize: 20, backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, + semanticLabel: S.of(context).scan, ), ] ], diff --git a/lib/new-ui/widgets/apps_widget.dart b/lib/new-ui/widgets/apps_widget.dart index d440d91bc..63cd77f4d 100644 --- a/lib/new-ui/widgets/apps_widget.dart +++ b/lib/new-ui/widgets/apps_widget.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/themes/core/theme_extension.dart'; import 'package:flutter/material.dart'; @@ -60,71 +61,86 @@ class AppsWidget extends StatelessWidget { end: Alignment.bottomCenter, ), ), - child: TextButton( - onPressed: onTap, - style: TextButton.styleFrom( - shape: RoundedSuperellipseBorder( - borderRadius: BorderRadius.circular(18), - side: BorderSide( - width: 1.25, color: Theme.of(context).colorScheme.surfaceContainerHigh), - ), - padding: EdgeInsets.all(24), - ), - child: Column( - children: [ - Row( + // The card is one node; the arrow glyph's "leaves the app" meaning + // becomes a hint instead of an unlabeled icon. + child: MergeSemantics( + child: Semantics( + hint: isLink == true ? S.of(context).opens_externally : null, + child: TextButton( + onPressed: onTap, + style: TextButton.styleFrom( + shape: RoundedSuperellipseBorder( + borderRadius: BorderRadius.circular(18), + side: BorderSide( + width: 1.25, color: Theme.of(context).colorScheme.surfaceContainerHigh), + ), + padding: EdgeInsets.all(24), + ), + child: Column( children: [ - Padding( - padding: const EdgeInsets.only(right: 20), - child: CakeImageWidget(imageUrl: image, height: 54, width: 54), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - spacing: 6.0, + Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 20), + child: ExcludeSemantics( + child: CakeImageWidget(imageUrl: image, height: 54, width: 54), + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row( + spacing: 6.0, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.w800, + fontSize: 20, + ), + softWrap: true, + ), + isCake == true + ? ExcludeSemantics( + child: CakeImageWidget( + imageUrl: "assets/new-ui/cakelabs-icon.svg", + color: Theme.of(context) + .colorScheme + .onSurfaceVariant), + ) + : SizedBox(), + ], + ), + SizedBox(height: 5), Text( - title, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurface, - fontWeight: FontWeight.w800, - fontSize: 20, + subTitle, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w500, ), softWrap: true, ), - isCake == true - ? CakeImageWidget( - imageUrl: "assets/new-ui/cakelabs-icon.svg", - color: Theme.of(context).colorScheme.onSurfaceVariant) - : SizedBox(), ], ), - SizedBox(height: 5), - Text( - subTitle, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontWeight: FontWeight.w500, - ), - softWrap: true, + ), + ExcludeSemantics( + child: Icon( + isLink == true ? Icons.arrow_outward : Icons.arrow_forward_ios, + color: Theme.of(context).colorScheme.onSurfaceVariant, + size: 20, ), - ], - ), + ) + ], ), - Icon( - isLink == true ? Icons.arrow_outward : Icons.arrow_forward_ios, - color: Theme.of(context).colorScheme.onSurfaceVariant, - size: 20, - ) + if (hint != null) ...[ + SizedBox(height: 10), + hint!, + ] ], ), - if (hint != null) ...[ - SizedBox(height: 10), - hint!, - ] - ], + ), ), ), ), @@ -132,10 +148,16 @@ class AppsWidget extends StatelessWidget { Positioned( top: 10, right: 10, - child: IconButton( - icon: Icon(Icons.close), - onPressed: onClose, - //color: Theme.of(context).colorScheme.onSurface, + // Label the icon-only button without adding a visible tooltip. + child: MergeSemantics( + child: Semantics( + label: S.of(context).close, + child: IconButton( + icon: Icon(Icons.close), + onPressed: onClose, + //color: Theme.of(context).colorScheme.onSurface, + ), + ), ), ), ], diff --git a/lib/new-ui/widgets/changelog_modal.dart b/lib/new-ui/widgets/changelog_modal.dart index 67599619d..1778b7cdc 100644 --- a/lib/new-ui/widgets/changelog_modal.dart +++ b/lib/new-ui/widgets/changelog_modal.dart @@ -107,7 +107,7 @@ class _ChangelogModalState extends State { NewPrimaryButton( onPressed: () { try { - launchUrl(Uri.https("blog.cakewallet.com")); + launchUrl(Uri.https("docs.cakewallet.com", "/release-notes")); } catch (_) {} }, text: S.of(context).view_more_info, diff --git a/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart b/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart index 4c02acfdf..cb2ff1015 100644 --- a/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart +++ b/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart @@ -23,47 +23,57 @@ class CoinActionButton extends StatelessWidget { final size = MediaQuery.of(context).size.width * sizeFactor; final double effectiveSize = min(size, 80); - return Column( - children: [ - Container( - width: effectiveSize, - height: effectiveSize, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: LinearGradient( - colors: [ - context.customColors.cardGradientColorPrimary, - context.customColors.cardGradientColorSecondary - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - border: Border.all( - color: Theme.of(context).colorScheme.surfaceContainerHigh, - width: 1, - ), - ), - child: IconButton( - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - onPressed: () { - HapticFeedback.mediumImpact(); - action(); - }, - icon: icon, - color: Theme.of(context).colorScheme.primary, - ), - ), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, + // The caption below the circle is the accessible name, so it is excluded + // from the semantics tree to keep this a single button node. + return MergeSemantics( + child: Semantics( + button: true, + label: label, + child: Column( + children: [ + Container( + width: effectiveSize, + height: effectiveSize, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [ + context.customColors.cardGradientColorPrimary, + context.customColors.cardGradientColorSecondary + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, ), - label, - ), + border: Border.all( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + width: 1, + ), + ), + child: IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () { + HapticFeedback.mediumImpact(); + action(); + }, + icon: ExcludeSemantics(child: icon), + color: Theme.of(context).colorScheme.primary, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: ExcludeSemantics( + child: Text( + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + label, + ), + ), + ), + ], ), - ], + ), ); } } diff --git a/lib/new-ui/widgets/coins_page/assets_history/asset_details_modal.dart b/lib/new-ui/widgets/coins_page/assets_history/asset_details_modal.dart index 3bb7084af..0ee643cf1 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/asset_details_modal.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/asset_details_modal.dart @@ -62,6 +62,7 @@ class AssetDetailsModal extends StatelessWidget { ModalTopBar( title: "", trailingIcon: Icon(Icons.close), + trailingSemanticLabel: S.of(context).close, onTrailingPressed: Navigator.of(context).pop, padding: EdgeInsets.only(top: 12, right: 18), ), @@ -356,17 +357,22 @@ class AssetDetailsModalBottomButton extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - spacing: 8, - children: [ - ModernButton.svg( - onPressed: onPressed, - svgPath: iconPath, - size: 60, - iconSize: 32, - ), - Text(title) - ], + // The caption is the button's accessible name, so it is excluded from the + // semantics tree to keep the whole control a single button node. + return MergeSemantics( + child: Column( + spacing: 8, + children: [ + ModernButton.svg( + onPressed: onPressed, + svgPath: iconPath, + size: 60, + iconSize: 32, + semanticLabel: title, + ), + ExcludeSemantics(child: Text(title)) + ], + ), ); } } 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 index 57e632dcf..585fe8465 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart @@ -39,125 +39,138 @@ class AssetTile extends StatelessWidget { Widget build(BuildContext context) { final iconPath = balance.asset.iconPath ?? ""; - return GestureDetector( - onTap: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (context) { - return AssetDetailsModal( - showSwap: showSwap, - showBridgeButton: showBridgeButton, - asset: balance.asset, - title: title ?? balance.asset.fullName ?? balance.asset.name, - chainTitle: "", - subtitle: trailingText ?? _getChainTitle(), - amount: showSecondary ? balance.secondAvailableBalance : balance.availableBalance, - currencyTitle: balance.asset.title, - fiatAmount: showSecondary - ? balance.fiatSecondAvailableBalance - : balance.fiatAvailableBalance, - iconPath: balance.asset.iconPath ?? "", - chainIconPath: chainIconPath, - mode: modalMode, - wallet: wallet, - ); - }); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 18.0), - child: Container( - width: double.infinity, - height: 72, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.vertical( - top: isFirst ? Radius.circular(18) : Radius.zero, - bottom: isLast ? Radius.circular(18) : Radius.zero, - ), - ), + // The row is one control: name, amount and fiat value merge into a single + // button node that opens the asset details sheet. + return MergeSemantics( + child: Semantics( + button: true, + child: GestureDetector( + onTap: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) { + return AssetDetailsModal( + showSwap: showSwap, + showBridgeButton: showBridgeButton, + asset: balance.asset, + title: title ?? balance.asset.fullName ?? balance.asset.name, + chainTitle: "", + subtitle: trailingText ?? _getChainTitle(), + amount: + showSecondary ? balance.secondAvailableBalance : balance.availableBalance, + currencyTitle: balance.asset.title, + fiatAmount: showSecondary + ? balance.fiatSecondAvailableBalance + : balance.fiatAvailableBalance, + iconPath: balance.asset.iconPath ?? "", + chainIconPath: chainIconPath, + mode: modalMode, + wallet: wallet, + ); + }); + }, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - iconPath.isNotEmpty - ? TokenImageWidget( - imageUrl: iconPath, - size: 36, - ) - : Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - shape: BoxShape.circle, - ), - child: Center( - child: Text( - balance.asset.name - .substring(0, min(2, balance.asset.name.length)), - style: TextStyle( - fontSize: 20, - color: Theme.of(context).colorScheme.onPrimary, + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Container( + width: double.infinity, + height: 72, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.vertical( + top: isFirst ? Radius.circular(18) : Radius.zero, + bottom: isLast ? Radius.circular(18) : Radius.zero, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Decorative: the asset name is already in the row text. + ExcludeSemantics( + child: iconPath.isNotEmpty + ? TokenImageWidget( + imageUrl: iconPath, + size: 36, + ) + : Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + shape: BoxShape.circle, + ), + child: Center( + child: Text( + balance.asset.name + .substring(0, min(2, balance.asset.name.length)), + style: TextStyle( + fontSize: 20, + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + ), ), - ), - ), - ), - SizedBox(width: 12.0), - Expanded( - child: Column( - spacing: 4.0, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - spacing: 4, + ), + SizedBox(width: 12.0), + Expanded( + child: Column( + spacing: 4.0, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - title ?? balance.asset.fullName ?? balance.asset.name, - style: TextStyle(fontWeight: FontWeight.w500), + Row( + spacing: 4, + children: [ + Text( + title ?? balance.asset.fullName ?? balance.asset.name, + style: TextStyle(fontWeight: FontWeight.w500), + ), + if (trailingText != null) + Text( + trailingText!, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + ], ), - if (trailingText != null) - Text( - trailingText!, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant), + Padding( + padding: const EdgeInsets.only(right: 4.0), + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + "${showSecondary ? balance.secondAvailableBalance : balance.availableBalance} ${balance.formattedAssetTitle.safeSubString(0, 6)}", + maxLines: 1, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), ), + ), ], ), - Padding( - padding: const EdgeInsets.only(right: 4.0), - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: Text( - "${showSecondary ? balance.secondAvailableBalance : balance.availableBalance} ${balance.formattedAssetTitle.safeSubString(0, 6)}", - maxLines: 1, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ), - ), - ], - ), + ), + ], ), - ], - ), + ), + Text( + showSecondary + ? balance.fiatSecondAvailableBalance + : balance.fiatAvailableBalance, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ], ), - Text( - showSecondary ? balance.fiatSecondAvailableBalance : balance.fiatAvailableBalance, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ], + ), ), ), ), diff --git a/lib/new-ui/widgets/coins_page/assets_history/assets_history_section.dart b/lib/new-ui/widgets/coins_page/assets_history/assets_history_section.dart index d36d67bf3..08eec7498 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/assets_history_section.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/assets_history_section.dart @@ -59,14 +59,13 @@ class _AssetsHistorySectionState extends State { AssetsSection( dashboardViewModel: widget.dashboardViewModel, ), - hasAssetsButton ? - AssetsHistorySectionActionButton(S.current.tokens, "assets/new-ui/options_slider.svg", - () { - Navigator.of(context).pushNamed( + hasAssetsButton ? AssetsHistorySectionActionButton(S.current.tokens, "assets/new-ui/options_slider.svg", + + () {Navigator.of(context).pushNamed( Routes.homeSettings, arguments: widget.dashboardViewModel.balanceViewModel, ); - }) : null), + }): null), AssetsHistorySectionTab( S.current.history, HistorySection( diff --git a/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart b/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart index 98bb99d9d..e04a179a7 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart @@ -22,6 +22,8 @@ class AssetsTopBar extends StatelessWidget { @override Widget build(BuildContext context) { + final actionButton = tabs[selectedTab].actionButton; + return SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.only(top: 32.0, bottom: 0.0, left: 12.0, right: 18.0), @@ -49,40 +51,49 @@ class AssetsTopBar extends StatelessWidget { key: ValueKey(selectedTab), spacing: 8, children: [ - Opacity( - opacity: tabs[selectedTab].actionButton != null ? 1 : 0, - child: GestureDetector( - onTap: () { - if (tabs[selectedTab].actionButton != null) { - tabs[selectedTab].actionButton?.onPressed(); - } - }, - child: Container( - height: 40, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(999999), - color: Theme.of(context).colorScheme.surfaceContainer, - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Row( - spacing: 6, - children: [ - if ((tabs[selectedTab].actionButton?.title ?? "").isNotEmpty) - Text( - tabs[selectedTab].actionButton?.title ?? "", - style: TextStyle(color: Theme.of(context).colorScheme.primary), - ), - CakeImageWidget( - imageUrl: tabs[selectedTab].actionButton?.iconPath, - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.primary, BlendMode.srcIn)), - ], + // Built conditionally rather than rendered at opacity 0: an + // invisible chip stayed focusable for screen readers. The + // SizedBox keeps the 40px header height the invisible chip + // used to occupy. + if (actionButton == null) + const SizedBox(height: 40) + else + MergeSemantics( + child: Semantics( + button: true, + child: GestureDetector( + onTap: actionButton.onPressed, + child: Container( + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(999999), + color: Theme.of(context).colorScheme.surfaceContainer, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + spacing: 6, + children: [ + if (actionButton.title.isNotEmpty) + Text( + actionButton.title, + style: + TextStyle(color: Theme.of(context).colorScheme.primary), + ), + ExcludeSemantics( + child: CakeImageWidget( + imageUrl: actionButton.iconPath, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn)), + ), + ], + ), + ), ), ), ), ), - ), ], ), ), diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_filters_page.dart b/lib/new-ui/widgets/coins_page/assets_history/history_filters_page.dart index 97c9260c0..383ced531 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/history_filters_page.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/history_filters_page.dart @@ -27,6 +27,7 @@ class HistoryFiltersPage extends StatelessWidget { ModalTopBar( title: S.of(context).filters, leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: Navigator.of(context).pop, ), Expanded( diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_modal.dart b/lib/new-ui/widgets/coins_page/assets_history/history_modal.dart index c35779974..913266746 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/history_modal.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/history_modal.dart @@ -25,11 +25,13 @@ class HistoryModal extends StatelessWidget { ModalTopBar( title: S.of(context).history, leadingIcon: Icon(Icons.close), + leadingSemanticLabel: S.of(context).close, onLeadingPressed: Navigator.of(context).maybePop, trailingIcon: CakeImageWidget( imageUrl: "assets/new-ui/tx_export.svg", colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn), ), + trailingSemanticLabel: S.of(context).export_csv, onTrailingPressed: () => CsvExportService().exportToCsv(dashboardViewModel.items, context), ), diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_section.dart b/lib/new-ui/widgets/coins_page/assets_history/history_section.dart index ab93a6901..cc73b556f 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/history_section.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/history_section.dart @@ -36,6 +36,15 @@ class HistorySection extends StatelessWidget { final bool roundedTopSection; final bool detailsAsPage; + /// A history row is a single button node: every text inside it (direction, + /// date, amounts) merges into one label. + Widget _historyRow({required VoidCallback onTap, required Widget child}) => MergeSemantics( + child: Semantics( + button: true, + child: GestureDetector(onTap: onTap, child: child), + ), + ); + @override Widget build(BuildContext context) { return SliverPadding( @@ -90,7 +99,7 @@ class HistorySection extends StatelessWidget { else asset = item.assetOfTransaction; - return GestureDetector( + return _historyRow( onTap: () { final page = getIt.get(param1: transaction); @@ -125,7 +134,7 @@ class HistorySection extends StatelessWidget { final tradeFrom = trade.from; final tradeTo = trade.to; - return GestureDetector( + return _historyRow( onTap: () => Navigator.of(context) .pushNamed(Routes.tradeDetails, arguments: trade), child: HistoryTradeTile( @@ -161,7 +170,7 @@ class HistorySection extends StatelessWidget { style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant))); } else if (item is OrderListItem) { - return GestureDetector( + return _historyRow( onTap: () => Navigator.of(context) .pushNamed(Routes.orderDetails, arguments: item.order), child: HistoryOrderTile( @@ -176,7 +185,7 @@ class HistorySection extends StatelessWidget { } else if (item is PayjoinTransactionListItem) { final session = item.session; - return GestureDetector( + return _historyRow( onTap: () => Navigator.of(context).pushNamed( Routes.payjoinDetails, arguments: [item.sessionId, item.transaction], @@ -196,7 +205,7 @@ class HistorySection extends StatelessWidget { } else if (item is AnonpayTransactionListItem) { final transactionInfo = item.transaction; - return GestureDetector( + return _historyRow( onTap: () => Navigator.of(context).pushNamed( Routes.anonPayDetailsPage, arguments: transactionInfo), diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_swap_providers_page.dart b/lib/new-ui/widgets/coins_page/assets_history/history_swap_providers_page.dart index 16051f30a..d58773f69 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/history_swap_providers_page.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/history_swap_providers_page.dart @@ -24,6 +24,7 @@ class HistorySwapProvidersPage extends StatelessWidget { ModalTopBar( title: S.of(context).filter_swap_providers, leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: Navigator.of(context).pop, ), Expanded( 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 index 02534372f..f26b0790e 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/history_tile.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/history_tile.dart @@ -142,7 +142,8 @@ class HistoryTile extends StatelessWidget { date: date, amount: amount, amountFiat: amountFiat, - leadingIcon: _getLeadingIcon(context), + // Decorative: `title` already reads out sent/received/pending. + leadingIcon: ExcludeSemantics(child: _getLeadingIcon(context)), primaryTextColor: _getPrimaryTextColor(), roundedTop: roundedTop, roundedBottom: roundedBottom, diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_top_bar.dart b/lib/new-ui/widgets/coins_page/assets_history/history_top_bar.dart index ad4f291a8..85865109e 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/history_top_bar.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/history_top_bar.dart @@ -11,46 +11,53 @@ class HistoryTopBar extends StatelessWidget { @override Widget build(BuildContext context) { return SliverToBoxAdapter( - child: GestureDetector( + child: Semantics( + button: true, + label: S.of(context).history, onTap: onTap, - behavior: HitTestBehavior.opaque, - child: Padding( - padding: const EdgeInsets.only(left: 16, right: 16, top: 24), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.vertical( - top: Radius.circular(18), - bottom: roundedBottom ? Radius.circular(18) : Radius.zero), - color: Theme.of(context).colorScheme.surfaceContainer), + child: ExcludeSemantics( + child: GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, child: Padding( - padding: EdgeInsets.symmetric( - vertical: 4, - horizontal: 12, - ), - child: Column( - spacing: 12, - children: [ - SizedBox.shrink(), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + padding: const EdgeInsets.only(left: 16, right: 16, top: 24), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical( + top: Radius.circular(18), + bottom: roundedBottom ? Radius.circular(18) : Radius.zero), + color: Theme.of(context).colorScheme.surfaceContainer), + child: Padding( + padding: EdgeInsets.symmetric( + vertical: 4, + horizontal: 12, + ), + child: Column( + spacing: 12, children: [ - Text(S.of(context).history), - CakeImageWidget( - imageUrl: "assets/new-ui/arrow_right.svg", - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), - ) + SizedBox.shrink(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(S.of(context).history), + CakeImageWidget( + imageUrl: "assets/new-ui/arrow_right.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), + ) + ], + ), + if (!roundedBottom) + Container( + height: 1, + width: double.infinity, + color: Theme.of(context).colorScheme.outlineVariant.withAlpha(175), + ) + else + Container(height: 2) ], ), - if (!roundedBottom) - Container( - height: 1, - width: double.infinity, - color: Theme.of(context).colorScheme.outlineVariant.withAlpha(175), - ) - else - Container(height: 2) - ], + ), ), ), ), diff --git a/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart b/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart index 0ac7e724a..4002d236a 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart @@ -1,24 +1,28 @@ -import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart'; -import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart'; -import 'package:cake_wallet/new-ui/widgets/copy_wrapper.dart'; -import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/transaction_details/confirmations_list_item.dart'; -import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart'; -import 'package:cake_wallet/src/screens/transaction_details/address_list_item.dart'; -import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart'; -import 'package:cake_wallet/utils/address_formatter.dart'; -import 'package:cake_wallet/view_model/transaction_details_view_model.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import "package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart"; +import "package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart"; +import "package:cake_wallet/generated/i18n.dart"; +import "package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart"; +import "package:cake_wallet/new-ui/widgets/copy_wrapper.dart"; +import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart"; +import "package:cake_wallet/routes.dart"; +import "package:cake_wallet/src/screens/transaction_details/address_list_item.dart"; +import "package:cake_wallet/src/screens/transaction_details/confirmations_list_item.dart"; +import "package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart"; +import "package:cake_wallet/src/widgets/new_list_row/new_list_section.dart"; +import "package:cake_wallet/utils/address_formatter.dart"; +import "package:cake_wallet/view_model/transaction_details_view_model.dart"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:flutter_mobx/flutter_mobx.dart"; +import "package:modal_bottom_sheet/modal_bottom_sheet.dart"; class TransactionDetailsModal extends StatefulWidget { const TransactionDetailsModal( - {super.key, required this.transactionDetailsViewModel, this.highlightNoteField = false}); + { + required this.transactionDetailsViewModel, + this.highlightNoteField = false, + super.key, + }); final TransactionDetailsViewModel transactionDetailsViewModel; final bool highlightNoteField; @@ -48,190 +52,230 @@ class _TransactionDetailsModalState extends State { } @override - Widget build(BuildContext context) { - final transactionInfoAmount = widget.transactionDetailsViewModel.transactionInfo.amount; - - return SafeArea( - bottom: false, - child: Padding( - padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), - child: GestureDetector( - onTap: FocusScope.of(context).unfocus, - child: Container( - decoration: BoxDecoration( + Widget build(BuildContext context) => SafeArea( + bottom: false, + child: Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: GestureDetector( + onTap: FocusScope.of(context).unfocus, + child: Container( + decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.vertical(top: Radius.circular(25))), - child: Column( - children: [ - ModalTopBar( - title: S.of(context).transaction, - leadingIcon: Icon(Icons.close), - onLeadingPressed: Navigator.of(context).pop, - ), - Expanded( - child: SingleChildScrollView( - controller: ModalScrollController.of(context), - child: Column( - children: [ - TokenImageWidget( - imageUrl: - widget.transactionDetailsViewModel.transactionAsset.iconPath ?? "", - size: 64, - ), - SizedBox(height: 10), - Text( - widget.transactionDetailsViewModel.formattedTitle + - widget.transactionDetailsViewModel.formattedStatus, - style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500), - ), - CopyWrapper( - requireLongPress: true, - data: ClipboardData(text: transactionInfoAmount.toString()), - builder: (context, copied) => AnimatedSwitcher( - duration: Duration(milliseconds: 300), - child: Text( - key: ValueKey(copied), - copied - ? S.of(context).copied - : transactionInfoAmount.toStringWithSymbol(), - style: TextStyle( - fontSize: 28, - color: copied - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface), + borderRadius: const BorderRadius.vertical(top: Radius.circular(25)), + ), + child: Column( + children: [ + ModalTopBar( + title: S.of(context).transaction, + leadingIcon: const Icon(Icons.close), + leadingSemanticLabel: S.of(context).close, + onLeadingPressed: Navigator.of(context).pop, + ), + Expanded( + child: SingleChildScrollView( + controller: ModalScrollController.of(context), + child: Column( + children: [ + TokenImageWidget( + imageUrl: + widget.transactionDetailsViewModel.transactionAsset.iconPath ?? "", + size: 64, + ), + const SizedBox(height: 10), + Text( + widget.transactionDetailsViewModel.formattedTitle + + widget.transactionDetailsViewModel.formattedStatus, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500), + ), + Observer( + builder: (_) => CopyWrapper( + requireLongPress: true, + data: ClipboardData( + text: widget.transactionDetailsViewModel.transactionCopyAmount, + ), + builder: (context, copied) => AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: Text( + key: ValueKey(copied), + copied + ? S.of(context).copied + : widget.transactionDetailsViewModel.transactionAmount, + style: TextStyle( + fontSize: 28, + color: copied + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurface, + ), + ), + ), ), ), - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - spacing: 12, - children: [ - NewListSections(sections: { - "": widget.transactionDetailsViewModel.items - .map((item) { - if (item.value.isEmpty) return null; + Padding( + padding: const EdgeInsets.all(16), + child: Column( + spacing: 12, + children: [ + NewListSections( + sections: { + "": widget.transactionDetailsViewModel.items + .map((item) { + if (item.value.isEmpty) { + return null; + } - final shouldBuildBottomWidget = item.value.length > 25; + final shouldBuildBottomWidget = item.value.length > 25; - return ListItemRegularRow( - copyableText: item.value, - showArrow: false, - keyValue: ((item.key as ValueKey?)?.value as String?) ?? - item.title, - label: item.title, - trailingWidget: shouldBuildBottomWidget - ? null - : _buildTrailingWIdget(item), - bottomWidget: shouldBuildBottomWidget - ? _buildBottomWidget(item) - : null); - }) - .whereType() - .toList(), - }), - Container( - decoration: BoxDecoration( + return ListItemRegularRow( + copyableText: item.value, + showArrow: false, + keyValue: ((item.key as ValueKey?)?.value as String?) ?? + item.title, + label: item.title, + trailingWidget: shouldBuildBottomWidget + ? null + : _buildTrailingWidget(item), + bottomWidget: shouldBuildBottomWidget + ? _buildBottomWidget(item) + : null, + ); + }) + .whereType() + .toList(), + }, + ), + Container( + decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), - color: Theme.of(context).colorScheme.surfaceContainer), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - spacing: 8, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(S.of(context).note), - TextField( - focusNode: noteFocusNode, - controller: noteController, - decoration: InputDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + spacing: 8, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(S.of(context).note), + TextField( + focusNode: noteFocusNode, + controller: noteController, + decoration: InputDecoration( hintText: S.of(context).add_a_note, border: InputBorder.none, focusedBorder: InputBorder.none, enabledBorder: InputBorder.none, contentPadding: EdgeInsets.zero, - isDense: true), - ) - ], + isDense: true, + ), + ), + ], + ), ), ), - ), - Observer( - builder: (_) => NewListSections(sections: { - "view tx": [ - ListItemRegularRow( - keyValue: "view tx on", - label: - widget.transactionDetailsViewModel.explorerDescription, - onTap: widget.transactionDetailsViewModel.launchExplorer, - foregroundColor: Theme.of(context).colorScheme.primary, - trailingIconPath: "assets/new-ui/link_arrow.svg", - trailingIconSize: 8) - ], - if (widget.transactionDetailsViewModel.canReplaceByFee) - "rbf": [ - ListItemRegularRow( - keyValue: "replace by fee", - label: S.of(context).bump_fee, - onTap: () { - Navigator.of(context) - .pushNamed(Routes.bumpFeePage, arguments: [ - widget.transactionDetailsViewModel.transactionInfo, - widget.transactionDetailsViewModel.rawTransaction - ]); - }) - ] - }), - ) - ], + Observer( + builder: (_) => NewListSections( + sections: { + "view tx": [ + ListItemRegularRow( + keyValue: "view tx on", + label: widget + .transactionDetailsViewModel.explorerDescription, + onTap: widget.transactionDetailsViewModel.launchExplorer, + foregroundColor: Theme.of(context).colorScheme.primary, + trailingIconPath: "assets/new-ui/link_arrow.svg", + trailingIconSize: 8, + ), + ], + if (widget.transactionDetailsViewModel.canReplaceByFee) + "rbf": [ + ListItemRegularRow( + keyValue: "replace by fee", + label: S.of(context).bump_fee, + onTap: () { + Navigator.of(context).pushNamed( + Routes.bumpFeePage, + arguments: [ + widget + .transactionDetailsViewModel.transactionInfo, + widget.transactionDetailsViewModel.rawTransaction, + ], + ); + }, + ), + ], + }, + ), + ), + ], + ), ), - ), - SizedBox(height: MediaQuery.of(context).viewPadding.bottom) - ], + SizedBox(height: MediaQuery.of(context).viewPadding.bottom), + ], + ), ), ), - ) - ], + ], + ), ), ), ), - ), - ); - } + ); - Widget _buildTrailingWIdget(TransactionDetailsListItem item) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: switch (item.runtimeType) { - ConfirmationsListItem => Row( - children: [ - Text((item as ConfirmationsListItem).current.toString(), - style: TextStyle(color: Theme.of(context).colorScheme.primary)), - if (item.needed > 0) - Text("/${item.needed}", - style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant)) - ], - ), - _ => Text( - item.value, - style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), - ) - }, - ); - } + Widget _buildTrailingWidget(TransactionDetailsListItem item) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: switch (item.runtimeType) { + ConfirmationsListItem => Row( + children: [ + Text( + (item as ConfirmationsListItem).current.toString(), + style: TextStyle(color: Theme.of(context).colorScheme.primary), + ), + if (item.needed > 0) + Text( + "/${item.needed}", + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + ], + ), + _ => Text( + item.value, + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), + ) + }, + ); Widget _buildBottomWidget(TransactionDetailsListItem item) { return switch (item.runtimeType) { - AddressListItem => AddressFormatter.buildSegmentedAddress( - address: item.value, - evenTextStyle: TextStyle( - fontSize: 12, - fontFamily: "IBM Plex Mono", - color: Theme.of(context).colorScheme.onSurface)), + AddressListItem => _segmentedAddressList(item.value), _ => Text( item.value, style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), ) }; } + + Widget _segmentedAddressList(String value) { + final style = TextStyle( + fontSize: 12, + fontFamily: "IBM Plex Mono", + color: Theme.of(context).colorScheme.onSurface, + ); + final lines = value + .split(RegExp(r'[,\n]')) + .map((final line) => line.trim()) + .where((final line) => line.isNotEmpty) + .toList(); + if (lines.length <= 1) { + return AddressFormatter.buildSegmentedAddress( + address: lines.isNotEmpty ? lines.first : value.trim(), + evenTextStyle: style, + ); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (final line in lines) + AddressFormatter.buildSegmentedAddress(address: line, evenTextStyle: style), + ], + ); + } } diff --git a/lib/new-ui/widgets/coins_page/cards/balance_card.dart b/lib/new-ui/widgets/coins_page/cards/balance_card.dart index da852bf59..20b16ed65 100644 --- a/lib/new-ui/widgets/coins_page/cards/balance_card.dart +++ b/lib/new-ui/widgets/coins_page/cards/balance_card.dart @@ -91,11 +91,14 @@ class BalanceCard extends StatelessWidget { ? ClipRSuperellipse( borderRadius: BorderRadius.circular(borderRadius), key: ValueKey(design.imagePath), - child: CakeImageWidget( - imageUrl: design.imagePath, - width: width, - height: height, - fit: BoxFit.fill, + // Purely decorative card artwork. + child: ExcludeSemantics( + child: CakeImageWidget( + imageUrl: design.imagePath, + width: width, + height: height, + fit: BoxFit.fill, + ), ), ) : const SizedBox.shrink( @@ -146,9 +149,14 @@ class BalanceCard extends StatelessWidget { AnimatedOpacity( opacity: selected ? 0 : 1, duration: textFadeDuration, - child: Text( - accountBalance, - style: TextStyle(color: design.colors.textColor, fontSize: 14), + // Opacity alone keeps the text readable by screen + // readers, so drop it while it is invisible. + child: ExcludeSemantics( + excluding: selected, + child: Text( + accountBalance, + style: TextStyle(color: design.colors.textColor, fontSize: 14), + ), ), ), ], @@ -156,40 +164,45 @@ class BalanceCard extends StatelessWidget { AnimatedOpacity( opacity: selected ? 1 : 0, duration: textFadeDuration, - child: AnimatedSwitcher( - duration: designSwitchDuration, - layoutBuilder: (currentChild, previousChildren) { - return Stack( - alignment: Alignment.centerLeft, - children: [ - ...previousChildren, - if (currentChild != null) currentChild, + // Only the selected card's balance is visible, so only it + // may be announced. + child: ExcludeSemantics( + excluding: !selected, + child: AnimatedSwitcher( + duration: designSwitchDuration, + layoutBuilder: (currentChild, previousChildren) { + return Stack( + alignment: Alignment.centerLeft, + children: [ + ...previousChildren, + if (currentChild != null) currentChild, + ], + ); + }, + child: Row( + key: ValueKey("$balance ${resolvedAssetName.toUpperCase()}"), + spacing: 8.0, + children: [ + AnimatedDefaultTextStyle( + duration: designSwitchDuration, + style: DefaultTextStyle.of(context).style.copyWith( + color: design.colors.textColor, + fontSize: 28, + fontWeight: FontWeight.w500, + letterSpacing: -0.4), + child: Text(fiatFirst ? fiatBalance : balance), + ), + AnimatedDefaultTextStyle( + duration: designSwitchDuration, + style: DefaultTextStyle.of(context).style.copyWith( + color: design.colors.textColorSecondary, + fontSize: 28, + fontWeight: FontWeight.w400, + letterSpacing: -0.4), + child: Text(resolvedAssetName), + ), ], - ); - }, - child: Row( - key: ValueKey("$balance ${resolvedAssetName.toUpperCase()}"), - spacing: 8.0, - children: [ - AnimatedDefaultTextStyle( - duration: designSwitchDuration, - style: DefaultTextStyle.of(context).style.copyWith( - color: design.colors.textColor, - fontSize: 28, - fontWeight: FontWeight.w500, - letterSpacing: -0.4), - child: Text(fiatFirst ? fiatBalance : balance), - ), - AnimatedDefaultTextStyle( - duration: designSwitchDuration, - style: DefaultTextStyle.of(context).style.copyWith( - color: design.colors.textColorSecondary, - fontSize: 28, - fontWeight: FontWeight.w400, - letterSpacing: -0.4), - child: Text(resolvedAssetName), - ), - ], + ), ), ), ), @@ -250,7 +263,10 @@ class BalanceCard extends StatelessWidget { switchInCurve: Curves.easeInOut, switchOutCurve: Curves.easeInOut, child: design.backgroundType == CardDesignBackgroundTypes.svgIcon - ? _CornerSvgIcon(design: design, iconWidth: iconWidth) + // Purely decorative card artwork. + ? ExcludeSemantics( + child: _CornerSvgIcon(design: design, iconWidth: iconWidth), + ) : const SizedBox.shrink( key: ValueKey('svgIconOff'), ), @@ -266,18 +282,28 @@ class BalanceCard extends StatelessWidget { child: AnimatedOpacity( duration: designSwitchDuration, opacity: onCustomizeTapped == null ? 0 : 1, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: onCustomizeTapped, - child: Container( - height: 40, - width: 40, - child: Center( - child: CakeImageWidget( - imageUrl: "assets/new-ui/3dots_vertical.svg", - alignment: Alignment.topRight, - colorFilter: - ColorFilter.mode(design.colors.textColorSecondary, BlendMode.srcIn), + // Faded out and inert: must not be a focusable phantom control. + child: ExcludeSemantics( + excluding: onCustomizeTapped == null, + child: Semantics( + button: true, + label: S.of(context).wallet_menu, + onTap: onCustomizeTapped, + child: GestureDetector( + excludeFromSemantics: true, + behavior: HitTestBehavior.opaque, + onTap: onCustomizeTapped, + child: Container( + height: 40, + width: 40, + child: Center( + child: CakeImageWidget( + imageUrl: "assets/new-ui/3dots_vertical.svg", + alignment: Alignment.topRight, + colorFilter: + ColorFilter.mode(design.colors.textColorSecondary, BlendMode.srcIn), + ), + ), ), ), ), @@ -289,27 +315,34 @@ class BalanceCard extends StatelessWidget { ); } - Widget getBalanceCardActionButton(BalanceCardAction action) => GestureDetector( + Widget getBalanceCardActionButton(BalanceCardAction action) => Semantics( + button: true, + label: action.label, onTap: action.onTap, - child: Container( - decoration: BoxDecoration( - color: design.colors.backgroundImageColor.withAlpha(75), - borderRadius: BorderRadius.circular(10000000), - ), - margin: const EdgeInsets.only(right: 10), - padding: const EdgeInsets.only(left: 10, right: 5, top: 5, bottom: 5), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(right: 6), - child: Text( - action.label, - style: TextStyle(color: design.colors.textColor, fontSize: 16), - ), + child: ExcludeSemantics( + child: GestureDetector( + onTap: action.onTap, + child: Container( + decoration: BoxDecoration( + color: design.colors.backgroundImageColor.withAlpha(75), + borderRadius: BorderRadius.circular(10000000), ), - Icon(action.icon, color: design.colors.textColorSecondary, size: action.iconSize), - ], + margin: const EdgeInsets.only(right: 10), + padding: const EdgeInsets.only(left: 10, right: 5, top: 5, bottom: 5), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(right: 6), + child: Text( + action.label, + style: TextStyle(color: design.colors.textColor, fontSize: 16), + ), + ), + Icon(action.icon, color: design.colors.textColorSecondary, size: action.iconSize), + ], + ), + ), ), ), ); diff --git a/lib/new-ui/widgets/coins_page/cards/cards_view.dart b/lib/new-ui/widgets/coins_page/cards/cards_view.dart index e68b61440..1aec566a7 100644 --- a/lib/new-ui/widgets/coins_page/cards/cards_view.dart +++ b/lib/new-ui/widgets/coins_page/cards/cards_view.dart @@ -77,6 +77,32 @@ class _CardsViewState extends State { final left = (parentWidth - effectiveCardWidth) / 2.0; + final isSelected = _selectedIndex == visualIndex; + final accounts = widget.accountListViewModel?.accounts; + final cardLabel = (accounts != null && realIndex < accounts.length) + ? accounts[realIndex].label + : S.of(context).balance; + + void onCardTap() { + // printV(visualIndex); + if (compactMode && visualIndex != 0) { + widget.onCompactModeBackgroundCardsTapped(); + } else if (!compactMode) { + setState(() { + if (widget.accountListViewModel != null) + widget.accountListViewModel!.select(widget.accountListViewModel!.accounts[realIndex]); + _selectedIndex = visualIndex; + }); + } + } + + void onCardLongPress() { + if (_selectedIndex == visualIndex) { + widget.dashboardViewModel.balanceViewModel.switchBalanceValue(); + } + HapticFeedback.heavyImpact(); + } + return AnimatedPositioned( key: ValueKey("$visualIndex $realIndex"), duration: animDuration, @@ -87,127 +113,125 @@ class _CardsViewState extends State { duration: animDuration, curve: Curves.easeOut, scale: scale, - child: GestureDetector( - onTap: () { - // printV(visualIndex); - if (compactMode && visualIndex != 0) { - widget.onCompactModeBackgroundCardsTapped(); - } else if (!compactMode) { - setState(() { - if (widget.accountListViewModel != null) - widget.accountListViewModel! - .select(widget.accountListViewModel!.accounts[realIndex]); - _selectedIndex = visualIndex; - }); - } - }, - onLongPress: () { - if (_selectedIndex == visualIndex) { - widget.dashboardViewModel.balanceViewModel.switchBalanceValue(); - } - ; - HapticFeedback.heavyImpact(); - }, - child: Observer(builder: (_) { - if (realIndex >= (widget.accountListViewModel?.accounts.length ?? 1)) { - return Container(); - } - final account = widget.accountListViewModel?.accounts[realIndex]; - - // The second balance should always be the lightning balance - // printV(widget.dashboardViewModel.balanceViewModel.formattedBalances.first.availableBalance); - final walletBalanceRecord = widget.dashboardViewModel.balanceViewModel - .getMainBalanceRecord(widget.lightningMode); - - late final String walletBalance; - late final String walletFiatBalance; - if (widget.dashboardViewModel.mwebEnabled && widget.dashboardViewModel.hasMweb) { - if (widget.dashboardViewModel.balanceViewModel.displayMode == - BalanceDisplayMode.hiddenBalance) { - walletBalance = '●●●●●●'; - walletFiatBalance = '●●●●●●'; - } else { - walletBalance = walletBalanceRecord?.combinedAvailableBalance ?? "0"; - walletFiatBalance = walletBalanceRecord?.combinedFiatAvailableBalance ?? "0.00"; + // The card is the tap target; the balances and the card's own buttons stay + // reachable as children of this node. + child: Semantics( + button: true, + selected: isSelected, + label: cardLabel, + hint: isSelected + ? (widget.dashboardViewModel.balanceViewModel.displayMode == + BalanceDisplayMode.hiddenBalance + ? S.of(context).long_press_show_balance + : S.of(context).long_press_hide_balance) + : null, + onTap: onCardTap, + onLongPress: isSelected ? onCardLongPress : null, + child: GestureDetector( + excludeFromSemantics: true, + onTap: onCardTap, + onLongPress: onCardLongPress, + child: Observer(builder: (_) { + if (realIndex >= (widget.accountListViewModel?.accounts.length ?? 1)) { + return Container(); } - } else if (widget.dashboardViewModel.balanceViewModel.showCombinedBalance) { - walletBalance = ""; - walletFiatBalance = widget.dashboardViewModel.balanceViewModel.combinedFiatBalance; - } else { - walletBalance = walletBalanceRecord?.availableBalance ?? "0"; - walletFiatBalance = walletBalanceRecord?.fiatAvailableBalance ?? "0.00"; - } + final account = widget.accountListViewModel?.accounts[realIndex]; - // the card designs is empty if widget gets built before it loads. - // should get populated before user sees anything - final CardDesign cardDesign; - if (widget.dashboardViewModel.cardDesigns.isEmpty || - realIndex >= widget.dashboardViewModel.cardDesigns.length) - cardDesign = CardDesign.genericDefault; - else if (widget.lightningMode) - cardDesign = widget.dashboardViewModel.cardDesigns[realIndex + 1]; - else - cardDesign = widget.dashboardViewModel.cardDesigns[realIndex]; + // The second balance should always be the lightning balance + // printV(widget.dashboardViewModel.balanceViewModel.formattedBalances.first.availableBalance); + final walletBalanceRecord = widget.dashboardViewModel.balanceViewModel + .getMainBalanceRecord(widget.lightningMode); - final String accountName; - final String accountBalance; - if (account == null) { - accountName = ""; - accountBalance = ""; - } else { - accountName = account.label; - accountBalance = account.balance ?? "0.00"; - } + late final String walletBalance; + late final String walletFiatBalance; + if (widget.dashboardViewModel.mwebEnabled && widget.dashboardViewModel.hasMweb) { + if (widget.dashboardViewModel.balanceViewModel.displayMode == + BalanceDisplayMode.hiddenBalance) { + walletBalance = '●●●●●●'; + walletFiatBalance = '●●●●●●'; + } else { + walletBalance = walletBalanceRecord?.combinedAvailableBalance ?? "0"; + walletFiatBalance = walletBalanceRecord?.combinedFiatAvailableBalance ?? "0.00"; + } + } else if (widget.dashboardViewModel.balanceViewModel.showCombinedBalance) { + walletBalance = ""; + walletFiatBalance = widget.dashboardViewModel.balanceViewModel.combinedFiatBalance; + } else { + walletBalance = walletBalanceRecord?.availableBalance ?? "0"; + walletFiatBalance = walletBalanceRecord?.fiatAvailableBalance ?? "0.00"; + } - final assetName = widget.dashboardViewModel.balanceViewModel.showCombinedBalance - ? "" - : walletBalanceRecord?.formattedAssetTitle ?? assetTitleFallback; + // the card designs is empty if widget gets built before it loads. + // should get populated before user sees anything + final CardDesign cardDesign; + if (widget.dashboardViewModel.cardDesigns.isEmpty || + realIndex >= widget.dashboardViewModel.cardDesigns.length) + cardDesign = CardDesign.genericDefault; + else if (widget.lightningMode) + cardDesign = widget.dashboardViewModel.cardDesigns[realIndex + 1]; + else + cardDesign = widget.dashboardViewModel.cardDesigns[realIndex]; - final List actions = widget.lightningMode - ? [ - BalanceCardAction( - label: S.current.bitcoin_lightning_deposit, - icon: Icons.arrow_downward, - onTap: depositToL2, - ), - BalanceCardAction( - label: S.current.bitcoin_lightning_withdraw, - icon: Icons.arrow_upward, - onTap: withdrawFromL2, - ) - ] - : widget.dashboardViewModel.isEnabledTradeAction - ? [ - BalanceCardAction( - label: S.current.buy, - icon: Icons.arrow_forward_ios_rounded, - iconSize: 12, - onTap: () { + final String accountName; + final String accountBalance; + if (account == null) { + accountName = ""; + accountBalance = ""; + } else { + accountName = account.label; + accountBalance = account.balance ?? "0.00"; + } + + final assetName = widget.dashboardViewModel.balanceViewModel.showCombinedBalance + ? "" + : walletBalanceRecord?.formattedAssetTitle ?? assetTitleFallback; + + final List actions = widget.lightningMode + ? [ + BalanceCardAction( + label: S.current.bitcoin_lightning_deposit, + icon: Icons.arrow_downward, + onTap: depositToL2, + ), + BalanceCardAction( + label: S.current.bitcoin_lightning_withdraw, + icon: Icons.arrow_upward, + onTap: withdrawFromL2, + ) + ] + : widget.dashboardViewModel.isEnabledTradeAction + ? [ + BalanceCardAction( + label: S.current.buy, + icon: Icons.arrow_forward_ios_rounded, + iconSize: 12, + onTap: () { showModalBottomSheet( context: context, builder: (context) => BuySellSelectorModal()); }, - ) - ] - : []; + ) + ] + : []; - return BalanceCard( - width: effectiveCardWidth, - accountName: accountName, - accountBalance: accountBalance, - designSwitchDuration: Duration(milliseconds: 150), - assetName: assetName, - capitalizeAssetName: _shouldCapitalizeAssetName(), - balance: walletBalance, - fiatCurrencyTitle: walletBalanceRecord?.fiatCurrency?.title ?? - widget.dashboardViewModel.settingsStore.fiatCurrency.title, - fiatFirst: widget.dashboardViewModel.balanceViewModel.showCombinedBalance, - fiatBalance: walletFiatBalance, - selected: _selectedIndex == visualIndex, - onCustomizeTapped: _selectedIndex == visualIndex ? widget.onCustomizeTapped : null, - design: cardDesign, - actions: actions, - ); - }), + return BalanceCard( + width: effectiveCardWidth, + accountName: accountName, + accountBalance: accountBalance, + designSwitchDuration: Duration(milliseconds: 150), + assetName: assetName, + capitalizeAssetName: _shouldCapitalizeAssetName(), + balance: walletBalance, + fiatCurrencyTitle: walletBalanceRecord?.fiatCurrency?.title ?? + widget.dashboardViewModel.settingsStore.fiatCurrency.title, + fiatFirst: widget.dashboardViewModel.balanceViewModel.showCombinedBalance, + fiatBalance: walletFiatBalance, + selected: _selectedIndex == visualIndex, + onCustomizeTapped: _selectedIndex == visualIndex ? widget.onCustomizeTapped : null, + design: cardDesign, + actions: actions, + ); + }), + ), ), ), ); diff --git a/lib/new-ui/widgets/coins_page/mweb_ad.dart b/lib/new-ui/widgets/coins_page/mweb_ad.dart index 5eae49cf2..9c6ef00fb 100644 --- a/lib/new-ui/widgets/coins_page/mweb_ad.dart +++ b/lib/new-ui/widgets/coins_page/mweb_ad.dart @@ -21,51 +21,63 @@ class MwebAd extends StatelessWidget { child: Column( spacing: 12, children: [ - GestureDetector( - onTap: () => Navigator.of(context).pushNamed(Routes.mwebSettings), - child: Container( - height: 64, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(18), - color: Theme.of(context).colorScheme.surfaceContainer), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - CakeImageWidget( - imageUrl: "assets/new-ui/settings_row_icons/mweb.svg", - width: 24, - height: 24, - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Text( - S.of(context).mweb_ad, - softWrap: true, - style: TextStyle(fontSize: 12), + MergeSemantics( + child: Semantics( + button: true, + child: GestureDetector( + onTap: () => Navigator.of(context).pushNamed(Routes.mwebSettings), + child: Container( + height: 64, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(18), + color: Theme.of(context).colorScheme.surfaceContainer), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ExcludeSemantics( + child: CakeImageWidget( + imageUrl: "assets/new-ui/settings_row_icons/mweb.svg", + width: 24, + height: 24, + ), ), - ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Text( + S.of(context).mweb_ad, + softWrap: true, + style: TextStyle(fontSize: 12), + ), + ), + ), + Icon( + size: 16, + Icons.arrow_forward_ios, + color: Theme.of(context).colorScheme.primary, + ) + ], ), - Icon( - size: 16, - Icons.arrow_forward_ios, - color: Theme.of(context).colorScheme.primary, - ) - ], + ), ), ), ), ), - GestureDetector( - onTap: () => dashboardViewModel.dismissMwebAd(false), - child: Text( - S.of(context).do_not_show_anymore, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.primary), + MergeSemantics( + child: Semantics( + button: true, + child: GestureDetector( + onTap: () => dashboardViewModel.dismissMwebAd(false), + child: Text( + S.of(context).do_not_show_anymore, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary), + ), + ), ), ) ], diff --git a/lib/new-ui/widgets/coins_page/top_bar_widget/chain_icon.dart b/lib/new-ui/widgets/coins_page/top_bar_widget/chain_icon.dart index 418abfe19..3fe2ebd76 100644 --- a/lib/new-ui/widgets/coins_page/top_bar_widget/chain_icon.dart +++ b/lib/new-ui/widgets/coins_page/top_bar_widget/chain_icon.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:flutter/material.dart'; @@ -27,10 +28,16 @@ class ChainIcon extends StatelessWidget { AnimatedOpacity( duration: Duration(milliseconds: 100), opacity: done ? 0 : 1, - child: CircularProgressIndicator( - value: progress, - color: Color(0xFFFFB84E), - strokeWidth: 2, + // Faded out means "nothing to report", so it must leave the tree too. + child: ExcludeSemantics( + excluding: done, + child: CircularProgressIndicator( + value: progress, + color: Color(0xFFFFB84E), + strokeWidth: 2, + semanticsLabel: S.of(context).synchronizing, + semanticsValue: "${(progress * 100).round()}%", + ), ), ), AnimatedScale( diff --git a/lib/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart b/lib/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart index 5c1fb7991..6ea000518 100644 --- a/lib/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart +++ b/lib/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -12,76 +13,84 @@ class LightningSwitcher extends StatelessWidget { @override Widget build(BuildContext context) { - return SizedBox( - child: InkWell( - onTap: () { - HapticFeedback.mediumImpact(); - onLightningSwitchPress(); - }, - child: Container( - decoration: ShapeDecoration( - shape: RoundedSuperellipseBorder(borderRadius: BorderRadiusGeometry.circular(900.0)), - color: Theme.of(context).colorScheme.surfaceContainer), - width: 70, - height: 36, - padding: EdgeInsets.symmetric(vertical: 2), - child: Stack( - children: [ - AnimatedContainer( - alignment: Alignment.centerRight, - margin: EdgeInsets.only(left: lightningMode ? 36 : 2), - duration: Duration(milliseconds: 250), - curve: Curves.easeOutCubic, - width: 32, - height: 32, - // height: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(9999990.0)), - color: Theme.of(context).colorScheme.primary), - ), - Container( - child: Row( - spacing: 2.0, - children: [ - SizedBox(), - AnimatedSwitcher( - duration: Duration(milliseconds: 150), - transitionBuilder: (child, animation) => - FadeTransition(opacity: animation, child: child), - child: CakeImageWidget( - imageUrl: 'assets/new-ui/switcher-bitcoin.svg', - key: ValueKey(lightningMode), - width: 32, - height: 32, - colorFilter: ColorFilter.mode( - lightningMode - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.surfaceContainer, - BlendMode.srcIn, - ), - ), - ), - AnimatedSwitcher( - duration: Duration(milliseconds: 150), - transitionBuilder: (child, animation) => - FadeTransition(opacity: animation, child: child), - child: CakeImageWidget( - imageUrl: 'assets/new-ui/switcher-lightning.svg', - key: ValueKey(lightningMode), - width: 32, - height: 32, - colorFilter: ColorFilter.mode( - lightningMode - ? Theme.of(context).colorScheme.surfaceContainer - : Theme.of(context).colorScheme.primary, - BlendMode.srcIn, - ), - ), - ), - ], + // One toggle node: the knob position and the coloured glyphs are the only + // visual cue for which mode is active. + return Semantics( + button: true, + toggled: lightningMode, + label: S.of(context).lightning_mode, + child: SizedBox( + child: InkWell( + onTap: () { + HapticFeedback.mediumImpact(); + onLightningSwitchPress(); + }, + child: Container( + decoration: ShapeDecoration( + shape: + RoundedSuperellipseBorder(borderRadius: BorderRadiusGeometry.circular(900.0)), + color: Theme.of(context).colorScheme.surfaceContainer), + width: 70, + height: 36, + padding: EdgeInsets.symmetric(vertical: 2), + child: Stack( + children: [ + AnimatedContainer( + alignment: Alignment.centerRight, + margin: EdgeInsets.only(left: lightningMode ? 36 : 2), + duration: Duration(milliseconds: 250), + curve: Curves.easeOutCubic, + width: 32, + height: 32, + // height: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(9999990.0)), + color: Theme.of(context).colorScheme.primary), ), - ), - ], + Container( + child: Row( + spacing: 2.0, + children: [ + SizedBox(), + AnimatedSwitcher( + duration: Duration(milliseconds: 150), + transitionBuilder: (child, animation) => + FadeTransition(opacity: animation, child: child), + child: CakeImageWidget( + imageUrl: 'assets/new-ui/switcher-bitcoin.svg', + key: ValueKey(lightningMode), + width: 32, + height: 32, + colorFilter: ColorFilter.mode( + lightningMode + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.surfaceContainer, + BlendMode.srcIn, + ), + ), + ), + AnimatedSwitcher( + duration: Duration(milliseconds: 150), + transitionBuilder: (child, animation) => + FadeTransition(opacity: animation, child: child), + child: CakeImageWidget( + imageUrl: 'assets/new-ui/switcher-lightning.svg', + key: ValueKey(lightningMode), + width: 32, + height: 32, + colorFilter: ColorFilter.mode( + lightningMode + ? Theme.of(context).colorScheme.surfaceContainer + : Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + ), + ], + ), + ), + ], + ), ), ), ), diff --git a/lib/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart b/lib/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart index 915ee54b5..97ba02326 100644 --- a/lib/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart +++ b/lib/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/core/sync_status_title.dart'; import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/pulsing_dot.dart'; import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; @@ -47,65 +48,55 @@ class SyncBar extends StatelessWidget { child: Stack( alignment: Alignment.centerLeft, children: [ - if (!_showFullBar()) - Row( - mainAxisSize: MainAxisSize.min, - spacing: 6, - children: [ - if (dashboardViewModel.isTorEnabled) - CakeImageWidget( - imageUrl: "assets/new-ui/tor.svg", - width: 20, - height: 20, - ), - if (_showDot()) PulsingDot(), - ], - ), + if (!_showFullBar()) _buildCompactBar(context), if (_showFullBar()) - GestureDetector( - onTap: () { - CupertinoScaffold.showCupertinoModalBottomSheet( - context: context, - barrierColor: Colors.black.withAlpha(85), - builder: (context) => FractionallySizedBox( - child: Material( - child: getIt.get(param1: false), - ))); - }, - child: AnimatedSwitcher( - duration: Duration(milliseconds: 100), - child: Container( - key: ValueKey(status.runtimeType), - height: 36, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(9999), - border: _getBorder(context, status.runtimeType), - color: _getBackgroundColor(context, status.runtimeType), - ), - child: Row( - spacing: 10, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - if (icon != null) icon, - // if (dashboardViewModel.silentPaymentsScanningActive && - // progressStatuses.contains(status.runtimeType)) ...[ - // Text( - // "${(status.progress() * 100).toInt()}%", - // style: TextStyle(fontSize: 12, color: Color(0xFFEFBA5E)), - // ), - // Text( - // "·", - // style: TextStyle(fontSize: 12), - // ) - // ], - Text( - syncStatusTitle( - status, dashboardViewModel.settingsStore.syncStatusDisplayMode), - style: _getTextStyle(context, status.runtimeType), + // A single node: the localized status text (plus any active + // Tor/MWEB/Silent Payments badge) is the label, and the hint says + // where tapping leads. Everything inside is redundant with it. + Semantics( + button: true, + label: _statusSemanticsLabel(context, status), + hint: S.of(context).manage_nodes, + onTap: () => _openNodeManagement(context), + child: ExcludeSemantics( + child: GestureDetector( + onTap: () => _openNodeManagement(context), + child: AnimatedSwitcher( + duration: Duration(milliseconds: 100), + child: Container( + key: ValueKey(status.runtimeType), + height: 36, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(9999), + border: _getBorder(context, status.runtimeType), + color: _getBackgroundColor(context, status.runtimeType), ), - ], + child: Row( + spacing: 10, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + if (icon != null) icon, + // if (dashboardViewModel.silentPaymentsScanningActive && + // progressStatuses.contains(status.runtimeType)) ...[ + // Text( + // "${(status.progress() * 100).toInt()}%", + // style: TextStyle(fontSize: 12, color: Color(0xFFEFBA5E)), + // ), + // Text( + // "·", + // style: TextStyle(fontSize: 12), + // ) + // ], + Text( + syncStatusTitle(status, + dashboardViewModel.settingsStore.syncStatusDisplayMode), + style: _getTextStyle(context, status.runtimeType), + ), + ], + ), + ), ), ), ), @@ -117,6 +108,55 @@ class SyncBar extends StatelessWidget { ); } + void _openNodeManagement(BuildContext context) => + CupertinoScaffold.showCupertinoModalBottomSheet( + context: context, + barrierColor: Colors.black.withAlpha(85), + builder: (context) => FractionallySizedBox( + child: Material( + child: getIt.get(param1: false), + ))); + + /// Compact mode shows sync state with a pulsing dot (and a Tor glyph) only, so + /// the whole row needs a text equivalent. + Widget _buildCompactBar(BuildContext context) { + final row = Row( + mainAxisSize: MainAxisSize.min, + spacing: 6, + children: [ + if (dashboardViewModel.isTorEnabled) + CakeImageWidget( + imageUrl: "assets/new-ui/tor.svg", + width: 20, + height: 20, + ), + if (_showDot()) PulsingDot(), + ], + ); + + final label = _joinLabels([ + if (dashboardViewModel.isTorEnabled) S.of(context).tor_connection, + if (_showDot()) S.of(context).synchronizing, + ]); + + if (label.isEmpty) return row; + + return Semantics(label: label, child: ExcludeSemantics(child: row)); + } + + String _statusSemanticsLabel(BuildContext context, SyncStatus status) { + final isFailure = failStatuses.contains(status.runtimeType); + + return _joinLabels([ + syncStatusTitle(status, dashboardViewModel.settingsStore.syncStatusDisplayMode), + if (!isFailure && dashboardViewModel.isTorEnabled) S.of(context).tor_connection, + if (!isFailure && dashboardViewModel.hasMweb) S.of(context).litecoin_mweb, + if (!isFailure && dashboardViewModel.hasSilentPayments) S.of(context).silent_payments, + ]); + } + + String _joinLabels(List parts) => parts.where((part) => part.isNotEmpty).join(", "); + Color? _getBackgroundColor(BuildContext context, Type status) { if (failStatuses.contains(status)) { return Theme.of(context).colorScheme.errorContainer.withAlpha(64); diff --git a/lib/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart b/lib/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart index de08f70bd..71627f9d9 100644 --- a/lib/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart +++ b/lib/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/chain_icon.dart'; import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart'; import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart'; @@ -53,6 +54,7 @@ class TopBar extends StatelessWidget { onSettingsButtonPress(); }, svgPath: "assets/new-ui/top-settings.svg", + semanticLabel: S.of(context).settings_title, ), ], ), diff --git a/lib/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart b/lib/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart index 5420d6a3f..912a592dd 100644 --- a/lib/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart +++ b/lib/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart @@ -1,8 +1,10 @@ import 'package:cake_wallet/generated/i18n.dart'; +import "package:cake_wallet/new-ui/widgets/coins_page/zcash_migration_modal.dart"; import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import "package:flutter/cupertino.dart"; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; @@ -21,6 +23,7 @@ class UnconfirmedBalanceWidget extends StatelessWidget { return Observer(builder: (_) { final show = dashboardViewModel.balanceViewModel .hasAdditionalBalance(dashboardViewModel.wallet.currency); + final isIronwoodMigration = dashboardViewModel.isMigratingToIronwood; return AnimatedSize( duration: const Duration(milliseconds: 300), @@ -44,13 +47,17 @@ class UnconfirmedBalanceWidget extends StatelessWidget { color: Colors.transparent, child: InkWell( onTap: () { + final modal = isIronwoodMigration + ? ZcashMigrationModal( + hasContinue: false, balance: "${balance} ${currency.symbol}") + : UnconfirmedBalanceModal( + balance: balance.toStringWithSymbol(), + currencyIconPath: currency.iconPath ?? "", + ); showMaterialModalBottomSheet( backgroundColor: Colors.transparent, context: context, - builder: (context) => UnconfirmedBalanceModal( - balance: balance.toStringWithSymbol(), - currencyIconPath: currency.iconPath ?? "", - )); + builder: (context) => modal); }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), @@ -63,8 +70,12 @@ class UnconfirmedBalanceWidget extends StatelessWidget { SizedBox( height: 20, width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, + child: isIronwoodMigration + ? CupertinoActivityIndicator( + color: Theme.of(context).colorScheme.primary, + ) + : CircularProgressIndicator( + strokeWidth: 2, backgroundColor: Theme.of(context).colorScheme.primary.withAlpha(50), color: Theme.of(context).colorScheme.primary, @@ -80,7 +91,7 @@ class UnconfirmedBalanceWidget extends StatelessWidget { color: Theme.of(context).colorScheme.primary, ), ), - Text(S.of(context).confirming), + Text(isIronwoodMigration ? S.of(context).migrating_to_ironwood : S.of(context).confirming), ], ) ], @@ -125,6 +136,7 @@ class UnconfirmedBalanceModal extends StatelessWidget { ModalTopBar( title: S.of(context).balance_confirmation, leadingIcon: Icon(Icons.close), + leadingSemanticLabel: S.of(context).close, onLeadingPressed: Navigator.of(context).pop), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), @@ -171,3 +183,4 @@ class UnconfirmedBalanceModal extends StatelessWidget { ); } } + diff --git a/lib/new-ui/widgets/coins_page/wallet_info.dart b/lib/new-ui/widgets/coins_page/wallet_info.dart index 212f57798..dcdbc23a5 100644 --- a/lib/new-ui/widgets/coins_page/wallet_info.dart +++ b/lib/new-ui/widgets/coins_page/wallet_info.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cw_core/wallet_info.dart'; @@ -20,15 +21,22 @@ class WalletInfoBar extends StatelessWidget { final bool hasCustomize; final VoidCallback onCustomizeButtonTap; + void _openAccountCustomizer() { + if (hasCustomize) { + onCustomizeButtonTap(); + HapticFeedback.mediumImpact(); + } + } + @override Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - if (hasCustomize) { - onCustomizeButtonTap(); - HapticFeedback.mediumImpact(); - } - }, + // The row, the hardware-wallet glyph and the inner accounts button are one + // control for a screen reader: a single labeled node opening the customizer. + final semanticsLabel = + hardwareWalletType == null ? name : "$name, ${S.of(context).hardware_wallet}"; + + final row = GestureDetector( + onTap: _openAccountCustomizer, child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, @@ -69,18 +77,26 @@ class WalletInfoBar extends StatelessWidget { SizedBox(width: 8), ModernButton.svg( size: 24, - onPressed: () { - if (hasCustomize) { - onCustomizeButtonTap(); - HapticFeedback.mediumImpact(); - } - }, + onPressed: _openAccountCustomizer, svgPath: "assets/new-ui/icon-accounts.svg", + semanticLabel: S.of(context).wallet_accounts, ) ] ], ), ); + + if (!hasCustomize) { + return Semantics(label: semanticsLabel, child: ExcludeSemantics(child: row)); + } + + return Semantics( + button: true, + label: semanticsLabel, + hint: S.of(context).wallet_accounts, + onTap: _openAccountCustomizer, + child: ExcludeSemantics(child: row), + ); } String? get hardwareWalletIcon { diff --git a/lib/new-ui/widgets/coins_page/zcash_migration_modal.dart b/lib/new-ui/widgets/coins_page/zcash_migration_modal.dart new file mode 100644 index 000000000..e1f66b1fb --- /dev/null +++ b/lib/new-ui/widgets/coins_page/zcash_migration_modal.dart @@ -0,0 +1,152 @@ +import "package:cake_wallet/generated/i18n.dart"; +import "package:cake_wallet/new-ui/widgets/new_primary_button.dart"; +import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart"; +import "package:cake_wallet/src/widgets/cake_image_widget.dart"; +import "package:flutter/material.dart"; +import "package:url_launcher/url_launcher.dart"; + +class ZcashMigrationModal extends StatelessWidget { + const ZcashMigrationModal({required this.hasContinue, required this.balance, super.key}); + + final String balance; + final bool hasContinue; + + @override + Widget build(BuildContext context) => SafeArea( + bottom: false, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),), + child: SafeArea( + top: false, + child: Column( + children: [ + ModalTopBar( + title: S.of(context).zcash_network_upgrade, + trailingIcon: const Icon(Icons.close), + onTrailingPressed: Navigator.of(context).pop, + trailingSemanticLabel: S.of(context).close, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + Expanded( + child: Column( + spacing: 40, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 146, + height: 79, + child: Stack( + children: [ + const Positioned( + left: 67, + top: 0, + bottom: 0, + child: CakeImageWidget( + imageUrl: "assets/new-ui/ironwood_migration.svg", + width: 75, + height: 75, + ), + ), + Positioned( + left: 0, + child: Container( + width: 79, + height: 79, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(99999999), + border: Border.all( + color: Theme.of(context).colorScheme.surface, + width: 4,),), + child: const CakeImageWidget( + imageUrl: "assets/new-ui/crypto_full_icons/zcash.svg", + height: 75, + width: 75, + ), + ), + ), + ], + ), + ), + Container( + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Theme.of(context).colorScheme.surfaceContainer,), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 4, + children: [ + Text( + balance, + style: TextStyle( + color: Theme.of(context).colorScheme.primary,), + ), + Text(S.of(context).is_currently_being_migrated), + ], + ), + ),), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: "${S.of(context).zcash_network_upgrade_desc_1}\n\n", + style: Theme.of(context).textTheme.bodyMedium,), + TextSpan( + text: "${S.of(context).zcash_network_upgrade_desc_2}\n\n", + style: Theme.of(context).textTheme.bodyMedium,), + TextSpan( + text: "${S.of(context).zcash_network_upgrade_desc_3}\n\n", + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: const Color(0xFFF9C03C), + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(12), + child: Column( + spacing: 12, + children: [ + NewPrimaryButton( + onPressed: _launchDocs, + text: S.of(context).learn_more, + color: Theme.of(context).colorScheme.surfaceContainer, + textColor: Theme.of(context).colorScheme.primary,), + if (hasContinue) + NewPrimaryButton( + onPressed: Navigator.of(context).pop, + text: S.of(context).continue_text, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary,), + ], + ), + ), + ], + ), + ), + ), + ); + + void _launchDocs() { + try { + launchUrl(Uri.parse("https://docs.cakewallet.com/cryptos/zcash/ironwood-migration")); + } catch (_) {} + } +} diff --git a/lib/new-ui/widgets/copy_wrapper.dart b/lib/new-ui/widgets/copy_wrapper.dart index 0f3255c48..3f9011ab4 100644 --- a/lib/new-ui/widgets/copy_wrapper.dart +++ b/lib/new-ui/widgets/copy_wrapper.dart @@ -1,8 +1,10 @@ import 'dart:io'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/utils/clipboard_util.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; import 'package:flutter/services.dart'; class CopyWrapper extends StatefulWidget { @@ -11,13 +13,25 @@ class CopyWrapper extends StatefulWidget { this.data, this.requireLongPress = false, this.isSensitive = false, - required this.builder, - this.duration = const Duration(milliseconds: 1200)}); + this.builder, + this.controlBuilder, + this.duration = const Duration(milliseconds: 1200)}) + : assert((builder == null) != (controlBuilder == null), + "Provide exactly one of builder or controlBuilder"); final ClipboardData? data; final bool isSensitive; final bool requireLongPress; - final Widget Function(BuildContext, bool) builder; + + /// Builds content that this widget makes copyable with its own gesture + /// detector. Receives the transient "copied" state. + final Widget Function(BuildContext, bool)? builder; + + /// Builds a control that performs the copy itself, so no extra gesture + /// detector or actionable semantics node is stacked on top of it. [onCopy] is + /// null when there is nothing to copy. + final Widget Function(BuildContext context, bool copied, VoidCallback? onCopy)? controlBuilder; + final Duration duration; @override @@ -31,6 +45,11 @@ class _CopyWrapperState extends State { if (widget.data == null) return; ClipboardUtil.setSensitiveDataToClipboard(widget.data!, isSensitive: widget.isSensitive); HapticFeedback.mediumImpact(); + // Screen readers learn about the copy from the transient "copied" state + // below, which build() marks as a live region, instead of from a push + // announcement: those are deprecated on Android and make TalkBack drop + // whatever it was saying. Where we don't render that state, android 13+ + // already shows its own clipboard confirmation. if (await shouldShowCopied()) { setState(() => copied = true); Future.delayed(widget.duration, () { @@ -41,11 +60,41 @@ class _CopyWrapperState extends State { @override Widget build(BuildContext context) { - return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: widget.requireLongPress ? null : handleCopy, - onLongPress: !widget.requireLongPress ? null : handleCopy, - child: widget.builder(context, copied), + final VoidCallback? onCopy = widget.data == null ? null : handleCopy; + + // The control's own label carries the copied state (Copy -> Copied), so + // merging it into one node and marking that node a live region while copied + // is enough for a screen reader to pick the change up. + if (widget.controlBuilder != null) { + return MergeSemantics( + child: Semantics( + liveRegion: copied, + child: widget.controlBuilder!(context, copied, onCopy), + ), + ); + } + + final content = widget.builder!(context, copied); + + // Nothing to copy: don't leave behind a gesture detector that a screen + // reader would present as an actionable target that does nothing. + if (onCopy == null) return content; + + return MergeSemantics( + child: Semantics( + button: !widget.requireLongPress, + liveRegion: copied, + hint: widget.requireLongPress ? S.of(context).long_press_to_copy : S.of(context).copy, + customSemanticsActions: { + CustomSemanticsAction(label: S.of(context).copy): onCopy, + }, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: widget.requireLongPress ? null : onCopy, + onLongPress: !widget.requireLongPress ? null : onCopy, + child: content, + ), + ), ); } diff --git a/lib/new-ui/widgets/currency_picker/chain_chip_strip.dart b/lib/new-ui/widgets/currency_picker/chain_chip_strip.dart index 76b1727a2..17910a4c5 100644 --- a/lib/new-ui/widgets/currency_picker/chain_chip_strip.dart +++ b/lib/new-ui/widgets/currency_picker/chain_chip_strip.dart @@ -56,24 +56,31 @@ class _ChainChip extends StatelessWidget { final colors = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsetsDirectional.only(end: 8), - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(80), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 10), - decoration: BoxDecoration( - color: isSelected ? colors.primary : colors.surfaceContainer, - borderRadius: BorderRadius.circular(20), - ), - child: Center( - child: Text( - label, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w500, - fontSize: isSelected ? 14 : 12, - letterSpacing: -0.07, - color: isSelected ? colors.onPrimary : colors.primary, - ), + child: MergeSemantics( + child: Semantics( + button: true, + selected: isSelected, + inMutuallyExclusiveGroup: true, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(80), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10), + decoration: BoxDecoration( + color: isSelected ? colors.primary : colors.surfaceContainer, + borderRadius: BorderRadius.circular(20), + ), + child: Center( + child: Text( + label, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + fontSize: isSelected ? 14 : 12, + letterSpacing: -0.07, + color: isSelected ? colors.onPrimary : colors.primary, + ), + ), + ), ), ), ), diff --git a/lib/new-ui/widgets/currency_picker/currency_picker_row.dart b/lib/new-ui/widgets/currency_picker/currency_picker_row.dart index 54d712200..52ac4c065 100644 --- a/lib/new-ui/widgets/currency_picker/currency_picker_row.dart +++ b/lib/new-ui/widgets/currency_picker/currency_picker_row.dart @@ -24,55 +24,65 @@ class CurrencyPickerRow extends StatelessWidget { @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; - return InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), - child: Row( - children: [ - _IconWithBadge(currency: currency, badgePath: chainBadgePath), - const SizedBox(width: 12), - Expanded( - child: Row( - children: [ - Flexible( - child: Text( - currency.fullName ?? currency.title, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w500, + return MergeSemantics( + child: Semantics( + button: true, + selected: isSelected, + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + child: Row( + children: [ + ExcludeSemantics( + child: _IconWithBadge(currency: currency, badgePath: chainBadgePath), + ), + const SizedBox(width: 12), + Expanded( + child: Row( + children: [ + Flexible( + child: Text( + currency.fullName ?? currency.title, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + ), + if (chainPillLabel != null) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: colors.surfaceContainerHigh, + borderRadius: BorderRadius.circular(10), ), - ), + child: Text( + chainPillLabel!, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontWeight: FontWeight.w500, + color: colors.onSecondaryContainer, + ), + ), + ), + ], + ], ), - if (chainPillLabel != null) ...[ - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: colors.surfaceContainerHigh, - borderRadius: BorderRadius.circular(10), - ), - child: Text( - chainPillLabel!, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - fontWeight: FontWeight.w500, - color: colors.onSecondaryContainer, - ), - ), - ), - ], - ], - ), + ), + const SizedBox(width: 8), + trailing, + const SizedBox(width: 8), + ExcludeSemantics( + child: Icon( + Icons.chevron_right, + size: 20, + color: isSelected ? colors.primary : colors.onSurfaceVariant, + ), + ), + ], ), - const SizedBox(width: 8), - trailing, - const SizedBox(width: 8), - Icon( - Icons.chevron_right, - size: 20, - color: isSelected ? colors.primary : colors.onSurfaceVariant, - ), - ], + ), ), ), ); diff --git a/lib/new-ui/widgets/currency_picker/currency_picker_search_field.dart b/lib/new-ui/widgets/currency_picker/currency_picker_search_field.dart index 8a62c35a7..25876f0f1 100644 --- a/lib/new-ui/widgets/currency_picker/currency_picker_search_field.dart +++ b/lib/new-ui/widgets/currency_picker/currency_picker_search_field.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/material.dart'; @@ -27,30 +28,41 @@ class CurrencyPickerSearchField extends StatelessWidget { ), child: Row( children: [ - Icon(Icons.search, size: 20, color: colors.primary), + ExcludeSemantics(child: Icon(Icons.search, size: 20, color: colors.primary)), const SizedBox(width: 10), Expanded( - child: TextFormField( - controller: controller, - style: Theme.of(context).textTheme.bodyMedium, - decoration: InputDecoration( - isCollapsed: true, - border: InputBorder.none, - focusedBorder: InputBorder.none, - hintText: hintText, - hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colors.onSurfaceVariant, - ), + // The hint names the field only while it is empty, so the label is + // supplied once there is text to keep exactly one announcement. + child: MergeSemantics( + child: Semantics( + label: controller.text.isEmpty ? null : hintText, + child: TextFormField( + controller: controller, + style: Theme.of(context).textTheme.bodyMedium, + decoration: InputDecoration( + isCollapsed: true, + border: InputBorder.none, + focusedBorder: InputBorder.none, + hintText: hintText, + hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colors.onSurfaceVariant, + ), + ), + ), ), ), ), if (controller.text.isNotEmpty) - InkWell( - onTap: controller.clear, - customBorder: const CircleBorder(), - child: Padding( - padding: const EdgeInsets.all(4), - child: Icon(Icons.close, size: 16, color: colors.onSurfaceVariant), + Semantics( + button: true, + label: S.of(context).clear, + child: InkWell( + onTap: controller.clear, + customBorder: const CircleBorder(), + child: Padding( + padding: const EdgeInsets.all(4), + child: Icon(Icons.close, size: 16, color: colors.onSurfaceVariant), + ), ), ), ], diff --git a/lib/new-ui/widgets/currency_picker/currency_picker_sheet.dart b/lib/new-ui/widgets/currency_picker/currency_picker_sheet.dart index be6600d52..a59b8d71b 100644 --- a/lib/new-ui/widgets/currency_picker/currency_picker_sheet.dart +++ b/lib/new-ui/widgets/currency_picker/currency_picker_sheet.dart @@ -43,6 +43,7 @@ class CurrencyPickerSheet extends StatelessWidget { ModalTopBar( title: S.of(context).select_asset, leadingIcon: const Icon(Icons.close), + leadingSemanticLabel: S.of(context).close, onLeadingPressed: () => Navigator.of(context).maybePop(), ), Expanded( diff --git a/lib/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart b/lib/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart index 47dba9f24..c4713708d 100644 --- a/lib/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart +++ b/lib/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart @@ -117,6 +117,7 @@ class _FiatCurrencyPickerSheetState extends State { ModalTopBar( title: S.of(context).select_fiat_currency_title, leadingIcon: const Icon(Icons.close), + leadingSemanticLabel: S.of(context).close, onLeadingPressed: () => Navigator.of(context).maybePop(), ), Expanded( diff --git a/lib/new-ui/widgets/currency_picker/select_network_page.dart b/lib/new-ui/widgets/currency_picker/select_network_page.dart index 988dc3b24..b395ec08c 100644 --- a/lib/new-ui/widgets/currency_picker/select_network_page.dart +++ b/lib/new-ui/widgets/currency_picker/select_network_page.dart @@ -35,14 +35,18 @@ class SelectNetworkPage extends StatelessWidget { ModalTopBar( title: S.of(context).select_network_title, leadingIcon: const Icon(Icons.arrow_back), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: () => Navigator.of(context).maybePop(), ), const SizedBox(height: 24), - CakeImageWidget( - imageUrl: assetIconPath, - width: 75, - height: 75, - fit: BoxFit.cover, + // Illustration of the asset named by the title below it. + ExcludeSemantics( + child: CakeImageWidget( + imageUrl: assetIconPath, + width: 75, + height: 75, + fit: BoxFit.cover, + ), ), const SizedBox(height: 12), Text( @@ -106,33 +110,42 @@ class _NetworkRow extends StatelessWidget { final networkIconPath = wt != null ? getCryptoCurrencyIconForWalletListItem(wt) : variant.iconPath; final networkName = chainNameForCurrency(variant); - return InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), - child: Row( - children: [ - CakeImageWidget( - imageUrl: networkIconPath, - width: 28, - height: 28, - fit: BoxFit.cover, + return MergeSemantics( + child: Semantics( + button: true, + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + child: Row( + children: [ + ExcludeSemantics( + child: CakeImageWidget( + imageUrl: networkIconPath, + width: 28, + height: 28, + fit: BoxFit.cover, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + networkName, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + ), + ExcludeSemantics( + child: Icon( + Icons.chevron_right, + size: 20, + color: colors.onSurfaceVariant, + ), + ), + ], ), - const SizedBox(width: 12), - Expanded( - child: Text( - networkName, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w500, - ), - ), - ), - Icon( - Icons.chevron_right, - size: 20, - color: colors.onSurfaceVariant, - ), - ], + ), ), ), ); diff --git a/lib/new-ui/widgets/digit_input.dart b/lib/new-ui/widgets/digit_input.dart index d6889db67..2994a75e9 100644 --- a/lib/new-ui/widgets/digit_input.dart +++ b/lib/new-ui/widgets/digit_input.dart @@ -1,5 +1,7 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; +import "dart:io"; + +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; class DigitInputController { String _text = ""; @@ -19,38 +21,37 @@ class DigitInputController { } class DigitInputPill extends StatelessWidget { - const DigitInputPill({super.key, this.digit, required this.highlighted}); + const DigitInputPill({required this.highlighted, super.key, this.digit}); final String? digit; final bool highlighted; @override - Widget build(BuildContext context) { - return Container( - width: 51, - height: 84, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHigh, - borderRadius: BorderRadius.circular(16), - border: - highlighted ? Border.all(color: Theme.of(context).colorScheme.primary, width: 2) : null, - ), - child: Center( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 16), - child: Text(digit ?? " ", style: TextStyle(fontSize: 40)), + Widget build(BuildContext context) => Container( + width: 51, + height: 84, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(16), + border: highlighted + ? Border.all(color: Theme.of(context).colorScheme.primary, width: 2) + : null, ), - ), - ); - } + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Text(digit ?? " ", style: const TextStyle(fontSize: 40)), + ), + ), + ); } class DigitInput extends StatefulWidget { const DigitInput({ - super.key, required this.controller, required this.desiredLength, this.breakAt = 3, + super.key, }); final DigitInputController controller; @@ -74,12 +75,9 @@ class _DigitInputState extends State implements TextInputClient { if (_textInputConnection == null || !_textInputConnection!.attached) { _textInputConnection = TextInput.attach( this, - const TextInputConfiguration( - inputType: TextInputType.numberWithOptions( - signed: false, - decimal: false, - ), - inputAction: TextInputAction.none, + TextInputConfiguration( + inputType: TextInputType.number, + inputAction: Platform.isIOS ? TextInputAction.done : TextInputAction.none, ), ); } @@ -102,30 +100,28 @@ class _DigitInputState extends State implements TextInputClient { } @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: _openVirtualKeyboard, - child: SizedBox( - height: 84, - child: ListView.separated( - scrollDirection: Axis.horizontal, - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemBuilder: (context, index) => DigitInputPill( - digit: widget.controller.text.length > index ? widget.controller.text[index] : null, - highlighted: widget.controller.text.length == index, + Widget build(BuildContext context) => GestureDetector( + onTap: _openVirtualKeyboard, + child: SizedBox( + height: 84, + child: ListView.separated( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => DigitInputPill( + digit: widget.controller.text.length > index ? widget.controller.text[index] : null, + highlighted: widget.controller.text.length == index, + ), + separatorBuilder: (context, index) => + SizedBox(width: (index + 1) % widget.breakAt == 0 ? 16 : 4), + itemCount: widget.desiredLength, ), - separatorBuilder: (context, index) => - SizedBox(width: (index + 1) % widget.breakAt == 0 ? 16 : 4), - itemCount: widget.desiredLength, ), - ), - ); - } + ); @override void updateEditingValue(TextEditingValue value) { - final RegExp numberRegExp = RegExp(r'^\d{0,6}$'); + final RegExp numberRegExp = RegExp(r"^\d{0,6}$"); if (numberRegExp.hasMatch(value.text)) { setState(() => widget.controller.text = value.text); diff --git a/lib/new-ui/widgets/dropdown_row.dart b/lib/new-ui/widgets/dropdown_row.dart index d06a2b291..7772cd8a9 100644 --- a/lib/new-ui/widgets/dropdown_row.dart +++ b/lib/new-ui/widgets/dropdown_row.dart @@ -11,38 +11,45 @@ class DropdownRow extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.vertical( - top: Radius.circular(18), - bottom: expanded ? Radius.zero : Radius.circular(18), + return Semantics( + button: true, + expanded: expanded, + label: text, + onTap: onTap, + excludeSemantics: true, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.vertical( + top: Radius.circular(18), + bottom: expanded ? Radius.zero : Radius.circular(18), + ), ), - ), - height: 48, - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - text, - style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.primary), - ), - AnimatedRotation( - duration: Duration(milliseconds: 300), - turns: expanded ? 0.0 : 0.5, - curve: Curves.easeOut, - child: CakeImageWidget( - imageUrl: "assets/new-ui/dropdown_arrow.svg", - colorFilter: - ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn), - )) - ], + height: 48, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + text, + style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.primary), + ), + AnimatedRotation( + duration: Duration(milliseconds: 300), + turns: expanded ? 0.0 : 0.5, + curve: Curves.easeOut, + child: CakeImageWidget( + imageUrl: "assets/new-ui/dropdown_arrow.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, BlendMode.srcIn), + )) + ], + ), ), ), ), diff --git a/lib/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart b/lib/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart index 2a06f37e6..b68a4011b 100644 --- a/lib/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart +++ b/lib/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart @@ -106,6 +106,7 @@ class _HardwareWalletProceedOnDeviceSheetState extends State showPopUp( context: context, builder: (context) => AlertWithTwoActions( @@ -143,6 +144,7 @@ class _HardwareWalletProceedOnDeviceSheetState extends State widget.trezorConnectVM.setParingPin(_controller.text.trim()), ), diff --git a/lib/new-ui/widgets/hardware_wallet/sync_key_images_sheet.dart b/lib/new-ui/widgets/hardware_wallet/sync_key_images_sheet.dart new file mode 100644 index 000000000..d8febdcbf --- /dev/null +++ b/lib/new-ui/widgets/hardware_wallet/sync_key_images_sheet.dart @@ -0,0 +1,224 @@ +import "package:cake_wallet/generated/i18n.dart"; +import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart"; +import "package:cake_wallet/new-ui/widgets/send_page/directional_switcher.dart"; +import "package:cake_wallet/routes.dart"; +import "package:cake_wallet/src/screens/connect_device/connect_device_page.dart"; +import "package:cake_wallet/src/widgets/alert_with_two_actions.dart"; +import "package:cake_wallet/src/widgets/base_alert_dialog.dart"; +import "package:cake_wallet/src/widgets/cake_image_widget.dart"; +import "package:cake_wallet/src/widgets/primary_button.dart"; +import "package:cake_wallet/store/app_store.dart"; +import "package:cake_wallet/utils/show_pop_up.dart"; +import "package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart"; +import "package:cw_core/wallet_base.dart"; +import "package:cw_core/wallet_info.dart"; +import "package:flutter/cupertino.dart"; +import "package:flutter/material.dart"; + +class SyncKeyImagesSheet extends StatefulWidget { + const SyncKeyImagesSheet({ + required this.appStore, + required this.trezorConnectVM, + super.key, + }); + + final AppStore appStore; + final TrezorConnectViewModelBase trezorConnectVM; + + WalletBase get wallet => appStore.wallet!; + + @override + State createState() => _HardwareWalletProceedOnDeviceSheetState(); +} + +class _HardwareWalletProceedOnDeviceSheetState extends State { + _KeyImageSyncState _state = _KeyImageSyncState.initial; + + @override + Widget build(BuildContext context) => PopScope( + canPop: false, + child: SafeArea( + bottom: false, + child: Container( + width: MediaQuery.of(context).size.width, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + ), + child: SafeArea( + child: Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: Column( + children: [ + ModalTopBar( + title: S.of(context).resync_device, + leadingIcon: Icon( + Icons.close, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + onLeadingPressed: () => showPopUp( + context: context, + builder: (context) => AlertWithTwoActions( + alertTitle: S.of(context).are_you_sure_exit, + alertContent: S.of(context).resync_device_cancel_warning_desc, + leftButtonText: S.of(context).cancel, + rightButtonText: S.of(context).yes_exit, + rightAlertButtonStyle: AlertButtonStyle.error(context), + actionLeftButton: Navigator.of(context).pop, + actionRightButton: () { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + }, + ), + ), + ), + Expanded( + child: Column( + spacing: 12, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CakeImageWidget( + imageUrl: hardwareWalletIcon, + width: 100, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, + BlendMode.srcIn, + ), + ), + DirectionalAnimatedSwitcher( + duration: const Duration(milliseconds: 400), + child: content, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: AnimatedOpacity( + curve: Curves.easeOutQuad, + opacity: _state == _KeyImageSyncState.initial ? 1 : 0, + duration: const Duration(milliseconds: 300), + child: PrimaryButton( + text: S.of(context).continue_text, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + onPressed: _onContinuePressed, + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + + Widget get content { + if (_state == _KeyImageSyncState.syncing) { + return _syncingKeyImages(); + } + + return _initial(); + } + + String? get hardwareWalletIcon { + switch (widget.wallet.hardwareWalletType) { + case HardwareWalletType.bitbox: + return "assets/new-ui/hardware_wallets/device_bitbox.svg"; + case HardwareWalletType.ledger: + return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg"; + case HardwareWalletType.trezor: + return "assets/new-ui/hardware_wallets/device_trezor_safe_7.svg"; + case HardwareWalletType.cupcake: + return "assets/images/cupcake.svg"; + case HardwareWalletType.coldcard: + case HardwareWalletType.seedsigner: + case HardwareWalletType.keystone: + case null: + return "assets/images/hardware_wallet/device_qr.svg"; + } + } + + Widget _initial() => Column( + key: const ValueKey(0), + spacing: 12, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + S.of(context).resync_device_desc, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ], + ); + + Widget _syncingKeyImages() => Column( + key: const ValueKey(1), + spacing: 12, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + S.of(context).proceed_on_device, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 20, + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 20), + child: Text( + S.of(context).proceed_on_device_description, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ) + ], + ); + + Future _onContinuePressed() async { + setState(() => _state = _KeyImageSyncState.syncing); + + if (!widget.trezorConnectVM.isConnected(widget.wallet.type)) { + await Navigator.of(context).pushNamed( + Routes.connectDevices, + arguments: ConnectDevicePageParams( + walletType: widget.wallet.type, + hardwareWalletType: widget.wallet.walletInfo.hardwareWalletType!, + onConnectDevice: (_, __) { + widget.trezorConnectVM.initWallet(widget.wallet); + Navigator.of(context).pop(); + }, + isReconnect: false, + ), + ); + + // Recheck to handle tap-backs + if (!widget.trezorConnectVM.isConnected(widget.wallet.type)) { + setState(() => _state = _KeyImageSyncState.initial); + return; + } + } else { + await widget.trezorConnectVM.initWallet(widget.wallet); + } + + final result = await widget.trezorConnectVM.syncKeyImages(widget.wallet); + if (result && context.mounted) { + Navigator.of(context).pop(); + } + } +} + +enum _KeyImageSyncState { + initial, + syncing; +} diff --git a/lib/new-ui/widgets/hardware_wallet/trezor_paring_sheet.dart b/lib/new-ui/widgets/hardware_wallet/trezor_paring_sheet.dart index 9b429edc4..5d0dcf1cc 100644 --- a/lib/new-ui/widgets/hardware_wallet/trezor_paring_sheet.dart +++ b/lib/new-ui/widgets/hardware_wallet/trezor_paring_sheet.dart @@ -32,6 +32,7 @@ class _HardwareWalletTrezorParingSheetState extends State { children: widget.tabs.map((item) { final index = widget.tabs.indexOf(item); - return GestureDetector( - onTap: () { - widget.onTabChange(index); - }, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AnimatedDefaultTextStyle( - duration: Duration(milliseconds: 150), - style: DefaultTextStyle.of(context).style.copyWith( - inherit: true, - fontSize: 16, - fontWeight: FontWeight.w500, - color: widget.selectedTab == index - ? Theme.of(context).colorScheme.onSurface - : Theme.of(context).colorScheme.onSurfaceVariant, + // One tab = one node: the visible label merges into a button node + // that also carries the selected state of this tab. + return MergeSemantics( + child: Semantics( + button: true, + selected: widget.selectedTab == index, + inMutuallyExclusiveGroup: true, + child: GestureDetector( + onTap: () { + widget.onTabChange(index); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedDefaultTextStyle( + duration: Duration(milliseconds: 150), + style: DefaultTextStyle.of(context).style.copyWith( + inherit: true, + fontSize: 16, + fontWeight: FontWeight.w500, + color: widget.selectedTab == index + ? Theme.of(context).colorScheme.onSurface + : Theme.of(context).colorScheme.onSurfaceVariant, + ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: itemPadding / 2), + child: Text( + item, + key: textWidgetKeys[index], + ), ), - child: Padding( - padding: EdgeInsets.symmetric(horizontal: itemPadding / 2), - child: Text( - item, - key: textWidgetKeys[index], ), - ), + ], ), - ], + ), ), ); }).toList())), diff --git a/lib/new-ui/widgets/long_press_menu.dart b/lib/new-ui/widgets/long_press_menu.dart index 3e660457b..6a97f0fdb 100644 --- a/lib/new-ui/widgets/long_press_menu.dart +++ b/lib/new-ui/widgets/long_press_menu.dart @@ -60,31 +60,38 @@ class _LongPressMenuState extends State { final color = item.color ?? Theme.of(context).colorScheme.onSurface; return Material( color: Colors.transparent, - child: InkWell( - onTap: item.onSelected, - child: Padding( - padding: EdgeInsets.only( - left: 16, - right: 16, - top: 12, - bottom: 12, - ), - child: Container( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.start, - spacing: 8, - children: [ - CakeImageWidget( - imageUrl: item.iconPath, - height: 20, - width: 20, - colorFilter: ColorFilter.mode(color, BlendMode.srcIn), + child: MergeSemantics( + child: Semantics( + button: true, + child: InkWell( + onTap: item.onSelected, + child: Padding( + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 12, + bottom: 12, + ), + child: Container( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + spacing: 8, + children: [ + ExcludeSemantics( + child: CakeImageWidget( + imageUrl: item.iconPath, + height: 20, + width: 20, + colorFilter: ColorFilter.mode(color, BlendMode.srcIn), + ), + ), + Text(item.label, + style: TextStyle( + color: color, fontSize: 14, fontWeight: FontWeight.w500)), + ], ), - Text(item.label, - style: TextStyle( - color: color, fontSize: 14, fontWeight: FontWeight.w500)), - ], + ), ), ), ), diff --git a/lib/new-ui/widgets/modern_button.dart b/lib/new-ui/widgets/modern_button.dart index 6f5e5a596..748aea817 100644 --- a/lib/new-ui/widgets/modern_button.dart +++ b/lib/new-ui/widgets/modern_button.dart @@ -11,6 +11,11 @@ class ModernButton extends StatelessWidget { final Color? backgroundColor; final double? iconSize; final String? label; + + /// Accessible name announced by screen readers. Falls back to [label] (the + /// visible caption) when omitted, so one of the two is required. Must be + /// localized by the caller. + final String? semanticLabel; static const iconSvgSizeRatio = 2 / 3; const ModernButton( @@ -21,7 +26,8 @@ class ModernButton extends StatelessWidget { this.iconSize, this.iconColor, this.backgroundColor, - this.label}) + this.label, + this.semanticLabel}) : svgPath = null; const ModernButton.svg( @@ -32,7 +38,8 @@ class ModernButton extends StatelessWidget { this.iconSize, this.iconColor, this.backgroundColor, - this.label}) + this.label, + this.semanticLabel}) : icon = null; @override @@ -56,31 +63,42 @@ class ModernButton extends StatelessWidget { child: icon!, ); - return Column( - spacing: 8, - children: [ - Container( - decoration: BoxDecoration( - color: resolvedBackgroundColor, - borderRadius: BorderRadius.circular(size), - ), - width: size, - height: size, - child: IconButton( - padding: EdgeInsets.zero, - onPressed: onPressed, - icon: resolvedIcon, - ), + // A single accessibility node for the whole control: the visible caption + // duplicates the accessible name, so it is excluded to avoid a second, + // detached stop for screen readers. + return MergeSemantics( + child: Semantics( + button: true, + label: semanticLabel ?? label, + child: Column( + spacing: 8, + children: [ + Container( + decoration: BoxDecoration( + color: resolvedBackgroundColor, + borderRadius: BorderRadius.circular(size), + ), + width: size, + height: size, + child: IconButton( + padding: EdgeInsets.zero, + onPressed: onPressed, + icon: ExcludeSemantics(child: resolvedIcon), + ), + ), + if (label != null && label!.isNotEmpty) + ExcludeSemantics( + child: Text( + label!, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface), + ), + ) + ], ), - if (label != null && label!.isNotEmpty) - Text( - label!, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface), - ) - ], + ), ); } } diff --git a/lib/new-ui/widgets/money/currency_symbol_text.dart b/lib/new-ui/widgets/money/currency_symbol_text.dart new file mode 100644 index 000000000..82467cce5 --- /dev/null +++ b/lib/new-ui/widgets/money/currency_symbol_text.dart @@ -0,0 +1,165 @@ +import "package:cake_wallet/new-ui/widgets/money/money_settings_cubit.dart"; +import "package:cw_core/amount/money.dart"; +import "package:cw_core/currency.dart"; +import "package:flutter/widgets.dart"; +import "package:flutter_bloc/flutter_bloc.dart"; + +/// The [CurrencySymbolText] widget displays [Currency.symbol] based on +/// usersettings as a formated string of text with single style. +class CurrencySymbolText extends StatelessWidget { + const CurrencySymbolText( + this.currency, { + super.key, + this.style, + this.strutStyle, + this.textAlign, + this.textDirection, + this.locale, + this.softWrap, + this.overflow, + this.textScaler, + this.maxLines, + this.semanticsLabel, + this.semanticsIdentifier, + this.textWidthBasis, + this.textHeightBehavior, + this.selectionColor, + this.useBaseUnit, + }); + + /// The currency to display. + final Currency currency; + + /// If non-null, the style to use for this text. + /// + /// If the style's "inherit" property is true, the style will be merged with + /// the closest enclosing [DefaultTextStyle]. Otherwise, the style will + /// replace the closest enclosing [DefaultTextStyle]. + /// + /// The user or platform may override this [style]'s [TextStyle.fontWeight], + /// [TextStyle.height], [TextStyle.letterSpacing], and [TextStyle.wordSpacing] + /// via a [MediaQuery] ancestor's [MediaQueryData.boldText], + /// [MediaQueryData.lineHeightScaleFactorOverride], + /// [MediaQueryData.letterSpacingOverride], and [MediaQueryData.wordSpacingOverride] + /// regardless of its [TextStyle.inherit] value. + final TextStyle? style; + + /// The user or platform may override this [strutStyle]'s [StrutStyle.height] + /// via a [MediaQuery] ancestor's [MediaQueryData.lineHeightScaleFactorOverride]. + final StrutStyle? strutStyle; + + /// How the text should be aligned horizontally. + final TextAlign? textAlign; + + /// The directionality of the text. + /// + /// This decides how [textAlign] values like [TextAlign.start] and + /// [TextAlign.end] are interpreted. + /// + /// This is also used to disambiguate how to render bidirectional text. For + /// example, if the [data] is an English phrase followed by a Hebrew phrase, + /// in a [TextDirection.ltr] context the English phrase will be on the left + /// and the Hebrew phrase to its right, while in a [TextDirection.rtl] + /// context, the English phrase will be on the right and the Hebrew phrase on + /// its left. + /// + /// Defaults to the ambient [Directionality], if any. + final TextDirection? textDirection; + + /// Used to select a font when the same Unicode character can + /// be rendered differently, depending on the locale. + /// + /// It's rarely necessary to set this property. By default its value + /// is inherited from the enclosing app with `Localizations.localeOf(context)`. + /// + /// See [RenderParagraph.locale] for more information. + final Locale? locale; + + /// Whether the text should break at soft line breaks. + /// + /// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space. + final bool? softWrap; + + /// How visual overflow should be handled. + /// + /// If this is null [TextStyle.overflow] will be used, otherwise the value + /// from the nearest [DefaultTextStyle] ancestor will be used. + final TextOverflow? overflow; + + final TextScaler? textScaler; + + /// An optional maximum number of lines for the text to span, wrapping if necessary. + /// If the text exceeds the given number of lines, it will be truncated according + /// to [overflow]. + /// + /// If this is 1, text will not wrap. Otherwise, text will be wrapped at the + /// edge of the box. + /// + /// If this is null, but there is an ambient [DefaultTextStyle] that specifies + /// an explicit number for its [DefaultTextStyle.maxLines], then the + /// [DefaultTextStyle] value will take precedence. You can use a [RichText] + /// widget directly to entirely override the [DefaultTextStyle]. + final int? maxLines; + + /// An alternative semantics label for this text. + /// + /// If present, the semantics of this widget will contain this value instead + /// of the actual text. This will overwrite any of the semantics labels applied + /// directly to the [TextSpan]s. + /// + /// This is useful for replacing abbreviations or shorthands with the full + /// text value: + /// + /// ```dart + /// const Text(r'$$', semanticsLabel: 'Double dollars') + /// ``` + final String? semanticsLabel; + + /// A unique identifier for the semantics node for this widget. + /// + /// This is useful for cases where the text widget needs to have a uniquely + /// identifiable ID that is recognized through the automation tools without + /// having a dependency on the actual content of the text that can possibly be + /// dynamic in nature. + final String? semanticsIdentifier; + + final TextWidthBasis? textWidthBasis; + + final TextHeightBehavior? textHeightBehavior; + + /// The color to use when painting the selection. + /// + /// This is ignored if [SelectionContainer.maybeOf] returns null + /// in the [BuildContext] of the [Text] widget. + /// + /// If null, the ambient [DefaultSelectionStyle] is used (if any); failing + /// that, the selection color defaults to [DefaultSelectionStyle.defaultColor] + /// (semi-transparent grey). + final Color? selectionColor; + + /// Show the amount in the base unit format of [Money.currency] + /// + /// If null, the displayAmountsInSatoshi setting is used. + final bool? useBaseUnit; + + @override + Widget build(BuildContext context) => BlocBuilder( + builder: (context, state) => Text( + state.getSymbol(currency, overrideSettings: useBaseUnit), + style: style, + strutStyle: strutStyle, + textAlign: textAlign, + textDirection: textDirection, + locale: locale, + softWrap: softWrap, + overflow: overflow, + textScaler: textScaler, + maxLines: maxLines, + semanticsLabel: semanticsLabel, + semanticsIdentifier: semanticsIdentifier, + textWidthBasis: textWidthBasis, + textHeightBehavior: textHeightBehavior, + selectionColor: selectionColor, + ), + ); +} diff --git a/lib/new-ui/widgets/money/money_settings_cubit.dart b/lib/new-ui/widgets/money/money_settings_cubit.dart new file mode 100644 index 000000000..0cc03ff33 --- /dev/null +++ b/lib/new-ui/widgets/money/money_settings_cubit.dart @@ -0,0 +1,78 @@ +import "package:bloc/bloc.dart"; +import "package:cake_wallet/entities/balance_display_mode.dart"; +import "package:cake_wallet/entities/bitcoin_amount_display_mode.dart"; +import "package:cake_wallet/src/screens/wallet_connect/utils/string_parsing.dart"; +import "package:cake_wallet/store/settings_store.dart"; +import "package:cw_core/crypto_currency.dart"; +import "package:cw_core/currency.dart"; +import "package:mobx/mobx.dart"; + +class MoneySettingsCubit extends Cubit { + MoneySettingsCubit(SettingsStore _settingsStore) + : super( + MoneySettingsState( + bitcoinAmountDisplayMode: _settingsStore.displayAmountsInSatoshi, + displayMode: _settingsStore.balanceDisplayMode, + ), + ) { + _bitcoinAmountDisplayModeDisposer = reaction( + (_) => _settingsStore.displayAmountsInSatoshi, + (displayMode) => emit(state.copyWith(bitcoinAmountDisplayMode: displayMode)), + ); + _displayModeDisposer = reaction( + (_) => _settingsStore.balanceDisplayMode, + (displayMode) => emit(state.copyWith(displayMode: displayMode)), + ); + } + + late final ReactionDisposer _bitcoinAmountDisplayModeDisposer; + + late final ReactionDisposer _displayModeDisposer; + + @override + Future close() { + if (!_bitcoinAmountDisplayModeDisposer.reaction.isDisposed) { + _bitcoinAmountDisplayModeDisposer.reaction.dispose(); + } + if (!_displayModeDisposer.reaction.isDisposed) { + _displayModeDisposer.reaction.dispose(); + } + return super.close(); + } +} + +class MoneySettingsState { + const MoneySettingsState({required this.bitcoinAmountDisplayMode, required this.displayMode}); + + final BitcoinAmountDisplayMode bitcoinAmountDisplayMode; + final BalanceDisplayMode displayMode; + + bool useBaseUnit(Currency currency) => + ([CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency) && + bitcoinAmountDisplayMode == BitcoinAmountDisplayMode.satoshi) || + (CryptoCurrency.btcln == currency && + bitcoinAmountDisplayMode == BitcoinAmountDisplayMode.satoshiForLightning); + + bool get isHidden => displayMode == BalanceDisplayMode.hiddenBalance; + + String getSymbol(Currency currency, {bool? overrideSettings}) { + if (overrideSettings == null) { + return useBaseUnit(currency) ? "sats" : currency.symbol.safeSubString(0, 8); + } + + if (overrideSettings == true && [CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency)) { + return "sats"; + } + return currency.symbol.safeSubString(0, 8); + } + + + MoneySettingsState copyWith({ + BitcoinAmountDisplayMode? bitcoinAmountDisplayMode, + BalanceDisplayMode? displayMode, + }) => + MoneySettingsState( + bitcoinAmountDisplayMode: bitcoinAmountDisplayMode ?? this.bitcoinAmountDisplayMode, + displayMode: displayMode ?? this.displayMode, + ); +} diff --git a/lib/new-ui/widgets/money/money_settings_provider.dart b/lib/new-ui/widgets/money/money_settings_provider.dart new file mode 100644 index 000000000..17831a28a --- /dev/null +++ b/lib/new-ui/widgets/money/money_settings_provider.dart @@ -0,0 +1,15 @@ +import "package:cake_wallet/new-ui/widgets/money/money_settings_cubit.dart"; +import "package:cake_wallet/store/settings_store.dart"; +import "package:flutter/material.dart"; +import "package:flutter_bloc/flutter_bloc.dart"; + +class MoneySettingsProvider extends StatelessWidget { + const MoneySettingsProvider({required this.settingsStore, this.child, super.key}); + + final Widget? child; + final SettingsStore settingsStore; + + @override + Widget build(BuildContext context) => + BlocProvider(create: (_) => MoneySettingsCubit(settingsStore), child: child); +} diff --git a/lib/new-ui/widgets/money/money_text.dart b/lib/new-ui/widgets/money/money_text.dart new file mode 100644 index 000000000..c96de9498 --- /dev/null +++ b/lib/new-ui/widgets/money/money_text.dart @@ -0,0 +1,256 @@ +import "package:cake_wallet/generated/i18n.dart"; +import "package:cake_wallet/new-ui/widgets/money/money_settings_cubit.dart"; +import "package:cw_core/amount/money.dart"; +import "package:flutter/widgets.dart"; +import "package:flutter_bloc/flutter_bloc.dart"; + +/// The [MoneyText] widget displays [Money] as a formated string of text with single style. +class MoneyText extends StatelessWidget { + const MoneyText( + this.amount, { + super.key, + this.style, + this.strutStyle, + this.textAlign, + this.textDirection, + this.locale, + this.softWrap, + this.overflow, + this.textScaler, + this.maxLines, + this.semanticsLabel, + this.semanticsIdentifier, + this.textWidthBasis, + this.textHeightBehavior, + this.selectionColor, + this.isHiddenAmount, + this.useBaseUnit, + this.fractionalDigits = 8, + this.showSymbol = true, + this.withSymbolPrefix = false, + this.trimZeros = true, + }); + + /// The [MoneyText.optional] returns a widget displaying [Money] as a + /// formated string or an [SizedBox.shrink]. + static Widget optional( + Money? amount, { + Key? key, + TextStyle? style, + StrutStyle? strutStyle, + TextAlign? textAlign, + TextDirection? textDirection, + Locale? locale, + bool? softWrap, + TextOverflow? overflow, + TextScaler? textScaler, + int? maxLines, + String? semanticsLabel, + String? semanticsIdentifier, + TextWidthBasis? textWidthBasis, + TextHeightBehavior? textHeightBehavior, + Color? selectionColor, + bool? isHiddenAmount, + bool? useBaseUnit, + int fractionalDigits = 8, + bool showSymbol = true, + bool withSymbolPrefix = false, + bool trimZeros = true, + }) => + amount != null + ? MoneyText( + amount, + key: key, + style: style, + strutStyle: strutStyle, + textAlign: textAlign, + textDirection: textDirection, + locale: locale, + softWrap: softWrap, + overflow: overflow, + textScaler: textScaler, + maxLines: maxLines, + semanticsLabel: semanticsLabel, + semanticsIdentifier: semanticsIdentifier, + textWidthBasis: textWidthBasis, + textHeightBehavior: textHeightBehavior, + selectionColor: selectionColor, + isHiddenAmount: isHiddenAmount, + useBaseUnit: useBaseUnit, + fractionalDigits: fractionalDigits, + showSymbol: showSymbol, + withSymbolPrefix: withSymbolPrefix, + trimZeros: trimZeros, + ) + : const SizedBox.shrink(); + + /// The amount to display. + final Money amount; + + /// If non-null, the style to use for this text. + /// + /// If the style's "inherit" property is true, the style will be merged with + /// the closest enclosing [DefaultTextStyle]. Otherwise, the style will + /// replace the closest enclosing [DefaultTextStyle]. + /// + /// The user or platform may override this [style]'s [TextStyle.fontWeight], + /// [TextStyle.height], [TextStyle.letterSpacing], and [TextStyle.wordSpacing] + /// via a [MediaQuery] ancestor's [MediaQueryData.boldText], + /// [MediaQueryData.lineHeightScaleFactorOverride], + /// [MediaQueryData.letterSpacingOverride], and [MediaQueryData.wordSpacingOverride] + /// regardless of its [TextStyle.inherit] value. + final TextStyle? style; + + /// The user or platform may override this [strutStyle]'s [StrutStyle.height] + /// via a [MediaQuery] ancestor's [MediaQueryData.lineHeightScaleFactorOverride]. + final StrutStyle? strutStyle; + + /// How the text should be aligned horizontally. + final TextAlign? textAlign; + + /// The directionality of the text. + /// + /// This decides how [textAlign] values like [TextAlign.start] and + /// [TextAlign.end] are interpreted. + /// + /// This is also used to disambiguate how to render bidirectional text. For + /// example, if the [data] is an English phrase followed by a Hebrew phrase, + /// in a [TextDirection.ltr] context the English phrase will be on the left + /// and the Hebrew phrase to its right, while in a [TextDirection.rtl] + /// context, the English phrase will be on the right and the Hebrew phrase on + /// its left. + /// + /// Defaults to the ambient [Directionality], if any. + final TextDirection? textDirection; + + /// Used to select a font when the same Unicode character can + /// be rendered differently, depending on the locale. + /// + /// It's rarely necessary to set this property. By default its value + /// is inherited from the enclosing app with `Localizations.localeOf(context)`. + /// + /// See [RenderParagraph.locale] for more information. + final Locale? locale; + + /// Whether the text should break at soft line breaks. + /// + /// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space. + final bool? softWrap; + + /// How visual overflow should be handled. + /// + /// If this is null [TextStyle.overflow] will be used, otherwise the value + /// from the nearest [DefaultTextStyle] ancestor will be used. + final TextOverflow? overflow; + + final TextScaler? textScaler; + + /// An optional maximum number of lines for the text to span, wrapping if necessary. + /// If the text exceeds the given number of lines, it will be truncated according + /// to [overflow]. + /// + /// If this is 1, text will not wrap. Otherwise, text will be wrapped at the + /// edge of the box. + /// + /// If this is null, but there is an ambient [DefaultTextStyle] that specifies + /// an explicit number for its [DefaultTextStyle.maxLines], then the + /// [DefaultTextStyle] value will take precedence. You can use a [RichText] + /// widget directly to entirely override the [DefaultTextStyle]. + final int? maxLines; + + /// An alternative semantics label for this text. + /// + /// If present, the semantics of this widget will contain this value instead + /// of the actual text. This will overwrite any of the semantics labels applied + /// directly to the [TextSpan]s. + /// + /// This is useful for replacing abbreviations or shorthands with the full + /// text value: + /// + /// ```dart + /// const Text(r'$$', semanticsLabel: 'Double dollars') + /// ``` + final String? semanticsLabel; + + /// A unique identifier for the semantics node for this widget. + /// + /// This is useful for cases where the text widget needs to have a uniquely + /// identifiable ID that is recognized through the automation tools without + /// having a dependency on the actual content of the text that can possibly be + /// dynamic in nature. + final String? semanticsIdentifier; + + final TextWidthBasis? textWidthBasis; + + final TextHeightBehavior? textHeightBehavior; + + /// The color to use when painting the selection. + /// + /// This is ignored if [SelectionContainer.maybeOf] returns null + /// in the [BuildContext] of the [Text] widget. + /// + /// If null, the ambient [DefaultSelectionStyle] is used (if any); failing + /// that, the selection color defaults to [DefaultSelectionStyle.defaultColor] + /// (semi-transparent grey). + final Color? selectionColor; + + /// Show the amount in the base unit format of [Money.currency] + /// + /// If null, the displayAmountsInSatoshi setting is used. + final bool? useBaseUnit; + + /// A limit for the display of the fractional digits of the amount + final int fractionalDigits; + + /// Hide the amount + /// + /// If null, the value from the balanceDisplayMode setting is used. + final bool? isHiddenAmount; + + /// Show the currency symbol + final bool showSymbol; + + /// Prefix the amount with the currency symbol if [showSymbol] is true + final bool withSymbolPrefix; + + /// Trim the zeros at the end of an amount + final bool trimZeros; + + @override + Widget build(BuildContext context) => BlocBuilder( + builder: (context, state) => Text( + isHiddenAmount ?? state.isHidden + ? "●●●●●●" + : showSymbol + ? amount.toLocalStringWithSymbol( + fractionalDigits: fractionalDigits, + trimZeros: trimZeros, + useBaseUnit: useBaseUnit ?? state.useBaseUnit(amount.currency), + withSymbolPrefix: withSymbolPrefix, + locale: (locale ?? Localizations.localeOf(context)).toString(), + ) + : amount.toLocalStringWithPrecision( + fractionalDigits: fractionalDigits, + trimZeros: trimZeros, + useBaseUnit: useBaseUnit ?? state.useBaseUnit(amount.currency), + locale: (locale ?? Localizations.localeOf(context)).toString(), + ), + style: style, + strutStyle: strutStyle, + textAlign: textAlign, + textDirection: textDirection, + locale: locale, + softWrap: softWrap, + overflow: overflow, + textScaler: textScaler, + maxLines: maxLines, + semanticsLabel: isHiddenAmount ?? state.isHidden + ? (semanticsLabel ?? S.of(context).amount_hidden) + : semanticsLabel, + semanticsIdentifier: semanticsIdentifier, + textWidthBasis: textWidthBasis, + textHeightBehavior: textHeightBehavior, + selectionColor: selectionColor, + ), + ); +} diff --git a/lib/new-ui/widgets/new_future_primary_button.dart b/lib/new-ui/widgets/new_future_primary_button.dart new file mode 100644 index 000000000..2440f1938 --- /dev/null +++ b/lib/new-ui/widgets/new_future_primary_button.dart @@ -0,0 +1,57 @@ +import "package:cake_wallet/new-ui/widgets/new_primary_button.dart"; +import "package:flutter/material.dart"; +import "package:flutter_svg/flutter_svg.dart"; + +class NewFuturePrimaryButton extends StatefulWidget { + const NewFuturePrimaryButton({ + required this.onPressed, + required this.text, + required this.color, + required this.textColor, + this.borderColor = Colors.transparent, + this.disabled = false, + this.image, + super.key, + }); + + final Future Function() onPressed; + final bool disabled; + final SvgPicture? image; + final Color color; + final Color textColor; + final Color borderColor; + final String text; + + @override + State createState() => _NewFuturePrimaryButtonState(); +} + +class _NewFuturePrimaryButtonState extends State { + bool isLoading = false; + + Future _onPressed() async { + if (isLoading) { + return; + } + setState(() => isLoading = true); + try { + await widget.onPressed.call(); + } finally { + if (mounted) { + setState(() => isLoading = false); + } + } + } + + @override + Widget build(BuildContext context) => NewPrimaryButton( + onPressed: _onPressed, + image: widget.image, + text: widget.text, + color: widget.color, + textColor: widget.textColor, + isLoading: isLoading, + borderColor: widget.borderColor, + disabled: widget.disabled, + ); +} diff --git a/lib/new-ui/widgets/picker.dart b/lib/new-ui/widgets/picker.dart index d654e4e39..3520dabc6 100644 --- a/lib/new-ui/widgets/picker.dart +++ b/lib/new-ui/widgets/picker.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; @@ -77,6 +78,7 @@ class _NewPickerState extends State> { ModalTopBar( title: widget.title, leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: Navigator.of(context).maybePop, ), Padding( @@ -387,6 +389,7 @@ class _PickerSliderPageState extends State> { ModalTopBar( title: widget.title, leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: Navigator.of(context).pop, ), Expanded( diff --git a/lib/new-ui/widgets/receive_page/payjoin_copy_modal.dart b/lib/new-ui/widgets/receive_page/payjoin_copy_modal.dart index 06ec6b4ef..435417330 100644 --- a/lib/new-ui/widgets/receive_page/payjoin_copy_modal.dart +++ b/lib/new-ui/widgets/receive_page/payjoin_copy_modal.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart'; import 'package:cw_core/payment_uris.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; import 'package:flutter/services.dart'; class PayjoinCopyModal extends StatelessWidget { @@ -25,6 +26,7 @@ class PayjoinCopyModal extends StatelessWidget { ModalTopBar( title: S.of(context).select_address_to_copy, leadingIcon: Icon(Icons.close), + leadingSemanticLabel: S.of(context).close, onLeadingPressed: Navigator.of(context).pop), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), @@ -32,7 +34,7 @@ class PayjoinCopyModal extends StatelessWidget { "": [ ListItemRegularRow( keyValue: "btc", - label: "Standard", + label: S.of(context).standard, iconPath: "assets/new-ui/pjmodal_btc.svg", onTap: () { Clipboard.setData( @@ -41,6 +43,7 @@ class PayjoinCopyModal extends StatelessWidget { ? BitcoinURI(amount: uri.amount, address: uri.address).toString() : uri.address), ); + _announceCopied(context); Navigator.of(context).pop(); }), ListItemRegularRow( @@ -51,6 +54,7 @@ class PayjoinCopyModal extends StatelessWidget { Clipboard.setData( ClipboardData(text: uri.toString()), ); + _announceCopied(context); Navigator.of(context).pop(); }) ] @@ -62,4 +66,15 @@ class PayjoinCopyModal extends StatelessWidget { ), ); } + + // Copying pops this sheet right away, so no widget survives to carry a + // semantics state change: this path still needs a direct announcement. It is + // skipped on platforms that deprecate announcements (android, where they + // clear TalkBack's speech queue and the system shows its own clipboard + // confirmation anyway). + void _announceCopied(BuildContext context) { + if (!MediaQuery.supportsAnnounceOf(context)) return; + SemanticsService.sendAnnouncement( + View.of(context), S.of(context).copied, Directionality.of(context)); + } } diff --git a/lib/new-ui/widgets/receive_page/receive_address_type.dart b/lib/new-ui/widgets/receive_page/receive_address_type.dart index 46a5c6cea..7331885b8 100644 --- a/lib/new-ui/widgets/receive_page/receive_address_type.dart +++ b/lib/new-ui/widgets/receive_page/receive_address_type.dart @@ -42,52 +42,64 @@ class ReceiveAddressTypeDisplay extends StatelessWidget { iconPath = "assets/new-ui/address-type-picker-icons/litecoin.svg"; } - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => _showPicker(context), - child: Row( - key: ValueKey("$text$largeQrMode"), - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - spacing: 12.0, - children: [ - if (iconPath != null) - CakeImageWidget( - imageUrl: iconPath, - width: 24, - height: 24, - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.primary, - BlendMode.srcIn, - ), - ), - Text( - text, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.primary, - ), - ), - if (!largeQrMode) - 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: () => _showPicker(context), - icon: (Icon( + // The row and the chevron button trigger the same picker, so only the + // row is exposed and the chevron is treated as decoration. + return MergeSemantics( + child: Semantics( + button: true, + label: S.of(context).address_type, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => _showPicker(context), + child: Row( + key: ValueKey("$text$largeQrMode"), + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + spacing: 12.0, + children: [ + if (iconPath != null) + ExcludeSemantics( + child: CakeImageWidget( + imageUrl: iconPath, + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + ), + Text( + text, + style: TextStyle( + fontSize: 16, color: Theme.of(context).colorScheme.primary, - size: 20, - Icons.keyboard_arrow_down, - )), + ), ), - ), - ], + if (!largeQrMode) + ExcludeSemantics( + child: 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: () => _showPicker(context), + icon: (Icon( + color: Theme.of(context).colorScheme.primary, + size: 20, + Icons.keyboard_arrow_down, + )), + ), + ), + ), + ], + ), + ), ), ); }, diff --git a/lib/new-ui/widgets/receive_page/receive_address_type_selector.dart b/lib/new-ui/widgets/receive_page/receive_address_type_selector.dart index ab4850caa..24e50ef3c 100644 --- a/lib/new-ui/widgets/receive_page/receive_address_type_selector.dart +++ b/lib/new-ui/widgets/receive_page/receive_address_type_selector.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/src/widgets/section_divider.dart'; import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; @@ -34,6 +35,7 @@ class _ReceiveAddressTypeSelectorState extends State @override Widget build(BuildContext context) { + return Observer(builder: (final _) { final commonOptions = widget.receiveOptionViewModel.options .where((element) => element.isCommon || @@ -71,6 +73,7 @@ class _ReceiveAddressTypeSelectorState extends State ModalTopBar( title: S.of(context).address_type, leadingIcon: Icon(Icons.close), + leadingSemanticLabel: S.of(context).close, onLeadingPressed: Navigator.of(context).pop, onTrailingPressed: () {}), SizedBox(height: 24), @@ -131,38 +134,49 @@ class _ReceiveAddressTypeSelectorState extends State borderRadius: BorderRadius.vertical( top: Radius.circular(20), bottom: _otherOptionsExpanded ? Radius.zero : Radius.circular(20)), - child: InkWell( - highlightColor: Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - bottom: !_otherOptionsExpanded ? Radius.zero : Radius.circular(20)), - onTap: () { - setState(() { - _otherOptionsExpanded = !_otherOptionsExpanded; - }); - }, - child: Container( - height: 64.0, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 18.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - S.of(context).more_options, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.primary), + child: MergeSemantics( + child: Semantics( + button: true, + expanded: _otherOptionsExpanded, + child: InkWell( + highlightColor: + Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + bottom: !_otherOptionsExpanded + ? Radius.zero + : Radius.circular(20)), + onTap: () { + setState(() { + _otherOptionsExpanded = !_otherOptionsExpanded; + }); + }, + child: Container( + height: 64.0, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + S.of(context).more_options, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.primary), + ), + ExcludeSemantics( + child: AnimatedRotation( + duration: ReceiveAddressTypeSelector + .otherOptionsExpandDuration, + turns: _otherOptionsExpanded ? 0.0 : 0.5, + curve: Curves.easeOut, + child: CakeImageWidget( + imageUrl: "assets/new-ui/dropdown_arrow.svg")), + ) + ], ), - AnimatedRotation( - duration: - ReceiveAddressTypeSelector.otherOptionsExpandDuration, - turns: _otherOptionsExpanded ? 0.0 : 0.5, - curve: Curves.easeOut, - child: CakeImageWidget( - imageUrl: "assets/new-ui/dropdown_arrow.svg")) - ], + ), ), ), ), @@ -177,40 +191,45 @@ class _ReceiveAddressTypeSelectorState extends State duration: ReceiveAddressTypeSelector.otherOptionsExpandDuration, curve: Curves.easeOut, alignment: Alignment.topCenter, - child: Container( - height: _otherOptionsExpanded ? null : 0, - child: ListView.separated( - shrinkWrap: true, - padding: EdgeInsets.zero, - physics: NeverScrollableScrollPhysics(), - itemCount: otherOptions.length, - itemBuilder: (context, index) { - final opt = otherOptions[index]; + // Collapsed to zero height: keep the options out of + // the semantics tree so they cannot be focused. + child: ExcludeSemantics( + excluding: !_otherOptionsExpanded, + child: Container( + height: _otherOptionsExpanded ? null : 0, + child: ListView.separated( + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: NeverScrollableScrollPhysics(), + itemCount: otherOptions.length, + itemBuilder: (context, index) { + final opt = otherOptions[index]; - return ReceiveAddressTypeRow( - option: opt, - roundedTop: false, - roundedBottom: index == otherOptions.length - 1, - selected: - widget.receiveOptionViewModel.selectedReceiveOption == opt, - onItemTap: () { - Navigator.of(context).pop(opt); - }, - receiveOptionViewModel: widget.receiveOptionViewModel, - ); - }, - separatorBuilder: (context, index) { - if ((widget.receiveOptionViewModel.selectedReceiveOption == - otherOptions[index]) || - (index != otherOptions.length - 1 && - widget.receiveOptionViewModel.selectedReceiveOption == - otherOptions[index + 1])) return Container(); + return ReceiveAddressTypeRow( + option: opt, + roundedTop: false, + roundedBottom: index == otherOptions.length - 1, + selected: + widget.receiveOptionViewModel.selectedReceiveOption == opt, + onItemTap: () { + Navigator.of(context).pop(opt); + }, + receiveOptionViewModel: widget.receiveOptionViewModel, + ); + }, + separatorBuilder: (context, index) { + if ((widget.receiveOptionViewModel.selectedReceiveOption == + otherOptions[index]) || + (index != otherOptions.length - 1 && + widget.receiveOptionViewModel.selectedReceiveOption == + otherOptions[index + 1])) return Container(); - return Padding( - padding: const EdgeInsets.only(left: 44.0, right: 36.0), - child: HorizontalSectionDivider(), - ); - }, + return Padding( + padding: const EdgeInsets.only(left: 44.0, right: 36.0), + child: HorizontalSectionDivider(), + ); + }, + ), ), ), ), @@ -223,6 +242,7 @@ class _ReceiveAddressTypeSelectorState extends State ], )), ); + }); } } @@ -260,65 +280,75 @@ class ReceiveAddressTypeRow extends StatelessWidget { top: roundedTop ? Radius.circular(20) : Radius.zero, bottom: roundedBottom ? Radius.circular(20) : Radius.zero, ), - child: InkWell( - onTap: onItemTap, - child: Container( - height: rowHeight, - decoration: BoxDecoration( - color: selected - ? Theme.of(context).colorScheme.surfaceContainerHigh - : Colors.transparent, - borderRadius: BorderRadius.vertical( - top: roundedTop ? Radius.circular(20) : Radius.zero, - bottom: roundedBottom ? Radius.circular(20) : Radius.zero, - )), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.max, - children: [ - Row( + // The row itself carries the selection state; the checkbox next to it is + // only a visual echo of it and must not be announced separately. + child: MergeSemantics( + child: Semantics( + selected: selected, + inMutuallyExclusiveGroup: true, + child: InkWell( + onTap: onItemTap, + child: Container( + height: rowHeight, + decoration: BoxDecoration( + color: selected + ? Theme.of(context).colorScheme.surfaceContainerHigh + : Colors.transparent, + borderRadius: BorderRadius.vertical( + top: roundedTop ? Radius.circular(20) : Radius.zero, + bottom: roundedBottom ? Radius.circular(20) : Radius.zero, + )), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, children: [ - if (iconPath != null) - CakeImageWidget( - imageUrl: iconPath!, - width: iconSize, - height: iconSize, - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), - ) - else - Container(width: iconSize, height: iconSize), - Padding( - padding: const EdgeInsets.all(10.0), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - option.value, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurface), + Row( + children: [ + if (iconPath != null) + ExcludeSemantics( + child: CakeImageWidget( + imageUrl: iconPath!, + width: iconSize, + height: iconSize, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), + ), + ) + else + Container(width: iconSize, height: iconSize), + Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + option.value, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface), + ), + if (option.description != null) + Text( + option.description!, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant), + ) + ], ), - if (option.description != null) - Text( - option.description!, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurfaceVariant), - ) - ], - ), - ) + ) + ], + ), + ExcludeSemantics(child: RoundedCheckbox(value: selected)) ], ), - RoundedCheckbox(value: selected) - ], + ), ), ), ), diff --git a/lib/new-ui/widgets/receive_page/receive_amount_display.dart b/lib/new-ui/widgets/receive_page/receive_amount_display.dart index 882bea55f..a69e03aaa 100644 --- a/lib/new-ui/widgets/receive_page/receive_amount_display.dart +++ b/lib/new-ui/widgets/receive_page/receive_amount_display.dart @@ -22,50 +22,54 @@ class ReceiveAmountDisplay extends StatelessWidget { curve: Curves.easeOutCubic, heightFactor: largeQrMode || walletAddressListViewModel.displayAmount.isEmpty ? 0 : 1, alignment: Alignment.topCenter, - child: Container( - width: double.infinity, - child: Row( - spacing: 4, - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Theme.of(context).colorScheme.surfaceContainer), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - spacing: 8.0, - children: [ - Text( - walletAddressListViewModel.displayAmount, - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontSize: 16, - fontWeight: FontWeight.w500), - ), - Text(walletAddressListViewModel.cryptoCurrencySymbol, + // Amount, currency symbol and fiat equivalent describe one value, so + // they are announced as a single node. + child: MergeSemantics( + child: Container( + width: double.infinity, + child: Row( + spacing: 4, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Theme.of(context).colorScheme.surfaceContainer), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + spacing: 8.0, + children: [ + Text( + walletAddressListViewModel.displayAmount, style: TextStyle( color: Theme.of(context).colorScheme.primary, fontSize: 16, - fontWeight: FontWeight.w500)) - ], + fontWeight: FontWeight.w500), + ), + Text(walletAddressListViewModel.cryptoCurrencySymbol, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontSize: 16, + fontWeight: FontWeight.w500)) + ], + ), ), ), - ), - if (!walletAddressListViewModel.isFiatDisabled) - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - _getFiatAmount(), - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: 16, - fontWeight: FontWeight.w500), - ), - ) - ], + if (!walletAddressListViewModel.isFiatDisabled) + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + _getFiatAmount(), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 16, + fontWeight: FontWeight.w500), + ), + ) + ], + ), ), ), ), diff --git a/lib/new-ui/widgets/receive_page/receive_amount_modal.dart b/lib/new-ui/widgets/receive_page/receive_amount_modal.dart index 882887020..4166750ba 100644 --- a/lib/new-ui/widgets/receive_page/receive_amount_modal.dart +++ b/lib/new-ui/widgets/receive_page/receive_amount_modal.dart @@ -56,6 +56,7 @@ class _ReceiveAmountModalState extends State { onLeadingPressed: Navigator.of(context).pop, onTrailingPressed: () {}, leadingIcon: Icon(Icons.close), + leadingSemanticLabel: S.of(context).close, ), Padding( padding: const EdgeInsets.all(18.0), @@ -65,54 +66,68 @@ class _ReceiveAmountModalState extends State { spacing: 12, children: [ if (widget.walletAddressListViewModel.hasTokensList) ...[ - Text("Token"), - GestureDetector( - onTap: () { - _presentTokenCurrencyPicker(context); - }, - child: Observer( - builder: (_) => Container( - height: 60, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHigh, - borderRadius: BorderRadius.circular(16), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - spacing: 8, - children: [ - TokenImageWidget( - imageUrl: widget.walletAddressListViewModel.tokenCurrency - ?.iconPath ?? - widget.walletAddressListViewModel.currencies.first - .iconPath ?? - "", - size: 32, - ), - Text((widget.walletAddressListViewModel.tokenCurrency ?? - widget.walletAddressListViewModel.currencies.first - as CryptoCurrency) - .title - .toUpperCase()) - ], - ), - RotatedBox( - quarterTurns: 2, - child: CakeImageWidget( - imageUrl: "assets/new-ui/dropdown_arrow.svg")) - ], + // The caption is reused as the picker's semantics label, + // so it must not be announced as a separate node. + ExcludeSemantics(child: Text(S.of(context).token)), + MergeSemantics( + child: Semantics( + button: true, + label: S.of(context).select_token, + child: GestureDetector( + onTap: () { + _presentTokenCurrencyPicker(context); + }, + child: Observer( + builder: (_) => Container( + height: 60, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + spacing: 8, + children: [ + ExcludeSemantics( + child: TokenImageWidget( + imageUrl: widget.walletAddressListViewModel + .tokenCurrency?.iconPath ?? + widget.walletAddressListViewModel.currencies.first + .iconPath ?? + "", + size: 32, + ), + ), + Text((widget.walletAddressListViewModel.tokenCurrency ?? + widget.walletAddressListViewModel.currencies.first + as CryptoCurrency) + .title + .toUpperCase()) + ], + ), + ExcludeSemantics( + child: RotatedBox( + quarterTurns: 2, + child: CakeImageWidget( + imageUrl: "assets/new-ui/dropdown_arrow.svg")), + ) + ], + ), ), ), ), - )), + ), + ), + ), ], SizedBox(), - Text(S.of(context).amount), + // The caption is reused as the field's semantics label. + ExcludeSemantics(child: Text(S.of(context).amount)), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -132,74 +147,91 @@ class _ReceiveAmountModalState extends State { width: 2, ), ), - child: TextField( - textAlign: TextAlign.left, - textAlignVertical: TextAlignVertical.center, - controller: _amountController, - keyboardType: TextInputType.numberWithOptions( - signed: false, - decimal: - widget.walletAddressListViewModel.selectedCurrencyDecimals > 0, - ), - inputFormatters: [ - DecimalInputFormatter( - maxDecimals: - widget.walletAddressListViewModel.selectedCurrencyDecimals, - ), - ], - decoration: InputDecoration( - hint: Text( - widget.walletAddressListViewModel.useSatoshi ? "0" : "0.00000000", + child: MergeSemantics( + child: Semantics( + label: S.of(context).amount, + child: TextField( textAlign: TextAlign.left, - style: TextStyle( - fontSize: 16, - color: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.5), + textAlignVertical: TextAlignVertical.center, + controller: _amountController, + keyboardType: TextInputType.numberWithOptions( + signed: false, + decimal: + widget.walletAddressListViewModel.selectedCurrencyDecimals > + 0, ), + inputFormatters: [ + DecimalInputFormatter( + maxDecimals: + widget.walletAddressListViewModel.selectedCurrencyDecimals, + ), + ], + decoration: InputDecoration( + hint: Text( + widget.walletAddressListViewModel.useSatoshi + ? "0" + : "0.00000000", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 16, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.5), + ), + ), + border: InputBorder.none, + filled: true, + fillColor: Colors.transparent, + ), + style: + TextStyle(color: Theme.of(context).colorScheme.onSurface), ), - border: InputBorder.none, - filled: true, - fillColor: Colors.transparent, ), - style: TextStyle(color: Theme.of(context).colorScheme.onSurface), ), ), ), Expanded( flex: 25, - child: GestureDetector( - onTap: () { - _presentFiatCurrencyPicker(context); - }, - child: Container( - height: 60, - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(0), - bottomLeft: Radius.circular(0), - topRight: Radius.circular(18), - bottomRight: Radius.circular(18), - ), - color: Theme.of(context).colorScheme.surfaceContainerHigh, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 4.0, - children: [ - Observer( - builder: (_) => Text( - widget.walletAddressListViewModel.selectedCurrencySymbol, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - ), + child: MergeSemantics( + child: Semantics( + button: true, + label: S.of(context).select_fiat_currency_title, + child: GestureDetector( + onTap: () { + _presentFiatCurrencyPicker(context); + }, + child: Container( + height: 60, + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(0), + bottomLeft: Radius.circular(0), + topRight: Radius.circular(18), + bottomRight: Radius.circular(18), ), + color: Theme.of(context).colorScheme.surfaceContainerHigh, ), - Icon( - Icons.keyboard_arrow_down, - color: Theme.of(context).colorScheme.primary, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 4.0, + children: [ + Observer( + builder: (_) => Text( + widget.walletAddressListViewModel.selectedCurrencySymbol, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ExcludeSemantics( + child: 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 index 3d7cb1f9f..e3cca096e 100644 --- a/lib/new-ui/widgets/receive_page/receive_bottom_buttons.dart +++ b/lib/new-ui/widgets/receive_page/receive_bottom_buttons.dart @@ -1,22 +1,10 @@ -import 'dart:io'; - -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; -import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:cake_wallet/new-ui/widgets/copy_wrapper.dart'; +import "package:cake_wallet/generated/i18n.dart"; +import "package:cake_wallet/new-ui/widgets/copy_wrapper.dart"; +import "package:cake_wallet/new-ui/widgets/modern_button.dart"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; class ReceiveBottomButtons extends StatefulWidget { - final bool largeQrMode; - final ClipboardData? copyData; - final VoidCallback onCopyButtonPressed; - final VoidCallback onAmountButtonPressed; - final VoidCallback onLabelButtonPressed; - final VoidCallback onAccountsButtonPressed; - final bool showLabelButton; - final bool showAccountsButton; - const ReceiveBottomButtons({ required this.largeQrMode, required this.onCopyButtonPressed, @@ -29,6 +17,15 @@ class ReceiveBottomButtons extends StatefulWidget { super.key, }); + final bool largeQrMode; + final ClipboardData? copyData; + final VoidCallback onCopyButtonPressed; + final VoidCallback onAmountButtonPressed; + final VoidCallback onLabelButtonPressed; + final VoidCallback onAccountsButtonPressed; + final bool showLabelButton; + final bool showAccountsButton; + @override State createState() => _ReceiveBottomButtonsState(); } @@ -49,60 +46,57 @@ class _ReceiveBottomButtonsState extends State { opacity: targetOpacity, curve: Curves.easeOut, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32.0), + padding: const EdgeInsets.symmetric(horizontal: 32), child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, spacing: 16, children: [ - GestureDetector( - onTap: widget.copyData == null ? widget.onCopyButtonPressed : null, - child: IgnorePointer( - ignoring: widget.copyData == null, - child: CopyWrapper( - data: widget.copyData, - builder: (context, copied) => AnimatedSwitcher( - duration: Duration(milliseconds: 200), - child: IgnorePointer( - child: ModernButton.svg( - key: ValueKey(copied), - size: 60, - iconSize: 32, - svgPath: "assets/new-ui/copy.svg", - onPressed: () {}, - label: copied ? S.of(context).copied : S.of(context).copy, - iconColor: copied - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.surfaceContainer, - backgroundColor: copied - ? Theme.of(context).colorScheme.surfaceContainerHighest - : Theme.of(context).colorScheme.primary, - ), - ), - ), + // The button itself is the only control: it copies when there + // is data to copy, otherwise it opens the payjoin copy modal. + CopyWrapper( + data: widget.copyData, + controlBuilder: (context, copied, onCopy) => AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: ModernButton.svg( + key: ValueKey(copied), + size: 60, + iconSize: 32, + svgPath: "assets/new-ui/copy.svg", + onPressed: onCopy ?? widget.onCopyButtonPressed, + label: copied ? S.of(context).copied : S.of(context).copy, + iconColor: copied + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.surfaceContainer, + backgroundColor: copied + ? Theme.of(context).colorScheme.surfaceContainerHighest + : Theme.of(context).colorScheme.primary, ), ), ), ModernButton.svg( - size: 60, - iconSize: 32, - svgPath: "assets/new-ui/set-amount.svg", - onPressed: widget.onAmountButtonPressed, - label: S.of(context).set_amount), + size: 60, + iconSize: 32, + svgPath: "assets/new-ui/set-amount.svg", + onPressed: widget.onAmountButtonPressed, + label: S.of(context).set_amount, + ), if (widget.showLabelButton) ModernButton.svg( - size: 60, - iconSize: 32, - svgPath: "assets/new-ui/add-label.svg", - onPressed: widget.onLabelButtonPressed, - label: S.of(context).label), + size: 60, + iconSize: 32, + svgPath: "assets/new-ui/add-label.svg", + onPressed: widget.onLabelButtonPressed, + label: S.of(context).label, + ), if (widget.showAccountsButton) ModernButton.svg( - size: 60, - iconSize: 32, - svgPath: "assets/new-ui/addr-book.svg", - onPressed: widget.onAccountsButtonPressed, - label: S.of(context).addresses), + size: 60, + iconSize: 32, + svgPath: "assets/new-ui/addr-book.svg", + onPressed: widget.onAccountsButtonPressed, + label: S.of(context).addresses, + ), ], ), ), diff --git a/lib/new-ui/widgets/receive_page/receive_info_box.dart b/lib/new-ui/widgets/receive_page/receive_info_box.dart index d2598b241..db34cef16 100644 --- a/lib/new-ui/widgets/receive_page/receive_info_box.dart +++ b/lib/new-ui/widgets/receive_page/receive_info_box.dart @@ -71,12 +71,14 @@ class ReceiveInfoBox extends StatelessWidget { spacing: 10, children: [ if (iconPath.isNotEmpty) - CakeImageWidget( - imageUrl: iconPath, - width: 16, - height: 16, - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), + ExcludeSemantics( + child: CakeImageWidget( + imageUrl: iconPath, + width: 16, + height: 16, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), + ), ), Flexible( child: Column( @@ -96,15 +98,20 @@ class ReceiveInfoBox extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ if (bottomWidget != null) bottomWidget!, - GestureDetector( - onTap: onDismissed, - child: Text( - S.of(context).dismiss, - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontSize: 12, - fontWeight: FontWeight.w300), - )), + MergeSemantics( + child: Semantics( + button: true, + child: GestureDetector( + onTap: onDismissed, + child: Text( + S.of(context).dismiss, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontSize: 12, + fontWeight: FontWeight.w300), + )), + ), + ), ], ) ]), @@ -136,65 +143,69 @@ class InfoboxCurrencyRow extends StatelessWidget { final double stackWidth = iconSize + (overlap * (currenciesLimited.length)); - return Row( - spacing: 8, - children: [ - CakeImageWidget( - imageUrl: chainIconPath, - width: 20, - height: 20, - colorFilter: - ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), - ), - Container( - height: 28, - width: 1, - color: Theme.of(context).colorScheme.surfaceContainerHigh, - ), - SizedBox( - height: iconSize + iconBorder * 2, - width: stackWidth, - child: Stack( - children: [ - Positioned( - top: iconBorder, - left: overlap * currenciesLimited.length, - child: Container( - width: 24, - height: 24, - decoration: BoxDecoration( - color: Colors.white.withAlpha(25), - borderRadius: BorderRadius.circular(9999999)), - child: Icon( - Icons.add, - size: 16, - color: Colors.white.withAlpha(128), + // Purely illustrative: the meaning ("receive any token on ") is + // carried by the infobox message text next to it. + return ExcludeSemantics( + child: Row( + spacing: 8, + children: [ + CakeImageWidget( + imageUrl: chainIconPath, + width: 20, + height: 20, + colorFilter: + ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), + ), + Container( + height: 28, + width: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + SizedBox( + height: iconSize + iconBorder * 2, + width: stackWidth, + child: Stack( + children: [ + Positioned( + top: iconBorder, + left: overlap * currenciesLimited.length, + child: Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: Colors.white.withAlpha(25), + borderRadius: BorderRadius.circular(9999999)), + child: Icon( + Icons.add, + size: 16, + color: Colors.white.withAlpha(128), + ), ), ), - ), - ...currenciesLimited - .asMap() - .entries - .map((entry) => Positioned( - left: 16.0 * entry.key, - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.surfaceContainer, - width: iconBorder), - borderRadius: BorderRadius.circular(9999999)), - child: TokenImageWidget( - imageUrl: entry.value.iconPath ?? '', - size: 24, + ...currenciesLimited + .asMap() + .entries + .map((entry) => Positioned( + left: 16.0 * entry.key, + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.surfaceContainer, + width: iconBorder), + borderRadius: BorderRadius.circular(9999999)), + child: TokenImageWidget( + imageUrl: entry.value.iconPath ?? '', + size: 24, + ), ), - ), - )) - .toList() - .reversed, - ], - ), - ) - ], + )) + .toList() + .reversed, + ], + ), + ) + ], + ), ); } } diff --git a/lib/new-ui/widgets/receive_page/receive_label_modal.dart b/lib/new-ui/widgets/receive_page/receive_label_modal.dart index 9dc05c19d..bf2e5e528 100644 --- a/lib/new-ui/widgets/receive_page/receive_label_modal.dart +++ b/lib/new-ui/widgets/receive_page/receive_label_modal.dart @@ -66,6 +66,7 @@ class _ReceiveLabelModalState extends State { ModalTopBar( title: S.of(context).label_address, leadingIcon: Icon(Icons.close), + leadingSemanticLabel: S.of(context).close, onLeadingPressed: Navigator.of(context).pop, onTrailingPressed: () {}), Padding( @@ -91,27 +92,32 @@ class _ReceiveLabelModalState extends State { return SizedBox(width: 8); }, itemBuilder: (context, index) { - return Container( - height: 36, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHigh, - borderRadius: BorderRadius.circular(999)), - child: Material( - color: Colors.transparent, - borderRadius: BorderRadius.circular(999), - child: InkWell( - borderRadius: BorderRadius.circular(999), - onTap: () { - _controller.text = defaultLabels[index]; - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10.0), - child: Center( - child: Text( - defaultLabels[index], - style: TextStyle( - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.primary), + return MergeSemantics( + child: Semantics( + button: true, + child: Container( + height: 36, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(999)), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(999), + child: InkWell( + borderRadius: BorderRadius.circular(999), + onTap: () { + _controller.text = defaultLabels[index]; + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Center( + child: Text( + defaultLabels[index], + style: TextStyle( + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.primary), + ), + ), ), ), ), diff --git a/lib/new-ui/widgets/receive_page/receive_label_widget.dart b/lib/new-ui/widgets/receive_page/receive_label_widget.dart index f34eaa2a2..f0657d5fa 100644 --- a/lib/new-ui/widgets/receive_page/receive_label_widget.dart +++ b/lib/new-ui/widgets/receive_page/receive_label_widget.dart @@ -22,14 +22,16 @@ class ReceiveLabelWidget extends StatelessWidget { spacing: 8, mainAxisSize: MainAxisSize.min, children: [ - ClipRect( - child: CakeImageWidget( - imageUrl: "assets/new-ui/label.svg", - width: 24, - height: 24, - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), - )), + ExcludeSemantics( + child: ClipRect( + child: CakeImageWidget( + imageUrl: "assets/new-ui/label.svg", + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), + )), + ), Text(name, style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant)), diff --git a/lib/new-ui/widgets/receive_page/receive_top_bar.dart b/lib/new-ui/widgets/receive_page/receive_top_bar.dart index 102df8d5c..0f909e0b9 100644 --- a/lib/new-ui/widgets/receive_page/receive_top_bar.dart +++ b/lib/new-ui/widgets/receive_page/receive_top_bar.dart @@ -15,7 +15,9 @@ class ModalTopBar extends StatelessWidget { this.padding, this.bottomText, this.leadingWidget, - this.trailingWidget}) { + this.trailingWidget, + this.leadingSemanticLabel, + this.trailingSemanticLabel}) { if (leadingIcon != null && leadingWidget != null) { throw Exception("Cannot have both leadingIcon and leadingWidget"); } @@ -35,6 +37,17 @@ class ModalTopBar extends StatelessWidget { final Widget? leadingWidget; final Widget? trailingWidget; + /// Accessible name for the leading chrome button. Required (and must be + /// non-empty) whenever a [leadingIcon] is supplied, because the icon alone + /// does not say whether it closes the modal or goes back. Must be localized + /// by the caller. + final String? leadingSemanticLabel; + + /// Accessible name for the trailing chrome button. Required (and must be + /// non-empty) whenever a [trailingIcon] is supplied. Must be localized by + /// the caller. + final String? trailingSemanticLabel; + static const buttonSize = 36.0; @override @@ -56,21 +69,27 @@ class ModalTopBar extends StatelessWidget { children: [ AnimatedSwitcher( duration: const Duration(milliseconds: 200), - child: Text( - title, - key: ValueKey(title), + child: Semantics( + key: ValueKey(title), + header: title.isNotEmpty, + // Android reads the heading from headingLevel since the + // Flutter 3.41 engine; header: alone only covers iOS. + headingLevel: title.isNotEmpty ? 1 : null, + child: Text( + title, style: TextStyle( fontSize: hasBottomText ? 16 : 18, fontWeight: FontWeight.w600), ), + ), ), if (hasBottomText) Text( bottomText!, style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), - ) - ], - ), + ), + ], + ), if (subtitle != null && subtitle!.isNotEmpty) Text(subtitle!, style: Theme.of(context) @@ -91,6 +110,7 @@ class ModalTopBar extends StatelessWidget { size: buttonSize, onPressed: onLeadingPressed, icon: leadingIcon!, + semanticLabel: leadingSemanticLabel, iconColor: Theme.of(context).colorScheme.onSurfaceVariant) : leadingWidget! else @@ -104,6 +124,7 @@ class ModalTopBar extends StatelessWidget { size: buttonSize, onPressed: onTrailingPressed, icon: trailingIcon!, + semanticLabel: trailingSemanticLabel, ) : trailingWidget!, ) diff --git a/lib/new-ui/widgets/scan_page/network_list.dart b/lib/new-ui/widgets/scan_page/network_list.dart index aa3d8e448..4cc738188 100644 --- a/lib/new-ui/widgets/scan_page/network_list.dart +++ b/lib/new-ui/widgets/scan_page/network_list.dart @@ -11,6 +11,20 @@ import 'package:url_launcher/url_launcher.dart'; class ScanPageNetworkList extends StatelessWidget { const ScanPageNetworkList({super.key}); + /// Same icon the row would render from `trailingIconPath`, but it now carries the + /// information that following the row leaves the app. + Widget _externalLinkIcon(BuildContext context) => Semantics( + label: S.of(context).opens_in_browser, + excludeSemantics: true, + child: CakeImageWidget( + imageUrl: "assets/new-ui/external_link.svg", + height: 18, + width: 18, + colorFilter: + ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), + ), + ); + @override Widget build(BuildContext context) { return Container( @@ -22,6 +36,7 @@ class ScanPageNetworkList extends StatelessWidget { ModalTopBar( title: S.of(context).compatible_services, leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: Navigator.of(context).pop, ), Padding( @@ -80,7 +95,7 @@ class ScanPageNetworkList extends StatelessWidget { keyValue: "wc", label: "WalletConnect", subtitle: S.of(context).wc_desc, - trailingIconPath: "assets/new-ui/external_link.svg", + trailingWidget: _externalLinkIcon(context), onTap: () { launchUrl(Uri.https("walletconnect.com")); }), @@ -89,7 +104,7 @@ class ScanPageNetworkList extends StatelessWidget { keyValue: "ocp", label: "OpenCryptoPay", subtitle: S.of(context).ocp_desc, - trailingIconPath: "assets/new-ui/external_link.svg", + trailingWidget: _externalLinkIcon(context), onTap: () { launchUrl(Uri.https("opencryptopay.io")); }) diff --git a/lib/new-ui/widgets/send_page/fiat_amount_bar.dart b/lib/new-ui/widgets/send_page/fiat_amount_bar.dart index 06725a4a7..338cdc676 100644 --- a/lib/new-ui/widgets/send_page/fiat_amount_bar.dart +++ b/lib/new-ui/widgets/send_page/fiat_amount_bar.dart @@ -1,23 +1,24 @@ -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; -import 'package:cw_core/crypto_amount_format.dart'; -import 'package:flutter/material.dart'; +import "package:cake_wallet/generated/i18n.dart"; +import "package:cake_wallet/new-ui/widgets/modern_button.dart"; +import "package:cw_core/crypto_amount_format.dart"; +import "package:flutter/material.dart"; class FiatAmountBar extends StatelessWidget { - const FiatAmountBar( - {super.key, - required this.fiatInputMode, - required this.onSwitchButtonPressed, - this.onAllButtonPressed, - required this.cryptoAmount, - required this.fiatAmount, - required this.cryptoCurrencySymbol, - required this.fiatCurrencySymbol, - this.allAmount, - this.foregroundElementColor, - this.textColor, - this.allAmountColor, - this.allAmountTextColor}); + const FiatAmountBar({ + required this.fiatInputMode, + required this.onSwitchButtonPressed, + required this.cryptoAmount, + required this.fiatAmount, + required this.cryptoCurrencySymbol, + required this.fiatCurrencySymbol, + super.key, + this.onAllButtonPressed, + this.allAmount, + this.foregroundElementColor, + this.textColor, + this.allAmountColor, + this.allAmountTextColor, + }); final bool fiatInputMode; final VoidCallback onSwitchButtonPressed; @@ -35,6 +36,10 @@ class FiatAmountBar extends StatelessWidget { @override Widget build(BuildContext context) { + final convertedAmount = fiatInputMode + ? "${cryptoAmount.isEmpty ? "0" : cryptoAmount.withMaxDecimals(8)} $cryptoCurrencySymbol" + : "${fiatAmount.isEmpty ? "0" : fiatAmount} $fiatCurrencySymbol"; + return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -47,14 +52,19 @@ class FiatAmountBar extends StatelessWidget { svgPath: "assets/new-ui/switch.svg", iconSize: 18, onPressed: onSwitchButtonPressed, + semanticLabel: S.of(context).switch_input_currency, ), - GestureDetector( - onTap: onSwitchButtonPressed, - child: Text( - fiatInputMode - ? "${cryptoAmount.isEmpty ? "0" : cryptoAmount.withMaxDecimals(8)} $cryptoCurrencySymbol" - : "${fiatAmount.isEmpty ? "0" : fiatAmount} $fiatCurrencySymbol", - style: TextStyle(color: textColor ?? Theme.of(context).colorScheme.onSurface), + // Announced as the converted value only: the switch button next to it already + // exposes the same action, so this must not become a second control. + Semantics( + label: convertedAmount, + excludeSemantics: true, + child: GestureDetector( + onTap: onSwitchButtonPressed, + child: Text( + convertedAmount, + style: TextStyle(color: textColor ?? Theme.of(context).colorScheme.onSurface), + ), ), ), ], @@ -63,37 +73,50 @@ class FiatAmountBar extends StatelessWidget { Row( spacing: 8, children: [ - Text( - "${S.of(context).max}.", - style: TextStyle(color: textColor ?? Theme.of(context).colorScheme.onSurface), + // The caption is part of the chip's label below. + ExcludeSemantics( + child: Text( + "${S.of(context).max}.", + style: TextStyle(color: textColor ?? Theme.of(context).colorScheme.onSurface), + ), ), Container( decoration: BoxDecoration(borderRadius: BorderRadius.circular(999999)), child: Material( - color: allAmountColor ?? - foregroundElementColor ?? - Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.circular(99999), + color: allAmountColor ?? + foregroundElementColor ?? + Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(99999), + child: Semantics( + button: true, + enabled: onAllButtonPressed != null, + label: S.of(context).max, + value: _formatAmount(allAmount!), + onTap: onAllButtonPressed, + excludeSemantics: true, child: InkWell( borderRadius: BorderRadius.circular(99999), onTap: onAllButtonPressed, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Text( - formatAmount(allAmount!), + _formatAmount(allAmount!), style: TextStyle( - color: allAmountTextColor ?? Theme.of(context).colorScheme.primary), + color: allAmountTextColor ?? Theme.of(context).colorScheme.primary, + ), ), ), - )), - ) + ), + ), + ), + ), ], - ) + ), ], ); } - String formatAmount(String amount) { + String _formatAmount(String amount) { try { return double.parse(amount).toStringAsPrecision(8).replaceFirst(RegExp(r"\.?0+$"), ""); } catch (e) { diff --git a/lib/new-ui/widgets/send_page/l2_action_wallet_selector.dart b/lib/new-ui/widgets/send_page/l2_action_wallet_selector.dart index cea40e197..e9aac029d 100644 --- a/lib/new-ui/widgets/send_page/l2_action_wallet_selector.dart +++ b/lib/new-ui/widgets/send_page/l2_action_wallet_selector.dart @@ -1,41 +1,41 @@ -import 'dart:async'; +import "dart:async"; -import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/main.dart'; -import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart'; -import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; -import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; -import 'package:cake_wallet/new-ui/widgets/send_page/l2_send_external_modal.dart'; -import 'package:cake_wallet/new-ui/widgets/send_page/send_address_input.dart'; -import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; -import 'package:cake_wallet/src/widgets/new_list_row/new_simple_checkbox.dart'; -import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; -import 'package:cake_wallet/view_model/send/send_view_model.dart'; -import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart'; -import 'package:cw_core/currency_for_wallet_type.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import "package:cake_wallet/bitcoin/bitcoin.dart"; +import "package:cake_wallet/generated/i18n.dart"; +import "package:cake_wallet/main.dart"; +import "package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart"; +import "package:cake_wallet/new-ui/widgets/new_primary_button.dart"; +import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart"; +import "package:cake_wallet/new-ui/widgets/send_page/l2_send_external_modal.dart"; +import "package:cake_wallet/new-ui/widgets/send_page/send_address_input.dart"; +import "package:cake_wallet/src/widgets/cake_image_widget.dart"; +import "package:cake_wallet/src/widgets/new_list_row/new_simple_checkbox.dart"; +import "package:cake_wallet/view_model/contact_list/contact_list_view_model.dart"; +import "package:cake_wallet/view_model/send/send_view_model.dart"; +import "package:cake_wallet/view_model/wallet_switcher_view_model.dart"; +import "package:cw_core/currency_for_wallet_type.dart"; +import "package:cw_core/wallet_info.dart"; +import "package:cw_core/wallet_type.dart"; +import "package:flutter/cupertino.dart"; +import "package:flutter/material.dart"; +import "package:modal_bottom_sheet/modal_bottom_sheet.dart"; -enum l2actions { deposit, withdraw } +enum L2Actions { deposit, withdraw } class L2ActionWalletSelector extends StatefulWidget { const L2ActionWalletSelector({ - super.key, required this.showOtherWallets, required this.sendViewModel, required this.action, required this.onSendInitiated, required this.contactListViewModel, required this.walletSwitcherViewModel, + super.key, }); final bool showOtherWallets; final SendViewModel sendViewModel; - final l2actions action; + final L2Actions action; final VoidCallback onSendInitiated; final ContactListViewModel contactListViewModel; final WalletSwitcherViewModel walletSwitcherViewModel; @@ -56,12 +56,18 @@ class _L2ActionWalletSelectorState extends State { super.initState(); if (widget.showOtherWallets) { () async { - items.addAll((await WalletInfo.getAll()).where((item) => - item.type == widget.sendViewModel.walletType && item.hardwareWalletType == null)); + items.addAll( + (await WalletInfo.getAll()).where( + (item) => + item.type == widget.sendViewModel.walletType && item.hardwareWalletType == null, + ), + ); items.sort((a, b) { - if (a.name == widget.sendViewModel.wallet.name) + if (a.name == widget.sendViewModel.wallet.name) { return -1; - else if (b.name == widget.sendViewModel.wallet.name) return 1; + } else if (b.name == widget.sendViewModel.wallet.name) { + return 1; + } return 0; }); setState(() {}); @@ -80,16 +86,17 @@ class _L2ActionWalletSelectorState extends State { mainAxisSize: MainAxisSize.min, children: [ ModalTopBar( - title: widget.action == l2actions.deposit + title: widget.action == L2Actions.deposit ? "${S.of(context).send_from}..." : "${S.of(context).receive_to}...", - leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingIcon: const Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: Navigator.of(context).pop, ), Flexible( child: SafeArea( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), + padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( spacing: 12, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -123,29 +130,17 @@ class _L2ActionWalletSelectorState extends State { itemBuilder: (context, index) { final item = items[index]; return Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), + padding: const EdgeInsets.symmetric(vertical: 4), child: WalletRow( currencyIconPath: getCryptoCurrencyIconForWalletListItem(item.type), walletName: item.name, isCurrent: item.name == widget.sendViewModel.wallet.name, isSelected: _selectedWalletIndex == index && !textEntered, - onTap: () async { + onTap: () { setState(() { addressController.text = ""; _selectedWalletIndex = index; }); - // if (widget.action == l2actions.withdraw) { - // widget.sendViewModel.outputs.first.address = item.address; - // widget.onSendInitiated(); - // } else if (widget.action == l2actions.deposit) { - // setState(() { - // loadingWalletName = item.name; - // }); - // await _handleChangeWallet(item); - // widget.onSendInitiated(); - // setState(() { - // loadingWalletName = null; - // }); }, ), ); @@ -198,7 +193,7 @@ class _L2ActionWalletSelectorState extends State { // ), // ), // ), - if (widget.action == l2actions.withdraw) + if (widget.action == L2Actions.withdraw) Row( children: [ Flexible( @@ -215,12 +210,12 @@ class _L2ActionWalletSelectorState extends State { Column( spacing: 8, children: [ - if (widget.action == l2actions.deposit && widget.showOtherWallets) ...[ + if (widget.action == L2Actions.deposit && widget.showOtherWallets) ...[ Container( height: 1, color: Theme.of(context).colorScheme.onSurfaceVariant.withAlpha(64), ), - SizedBox(), + const SizedBox(), Container( height: 52, decoration: BoxDecoration( @@ -232,7 +227,7 @@ class _L2ActionWalletSelectorState extends State { color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(16), - onTap: () async { + onTap: () { Navigator.of(context, rootNavigator: true).pop(); showCupertinoModalBottomSheet( context: navigatorKey.currentContext ?? context, @@ -246,7 +241,7 @@ class _L2ActionWalletSelectorState extends State { ); }, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), + padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( mainAxisAlignment: MainAxisAlignment.center, spacing: 10, @@ -275,19 +270,22 @@ class _L2ActionWalletSelectorState extends State { onPressed: () async { if (widget.sendViewModel.wallet.type == WalletType.bitcoin || widget.sendViewModel.wallet.type == WalletType.litecoin) { - if (widget.action == l2actions.withdraw) { + if (widget.action == L2Actions.withdraw) { widget.sendViewModel.outputs.first.address = bitcoin!.getUnusedSegwitAddress(widget.sendViewModel.wallet)!; } if (widget.showOtherWallets) { - if (widget.action == l2actions.deposit) { + if (widget.action == L2Actions.deposit) { await _handleChangeWallet(items[_selectedWalletIndex]); } else { - widget.sendViewModel.outputs.first.address = - addressController.text.isNotEmpty - ? addressController.text - : items[_selectedWalletIndex].address; + if (items[_selectedWalletIndex].name != + widget.sendViewModel.wallet.name) { + widget.sendViewModel.outputs.first.address = + addressController.text.isNotEmpty + ? addressController.text + : items[_selectedWalletIndex].address; + } } } widget.onSendInitiated(); @@ -298,7 +296,7 @@ class _L2ActionWalletSelectorState extends State { textColor: Theme.of(context).colorScheme.onPrimary, isLoading: _isLoading, ), - SizedBox() + const SizedBox() ], ), ], @@ -339,13 +337,13 @@ class _L2ActionWalletSelectorState extends State { class WalletRow extends StatelessWidget { const WalletRow({ - super.key, required this.currencyIconPath, required this.walletName, required this.onTap, this.isLoading = false, this.isCurrent = false, this.isSelected, + super.key, }); final String currencyIconPath; @@ -370,7 +368,7 @@ class WalletRow extends StatelessWidget { borderRadius: BorderRadius.circular(16), onTap: onTap, child: Padding( - padding: EdgeInsets.symmetric(horizontal: 12), + padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/lib/new-ui/widgets/send_page/l2_send_external_modal.dart b/lib/new-ui/widgets/send_page/l2_send_external_modal.dart index 82c077da6..6fb6ddf94 100644 --- a/lib/new-ui/widgets/send_page/l2_send_external_modal.dart +++ b/lib/new-ui/widgets/send_page/l2_send_external_modal.dart @@ -73,6 +73,7 @@ class _L2SendExternalModalState extends State { ], ), trailingIcon: Icon(Icons.close), + trailingSemanticLabel: S.of(context).close, onTrailingPressed: Navigator.of(context).pop, ), SafeArea( diff --git a/lib/new-ui/widgets/send_page/recipient_dot_row.dart b/lib/new-ui/widgets/send_page/recipient_dot_row.dart index 18c8f3df0..29ec16fe5 100644 --- a/lib/new-ui/widgets/send_page/recipient_dot_row.dart +++ b/lib/new-ui/widgets/send_page/recipient_dot_row.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/generated/i18n.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -179,32 +180,44 @@ class _RecipientDotState extends State { curve: Curves.easeOutCubic, top: _created ? 0 : widget.size, child: Material( - child: AnimatedSwitcher( - duration: Duration(milliseconds: 150), - child: GestureDetector( - key: ValueKey(widget.selected), + // Dots scaled all the way down are not visible, so they must not be + // reachable by a screen reader either. + child: ExcludeSemantics( + excluding: widget.textOpacity <= 0, + child: Semantics( + button: true, + selected: widget.selected, + label: S.of(context).recipient_number("${widget.index + 1}"), onTap: widget.onTap, - child: Container( - width: widget.size, - height: widget.size, - margin: EdgeInsets.symmetric(horizontal: widget.spacing / 2), - decoration: BoxDecoration( - color: widget.selected - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.surfaceContainer, - shape: BoxShape.circle, - ), - alignment: Alignment.center, - child: Opacity( - opacity: widget.textOpacity, - child: Text( - (widget.index + 1).toString(), - style: TextStyle( - color: widget.selected - ? Theme.of(context).colorScheme.onPrimary - : Theme.of(context).colorScheme.onSurface, - fontSize: 14, - fontWeight: FontWeight.w500), + excludeSemantics: true, + child: AnimatedSwitcher( + duration: Duration(milliseconds: 150), + child: GestureDetector( + key: ValueKey(widget.selected), + onTap: widget.onTap, + child: Container( + width: widget.size, + height: widget.size, + margin: EdgeInsets.symmetric(horizontal: widget.spacing / 2), + decoration: BoxDecoration( + color: widget.selected + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.surfaceContainer, + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: Opacity( + opacity: widget.textOpacity, + child: Text( + (widget.index + 1).toString(), + style: TextStyle( + color: widget.selected + ? Theme.of(context).colorScheme.onPrimary + : Theme.of(context).colorScheme.onSurface, + fontSize: 14, + fontWeight: FontWeight.w500), + ), + ), ), ), ), diff --git a/lib/new-ui/widgets/send_page/send_address_input.dart b/lib/new-ui/widgets/send_page/send_address_input.dart index 704a41571..87fc67fcc 100644 --- a/lib/new-ui/widgets/send_page/send_address_input.dart +++ b/lib/new-ui/widgets/send_page/send_address_input.dart @@ -86,22 +86,27 @@ class _NewSendAddressInputState extends State { Expanded( child: Stack( children: [ - TextField( - focusNode: widget.focusNode, - autocorrect: false, - enableSuggestions: false, - onSubmitted: (val) => FocusScope.of(context).unfocus(), - onChanged: state.didChange, - onEditingComplete: () { - widget.onEditingComplete(); - }, - onTapOutside: (_) { - widget.onEditingComplete(); - }, - controller: widget.addressController, - decoration: InputDecoration( - hintText: widget.hintText ?? S.of(context).search_or_enter, - errorMaxLines: 3, + MergeSemantics( + child: Semantics( + label: _fieldSemanticsLabel(context), + child: TextField( + focusNode: widget.focusNode, + autocorrect: false, + enableSuggestions: false, + onSubmitted: (val) => FocusScope.of(context).unfocus(), + onChanged: state.didChange, + onEditingComplete: () { + widget.onEditingComplete(); + }, + onTapOutside: (_) { + widget.onEditingComplete(); + }, + controller: widget.addressController, + decoration: InputDecoration( + hintText: widget.hintText ?? S.of(context).search_or_enter, + errorMaxLines: 3, + ), + ), ), ), Positioned.fill( @@ -113,9 +118,13 @@ class _NewSendAddressInputState extends State { widget.addressController.text.isEmpty) ? 0 : 1, - child: SendAddressOverlay( - address: widget.addressController.text, - displayName: widget.displayName, + // Purely visual copy of the field content; announcing it again + // would read the address twice. + child: ExcludeSemantics( + child: SendAddressOverlay( + address: widget.addressController.text, + displayName: widget.displayName, + ), )), ), ), @@ -150,9 +159,15 @@ class _NewSendAddressInputState extends State { if (state.hasError) Padding( padding: EdgeInsets.only(top: 6, left: 8), - child: Text( - state.errorText!, - style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.error), + child: Semantics( + container: true, + liveRegion: true, + label: "${S.of(context).address_or_alias}, ${state.errorText!}", + excludeSemantics: true, + child: Text( + state.errorText!, + style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.error), + ), ), ) ], @@ -161,6 +176,19 @@ class _NewSendAddressInputState extends State { ); } + String _fieldSemanticsLabel(BuildContext context) { + final label = S.of(context).address_or_alias; + final displayName = widget.displayName; + + if (displayName == null || + displayName.isEmpty || + displayName == widget.addressController.text) { + return label; + } + + return "$label, $displayName"; + } + Future _presentQRScanner(BuildContext context) async { bool isCameraPermissionGranted = await PermissionHandler.checkPermission(Permission.camera, context); diff --git a/lib/new-ui/widgets/send_page/send_amount_input.dart b/lib/new-ui/widgets/send_page/send_amount_input.dart index c66006452..051100bff 100644 --- a/lib/new-ui/widgets/send_page/send_amount_input.dart +++ b/lib/new-ui/widgets/send_page/send_amount_input.dart @@ -1,20 +1,21 @@ -import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart'; -import 'package:cake_wallet/new-ui/widgets/send_page/floating_icon_button.dart'; -import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; -import 'package:cake_wallet/utils/decimal_input_formatter.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; +import "package:cake_wallet/generated/i18n.dart"; +import "package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart"; +import "package:cake_wallet/new-ui/widgets/send_page/floating_icon_button.dart"; +import "package:cake_wallet/src/widgets/cake_image_widget.dart"; +import "package:cake_wallet/utils/decimal_input_formatter.dart"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:flutter_mobx/flutter_mobx.dart"; class NewSendAmountInput extends StatefulWidget { const NewSendAmountInput({ - super.key, required this.currency, required this.maxDecimals, required this.hasPicker, required this.onPickerClicked, required this.currencyIconPath, required this.amountController, + super.key, this.validator, }); @@ -41,31 +42,40 @@ class _NewSendAmountInputState extends State { } @override - Widget build(BuildContext context) { - return FormField( - key: formFieldKey, - initialValue: widget.amountController.text, - validator: widget.validator, - builder: (state) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( + Widget build(BuildContext context) => FormField( + key: formFieldKey, + initialValue: widget.amountController.text, + validator: widget.validator, + builder: (state) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( decoration: BoxDecoration( - color: widget.hasPicker - ? Theme.of(context).colorScheme.surfaceContainerHigh - : Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.circular(18)), + color: widget.hasPicker + ? Theme.of(context).colorScheme.surfaceContainerHigh + : Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(18), + ), child: Row( children: [ Expanded( child: Container( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.horizontal(left: Radius.circular(18))), + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: const BorderRadius.horizontal(left: Radius.circular(18)), + ), child: Row( mainAxisSize: MainAxisSize.max, spacing: 8, children: [ + // Deliberately unwrapped. Naming the field with + // MergeSemantics > Semantics(label:) made Android announce + // the amount twice: FormField's own wrapper node reflects + // its descendants' text, so the authored label came back a + // second time on the container. The visible "Amount:" + // caption on the send page carries the name instead, and it + // must stay in the semantics tree for this field to be + // named at all. Expanded( child: TextField( keyboardType: TextInputType.numberWithOptions( @@ -79,82 +89,107 @@ class _NewSendAmountInputState extends State { ], controller: widget.amountController, decoration: InputDecoration( - hintText: widget.maxDecimals > 0 ? "0" : "0.00", + hintText: widget.maxDecimals == 0 ? "0" : "0.00", errorMaxLines: 3, ), onChanged: state.didChange, ), ), FloatingIconButton( - iconPath: "assets/new-ui/paste.svg", - onPressed: () async { - final data = await Clipboard.getData(Clipboard.kTextPlain); - if (data != null && data.text != null) { - final text = data.text!; - widget.amountController.value = TextEditingValue( - text: text, - selection: TextSelection.collapsed(offset: text.length), - ); - } - }), + iconPath: "assets/new-ui/paste.svg", + onPressed: () async { + final data = await Clipboard.getData(Clipboard.kTextPlain); + if (data != null && data.text != null) { + final text = data.text!; + widget.amountController.value = TextEditingValue( + text: text, + selection: TextSelection.collapsed(offset: text.length), + ); + } + }, + ), ], ), ), ), IntrinsicWidth( child: Observer( - builder: (_) { - return GestureDetector( + // `container: true` is load bearing. FormField wraps this + // builder in its own non-container Semantics annotation, so + // without a container of its own this configuration is + // absorbed into that wrapper node — which then parents the + // amount TextField and hides it from screen readers. + builder: (_) => Semantics( + container: true, + button: widget.hasPicker ? true : null, + enabled: widget.hasPicker ? true : null, + label: widget.hasPicker ? S.of(context).select_asset : widget.currency, + value: widget.hasPicker ? widget.currency : null, + onTap: widget.hasPicker ? widget.onPickerClicked : null, + excludeSemantics: true, + child: GestureDetector( onTap: widget.onPickerClicked, child: Container( - alignment: Alignment.center, - width: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topRight: Radius.circular(18), - bottomRight: Radius.circular(18)), - color: widget.hasPicker - ? Theme.of(context).colorScheme.surfaceContainerHigh - : Theme.of(context).colorScheme.surfaceContainer, + alignment: Alignment.center, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topRight: Radius.circular(18), + bottomRight: Radius.circular(18), ), - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - spacing: 8, - children: [ - if (widget.hasPicker && widget.currencyIconPath.isNotEmpty) - TokenImageWidget( - imageUrl: widget.currencyIconPath, size: 24), - Text(widget.currency), - if (widget.hasPicker) - CakeImageWidget( - imageUrl: "assets/new-ui/chooser.svg", - width: 12, - height: 12, - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.primary, - BlendMode.srcIn), - ) - ], - ))), - ); - }, + color: widget.hasPicker + ? Theme.of(context).colorScheme.surfaceContainerHigh + : Theme.of(context).colorScheme.surfaceContainer, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + spacing: 8, + children: [ + if (widget.hasPicker && widget.currencyIconPath.isNotEmpty) + TokenImageWidget( + imageUrl: widget.currencyIconPath, + size: 24, + ), + Text(widget.currency), + if (widget.hasPicker) + CakeImageWidget( + imageUrl: "assets/new-ui/chooser.svg", + width: 12, + height: 12, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + ], + ), + ), + ), + ), + ), ), ), ], - )), - if (state.hasError) - Padding( - padding: EdgeInsets.only(top: 6, left: 8), - child: Text( - state.errorText!, - style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.error), ), - ) - ], - ), - ); - } + ), + if (state.hasError) + Padding( + padding: const EdgeInsets.only(top: 6, left: 8), + child: Semantics( + container: true, + liveRegion: true, + label: "${S.of(context).amount}${state.errorText!}", + excludeSemantics: true, + child: Text( + state.errorText!, + style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.error), + ), + ), + ), + ], + ), + ); } diff --git a/lib/new-ui/widgets/send_page/send_confirm_bottom_widget.dart b/lib/new-ui/widgets/send_page/send_confirm_bottom_widget.dart index 540910b3e..950932e9a 100644 --- a/lib/new-ui/widgets/send_page/send_confirm_bottom_widget.dart +++ b/lib/new-ui/widgets/send_page/send_confirm_bottom_widget.dart @@ -80,18 +80,25 @@ class LoadingBottomWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 12, - children: [ - CupertinoActivityIndicator( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - Text( - text, - style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), - ) - ], + // One node per state so each step of the send is announced exactly once. + return Semantics( + container: true, + liveRegion: true, + label: text, + excludeSemantics: true, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 12, + children: [ + CupertinoActivityIndicator( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + Text( + text, + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), + ) + ], + ), ); } } @@ -106,45 +113,51 @@ class TransactionErrorActions extends StatelessWidget { return Column( spacing: 12, children: [ - Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.errorContainer.withAlpha(64), - borderRadius: BorderRadius.circular(16), - ), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - spacing: 12, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 8, - children: [ - CakeImageWidget( - imageUrl: "assets/new-ui/warning.svg", - height: 24, - width: 24, - colorFilter: - ColorFilter.mode(Theme.of(context).colorScheme.error, BlendMode.srcIn), - ), - Text( - S.of(context).transaction_error, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.error), - ) - ], - ), - Text( - errorText, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.error), - textAlign: TextAlign.center, - ), - ], + Semantics( + container: true, + liveRegion: true, + label: "${S.of(context).transaction_error}\n$errorText", + excludeSemantics: true, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.errorContainer.withAlpha(64), + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + spacing: 12, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 8, + children: [ + CakeImageWidget( + imageUrl: "assets/new-ui/warning.svg", + height: 24, + width: 24, + colorFilter: + ColorFilter.mode(Theme.of(context).colorScheme.error, BlendMode.srcIn), + ), + Text( + S.of(context).transaction_error, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.error), + ) + ], + ), + Text( + errorText, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.error), + textAlign: TextAlign.center, + ), + ], + ), ), ), ), diff --git a/lib/new-ui/widgets/send_page/send_confirm_sheet.dart b/lib/new-ui/widgets/send_page/send_confirm_sheet.dart index 165823a1a..5b34686d3 100644 --- a/lib/new-ui/widgets/send_page/send_confirm_sheet.dart +++ b/lib/new-ui/widgets/send_page/send_confirm_sheet.dart @@ -87,11 +87,15 @@ class _SendConfirmSheetState extends State { offset: _committed ? const Offset(-1, 0) : Offset.zero, duration: const Duration(milliseconds: 300), curve: Curves.easeOutCubic, - child: SendTransactionDetails( - sendViewModel: widget.sendViewModel, - isPage: widget.isPage, - title: widget.title, - iconPath: widget.iconPath, + // Both screens stay mounted; only the visible one may be reachable. + child: ExcludeSemantics( + excluding: _committed, + child: SendTransactionDetails( + sendViewModel: widget.sendViewModel, + isPage: widget.isPage, + title: widget.title, + iconPath: widget.iconPath, + ), ), ), ), @@ -102,8 +106,11 @@ class _SendConfirmSheetState extends State { offset: _committed ? Offset.zero : const Offset(1, 0), duration: const Duration(milliseconds: 300), curve: Curves.easeOutCubic, - child: TransactionCommitedScreen( - sendViewModel: widget.sendViewModel, + child: ExcludeSemantics( + excluding: !_committed, + child: TransactionCommitedScreen( + sendViewModel: widget.sendViewModel, + ), ), ), ), @@ -154,13 +161,20 @@ class SendTransactionDetails extends StatelessWidget { width: 28, height: 28, ), - Text( - title ?? S.of(context).send, - style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20), + Semantics( + header: true, + // Android reads the heading from headingLevel since the + // Flutter 3.41 engine; header: alone only covers iOS. + headingLevel: 1, + child: Text( + title ?? S.of(context).send, + style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20), + ), ) ], ), trailingIcon: Icon(Icons.close), + trailingSemanticLabel: S.of(context).close, onTrailingPressed: Navigator.of(context).maybePop, ), isPage @@ -249,37 +263,40 @@ class SendTransactionDetails extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, spacing: 24, children: [ - Column( - children: [ - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - spacing: 4, - children: [ - Flexible( - child: Text( - amount, - style: TextStyle( - fontSize: 36, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface), + // The amount being sent is the value under review: announce it as one group. + MergeSemantics( + child: Column( + children: [ + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + spacing: 4, + children: [ + Flexible( + child: Text( + amount, + style: TextStyle( + fontSize: 36, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface), + ), ), - ), - Text(currencySymbol, - style: TextStyle( - fontSize: 36, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurfaceVariant)) - ], - ), - Text( - fiatAmount, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurfaceVariant), - ), - ], + Text(currencySymbol, + style: TextStyle( + fontSize: 36, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant)) + ], + ), + Text( + fiatAmount, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + ], + ), ), if (outputs.length >= 1 && (outputs.first.extractedAddress.isNotEmpty || outputs.first.address.isNotEmpty) && @@ -351,32 +368,34 @@ class SendTransactionDetails extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.all(12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(S.of(context).fee, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface)), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - "${fee.withLocalSeperator(sendViewModel.languageCode)} ${sendViewModel.currencySymbol}", + child: MergeSemantics( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(S.of(context).fee, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurfaceVariant), - ), - Text(fiatFee.withLocalSeperator(sendViewModel.languageCode), + color: Theme.of(context).colorScheme.onSurface)), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "${fee.withLocalSeperator(sendViewModel.languageCode)} ${sendViewModel.currencySymbol}", style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurfaceVariant)) - ], - ) - ], + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + Text(fiatFee.withLocalSeperator(sendViewModel.languageCode), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant)) + ], + ) + ], + ), ), ), if (sendViewModel.isElectrumWallet) ...[ @@ -389,27 +408,29 @@ class SendTransactionDetails extends StatelessWidget { ), Padding( padding: const EdgeInsets.all(12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(S.of(context).network, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface)), - Column( - children: [ - Text( - sendViewModel.selectedCryptoCurrency == CryptoCurrency.btcln - ? "Lightning" - : bitcoin!.getNetworkName(sendViewModel.wallet), - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurfaceVariant)) - ], - ) - ], + child: MergeSemantics( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(S.of(context).network, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface)), + Column( + children: [ + Text( + sendViewModel.selectedCryptoCurrency == CryptoCurrency.btcln + ? "Lightning" + : bitcoin!.getNetworkName(sendViewModel.wallet), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant)) + ], + ) + ], + ), ), ) ], @@ -458,9 +479,14 @@ class _TransactionCommitedScreenState extends State { SizedBox( height: 12, ), - Text( - S.of(context).transaction_sent_new, - style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600), + // The sheet swaps its content in place, so this title becoming visible is what + // tells a screen reader that the transaction went through. + Semantics( + liveRegion: true, + child: Text( + S.of(context).transaction_sent_new, + style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600), + ), ), SizedBox(), CakeImageWidget(width: 200, height: 200, imageUrl: "assets/new-ui/birthday_cake.svg"), @@ -513,7 +539,8 @@ class _TransactionCommitedScreenState extends State { showModalBottomSheet( isScrollControlled: true, context: context, - builder: (context) => page); + builder: (context) => + FractionallySizedBox(heightFactor: 0.9, child: page)); }), ], ), @@ -550,37 +577,44 @@ class TransactionCommittedScreenActionButton extends StatelessWidget { @override Widget build(BuildContext context) { return Flexible( - child: GestureDetector( - onTap: onTap, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - color: Theme.of(context).colorScheme.surfaceContainer), - child: Padding( - padding: EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 10, - children: [ - isLoading - ? CupertinoActivityIndicator() - : CakeImageWidget( - imageUrl: iconPath, - width: 24, - height: 24, - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.primary, BlendMode.srcIn), - ), - Text( - text, - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.w500), - ) - ], - ), - ), - ))); + child: Semantics( + button: true, + enabled: !isLoading, + label: text, + value: isLoading ? S.of(context).loading : null, + onTap: isLoading ? null : onTap, + excludeSemantics: true, + child: GestureDetector( + onTap: isLoading ? null : onTap, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Theme.of(context).colorScheme.surfaceContainer), + child: Padding( + padding: EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 10, + children: [ + isLoading + ? CupertinoActivityIndicator() + : CakeImageWidget( + imageUrl: iconPath, + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, BlendMode.srcIn), + ), + Text( + text, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.w500), + ) + ], + ), + ), + )))); } } @@ -621,16 +655,28 @@ class _MultiSendAddressPreviewState extends State { style: TextStyle(fontFamily: "IBM Plex Mono"), ), if (!_expanded) - GestureDetector( - onTap: () { - setState(() { - _expanded = true; - }); - }, - child: Text(middleTruncate(widget.address, 8, 8), - style: TextStyle( - fontFamily: "IBM Plex Mono", - color: Theme.of(context).colorScheme.primary))) + Semantics( + button: true, + // Read the whole address rather than the truncated form. + label: widget.address, + hint: S.of(context).show_full_address, + onTap: () { + setState(() { + _expanded = true; + }); + }, + excludeSemantics: true, + child: GestureDetector( + onTap: () { + setState(() { + _expanded = true; + }); + }, + child: Text(middleTruncate(widget.address, 8, 8), + style: TextStyle( + fontFamily: "IBM Plex Mono", + color: Theme.of(context).colorScheme.primary))), + ) else AddressFormatter.buildSegmentedAddress( address: widget.address, diff --git a/lib/new-ui/widgets/send_page/send_memo_input.dart b/lib/new-ui/widgets/send_page/send_memo_input.dart index d3f408223..5dc69491a 100644 --- a/lib/new-ui/widgets/send_page/send_memo_input.dart +++ b/lib/new-ui/widgets/send_page/send_memo_input.dart @@ -31,11 +31,16 @@ class NewSendMemoInput extends StatelessWidget { child: Row( children: [ Expanded( - child: TextField( - maxLength: maxMemoLength, - controller: memoController, - decoration: InputDecoration( - hintText: hintText ?? S.of(context).memo_optional, counterText: ""), + child: MergeSemantics( + child: Semantics( + label: hintText ?? S.of(context).memo_optional, + child: TextField( + maxLength: maxMemoLength, + controller: memoController, + decoration: InputDecoration( + hintText: hintText ?? S.of(context).memo_optional, counterText: ""), + ), + ), ), ), SizedBox(width: 12), @@ -61,8 +66,12 @@ class NewSendMemoInput extends StatelessWidget { style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant)), ), - Text("${memoController.text.length} / ${maxMemoLength}", - style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.primary)) + // The field itself already reports the character count through + // maxValueLength/currentValueLength, so this visual counter stays silent. + ExcludeSemantics( + child: Text("${memoController.text.length} / ${maxMemoLength}", + style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.primary)), + ) ], ), ) diff --git a/lib/new-ui/widgets/send_page/send_syncing_indicator.dart b/lib/new-ui/widgets/send_page/send_syncing_indicator.dart index 946efa116..516e071ee 100644 --- a/lib/new-ui/widgets/send_page/send_syncing_indicator.dart +++ b/lib/new-ui/widgets/send_page/send_syncing_indicator.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cw_core/sync_status.dart'; import 'package:flutter/material.dart'; @@ -18,28 +19,40 @@ class SendSyncingIndicator extends StatelessWidget { eta = ""; } - return Container( - height: 48, - decoration: BoxDecoration(color: backgroundColor, borderRadius: BorderRadius.circular(99999)), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Row( - mainAxisSize: MainAxisSize.min, - spacing: 10, - children: [ - if (status.progress() > 0) - SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - value: status.progress(), - strokeWidth: 3, - strokeCap: StrokeCap.round, - color: outlineColor, + final statusText = "${S.of(context).synchronizing}... ${eta}"; + + // A single status node: progress ticks keep updating the value, and without a live + // region they do not interrupt the user on every tick. + return Semantics( + container: true, + label: statusText, + value: status.progress() > 0 ? "${(status.progress() * 100).round()}%" : null, + excludeSemantics: true, + child: Container( + height: 48, + decoration: + BoxDecoration(color: backgroundColor, borderRadius: BorderRadius.circular(99999)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 10, + children: [ + if (status.progress() > 0) + SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + value: status.progress(), + strokeWidth: 3, + strokeCap: StrokeCap.round, + color: outlineColor, + semanticsLabel: S.of(context).synchronizing, + ), ), - ), - Text("Synchronizing... ${eta}", style: TextStyle(color: outlineColor)), - ], + Text(statusText, style: TextStyle(color: outlineColor)), + ], + ), ), ), ); diff --git a/lib/new-ui/widgets/swap_page/provider_options_page.dart b/lib/new-ui/widgets/swap_page/provider_options_page.dart index 782a5b0ac..88333451f 100644 --- a/lib/new-ui/widgets/swap_page/provider_options_page.dart +++ b/lib/new-ui/widgets/swap_page/provider_options_page.dart @@ -37,6 +37,7 @@ class ProviderOptionsPage extends StatelessWidget { title: S.of(context).swap_providers, onLeadingPressed: Navigator.of(context).pop, leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.of(context).seed_alert_back, ), Expanded( child: SingleChildScrollView( diff --git a/lib/new-ui/widgets/swap_page/provider_selector_page.dart b/lib/new-ui/widgets/swap_page/provider_selector_page.dart index e521ea937..c8e15e8f0 100644 --- a/lib/new-ui/widgets/swap_page/provider_selector_page.dart +++ b/lib/new-ui/widgets/swap_page/provider_selector_page.dart @@ -24,6 +24,7 @@ class ProviderSelectorPage extends StatelessWidget { ModalTopBar( title: S.of(context).change_provider, leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: Navigator.of(context).maybePop, ), Expanded( diff --git a/lib/new-ui/widgets/swap_page/refund_address_modal.dart b/lib/new-ui/widgets/swap_page/refund_address_modal.dart index 6a8f0e2b9..031d67e5a 100644 --- a/lib/new-ui/widgets/swap_page/refund_address_modal.dart +++ b/lib/new-ui/widgets/swap_page/refund_address_modal.dart @@ -44,6 +44,7 @@ class _RefundAddressModalState extends State { ModalTopBar( title: S.of(context).set_refund_address, trailingIcon: Icon(Icons.close), + trailingSemanticLabel: S.of(context).close, onTrailingPressed: Navigator.of(context).pop, ), SafeArea( diff --git a/lib/new-ui/widgets/swap_page/swap_address_selection_modal.dart b/lib/new-ui/widgets/swap_page/swap_address_selection_modal.dart index 67eb2d125..8c6d83498 100644 --- a/lib/new-ui/widgets/swap_page/swap_address_selection_modal.dart +++ b/lib/new-ui/widgets/swap_page/swap_address_selection_modal.dart @@ -82,6 +82,7 @@ class _SwapAddressSelectionModalState extends State { ? "${S.of(context).receive_to}..." : "${S.of(context).send_from}...", leadingIcon: Icon(Icons.close), + leadingSemanticLabel: S.of(context).close, onLeadingPressed: Navigator.of(context).pop, ), Flexible( diff --git a/lib/new-ui/widgets/swap_page/swap_confirm_sheet.dart b/lib/new-ui/widgets/swap_page/swap_confirm_sheet.dart index 6a7f49900..b5626056b 100644 --- a/lib/new-ui/widgets/swap_page/swap_confirm_sheet.dart +++ b/lib/new-ui/widgets/swap_page/swap_confirm_sheet.dart @@ -47,15 +47,23 @@ class _SwapConfirmSheetState extends State { if (sendVM.wallet.isHardwareWallet) { if (!sendVM.hardwareWalletViewModel!.isConnected(sendVM.walletType)) { - await Navigator.of(context).pushNamed(Routes.connectDevices, - arguments: ConnectDevicePageParams( - walletType: sendVM.walletType, - hardwareWalletType: sendVM.wallet.walletInfo.hardwareWalletType!, - onConnectDevice: (context, _) { - sendVM.hardwareWalletViewModel!.initWallet(sendVM.wallet); - Navigator.of(context).pop(); - }, - )); + await Navigator.of(context).pushNamed( + Routes.connectDevices, + arguments: ConnectDevicePageParams( + walletType: sendVM.walletType, + hardwareWalletType: sendVM.wallet.walletInfo.hardwareWalletType!, + onConnectDevice: (context, _) { + sendVM.hardwareWalletViewModel!.initWallet(sendVM.wallet); + Navigator.of(context).pop(); + }, + isReconnect: false, + ), + ); + + // Recheck to handle tap-backs + if (!sendVM.hardwareWalletViewModel!.isConnected(sendVM.walletType)) { + return; + } } else { sendVM.hardwareWalletViewModel!.initWallet(sendVM.wallet); } @@ -148,6 +156,7 @@ class SwapTransactionDetails extends StatelessWidget { fromIconPath: exchangeViewModel.depositCurrency.iconPath ?? "", toIconPath: exchangeViewModel.receiveCurrency.iconPath ?? ""), trailingIcon: Icon(Icons.close), + trailingSemanticLabel: S.of(context).close, onTrailingPressed: Navigator.of(context).maybePop, ), SafeArea( diff --git a/lib/new-ui/widgets/swap_page/swap_options_page.dart b/lib/new-ui/widgets/swap_page/swap_options_page.dart index b4726440b..98c710e9d 100644 --- a/lib/new-ui/widgets/swap_page/swap_options_page.dart +++ b/lib/new-ui/widgets/swap_page/swap_options_page.dart @@ -25,6 +25,7 @@ class SwapOptionsPage extends StatelessWidget { ModalTopBar( title: S.of(context).configure, leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: Navigator.of(context).pop, ), Padding( diff --git a/lib/new-ui/widgets/swap_page/swap_send_external_modal.dart b/lib/new-ui/widgets/swap_page/swap_send_external_modal.dart index 71d34304c..f2185ce39 100644 --- a/lib/new-ui/widgets/swap_page/swap_send_external_modal.dart +++ b/lib/new-ui/widgets/swap_page/swap_send_external_modal.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/copy_wrapper.dart'; import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; import 'package:cake_wallet/new-ui/widgets/swap_page/swap_modal_header.dart'; @@ -9,6 +10,7 @@ import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/payment_uris.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; class SwapSendExternalModal extends StatefulWidget { const SwapSendExternalModal( @@ -67,6 +69,7 @@ class _SwapSendExternalModalState extends State { leadingWidget: SwapModalHeader( fromIconPath: widget.from.iconPath ?? "", toIconPath: widget.to.iconPath ?? ""), trailingIcon: Icon(Icons.close), + trailingSemanticLabel: S.of(context).close, onTrailingPressed: Navigator.of(context).pop, ), Padding( @@ -166,10 +169,63 @@ class _SwapSendExternalModalState extends State { ), )), if (widget.exchangeTradeViewModel.trade.extraId != null) - Text( - "${S.of(context).destination_tag} ${widget.exchangeTradeViewModel.trade.extraId}", - style: TextStyle( - fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), + CopyWrapper( + data: ClipboardData( + text: widget.exchangeTradeViewModel.trade.extraId!), + builder: (context, copied) => Container( + width: double.infinity, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + Text( + S.of(context).destination_tag, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8, + children: [ + Flexible( + child: Text( + widget.exchangeTradeViewModel.trade.extraId!, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.primary), + ), + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: Icon( + copied ? Icons.check : Icons.copy, + key: ValueKey(copied), + // Only the icon changes here, so label + // the copied state for CopyWrapper's + // live region to announce. + semanticLabel: copied ? S.of(context).copied : null, + size: 18, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + ], + ), + ), + ), ), Row( spacing: 8, diff --git a/lib/new-ui/widgets/swap_page/trocador_providers_settings.dart b/lib/new-ui/widgets/swap_page/trocador_providers_settings.dart index 36474acd4..a07b2cd12 100644 --- a/lib/new-ui/widgets/swap_page/trocador_providers_settings.dart +++ b/lib/new-ui/widgets/swap_page/trocador_providers_settings.dart @@ -25,6 +25,7 @@ class TrocadorProvidersSettings extends StatelessWidget { ModalTopBar( title: "Trocador ${S.of(context).providers}", leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: Navigator.of(context).pop, ), Expanded( diff --git a/lib/new-ui/widgets/wallet_deprecation_popup.dart b/lib/new-ui/widgets/wallet_deprecation_popup.dart index 2b233c0ac..a28c5d1f0 100644 --- a/lib/new-ui/widgets/wallet_deprecation_popup.dart +++ b/lib/new-ui/widgets/wallet_deprecation_popup.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/new-ui/widgets/animated_dropdown.dart'; import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; @@ -27,6 +28,7 @@ class WalletDeprecationPopup extends StatelessWidget { ModalTopBar( title: "", leadingIcon: Icon(Icons.close), + leadingSemanticLabel: S.of(context).close, onLeadingPressed: Navigator.of(context).pop, ), Padding( diff --git a/lib/reactions/on_authentication_state_change.dart b/lib/reactions/on_authentication_state_change.dart index 02ddd5639..d63c54781 100644 --- a/lib/reactions/on_authentication_state_change.dart +++ b/lib/reactions/on_authentication_state_change.dart @@ -146,7 +146,9 @@ void startAuthenticationStateChange( ), ); } else { - await navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false); + unawaited( + navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false), + ); } WidgetsBinding.instance.addPostFrameCallback((_) { final linkViewModel = getIt.get(); diff --git a/lib/router.dart b/lib/router.dart index 40772c8b1..39a28a42f 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -4,14 +4,15 @@ 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/new-ui/pages/about_page.dart'; -import 'package:cake_wallet/new-ui/pages/bridge/bridge_confirm_sheet.dart'; import 'package:cake_wallet/new-ui/pages/bridge/bridge_history_page.dart'; import 'package:cake_wallet/new-ui/pages/bridge/bridge_network_page.dart'; import 'package:cake_wallet/new-ui/pages/bridge/bridge_receiving_wallet_page.dart'; import 'package:cake_wallet/new-ui/pages/coin_control_page.dart'; import 'package:cake_wallet/new-ui/pages/addresses_page.dart'; import 'package:cake_wallet/new-ui/pages/lightning_username_page.dart'; +import "package:cake_wallet/new-ui/pages/receive_page.dart"; import 'package:cake_wallet/new-ui/pages/send_page.dart'; +import 'package:cake_wallet/new-ui/widgets/hardware_wallet/sync_key_images_sheet.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'; @@ -50,6 +51,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/store/app_store.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'; @@ -125,7 +127,6 @@ import 'package:cake_wallet/src/screens/trade_details/trade_details_page.dart'; import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart'; -import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart'; import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart'; import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart'; @@ -145,7 +146,7 @@ import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; -import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; +import 'package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; @@ -162,6 +163,7 @@ 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:cake_wallet/zcash/zcash_network_type.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -468,6 +470,16 @@ Route createRoute(RouteSettings settings) { return handleRouteWithPlatformAwareness((context) => getIt.get(), settings: settings); + case Routes.newReceivePage: + if (FeatureFlag.hasNewUi) { + return handleRouteWithPlatformAwareness( + (context) => Material(child: getIt.get(param1: false, param2: null)), + settings: settings, + ); + } + return handleRouteWithPlatformAwareness((context) => getIt.get(), + settings: settings); + case Routes.transactionDetails: return CupertinoPageRoute( fullscreenDialog: true, @@ -822,6 +834,9 @@ Route createRoute(RouteSettings settings) { final isChildWallet = args['isChildWallet'] as bool? ?? false; final useTestnet = args['useTestnet'] as bool; final toggleTestnet = args['toggleTestnet'] as Function(bool? val); + final zcashNetwork = args['zcashNetwork'] as int? ?? ZcashNetworkType.mainnet; + final setZcashNetwork = + args['setZcashNetwork'] as void Function(int network)? ?? (_) {}; final restoredWallet = args['restoredWallet'] as RestoredWallet?; final viewModelParam = {'type': type, 'isPow': false}; @@ -832,6 +847,8 @@ Route createRoute(RouteSettings settings) { isChildWallet: isChildWallet, useTestnet: useTestnet, toggleUseTestnet: toggleTestnet, + zcashNetwork: zcashNetwork, + setZcashNetwork: setZcashNetwork, advancedPrivacySettingsViewModel: getIt.get(param1: type), nodeViewModel: getIt.get(param1: viewModelParam), @@ -891,6 +908,19 @@ Route createRoute(RouteSettings settings) { case Routes.setup2faInfoPage: return MaterialPageRoute(builder: (_) => getIt.get()); + case Routes.syncKeyImagesDevices: + final hardwareWalletType = getIt().wallet!.hardwareWalletType; + if (hardwareWalletType == HardwareWalletType.trezor) { + return MaterialPageRoute( + builder: (_) => SyncKeyImagesSheet( + appStore: getIt(), + trezorConnectVM: getIt(), + ), + ); + } + return MaterialPageRoute( + builder: (_) => getIt.get(param1: settings.arguments)); + case Routes.urqrAnimatedPage: return MaterialPageRoute( builder: (_) => getIt.get(param1: settings.arguments)); diff --git a/lib/routes.dart b/lib/routes.dart index 96ac8264d..b21e2bc38 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -71,6 +71,7 @@ class Routes { static const unspentCoinsList = '/unspent_coins_list'; static const unspentCoinsDetails = '/unspent_coins_details'; static const addressPage = '/address_page'; + static const newReceivePage = '/new_receive_page'; static const fullscreenQR = '/fullscreen_qr'; static const cakePayWelcomePage = '/cake_pay_welcome_page'; static const cakePayLoginPage = '/cake_pay_login_page'; @@ -132,6 +133,7 @@ class Routes { static const signPage = '/sign_page'; static const connectDevices = '/device/connect'; + static const syncKeyImagesDevices = '/device/sync_key_images'; static const urqrAnimatedPage = '/urqr/animated_page'; static const walletGroupsDisplayPage = '/wallet_groups_display_page'; static const walletGroupDescription = '/wallet_group_description'; diff --git a/lib/solana/cw_solana.dart b/lib/solana/cw_solana.dart index d552ed11c..2aeb722a3 100644 --- a/lib/solana/cw_solana.dart +++ b/lib/solana/cw_solana.dart @@ -116,6 +116,12 @@ class CWSolana extends Solana { return await solanaWallet.getSPLToken(mintAddress); } + @override + Future isTokenVerifiedOnJupiter(WalletBase wallet, String mintAddress) { + final solanaWallet = wallet as SolanaWallet; + return solanaWallet.isTokenVerifiedOnJupiter(mintAddress); + } + @override CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { if (transaction.amount.currency.symbol == CryptoCurrency.sol.symbol) { diff --git a/lib/src/screens/auth/auth_page.dart b/lib/src/screens/auth/auth_page.dart index 4cee26565..df1b72b39 100644 --- a/lib/src/screens/auth/auth_page.dart +++ b/lib/src/screens/auth/auth_page.dart @@ -141,6 +141,7 @@ class AuthPagePinCodeStateImpl extends AuthPageState { size: 36, icon: Icon(Icons.close), onPressed: Navigator.of(context).pop, + semanticLabel: S.of(context).close, iconColor: Theme.of(context).colorScheme.onSurfaceVariant) : Container(), backgroundColor: Theme.of(context).colorScheme.surface, diff --git a/lib/src/screens/backup/edit_backup_password_page.dart b/lib/src/screens/backup/edit_backup_password_page.dart index d3ac5e740..4abb460ef 100644 --- a/lib/src/screens/backup/edit_backup_password_page.dart +++ b/lib/src/screens/backup/edit_backup_password_page.dart @@ -36,6 +36,7 @@ class _EditBackupPasswordPageState extends State { ModalTopBar( title: S.of(context).edit_backup_password, leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: Navigator.of(context).pop, ), Expanded( diff --git a/lib/src/screens/base_page.dart b/lib/src/screens/base_page.dart index 4602c92b6..ef119faa2 100644 --- a/lib/src/screens/base_page.dart +++ b/lib/src/screens/base_page.dart @@ -84,20 +84,16 @@ abstract class BasePage extends StatelessWidget { return null; } - return MergeSemantics( - child: Padding( - padding: const EdgeInsets.only(top: 4.0), - child: ButtonTheme( - minWidth: double.minPositive, - child: Semantics( - label: S.of(context).seed_alert_back, - child: ModernButton( - size: 37, - icon: Icon(CupertinoIcons.back), - iconColor: Theme.of(context).colorScheme.onSurfaceVariant, - onPressed: () => onClose(context), - ), - ), + return Padding( + padding: const EdgeInsets.only(top: 4.0), + child: ButtonTheme( + minWidth: double.minPositive, + child: ModernButton( + size: 37, + icon: Icon(CupertinoIcons.back), + semanticLabel: S.of(context).seed_alert_back, + iconColor: Theme.of(context).colorScheme.onSurfaceVariant, + onPressed: () => onClose(context), ), ), ); diff --git a/lib/src/screens/connect_device/connect_device_page.dart b/lib/src/screens/connect_device/connect_device_page.dart index 870134dc7..123c0656c 100644 --- a/lib/src/screens/connect_device/connect_device_page.dart +++ b/lib/src/screens/connect_device/connect_device_page.dart @@ -42,7 +42,7 @@ class ConnectDevicePage extends BasePage { ConnectDevicePage(ConnectDevicePageParams params, this.hardwareWalletVM) : walletType = params.walletType, onConnectDevice = params.onConnectDevice, - allowChangeWallet = params.allowChangeWallet, + allowChangeWallet = params.allowChangeWallet || params.isReconnect, isReconnect = params.isReconnect; final WalletType walletType; final OnConnectDevice onConnectDevice; @@ -142,12 +142,22 @@ class ConnectDevicePageBodyState extends State { super.dispose(); } + var _isRefreshingUsb = false; Future _refreshUsbDevices() async { - final dev = await widget.hardwareWalletVM.getAllUsbDevices(); - - if (usbDevices.length != dev.length) { - setState(() => usbDevices = dev); + if (_isRefreshingUsb) { + return; } + _isRefreshingUsb = true; + try { + final dev = await widget.hardwareWalletVM.getAllUsbDevices(); + + if (usbDevices.length != dev.length) { + setState(() => usbDevices = dev); + } + } catch(e) { + printV(e); + } + _isRefreshingUsb = false; } Future _refreshBleDevices() async { diff --git a/lib/src/screens/connect_device/monero_hardware_wallet_options_page.dart b/lib/src/screens/connect_device/monero_hardware_wallet_options_page.dart index 7e77d4350..e6286a80d 100644 --- a/lib/src/screens/connect_device/monero_hardware_wallet_options_page.dart +++ b/lib/src/screens/connect_device/monero_hardware_wallet_options_page.dart @@ -1,19 +1,22 @@ -import 'package:cake_wallet/core/wallet_name_validator.dart'; -import 'package:cw_core/generate_name.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; -import 'package:cake_wallet/src/widgets/blockchain_height_widget.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; -import 'package:cake_wallet/utils/responsive_layout_util.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:mobx/mobx.dart'; +import "dart:async"; + +import "package:cake_wallet/core/wallet_name_validator.dart"; +import "package:cake_wallet/generated/i18n.dart"; +import "package:cake_wallet/src/screens/base_page.dart"; +import "package:cake_wallet/src/screens/connect_device/monero_hardware_wallet_passphrase_input.dart"; +import "package:cake_wallet/src/widgets/alert_with_one_action.dart"; +import "package:cake_wallet/src/widgets/base_text_form_field.dart"; +import "package:cake_wallet/src/widgets/blockchain_height_widget.dart"; +import "package:cake_wallet/src/widgets/primary_button.dart"; +import "package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart"; +import "package:cake_wallet/utils/responsive_layout_util.dart"; +import "package:cake_wallet/utils/show_pop_up.dart"; +import "package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart"; +import "package:cw_core/generate_name.dart"; +import "package:cw_core/wallet_type.dart"; +import "package:flutter/material.dart"; +import "package:flutter_mobx/flutter_mobx.dart"; +import "package:mobx/mobx.dart"; class MoneroHardwareWalletOptionsPage extends BasePage { MoneroHardwareWalletOptionsPage(this._walletHardwareRestoreVM); @@ -42,13 +45,15 @@ class _MoneroHardwareWalletOptionsFormState extends State<_MoneroHardwareWalletO : _formKey = GlobalKey(), _blockchainHeightKey = GlobalKey(), _blockHeightFocusNode = FocusNode(), - _controller = TextEditingController(); + _walletNameController = TextEditingController(), + _passphraseController = TextEditingController(); final GlobalKey _formKey; final GlobalKey _blockchainHeightKey; final FocusNode _blockHeightFocusNode; final WalletHardwareRestoreViewModel _walletHardwareRestoreVM; - final TextEditingController _controller; + final TextEditingController _walletNameController; + final TextEditingController _passphraseController; @override void initState() { @@ -57,131 +62,159 @@ class _MoneroHardwareWalletOptionsFormState extends State<_MoneroHardwareWalletO } @override - Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.only(top: 24), - child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), - content: Center( - child: ConstrainedBox( - constraints: - BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: EdgeInsets.only(top: 0), - child: Form( - key: _formKey, - child: Stack( - alignment: Alignment.centerRight, - children: [ - BaseTextFormField( - onChanged: (value) => _walletHardwareRestoreVM.name = value, - controller: _controller, - textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 20.0, - fontWeight: FontWeight.w600, - ), - placeholderTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 18.0, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - hintText: S.of(context).wallet_name, - suffixIcon: Semantics( - label: S.of(context).generate_name, - child: IconButton( - onPressed: _onGenerateName, - icon: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6.0), - color: Theme.of(context).colorScheme.surfaceContainerHighest, + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.only(top: 24), + child: ScrollableWithBottomSection( + contentPadding: const EdgeInsets.only(left: 24, right: 24, bottom: 24), + content: Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.zero, + child: Form( + key: _formKey, + child: Stack( + alignment: Alignment.centerRight, + children: [ + BaseTextFormField( + onChanged: (value) => _walletHardwareRestoreVM.name = value, + controller: _walletNameController, + textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 20, + fontWeight: FontWeight.w600, ), - width: 34, - height: 34, - child: Image.asset( - 'assets/images/refresh_icon.png', - color: Theme.of(context).colorScheme.primary, + placeholderTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + hintText: S.of(context).wallet_name, + suffixIcon: Semantics( + label: S.of(context).generate_name, + child: IconButton( + onPressed: _onGenerateName, + icon: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: Theme.of(context).colorScheme.surfaceContainerHighest, + ), + width: 34, + height: 34, + child: Image.asset( + "assets/images/refresh_icon.png", + color: Theme.of(context).colorScheme.primary, + ), ), ), ), + validator: WalletNameValidator(), ), - validator: WalletNameValidator(), - ), - ], + ], + ), ), ), - ), - Padding( - padding: EdgeInsets.only(top: 20), - child: BlockchainHeightWidget( - focusNode: _blockHeightFocusNode, - key: _blockchainHeightKey, - hasDatePicker: true, - walletType: WalletType.monero, + Padding( + padding: const EdgeInsets.only(top: 20), + child: BlockchainHeightWidget( + focusNode: _blockHeightFocusNode, + key: _blockchainHeightKey, + hasDatePicker: true, + walletType: WalletType.monero, + ), ), + ], + ), + ), + ), + bottomSectionPadding: const EdgeInsets.all(24), + bottomSection: Observer( + builder: (_) => Column( + children: [ + if (_walletHardwareRestoreVM.passphraseAvailable) ...[ + TextButton( + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => MoneroHardwareWalletPassphraseInputModal( + controller: _passphraseController, + ), + ); + }, + child: Text(S.of(context).add_a_passphrase), + ), + ], + LoadingPrimaryButton( + onPressed: _confirmForm, + text: S.of(context).seed_language_next, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + isDisabled: _walletHardwareRestoreVM.name.isEmpty, ), ], ), ), ), - bottomSectionPadding: EdgeInsets.all(24), - bottomSection: Observer( - builder: (context) => LoadingPrimaryButton( - onPressed: _confirmForm, - text: S.of(context).seed_language_next, - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimary, - isDisabled: _walletHardwareRestoreVM.name.isEmpty, - ), - ), - ), - ); - } + ); Future _onGenerateName() async { final rName = await generateName(); FocusManager.instance.primaryFocus?.unfocus(); setState(() { - _controller.text = rName; + _walletNameController.text = rName; _walletHardwareRestoreVM.name = rName; - _controller.selection = - TextSelection.fromPosition(TextPosition(offset: _controller.text.length)); + _walletNameController.selection = + TextSelection.fromPosition(TextPosition(offset: _walletNameController.text.length)); }); } Future _confirmForm() async { - showPopUp( - context: context, - builder: (BuildContext context) => AlertWithOneAction( - alertTitle: S.of(context).proceed_on_device, - alertContent: S.of(context).proceed_on_device_description, - buttonText: S.of(context).cancel, - alertBarrierDismissible: false, - buttonAction: () => Navigator.of(context).pop(), + unawaited( + showPopUp( + context: context, + builder: (context) => AlertWithOneAction( + alertTitle: S.of(context).proceed_on_device, + alertContent: S.of(context).proceed_on_device_description, + buttonText: S.of(context).cancel, + alertBarrierDismissible: false, + buttonAction: () => Navigator.of(context).pop(), + ), ), ); - final options = {'height': _blockchainHeightKey.currentState?.height ?? -1}; + final options = {"height": _blockchainHeightKey.currentState?.height ?? -1}; + + if (_walletHardwareRestoreVM.passphraseAvailable && _passphraseController.text.isNotEmpty) { + options["passphrase"] = _passphraseController.text; + } await _walletHardwareRestoreVM.create(options: options); } bool _effectsInstalled = false; void _setEffects(BuildContext context) { - if (_effectsInstalled) return; + if (_effectsInstalled) { + return; + } - reaction((_) => _walletHardwareRestoreVM.error, (String? error) { + reaction((_) => _walletHardwareRestoreVM.error, (error) { if (error != null) { - if (error == S.current.ledger_connection_error) Navigator.of(context).pop(); + if (error == S.current.ledger_connection_error) { + Navigator.of(context).pop(); + } WidgetsBinding.instance.addPostFrameCallback((_) { showPopUp( context: context, - builder: (BuildContext context) => AlertWithOneAction( + builder: (context) => AlertWithOneAction( alertTitle: S.of(context).error, alertContent: error, buttonText: S.of(context).ok, diff --git a/lib/src/screens/connect_device/monero_hardware_wallet_passphrase_input.dart b/lib/src/screens/connect_device/monero_hardware_wallet_passphrase_input.dart new file mode 100644 index 000000000..f893b532b --- /dev/null +++ b/lib/src/screens/connect_device/monero_hardware_wallet_passphrase_input.dart @@ -0,0 +1,57 @@ +import "package:cake_wallet/generated/i18n.dart"; +import "package:cake_wallet/new-ui/widgets/new_primary_button.dart"; +import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart"; +import "package:cake_wallet/src/widgets/base_text_form_field.dart"; +import "package:flutter/material.dart"; + +class MoneroHardwareWalletPassphraseInputModal extends StatelessWidget { + const MoneroHardwareWalletPassphraseInputModal({required this.controller, super.key}); + + final TextEditingController controller; + + @override + Widget build(BuildContext context) => Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.vertical(top: Radius.circular(18)), + color: Theme.of(context).colorScheme.surface, + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ModalTopBar( + title: S.of(context).passphrase_entry, + leadingIcon: const Icon(Icons.close), + leadingSemanticLabel: S.of(context).close, + onLeadingPressed: Navigator.of(context).pop, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18), + child: Column( + spacing: 12, + children: [ + BaseTextFormField( + controller: controller, + hintText: S.of(context).passphrase_raw, + autocorrect: false, + enableSuggestions: false, + ), + const SizedBox(), + NewPrimaryButton( + onPressed: Navigator.of(context).pop, + text: S.of(context).restore_next, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + ), + const SizedBox(), + ], + ), + ), + ], + ), + ), + ), + ); +} diff --git a/lib/src/screens/dashboard/edit_token_page.dart b/lib/src/screens/dashboard/edit_token_page.dart index 97e43411f..9becae8cd 100644 --- a/lib/src/screens/dashboard/edit_token_page.dart +++ b/lib/src/screens/dashboard/edit_token_page.dart @@ -40,13 +40,11 @@ class EditTokenPage extends BasePage { : S.current.add_token; @override - Widget body(BuildContext context) { - return EditTokenPageBody( + Widget body(BuildContext context) => EditTokenPageBody( homeSettingsViewModel: homeSettingsViewModel, token: token, initialContractAddress: initialContractAddress, ); - } } class EditTokenPageBody extends StatefulWidget { @@ -83,6 +81,11 @@ class _EditTokenPageBodyState extends State { bool _disclaimerChecked = false; bool isEditingToken = false; bool _isTokenVerified = false; + bool _isJupiterVerified = false; + String? _jupiterVerifiedAddress; + + bool get _isJupiterVerifiedForCurrentAddress => + _isJupiterVerified && _jupiterVerifiedAddress == _contractAddressController.text; @override void initState() { @@ -129,14 +132,28 @@ class _EditTokenPageBodyState extends State { final isVerified = widget.homeSettingsViewModel.checkIfTokenIsWhitelisted(contractAddress); if (!mounted) return; + setState(() { _isTokenVerified = isVerified; }); + + _checkIfTokenIsVerifiedOnJupiter(contractAddress); + } + + Future _checkIfTokenIsVerifiedOnJupiter(String contractAddress) async { + final isVerified = + await widget.homeSettingsViewModel.checkIfTokenIsVerifiedOnJupiter(contractAddress); + + if (!mounted || _contractAddressController.text != contractAddress) return; + + setState(() { + _isJupiterVerified = isVerified ?? false; + _jupiterVerifiedAddress = contractAddress; + }); } @override - Widget build(BuildContext context) { - return GestureDetector( + Widget build(BuildContext context) => GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: ScrollableWithBottomSection( contentPadding: EdgeInsets.zero, @@ -174,8 +191,7 @@ class _EditTokenPageBodyState extends State { SizedBox(height: 20), ], Observer( - builder: (context) { - return Row( + builder: (context) => Row( children: [ Expanded( child: LoadingPrimaryButton( @@ -210,14 +226,12 @@ class _EditTokenPageBodyState extends State { if (isTokenAlreadyAdded) { showPopUp( context: context, - builder: (dialogContext) { - return AlertWithOneAction( + builder: (dialogContext) => AlertWithOneAction( alertTitle: S.current.warning, alertContent: S.of(context).token_already_exists, buttonText: S.of(context).ok, buttonAction: () => Navigator.of(dialogContext).pop(), - ); - }, + ), ); return; } @@ -225,13 +239,15 @@ class _EditTokenPageBodyState extends State { final isWhitelisted = await widget.homeSettingsViewModel .checkIfTokenIsWhitelisted(_contractAddressController.text); - final hasPotentialError = !isWhitelisted && + final isTrusted = isWhitelisted || _isJupiterVerifiedForCurrentAddress; + + final hasPotentialError = !isTrusted && await widget.homeSettingsViewModel .checkIfERC20TokenContractAddressIsAPotentialScamAddress( _contractAddressController.text, ); - bool isPotentialScam = hasPotentialError && !isWhitelisted; + bool isPotentialScam = hasPotentialError && !isTrusted; // Normalize to catch homoglyph spoofing attacks final tokenSymbol = normalizeHomoglyphs( @@ -243,7 +259,7 @@ class _EditTokenPageBodyState extends State { // (e.g. fake ETH on Ethereum, fake SOL on Solana) final nativeSymbol = widget.homeSettingsViewModel.nativeToken.title.toUpperCase(); - if (tokenSymbol == nativeSymbol && !isWhitelisted) { + if (tokenSymbol == nativeSymbol && !isTrusted) { isPotentialScam = true; } @@ -252,7 +268,7 @@ class _EditTokenPageBodyState extends State { if (widget.homeSettingsViewModel.checkIfTokenSymbolMatchesDefaultToken( tokenSymbol, ) && - !isWhitelisted) { + !isTrusted) { isPotentialScam = true; } @@ -277,23 +293,20 @@ class _EditTokenPageBodyState extends State { } catch (e) { showPopUp( context: context, - builder: (dialogContext) { - return AlertWithOneAction( + builder: (dialogContext) => AlertWithOneAction( alertTitle: S.current.warning, alertContent: e.toString(), buttonText: S.of(context).ok, buttonAction: () => Navigator.of(dialogContext).pop(), - ); - }, + ), ); } }; - if (hasPotentialError && !isWhitelisted) { + if (hasPotentialError && !isTrusted) { showPopUp( context: context, - builder: (dialogContext) { - return AlertWithTwoActions( + builder: (dialogContext) => AlertWithTwoActions( alertTitle: S.current.warning, alertContent: S.current.contract_warning, rightButtonText: S.of(context).continue_text, @@ -303,8 +316,7 @@ class _EditTokenPageBodyState extends State { await actionCall(); }, actionLeftButton: () => Navigator.of(dialogContext).pop(), - ); - }, + ), ); } else { try { @@ -312,14 +324,12 @@ class _EditTokenPageBodyState extends State { } catch (e) { showPopUp( context: context, - builder: (dialogContext) { - return AlertWithOneAction( + builder: (dialogContext) => AlertWithOneAction( alertTitle: "Unable to add token", alertContent: "$e", buttonText: S.of(context).ok, buttonAction: () => Navigator.of(context).pop(), - ); - }, + ), ); } if (mounted) { @@ -334,14 +344,12 @@ class _EditTokenPageBodyState extends State { ), ), ], - ); - }, + ), ), ], ), ), ); - } void _getTokenInfo() async { if (_contractAddressController.text.isNotEmpty) { @@ -377,8 +385,7 @@ class _EditTokenPageBodyState extends State { } } - Widget _tokenForm() { - return Form( + Widget _tokenForm() => Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, @@ -402,7 +409,7 @@ class _EditTokenPageBodyState extends State { size: 75, ), ), - if (_isTokenVerified) + if (_isTokenVerified || _isJupiterVerifiedForCurrentAddress) Positioned( bottom: 0, right: 0, @@ -425,6 +432,16 @@ class _EditTokenPageBodyState extends State { ), ], ), + if (_isJupiterVerifiedForCurrentAddress) ...[ + const SizedBox(height: 12), + Text( + S.of(context).token_verified_on_jupiter, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], SizedBox(height: 25), AddressTextField( controller: _contractAddressController, @@ -517,5 +534,4 @@ class _EditTokenPageBodyState extends State { ], ), ); - } } diff --git a/lib/src/screens/dashboard/favorite_token_modal.dart b/lib/src/screens/dashboard/favorite_token_modal.dart index ef461140a..1b0c9ac13 100644 --- a/lib/src/screens/dashboard/favorite_token_modal.dart +++ b/lib/src/screens/dashboard/favorite_token_modal.dart @@ -22,6 +22,7 @@ class FavoriteTokenModal extends StatelessWidget { ModalTopBar( title: S.of(context).favorite_token, leadingIcon: Icon(Icons.close), + leadingSemanticLabel: S.of(context).close, onLeadingPressed: Navigator.of(context).pop, ), Expanded( diff --git a/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart b/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart index 7c4a6ff46..932a220bd 100644 --- a/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart +++ b/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart @@ -334,37 +334,6 @@ class CryptoBalanceWidget extends StatelessWidget { ), ), ], - if (dashboardViewModel.showZcashMissingFundsCard) ...[ - SizedBox(height: 10), - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), - child: InfoCard( - title: S.of(context).zcash_card_missing_funds, - description: S.of(context).zcash_card_description, - leftButtonTitle: S.of(context).zcash_card_dismiss, - rightButtonTitle: S.of(context).zcash_card_scan, - image: 'assets/new-ui/crypto_full_icons/zcash.svg', - leftButtonAction: () => _dismissZcash(context), - rightButtonAction: () => _rescanInternalChangeZcash(context), - hintWidget: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => launchUrl( - Uri.parse( - "https://docs.cakewallet.com/tutorials/zashi/#creating-a-new-zcash-wallet-on-cake"), - mode: LaunchMode.externalApplication, - ), - child: Text( - S.of(context).learn_more, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - height: 1, - ), - softWrap: true, - ), - ), - ), - ), - ], if (dashboardViewModel.showDecredInfoCard) ...[ SizedBox(height: 10), Padding( @@ -494,21 +463,6 @@ class CryptoBalanceWidget extends StatelessWidget { dashboardViewModel.setMwebEnabled(); } - Future _rescanInternalChangeZcash(BuildContext context) async { - dashboardViewModel.rescanInternalChangeZcash(); - await showPopUp( - context: context, - builder: (BuildContext context) => AlertWithOneAction( - alertTitle: S.of(context).alert_notice, - alertContent: S.of(context).zcash_card_warning, - buttonText: S.of(context).understand, - buttonAction: () { - Navigator.of(context).pop(); - }, - ), - ); - } - Future _dismissMweb(BuildContext context) async { await showPopUp( context: context, @@ -522,18 +476,4 @@ class CryptoBalanceWidget extends StatelessWidget { )); dashboardViewModel.dismissMweb(); } - - Future _dismissZcash(BuildContext context) async { - await showPopUp( - context: context, - builder: (BuildContext context) => AlertWithOneAction( - alertTitle: S.of(context).alert_notice, - alertContent: S.of(context).zcash_card_enable_later, - buttonText: S.of(context).understand, - buttonAction: () { - Navigator.of(context).pop(); - }, - )); - dashboardViewModel.dismissZcash(); - } } diff --git a/lib/src/screens/dashboard/pages/transactions_page.dart b/lib/src/screens/dashboard/pages/transactions_page.dart index 9e623c044..6af5f75b0 100644 --- a/lib/src/screens/dashboard/pages/transactions_page.dart +++ b/lib/src/screens/dashboard/pages/transactions_page.dart @@ -111,7 +111,7 @@ class TransactionsPage extends StatelessWidget { key: item.key, onTap: () => Navigator.of(context) .pushNamed(Routes.transactionDetails, arguments: transaction), - isShield: transaction.additionalInfo['autoShield'] == true, + isShield: transaction.additionalInfo['isAutoShield'] == true, direction: transaction.direction, formattedDate: DateFormat('HH:mm').format(transaction.date), formattedAmount: item.formattedCryptoAmount, diff --git a/lib/src/screens/dashboard/sign_page.dart b/lib/src/screens/dashboard/sign_page.dart index 5e805c30c..4a6f70b9b 100644 --- a/lib/src/screens/dashboard/sign_page.dart +++ b/lib/src/screens/dashboard/sign_page.dart @@ -59,6 +59,7 @@ class _SignPageState extends State { ModalTopBar( title: S.current.sign_verify_title, leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.current.seed_alert_back, onLeadingPressed: Navigator.of(context).pop, ), Expanded( 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 5e2f636f0..45883ae68 100644 --- a/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart +++ b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart @@ -187,50 +187,56 @@ class _NEWNewMainNavBarState extends State { ? iconHorizontalPadding / 100 : 0), curve: Curves.easeOutCubic, - child: InkWell( - splashFactory: NoSplash.splashFactory, - splashColor: Colors.transparent, - borderRadius: BorderRadius.circular(pillBorderRadius), - onTap: () => _onItemTap(i), - child: AnimatedContainer( - duration: - _firstFrame ? Duration.zero : inactiveIconMoveDuration, - curve: Curves.easeOutCubic, - width: i == widget.selectedIndex ? pillWidth : iconBoxWidth, - alignment: Alignment.center, - child: AnimatedAlign( - duration: inactiveIconFadeDuration, + child: Semantics( + button: true, + selected: i == widget.selectedIndex, + inMutuallyExclusiveGroup: true, + label: visibleActions[i].name(context), + child: InkWell( + splashFactory: NoSplash.splashFactory, + splashColor: Colors.transparent, + borderRadius: BorderRadius.circular(pillBorderRadius), + onTap: () => _onItemTap(i), + child: AnimatedContainer( + duration: + _firstFrame ? Duration.zero : inactiveIconMoveDuration, curve: Curves.easeOutCubic, + width: i == widget.selectedIndex ? pillWidth : iconBoxWidth, alignment: Alignment.center, - child: AnimatedScale( - duration: inactiveIconAppearDuration, + child: AnimatedAlign( + duration: inactiveIconFadeDuration, curve: Curves.easeOutCubic, - 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 Container( - height: NewMainNavBar.barHeight, - child: CakeImageWidget( - imageUrl: visibleActions[i].image, - width: iconWidth, - height: iconHeight, - //fit: BoxFit.scaleDown, - colorFilter: ColorFilter.mode( - value ?? inactiveColor, - BlendMode.srcIn, + alignment: Alignment.center, + child: AnimatedScale( + duration: inactiveIconAppearDuration, + curve: Curves.easeOutCubic, + 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 Container( + height: NewMainNavBar.barHeight, + child: CakeImageWidget( + imageUrl: visibleActions[i].image, + width: iconWidth, + height: iconHeight, + //fit: BoxFit.scaleDown, + colorFilter: ColorFilter.mode( + value ?? inactiveColor, + BlendMode.srcIn, + ), ), - ), - ); - }), + ); + }), + ), ), ), ), @@ -306,11 +312,15 @@ class AnimatedPill extends StatelessWidget { children: [ Padding( padding: EdgeInsets.only(left: pillIconWidth + 2), - child: Text( - currentAction.name(context), - style: pillTextStyle.copyWith(color: contentColor), - overflow: TextOverflow.fade, - softWrap: false, + // The selected tab's InkWell already announces this name, so the + // pill text must not become a second stop for screen readers. + child: ExcludeSemantics( + child: Text( + currentAction.name(context), + style: pillTextStyle.copyWith(color: contentColor), + overflow: TextOverflow.fade, + softWrap: false, + ), ), ), ], diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index a0efa4733..713d316d7 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -659,8 +659,8 @@ class ExchangePage extends BasePage { final parsedAddresses = await adrResService.resolve( query: domain, wallet: exchangeViewModel.wallet, currency: currency); return parsedAddresses.isNotEmpty - ? parsedAddresses.first.parsedAddressByCurrencyMap[currency] ?? '' - : ''; + ? (parsedAddresses.first.parsedAddressByCurrencyMap[currency] ?? domain) + : domain; } void _showFeeAlert(BuildContext context) async { diff --git a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart index 383e0d161..2f190c857 100644 --- a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart +++ b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart @@ -14,11 +14,13 @@ import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/choices_list_item.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:cake_wallet/zcash/zcash_network_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -28,6 +30,8 @@ class AdvancedPrivacySettingsPage extends BasePage { required this.isChildWallet, required this.useTestnet, required this.toggleUseTestnet, + required this.zcashNetwork, + required this.setZcashNetwork, required this.advancedPrivacySettingsViewModel, required this.nodeViewModel, required this.seedSettingsViewModel, @@ -44,6 +48,8 @@ class AdvancedPrivacySettingsPage extends BasePage { final bool isChildWallet; final bool useTestnet; final Function(bool? val) toggleUseTestnet; + final int zcashNetwork; + final void Function(int network) setZcashNetwork; @override Widget body(BuildContext context) => _AdvancedPrivacySettingsBody( @@ -51,6 +57,8 @@ class AdvancedPrivacySettingsPage extends BasePage { isChildWallet, useTestnet, toggleUseTestnet, + zcashNetwork, + setZcashNetwork, advancedPrivacySettingsViewModel, nodeViewModel, seedSettingsViewModel, @@ -63,6 +71,8 @@ class _AdvancedPrivacySettingsBody extends StatefulWidget { this.isChildWallet, this.useTestnet, this.toggleUseTestnet, + this.zcashNetwork, + this.setZcashNetwork, this.privacySettingsViewModel, this.nodeViewModel, this.seedTypeViewModel, @@ -76,6 +86,8 @@ class _AdvancedPrivacySettingsBody extends StatefulWidget { final bool isChildWallet; final bool useTestnet; final Function(bool? val) toggleUseTestnet; + final int zcashNetwork; + final void Function(int network) setZcashNetwork; @override _AdvancedPrivacySettingsBodyState createState() => _AdvancedPrivacySettingsBodyState(); @@ -87,6 +99,7 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo final _formKey = GlobalKey(); final _passphraseFormKey = GlobalKey(); bool? testnetValue; + int? zcashNetworkValue; bool obscurePassphrase = true; @@ -112,6 +125,7 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo if (testnetValue == null && widget.useTestnet) { testnetValue = widget.useTestnet; } + zcashNetworkValue ??= widget.zcashNetwork; return Container( padding: EdgeInsets.only(top: 24), @@ -281,6 +295,20 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo ], ); }), + if (FeatureFlag.hasDevOptions && + widget.privacySettingsViewModel.type == WalletType.zcash) + SettingsChoicesCell( + ChoicesListItem( + title: 'Zcash network', + items: ZcashNetworkType.values, + selectedItem: zcashNetworkValue ?? ZcashNetworkType.mainnet, + displayItem: ZcashNetworkType.label, + onItemSelected: (final network) { + setState(() => zcashNetworkValue = network); + widget.setZcashNetwork(network); + }, + ), + ), if (widget.privacySettingsViewModel.type == WalletType.bitcoin || widget.privacySettingsViewModel.type == WalletType.decred) Builder(builder: (_) { @@ -332,11 +360,9 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo widget.nodeViewModel.save(); } - if (passphraseController.text.isNotEmpty) { - if (_passphraseFormKey.currentState != null && - !_passphraseFormKey.currentState!.validate()) { - return; - } + if (_passphraseFormKey.currentState != null && + !_passphraseFormKey.currentState!.validate()) { + return; } widget.seedTypeViewModel.setPassphrase(passphraseController.text); diff --git a/lib/src/screens/new_wallet/new_wallet_page.dart b/lib/src/screens/new_wallet/new_wallet_page.dart index 887652999..f6a4fb25a 100644 --- a/lib/src/screens/new_wallet/new_wallet_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_page.dart @@ -316,6 +316,8 @@ class _WalletNameFormState extends State { "type": _walletNewVM.type, "useTestnet": _walletNewVM.useTestnet, "toggleTestnet": _walletNewVM.toggleUseTestnet, + "zcashNetwork": _walletNewVM.zcashNetwork, + "setZcashNetwork": _walletNewVM.setZcashNetwork, "isChildWallet": widget.isChildWallet, }); }, diff --git a/lib/src/screens/nodes/node_create_or_edit_page.dart b/lib/src/screens/nodes/node_create_or_edit_page.dart index 1ac1d5ee1..78a76f8b5 100644 --- a/lib/src/screens/nodes/node_create_or_edit_page.dart +++ b/lib/src/screens/nodes/node_create_or_edit_page.dart @@ -93,8 +93,10 @@ class _NodeCreateOrEditPageState extends State { ModalTopBar( title: widget.editingNode != null ? S.current.edit_node : S.current.node_new, leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.current.seed_alert_back, onLeadingPressed: Navigator.of(context).pop, trailingIcon: Icon(Icons.check), + trailingSemanticLabel: S.current.save, onTrailingPressed: () async { if (_nodeFormKey.currentState != null && !_nodeFormKey.currentState!.validate()) { return; diff --git a/lib/src/screens/nodes/node_share_page.dart b/lib/src/screens/nodes/node_share_page.dart index bb96b5b0c..1258f12fb 100644 --- a/lib/src/screens/nodes/node_share_page.dart +++ b/lib/src/screens/nodes/node_share_page.dart @@ -24,6 +24,7 @@ class NodeSharePage extends StatelessWidget { ModalTopBar( title: S.of(context).share, leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: Navigator.of(context).pop, ), Expanded( diff --git a/lib/src/screens/nodes/pow_node_create_or_edit_page.dart b/lib/src/screens/nodes/pow_node_create_or_edit_page.dart index 0763fc8e2..0e9896094 100644 --- a/lib/src/screens/nodes/pow_node_create_or_edit_page.dart +++ b/lib/src/screens/nodes/pow_node_create_or_edit_page.dart @@ -75,6 +75,7 @@ class _PowNodeCreateOrEditPageState extends State { ModalTopBar( title: widget.editingNode != null ? S.current.edit_node : S.current.node_new, leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.current.seed_alert_back, onLeadingPressed: Navigator.of(context).pop, trailingIcon: CakeImageWidget( imageUrl: "assets/new-ui/scan.svg", @@ -83,6 +84,7 @@ class _PowNodeCreateOrEditPageState extends State { colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn), ), + trailingSemanticLabel: S.current.scan, onTrailingPressed: () => widget.nodeCreateOrEditViewModel.scanQRCodeForNewNode(context), ), diff --git a/lib/src/screens/receive/address_list_page.dart b/lib/src/screens/receive/address_list_page.dart index abcf9e1e1..f310a5546 100644 --- a/lib/src/screens/receive/address_list_page.dart +++ b/lib/src/screens/receive/address_list_page.dart @@ -22,6 +22,7 @@ class AddressListPage extends StatelessWidget { ModalTopBar( title: S.of(context).accounts_subaddresses, leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: Navigator.of(context).pop, ), Expanded( diff --git a/lib/src/screens/rescan/rescan_page.dart b/lib/src/screens/rescan/rescan_page.dart index e678e0185..8d7395068 100644 --- a/lib/src/screens/rescan/rescan_page.dart +++ b/lib/src/screens/rescan/rescan_page.dart @@ -114,6 +114,7 @@ class _RescanPageState extends State { ? S.current.silent_payments_scanning : S.current.rescan, leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.current.seed_alert_back, onLeadingPressed: Navigator.of(context).pop), Expanded( child: Padding( diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 2588fd35c..de2157bee 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -99,7 +99,7 @@ class WalletRestorePage extends BasePage { children: [ Observer( builder: (context) { - return walletRestoreViewModel.mode == WalletRestoreMode.seed + return walletRestoreViewModel.passphraseAvailable ? StandardCheckbox( captionColor: Theme.of(context).colorScheme.onSecondaryContainer, value: walletRestoreViewModel.hasPassphrase, @@ -121,7 +121,9 @@ class WalletRestorePage extends BasePage { 'isFromRestore': true, 'type': walletRestoreViewModel.type, 'useTestnet': walletRestoreViewModel.useTestnet, - 'toggleTestnet': walletRestoreViewModel.toggleUseTestnet + 'toggleTestnet': walletRestoreViewModel.toggleUseTestnet, + 'zcashNetwork': walletRestoreViewModel.zcashNetwork, + 'setZcashNetwork': walletRestoreViewModel.setZcashNetwork, }, ); }, diff --git a/lib/src/screens/settings/connection_sync_page.dart b/lib/src/screens/settings/connection_sync_page.dart index 6406f5854..fb251f55f 100644 --- a/lib/src/screens/settings/connection_sync_page.dart +++ b/lib/src/screens/settings/connection_sync_page.dart @@ -34,6 +34,7 @@ class ConnectionSyncPage extends BasePage { topBar: ModalTopBar( title: "", leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: () => Navigator.of(context).pop()), header: ModalHeader( iconPath: "assets/new-ui/settings_row_icons/connections.svg", diff --git a/lib/src/screens/settings/display_settings_page.dart b/lib/src/screens/settings/display_settings_page.dart index 5a04b86e2..392a62289 100644 --- a/lib/src/screens/settings/display_settings_page.dart +++ b/lib/src/screens/settings/display_settings_page.dart @@ -35,6 +35,7 @@ class DisplaySettingsPage extends StatelessWidget { topBar: ModalTopBar( title: S.of(context).display, leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: () => Navigator.of(context).pop(), ), content: Column( @@ -115,21 +116,12 @@ class DisplaySettingsPage extends StatelessWidget { onChanged: (val) { _displaySettingsViewModel.setDisableTradeOption(val); }), - if (_displaySettingsViewModel.showZcashCardSetting) - ListItemToggle( - keyValue: "display_settings_show_zcashcard", - label: S.of(context).show_zcash_card, - value: _displaySettingsViewModel.showZcashCard, - onChanged: (val) { - _displaySettingsViewModel.setShowZcashCard(val); - }), ListItemSelector( keyValue: "display_settings_sync_status_display", label: S.of(context).sync_status_display_mode, options: [_displaySettingsViewModel.syncStatusDisplayMode.title], onTap: () async { final items = SyncStatusDisplayMode.values.toList(); - final selectedAtIndex = items.indexOf(_displaySettingsViewModel.syncStatusDisplayMode); diff --git a/lib/src/screens/settings/manage_nodes_page.dart b/lib/src/screens/settings/manage_nodes_page.dart index c750397e0..f867ef2f8 100644 --- a/lib/src/screens/settings/manage_nodes_page.dart +++ b/lib/src/screens/settings/manage_nodes_page.dart @@ -40,6 +40,7 @@ class _ManageNodesPageState extends State { topBar: ModalTopBar( title: S.of(context).manage_nodes, leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: () => Navigator.of(context).pop(), trailingWidget: Row( spacing: 8, @@ -50,11 +51,13 @@ class _ManageNodesPageState extends State { icon: widget.nodeListViewModel.isTestingNodeSpeed ? CupertinoActivityIndicator() : Icon(Icons.refresh), + semanticLabel: S.of(context).test_node_speeds, onPressed: () => widget.nodeListViewModel.speedTestNodes()), ), ModernButton( size: 36, icon: Icon(Icons.add), + semanticLabel: S.of(context).add_new_node, onPressed: () async { final res = await Navigator.of(context) .pushNamed(widget.isPow ? Routes.newPowNode : Routes.newNode); diff --git a/lib/src/screens/settings/mweb_node_page.dart b/lib/src/screens/settings/mweb_node_page.dart index f5be67ee0..a05feed8d 100644 --- a/lib/src/screens/settings/mweb_node_page.dart +++ b/lib/src/screens/settings/mweb_node_page.dart @@ -34,6 +34,7 @@ class _MwebNodePageState extends State { topBar: ModalTopBar( title: S.current.litecoin_mweb_settings, onLeadingPressed: Navigator.of(context).pop, + leadingSemanticLabel: S.current.seed_alert_back, leadingIcon: Icon(Icons.arrow_back_ios_new)), content: Container( child: NewListSections(controllers: { diff --git a/lib/src/screens/settings/other_settings_page.dart b/lib/src/screens/settings/other_settings_page.dart index 7a08d6d86..3f5291984 100644 --- a/lib/src/screens/settings/other_settings_page.dart +++ b/lib/src/screens/settings/other_settings_page.dart @@ -42,6 +42,7 @@ class OtherSettingsPage extends BasePage { topBar: ModalTopBar( title: S.of(context).other, leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: () => Navigator.of(context).pop(), ), // header: ModalHeader( diff --git a/lib/src/screens/settings/privacy_page.dart b/lib/src/screens/settings/privacy_page.dart index 3d8b651a1..3e35f08c3 100644 --- a/lib/src/screens/settings/privacy_page.dart +++ b/lib/src/screens/settings/privacy_page.dart @@ -26,6 +26,7 @@ class PrivacyPage extends BasePage { topBar: ModalTopBar( title: "", leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: () => Navigator.of(context).pop(), ), header: ModalHeader( diff --git a/lib/src/screens/settings/security_backup_page.dart b/lib/src/screens/settings/security_backup_page.dart index fc8f5a1be..916189566 100644 --- a/lib/src/screens/settings/security_backup_page.dart +++ b/lib/src/screens/settings/security_backup_page.dart @@ -45,6 +45,7 @@ class SecurityBackupPage extends BasePage { topBar: ModalTopBar( title: "", leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.of(context).seed_alert_back, onLeadingPressed: () => Navigator.of(context).pop()), header: ModalHeader( iconPath: "assets/new-ui/settings_row_icons/security.svg", diff --git a/lib/src/screens/settings/silent_payments_settings.dart b/lib/src/screens/settings/silent_payments_settings.dart index 15184d5e5..4505a4e17 100644 --- a/lib/src/screens/settings/silent_payments_settings.dart +++ b/lib/src/screens/settings/silent_payments_settings.dart @@ -22,6 +22,7 @@ class SilentPaymentsSettingsPage extends StatelessWidget { ModalTopBar( title: S.current.silent_payments_settings, leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.current.seed_alert_back, onLeadingPressed: Navigator.of(context).pop, ), Expanded( diff --git a/lib/src/screens/setup_2fa/setup_2fa_enter_code_page.dart b/lib/src/screens/setup_2fa/setup_2fa_enter_code_page.dart index a9f992787..8a6df6f98 100644 --- a/lib/src/screens/setup_2fa/setup_2fa_enter_code_page.dart +++ b/lib/src/screens/setup_2fa/setup_2fa_enter_code_page.dart @@ -144,6 +144,7 @@ class _TOTPEnterCodeState extends State { ModalTopBar( title: widget.isForSetup ? S.current.setup_2fa : S.current.verify_with_2fa, leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.current.seed_alert_back, onLeadingPressed: Navigator.of(context).pop, ), Expanded( diff --git a/lib/src/screens/support_chat/support_chat_page.dart b/lib/src/screens/support_chat/support_chat_page.dart index d2a3cae76..88079add3 100644 --- a/lib/src/screens/support_chat/support_chat_page.dart +++ b/lib/src/screens/support_chat/support_chat_page.dart @@ -22,6 +22,7 @@ class SupportChatPage extends StatelessWidget { ModalTopBar( title: S.current.settings_support, leadingIcon: Icon(Icons.arrow_back_ios_new), + leadingSemanticLabel: S.current.seed_alert_back, onLeadingPressed: Navigator.of(context).pop), FutureBuilder( future: getCookie(), diff --git a/lib/src/screens/transaction_details/rbf_details_page.dart b/lib/src/screens/transaction_details/rbf_details_page.dart index 0ec4d5acc..78a5321de 100644 --- a/lib/src/screens/transaction_details/rbf_details_page.dart +++ b/lib/src/screens/transaction_details/rbf_details_page.dart @@ -1,34 +1,36 @@ -import 'package:cake_wallet/core/execution_state.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart'; -import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; -import 'package:cake_wallet/src/screens/transaction_details/textfield_list_item.dart'; -import 'package:cake_wallet/src/screens/transaction_details/transaction_expandable_list_item.dart'; -import 'package:cake_wallet/src/screens/transaction_details/widgets/textfield_list_row.dart'; -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; -import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart'; -import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart'; -import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; -import 'package:cake_wallet/src/widgets/list_row.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/standard_expandable_list.dart'; -import 'package:cake_wallet/src/widgets/standard_list.dart'; -import 'package:cake_wallet/src/widgets/standard_picker_list.dart'; -import 'package:cake_wallet/utils/show_bar.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; -import 'package:cake_wallet/view_model/transaction_details_view_model.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:mobx/mobx.dart'; +import "package:cake_wallet/core/execution_state.dart"; +import "package:cake_wallet/generated/i18n.dart"; +import "package:cake_wallet/src/screens/base_page.dart"; +import "package:cake_wallet/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart"; +import "package:cake_wallet/src/screens/transaction_details/standart_list_item.dart"; +import "package:cake_wallet/src/screens/transaction_details/textfield_list_item.dart"; +import "package:cake_wallet/src/screens/transaction_details/transaction_expandable_list_item.dart"; +import "package:cake_wallet/src/screens/transaction_details/widgets/textfield_list_row.dart"; +import "package:cake_wallet/src/widgets/alert_with_one_action.dart"; +import "package:cake_wallet/src/widgets/alert_with_two_actions.dart"; +import "package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart"; +import "package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart"; +import "package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart"; +import "package:cake_wallet/src/widgets/list_row.dart"; +import "package:cake_wallet/src/widgets/primary_button.dart"; +import "package:cake_wallet/src/widgets/standard_expandable_list.dart"; +import "package:cake_wallet/src/widgets/standard_list.dart"; +import "package:cake_wallet/src/widgets/standard_picker_list.dart"; +import "package:cake_wallet/utils/show_bar.dart"; +import "package:cake_wallet/utils/show_pop_up.dart"; +import "package:cake_wallet/view_model/send/send_view_model_state.dart"; +import "package:cake_wallet/view_model/transaction_details_view_model.dart"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:flutter_mobx/flutter_mobx.dart"; +import "package:mobx/mobx.dart"; class RBFDetailsPage extends BasePage { RBFDetailsPage({required this.transactionDetailsViewModel, required this.rawTransaction}) { transactionDetailsViewModel.addBumpFeesListItems( - transactionDetailsViewModel.transactionInfo, rawTransaction); + transactionDetailsViewModel.transactionInfo, + rawTransaction, + ); } @override @@ -46,70 +48,70 @@ class RBFDetailsPage extends BasePage { children: [ Expanded( child: SectionStandardList( - sectionCount: 1, - itemCounter: (int _) => transactionDetailsViewModel.RBFListItems.length, - itemBuilder: (__, index) { - final item = transactionDetailsViewModel.RBFListItems[index]; + sectionCount: 1, + itemCounter: (_) => transactionDetailsViewModel.rbfListItems.length, + itemBuilder: (__, index) { + final item = transactionDetailsViewModel.rbfListItems[index]; - if (item is StandartListItem) { - return GestureDetector( - onTap: () { - Clipboard.setData(ClipboardData(text: item.value)); - showBar(context, S.of(context).transaction_details_copied(item.title)); - }, - child: ListRow(title: '${item.title}:', value: item.value), - ); - } + if (item is StandartListItem) { + return GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: item.value)); + showBar(context, S.of(context).transaction_details_copied(item.title)); + }, + child: ListRow(title: "${item.title}:", value: item.value), + ); + } - if (item is StandardExpandableListItem) { - return StandardExpandableList( - title: '${item.title}: ${item.expandableItems.length}', - expandableItems: item.expandableItems, - ); - } + if (item is StandardExpandableListItem) { + return StandardExpandableList( + title: "${item.title}: ${item.expandableItems.length}", + expandableItems: item.expandableItems, + ); + } - if (item is StandardPickerListItem) { - return StandardPickerList( - title: item.title, - value: item.value, - items: item.items, - displayItem: item.displayItem, - onSliderChanged: item.onSliderChanged, - onItemSelected: item.onItemSelected, - selectedIdx: item.selectedIdx, - customItemIndex: item.customItemIndex, - customValue: item.customValue, - maxValue: item.maxValue, - ); - } + if (item is StandardPickerListItem) { + return StandardPickerList( + title: item.title, + value: item.value, + items: item.items, + displayItem: item.displayItem, + onSliderChanged: item.onSliderChanged, + onItemSelected: item.onItemSelected, + selectedIdx: item.selectedIdx, + customItemIndex: item.customItemIndex, + customValue: item.customValue, + maxValue: item.maxValue, + ); + } - if (item is TextFieldListItem) { - return TextFieldListRow( - title: item.title, - value: item.value, - onSubmitted: item.onSubmitted, - ); - } + if (item is TextFieldListItem) { + return TextFieldListRow( + title: item.title, + value: item.value, + onSubmitted: item.onSubmitted, + ); + } - return Container(); - }), + return Container(); + }, + ), ), Padding( - padding: const EdgeInsets.all(24), - child: Observer( - builder: (_) => LoadingPrimaryButton( - onPressed: () async { - transactionDetailsViewModel - .replaceByFee(transactionDetailsViewModel.newFee.toString()); - }, - text: S.of(context).send, - isLoading: - transactionDetailsViewModel.sendViewModel.state is IsExecutingState, - isDisabled: transactionDetailsViewModel.sendViewModel.state - is ExecutedSuccessfullyState, - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimary, - ))), + padding: const EdgeInsets.all(24), + child: Observer( + builder: (_) => LoadingPrimaryButton( + onPressed: () => transactionDetailsViewModel + .replaceByFee(transactionDetailsViewModel.newFee.toString()), + text: S.of(context).send, + isLoading: transactionDetailsViewModel.sendViewModel.state is IsExecutingState, + isDisabled: + transactionDetailsViewModel.sendViewModel.state is ExecutedSuccessfullyState, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + ), + ), + ), ], ); } @@ -121,7 +123,7 @@ class RBFDetailsPage extends BasePage { return; } - reaction((_) => transactionDetailsViewModel.sendViewModel.state, (ExecutionState state) { + reaction((_) => transactionDetailsViewModel.sendViewModel.state, (state) { if (state is! IsExecutingState && loadingBottomSheetContext != null && loadingBottomSheetContext!.mounted) { @@ -131,35 +133,35 @@ class RBFDetailsPage extends BasePage { if (state is FailureState) { WidgetsBinding.instance.addPostFrameCallback((_) { showPopUp( - context: context, - builder: (BuildContext popupContext) { - return AlertWithOneAction( - alertTitle: S.of(popupContext).error, - alertContent: state.error, - buttonText: S.of(popupContext).ok, - buttonAction: () => Navigator.of(popupContext).pop()); - }); + context: context, + builder: (popupContext) => AlertWithOneAction( + alertTitle: S.of(popupContext).error, + alertContent: state.error, + buttonText: S.of(popupContext).ok, + buttonAction: () => Navigator.of(popupContext).pop(), + ), + ); }); } if (state is AwaitingConfirmationState) { WidgetsBinding.instance.addPostFrameCallback((_) { showPopUp( - context: context, - builder: (BuildContext popupContext) { - return AlertWithTwoActions( - alertTitle: state.title ?? '', - alertContent: state.message ?? '', - rightButtonText: S.of(context).ok, - leftButtonText: S.of(context).cancel, - actionRightButton: () { - state.onConfirm?.call(); - Navigator.of(popupContext).pop(); - }, - actionLeftButton: () { - state.onCancel?.call(); - Navigator.of(popupContext).pop(); - }); - }); + context: context, + builder: (popupContext) => AlertWithTwoActions( + alertTitle: state.title ?? "", + alertContent: state.message ?? "", + rightButtonText: S.of(context).ok, + leftButtonText: S.of(context).cancel, + actionRightButton: () { + state.onConfirm?.call(); + Navigator.of(popupContext).pop(); + }, + actionLeftButton: () { + state.onCancel?.call(); + Navigator.of(popupContext).pop(); + }, + ), + ); }); } @@ -169,7 +171,7 @@ class RBFDetailsPage extends BasePage { showModalBottomSheet( context: context, isDismissible: false, - builder: (BuildContext context) { + builder: (context) { loadingBottomSheetContext = context; return LoadingBottomSheet( titleText: S.of(context).generating_transaction, @@ -187,42 +189,42 @@ class RBFDetailsPage extends BasePage { context: context, isDismissible: false, isScrollControlled: true, - builder: (BuildContext bottomSheetContext) { - return ConfirmSendingBottomSheet( - key: ValueKey('rbf_confirm_sending_bottom_sheet'), - titleText: S.of(bottomSheetContext).confirm_transaction, - isSlideActionEnabled: transactionDetailsViewModel.sendViewModel.isReadyForSend, - walletType: transactionDetailsViewModel.sendViewModel.walletType, - titleIconPath: - transactionDetailsViewModel.sendViewModel.selectedCryptoCurrency.iconPath, - currency: transactionDetailsViewModel.sendViewModel.selectedCryptoCurrency, - amount: S.of(bottomSheetContext).send_amount, - amountValue: - transactionDetailsViewModel.sendViewModel.pendingTransaction!.amountFormatted, - fiatAmountValue: transactionDetailsViewModel - .sendViewModel.pendingTransactionFiatAmountFormatted, - fee: S.of(bottomSheetContext).send_fee, - feeValue: - transactionDetailsViewModel.sendViewModel.pendingTransaction!.feeFormatted, - feeFiatAmount: transactionDetailsViewModel - .sendViewModel.pendingTransactionFeeFiatAmountFormatted, - outputs: transactionDetailsViewModel.sendViewModel.outputs, - footerType: FooterType.slideActionButton, - accessibleNavigationModeSlideActionButtonText: S.of(context).send, - onSlideActionComplete: () async { - Navigator.of(bottomSheetContext).pop(); - await transactionDetailsViewModel.sendViewModel.commitTransaction(context); - try { + builder: (bottomSheetContext) => ConfirmSendingBottomSheet( + key: const ValueKey("rbf_confirm_sending_bottom_sheet"), + titleText: S.of(bottomSheetContext).confirm_transaction, + isSlideActionEnabled: transactionDetailsViewModel.sendViewModel.isReadyForSend, + walletType: transactionDetailsViewModel.sendViewModel.walletType, + titleIconPath: + transactionDetailsViewModel.sendViewModel.selectedCryptoCurrency.iconPath, + currency: transactionDetailsViewModel.sendViewModel.selectedCryptoCurrency, + amount: S.of(bottomSheetContext).send_amount, + amountValue: + transactionDetailsViewModel.sendViewModel.pendingTransaction!.amountFormatted, + fiatAmountValue: + transactionDetailsViewModel.sendViewModel.pendingTransactionFiatAmountFormatted, + fee: S.of(bottomSheetContext).send_fee, + feeValue: + transactionDetailsViewModel.sendViewModel.pendingTransaction!.feeFormatted, + feeFiatAmount: transactionDetailsViewModel + .sendViewModel.pendingTransactionFeeFiatAmountFormatted, + outputs: transactionDetailsViewModel.sendViewModel.outputs, + footerType: FooterType.slideActionButton, + accessibleNavigationModeSlideActionButtonText: S.of(context).send, + onSlideActionComplete: () async { + Navigator.of(bottomSheetContext).pop(); + await transactionDetailsViewModel.sendViewModel.commitTransaction(context); + try { + if (bottomSheetContext.mounted) { Navigator.of(bottomSheetContext).pop(); - } catch (_) {} - }, - change: transactionDetailsViewModel.sendViewModel.pendingTransaction!.change, - amountParsingProxy: transactionDetailsViewModel.sendViewModel.amountParsingProxy, - ); - }, + } + } catch (_) {} + }, + change: transactionDetailsViewModel.sendViewModel.pendingTransaction!.change, + amountParsingProxy: transactionDetailsViewModel.sendViewModel.amountParsingProxy, + ), ); if (result == null) { - transactionDetailsViewModel.sendViewModel.dismissTransaction(); + await transactionDetailsViewModel.sendViewModel.dismissTransaction(); } } }); @@ -232,14 +234,14 @@ class RBFDetailsPage extends BasePage { WidgetsBinding.instance.addPostFrameCallback((_) { if (context.mounted) { showPopUp( - context: context, - builder: (BuildContext popupContext) { - return AlertWithOneAction( - alertTitle: S.of(popupContext).sending, - alertContent: S.of(popupContext).transaction_sent, - buttonText: S.of(popupContext).ok, - buttonAction: () => Navigator.of(popupContext).pop()); - }); + context: context, + builder: (popupContext) => AlertWithOneAction( + alertTitle: S.of(popupContext).sending, + alertContent: S.of(popupContext).transaction_sent, + buttonText: S.of(popupContext).ok, + buttonAction: () => Navigator.of(popupContext).pop(), + ), + ); } }); } diff --git a/lib/src/screens/wallet_connect/wc_connections_listing_view.dart b/lib/src/screens/wallet_connect/wc_connections_listing_view.dart index 1a2e4f44a..b422b801b 100644 --- a/lib/src/screens/wallet_connect/wc_connections_listing_view.dart +++ b/lib/src/screens/wallet_connect/wc_connections_listing_view.dart @@ -131,6 +131,7 @@ class WalletConnectConnectionsView extends StatelessWidget { size: 40, onPressed: () => Navigator.of(context).maybePop(), icon: Icon(Icons.arrow_back_ios_new, size: 16), + semanticLabel: S.of(context).seed_alert_back, iconColor: Theme.of(context).colorScheme.onSurfaceVariant, ), ), diff --git a/lib/src/screens/wallet_keys/wallet_keys_page.dart b/lib/src/screens/wallet_keys/wallet_keys_page.dart index ebd4e7496..e5cb40c16 100644 --- a/lib/src/screens/wallet_keys/wallet_keys_page.dart +++ b/lib/src/screens/wallet_keys/wallet_keys_page.dart @@ -300,6 +300,7 @@ class _WalletKeysPageBodyState extends State color: Theme.of(context).colorScheme.surfaceContainer, ), child: Row( + crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Text( @@ -313,21 +314,25 @@ class _WalletKeysPageBodyState extends State const SizedBox(width: 6), Expanded( child: Row( + crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Observer( - builder: (BuildContext context) { - return Text( - (widget.walletKeysViewModel.obscurePassphrase) - ? "*****" - : widget.walletKeysViewModel.passphrase, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ); - }, + Expanded( + child: Observer( + builder: (BuildContext context) { + return Text( + (widget.walletKeysViewModel.obscurePassphrase) + ? "*****" + : widget.walletKeysViewModel.passphrase, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ); + }, + ), ), + const SizedBox(width: 6), Observer( builder: (BuildContext context) { return GestureDetector( diff --git a/lib/src/widgets/bottom_sheet/add_passphrase_bottom_sheet_widget.dart b/lib/src/widgets/bottom_sheet/add_passphrase_bottom_sheet_widget.dart index dde80d139..c1d1f1dd4 100644 --- a/lib/src/widgets/bottom_sheet/add_passphrase_bottom_sheet_widget.dart +++ b/lib/src/widgets/bottom_sheet/add_passphrase_bottom_sheet_widget.dart @@ -20,6 +20,7 @@ class AddPassphraseBottomSheet extends StatefulWidget { class _AddPassphraseBottomSheetState extends State { late final TextEditingController passphraseController; late final TextEditingController confirmPassphraseController; + final _passphraseFormKey = GlobalKey(); @override void initState() { @@ -112,51 +113,60 @@ class _AddPassphraseBottomSheetState extends State { ), ), SizedBox(height: 24), - BaseTextFormField( - key: ValueKey('add_passphrase_bottom_sheet_widget_passphrase_textfield_key'), - controller: passphraseController, - obscureText: obscurePassphrase, - contentPadding: EdgeInsets.symmetric(horizontal: 12), - hintText: S.of(context).required_passphrase, - suffixIcon: GestureDetector( - onTap: () { - setState(() { - obscurePassphrase = !obscurePassphrase; - }); - }, - child: Icon( - obscurePassphrase ? Icons.visibility_off : Icons.visibility, - size: 24, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - ), - ), - ), - SizedBox(height: 8), - BaseTextFormField( - key: ValueKey('add_passphrase_bottom_sheet_widget_confirm_passphrase_textfield_key'), - controller: confirmPassphraseController, - obscureText: obscurePassphrase, - contentPadding: EdgeInsets.symmetric(horizontal: 12), - hintText: S.of(context).confirm_passphrase, - suffixIcon: GestureDetector( - onTap: () { - setState(() { - obscurePassphrase = !obscurePassphrase; - }); - }, - child: Icon( - obscurePassphrase ? Icons.visibility_off : Icons.visibility, - size: 24, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - ), - ), - validator: (text) { - if (text == passphraseController.text) { - return null; - } + Form( + key: _passphraseFormKey, + child: Column( + children: [ + BaseTextFormField( + key: ValueKey('add_passphrase_bottom_sheet_widget_passphrase_textfield_key'), + controller: passphraseController, + obscureText: obscurePassphrase, + contentPadding: EdgeInsets.symmetric(horizontal: 12), + hintText: S.of(context).required_passphrase, + suffixIcon: GestureDetector( + onTap: () { + setState(() { + obscurePassphrase = !obscurePassphrase; + }); + }, + child: Icon( + obscurePassphrase ? Icons.visibility_off : Icons.visibility, + size: 24, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + ), + SizedBox(height: 8), + BaseTextFormField( + key: ValueKey( + 'add_passphrase_bottom_sheet_widget_confirm_passphrase_textfield_key', + ), + controller: confirmPassphraseController, + obscureText: obscurePassphrase, + contentPadding: EdgeInsets.symmetric(horizontal: 12), + hintText: S.of(context).confirm_passphrase, + suffixIcon: GestureDetector( + onTap: () { + setState(() { + obscurePassphrase = !obscurePassphrase; + }); + }, + child: Icon( + obscurePassphrase ? Icons.visibility_off : Icons.visibility, + size: 24, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + validator: (text) { + if (text == passphraseController.text) { + return null; + } - return S.of(context).passphrases_doesnt_match; - }, + return S.of(context).passphrases_doesnt_match; + }, + ), + ], + ), ), SizedBox(height: 16), Padding( @@ -184,6 +194,11 @@ class _AddPassphraseBottomSheetState extends State { child: PrimaryButton( key: ValueKey('add_passphrase_bottom_sheet_widget_restore_button_key'), onPressed: () { + if (_passphraseFormKey.currentState != null && + !_passphraseFormKey.currentState!.validate()) { + return; + } + Navigator.pop(context); widget.onRestoreButtonPressed(passphraseController.text); }, diff --git a/lib/src/widgets/primary_button.dart b/lib/src/widgets/primary_button.dart index 07915f867..48d00ba97 100644 --- a/lib/src/widgets/primary_button.dart +++ b/lib/src/widgets/primary_button.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:dotted_border/dotted_border.dart'; import 'package:flutter/cupertino.dart'; @@ -92,35 +93,53 @@ class LoadingPrimaryButton extends StatelessWidget { @override Widget build(BuildContext context) { - return ConstrainedBox( - constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), - child: SizedBox( - width: double.infinity, - height: 52.0, - child: TextButton( - onPressed: (isLoading || isDisabled) - ? null - : () { - FocusScope.of(context).unfocus(); - onPressed.call(); - }, - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all(isDisabled ? color.withOpacity(0.5) : color), - shape: WidgetStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(18.0), - ), - )), - child: isLoading - ? CupertinoActivityIndicator(animating: true, color: textColor) - : Text( - text, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 15.0, - fontWeight: FontWeight.w600, - color: isDisabled ? textColor.withOpacity(0.5) : textColor, - ), - ), + final isEnabled = !isLoading && !isDisabled; + + // The spinner replaces the label while loading, so the name is kept on a single + // semantics node that also reports the loading and disabled states. + return Semantics( + button: true, + enabled: isEnabled, + label: text, + value: isLoading ? S.of(context).loading : null, + onTap: isEnabled + ? () { + FocusScope.of(context).unfocus(); + onPressed.call(); + } + : null, + excludeSemantics: true, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), + child: SizedBox( + width: double.infinity, + height: 52.0, + child: TextButton( + onPressed: (isLoading || isDisabled) + ? null + : () { + FocusScope.of(context).unfocus(); + onPressed.call(); + }, + style: ButtonStyle( + backgroundColor: + WidgetStateProperty.all(isDisabled ? color.withOpacity(0.5) : color), + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + ), + )), + child: isLoading + ? CupertinoActivityIndicator(animating: true, color: textColor) + : Text( + text, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 15.0, + fontWeight: FontWeight.w600, + color: isDisabled ? textColor.withOpacity(0.5) : textColor, + ), + ), + ), ), ), ); diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index d67c1f045..95ab5d52c 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -151,6 +151,7 @@ abstract class SettingsStoreBase with Store { required this.mwebNodeUri, required this.mwebAdDismissed, required this.balanceHideCounter, + required this.zcashMigrationModalViewed, required bool initialEnableAutomaticNodeSwitching, required String initialBackgroundImage, TransactionPriority? initialBitcoinTransactionPriority, @@ -766,6 +767,9 @@ abstract class SettingsStoreBase with Store { (int balanceHideCounter) => _sharedPreferences.setInt(PreferencesKey.balanceHideCounter, balanceHideCounter)); + reaction((_) => zcashMigrationModalViewed, + (val) => _sharedPreferences.setBool(PreferencesKey.zcashMigrationModalViewed, val)); + this.nodes.observe((change) { if (change.newValue != null && change.key != null) { _saveCurrentNode(change.newValue!, change.key!); @@ -1075,6 +1079,9 @@ abstract class SettingsStoreBase with Store { @observable bool mwebAdDismissed; + @observable + bool zcashMigrationModalViewed; + final SecureStorage _secureStorage; final SharedPreferences _sharedPreferences; @@ -1350,6 +1357,7 @@ abstract class SettingsStoreBase with Store { final enableAutomaticNodeSwitching = sharedPreferences.getBool(PreferencesKey.enableAutomaticNodeSwitching) ?? true; final backgroundImage = sharedPreferences.getString(PreferencesKey.backgroundImage) ?? ''; + final zcashMigrationModalViewed = sharedPreferences.getBool(PreferencesKey.zcashMigrationModalViewed) ?? false; // If no value if (pinLength == null || pinLength == 0) { @@ -1762,6 +1770,7 @@ abstract class SettingsStoreBase with Store { initialBuiltinTor: builtinTor, mwebAdDismissed: mwebAdDismissed, balanceHideCounter: balanceHideCounter, + zcashMigrationModalViewed: zcashMigrationModalViewed ); } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 17ffd334a..fb2d53b74 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -96,6 +96,7 @@ abstract class DashboardViewModelBase with Store { isShowFirstYatIntroduction = false, isShowSecondYatIntroduction = false, isShowThirdYatIntroduction = false, + isMigratingToIronwood = false, filterItems = [], exchangeFilterItems = [], subname = '', @@ -250,6 +251,8 @@ abstract class DashboardViewModelBase with Store { reaction((_) => tradesStore.trades, (_) => tradeMonitor.monitorActiveTrades(wallet.id)); + addZcashMigrationReaction(); + tradeMonitor.monitorActiveTrades(wallet.id); } @@ -374,6 +377,22 @@ abstract class DashboardViewModelBase with Store { ); } + @observable + ReactionDisposer? zcashMigrationReactionDisposer; + + @action + void addZcashMigrationReaction() { + zcashMigrationReactionDisposer?.reaction.dispose(); + zcashMigrationReactionDisposer = null; + + if (wallet.type == WalletType.zcash) { + zcashMigrationReactionDisposer = reaction((_) => wallet.balance.values.first, (_) async { + isMigratingToIronwood = + wallet.type == WalletType.zcash && await zcash!.hasOrchardMigratableBalance(wallet); + }); + } + } + @computed bool get isSyncHeavy { if ([ @@ -538,10 +557,16 @@ abstract class DashboardViewModelBase with Store { .map((item) => _txIdentityString(item.transaction.txHash, item.transaction.direction)) .toSet(); - transactions.removeWhere( - (item) => newKeys - .contains(_txIdentityString(item.transaction.txHash, item.transaction.direction)), - ); + transactions.removeWhere((item) { + if (wallet.type == WalletType.zcash) { + return newTransactions.any( + (n) => n.transaction.txHash == item.transaction.txHash, + ); + } + return newKeys.contains( + _txIdentityString(item.transaction.txHash, item.transaction.direction), + ); + }); transactions.addAll(newTransactions); // transactions.clear(); @@ -1264,6 +1289,8 @@ abstract class DashboardViewModelBase with Store { name = wallet.name; loadFilterItems(); + addZcashMigrationReaction(); + if (wallet.type == WalletType.monero) { subname = monero!.getCurrentAccount(wallet).label; @@ -1545,6 +1572,9 @@ abstract class DashboardViewModelBase with Store { } } + @observable + bool isMigratingToIronwood; + String getTransactionType(TransactionInfo tx) { if (wallet.type == WalletType.bitcoin) { if (tx.isReplaced == true) return ' (replaced)'; diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index e030f8e25..5039c927b 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -408,6 +408,14 @@ abstract class HomeSettingsViewModelBase with Store { return null; } + Future checkIfTokenIsVerifiedOnJupiter(String mintAddress) async { + if (_balanceViewModel.wallet.type != WalletType.solana) { + return null; + } + + return solana!.isTokenVerifiedOnJupiter(_balanceViewModel.wallet, mintAddress); + } + CryptoCurrency get nativeToken => _balanceViewModel.wallet.currency; void _updateFiatPrices(CryptoCurrency token) async { diff --git a/lib/view_model/dashboard/receive_option_view_model.dart b/lib/view_model/dashboard/receive_option_view_model.dart index a86890fc7..0f3afeff9 100644 --- a/lib/view_model/dashboard/receive_option_view_model.dart +++ b/lib/view_model/dashboard/receive_option_view_model.dart @@ -18,7 +18,14 @@ abstract class ReceiveOptionViewModelBase with Store { ? ReceivePageOption.testnet : _wallet.type == WalletType.zcash ? zcash!.getSelectedAddressType(_wallet) - : ReceivePageOption.mainnet); + : ReceivePageOption.mainnet) { + if (_wallet.type == WalletType.zcash) { + reaction( + (_) => zcash!.ironwoodActive(_wallet.walletAddresses), + (_) => _syncZcashShieldedLabel(), + ); + } + } final WalletBase _wallet; @@ -27,10 +34,21 @@ abstract class ReceiveOptionViewModelBase with Store { @observable ReceivePageOption selectedReceiveOption; + @computed List get options => _wallet.walletAddresses.receivePageOptions; String get walletTypeString => walletTypeToString(_wallet.type); @action void selectReceiveOption(ReceivePageOption option) => selectedReceiveOption = option; + + @action + void _syncZcashShieldedLabel() { + for (final option in options) { + if (option.value == selectedReceiveOption.value) { + selectedReceiveOption = option; + return; + } + } + } } diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 4d6a0d527..9a7dfa655 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -60,8 +60,16 @@ class TransactionListItem extends ActionListItem with Keyable { return 'Transaction has missing data'; } - if (transaction.additionalInfo['autoShield'] == true) { - return "Autoshield"; + if (transaction.additionalInfo['isIronwoodMigration'] == true) { + return 'Migration'; + } + if (transaction.additionalInfo['isAutoShield'] == true) { + if (transaction.isPending) { + final status = formattedStatus; + final baseString = S.current.shielding; + return status.isNotEmpty ? "$baseString $status" : "$baseString..."; + } + return S.current.shielding; } if (transaction.isPending) { final status = formattedStatus; diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 6e039e271..5ca718eb4 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -442,16 +442,11 @@ abstract class ExchangeTradeViewModelBase with Store { case WalletType.dogecoin: return DogeURI(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.bsc: - return BSCURI(amount: amount, address: inputAddress); + return _createERC681URI(fromCurrency, inputAddress, amount); case WalletType.solana: return SolanaURI(amount: amount, address: inputAddress); case WalletType.tron: diff --git a/lib/view_model/hardware_wallet/trezor_connect_view_model.dart b/lib/view_model/hardware_wallet/trezor_connect_view_model.dart index c048edf7c..28116e3cb 100644 --- a/lib/view_model/hardware_wallet/trezor_connect_view_model.dart +++ b/lib/view_model/hardware_wallet/trezor_connect_view_model.dart @@ -1,43 +1,43 @@ -import 'dart:async'; -import 'dart:io'; +import "dart:async"; +import "dart:convert"; +import "dart:io"; -import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart'; -import 'package:cake_wallet/evm/evm.dart'; -import 'package:cake_wallet/main.dart'; -import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart'; -import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; -import 'package:cake_wallet/wallet_type_utils.dart'; -import 'package:cw_core/hardware/device_connection_type.dart'; -import 'package:cw_core/hardware/hardware_wallet_service.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter/material.dart'; -import 'package:mobx/mobx.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:trezor_connect/trezor_connect.dart' as connect_sdk; -import 'package:trezor_flutter/trezor_flutter.dart' as sdk; +import "package:cake_wallet/bitcoin/bitcoin.dart"; +import "package:cake_wallet/core/secure_storage.dart"; +import "package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart"; +import "package:cake_wallet/evm/evm.dart"; +import "package:cake_wallet/main.dart"; +import "package:cake_wallet/monero/monero.dart"; +import "package:cake_wallet/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart"; +import "package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart"; +import "package:cake_wallet/wallet_type_utils.dart"; +import "package:cw_core/encryption_file_utils.dart"; +import "package:cw_core/hardware/device_connection_type.dart"; +import "package:cw_core/hardware/hardware_wallet_service.dart"; +import "package:cw_core/key.dart"; +import "package:cw_core/root_dir.dart"; +import "package:cw_core/utils/print_verbose.dart"; +import "package:cw_core/wallet_base.dart"; +import "package:cw_core/wallet_info.dart"; +import "package:cw_core/wallet_type.dart"; +import "package:device_info_plus/device_info_plus.dart"; +import "package:flutter/material.dart"; +import "package:mobx/mobx.dart"; +import "package:permission_handler/permission_handler.dart"; +import "package:trezor_connect/trezor_connect.dart" as connect_sdk; +import "package:trezor_flutter/trezor_flutter.dart" as sdk; -part 'trezor_connect_view_model.g.dart'; +part "trezor_connect_view_model.g.dart"; class TrezorConnectViewModel = TrezorConnectViewModelBase with _$TrezorConnectViewModel; abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with Store { - final connect_sdk.TrezorConnect trezorConnect; - - late final sdk.TrezorInterface trezorBLE; - late final sdk.TrezorInterface trezorUSB; - - sdk.ThpState _state = sdk.ThpState(); - - TrezorConnectViewModelBase(this.trezorConnect) { + TrezorConnectViewModelBase(this.trezorConnect, this._secureStorage) { if (_doesSupportHardwareWallets) { reaction((_) => isBleEnabled, (_) { - if (isBleEnabled) _initBLE(); + if (isBleEnabled) { + _initBLE(); + } }); updateBleState(); @@ -47,11 +47,21 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S } } + final connect_sdk.TrezorConnect trezorConnect; + final SecureStorage _secureStorage; + + late final sdk.TrezorInterface trezorBLE; + late final sdk.TrezorInterface trezorUSB; + + sdk.ThpState? _state; + bool get _doesSupportHardwareWallets { if (isMoneroOnly) { return DeviceConnectionType.supportedConnectionTypes( - WalletType.monero, HardwareWalletType.trezor, Platform.isIOS) - .isNotEmpty; + WalletType.monero, + HardwareWalletType.trezor, + Platform.isIOS, + ).isNotEmpty; } return true; @@ -63,9 +73,11 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S if (isBleEnabled && !_bleIsInitialized) { trezorBLE = sdk.TrezorInterface.ble( onPermissionRequest: (_) async { - if (Platform.isMacOS) return true; + if (Platform.isMacOS) { + return true; + } - Map statuses = await [ + final Map statuses = await [ Permission.bluetoothScan, Permission.bluetoothConnect, Permission.bluetoothAdvertise, @@ -73,7 +85,7 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S return statuses.values.where((status) => status.isDenied).isEmpty; }, - bleOptions: sdk.BluetoothOptions(maxScanDuration: Duration(minutes: 5)), + bleOptions: sdk.BluetoothOptions(maxScanDuration: const Duration(minutes: 5)), ); _bleIsInitialized = true; } @@ -99,21 +111,27 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S final newState = bleState == sdk.AvailabilityState.poweredOn; - if (newState != isBleEnabled) isBleEnabled = newState; + if (newState != isBleEnabled) { + isBleEnabled = newState; + } } @override Stream scanForBleDevices() => - trezorBLE.scan().map((d) => TrezorHardwareWalletDevice(d)); + trezorBLE.scan().map(TrezorHardwareWalletDevice.new); @override - Future> getAllUsbDevices() => trezorUSB.devices - .then((devices) => devices.map((d) => TrezorHardwareWalletDevice(d)).toList()); + Future> getAllUsbDevices() => + trezorUSB.devices.then((devices) => devices.map(TrezorHardwareWalletDevice.new).toList()); @override Future stopScanning() async { - if (_bleIsInitialized) await trezorBLE.stopScanning(); - if (!Platform.isIOS) await trezorUSB.stopScanning(); + if (_bleIsInitialized) { + await trezorBLE.stopScanning(); + } + if (!Platform.isIOS) { + await trezorUSB.stopScanning(); + } } sdk.TrezorClient? _client; @@ -127,10 +145,17 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S @override @action - Future connectDevice(HardwareWalletDevice device, WalletType type, - [bool isRetry = false]) async { - if (!(device is TrezorHardwareWalletDevice)) return false; - if (isConnecting) return false; + Future connectDevice( + HardwareWalletDevice device, + WalletType type, [ + bool isRetry = false, + ]) async { + if (device is! TrezorHardwareWalletDevice) { + return false; + } + if (isConnecting) { + return false; + } isConnecting = true; paringState = TrezorParingState.initial; @@ -140,16 +165,19 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S final connection = await trezorInterface.connect(device.device); if (!isRetry) { - showModalBottomSheet( - context: navigatorKey.currentContext!, - isScrollControlled: true, - isDismissible: false, - enableDrag: false, - useSafeArea: true, - builder: (context) => HardwareWalletProceedOnDeviceSheet( + unawaited( + showModalBottomSheet( + context: navigatorKey.currentContext!, + isScrollControlled: true, + isDismissible: false, + enableDrag: false, + useSafeArea: true, + builder: (_) => HardwareWalletProceedOnDeviceSheet( hardwareWalletType: hardwareWalletType, trezorConnectVM: this, - onRetry: () => connectDevice(device, type, true)), + onRetry: () => connectDevice(device, type, true), + ), + ), ); } @@ -159,15 +187,18 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S final res = await _pinCompleter!.future; paringState = TrezorParingState.verifyingPin; - if (res == null) throw Exception(); + if (res == null) { + throw Exception(); + } return res; } final deviceInfo = await _deviceName; + _state ??= await _getState(); _client = sdk.TrezorClient.getClientForConnection( connection, - _state, + _state!, "Cake Wallet", deviceInfo, onPinCode, @@ -175,6 +206,16 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S await _client!.createChannel(); + if (!_state!.pairingCredentials.any((c) => c.autoconnect == true)) { + if (_client case final sdk.TrezorClientV2 clientV2) { + try { + final auto = await clientV2.getAutoPairingCredentials(); + _state!.setPairingCredentials([auto]); + await _saveState(); + } catch (_) {} + } + } + paringState = TrezorParingState.success; return true; } catch (e) { @@ -192,8 +233,12 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S Future get _deviceName async { final deviceInfo = DeviceInfoPlugin(); - if (Platform.isAndroid) return (await deviceInfo.androidInfo).model; - if (Platform.isIOS) return (await deviceInfo.iosInfo).name; + if (Platform.isAndroid) { + return (await deviceInfo.androidInfo).model; + } + if (Platform.isIOS) { + return (await deviceInfo.iosInfo).name; + } return "Computer"; } @@ -231,9 +276,61 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S case WalletType.polygon: return evm!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type)); default: - throw Exception('Unexpected wallet type: ${wallet.type} for trezor'); + throw Exception("Unexpected wallet type: ${wallet.type} for trezor"); } } + + final EncryptionFileUtils _encryptionFileUtils = encryptionFileUtilsFor(true); + static const String _secureStorageKey = "com.cakewallet.trezor/thp_state"; + + Future get _thpJsonFile async => "${(await getAppDir()).path}/thp_state.json.enc"; + + Future _getState() async { + final password = await _secureStorage.read(key: _secureStorageKey); + if (password == null) { + return _saveState(); + } + try { + final state = await _encryptionFileUtils.read(path: await _thpJsonFile, password: password); + return sdk.ThpState.fromJson(state); + } catch (_) { + return _saveState(); + } + } + + Future _saveState() async { + try { + final password = generateKey(); + await _secureStorage.write(key: _secureStorageKey, value: password); + + final file = File(await _thpJsonFile); + if (!file.existsSync()) { + file.createSync(); + } + + final state = _state ?? sdk.ThpState(); + await _encryptionFileUtils.write( + path: file.path, + password: password, + data: jsonEncode(state.toMap()), + ); + _state = state; + return state; + } catch (_) { + throw Exception("Unable to save Trezor State"); + } + } + + Future syncKeyImages(WalletBase wallet) async { + if (wallet.type == WalletType.monero) { + try { + await monero!.syncTrezor(wallet); + } catch (_) { + return false; + } + } + return true; + } } abstract class TrezorParingState { @@ -241,6 +338,7 @@ abstract class TrezorParingState { static TrezorParingState enterPin = EnterPinTrezorParingState(); static TrezorParingState verifyingPin = VerifyingPinTrezorParingState(); static TrezorParingState success = SuccessTrezorParingState(); + static TrezorParingState fail(String message) => FailTrezorParingState(message); } @@ -253,7 +351,7 @@ class VerifyingPinTrezorParingState extends TrezorParingState {} class SuccessTrezorParingState extends TrezorParingState {} class FailTrezorParingState extends TrezorParingState { - final String message; - FailTrezorParingState(this.message); + + final String message; } diff --git a/lib/view_model/link_view_model.dart b/lib/view_model/link_view_model.dart index a9e742733..f14718ec5 100644 --- a/lib/view_model/link_view_model.dart +++ b/lib/view_model/link_view_model.dart @@ -47,7 +47,7 @@ class LinkViewModel { case 'send': return Routes.send; case 'receive': - return Routes.addressPage; + return Routes.newReceivePage; default: return null; } diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 2933fbef8..ebd12a16d 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -21,6 +21,7 @@ import 'package:cw_core/balance.dart'; import 'package:cw_core/crypto_amount_format.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; +import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -50,8 +51,9 @@ abstract class OutputBase with Store { parsedAddress = ParsedAddress(parsedAddressByCurrencyMap: {}) { autorun((_) { final status = _wallet.syncStatus; - printV("Sync status changed to $status. Recalculating fees"); - + if (status is! SyncedSyncStatus) { + return; + } calculateEstimatedFee(); }); } diff --git a/lib/view_model/send/send_template_view_model.dart b/lib/view_model/send/send_template_view_model.dart index 75a99bb4d..f2fca86b7 100644 --- a/lib/view_model/send/send_template_view_model.dart +++ b/lib/view_model/send/send_template_view_model.dart @@ -48,8 +48,11 @@ abstract class SendTemplateViewModelBase with Store { amountParsingProxy: _appStore.amountParsingProxy, ); - AddressValidator get addressValidator => - AddressValidator(type: _wallet.currency, isTestnet: _wallet.isTestnet); + AddressValidator get addressValidator => AddressValidator( + type: _wallet.currency, + isTestnet: _wallet.isTestnet, + network: _wallet.walletInfo.network, + ); TemplateValidator get templateValidator => TemplateValidator(); diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 8c49cdc83..b3b2c17e8 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart'; -import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/amount_parsing_proxy.dart'; import 'package:cake_wallet/core/amount_validator.dart'; @@ -128,8 +127,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor outputs.add(Output(wallet, _appStore, _fiatConversationStore, _outputCryptoCurrencyHandler)); unspentCoinsListViewModel - .initialSetup() - .then((_) => unspentCoinsListViewModel.resetUnspentCoinsInfoSelections()); + .initialSetup(); + // .then((_) => unspentCoinsListViewModel.resetUnspentCoinsInfoSelections()); reaction((_) { if (isEVMCompatibleChain(wallet.type)) { @@ -191,7 +190,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor // Check if the address is valid for the current currency (except for Zano, which can use handles as addresses) if (selectedCryptoCurrency != CryptoCurrency.zano) { - final isValidAddress = AddressValidator(type: selectedCryptoCurrency).isValid(query); + final isValidAddress = AddressValidator( + type: selectedCryptoCurrency, + network: wallet.walletInfo.network, + ).isValid(query); if (isValidAddress) return null; } @@ -318,8 +320,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor Validator get allAmountValidator => AllAmountValidator(); - Validator get addressValidator => - AddressValidator(type: selectedCryptoCurrency, isTestnet: wallet.isTestnet); + Validator get addressValidator => AddressValidator( + type: selectedCryptoCurrency, + isTestnet: wallet.isTestnet, + network: wallet.walletInfo.network, + ); Validator get textValidator => TextValidator(); diff --git a/lib/view_model/support_view_model.dart b/lib/view_model/support_view_model.dart index 96cf66fb9..4f1120158 100644 --- a/lib/view_model/support_view_model.dart +++ b/lib/view_model/support_view_model.dart @@ -44,7 +44,7 @@ abstract class SupportViewModelBase with Store { LinkListItem( title: 'Telegram', icon: 'assets/images/Telegram.png', - linkTitle: 't.me/cakewallet', + linkTitle: 't.me/cakewalletannouncements', link: 'https://t.me/cakewalletannouncements'), LinkListItem( title: 'Telegram Support Bot', @@ -75,7 +75,7 @@ abstract class SupportViewModelBase with Store { title: 'SwapTrade', icon: 'assets/images/swap_trade.png', linkTitle: 'help.swaptrade.io', - link: 'mailto:support@exolix.com'), + link: 'https://help.swaptrade.io/contact/'), LinkListItem( title: 'Trocador', icon: 'assets/images/trocador.png', diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index a3b7d8ae8..df9c72721 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -1,50 +1,50 @@ -import 'package:cake_wallet/reactions/wallet_connect.dart'; -import 'package:cake_wallet/solana/solana.dart'; -import 'package:cake_wallet/src/screens/transaction_details/address_list_item.dart'; -import 'package:cake_wallet/src/screens/transaction_details/confirmations_list_item.dart'; -import 'package:cake_wallet/store/app_store.dart'; -import 'package:cake_wallet/core/address_validator.dart'; -import 'package:cake_wallet/tron/tron.dart'; -import 'package:cake_wallet/zano/zano.dart'; -import 'package:cw_core/crypto_amount_format.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/currency_for_wallet_type.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/transaction_info.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/evm/evm.dart'; -import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; -import 'package:cake_wallet/entities/transaction_description.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart'; -import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; -import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart'; -import 'package:cake_wallet/src/screens/transaction_details/transaction_expandable_list_item.dart'; -import 'package:cake_wallet/utils/date_formatter.dart'; -import 'package:cake_wallet/view_model/send/send_view_model.dart'; -import 'package:collection/collection.dart'; -import 'package:cw_core/transaction_direction.dart'; -import 'package:cw_core/transaction_priority.dart'; -import 'package:flutter/foundation.dart'; -import 'package:hive/hive.dart'; -import 'package:intl/intl.dart'; -import 'package:mobx/mobx.dart'; -import 'package:url_launcher/url_launcher.dart'; +import "package:cake_wallet/bitcoin/bitcoin.dart"; +import "package:cake_wallet/core/address_validator.dart"; +import "package:cake_wallet/entities/priority_for_wallet_type.dart"; +import "package:cake_wallet/entities/transaction_description.dart"; +import "package:cake_wallet/evm/evm.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/solana/solana.dart"; +import "package:cake_wallet/src/screens/transaction_details/address_list_item.dart"; +import "package:cake_wallet/src/screens/transaction_details/confirmations_list_item.dart"; +import "package:cake_wallet/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart"; +import "package:cake_wallet/src/screens/transaction_details/standart_list_item.dart"; +import "package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart"; +import "package:cake_wallet/src/screens/transaction_details/transaction_expandable_list_item.dart"; +import "package:cake_wallet/store/app_store.dart"; +import "package:cake_wallet/tron/tron.dart"; +import "package:cake_wallet/view_model/send/send_view_model.dart"; +import "package:cake_wallet/zano/zano.dart"; +import "package:collection/collection.dart"; +import "package:cw_core/crypto_currency.dart"; +import "package:cw_core/currency_for_wallet_type.dart"; +import "package:cw_core/transaction_direction.dart"; +import "package:cw_core/transaction_info.dart"; +import "package:cw_core/transaction_priority.dart"; +import "package:cw_core/wallet_base.dart"; +import "package:cw_core/wallet_type.dart"; +import "package:flutter/foundation.dart"; +import "package:hive/hive.dart"; +import "package:intl/intl.dart"; +import "package:mobx/mobx.dart"; +import "package:url_launcher/url_launcher.dart"; -part 'transaction_details_view_model.g.dart'; +part "transaction_details_view_model.g.dart"; bool _trueFunc(_) => true; /// We're adding a regex here so we can remove any already saved address that has the account in it. /// In the refactor, we will make another separate variable for accounts and the UI would handle it as needed. String _moneroRecipientAddressForDisplay(String raw, WalletType walletType) { - if (walletType != WalletType.monero || raw.isEmpty) return raw; + if (walletType != WalletType.monero || raw.isEmpty) { + return raw; + } - final compact = raw.replaceAll(RegExp(r'\s'), ''); + final compact = raw.replaceAll(RegExp(r"\s"), ""); final match = RegExp( - r'4[0-9a-zA-Z]{94}|8[0-9a-zA-Z]{94}|[0-9a-zA-Z]{106}', + r"4[0-9a-zA-Z]{94}|8[0-9a-zA-Z]{94}|[0-9a-zA-Z]{106}", caseSensitive: false, ).firstMatch(compact); return match?.group(0) ?? raw.trim(); @@ -55,6 +55,14 @@ bool isLightning(TransactionInfo tx) => (tx.additionalInfo["isLightning"] as boo bool hasLightningPreimage(TransactionInfo tx) => (tx.additionalInfo["preimage"] as String?) != null; class TxDetailRowDefinition { + TxDetailRowDefinition({ + required this.keyString, + required this.title, + required this.valueGetter, + this.applicable = _trueFunc, + this.listItemBuilder = StandartListItem.new, + }); + final String keyString; final String title; final String Function(TransactionDetailsViewModelBase) valueGetter; @@ -65,20 +73,13 @@ class TxDetailRowDefinition { required Key key, }) listItemBuilder; - TxDetailRowDefinition( - {required this.keyString, - required this.title, - required this.valueGetter, - this.applicable = _trueFunc, - this.listItemBuilder = StandartListItem.new}); - static final List defs = [ TxDetailRowDefinition( - keyString: "standard_list_item_transaction_details_date_key", - title: S.current.transaction_details_date, - valueGetter: (vm) => - DateFormat("d MMMM yyyy, HH:mm", vm._appStore.settingsStore.languageCode) - .format(vm.transactionInfo.date)), + keyString: "standard_list_item_transaction_details_date_key", + title: S.current.transaction_details_date, + valueGetter: (vm) => DateFormat("d MMMM yyyy, HH:mm", vm._appStore.settingsStore.languageCode) + .format(vm.transactionInfo.date), + ), TxDetailRowDefinition( keyString: "standard_list_item_transaction_details_height_key", title: S.current.transaction_details_height, @@ -88,125 +89,137 @@ class TxDetailRowDefinition { !isLightning(vm.transactionInfo), ), TxDetailRowDefinition( - keyString: "standard_list_item_transaction_details_fee_key", - title: S.current.transaction_details_fee, - valueGetter: (vm) => vm.transactionInfo.fee!.toStringWithSymbol(), - applicable: (vm) => - vm.wallet.type != WalletType.nano && - (vm.transactionInfo.fee?.toStringWithSymbol() ?? "").isNotEmpty), + keyString: "standard_list_item_transaction_details_fee_key", + title: S.current.transaction_details_fee, + valueGetter: (vm) => vm.feeAmount, + applicable: (vm) => + vm.wallet.type != WalletType.nano && + (vm.transactionInfo.fee?.toStringWithSymbol() ?? "").isNotEmpty, + ), TxDetailRowDefinition( - keyString: "standard_list_item_transaction_confirmations_key", - title: S.current.confirmations, - valueGetter: (vm) => "${vm.transactionInfo.confirmations}/${vm.neededConfirmations}", - applicable: (vm) => - [...electrumWalletTypes, ...evmWalletTypes, WalletType.zcash, WalletType.monero] - .contains(vm.wallet.type) && - !isLightning(vm.transactionInfo), - listItemBuilder: ConfirmationsListItem.new), + keyString: "standard_list_item_transaction_confirmations_key", + title: S.current.confirmations, + valueGetter: (vm) => "${vm.transactionInfo.confirmations}/${vm.neededConfirmations}", + applicable: (vm) => + [...electrumWalletTypes, ...evmWalletTypes, WalletType.zcash, WalletType.monero] + .contains(vm.wallet.type) && + !isLightning(vm.transactionInfo), + listItemBuilder: ConfirmationsListItem.new, + ), TxDetailRowDefinition( - keyString: "standard_list_item_transaction_details_recipient_address_key", - title: S.current.transaction_details_recipient_address, - valueGetter: (vm) { - String? ret = null; + keyString: "standard_list_item_transaction_details_recipient_address_key", + title: S.current.transaction_details_recipient_address, + valueGetter: (vm) { + String? ret; - switch (vm.wallet.type) { - case WalletType.monero: - if (vm.transactionInfo.direction == TransactionDirection.incoming) { - ret = monero!.getTransactionAddress( - vm.wallet, - vm.transactionInfo.additionalInfo['accountIndex'] as int, - vm.transactionInfo.additionalInfo['addressIndex'] as int); - } - case WalletType.bitcoin: - ret = (bitcoin!.getTransactionAddresses(vm.wallet, vm.transactionInfo) ?? []) - .firstOrNull ?? - ""; - case WalletType.tron: - if (vm.transactionInfo.to != null) - ret = tron!.getTronBase58Address(vm.transactionInfo.to!, vm.wallet); - default: - break; - } - if (ret == null) { - ret = vm.transactionInfo.to ?? ""; - } - final resolvedAddress = _moneroRecipientAddressForDisplay(ret, vm.wallet.type); - vm.isRecipientAddressShown = resolvedAddress.isNotEmpty; - return resolvedAddress; - }, - applicable: (vm) => - vm.showRecipientAddress && - (vm.transactionInfo.to != null || - [WalletType.monero, WalletType.tron].contains(vm.wallet.type) || - vm.wallet.type == WalletType.bitcoin && - vm.transactionInfo.direction == TransactionDirection.incoming), - listItemBuilder: AddressListItem.new), - TxDetailRowDefinition( - keyString: "standard_list_item_transaction_details_source_address_key", - title: S.current.transaction_details_source_address, - valueGetter: (vm) { - switch (vm.wallet.type) { - case WalletType.tron: - return tron!.getTronBase58Address(vm.transactionInfo.from!, vm.wallet); - default: - return vm.transactionInfo.from!; - } - }, - applicable: (vm) => vm.transactionInfo.from != null, - listItemBuilder: AddressListItem.new), - TxDetailRowDefinition( - keyString: "standard_list_item_address_label_key", - title: S.current.address_label, - valueGetter: (vm) => monero!.getSubaddressLabel( - vm.wallet, - vm.transactionInfo.additionalInfo['accountIndex'] as int, - vm.transactionInfo.additionalInfo['addressIndex'] as int), - applicable: (vm) => vm.wallet.type == WalletType.monero), - TxDetailRowDefinition( - keyString: "standard_list_item_transaction_key", - title: S.current.transaction_key, - valueGetter: (vm) { - final descriptionKey = - '${vm.transactionInfo.txHash}_${vm.wallet.walletAddresses.primaryAddress}'; + switch (vm.wallet.type) { + case WalletType.monero: + if (vm.transactionInfo.direction == TransactionDirection.incoming) { + ret = monero!.getTransactionAddress( + vm.wallet, + vm.transactionInfo.additionalInfo["accountIndex"] as int, + vm.transactionInfo.additionalInfo["addressIndex"] as int, + ); + } + case WalletType.bitcoin: + ret = (bitcoin!.getTransactionAddresses(vm.wallet, vm.transactionInfo) ?? []) + .firstOrNull ?? + ""; + case WalletType.tron: + if (vm.transactionInfo.to != null) { + ret = tron!.getTronBase58Address(vm.transactionInfo.to!, vm.wallet); + } + default: + break; + } + ret ??= vm.transactionInfo.to ?? ""; - final description = vm.transactionDescriptionBox.values.firstWhere( - (val) => val.id == descriptionKey || val.id == vm.transactionInfo.txHash, - orElse: () => TransactionDescription(id: descriptionKey)); - return vm.transactionInfo.additionalInfo['key'] as String? ?? - description.transactionKey ?? - ""; - }, - applicable: (vm) => vm.wallet.type == WalletType.monero), + final resolvedAddress = _moneroRecipientAddressForDisplay(ret, vm.wallet.type); + vm.isRecipientAddressShown = resolvedAddress.isNotEmpty; + return resolvedAddress; + }, + applicable: (vm) => + vm.showRecipientAddress && + (vm.transactionInfo.to != null || + [WalletType.monero, WalletType.tron].contains(vm.wallet.type) || + vm.wallet.type == WalletType.bitcoin && + vm.transactionInfo.direction == TransactionDirection.incoming), + listItemBuilder: AddressListItem.new, + ), + TxDetailRowDefinition( + keyString: "standard_list_item_transaction_details_source_address_key", + title: S.current.transaction_details_source_address, + valueGetter: (vm) { + switch (vm.wallet.type) { + case WalletType.tron: + return tron!.getTronBase58Address(vm.transactionInfo.from!, vm.wallet); + default: + return vm.transactionInfo.from!; + } + }, + applicable: (vm) => vm.transactionInfo.from != null, + listItemBuilder: AddressListItem.new, + ), + TxDetailRowDefinition( + keyString: "standard_list_item_address_label_key", + title: S.current.address_label, + valueGetter: (vm) => monero!.getSubaddressLabel( + vm.wallet, + vm.transactionInfo.additionalInfo["accountIndex"] as int, + vm.transactionInfo.additionalInfo["addressIndex"] as int, + ), + applicable: (vm) => vm.wallet.type == WalletType.monero, + ), + TxDetailRowDefinition( + keyString: "standard_list_item_transaction_key", + title: S.current.transaction_key, + valueGetter: (vm) { + final descriptionKey = + "${vm.transactionInfo.txHash}_${vm.wallet.walletAddresses.primaryAddress}"; + + final description = vm.transactionDescriptionBox.values.firstWhere( + (val) => val.id == descriptionKey || val.id == vm.transactionInfo.txHash, + orElse: () => TransactionDescription(id: descriptionKey), + ); + return vm.transactionInfo.additionalInfo["key"] as String? ?? + description.transactionKey ?? + ""; + }, + applicable: (vm) => vm.wallet.type == WalletType.monero, + ), TxDetailRowDefinition( keyString: "standard_list_item_lightning_preimage", title: S.current.transaction_preimage, - valueGetter: (vm) => vm.transactionInfo.additionalInfo['preimage'] as String? ?? "", + valueGetter: (vm) => vm.transactionInfo.additionalInfo["preimage"] as String? ?? "", applicable: (vm) => hasLightningPreimage(vm.transactionInfo) && isLightning(vm.transactionInfo), ), TxDetailRowDefinition( - keyString: "standard_list_item_transaction_confirmed_key", - title: S.current.confirmed_tx, - valueGetter: (vm) => (vm.transactionInfo.confirmations > 0).toString(), - applicable: (vm) => vm.wallet.type == WalletType.nano), + keyString: "standard_list_item_transaction_confirmed_key", + title: S.current.confirmed_tx, + valueGetter: (vm) => (vm.transactionInfo.confirmations > 0).toString(), + applicable: (vm) => vm.wallet.type == WalletType.nano, + ), TxDetailRowDefinition( - keyString: "standard_list_item_transaction_details_memo_key", - title: S.current.memo, - valueGetter: (vm) => vm.transactionInfo.additionalInfo['memo'] as String, - applicable: (vm) => - vm.wallet.type == WalletType.zcash && - vm.transactionInfo.additionalInfo["memo"] != null), + keyString: "standard_list_item_transaction_details_memo_key", + title: S.current.memo, + valueGetter: (vm) => vm.transactionInfo.additionalInfo["memo"] as String, + applicable: (vm) => + vm.wallet.type == WalletType.zcash && vm.transactionInfo.additionalInfo["memo"] != null, + ), TxDetailRowDefinition( - keyString: "standard_list_item_transaction_details_asset_id_key", - title: "Asset ID", - valueGetter: (vm) => - vm.transactionInfo.additionalInfo["assetId"] as String? ?? "Unknown asset id", - applicable: (vm) => vm.wallet.type == WalletType.zano), + keyString: "standard_list_item_transaction_details_asset_id_key", + title: "Asset ID", + valueGetter: (vm) => + vm.transactionInfo.additionalInfo["assetId"] as String? ?? "Unknown asset id", + applicable: (vm) => vm.wallet.type == WalletType.zano, + ), TxDetailRowDefinition( - keyString: "standard_list_item_transaction_details_comment_key", - title: S.current.transaction_details_title, - valueGetter: (vm) => vm.transactionInfo.additionalInfo['comment'] as String? ?? "", - applicable: (vm) => vm.wallet.type == WalletType.zano), + keyString: "standard_list_item_transaction_details_comment_key", + title: S.current.transaction_details_title, + valueGetter: (vm) => vm.transactionInfo.additionalInfo["comment"] as String? ?? "", + applicable: (vm) => vm.wallet.type == WalletType.zano, + ), TxDetailRowDefinition( keyString: "standard_list_item_transaction_details_id_key", title: S.current.transaction_details_transaction_id, @@ -227,7 +240,7 @@ abstract class TransactionDetailsViewModelBase with Store { required this.sendViewModel, this.canReplaceByFee = false, }) : items = [], - RBFListItems = [], + rbfListItems = [], newFee = 0, isRecipientAddressShown = false, _appStore = appStore, @@ -236,19 +249,23 @@ abstract class TransactionDetailsViewModelBase with Store { for (final def in TxDetailRowDefinition.defs) { if (def.applicable(this)) { - items.add(def.listItemBuilder( + items.add( + def.listItemBuilder( title: def.title, value: def.valueGetter(this), - key: ValueKey(def.keyString)) as TransactionDetailsListItem); + key: ValueKey(def.keyString), + ) as TransactionDetailsListItem, + ); } } _checkForRBF(tx); - final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}'; + final descriptionKey = "${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}"; final description = transactionDescriptionBox.values.firstWhere( - (val) => val.id == descriptionKey || val.id == transactionInfo.txHash, - orElse: () => TransactionDescription(id: descriptionKey)); + (val) => val.id == descriptionKey || val.id == transactionInfo.txHash, + orElse: () => TransactionDescription(id: descriptionKey), + ); if (showRecipientAddress && !isRecipientAddressShown) { final recipientAddress = description.recipientAddress; @@ -260,7 +277,7 @@ abstract class TransactionDetailsViewModelBase with Store { AddressListItem( title: S.current.transaction_details_recipient_address, value: recipientAddressForDisplay, - key: ValueKey('standard_list_item_${recipientAddressForDisplay}_key'), + key: ValueKey("standard_list_item_${recipientAddressForDisplay}_key"), ), ); } @@ -268,10 +285,11 @@ abstract class TransactionDetailsViewModelBase with Store { } void updateNote(String note) { - final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}'; + final descriptionKey = "${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}"; final description = transactionDescriptionBox.values.firstWhere( - (val) => val.id == descriptionKey || val.id == transactionInfo.txHash, - orElse: () => TransactionDescription(id: descriptionKey)); + (val) => val.id == descriptionKey || val.id == transactionInfo.txHash, + orElse: () => TransactionDescription(id: descriptionKey), + ); description.transactionNote = note; @@ -283,9 +301,10 @@ abstract class TransactionDetailsViewModelBase with Store { } String get note { - final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}'; + final descriptionKey = "${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}"; final description = transactionDescriptionBox.values - .firstWhereOrNull((val) => val.id == descriptionKey || val.id == transactionInfo.txHash); + .firstWhereOrNull((val) => val.id == descriptionKey || val.id == transactionInfo.txHash, + ); return description?.transactionNote ?? ""; } @@ -296,7 +315,7 @@ abstract class TransactionDetailsViewModelBase with Store { final AppStore _appStore; final List items; - final List RBFListItems; + final List rbfListItems; bool showRecipientAddress; bool isRecipientAddressShown; int newFee; @@ -304,9 +323,13 @@ abstract class TransactionDetailsViewModelBase with Store { TransactionPriority? transactionPriority; CryptoCurrency get transactionAsset { - if (isEVMCompatibleChain(wallet.type)) return evm!.assetOfTransaction(wallet, transactionInfo); + if (isEVMCompatibleChain(wallet.type)) { + return evm!.assetOfTransaction(wallet, transactionInfo); + } - if (isLightning(transactionInfo)) return CryptoCurrency.btcln; + if (isLightning(transactionInfo)) { + return CryptoCurrency.btcln; + } return switch (wallet.type) { WalletType.solana => solana!.assetOfTransaction(wallet, transactionInfo), @@ -316,26 +339,38 @@ abstract class TransactionDetailsViewModelBase with Store { }; } - // TODO integrate these getters with the TransactionInfo object + @computed + String get transactionAmount => + _appStore.amountParsingProxy.asDisplayStringWithSymbol(transactionInfo.amount); + + @computed + String get feeAmount => + _appStore.amountParsingProxy.asDisplayStringWithSymbol(transactionInfo.fee!); + + @computed + String get transactionCopyAmount => + _appStore.amountParsingProxy.asDisplayString(transactionInfo.amount); + + // TODO(malik1004x): integrate these getters with the TransactionInfo object String get formattedPendingStatus { switch (wallet.type) { case WalletType.monero: case WalletType.haven: case WalletType.zano: if (transactionInfo.confirmations >= 0 && transactionInfo.confirmations < 10) { - return ' (${transactionInfo.confirmations}/10)'; + return " (${transactionInfo.confirmations}/10)"; } break; case WalletType.wownero: if (transactionInfo.confirmations >= 0 && transactionInfo.confirmations < 3) { - return ' (${transactionInfo.confirmations}/3)'; + return " (${transactionInfo.confirmations}/3)"; } break; case WalletType.litecoin: - bool isPegIn = (transactionInfo.additionalInfo["isPegIn"] as bool?) ?? false; - bool isPegOut = (transactionInfo.additionalInfo["isPegOut"] as bool?) ?? false; - bool fromPegOut = (transactionInfo.additionalInfo["fromPegOut"] as bool?) ?? false; - String str = ''; + final isPegIn = (transactionInfo.additionalInfo["isPegIn"] as bool?) ?? false; + final isPegOut = (transactionInfo.additionalInfo["isPegOut"] as bool?) ?? false; + final fromPegOut = (transactionInfo.additionalInfo["fromPegOut"] as bool?) ?? false; + String str = ""; if (transactionInfo.confirmations <= 0) { str = S.current.pending; } @@ -352,10 +387,10 @@ abstract class TransactionDetailsViewModelBase with Store { } return str; default: - return ''; + return ""; } - return ''; + return ""; } String get formattedStatus { @@ -369,7 +404,7 @@ abstract class TransactionDetailsViewModelBase with Store { return formattedPendingStatus; } - return transactionInfo.isPending ? S.current.pending : ''; + return transactionInfo.isPending ? S.current.pending : ""; } int get neededConfirmations { @@ -381,9 +416,11 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.wownero: return 3; case WalletType.litecoin: - bool isPegOut = (transactionInfo.additionalInfo["isPegOut"] as bool?) ?? false; - bool fromPegOut = (transactionInfo.additionalInfo["fromPegOut"] as bool?) ?? false; - if (isPegOut || fromPegOut) return 6; + final isPegOut = (transactionInfo.additionalInfo["isPegOut"] as bool?) ?? false; + final fromPegOut = (transactionInfo.additionalInfo["fromPegOut"] as bool?) ?? false; + if (isPegOut || fromPegOut) { + return 6; + } default: return 0; } @@ -391,8 +428,11 @@ abstract class TransactionDetailsViewModelBase with Store { } String get formattedTitle { - if (transactionInfo.additionalInfo['autoShield'] == true) { - return "Autoshield"; + if (transactionInfo.additionalInfo['isIronwoodMigration'] == true) { + return 'Migration'; + } + if (transactionInfo.additionalInfo['isAutoShield'] == true) { + return S.current.shielding; } if (transactionInfo.direction == TransactionDirection.incoming) { return S.current.received; @@ -408,52 +448,56 @@ abstract class TransactionDetailsViewModelBase with Store { final txId = transactionInfo.txHash; if (wallet.chainId != null) { final explorerUrl = evm!.getExplorerUrlForChainId(wallet.chainId!); - if (explorerUrl != null) return '$explorerUrl/tx/${txId}'; + if (explorerUrl != null) { + return "$explorerUrl/tx/${txId}"; + } } switch (wallet.type) { case WalletType.monero: - return 'https://monero.com/tx/${txId}'; + return "https://monero.com/tx/${txId}"; case WalletType.bitcoin: - return 'https://mempool.cakewallet.com/${wallet.isTestnet ? "testnet/" : ""}tx/${txId}'; + return isLightning(transactionInfo) + ? "https://sparkscan.io/tx/${txId}" + : 'https://mempool.cakewallet.com/${wallet.isTestnet ? "testnet/" : ""}tx/${txId}'; case WalletType.litecoin: return bitcoin!.txIsMweb(transactionInfo) ? "https://www.mwebexplorer.com/blocks/block/${transactionInfo.height}" - : 'https://blockchair.com/litecoin/transaction/${txId}'; + : "https://blockchair.com/litecoin/transaction/${txId}"; case WalletType.bitcoinCash: - return 'https://blockchair.com/bitcoin-cash/transaction/${txId}'; + return "https://blockchair.com/bitcoin-cash/transaction/${txId}"; case WalletType.haven: - return 'https://explorer.havenprotocol.org/search?value=${txId}'; + return "https://explorer.havenprotocol.org/search?value=${txId}"; case WalletType.ethereum: - return 'https://etherscan.io/tx/${txId}'; + return "https://etherscan.io/tx/${txId}"; case WalletType.base: - return 'https://basescan.org/tx/${txId}'; + return "https://basescan.org/tx/${txId}"; case WalletType.arbitrum: - return 'https://arbiscan.io/tx/${txId}'; + return "https://arbiscan.io/tx/${txId}"; case WalletType.bsc: - return 'https://bscscan.com/tx/${txId}'; + return "https://bscscan.com/tx/${txId}"; case WalletType.polygon: - return 'https://polygonscan.com/tx/${txId}'; + return "https://polygonscan.com/tx/${txId}"; case WalletType.nano: - return 'https://nanexplorer.com/nano/block/${txId}'; + return "https://nanexplorer.com/nano/block/${txId}"; case WalletType.banano: - return 'https://nanexplorer.com/banano/block/${txId}'; + return "https://nanexplorer.com/banano/block/${txId}"; case WalletType.solana: - return 'https://solscan.io/tx/${txId}'; + return "https://solscan.io/tx/${txId}"; case WalletType.tron: - return 'https://tronscan.org/#/transaction/${txId}'; + return "https://tronscan.org/#/transaction/${txId}"; case WalletType.wownero: - return 'https://explore.wownero.com/tx/${txId}'; + return "https://explore.wownero.com/tx/${txId}"; case WalletType.zano: - return 'https://explorer.zano.org/transaction/${txId}'; + return "https://explorer.zano.org/transaction/${txId}"; case WalletType.decred: return 'https://${wallet.isTestnet ? "testnet" : "dcrdata"}.decred.org/tx/${txId.split(':')[0]}'; case WalletType.dogecoin: - return 'https://blockchair.com/dogecoin/transaction/${txId}'; + return "https://blockchair.com/dogecoin/transaction/${txId}"; case WalletType.zcash: - return 'https://blockchair.com/zcash/transaction/${txId}'; + return "https://blockchair.com/zcash/transaction/${txId}"; case WalletType.none: - return ''; + return ""; } } @@ -473,13 +517,17 @@ abstract class TransactionDetailsViewModelBase with Store { : transactionInfo.outputAddresses!.length; newFee = bitcoin!.getFeeAmountForPriority( - wallet, bitcoin!.getBitcoinTransactionPriorityMedium(), inputsCount, outputsCount); + wallet, + bitcoin!.getBitcoinTransactionPriorityMedium(), + inputsCount, + outputsCount, + ); - RBFListItems.add( + rbfListItems.add( StandartListItem( title: S.current.old_fee, - value: tx.fee?.toStringWithSymbol() ?? '0.0', - key: ValueKey('standard_list_item_rbf_old_fee_key'), + value: tx.fee?.toStringWithSymbol() ?? "0.0", + key: const ValueKey("standard_list_item_rbf_old_fee_key"), ), ); @@ -488,32 +536,33 @@ abstract class TransactionDetailsViewModelBase with Store { final recommendedRate = (transactionInfo.fee! / BigInt.from(size)) + transactionInfo.fee!.copyWith(amount: BigInt.one); - RBFListItems.add( - StandartListItem(title: 'New recommended fee rate', value: '$recommendedRate sat/byte')); + rbfListItems.add( + StandartListItem(title: "New recommended fee rate", value: "$recommendedRate sat/byte"), + ); } final priorities = priorityForWalletType(wallet.type); final selectedItem = priorities.indexOf(sendViewModel.feesViewModel.transactionPriority); final customItem = priorities.firstWhereOrNull( - (element) => element == sendViewModel.feesViewModel.bitcoinTransactionPriorityCustom); + (element) => element == sendViewModel.feesViewModel.bitcoinTransactionPriorityCustom, + ); final customItemIndex = customItem != null ? priorities.indexOf(customItem) : null; final maxCustomFeeRate = sendViewModel.feesViewModel.maxCustomFeeRate?.toDouble(); - RBFListItems.add( + rbfListItems.add( StandardPickerListItem( - key: ValueKey('standard_picker_list_item_transaction_priorities_key'), + key: const ValueKey("standard_picker_list_item_transaction_priorities_key"), title: S.current.estimated_new_fee, - value: bitcoin!.formatterBitcoinAmountToString(amount: newFee) + ' ${wallet.currency}', + value: "${bitcoin!.formatterBitcoinAmountToString(amount: newFee)} ${wallet.currency}", items: priorityForWalletType(wallet.type), customValue: _appStore.settingsStore.customBitcoinFeeRate.toDouble(), maxValue: maxCustomFeeRate, selectedIdx: selectedItem, customItemIndex: customItemIndex ?? 0, - displayItem: (dynamic priority, double sliderValue) => + displayItem: (dynamic priority, sliderValue) => sendViewModel.feesViewModel.displayFeeRate(priority, sliderValue.round()), - onSliderChanged: (double newValue) => - setNewFee(value: newValue, priority: transactionPriority!), - onItemSelected: (dynamic item, double sliderValue) { + onSliderChanged: (newValue) => setNewFee(value: newValue, priority: transactionPriority!), + onItemSelected: (dynamic item, sliderValue) { transactionPriority = item as TransactionPriority; return setNewFee(value: sliderValue, priority: transactionPriority!); }, @@ -521,9 +570,9 @@ abstract class TransactionDetailsViewModelBase with Store { ); if (transactionInfo.inputAddresses != null && transactionInfo.inputAddresses!.isNotEmpty) { - RBFListItems.add( + rbfListItems.add( StandardExpandableListItem( - key: ValueKey('standard_expandable_list_item_transaction_input_addresses_key'), + key: const ValueKey("standard_expandable_list_item_transaction_input_addresses_key"), title: S.current.inputs, expandableItems: transactionInfo.inputAddresses!, ), @@ -532,17 +581,17 @@ abstract class TransactionDetailsViewModelBase with Store { if (transactionInfo.outputAddresses != null && transactionInfo.outputAddresses!.isNotEmpty) { final outputAddresses = transactionInfo.outputAddresses!.map((element) { - if (element.contains('OP_RETURN:') && element.length > 40) { - return element.substring(0, 40) + '...'; + if (element.contains("OP_RETURN:") && element.length > 40) { + return "${element.substring(0, 40)}..."; } return element; }).toList(); - RBFListItems.add( + rbfListItems.add( StandardExpandableListItem( title: S.current.outputs, expandableItems: outputAddresses, - key: ValueKey('standard_expandable_list_item_transaction_output_addresses_key'), + key: const ValueKey("standard_expandable_list_item_transaction_output_addresses_key"), ), ); } @@ -552,7 +601,7 @@ abstract class TransactionDetailsViewModelBase with Store { Future _checkForRBF(TransactionInfo tx) async { if (wallet.type == WalletType.bitcoin && transactionInfo.direction == TransactionDirection.outgoing) { - final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}'; + final descriptionKey = "${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}"; final description = transactionDescriptionBox.values .firstWhereOrNull((val) => val.id == descriptionKey || val.id == transactionInfo.txHash); @@ -569,45 +618,23 @@ abstract class TransactionDetailsViewModelBase with Store { } } - String setNewFee({double? value, required TransactionPriority priority}) { + String setNewFee({required TransactionPriority priority, double? value}) { newFee = priority == bitcoin!.getBitcoinTransactionPriorityCustom() && value != null ? bitcoin!.feeAmountWithFeeRate( wallet, value.round(), transactionInfo.inputAddresses?.length ?? 1, - transactionInfo.outputAddresses?.length ?? 1) + transactionInfo.outputAddresses?.length ?? 1, + ) : bitcoin!.getFeeAmountForPriority( wallet, priority, transactionInfo.inputAddresses?.length ?? 1, - transactionInfo.outputAddresses?.length ?? 1); + transactionInfo.outputAddresses?.length ?? 1, + ); return bitcoin!.formatterBitcoinAmountToString(amount: newFee); } - String get formattedCryptoAmount { - if (wallet.type == WalletType.bitcoin) { - final crypto = isLightning(transactionInfo) ? CryptoCurrency.btcln : CryptoCurrency.btc; - final amount = _appStore.amountParsingProxy - .asDisplayString(transactionInfo.amount) - .withMaxDecimals(8) - .withLocalSeperator(_appStore.settingsStore.languageCode); - - return '$amount ${_appStore.amountParsingProxy.getCryptoSymbol(crypto)}'; - } - - return transactionInfo.amount.toStringWithSymbol(); - } - void replaceByFee(String newFee) => sendViewModel.replaceByFee(transactionInfo, newFee); - - @computed - String get pendingTransactionFiatAmountValueFormatted => sendViewModel.isFiatDisabled - ? '' - : sendViewModel.pendingTransactionFiatAmount + ' ' + sendViewModel.fiat.title; - - @computed - String get pendingTransactionFeeFiatAmountFormatted => sendViewModel.isFiatDisabled - ? '' - : sendViewModel.pendingTransactionFeeFiatAmount + ' ' + sendViewModel.fiat.title; } 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 65ee7aef8..0610381cb 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,59 +1,58 @@ -import 'dart:core'; -import 'dart:developer' as dev; +import "dart:core"; +import "dart:developer" as dev; -import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/core/address_resolver/yat/yat_store.dart'; -import 'package:cake_wallet/core/amount_parsing_proxy.dart'; -import 'package:cake_wallet/core/fiat_conversion_service.dart'; -import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; -import 'package:cake_wallet/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'; -import 'package:cake_wallet/evm/evm.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/reactions/wallet_utils.dart'; -import 'package:cake_wallet/solana/solana.dart'; -import 'package:cake_wallet/store/app_store.dart'; -import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; -import 'package:cake_wallet/tron/tron.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/view_model/wallet_address_list/wallet_address_util.dart'; -import 'package:cake_wallet/wownero/wownero.dart'; -import 'package:cake_wallet/zcash/zcash.dart'; -import 'package:cake_wallet/zano/zano.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/currency.dart'; -import 'package:cw_core/currency_for_wallet_type.dart'; -import 'package:cw_core/erc20_token.dart'; -import 'package:cw_core/payment_uris.dart'; -import 'package:cw_core/spl_token.dart'; -import 'package:cw_core/tron_token.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:mobx/mobx.dart'; +import "package:cake_wallet/bitcoin/bitcoin.dart"; +import "package:cake_wallet/core/address_resolver/yat/yat_store.dart"; +import "package:cake_wallet/core/amount_parsing_proxy.dart"; +import "package:cake_wallet/core/fiat_conversion_service.dart"; +import "package:cake_wallet/core/wallet_change_listener_view_model.dart"; +import "package:cake_wallet/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"; +import "package:cake_wallet/evm/evm.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/reactions/wallet_utils.dart"; +import "package:cake_wallet/solana/solana.dart"; +import "package:cake_wallet/store/app_store.dart"; +import "package:cake_wallet/store/dashboard/fiat_conversion_store.dart"; +import "package:cake_wallet/tron/tron.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/view_model/wallet_address_list/wallet_address_util.dart"; +import "package:cake_wallet/wownero/wownero.dart"; +import "package:cake_wallet/zano/zano.dart"; +import "package:cake_wallet/zcash/zcash.dart"; +import "package:cw_core/crypto_currency.dart"; +import "package:cw_core/currency.dart"; +import "package:cw_core/currency_for_wallet_type.dart"; +import "package:cw_core/erc20_token.dart"; +import "package:cw_core/payment_uris.dart"; +import "package:cw_core/spl_token.dart"; +import "package:cw_core/tron_token.dart"; +import "package:cw_core/wallet_type.dart"; +import "package:mobx/mobx.dart"; -part 'wallet_address_list_view_model.g.dart'; +part "wallet_address_list_view_model.g.dart"; class WalletAddressListViewModel = WalletAddressListViewModelBase with _$WalletAddressListViewModel; abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store { WalletAddressListViewModelBase({ - required AppStore appStore, + required super.appStore, required this.yatStore, required this.fiatConversionStore, }) : _baseItems = [], selectedCurrency = appStore.wallet!.currency, hasAccounts = [WalletType.monero, WalletType.wownero].contains(appStore.wallet!.type), _appStore = appStore, - receivePageOption = appStore.wallet!.walletAddresses.walletInfo.addressPageType ?? '', - super(appStore: appStore) { + receivePageOption = appStore.wallet!.walletAddresses.walletInfo.addressPageType ?? "" { _init(); } @@ -81,8 +80,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo CryptoCurrency? tokenCurrency; @computed - String get cryptoCurrencySymbol => - _appStore.amountParsingProxy.getCryptoSymbol(tokenCurrency ?? wallet.currency); + String get cryptoCurrencySymbol => _appStore.amountParsingProxy.getCryptoSymbol( + tokenCurrency ?? + (selectedCurrency is CryptoCurrency + ? selectedCurrency as CryptoCurrency + : wallet.currency), + ); void setTokenCurrency(Currency curr) { if (curr == wallet.currency || curr == CryptoCurrency.btcln) { @@ -122,7 +125,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo _appStore.amountParsingProxy.useSatoshi(selectedCurrency as CryptoCurrency); @observable - String searchText = ''; + String searchText = ""; @computed int get selectedCurrencyIndex => currencies.indexOf(selectedCurrency); @@ -131,30 +134,45 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo int get tokenCurrencyIndex => tokenCurrency == null ? 0 : tokenCurrencies.indexOf(tokenCurrency!); @observable - String _amount = ''; + String _amount = ""; @computed - String get displayAmount => _appStore.amountParsingProxy - .getDisplayCryptoAmount(_amount, tokenCurrency ?? wallet.currency); + String get displayAmount => _appStore.amountParsingProxy.getDisplayCryptoAmount( + _amount, + tokenCurrency ?? + (selectedCurrency is CryptoCurrency + ? selectedCurrency as CryptoCurrency + : wallet.currency), + ); // NOT PRECISE! just for display purposes. @computed String get fiatAmount { - if (_amount.isEmpty) return ""; + if (_amount.isEmpty) { + return ""; + } + var cryptoCurrency = tokenCurrency ?? wallet.currency; - if (cryptoCurrency == CryptoCurrency.btcln) cryptoCurrency = CryptoCurrency.btc; + if (cryptoCurrency == CryptoCurrency.btcln) { + cryptoCurrency = CryptoCurrency.btc; + } + if (selectedCurrency is FiatCurrency && _fiatRate != null) { return selectedCurrencyFiatAmount; } - if (!fiatConversionStore.prices.containsKey(cryptoCurrency)) return ""; + if (!fiatConversionStore.prices.containsKey(cryptoCurrency)) { + return ""; + } final amount = double.tryParse(_amount) ?? 0; return (amount * fiatConversionStore.prices[cryptoCurrency]!).toStringAsFixed(2); } @computed String get selectedCurrencyFiatAmount { - if (_fiatRate == null) return ""; + if (_fiatRate == null) { + return ""; + } final amount = double.tryParse(_amount) ?? 0; return (amount * _fiatRate!).toStringAsFixed(2); } @@ -201,26 +219,29 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo PaymentURI get uri { if (tokenCurrency != null && isEVMCompatibleChain(wallet.type)) { return ERC681URI( - chainId: wallet.chainId ?? 1, - address: wallet.walletAddresses.address, - amount: _amount, - contractAddress: (tokenCurrency as Erc20Token).contractAddress); + chainId: wallet.chainId ?? 1, + address: wallet.walletAddresses.address, + amount: _amount, + contractAddress: (tokenCurrency! as Erc20Token).contractAddress, + ); } if (tokenCurrency is TronToken && wallet.type == WalletType.tron) { return TronURI( amount: _amount, address: wallet.walletAddresses.address, - contractAddress: (tokenCurrency as TronToken).contractAddress, + contractAddress: (tokenCurrency! as TronToken).contractAddress, ); } if (tokenCurrency is SPLToken && wallet.type == WalletType.solana) { return SolanaURI( amount: _amount, address: wallet.walletAddresses.address, - contractAddress: (tokenCurrency as SPLToken).mintAddress, + contractAddress: (tokenCurrency! as SPLToken).mintAddress, ); } - if (isLightning && _lnPaymentRequest != null) return _lnPaymentRequest!; + if (isLightning && _lnPaymentRequest != null) { + return _lnPaymentRequest!; + } return wallet.walletAddresses.getPaymentUri(_amount); } @@ -257,10 +278,11 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo final isPrimary = subaddress == primaryAddress; return WalletAddressListItem( - id: subaddress.id, - isPrimary: isPrimary, - name: subaddress.label, - address: subaddress.address); + id: subaddress.id, + isPrimary: isPrimary, + name: subaddress.label, + address: subaddress.address, + ); }); addressList.addAll(addressItems); } @@ -285,37 +307,37 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo addressList.addAll(addressItems); addressList.add(WalletAddressListHeader(title: S.current.received)); - final receivedAddressItems = - bitcoin!.getSilentPaymentReceivedAddresses(wallet).map((address) { - return WalletAddressListItem( - id: address.id, - isPrimary: false, - name: address.name, - address: address.address, - txCount: address.txCount, - balance: _appStore.amountParsingProxy - .getDisplayCryptoString(address.balance, walletTypeToCryptoCurrency(type)), - isChange: address.isChange, - isOneTimeReceiveAddress: true, - derivationPath: address.derivationPath, - ); - }); + final receivedAddressItems = bitcoin!.getSilentPaymentReceivedAddresses(wallet).map( + (address) => WalletAddressListItem( + id: address.id, + isPrimary: false, + name: address.name, + address: address.address, + txCount: address.txCount, + balance: _appStore.amountParsingProxy + .getDisplayCryptoString(address.balance, walletTypeToCryptoCurrency(type)), + isChange: address.isChange, + isOneTimeReceiveAddress: true, + derivationPath: address.derivationPath, + ), + ); addressList.addAll(receivedAddressItems); } else { var addressItems = bitcoin!.getSubAddresses(wallet).map((subaddress) { final isPrimary = subaddress.id == 0; return WalletAddressListItem( - id: subaddress.id, - isPrimary: isPrimary, - name: subaddress.name, - address: subaddress.address, - txCount: subaddress.txCount, - balance: _appStore.amountParsingProxy - .getDisplayCryptoString(subaddress.balance, walletTypeToCryptoCurrency(type)), - isChange: subaddress.isChange, - isLegacyDerivation: subaddress.isLegacyDerivation, - derivationPath: subaddress.derivationPath); + id: subaddress.id, + isPrimary: isPrimary, + name: subaddress.name, + address: subaddress.address, + txCount: subaddress.txCount, + balance: _appStore.amountParsingProxy + .getDisplayCryptoString(subaddress.balance, walletTypeToCryptoCurrency(type)), + isChange: subaddress.isChange, + isLegacyDerivation: subaddress.isLegacyDerivation, + derivationPath: subaddress.derivationPath, + ); }); // don't show all 1000+ mweb addresses: @@ -346,11 +368,13 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo } if (wallet.type == WalletType.nano) { - addressList.add(WalletAddressListItem( - isPrimary: true, - name: null, - address: wallet.walletAddresses.address, - )); + addressList.add( + WalletAddressListItem( + isPrimary: true, + name: null, + address: wallet.walletAddresses.address, + ), + ); } if (wallet.type == WalletType.tron) { @@ -361,29 +385,35 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo if (wallet.type == WalletType.decred) { final addrInfos = decred!.getAddressInfos(wallet); - addrInfos.forEach((info) { + for (final info in addrInfos) { addressList.add( - new WalletAddressListItem(isPrimary: false, address: info.address, name: info.label)); - }); + WalletAddressListItem(isPrimary: false, address: info.address, name: info.label), + ); + } } if (wallet.type == WalletType.zcash) { final addrInfos = zcash!.getAddressInfos(wallet); - addrInfos.forEach((info) { + for (final info in addrInfos) { addressList.add( - new WalletAddressListItem(isPrimary: false, address: info.address, name: info.label)); - }); + WalletAddressListItem(isPrimary: false, address: info.address, name: info.label), + ); + } } for (var i = 0; i < addressList.length; i++) { - if (!(addressList[i] is WalletAddressListItem)) continue; + if (addressList[i] is! WalletAddressListItem) { + continue; + } final item = addressList[i] as WalletAddressListItem; item.isHidden = wallet.walletAddresses.hiddenAddresses.contains(item.address) || (isElectrumWallet && item.isLegacyDerivation); } for (var i = 0; i < addressList.length; i++) { - if (!(addressList[i] is WalletAddressListItem)) continue; + if (addressList[i] is! WalletAddressListItem) { + continue; + } (addressList[i] as WalletAddressListItem).isManual = wallet.walletAddresses.manualAddresses .contains((addressList[i] as WalletAddressListItem).address); } @@ -395,25 +425,25 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo } if (searchText.isNotEmpty) { - return ObservableList.of(addressList.where((item) { - if (item is WalletAddressListItem) { - return item.address.toLowerCase().contains(searchText.toLowerCase()); - } - return false; - })); + return ObservableList.of( + addressList.where((item) { + if (item is WalletAddressListItem) { + return item.address.toLowerCase().contains(searchText.toLowerCase()); + } + return false; + }), + ); } return addressList; } @computed - ObservableList get addressList { - return _computeAddressList(); - } + ObservableList get addressList => _computeAddressList(); List get forceRecomputeItems { // necessary because the addressList contains non-observable items - List recomputed = []; + final recomputed = []; recomputed.addAll(_baseItems); recomputed.addAll(_computeAddressList()); return recomputed; @@ -451,9 +481,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo case WalletType.wownero: wownero!.getCurrentAccount(wallet).label; default: - return ''; + return ""; } - return ''; + return ""; } @computed @@ -473,7 +503,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo WalletType.litecoin, WalletType.decred, WalletType.dogecoin, - WalletType.zcash + WalletType.zcash, ].contains(wallet.type) && !isLightning && isZCashTransparent; @@ -486,7 +516,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash, - WalletType.dogecoin + WalletType.dogecoin, ].contains(wallet.type); List getWalletImages(int? chainId) { @@ -494,44 +524,44 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo switch (chainId) { case 1: return [ - 'assets/new-ui/crypto_full_icons/ethereum.svg', - 'assets/images/usdc_icon.svg', - 'assets/images/usdt_wallet_icon.svg', - 'assets/images/deuro_icon.svg', - 'assets/images/more_tokens.svg', + "assets/new-ui/crypto_full_icons/ethereum.svg", + "assets/images/usdc_icon.svg", + "assets/images/usdt_wallet_icon.svg", + "assets/images/deuro_icon.svg", + "assets/images/more_tokens.svg", ]; case 137: return [ - 'assets/new-ui/crypto_full_icons/polygon.svg', - 'assets/images/eth_pol_icon.svg', - 'assets/images/usdc_icon.svg', - 'assets/images/usdt_wallet_icon.svg', - 'assets/images/more_tokens.svg', + "assets/new-ui/crypto_full_icons/polygon.svg", + "assets/images/eth_pol_icon.svg", + "assets/images/usdc_icon.svg", + "assets/images/usdt_wallet_icon.svg", + "assets/images/more_tokens.svg", ]; case 8453: return [ - 'assets/new-ui/crypto_full_icons/ethereum.svg', - 'assets/images/usdc_icon.svg', - 'assets/images/more_tokens.svg', + "assets/new-ui/crypto_full_icons/ethereum.svg", + "assets/images/usdc_icon.svg", + "assets/images/more_tokens.svg", ]; case 42161: return [ - 'assets/new-ui/crypto_full_icons/arbitrum.svg', - 'assets/images/usdc_icon.svg', - 'assets/images/more_tokens.svg', + "assets/new-ui/crypto_full_icons/arbitrum.svg", + "assets/images/usdc_icon.svg", + "assets/images/more_tokens.svg", ]; case 56: return [ - 'assets/new-ui/crypto_full_icons/bnb.svg', - 'assets/images/usdc_icon.svg', - 'assets/images/usdt_wallet_icon.svg', - 'assets/images/more_tokens.svg', + "assets/new-ui/crypto_full_icons/bnb.svg", + "assets/images/usdc_icon.svg", + "assets/images/usdt_wallet_icon.svg", + "assets/images/more_tokens.svg", ]; default: return [ - 'assets/new-ui/crypto_full_icons/ethereum.svg', - 'assets/images/usdc_icon.svg', - 'assets/images/usdt_wallet_icon.svg', + "assets/new-ui/crypto_full_icons/ethereum.svg", + "assets/images/usdc_icon.svg", + "assets/images/usdt_wallet_icon.svg", ]; } } @@ -539,22 +569,22 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo switch (wallet.type) { case WalletType.solana: return [ - 'assets/images/sol_icon.svg', - 'assets/images/usdc_icon.svg', - 'assets/images/usdt_wallet_icon.svg', - 'assets/images/more_tokens.svg', + "assets/images/sol_icon.svg", + "assets/images/usdc_icon.svg", + "assets/images/usdt_wallet_icon.svg", + "assets/images/more_tokens.svg", ]; case WalletType.tron: return [ - 'assets/images/trx_icon.svg', - 'assets/images/usdc_icon.svg', - 'assets/images/usdt_wallet_icon.svg', - 'assets/images/more_tokens.svg', + "assets/images/trx_icon.svg", + "assets/images/usdc_icon.svg", + "assets/images/usdt_wallet_icon.svg", + "assets/images/more_tokens.svg", ]; case WalletType.zano: return [ - 'assets/images/zano_icon.svg', - 'assets/images/more_tokens.svg', + "assets/images/zano_icon.svg", + "assets/images/more_tokens.svg", ]; default: return []; @@ -563,7 +593,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @computed String get qrImage { - if (isLightning) return 'assets/images/btc_chain_qr_lightning.svg'; + if (isLightning) { + return "assets/images/btc_chain_qr_lightning.svg"; + } return getQrImage(type); } @@ -582,8 +614,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @computed bool get isLightning => - wallet.type == WalletType.bitcoin && - (wallet.walletAddresses.getPaymentUri(_amount) is LightningPaymentRequest); + wallet.type == WalletType.bitcoin && selectedCurrency == CryptoCurrency.btcln; @computed bool get isZCashTransparent { @@ -698,16 +729,14 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @action void changeAmount(String amount) { if (selectedCurrency is FiatCurrency) { - this._amount = amount; + _amount = amount; _convertAmountToCrypto(); } else if (selectedCurrency is CryptoCurrency) { - this._amount = _appStore.amountParsingProxy + _amount = _appStore.amountParsingProxy .getCanonicalCryptoAmount(amount, selectedCurrency as CryptoCurrency); } if (isLightning) { - wallet.walletAddresses - .getPaymentRequestUri(this._amount) - .then((uri) => _lnPaymentRequest = uri); + wallet.walletAddresses.getPaymentRequestUri(_amount).then((uri) => _lnPaymentRequest = uri); } } @@ -719,22 +748,24 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @action void _convertAmountToCrypto() { var cryptoCurrency = tokenCurrency ?? wallet.currency; - if (cryptoCurrency == CryptoCurrency.btcln) cryptoCurrency = CryptoCurrency.btc; + if (cryptoCurrency == CryptoCurrency.btcln) { + cryptoCurrency = CryptoCurrency.btc; + } final fiatRate = _fiatRate ?? (fiatConversionStore.prices[cryptoCurrency] ?? 0.0); if (fiatRate <= 0.0) { dev.log("Invalid Fiat Rate $fiatRate"); - _amount = ''; + _amount = ""; return; } try { - final crypto = (double.parse(_amount.replaceAll(',', '.')) / fiatRate).toStringAsFixed(8); + final crypto = (double.parse(_amount.replaceAll(",", ".")) / fiatRate).toStringAsFixed(8); if (_amount != crypto) { _amount = crypto; } } catch (e) { - _amount = ''; + _amount = ""; } } diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 321a331ae..eeb2bd476 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -17,6 +17,7 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:cake_wallet/zcash/zcash_network_type.dart'; import 'package:mobx/mobx.dart'; import 'package:polyseed/polyseed.dart'; @@ -33,9 +34,13 @@ abstract class WalletCreationVMBase with Store { @observable bool _useTestnet = false; + int _zcashNetwork = ZcashNetworkType.mainnet; + @computed bool get useTestnet => _useTestnet; + int get zcashNetwork => _zcashNetwork; + @observable String name; @@ -252,4 +257,8 @@ abstract class WalletCreationVMBase with Store { void toggleUseTestnet(bool? value) { _useTestnet = value ?? !_useTestnet; } + + void setZcashNetwork(int network) { + _zcashNetwork = network; + } } diff --git a/lib/view_model/wallet_hardware_restore_view_model.dart b/lib/view_model/wallet_hardware_restore_view_model.dart index 37f6467d3..35de089c8 100644 --- a/lib/view_model/wallet_hardware_restore_view_model.dart +++ b/lib/view_model/wallet_hardware_restore_view_model.dart @@ -6,13 +6,13 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; -import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; @@ -43,6 +43,8 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with @observable String? error = null; + bool get passphraseAvailable => hardwareWalletVM.hardwareWalletType == HardwareWalletType.trezor; + // @observable ObservableList availableAccounts = ObservableList(); @@ -87,6 +89,7 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with hardwareWalletService: hardwareWalletVM.getHardwareWalletService(type), password: password, height: _options['height'] as int? ?? 0, + passphrase: _options['passphrase'] as String?, ); default: throw Exception('Unexpected type: ${type.toString()}'); diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index cb2f3ed7f..f7fa7c7c7 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -146,6 +146,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { password: walletPassword, mnemonic: newWalletArguments!.mnemonic, passphrase: passphrase, + network: zcashNetwork, ); case WalletType.decred: return decred!.createDecredNewWalletCredentials(name: name); diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 0bc32341b..467ad065a 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -108,6 +108,10 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { @observable WalletRestoreMode mode; + @computed + bool get passphraseAvailable => + mode == WalletRestoreMode.seed || hardwareWalletType == HardwareWalletType.trezor; + @observable bool hasPassphrase; @@ -220,6 +224,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { password: password, passphrase: passphrase, height: height, + network: zcashNetwork, ); case WalletType.none: case WalletType.haven: diff --git a/lib/zcash/cw_zcash.dart b/lib/zcash/cw_zcash.dart index ee6505fb1..51c9dc53e 100644 --- a/lib/zcash/cw_zcash.dart +++ b/lib/zcash/cw_zcash.dart @@ -7,12 +7,14 @@ class CWZcash extends Zcash { WalletInfo? walletInfo, String? password, String? mnemonic, - required String? passphrase}) { + required String? passphrase, + int network = 0}) { return ZcashNewWalletCredentials( name: name, passphrase: passphrase, password: password, mnemonic: mnemonic, + network: network, ); } @@ -36,9 +38,10 @@ class CWZcash extends Zcash { required String mnemonic, required String password, String? passphrase, - required int? height}) { + required int? height, + int network = 0}) { return ZcashFromSeedWalletCredentials( - name: name, seed: mnemonic, passphrase: passphrase, password: password, height: height); + name: name, seed: mnemonic, passphrase: passphrase, password: password, height: height, network: network); } @override @@ -161,9 +164,12 @@ class CWZcash extends Zcash { @override ReceivePageOption getSelectedAddressType(Object wallet) { final zcashWallet = wallet as ZcashWallet; - final t = - (zcashWallet.walletAddresses as ZcashWalletAddresses).walletInfo.addressPageType ?? ""; - return ZcashReceivePageOption.fromType(ZcashReceivePageOption.typeFromString(t)); + final addresses = zcashWallet.walletAddresses as ZcashWalletAddresses; + final type = ZcashReceivePageOption.typeFromString(addresses.walletInfo.addressPageType ?? ""); + if (type == ZcashAddressType.shieldedOrchard) { + return ZcashReceivePageOption.shieldedOrchard(ironwood: addresses.ironwoodActive); + } + return ZcashReceivePageOption.fromType(type); } bool hasSelectedTransparentAddress(Object wallet) { @@ -172,18 +178,7 @@ class CWZcash extends Zcash { @override dynamic getZcashAddressType(ReceivePageOption option) { - switch (option) { - case ZcashReceivePageOption.unified: - return ZcashAddressType.unifiedType; - case ZcashReceivePageOption.transparent: - return ZcashAddressType.transparent; - case ZcashReceivePageOption.shieldedSapling: - return ZcashAddressType.shieldedSapling; - case ZcashReceivePageOption.shieldedOrchard: - return ZcashAddressType.shieldedOrchard; - default: - throw Exception("Unknown ReceivePageOption!"); - } + return (option as ZcashReceivePageOption).toType(); } @override @@ -209,15 +204,19 @@ class CWZcash extends Zcash { } @override - bool showMissingFundsCard(WalletBase wallet) { - final zcashWallet = wallet as ZcashWallet; - return zcashWallet.couldBeZashiWallet(); + bool showMissingFundsCard(WalletBase wallet) => false; + + @override + Future rescanInternalChange(WalletBase wallet) async {} + + @override + bool ironwoodActive(WalletAddresses walletAddresses) { + return (walletAddresses as ZcashWalletAddresses).ironwoodActive; } @override - Future rescanInternalChange(WalletBase wallet) { - final zcashWallet = wallet as ZcashWallet; - return zcashWallet.rescanInternalChange(); + Future hasOrchardMigratableBalance(WalletBase wallet) { + return (wallet as ZcashWallet).hasOrchardMigratableBalance(); } } diff --git a/lib/zcash/zcash_network_type.dart b/lib/zcash/zcash_network_type.dart new file mode 100644 index 000000000..4508711d5 --- /dev/null +++ b/lib/zcash/zcash_network_type.dart @@ -0,0 +1,17 @@ +/// Zcash network indices passed from lib/ into cw_zcash. +abstract final class ZcashNetworkType { + static const int mainnet = 0; + static const int testnet = 1; + static const int regtest = 2; + + static const List values = [mainnet, testnet, regtest]; + + static String label(final int network) => switch (network) { + testnet => 'Testnet', + regtest => 'Regtest', + _ => 'Mainnet', + }; + + static bool isDevNetwork(final String? network) => + network?.toLowerCase() == 'testnet' || network?.toLowerCase() == 'regtest'; +} diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 594c53e5b..6c07ce789 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,12 +1,10 @@ PODS: - bitbox_flutter (0.0.1): - FlutterMacOS - - breez_sdk_spark_flutter (0.11.0): + - breez_sdk_spark_flutter (0.14.0): - FlutterMacOS - connectivity_plus (0.0.1): - FlutterMacOS - - cw_decred (0.0.1): - - FlutterMacOS - cw_mweb (0.0.1): - FlutterMacOS - device_info_plus (0.0.1): @@ -30,19 +28,9 @@ PODS: - FlutterMacOS (1.0.0) - in_app_review (2.0.0): - FlutterMacOS - - local_auth_darwin (0.0.1): - - Flutter - - FlutterMacOS - OrderedSet (6.0.3) - package_info_plus (0.0.1): - FlutterMacOS - - path_provider_foundation (0.0.1): - - Flutter - - FlutterMacOS - - rive_common (0.0.1): - - FlutterMacOS - - rlz (0.0.1): - - FlutterMacOS - share_plus (0.0.1): - FlutterMacOS - shared_preferences_foundation (0.0.1): @@ -92,7 +80,6 @@ DEPENDENCIES: - bitbox_flutter (from `Flutter/ephemeral/.symlinks/plugins/bitbox_flutter/macos`) - breez_sdk_spark_flutter (from `Flutter/ephemeral/.symlinks/plugins/breez_sdk_spark_flutter/macos`) - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) - - cw_decred (from `Flutter/ephemeral/.symlinks/plugins/cw_decred/macos`) - cw_mweb (from `Flutter/ephemeral/.symlinks/plugins/cw_mweb/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - devicelocale (from `Flutter/ephemeral/.symlinks/plugins/devicelocale/macos`) @@ -105,11 +92,7 @@ DEPENDENCIES: - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`) - - local_auth_darwin (from `Flutter/ephemeral/.symlinks/plugins/local_auth_darwin/darwin`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - - rive_common (from `Flutter/ephemeral/.symlinks/plugins/rive_common/macos`) - - rlz (from `Flutter/ephemeral/.symlinks/plugins/rlz/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - sp_scanner (from `Flutter/ephemeral/.symlinks/plugins/sp_scanner/macos`) @@ -132,8 +115,6 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/breez_sdk_spark_flutter/macos connectivity_plus: :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos - cw_decred: - :path: Flutter/ephemeral/.symlinks/plugins/cw_decred/macos cw_mweb: :path: Flutter/ephemeral/.symlinks/plugins/cw_mweb/macos device_info_plus: @@ -158,16 +139,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral in_app_review: :path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos - local_auth_darwin: - :path: Flutter/ephemeral/.symlinks/plugins/local_auth_darwin/darwin package_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos - path_provider_foundation: - :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin - rive_common: - :path: Flutter/ephemeral/.symlinks/plugins/rive_common/macos - rlz: - :path: Flutter/ephemeral/.symlinks/plugins/rlz/macos share_plus: :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos shared_preferences_foundation: @@ -188,38 +161,33 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos SPEC CHECKSUMS: - bitbox_flutter: 16088f5c7febacf894f4d07bbdfb441c9f0757fe - breez_sdk_spark_flutter: 2e864a52e699546aedea3d64d0e0cc88bc0ac595 - connectivity_plus: 0a976dfd033b59192912fa3c6c7b54aab5093802 - cw_decred: 0c93fbeb31bd97a6ad4ec5680960af0943bfca78 - cw_mweb: 7440b12ead811dda972a9918442ea2a458e8742c - device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f - devicelocale: 9f0f36ac651cabae2c33f32dcff4f32b61c38225 + bitbox_flutter: b3c6d3c4970b134a78a17fddef0ef429cfec2593 + breez_sdk_spark_flutter: 0da35c86276ec740d938139262e198a475586548 + connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e + cw_mweb: 4746a3b5cc5bd4afe6fadd2481c4f7b239e391d0 + device_info_plus: b0fafc687fb901e2af612763340f1b0d4352f8e5 + devicelocale: 456a07c045d3113938aa4983065bd8526e2af792 dnssec_proof: d461cac7bd3301eb7447f87936745a0c1ae0a67e - fast_scanner: d31bae07e2653403a69dac99fb710c1722b16a97 - file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d - flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b - flutter_local_authentication: 85674893931e1c9cfa7c9e4f5973cb8c56b018b0 - flutter_local_notifications: 4ccab5b7a22835214a6672e3f9c5e8ae207dab36 - flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea - FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - in_app_review: 866c9b17c87a7b46a395bda43f5d3ca02deb585a - local_auth_darwin: fa4b06454df7df8e97c18d7ee55151c57e7af0de + fast_scanner: a4ead8e56f185cd565e25250fdb97fa669ea1d2e + file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7 + flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d + flutter_local_authentication: 2f9a2682f498abcc12d7e9729b5007a947170fdc + flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0 + flutter_secure_storage_macos: 3dacac420d84c4d0dc5e868194ee43c8664ec797 + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + in_app_review: 66e7680752b632d83f4f0e88b34d52ed303fbff4 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 - package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - rive_common: 9580b9f1f08a8c85006083078e80479edf3db26a - rlz: fa94ecbcb2ca519363c320ec7a0f22525a536b59 - share_plus: 1fa619de8392a4398bfaf176d441853922614e89 - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sp_scanner: 269d96e0ec3173e69156be7239b95182be3b8303 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + package_info_plus: f0052d280d17aa382b932f399edf32507174e870 + share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + sp_scanner: d9316427b628dd86d6e9e3c5d42ed6b4b410157c + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b - sqlite3_flutter_libs: 86f82662868ee26ff3451f73cac9c5fc2a1f57fa - torch_dart: 2e94855e15fcc87bd824e179f1f9c3e0f1e1d302 - universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6 - url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 - wakelock_plus: 9d63063ffb7af1c215209769067c57103bde719d + sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1 + torch_dart: 7e4f682c34ee0d967a35977102590094a7510c90 + universal_ble: a322ebabee64f0ec27a313e89a5dd6967f37a60f + url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd + wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b PODFILE CHECKSUM: 65ec1541137fb5b35d00490dec1bb48d4d9586bb diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index fec311d11..84a01065a 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -433,7 +433,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -521,7 +521,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -568,7 +568,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/pubspec_base.yaml b/pubspec_base.yaml index afe2d64be..dc0b0bf62 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -268,8 +268,6 @@ flutter: - assets/text/ - assets/faq/ - assets/animation/ - - scripts/zcash_lib/assets/sapling-spend.params - - scripts/zcash_lib/assets/sapling-output.params - assets/new-ui/ - assets/new-ui/icons/ - assets/new-ui/hero/ diff --git a/res/pictures/ironwood_migration.svg b/res/pictures/ironwood_migration.svg new file mode 100644 index 000000000..114e7cc1c --- /dev/null +++ b/res/pictures/ironwood_migration.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/pictures/settings_row_icons/sync-balance.svg b/res/pictures/settings_row_icons/sync-balance.svg new file mode 100644 index 000000000..fc3274a66 --- /dev/null +++ b/res/pictures/settings_row_icons/sync-balance.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 125537f7b..c74f1a2ab 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -76,6 +76,7 @@ "approve_tokens": "الموافقة على الرموز", "apps": "التطبيقات", "arbiscan_history": "سجل ArbiScan", + "are_you_sure_exit": "هل أنت متأكد أنك تريد الخروج؟", "arrive_in_this_address": "سيصل ${currency} ${tag} إلى هذا العنوان", "ascending": "تصاعديًا", "ask_each_time": "اسأل في كل مرة", @@ -89,6 +90,7 @@ "auto_generate_addresses": "إنشاء العناوين تلقائيًا", "auto_generate_subaddresses": "إنشاء العناوين الفرعية تلقائيًا", "automatic": "تلقائي", + "available": "متاح", "available_balance": "الرصيد المتاح", "available_balance_description": "يشير “Available Balance” أو “Confirmed Balance” إلى الأموال التي يمكن إنفاقها فورًا. إذا ظهرت الأموال في الرصيد السفلي ولكن ليس في الرصيد العلوي، فعليك الانتظار بضع دقائق حتى تحصل الأموال الواردة على المزيد من تأكيدات الشبكة. بعد حصولها على المزيد من التأكيدات، ستصبح قابلة للإنفاق.", "avg_savings": "متوسط التوفير", @@ -125,6 +127,7 @@ "block_remaining": "كتلة واحدة متبقية", "Blocks_remaining": "${status} كتل متبقية", "bluetooth": "بلوتوث", + "bridging": "جسر العملات", "bright_theme": "ساطع", "bscscan_history": "سجل BSCScan", "bump_fee": "زيادة الرسوم", @@ -216,6 +219,8 @@ "color_theme": "سمة الألوان", "coming_soon_tag": "قريبًا", "commit_transaction_amount_fee": "تأكيد المعاملة\nالمبلغ: ${amount}\nالرسوم: ${fee}", + "compatible_services": "الخدمات المتوافقة", + "compatible_services_desc": "استخدم أداة المسح لقراءة الأنواع التالية من رموز QR المدعومة في Cake Wallet:", "configure": "تهيئة", "confirm": "تأكيد", "confirm_delete_template": "سيؤدي هذا الإجراء إلى حذف هذا القالب. هل ترغب في المتابعة؟", @@ -279,6 +284,7 @@ "creating_new_wallet": "جارٍ إنشاء محفظة جديدة", "creating_new_wallet_error": "خطأ: ${description}", "creation_date": "تاريخ الإنشاء", + "crypto_addresses": "عناوين العملات المشفرة", "csv_nothing_to_export": "لا يوجد شيء للتصدير", "current_swap": "المبادلة الحالية", "current_wallet": "المحفظة الحالية", @@ -301,6 +307,7 @@ "default_sell_provider": "مزود البيع الافتراضي", "delete": "حذف", "delete_account": "حذف الحساب", + "delete_this_node": "حذف هذه العقدة", "delete_wallet": "حذف المحفظة", "delete_wallet_confirm_message": "هل أنت متأكد أنك تريد حذف محفظة ${wallet_name}؟", "deleteConnectionConfirmationPrompt": "هل أنت متأكد أنك تريد حذف الاتصال بـ", @@ -330,6 +337,7 @@ "deuro_savings_welcome_description": "dEURO هو عملة مستقرة لامركزية بالكامل، مضمونة بضمانات ومربوطة باليورو — آمنة، متعددة الاستخدامات، ومقاومة للرقابة دون الحاجة إلى الاعتماد على الأوراكل. 1 dEURO = 1 EURO.", "deuro_tooltip_no_eth": "لتفعيل dEURO، يجب أن يكون لديك ETH في محفظتك.", "deuro_tx_commited_content": "قد يستغرق الأمر بضع ثوانٍ حتى يتم تأكيد المعاملة وتنعكس على الشاشة", + "device_confirmation": "تأكيد الجهاز", "device_is_signing": "الجهاز يقوم بالتوقيع", "dfx_option_description": "اشترِ العملات المشفرة بـ EUR و CHF. للعملاء الأفراد والشركات في أوروبا", "did_you_back_up_seeds": "هل قمت بعمل نسخة احتياطية لجميع عبارات الاسترداد الخاصة بك؟", @@ -466,6 +474,8 @@ "extracted_address_content": "ستقوم بإرسال الأموال إلى\n${recipient_name}", "failed_authentication": "فشلت المصادقة. ${state_error}", "faq": "الأسئلة الشائعة", + "favorite_token": "التوكن المفضل", + "favorite_token_desc": "سيظهر رصيد التوكن المفضل على بطاقة الرصيد.", "features": "الميزات", "fee": "رسوم", "fee_less_than_min": "الرسوم المحددة أقل من الحد الأدنى. يُرجى زيادة الرسوم لتتمكن من إرسال المعاملة", @@ -491,6 +501,7 @@ "frozen": "مجمَّد", "frozen_balance": "الرصيد المجمّد", "full_balance": "الرصيد الكامل", + "gallery": "المعرض", "gas_exceeds_allowance": "الغاز المطلوب للمعاملة يتجاوز الحدّ المسموح به.", "gas_price": "سعر الغاز", "general": "عام", @@ -526,10 +537,10 @@ "how_to_connect": "كيفية الاتصال", "how_to_use": "كيفية الاستخدام", "how_to_use_card": "كيفية استخدام هذه البطاقة", + "hww_exit_desc": "سيؤدي هذا إلى إنهاء عملية الإقران الجارية مع جهازك الصلب", "i_understand": "أفهم", "icon_style": "نمط الأيقونة", "id": "المعرّف: ", - "if_you_dont_see_your_device": "إذا لم يظهر جهازك أعلاه، يُرجى التأكد من أن Ledger قيد التشغيل ومفتوح القفل!", "ignor": "تجاهل", "import": "استيراد", "importNFTs": "استيراد رموز NFT", @@ -541,6 +552,7 @@ "infobox_auto_address": "سيدور عنوان الاستلام الخاص بك تلقائيًا في كل مرة تستلم فيها معاملة", "infobox_multichain": "استخدم هذا العنوان لاستلام أي توكن أو مقتنى على", "infobox_rotation": "سيتم تدوير عنوان الاستلام الخاص بك في كل مرة تُغلق فيها هذه الشاشة وتعيد فتحها", + "input": "إدخال", "inputs": "المدخلات", "insufficient_funds_for_tx": "أموال غير كافية لتنفيذ المعاملة بنجاح.", "insufficient_lamport_for_tx": "ليس لديك ما يكفي من SOL لتغطية المعاملة ورسومها. يُرجى إضافة المزيد من SOL إلى محفظتك أو تقليل مبلغ SOL الذي ترسله.", @@ -606,6 +618,7 @@ "manage_pow_nodes": "إدارة عُقَد PoW", "manage_providers": "إدارة المزوّدين", "manage_yats": "إدارة Yats", + "manual_input": "إدخال يدوي", "mark_as_redeemed": "وضع علامة كمسترد", "market_place": "المتجر", "mask": "قناع", @@ -651,6 +664,7 @@ "nanogpt_subtitle": "جميع أحدث النماذج (GPT-5.3، Claude 4.6).\nبدون اشتراك، ادفع بالعملات المشفرة.", "narrow": "ضيق", "network": "الشبكة", + "networks": "الشبكات", "new_first_wallet_text": "الحفاظ على أمان عملاتك المشفرة سهل للغاية", "new_node_testing": "اختبار العقدة الجديدة", "new_subaddress_create": "إنشاء", @@ -679,8 +693,10 @@ "node_new": "عقدة جديدة", "node_port": "منفذ العقدة", "node_reset_settings_title": "إعادة تعيين الإعدادات", + "node_share_desc": "امسح رمز QR هذا من شاشة المسح لاستيراد هذه العقدة.", "node_test": "اختبار", "nodes": "العُقَد", + "nodes_desc": "إدارة العقد الموجودة أو إضافة عقدة مخصصة.", "nodes_list_reset_to_default_message": "هل أنت متأكد أنك تريد إعادة تعيين الإعدادات إلى الإعدادات الافتراضية؟", "none_of_selected_providers_can_exchange": "لا يمكن لأيٍّ من المزوّدين المحدّدين إجراء هذه المبادلة", "noNFTYet": "لا توجد رموز NFT بعد", @@ -690,6 +706,7 @@ "note_tap_to_change": "ملاحظة (اضغط للتغيير)", "notification_permission_denied": "تم رفض إذن الإشعارات بشكل دائم. يُرجى تمكينه يدويًا من الإعدادات", "nullURIError": "URI فارغ", + "ocp_desc": "ادفع بالعملات المشفرة في المتاجر", "offer_expires_in": "ينتهي العرض خلال: ", "offline": "غير متصل", "ok": "موافق", @@ -716,8 +733,13 @@ "outputs": "المخرجات", "overshot": "تجاوز الحد", "overwrite_amount": "استبدال المبلغ", + "pairing_code": "رمز الإقران", + "pairing_code_desc_1": "سيعرض جهازك رمز إقران.", + "pairing_code_desc_2": "أدخله هنا للمتابعة", + "pairing_error": "خطأ في الإقران", "pairingInvalidEvent": "حدث إقران غير صالح", "passphrase": "عبارة المرور (اختياري)", + "passphrase_view_keys": "عبارة المرور", "passphrases_doesnt_match": "عبارتا المرور غير متطابقتين، يرجى المحاولة مرة أخرى", "password": "كلمة المرور", "paste": "لصق", @@ -733,6 +755,7 @@ "payjoin_unavailable_sheet_content": "يتطلب استلام معاملة Payjoin أن يكون لديك بيتكوين جاهز للاستخدام.\n\nبمجرد أن تحتوي محفظتك على رصيد، سيتم تفعيل Payjoin تلقائيًا.", "payjoin_unavailable_sheet_title": "لماذا Payjoin غير متاح؟", "payment_id": "معرّف الدفع: ", + "payment_invoices": "فواتير الدفع", "payment_made_easy": "المدفوعات أصبحت أسهل", "payment_was_received": "تم استلام دفعتك.", "payments": "المدفوعات", @@ -888,6 +911,7 @@ "save": "حفظ", "save_backup_password": "يرجى التأكد من أنك حفظت كلمة مرور النسخة الاحتياطية. لن تتمكن من استيراد ملفات النسخ الاحتياطي بدونها.", "save_backup_password_alert": "حفظ كلمة مرور النسخة الاحتياطية", + "save_contact": "حفظ جهة الاتصال", "save_to_downloads": "حفظ في التنزيلات", "saved_the_trade_id": "لقد حفظت معرّف الصفقة", "savings": "المدخرات", @@ -1037,6 +1061,7 @@ "setup_your_debit_card": "قم بإعداد بطاقة الخصم الخاصة بك", "share": "مشاركة", "share_address": "مشاركة العنوان", + "share_this_node": "مشاركة هذه العقدة", "shared_seed_wallet_groups": "مجموعات محافظ عبارة الاسترداد المشتركة", "shared_seed_wallets": "محافظ العبارة الأولية المشتركة", "show": "إظهار", @@ -1044,6 +1069,7 @@ "show_balance": "اضغط مطولًا لإظهار الرصيد", "show_balance_send_page": "انقر للكشف", "show_balance_toast": "اضغط مطولًا لإخفاء الرصيد أو إظهاره", + "show_combined_balance": "إظهار الرصيد المجمّع", "show_details": "عرض التفاصيل", "show_hidden_addresses": "إظهار العناوين المخفية", "show_keys": "إظهار عبارة الاسترداد/المفاتيح", @@ -1109,6 +1135,7 @@ "swap_providers": "مزودو المبادلة", "sweeping_wallet": "محفظة السحب الشامل", "sweeping_wallet_alert": "لن يستغرق هذا وقتًا طويلًا. لا تغادر هذه الشاشة وإلا فقد تُفقد الأموال المُسحوبة.", + "swipe_to_bridge": "اسحب لإجراء الجسر", "swipe_to_send": "اسحب للإرسال", "switch_wallet": "تبديل المحفظة", "switchToETHWallet": "يرجى التبديل إلى محفظة Ethereum والمحاولة مرة أخرى", @@ -1149,6 +1176,7 @@ "testnet_coins_no_value": "عملات شبكة الاختبار (Testnet) لا قيمة لها", "third_intro_content": "توجد Yats خارج Cake Wallet أيضًا. يمكن استبدال أي عنوان محفظة في العالم بـ Yat!", "third_intro_title": "Yat يعمل بسلاسة مع الآخرين", + "this_can_take_few_seconds": "قد يستغرق هذا بضع ثوانٍ", "this_pair_is_not_supported_warning": "هذا الزوج غير مدعوم مع منصّة/منصّات التداول المحددة حاليًا. يُرجى اختيار منصّة تداول أخرى.", "thorchain_contract_address_not_supported": "THORChain لا يدعم الإرسال إلى عنوان عقد", "thorchain_taproot_address_not_supported": "لا يدعم مزوّد ThorChain عناوين Taproot. يُرجى تغيير العنوان أو اختيار مزوّد آخر.", @@ -1162,6 +1190,7 @@ "token_decimal": "عدد المنازل العشرية للتوكن (مثال: USDT)", "token_name": "اسم التوكن (مثال: Tether)", "token_symbol": "رمز التوكن (مثل: USDT)", + "token_verified_on_jupiter": "تم التحقق من هذا الرمز المميز على Jupiter", "tokenID": "المعرّف", "tokens": "التوكنات", "tokens_strong_warning": "لا تُرسل الرموز المميّزة إلى هذا العنوان أبداً!", @@ -1290,6 +1319,7 @@ "username": "اسم المستخدم", "username_available": "اسم المستخدم متاح", "username_character_error": "يجب أن تحتوي أسماء المستخدمين فقط على", + "username_check_your_network": "تعذّر التحقق من اسم المستخدم. يُرجى التحقق من اتصالك بالشبكة.", "username_length_error": "يجب أن تتكون أسماء المستخدمين من 6 إلى 32 حرفًا أو رقمًا.", "username_length_error_1_64": "يجب أن تحتوي أسماء المستخدمين على 1 إلى 64 حرفًا أو رقمًا.", "username_not_available": "اسم المستخدم هذا غير متاح.", @@ -1300,6 +1330,7 @@ "verify_message": "التحقق من الرسالة", "verify_seed": "تحقق من عبارة الاسترداد", "verify_with_2fa": "تحقق عبر Cake 2FA", + "verifying_code": "جارٍ التحقق من الرمز", "version": "الإصدار ${currentVersion}", "view_all": "عرض الكل", "view_in_block_explorer": "عرض في مستكشف الكتل", @@ -1311,6 +1342,7 @@ "waitFewSecondForTxUpdate": "يرجى الانتظار بضع ثوانٍ حتى تنعكس المعاملة في سجل المعاملات", "wallet": "محفظة", "wallet_accounts": "حسابات المحفظة", + "wallet_balance": "رصيد المحفظة", "wallet_group": "مجموعة المحفظة", "wallet_group_description_existing_seed": "لقد اخترت استخدام عبارة الاسترداد (Seed) الموجودة لهذه المحفظة. يمكنك التحقق من عبارة الاسترداد (Seed) مرة أخرى إذا كنت بحاجة إلى تأكيدها أو تدوينها.", "wallet_group_description_four": "لإنشاء محفظة بعبارة استرداد جديدة تمامًا.", @@ -1352,6 +1384,7 @@ "wc_approve_request_title": "الموافقة على الطلب", "wc_connect_request_title": "طلب الاتصال", "wc_connected_to": "متصل بـ ${name}", + "wc_desc": "اتصل بسهولة بتطبيقات DApps", "wc_max_network_fee": "الحد الأقصى لرسوم الشبكة", "wc_message_to_sign": "رسالة للتوقيع", "wc_network_fee": "رسوم الشبكة", @@ -1402,6 +1435,7 @@ "yat_popup_content": "يمكنك الآن إرسال واستلام العملات المشفرة في Cake Wallet باستخدام Yat - اسم مستخدم قصير يعتمد على الرموز التعبيرية. يمكنك إدارة Yats في أي وقت من شاشة الإعدادات", "yat_popup_title": "يمكن تحويل عنوان محفظتك إلى رموز تعبيرية.", "yes": "نعم", + "yes_exit": "نعم. اخرج", "yesterday": "الأمس", "you_now_have_debit_card": "لديك الآن بطاقة خصم", "you_pay": "أنت تدفع", @@ -1451,4 +1485,4 @@ "wallet_balance": "رصيد المحفظة", "wc_desc": "اتصل بسهولة بتطبيقات DApps", "yes_exit": "نعم. اخرج" -} \ No newline at end of file +} diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 59d11edb6..21bc47177 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -76,6 +76,7 @@ "approve_tokens": "Одобряване на токени", "apps": "Приложения", "arbiscan_history": "История в ArbiScan", + "are_you_sure_exit": "Сигурни ли сте, че искате да излезете?", "arrive_in_this_address": "${currency} ${tag}ще пристигне на този адрес", "ascending": "Възходящ", "ask_each_time": "Питай всеки път", @@ -89,6 +90,7 @@ "auto_generate_addresses": "Автоматично генериране на адреси", "auto_generate_subaddresses": "Автоматично генериране на подадреси", "automatic": "Автоматично", + "available": "Налично", "available_balance": "Наличен баланс", "available_balance_description": "„Наличният баланс“ или „Потвърденият баланс“ са средства, които могат да бъдат изразходвани веднага. Ако средствата се появяват в долния баланс, но не и в горния, трябва да изчакате няколко минути, докато входящите средства получат повече мрежови потвърждения. След като получат повече потвърждения, те ще могат да бъдат изразходвани.", "avg_savings": "Средни спестявания", @@ -125,6 +127,7 @@ "block_remaining": "Остава 1 блок", "Blocks_remaining": "Остават ${status} блока", "bluetooth": "Bluetooth", + "bridging": "Прехвърляне", "bright_theme": "Светла", "bscscan_history": "История в BSCScan", "bump_fee": "Увеличи таксата", @@ -216,6 +219,8 @@ "color_theme": "Цветова тема", "coming_soon_tag": "Очаквайте скоро", "commit_transaction_amount_fee": "Потвърдете транзакцията\nСума: ${amount}\nТакса: ${fee}", + "compatible_services": "Съвместими услуги", + "compatible_services_desc": "Използвайте инструмента за сканиране, за да четете следните типове QR кодове, поддържани в Cake Wallet:", "configure": "Конфигуриране", "confirm": "Потвърди", "confirm_delete_template": "Това действие ще изтрие този шаблон. Желаете ли да продължите?", @@ -279,6 +284,7 @@ "creating_new_wallet": "Създаване на нов портфейл", "creating_new_wallet_error": "Грешка: ${description}", "creation_date": "Дата на създаване", + "crypto_addresses": "Крипто адреси", "csv_nothing_to_export": "Няма какво да се експортира", "current_swap": "Текущ суап", "current_wallet": "Текущ портфейл", @@ -301,6 +307,7 @@ "default_sell_provider": "Доставчик за продажба по подразбиране", "delete": "Изтрий", "delete_account": "Изтриване на акаунта", + "delete_this_node": "Изтриване на този възел", "delete_wallet": "Изтриване на портфейл", "delete_wallet_confirm_message": "Сигурни ли сте, че искате да изтриете портфейла ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Сигурни ли сте, че искате да изтриете връзката с", @@ -330,6 +337,7 @@ "deuro_savings_welcome_description": "dEURO е напълно децентрализиран, обезпечен стабилкойн, обвързан с еврото — сигурен, универсален и устойчив на цензура, без да се налага да се разчита на оракули. 1 dEURO = 1 EURO.", "deuro_tooltip_no_eth": "За да активирате dEURO, трябва да имате ETH в портфейла си.", "deuro_tx_commited_content": "Може да отнеме няколко секунди, докато транзакцията се потвърди и се отрази на екрана", + "device_confirmation": "Потвърждение от устройството", "device_is_signing": "Устройството подписва", "dfx_option_description": "Купете крипто с EUR и CHF. За частни и корпоративни клиенти в Европа", "did_you_back_up_seeds": "Архивирахте ли всички свои seed фрази?", @@ -466,6 +474,8 @@ "extracted_address_content": "Ще изпратите средства до\n${recipient_name}", "failed_authentication": "Неуспешно удостоверяване. ${state_error}", "faq": "ЧЗВ", + "favorite_token": "Любим токен", + "favorite_token_desc": "Балансът на любимия токен ще се показва на картата с баланса.", "features": "Функции", "fee": "Такса", "fee_less_than_min": "Избраната такса е по-ниска от минималната. Моля, увеличете таксата, за да можете да изпратите транзакцията.", @@ -491,6 +501,7 @@ "frozen": "Замразено", "frozen_balance": "Замразен баланс", "full_balance": "Пълен баланс", + "gallery": "Галерия", "gas_exceeds_allowance": "Газът, необходим за транзакцията, надвишава допустимото.", "gas_price": "Цена на gas", "general": "Общи", @@ -526,10 +537,10 @@ "how_to_connect": "Как да се свържете", "how_to_use": "Как да използвате", "how_to_use_card": "Как да използвате тази карта", + "hww_exit_desc": "Това ще прекрати текущия процес на сдвояване с вашето хардуерно устройство", "i_understand": "Разбирам", "icon_style": "Стил на иконата", "id": "ID: ", - "if_you_dont_see_your_device": "Ако не виждате устройството си по-горе, моля, уверете се, че вашият Ledger е буден и отключен!", "ignor": "Игнорирай", "import": "Импортиране", "importNFTs": "Импортиране на NFT", @@ -541,6 +552,7 @@ "infobox_auto_address": "Вашият адрес за получаване ще се сменя всеки път, когато получите транзакция", "infobox_multichain": "Използвайте този адрес, за да получавате всеки токен или колекционерски предмет в", "infobox_rotation": "Вашият адрес за получаване ще се сменя всеки път, когато затворите и отворите отново този екран", + "input": "Вход", "inputs": "Входове", "insufficient_funds_for_tx": "Недостатъчни средства за успешно изпълнение на транзакцията.", "insufficient_lamport_for_tx": "Нямате достатъчно SOL, за да покриете транзакцията и таксата за нея. Моля, добавете още SOL към портфейла си или намалете количеството SOL, което изпращате.", @@ -606,6 +618,7 @@ "manage_pow_nodes": "Управление на PoW възли", "manage_providers": "Управление на доставчици", "manage_yats": "Управление на Yats", + "manual_input": "Ръчно въвеждане", "mark_as_redeemed": "Маркирай като осребрен", "market_place": "Пазар", "mask": "Маска", @@ -651,6 +664,7 @@ "nanogpt_subtitle": "Всички най-нови модели (GPT-5.3, Claude 4.6).\\nБез абонамент, плащайте с крипто.", "narrow": "Тесен", "network": "Мрежа", + "networks": "Мрежи", "new_first_wallet_text": "Да пазите криптовалутата си в безопасност е лесно като парче торта", "new_node_testing": "Тестване на нов възел", "new_subaddress_create": "Създай", @@ -679,8 +693,10 @@ "node_new": "Нов възел", "node_port": "Порт на възела", "node_reset_settings_title": "Нулиране на настройките", + "node_share_desc": "Сканирайте този QR код от екрана за сканиране, за да импортирате този възел.", "node_test": "Тест", "nodes": "Възли", + "nodes_desc": "Управлявайте съществуващите възли или добавете персонализиран.", "nodes_list_reset_to_default_message": "Сигурни ли сте, че искате да възстановите настройките по подразбиране?", "none_of_selected_providers_can_exchange": "Нито един от избраните доставчици не може да извърши този суап", "noNFTYet": "Все още няма NFT", @@ -690,6 +706,7 @@ "note_tap_to_change": "Бележка (докоснете, за да промените)", "notification_permission_denied": "Разрешението за известия е окончателно отказано. Моля, включете го ръчно в настройките.", "nullURIError": "URI е null", + "ocp_desc": "Плащайте с крипто в магазини", "offer_expires_in": "Офертата изтича след: ", "offline": "Офлайн", "ok": "ОК", @@ -716,8 +733,13 @@ "outputs": "Изходи", "overshot": "Надвишено", "overwrite_amount": "Презапиши сумата", + "pairing_code": "Код за сдвояване", + "pairing_code_desc_1": "Вашето устройство ще покаже код за сдвояване.", + "pairing_code_desc_2": "Въведете го тук, за да продължите", + "pairing_error": "Грешка при сдвояване", "pairingInvalidEvent": "Невалидно събитие при сдвояване", "passphrase": "Парола за достъп (по избор)", + "passphrase_view_keys": "Парола за достъп", "passphrases_doesnt_match": "Пасфразите не съвпадат, моля, опитайте отново", "password": "Парола", "paste": "Поставяне", @@ -733,6 +755,7 @@ "payjoin_unavailable_sheet_content": "Получаването на Payjoin транзакция изисква да имате наличен Bitcoin за използване.\n\nВеднага щом портфейлът ви има средства, Payjoin ще бъде автоматично активиран.", "payjoin_unavailable_sheet_title": "Защо Payjoin не е наличен?", "payment_id": "ID на плащане: ", + "payment_invoices": "Фактури за плащане", "payment_made_easy": "Плащанията са лесни", "payment_was_received": "Плащането ви беше получено.", "payments": "Плащания", @@ -888,6 +911,7 @@ "save": "Запази", "save_backup_password": "Моля, уверете се, че сте запазили паролата за резервното копие. Няма да можете да импортирате файловете от резервното си копие без нея.", "save_backup_password_alert": "Запазване на паролата за резервно копие", + "save_contact": "Запазване на контакт", "save_to_downloads": "Запази в „Изтегляния“", "saved_the_trade_id": "Запазих ID на сделката", "savings": "Спестявания", @@ -1037,6 +1061,7 @@ "setup_your_debit_card": "Настройте дебитната си карта", "share": "Споделяне", "share_address": "Споделяне на адрес", + "share_this_node": "Споделяне на този възел", "shared_seed_wallet_groups": "Групи портфейли със споделена seed фраза", "shared_seed_wallets": "Портфейли със споделена seed фраза", "show": "Покажи", @@ -1044,6 +1069,7 @@ "show_balance": "Натиснете и задръжте, за да покажете баланса", "show_balance_send_page": "Докоснете, за да разкриете", "show_balance_toast": "Натиснете и задръжте, за да скриете или покажете баланса", + "show_combined_balance": "Показване на общия баланс", "show_details": "Покажи подробности", "show_hidden_addresses": "Показване на скритите адреси", "show_keys": "Показване на seed/ключове", @@ -1109,6 +1135,7 @@ "swap_providers": "Доставчици за суап", "sweeping_wallet": "Изчистване на портфейл", "sweeping_wallet_alert": "Това не би трябвало да отнеме много време. НЕ НАПУСКАЙТЕ ТОЗИ ЕКРАН, ИНАЧЕ ИЗМЕТЕНИТЕ СРЕДСТВА МОГАТ ДА БЪДАТ ЗАГУБЕНИ.", + "swipe_to_bridge": "Плъзнете, за да прехвърлите", "swipe_to_send": "Плъзнете, за да изпратите", "switch_wallet": "Превключване на портфейл", "switchToETHWallet": "Моля, превключете към портфейл за Ethereum и опитайте отново", @@ -1149,6 +1176,7 @@ "testnet_coins_no_value": "Монетите в тестовата мрежа нямат стойност", "third_intro_content": "Yats също съществуват извън Cake Wallet. Всеки адрес на портфейл по света може да бъде заменен с Yat!", "third_intro_title": "Yat работи добре с другите", + "this_can_take_few_seconds": "Това може да отнеме няколко секунди", "this_pair_is_not_supported_warning": "Тази двойка не се поддържа от избраната(ите) в момента борса(и). Моля, изберете друга борса.", "thorchain_contract_address_not_supported": "THORChain не поддържа изпращане към адрес на смарт договор", "thorchain_taproot_address_not_supported": "Доставчикът на ThorChain не поддържа Taproot адреси. Моля, променете адреса или изберете друг доставчик.", @@ -1162,6 +1190,7 @@ "token_decimal": "Десетични знаци на токена (напр.: USDT)", "token_name": "Име на токен (напр.: Tether)", "token_symbol": "Символ на токена (напр.: USDT)", + "token_verified_on_jupiter": "Този токен е проверен на Jupiter", "tokenID": "ID", "tokens": "Токени", "tokens_strong_warning": "Никога не изпращайте токени на този адрес!", @@ -1290,6 +1319,7 @@ "username": "Потребителско име", "username_available": "Потребителското име е налично", "username_character_error": "Потребителските имена трябва да съдържат само", + "username_check_your_network": "Неуспешна проверка на потребителското име. Моля, проверете мрежовата си връзка.", "username_length_error": "Потребителските имена трябва да съдържат 6–32 буквено-цифрови знака.", "username_length_error_1_64": "Потребителските имена трябва да съдържат 1-64 буквено-цифрови знака.", "username_not_available": "Това потребителско име не е налично.", @@ -1300,6 +1330,7 @@ "verify_message": "Потвърдете съобщението", "verify_seed": "Потвърдете seed фразата", "verify_with_2fa": "Потвърдете с Cake 2FA", + "verifying_code": "Проверка на кода", "version": "Версия ${currentVersion}", "view_all": "Виж всички", "view_in_block_explorer": "Преглед в Block Explorer", @@ -1311,6 +1342,7 @@ "waitFewSecondForTxUpdate": "Моля, изчакайте няколко секунди, докато транзакцията се появи в историята на транзакциите", "wallet": "Портфейл", "wallet_accounts": "Акаунти на портфейла", + "wallet_balance": "Баланс на портфейла", "wallet_group": "Група портфейли", "wallet_group_description_existing_seed": "Избрали сте да използвате съществуваща seed фраза за този портфейл. Можете да я проверите отново, ако трябва да я потвърдите или да я запишете.", "wallet_group_description_four": "за да създадете портфейл с изцяло нова seed фраза.", @@ -1352,6 +1384,7 @@ "wc_approve_request_title": "Одобряване на заявката", "wc_connect_request_title": "Заявка за свързване", "wc_connected_to": "Свързан с ${name}", + "wc_desc": "Свържете се лесно с dApps", "wc_max_network_fee": "Максимална мрежова такса", "wc_message_to_sign": "Съобщение за подпис", "wc_network_fee": "Мрежова такса", @@ -1402,6 +1435,7 @@ "yat_popup_content": "Вече можете да изпращате и получавате крипто в Cake Wallet с вашия Yat – кратко потребителско име, базирано на емоджита. Управлявайте своите Yats по всяко време от екрана с настройки", "yat_popup_title": "Адресът на вашия портфейл може да бъде превърнат в емоджита.", "yes": "Да", + "yes_exit": "Да. Изход", "yesterday": "Вчера", "you_now_have_debit_card": "Вече имате дебитна карта", "you_pay": "Вие плащате", @@ -1451,4 +1485,4 @@ "wallet_balance": "Баланс на портфейла", "wc_desc": "Свържете се лесно с dApps", "yes_exit": "Да. Изход" -} \ No newline at end of file +} diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index c2c5c49c0..8bc91b7a0 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -76,6 +76,7 @@ "approve_tokens": "Schválit tokeny", "apps": "Aplikace", "arbiscan_history": "Historie ArbiScan", + "are_you_sure_exit": "Opravdu chcete ukončit aplikaci?", "arrive_in_this_address": "${currency} ${tag}dorazí na tuto adresu", "ascending": "Vzestupně", "ask_each_time": "Zeptat se pokaždé", @@ -89,6 +90,7 @@ "auto_generate_addresses": "Automaticky generovat adresy", "auto_generate_subaddresses": "Automaticky generovat podadresy", "automatic": "Automaticky", + "available": "Dostupné", "available_balance": "Dostupný zůstatek", "available_balance_description": "„Dostupný zůstatek“ nebo „Potvrzený zůstatek“ jsou prostředky, které lze okamžitě utratit. Pokud se prostředky zobrazují ve spodním zůstatku, ale ne v horním, musíte několik minut počkat, než příchozí prostředky získají více síťových potvrzení. Jakmile získají více potvrzení, bude možné je utratit.", "avg_savings": "Prům. úspora", @@ -125,6 +127,7 @@ "block_remaining": "Zbývá 1 blok", "Blocks_remaining": "Zbývá ${status} bloků", "bluetooth": "Bluetooth", + "bridging": "Přemostění", "bright_theme": "Světlý", "bscscan_history": "Historie BSCScan", "bump_fee": "Navýšit poplatek", @@ -216,6 +219,8 @@ "color_theme": "Barevné téma", "coming_soon_tag": "Již brzy", "commit_transaction_amount_fee": "Potvrdit transakci\nČástka: ${amount}\nPoplatek: ${fee}", + "compatible_services": "Kompatibilní služby", + "compatible_services_desc": "Pomocí nástroje pro skenování načtěte následující typy QR kódů podporované v Cake Wallet:", "configure": "Konfigurovat", "confirm": "Potvrdit", "confirm_delete_template": "Tato akce odstraní tuto šablonu. Přejete si pokračovat?", @@ -279,6 +284,7 @@ "creating_new_wallet": "Vytváření nové peněženky", "creating_new_wallet_error": "Chyba: ${description}", "creation_date": "Datum vytvoření", + "crypto_addresses": "Krypto adresy", "csv_nothing_to_export": "Není co exportovat", "current_swap": "Aktuální swap", "current_wallet": "Aktuální peněženka", @@ -301,6 +307,7 @@ "default_sell_provider": "Výchozí poskytovatel pro prodej", "delete": "Smazat", "delete_account": "Smazat účet", + "delete_this_node": "Smazat tento uzel", "delete_wallet": "Smazat peněženku", "delete_wallet_confirm_message": "Opravdu chcete smazat peněženku ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Opravdu chcete odstranit připojení k", @@ -330,6 +337,7 @@ "deuro_savings_welcome_description": "dEURO je plně decentralizovaný, kolateralizovaný stablecoin navázaný na euro — bezpečný, všestranný a odolný vůči cenzuře, aniž by se musel spoléhat na orákly. 1 dEURO = 1 EURO.", "deuro_tooltip_no_eth": "Abyste mohli povolit dEURO, musíte mít v peněžence ETH.", "deuro_tx_commited_content": "Může trvat několik sekund, než se transakce potvrdí a projeví se na obrazovce", + "device_confirmation": "Potvrzení na zařízení", "device_is_signing": "Zařízení podepisuje", "dfx_option_description": "Kupte krypto za EUR a CHF. Pro maloobchodní a firemní zákazníky v Evropě", "did_you_back_up_seeds": "Zálohovali jste všechny své seed fráze?", @@ -466,6 +474,8 @@ "extracted_address_content": "Budete odesílat prostředky na\n${recipient_name}", "failed_authentication": "Ověření se nezdařilo. ${state_error}", "faq": "FAQ", + "favorite_token": "Oblíbený token", + "favorite_token_desc": "Zůstatek oblíbeného tokenu se zobrazí na kartě zůstatku.", "features": "Funkce", "fee": "Poplatek", "fee_less_than_min": "Vybraný poplatek je nižší než minimum. Zvyšte prosím poplatek, abyste mohli transakci odeslat.", @@ -491,6 +501,7 @@ "frozen": "Zmrazeno", "frozen_balance": "Zmrazený zůstatek", "full_balance": "Celkový zůstatek", + "gallery": "Galerie", "gas_exceeds_allowance": "Plyn vyžadovaný transakcí překračuje povolený limit.", "gas_price": "Cena gasu", "general": "Obecné", @@ -526,10 +537,10 @@ "how_to_connect": "Jak se připojit", "how_to_use": "Jak používat", "how_to_use_card": "Jak tuto kartu používat", + "hww_exit_desc": "Tímto ukončíte probíhající proces párování s vaším hardwarovým zařízením", "i_understand": "Rozumím", "icon_style": "Styl ikony", "id": "ID: ", - "if_you_dont_see_your_device": "Pokud své zařízení výše nevidíte, ujistěte se, že je váš Ledger zapnutý a odemčený!", "ignor": "Ignorovat", "import": "Importovat", "importNFTs": "Importovat NFT", @@ -541,6 +552,7 @@ "infobox_auto_address": "Vaše přijímací adresa se bude měnit pokaždé, když obdržíte transakci", "infobox_multichain": "Použijte tuto adresu k přijetí jakéhokoli tokenu nebo sběratelského předmětu na", "infobox_rotation": "Vaše přijímací adresa se bude měnit pokaždé, když zavřete a znovu otevřete tuto obrazovku", + "input": "Vstup", "inputs": "Vstupy", "insufficient_funds_for_tx": "Nedostatečné prostředky pro úspěšné provedení transakce.", "insufficient_lamport_for_tx": "Nemáte dostatek SOL na pokrytí transakce a transakčního poplatku. Přidejte do své peněženky více SOL nebo snižte množství SOL, které odesíláte.", @@ -606,6 +618,7 @@ "manage_pow_nodes": "Spravovat PoW uzly", "manage_providers": "Spravovat poskytovatele", "manage_yats": "Spravovat Yats", + "manual_input": "Ruční zadání", "mark_as_redeemed": "Označit jako uplatněné", "market_place": "Marketplace", "mask": "Maska", @@ -651,6 +664,7 @@ "nanogpt_subtitle": "Všechny nejnovější modely (GPT-5.3, Claude 4.6).\nBez předplatného, plaťte kryptem.", "narrow": "Úzké", "network": "Síť", + "networks": "Sítě", "new_first_wallet_text": "Udržet vaše krypto v bezpečí je hračka", "new_node_testing": "Testování nového uzlu", "new_subaddress_create": "Vytvořit", @@ -679,8 +693,10 @@ "node_new": "Nový uzel", "node_port": "Port uzlu", "node_reset_settings_title": "Obnovit nastavení", + "node_share_desc": "Naskenováním tohoto QR kódu na obrazovce skenování tento uzel importujete.", "node_test": "Test", "nodes": "Uzly", + "nodes_desc": "Spravujte stávající uzly nebo přidejte vlastní.", "nodes_list_reset_to_default_message": "Opravdu chcete obnovit nastavení na výchozí?", "none_of_selected_providers_can_exchange": "Žádný z vybraných poskytovatelů nemůže tento swap provést", "noNFTYet": "Zatím žádné NFT", @@ -690,6 +706,7 @@ "note_tap_to_change": "Poznámka (klepnutím změnit)", "notification_permission_denied": "Oprávnění k oznámením bylo trvale zamítnuto, prosím ručně jej povolte v nastavení", "nullURIError": "URI je prázdné", + "ocp_desc": "Plaťte kryptem v obchodech", "offer_expires_in": "Platnost nabídky vyprší za: ", "offline": "Offline", "ok": "OK", @@ -716,8 +733,13 @@ "outputs": "Výstupy", "overshot": "Přestřelení", "overwrite_amount": "Přepsat částku", + "pairing_code": "Párovací kód", + "pairing_code_desc_1": "Vaše zařízení zobrazí párovací kód.", + "pairing_code_desc_2": "Zadejte ho zde a pokračujte", + "pairing_error": "Chyba párování", "pairingInvalidEvent": "Neplatná událost párování", "passphrase": "Přístupová fráze (volitelné)", + "passphrase_view_keys": "Přístupová fráze", "passphrases_doesnt_match": "Heslové fráze se neshodují, zkuste to znovu", "password": "Heslo", "paste": "Vložit", @@ -733,6 +755,7 @@ "payjoin_unavailable_sheet_content": "Přijetí transakce Payjoin vyžaduje, abyste měli Bitcoin připravený k použití.\n\nJakmile bude mít vaše peněženka prostředky, Payjoin se automaticky povolí.", "payjoin_unavailable_sheet_title": "Proč je Payjoin nedostupný?", "payment_id": "ID platby: ", + "payment_invoices": "Platební faktury", "payment_made_easy": "Platby snadno", "payment_was_received": "Vaše platba byla přijata.", "payments": "Platby", @@ -888,6 +911,7 @@ "save": "Uložit", "save_backup_password": "Ujistěte se prosím, že máte uložené heslo k záloze. Bez něj nebudete moci importovat záložní soubory.", "save_backup_password_alert": "Uložit heslo k záloze", + "save_contact": "Uložit kontakt", "save_to_downloads": "Uložit do složky Stažené", "saved_the_trade_id": "Uložil jsem si ID obchodu", "savings": "Úspory", @@ -1037,6 +1061,7 @@ "setup_your_debit_card": "Nastavte si debetní kartu", "share": "Sdílet", "share_address": "Sdílet adresu", + "share_this_node": "Sdílet tento uzel", "shared_seed_wallet_groups": "Skupiny peněženek se sdílenou seed frází", "shared_seed_wallets": "Peněženky se sdíleným seedem", "show": "Zobrazit", @@ -1044,6 +1069,7 @@ "show_balance": "Dlouhým stisknutím zobrazíte zůstatek", "show_balance_send_page": "Klepnutím odhalíte", "show_balance_toast": "Dlouhým stisknutím skryjete nebo zobrazíte zůstatek", + "show_combined_balance": "Zobrazit souhrnný zůstatek", "show_details": "Zobrazit podrobnosti", "show_hidden_addresses": "Zobrazit skryté adresy", "show_keys": "Zobrazit seed/klíče", @@ -1109,6 +1135,7 @@ "swap_providers": "Poskytovatelé swapu", "sweeping_wallet": "Peněženka pro sweep", "sweeping_wallet_alert": "To by nemělo trvat dlouho. NEOPUŠTĚJTE TUTO OBRAZOVKU, JINAK MOHOU BÝT PŘESUNUTÉ PROSTŘEDKY ZTRACENY.", + "swipe_to_bridge": "Přemostění přejetím prstem", "swipe_to_send": "Přejeďte pro odeslání", "switch_wallet": "Přepnout peněženku", "switchToETHWallet": "Přepněte na peněženku Ethereum a zkuste to znovu", @@ -1149,6 +1176,7 @@ "testnet_coins_no_value": "Testnetové mince nemají žádnou hodnotu", "third_intro_content": "Yaty existují i mimo Cake Wallet. Jakákoli adresa peněženky na světě může být nahrazena Yatem!", "third_intro_title": "Yat si dobře rozumí s ostatními", + "this_can_take_few_seconds": "Může to trvat několik sekund", "this_pair_is_not_supported_warning": "Tento pár není podporován aktuálně vybranou burzou (burzami). Vyberte prosím jinou burzu.", "thorchain_contract_address_not_supported": "THORChain nepodporuje odesílání na adresu kontraktu", "thorchain_taproot_address_not_supported": "Poskytovatel ThorChain nepodporuje Taproot adresy. Změňte adresu nebo vyberte jiného poskytovatele.", @@ -1162,6 +1190,7 @@ "token_decimal": "Desetinná místa tokenu (např.: USDT)", "token_name": "Název tokenu (např.: Tether)", "token_symbol": "Symbol tokenu (např.: USDT)", + "token_verified_on_jupiter": "Tento token je ověřen na Jupiteru", "tokenID": "ID", "tokens": "Tokeny", "tokens_strong_warning": "Nikdy neposílejte tokeny na tuto adresu!", @@ -1290,6 +1319,7 @@ "username": "Uživatelské jméno", "username_available": "Uživatelské jméno je k dispozici", "username_character_error": "Uživatelská jména mohou obsahovat pouze", + "username_check_your_network": "Nelze ověřit uživatelské jméno. Zkontrolujte prosím připojení k síti.", "username_length_error": "Uživatelská jména musí obsahovat 6–32 alfanumerických znaků.", "username_length_error_1_64": "Uživatelská jména musí obsahovat 1–64 alfanumerických znaků.", "username_not_available": "Toto uživatelské jméno není k dispozici.", @@ -1300,6 +1330,7 @@ "verify_message": "Ověřit zprávu", "verify_seed": "Ověřit seed", "verify_with_2fa": "Ověřit pomocí Cake 2FA", + "verifying_code": "Ověřování kódu", "version": "Verze ${currentVersion}", "view_all": "Zobrazit vše", "view_in_block_explorer": "Zobrazit v průzkumníku bloků", @@ -1311,6 +1342,7 @@ "waitFewSecondForTxUpdate": "Počkejte prosím několik sekund, než se transakce zobrazí v historii transakcí", "wallet": "Peněženka", "wallet_accounts": "Účty v peněžence", + "wallet_balance": "Zůstatek peněženky", "wallet_group": "Skupina peněženek", "wallet_group_description_existing_seed": "Zvolili jste použití existující seed fráze pro tuto peněženku. Seed frázi můžete znovu ověřit, pokud ji potřebujete potvrdit nebo si ji zapsat.", "wallet_group_description_four": "vytvořit peněženku se zcela novým seedem.", @@ -1352,6 +1384,7 @@ "wc_approve_request_title": "Schválit žádost", "wc_connect_request_title": "Žádost o připojení", "wc_connected_to": "Připojeno k ${name}", + "wc_desc": "Snadno se připojte k dApps", "wc_max_network_fee": "Max síťový poplatek", "wc_message_to_sign": "Zpráva k podpisu", "wc_network_fee": "Síťový poplatek", @@ -1402,6 +1435,7 @@ "yat_popup_content": "Nyní můžete v Cake Wallet posílat a přijímat kryptoměny pomocí svého Yatu – krátkého uživatelského jména složeného z emoji. Yaty můžete kdykoli spravovat na obrazovce nastavení", "yat_popup_title": "Adresa vaší peněženky může být emojifikována.", "yes": "Ano", + "yes_exit": "Ano. Ukončit", "yesterday": "Včera", "you_now_have_debit_card": "Nyní máte debetní kartu", "you_pay": "Platíte", @@ -1451,4 +1485,4 @@ "wallet_balance": "Zůstatek peněženky", "wc_desc": "Snadno se připojte k dApps", "yes_exit": "Ano. Ukončit" -} \ No newline at end of file +} diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 99bbb8b0d..b55be3560 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -62,6 +62,7 @@ "already_your_username": "Das ist bereits Ihr Benutzername!", "always": "Immer", "amount": "Betrag: ", + "amount_hidden": "Versteckter Betrag", "amount_is_below_minimum_limit": "Ihr Guthaben nach Gebühren wäre geringer als der für den Tausch erforderliche Mindestbetrag (${min})", "amount_is_estimate": "Der Empfangsbetrag ist eine Schätzung", "amount_is_guaranteed": "Der Empfangsbetrag ist garantiert", @@ -76,6 +77,7 @@ "approve_tokens": "Tokens genehmigen", "apps": "Apps", "arbiscan_history": "ArbiScan-Verlauf", + "are_you_sure_exit": "Möchten Sie die App wirklich verlassen?", "arrive_in_this_address": "${currency} ${tag} wird an dieser Adresse eingehen", "ascending": "Aufsteigend", "ask_each_time": "Jedes Mal fragen", @@ -89,6 +91,7 @@ "auto_generate_addresses": "Adressen automatisch generieren", "auto_generate_subaddresses": "Unteradressen automatisch generieren", "automatic": "Automatisch", + "available": "Verfügbar", "available_balance": "Verfügbares Guthaben", "available_balance_description": "Der „Verfügbare Kontostand“ bzw. „Bestätigter Kontostand“ sind Guthaben, die sofort ausgegeben werden können. Wenn Guthaben im unteren Kontostand angezeigt wird, aber nicht im oberen, müssen Sie ein paar Minuten warten, bis die eingehenden Gelder weitere Netzwerkbestätigungen erhalten. Sobald sie mehr Bestätigungen haben, können sie ausgegeben werden.", "avg_savings": "Durchschn. Ersparnis", @@ -125,6 +128,7 @@ "block_remaining": "1 Block verbleibend", "Blocks_remaining": "${status} Blöcke verbleibend", "bluetooth": "Bluetooth", + "bridging": "Bridge-Vorgang", "bright_theme": "Hell", "bscscan_history": "BSCScan-Verlauf", "bump_fee": "Gebühr erhöhen", @@ -216,6 +220,8 @@ "color_theme": "Farbschema", "coming_soon_tag": "Demnächst verfügbar", "commit_transaction_amount_fee": "Transaktion bestätigen\nBetrag: ${amount}\nGebühr: ${fee}", + "compatible_services": "Kompatible Dienste", + "compatible_services_desc": "Verwenden Sie das Scan-Tool, um die folgenden von Cake Wallet unterstützten QR-Code-Typen zu lesen:", "configure": "Konfigurieren", "confirm": "Bestätigen", "confirm_delete_template": "Diese Aktion wird diese Vorlage löschen. Möchten Sie fortfahren?", @@ -279,6 +285,7 @@ "creating_new_wallet": "Neues Wallet wird erstellt", "creating_new_wallet_error": "Fehler: ${description}", "creation_date": "Erstellungsdatum", + "crypto_addresses": "Krypto-Adressen", "csv_nothing_to_export": "Nichts zum Exportieren", "current_swap": "Aktueller Swap", "current_wallet": "Aktuelle Wallet", @@ -301,6 +308,7 @@ "default_sell_provider": "Standardanbieter für Verkäufe", "delete": "Löschen", "delete_account": "Konto löschen", + "delete_this_node": "Diesen Node löschen", "delete_wallet": "Wallet löschen", "delete_wallet_confirm_message": "Sind Sie sicher, dass Sie die Wallet ${wallet_name} löschen möchten?", "deleteConnectionConfirmationPrompt": "Sind Sie sicher, dass Sie die Verbindung zu", @@ -330,6 +338,7 @@ "deuro_savings_welcome_description": "dEURO ist ein vollständig dezentraler, besicherter Stablecoin, der an den Euro gekoppelt ist – sicher, vielseitig und zensurresistent, ohne auf Orakel angewiesen zu sein. 1 dEURO = 1 EURO.", "deuro_tooltip_no_eth": "Um dEURO zu aktivieren, müssen Sie ETH in Ihrem Wallet haben.", "deuro_tx_commited_content": "Es kann ein paar Sekunden dauern, bis die Transaktion bestätigt wird und auf dem Bildschirm angezeigt wird.", + "device_confirmation": "Gerätebestätigung", "device_is_signing": "Das Gerät signiert", "dfx_option_description": "Kaufen Sie Krypto mit EUR & CHF. Für Privat- und Unternehmenskunden in Europa", "did_you_back_up_seeds": "Haben Sie alle Ihre Seeds gesichert?", @@ -368,6 +377,8 @@ "duress_PIN": "Notfall-PIN", "duress_pin_description": "Dadurch wird eine Notfall-PIN eingerichtet, eine erweiterte Funktion, die von den meisten Nutzern nicht verwendet werden sollte. Diese PIN sollte nur verwendet werden, wenn Sie in Gefahr sind. Nachdem Sie diese PIN verwendet haben, werden alle Ihre Wallets gelöscht. Stellen Sie daher bitte sicher, dass alle Ihre Seeds gesichert sind, bevor Sie sie verwenden.", "duress_PIN_set_up_successfully": "Die Notfall-PIN wurde erfolgreich eingerichtet", + "durres_PIN": "Notfall-PIN", + "durres_PIN_set_up_successfully": "Die Notfall-PIN wurde erfolgreich eingerichtet", "e_sign_consent": "Einwilligung zur E-Signatur", "edit": "Bearbeiten", "edit_account": "Konto bearbeiten", @@ -466,6 +477,8 @@ "extracted_address_content": "Sie senden Guthaben an\n${recipient_name}", "failed_authentication": "Authentifizierung fehlgeschlagen. ${state_error}", "faq": "FAQ", + "favorite_token": "Bevorzugter Token", + "favorite_token_desc": "Der Kontostand des bevorzugten Tokens wird auf der Guthabenkarte angezeigt.", "features": "Funktionen", "fee": "Gebühr", "fee_less_than_min": "Die ausgewählte Gebühr liegt unter dem Minimum. Bitte erhöhen Sie die Gebühr, um die Transaktion senden zu können.", @@ -491,6 +504,7 @@ "frozen": "Eingefroren", "frozen_balance": "Eingefrorenes Guthaben", "full_balance": "Gesamtsaldo", + "gallery": "Galerie", "gas_exceeds_allowance": "Das für die Transaktion erforderliche Gas überschreitet das Limit.", "gas_price": "Gaspreis", "general": "Allgemein", @@ -526,10 +540,11 @@ "how_to_connect": "So verbinden Sie sich", "how_to_use": "So verwenden Sie", "how_to_use_card": "So verwenden Sie diese Karte", + "hww_exit_desc": "Dadurch wird der laufende Kopplungsvorgang mit Ihrem Hardware-Gerät beendet", "i_understand": "Ich verstehe", "icon_style": "Symbolstil", "id": "ID: ", - "if_you_dont_see_your_device": "Wenn Sie Ihr Gerät oben nicht sehen, stellen Sie bitte sicher, dass Ihr Ledger eingeschaltet und entsperrt ist!", + "if_you_dont_see_your_device": "Wenn Sie Ihr Gerät oben nicht sehen, stellen Sie bitte sicher, dass Ihr Gerät eingeschaltet und entsperrt ist!", "ignore": "Ignorieren", "import": "Importieren", "importNFTs": "NFTs importieren", @@ -541,6 +556,7 @@ "infobox_auto_address": "Ihre Empfangsadresse wird jedes Mal gewechselt, wenn Sie eine Transaktion erhalten", "infobox_multichain": "Verwenden Sie diese Adresse, um ein beliebiges Token oder Sammlerstück zu empfangen auf", "infobox_rotation": "Ihre Empfangsadresse wird jedes Mal rotiert, wenn Sie diesen Bildschirm schließen und erneut öffnen", + "input": "Eingabe", "inputs": "Eingaben", "insufficient_funds_for_tx": "Unzureichendes Guthaben, um die Transaktion erfolgreich auszuführen.", "insufficient_lamport_for_tx": "Sie haben nicht genug SOL, um die Transaktion und die Transaktionsgebühr zu decken. Bitte fügen Sie Ihrer Wallet mehr SOL hinzu oder reduzieren Sie den SOL-Betrag, den Sie senden.", @@ -606,6 +622,7 @@ "manage_pow_nodes": "PoW-Nodes verwalten", "manage_providers": "Anbieter verwalten", "manage_yats": "Yats verwalten", + "manual_input": "Manuelle Eingabe", "mark_as_redeemed": "Als eingelöst markieren", "market_place": "Marktplatz", "mask": "Maske", @@ -651,6 +668,7 @@ "nanogpt_subtitle": "Alle neuesten Modelle (GPT-5.3, Claude 4.6).\nKein Abonnement, bezahle mit Krypto.", "narrow": "Schmal", "network": "Netzwerk", + "networks": "Netzwerke", "new_first_wallet_text": "Ihre Kryptowährungen sicher aufzubewahren ist ein Kinderspiel", "new_node_testing": "Testen des neuen Nodes", "new_subaddress_create": "Erstellen", @@ -679,8 +697,10 @@ "node_new": "Neuer Node", "node_port": "Node-Port", "node_reset_settings_title": "Einstellungen zurücksetzen", + "node_share_desc": "Scannen Sie diesen QR-Code im Scan-Bildschirm, um diesen Node zu importieren.", "node_test": "Test", "nodes": "Nodes", + "nodes_desc": "Verwalten Sie bestehende Nodes oder fügen Sie einen benutzerdefinierten Node hinzu.", "nodes_list_reset_to_default_message": "Sind Sie sicher, dass Sie die Einstellungen auf die Standardeinstellungen zurücksetzen möchten?", "none_of_selected_providers_can_exchange": "Keiner der ausgewählten Anbieter kann diesen Swap durchführen", "noNFTYet": "Noch keine NFTs", @@ -690,6 +710,7 @@ "note_tap_to_change": "Notiz (zum Ändern tippen)", "notification_permission_denied": "Die Benachrichtigungsberechtigung wurde dauerhaft verweigert. Bitte aktivieren Sie sie manuell in den Einstellungen.", "nullURIError": "URI ist null", + "ocp_desc": "Mit Krypto im Geschäft bezahlen", "offer_expires_in": "Angebot läuft ab in: ", "offline": "Offline", "ok": "OK", @@ -716,8 +737,13 @@ "outputs": "Ausgänge", "overshot": "Überschuss", "overwrite_amount": "Betrag überschreiben", + "pairing_code": "Kopplungscode", + "pairing_code_desc_1": "Ihr Gerät zeigt einen Kopplungscode an.", + "pairing_code_desc_2": "Geben Sie ihn hier ein, um fortzufahren", + "pairing_error": "Kopplungsfehler", "pairingInvalidEvent": "Ungültiges Pairing-Ereignis", "passphrase": "Passphrase (Optional)", + "passphrase_view_keys": "Passphrase", "passphrases_doesnt_match": "Passphrasen stimmen nicht überein, bitte versuchen Sie es erneut", "password": "Passwort", "paste": "Einfügen", @@ -733,6 +759,7 @@ "payjoin_unavailable_sheet_content": "Um eine Payjoin-Transaktion zu empfangen, müssen Sie Bitcoin in Ihrem Wallet verfügbar haben.\n\nSobald Ihr Wallet über Guthaben verfügt, wird Payjoin automatisch aktiviert.", "payjoin_unavailable_sheet_title": "Warum ist Payjoin nicht verfügbar?", "payment_id": "Zahlungs-ID: ", + "payment_invoices": "Zahlungsanfragen", "payment_made_easy": "Zahlungen leicht gemacht", "payment_was_received": "Ihre Zahlung ist eingegangen.", "payments": "Zahlungen", @@ -762,8 +789,8 @@ "please_choose_one": "Bitte wählen Sie eine Option", "please_fill_totp": "Bitte geben Sie den 8-stelligen Code ein, der auf Ihrem anderen Gerät angezeigt wird", "please_make_selection": "Bitte treffen Sie unten eine Auswahl, um Ihre Wallet zu erstellen oder wiederherzustellen.", - "please_reference_document": "Bitte beachten Sie die untenstehenden Dokumente für weitere Informationen.", "Please_reference_document": "Weitere Informationen finden Sie in den Dokumenten unten.", + "please_reference_document": "Bitte beachten Sie die untenstehenden Dokumente für weitere Informationen.", "please_select": "Bitte auswählen:", "please_select_backup_file": "Bitte wählen Sie eine Sicherungsdatei aus und geben Sie das Sicherungskennwort ein.", "please_try_to_connect_to_another_node": "Bitte versuchen Sie, eine Verbindung zu einem anderen Node herzustellen", @@ -881,6 +908,9 @@ "restore_wallet": "Wallet wiederherstellen", "restore_wallet_name": "Wallet-Name", "restore_wallet_restore_description": "Beschreibung der Wallet-Wiederherstellung", + "resync_device": "Gerät resyncen", + "resync_device_desc": "Um das Guthaben aktuell zu halten muss Cake Wallet gelegentlich sich mit dem Gerät synchonisieren.", + "resync_device_cancel_warning_desc": "Dadurch wird der laufende Synchronisierungsprozess mit Ihrem Hardware-Gerät beendet.", "return_address_desc": "Im seltenen Fall eines fehlgeschlagenen Swaps können unsere Partner Ihre Zahlung automatisch an die angegebene Rückerstattungsadresse erstatten.", "return_address_desc_external": "Da Sie „Von extern senden“ ausgewählt haben, legen Sie bitte unten manuell eine Erstattungsadresse fest.", "robinhood_option_description": "Kaufen und übertragen Sie sofort mit Ihrer Debitkarte, Ihrem Bankkonto oder Ihrem Robinhood-Guthaben. Nur USA.", @@ -889,6 +919,7 @@ "save": "Speichern", "save_backup_password": "Bitte stellen Sie sicher, dass Sie Ihr Sicherungskennwort gespeichert haben. Sie können Ihre Sicherungsdateien ohne dieses nicht importieren.", "save_backup_password_alert": "Backup-Passwort speichern", + "save_contact": "Kontakt speichern", "save_to_downloads": "In „Downloads“ speichern", "saved_the_trade_id": "Ich habe die Trade-ID gespeichert", "savings": "Ersparnisse", @@ -1038,6 +1069,7 @@ "setup_your_debit_card": "Richten Sie Ihre Debitkarte ein", "share": "Teilen", "share_address": "Adresse teilen", + "share_this_node": "Diesen Node teilen", "shared_seed_wallet_groups": "Geteilte Seed-Wallet-Gruppen", "shared_seed_wallets": "Wallets mit gemeinsamem Seed", "show": "Anzeigen", @@ -1045,6 +1077,7 @@ "show_balance": "Lange drücken, um den Kontostand anzuzeigen", "show_balance_send_page": "Tippen, um anzuzeigen", "show_balance_toast": "Lange drücken, um den Kontostand auszublenden oder anzuzeigen", + "show_combined_balance": "Kombinierten Kontostand anzeigen", "show_details": "Details anzeigen", "show_hidden_addresses": "Versteckte Adressen anzeigen", "show_keys": "Seed/Schlüssel anzeigen", @@ -1110,6 +1143,7 @@ "swap_providers": "Swap-Anbieter", "sweeping_wallet": "Wallet leeren", "sweeping_wallet_alert": "Das sollte nicht lange dauern. Verlassen Sie diesen Bildschirm nicht, sonst können die geleerten Guthaben verloren gehen.", + "swipe_to_bridge": "Zum Bridging wischen", "swipe_to_send": "Zum Senden wischen", "switch_wallet": "Wallet wechseln", "switchToETHWallet": "Bitte wechseln Sie zu einer Ethereum-Wallet und versuchen Sie es erneut", @@ -1150,6 +1184,7 @@ "testnet_coins_no_value": "Testnet-Coins haben keinen Wert", "third_intro_content": "Yats existieren auch außerhalb von Cake Wallet. Jede Wallet-Adresse auf der Welt kann durch ein Yat ersetzt werden!", "third_intro_title": "Yat kommt gut mit anderen zurecht", + "this_can_take_few_seconds": "Dies kann einige Sekunden dauern", "this_pair_is_not_supported_warning": "Dieses Paar wird von den derzeit ausgewählten Börsen nicht unterstützt. Bitte wählen Sie eine andere Börse.", "thorchain_contract_address_not_supported": "THORChain unterstützt das Senden an eine Vertragsadresse nicht", "thorchain_taproot_address_not_supported": "Der ThorChain-Anbieter unterstützt keine Taproot-Adressen. Bitte ändern Sie die Adresse oder wählen Sie einen anderen Anbieter aus.", @@ -1163,6 +1198,7 @@ "token_decimal": "Token-Dezimalstelle (z. B.: USDT)", "token_name": "Token-Name (z. B.: Tether)", "token_symbol": "Token-Symbol (z. B.: USDT)", + "token_verified_on_jupiter": "Dieser Token ist auf Jupiter verifiziert", "tokenID": "ID", "tokens": "Token", "tokens_strong_warning": "Senden Sie niemals Token an diese Adresse!", @@ -1303,6 +1339,7 @@ "verify_message": "Nachricht verifizieren", "verify_seed": "Seed verifizieren", "verify_with_2fa": "Mit Cake 2FA verifizieren", + "verifying_code": "Code wird überprüft", "version": "Version ${currentVersion}", "view_all": "Alle anzeigen", "view_in_block_explorer": "Im Block Explorer anzeigen", @@ -1315,6 +1352,7 @@ "waiting_payment_confirmation": "Warte auf Zahlungsbestätigung", "wallet": "Wallet", "wallet_accounts": "Wallet-Konten", + "wallet_balance": "Wallet-Guthaben", "wallet_group": "Wallet-Gruppe", "wallet_group_description_existing_seed": "Sie haben sich entschieden, für diese Wallet einen vorhandenen Seed zu verwenden. Sie können den Seed erneut überprüfen, wenn Sie ihn bestätigen oder aufschreiben müssen.", "wallet_group_description_four": "um eine Wallet mit einem völlig neuen Seed zu erstellen.", @@ -1356,6 +1394,7 @@ "wc_approve_request_title": "Anfrage genehmigen", "wc_connect_request_title": "Verbindungsanfrage", "wc_connected_to": "Verbunden mit ${name}", + "wc_desc": "Einfach mit DApps verbinden", "wc_max_network_fee": "Maximale Netzwerkgebühr", "wc_message_to_sign": "Zu signierende Nachricht", "wc_network_fee": "Netzwerkgebühr", @@ -1406,6 +1445,7 @@ "yat_popup_content": "Sie können jetzt in Cake Wallet mit Ihrem Yat Krypto senden und empfangen – ein kurzer, emoji-basierter Benutzername. Verwalten Sie Yats jederzeit im Einstellungsbildschirm", "yat_popup_title": "Ihre Wallet-Adresse kann in Emojis umgewandelt werden.", "yes": "Ja", + "yes_exit": "Ja, beenden", "yesterday": "Gestern", "you_now_have_debit_card": "Sie haben jetzt eine Debitkarte", "you_pay": "Sie zahlen", @@ -1420,41 +1460,5 @@ "zcash_card_enable_later": "Sie können diese Karte jederzeit später in den Einstellungen aktivieren", "zcash_card_missing_funds": "Fehlende Gelder?", "zcash_card_scan": "Scannen", - "zcash_card_warning": "Schließen Sie die App nicht, bis der Vorgang abgeschlossen ist. Andernfalls muss der Vorgang von Grund auf neu gestartet werden.", - "are_you_sure_exit": "Möchten Sie die App wirklich verlassen?", - "available": "Verfügbar", - "bridging": "Bridge-Vorgang", - "compatible_services": "Kompatible Dienste", - "compatible_services_desc": "Verwenden Sie das Scan-Tool, um die folgenden von Cake Wallet unterstützten QR-Code-Typen zu lesen:", - "crypto_addresses": "Krypto-Adressen", - "delete_this_node": "Diesen Node löschen", - "device_confirmation": "Gerätebestätigung", - "durres_PIN": "Notfall-PIN", - "durres_PIN_set_up_successfully": "Die Notfall-PIN wurde erfolgreich eingerichtet", - "favorite_token": "Bevorzugter Token", - "favorite_token_desc": "Der Kontostand des bevorzugten Tokens wird auf der Guthabenkarte angezeigt.", - "gallery": "Galerie", - "hww_exit_desc": "Dadurch wird der laufende Kopplungsvorgang mit Ihrem Hardware-Gerät beendet", - "ignor": "Ignorieren", - "input": "Eingabe", - "manual_input": "Manuelle Eingabe", - "networks": "Netzwerke", - "node_share_desc": "Scannen Sie diesen QR-Code im Scan-Bildschirm, um diesen Node zu importieren.", - "nodes_desc": "Verwalten Sie bestehende Nodes oder fügen Sie einen benutzerdefinierten Node hinzu.", - "ocp_desc": "Mit Krypto im Geschäft bezahlen", - "pairing_code": "Kopplungscode", - "pairing_code_desc_1": "Ihr Gerät zeigt einen Kopplungscode an.", - "pairing_code_desc_2": "Geben Sie ihn hier ein, um fortzufahren", - "pairing_error": "Kopplungsfehler", - "passphrase_view_keys": "Passphrase", - "payment_invoices": "Zahlungsanfragen", - "save_contact": "Kontakt speichern", - "share_this_node": "Diesen Node teilen", - "show_combined_balance": "Kombinierten Kontostand anzeigen", - "swipe_to_bridge": "Zum Bridging wischen", - "this_can_take_few_seconds": "Dies kann einige Sekunden dauern", - "verifying_code": "Code wird überprüft", - "wallet_balance": "Wallet-Guthaben", - "wc_desc": "Einfach mit DApps verbinden", - "yes_exit": "Ja, beenden" -} + "zcash_card_warning": "Schließen Sie die App nicht, bis der Vorgang abgeschlossen ist. Andernfalls muss der Vorgang von Grund auf neu gestartet werden." +} \ No newline at end of file diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index f1ed60c63..06f271eb2 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -14,6 +14,7 @@ "activeConnectionsPrompt": "Active connections will appear here", "add": "Add", "add_a_note": "Add a note", + "add_a_passphrase": "Add a passphrase", "add_account": "Add Account", "add_contact": "Add contact", "add_contact_to_address_book": "Would you like to add this contact to your address book?", @@ -63,6 +64,7 @@ "already_your_username": "This is already your username!", "always": "Always", "amount": "Amount: ", + "amount_hidden": "Amount hidden", "amount_is_below_minimum_limit": "Your balance after fees would be less than the minimum amount needed for the exchange (${min})", "amount_is_estimate": "The receive amount is an estimate", "amount_is_guaranteed": "The receive amount is guaranteed", @@ -412,6 +414,7 @@ "enter_code": "Enter code", "enter_greater_than": "Please enter an amount greater than", "enter_less_than": "Please enter an amount less than", + "enter_on_device_instead": "Enter on device instead", "enter_recipient_address": "Please enter recipient address", "enter_seed_phrase": "Enter your seed phrase", "enter_totp_code": "Please enter the TOTP Code.", @@ -536,6 +539,7 @@ "got_it": "Got it", "gross_balance": "Gross Balance", "group_by_type": "Group by type", + "hardware_wallet": "Hardware wallet", "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "help", @@ -552,11 +556,11 @@ "how_to_connect": "How to Connect", "how_to_use": "How to use", "how_to_use_card": "How to use this card", - "hww_exit_desc": "This will ed the ongoing pairing process with your hardware device", + "hww_exit_desc": "This will end the ongoing pairing process with your hardware device", "i_understand": "I understand", "icon_style": "Icon style", "id": "ID: ", - "if_you_dont_see_your_device": "If you don't see your device above, please be sure your Ledger is awake and unlocked!", + "if_you_dont_see_your_device": "If you don't see your device above, please be sure your device is awake and unlocked!", "ignor": "Ignore", "import": "Import", "importNFTs": "Import NFTs", @@ -565,8 +569,9 @@ "incorrect_seed": "The text entered is not valid.", "incorrect_seed_option": "Incorrect. Please try again", "incorrect_seed_option_back": "Incorrect. Please make sure your seed is saved correctly and try again.", + "info": "Info", "infobox_auto_address": "Your receive address will rotate every time you receive a transaction", - "infobox_multichain": "Use this addess to receive any token or collectible on", + "infobox_multichain": "Use this address to receive any token or collectible on", "infobox_rotation": "Your receive address will rotate every time you close and reopen this screen", "input": "Input", "inputs": "Inputs", @@ -579,6 +584,7 @@ "invalid_input": "Invalid input", "invalid_password": "Invalid password", "invoice_details": "Invoice details", + "is_currently_being_migrated": "is currently being migrated", "is_percentage": "is", "keys": "Keys", "label": "Label", @@ -597,6 +603,7 @@ "lightning_deposit_desc": "When you deposit to Lightning, you are swapping your on-chain Bitcoin from this wallet to your Lightning account.", "lightning_deposit_disclaimer": "The new Lightning balance will not be available until the Bitcoin transaction is fully confirmed.", "lightning_external_disclaimer": "Don't close this page until you send the funds, or else the deposit won't proceed.", + "lightning_mode": "Lightning mode", "lightning_username_desc": "Easily receive Lightning payments with a Lightning username by choosing a custom name or using a randomly generated one below.", "lightning_username_desc_completed": "You can use this username to receive payments from any Lightning wallet.", "lightning_username_setup_later": "If you skip now, you can set this up later in the Settings.", @@ -628,7 +635,9 @@ "login": "Login", "logout": "Logout", "long_press_edit_address": "Long press to edit address", + "long_press_hide_balance": "Long press card to hide balance", "long_press_show_balance": "Long press card to show balance", + "long_press_to_copy": "Long press to copy", "low_fee": "Low fee", "low_fee_alert": "You currently are using a low network fee priority. This could cause long waits, different rates, or canceled trades. We recommend setting a higher fee for a better experience.", "made_easy": "made easy", @@ -653,6 +662,7 @@ "messages": "Messages", "method": "Method", "methods": "Methods", + "migrating_to_ironwood": "migrating to Ironwood", "min_amount": "Min: ${value}", "min_value": "Min: ${value} ${currency}", "mining": "Mining", @@ -736,6 +746,8 @@ "onramper_option_description": "Quickly buy crypto with many payment methods. Available in most countries. Spreads and fees vary.", "open_gift_card": "Open Gift Card", "open_wallet": "Open Wallet", + "opens_in_browser": "Opens in browser", + "opens_externally": "Opens externally", "optional_description": "Optional description", "optional_email_hint": "Optional payee notification email", "optional_name": "Optional recipient name", @@ -758,6 +770,8 @@ "pairing_error": "Pairing Error", "pairingInvalidEvent": "Pairing Invalid Event", "passphrase": "Passphrase (Optional)", + "passphrase_entry": "Passphrase entry", + "passphrase_raw": "Passphrase", "passphrase_view_keys": "Passphrase", "passphrases_doesnt_match": "Passphrases do not match, please try again", "password": "Password", @@ -848,6 +862,7 @@ "qr_code_format_note": "Different QR code formats are used by different hardware wallets in the Bitcoin space", "qr_fullscreen": "Tap to open full screen QR code", "qr_instruction": "Use this address to receive any token or collectible on", + "qr_parts_scanned": "Scanned ${scanned} of ${total} QR code parts", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", "quantity": "Quantity", "question_to_disable_2fa": "Are you sure that you want to disable Cake 2FA? A 2FA code will no longer be needed to access the wallet and certain functions.", @@ -860,6 +875,7 @@ "receiving": "Receiving", "recieving": "Recieving", "recipient_address": "Recipient address", + "recipient_number": "Recipient ${number}", "reconnect": "Reconnect", "reconnect_alert_text": "Are you sure you want to reconnect?", "reconnect_your_hardware_wallet": "Reconnect your Hardware Wallet", @@ -927,9 +943,13 @@ "restore_wallet": "Restore Wallet", "restore_wallet_name": "Wallet name", "restore_wallet_restore_description": "Wallet restore description", + "resync_device": "Resync device", + "resync_device_desc": "Balance updating requires occasional resyncing Cake Wallet with the device.", + "resync_device_cancel_warning_desc": "This will end the ongoing sync process with your hardware device", "return_address_desc": "In the rare case of a failed swap, our partners can automatically refund your payment to the provided refund address.", "return_address_desc_external": "Since you chose Send From External, please set a refund address manually below.", "robinhood_option_description": "Buy and transfer instantly using your debit card, bank account, or Robinhood balance. USA only.", + "rotate_address": "Get new address", "router_no_route": "No route defined for ${name}", "salary": "Salary", "save": "Save", @@ -960,7 +980,7 @@ "seed_alert_yes": "Yes, I have", "seed_and_keys": "Seed & Keys", "seed_choose": "Choose seed language", - "seed_display_path": "Menu → Security and Backup → Show key/seeds", + "seed_display_path": "Settings → Wallet settings → Seed & Keys", "seed_hex_form": "Wallet seed (hex form)", "seed_key": "Seed key", "seed_language": "Seed language", @@ -1090,6 +1110,7 @@ "share_this_node": "Share this node", "shared_seed_wallet_groups": "Shared Seed Wallet Groups", "shared_seed_wallets": "Shared Seed Wallets", + "shielding": "Autoshield", "show": "Show", "show_address_book_popup": "Show Address Book popup", "show_balance": "Long Press to Show Balance", @@ -1097,6 +1118,7 @@ "show_balance_toast": "Long press to hide or show balance", "show_combined_balance": "Show combined balance", "show_details": "Show Details", + "show_full_address": "Show full address", "show_hidden_addresses": "Show hidden addresses", "show_keys": "Show seed/keys", "show_market_place": "Show Marketplace", @@ -1139,6 +1161,7 @@ "sort_by": "Sort by", "spend_key_private": "Spend key (private)", "spend_key_public": "Spend key (public)", + "standard": "Standard", "starting_tor_proxy": "Starting Tor proxy", "status": "Status: ", "step": "Step", @@ -1159,10 +1182,13 @@ "swap_id": "Swap ID", "swap_provider_initial_desc": "Select the type of swap provider you prefer to use when swapping in", "swap_providers": "Swap providers", + "swap_reverse_direction": "Reverse swap direction", "sweeping_wallet": "Sweeping wallet", "sweeping_wallet_alert": "This shouldn’t take long. DO NOT LEAVE THIS SCREEN OR THE SWEPT FUNDS MAY BE LOST.", "swipe_to_bridge": "Swipe to bridge", "swipe_to_send": "Swipe to send", + "switch_camera": "Switch camera", + "switch_input_currency": "Switch between crypto and fiat entry", "switch_wallet": "Switch Wallet", "switchToETHWallet": "Please switch to an Ethereum wallet and try again", "switchToWCCompatibleWallet": "Please switch to a WalletConnect compatible wallet and try again (${chains}).", @@ -1199,6 +1225,7 @@ "tap_to_copy": "Tap to copy", "template": "Template", "template_name": "Template Name", + "test_node_speeds": "Test node speeds", "testnet_coins_no_value": "Testnet coins have no value", "third_intro_content": "Yats live outside of Cake Wallet, too. Any wallet address on earth can be replaced with a Yat!", "third_intro_title": "Yat plays nicely with others", @@ -1212,10 +1239,12 @@ "to_lightning": "to Lightning", "to_on_chain": "to on-chain", "today": "Today", + "token": "Token", "token_already_exists": "Token already exists", "token_decimal": "Token decimal (eg: 6 for USDT)", "token_name": "Token name (eg: Tether)", "token_symbol": "Token symbol (eg: USDT)", + "token_verified_on_jupiter": "This token is verified on Jupiter", "tokenID": "ID", "tokens": "Tokens", "tokens_strong_warning": "Never send tokens to this address!", @@ -1297,6 +1326,8 @@ "trusted": "Trusted", "try_again": "Try again", "trying_to_spend_locked_funds": "Transaction rejected because you are trying to spend funds that have relative time lock, please wait and try again later, or disable the locked funds from coin control", + "turn_flash_off": "Turn flash off", + "turn_flash_on": "Turn flash on", "tx_commit_exception_no_dust_on_change": "The transaction is rejected with this amount. With these coins you can send ${min} without change or ${max} that returns change.", "tx_commit_failed": "Transaction commit failed. Please contact support.", "tx_commit_failed_no_peers": "Transaction failed to broadcast, please try again in a second", @@ -1478,5 +1509,9 @@ "zcash_card_enable_later": "You can always enable this card later in settings", "zcash_card_missing_funds": "Missing funds?", "zcash_card_scan": "Scan", - "zcash_card_warning": "Do not close the app until the procedure completes, if you do so this process will need to restart from scratch." + "zcash_card_warning": "Do not close the app until the procedure completes, if you do so this process will need to restart from scratch.", + "zcash_network_upgrade": "Zcash Network Upgrade", + "zcash_network_upgrade_desc_1": "Zcash has undergone a network upgrade called Ironwood that requires migration of funds before they can be spent. This migration is automatic, privacy-preserving, and starts as soon as you open your Zcash wallet.", + "zcash_network_upgrade_desc_2": "When the migration finishes, your Available Balance will return to normal, and you can continue to send and receive Zcash as usual.", + "zcash_network_upgrade_desc_3": "Please allow your Zcash wallet to finishing syncing, and then leave the app open until migration finishes which can take a few minutes depending on the balance." } diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 91c41c6f2..16a6b5a3e 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -76,6 +76,7 @@ "approve_tokens": "Aprobar tokens", "apps": "Apps", "arbiscan_history": "Historial de ArbiScan", + "are_you_sure_exit": "¿Seguro que quieres salir?", "arrive_in_this_address": "${currency} ${tag}llegará a esta dirección", "ascending": "Ascendente", "ask_each_time": "Preguntar cada vez", @@ -89,6 +90,7 @@ "auto_generate_addresses": "Generar direcciones automáticamente", "auto_generate_subaddresses": "Generar subdirecciones automáticamente", "automatic": "Automático", + "available": "Disponible", "available_balance": "Saldo disponible", "available_balance_description": "El “Saldo Disponible” o “Saldo Confirmado” son fondos que pueden gastarse de inmediato. Si los fondos aparecen en el saldo inferior pero no en el saldo superior, deberás esperar unos minutos a que los fondos entrantes obtengan más confirmaciones de la red. Una vez que tengan más confirmaciones, podrás gastarlos.", "avg_savings": "Ahorro prom.", @@ -125,6 +127,7 @@ "block_remaining": "1 bloque restante", "Blocks_remaining": "${status} bloques restantes", "bluetooth": "Bluetooth", + "bridging": "Transfiriendo entre cadenas", "bright_theme": "Brillante", "bscscan_history": "Historial de BSCScan", "bump_fee": "Aumentar comisión", @@ -216,6 +219,8 @@ "color_theme": "Tema de color", "coming_soon_tag": "Próximamente", "commit_transaction_amount_fee": "Confirmar transacción\nCantidad: ${amount}\nComisión: ${fee}", + "compatible_services": "Servicios compatibles", + "compatible_services_desc": "Usa la herramienta Escanear para leer los siguientes tipos de códigos QR compatibles con Cake Wallet:", "configure": "Configurar", "confirm": "Confirmar", "confirm_delete_template": "Esta acción eliminará esta plantilla. ¿Deseas continuar?", @@ -279,6 +284,7 @@ "creating_new_wallet": "Creando una nueva billetera", "creating_new_wallet_error": "Error: ${description}", "creation_date": "Fecha de creación", + "crypto_addresses": "Direcciones cripto", "csv_nothing_to_export": "No hay nada que exportar", "current_swap": "Intercambio actual", "current_wallet": "Cartera actual", @@ -301,6 +307,7 @@ "default_sell_provider": "Proveedor de venta predeterminado", "delete": "Eliminar", "delete_account": "Eliminar cuenta", + "delete_this_node": "Eliminar este nodo", "delete_wallet": "Eliminar monedero", "delete_wallet_confirm_message": "¿Seguro que deseas eliminar la wallet ${wallet_name}?", "deleteConnectionConfirmationPrompt": "¿Estás seguro de que quieres eliminar la conexión con", @@ -330,6 +337,7 @@ "deuro_savings_welcome_description": "dEURO es una stablecoin totalmente descentralizada y colateralizada, vinculada al euro — segura, versátil y resistente a la censura sin tener que depender de oráculos. 1 dEURO = 1 EURO.", "deuro_tooltip_no_eth": "Para habilitar dEURO, debes tener ETH en tu billetera.", "deuro_tx_commited_content": "Puede tardar un par de segundos en que la transacción se confirme y se refleje en la pantalla", + "device_confirmation": "Confirmación del dispositivo", "device_is_signing": "El dispositivo está firmando", "dfx_option_description": "Compra cripto con EUR y CHF. Para clientes minoristas y corporativos en Europa", "did_you_back_up_seeds": "¿Hiciste una copia de seguridad de todas tus semillas?", @@ -466,6 +474,8 @@ "extracted_address_content": "Enviarás fondos a\n${recipient_name}", "failed_authentication": "Falló la autenticación. ${state_error}", "faq": "Preguntas frecuentes", + "favorite_token": "Token favorito", + "favorite_token_desc": "El saldo del token favorito se mostrará en la tarjeta de saldo.", "features": "Funciones", "fee": "Comisión", "fee_less_than_min": "La comisión seleccionada es inferior a la mínima. Por favor, aumenta la comisión para poder enviar la transacción.", @@ -491,6 +501,7 @@ "frozen": "Congelado", "frozen_balance": "Saldo congelado", "full_balance": "Saldo total", + "gallery": "Galería", "gas_exceeds_allowance": "El gas requerido por la transacción excede el límite permitido.", "gas_price": "Precio del gas", "general": "General", @@ -526,10 +537,10 @@ "how_to_connect": "Cómo conectarse", "how_to_use": "Cómo usar", "how_to_use_card": "Cómo usar esta tarjeta", + "hww_exit_desc": "Esto finalizará el proceso de emparejamiento en curso con tu dispositivo de hardware", "i_understand": "Entiendo", "icon_style": "Estilo de icono", "id": "ID: ", - "if_you_dont_see_your_device": "Si no ves tu dispositivo arriba, asegúrate de que tu Ledger esté encendido y desbloqueado.", "ignor": "Ignorar", "import": "Importar", "importNFTs": "Importar NFTs", @@ -541,6 +552,7 @@ "infobox_auto_address": "Su dirección de recepción rotará cada vez que reciba una transacción", "infobox_multichain": "Utilice esta dirección para recibir cualquier token o coleccionable en", "infobox_rotation": "Su dirección de recepción rotará cada vez que cierre y vuelva a abrir esta pantalla", + "input": "Entrada", "inputs": "Entradas", "insufficient_funds_for_tx": "Fondos insuficientes para ejecutar correctamente la transacción.", "insufficient_lamport_for_tx": "No tienes suficiente SOL para cubrir la transacción y su comisión. Por favor, añade más SOL a tu billetera o reduce la cantidad de SOL que estás enviando.", @@ -555,8 +567,8 @@ "keys": "Claves", "label": "Etiqueta", "label_address": "Etiquetar dirección", - "last_7_days": "Últimos 7 días", "last_30_days": "Últimos 30 días", + "last_7_days": "Últimos 7 días", "learn_more": "Más información", "ledger_connection_error": "No se pudo conectar a tu Ledger. Inténtalo de nuevo.", "ledger_error_device_locked": "El Ledger está bloqueado", @@ -607,6 +619,7 @@ "manage_pow_nodes": "Gestionar nodos PoW", "manage_providers": "Gestionar proveedores", "manage_yats": "Gestionar Yats", + "manual_input": "Entrada manual", "mark_as_redeemed": "Marcar como canjeado", "market_place": "Marketplace", "mask": "Máscara", @@ -652,6 +665,7 @@ "nanogpt_subtitle": "Todos los modelos más recientes (GPT-5.3, Claude 4.6).\nSin suscripción, paga con cripto.", "narrow": "Estrecho", "network": "Red", + "networks": "Redes", "new_first_wallet_text": "Mantener tus criptomonedas seguras es pan comido", "new_node_testing": "Prueba de nuevo nodo", "new_subaddress_create": "Crear", @@ -680,8 +694,10 @@ "node_new": "Nuevo nodo", "node_port": "Puerto del nodo", "node_reset_settings_title": "Restablecer configuración", + "node_share_desc": "Escanea este código QR desde la pantalla Escanear para importar este nodo.", "node_test": "Prueba", "nodes": "Nodos", + "nodes_desc": "Administra los nodos existentes o añade uno personalizado.", "nodes_list_reset_to_default_message": "¿Seguro que deseas restablecer la configuración a los valores predeterminados?", "none_of_selected_providers_can_exchange": "Ninguno de los proveedores seleccionados puede realizar este swap", "noNFTYet": "Aún no hay NFT", @@ -691,6 +707,7 @@ "note_tap_to_change": "Nota (toca para cambiar)", "notification_permission_denied": "El permiso de notificaciones se ha denegado permanentemente. Por favor, habilítalo manualmente en la configuración.", "nullURIError": "El URI es nulo", + "ocp_desc": "Paga con cripto en tiendas", "offer_expires_in": "La oferta caduca en: ", "offline": "Sin conexión", "ok": "OK", @@ -717,8 +734,13 @@ "outputs": "Salidas", "overshot": "Excedido", "overwrite_amount": "Sobrescribir monto", + "pairing_code": "Código de emparejamiento", + "pairing_code_desc_1": "Tu dispositivo mostrará un código de emparejamiento.", + "pairing_code_desc_2": "Introdúcelo aquí para continuar", + "pairing_error": "Error de emparejamiento", "pairingInvalidEvent": "Evento de emparejamiento no válido", "passphrase": "Frase de contraseña (opcional)", + "passphrase_view_keys": "Frase de contraseña", "passphrases_doesnt_match": "Las frases de contraseña no coinciden, inténtelo de nuevo", "password": "Contraseña", "paste": "Pegar", @@ -734,6 +756,7 @@ "payjoin_unavailable_sheet_content": "Recibir una transacción Payjoin requiere que tenga Bitcoin listo para usar.\n\nEn cuanto su billetera tenga fondos, Payjoin se habilitará automáticamente.", "payjoin_unavailable_sheet_title": "¿Por qué Payjoin no está disponible?", "payment_id": "ID de pago: ", + "payment_invoices": "Facturas de pago", "payment_made_easy": "Pagos fáciles", "payment_was_received": "Tu pago fue recibido.", "payments": "Pagos", @@ -890,6 +913,7 @@ "save": "Guardar", "save_backup_password": "Asegúrate de haber guardado tu contraseña de copia de seguridad. No podrás importar tus archivos de copia de seguridad sin ella.", "save_backup_password_alert": "Guardar contraseña de la copia de seguridad", + "save_contact": "Guardar contacto", "save_to_downloads": "Guardar en Descargas", "saved_the_trade_id": "He guardado el ID de la operación", "savings": "Ahorros", @@ -1039,6 +1063,7 @@ "setup_your_debit_card": "Configura tu tarjeta de débito", "share": "Compartir", "share_address": "Compartir dirección", + "share_this_node": "Compartir este nodo", "shared_seed_wallet_groups": "Grupos de carteras con semilla compartida", "shared_seed_wallets": "Carteras con semilla compartida", "show": "Mostrar", @@ -1046,6 +1071,7 @@ "show_balance": "Mantén pulsado para mostrar el saldo", "show_balance_send_page": "Toca para revelar", "show_balance_toast": "Mantén pulsado para ocultar o mostrar el saldo", + "show_combined_balance": "Mostrar saldo combinado", "show_details": "Mostrar detalles", "show_hidden_addresses": "Mostrar direcciones ocultas", "show_keys": "Mostrar frase semilla/claves", @@ -1111,6 +1137,7 @@ "swap_providers": "Proveedores de intercambio", "sweeping_wallet": "Cartera de barrido", "sweeping_wallet_alert": "Esto no debería tardar mucho. NO SALGAS DE ESTA PANTALLA O LOS FONDOS BARRIDOS PODRÍAN PERDERSE.", + "swipe_to_bridge": "Desliza para transferir entre cadenas", "swipe_to_send": "Desliza para enviar", "switch_wallet": "Cambiar cartera", "switchToETHWallet": "Por favor, cambia a una billetera de Ethereum y vuelve a intentarlo.", @@ -1151,6 +1178,7 @@ "testnet_coins_no_value": "Las monedas de testnet no tienen valor", "third_intro_content": "Los Yats también existen fuera de Cake Wallet. ¡Cualquier dirección de billetera del mundo puede reemplazarse por un Yat!", "third_intro_title": "Yat se lleva bien con otros", + "this_can_take_few_seconds": "Esto puede tardar unos segundos", "this_pair_is_not_supported_warning": "Este par no es compatible con el/los exchange(s) seleccionado(s) actualmente. Seleccione otro exchange.", "thorchain_contract_address_not_supported": "THORChain no admite envíos a una dirección de contrato", "thorchain_taproot_address_not_supported": "El proveedor de ThorChain no admite direcciones Taproot. Por favor, cambia la dirección o selecciona otro proveedor.", @@ -1164,6 +1192,7 @@ "token_decimal": "Decimales del token (ej.: USDT)", "token_name": "Nombre del token (p. ej., Tether)", "token_symbol": "Símbolo del token (p. ej.: USDT)", + "token_verified_on_jupiter": "Este token está verificado en Jupiter", "tokenID": "ID", "tokens": "Tokens", "tokens_strong_warning": "¡Nunca envíes tokens a esta dirección!", @@ -1292,6 +1321,7 @@ "username": "Nombre de usuario", "username_available": "Nombre de usuario disponible", "username_character_error": "Los nombres de usuario solo deben contener", + "username_check_your_network": "No se pudo verificar el nombre de usuario. Comprueba tu conexión de red.", "username_length_error": "Los nombres de usuario deben contener entre 6 y 32 caracteres alfanuméricos.", "username_length_error_1_64": "Los nombres de usuario deben contener entre 1 y 64 caracteres alfanuméricos.", "username_not_available": "Este nombre de usuario no está disponible.", @@ -1302,6 +1332,7 @@ "verify_message": "Verificar mensaje", "verify_seed": "Verificar frase semilla", "verify_with_2fa": "Verificar con Cake 2FA", + "verifying_code": "Verificando código", "version": "Versión ${currentVersion}", "view_all": "Ver todo", "view_in_block_explorer": "Ver en el explorador de bloques", @@ -1313,6 +1344,7 @@ "waitFewSecondForTxUpdate": "Por favor, espera unos segundos para que la transacción se refleje en el historial de transacciones.", "wallet": "Cartera", "wallet_accounts": "Cuentas de la cartera", + "wallet_balance": "Saldo de la cartera", "wallet_group": "Grupo de billeteras", "wallet_group_description_existing_seed": "Has elegido usar una frase semilla existente para esta billetera. Puedes verificar la frase semilla de nuevo si necesitas confirmarla o anotarla.", "wallet_group_description_four": "para crear una billetera con una frase semilla completamente nueva.", @@ -1354,6 +1386,7 @@ "wc_approve_request_title": "Aprobar solicitud", "wc_connect_request_title": "Solicitud de conexión", "wc_connected_to": "Conectado a ${name}", + "wc_desc": "Conéctate fácilmente a dApps", "wc_max_network_fee": "Tarifa máxima de red", "wc_message_to_sign": "Mensaje para firmar", "wc_network_fee": "Tarifa de red", @@ -1405,6 +1438,7 @@ "yat_popup_content": "Ahora puedes enviar y recibir criptomonedas en Cake Wallet con tu Yat, un nombre de usuario corto basado en emojis. Administra tus Yats en cualquier momento desde la pantalla de configuración", "yat_popup_title": "La dirección de tu billetera se puede convertir en emojis.", "yes": "Sí", + "yes_exit": "Sí. Salir", "yesterday": "Ayer", "you_now_have_debit_card": "Ahora tienes una tarjeta de débito", "you_pay": "Tú pagas", @@ -1454,4 +1488,4 @@ "wallet_balance": "Saldo de la cartera", "wc_desc": "Conéctate fácilmente a dApps", "yes_exit": "Sí. Salir" -} \ No newline at end of file +} diff --git a/res/values/strings_fa.arb b/res/values/strings_fa.arb index 40b83e118..bbdc78adc 100644 --- a/res/values/strings_fa.arb +++ b/res/values/strings_fa.arb @@ -76,6 +76,7 @@ "approve_tokens": "تأیید توکن‌ها", "apps": "برنامه‌ها", "arbiscan_history": "تاریخچه ArbiScan", + "are_you_sure_exit": "آیا مطمئن هستید که می‌خواهید خارج شوید؟", "arrive_in_this_address": "${currency} ${tag}به این آدرس خواهد رسید", "ascending": "صعودی", "ask_each_time": "هر بار بپرس", @@ -89,6 +90,7 @@ "auto_generate_addresses": "تولید خودکار آدرس‌ها", "auto_generate_subaddresses": "تولید خودکار زیرآدرس‌ها", "automatic": "خودکار", + "available": "در دسترس", "available_balance": "موجودی در دسترس", "available_balance_description": "«موجودی قابل‌دسترس» یا «موجودی تأییدشده» وجوهی هستند که می‌توان بلافاصله خرج کرد.\nاگر وجوه در موجودی پایین‌تر ظاهر شوند اما در موجودی بالا نباشند، باید چند دقیقه صبر کنید تا وجوه ورودی تأییدهای شبکه بیشتری دریافت کنند.\nپس از دریافت تأییدهای بیشتر، قابل خرج کردن خواهند بود.", "avg_savings": "میانگین صرفه‌جویی", @@ -118,11 +120,14 @@ "bitcoin_amount_display": "نمایش مقدار بیت‌کوین", "bitcoin_dark_theme": "تم تیره بیت‌کوین", "bitcoin_light_theme": "تم روشن بیت‌کوین", + "bitcoin_lightning_deposit": "واریز", + "bitcoin_lightning_withdraw": "برداشت", "bitcoin_payments_require_1_confirmation": "پرداخت‌های بیت‌کوین به ۱ تأیید نیاز دارند که ممکن است ۲۰ دقیقه یا بیشتر طول بکشد.\nاز شکیبایی شما سپاسگزاریم! پس از تأیید پرداخت، از طریق ایمیل به شما اطلاع داده خواهد شد.", "block_height": "ارتفاع بلاک", "block_remaining": "۱ بلاک باقی مانده", "Blocks_remaining": "${status} بلاک باقی‌مانده", "bluetooth": "بلوتوث", + "bridging": "در حال پل‌زدن", "bright_theme": "روشن", "bscscan_history": "تاریخچه BSCScan", "bump_fee": "افزایش کارمزد", @@ -214,6 +219,8 @@ "color_theme": "تم رنگ", "coming_soon_tag": "به‌زودی", "commit_transaction_amount_fee": "ثبت تراکنش\nمقدار: ${amount}\nکارمزد: ${fee}", + "compatible_services": "سرویس‌های سازگار", + "compatible_services_desc": "از ابزار اسکن برای خواندن انواع کدهای QR زیر که در Cake Wallet پشتیبانی می‌شوند استفاده کنید:", "configure": "پیکربندی", "confirm": "تأیید", "confirm_delete_template": "این اقدام این قالب را حذف می‌کند.\nآیا مایلید ادامه دهید؟", @@ -277,6 +284,7 @@ "creating_new_wallet": "در حال ایجاد کیف پول جدید", "creating_new_wallet_error": "خطا: ${description}", "creation_date": "تاریخ ایجاد", + "crypto_addresses": "آدرس‌های رمزارز", "csv_nothing_to_export": "چیزی برای صدور وجود ندارد", "current_swap": "سواپ فعلی", "current_wallet": "کیف پول فعلی", @@ -299,6 +307,7 @@ "default_sell_provider": "ارائه‌دهنده پیش‌فرض فروش", "delete": "حذف", "delete_account": "حذف حساب", + "delete_this_node": "حذف این نود", "delete_wallet": "حذف کیف پول", "delete_wallet_confirm_message": "آیا مطمئن هستید که می‌خواهید کیف پول ${wallet_name} را حذف کنید؟", "deleteConnectionConfirmationPrompt": "آیا مطمئن هستید که می‌خواهید اتصال به", @@ -328,6 +337,7 @@ "deuro_savings_welcome_description": "dEURO یک استیبل‌کوین کاملاً غیرمتمرکز و وثیقه‌گذاری‌شده است که به یورو متصل است — امن، چندمنظوره و مقاوم در برابر سانسور، بدون نیاز به تکیه بر اوراکل‌ها. 1 dEURO = 1 EURO.", "deuro_tooltip_no_eth": "برای فعال‌سازی dEURO، باید در کیف پول خود ETH داشته باشید.", "deuro_tx_commited_content": "ممکن است چند ثانیه طول بکشد تا تراکنش تأیید شود و در صفحه نمایش داده شود", + "device_confirmation": "تأیید دستگاه", "device_is_signing": "دستگاه در حال امضا کردن است", "dfx_option_description": "خرید کریپتو با EUR و CHF.\nبرای مشتریان خرد و شرکتی در اروپا", "did_you_back_up_seeds": "آیا از تمام عبارت‌های بازیابی خود نسخهٔ پشتیبان تهیه کرده‌اید؟", @@ -464,6 +474,8 @@ "extracted_address_content": "شما در حال ارسال وجه به\n${recipient_name} هستید", "failed_authentication": "احراز هویت ناموفق بود. ${state_error}", "faq": "پرسش‌های متداول", + "favorite_token": "توکن موردعلاقه", + "favorite_token_desc": "موجودی توکن موردعلاقه روی کارت موجودی نمایش داده می‌شود.", "features": "ویژگی‌ها", "fee": "کارمزد", "fee_less_than_min": "کارمزد انتخاب‌شده کمتر از حداقل است؛ لطفاً برای اینکه بتوانید تراکنش را ارسال کنید، کارمزد را افزایش دهید", @@ -489,6 +501,7 @@ "frozen": "منجمد", "frozen_balance": "موجودی فریز شده", "full_balance": "موجودی کامل", + "gallery": "گالری", "gas_exceeds_allowance": "گس (Gas) مورد نیاز برای تراکنش از حد مجاز فراتر می‌رود.", "gas_price": "قیمت گس", "general": "عمومی", @@ -524,10 +537,10 @@ "how_to_connect": "چگونه متصل شویم", "how_to_use": "نحوه استفاده", "how_to_use_card": "چگونه از این کارت استفاده کنیم", + "hww_exit_desc": "این کار، فرایند جفت‌سازی در حال انجام با دستگاه سخت‌افزاری شما را پایان می‌دهد", "i_understand": "متوجه شدم", "icon_style": "سبک آیکون", "id": "شناسه: ", - "if_you_dont_see_your_device": "اگر دستگاه خود را در بالا نمی‌بینید، لطفاً مطمئن شوید که Ledger شما روشن است و قفل آن باز شده است!", "ignor": "نادیده گرفتن", "import": "درون‌ریزی", "importNFTs": "وارد کردن NFTها", @@ -539,6 +552,7 @@ "infobox_auto_address": "آدرس دریافت شما هر بار که یک تراکنش دریافت می‌کنید تغییر می‌کند", "infobox_multichain": "از این آدرس برای دریافت هر توکن یا کالکتیبل در", "infobox_rotation": "هر بار که این صفحه را ببندید و دوباره باز کنید، آدرس دریافت شما تغییر می‌کند", + "input": "ورودی", "inputs": "ورودی‌ها", "insufficient_funds_for_tx": "موجودی کافی برای انجام موفقیت‌آمیز تراکنش وجود ندارد.", "insufficient_lamport_for_tx": "شما SOL کافی برای پوشش تراکنش و کارمزد آن ندارید.\nلطفاً SOL بیشتری به کیف پول خود اضافه کنید یا مقدار SOL ارسالی را کاهش دهید.", @@ -604,6 +618,7 @@ "manage_pow_nodes": "مدیریت نودهای PoW", "manage_providers": "مدیریت ارائه‌دهندگان", "manage_yats": "مدیریت Yatها", + "manual_input": "ورود دستی", "mark_as_redeemed": "علامت‌گذاری به‌عنوان بازخریدشده", "market_place": "بازار", "mask": "ماسک", @@ -649,6 +664,7 @@ "nanogpt_subtitle": "همه جدیدترین مدل‌ها (GPT-5.3, Claude 4.6).\nبدون اشتراک، پرداخت با کریپتو.", "narrow": "باریک", "network": "شبکه", + "networks": "شبکه‌ها", "new_first_wallet_text": "امن نگه داشتن رمزارزهای شما مثل آب خوردن است", "new_node_testing": "آزمایش نود جدید", "new_subaddress_create": "ایجاد", @@ -677,8 +693,10 @@ "node_new": "گره جدید", "node_port": "پورت نود", "node_reset_settings_title": "بازنشانی تنظیمات", + "node_share_desc": "برای وارد کردن این نود، این کد QR را از صفحهٔ اسکن اسکن کنید.", "node_test": "آزمایشی", "nodes": "نودها", + "nodes_desc": "نودهای موجود را مدیریت کنید یا یک نود سفارشی اضافه کنید.", "nodes_list_reset_to_default_message": "آیا مطمئن هستید که می‌خواهید تنظیمات را به حالت پیش‌فرض بازنشانی کنید؟", "none_of_selected_providers_can_exchange": "هیچ‌یک از ارائه‌دهندگان انتخاب‌شده نمی‌توانند این سواپ را انجام دهند", "noNFTYet": "هنوز هیچ NFT‌ای وجود ندارد", @@ -688,6 +706,7 @@ "note_tap_to_change": "یادداشت (برای تغییر ضربه بزنید)", "notification_permission_denied": "مجوز اعلان‌ها به‌طور دائمی رد شده است، لطفاً آن را به‌صورت دستی در تنظیمات فعال کنید", "nullURIError": "URI تهی است", + "ocp_desc": "پرداخت با رمزارز در فروشگاه‌ها", "offer_expires_in": "این پیشنهاد تا این زمان معتبر است: ", "offline": "آفلاین", "ok": "تأیید", @@ -714,6 +733,10 @@ "outputs": "خروجی‌ها", "overshot": "فراتر رفت", "overwrite_amount": "بازنویسی مقدار", + "pairing_code": "کد جفت‌سازی", + "pairing_code_desc_1": "دستگاه شما یک کد جفت‌سازی نمایش می‌دهد.", + "pairing_code_desc_2": "برای ادامه، اینجا وارد کنید", + "pairing_error": "خطای جفت‌سازی", "pairingInvalidEvent": "رویداد نامعتبر جفت‌سازی", "passphrase": "عبارت عبور (اختیاری)", "passphrase_view_keys": "عبارت عبور (Passphrase)", @@ -732,6 +755,7 @@ "payjoin_unavailable_sheet_content": "برای دریافت یک تراکنش Payjoin باید بیت‌کوین آمادهٔ استفاده داشته باشید.\n\nبه محض اینکه کیف پول شما موجودی داشته باشد، Payjoin به‌طور خودکار فعال می‌شود.", "payjoin_unavailable_sheet_title": "چرا Payjoin در دسترس نیست؟", "payment_id": "شناسه پرداخت: ", + "payment_invoices": "صورتحساب‌های پرداخت", "payment_made_easy": "پرداخت‌ها آسان شده‌اند", "payment_was_received": "پرداخت شما دریافت شد.", "payments": "پرداخت‌ها", @@ -887,6 +911,7 @@ "save": "ذخیره", "save_backup_password": "لطفاً مطمئن شوید که رمز عبور پشتیبان خود را ذخیره کرده‌اید.\nبدون آن نمی‌توانید فایل‌های پشتیبان خود را وارد کنید.", "save_backup_password_alert": "ذخیره گذرواژهٔ پشتیبان", + "save_contact": "ذخیره مخاطب", "save_to_downloads": "ذخیره در پوشهٔ دانلودها", "saved_the_trade_id": "شناسه معامله را ذخیره کردم", "savings": "پس‌انداز", @@ -1036,6 +1061,7 @@ "setup_your_debit_card": "راه‌اندازی کارت دبیت خود", "share": "اشتراک‌گذاری", "share_address": "اشتراک‌گذاری آدرس", + "share_this_node": "اشتراک‌گذاری این نود", "shared_seed_wallet_groups": "گروه‌های کیف پول با سید مشترک", "shared_seed_wallets": "کیف پول‌های سید مشترک", "show": "نمایش", @@ -1043,6 +1069,7 @@ "show_balance": "برای نمایش موجودی، لمس طولانی کنید", "show_balance_send_page": "برای آشکار کردن ضربه بزنید", "show_balance_toast": "برای مخفی یا نمایش دادن موجودی، لمس طولانی کنید", + "show_combined_balance": "نمایش موجودی ترکیبی", "show_details": "نمایش جزئیات", "show_hidden_addresses": "نمایش آدرس‌های مخفی", "show_keys": "نمایش عبارت بازیابی/کلیدها", @@ -1108,6 +1135,7 @@ "swap_providers": "ارائه‌دهندگان سواپ", "sweeping_wallet": "در حال انتقال موجودی کیف پول", "sweeping_wallet_alert": "این کار نباید طول بکشد.\nاین صفحه را ترک نکنید، وگرنه ممکن است وجوه انتقال‌یافته از دست برود.", + "swipe_to_bridge": "برای پل‌زدن، انگشت خود را بکشید", "swipe_to_send": "برای ارسال، انگشت خود را بکشید", "switch_wallet": "تعویض کیف پول", "switchToETHWallet": "لطفاً به کیف پول اتریوم تغییر دهید و دوباره تلاش کنید", @@ -1148,6 +1176,7 @@ "testnet_coins_no_value": "کوین‌های تست‌نت هیچ ارزشی ندارند", "third_intro_content": "Yatها خارج از Cake Wallet هم وجود دارند.\nهر آدرس کیف پولی در دنیا می‌تواند با یک Yat جایگزین شود!", "third_intro_title": "Yat با دیگران به‌خوبی کار می‌کند", + "this_can_take_few_seconds": "این ممکن است چند ثانیه طول بکشد", "this_pair_is_not_supported_warning": "این جفت‌ارز با صرافی(های) انتخاب‌شده در حال حاضر پشتیبانی نمی‌شود.\nلطفاً صرافی دیگری را انتخاب کنید.", "thorchain_contract_address_not_supported": "THORChain از ارسال به آدرس قرارداد پشتیبانی نمی‌کند", "thorchain_taproot_address_not_supported": "ارائه‌دهنده ThorChain از آدرس‌های Taproot پشتیبانی نمی‌کند.\nلطفاً آدرس را تغییر دهید یا ارائه‌دهنده دیگری را انتخاب کنید.", @@ -1161,6 +1190,7 @@ "token_decimal": "اعشار توکن (مثلاً: ۶ برای USDT)", "token_name": "نام توکن (مثلاً: Tether)", "token_symbol": "نماد توکن (مثلاً: USDT)", + "token_verified_on_jupiter": "این توکن در Jupiter تأیید شده است", "tokenID": "شناسه", "tokens": "توکن‌ها", "tokens_strong_warning": "هرگز توکن‌ها را به این آدرس ارسال نکنید!", @@ -1252,6 +1282,7 @@ "tx_rejected_dust_output": "تراکنش به‌دلیل قوانین شبکه رد شد؛ مقدار خروجی کم است (داست).\nلطفاً مقدار را افزایش دهید.", "tx_rejected_dust_output_send_all": "تراکنش طبق قوانین شبکه رد شد؛ مقدار خروجی کم است (داست).\nلطفاً موجودی کوین‌های انتخاب‌شده در Coin Control را بررسی کنید.", "tx_rejected_vout_negative": "موجودی کافی برای پرداخت کارمزدهای این تراکنش وجود ندارد.\nلطفاً موجودی کوین‌ها را در بخش Coin Control بررسی کنید.", + "tx_retry_message": "هنگام پردازش تراکنش خطایی رخ داد. لطفاً تراکنش را دوباره تلاش کنید.", "tx_wrong_balance_exception": "شما به اندازه کافی ${currency} برای ارسال این مقدار ندارید.", "tx_wrong_balance_with_amount_exception": "شما به اندازه کافی ${currency} برای ارسال مبلغ کل ${amount} ندارید", "tx_zero_fee_exception": "نمی‌توان تراکنشی با کارمزد ۰ ارسال کرد.\nسعی کنید نرخ را افزایش دهید یا برای دریافت آخرین برآوردها اتصال خود را بررسی کنید.", @@ -1276,6 +1307,7 @@ "upto": "تا ${value}", "usb": "USB", "use": "تغییر به ", + "use_blink_protection": "استفاده از محافظت Blink", "use_card_info_three": "از کارت دیجیتال به‌صورت آنلاین یا با روش‌های پرداخت بدون تماس استفاده کنید.", "use_card_info_two": "وجوه زمانی که در حساب پیش‌پرداخت نگهداری می‌شوند به دلار آمریکا (USD) تبدیل می‌شوند، نه به ارزهای دیجیتال.", "use_device_theme": "استفاده از تم دستگاه", @@ -1287,6 +1319,7 @@ "username": "نام کاربری", "username_available": "نام کاربری در دسترس است", "username_character_error": "نام‌های کاربری فقط باید شامل", + "username_check_your_network": "بررسی نام کاربری امکان‌پذیر نیست. لطفاً اتصال شبکهٔ خود را بررسی کنید.", "username_length_error": "نام‌های کاربری باید شامل ۶ تا ۳۲ کاراکتر حرفی-عددی باشند.", "username_length_error_1_64": "نام‌های کاربری باید شامل ۱ تا ۶۴ نویسهٔ حرفی-عددی باشند.", "username_not_available": "این نام کاربری در دسترس نیست.", @@ -1297,6 +1330,7 @@ "verify_message": "تأیید پیام", "verify_seed": "تأیید عبارت بازیابی", "verify_with_2fa": "تأیید با Cake 2FA", + "verifying_code": "در حال تأیید کد", "version": "نسخه ${currentVersion}", "view_all": "مشاهده همه", "view_in_block_explorer": "مشاهده در بلاک اکسپلورر", @@ -1308,6 +1342,7 @@ "waitFewSecondForTxUpdate": "لطفاً چند ثانیه صبر کنید تا تراکنش در تاریخچه تراکنش‌ها نمایش داده شود", "wallet": "کیف پول", "wallet_accounts": "حساب‌های کیف پول", + "wallet_balance": "موجودی کیف پول", "wallet_group": "گروه کیف پول", "wallet_group_description_existing_seed": "شما انتخاب کرده‌اید که از یک سید موجود برای این کیف پول استفاده کنید.\nاگر لازم است آن را تأیید کنید یا یادداشتش کنید، می‌توانید سید را دوباره بررسی کنید.", "wallet_group_description_four": "برای ایجاد یک کیف پول با سید کاملاً جدید.", @@ -1349,6 +1384,7 @@ "wc_approve_request_title": "درخواست را تایید کنید", "wc_connect_request_title": "درخواست اتصال", "wc_connected_to": "متصل به ${name}", + "wc_desc": "به‌راحتی به dAppها متصل شوید", "wc_max_network_fee": "حداکثر هزینه شبکه", "wc_message_to_sign": "برای امضا پیام دهید", "wc_network_fee": "هزینه شبکه", @@ -1399,6 +1435,7 @@ "yat_popup_content": "اکنون می‌توانید در Cake Wallet با Yat خود — یک نام کاربری کوتاه مبتنی بر ایموجی — رمزارز ارسال و دریافت کنید.\nYatها را هر زمان از صفحه تنظیمات مدیریت کنید", "yat_popup_title": "آدرس کیف پول شما می‌تواند به ایموجی تبدیل شود.", "yes": "بله", + "yes_exit": "بله. خروج", "yesterday": "دیروز", "you_now_have_debit_card": "شما اکنون یک کارت دبیت دارید", "you_pay": "شما پرداخت می‌کنید", @@ -1451,4 +1488,4 @@ "wallet_balance": "موجودی کیف پول", "wc_desc": "به‌راحتی به dAppها متصل شوید", "yes_exit": "بله. خروج" -} \ No newline at end of file +} diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 5327d2029..dd01db39e 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -76,6 +76,7 @@ "approve_tokens": "Approuver les tokens", "apps": "Apps", "arbiscan_history": "Historique ArbiScan", + "are_you_sure_exit": "Êtes-vous sûr de vouloir quitter ?", "arrive_in_this_address": "${currency} ${tag}arriveront à cette adresse", "ascending": "Croissant", "ask_each_time": "Demander à chaque fois", @@ -89,6 +90,7 @@ "auto_generate_addresses": "Générer automatiquement des adresses", "auto_generate_subaddresses": "Générer automatiquement des sous-adresses", "automatic": "Automatique", + "available": "Disponible", "available_balance": "Solde disponible", "available_balance_description": "Le « Solde disponible » ou le « Solde confirmé » correspond aux fonds que vous pouvez dépenser immédiatement. Si des fonds apparaissent dans le solde inférieur mais pas dans le solde supérieur, vous devez attendre quelques minutes pour que les fonds entrants obtiennent davantage de confirmations sur le réseau. Une fois qu’ils auront reçu plus de confirmations, ils pourront être dépensés.", "avg_savings": "Économies moy.", @@ -125,6 +127,7 @@ "block_remaining": "1 bloc restant", "Blocks_remaining": "${status} blocs restants", "bluetooth": "Bluetooth", + "bridging": "Transfert inter-chaînes", "bright_theme": "Clair", "bscscan_history": "Historique BSCScan", "bump_fee": "Augmenter les frais", @@ -216,6 +219,8 @@ "color_theme": "Thème de couleur", "coming_soon_tag": "Bientôt disponible", "commit_transaction_amount_fee": "Confirmer la transaction\nMontant : ${amount}\nFrais : ${fee}", + "compatible_services": "Services compatibles", + "compatible_services_desc": "Utilisez l'outil Scan pour lire les types de codes QR suivants pris en charge par Cake Wallet :", "configure": "Configurer", "confirm": "Confirmer", "confirm_delete_template": "Cette action supprimera ce modèle. Souhaitez-vous continuer ?", @@ -279,6 +284,7 @@ "creating_new_wallet": "Création d'un nouveau portefeuille", "creating_new_wallet_error": "Erreur : ${description}", "creation_date": "Date de création", + "crypto_addresses": "Adresses crypto", "csv_nothing_to_export": "Nothing to export", "current_swap": "Swap en cours", "current_wallet": "Portefeuille actuel", @@ -301,6 +307,7 @@ "default_sell_provider": "Fournisseur de vente par défaut", "delete": "Supprimer", "delete_account": "Supprimer le compte", + "delete_this_node": "Supprimer ce nœud", "delete_wallet": "Supprimer le portefeuille", "delete_wallet_confirm_message": "Êtes-vous sûr de vouloir supprimer le portefeuille ${wallet_name} ?", "deleteConnectionConfirmationPrompt": "Êtes-vous sûr de vouloir supprimer la connexion à", @@ -330,6 +337,7 @@ "deuro_savings_welcome_description": "dEURO est un stablecoin entièrement décentralisé et surcollatéralisé, indexé sur l’euro — sécurisé, polyvalent et résistant à la censure, sans avoir à dépendre d’oracles. 1 dEURO = 1 EURO.", "deuro_tooltip_no_eth": "Pour activer dEURO, vous devez avoir de l'ETH dans votre portefeuille.", "deuro_tx_commited_content": "La transaction peut prendre quelques secondes à être confirmée et à s'afficher à l'écran", + "device_confirmation": "Confirmation sur l'appareil", "device_is_signing": "L'appareil signe", "dfx_option_description": "Achetez des cryptos avec EUR & CHF. Pour les clients particuliers et entreprises en Europe", "did_you_back_up_seeds": "Avez-vous sauvegardé toutes vos phrases de récupération ?", @@ -466,6 +474,8 @@ "extracted_address_content": "Vous allez envoyer des fonds à\n${recipient_name}", "failed_authentication": "Échec de l'authentification. ${state_error}", "faq": "FAQ", + "favorite_token": "Jeton favori", + "favorite_token_desc": "Le solde du jeton favori s'affichera sur la carte de solde.", "features": "Fonctionnalités", "fee": "Frais", "fee_less_than_min": "Les frais sélectionnés sont inférieurs au minimum. Veuillez augmenter les frais afin de pouvoir envoyer la transaction.", @@ -491,6 +501,7 @@ "frozen": "Gelé", "frozen_balance": "Solde gelé", "full_balance": "Solde total", + "gallery": "Galerie", "gas_exceeds_allowance": "Le gas requis par la transaction dépasse l'allocation.", "gas_price": "Prix du gas", "general": "Général", @@ -526,10 +537,10 @@ "how_to_connect": "Comment se connecter", "how_to_use": "Comment utiliser", "how_to_use_card": "Comment utiliser cette carte", + "hww_exit_desc": "Cela mettra fin au processus de jumelage en cours avec votre appareil matériel", "i_understand": "Je comprends", "icon_style": "Style d'icône", "id": "ID : ", - "if_you_dont_see_your_device": "Si vous ne voyez pas votre appareil ci‑dessus, assurez‑vous que votre Ledger est allumé et déverrouillé !", "ignor": "Ignorer", "import": "Importer", "importNFTs": "Importer des NFTs", @@ -541,6 +552,7 @@ "infobox_auto_address": "Votre adresse de réception changera à chaque fois que vous recevrez une transaction", "infobox_multichain": "Utilisez cette adresse pour recevoir n'importe quel jeton ou objet de collection sur", "infobox_rotation": "Votre adresse de réception sera renouvelée à chaque fois que vous fermerez et rouvrirez cet écran", + "input": "Entrée", "inputs": "Entrées", "insufficient_funds_for_tx": "Fonds insuffisants pour exécuter la transaction avec succès.", "insufficient_lamport_for_tx": "Vous n'avez pas assez de SOL pour couvrir la transaction et ses frais. Veuillez ajouter du SOL à votre portefeuille ou réduire le montant de SOL que vous envoyez.", @@ -606,6 +618,7 @@ "manage_pow_nodes": "Gérer les nœuds PoW", "manage_providers": "Gérer les prestataires", "manage_yats": "Gérer les Yats", + "manual_input": "Saisie manuelle", "mark_as_redeemed": "Marquer comme utilisé", "market_place": "Marketplace", "mask": "Masquer", @@ -651,6 +664,7 @@ "nanogpt_subtitle": "Tous les modèles les plus récents (GPT-5.3, Claude 4.6).\nPas d'abonnement, payez en crypto.", "narrow": "Étroit", "network": "Réseau", + "networks": "Réseaux", "new_first_wallet_text": "Garder vos cryptos en sécurité, c'est du gâteau", "new_node_testing": "Test d’un nouveau nœud", "new_subaddress_create": "Créer", @@ -679,8 +693,10 @@ "node_new": "Nouveau nœud", "node_port": "Port du nœud", "node_reset_settings_title": "Réinitialiser les paramètres", + "node_share_desc": "Scannez ce code QR depuis l'écran Scan pour importer ce nœud.", "node_test": "Test", "nodes": "Nœuds", + "nodes_desc": "Gérez les nœuds existants ou ajoutez-en un personnalisé.", "nodes_list_reset_to_default_message": "Êtes-vous sûr de vouloir réinitialiser les paramètres par défaut ?", "none_of_selected_providers_can_exchange": "Aucun des fournisseurs sélectionnés ne peut effectuer ce swap", "noNFTYet": "Aucun NFT pour le moment", @@ -690,6 +706,7 @@ "note_tap_to_change": "Note (appuyez pour modifier)", "notification_permission_denied": "L'autorisation des notifications a été refusée de manière permanente. Veuillez l'activer manuellement dans les paramètres.", "nullURIError": "L'URI est null", + "ocp_desc": "Payez avec des cryptos en magasin", "offer_expires_in": "L'offre expire dans : ", "offline": "Hors ligne", "ok": "OK", @@ -716,8 +733,13 @@ "outputs": "Sorties", "overshot": "Dépassement", "overwrite_amount": "Écraser le montant", + "pairing_code": "Code de jumelage", + "pairing_code_desc_1": "Votre appareil affichera un code de jumelage.", + "pairing_code_desc_2": "Saisissez-le ici pour continuer", + "pairing_error": "Erreur de jumelage", "pairingInvalidEvent": "Événement de jumelage non valide", "passphrase": "Phrase secrète (facultatif)", + "passphrase_view_keys": "Phrase secrète", "passphrases_doesnt_match": "Les phrases secrètes ne correspondent pas, veuillez réessayer", "password": "Mot de passe", "paste": "Coller", @@ -733,6 +755,7 @@ "payjoin_unavailable_sheet_content": "La réception d'une transaction Payjoin nécessite que vous ayez des bitcoins disponibles.\n\nDès que votre portefeuille sera approvisionné, Payjoin sera automatiquement activé.", "payjoin_unavailable_sheet_title": "Pourquoi Payjoin est-il indisponible ?", "payment_id": "ID de paiement : ", + "payment_invoices": "Factures de paiement", "payment_made_easy": "Paiements simplifiés", "payment_was_received": "Votre paiement a été reçu.", "payments": "Paiements", @@ -888,6 +911,7 @@ "save": "Enregistrer", "save_backup_password": "Veuillez vous assurer d'avoir bien enregistré votre mot de passe de sauvegarde. Vous ne pourrez pas importer vos fichiers de sauvegarde sans celui-ci.", "save_backup_password_alert": "Enregistrer le mot de passe de sauvegarde", + "save_contact": "Enregistrer le contact", "save_to_downloads": "Enregistrer dans Téléchargements", "saved_the_trade_id": "J'ai enregistré l'ID de la transaction", "savings": "Épargne", @@ -1037,6 +1061,7 @@ "setup_your_debit_card": "Configurez votre carte de débit", "share": "Partager", "share_address": "Partager l’adresse", + "share_this_node": "Partager ce nœud", "shared_seed_wallet_groups": "Groupes de portefeuilles à phrase de récupération partagée", "shared_seed_wallets": "Portefeuilles à phrase de récupération partagée", "show": "Afficher", @@ -1044,6 +1069,7 @@ "show_balance": "Appuyez longuement pour afficher le solde", "show_balance_send_page": "Appuyez pour afficher", "show_balance_toast": "Appuyez longuement pour masquer ou afficher le solde", + "show_combined_balance": "Afficher le solde combiné", "show_details": "Afficher les détails", "show_hidden_addresses": "Afficher les adresses masquées", "show_keys": "Afficher la phrase de récupération/les clés", @@ -1109,6 +1135,7 @@ "swap_providers": "Fournisseurs de swap", "sweeping_wallet": "Balayage du portefeuille", "sweeping_wallet_alert": "Cela ne devrait pas prendre longtemps. NE QUITTEZ PAS CET ÉCRAN, SINON LES FONDS BALAYÉS RISQUENT D’ÊTRE PERDUS.", + "swipe_to_bridge": "Glissez pour transférer (bridge)", "swipe_to_send": "Faites glisser pour envoyer", "switch_wallet": "Changer de portefeuille", "switchToETHWallet": "Veuillez passer à un portefeuille Ethereum et réessayer", @@ -1149,6 +1176,7 @@ "testnet_coins_no_value": "Les coins de testnet n'ont aucune valeur", "third_intro_content": "Les Yats existent aussi en dehors de Cake Wallet. N'importe quelle adresse de portefeuille peut être remplacée par un Yat !", "third_intro_title": "Yat s'entend bien avec les autres", + "this_can_take_few_seconds": "Cela peut prendre quelques secondes", "this_pair_is_not_supported_warning": "Cette paire n'est pas prise en charge avec le(s) échange(s) actuellement sélectionné(s). Veuillez sélectionner un autre échange.", "thorchain_contract_address_not_supported": "THORChain ne prend pas en charge l'envoi vers une adresse de contrat", "thorchain_taproot_address_not_supported": "Le fournisseur ThorChain ne prend pas en charge les adresses Taproot. Veuillez modifier l'adresse ou sélectionner un autre fournisseur.", @@ -1162,6 +1190,7 @@ "token_decimal": "Décimales du token (ex. : USDT)", "token_name": "Nom du jeton (ex. : Tether)", "token_symbol": "Symbole du jeton (ex. : USDT)", + "token_verified_on_jupiter": "Ce jeton est vérifié sur Jupiter", "tokenID": "ID", "tokens": "Jetons", "tokens_strong_warning": "N'envoyez jamais de jetons à cette adresse !", @@ -1290,6 +1319,7 @@ "username": "Nom d'utilisateur", "username_available": "Nom d'utilisateur disponible", "username_character_error": "Les noms d'utilisateur ne doivent contenir que", + "username_check_your_network": "Impossible de vérifier le nom d'utilisateur. Veuillez vérifier votre connexion réseau.", "username_length_error": "Les noms d'utilisateur doivent contenir entre 6 et 32 caractères alphanumériques.", "username_length_error_1_64": "Les noms d'utilisateur doivent contenir entre 1 et 64 caractères alphanumériques.", "username_not_available": "Ce nom d'utilisateur n'est pas disponible.", @@ -1300,6 +1330,7 @@ "verify_message": "Vérifier le message", "verify_seed": "Vérifier la phrase de récupération", "verify_with_2fa": "Vérifier avec Cake 2FA", + "verifying_code": "Vérification du code", "version": "Version ${currentVersion}", "view_all": "Voir tout", "view_in_block_explorer": "Voir dans l'explorateur de blocs", @@ -1311,6 +1342,7 @@ "waitFewSecondForTxUpdate": "Veuillez patienter quelques secondes pour que la transaction apparaisse dans l'historique des transactions.", "wallet": "Portefeuille", "wallet_accounts": "Comptes du portefeuille", + "wallet_balance": "Solde du portefeuille", "wallet_group": "Groupe de portefeuilles", "wallet_group_description_existing_seed": "Vous avez choisi d'utiliser une seed existante pour ce portefeuille. Vous pouvez la vérifier à nouveau si vous devez la confirmer ou la noter.", "wallet_group_description_four": "pour créer un portefeuille avec une nouvelle phrase mnémonique.", @@ -1352,6 +1384,7 @@ "wc_approve_request_title": "Approuver la demande", "wc_connect_request_title": "Demande de connexion", "wc_connected_to": "Connecté à ${name}", + "wc_desc": "Connectez-vous facilement aux dApps", "wc_max_network_fee": "Frais de réseau maximum", "wc_message_to_sign": "Message à signer", "wc_network_fee": "Frais de réseau", @@ -1402,6 +1435,7 @@ "yat_popup_content": "Vous pouvez désormais envoyer et recevoir des cryptos dans Cake Wallet avec votre Yat — un nom d'utilisateur court basé sur des emojis. Gérez vos Yats à tout moment depuis l'écran des paramètres", "yat_popup_title": "L'adresse de votre portefeuille peut être convertie en emojis.", "yes": "Oui", + "yes_exit": "Oui. Quitter", "yesterday": "Hier", "you_now_have_debit_card": "Vous avez maintenant une carte de débit", "you_pay": "Vous payez", @@ -1451,4 +1485,4 @@ "wallet_balance": "Solde du portefeuille", "wc_desc": "Connectez-vous facilement aux dApps", "yes_exit": "Oui. Quitter" -} \ No newline at end of file +} diff --git a/res/values/strings_gn.arb b/res/values/strings_gn.arb index 693fe80b0..e5920374e 100644 --- a/res/values/strings_gn.arb +++ b/res/values/strings_gn.arb @@ -2,6 +2,7 @@ "about": "Marandu", "about_cake_pay": "Cake Pay omoneĩ ndéve reñogua hag̃ua tarjetas de regalo activos virtual reheve, ha ikatu reiporu pya’e hag̃ua hetave 150.000 comerciante-pe.", "about_litecoin_privacy": "Litecoin ñemigua rehegua", + "accent_color": "Sa'y acento", "account": "Cuenta", "account_customizer_desc": "Emoinge ha eity umi tarjeta emohenda hag̃ua umi cuenta.", "account_name": "Téra mba'ete", @@ -21,12 +22,16 @@ "add_fund_to_card": "Emoĩve viru jehepyme'ẽva tarjeta-pe (peve ${value})", "add_new_node": "Embojuaju nodo pyahu", "add_new_word": "Emoĩve ñe'ẽ pyahu", + "add_passphrase": "Embojoapy Passphrase", + "add_passphrase_warning_text": "Emoinge passphrase ramo añoite reipuru ra'ekue peteĩ ko billetera-pe yma. Remoingéramo passphrase hekope'ỹva térã ndereipuruiva'ekue passphrase ko billetera-pe, ndahecha mo'ãi mba'eve nde viru oĩva térã rembiasakue.", "add_receiver": "Embojoapy ambue ohupytýva (poravokuaa)", "add_secret_code": "Térã, embojuaju ko ayvu ñemigua peteĩ tembiporu'i jehechaukaha (authenticator) app-pe", "add_tip": "Emoĩve propína", + "add_token": "Embojoapy Token", "add_token_disclaimer_check": "Amoañetéma pe token contrato dirección ha marandu oúvo peteĩ fuente jeroviaha. Oñemoĩramo marandu ivaíva térã hekope'ỹva, ikatu ogueru viru ñehundi.", "add_token_warning": "Ani remohenda térã remoĩ token kuéra estafadór-kuéra he'íhaicha.\nEhechajey hag̃ua umi token dirección jerovia hag̃uáicha hag̃ua umi fuente jerovia hag̃uáicha!", "add_value": "Embojoapy tepykue", + "added_message_for_ata_error": "Ehecha porãke eguereko hetakue SOL saldo emohu'ã hag̃ua ne transacción.", "address": "Kundaharape", "address_book": "Kuatiañe’ẽ renda", "address_book_menu": "Kuatiajoapy'i kundaharape", @@ -47,7 +52,9 @@ "agree": "Aime", "agree_and_continue": "Amoneĩ ha Eku’e", "agree_to": "Emoheñóivo peteĩ kuenta, remoneĩ ", + "alert_notice": "Marandu", "all": "OPA", + "all_coins": "Opaite Moneda", "all_trades": "Opavave ñemuguata", "all_transactions": "Opavave ñembohasa", "alphabetical": "Alfabéticamente", @@ -63,8 +70,13 @@ "anonpay_description": "Ejapo ${type}. Pe ohupytýva ikatu ${method} oipurúvo oimeraẽva criptomoneda oipytyvõva, ha rehupytýta umi viru ko billetera-pe.", "apk_update": "APK ñembohekopyahu", "app_settings": "Aplikasión ñemboheko", + "appearance": "Jehechaha", "approve": "Moneĩ", + "approve_request": "Emoneĩ Jerure", + "approve_tokens": "Emoneĩ token-kuéra", "apps": "Apps", + "arbiscan_history": "ArbiScan rembiasakue", + "are_you_sure_exit": "¿Reime añetehápe resẽse?", "arrive_in_this_address": "${currency} ${tag} og̃uahẽta ko dirección-pe", "ascending": "Ojupíva", "ask_each_time": "Eporandu hag̃ua peteĩteĩ jey", @@ -78,12 +90,18 @@ "auto_generate_addresses": "Emoheñói hag̃ua dirección-kuera ijehegui", "auto_generate_subaddresses": "Heñoĩ ijehegui umi subdirección", "automatic": "Automático", + "available": "Oĩva", "available_balance": "Saldo ojepurukuaáva", "available_balance_description": "Pe “Available Balance” térã “Confirmed Balance” ha'e umi fondo ikatúva reipuru pya'e. Umi fondo ojehecháramo pe saldo yvýpe ha ndaha'éi pe saldo yvate gotyo, upéicharõ eha'arõ sapy'ami hag̃ua umi fondo og̃uahẽva ohupytyve hag̃ua confirmación de red. Oguerekóvo hetave confirmación, ikatúta reipuru.", "avg_savings": "Ñongatu mbytegua", "awaitDAppProcessing": "Eha'arõmi pe dApp omohu'ã hag̃ua hembiapo.", "awaiting_payment_confirmation": "Oha’arõ hína ñemoañete jehepyme’ẽ", + "background_sync": "Ñembojuehe tapykuegua", "background_sync_mode": "Modo de sincronización en segundo plano", + "background_sync_on_battery_low": "Embojuehe batería sa'ív>ramo", + "background_sync_on_charging": "Embojuehe oñembohyru jave añoite", + "background_sync_on_device_idle": "Embojuehe pe mba'e'oka ojepuru'ỹrõ añoite", + "background_sync_on_unmetered_network": "Oñeikotevẽ ñanduti ndojehepyme'ẽiva (unmetered)", "backup": "Ñongatu hag̃ua", "backup_file": "Marandurenda ñongatuha", "backup_password": "Ñe’ẽñemi ñongatuhápe g̃uarã", @@ -93,6 +111,7 @@ "balance_confirmation_desc_2": "Ojejapopa rire opaite ñemoañete, umi fondo oĩta eipuru hag̃ua.", "balance_confirmation_desc_3": "Ikatu rehecha hetave detalle repoko hag̃ua transacción oĩva omoañetéva hag̃ua tembiasápe.", "balance_page": "Balance rehegua kuatia", + "basescan_history": "BaseScan rembiasakue", "best_rate": "Tasa iporãvéva", "best_rate_mixed": "Tarifa iporãvéva (mezclada)", "bill_amount": "Kuatiañemuhague repy", @@ -101,10 +120,14 @@ "bitcoin_amount_display": "Bitcoin cantidad jehechauka", "bitcoin_dark_theme": "Tema ypytũ Bitcoin rehegua", "bitcoin_light_theme": "Tema sa'y Bitcoin rehegua", + "bitcoin_lightning_deposit": "Depósito", + "bitcoin_lightning_withdraw": "Ñeguenohẽ", "bitcoin_payments_require_1_confirmation": "Umi jehepyme’ẽ Bitcoin-pe oikotevẽ 1 ñemoneĩ, ikatúva hi’are 20 aravo’i térã hetave. Aguyje nde py’aguapýre! Oñemondóta ndéve peteĩ marandu correo rupive pe jehepyme’ẽ oñemoneĩ vove.", + "block_height": "Bloque yvate (block height)", "block_remaining": "Opyta 1 bloque", "Blocks_remaining": "${status} Bloque hembýva", "bluetooth": "Bluetooth", + "bridging": "Bridging", "bright_theme": "Tesakã", "bscscan_history": "Historial de BSCScan", "bump_fee": "Embohetave mba’erepy", @@ -114,17 +137,25 @@ "buy_bitcoin": "Ejogua Bitcoin", "buy_now": "Ejogua ko’ág̃a", "buy_provider_unavailable": "Pe proveedor ndaipóri ko’ág̃a.", + "buy_sell_pair_is_not_supported_warning": "Ko par moneda rehegua ndojepytyvõi mavave me'ẽhágui pe jehepyme'ẽrã ojeporavóvape. Eiporavo ambue par térã eha'ã emoambue pe jehepyme'ẽrã.", "buy_with": "Ejogua hag̃ua", "by_cake_pay": "Cake Pay rupive", "cake_2fa_preset": "Cake 2FA ñemboheko ypykue", "cake_dark_theme": "Tema pytũ Cake", "cake_pay_account_note": "Eñemboheraguapy peteĩ correo electrónico rupive añoite rehecha hag̃ua ha rejogua hag̃ua tarjeta. ¡Oĩ avei umi oĩva descuento-pe!", + "cake_pay_card_email_delivered_message": "Ne tarjeta regalo og̃uahẽta correo rupive umi ñemoañete tekotevẽva rire.", "cake_pay_learn_more": "Ejogua ha emboguejy hag̃ua tarjeta regálo pya’ete app rupive!\nEmbohasa nde kuã asúpe guive akatúape hag̃ua reikuaave.", "cake_pay_save_order": "Pe tarjeta oñemondóta nde correo electrónico-pe peteĩ ára hag̃ua (ára hag̃ua oñemba’apóva)\nEñongatu nde Order ID:", "cake_pay_subtitle": "Ejogua tarjeta prepagada ha tarjeta regalo opa tetã rupi.", "cake_pay_web_cards_subtitle": "Ejogua tarjeta prepagada ha tarjeta regalo opa tetã rupi.", "cake_pay_web_cards_title": "Cake Pay Web Cards", + "cake_seeds_save_disclaimer": "Eñongatúke ko'ã ñe'ẽ peteĩ tenda hekorosãvape! Reikotevẽta ko'ã ñe'ẽ emoĩporã jey hag̃ua ne billetera peteĩ mba'e'oka pyahúpe.", "cake_wallet": "Cake Wallet", + "cakepay_confirm_no_vpn": "Amoneĩ ndaipuruiha Proxy térã VPN", + "cakepay_confirm_purchase": "Emoneĩ Jejogua", + "cakepay_confirm_terms_agreed": "Amoneĩ umi término ha condición ojehechaukáva ko'ápe:", + "cakepay_confirm_voided_refund": "Aikũmby umi ñeha'ã reñemboguevi hag̃ua peteĩ tetã jejokopyrégui omboguéta oimeraẽ reembolso", + "cakepay_ios_not_available": "Ñembyasy, ko tarjeta regalo ndaipóri iOS-pe. Ikatu rejogua Android-pe térã ore web rupive.", "cakepay_prepaid_card": "Tarjeta de débito prepagada CakePay", "camera_consent": "Ojepurúta nde cámara ojapyhy hag̃ua ta’ãngamýi hag̃ua nde jehechakuaa hag̃ua ${provider} rupive. Eporandu hag̃ua mba’evéichapa omba’apo hag̃ua, ehecha hag̃ua i Política de Privacidad.", "camera_permission_is_required": "Oikotevẽ permiso hag̃ua ojepuru cámara. \nEmyandy hag̃ua app ñemohendápe.", @@ -136,6 +167,7 @@ "cardholder_agreement": "Acuerdo del titular de la tarjeta", "cards": "Tarjetas", "centralized": "Centralizado", + "chain_id": "Chain ID", "chains": "Cadenas", "change": "Moambue", "change_backup_password_alert": "Nde marandu ñongatupyre ymaveguare ndaikatumo’ãi ojegueru hag̃ua pe ñe’ẽñemi ñongatupyre pyahu reheve. Pe ñe’ẽñemi ñongatupyre pyahu ojepurúta añoite marandu ñongatupyre pyahúpe g̃uarã. Añetehápepa remoambue hag̃ua pe ñe’ẽñemi ñongatupyre rehegua?", @@ -153,19 +185,26 @@ "change_rep": "Emoambue pe Representante", "change_rep_message": "¿Reime añetehápe reipota emoambue umi representante?", "change_rep_successful": "Oñemoambue porãma pe representante", + "change_selected_exchanges": "Emoambue umi Intercambio Ojeporavóva", + "change_selected_pair": "Emoambue Par Ojeporavóva", "change_username": "Emoambue puruhára réra", "change_wallet_alert_content": "¿Reipotápa emoambue ko'ág̃a pe billetera hag̃ua ${wallet_name}?", "change_wallet_alert_title": "Emoambue billetera ko'ágãgua", "check_receiver_forms": "Ikatúpa, ehecha umi formulario receptor rehegua.", + "choose_a_payment_method": "Eiporavo peteĩ jehepyme'ẽrã", + "choose_a_provider": "Eiporavo peteĩ me'ẽha", "choose_account": "Eiporavo cuenta", "choose_address": "\n\nEiporavo hag̃ua pe dirección:", "choose_card_value": "Eiporavo peteĩ tarjeta repykue", "choose_derivation": "Eiporavo pe derivación pe billeterápe g̃uarã", "choose_from_available_options": "Eiporavo umi poravokuaa ojeguerekóva apytégui:", "choose_one": "Eiporavo peteĩ", + "choose_qr_code_format": "Eiporavo QR Code Formáto", + "choose_qr_code_format_note": "Eiporavo pe formáto hekopegua ne hardware wallet-pe g̃uarã:", "choose_relay": "Eiporavo peteĩ rele reipuru hag̃ua", "choose_wallet": "Eiporavo wallet", "choose_wallet_currency": "Eiporavo por favor pe moneda pe billetera-pe g̃uarã:", + "choose_wallet_group": "Eiporavo Billetera Atyrã (Wallet Group)", "clear": "Mopotĩ", "clearnet_link": "Joaju clearnet", "close": "Mboty", @@ -173,26 +212,41 @@ "coin_control_desc": "Embogua hag̃ua umi outputs ko transacción-pe guarã. Umi ñemoambue rejapóva ko’ápe ojehechaukáta pe transacción ko’ág̃aguáre añoite.", "coin_control_load_failed": "Ndojekargai umi moneda ndojeporúiva .", "cold_or_recover_wallet": "Embojoapy peteĩ cold wallet air-gapped térã emboykeréi peteĩ paper wallet.", + "collection_address": "Colección Kundaharape", + "collection_description": "Colección Ñemombe'u", + "collection_name": "Colección Téra", "color": "Sa'y", "color_theme": "Téma sa'y", + "coming_soon_tag": "Og̃uahẽ pototáma", "commit_transaction_amount_fee": "Emoneĩ ñembohasa\nMboheta: ${amount}\nHepyme’ẽ: ${fee}", + "compatible_services": "Servicio-kuéra Ojokupytýva", + "compatible_services_desc": "Eipuru pe Scan tembiporu emoñe'ẽ hag̃ua ko'ã QR código Cake Wallet oipytyvõva:", "configure": "Configurar", "confirm": "Moneĩ", "confirm_delete_template": "Ko tembiapo omboguepáta ko plantilla. ¿Reipotápa reku’ejey hag̃ua?", "confirm_delete_wallet": "Ko tembiapo omboguepáta ko billetera. ¿Reipota hag̃ua jajapo hag̃ua hag̃ua?", "confirm_fee_deduction": "Emoañete comisión jeipe'a", "confirm_fee_deduction_content": "¿Reime’ẽpa hag̃ua ojepe’a hag̃ua pe comisión pe salida-gui?", + "confirm_passphrase": "Emoneĩ passphrase", "confirm_sending": "Emoañete emondo hag̃ua", "confirm_silent_payments_switch_node": "Nde nodo ko'ágagua ndoipytyvõi Silent Payments.\n\nCake Wallet omoambuéta peteĩ nodo oikóva hag̃ua hag̃uaicha ojehechajey hag̃ua (escaneo) aja.", + "confirm_transaction": "Emoneĩ transacción", "confirmations": "Moneĩ", "confirmed": "Saldo confirmado", "confirmed_tx": "Omoañetepyréva", "confirming": "Oñemoneĩhína", "congratulations": "¡Vy'apavẽ!", "connect_an_existing_yat": "Embojoaju peteĩ Yat oĩmavare", + "connect_hw_info_step_1": "Ehecha porã ne mba'e'oka hendyha (ON).", + "connect_hw_info_step_2": "Emyendy Bluetooth mokõive mba'e'okápe ha embojoaju pe teléfono guive, TÉRÃ embojoaju ne mba'e'okakuéra peteĩ cable USB rupive.", + "connect_hw_info_step_3": "Eipe'a ne mba'e'oka ha eho pe cripto app reipotávape.", + "connect_hw_info_step_4": "Eiporavo ne mba'e'oka Cake Wallet-pe ha eku'ejey pe ñembosako'i ndive.", "connect_yats": "Embojoaju Yats", "connect_your_hardware_wallet": "Embojoaju nde billetera hardware Bluetooth térã USB rupive", + "connect_your_hardware_wallet_ble": "Embojoaju ne hardware wallet Bluetooth rupive", "connect_your_hardware_wallet_ios": "Embojoaju nde billetera hardware Bluetooth ndive", + "connect_your_hardware_wallet_usb": "Embojoaju ne hardware wallet USB rupive", + "connected": "Oñembojoaju", "connection_sync": "Joaju ha ñembojuehe", "connections": "Jeikeha", "connections_desc": "Emohenda joaju umi servicio ha API mbohapýha rehegua.", @@ -201,9 +255,11 @@ "contact_list_contacts": "Ñe'ẽngára", "contact_list_wallets": "Che Billeterakuéra", "contact_name": "Téra rera", + "contact_name_exists": "Oĩma peteĩ contacto ko téra reheve. Eiporavo ambue téra.", "contact_support": "Eñe’ẽ Support ndive", "contacts": "Contactos", "continue_text": "Eku'e", + "contract_address": "Contrato Kundaharape", "contract_warning": "Ko contrato dirección ojehechakuaa hag̃ua ikatuha haʼe peteĩ ñembotavy térã mba’e vai. Eñangareko porãke.", "contractName": "Contrato réra", "contractSymbol": "Símbolo del contrato", @@ -213,7 +269,10 @@ "copy": "Kopia", "copy_address": "Kopia dirección", "copy_id": "Emonguatia ID", + "copy_payjoin_address": "Emonguatia Payjoin Kundaharape", + "copy_payjoin_url": "Emonguatia Payjoin URL", "copyWalletConnectLink": "Emokopia dApp-gui pe juaju WalletConnect ha emboja ko'ápe", + "corrupted_seed_notice": "Ko billetera marandurenda oñembyai ha ndaikatúi ojepe'a. Ehecha pe seed, eñongatu, ha emoĩporã jey pe billetera.\n\nPe tepykue nandíramo, pe seed ndaikatúi oñemoĩporã hekopete.", "countries": "Tetãnguéra", "create_account": "Emoheñói cuenta", "create_backup": "Emoheñói peteĩ backup", @@ -221,10 +280,12 @@ "create_invoice": "Ejapo kuatiañemuhã", "create_new": "Emoheñói Billetera Pyahu", "create_new_account": "Emoheñói peteĩ cuenta pyahu", + "create_new_seed": "Emoheñói Seed Pyahu", "creating_backup": "Ojejapohína backup", "creating_new_wallet": "Omoheñóvo billetera pyahu", "creating_new_wallet_error": "Jejavy: ${description}", "creation_date": "Arange ojejapo hague", + "crypto_addresses": "Cripto Kundaharape-kuéra", "csv_nothing_to_export": "Nothing to export", "current_swap": "Intercambio ko'ág̃a", "current_wallet": "Billetera actual", @@ -233,6 +294,7 @@ "custom_fee": "Comisión personalizada", "custom_redeem_amount": "Hetakue jererepy'ỹva reñemboguevi hag̃ua", "custom_value": "Tepykue ñemomba'epyre", + "customBackgroundDescription": "Eguerekóma peteĩ background ne mba'éva.\\nReipota emyengovia térã embogue?", "dark_theme": "Pytũ", "debit_card": "Tarjeta de débito", "debit_card_terms": "Pe ñeñongatu ha jeiporu nde kuatia’atã ñemuhã papapy (ha umi credencial oñembojoajúva upe papapy rehe) ko billetera digital-pe oĩ ipoguýpe umi Término ha Condición oĩva pe kuatia’atã ñemuhã jára ñe’ẽme’ẽme, pe kuatia’atã ome’ẽva ndive, oĩ hag̃uáicha sapy’ánte.", @@ -240,10 +302,13 @@ "decentralized_only": "Descentralizado añoite hag̃ua", "decimal_places_error": "Hetaite tenda decimal", "decimals_cannot_be_zero": "Pe token decimal ndaikatúi haʼe cero (0).", + "decred_info_card_details": "Decred oipuru peteĩ ñembojuehe descentralizado ha oñangarekóva tekoñemíre, 'SPV' hérava, ohóva hi'are vyterei peteĩ Bitcoin billetera tee-gui. Reikuaave hag̃ua, eikutu iguýpe.", + "decred_info_title": "Ñembojuehe Decred-pe", "default_buy_provider": "Me’ẽhára ijypykuéva hag̃ua ojogua", "default_sell_provider": "Ñeme’ẽhára ñemu hag̃ua ijypykuéva", "delete": "Embogue", "delete_account": "Embogue hag̃ua", + "delete_this_node": "Embogue ko nodo", "delete_wallet": "Embogue billetera", "delete_wallet_confirm_message": "¿Reime añete rembogueseha ${wallet_name} billetera?", "deleteConnectionConfirmationPrompt": "¿Reime añete rembogueseha pe joaju hag̃ua?", @@ -254,19 +319,44 @@ "destination_tag": "Teramoĩha hag̃ua:", "destination_tag_optional": "Etiqueta destino rehegua (opcional) .", "destination_tag_swap_disclaimer": "Ojeruréva receptor oacredita haguã nde fondo. Etiqueta ofaltáva térã ojavýva he'ise fondo okañýva.", + "deuro_about_deuro": "dEURO rehegua", + "deuro_collect_interest": "Embyaty", + "deuro_reinvest_interest": "Einvertí jey", + "deuro_savings": "dEuro Ñongatu", + "deuro_savings_add": "Depósito", + "deuro_savings_approve_app": "Emoneĩ App", + "deuro_savings_approve_app_description": "Eheja pe dEURO smart contract oñangareko ne dEURO token-kuérare ñongatúpe g̃uarã, ha katu ndaikatumo'ãi omondo térã ohupyty ne dEURO.", + "deuro_savings_available_to_add": "Ojepurukuaáva embojoapy hag̃ua", + "deuro_savings_available_to_remove": "Ojepurukuaáva eipe'a hag̃ua", + "deuro_savings_balance": "Ñongatu Saldo", + "deuro_savings_balance_tooltip": "Kóva ohechauka ne ñongatu saldo ko'ag̃agua, ohupytýva interés ára ohasávo. Ikatu redepositá térã reguenohẽ ne dEURO oimeraẽ ára.", + "deuro_savings_collect_interest": "Embyaty interés", + "deuro_savings_collect_interest_tooltip": "Kóva ohechauka ne interés rehupytýmava, ikatúva embyaty ne dEURO saldo-pe oimeraẽ ára.", + "deuro_savings_remove": "Ñeguenohẽ", + "deuro_savings_set_approval": "Emoĩ ñemoneĩ", + "deuro_savings_subtitle": "Eg̃ana 10% peve interés ne dEuro Stablecoin rehe", + "deuro_savings_welcome_description": "dEURO ha'e peteĩ stablecoin descentralizado ha colateralizado, ojoajúva Euro-re — hekorosã, ojepuruporãva, ha noñembyaikuaáiva oraculo-re ojejerovia'ỹre. 1 dEURO = 1 EURO.", + "deuro_tooltip_no_eth": "Emboguata hag̃ua dEURO, tekotevẽ ereko ETH ne billetera-pe.", + "deuro_tx_commited_content": "Ikatu ohasa mokõi-mbohapy segundo pe transacción oñemoneĩ ha ojehechauka hag̃ua pantalla-pe", + "device_confirmation": "Mba'e'oka Ñemoneĩ", + "device_is_signing": "Pe mba'e'oka oñemboheraguapýhína", "dfx_option_description": "Ejogua cripto EUR ha CHF reheve. Oipurukuaa hag̃ua ñemuhára michĩ ha mba’apohaguasu Europape.", "did_you_back_up_seeds": "¿Rejapo raʼe backup opaite nde seed rehegua?", "didnt_get_code": "¿Nererejapýi kódigo?", "digit_pin": "PIN -dígito", "digital_and_physical_card": "Tarjeta de débito prepago digital y física", "disable": "Mbogue", + "disable_automatic_exchange_status_updates": "Embogue Intercambio Tekotee Ñembohekopyahu Ijeheguíva", "disable_bulletin": "Embogue pe marandu hag̃ua mba’éichapa oĩ pe servicio", "disable_buy": "Eipe'a hag̃ua jejogua", "disable_cake_2fa": "Embogue Cake 2FA", "disable_exchange": "Eipe'a hag̃ua ñembohasa", + "disable_exchange_option": "Embogue Intercambio jeporavorã", "disable_fee_api_warning": "Embogue hag̃ua péva, umi tarifa comisión ikatu ndoikoporãi heta hendápe, upévare ikatu rembojehepyme’ẽ hetave térã sa’ive pe comisión ne transaksiónpe g̃uarã.", "disable_fiat": "Eipe'a fiat", "disable_sell": "Eipe'a hag̃ua ñemu hag̃ua", + "disable_tor": "Embogue Tor", + "disable_trade_option": "Embogue ñemu jeporavorã", "disableBatteryOptimization": "Eipe'a hag̃ua ñemopotĩ batería rehegua", "disableBatteryOptimizationDescription": "¿Reipe’asépa batería ñemoporã hag̃ua pe ñembojoaju tapykuepeguá oiko hag̃ua isãso ha iporãve hag̃ua?", "disabled": "Ojeipe'a", @@ -284,6 +374,7 @@ "domain_looks_up": "Jeheka dominio", "donation": "Me’ẽ", "donation_link_details": "Mba’ekuaarã juajuha ñeme’ẽrei rehegua", + "done": "Oĩmbáma", "duress_pin_description": "Kóva omoĩta peteĩ PIN duress rehegua, peteĩ tembiapoite iñambuéva ndaha’éiva ojepuru hag̃uarã hetavéva puruhára. Ko PIN ojepuruva’erã reime ramo peligro-pe añoite. Reipuru rire ko PIN, opaite ne billetera oñemboguéta, upévare eñangareko opaite ne seed oñeñongatúma hag̃ua reipuru mboyve.", "durres_PIN": "Duress PIN", "durres_PIN_set_up_successfully": "Oñemboheko porãma Duress PIN.", @@ -297,7 +388,12 @@ "edit_token": "Editar token", "electrum_address_disclaimer": "Romoheñói dirección pyahu peteĩteĩ reipuru vove peteĩ, ha katu umi dirección ymaguare gueteri omba'apo.", "email_address": "Dirección de correo electrónico", + "enable": "Emboguata", + "enable_auto_node_switching": "Emboguata Nodo Ñemoambue Ijeheguíva", + "enable_builtin_tor": "Emboguata Tor oñembojoajupyréva", + "enable_for_auto_switching": "Emboguata Ñemoambue Ijehegui-rã", "enable_lightning": "Emboguata Lightning", + "enable_mempool_api": "Mempool API hepyme'ẽ ha arange hekopeguáva g̃uarã", "enable_replace_by_fee": "Embojuruja Replace-By-Fee", "enable_silent_payments_scanning": "Eñepyrũ eheka hag̃ua umi transaksión oñemondo hag̃ua nde Silent Payment kundaharape hag̃ua.", "enabled": "Myandy", @@ -337,15 +433,21 @@ "error_text_template": "Pe téra ha dirección plantilla rehegua ndaikatúi oguereko umi símbolo ` , ' \"\nha oguerekova’erã 1 guive 106 peve tai ipukukue", "error_text_wallet_name": "Pe billetera réra ikatu oguereko tai, papapy ha umi ta’ãnga _ - añoite\nha oĩva’erã 1 ha 33 tai pukukue", "error_text_xmr": "Pe valor XMR ndaikatúi ohasa pe saldo eiporukuaa hag̃.\nPe papapy dígito fraccionario rehegua va'erã sa'ive térã peteĩcha 12", + "error_while_processing": "Oiko peteĩ jejavy oñeprocesa aja", "errorGettingCredentials": "Ojavy: jejavy ojejuhúvo credenciales", "errorSigningTransaction": "Oiko jejavy oñemboheraguapývo pe transacción.", + "establishing_tor_connection": "Oñemboheko hína Tor jeikeha", "estimated": "Jehaipyre", "estimated_new_fee": "Mba’erepy pyahu jehaipyre", "estimated_receive_amount": "Mbohetapy ñemog̃uahẽ hag̃ua jehaipyre", + "eta": "ETA", + "ethereum_ecosystem": "Ethereum Ecosistema", "etherscan_history": "Etherscan rembiasakue", "event": "Tembiaporã", "events": "Tembiaporã", + "evm_ecosystem_description": "Tekotevẽramo, emoambue pe red ha token reipuruséva ko jehepyme'ẽme g̃uarã.", "exchange": "Mbojopyru", + "exchange_extra_info": "Anike nde resarái emombe'u hag̃ua pe Memo térã Extra ID emondóvo ko transacción intercambio-rã", "exchange_incorrect_current_wallet_for_xmr": "Embojopyruse hag̃ua XMR nde saldo Monero Cake Wallet-pe guive, emoambue raẽ nde Monero wallet-pe.", "exchange_new_template": "Plantilla nueva", "exchange_provider_unsupported": "¡${providerName} ndaikatúi ojepuruvé!", @@ -356,6 +458,7 @@ "exchange_result_write_down_ID": "*Ekopiáke térã ehaíke nde ID ojehechaukáva yvate.", "exchange_result_write_down_trade_id": "Ikatúramo, ekopia térã rehai pe trade ID hag̃ua resegí hag̃ua.", "exchange_sync_alert_content": "Eha'arõ ikatu hag̃uáicha nde billetera oñembojuehe hag̃ua.", + "exchange_trade_result_confirm": "Eikutúvo emondo Cake Wallet guive, remondóta ${fetchingLabel} ${from} ne billetera hérava ${walletName} guive pe kundaharape ojehechaukáva yvýpe. Térã ikatu remondo ne billetera okapegua guive pe kundaharape/QR código oĩva 'send from external wallet' kuatiarogue-pe.\n\nEikutúke oimeraẽ botõ resegí hag̃ua térã ejevy emoambue hag̃ua umi monto.", "expired": "Opa", "expires": "Opa hag̃ua", "expiresOn": "Opa hag̃ua", @@ -363,20 +466,28 @@ "export_backup": "Emondo hag̃ua backup", "export_csv": "Export CSV", "export_lightning_logs": "Emondo Lightning rembiapokue", + "export_logs": "Emondo registro-kuéra", + "export_outputs": "Emondo outputs", "export_payjoin_logs": "Emondo okápe Payjoin registro-kuéra", + "extend_session": "Embotuichave Sesión", "external": "Okápegua", "external_wallet": "Billetera externa", "extra_id": "ID extra:", "extracted_address_content": "Emondo hag̃ua viru\n${recipient_name}-pe", "failed_authentication": "Ojejavy hag̃ua jehechajey. ${state_error}", "faq": "FAQ", + "favorite_token": "Token ojehayhúva", + "favorite_token_desc": "Pe token ojehayhúva saldo ojehechaukáta pe saldo tarjeta-pe.", "features": "Tembiapoite", "fee": "Comisión", + "fee_less_than_min": "Pe hepyme'ẽ ojeporavóva michĩve pe mínimo-gui; embohetave pe hepyme'ẽ ikatu hag̃ua remondo pe transacción", + "fee_rate": "Hepyme'ẽ tasa", "fees": "Comisión", "fetching": "Oñembyatyhína", "fiat_api": "API Fiat", "fiat_balance": "Fiat saldo", "field_required": "Ko kora tekotevẽva", + "file_saved": "Marandurenda oñeñongatúma", "fill_code": "Ikatúpiko emyenyhẽ pe ayvu ñemi jehechaukaha oñemondóva nde korreópe.", "filter_by": "Mboguaha rupi", "filters": "Filtro-kuéra rehegua", @@ -388,12 +499,18 @@ "forgot_password": "Nderesarái ñe’ẽñemi", "freeze": "Mbo’y", "frequently_asked_questions": "Porandu ha mbohovái ojeporuvéva", + "from": "Guive", "frozen": "Congelado", + "frozen_balance": "Saldo Oñembotýva", "full_balance": "Saldo completo", + "gallery": "Galería", + "gas_exceeds_allowance": "Pe gas oikotevẽva transacción rupi ohasa pe ñemoneĩ (allowance).", + "gas_price": "Gas repy", "general": "Mba'eichagua", "generate_name": "Emoheñói Téra", "generating_csv": "Generating CSV...", "generating_gift_card": "Oñemoheñói hína tarjeta jopói", + "generating_transaction": "Oñemoheñói hína transacción", "get_a": "Ereko peteĩ ", "get_card_note": "Ikatu emyenyhẽjey viru digital rupive. Ndoikotevẽi ambue marandu!", "get_your_yat": "Eguereko nde Yat", @@ -410,36 +527,47 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Ñangarekoha iporãiterei Haven-pe g̃uarã", "help": "Pytyvõ", + "hidden_addresses": "Kundaharape Kañymby-kuéra", "hidden_balance": "Saldo ñemi", + "hide": "Mokañy", "hide_address": "Eñomi dirección", "hide_details": "Mokañy Detalles", "high_contrast_theme": "Téma Kontráste Yvate", "history": "Tembiasakue", "home": "Ñepyrũ", "home_screen_settings": "Ñemboheko pe pantalla ñepyrũme", + "how_to_connect": "Mba'éichapa Oñembojoaju", "how_to_use": "Mba’éichapa ojepuru", "how_to_use_card": "Mba’éichapa ojeiporu ko tarjeta", + "hww_exit_desc": "Kóva omboty pe joaju oikóva ne hardware mba'e'oka ndive", "i_understand": "Aikũmby", "icon_style": "Estilo icono rehegua", "id": "ID: ", + "if_you_dont_see_your_device": "Ndehecháiramo ne mba'e'oka yvate, ehecha porãke ne Ledger hesãi ha ipe'apyre!", "ignor": "Eheja", "import": "Embohasa", "importNFTs": "Embogueru NFTs", "in_store": "Ñemuhápe", "incoming": "Oikeva", "incorrect_seed": "Pe jehaipyre emoingeva ndoikói.", + "incorrect_seed_option": "Hekope'ỹ. Eha'ã jey", + "incorrect_seed_option_back": "Hekope'ỹ. Ehecha porãke ne seed oñeñongatu hekopete ha eha'ã jey.", "infobox_auto_address": "Nde dirección de recepción ojere va’erã opaite jey rehupytývo peteĩ transacción.", "infobox_multichain": "Eipuru ko dirección rehe hag̃ua rehepytyvõ hag̃ua oimeraẽva token térã coleccionable.", "infobox_rotation": "Nde dirección rehegua hag̃ua reñemog̃uahẽta ojere hag̃ua opaite jey remboty ha reike jey vove ko pantalla-pe.", + "input": "Jeikeha (input)", "inputs": "Jeikeha", + "insufficient_funds_for_tx": "Ndaipóri viru he'íva emohu'ã hag̃ua pe transacción.", "insufficient_lamport_for_tx": "Ndaipóri ndéve heta SOL ohepyme’ẽ hag̃ua pe transacción ha hepyme’ẽ hag̃ua avei pe comisión. Ikatu remoĩve SOL nde billetera-pe térã remomichĩve pe SOL remondoséva.", "insufficient_lamports": "Ndaipóri ndéve heta SOL hag̃ua ecoveri hag̃ua pe transacción ha pe hepykue transacción rehegua. Reikotevẽ hag̃ua michĩmi ${solValueNeeded} SOL. Ikatúpa embojoapyve SOL nde billetera-pe térã emomichĩve pe SOL remondoséva.", "insufficientFundsForRentError": "Ndaipóri ndéve SOL hembýva hag̃ua rehepyme’ẽ pe transacción ha pe renta hag̃ua pe cuenta. Ikatúpa remoĩve SOL nde billetera-pe térã remomichĩve pe SOL remondoséva.", + "insufficientFundsForRentErrorReceiver": "Pe ohupytýva cuenta ndoguerekói SOL he'íva ohepyme'ẽ hag̃ua pe renta. Ejerure pe ohupytývape ombojoapy hetave SOL icuenta-pe.", "introducing_cake_pay": "¡Roikuaauka Cake Pay!", "invalid_input": "Jeikeha ndoikóiva", "invalid_password": "Ñe'ẽñemi ndoikói", "invoice_details": "Factura rehegua marandu", "is_percentage": "ha'e", + "keys": "Llave-kuéra", "label": "Teramoĩ", "label_address": "Emboheramo dirección", "last_30_days": "Umi 30 ára ohasáva", @@ -449,6 +577,7 @@ "ledger_error_tx_rejected_by_user": "Pe transacción oñemboyke pe dispositívo-pe.", "ledger_error_wrong_app": "Ikatúpa ehecha porã reipeʼa hague pe app hekopete nde Ledger-pe.", "ledger_please_enable_bluetooth": "Emyandy Bluetooth ikatu hag̃ja ehecha nde Ledger", + "legacy": "Legacy", "light_theme": "Tesakã", "lightning_address_desc": "Pago pya'e, comisión michĩva.", "lightning_deposit_desc": "Edepositávo Lightning-pe, reintercambia hína nde Bitcoin on-chain ko billetera-gui nde cuenta Lightning-pe.", @@ -459,7 +588,24 @@ "lightning_username_setup_later": "Eheja hag̃ramo ko’ág̃a, ikatúta remohenda upéi hag̃ua Settings-pe.", "lightning_withdraw_desc": "Reipe'a jave Lightning-gui, remongu'e hína peteĩva térã opaite nde saldo Lightning-gua Bitcoin on-chain-pe.", "lightning_withdraw_disclaimer": "Pe saldo pyahu Bitcoin-pe ndojeguerekomo'ãi pe transacción oñemoañetepa peve.", + "litecoin_enable_mweb_sync": "Emboguata MWEB jeheka (scanning)", + "litecoin_mweb": "MWEB", + "litecoin_mweb_allow_coins": "Eheja MWEB moneda-kuéra", + "litecoin_mweb_always_scan": "Emoĩ MWEB ojeheka katuete hag̃ua", + "litecoin_mweb_description": "MWEB ha'e peteĩ protocolo pyahu ogerúva transacción ipya'eve, hepyve'ỹva ha tekoñemívéva Litecoin-pe", + "litecoin_mweb_dismiss": "Mboyke", "litecoin_mweb_display": "Ehechauka MWEB pantalla ñepyrũme", + "litecoin_mweb_display_card": "Ehechauka MWEB tarjeta", + "litecoin_mweb_enable": "Emboguata MWEB", + "litecoin_mweb_enable_later": "Ikatu eiporavo emboguata jey MWEB Display Settings guýpe.", + "litecoin_mweb_logs": "MWEB Logs", + "litecoin_mweb_node": "MWEB Nodo", + "litecoin_mweb_pegin": "Peg In", + "litecoin_mweb_pegout": "Peg Out", + "litecoin_mweb_scanning": "MWEB Jeheka", + "litecoin_mweb_settings": "MWEB ñemboheko", + "litecoin_mweb_warning": "MWEB jeporu ñepyrũrã omboguejýta ~600MB datos, ha ikatu ohupyty 30 aravo'i peve ñanduti pya'ekue heuti. Ko datos ñepyrũrã omboguejy peteĩ jeýnte ha oĩta opaite Litecoin billetera-pe g̃uarã", + "litecoin_what_is_mweb": "¿Mba'épa MWEB?", "live_fee_rates": "Tasa hepyme’ẽ rehegua hag̃uaicha hag̃ua API rupive", "load_more": "Hupi hetave", "loading": "Oñemyanyhẽhína", @@ -470,10 +616,12 @@ "long_press_show_balance": "Tarjeta de prensa puku ohechauka haguã equilibrio", "low_fee": "Jehepyme’ẽ michĩva", "low_fee_alert": "Koʼág̃a reiporu hína peteĩ prioridad jehepymeʼẽ ñanduti rehegua imbeguéva. Kóva ikatu omoheñói ñehaʼãro puku, tasa iñambuéva térã ñemurã ñembotove. Roñeʼẽporã emoĩ hag̃ua jehepymeʼẽ yvateve hag̃ua reiko porãve hag̃ua.", + "made_easy": "hasy'ỹre", "manage_nodes": "Eñangareko nodo-kuérare", "manage_pow_nodes": "Eñangareko nodo-kuéra PoW rehe", "manage_providers": "Eñangareko umi proveedor-kuéra rehe", "manage_yats": "Eñangareko Yats", + "manual_input": "Jeikeha pópe (manual)", "mark_as_redeemed": "Emoĩ hag̃ua ojehepyme’ẽmava", "market_place": "Ñemuhaguasu", "mask": "Mask", @@ -487,10 +635,13 @@ "memo_swap_disclaimer": "Ojeruréva receptor oacredita haguã nde fondo. Memo ofaltáva térã ojavýva he'ise fondo okañýva.", "message": "Marandu", "message_verified": "Pe marandu ojehechajey porã", + "messages": "Marandu-kuéra", + "method": "Mba'éichapa", "methods": "Mba’éichapa", "min_amount": "Michĩvéva: ${value}", "min_value": "Michĩ: ${value} ${currency}", "mining": "Minería", + "mint_address": "Mint Kundaharape", "minutes_to_pin_code": "${minute} aravo'i", "mm": "MM", "modify_2fa": "Emoambue Cake 2FA", @@ -499,12 +650,16 @@ "monero_dark_theme": "Tema iñypytũva Monero rehegua", "monero_light_theme": "Monero tema sa'y", "moonpay_alert_text": "Pe monto repy tekotevẽ tuichave térã ojoja ${minAmount} ${fiatCurrency}-re.", + "moralis_nft_error": "Oiko peteĩ jejavy oñembyaty aja umi NFT. Ehecha porãke ne ñanduti joaju ha eha'ã jey.", "more_details": "Detallesve", "more_options": "Hetave jeporavorã", + "multiple_addresses_detected": "Heta kundaharape ojejuhu", "mweb_ad": "Emboguata MWEB, pe capa ñemigua Litecoin-pe g̃uarã omoĩva anonimato extra nde viru'ikuérape", + "mweb_confirmed": "MWEB Oñemoañetéva", "mweb_help_desc_1": "MWEB ha’e peteĩ capa privacidad opcional Litecoin-pe g̃uarã, ohejáva puruhárape omokañy umi transacción monto ha omoporãve iprivacidad.", "mweb_help_desc_2": "Umi Litecoin oĩva cadena principal-pe opyta transparente, ha umi moneda oñemomýiva capa MWEB-pe ogana anonimato extra.", "mweb_help_disclaimer": "Ikatu remongu’e pya’e umi moneda mokõi capa apytépe emaskarávo ha demaskarávo rupive.", + "mweb_unconfirmed": "MWEB Noñemoañetéiva", "name": "Téra", "nano_current_rep": "Omyakãva ko'ág̃a", "nano_gpt_thanks_message": "Aguyje reiporu hag̃ua NanoGPT! Nemandu'áke reho jey kundahárape gotyo oĩmba rire ne transacción.", @@ -512,19 +667,24 @@ "nanogpt_subtitle": "Opaite umi modelo ipyahuvéva (GPT-5.3, Claude 4.6).\nNdaipóri suscripción, ehepyme’ẽ cripto rupive.", "narrow": "Poʼi", "network": "Red", + "networks": "Red-kuéra", "new_first_wallet_text": "Eñangareko hag̃ua nde criptomoneda rehe ha'e peteĩ mba'e ndahasýiva", "new_node_testing": "Ñeha’ã nodo pyahu", "new_subaddress_create": "Moheñói", "new_subaddress_label_name": "Téra etiqueta", "new_subaddress_title": "Kundaharape pyahu", + "new_tag": "Pyahu", "new_template": "Plantilla Pyahu", + "new_transactions_notifications": "Emondo marandu'i umi transacción pyahúre", "new_wallet": "Billetera pyahu", "newConnection": "Jeike pyahu", "nfts": "NFTs", "no": "Nahániri", "no_cards_found": "Ndojejuhúi kuatia'atã", + "no_extra_detail": "Ndaipóri mba'ekuaarã adicional", "no_id_needed": "Ndoikotevẽi ID!", "no_id_required": "Ndoikotevẽi ID. Embohetave ha eipuru reñemuhápe mamove", + "no_providers_available": "Ndaipóri me'ẽha oĩva", "no_relay_on_domain": "Ndaipóri peteĩ relé poruhára dominio hag̃ua, térã pe relé ndojepurukuaái. Ikatúpiko eiporavo peteĩ relé reipuru hag̃ua.", "no_relays": "Ndaipóri relays", "no_relays_message": "Rojuhu peteĩ registro Nostr NIP-05 ko usuario-pe g̃uarã, ha ndoguerekói mba’eveichagua relays. Ikatúpiko ere pe ohupytysévape ombojoapy hag̃ua relays iregistro Nostr-pe.", @@ -536,8 +696,10 @@ "node_new": "Nodo Pyahu", "node_port": "Nodo hag̃ua", "node_reset_settings_title": "Emoñepyrũjey ñemohenda", + "node_share_desc": "Emoñe'ẽ ko QR código pe Scan kuatiarogue-gui egueru hag̃ua ko nodo.", "node_test": "Ñeha'ã", "nodes": "Nodo-kuéra", + "nodes_desc": "Emohenda umi nodo oĩva térã embojoapy peteĩ ne mba'éva.", "nodes_list_reset_to_default_message": "¿Reime añetete remoĩjeyseha umi ñemboheko ijypykuévaramo?", "none_of_selected_providers_can_exchange": "Ndaipóri umi proveedor ojeiporavóva apytépe ikatúva ojapo ko swap.", "noNFTYet": "Ndaipóri NFT gueteri", @@ -545,15 +707,19 @@ "note": "Haipy", "note_optional": "Jehaipy (opcional)", "note_tap_to_change": "Jehaipy (eikutu emoambue hag̃ua)", + "notification_permission_denied": "Pe marandu'i ñemoneĩ oñemboty opaite g̃uarãma; emboguata po rupi ñembohekópe", "nullURIError": "URI nulo", + "ocp_desc": "Ehepyme'ẽ cripto-pe ñemuhakuérape", "offer_expires_in": "Ñemurã opáta hag̃ua:", "offline": "ndaipóri", "ok": "OK", "old_fee": "Tembiapo repy ymaguare", + "oled_mode": "OLED modo", "onion_link": "Onion link", "online": "En línea", "onramper_option_description": "Ejogua pyaʼe cripto heta hag̃ua rehepymeʼẽ hag̃ua. Ojeguereko hetave tetãme. Umi joavy ha mbaʼerepy iñambue.", "open_gift_card": "Eipeʼa kuatia'atã meʼẽrã", + "open_wallet": "Eipe'a Billetera", "optional_description": "Ñemombe’u opcional", "optional_email_hint": "Ñandutípe ñemomarandu optional pe ohupytýtavape", "optional_name": "Téra opcional pe ohupytýtavape g̃uarã", @@ -568,14 +734,34 @@ "outdated_electrum_wallet_receive_warning": "Ko billetera oguerekóramo peteĩ seed 12 ñe’ẽgui ha ojejapo Cake-pe, ANI REMOINGE Bitcoin ko billeterápe. Oimeraẽ BTC oñembohasáva ko billeterápe ikatu okañy. Emoheñói peteĩ billetera pyahu 24 ñe’ẽgui (epoko pe menú yvate akatúa gotyo, eiporavo Wallets, eiporavo Create New Wallet, ha upéi eiporavo Bitcoin) ha pya’eterei emongu’e nde BTC upépe. Umi billetera pyahu BTC (24 ñe’ẽ) ojapóva Cake-gui ha’e hekorosã", "outgoing": "Oñemondo", "outputs": "Osẽha", + "overshot": "Ohasa hetave (overshot)", "overwrite_amount": "Embyekovia hetakue", + "pairing_code": "Pairing Código", + "pairing_code_desc_1": "Ne mba'e'oka ohechaukáta peteĩ pairing código.", + "pairing_code_desc_2": "Emoinge ko'ápe resegí hag̃ua", + "pairing_error": "Pairing Jejavy", "pairingInvalidEvent": "Mba'e oiko ndoikóiva pe joaju hag̃ua", "passphrase": "Ñe’ẽñemi (Opcional)", + "passphrase_view_keys": "Passphrase", + "passphrases_doesnt_match": "Umi passphrase ndojojái, eha'ã jey", "password": "Ñe’ẽñemi", "paste": "Mboja", "pause_wallet_creation": "Ko'ág̃a ojejoko sapy'aitemi pe ikatu hag̃a ojejapo Haven Wallet.", + "payjoin_card_content": "Payjoin ha'e peteĩ tembiapoite pyahu oñangarekóva tekoñemíre ha oñongatúva hepyme'ẽ Bitcoin-pe.", + "payjoin_details": "Payjoin mba'ekuaarã", + "payjoin_enabled": "Payjoin oñemboguatáva", + "payjoin_enabling_popup_content": "Ikatu emyendy/embogue Payjoin Menú → Privacy → Use Payjoin guýpe upéi.", + "payjoin_enabling_popup_title": "Payjoin Ñemboguata", + "payjoin_request_awaiting_tx": "Oha'arõ Transacción", + "payjoin_request_in_progress": "Oikohína", + "payjoin_unavailable": "Payjoin ndojepurukuaái", + "payjoin_unavailable_sheet_content": "Peteĩ Payjoin transacción ohupyty hag̃ua tekotevẽ ereko Bitcoin oĩmbáva reipuru hag̃ua.\n\nNe billetera oguerekóvo viru, Payjoin oñemboguatáta ijeheguíva.", + "payjoin_unavailable_sheet_title": "¿Mba'érepa Payjoin ndojepurukuaái?", "payment_id": "ID jehepyme’ẽ rehegua: ", + "payment_invoices": "Jehepyme'ẽ Kuatiañemuhã-kuéra", + "payment_made_easy": "Jehepyme'ẽ hasy'ỹre", "payment_was_received": "Nde pago oñemog̃uahẽma.", + "payments": "Jehepyme'ẽ-kuéra", "pending": " (oĩhína oñehaʼãrõme)", "percentageOf": "rehe ${amount}", "personalizado": "Ojeiporavokuaava", @@ -600,6 +786,7 @@ "pin_number": "PIN papapy", "placeholder_contacts": "Nde contacto kuéra ojehechaukáta koʼápe.", "placeholder_transactions": "Nde transacción-kuéra ojehechaukáta koʼápe.", + "please_choose_one": "Eiporavo peteĩ", "please_fill_totp": "Emoinge pe kódigo 8 dígito oĩva nde ambue tembipurupe.", "please_make_selection": "Eiporavo koʼágui hag̃ua emoheñói térã erujey nde billetera.", "please_reference_document": "Ehecha umi kuatia oĩva iguýpe reikuaave hag̃ua.", @@ -608,6 +795,7 @@ "please_try_to_connect_to_another_node": "Ikatúpa eñeha'ã eñembojoaju ambue nodo rehe", "please_wait": "Eha'ãmi", "polygonscan_history": "PolygonScan rembiasakue", + "potential_scam": "Ñembotavyrã (Potential Scam)", "powered_by": "Oñemombarete ${title} rupive", "pre_seed_button_text": "Aikuaa. Ehechauka chéve che ñe’ẽñemi", "pre_seed_description": "Pe kuatiarogue oúvape rehecháta peteĩ aty ñeʼẽ. Péva haʼe nde seed ijojahaʼỹva ha ñemigua, ha péva añoite hína tape ikatu hag̃a rehejuhu jey nde wallet reñehundi térã ndoikói ramo. Nde rembiapo hína rehai hag̃ua ha reñongatu hag̃ua peteĩ tenda hekorosãvape, okápe Cake Wallet app-gui.", @@ -615,6 +803,7 @@ "preference": "Jerohoryvéva", "prepaid_cards": "Kárta jehepyme’ẽva", "prevent_screenshots": "Ani ejapo hag̃ua ta'angajapyhy térã pantalla grabación", + "primary_address": "Kundaharape Mba'eguasu (primary)", "privacy": "Ñemigua", "privacy_and_security": "Privasida ha Tekorosã", "privacy_and_security_desc": "Emohenda jehechajey ha tekorosã ñemboheko ha emboheko Cake 2FA.", @@ -627,6 +816,8 @@ "proceed_after_one_minute": "Pe pantalla ndohoiramo tenonde 1 aravo’i rire, ehecha nde correo.", "proceed_on_device": "Eku’ejey nde mba’e’okápe.", "proceed_on_device_description": "Eikuaa hag̃ua, ejapo umi mba’e he’íva nde hardware wallet-pe", + "processing": "Oñeprocesahína", + "processing_signed_tx": "Oñeprocesahína transacción oñemboheraguapýva", "profile": "Perfil", "provider": "Proveedor", "provider_error": "Jejavy ${provider}", @@ -634,7 +825,11 @@ "public_key": "Clave pública", "purchase_gift_card": "Ehepyme Tarjeta Regalo", "purple_dark_theme": "Téma pytãũ ypytũva", + "qr_code_format": "QR Code Formáto", + "qr_code_format_body": "BCUR (Blockchain Commons Uniform Resources) ha'e pe estándar industria-pegua, ha ojokupyty hetave Bitcoin hardware wallet-kuéra ndive, ha ojeporavova'erã hetavéva puruhárape g̃uarã. BBQR (Better Bitcoin QR) ha'e peteĩ formáto ojepurúva COLDCARD hardware wallet-kuérape añoite, imodelo Q guive.", + "qr_code_format_note": "Hardware wallet-kuéra iñambuéva oipuru QR código formáto iñambuéva Bitcoin ñe'ãme", "qr_fullscreen": "Eikutu hag̃ua eipe'a hag̃ua QR ayvu tuichakuépe", + "qr_instruction": "Eipuru ko kundaharape ehupyty hag̃ua oimeraẽ token térã coleccionable ko'ápe:", "qr_payment_amount": "Ko QR oguereko peteĩ monto jehepyme’ẽ rehegua. Reipotápa rehai jey pe valor ko’ag̃agua ári?", "quantity": "Hetakue", "question_to_disable_2fa": "¿Añetehápepa remboguese Cake 2FA? Upeicháramo, ndaikotevẽvéima peteĩ kódigo 2FA reike hag̃ua pe wallet-pe ha umi tembiapo hag̃uaicha.", @@ -648,6 +843,7 @@ "recipient_address": "Ojapyhýva kundaharape", "reconnect": "Eñembojoaju jey", "reconnect_alert_text": "¿Reime añetehápe reñembojoaju jeyse?", + "reconnect_your_hardware_wallet": "Embojoaju jey ne Hardware Wallet", "reconnection": "Jeike jey", "red_dark_theme": "Tema pytã pytũ", "red_light_theme": "Tema pytã sa'yju", @@ -663,6 +859,8 @@ "rep_warning_sub": "Nde moha'angáva ndojekuaái oĩha hag̃ua iporã hag̃aicha. Epoko ko'ápe reiporavo hag̃ua peteĩ pyahu", "repeat_wallet_password": "Ehai jey billetera ñe’ẽñemi", "repeated_password_is_incorrect": "Pe ñe’ẽñemi ojehai jeyva ndoikói. Ikatúpiko ehaijey pe billetera ñe’ẽñemi.", + "replace": "Emyengovia", + "requested": "Ojejeruréva", "require_for_adding_contacts": "Oñeikotevẽva oñemoinge hag̃ua contacto-kuéra", "require_for_all_security_and_backup_settings": "Oñeikotevẽ opaite ñemboheko tekorosã ha backup hag̃ua.", "require_for_assessing_wallet": "Ejerure hag̃ua eike hag̃ua wallet-pe", @@ -673,20 +871,24 @@ "require_for_sends_to_internal_wallets": "Oñeikotevẽ hag̃ua oñembohasa billetera ryepýpe", "require_for_sends_to_non_contacts": "Oñeikotevẽ hag̃ua oñembohasa umi ndaha'éiva ne contacto-pe", "require_pin_after": "Oñeikotevẽ PIN upéi", + "required_passphrase": "Passphrase", "rescan": "Eescanea jey", "resend_code": "Emondo jey, ikatúpa emondo jey.", "reset": "Moĩjey", "reset_password": "Emoĩ jey ñe’ẽñemi", + "restore": "Emoĩporã jey", "restore_active_seed": "Seed aktívo", "restore_address": "Dirección", "restore_bitcoin_description_from_keys": "Emoheñói jey nde billetera peteĩ WIF joajuhágui, heñóiva nde clave privada-gui.", "restore_bitcoin_description_from_seed": "Emoheñói jey nde billetera pe kódigo ñembojuehegua 24 ñe’ẽgui.", "restore_bitcoin_title_from_keys": "Emboguejy WIF-gui", "restore_description_from_backup": "Emoĩporã jey Cake Wallet hína chagüe oĩva'ekue rami, emoĩporã jey ñongatupyre archivo-gui.", + "restore_description_from_cupcake": "Embojoaju ore irũ app air-gapped ndive, oñemohendáva ambue teléfono-pe.", "restore_description_from_hardware_wallet": "Embojuaju ne hardware wallet mba’e’oka heta hag̃ua fabricante-gui.", "restore_description_from_keys": "Emoĩporã jey nde billetera umi teclakuéra heñóiva rehegua, oñeñongatúva nde clave privada-gui.", "restore_description_from_seed": "Emoheñói jey nde billetera pe kódigo ñembojoaju 25 térã 13 ñe’ẽgui.", "restore_description_from_seed_keys": "Emoĩporã jey ne billetera ñe'ẽñemi seed-gui térã mba'ekuaarepy oñongatúvagui.", + "restore_existing_wallet": "Emoĩporã jey Billetera Oĩmava", "restore_from_date_or_blockheight": "Ikatúpiko emoinge peteĩ arange ko billetera emoheñói mboyve heta’ẽmi ára. Térã reikuaáramo pe blockheight, emoinge upéva hendaguépe.", "restore_from_seed_placeholder": "Emoinge térã emboja nde seed ko’ápe.", "restore_new_seed": "Seed pyahu", @@ -696,6 +898,7 @@ "restore_seed_keys_restore": "Ñemoĩporã jey Seed/Keys", "restore_spend_key_private": "Spend key (ñemigua)", "restore_title_from_backup": "Archivo de backup", + "restore_title_from_cupcake": "Cupcake App", "restore_title_from_hardware_wallet": "Hardware wallet ", "restore_title_from_keys": "Emboguejy hag̃ua clave-kuéra guive", "restore_title_from_seed": "Emboguyjey ñe’ẽñemigua guive", @@ -712,6 +915,7 @@ "save": "Ñongatu", "save_backup_password": "Ehechajey reñongatúma hague nde backup ñe’ẽñemi. Ndaikatumo’ãi remoinge jey nde backup marandurenda’ỹre upeva.", "save_backup_password_alert": "Eñongatu ñe’ẽñemi backup hag̃ua", + "save_contact": "Eñongatu Contacto", "save_to_downloads": "Ñongatu Ñemboguejy hag̃ua", "saved_the_trade_id": "Añongatu trade ID", "savings": "Ahorro", @@ -728,6 +932,7 @@ "search_or_enter": "Eheka térã emoinge", "second_intro_content": "Nde Yat ha'e peteĩ dirección emoji ijojaha'ỹva ha peteĩnte, omyengoviáva opaite nde dirección hexadecimal ipukúva opaite nde moneda hag̃ua.", "second_intro_title": "Peteĩ dirección emoji hag̃ua omanda hag̃ua opavavépe", + "security": "Tekorosã", "security_and_backup": "Tekorosã ha backup", "seed_alert_back": "Ejevy", "seed_alert_content": "Pe seed ha’eñoite pe tape ikatu hag̃ hag̃ rehupyty jey nde billetera. ¿Rehaíma hag̃ua?", @@ -735,6 +940,7 @@ "seed_alert_yes": "Hêe, aguereko", "seed_and_keys": "Seed ha Llave kuéra", "seed_choose": "Eiporavo seed ñe'ẽ", + "seed_display_path": "Menú → Security and Backup → Show key/seeds", "seed_hex_form": "Billetera seed (hex form)", "seed_key": "Seed key", "seed_language": "Seed ñe'ẽ", @@ -753,12 +959,19 @@ "seed_language_russian": "Ruso", "seed_language_spanish": "Español", "seed_phrase_length": "Seed pukukue", + "seed_position_question_one": "¿Mba'épa pe", + "seed_position_question_two": "ñe'ẽ ne seed-gui?", "seed_reminder": "Ehai koʼãva ikatu hag̃ua ojehu ramo ndehegui nde teléfono okañy térã oñembogue.", "seed_share": "Emoherakuã seed", "seed_title": "Seed", + "seed_verified": "Seed Ojehechajeýva", + "seed_verified_subtext": "Ikatu reipuru ne seed oñeñongatúva upéi emoĩporã jey hag̃ua ko billetera oñembyai térã ne mba'e'oka okañy ramo.\n\nIkatu rehecha jey ko seed ko'águi:", "seedtype": "Mba'eichagua seed", + "seedtype_alert_content": "Seed ñembohasa ambue billetera ndive ikatúnte BIP39 SeedType reheve.", + "seedtype_alert_title": "SeedType Marandu", "seedtype_legacy": "Temimondo (25 ñe’ẽ)", "seedtype_polyseed": "Polieta (16 ñe’ẽ)", + "select_a_wallet": "Eiporavo peteĩ billetera", "select_address_to_copy": "Eiporavo dirección emokopia hag̃ua", "select_all": "Eiporavo opaite", "select_asset": "Eiporavo Activo", @@ -766,17 +979,24 @@ "select_buy_provider_notice": "Eiporavo peteĩ jogua-me’ẽha yvate. Ikatu rehasapa ko pantalla emoĩvo nde jogua-me’ẽha ijypykuéramo ñembohekorã aplicación-pe.", "select_destination": "Eiporavo moõpa oñeñongatúta marandurenda backup.", "select_fiat_currency_title": "Eiporavo Moneda", + "select_hw_account_below": "Eiporavo mávapa cuenta emoĩporã jeýta iguýpe:", + "select_manufacturer_title": "Eiporavo Fabricante", + "select_network": "Eiporavo Red", "select_network_subtitle": "Ko activo oime presente heta red-pe. Eiporavo pe reiporuséva", "select_network_title": "Eiporavo Red", "select_other_wallet": "Eiporavo ambue billetera", "select_receiver": "Eiporavo Mbyhyha", "select_sell_provider_notice": "Eiporavo yvate peteĩ ñemuha. Ikatu rehasa hag̃ua ko pantalla emoĩvo nde ñemuha ijypykuéramo umi ñemboheko hag̃ua aplicación-pe.", + "select_token": "Eiporavo Token", + "select_your_country": "Eiporavo ne tetã", + "selected_trocador_provider": "Trocador me'ẽha ojeporavóva", "sell": "Vende", "sell_alert_content": "Ko’ág̃a rupi roipytyvõ venta Bitcoin, Ethereum ha Litecoin-pe añoite. Ikatúpiko emoheñói térã emoambue nde billetera Bitcoin, Ethereum térã Litecoin-pe.", "sell_monero_com_alert_content": "Ndaikatúi gueteri reñemu Monero.", "send": "Emondo", "send_address": "Dirección ${cryptoCurrency} rehegua", "send_amount": "Hetakue:", + "send_change_to_you": "Ñemoambue, ndéve:", "send_creating_transaction": "Ojejapo hag̃ua ñembohasa", "send_error_currency": "Pe moneda ikatu oguereko papapy añoite.", "send_error_minimum_value": "Pe tepykue michĩvéva hag̃ua pe monto ha’e 0,01", @@ -785,9 +1005,12 @@ "send_external_desc": "Ani remboty ko página rehegua pe emondo hag̃ua umi fondo, ndaha'éiramo upéicha pe depósito ndohóimo'ãi hag̃ua.", "send_fee": "Hepyme’ẽ:", "send_from": "Emondo guive", + "send_from_cake_wallet": "Emondo Cake Wallet guive", "send_from_external": "Emondo Okápe", + "send_from_external_wallet": "Emondo Billetera Okapegua guive", "send_name": "Téra", "send_new": "Pyahu", + "send_payjoin": "Emondo Payjoin", "send_payment_id": "Payment ID (opcional)", "send_priority": "Ko’ánga, pe comisión oñemohenda ${transactionPriority} prioridad-pe.\nPe transacción prioridad ikatu oñemboheko hag̃ua umi ñemboheko hag̃ua.", "send_sending": "Oñemondo...", @@ -803,11 +1026,13 @@ "sent_the_funds": "Amondo umi fondo.", "service_health_disabled": "Pe Marandu Tesãi Mba’apo rehegua oĩ ñemboguepyre.", "service_health_disabled_message": "Kóva ha’e kuatiarogue marandu’i Tesãi Mba’apohágui; ikatu remyendy ko kuatiarogue Ñemboheko → Ñemigua-pe.", + "set_a_pin": "Emoĩ peteĩ PIN", "set_amount": "Emoĩ hetakue", "set_fees": "Emohenda umi tarifa", "set_fees_desc": "Odepende pe monto de comisión rehe, pe depósito ohasáta pyaʼeve.", "set_label": "Establecer etiqueta", "set_refund_address": "Emoĩ Dirección de reembolso", + "set_up_a_wallet": "Embosako'i peteĩ Billetera", "settings": "Ñemboheko", "settings_all": "OPA", "settings_allow_biometrical_authentication": "Eheja jehechakuaa biométrika rupive", @@ -842,15 +1067,24 @@ "setup_your_debit_card": "Emoĩporã nde tarjeta débito", "share": "Mbohasa", "share_address": "Emoherakuã dirección", + "share_this_node": "Emoherakuã ko nodo", + "shared_seed_wallet_groups": "Billetera Atyrã Seed Ojoja'ekuéva", "shared_seed_wallets": "Billetera kuéra seed compartida rehegua", + "show": "Hechauka", + "show_address_book_popup": "Ehechauka Address Book popup", + "show_balance": "Eipoko Puku Ehechauka hag̃ua Saldo", "show_balance_send_page": "Eikutu ehechauka hag̃ua", + "show_balance_toast": "Eipoko puku emokañy térã ehechauka hag̃ua saldo", + "show_combined_balance": "Ehechauka saldo ojembyatypyréva", "show_details": "Ehechauka mba’emimi", "show_hidden_addresses": "Ehechauka umi dirección kañymby.", "show_keys": "Hechauka seed/keys", "show_market_place": "Hechauka Ñemuha Renda", "show_seed": "Hechauka seed", "show_zcash_card": "Ohechauka zcash tarjeta umi viru oĩ'ỹva rehegua.", + "sign_all": "Emboheraguapy Opaite", "sign_message": "Emoheraguapy marandu", + "sign_one": "Emboheraguapy Peteĩ", "sign_up": "Eñemboheraguapy", "sign_verify_message": "Emboheraguapy / Ehechajey", "sign_verify_message_sub": "Emboheraguapy térã ehechajey peteĩ marandu reipuruvo nde clave privada", @@ -861,28 +1095,38 @@ "signTransaction": "Emoheraguapy Transacción", "signup_for_card_accept_terms": "Eñemboheraguapy hag̃ua pe tarjeta ha emoneĩ umi término.", "silent_address_desc": "Dirección estática omomba’eporãva tekoñemi hag̃ua", + "silent_payment": "Silent Payment", "silent_payments": "Jehepyme'ẽ Kirirĩ", "silent_payments_always_scan": "Emoĩ Silent Payments jehechaha (scan) katuete hag̃ua.", "silent_payments_disclaimer": "Umi dirección pyahu ndaha’éi identidad pyahu. Ha’e peteĩ identidad oĩmavavoi ojepurujeyva peteĩ teratee ambuéva ndive.", "silent_payments_display_card": "Ehechauka Silent Payments tarjeta.", + "silent_payments_logs": "Silent Payment Logs", "silent_payments_scan_from_date": "Eñepyrũ je-escaneo arange guive", "silent_payments_scan_from_date_or_blockheight": "Emoinge pe bloque yvate térã pe arange reipuru hag̃uáicha reñepyrũ hag̃ua re-escanea hag̃ua umi Silent Payments oikéva. Ikatu reiporavo pe billetera ocontinua hag̃ua oescanea bloque peteĩteĩva, térã ohecha hag̃ua pe yvate añoite oñemohendáva.", "silent_payments_scan_from_height": "Ehecha hag̃ua yvatekuéra bloque guive", "silent_payments_scanned_tip": "Ojeescanea hag̃ua pe ipahaite peve! (${tip})", "silent_payments_scanning": "Jeheka Silent Payments rehegua", "silent_payments_settings": "Ñemboheko Silent Payments rehegua", + "single_seed_wallets_group": "Billetera Seed Peteĩva", "skip": "Eheja", "skip_set_later": "Eheja hag̃ua térã emohenda ag̃ave", "slidable": "Oñemongu'e hag̃ hag̃ hag̃ua", + "solana_create_associated_token_account_exception": "Jejavy oñemoheñói aja pe associated token account pe ohupytýva kundaharapépe.", "solana_invalid_data_message": "Jejavy eñemoñe'ẽ hag̃ua pe cuenta token asociada hag̃ua pe token-pe. Ikatúpiko ehechajey pe dirección de recepción ha eñeha'ã jey.", + "solana_no_associated_token_account_exception": "Ndaipóri associated token account ko kundaharapépe.", + "solana_sign_native_transaction_rent_exception": "Ndaikatúi oñemohu'ã pe transacción. Ndaipóri SOL hembýva pe renta-rã pe transacción rire. Embohetave ne SOL saldo térã emomichĩ pe SOL remondóva.", + "solana_sign_spl_token_transaction_rent_exception": "Ndaikatúi oñemohu'ã pe transacción. Ndaipóri SOL hembýva pe renta-rã pe transacción rire. Embohetave ne SOL saldo.", "sort_by": "Mohenda hag̃ua", "spend_key_private": "Clave de gasto (privada)", "spend_key_public": "Mba’eñemi hag̃ua (público)", + "starting_tor_proxy": "Oñepyrũhína Tor proxy", "status": "Teko: ", + "step": "Pa'ũ (step)", "string_default": "Ijypykue", "subaddress_title": "Subdirección rysýi", "subaddresses": "Subdirección-kuéra", "submit_request": "Emondo peteĩ jerure", + "success": "Oiko porã", "successful": "Oiko porã", "support_description_guides": "Kuatiahaipyre ha pytyvõ mba’e apañuãi hag̃ua hag̃ua jepigua rehegua", "support_description_live_chat": "¡Rehepyme’ẽ’ỹ ha pya’e! Umi pytyvõhára katupyry oĩ hag̃uaicha ndepytyvõ hag̃ua", @@ -890,12 +1134,16 @@ "support_title_guides": "Kuatiaite Cake Wallet rehegua", "support_title_live_chat": "Pytyvõ hag̃ua hag̃uaicha", "support_title_other_links": "Ambue juaju pytyvõ rehegua", + "supported": "Ojepytyvõva", + "swap": "Swap", "swap_id": "ID ñemoambue rehegua", "swap_provider_initial_desc": "Eiporavo mba’eichagua swap puruhára reipuruséva rembojopyru hag̃ua swap jave.", "swap_providers": "Proveedor-kuéra intercambio rehegua", "sweeping_wallet": "Billetera ñemopotĩ", "sweeping_wallet_alert": "Kóva ndoguerahámo’ãi heta aravo. ANI REHEJA KO PANTALLA, térã umi fondo oñemopotĩva ikatu okañy.", + "swipe_to_bridge": "Embosyryry embridge hag̃ua", "swipe_to_send": "Emoinge hag̃ua emondo hag̃ua.", + "switch_wallet": "Emoambue Billetera", "switchToETHWallet": "Ikatúpa emoambue peteĩ billetera Ethereum-pe ha eha’ãjey.", "switchToWCCompatibleWallet": "Embohasa peteĩ billetera WalletConnect ndive ha eñeha’ã jey (${chains}).", "symbol": "Ta'ãnga’i", @@ -907,9 +1155,13 @@ "sync_not_connected": "Ndojeikói conectádo", "sync_offline": "Offline", "sync_starting_scan": "Oñepyrũvo escaneo", + "sync_status_attempting_scan": "OÑEHA'Ã ESCANEO", "sync_status_attempting_sync": "Oñeha’ã ojesinkronisa", "sync_status_connected": "OÑEMBOJOAJU", "sync_status_connecting": "OÑEMBOJOAJUHÍNA", + "sync_status_display_mode": "Ñembojuehe Tekotee Jehechauka", + "sync_status_display_mode_blocks": "Ehechauka Bloque Hembýva", + "sync_status_display_mode_eta": "Ehechauka ETA", "sync_status_failed_connect": "OJEIÑE'Ĩ", "sync_status_not_connected": "NDOJOAJÚI", "sync_status_starting_scan": "Oñepyrũhína escaneo (guive ${height})", @@ -921,6 +1173,7 @@ "sync_syncing": "Oñembojuehe", "sync_timed_out": "Sincronización ohasáma aravo", "sync_unsupported_node": "Nodo ndojehechaukáiva", + "synchronizing": "Oñembojuehehína", "syncing_wallet_alert_content": "Ne saldo ha ne transacción rysýi ikatu ndoĩporãmbái he’í peve “SYNCHRONIZED” yvate gotyo. Ehecha/ejopy térã epoko reikuaave hag̃ua.", "syncing_wallet_alert_title": "Nde billetera oñembojuehehína", "tap_to_copy": "Eikutu hag̃ua ekopia", @@ -929,20 +1182,28 @@ "testnet_coins_no_value": "Umi testnet moneda ndorekói mba’e repy.", "third_intro_content": "Umi Yat oiko avei okápe Cake Wallet-gui. Oimeraẽva billetera kundaharape ko yvy ape ári ikatu oñemyengovia peteĩ Yat rehe!", "third_intro_title": "Yat ojapo porã ambuekuéra ndive.", + "this_can_take_few_seconds": "Kóva ikatu ohupyty mbovymi segundo", + "this_pair_is_not_supported_warning": "Ko par ndojokupytýi umi intercambio ojeporavóva ko'ag̃agua ndive. Eiporavo ambue intercambio.", "thorchain_contract_address_not_supported": "THORChain ndohejáiva oñemondo hag̃ua peteĩ contrato dirección-pe.", "thorchain_taproot_address_not_supported": "Pe ThorChain me’ẽha ndoipytyvõi Taproot kundaharape. Ikatúpa emoambue pe kundaharape térã eiporavo ambue me’ẽha.", "time": "${minutes}m ${seconds}s", "tip": "Ñemoñe’ẽ:", + "to": "Peve (To)", "to_lightning": "a Lightning", "to_on_chain": "ha on-chain", "today": "Kóina", + "token_already_exists": "Pe token oĩma", "token_contract_address": "Pe dirección contrato token rehegua", "token_decimal": "Token decimal (techapyrã: USDT)", "token_name": "Token réra (techapyrã: Tether)", "token_symbol": "Token ra'ãnga'i (techapyrã: USDT)", + "token_verified_on_jupiter": "Ko token ojeverifika Jupiter rehe", "tokenID": "ID", "tokens": "Tokens", + "tokens_strong_warning": "¡Anive remondo token ko kundaharapépe!", "tor_connection": "Tor jeikeha", + "tor_connection_timeout": "Tor jeikeha ohasáma aravo", + "tor_experimental": "Kóva ha'e peteĩ tembiapoite experimental, ndaha'éi opaite joaju Cake Wallet guive oipytyvõ Tor ruteo — oĩ joaju oipurúva clearnet; ruteo hag̃ua opaite app tráfico eipuru Tor VPN modo ndive.", "tor_only": "Tor añoite hag̃", "total": "Total", "total_saving": "Ñongatu total", @@ -985,6 +1246,7 @@ "trades": "Ñemuha", "transaction": "Transacción rehegua", "transaction_commited": "Transacción ojejapóma.", + "transaction_cost": "Transacción Repykue", "transaction_details_amount": "Hetakue", "transaction_details_copied": "${title} ojekopia portapapélpe", "transaction_details_date": "Arange", @@ -1009,13 +1271,16 @@ "transactions_by_date": "Ñembohasa kuéra arange rupi", "transactions_will_appear_here": "Ko’ápe ojehechaukáta ne transacción-kuéra.", "transparent_secret_key": "Clave Secreta Transparente", + "transport_type": "Transporte Mba'eichagua", "trocador_desc": "ha'e peteĩ agregador de intercambio oiporúva heta intercambio subyacente ojuhú hag̃ua tasa competitiva.", "trocador_desc_kyc": "Pe proveedor réra ykére, ojehechauka peteĩ calificación KYC rehegua. Péva omoĩ Trocador ha oñemopyenda ijehegui hag̃uaichaite hag̃ua.", "trongrid_history": "TronGrid rembiasakue", "trusted": "Jeroviaha", + "try_again": "Eha'ã jey", "trying_to_spend_locked_funds": "Transacción oñembotove reñeha’ãgui reipuru umi fondo orekóva bloqueo de tiempo relativo; eha’arõ ha eñeha’ã jey ag̃ave, térã embogue umi fondo bloqueado coin control-pe", "tx_commit_exception_no_dust_on_change": "Oñemboyke pe transacción ko monto reheve. Ko’ã moneda rupive ikatu remondo ${min} ndorekóiva ñemoambue térã ${max} ome’ẽva ñemoambue.", "tx_commit_failed": "Ndoikói transacción ñemoneĩ. Eñe’ẽ soporte ndive.", + "tx_commit_failed_no_peers": "Pe transacción ndaikatúi oñemyasãi, eha'ã jey peteĩ segundo-pe", "tx_invalid_input": "Reipuru hag̃ua peteĩchagua jeike hekope’ỹva koichagua jehepyme’ẽ hag̃ua.", "tx_no_dust_exception": "Ko transacción oñemboyke oñemondóramo peteĩ monto michĩeterei. Ikatúpiko eha’ã embohetave pe monto.", "tx_not_enough_inputs_exception": "Ndaipóri jeikeha he'íva. Ikatúpa eiporavo hetave Coin Control-pe.", @@ -1038,6 +1303,8 @@ "unlock": "Eipe’a", "unmask": "Mokañy'ỹ", "unmatched_currencies": "Pe moneda ndebilletera ko’agagua ndojoajúi pe QR escaneado ndive", + "unrestricted_background_service": "Servicio tapykuegua ñemboaty'ỹ (unrestricted)", + "unrestricted_background_service_notice": "Emboguata hag̃ua ñembojuehe tapykuegua tekotevẽ emboguata servicio tapykuegua ñemboaty'ỹ", "unselect_all": "Eipe'a opaite jeporavo", "unspent_change": "Ñemoambue", "unspent_coins_details_title": "Marandu viru ndojepurukuaáiva rehegua", @@ -1050,12 +1317,16 @@ "use_blink_protection": "Eipuru Blink Protection", "use_card_info_three": "Eipuru tarjeta dijitálva ñandutípe térã jehepyme’ẽ hag̃ua tapicha’ỹre (contactless).", "use_card_info_two": "Pe viru oñemoambue USD-pe oñeñongatúramo pe cuenta prepaga-pe, ndaha’éi moneda digital-pe.", + "use_device_theme": "Eipuru Mba'e'oka Téma", + "use_payjoin": "Eipuru Payjoin", "use_ssl": "Eipuru SSL", "use_suggested": "Eipuru je'epyréva", "use_testnet": "Eipuru Testnet", + "user_rejected_method": "Puruhára ombotove pe método", "username": "Puruhára réra", "username_available": "Tembipuru réra ojepuru hag̃ua", "username_character_error": "Umi puruhára réra oguerekova’erãnte", + "username_check_your_network": "Ndaikatúi ojehechajey pe puruhára réra. Ehecha ne ñanduti joaju.", "username_length_error": "Umi puruhára réra oguerekova’erã 6-32 tai ha papapy (alfanumérico).", "username_length_error_1_64": "Umi puruhára réra oguerekova’erã 1-64 tai alfanumérico.", "username_not_available": "Ko puruhára réra ndojepurukuaái.", @@ -1064,7 +1335,9 @@ "variable_pair_not_supported": "Ko par iñambuekuáva ndojokupytýi umi ñembohasa ojeporavóva ndive.", "verification": "Jehechajey", "verify_message": "Marandu jehechajey", + "verify_seed": "Ehechajey Seed", "verify_with_2fa": "Jehechajey Cake 2FA ndive", + "verifying_code": "Oñehechajeýhína Código", "version": "Versión ${currentVersion}", "view_all": "Hecha opaite", "view_in_block_explorer": "Ehecha Block Explorer-pe", @@ -1074,10 +1347,24 @@ "view_transaction_on": "Ehecha transacción rehegua hag̃ua", "voting_weight": "Pohýi votación rehegua", "waitFewSecondForTxUpdate": "Eha’ãro sapy’ami hag̃ua pe transacción ojehecha hag̃ua ñembohasa rembiasakue-pe.", + "wallet": "Billetera", "wallet_accounts": "Mba'ete Billetera-pe", + "wallet_balance": "Billetera Saldo", + "wallet_group": "Billetera Atyrã (Wallet Group)", + "wallet_group_description_existing_seed": "Reiporavo reipuru hag̃ua peteĩ seed oĩmava ko billetera-pe. Ikatu rehechajey jey pe seed reikotevẽrõ emoneĩ térã rehai.", + "wallet_group_description_four": "emoheñói hag̃ua peteĩ billetera seed ipyahúva reheve.", + "wallet_group_description_one": "Cake Wallet-pe, ikatu emoheñói peteĩ", + "wallet_group_description_open_wallet": "Ndaupéichairõ, ikatu eku'ejey eipe'a hag̃ua pe billetera", + "wallet_group_description_three": "ehecha hag̃ua umi billetera ha/térã billetera atyrã kuatiarogue. Térã eiporavo", + "wallet_group_description_two": "eiporavóvo peteĩ billetera oĩmava emoherakuã hag̃ua peteĩ seed hendive. Peteĩteĩ billetera atyrã ikatu oguereko peteĩ billetera moneda mba'eichagua-pe.\n\nIkatu eiporavo", + "wallet_group_description_view_seed": "Ikatu rehecha jey ko seed ko'águi", + "wallet_group_empty_state_text_one": "Ndereguerekóiramoguáicha mavave billetera atyrã ojokupytýva.\n\nEikutu", + "wallet_group_empty_state_text_two": "iguýpe emoheñói hag̃ua peteĩ pyahu.", + "wallet_has_passphrase": "Ko billetera oguereko peteĩ passphrase", "wallet_is_syncing": "Pe billetera oĩ hína oñembojuehe hag̃ua", "wallet_keys": "Billetera seed/clave", "wallet_list_create_new_wallet": "Emoheñói billetera pyahu", + "wallet_list_edit_group_name": "Embosako'i Atyrã Téra", "wallet_list_edit_wallet": "Embosako’i wallet", "wallet_list_failed_to_load": "Ndaikatúi ojehupi la billetera ${wallet_name}. ${error}", "wallet_list_failed_to_remove": "Ndoikói emboguete hag̃ua billetera ${wallet_name}. ${error}", @@ -1104,6 +1391,7 @@ "wc_approve_request_title": "Omoneĩ mba’ejerure", "wc_connect_request_title": "Ombojoaju mba’ejerure", "wc_connected_to": "Oñembojoaju ${name} ndive.", + "wc_desc": "Embojoaju hasy'ỹre DApp-kuérandi", "wc_max_network_fee": "Max cuota red rehegua", "wc_message_to_sign": "Marandu ofirma haguã", "wc_network_fee": "Cuota de red rehegua", @@ -1125,8 +1413,12 @@ "wc_verified": "Ojehecháma", "wc_would_like_to_connect_to": "${name} oñembojoajuse", "wc_would_like_to_sign": "${name} ofirmase", + "website": "Website", "welcome": "Tapeguahẽ porãite", + "welcome_subtitle_new_wallet": "Reñepyrũségui pyahúgui, eikutu Create New Wallet iguýpe ha reñepyrũta.", + "welcome_subtitle_restore_wallet": "Ereko ramo peteĩ billetera oĩmava regueruséva Cake-pe, eiporavónte Restore Existing Wallet ha rohechaukáta ndéve mba'éichapa.", "welcome_to_cakepay": "¡Eg̃uahẽporãite Cake Pay-pe!", + "what_is_payjoin": "¿Mba'épa Payjoin?", "what_is_silent_payments": "¿Mbaʼépa hína Jehepymeʼẽ Kirirĩva?", "whats_new": "Mba’e pyahu", "widgets_address": "Dirección", @@ -1152,11 +1444,15 @@ "yat_popup_content": "Ko’ág̃a ikatu remondo ha rehupyty cripto Cake Wallet-pe nde Yat rupive, peteĩ poruhára réra mbykymi, emoji rehe oñemopyendáva. Eñangareko ha emohenda ne Yat oimeraẽ ára hag̃ua pe ñemboheko hag̃ua hag̃ua.", "yat_popup_title": "Ikatu emoambue nde wallet kundaharape emoji-pe.", "yes": "Hẽe", + "yes_exit": "Hẽe. Esẽ", "yesterday": "Kuehe", "you_now_have_debit_card": "Koʼág̃a reguereko peteĩ tarjeta débito.", "you_pay": "Nde rehepy", "you_will_get": "Emoambue hag̃ua", + "you_will_receive_estimated_amount": "Rehupytýta (jehaipyre)", "you_will_send": "Emoambue guive", + "youCanGoBackToYourDapp": "Ikatúma rejevy ne dApp-pe", + "your": "Nde", "yy": "YY", "zcash_card_description": "Ndohecháiramo nde saldo completo, Cake Wallet ikatu oñeha’ã ombyaty (sweep) umi fondo umi dirección de cambio interno-gui oiporúva heta billetera.", "zcash_card_dismiss": "Mboyke", @@ -1285,7 +1581,6 @@ "hide": "Mokañy", "how_to_connect": "Mba'éichapa Oñembojoaju", "hww_exit_desc": "Kóva omboty pe joaju oikóva ne hardware mba'e'oka ndive", - "if_you_dont_see_your_device": "Ndehecháiramo ne mba'e'oka yvate, ehecha porãke ne Ledger hesãi ha ipe'apyre!", "incorrect_seed_option": "Hekope'ỹ. Eha'ã jey", "incorrect_seed_option_back": "Hekope'ỹ. Ehecha porãke ne seed oñeñongatu hekopete ha eha'ã jey.", "input": "Jeikeha (input)", @@ -1459,4 +1754,4 @@ "you_will_receive_estimated_amount": "Rehupytýta (jehaipyre)", "youCanGoBackToYourDapp": "Ikatúma rejevy ne dApp-pe", "your": "Nde" -} \ No newline at end of file +} diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index b4c142360..66f1df52f 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -76,6 +76,7 @@ "approve_tokens": "Amince token", "apps": "Manhajoji", "arbiscan_history": "Tarihin ArbiScan", + "are_you_sure_exit": "Kun tabbata kuna so ku fita?", "arrive_in_this_address": "${currency} ${tag} za su iso a wannan adireshin", "ascending": "Hawa", "ask_each_time": "Tambaya kowane lokaci", @@ -89,6 +90,7 @@ "auto_generate_addresses": "Samar da adireshi ta atomatik", "auto_generate_subaddresses": "Ƙirƙiri ƙananan adireshi ta atomatik", "automatic": "Atomatik", + "available": "Akwai", "available_balance": "Ma’aunin da ake da shi", "available_balance_description": "“Ma'aunin da ake da shi” ko “Tabbataccen Ma'auni” su ne kuɗaɗen da za a iya kashewa nan take. Idan kuɗaɗe sun bayyana a ƙananan ma'auni amma ba a babban ma'auni ba, to dole ne ku jira 'yan mintoci kaɗan domin kuɗaɗen da ke shigowa su sami ƙarin tabbaci daga hanyar sadarwa. Bayan sun sami ƙarin tabbaci, za a iya kashe su.", "avg_savings": "Matsakaitan Ajiya", @@ -125,6 +127,7 @@ "block_remaining": "Toshe 1 ya rage", "Blocks_remaining": "${status} Tubalan da suka rage", "bluetooth": "Bluetooth", + "bridging": "Ana gada", "bright_theme": "Haske", "bscscan_history": "Tarihin BSCScan", "bump_fee": "Ƙara kuɗin ma'amala", @@ -216,6 +219,8 @@ "color_theme": "Jigon launi", "coming_soon_tag": "Zai zo nan ba da jimawa ba", "commit_transaction_amount_fee": "Tabbatar da ma'amala\nAdadi: ${amount}\nKuɗi: ${fee}", + "compatible_services": "Sabis masu jituwa", + "compatible_services_desc": "Yi amfani da kayan aikin Scan don karanta nau'ikan QR code masu zuwa da Cake Wallet ke goyon baya:", "configure": "Saita", "confirm": "Tabbatar", "confirm_delete_template": "Wannan aikin zai goge wannan samfuri. Kuna so ku ci gaba?", @@ -279,6 +284,7 @@ "creating_new_wallet": "Ana ƙirƙirar sabon walat", "creating_new_wallet_error": "Kuskure: ${description}", "creation_date": "Ranar ƙirƙira", + "crypto_addresses": "Adiresoshin crypto", "csv_nothing_to_export": "Babu abin da za a fitar", "current_swap": "Musayar Yanzu", "current_wallet": "Walit na yanzu", @@ -301,6 +307,7 @@ "default_sell_provider": "Mai Ba da Sabis na Siyarwa na Tsoho", "delete": "Sharewa", "delete_account": "Share Account", + "delete_this_node": "Share wannan node", "delete_wallet": "Share walat", "delete_wallet_confirm_message": "Shin kun tabbata cewa kuna son goge walat ɗin ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Shin kun tabbata kuna son share haɗin zuwa", @@ -330,6 +337,7 @@ "deuro_savings_welcome_description": "dEURO stablecoin ne mai cikakken rarrabawa (decentralized) kuma mai jingina (collateralized) da aka daure shi da Yuro — amintacce, mai sassauci, kuma mai jurewa takunkumi ba tare da dogaro da oracles ba. 1 dEURO = 1 EURO.", "deuro_tooltip_no_eth": "Don kunna dEURO, dole ne ka kasance da ETH a cikin walat ɗinka.", "deuro_tx_commited_content": "Yana iya ɗaukar 'yan daƙiƙu kaɗan kafin ma'amalar ta tabbatar kuma ta bayyana a allon", + "device_confirmation": "Tabbatarwa ta na'ura", "device_is_signing": "Na'urar tana sa hannu", "dfx_option_description": "Sayi crypto da EUR & CHF. Don abokan cinikin dillalai da na kamfanoni a Turai", "did_you_back_up_seeds": "Ka adana dukkan seed ɗinka?", @@ -466,6 +474,8 @@ "extracted_address_content": "Za ka tura kuɗi zuwa\n${recipient_name}", "failed_authentication": "An kasa tantancewa. ${state_error}", "faq": "FAQ", + "favorite_token": "Token da aka fi so", + "favorite_token_desc": "Ma'aunin token da aka fi so zai bayyana a katin ma'auni.", "features": "Fasali", "fee": "Kuɗi", "fee_less_than_min": "Kudin da aka zaɓa ya yi ƙasa da mafi ƙarancin kuɗin ma'amala, don Allah ƙara kuɗin ma'amala domin a iya aika ma'amalar.", @@ -491,6 +501,7 @@ "frozen": "Daskare", "frozen_balance": "Ma'aunin da aka daskare", "full_balance": "Cikakken Ma’auni", + "gallery": "Hoton gallery", "gas_exceeds_allowance": "Gas da ake buƙata don ma'amalar ya wuce adadin da aka amince.", "gas_price": "Farashin gas", "general": "Gabaɗaya", @@ -526,10 +537,10 @@ "how_to_connect": "Yadda ake Haɗawa", "how_to_use": "Yadda ake amfani da shi", "how_to_use_card": "Yadda ake amfani da wannan kati", + "hww_exit_desc": "Wannan zai kawo ƙarshen aikin haɗawa (pairing) da ke gudana da na'urarka ta hardware", "i_understand": "Na fahimta", "icon_style": "Salon icon", "id": "ID: ", - "if_you_dont_see_your_device": "Idan ba ka ga na'urarka a sama ba, da fatan ka tabbatar Ledger ɗinka yana kunne kuma a buɗe!", "ignor": "Yi watsi", "import": "Shigo da", "importNFTs": "Shigo da NFT", @@ -541,6 +552,7 @@ "infobox_auto_address": "Adireshin karɓar ku zai canza duk lokacin da kuka karɓi ma'amala", "infobox_multichain": "Yi amfani da wannan adireshin don karɓar kowace token ko collectible a", "infobox_rotation": "Adireshin karɓar ku zai canza duk lokacin da kuka rufe sannan ku sake buɗe wannan allon", + "input": "Shigarwa", "inputs": "Shigarwa", "insufficient_funds_for_tx": "Ba a da isasshen kuɗi don kammala ma'amala cikin nasara.", "insufficient_lamport_for_tx": "Ba ku da isasshen SOL don rufe ma'amalar da kuɗin ma'amalarta. Da fatan za ku ƙara SOL a walat ɗinku ko ku rage adadin SOL da kuke aikawa.", @@ -606,6 +618,7 @@ "manage_pow_nodes": "Sarrafa nododin PoW", "manage_providers": "Sarrafa masu ba da sabis", "manage_yats": "Sarrafa Yats ɗinka", + "manual_input": "Shigarwa da hannu", "mark_as_redeemed": "Yi Alama a Matsayin An Fansa", "market_place": "Kasuwa", "mask": "Ɓoye", @@ -651,6 +664,7 @@ "nanogpt_subtitle": "Duk sabbin samfura (GPT-5.3, Claude 4.6).\nBabu biyan kuɗin rajista, biya da crypto.", "narrow": "Kunkuntar", "network": "Hanyar sadarwa", + "networks": "Hanyoyin sadarwa", "new_first_wallet_text": "Kare kuɗin crypto ɗinka cikin aminci abu ne mai sauƙi", "new_node_testing": "Gwajin sabon node", "new_subaddress_create": "Ƙirƙira", @@ -679,8 +693,10 @@ "node_new": "Sabon Node", "node_port": "Tashar node", "node_reset_settings_title": "Sake saiti", + "node_share_desc": "Duba wannan QR code daga allon Scan don shigo da wannan node.", "node_test": "Gwaji", "nodes": "Nododi", + "nodes_desc": "Sarrafa nododi da ke akwai ko ƙara naka na musamman.", "nodes_list_reset_to_default_message": "Kuna da tabbacin kuna so ku mayar da saituna zuwa tsoho?", "none_of_selected_providers_can_exchange": "Babu ɗaya daga cikin masu ba da sabis da aka zaɓa da zai iya yin wannan musayar", "noNFTYet": "Babu NFT tukuna", @@ -690,6 +706,7 @@ "note_tap_to_change": "Bayanan kula (taɓa don canjawa)", "notification_permission_denied": "An ƙi ba da izinin sanarwa har abada, don Allah ka kunna shi da hannu a cikin saituna", "nullURIError": "URI ba shi da komai", + "ocp_desc": "Biya da crypto a shaguna", "offer_expires_in": "Tayin zai ƙare a: ", "offline": "Ba ya kan layi", "ok": "OK", @@ -718,8 +735,13 @@ "outputs": "Fitarwa", "overshot": "Overshot", "overwrite_amount": "Maye gurbin adadi", + "pairing_code": "Lambar haɗawa (pairing)", + "pairing_code_desc_1": "Na'urarka za ta nuna lambar haɗawa (pairing).", + "pairing_code_desc_2": "Shigar da ita a nan don ci gaba", + "pairing_error": "Kuskuren haɗawa (pairing)", "pairingInvalidEvent": "Taron Haɗawa mara inganci", "passphrase": "Passphrase (Zaɓi)", + "passphrase_view_keys": "Passphrase", "passphrases_doesnt_match": "Passphrases ba su dace ba, don Allah a sake gwadawa", "password": "Kalmar sirri", "paste": "Manna", @@ -735,6 +757,7 @@ "payjoin_unavailable_sheet_content": "Karɓar ma'amalar Payjoin yana buƙatar ka/ki kasance da Bitcoin a shirye don amfani.\n\nDa zarar walat ɗinka/ɗinki yana da kuɗi, Payjoin za a kunna ta atomatik.", "payjoin_unavailable_sheet_title": "Me yasa Payjoin ba ya samuwa?", "payment_id": "ID na biyan kuɗi: ", + "payment_invoices": "Daftarin biyan kuɗi", "payment_made_easy": "An sauƙaƙa biyan kuɗi", "payment_was_received": "An karɓi biyan ku.", "payments": "Biyan kuɗi", @@ -890,6 +913,7 @@ "save": "Ajiye", "save_backup_password": "Da fatan a tabbatar kun adana kalmar sirrin ajiyar ku. Ba za ku iya shigo da fayilolin ajiyar ku ba tare da ita ba.", "save_backup_password_alert": "Ajiye kalmar sirrin ajiyar baya", + "save_contact": "Ajiye Tuntuɓa", "save_to_downloads": "Ajiye zuwa Fayilolin da aka zazzage", "saved_the_trade_id": "Na ajiye ID na ciniki", "savings": "Ajiya", @@ -1039,6 +1063,7 @@ "setup_your_debit_card": "Saita katin cire kuɗinka", "share": "Raba", "share_address": "Raba adireshi", + "share_this_node": "Raba wannan node", "shared_seed_wallet_groups": "Ƙungiyoyin Walat na Iri ɗaya da aka Raba", "shared_seed_wallets": "Wallets Masu Seed da Aka Raba", "show": "Nuna", @@ -1046,6 +1071,7 @@ "show_balance": "Danna ka riƙe don nuna ma'auni", "show_balance_send_page": "Taɓa don bayyana", "show_balance_toast": "Danna ka riƙe don ɓoye ko nuna ma'auni", + "show_combined_balance": "Nuna haɗaɗɗen ma'auni", "show_details": "Nuna Cikakkun Bayani", "show_hidden_addresses": "Nuna adiresoshin da aka ɓoye", "show_keys": "Nuna seed/maɓallai", @@ -1111,6 +1137,7 @@ "swap_providers": "Masu samar da Swap", "sweeping_wallet": "Ana kwashe kuɗi daga walat", "sweeping_wallet_alert": "Wannan bai kamata ya ɗauki lokaci mai tsawo ba. KADA KA BAR WANNAN ALLON KO KUDADEN DA AKA KWASO NA IYA ɓACEWA.", + "swipe_to_bridge": "Zame don gada", "swipe_to_send": "Zame don aikawa", "switch_wallet": "Canja Wallet", "switchToETHWallet": "Da fatan za a koma zuwa walat ɗin Ethereum kuma a sake gwadawa", @@ -1151,6 +1178,7 @@ "testnet_coins_no_value": "Tsabar kuɗin Testnet ba su da daraja", "third_intro_content": "Yats suna aiki a wajen Cake Wallet ma. Ana iya maye gurbin kowane adireshin walat a duniya da Yat!", "third_intro_title": "Yat yana aiki da kyau tare da wasu", + "this_can_take_few_seconds": "Wannan na iya ɗaukar 'yan daƙiƙu kaɗan", "this_pair_is_not_supported_warning": "Ba a tallafa wa wannan ma'aurata a musayar(ori) da aka zaɓa a yanzu ba. Da fatan za a zaɓi wata musaya.", "thorchain_contract_address_not_supported": "THORChain ba ya goyon bayan aika zuwa adireshin kwangila", "thorchain_taproot_address_not_supported": "Mai ba da sabis na ThorChain ba ya goyon bayan adiresoshin Taproot. Da fatan za a canza adireshin ko a zaɓi wani mai ba da sabis daban.", @@ -1164,6 +1192,7 @@ "token_decimal": "Adadin decimal na token (misali: USDT)", "token_name": "Sunan token (misali: Tether)", "token_symbol": "Alamar token (misali: USDT)", + "token_verified_on_jupiter": "An tabbatar da wannan alamar akan Jupiter", "tokenID": "ID", "tokens": "Token", "tokens_strong_warning": "Kada ka taɓa aika token zuwa wannan adireshin!", @@ -1292,6 +1321,7 @@ "username": "Sunan mai amfani", "username_available": "Sunan mai amfani yana samuwa", "username_character_error": "Sunayen masu amfani dole ne su ƙunshi kawai", + "username_check_your_network": "An kasa duba sunan mai amfani. Da fatan za a duba haɗin cibiyar sadarwarka.", "username_length_error": "Dole sunayen masu amfani su ƙunshi haruffa da lambobi 6-32.", "username_length_error_1_64": "Dole ne sunayen masu amfani su ƙunshi haruffa da lambobi 1-64.", "username_not_available": "Wannan sunan mai amfani ba ya samuwa.", @@ -1302,6 +1332,7 @@ "verify_message": "Tabbatar da Saƙo", "verify_seed": "Tabbatar da Seed", "verify_with_2fa": "Tabbatar da Cake 2FA", + "verifying_code": "Ana tabbatar da lamba", "version": "Sigar ${currentVersion}", "view_all": "Duba duk", "view_in_block_explorer": "Duba a Block Explorer", @@ -1313,6 +1344,7 @@ "waitFewSecondForTxUpdate": "Da fatan za a jira ƴan daƙiƙa kaɗan domin ma'amalar ta bayyana a tarihin ma'amaloli", "wallet": "Walati", "wallet_accounts": "Asusun Walat", + "wallet_balance": "Ma'aunin walat", "wallet_group": "Rukunin Wallet", "wallet_group_description_existing_seed": "Ka zaɓi amfani da seed da ke akwai don wannan Wallet. Za ka iya sake tabbatar da seed ɗin idan kana buƙatar tabbatarwa ko rubuta shi.", "wallet_group_description_four": "don ƙirƙirar walat tare da sabon iri gaba ɗaya.", @@ -1354,6 +1386,7 @@ "wc_approve_request_title": "Amincewa da buƙata", "wc_connect_request_title": "Buƙatar haɗin kai", "wc_connected_to": "An haɗa zuwa ${name}", + "wc_desc": "Haɗa da DApps cikin sauƙi", "wc_max_network_fee": "Matsakaicin kuɗin hanyar sadarwa", "wc_message_to_sign": "Saƙo don sa hannu", "wc_network_fee": "Kudin hanyar sadarwa", @@ -1405,6 +1438,7 @@ "yat_popup_content": "Yanzu za ku iya aikawa da karɓar crypto a cikin Cake Wallet ta amfani da Yat ɗinku — gajeren sunan mai amfani na emoji. Kuna iya sarrafa Yats a kowane lokaci a allon saituna", "yat_popup_title": "Ana iya mayar da adireshin walat ɗinka zuwa emojis.", "yes": "Eh", + "yes_exit": "Eh. Fita", "yesterday": "Jiya", "you_now_have_debit_card": "Yanzu kana da katin zare kudi", "you_pay": "Ka Biya", @@ -1454,4 +1488,4 @@ "wallet_balance": "Ma'aunin walat", "wc_desc": "Haɗa da DApps cikin sauƙi", "yes_exit": "Eh. Fita" -} \ No newline at end of file +} diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 6f21d5d86..08a42cecc 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -76,6 +76,7 @@ "approve_tokens": "टोकन अनुमोदित करें", "apps": "ऐप्स", "arbiscan_history": "ArbiScan इतिहास", + "are_you_sure_exit": "क्या आप वाकई बाहर निकलना चाहते हैं?", "arrive_in_this_address": "${currency} ${tag} इस पते पर आएंगे", "ascending": "आरोही क्रम", "ask_each_time": "हर बार पूछें", @@ -89,6 +90,7 @@ "auto_generate_addresses": "पते स्वतः जनरेट करें", "auto_generate_subaddresses": "उप-पते स्वतः जनरेट करें", "automatic": "स्वचालित", + "available": "उपलब्ध", "available_balance": "उपलब्ध शेष राशि", "available_balance_description": "“Available Balance” या “Confirmed Balance” वे धनराशि हैं जिन्हें तुरंत खर्च किया जा सकता है। यदि फंड नीचे वाले बैलेंस में दिखाई देते हैं, लेकिन ऊपर वाले बैलेंस में नहीं, तो आने वाले फंड को नेटवर्क पर और अधिक पुष्टिकरण मिलने के लिए आपको कुछ मिनट इंतज़ार करना होगा। अधिक पुष्टिकरण मिलने के बाद, वे खर्च करने योग्य हो जाएंगे।", "avg_savings": "औसत बचत", @@ -125,6 +127,7 @@ "block_remaining": "1 ब्लॉक शेष", "Blocks_remaining": "${status} ब्लॉक शेष", "bluetooth": "ब्लूटूथ", + "bridging": "ब्रिजिंग", "bright_theme": "उज्ज्वल", "bscscan_history": "BSCScan इतिहास", "bump_fee": "फीस बढ़ाएँ", @@ -216,6 +219,8 @@ "color_theme": "रंग थीम", "coming_soon_tag": "जल्द आ रहा है", "commit_transaction_amount_fee": "लेन-देन कमिट करें\nराशि: ${amount}\nशुल्क: ${fee}", + "compatible_services": "संगत सेवाएँ", + "compatible_services_desc": "Cake Wallet पर समर्थित निम्न प्रकार के QR कोड पढ़ने के लिए स्कैन टूल का उपयोग करें:", "configure": "कॉन्फ़िगर करें", "confirm": "पुष्टि करें", "confirm_delete_template": "यह कार्रवाई इस टेम्पलेट को हटा देगी। क्या आप जारी रखना चाहते हैं?", @@ -279,6 +284,7 @@ "creating_new_wallet": "नया वॉलेट बनाया जा रहा है", "creating_new_wallet_error": "त्रुटि: ${description}", "creation_date": "निर्माण तिथि", + "crypto_addresses": "क्रिप्टो पते", "csv_nothing_to_export": "निर्यात करने के लिए कुछ नहीं है", "current_swap": "वर्तमान स्वैप", "current_wallet": "वर्तमान वॉलेट", @@ -301,6 +307,7 @@ "default_sell_provider": "डिफ़ॉल्ट सेल प्रदाता", "delete": "हटाएं", "delete_account": "खाता हटाएँ", + "delete_this_node": "इस नोड को हटाएँ", "delete_wallet": "वॉलेट हटाएँ", "delete_wallet_confirm_message": "क्या आप वाकई ${wallet_name} वॉलेट को हटाना चाहते हैं?", "deleteConnectionConfirmationPrompt": "क्या आप वाकई इसके साथ कनेक्शन हटाना चाहते हैं", @@ -330,6 +337,7 @@ "deuro_savings_welcome_description": "dEURO एक पूरी तरह से विकेंद्रीकृत, संपार्श्विक-समर्थित स्टेबलकॉइन है जो यूरो से पेग्ड है — सुरक्षित, बहुमुखी और सेंसरशिप-प्रतिरोधी, और इसके लिए oracles पर निर्भर होने की ज़रूरत नहीं होती। 1 dEURO = 1 EURO.", "deuro_tooltip_no_eth": "dEURO को सक्षम करने के लिए, आपके वॉलेट में ETH होना चाहिए।", "deuro_tx_commited_content": "लेन-देन की पुष्टि होने और स्क्रीन पर दिखाई देने में कुछ सेकंड लग सकते हैं", + "device_confirmation": "डिवाइस पुष्टिकरण", "device_is_signing": "डिवाइस साइन कर रहा है", "dfx_option_description": "EUR और CHF के साथ क्रिप्टो खरीदें। यूरोप में खुदरा और कॉर्पोरेट ग्राहकों के लिए", "did_you_back_up_seeds": "क्या आपने अपने सभी सीड्स का बैकअप ले लिया?", @@ -466,6 +474,8 @@ "extracted_address_content": "आप निम्न को धनराशि भेजेंगे\n${recipient_name}", "failed_authentication": "प्रमाणीकरण विफल रहा। ${state_error}", "faq": "FAQ", + "favorite_token": "पसंदीदा टोकन", + "favorite_token_desc": "पसंदीदा टोकन का बैलेंस बैलेंस कार्ड पर दिखाई देगा।", "features": "विशेषताएँ", "fee": "फीस", "fee_less_than_min": "चयनित शुल्क न्यूनतम से कम है, कृपया लेन-देन भेजने के लिए शुल्क बढ़ाएँ", @@ -491,6 +501,7 @@ "frozen": "फ्रीज़", "frozen_balance": "फ्रीज़ किया गया बैलेंस", "full_balance": "कुल शेष राशि", + "gallery": "गैलरी", "gas_exceeds_allowance": "लेनदेन के लिए आवश्यक गैस अनुमत सीमा से अधिक है।", "gas_price": "गैस मूल्य", "general": "सामान्य", @@ -526,10 +537,10 @@ "how_to_connect": "कनेक्ट कैसे करें", "how_to_use": "कैसे उपयोग करें", "how_to_use_card": "इस कार्ड का उपयोग कैसे करें", + "hww_exit_desc": "इससे आपके हार्डवेयर डिवाइस के साथ चल रही पेयरिंग प्रक्रिया समाप्त हो जाएगी", "i_understand": "मैं समझता/समझती हूँ", "icon_style": "चिह्न शैली", "id": "ID: ", - "if_you_dont_see_your_device": "यदि आपको ऊपर अपना डिवाइस नहीं दिख रहा है, तो कृपया सुनिश्चित करें कि आपका Ledger चालू है और अनलॉक है!", "ignor": "नज़रअंदाज़ करें", "import": "इम्पोर्ट", "importNFTs": "NFTs आयात करें", @@ -541,6 +552,7 @@ "infobox_auto_address": "हर बार जब आप कोई लेन-देन प्राप्त करेंगे, आपका प्राप्ति पता बदल जाएगा", "infobox_multichain": "किसी भी टोकन या संग्रहणीय वस्तु को प्राप्त करने के लिए इस पते का उपयोग करें", "infobox_rotation": "जब भी आप इस स्क्रीन को बंद करके दोबारा खोलेंगे, आपका प्राप्ति पता बदल जाएगा", + "input": "इनपुट", "inputs": "इनपुट", "insufficient_funds_for_tx": "लेनदेन को सफलतापूर्वक निष्पादित करने के लिए अपर्याप्त धनराशि।", "insufficient_lamport_for_tx": "आपके पास लेनदेन और उसके शुल्क को कवर करने के लिए पर्याप्त SOL नहीं है। कृपया अपने वॉलेट में और SOL जोड़ें या आप जितनी SOL भेज रहे हैं, वह राशि कम करें।", @@ -606,6 +618,7 @@ "manage_pow_nodes": "PoW नोड प्रबंधित करें", "manage_providers": "प्रोवाइडर्स प्रबंधित करें", "manage_yats": "Yats मैनेज करें", + "manual_input": "मैन्युअल इनपुट", "mark_as_redeemed": "रिडीम के रूप में चिह्नित करें", "market_place": "मार्केटप्लेस", "mask": "मास्क", @@ -651,6 +664,7 @@ "nanogpt_subtitle": "सभी नवीनतम मॉडल (GPT-5.3, Claude 4.6)।\nकोई सब्सक्रिप्शन नहीं, क्रिप्टो से भुगतान करें।", "narrow": "संकीर्ण", "network": "नेटवर्क", + "networks": "नेटवर्क", "new_first_wallet_text": "अपने क्रिप्टो को सुरक्षित रखना बहुत आसान है", "new_node_testing": "नए नोड का परीक्षण", "new_subaddress_create": "बनाएँ", @@ -679,8 +693,10 @@ "node_new": "नया नोड", "node_port": "नोड पोर्ट", "node_reset_settings_title": "सेटिंग्स रीसेट करें", + "node_share_desc": "इस नोड को आयात करने के लिए स्कैन स्क्रीन से इस QR कोड को स्कैन करें।", "node_test": "टेस्ट", "nodes": "नोड्स", + "nodes_desc": "मौजूदा नोड्स प्रबंधित करें या एक कस्टम नोड जोड़ें।", "nodes_list_reset_to_default_message": "क्या आप वाकई सेटिंग्स को डिफ़ॉल्ट पर रीसेट करना चाहते हैं?", "none_of_selected_providers_can_exchange": "चयनित प्रदाताओं में से कोई भी यह स्वैप नहीं कर सकता", "noNFTYet": "अभी तक कोई NFT नहीं", @@ -690,6 +706,7 @@ "note_tap_to_change": "नोट (बदलने के लिए टैप करें)", "notification_permission_denied": "अधिसूचना की अनुमति स्थायी रूप से अस्वीकार कर दी गई है, कृपया इसे सेटिंग्स में जाकर मैन्युअल रूप से सक्षम करें", "nullURIError": "URI शून्य है", + "ocp_desc": "स्टोर में क्रिप्टो से भुगतान करें", "offer_expires_in": "ऑफ़र की समय-सीमा समाप्त होने में: ", "offline": "ऑफ़लाइन", "ok": "ठीक है", @@ -716,8 +733,13 @@ "outputs": "आउटपुट", "overshot": "लक्ष्य से अधिक हो गया", "overwrite_amount": "राशि ओवरराइट करें", + "pairing_code": "पेयरिंग कोड", + "pairing_code_desc_1": "आपका डिवाइस एक पेयरिंग कोड दिखाएगा।", + "pairing_code_desc_2": "जारी रखने के लिए यहाँ दर्ज करें", + "pairing_error": "पेयरिंग त्रुटि", "pairingInvalidEvent": "अमान्य पेयरिंग इवेंट", "passphrase": "पासफ़्रेज़ (वैकल्पिक)", + "passphrase_view_keys": "पासफ़्रेज़", "passphrases_doesnt_match": "Passphrase मेल नहीं खाते, कृपया फिर से प्रयास करें", "password": "पासवर्ड", "paste": "पेस्ट करें", @@ -733,6 +755,7 @@ "payjoin_unavailable_sheet_content": "Payjoin लेन-देन प्राप्त करने के लिए आपके पास उपयोग के लिए तैयार Bitcoin होना आवश्यक है।\n\nजैसे ही आपके वॉलेट में फंड होंगे, Payjoin अपने-आप सक्षम हो जाएगा।", "payjoin_unavailable_sheet_title": "Payjoin उपलब्ध क्यों नहीं है?", "payment_id": "भुगतान आईडी: ", + "payment_invoices": "भुगतान इनवॉइस", "payment_made_easy": "भुगतान करना आसान", "payment_was_received": "आपका भुगतान प्राप्त हो गया।", "Payment_was_received": "आपका भुगतान प्राप्त हो गया था।", @@ -890,6 +913,7 @@ "save": "सहेजें", "save_backup_password": "कृपया सुनिश्चित करें कि आपने अपना बैकअप पासवर्ड सहेज लिया है। इसके बिना आप अपनी बैकअप फ़ाइलें आयात नहीं कर पाएंगे।", "save_backup_password_alert": "बैकअप पासवर्ड सहेजें", + "save_contact": "संपर्क सहेजें", "save_to_downloads": "डाउनलोड में सहेजें", "saved_the_trade_id": "मैंने ट्रेड ID सेव कर ली है", "savings": "बचत", @@ -1039,6 +1063,7 @@ "setup_your_debit_card": "अपना डेबिट कार्ड सेट अप करें", "share": "शेयर करें", "share_address": "पता साझा करें", + "share_this_node": "इस नोड को साझा करें", "shared_seed_wallet_groups": "साझा सीड वॉलेट समूह", "shared_seed_wallets": "साझा सीड वॉलेट", "show": "दिखाएँ", @@ -1046,6 +1071,7 @@ "show_balance": "बैलेंस दिखाने के लिए देर तक दबाएँ", "show_balance_send_page": "दिखाने के लिए टैप करें", "show_balance_toast": "बैलेंस छिपाने या दिखाने के लिए लंबे समय तक दबाएं", + "show_combined_balance": "संयुक्त बैलेंस दिखाएँ", "show_details": "विवरण दिखाएँ", "show_hidden_addresses": "छिपे हुए पते दिखाएँ", "show_keys": "सीड/कुंजियाँ दिखाएँ", @@ -1111,6 +1137,7 @@ "swap_providers": "स्वैप प्रदाता", "sweeping_wallet": "वॉलेट स्वीप करना", "sweeping_wallet_alert": "इसमें ज़्यादा समय नहीं लगना चाहिए। इस स्क्रीन को न छोड़ें, वरना स्वीप किए गए फंड खो सकते हैं।", + "swipe_to_bridge": "ब्रिज करने के लिए स्वाइप करें", "swipe_to_send": "भेजने के लिए स्वाइप करें", "switch_wallet": "वॉलेट बदलें", "switchToETHWallet": "कृपया Ethereum वॉलेट पर स्विच करें और फिर से प्रयास करें", @@ -1151,6 +1178,7 @@ "testnet_coins_no_value": "टेस्टनेट कॉइन्स का कोई मूल्य नहीं है", "third_intro_content": "Yats, Cake Wallet के बाहर भी मौजूद हैं। दुनिया के किसी भी वॉलेट पते को Yat से बदला जा सकता है!", "third_intro_title": "Yat दूसरों के साथ अच्छी तरह काम करता है", + "this_can_take_few_seconds": "इसमें कुछ सेकंड लग सकते हैं", "this_pair_is_not_supported_warning": "यह जोड़ी वर्तमान में चयनित एक्सचेंज(ों) के साथ समर्थित नहीं है। कृपया कोई अन्य एक्सचेंज चुनें।", "thorchain_contract_address_not_supported": "THORChain कॉन्ट्रैक्ट पते पर भेजने का समर्थन नहीं करता है", "thorchain_taproot_address_not_supported": "ThorChain प्रदाता Taproot पतों का समर्थन नहीं करता है। कृपया पता बदलें या कोई अन्य प्रदाता चुनें।", @@ -1164,6 +1192,7 @@ "token_decimal": "टोकन दशमलव (उदा: USDT)", "token_name": "टोकन का नाम (उदाहरण: Tether)", "token_symbol": "टोकन प्रतीक (उदा.: USDT)", + "token_verified_on_jupiter": "यह टोकन Jupiter पर सत्यापित है", "tokenID": "आईडी", "tokens": "टोकन", "tokens_strong_warning": "इस पते पर कभी भी टोकन न भेजें!", @@ -1292,6 +1321,7 @@ "username": "उपयोगकर्ता नाम", "username_available": "उपयोगकर्ता नाम उपलब्ध है", "username_character_error": "यूज़रनेम में केवल शामिल हो सकते हैं", + "username_check_your_network": "उपयोगकर्ता नाम जाँचने में असमर्थ। कृपया अपना नेटवर्क कनेक्शन जाँचें।", "username_length_error": "उपयोगकर्ता नाम में 6-32 अल्फ़ान्यूमेरिक वर्ण होने चाहिए।", "username_length_error_1_64": "उपयोगकर्ता नाम में 1-64 अल्फ़ान्यूमेरिक वर्ण होने चाहिए।", "username_not_available": "यह उपयोगकर्ता नाम उपलब्ध नहीं है।", @@ -1302,6 +1332,7 @@ "verify_message": "संदेश सत्यापित करें", "verify_seed": "सीड सत्यापित करें", "verify_with_2fa": "Cake 2FA के साथ सत्यापित करें", + "verifying_code": "कोड सत्यापित किया जा रहा है", "version": "संस्करण ${currentVersion}", "view_all": "सभी देखें", "view_in_block_explorer": "ब्लॉक एक्सप्लोरर में देखें", @@ -1313,6 +1344,7 @@ "waitFewSecondForTxUpdate": "कृपया लेन-देन इतिहास में लेन-देन दिखने के लिए कुछ सेकंड प्रतीक्षा करें", "wallet": "वॉलेट", "wallet_accounts": "वॉलेट खाते", + "wallet_balance": "वॉलेट बैलेंस", "wallet_group": "वॉलेट समूह", "wallet_group_description_existing_seed": "आपने इस वॉलेट के लिए मौजूदा सीड का उपयोग करना चुना है। यदि आपको पुष्टि करने या लिखने के लिए इसकी आवश्यकता हो, तो आप सीड को फिर से सत्यापित कर सकते हैं।", "wallet_group_description_four": "पूरी तरह नए सीड के साथ वॉलेट बनाने के लिए।", @@ -1354,6 +1386,7 @@ "wc_approve_request_title": "अनुरोध स्वीकृत करें", "wc_connect_request_title": "कनेक्ट अनुरोध", "wc_connected_to": "${name} से कनेक्टेड", + "wc_desc": "DApps से आसानी से कनेक्ट करें", "wc_max_network_fee": "अधिकतम नेटवर्क शुल्क", "wc_message_to_sign": "हस्ताक्षर करने के लिए संदेश", "wc_network_fee": "नेटवर्क शुल्क", @@ -1404,6 +1437,7 @@ "yat_popup_content": "अब आप Cake Wallet में अपने Yat—एक छोटा, इमोजी-आधारित उपयोगकर्ता नाम—के साथ क्रिप्टो भेज और प्राप्त कर सकते हैं। सेटिंग्स स्क्रीन पर किसी भी समय Yats को प्रबंधित करें", "yat_popup_title": "आपका वॉलेट पता इमोजी में बदला जा सकता है।", "yes": "हाँ", + "yes_exit": "हाँ। बाहर निकलें", "yesterday": "कल", "you_now_have_debit_card": "अब आपके पास एक डेबिट कार्ड है", "you_pay": "आप भुगतान करते हैं", @@ -1453,4 +1487,4 @@ "wallet_balance": "वॉलेट बैलेंस", "wc_desc": "DApps से आसानी से कनेक्ट करें", "yes_exit": "हाँ। बाहर निकलें" -} \ No newline at end of file +} diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index eb66c8e61..6771d0833 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -76,6 +76,7 @@ "approve_tokens": "Odobri tokene", "apps": "Aplikacije", "arbiscan_history": "Povijest na ArbiScanu", + "are_you_sure_exit": "Jeste li sigurni da želite izaći?", "arrive_in_this_address": "${currency} ${tag}će stići na ovu adresu", "ascending": "Uzlazno", "ask_each_time": "Pitaj svaki put", @@ -89,6 +90,7 @@ "auto_generate_addresses": "Automatsko generiranje adresa", "auto_generate_subaddresses": "Automatski generiraj podadrese", "automatic": "Automatski", + "available": "Dostupno", "available_balance": "Raspoloživo stanje", "available_balance_description": "“Dostupno stanje” ili “Potvrđeno stanje” su sredstva koja možete odmah potrošiti. Ako se sredstva prikazuju u donjem stanju, ali ne i u gornjem, morate pričekati nekoliko minuta da dolazna sredstva dobiju više mrežnih potvrda. Nakon što dobiju više potvrda, moći ćete ih potrošiti.", "avg_savings": "Prosj. ušteda", @@ -125,6 +127,7 @@ "block_remaining": "Preostao je 1 blok", "Blocks_remaining": "${status} preostalih blokova", "bluetooth": "Bluetooth", + "bridging": "Premošćivanje", "bright_theme": "Svijetla", "bscscan_history": "Povijest BSCScan-a", "bump_fee": "Povećaj naknadu", @@ -216,6 +219,8 @@ "color_theme": "Tema boja", "coming_soon_tag": "Uskoro", "commit_transaction_amount_fee": "Potvrdi transakciju\nIznos: ${amount}\nNaknada: ${fee}", + "compatible_services": "Kompatibilne usluge", + "compatible_services_desc": "Upotrijebite alat Skeniraj za čitanje sljedećih vrsta QR kodova podržanih u Cake Walletu:", "configure": "Konfiguriraj", "confirm": "Potvrdi", "confirm_delete_template": "Ovom radnjom izbrisat ćete ovaj predložak. Želite li nastaviti?", @@ -279,6 +284,7 @@ "creating_new_wallet": "Stvaranje novog novčanika", "creating_new_wallet_error": "Greška: ${description}", "creation_date": "Datum izrade", + "crypto_addresses": "Kripto adrese", "csv_nothing_to_export": "Nema ničega za izvoz", "current_swap": "Trenutna zamjena", "current_wallet": "Trenutni novčanik", @@ -301,6 +307,7 @@ "default_sell_provider": "Zadani pružatelj usluge prodaje", "delete": "Izbriši", "delete_account": "Izbriši račun", + "delete_this_node": "Izbriši ovaj čvor", "delete_wallet": "Izbriši novčanik", "delete_wallet_confirm_message": "Jeste li sigurni da želite izbrisati novčanik ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Jeste li sigurni da želite izbrisati vezu s", @@ -330,6 +337,7 @@ "deuro_savings_welcome_description": "dEURO je potpuno decentralizirani, kolateralizirani stablecoin vezan uz euro — siguran, svestran i otporan na cenzuru, bez potrebe oslanjanja na orakle. 1 dEURO = 1 EURO.", "deuro_tooltip_no_eth": "Da biste omogućili dEURO, morate imati ETH u svom novčaniku.", "deuro_tx_commited_content": "Može potrajati nekoliko sekundi da se transakcija potvrdi i prikaže na zaslonu", + "device_confirmation": "Potvrda uređaja", "device_is_signing": "Uređaj potpisuje", "dfx_option_description": "Kupite kriptovalute uz EUR i CHF. Za maloprodajne i korporativne kupce u Europi", "did_you_back_up_seeds": "Jeste li sigurnosno kopirali sve svoje seed fraze?", @@ -466,6 +474,8 @@ "extracted_address_content": "Poslat ćete sredstva na\n${recipient_name}", "failed_authentication": "Neuspješna autentifikacija. ${state_error}", "faq": "FAQ", + "favorite_token": "Omiljeni token", + "favorite_token_desc": "Stanje omiljenog tokena prikazat će se na kartici stanja.", "features": "Značajke", "fee": "Naknada", "fee_less_than_min": "Odabrana naknada je manja od minimalne, povećajte naknadu kako biste mogli poslati transakciju", @@ -491,6 +501,7 @@ "frozen": "Zamrznuto", "frozen_balance": "Zamrznuto stanje", "full_balance": "Puni saldo", + "gallery": "Galerija", "gas_exceeds_allowance": "Gas potreban za transakciju premašuje dopušteni iznos.", "gas_price": "Cijena gasa", "general": "Općenito", @@ -526,10 +537,10 @@ "how_to_connect": "Kako se povezati", "how_to_use": "Kako koristiti", "how_to_use_card": "Kako koristiti ovu karticu", + "hww_exit_desc": "Ovo će prekinuti tekući postupak uparivanja s vašim hardverskim uređajem", "i_understand": "Razumijem", "icon_style": "Stil ikone", "id": "ID: ", - "if_you_dont_see_your_device": "Ako gore ne vidite svoj uređaj, provjerite je li vaš Ledger uključen i otključan!", "ignor": "Zanemari", "import": "Uvoz", "importNFTs": "Uvezi NFT-ove", @@ -541,6 +552,7 @@ "infobox_auto_address": "Vaša adresa za primanje mijenjat će se svaki put kada primite transakciju", "infobox_multichain": "Koristite ovu adresu za primanje bilo kojeg tokena ili kolekcionarskog predmeta na", "infobox_rotation": "Vaša adresa za primanje će se mijenjati svaki put kada zatvorite i ponovno otvorite ovaj zaslon", + "input": "Unos", "inputs": "Ulazi", "insufficient_funds_for_tx": "Nedovoljno sredstava za uspješno izvršenje transakcije.", "insufficient_lamport_for_tx": "Nemate dovoljno SOL-a za pokrivanje transakcije i transakcijske naknade. Dodajte više SOL-a u svoj novčanik ili smanjite iznos SOL-a koji šaljete.", @@ -606,6 +618,7 @@ "manage_pow_nodes": "Upravljaj PoW čvorovima", "manage_providers": "Upravljanje pružateljima", "manage_yats": "Upravljanje Yatovima", + "manual_input": "Ručni unos", "mark_as_redeemed": "Označi kao iskorišteno", "market_place": "Tržište", "mask": "Maska", @@ -651,6 +664,7 @@ "nanogpt_subtitle": "Svi najnoviji modeli (GPT-5.3, Claude 4.6).\nBez pretplate, plaćajte kriptom.", "narrow": "Usko", "network": "Mreža", + "networks": "Mreže", "new_first_wallet_text": "Čuvanje vaše kriptovalute sigurnom je mačji kašalj", "new_node_testing": "Testiranje novog noda", "new_subaddress_create": "Izradi", @@ -679,8 +693,10 @@ "node_new": "Novi čvor", "node_port": "Port čvora", "node_reset_settings_title": "Ponovno postavi postavke", + "node_share_desc": "Skenirajte ovaj QR kod sa zaslona Skeniraj da biste uvezli ovaj čvor.", "node_test": "Test", "nodes": "Čvorovi", + "nodes_desc": "Upravljajte postojećim čvorovima ili dodajte prilagođeni.", "nodes_list_reset_to_default_message": "Jeste li sigurni da želite vratiti postavke na zadane vrijednosti?", "none_of_selected_providers_can_exchange": "Nijedan od odabranih pružatelja ne može izvršiti ovu zamjenu", "noNFTYet": "Još nema NFT-ova", @@ -690,6 +706,7 @@ "note_tap_to_change": "Napomena (dodirnite za promjenu)", "notification_permission_denied": "Dozvola za obavijesti trajno je odbijena, molimo ručno je omogućite u postavkama", "nullURIError": "URI je null", + "ocp_desc": "Plaćajte kriptovalutom u trgovinama", "offer_expires_in": "Ponuda istječe za: ", "offline": "Izvan mreže", "ok": "OK", @@ -716,8 +733,13 @@ "outputs": "Izlazi", "overshot": "Prekoračeno", "overwrite_amount": "Prepiši iznos", + "pairing_code": "Kod za uparivanje", + "pairing_code_desc_1": "Vaš će uređaj prikazati kod za uparivanje.", + "pairing_code_desc_2": "Unesite ga ovdje za nastavak", + "pairing_error": "Pogreška pri uparivanju", "pairingInvalidEvent": "Nevažeći događaj uparivanja", "passphrase": "Lozinka (neobavezno)", + "passphrase_view_keys": "Lozinka", "passphrases_doesnt_match": "Lozinke se ne podudaraju, pokušajte ponovno", "password": "Lozinka", "paste": "Zalijepi", @@ -733,6 +755,7 @@ "payjoin_unavailable_sheet_content": "Primanje Payjoin transakcije zahtijeva da imate Bitcoin spreman za korištenje.\n\nČim vaš novčanik bude imao sredstva, Payjoin će se automatski omogućiti.", "payjoin_unavailable_sheet_title": "Zašto Payjoin nije dostupan?", "payment_id": "ID plaćanja: ", + "payment_invoices": "Računi za plaćanje", "payment_made_easy": "Plaćanja su jednostavna", "payment_was_received": "Vaša uplata je primljena.", "payments": "Plaćanja", @@ -888,6 +911,7 @@ "save": "Spremi", "save_backup_password": "Molimo provjerite jeste li spremili lozinku za sigurnosnu kopiju. Bez nje nećete moći uvesti svoje datoteke sigurnosne kopije.", "save_backup_password_alert": "Spremi lozinku sigurnosne kopije", + "save_contact": "Spremi kontakt", "save_to_downloads": "Spremi u Preuzimanja", "saved_the_trade_id": "Spremio/la sam ID trgovine", "savings": "Štednja", @@ -1037,6 +1061,7 @@ "setup_your_debit_card": "Postavite svoju debitnu karticu", "share": "Podijeli", "share_address": "Podijeli adresu", + "share_this_node": "Podijeli ovaj čvor", "shared_seed_wallet_groups": "Grupe novčanika sa zajedničkom seed frazom", "shared_seed_wallets": "Novčanici sa zajedničkom seed frazom", "show": "Prikaži", @@ -1044,6 +1069,7 @@ "show_balance": "Dugo pritisnite za prikaz stanja", "show_balance_send_page": "Dodirnite za prikaz", "show_balance_toast": "Dugo pritisnite za skrivanje ili prikaz stanja", + "show_combined_balance": "Prikaži kombinirano stanje", "show_details": "Prikaži detalje", "show_hidden_addresses": "Prikaži skrivene adrese", "show_keys": "Prikaži seed/ključeve", @@ -1109,6 +1135,7 @@ "swap_providers": "Pružatelji usluga zamjene", "sweeping_wallet": "Prebacivanje sredstava u novčanik", "sweeping_wallet_alert": "Ovo ne bi trebalo dugo trajati. NE NAPUŠTAJTE OVAJ ZASLON ILI PREBAČENA SREDSTVA MOGU BITI IZGUBLJENA.", + "swipe_to_bridge": "Prijeđite prstom za premošćivanje", "swipe_to_send": "Prijeđite prstom za slanje", "switch_wallet": "Prebaci novčanik", "switchToETHWallet": "Molimo prebacite se na Ethereum novčanik i pokušajte ponovno", @@ -1149,6 +1176,7 @@ "testnet_coins_no_value": "Testnet kovanice nemaju vrijednost", "third_intro_content": "Yatovi žive i izvan Cake Walleta. Bilo koja adresa novčanika na svijetu može se zamijeniti Yatom!", "third_intro_title": "Yat se dobro slaže s drugima", + "this_can_take_few_seconds": "Ovo može potrajati nekoliko sekundi", "this_pair_is_not_supported_warning": "Ovaj par nije podržan na trenutno odabranoj razmjeni/razmjenama. Molimo odaberite drugu razmjenu.", "thorchain_contract_address_not_supported": "THORChain ne podržava slanje na adresu ugovora", "thorchain_taproot_address_not_supported": "Pružatelj usluge ThorChain ne podržava Taproot adrese. Promijenite adresu ili odaberite drugog pružatelja usluge.", @@ -1162,6 +1190,7 @@ "token_decimal": "Decimalni broj tokena (npr.: USDT)", "token_name": "Naziv tokena (npr.: Tether)", "token_symbol": "Simbol tokena (npr.: USDT)", + "token_verified_on_jupiter": "Ovaj token je verificiran na Jupiteru", "tokenID": "ID", "tokens": "Tokeni", "tokens_strong_warning": "Nikada nemojte slati tokene na ovu adresu!", @@ -1290,6 +1319,7 @@ "username": "Korisničko ime", "username_available": "Korisničko ime je dostupno", "username_character_error": "Korisnička imena smiju sadržavati samo", + "username_check_your_network": "Nije moguće provjeriti korisničko ime. Provjerite mrežnu vezu.", "username_length_error": "Korisnička imena moraju sadržavati 6–32 alfanumerička znaka.", "username_length_error_1_64": "Korisnička imena moraju sadržavati 1–64 alfanumerička znaka.", "username_not_available": "Ovo korisničko ime nije dostupno.", @@ -1300,6 +1330,7 @@ "verify_message": "Potvrdi poruku", "verify_seed": "Potvrdite seed", "verify_with_2fa": "Potvrdite uz Cake 2FA", + "verifying_code": "Provjera koda", "version": "Verzija ${currentVersion}", "view_all": "Prikaži sve", "view_in_block_explorer": "Prikaži u Block Exploreru", @@ -1311,6 +1342,7 @@ "waitFewSecondForTxUpdate": "Pričekajte nekoliko sekundi da se transakcija prikaže u povijesti transakcija.", "wallet": "Novčanik", "wallet_accounts": "Računi u novčaniku", + "wallet_balance": "Stanje novčanika", "wallet_group": "Grupa novčanika", "wallet_group_description_existing_seed": "Odabrali ste koristiti postojeću seed frazu za ovaj novčanik. Možete je ponovno provjeriti ako je trebate potvrditi ili zapisati.", "wallet_group_description_four": "za izradu novčanika s potpuno novom seed frazom.", @@ -1352,6 +1384,7 @@ "wc_approve_request_title": "Odobri zahtjev", "wc_connect_request_title": "Zahtjev za povezivanje", "wc_connected_to": "Povezan s ${name}", + "wc_desc": "Jednostavno se povežite s DApp-ovima", "wc_max_network_fee": "Maksimalna mrežna naknada", "wc_message_to_sign": "Poruka za potpisivanje", "wc_network_fee": "Mrežna naknada", @@ -1402,6 +1435,7 @@ "yat_popup_content": "Sada možete slati i primati kripto u Cake Walletu pomoću svog Yata – kratkog korisničkog imena temeljenog na emojijima. Yatovima možete upravljati u bilo kojem trenutku na zaslonu postavki", "yat_popup_title": "Adresa vašeg novčanika može se pretvoriti u emojije.", "yes": "Da", + "yes_exit": "Da. Izađi", "yesterday": "Jučer", "you_now_have_debit_card": "Sada imate debitnu karticu", "you_pay": "Vi plaćate", @@ -1451,4 +1485,4 @@ "wallet_balance": "Stanje novčanika", "wc_desc": "Jednostavno se povežite s DApp-ovima", "yes_exit": "Da. Izađi" -} \ No newline at end of file +} diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index c81c4e463..34a6fca5d 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -76,6 +76,7 @@ "approve_tokens": "Հաստատել թոքենները", "apps": "Հավելվածներ", "arbiscan_history": "ArbiScan-ի պատմություն", + "are_you_sure_exit": "Վստա՞հ եք, որ ցանկանում եք դուրս գալ։", "arrive_in_this_address": "${currency} ${tag}կհասնի այս հասցեին", "ascending": "Աճման կարգով", "ask_each_time": "Հարցնել ամեն անգամ", @@ -89,6 +90,7 @@ "auto_generate_addresses": "Հասցեների ավտոմատ ստեղծում", "auto_generate_subaddresses": "Ենթահասցեների ավտոմատ ստեղծում", "automatic": "Ավտոմատ", + "available": "Հասանելի", "available_balance": "Հասանելի մնացորդ", "available_balance_description": "«Հասանելի մնացորդը» կամ «Հաստատված մնացորդը» միջոցներ են, որոնք կարելի է անմիջապես ծախսել։ Եթե միջոցները երևում են ստորին մնացորդում, բայց ոչ վերևի մնացորդում, ապա պետք է մի քանի րոպե սպասեք, մինչև մուտքագրվող միջոցները ստանան ավելի շատ ցանցային հաստատումներ։ Ավելի շատ հաստատումներ ստանալուց հետո դրանք հնարավոր կլինի ծախսել։", "avg_savings": "Միջին խնայողություն", @@ -125,6 +127,7 @@ "block_remaining": "Մնացել է 1 բլոկ", "Blocks_remaining": "${status} Բլոկ է մնացել", "bluetooth": "Bluetooth", + "bridging": "Կամրջում", "bright_theme": "Պայծառ", "bscscan_history": "BSCScan-ի պատմություն", "bump_fee": "Բարձրացնել վճարը", @@ -216,6 +219,8 @@ "color_theme": "Գունային թեմա", "coming_soon_tag": "Շուտով", "commit_transaction_amount_fee": "Հաստատել գործարքը\nԳումար՝ ${amount}\nՄիջնորդավճար՝ ${fee}", + "compatible_services": "Համատեղելի ծառայություններ", + "compatible_services_desc": "Օգտագործեք Սկանավորման գործիքը՝ Cake Wallet-ի կողմից աջակցվող QR կոդերի հետևյալ տեսակները կարդալու համար՝", "configure": "Կարգավորել", "confirm": "Հաստատել", "confirm_delete_template": "Այս գործողությունը կջնջի այս ձևանմուշը։ Ցանկանու՞մ եք շարունակել։", @@ -279,6 +284,7 @@ "creating_new_wallet": "Ստեղծվում է նոր դրամապանակ", "creating_new_wallet_error": "Սխալ՝ ${description}", "creation_date": "Ստեղծման ամսաթիվ", + "crypto_addresses": "Կրիպտո հասցեներ", "csv_nothing_to_export": "Արտահանելու ոչինչ չկա", "current_swap": "Ընթացիկ փոխանակում", "current_wallet": "Ընթացիկ դրամապանակ", @@ -301,6 +307,7 @@ "default_sell_provider": "Վաճառքի լռելյայն մատակարար", "delete": "Ջնջել", "delete_account": "Ջնջել հաշիվը", + "delete_this_node": "Ջնջել այս հանգույցը", "delete_wallet": "Ջնջել դրամապանակը", "delete_wallet_confirm_message": "Վստա՞հ եք, որ ցանկանում եք ջնջել ${wallet_name} դրամապանակը։", "deleteConnectionConfirmationPrompt": "Վստա՞հ եք, որ ցանկանում եք ջնջել կապը", @@ -330,6 +337,7 @@ "deuro_savings_welcome_description": "dEURO-ն լիովին ապակենտրոնացված, գրավով ապահովված ստեյբլքոյն է՝ կապված եվրոյին․ անվտանգ, բազմակողմանի և գրաքննության նկատմամբ դիմացկուն՝ առանց օրաքլների վրա հենվելու։ 1 dEURO = 1 EURO։", "deuro_tooltip_no_eth": "dEURO-ն միացնելու համար ձեր դրամապանակում պետք է ունենաք ETH։", "deuro_tx_commited_content": "Գործարքի հաստատման և էկրանին արտացոլվելու համար կարող է պահանջվել մի քանի վայրկյան", + "device_confirmation": "Սարքի հաստատում", "device_is_signing": "Սարքը ստորագրում է", "dfx_option_description": "Գնեք կրիպտոարժույթ EUR և CHF-ով։ Եվրոպայում գործող մանրածախ և կորպորատիվ հաճախորդների համար", "did_you_back_up_seeds": "Դուք պահուստավորե՞լ եք ձեր բոլոր սիդերը։", @@ -466,6 +474,8 @@ "extracted_address_content": "Դուք կուղարկեք միջոցներ\n${recipient_name}-ին", "failed_authentication": "Նույնականացումը ձախողվեց. ${state_error}", "faq": "ՀՏՀ", + "favorite_token": "Նախընտրած թոքեն", + "favorite_token_desc": "Նախընտրած թոքենի մնացորդը կցուցադրվի մնացորդի քարտի վրա։", "features": "Ֆունկցիաներ", "fee": "Միջնորդավճար", "fee_less_than_min": "Ընտրված միջնորդավճարը նվազագույնից պակաս է, խնդրում ենք ավելացնել միջնորդավճարը, որպեսզի կարողանաք ուղարկել գործարքը", @@ -491,6 +501,7 @@ "frozen": "Սառեցված", "frozen_balance": "Սառեցված մնացորդ", "full_balance": "Ամբողջական մնացորդ", + "gallery": "Պատկերասրահ", "gas_exceeds_allowance": "Գործարքի համար պահանջվող գազը գերազանցում է թույլատրելի սահմանաչափը։", "gas_price": "Գազի գին", "general": "Ընդհանուր", @@ -526,10 +537,10 @@ "how_to_connect": "Ինչպես միանալ", "how_to_use": "Ինչպես օգտագործել", "how_to_use_card": "Ինչպես օգտագործել այս քարտը", + "hww_exit_desc": "Սա կդադարեցնի ձեր ապարատային սարքի հետ ընթացիկ զուգակցման գործընթացը", "i_understand": "Հասկացա", "icon_style": "Սրբապատկերների ոճը", "id": "ID: ", - "if_you_dont_see_your_device": "Եթե վերևում չեք տեսնում ձեր սարքը, համոզվեք, որ ձեր Ledger-ը միացված է և ապակողպված է։", "ignor": "Անտեսել", "import": "Ներմուծում", "importNFTs": "Ներմուծել NFT-ներ", @@ -541,6 +552,7 @@ "infobox_auto_address": "Ձեր ստացման հասցեն կպտտվի ամեն անգամ, երբ դուք ստանաք գործարք", "infobox_multichain": "Օգտագործեք այս հասցեն՝ ցանկացած թոքեն կամ հավաքածու ստանալու համար", "infobox_rotation": "Ձեր ստացման հասցեն կփոխվի ամեն անգամ, երբ փակեք և նորից բացեք այս էկրանը", + "input": "Մուտք", "inputs": "Մուտքեր", "insufficient_funds_for_tx": "Գործարքը հաջողությամբ կատարելու համար բավարար միջոցներ չկան։", "insufficient_lamport_for_tx": "Դուք չունեք բավարար SOL՝ գործարքը և դրա գործարքի վճարը ծածկելու համար։ Խնդրում ենք ավելացնել ավելի շատ SOL ձեր դրամապանակում կամ նվազեցնել ուղարկվող SOL-ի քանակը։", @@ -606,6 +618,7 @@ "manage_pow_nodes": "Կառավարել PoW հանգույցները", "manage_providers": "Կառավարել մատակարարներին", "manage_yats": "Կառավարել Yats-երը", + "manual_input": "Ձեռքով մուտքագրում", "mark_as_redeemed": "Նշել որպես օգտագործված", "market_place": "Շուկա", "mask": "Դիմակ", @@ -651,6 +664,7 @@ "nanogpt_subtitle": "Բոլոր ամենանոր մոդելները (GPT-5.3, Claude 4.6).\nԱռանց բաժանորդագրության՝ վճարեք կրիպտոարժույթով։", "narrow": "Նեղ", "network": "Ցանց", + "networks": "Ցանցեր", "new_first_wallet_text": "Ձեր կրիպտոն անվտանգ պահելը շատ հեշտ է", "new_node_testing": "Նոր հանգույցի փորձարկում", "new_subaddress_create": "Ստեղծել", @@ -667,6 +681,7 @@ "no_extra_detail": "Լրացուցիչ մանրամասներ հասանելի չեն", "no_id_needed": "ID չի պահանջվում!", "no_id_required": "Անձը հաստատող փաստաթուղթ չի պահանջվում։ Լրացրեք հաշիվը և ծախսեք ամենուր", + "no_providers_available": "Մատակարարներ չկան", "no_relay_on_domain": "Օգտատիրոջ տիրույթի համար ռելե չկա կամ ռելեն անհասանելի է։ Խնդրում ենք ընտրել օգտագործելու համար ռելե։", "no_relays": "Ռելեներ չկան", "no_relays_message": "Մենք գտել ենք Nostr NIP-05 գրառում այս օգտատիրոջ համար, բայց այն չի պարունակում որևէ relay։ Խնդրում ենք ստացողին տեղեկացնել, որ իր Nostr գրառման մեջ ավելացնի relay-ներ։", @@ -678,8 +693,10 @@ "node_new": "Նոր հանգույց", "node_port": "Հանգույցի պորտ", "node_reset_settings_title": "Կարգավորումների վերակայում", + "node_share_desc": "Սկանավորեք այս QR կոդը Սկանավորման էկրանից՝ այս հանգույցը ներմուծելու համար։", "node_test": "Թեստ", "nodes": "Հանգույցներ", + "nodes_desc": "Կառավարեք առկա հանգույցները կամ ավելացրեք անհատական հանգույց։", "nodes_list_reset_to_default_message": "Վստա՞հ եք, որ ցանկանում եք վերականգնել կարգավորումները լռելյայնին:", "none_of_selected_providers_can_exchange": "Ընտրված մատակարարներից ոչ մեկը չի կարող կատարել այս սվոփը", "noNFTYet": "Դեռևս NFT-ներ չկան", @@ -689,6 +706,7 @@ "note_tap_to_change": "Նշում (հպեք՝ փոխելու համար)", "notification_permission_denied": "Ծանուցումների թույլտվությունը մշտապես մերժվել է, խնդրում ենք այն ձեռքով միացնել կարգավորումներում", "nullURIError": "URI-ն null է", + "ocp_desc": "Վճարեք կրիպտոյով խանութներում", "offer_expires_in": "Առաջարկը կավարտվի՝ ", "offline": "Անցանց", "ok": "Լավ", @@ -715,8 +733,13 @@ "outputs": "Ելքեր", "overshot": "Գերազանցել է", "overwrite_amount": "Վերագրել գումարը", + "pairing_code": "Զուգակցման կոդ", + "pairing_code_desc_1": "Ձեր սարքը ցույց կտա զուգակցման կոդ։", + "pairing_code_desc_2": "Մուտքագրեք այստեղ՝ շարունակելու համար", + "pairing_error": "Զուգակցման սխալ", "pairingInvalidEvent": "Զուգակցման անվավեր իրադարձություն", "passphrase": "Գաղտնաբառային արտահայտություն (Ոչ պարտադիր)", + "passphrase_view_keys": "Գաղտնաբառային արտահայտություն", "passphrases_doesnt_match": "Գաղտնաբառերը չեն համընկնում, խնդրում ենք կրկին փորձել", "password": "Գաղտնաբառ", "paste": "Փակցնել", @@ -732,6 +755,7 @@ "payjoin_unavailable_sheet_content": "Payjoin գործարք ստանալու համար անհրաժեշտ է, որ ձեր դրամապանակում օգտագործման համար պատրաստ Bitcoin ունենաք։\n\nՀենց որ ձեր դրամապանակում միջոցներ լինեն, Payjoin-ը ավտոմատ կերպով կմիանա։", "payjoin_unavailable_sheet_title": "Ինչո՞ւ է Payjoin-ը անհասանելի։", "payment_id": "Վճարման ID՝ ", + "payment_invoices": "Վճարման հաշիվ-ապրանքագրեր", "payment_made_easy": "Վճարումները՝ հեշտ ու արագ", "payment_was_received": "Ձեր վճարումը ստացվել է։", "payments": "Վճարումներ", @@ -887,6 +911,7 @@ "save": "Պահպանել", "save_backup_password": "Խնդրում ենք համոզվել, որ պահպանել եք ձեր կրկնօրինակի գաղտնաբառը։ Առանց դրա դուք չեք կարողանա ներմուծել ձեր կրկնօրինակման ֆայլերը։", "save_backup_password_alert": "Պահպանել պահուստային պատճենման գաղտնաբառը", + "save_contact": "Պահպանել կոնտակտը", "save_to_downloads": "Պահպանել «Ներբեռնումներ» պանակում", "saved_the_trade_id": "Ես պահպանել եմ փոխանակման ID-ն", "savings": "Խնայողություններ", @@ -958,6 +983,7 @@ "select_sell_provider_notice": "Վերևում ընտրեք վաճառքի մատակարար։ Դուք կարող եք բաց թողնել այս էկրանը՝ հավելվածի կարգավորումներում սահմանելով ձեր լռելայն վաճառքի մատակարարին։", "select_token": "Ընտրեք թոքեն", "select_your_country": "Խնդրում ենք ընտրել ձեր երկիրը", + "selected_trocador_provider": "ընտրված Trocador մատակարար", "sell": "Վաճառել", "sell_alert_content": "Մենք ներկայումս աջակցում ենք միայն Bitcoin, Ethereum և Litecoin-ի վաճառքին։ Խնդրում ենք ստեղծել կամ անցնել ձեր Bitcoin, Ethereum կամ Litecoin դրամապանակին։", "sell_monero_com_alert_content": "Monero-ի վաճառքը դեռ չի աջակցվում", @@ -1035,6 +1061,7 @@ "setup_your_debit_card": "Կարգավորել ձեր դեբետային քարտը", "share": "Կիսվել", "share_address": "Կիսվել հասցեով", + "share_this_node": "Կիսվել այս հանգույցով", "shared_seed_wallet_groups": "Համօգտագործվող սիդով դրամապանակների խմբեր", "shared_seed_wallets": "Համօգտագործվող սիդով դրամապանակներ", "show": "Ցուցադրել", @@ -1042,6 +1069,7 @@ "show_balance": "Երկար սեղմեք՝ մնացորդը ցուցադրելու համար", "show_balance_send_page": "Հպեք՝ ցուցադրելու համար", "show_balance_toast": "Երկար սեղմեք՝ մնացորդը թաքցնելու կամ ցույց տալու համար", + "show_combined_balance": "Ցուցադրել համակցված մնացորդը", "show_details": "Ցույց տալ մանրամասները", "show_hidden_addresses": "Ցույց տալ թաքնված հասցեները", "show_keys": "Ցուցադրել seed-ը/բանալիները", @@ -1107,6 +1135,7 @@ "swap_providers": "Փոխանակման մատակարարներ", "sweeping_wallet": "Դրամապանակի սվիպինգ", "sweeping_wallet_alert": "Սա երկար չի տևի։ ՄԻ ԼՔԵՔ ԱՅՍ ԷԿՐԱՆԸ, ՀԱԿԱՌԱԿ ԴԵՊՔՈՒՄ ՄԱՔՐՎՈՂ ՄԻՋՈՑՆԵՐԸ ԿԱՐՈՂ ԵՆ ԿՈՐՉԵԼ։", + "swipe_to_bridge": "Սահեցրեք՝ կամրջելու համար", "swipe_to_send": "Ուղարկելու համար սահեցրեք", "switch_wallet": "Փոխել դրամապանակը", "switchToETHWallet": "Խնդրում ենք անցնել Ethereum դրամապանակի և կրկին փորձել", @@ -1147,6 +1176,7 @@ "testnet_coins_no_value": "Testnet մետաղադրամները արժեք չունեն", "third_intro_content": "Yat-երը գործում են նաև Cake Wallet-ից դուրս։ Աշխարհի ցանկացած դրամապանակի հասցե կարելի է փոխարինել Yat-ով։", "third_intro_title": "Yat-ը լավ է համատեղվում ուրիշների հետ", + "this_can_take_few_seconds": "Սա կարող է տևել մի քանի վայրկյան", "this_pair_is_not_supported_warning": "Այս զույգը չի աջակցվում ներկայումս ընտրված փոխանակման հարթակ(ներ)ում։ Խնդրում ենք ընտրել մեկ այլ փոխանակման հարթակ։", "thorchain_contract_address_not_supported": "THORChain-ը չի աջակցում պայմանագրային հասցե ուղարկելուն", "thorchain_taproot_address_not_supported": "ThorChain մատակարարը չի աջակցում Taproot հասցեներին։ Խնդրում ենք փոխել հասցեն կամ ընտրել այլ մատակարար։", @@ -1160,6 +1190,7 @@ "token_decimal": "Թոքենի տասնորդականներ (օր.՝ USDT)", "token_name": "Թոքենի անունը (օր.՝ Tether)", "token_symbol": "Թոքենի նշան (օր.՝ USDT)", + "token_verified_on_jupiter": "Այս նշանը հաստատված է Jupiter-ի վրա", "tokenID": "ID", "tokens": "Թոքեններ", "tokens_strong_warning": "Երբեք մի ուղարկեք թոքեններ այս հասցեին!", @@ -1288,6 +1319,7 @@ "username": "Օգտատիրոջ անուն", "username_available": "Օգտանունը հասանելի է", "username_character_error": "Օգտվողի անունները պետք է պարունակեն միայն", + "username_check_your_network": "Չհաջողվեց ստուգել օգտանունը։ Խնդրում ենք ստուգել ձեր ցանցային կապը։", "username_length_error": "Օգտվողի անունները պետք է պարունակեն 6-32 այբբենաթվային նիշ։", "username_length_error_1_64": "Օգտվողի անունները պետք է պարունակեն 1-64 այբբենաթվային նիշ։", "username_not_available": "Այս օգտանունը հասանելի չէ։", @@ -1298,6 +1330,7 @@ "verify_message": "Ստուգել հաղորդագրությունը", "verify_seed": "Հաստատել սիդը", "verify_with_2fa": "Հաստատեք Cake 2FA-ով", + "verifying_code": "Կոդի ստուգում", "version": "Տարբերակ ${currentVersion}", "view_all": "Դիտել բոլորը", "view_in_block_explorer": "Դիտել բլոկների դիտարկիչում", @@ -1309,6 +1342,7 @@ "waitFewSecondForTxUpdate": "Խնդրում ենք սպասել մի քանի վայրկյան, որպեսզի գործարքը արտացոլվի գործարքների պատմության մեջ", "wallet": "Դրամապանակ", "wallet_accounts": "Դրամապանակի հաշիվներ", + "wallet_balance": "Դրամապանակի մնացորդ", "wallet_group": "Դրամապանակների խումբ", "wallet_group_description_existing_seed": "Դուք ընտրել եք այս դրամապանակի համար օգտագործել գոյություն ունեցող սիդը։ Անհրաժեշտության դեպքում կարող եք կրկին ստուգել սիդը՝ այն հաստատելու կամ գրի առնելու համար։", "wallet_group_description_four": "ամբողջովին նոր սիդով դրամապանակ ստեղծելու համար։", @@ -1346,9 +1380,11 @@ "walletConnect": "WalletConnect", "wallets": "Դրամապանակներ", "warning": "Զգուշացում", + "wc_action_approve": "Հաստատել", "wc_approve_request_title": "Հաստատել հարցումը", "wc_connect_request_title": "Միացման հարցում", "wc_connected_to": "Միացված է ${name}-ին", + "wc_desc": "Հեշտությամբ միացեք DApps-երին", "wc_max_network_fee": "Ցանցի առավելագույն վճար", "wc_message_to_sign": "Հաղորդագրություն ստորագրելու համար", "wc_network_fee": "Ցանցի վճար", @@ -1399,6 +1435,7 @@ "yat_popup_content": "Այժմ դուք կարող եք ուղարկել և ստանալ կրիպտո Cake Wallet-ում ձեր Yat-ի միջոցով՝ կարճ, էմոջի-ների վրա հիմնված օգտանունով։ Կառավարեք Yat-երը ցանկացած ժամանակ կարգավորումների էկրանին", "yat_popup_title": "Ձեր դրամապանակի հասցեն կարող է էմոջիֆիկացվել։", "yes": "Այո", + "yes_exit": "Այո։ Դուրս գալ", "yesterday": "Երեկ", "you_now_have_debit_card": "Այժմ դուք ունեք դեբետային քարտ", "you_pay": "Դուք վճարում եք", @@ -1451,4 +1488,4 @@ "wc_action_approve": "Հաստատել", "wc_desc": "Հեշտությամբ միացեք DApps-երին", "yes_exit": "Այո։ Դուրս գալ" -} \ No newline at end of file +} diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 877fac35f..29bed2a16 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -76,6 +76,7 @@ "approve_tokens": "Setujui token", "apps": "Aplikasi", "arbiscan_history": "Riwayat ArbiScan", + "are_you_sure_exit": "Apakah Anda yakin ingin keluar?", "arrive_in_this_address": "${currency} ${tag}akan masuk ke alamat ini", "ascending": "Urut naik", "ask_each_time": "Tanya setiap kali", @@ -89,6 +90,7 @@ "auto_generate_addresses": "Buat alamat secara otomatis", "auto_generate_subaddresses": "Buat subalamat secara otomatis", "automatic": "Otomatis", + "available": "Tersedia", "available_balance": "Saldo Tersedia", "available_balance_description": "“Saldo Tersedia” atau “Saldo Terkonfirmasi” adalah dana yang dapat langsung dibelanjakan. Jika dana muncul di saldo bawah tetapi tidak di saldo atas, maka Anda harus menunggu beberapa menit agar dana masuk mendapatkan konfirmasi jaringan tambahan. Setelah mendapatkan lebih banyak konfirmasi, dana tersebut akan dapat dibelanjakan.", "avg_savings": "Rata-rata Penghematan", @@ -125,6 +127,7 @@ "block_remaining": "1 Blok Tersisa", "Blocks_remaining": "${status} Blok Tersisa", "bluetooth": "Bluetooth", + "bridging": "Menjembatani", "bright_theme": "Terang", "bscscan_history": "Riwayat BSCScan", "bump_fee": "Naikkan biaya", @@ -216,6 +219,8 @@ "color_theme": "Tema warna", "coming_soon_tag": "Segera hadir", "commit_transaction_amount_fee": "Konfirmasi transaksi\nJumlah: ${amount}\nBiaya: ${fee}", + "compatible_services": "Layanan yang Kompatibel", + "compatible_services_desc": "Gunakan alat Pindai untuk membaca jenis kode QR berikut yang didukung di Cake Wallet:", "configure": "Konfigurasi", "confirm": "Konfirmasi", "confirm_delete_template": "Tindakan ini akan menghapus templat ini. Apakah Anda ingin melanjutkan?", @@ -279,6 +284,7 @@ "creating_new_wallet": "Membuat dompet baru", "creating_new_wallet_error": "Kesalahan: ${description}", "creation_date": "Tanggal Pembuatan", + "crypto_addresses": "Alamat Kripto", "csv_nothing_to_export": "Tidak ada yang dapat diekspor", "current_swap": "Swap Saat Ini", "current_wallet": "Dompet saat ini", @@ -301,6 +307,7 @@ "default_sell_provider": "Penyedia Jual Default", "delete": "Hapus", "delete_account": "Hapus Akun", + "delete_this_node": "Hapus node ini", "delete_wallet": "Hapus dompet", "delete_wallet_confirm_message": "Apakah Anda yakin ingin menghapus dompet ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Apakah Anda yakin ingin menghapus koneksi ke", @@ -330,6 +337,7 @@ "deuro_savings_welcome_description": "dEURO adalah stablecoin yang sepenuhnya terdesentralisasi dan dijaminkan, dipatok ke Euro — aman, serbaguna, dan tahan sensor tanpa harus bergantung pada oracle. 1 dEURO = 1 EURO.", "deuro_tooltip_no_eth": "Untuk mengaktifkan dEURO, Anda harus memiliki ETH di dompet Anda.", "deuro_tx_commited_content": "Mungkin perlu beberapa detik agar transaksi dikonfirmasi dan ditampilkan di layar", + "device_confirmation": "Konfirmasi Perangkat", "device_is_signing": "Perangkat sedang menandatangani", "dfx_option_description": "Beli kripto dengan EUR & CHF. Untuk pelanggan ritel dan korporat di Eropa", "did_you_back_up_seeds": "Apakah Anda sudah mencadangkan semua seed Anda?", @@ -466,6 +474,8 @@ "extracted_address_content": "Anda akan mengirim dana ke\n${recipient_name}", "failed_authentication": "Autentikasi gagal. ${state_error}", "faq": "FAQ", + "favorite_token": "Token favorit", + "favorite_token_desc": "Saldo token favorit akan ditampilkan di kartu saldo.", "features": "Fitur", "fee": "Biaya", "fee_less_than_min": "Biaya yang dipilih lebih rendah dari minimum, harap tingkatkan biaya agar dapat mengirim transaksi", @@ -491,6 +501,7 @@ "frozen": "Dibekukan", "frozen_balance": "Saldo Dibekukan", "full_balance": "Saldo Penuh", + "gallery": "Galeri", "gas_exceeds_allowance": "Gas yang diperlukan oleh transaksi melebihi batas yang diizinkan.", "gas_price": "Harga gas", "general": "Umum", @@ -526,10 +537,10 @@ "how_to_connect": "Cara Menghubungkan", "how_to_use": "Cara menggunakan", "how_to_use_card": "Cara menggunakan kartu ini", + "hww_exit_desc": "Ini akan mengakhiri proses pairing yang sedang berlangsung dengan perangkat keras Anda", "i_understand": "Saya mengerti", "icon_style": "Gaya ikon", "id": "ID: ", - "if_you_dont_see_your_device": "Jika Anda tidak melihat perangkat Anda di atas, pastikan Ledger Anda menyala dan tidak terkunci!", "ignor": "Abaikan", "import": "Impor", "importNFTs": "Impor NFT", @@ -541,6 +552,7 @@ "infobox_auto_address": "Alamat penerimaan Anda akan berganti setiap kali Anda menerima transaksi", "infobox_multichain": "Gunakan alamat ini untuk menerima token atau koleksi apa pun di", "infobox_rotation": "Alamat penerimaan Anda akan berubah setiap kali Anda menutup dan membuka kembali layar ini", + "input": "Input", "inputs": "Input", "insufficient_funds_for_tx": "Saldo tidak mencukupi untuk berhasil mengeksekusi transaksi.", "insufficient_lamport_for_tx": "Anda tidak memiliki cukup SOL untuk menutupi transaksi dan biaya transaksinya. Mohon tambahkan lebih banyak SOL ke dompet Anda atau kurangi jumlah SOL yang Anda kirim.", @@ -606,6 +618,7 @@ "manage_pow_nodes": "Kelola node PoW", "manage_providers": "Kelola penyedia", "manage_yats": "Kelola Yats", + "manual_input": "Input Manual", "mark_as_redeemed": "Tandai sebagai Ditukarkan", "market_place": "Marketplace", "mask": "Mask", @@ -651,6 +664,7 @@ "nanogpt_subtitle": "Semua model terbaru (GPT-5.3, Claude 4.6).\nTanpa langganan, bayar dengan kripto.", "narrow": "Sempit", "network": "Jaringan", + "networks": "Jaringan", "new_first_wallet_text": "Menjaga kripto Anda tetap aman itu semudah membalik telapak tangan", "new_node_testing": "Pengujian node baru", "new_subaddress_create": "Buat", @@ -679,8 +693,10 @@ "node_new": "Node Baru", "node_port": "Port node", "node_reset_settings_title": "Atur ulang pengaturan", + "node_share_desc": "Pindai kode QR ini dari layar Pindai untuk mengimpor node ini.", "node_test": "Uji", "nodes": "Node", + "nodes_desc": "Kelola node yang ada atau tambahkan node kustom.", "nodes_list_reset_to_default_message": "Apakah Anda yakin ingin mengatur ulang pengaturan ke pengaturan bawaan?", "none_of_selected_providers_can_exchange": "Tidak ada penyedia yang dipilih yang dapat melakukan swap ini", "noNFTYet": "Belum ada NFT", @@ -690,6 +706,7 @@ "note_tap_to_change": "Catatan (ketuk untuk mengubah)", "notification_permission_denied": "Izin notifikasi ditolak secara permanen, mohon aktifkan secara manual di pengaturan", "nullURIError": "URI bernilai null", + "ocp_desc": "Bayar dengan kripto di toko", "offer_expires_in": "Penawaran berakhir dalam: ", "offline": "Offline", "ok": "OK", @@ -718,8 +735,13 @@ "outputs": "Output", "overshot": "Melebihi", "overwrite_amount": "Timpa jumlah", + "pairing_code": "Kode Pairing", + "pairing_code_desc_1": "Perangkat Anda akan menampilkan kode pairing.", + "pairing_code_desc_2": "Masukkan di sini untuk melanjutkan", + "pairing_error": "Kesalahan Pairing", "pairingInvalidEvent": "Peristiwa Pairing Tidak Valid", "passphrase": "Frasa sandi (Opsional)", + "passphrase_view_keys": "Frasa sandi", "passphrases_doesnt_match": "Frasa sandi tidak cocok, silakan coba lagi", "password": "Kata Sandi", "paste": "Tempel", @@ -735,6 +757,7 @@ "payjoin_unavailable_sheet_content": "Menerima transaksi Payjoin mengharuskan Anda memiliki Bitcoin yang siap digunakan.\n\nBegitu dompet Anda memiliki dana, Payjoin akan otomatis diaktifkan.", "payjoin_unavailable_sheet_title": "Mengapa Payjoin tidak tersedia?", "payment_id": "ID Pembayaran: ", + "payment_invoices": "Faktur Pembayaran", "payment_made_easy": "Pembayaran jadi mudah", "payment_was_received": "Pembayaran Anda telah diterima.", "payments": "Pembayaran", @@ -891,6 +914,7 @@ "save": "Simpan", "save_backup_password": "Harap pastikan Anda telah menyimpan kata sandi cadangan Anda. Anda tidak akan dapat mengimpor file cadangan Anda tanpanya.", "save_backup_password_alert": "Simpan kata sandi cadangan", + "save_contact": "Simpan Kontak", "save_to_downloads": "Simpan ke Unduhan", "saved_the_trade_id": "Saya telah menyimpan ID transaksi", "savings": "Tabungan", @@ -1040,6 +1064,7 @@ "setup_your_debit_card": "Siapkan kartu debit Anda", "share": "Bagikan", "share_address": "Bagikan alamat", + "share_this_node": "Bagikan node ini", "shared_seed_wallet_groups": "Grup Dompet Seed Bersama", "shared_seed_wallets": "Dompet Seed Bersama", "show": "Tampilkan", @@ -1047,6 +1072,7 @@ "show_balance": "Tekan lama untuk menampilkan saldo", "show_balance_send_page": "Ketuk untuk menampilkan", "show_balance_toast": "Tekan lama untuk menyembunyikan atau menampilkan saldo", + "show_combined_balance": "Tampilkan saldo gabungan", "show_details": "Tampilkan Detail", "show_hidden_addresses": "Tampilkan alamat yang disembunyikan", "show_keys": "Tampilkan seed/kunci", @@ -1112,6 +1138,7 @@ "swap_providers": "Penyedia swap", "sweeping_wallet": "Dompet sweep", "sweeping_wallet_alert": "Ini seharusnya tidak memakan waktu lama. JANGAN TINGGALKAN LAYAR INI ATAU DANA YANG DISAPU DAPAT HILANG.", + "swipe_to_bridge": "Geser untuk menjembatani", "swipe_to_send": "Geser untuk mengirim", "switch_wallet": "Ganti Wallet", "switchToETHWallet": "Silakan beralih ke dompet Ethereum dan coba lagi", @@ -1152,6 +1179,7 @@ "testnet_coins_no_value": "Koin testnet tidak memiliki nilai", "third_intro_content": "Yat juga bisa digunakan di luar Cake Wallet. Alamat dompet apa pun di dunia dapat digantikan dengan Yat!", "third_intro_title": "Yat cocok digunakan dengan yang lain", + "this_can_take_few_seconds": "Ini dapat memakan waktu beberapa detik", "this_pair_is_not_supported_warning": "Pasangan ini tidak didukung oleh bursa yang saat ini dipilih. Silakan pilih bursa lain.", "thorchain_contract_address_not_supported": "THORChain tidak mendukung pengiriman ke alamat kontrak", "thorchain_taproot_address_not_supported": "Penyedia ThorChain tidak mendukung alamat Taproot. Harap ubah alamat tersebut atau pilih penyedia lain.", @@ -1165,6 +1193,7 @@ "token_decimal": "Desimal token (mis: USDT)", "token_name": "Nama token (mis: Tether)", "token_symbol": "Simbol token (mis: USDT)", + "token_verified_on_jupiter": "Token ini diverifikasi di Jupiter", "tokenID": "ID", "tokens": "Token", "tokens_strong_warning": "Jangan pernah mengirim token ke alamat ini!", @@ -1293,6 +1322,7 @@ "username": "Nama pengguna", "username_available": "Nama Pengguna Tersedia", "username_character_error": "Nama pengguna hanya boleh berisi", + "username_check_your_network": "Tidak dapat memeriksa nama pengguna. Silakan periksa koneksi jaringan Anda.", "username_length_error": "Nama pengguna harus terdiri dari 6-32 karakter alfanumerik.", "username_length_error_1_64": "Nama pengguna harus terdiri dari 1–64 karakter alfanumerik.", "username_not_available": "Nama pengguna ini tidak tersedia.", @@ -1303,6 +1333,7 @@ "verify_message": "Verifikasi Pesan", "verify_seed": "Verifikasi seed", "verify_with_2fa": "Verifikasi dengan Cake 2FA", + "verifying_code": "Memverifikasi Kode", "version": "Versi ${currentVersion}", "view_all": "Lihat semua", "view_in_block_explorer": "Lihat di Penjelajah Blok", @@ -1314,6 +1345,7 @@ "waitFewSecondForTxUpdate": "Mohon tunggu beberapa detik hingga transaksi muncul di riwayat transaksi", "wallet": "Dompet", "wallet_accounts": "Akun Dompet", + "wallet_balance": "Saldo Dompet", "wallet_group": "Grup Dompet", "wallet_group_description_existing_seed": "Anda telah memilih untuk menggunakan seed yang sudah ada untuk dompet ini. Anda dapat memverifikasi seed lagi jika perlu untuk mengonfirmasi atau mencatatnya.", "wallet_group_description_four": "untuk membuat dompet dengan seed yang sepenuhnya baru.", @@ -1351,9 +1383,11 @@ "walletConnect": "WalletConnect", "wallets": "Dompet", "warning": "Peringatan", + "wc_action_approve": "Setujui", "wc_approve_request_title": "Setujui permintaan", "wc_connect_request_title": "Hubungkan permintaan", "wc_connected_to": "Terhubung ke ${name}", + "wc_desc": "Hubungkan dengan mudah ke DApps", "wc_max_network_fee": "Biaya jaringan maksimal", "wc_message_to_sign": "Pesan untuk ditandatangani", "wc_network_fee": "Biaya jaringan", @@ -1404,6 +1438,7 @@ "yat_popup_content": "Anda kini dapat mengirim dan menerima kripto di Cake Wallet dengan Yat Anda—nama pengguna pendek berbasis emoji. Kelola Yat kapan saja di layar pengaturan", "yat_popup_title": "Alamat dompet Anda dapat diubah menjadi emoji.", "yes": "Ya", + "yes_exit": "Ya. Keluar", "yesterday": "Kemarin", "you_now_have_debit_card": "Anda sekarang memiliki kartu debit", "you_pay": "Anda Bayar", @@ -1454,4 +1489,4 @@ "wc_action_approve": "Setujui", "wc_desc": "Hubungkan dengan mudah ke DApps", "yes_exit": "Ya. Keluar" -} \ No newline at end of file +} diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 2bd228544..c0e2d1915 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -76,6 +76,7 @@ "approve_tokens": "Approva token", "apps": "App", "arbiscan_history": "Cronologia ArbiScan", + "are_you_sure_exit": "Sei sicuro di voler uscire?", "arrive_in_this_address": "${currency} ${tag}arriverà a questo indirizzo", "ascending": "Crescente", "ask_each_time": "Chiedi ogni volta", @@ -89,6 +90,7 @@ "auto_generate_addresses": "Genera automaticamente gli indirizzi", "auto_generate_subaddresses": "Genera automaticamente sottoindirizzi", "automatic": "Automatico", + "available": "Disponibile", "available_balance": "Saldo disponibile", "available_balance_description": "Il “Saldo disponibile” o “Saldo confermato” sono fondi che possono essere spesi immediatamente. Se i fondi compaiono nel saldo in basso ma non in quello in alto, devi attendere qualche minuto affinché i fondi in arrivo ottengano ulteriori conferme di rete. Dopo aver ricevuto più conferme, saranno spendibili.", "avg_savings": "Risparmio medio", @@ -125,6 +127,7 @@ "block_remaining": "1 blocco rimanente", "Blocks_remaining": "${status} blocchi rimanenti", "bluetooth": "Bluetooth", + "bridging": "Bridging in corso", "bright_theme": "Chiaro", "bscscan_history": "Cronologia BSCScan", "bump_fee": "Aumento commissione", @@ -216,6 +219,8 @@ "color_theme": "Tema colore", "coming_soon_tag": "Prossimamente", "commit_transaction_amount_fee": "Conferma transazione\nImporto: ${amount}\nCommissione: ${fee}", + "compatible_services": "Servizi compatibili", + "compatible_services_desc": "Usa lo strumento Scansione per leggere i seguenti tipi di codici QR supportati su Cake Wallet:", "configure": "Configura", "confirm": "Conferma", "confirm_delete_template": "Questa azione eliminerà questo modello. Desideri continuare?", @@ -279,6 +284,7 @@ "creating_new_wallet": "Creazione di un nuovo portafoglio", "creating_new_wallet_error": "Errore: ${description}", "creation_date": "Data di creazione", + "crypto_addresses": "Indirizzi crypto", "csv_nothing_to_export": "Niente da esportare", "current_swap": "Scambio in corso", "current_wallet": "Portafoglio corrente", @@ -301,6 +307,7 @@ "default_sell_provider": "Provider di vendita predefinito", "delete": "Elimina", "delete_account": "Elimina account", + "delete_this_node": "Elimina questo nodo", "delete_wallet": "Elimina wallet", "delete_wallet_confirm_message": "Sei sicuro di voler eliminare il wallet ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Sei sicuro di voler eliminare la connessione a", @@ -330,6 +337,7 @@ "deuro_savings_welcome_description": "dEURO è una stablecoin completamente decentralizzata e collateralizzata, ancorata all'euro — sicura, versatile e resistente alla censura senza dover fare affidamento su oracoli. 1 dEURO = 1 EURO.", "deuro_tooltip_no_eth": "Per abilitare dEURO, devi avere ETH nel tuo wallet.", "deuro_tx_commited_content": "Potrebbero essere necessari un paio di secondi affinché la transazione venga confermata e venga visualizzata sullo schermo", + "device_confirmation": "Conferma sul dispositivo", "device_is_signing": "Il dispositivo sta firmando", "dfx_option_description": "Acquista crypto con EUR e CHF. Per clienti retail e aziendali in Europa", "did_you_back_up_seeds": "Hai eseguito il backup di tutte le seed?", @@ -466,6 +474,8 @@ "extracted_address_content": "Invierai fondi a\n${recipient_name}", "failed_authentication": "Autenticazione non riuscita. ${state_error}", "faq": "FAQ", + "favorite_token": "Token preferito", + "favorite_token_desc": "Il saldo del token preferito verrà mostrato sulla scheda del saldo.", "features": "Funzionalità", "fee": "Commissione", "fee_less_than_min": "La commissione selezionata è inferiore al minimo. Aumenta la commissione per poter inviare la transazione", @@ -491,6 +501,7 @@ "frozen": "Congelato", "frozen_balance": "Saldo congelato", "full_balance": "Saldo totale", + "gallery": "Galleria", "gas_exceeds_allowance": "Il gas richiesto dalla transazione supera il limite consentito.", "gas_price": "Prezzo del gas", "general": "Generale", @@ -526,10 +537,10 @@ "how_to_connect": "Come connettersi", "how_to_use": "Come usare", "how_to_use_card": "Come usare questa carta", + "hww_exit_desc": "Questo terminerà il processo di associazione in corso con il tuo dispositivo hardware", "i_understand": "Ho capito", "icon_style": "Stile dell'icona", "id": "ID: ", - "if_you_dont_see_your_device": "Se non vedi il tuo dispositivo qui sopra, assicurati che il tuo Ledger sia acceso e sbloccato!", "ignor": "Ignora", "import": "Importa", "importNFTs": "Importa NFT", @@ -541,6 +552,7 @@ "infobox_auto_address": "Il tuo indirizzo di ricezione cambierà ogni volta che ricevi una transazione", "infobox_multichain": "Usa questo indirizzo per ricevere qualsiasi token o oggetto da collezione su", "infobox_rotation": "Il tuo indirizzo di ricezione cambierà ogni volta che chiudi e riapri questa schermata", + "input": "Input", "inputs": "Input", "insufficient_funds_for_tx": "Fondi insufficienti per eseguire correttamente la transazione.", "insufficient_lamport_for_tx": "Non hai abbastanza SOL per coprire la transazione e la relativa commissione. Aggiungi più SOL al tuo wallet oppure riduci l'importo di SOL che stai inviando.", @@ -606,6 +618,7 @@ "manage_pow_nodes": "Gestisci i nodi PoW", "manage_providers": "Gestisci i provider", "manage_yats": "Gestisci gli Yats", + "manual_input": "Inserimento manuale", "mark_as_redeemed": "Segna come riscattato", "market_place": "Marketplace", "mask": "Maschera", @@ -651,6 +664,7 @@ "nanogpt_subtitle": "Tutti i modelli più recenti (GPT-5.3, Claude 4.6).\nNessun abbonamento, paga con cripto.", "narrow": "Stretto", "network": "Rete", + "networks": "Reti", "new_first_wallet_text": "Tenere al sicuro le tue crypto è un gioco da ragazzi", "new_node_testing": "Test di un nuovo nodo", "new_subaddress_create": "Crea", @@ -679,8 +693,10 @@ "node_new": "Nuovo nodo", "node_port": "Porta del nodo", "node_reset_settings_title": "Ripristina le impostazioni", + "node_share_desc": "Scansiona questo codice QR dalla schermata Scansione per importare questo nodo.", "node_test": "Test", "nodes": "Nodi", + "nodes_desc": "Gestisci i nodi esistenti o aggiungine uno personalizzato.", "nodes_list_reset_to_default_message": "Sei sicuro di voler ripristinare le impostazioni predefinite?", "none_of_selected_providers_can_exchange": "Nessuno dei provider selezionati può effettuare questo swap", "noNFTYet": "Nessun NFT ancora", @@ -690,6 +706,7 @@ "note_tap_to_change": "Nota (tocca per cambiare)", "notification_permission_denied": "L'autorizzazione alle notifiche è stata negata in modo permanente, abilitala manualmente nelle impostazioni", "nullURIError": "L'URI è null", + "ocp_desc": "Paga con crypto nei negozi", "offer_expires_in": "L'offerta scade tra: ", "offline": "Offline", "ok": "OK", @@ -717,8 +734,13 @@ "outputs": "Output", "overshot": "Superato", "overwrite_amount": "Sovrascrivi importo", + "pairing_code": "Codice di associazione", + "pairing_code_desc_1": "Il tuo dispositivo mostrerà un codice di associazione.", + "pairing_code_desc_2": "Inseriscilo qui per continuare", + "pairing_error": "Errore di associazione", "pairingInvalidEvent": "Evento di associazione non valido", "passphrase": "Passphrase (Opzionale)", + "passphrase_view_keys": "Passphrase", "passphrases_doesnt_match": "Le passphrase non corrispondono, riprova", "password": "Password", "paste": "Incolla", @@ -734,6 +756,7 @@ "payjoin_unavailable_sheet_content": "Per ricevere una transazione Payjoin è necessario avere Bitcoin pronti all'uso.\n\nNon appena il tuo portafoglio avrà dei fondi, Payjoin verrà abilitato automaticamente.", "payjoin_unavailable_sheet_title": "Perché Payjoin non è disponibile?", "payment_id": "ID pagamento: ", + "payment_invoices": "Fatture di pagamento", "payment_made_easy": "Pagamenti resi semplici", "payment_was_received": "Il tuo pagamento è stato ricevuto.", "payments": "Pagamenti", @@ -813,6 +836,7 @@ "receive_to": "Ricevi su", "received": "Ricevuto", "receiving": "Ricezione", + "recieving": "Ricezione", "recipient_address": "Indirizzo del destinatario", "reconnect": "Riconnetti", "reconnect_alert_text": "Sei sicuro di voler riconnetterti?", @@ -888,6 +912,7 @@ "save": "Salva", "save_backup_password": "Assicurati di aver salvato la password di backup. Senza di essa non potrai importare i tuoi file di backup.", "save_backup_password_alert": "Salva la password di backup", + "save_contact": "Salva contatto", "save_to_downloads": "Salva in Download", "saved_the_trade_id": "Ho salvato l'ID della transazione", "savings": "Risparmi", @@ -1037,6 +1062,7 @@ "setup_your_debit_card": "Configura la tua carta di debito", "share": "Condividi", "share_address": "Condividi indirizzo", + "share_this_node": "Condividi questo nodo", "shared_seed_wallet_groups": "Gruppi di wallet con seed condiviso", "shared_seed_wallets": "Portafogli con seed condiviso", "show": "Mostra", @@ -1044,6 +1070,7 @@ "show_balance": "Tieni premuto per mostrare il saldo", "show_balance_send_page": "Tocca per mostrare", "show_balance_toast": "Tieni premuto per nascondere o mostrare il saldo", + "show_combined_balance": "Mostra saldo combinato", "show_details": "Mostra dettagli", "show_hidden_addresses": "Mostra indirizzi nascosti", "show_keys": "Mostra seed/chiavi", @@ -1109,6 +1136,7 @@ "swap_providers": "Provider di swap", "sweeping_wallet": "Svuotamento del portafoglio", "sweeping_wallet_alert": "Questo non dovrebbe richiedere molto tempo. NON LASCIARE QUESTA SCHERMATA O I FONDI SPOSTATI POTREBBERO ANDARE PERSI.", + "swipe_to_bridge": "Scorri per il bridge", "swipe_to_send": "Scorri per inviare", "switch_wallet": "Cambia wallet", "switchToETHWallet": "Passa a un wallet Ethereum e riprova", @@ -1149,6 +1177,7 @@ "testnet_coins_no_value": "Le monete di testnet non hanno valore", "third_intro_content": "Gli Yat esistono anche al di fuori di Cake Wallet. Qualsiasi indirizzo di portafoglio al mondo può essere sostituito con uno Yat!", "third_intro_title": "Yat va d'accordo con gli altri", + "this_can_take_few_seconds": "Questo può richiedere alcuni secondi", "this_pair_is_not_supported_warning": "Questa coppia non è supportata con l'exchange attualmente selezionato. Seleziona un altro exchange.", "thorchain_contract_address_not_supported": "THORChain non supporta l'invio a un indirizzo di contratto", "thorchain_taproot_address_not_supported": "Il provider ThorChain non supporta gli indirizzi Taproot. Modifica l'indirizzo o seleziona un provider diverso.", @@ -1162,6 +1191,7 @@ "token_decimal": "Decimali del token (es: USDT)", "token_name": "Nome del token (es: Tether)", "token_symbol": "Simbolo del token (es: USDT)", + "token_verified_on_jupiter": "Questo token è verificato su Jupiter", "tokenID": "ID", "tokens": "Token", "tokens_strong_warning": "Non inviare mai token a questo indirizzo!", @@ -1290,6 +1320,7 @@ "username": "Nome utente", "username_available": "Nome utente disponibile", "username_character_error": "I nomi utente devono contenere solo", + "username_check_your_network": "Impossibile verificare il nome utente. Controlla la tua connessione di rete.", "username_length_error": "I nomi utente devono contenere da 6 a 32 caratteri alfanumerici.", "username_length_error_1_64": "I nomi utente devono contenere 1-64 caratteri alfanumerici.", "username_not_available": "Questo nome utente non è disponibile.", @@ -1300,6 +1331,7 @@ "verify_message": "Verifica messaggio", "verify_seed": "Verifica il seed", "verify_with_2fa": "Verifica con Cake 2FA", + "verifying_code": "Verifica del codice in corso", "version": "Versione ${currentVersion}", "view_all": "Visualizza tutti", "view_in_block_explorer": "Visualizza nel Block Explorer", @@ -1312,6 +1344,7 @@ "waiting_payment_confirmation": "In attesa di conferma del pagamento", "wallet": "Wallet", "wallet_accounts": "Account del wallet", + "wallet_balance": "Saldo del wallet", "wallet_group": "Gruppo di wallet", "wallet_group_description_existing_seed": "Hai scelto di utilizzare un seed esistente per questo wallet. Puoi verificare di nuovo il seed se devi confermarlo o annotarlo.", "wallet_group_description_four": "per creare un wallet con un seed completamente nuovo.", @@ -1349,9 +1382,11 @@ "walletConnect": "WalletConnect", "wallets": "Portafogli", "warning": "Attenzione", + "wc_action_approve": "Approva", "wc_approve_request_title": "Approva la richiesta", "wc_connect_request_title": "Richiesta di connessione", "wc_connected_to": "Connesso a ${name}", + "wc_desc": "Connettiti facilmente alle dApp", "wc_max_network_fee": "Tariffa di rete massima", "wc_message_to_sign": "Messaggio da firmare", "wc_network_fee": "Tariffa di rete", @@ -1403,6 +1438,7 @@ "yat_popup_content": "Ora puoi inviare e ricevere criptovalute in Cake Wallet con il tuo Yat, un breve nome utente basato su emoji. Puoi gestire gli Yat in qualsiasi momento nella schermata delle impostazioni", "yat_popup_title": "L'indirizzo del tuo wallet può essere trasformato in emoji.", "yes": "Sì", + "yes_exit": "Sì. Esci", "yesterday": "Ieri", "you_now_have_debit_card": "Ora hai una carta di debito", "you_pay": "Tu paghi", @@ -1454,4 +1490,4 @@ "wc_action_approve": "Approva", "wc_desc": "Connettiti facilmente alle dApp", "yes_exit": "Sì. Esci" -} \ No newline at end of file +} diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 87f1193ca..a94ac6931 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -76,6 +76,7 @@ "approve_tokens": "トークンを承認", "apps": "アプリ", "arbiscan_history": "ArbiScan 履歴", + "are_you_sure_exit": "終了してもよろしいですか?", "arrive_in_this_address": "${currency} ${tag}はこのアドレスに着金します", "ascending": "昇順", "ask_each_time": "毎回確認する", @@ -89,6 +90,7 @@ "auto_generate_addresses": "アドレスを自動生成", "auto_generate_subaddresses": "サブアドレスを自動生成", "automatic": "自動", + "available": "利用可能", "available_balance": "利用可能残高", "available_balance_description": "「利用可能な残高」または「承認済み残高」は、すぐに使用できる資金です。下の残高には表示されているのに上の残高に表示されない場合は、受信した資金がネットワークでさらに承認(確認)されるまで数分待つ必要があります。承認数が増えると、使用できるようになります。", "avg_savings": "平均節約額", @@ -125,6 +127,7 @@ "block_remaining": "残り1ブロック", "Blocks_remaining": "${status} ブロック残り", "bluetooth": "Bluetooth", + "bridging": "ブリッジ中", "bright_theme": "明るい", "bscscan_history": "BSCScan履歴", "bump_fee": "手数料を上げる", @@ -216,6 +219,8 @@ "color_theme": "カラーテーマ", "coming_soon_tag": "近日公開", "commit_transaction_amount_fee": "トランザクションを確定\n金額: ${amount}\n手数料: ${fee}", + "compatible_services": "対応サービス", + "compatible_services_desc": "スキャンツールを使用して、Cake Walletで対応している以下の種類のQRコードを読み取れます:", "configure": "設定", "confirm": "確認", "confirm_delete_template": "この操作により、このテンプレートは削除されます。続行しますか?", @@ -279,6 +284,7 @@ "creating_new_wallet": "新しいウォレットを作成中", "creating_new_wallet_error": "エラー: ${description}", "creation_date": "作成日", + "crypto_addresses": "暗号資産アドレス", "csv_nothing_to_export": "エクスポートするものがありません", "current_swap": "現在のスワップ", "current_wallet": "現在のウォレット", @@ -301,6 +307,7 @@ "default_sell_provider": "デフォルトの売却プロバイダー", "delete": "削除", "delete_account": "アカウントを削除", + "delete_this_node": "このノードを削除", "delete_wallet": "ウォレットを削除", "delete_wallet_confirm_message": "${wallet_name} ウォレットを削除してもよろしいですか?", "deleteConnectionConfirmationPrompt": "への接続を削除してもよろしいですか?", @@ -330,6 +337,7 @@ "deuro_savings_welcome_description": "dEUROは、オラクルに頼ることなく、ユーロにペッグされた完全分散型の担保型ステーブルコインです。安全で汎用性が高く、検閲耐性を備えています。1 dEURO = 1 EURO。", "deuro_tooltip_no_eth": "dEUROを有効にするには、ウォレットにETHが必要です。", "deuro_tx_commited_content": "トランザクションが承認され、画面に反映されるまでに数秒かかる場合があります", + "device_confirmation": "デバイスでの確認", "device_is_signing": "デバイスが署名中です", "dfx_option_description": "EUR・CHFで暗号資産を購入。ヨーロッパの個人および法人のお客様向け", "did_you_back_up_seeds": "すべてのシードをバックアップしましたか?", @@ -466,6 +474,8 @@ "extracted_address_content": "次の宛先に送金します\n${recipient_name}", "failed_authentication": "認証に失敗しました。${state_error}", "faq": "FAQ", + "favorite_token": "お気に入りのトークン", + "favorite_token_desc": "お気に入りのトークンの残高が残高カードに表示されます。", "features": "機能", "fee": "手数料", "fee_less_than_min": "選択した手数料が最小値を下回っています。トランザクションを送信できるように、手数料を増やしてください", @@ -491,6 +501,7 @@ "frozen": "凍結", "frozen_balance": "凍結残高", "full_balance": "全残高", + "gallery": "ギャラリー", "gas_exceeds_allowance": "取引に必要なガスが許容上限を超えています。", "gas_price": "ガス価格", "general": "一般", @@ -527,10 +538,10 @@ "how_to_connect": "接続方法", "how_to_use": "使い方", "how_to_use_card": "このカードの使い方", + "hww_exit_desc": "ハードウェアデバイスとの進行中のペアリング処理を終了します", "i_understand": "理解しました", "icon_style": "アイコンのスタイル", "id": "ID: ", - "if_you_dont_see_your_device": "上記にデバイスが表示されない場合は、Ledgerが起動していてロック解除されていることを確認してください!", "ignor": "無視", "import": "インポート", "importNFTs": "NFTをインポート", @@ -542,6 +553,7 @@ "infobox_auto_address": "受信アドレスは、トランザクションを受信するたびに自動的に切り替わります", "infobox_multichain": "このアドレスを使用して、次のネットワーク上で任意のトークンまたはコレクティブルを受け取れます", "infobox_rotation": "この画面を閉じて再度開くたびに、受信アドレスがローテーションされます。", + "input": "入力", "inputs": "入力", "insufficient_funds_for_tx": "トランザクションを正常に実行するための残高が不足しています。", "insufficient_lamport_for_tx": "トランザクションおよびその取引手数料を支払うためのSOLが不足しています。ウォレットにSOLを追加するか、送信するSOLの量を減らしてください。", @@ -607,6 +619,7 @@ "manage_pow_nodes": "PoWノードを管理", "manage_providers": "プロバイダーを管理", "manage_yats": "Yatsを管理", + "manual_input": "手動入力", "mark_as_redeemed": "使用済みとしてマーク", "market_place": "マーケットプレイス", "mask": "マスク", @@ -652,6 +665,7 @@ "nanogpt_subtitle": "すべての最新モデル(GPT-5.3、Claude 4.6)。\nサブスクリプション不要、暗号資産で支払い。", "narrow": "狭い", "network": "ネットワーク", + "networks": "ネットワーク", "new_first_wallet_text": "暗号資産を安全に守るのはお手のものです", "new_node_testing": "新しいノードのテスト", "new_subaddress_create": "作成", @@ -680,8 +694,10 @@ "node_new": "新規ノード", "node_port": "ノードポート", "node_reset_settings_title": "設定をリセット", + "node_share_desc": "このノードをインポートするには、スキャン画面でこのQRコードをスキャンしてください。", "node_test": "テスト", "nodes": "ノード", + "nodes_desc": "既存のノードを管理するか、カスタムノードを追加します。", "nodes_list_reset_to_default_message": "設定をデフォルトにリセットしてもよろしいですか?", "none_of_selected_providers_can_exchange": "選択したプロバイダーはいずれもこのスワップを実行できません", "noNFTYet": "NFTはまだありません", @@ -691,6 +707,7 @@ "note_tap_to_change": "メモ(タップして変更)", "notification_permission_denied": "通知の権限が永久に拒否されました。設定で手動で有効にしてください", "nullURIError": "URIがnullです", + "ocp_desc": "店舗で暗号資産を使って支払う", "offer_expires_in": "オファーの有効期限: ", "offline": "オフライン", "ok": "OK", @@ -717,8 +734,13 @@ "outputs": "アウトプット", "overshot": "オーバーシュート", "overwrite_amount": "金額を上書き", + "pairing_code": "ペアリングコード", + "pairing_code_desc_1": "デバイスにペアリングコードが表示されます。", + "pairing_code_desc_2": "続行するにはここに入力してください", + "pairing_error": "ペアリングエラー", "pairingInvalidEvent": "無効なペアリングイベント", "passphrase": "パスフレーズ(任意)", + "passphrase_view_keys": "パスフレーズ", "passphrases_doesnt_match": "パスフレーズが一致しません。もう一度お試しください", "password": "パスワード", "paste": "貼り付け", @@ -734,6 +756,7 @@ "payjoin_unavailable_sheet_content": "Payjoinトランザクションを受信するには、使用可能なビットコインが必要です。\n\nウォレットに資金が入ると、Payjoinは自動的に有効になります。", "payjoin_unavailable_sheet_title": "Payjoinが利用できないのはなぜですか?", "payment_id": "支払いID: ", + "payment_invoices": "支払い請求書", "payment_made_easy": "支払いが簡単に", "payment_was_received": "お支払いを受領しました。", "payments": "支払い", @@ -813,6 +836,7 @@ "receive_to": "受取先", "received": "受信", "receiving": "受信", + "recieving": "受信中", "recipient_address": "受取人アドレス", "reconnect": "再接続", "reconnect_alert_text": "再接続してもよろしいですか?", @@ -888,6 +912,7 @@ "save": "保存", "save_backup_password": "バックアップパスワードを保存していることを必ず確認してください。これがないとバックアップファイルをインポートできません。", "save_backup_password_alert": "バックアップ用パスワードを保存", + "save_contact": "連絡先を保存", "save_to_downloads": "ダウンロードに保存", "saved_the_trade_id": "取引IDを保存しました", "savings": "貯蓄", @@ -1037,6 +1062,7 @@ "setup_your_debit_card": "デビットカードを設定する", "share": "共有", "share_address": "アドレスを共有", + "share_this_node": "このノードを共有", "shared_seed_wallet_groups": "共有シードのウォレットグループ", "shared_seed_wallets": "共有シードのウォレット", "show": "表示", @@ -1044,6 +1070,7 @@ "show_balance": "長押しして残高を表示", "show_balance_send_page": "タップして表示", "show_balance_toast": "長押しして残高を非表示/表示", + "show_combined_balance": "合計残高を表示", "show_details": "詳細を表示", "show_hidden_addresses": "非表示のアドレスを表示", "show_keys": "シード/キーを表示", @@ -1109,6 +1136,7 @@ "swap_providers": "スワッププロバイダー", "sweeping_wallet": "スイーピングウォレット", "sweeping_wallet_alert": "すぐに完了します。この画面から離れないでください。離れると、スイープした資金が失われる可能性があります。", + "swipe_to_bridge": "スワイプしてブリッジ", "swipe_to_send": "スワイプして送信", "switch_wallet": "ウォレットを切り替える", "switchToETHWallet": "Ethereumウォレットに切り替えて、もう一度お試しください", @@ -1149,6 +1177,7 @@ "testnet_coins_no_value": "テストネットコインには価値がありません", "third_intro_content": "YatはCake Walletの外でも使えます。地球上のどのウォレットアドレスもYatに置き換えられます!", "third_intro_title": "Yatは他のサービスとも相性がいい", + "this_can_take_few_seconds": "数秒かかる場合があります", "this_pair_is_not_supported_warning": "このペアは、現在選択されている取引所ではサポートされていません。別の取引所を選択してください。", "thorchain_contract_address_not_supported": "THORChainはコントラクトアドレスへの送信をサポートしていません", "thorchain_taproot_address_not_supported": "ThorChainプロバイダーはTaprootアドレスをサポートしていません。アドレスを変更するか、別のプロバイダーを選択してください。", @@ -1162,6 +1191,7 @@ "token_decimal": "トークンの小数点以下桁数(例:USDT)", "token_name": "トークン名(例:Tether)", "token_symbol": "トークンシンボル(例:USDT)", + "token_verified_on_jupiter": "このトークンは Jupiter で検証されています", "tokenID": "ID", "tokens": "トークン", "tokens_strong_warning": "このアドレスにトークンを絶対に送信しないでください!", @@ -1290,6 +1320,7 @@ "username": "ユーザー名", "username_available": "ユーザー名は利用可能です", "username_character_error": "ユーザー名には次の文字のみを含める必要があります", + "username_check_your_network": "ユーザー名を確認できません。ネットワーク接続を確認してください。", "username_length_error": "ユーザー名は6~32文字の英数字である必要があります。", "username_length_error_1_64": "ユーザー名は1~64文字の英数字である必要があります。", "username_not_available": "このユーザー名は利用できません。", @@ -1300,6 +1331,7 @@ "verify_message": "メッセージを検証", "verify_seed": "シードを確認", "verify_with_2fa": "Cake 2FAで認証する", + "verifying_code": "コードを確認中", "version": "バージョン ${currentVersion}", "view_all": "すべて表示", "view_in_block_explorer": "ブロックエクスプローラーで表示", @@ -1311,6 +1343,7 @@ "waitFewSecondForTxUpdate": "取引履歴に取引が反映されるまで、数秒お待ちください。", "wallet": "ウォレット", "wallet_accounts": "ウォレットアカウント", + "wallet_balance": "ウォレット残高", "wallet_group": "ウォレットグループ", "wallet_group_description_existing_seed": "このウォレットでは既存のシードを使用することを選択しました。確認したり書き留めたりする必要がある場合は、シードをもう一度確認できます。", "wallet_group_description_four": "まったく新しいシードでウォレットを作成するため。", @@ -1348,9 +1381,11 @@ "walletConnect": "WalletConnect", "wallets": "ウォレット", "warning": "警告", + "wc_action_approve": "承認", "wc_approve_request_title": "リクエストの承認", "wc_connect_request_title": "接続リクエスト", "wc_connected_to": "${name} に接続しました", + "wc_desc": "dApp に簡単に接続", "wc_max_network_fee": "最大ネットワーク料金", "wc_message_to_sign": "署名するメッセージ", "wc_network_fee": "ネットワーク料金", @@ -1401,6 +1436,7 @@ "yat_popup_content": "Yat(短い絵文字ベースのユーザー名)を使って、Cake Walletで暗号資産を送受信できるようになりました。設定画面でいつでもYatを管理できます", "yat_popup_title": "あなたのウォレットアドレスは絵文字化できます。", "yes": "はい", + "yes_exit": "はい。終了する", "yesterday": "昨日", "you_now_have_debit_card": "デビットカードを利用できるようになりました", "you_pay": "支払う金額", @@ -1452,4 +1488,4 @@ "wc_action_approve": "承認", "wc_desc": "dApp に簡単に接続", "yes_exit": "はい。終了する" -} \ No newline at end of file +} diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index b756745bb..4f74220a5 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -76,6 +76,7 @@ "approve_tokens": "토큰 승인", "apps": "앱", "arbiscan_history": "ArbiScan 기록", + "are_you_sure_exit": "정말 종료하시겠습니까?", "arrive_in_this_address": "${currency} ${tag}은(는) 이 주소로 입금됩니다", "ascending": "오름차순", "ask_each_time": "매번 확인", @@ -89,6 +90,7 @@ "auto_generate_addresses": "주소 자동 생성", "auto_generate_subaddresses": "서브주소 자동 생성", "automatic": "자동", + "available": "사용 가능", "available_balance": "사용 가능 잔액", "available_balance_description": "“사용 가능 잔액” 또는 “확정 잔액”은 즉시 사용할 수 있는 자금입니다. 자금이 아래 잔액에는 표시되지만 위 잔액에는 표시되지 않는 경우, 입금된 자금이 네트워크에서 더 많은 컨펌(확인)을 받을 때까지 몇 분 정도 기다려야 합니다. 더 많은 컨펌을 받은 후에는 사용할 수 있습니다.", "avg_savings": "평균 절감액", @@ -125,6 +127,7 @@ "block_remaining": "1 블록 남음", "Blocks_remaining": "${status}개 블록 남음", "bluetooth": "블루투스", + "bridging": "브리징", "bright_theme": "밝게", "bscscan_history": "BSCScan 기록", "bump_fee": "수수료 올리기", @@ -216,6 +219,8 @@ "color_theme": "색상 테마", "coming_soon_tag": "출시 예정", "commit_transaction_amount_fee": "트랜잭션 제출\n금액: ${amount}\n수수료: ${fee}", + "compatible_services": "호환 서비스", + "compatible_services_desc": "스캔 도구를 사용하여 Cake Wallet에서 지원하는 다음 유형의 QR 코드를 읽으세요:", "configure": "설정", "confirm": "확인", "confirm_delete_template": "이 작업을 수행하면 이 템플릿이 삭제됩니다. 계속하시겠습니까?", @@ -279,6 +284,7 @@ "creating_new_wallet": "새 지갑 생성 중", "creating_new_wallet_error": "오류: ${description}", "creation_date": "생성일", + "crypto_addresses": "암호화폐 주소", "csv_nothing_to_export": "내보낼 항목이 없습니다", "current_swap": "현재 스왑", "current_wallet": "현재 지갑", @@ -301,6 +307,7 @@ "default_sell_provider": "기본 판매 제공업체", "delete": "삭제", "delete_account": "계정 삭제", + "delete_this_node": "이 노드 삭제", "delete_wallet": "지갑 삭제", "delete_wallet_confirm_message": "${wallet_name} 지갑을 삭제하시겠습니까?", "deleteConnectionConfirmationPrompt": "다음에 대한 연결을 삭제하시겠습니까?", @@ -330,6 +337,7 @@ "deuro_savings_welcome_description": "dEURO는 유로에 페그된 완전한 탈중앙화 담보 기반 스테이블코인으로, 오라클에 의존하지 않으면서도 안전하고 다용도로 사용할 수 있으며 검열 저항성이 있습니다. 1 dEURO = 1 EURO.", "deuro_tooltip_no_eth": "dEURO를 활성화하려면 지갑에 ETH가 있어야 합니다.", "deuro_tx_commited_content": "거래가 확인되어 화면에 반영되기까지 몇 초 정도 걸릴 수 있습니다.", + "device_confirmation": "기기 확인", "device_is_signing": "장치가 서명하고 있습니다", "dfx_option_description": "EUR 및 CHF로 암호화폐 구매. 유럽의 개인 및 기업 고객용", "did_you_back_up_seeds": "모든 시드 문구를 백업하셨나요?", @@ -466,6 +474,8 @@ "extracted_address_content": "다음 수신자에게 자금을 전송합니다\n${recipient_name}", "failed_authentication": "인증에 실패했습니다. ${state_error}", "faq": "FAQ", + "favorite_token": "즐겨찾는 토큰", + "favorite_token_desc": "즐겨찾는 토큰의 잔액이 잔액 카드에 표시됩니다.", "features": "기능", "fee": "수수료", "fee_less_than_min": "선택한 수수료가 최소 수수료보다 낮습니다. 거래를 전송하려면 수수료를 높여주세요.", @@ -491,6 +501,7 @@ "frozen": "동결", "frozen_balance": "동결 잔액", "full_balance": "전체 잔액", + "gallery": "갤러리", "gas_exceeds_allowance": "트랜잭션에 필요한 가스가 허용량을 초과합니다.", "gas_price": "가스 가격", "general": "일반", @@ -526,10 +537,10 @@ "how_to_connect": "연결하는 방법", "how_to_use": "사용 방법", "how_to_use_card": "이 카드 사용 방법", + "hww_exit_desc": "하드웨어 기기와 진행 중인 페어링 과정을 종료합니다", "i_understand": "이해했습니다", "icon_style": "아이콘 스타일", "id": "ID: ", - "if_you_dont_see_your_device": "위에서 기기가 보이지 않으면 Ledger가 켜져 있고 잠금 해제되어 있는지 확인하세요!", "ignor": "무시", "import": "가져오기", "importNFTs": "NFT 가져오기", @@ -541,6 +552,7 @@ "infobox_auto_address": "수신 주소는 거래를 받을 때마다 변경됩니다.", "infobox_multichain": "이 주소를 사용하여 다음에서 어떤 토큰이나 수집품이든 받으세요", "infobox_rotation": "이 화면을 닫았다가 다시 열 때마다 수신 주소가 변경됩니다.", + "input": "입력", "inputs": "입력값", "insufficient_funds_for_tx": "트랜잭션을 성공적으로 실행하기에 잔액이 부족합니다.", "insufficient_lamport_for_tx": "트랜잭션 및 트랜잭션 수수료를 충당할 SOL이 부족합니다. 지갑에 SOL을 더 추가하거나 전송하는 SOL 금액을 줄여 주세요.", @@ -606,6 +618,7 @@ "manage_pow_nodes": "PoW 노드 관리", "manage_providers": "제공업체 관리", "manage_yats": "Yats 관리", + "manual_input": "수동 입력", "mark_as_redeemed": "사용 완료로 표시", "market_place": "마켓플레이스", "mask": "마스크", @@ -651,6 +664,7 @@ "nanogpt_subtitle": "최신 모델 모두(GPT-5.3, Claude 4.6).\n구독 없이 암호화폐로 결제.", "narrow": "좁게", "network": "네트워크", + "networks": "네트워크", "new_first_wallet_text": "암호화폐를 안전하게 지키는 건 식은 죽 먹기예요", "new_node_testing": "새 노드 테스트", "new_subaddress_create": "생성", @@ -679,8 +693,10 @@ "node_new": "새 노드", "node_port": "노드 포트", "node_reset_settings_title": "설정 재설정", + "node_share_desc": "이 노드를 가져오려면 스캔 화면에서 이 QR 코드를 스캔하세요.", "node_test": "테스트", "nodes": "노드", + "nodes_desc": "기존 노드를 관리하거나 사용자 지정 노드를 추가하세요.", "nodes_list_reset_to_default_message": "설정을 기본값으로 재설정하시겠습니까?", "none_of_selected_providers_can_exchange": "선택한 제공업체 중 이 스왑을 지원하는 곳이 없습니다", "noNFTYet": "아직 NFT가 없습니다", @@ -690,6 +706,7 @@ "note_tap_to_change": "메모 (탭하여 변경)", "notification_permission_denied": "알림 권한이 영구적으로 거부되었습니다. 설정에서 수동으로 활성화해 주세요.", "nullURIError": "URI가 null입니다", + "ocp_desc": "매장에서 암호화폐로 결제", "offer_expires_in": "오퍼 만료까지: ", "offline": "오프라인", "ok": "확인", @@ -716,6 +733,10 @@ "outputs": "출력", "overshot": "초과", "overwrite_amount": "금액 덮어쓰기", + "pairing_code": "페어링 코드", + "pairing_code_desc_1": "기기에 페어링 코드가 표시됩니다.", + "pairing_code_desc_2": "계속하려면 여기에 입력하세요", + "pairing_error": "페어링 오류", "pairingInvalidEvent": "유효하지 않은 페어링 이벤트", "passphrase": "패스프레이즈(선택 사항)", "passphrase_view_keys": "패스프레이즈", @@ -734,6 +755,7 @@ "payjoin_unavailable_sheet_content": "Payjoin 트랜잭션을 수신하려면 사용할 수 있는 비트코인이 준비되어 있어야 합니다.\n\n지갑에 자금이 들어오면 Payjoin이 자동으로 활성화됩니다.", "payjoin_unavailable_sheet_title": "Payjoin을 사용할 수 없는 이유는 무엇인가요?", "payment_id": "결제 ID: ", + "payment_invoices": "결제 청구서", "payment_made_easy": "결제를 쉽게", "payment_was_received": "결제가 수신되었습니다.", "payments": "결제", @@ -813,6 +835,7 @@ "receive_to": "받는 주소", "received": "수신됨", "receiving": "수신", + "recieving": "수신 중", "recipient_address": "수신자 주소", "reconnect": "재연결", "reconnect_alert_text": "정말 다시 연결하시겠습니까?", @@ -888,6 +911,7 @@ "save": "저장", "save_backup_password": "백업 비밀번호를 저장했는지 꼭 확인하세요. 비밀번호가 없으면 백업 파일을 가져올 수 없습니다.", "save_backup_password_alert": "백업 비밀번호 저장", + "save_contact": "연락처 저장", "save_to_downloads": "다운로드 폴더에 저장", "saved_the_trade_id": "거래 ID를 저장했어요", "savings": "저축", @@ -1037,6 +1061,7 @@ "setup_your_debit_card": "직불카드 설정", "share": "공유", "share_address": "주소 공유", + "share_this_node": "이 노드 공유", "shared_seed_wallet_groups": "공유 시드 지갑 그룹", "shared_seed_wallets": "공유 시드 지갑", "show": "보기", @@ -1044,6 +1069,7 @@ "show_balance": "길게 눌러 잔액 표시", "show_balance_send_page": "탭하여 표시", "show_balance_toast": "길게 눌러 잔액을 숨기거나 표시하세요", + "show_combined_balance": "합산 잔액 표시", "show_details": "세부 정보 보기", "show_hidden_addresses": "숨겨진 주소 표시", "show_keys": "시드/키 보기", @@ -1109,6 +1135,7 @@ "swap_providers": "스왑 제공업체", "sweeping_wallet": "지갑 스윕", "sweeping_wallet_alert": "오래 걸리지 않습니다. 이 화면을 떠나지 마세요. 그렇지 않으면 스윕된 자금이 손실될 수 있습니다.", + "swipe_to_bridge": "스와이프하여 브리지", "swipe_to_send": "밀어서 보내기", "switch_wallet": "지갑 전환", "switchToETHWallet": "이더리움 지갑으로 전환한 후 다시 시도하세요", @@ -1149,6 +1176,7 @@ "testnet_coins_no_value": "테스트넷 코인은 가치가 없습니다", "third_intro_content": "Yat은 Cake Wallet 밖에서도 사용할 수 있어요. 지구상의 어떤 지갑 주소든 Yat으로 대체할 수 있습니다!", "third_intro_title": "Yat은 다른 서비스와도 잘 어울립니다", + "this_can_take_few_seconds": "몇 초 정도 걸릴 수 있습니다", "this_pair_is_not_supported_warning": "현재 선택된 거래소에서는 이 페어가 지원되지 않습니다. 다른 거래소를 선택하세요.", "thorchain_contract_address_not_supported": "THORChain은 컨트랙트 주소로의 전송을 지원하지 않습니다", "thorchain_taproot_address_not_supported": "ThorChain 제공자는 Taproot 주소를 지원하지 않습니다. 주소를 변경하거나 다른 제공자를 선택하세요.", @@ -1162,6 +1190,7 @@ "token_decimal": "토큰 소수점(예: USDT)", "token_name": "토큰 이름(예: 테더)", "token_symbol": "토큰 심볼(예: USDT)", + "token_verified_on_jupiter": "이 토큰은 Jupiter에서 확인되었습니다.", "tokenID": "ID", "tokens": "토큰", "tokens_strong_warning": "절대 이 주소로 토큰을 보내지 마세요!", @@ -1290,6 +1319,7 @@ "username": "사용자명", "username_available": "사용 가능한 사용자 이름", "username_character_error": "사용자 이름에는 다음만 포함될 수 있습니다.", + "username_check_your_network": "사용자 이름을 확인할 수 없습니다. 네트워크 연결을 확인해 주세요.", "username_length_error": "사용자 이름은 6~32자의 영숫자로 구성되어야 합니다.", "username_length_error_1_64": "사용자 이름은 1~64자의 영숫자 문자로 구성되어야 합니다.", "username_not_available": "이 사용자 이름은 사용할 수 없습니다.", @@ -1300,6 +1330,7 @@ "verify_message": "메시지 확인", "verify_seed": "시드 문구 확인", "verify_with_2fa": "Cake 2FA로 인증", + "verifying_code": "코드 확인 중", "version": "버전 ${currentVersion}", "view_all": "모두 보기", "view_in_block_explorer": "블록 탐색기에서 보기", @@ -1311,6 +1342,7 @@ "waitFewSecondForTxUpdate": "트랜잭션이 거래 내역에 반영될 때까지 몇 초간 기다려 주십시오", "wallet": "지갑", "wallet_accounts": "지갑 계정", + "wallet_balance": "지갑 잔액", "wallet_group": "지갑 그룹", "wallet_group_description_existing_seed": "이 지갑에 기존 시드를 사용하도록 선택했습니다. 확인하거나 적어둘 필요가 있다면 시드를 다시 확인할 수 있습니다.", "wallet_group_description_four": "완전히 새로운 시드로 지갑을 생성하려면", @@ -1348,9 +1380,11 @@ "walletConnect": "WalletConnect", "wallets": "지갑", "warning": "경고", + "wc_action_approve": "승인", "wc_approve_request_title": "요청 승인", "wc_connect_request_title": "연결 요청", "wc_connected_to": "${name}에 연결됨", + "wc_desc": "DApp에 손쉽게 연결", "wc_max_network_fee": "최대 네트워크 수수료", "wc_message_to_sign": "서명하라는 메시지", "wc_network_fee": "네트워크 수수료", @@ -1401,6 +1435,7 @@ "yat_popup_content": "이제 짧은 이모지 기반 사용자 이름인 Yat으로 Cake Wallet에서 암호화폐를 보내고 받을 수 있습니다. 설정 화면에서 언제든지 Yat을 관리할 수 있습니다.", "yat_popup_title": "지갑 주소를 이모지로 변환할 수 있습니다.", "yes": "예", + "yes_exit": "예. 종료", "yesterday": "어제", "you_now_have_debit_card": "이제 직불카드가 생겼습니다", "you_pay": "결제 금액", @@ -1451,4 +1486,4 @@ "wc_action_approve": "승인", "wc_desc": "DApp에 손쉽게 연결", "yes_exit": "예. 종료" -} \ No newline at end of file +} diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 51f7c231f..bde9bfa6a 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -76,6 +76,7 @@ "approve_tokens": "တိုကင်များကို အတည်ပြုပါ", "apps": "အက်ပ်များ", "arbiscan_history": "ArbiScan မှတ်တမ်း", + "are_you_sure_exit": "ထွက်လိုသည်မှာ သေချာပါသလား။", "arrive_in_this_address": "${currency} ${tag}သည် ဤလိပ်စာသို့ ရောက်ရှိပါမည်", "ascending": "အတက်", "ask_each_time": "အကြိမ်တိုင်း မေးပါ", @@ -89,6 +90,7 @@ "auto_generate_addresses": "လိပ်စာများကို အလိုအလျောက် ထုတ်ပေးရန်", "auto_generate_subaddresses": "Subaddress များကို အလိုအလျောက် ဖန်တီးပါ", "automatic": "အလိုအလျောက်", + "available": "ရရှိနိုင်သည်", "available_balance": "ရရှိနိုင်သော လက်ကျန်", "available_balance_description": "“Available Balance” သို့မဟုတ် “Confirmed Balance” သည် ချက်ချင်းသုံးစွဲနိုင်သော လက်ကျန်ငွေဖြစ်သည်။ လက်ကျန်အောက်ပိုင်းတွင်သာ ဝင်လာသော်လည်း အပေါ်ပိုင်းလက်ကျန်တွင် မပေါ်သေးပါက ကွန်ယက်အတည်ပြုချက် (network confirmations) ပိုမိုရရှိရန် မိနစ်အနည်းငယ် စောင့်ရပါမည်။ အတည်ပြုချက်များ ပိုမိုရရှိပြီးနောက် ထိုငွေများကို သုံးစွဲနိုင်ပါလိမ့်မည်။", "avg_savings": "ပျမ်းမျှ ချွေတာမှု", @@ -125,6 +127,7 @@ "block_remaining": "Block 1 ခု ကျန်ရှိသေးသည်", "Blocks_remaining": "${status} ဘလောက် ကျန်နေပါသည်", "bluetooth": "ဘလူးတုသ်", + "bridging": "Bridge လုပ်နေသည်", "bright_theme": "တောက်ပ", "bscscan_history": "BSCScan မှတ်တမ်း", "bump_fee": "အခကြေးငွေ တိုးမြှင့်ရန်", @@ -216,6 +219,8 @@ "color_theme": "အရောင်အပြင်အဆင်", "coming_soon_tag": "မကြာမီလာမည်", "commit_transaction_amount_fee": "ငွေလွှဲမှုကို အတည်ပြုပါ\nပမာဏ: ${amount}\nအခကြေးငွေ: ${fee}", + "compatible_services": "တွဲဖက်အသုံးပြုနိုင်သော ဝန်ဆောင်မှုများ", + "compatible_services_desc": "Cake Wallet တွင် ပံ့ပိုးထားသော အောက်ပါ QR ကုဒ်အမျိုးအစားများကို ဖတ်ရန် Scan tool ကို အသုံးပြုပါ:", "configure": "ပြင်ဆင်ရန်", "confirm": "အတည်ပြု", "confirm_delete_template": "ဤလုပ်ဆောင်ချက်သည် ဤတမ်းပလိတ်ကို ဖျက်ပစ်မည်ဖြစ်သည်။ ဆက်လက်လုပ်ဆောင်လိုပါသလား။", @@ -279,6 +284,7 @@ "creating_new_wallet": "ပိုက်ဆံအိတ်အသစ် ဖန်တီးနေသည်", "creating_new_wallet_error": "အမှား: ${description}", "creation_date": "ဖန်တီးသည့်နေ့စွဲ", + "crypto_addresses": "Crypto လိပ်စာများ", "csv_nothing_to_export": "ထုတ်ယူရန် မရှိပါ", "current_swap": "လက်ရှိ Swap", "current_wallet": "လက်ရှိ Wallet", @@ -301,6 +307,7 @@ "default_sell_provider": "မူလရောင်းချဝန်ဆောင်မှုပေးသူ", "delete": "ဖျက်ရန်", "delete_account": "အကောင့်ကို ဖျက်ပါ", + "delete_this_node": "ဤ node ကို ဖျက်ပါ", "delete_wallet": "ပိုက်ဆံအိတ်ကို ဖျက်မည်", "delete_wallet_confirm_message": "${wallet_name} wallet ကို ဖျက်ချင်တာ သေချာပါသလား။", "deleteConnectionConfirmationPrompt": "ဤချိတ်ဆက်မှုကို ဖျက်လိုသည်မှာ သေချာပါသလား", @@ -330,6 +337,7 @@ "deuro_savings_welcome_description": "dEURO သည် ယူရိုနှင့် ချိတ်ဆက်ထားသော အပြည့်အဝ ဗဟိုချုပ်ကိုင်မှုမရှိသော အာမခံပစ္စည်းအခြေပြု stablecoin တစ်ခုဖြစ်ပြီး — လုံခြုံမှုရှိ၊ အသုံးချနိုင်မှုမြင့်ပြီး၊ censorship ကိုခံနိုင်ရည်ရှိကာ oracles များကို မမှီခိုဘဲ လုပ်ဆောင်နိုင်သည်။ 1 dEURO = 1 EURO။", "deuro_tooltip_no_eth": "dEURO ကို enable လုပ်ရန် သင့်ပိုက်ဆံအိတ်ထဲမှာ ETH ရှိရမယ်။", "deuro_tx_commited_content": "ငွေပေးငွေယူကို အတည်ပြုရန်နှင့် မျက်နှာပြင်ပေါ်တွင် ပြသလာရန် စက္ကန့်အနည်းငယ် ကြာနိုင်ပါသည်", + "device_confirmation": "စက်ပစ္စည်း အတည်ပြုခြင်း", "device_is_signing": "ကိရိယာက လက်မှတ်ရေးထိုးနေသည်", "dfx_option_description": "EUR & CHF ဖြင့် crypto ကို ဝယ်ယူပါ။ ဥရောပရှိ လက်လီနှင့် ကော်ပိုရိတ် ဖောက်သည်များအတွက်", "did_you_back_up_seeds": "သင့် Seed စကားလုံးများအားလုံးကို အရန်ကူးယူပြီးပြီလား?", @@ -466,6 +474,8 @@ "extracted_address_content": "သင်သည် ရန်ပုံငွေများကို\n${recipient_name} သို့ ပေးပို့ပါမည်", "failed_authentication": "အထောက်အထားစိစစ်ခြင်း မအောင်မြင်ပါ။ ${state_error}", "faq": "FAQ", + "favorite_token": "အကြိုက်ဆုံး တိုကင်", + "favorite_token_desc": "အကြိုက်ဆုံး တိုကင်၏ လက်ကျန်ကို လက်ကျန် ကတ်ပေါ်တွင် ပြသပါမည်။", "features": "အင်္ဂါရပ်များ", "fee": "အခကြေးငွေ", "fee_less_than_min": "ရွေးချယ်ထားသော အခကြေးငွေသည် အနည်းဆုံးသတ်မှတ်ချက်ထက် နည်းနေပါသည်။ လွှဲပြောင်းမှုကို ပို့နိုင်ရန် အခကြေးငွေကို တိုးမြှင့်ပါ။", @@ -491,6 +501,7 @@ "frozen": "ရပ်ဆိုင်းထားသည်", "frozen_balance": "အေးခဲထားသော လက်ကျန်", "full_balance": "လက်ကျန်ငွေ အပြည့်", + "gallery": "ပြခန်း", "gas_exceeds_allowance": "ငွေပေးငွေယူအတွက် လိုအပ်သော Gas ပမာဏသည် ခွင့်ပြုထားသော ပမာဏကို ကျော်လွန်နေသည်။", "gas_price": "ဂတ်စ်စျေးနှုန်း", "general": "အထွေထွေ", @@ -526,10 +537,10 @@ "how_to_connect": "ချိတ်ဆက်နည်း", "how_to_use": "အသုံးပြုနည်း", "how_to_use_card": "ဒီကတ်ကို ဘယ်လိုသုံးရမလဲ", + "hww_exit_desc": "၎င်းသည် သင့်ဟာ့ဒ်ဝဲ စက်ပစ္စည်းနှင့် ဆက်လက်လုပ်ဆောင်နေသော တွဲချိတ်မှု လုပ်ငန်းစဉ်ကို ရပ်တန့်စေမည်ဖြစ်သည်။", "i_understand": "နားလည်ပါတယ်", "icon_style": "အိုင်ကွန်စတိုင်", "id": "ID: ", - "if_you_dont_see_your_device": "အထက်မှာ သင့်စက်ကို မတွေ့ပါက သင့် Ledger ကို ဖွင့်ထားပြီး လော့ခ်ဖွင့်ထားကြောင်း သေချာပါစေ။", "ignor": "လျစ်လျူရှုပါ", "import": "တင်သွင်း", "importNFTs": "NFT များကို တင်သွင်းပါ", @@ -541,6 +552,7 @@ "infobox_auto_address": "သင့်လက်ခံရရှိရန် လိပ်စာသည် ငွေပေးငွေယူတစ်ခု လက်ခံရရှိတိုင်း အလိုအလျောက် ပြောင်းလဲသွားမည်ဖြစ်သည်။", "infobox_multichain": "မည်သည့် တိုကင် သို့မဟုတ် စုဆောင်းပစ္စည်းကိုမဆို လက်ခံရန် ဤလိပ်စာကို အသုံးပြုပါ။", "infobox_rotation": "သင် ဤစခရင်ကို ပိတ်ပြီး ပြန်ဖွင့်သည့်အခါတိုင်း သင့်လက်ခံရရှိရန် လိပ်စာသည် ပြောင်းလဲသွားပါမည်။", + "input": "ထည့်သွင်းမှု", "inputs": "အဝင်များ", "insufficient_funds_for_tx": "ငွေပေးငွေယူကို အောင်မြင်စွာ ဆောင်ရွက်ရန် လက်ကျန်ငွေ မလုံလောက်ပါ။", "insufficient_lamport_for_tx": "ငွေပေးငွေယူနှင့် ၎င်း၏ ငွေပေးငွေယူခကို ပေးချေရန် SOL မလုံလောက်ပါ။ ကျေးဇူးပြု၍ သင့် wallet ထဲသို့ SOL ကို ထပ်မံထည့်ပါ သို့မဟုတ် သင်ပို့မည့် SOL ပမာဏကို လျှော့ချပါ။", @@ -606,6 +618,7 @@ "manage_pow_nodes": "PoW node များကို စီမံပါ", "manage_providers": "ဝန်ဆောင်မှုပေးသူများကို စီမံခန့်ခွဲပါ", "manage_yats": "Yats ကို စီမံခန့်ခွဲပါ", + "manual_input": "လက်ဖြင့် ထည့်သွင်းခြင်း", "mark_as_redeemed": "အသုံးပြုပြီးဖြစ်သည်ဟု အမှတ်အသားပြုပါ", "market_place": "စျေးကွက်", "mask": "Mask", @@ -651,6 +664,7 @@ "nanogpt_subtitle": "နောက်ဆုံးပေါ်မော်ဒယ်များအားလုံး (GPT-5.3, Claude 4.6)။\nစာရင်းသွင်းမှုမလို၊ crypto ဖြင့်ပေးချေပါ။", "narrow": "ကျဉ်း", "network": "ကွန်ရက်", + "networks": "ကွန်ရက်များ", "new_first_wallet_text": "သင့်ရဲ့ crypto ကို လုံခြုံအောင် ထိန်းသိမ်းတာက လွယ်လွယ်ကူကူပါပဲ", "new_node_testing": "နိုဒ်အသစ် စမ်းသပ်ခြင်း", "new_subaddress_create": "ဖန်တီးရန်", @@ -679,8 +693,10 @@ "node_new": "နိုဒ်အသစ်", "node_port": "Node ပေါက်", "node_reset_settings_title": "ဆက်တင်များကို ပြန်လည်သတ်မှတ်ပါ", + "node_share_desc": "ဤ node ကို တင်သွင်းရန် Scan စခရင်မှ ဤ QR ကုဒ်ကို စကင်ဖတ်ပါ။", "node_test": "စမ်းသပ်", "nodes": "နိုဒ်များ", + "nodes_desc": "ရှိပြီးသား node များကို စီမံပါ သို့မဟုတ် စိတ်ကြိုက်တစ်ခု ထည့်ပါ။", "nodes_list_reset_to_default_message": "ဆက်တင်များကို မူရင်းအတိုင်း ပြန်လည်သတ်မှတ်လိုသည်မှာ သေချာပါသလား။", "none_of_selected_providers_can_exchange": "ရွေးချယ်ထားသော ပံ့ပိုးပေးသူများထဲမှ မည်သူမျှ ဒီ swap ကို မလုပ်နိုင်ပါ", "noNFTYet": "NFT မရှိသေးပါ", @@ -690,6 +706,7 @@ "note_tap_to_change": "မှတ်ချက် (ပြောင်းလဲရန် နှိပ်ပါ)", "notification_permission_denied": "အသိပေးချက်ခွင့်ပြုချက်ကို အပြီးတိုင် ငြင်းပယ်ထားပါသည်၊ ကျေးဇူးပြု၍ ချိန်ညှိချက်များတွင် လက်ဖြင့် ဖွင့်ပေးပါ", "nullURIError": "URI သည် null ဖြစ်သည်", + "ocp_desc": "ဆိုင်များတွင် crypto ဖြင့် ပေးချေပါ", "offer_expires_in": "ကမ်းလှမ်းချက် သက်တမ်းကုန်ဆုံးမည့်အချိန်: ", "offline": "အော့ဖ်လိုင်း", "ok": "OK", @@ -716,8 +733,13 @@ "outputs": "အထွက်များ", "overshot": "ကျော်လွန်သွားသည်", "overwrite_amount": "ပမာဏကို အစားထိုးရေးပါ", + "pairing_code": "တွဲချိတ်ကုဒ်", + "pairing_code_desc_1": "သင့်စက်ပစ္စည်းသည် တွဲချိတ်ကုဒ်တစ်ခုကို ပြသပါမည်။", + "pairing_code_desc_2": "ဆက်လက်လုပ်ဆောင်ရန် ဤနေရာတွင် ထည့်ပါ", + "pairing_error": "တွဲချိတ်မှု အမှား", "pairingInvalidEvent": "မမှန်ကန်သော တွဲချိတ်မှု ဖြစ်ရပ်", "passphrase": "စကားဝှက်စာစု (မဖြစ်မနေ မလိုအပ်ပါ)", + "passphrase_view_keys": "Passphrase", "passphrases_doesnt_match": "Passphrase များ မကိုက်ညီပါ၊ ကျေးဇူးပြု၍ ထပ်မံကြိုးစားပါ", "password": "စကားဝှက်", "paste": "ကူးထည့်", @@ -733,6 +755,7 @@ "payjoin_unavailable_sheet_content": "Payjoin လွှဲပြောင်းမှုကို လက်ခံရန် သင့်တွင် အသုံးပြုရန် အသင့်ဖြစ်သော Bitcoin ရှိထားရန် လိုအပ်သည်။\n\nသင့်ပိုက်ဆံအိတ်တွင် ရန်ပုံငွေများ ရှိလာသည်နှင့်တပြိုင်နက် Payjoin ကို အလိုအလျောက် ဖွင့်ပေးမည်ဖြစ်သည်။", "payjoin_unavailable_sheet_title": "Payjoin ကို ဘာကြောင့် မရနိုင်တာလဲ။", "payment_id": "Payment ID: ", + "payment_invoices": "ငွေပေးချေမှု ငွေတောင်းခံလွှာများ", "payment_made_easy": "ငွေပေးချေမှုကို လွယ်ကူစေသည်", "payment_was_received": "သင့်ငွေပေးချေမှုကို လက်ခံရရှိပါသည်။", "payments": "ပေးချေမှုများ", @@ -812,6 +835,7 @@ "receive_to": "သို့ လက်ခံရန်", "received": "လက်ခံရရှိ", "receiving": "လက်ခံနေသည်", + "recieving": "လက်ခံနေသည်", "recipient_address": "လက်ခံသူ၏ လိပ်စာ", "reconnect": "ပြန်လည်ချိတ်ဆက်ရန်", "reconnect_alert_text": "ပြန်လည်ချိတ်ဆက်လိုသည်မှာ သေချာပါသလား?", @@ -887,6 +911,7 @@ "save": "သိမ်းဆည်း", "save_backup_password": "သင်၏ အရန်စကားဝှက်ကို သိမ်းဆည်းထားပြီးဖြစ်ကြောင်း သေချာပါစေ။ ၎င်းမရှိပါက သင်၏ အရန်ဖိုင်များကို တင်သွင်းနိုင်မည် မဟုတ်ပါ။", "save_backup_password_alert": "အရန်ကူးယူမှု စကားဝှက်ကို သိမ်းဆည်းပါ", + "save_contact": "အဆက်အသွယ်ကို သိမ်းဆည်းပါ", "save_to_downloads": "ဒေါင်းလုဒ်များသို့ သိမ်းဆည်းပါ", "saved_the_trade_id": "ကုန်သွယ်မှု ID ကို သိမ်းဆည်းထားပြီးပါပြီ။", "savings": "စုဆောင်းငွေ", @@ -1036,6 +1061,7 @@ "setup_your_debit_card": "သင့်ဒက်ဘစ်ကတ်ကို စနစ်ထည့်သွင်းပါ", "share": "မျှဝေ", "share_address": "လိပ်စာကို မျှဝေပါ", + "share_this_node": "ဤ node ကို မျှဝေပါ", "shared_seed_wallet_groups": "မျှဝေထားသော Seed ပိုက်ဆံအိတ်အုပ်စုများ", "shared_seed_wallets": "မျှဝေထားသော Seed Wallet များ", "show": "ပြပါ", @@ -1043,6 +1069,7 @@ "show_balance": "လက်ကျန်ငွေကို ပြရန် ကြာကြာနှိပ်ပါ", "show_balance_send_page": "ဖော်ပြရန် နှိပ်ပါ", "show_balance_toast": "လက်ချောင်းကို ကြာကြာဖိထားပြီး လက်ကျန်ငွေကို ဖျောက်/ပြပါ", + "show_combined_balance": "ပေါင်းစပ်လက်ကျန်ကို ပြပါ", "show_details": "အသေးစိတ်ပြရန်", "show_hidden_addresses": "ဝှက်ထားသောလိပ်စာများကို ပြပါ", "show_keys": "Seed/Key များကို ပြပါ", @@ -1108,6 +1135,7 @@ "swap_providers": "Swap ဝန်ဆောင်မှုပေးသူများ", "sweeping_wallet": "ပိုက်ဆံအိတ် ရှင်းလင်းခြင်း", "sweeping_wallet_alert": "ဒါက ကြာကြာမခံသင့်ပါဘူး။ ဒီစခရင်ကနေ မထွက်ပါနဲ့၊ မဟုတ်ရင် sweep လုပ်ထားတဲ့ ရန်ပုံငွေတွေ ပျောက်ဆုံးနိုင်ပါတယ်။", + "swipe_to_bridge": "Bridge လုပ်ရန် ပွတ်ဆွဲပါ", "swipe_to_send": "ပို့ရန် ပွတ်ဆွဲပါ", "switch_wallet": "ပိုက်ဆံအိတ် ပြောင်းရန်", "switchToETHWallet": "ကျေးဇူးပြု၍ Ethereum ပိုက်ဆံအိတ်သို့ ပြောင်းပြီး ထပ်မံကြိုးစားကြည့်ပါ။", @@ -1148,6 +1176,7 @@ "testnet_coins_no_value": "Testnet ဒင်္ဂါးများမှာ တန်ဖိုးမရှိပါ", "third_intro_content": "Yats သည် Cake Wallet အပြင်ဘက်တွင်လည်း အသုံးပြုနိုင်ပါသည်။ ကမ္ဘာပေါ်ရှိ မည်သည့် wallet address မဆို Yat ဖြင့် အစားထိုးနိုင်ပါသည်။", "third_intro_title": "Yat သည် အခြားဝန်ဆောင်မှုများနှင့်လည်း ကောင်းစွာ တွဲဖက်အသုံးပြုနိုင်သည်။", + "this_can_take_few_seconds": "၎င်းသည် စက္ကန့်အနည်းငယ် ကြာနိုင်ပါသည်", "this_pair_is_not_supported_warning": "ဤအတွဲကို လက်ရှိရွေးချယ်ထားသော လဲလှယ်ရေး(များ) ဖြင့် မပံ့ပိုးပါ။ ကျေးဇူးပြု၍ အခြား လဲလှယ်ရေးကို ရွေးချယ်ပါ။", "thorchain_contract_address_not_supported": "THORChain သည် စာချုပ်လိပ်စာသို့ ပို့ခြင်းကို မပံ့ပိုးပါ", "thorchain_taproot_address_not_supported": "ThorChain ပံ့ပိုးပေးသူသည် Taproot လိပ်စာများကို မထောက်ပံ့ပါ။ ကျေးဇူးပြု၍ လိပ်စာကို ပြောင်းလဲပါ သို့မဟုတ် အခြားပံ့ပိုးပေးသူကို ရွေးချယ်ပါ။", @@ -1161,6 +1190,7 @@ "token_decimal": "တိုကင် ဒဿမ (ဥပမာ: USDT)", "token_name": "တိုကင်အမည် (ဥပမာ: Tether)", "token_symbol": "တိုကင်သင်္ကေတ (ဥပမာ: USDT)", + "token_verified_on_jupiter": "ဤတိုကင်ကို Jupiter တွင် အတည်ပြုထားသည်။", "tokenID": "ID", "tokens": "တိုကင်များ", "tokens_strong_warning": "ဒီလိပ်စာသို့ တိုကင်များကို ဘယ်တော့မှ မပို့ပါနဲ့!", @@ -1289,6 +1319,7 @@ "username": "အသုံးပြုသူအမည်", "username_available": "အသုံးပြုသူအမည် ရနိုင်သည်", "username_character_error": "အသုံးပြုသူအမည်များတွင်သာ ပါဝင်ရမည်", + "username_check_your_network": "အသုံးပြုသူအမည်ကို စစ်ဆေး၍ မရပါ။ ကျေးဇူးပြု၍ သင့်ကွန်ရက်ချိတ်ဆက်မှုကို စစ်ဆေးပါ။", "username_length_error": "အသုံးပြုသူအမည်များတွင် အက္ခရာနှင့် ဂဏန်း ၆-၃၂ လုံး ပါဝင်ရပါမည်။", "username_length_error_1_64": "အသုံးပြုသူအမည်များတွင် အက္ခရာနှင့် ဂဏန်း စာလုံး 1-64 လုံး ပါဝင်ရပါမည်။", "username_not_available": "ဤအသုံးပြုသူအမည်ကို မရနိုင်ပါ။", @@ -1299,6 +1330,7 @@ "verify_message": "မက်ဆေ့ချ်ကို အတည်ပြုရန်", "verify_seed": "Seed ကို အတည်ပြုပါ", "verify_with_2fa": "Cake 2FA ဖြင့် အတည်ပြုပါ", + "verifying_code": "ကုဒ်ကို စိစစ်နေသည်", "version": "ဗားရှင်း ${currentVersion}", "view_all": "အားလုံးကြည့်ရန်", "view_in_block_explorer": "Block Explorer တွင် ကြည့်ရှုပါ", @@ -1310,6 +1342,7 @@ "waitFewSecondForTxUpdate": "ငွေပေးငွေယူမှတ်တမ်းတွင် ပြသလာရန် စက္ကန့်အနည်းငယ် စောင့်ပါ။", "wallet": "ပိုက်ဆံအိတ်", "wallet_accounts": "Wallet အကောင့်များ", + "wallet_balance": "ပိုက်ဆံအိတ် လက်ကျန်", "wallet_group": "ပိုက်ဆံအိတ်အုပ်စု", "wallet_group_description_existing_seed": "ဒီပိုက်ဆံအိတ်အတွက် ရှိပြီးသား seed ကို သုံးဖို့ သင်ရွေးချယ်ထားပါတယ်။ အတည်ပြုရန် သို့မဟုတ် မှတ်သားရေးသားရန် လိုအပ်ပါက seed ကို ထပ်မံစစ်ဆေးအတည်ပြုနိုင်ပါတယ်။", "wallet_group_description_four": "လုံးဝအသစ်သော seed ဖြင့် wallet တစ်ခုဖန်တီးရန်။", @@ -1347,9 +1380,11 @@ "walletConnect": "WalletConnect", "wallets": "ဝေါလက်များ", "warning": "သတိပေးချက်", + "wc_action_approve": "အတည်ပြု", "wc_approve_request_title": "တောင်းဆိုချက်ကို အတည်ပြုပါ။", "wc_connect_request_title": "တောင်းဆိုချက်ကို ချိတ်ဆက်ပါ။", "wc_connected_to": "${name} သို့ ချိတ်ဆက်ထားသည်", + "wc_desc": "dApp များသို့ လွယ်ကူစွာ ချိတ်ဆက်ပါ", "wc_max_network_fee": "အများဆုံးကွန်ရက်အခကြေးငွေ", "wc_message_to_sign": "လက်မှတ်ထိုးရန် မက်ဆေ့ချ်", "wc_network_fee": "ကွန်ရက်ကြေး", @@ -1400,6 +1435,7 @@ "yat_popup_content": "သင်၏ Yat — အီမိုဂျီအခြေပြု တိုတောင်းသော အသုံးပြုသူအမည် — ဖြင့် Cake Wallet တွင် crypto ကို ယခု ပေးပို့ခြင်းနှင့် လက်ခံခြင်း ပြုလုပ်နိုင်ပါပြီ။ ဆက်တင်များ စခရင်ပေါ်တွင် Yats ကို အချိန်မရွေး စီမံခန့်ခွဲနိုင်ပါသည်။", "yat_popup_title": "သင့်ပိုက်ဆံအိတ်လိပ်စာကို emoji အဖြစ်ပြောင်းလဲနိုင်ပါသည်။", "yes": "ဟုတ်ကဲ့", + "yes_exit": "ဟုတ်ကဲ့။ ထွက်ပါ", "yesterday": "မနေ့က", "you_now_have_debit_card": "ယခု သင့်တွင် ဒက်ဘစ်ကတ် ရရှိပြီးပါပြီ", "you_pay": "သင်ပေးချေသည်", @@ -1451,4 +1487,4 @@ "wc_action_approve": "အတည်ပြု", "wc_desc": "dApp များသို့ လွယ်ကူစွာ ချိတ်ဆက်ပါ", "yes_exit": "ဟုတ်ကဲ့။ ထွက်ပါ" -} \ No newline at end of file +} diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 9285015d8..728ca3fc4 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -76,9 +76,11 @@ "approve_tokens": "Tokens goedkeuren", "apps": "Apps", "arbiscan_history": "ArbiScan-geschiedenis", + "are_you_sure_exit": "Weet je zeker dat je wilt afsluiten?", "arrive_in_this_address": "${currency} ${tag}komt aan op dit adres", "ascending": "Stijgend", "ask_each_time": "Vraag het elke keer", + "assets": "Activa", "auth_store_ban_timeout": "Blokkeringstime-out", "auth_store_banned_for": "Geblokkeerd voor ", "auth_store_banned_minutes": " minuten", @@ -88,6 +90,7 @@ "auto_generate_addresses": "Automatisch adressen genereren", "auto_generate_subaddresses": "Automatisch subadressen genereren", "automatic": "Automatisch", + "available": "Beschikbaar", "available_balance": "Beschikbaar saldo", "available_balance_description": "Het “Beschikbare saldo” of “Bevestigde saldo” zijn fondsen die direct kunnen worden uitgegeven. Als fondsen in het lagere saldo verschijnen maar niet in het bovenste saldo, dan moet je een paar minuten wachten tot de inkomende fondsen meer netwerkbevestigingen krijgen. Daarna kan het worden uitgegeven.", "avg_savings": "Gem. besparingen", @@ -124,6 +127,7 @@ "block_remaining": "1 blok resterend", "Blocks_remaining": "${status} resterende blokken", "bluetooth": "Bluetooth", + "bridging": "Bridgen", "bright_theme": "Helder", "bscscan_history": "BscScan-geschiedenis", "bump_fee": "Vergoeding verhogen", @@ -198,6 +202,7 @@ "choose_qr_code_format": "Kies QR-code-formaat", "choose_qr_code_format_note": "Kies het juiste formaat voor je hardware-wallet:", "choose_relay": "Kies een relay die je wilt gebruiken", + "choose_wallet": "Kies wallet", "choose_wallet_currency": "Kies een walletvaluta:", "choose_wallet_group": "Kies walletgroep", "clear": "Wissen", @@ -214,6 +219,8 @@ "color_theme": "Kleurthema", "coming_soon_tag": "Binnenkort", "commit_transaction_amount_fee": "Bevestig transactie\nBedrag: ${amount}\nKosten: ${fee}", + "compatible_services": "Compatibele services", + "compatible_services_desc": "Gebruik de Scan-tool om de volgende soorten QR-codes te lezen die door Cake Wallet worden ondersteund:", "configure": "Configureren", "confirm": "Bevestigen", "confirm_delete_template": "Met deze actie wordt deze sjabloon verwijderd. Wil je doorgaan?", @@ -277,6 +284,7 @@ "creating_new_wallet": "Nieuwe wallet aanmaken", "creating_new_wallet_error": "Fout: ${description}", "creation_date": "Aanmaakdatum", + "crypto_addresses": "Crypto-adressen", "csv_nothing_to_export": "Niets om te exporteren", "current_swap": "Huidige swap", "current_wallet": "Huidige wallet", @@ -299,6 +307,7 @@ "default_sell_provider": "Standaard verkoopprovider", "delete": "Verwijderen", "delete_account": "Account verwijderen", + "delete_this_node": "Deze node verwijderen", "delete_wallet": "Wallet verwijderen", "delete_wallet_confirm_message": "Weet je zeker dat je de wallet ${wallet_name} wilt verwijderen?", "deleteConnectionConfirmationPrompt": "Weet je zeker dat je de verbinding wilt verwijderen met", @@ -328,6 +337,7 @@ "deuro_savings_welcome_description": "dEURO is een volledig gedecentraliseerde, door onderpand gedekte stablecoin gekoppeld aan de euro — beveiligd, veelzijdig en censuurbestendig zonder te moeten vertrouwen op orakels. 1 dEURO = 1 EURO.", "deuro_tooltip_no_eth": "Om dEURO in te schakelen, moet je ETH in je wallet hebben.", "deuro_tx_commited_content": "Het kan een paar seconden duren voordat de transactie wordt bevestigd en weerspiegeld op het scherm", + "device_confirmation": "Apparaatbevestiging", "device_is_signing": "Apparaat ondertekent", "dfx_option_description": "Koop crypto met EUR & CHF. Voor retail- en zakelijke klanten in Europa", "did_you_back_up_seeds": "Heb je een back-up gemaakt van al je seeds?", @@ -464,6 +474,8 @@ "extracted_address_content": "Je stuurt fondsen naar\n${recipient_name}", "failed_authentication": "Mislukte authenticatie. ${state_error}", "faq": "FAQ", + "favorite_token": "Favoriete token", + "favorite_token_desc": "Het saldo van de favoriete token wordt op de saldokaart weergegeven.", "features": "Functies", "fee": "Kosten", "fee_less_than_min": "Geselecteerde vergoeding is lager dan het minimum, verhoog de vergoeding om de transactie te kunnen verzenden", @@ -489,6 +501,7 @@ "frozen": "Bevroren", "frozen_balance": "Bevroren saldo", "full_balance": "Volledig saldo", + "gallery": "Galerij", "gas_exceeds_allowance": "Gas vereist door transactie overschrijdt het toegestane maximum.", "gas_price": "Gasprijs", "general": "Algemeen", @@ -524,10 +537,10 @@ "how_to_connect": "Hoe te verbinden", "how_to_use": "Hoe te gebruiken", "how_to_use_card": "Hoe je deze kaart gebruikt", + "hww_exit_desc": "Hiermee wordt het lopende koppelingsproces met je hardware-apparaat beëindigd", "i_understand": "Ik begrijp het", "icon_style": "Pictogramstijl", "id": "ID: ", - "if_you_dont_see_your_device": "Als je je apparaat hierboven niet ziet, zorg er dan voor dat je Ledger ontgrendeld is!", "ignor": "Negeren", "import": "Importeren", "importNFTs": "NFT's importeren", @@ -539,6 +552,7 @@ "infobox_auto_address": "Je ontvangstadres wisselt elke keer dat je een transactie ontvangt", "infobox_multichain": "Gebruik dit adres om een token of collectible te ontvangen op", "infobox_rotation": "Je ontvangstadres wisselt telkens wanneer je dit scherm sluit en opnieuw opent", + "input": "Invoer", "inputs": "Invoer", "insufficient_funds_for_tx": "Onvoldoende fondsen om de transactie met succes uit te voeren.", "insufficient_lamport_for_tx": "Je hebt niet genoeg SOL om de transactie en de transactiekosten te dekken. Voeg meer SOL toe aan je wallet of verminder de hoeveelheid SOL die je verzendt.", @@ -577,6 +591,7 @@ "litecoin_mweb_always_scan": "Stel MWEB altijd op scannen", "litecoin_mweb_description": "MWEB is een nieuw protocol dat snellere, goedkopere en meer privacy-vriendelijke transacties naar Litecoin brengt", "litecoin_mweb_dismiss": "Afwijzen", + "litecoin_mweb_display": "Toon MWEB op startscherm", "litecoin_mweb_display_card": "Toon MWEB-kaart", "litecoin_mweb_enable": "MWEB inschakelen", "litecoin_mweb_enable_later": "Je kunt ervoor kiezen om MWEB opnieuw in te schakelen onder weergave-instellingen.", @@ -603,6 +618,7 @@ "manage_pow_nodes": "PoW-nodes beheren", "manage_providers": "Providers beheren", "manage_yats": "Yats beheren", + "manual_input": "Handmatige invoer", "mark_as_redeemed": "Markeer als ingewisseld", "market_place": "Marketplace", "mask": "Masker", @@ -648,6 +664,7 @@ "nanogpt_subtitle": "Alle nieuwste modellen (GPT-5.3, Claude 4.6).\nGeen abonnement, betaal met crypto.", "narrow": "Smal", "network": "Netwerk", + "networks": "Netwerken", "new_first_wallet_text": "Je crypto veilig houden is een fluitje van een cent", "new_node_testing": "Nieuwe node testen", "new_subaddress_create": "Creëren", @@ -676,8 +693,10 @@ "node_new": "Nieuwe node", "node_port": "Node-poort", "node_reset_settings_title": "Reset instellingen", + "node_share_desc": "Scan deze QR-code vanaf het Scan-scherm om deze node te importeren.", "node_test": "Test", "nodes": "Nodes", + "nodes_desc": "Beheer bestaande nodes of voeg een aangepaste toe.", "nodes_list_reset_to_default_message": "Weet je zeker dat je de standaardinstellingen wilt herstellen?", "none_of_selected_providers_can_exchange": "Geen van de geselecteerde providers kan deze swap maken", "noNFTYet": "Nog geen NFT's", @@ -687,6 +706,7 @@ "note_tap_to_change": "Notitie (tik om te wijzigen)", "notification_permission_denied": "Meldingstoestemming is permanent geweigerd. Schakel deze handmatig in via de instellingen.", "nullURIError": "URI is null", + "ocp_desc": "Betaal met crypto in winkels", "offer_expires_in": "Aanbieding verloopt over: ", "offline": "Offline", "ok": "OK", @@ -713,8 +733,13 @@ "outputs": "Outputs", "overshot": "Overschreden", "overwrite_amount": "Bedrag overschrijven", + "pairing_code": "Koppelingscode", + "pairing_code_desc_1": "Je apparaat toont een koppelingscode.", + "pairing_code_desc_2": "Voer deze hier in om door te gaan", + "pairing_error": "Koppelingsfout", "pairingInvalidEvent": "Ongeldige koppelingsgebeurtenis", "passphrase": "Wachtwoordzin (optioneel)", + "passphrase_view_keys": "Wachtwoordzin", "passphrases_doesnt_match": "Wachtwoordzinnen komen niet overeen, probeer het opnieuw", "password": "Wachtwoord", "paste": "Plakken", @@ -730,6 +755,7 @@ "payjoin_unavailable_sheet_content": "Het ontvangen van een Payjoin-transactie vereist dat je Bitcoin klaar hebt om te gebruiken.\n\nZodra je wallet fondsen heeft, wordt Payjoin automatisch ingeschakeld.", "payjoin_unavailable_sheet_title": "Waarom is Payjoin niet beschikbaar?", "payment_id": "Betaling ID: ", + "payment_invoices": "Betalingsfacturen", "payment_made_easy": "Betalingen gemakkelijk gemaakt", "payment_was_received": "Je betaling is ontvangen.", "payments": "Betalingen", @@ -809,6 +835,7 @@ "receive_to": "Ontvangen op", "received": "Ontvangen", "receiving": "Bezig met ontvangen", + "recieving": "Bezig met ontvangen", "recipient_address": "Adres ontvanger", "reconnect": "Opnieuw verbinden", "reconnect_alert_text": "Weet je zeker dat je opnieuw verbinding wilt maken?", @@ -884,6 +911,7 @@ "save": "Opslaan", "save_backup_password": "Zorg ervoor dat je je back-upwachtwoord hebt opgeslagen. Zonder dit kun je je back-upbestanden niet importeren.", "save_backup_password_alert": "Bewaar back-upwachtwoord", + "save_contact": "Contact opslaan", "save_to_downloads": "Opslaan in downloads", "saved_the_trade_id": "Ik heb de trade-ID opgeslagen", "savings": "Spaargeld", @@ -1033,6 +1061,7 @@ "setup_your_debit_card": "Stel je debetkaart in", "share": "Deel", "share_address": "Deel adres", + "share_this_node": "Deel deze node", "shared_seed_wallet_groups": "Gedeelde seed-walletgroepen", "shared_seed_wallets": "Gedeelde seed-wallets", "show": "Tonen", @@ -1040,6 +1069,7 @@ "show_balance": "Lang indrukken om saldo te tonen", "show_balance_send_page": "Tik om te tonen", "show_balance_toast": "Lang indrukken om saldo te verbergen of te tonen", + "show_combined_balance": "Toon gecombineerd saldo", "show_details": "Toon details", "show_hidden_addresses": "Verborgen adressen tonen", "show_keys": "Toon seed/sleutels", @@ -1105,6 +1135,7 @@ "swap_providers": "Swapproviders", "sweeping_wallet": "Wallet wordt geveegd", "sweeping_wallet_alert": "Dit duurt niet lang. VERLAAT DIT SCHERM NIET, ANDERS KUNNEN DE FONDSEN VERLOREN GAAN.", + "swipe_to_bridge": "Veeg om te bridgen", "swipe_to_send": "Swipe om te verzenden", "switch_wallet": "Wissel van wallet", "switchToETHWallet": "Schakel over naar een Ethereum-wallet en probeer het opnieuw", @@ -1145,6 +1176,7 @@ "testnet_coins_no_value": "Testnet-munten hebben geen waarde", "third_intro_content": "Yats bestaan ook buiten Cake Wallet. Elk walletadres op aarde kan worden vervangen door een Yat!", "third_intro_title": "Yat werkt goed samen met anderen", + "this_can_take_few_seconds": "Dit kan een paar seconden duren", "this_pair_is_not_supported_warning": "Dit paar wordt niet ondersteund met de momenteel geselecteerde exchange(s). Selecteer een andere exchange.", "thorchain_contract_address_not_supported": "THORChain ondersteunt het verzenden naar een contractadres niet", "thorchain_taproot_address_not_supported": "De THORChain-provider ondersteunt geen Taproot-adressen. Wijzig het adres of selecteer een andere provider.", @@ -1158,6 +1190,7 @@ "token_decimal": "Token decimaal (bijv. 6 voor USDT)", "token_name": "Tokennaam (bijv. Tether)", "token_symbol": "Tokensymbool (bijv. USDT)", + "token_verified_on_jupiter": "Dit token is geverifieerd op Jupiter", "tokenID": "Token-ID", "tokens": "Tokens", "tokens_strong_warning": "Stuur nooit tokens naar dit adres!", @@ -1286,6 +1319,7 @@ "username": "Gebruikersnaam", "username_available": "Gebruikersnaam beschikbaar", "username_character_error": "Gebruikersnamen mogen alleen bevatten", + "username_check_your_network": "Kan gebruikersnaam niet controleren. Controleer je netwerkverbinding.", "username_length_error": "Gebruikersnamen moeten 6-32 alfanumerieke tekens bevatten.", "username_length_error_1_64": "Gebruikersnamen moeten 1-64 alfanumerieke tekens bevatten.", "username_not_available": "Deze gebruikersnaam is niet beschikbaar.", @@ -1296,6 +1330,7 @@ "verify_message": "Verifieer bericht", "verify_seed": "Controleer seed", "verify_with_2fa": "Controleer met Cake 2FA", + "verifying_code": "Code verifiëren", "version": "Versie ${currentVersion}", "view_all": "Alles bekijken", "view_in_block_explorer": "Bekijk in Block Explorer", @@ -1308,6 +1343,7 @@ "waiting_payment_confirmation": "In afwachting van betalingsbevestiging", "wallet": "Wallet", "wallet_accounts": "Walletaccounts", + "wallet_balance": "Walletsaldo", "wallet_group": "Walletgroep", "wallet_group_description_existing_seed": "Je hebt ervoor gekozen om een bestaande seed voor deze wallet te gebruiken. Je kunt de seed opnieuw verifiëren als je het moet bevestigen of opschrijven.", "wallet_group_description_four": "om een wallet te maken met een geheel nieuwe seed.", @@ -1345,9 +1381,11 @@ "walletConnect": "WalletConnect", "wallets": "Wallets", "warning": "Waarschuwing", + "wc_action_approve": "Goedkeuren", "wc_approve_request_title": "Aanvraag goedkeuren", "wc_connect_request_title": "Verbind verzoek", "wc_connected_to": "Verbonden met ${name}", + "wc_desc": "Maak eenvoudig verbinding met dApps", "wc_max_network_fee": "Maximale netwerkkosten", "wc_message_to_sign": "Bericht om te ondertekenen", "wc_network_fee": "Netwerk vergoeding", @@ -1398,6 +1436,7 @@ "yat_popup_content": "Je kunt nu crypto verzenden en ontvangen in Cake Wallet met je Yat - een korte, op emoji gebaseerde gebruikersnaam. Beheer Yats op elk moment op het instellingenscherm", "yat_popup_title": "Je walletadres kan worden omgezet naar emoji's.", "yes": "Ja", + "yes_exit": "Ja. Afsluiten", "yesterday": "Gisteren", "you_now_have_debit_card": "Je hebt nu een debetkaart", "you_pay": "Je betaalt", @@ -1452,4 +1491,4 @@ "wc_action_approve": "Goedkeuren", "wc_desc": "Maak eenvoudig verbinding met dApps", "yes_exit": "Ja. Afsluiten" -} \ No newline at end of file +} diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 16045cd4b..977141c2a 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -76,6 +76,7 @@ "approve_tokens": "Zatwierdź tokeny", "apps": "Aplikacje", "arbiscan_history": "Historia ArbiScan", + "are_you_sure_exit": "Czy na pewno chcesz wyjść?", "arrive_in_this_address": "${currency} ${tag}dotrze na ten adres", "ascending": "Rosnąco", "ask_each_time": "Pytaj za każdym razem", @@ -89,6 +90,7 @@ "auto_generate_addresses": "Automatyczne generowanie adresów", "auto_generate_subaddresses": "Automatyczne generowanie podadresów", "automatic": "Automatyczny", + "available": "Dostępne", "available_balance": "Dostępne saldo", "available_balance_description": "„Dostępne saldo” lub „Potwierdzone saldo” to środki, które można wydać od razu. Jeśli środki pojawiają się w dolnym saldzie, ale nie w górnym, musisz poczekać kilka minut, aż przychodzące środki uzyskają więcej potwierdzeń sieci. Gdy uzyskają więcej potwierdzeń, będzie można je wydać.", "avg_savings": "Śr. oszczędności", @@ -125,6 +127,7 @@ "block_remaining": "Pozostał 1 blok", "Blocks_remaining": "Pozostało bloków: ${status}", "bluetooth": "Bluetooth", + "bridging": "Mostkowanie", "bright_theme": "Jasny", "bscscan_history": "Historia BSCScan", "bump_fee": "Podbij opłatę", @@ -216,6 +219,8 @@ "color_theme": "Motyw kolorystyczny", "coming_soon_tag": "Wkrótce", "commit_transaction_amount_fee": "Zatwierdź transakcję\nKwota: ${amount}\nOpłata: ${fee}", + "compatible_services": "Zgodne usługi", + "compatible_services_desc": "Użyj narzędzia Skanuj, aby odczytać następujące typy kodów QR obsługiwane w Cake Wallet:", "configure": "Konfiguruj", "confirm": "Potwierdź", "confirm_delete_template": "Ta czynność usunie ten szablon. Czy chcesz kontynuować?", @@ -279,6 +284,7 @@ "creating_new_wallet": "Tworzenie nowego portfela", "creating_new_wallet_error": "Błąd: ${description}", "creation_date": "Data utworzenia", + "crypto_addresses": "Adresy krypto", "csv_nothing_to_export": "Brak danych do eksportu", "current_swap": "Bieżący swap", "current_wallet": "Bieżący portfel", @@ -301,6 +307,7 @@ "default_sell_provider": "Domyślny dostawca sprzedaży", "delete": "Usuń", "delete_account": "Usuń konto", + "delete_this_node": "Usuń ten węzeł", "delete_wallet": "Usuń portfel", "delete_wallet_confirm_message": "Czy na pewno chcesz usunąć portfel ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Czy na pewno chcesz usunąć połączenie z", @@ -330,6 +337,7 @@ "deuro_savings_welcome_description": "dEURO jest w pełni zdecentralizowanym, zabezpieczonym stablecoinem powiązanym z euro — bezpiecznym, wszechstronnym i odpornym na cenzurę, bez konieczności polegania na wyroczniach. 1 dEURO = 1 EURO.", "deuro_tooltip_no_eth": "Aby włączyć dEURO, musisz mieć ETH w swoim portfelu.", "deuro_tx_commited_content": "Może to potrwać kilka sekund, zanim transakcja zostanie potwierdzona i pojawi się na ekranie", + "device_confirmation": "Potwierdzenie na urządzeniu", "device_is_signing": "Urządzenie podpisuje", "dfx_option_description": "Kup kryptowaluty za EUR i CHF. Dla klientów detalicznych i firm w Europie", "did_you_back_up_seeds": "Czy wykonałeś kopię zapasową wszystkich fraz seed?", @@ -435,6 +443,7 @@ "etherscan_history": "Historia w Etherscan", "event": "Zdarzenie", "events": "Zdarzenia", + "evm_ecosystem_description": "W razie potrzeby zmień sieć i token, których chcesz użyć do tej płatności.", "exchange": "Wymiana", "exchange_extra_info": "Proszę nie zapomnieć podać Memo lub Extra ID podczas wysyłania tej transakcji do wymiany", "exchange_incorrect_current_wallet_for_xmr": "Jeśli chcesz wymienić XMR z salda Monero w Cake Wallet, najpierw przełącz się na swój portfel Monero.", @@ -465,6 +474,8 @@ "extracted_address_content": "Wyślesz środki do\n${recipient_name}", "failed_authentication": "Nieudane uwierzytelnienie. ${state_error}", "faq": "FAQ", + "favorite_token": "Ulubiony token", + "favorite_token_desc": "Saldo ulubionego tokena będzie wyświetlane na karcie salda.", "features": "Funkcje", "fee": "Opłata", "fee_less_than_min": "Wybrana opłata jest niższa niż minimalna. Zwiększ opłatę, aby móc wysłać transakcję.", @@ -490,6 +501,7 @@ "frozen": "Zamrożone", "frozen_balance": "Zamrożone saldo", "full_balance": "Pełne saldo", + "gallery": "Galeria", "gas_exceeds_allowance": "Gaz wymagany przez transakcję przekracza dozwolony limit.", "gas_price": "Cena gas", "general": "Ogólne", @@ -525,10 +537,10 @@ "how_to_connect": "Jak się połączyć", "how_to_use": "Jak używać", "how_to_use_card": "Jak używać tej karty", + "hww_exit_desc": "Spowoduje to zakończenie trwającego procesu parowania z Twoim urządzeniem sprzętowym", "i_understand": "Rozumiem", "icon_style": "Styl ikony", "id": "ID: ", - "if_you_dont_see_your_device": "Jeśli nie widzisz swojego urządzenia powyżej, upewnij się, że Twój Ledger jest aktywny i odblokowany!", "ignor": "Ignoruj", "import": "Importuj", "importNFTs": "Importuj NFT", @@ -540,6 +552,7 @@ "infobox_auto_address": "Twój adres do odbioru będzie się zmieniał za każdym razem, gdy otrzymasz transakcję", "infobox_multichain": "Użyj tego adresu, aby otrzymać dowolny token lub przedmiot kolekcjonerski na", "infobox_rotation": "Twój adres do odbioru będzie się zmieniać za każdym razem, gdy zamkniesz i ponownie otworzysz ten ekran", + "input": "Dane wejściowe", "inputs": "Wejścia", "insufficient_funds_for_tx": "Niewystarczające środki, aby pomyślnie zrealizować transakcję.", "insufficient_lamport_for_tx": "Nie masz wystarczającej ilości SOL, aby pokryć transakcję oraz opłatę transakcyjną. Dodaj więcej SOL do swojego portfela lub zmniejsz kwotę SOL, którą wysyłasz.", @@ -605,6 +618,7 @@ "manage_pow_nodes": "Zarządzaj węzłami PoW", "manage_providers": "Zarządzaj dostawcami", "manage_yats": "Zarządzaj Yats", + "manual_input": "Wprowadzanie ręczne", "mark_as_redeemed": "Oznacz jako zrealizowane", "market_place": "Marketplace", "mask": "Maska", @@ -650,6 +664,7 @@ "nanogpt_subtitle": "Wszystkie najnowsze modele (GPT-5.3, Claude 4.6).\nBez subskrypcji, płać kryptowalutą.", "narrow": "Wąski", "network": "Sieć", + "networks": "Sieci", "new_first_wallet_text": "Bezpieczne przechowywanie Twoich kryptowalut to bułka z masłem", "new_node_testing": "Testowanie nowego węzła", "new_subaddress_create": "Utwórz", @@ -678,8 +693,10 @@ "node_new": "Nowy węzeł", "node_port": "Port węzła", "node_reset_settings_title": "Resetuj ustawienia", + "node_share_desc": "Zeskanuj ten kod QR z ekranu Skanuj, aby zaimportować ten węzeł.", "node_test": "Test", "nodes": "Węzły", + "nodes_desc": "Zarządzaj istniejącymi węzłami lub dodaj własny.", "nodes_list_reset_to_default_message": "Czy na pewno chcesz przywrócić ustawienia domyślne?", "none_of_selected_providers_can_exchange": "Żaden z wybranych dostawców nie może wykonać tej wymiany", "noNFTYet": "Brak NFT", @@ -689,6 +706,7 @@ "note_tap_to_change": "Notatka (stuknij, aby zmienić)", "notification_permission_denied": "Uprawnienie do powiadomień zostało trwale odrzucone. Włącz je ręcznie w ustawieniach.", "nullURIError": "URI ma wartość null", + "ocp_desc": "Płać kryptowalutą w sklepach", "offer_expires_in": "Oferta wygasa za: ", "offline": "Offline", "ok": "OK", @@ -715,8 +733,13 @@ "outputs": "Wyjścia", "overshot": "Przekroczono", "overwrite_amount": "Nadpisz kwotę", + "pairing_code": "Kod parowania", + "pairing_code_desc_1": "Twoje urządzenie wyświetli kod parowania.", + "pairing_code_desc_2": "Wprowadź go tutaj, aby kontynuować", + "pairing_error": "Błąd parowania", "pairingInvalidEvent": "Nieprawidłowe zdarzenie parowania", "passphrase": "Fraza hasła (Opcjonalnie)", + "passphrase_view_keys": "Fraza hasła", "passphrases_doesnt_match": "Hasła nie pasują, spróbuj ponownie", "password": "Hasło", "paste": "Wklej", @@ -732,6 +755,7 @@ "payjoin_unavailable_sheet_content": "Otrzymanie transakcji Payjoin wymaga posiadania gotowych do użycia bitcoinów.\n\nGdy tylko w Twoim portfelu pojawią się środki, Payjoin zostanie automatycznie włączony.", "payjoin_unavailable_sheet_title": "Dlaczego Payjoin jest niedostępny?", "payment_id": "Identyfikator płatności: ", + "payment_invoices": "Faktury płatności", "payment_made_easy": "Płatności są proste", "payment_was_received": "Twoja płatność została otrzymana.", "payments": "Płatności", @@ -811,6 +835,7 @@ "receive_to": "Odbierz na", "received": "Otrzymane", "receiving": "Odbieranie", + "recieving": "Odbieranie", "recipient_address": "Adres odbiorcy", "reconnect": "Połącz ponownie", "reconnect_alert_text": "Czy na pewno chcesz połączyć się ponownie?", @@ -886,6 +911,7 @@ "save": "Zapisz", "save_backup_password": "Upewnij się, że zapisałeś hasło do kopii zapasowej. Bez niego nie będziesz mógł zaimportować plików kopii zapasowej.", "save_backup_password_alert": "Zapisz hasło kopii zapasowej", + "save_contact": "Zapisz kontakt", "save_to_downloads": "Zapisz w folderze Pobrane", "saved_the_trade_id": "Zapisałem identyfikator transakcji", "savings": "Oszczędności", @@ -1035,6 +1061,7 @@ "setup_your_debit_card": "Skonfiguruj swoją kartę debetową", "share": "Udostępnij", "share_address": "Udostępnij adres", + "share_this_node": "Udostępnij ten węzeł", "shared_seed_wallet_groups": "Grupy portfeli ze współdzielonym seedem", "shared_seed_wallets": "Portfele ze współdzieloną frazą seed", "show": "Pokaż", @@ -1042,6 +1069,7 @@ "show_balance": "Naciśnij i przytrzymaj, aby wyświetlić saldo", "show_balance_send_page": "Dotknij, aby odkryć", "show_balance_toast": "Naciśnij i przytrzymaj, aby ukryć lub pokazać saldo", + "show_combined_balance": "Pokaż łączne saldo", "show_details": "Pokaż szczegóły", "show_hidden_addresses": "Pokaż ukryte adresy", "show_keys": "Pokaż seed/klucze", @@ -1107,6 +1135,7 @@ "swap_providers": "Dostawcy wymiany", "sweeping_wallet": "Opróżnianie portfela", "sweeping_wallet_alert": "To nie powinno zająć dużo czasu. NIE OPUSZCZAJ TEGO EKRANU, W PRZECIWNYM RAZIE ZAMIATANE ŚRODKI MOGĄ ZOSTAĆ UTRACONE.", + "swipe_to_bridge": "Przesuń, aby zmostkować", "swipe_to_send": "Przesuń, aby wysłać", "switch_wallet": "Przełącz portfel", "switchToETHWallet": "Przełącz się na portfel Ethereum i spróbuj ponownie", @@ -1147,6 +1176,7 @@ "testnet_coins_no_value": "Monety testnet nie mają wartości", "third_intro_content": "Yaty działają także poza Cake Wallet. Każdy adres portfela na świecie można zastąpić Yatem!", "third_intro_title": "Yat dobrze współpracuje z innymi", + "this_can_take_few_seconds": "To może potrwać kilka sekund", "this_pair_is_not_supported_warning": "Ta para nie jest obsługiwana przez aktualnie wybraną giełdę/wybrane giełdy. Wybierz inną giełdę.", "thorchain_contract_address_not_supported": "THORChain nie obsługuje wysyłania na adres kontraktu", "thorchain_taproot_address_not_supported": "Dostawca ThorChain nie obsługuje adresów Taproot. Zmień adres lub wybierz innego dostawcę.", @@ -1160,6 +1190,7 @@ "token_decimal": "Liczba miejsc dziesiętnych tokena (np. USDT)", "token_name": "Nazwa tokena (np. Tether)", "token_symbol": "Symbol tokenu (np.: USDT)", + "token_verified_on_jupiter": "Token ten jest zweryfikowany na Jupiterze", "tokenID": "ID", "tokens": "Tokeny", "tokens_strong_warning": "Nigdy nie wysyłaj tokenów na ten adres!", @@ -1288,6 +1319,7 @@ "username": "Nazwa użytkownika", "username_available": "Nazwa użytkownika dostępna", "username_character_error": "Nazwy użytkowników mogą zawierać tylko", + "username_check_your_network": "Nie można sprawdzić nazwy użytkownika. Sprawdź połączenie sieciowe.", "username_length_error": "Nazwy użytkowników muszą zawierać 6–32 znaki alfanumeryczne.", "username_length_error_1_64": "Nazwy użytkowników muszą zawierać 1–64 znaki alfanumeryczne.", "username_not_available": "Ta nazwa użytkownika jest niedostępna.", @@ -1298,6 +1330,7 @@ "verify_message": "Zweryfikuj wiadomość", "verify_seed": "Zweryfikuj frazę seed", "verify_with_2fa": "Zweryfikuj za pomocą Cake 2FA", + "verifying_code": "Weryfikowanie kodu", "version": "Wersja ${currentVersion}", "view_all": "Zobacz wszystko", "view_in_block_explorer": "Wyświetl w eksploratorze bloków", @@ -1309,6 +1342,7 @@ "waitFewSecondForTxUpdate": "Proszę poczekać kilka sekund, aż transakcja pojawi się w historii transakcji", "wallet": "Portfel", "wallet_accounts": "Konta portfela", + "wallet_balance": "Saldo portfela", "wallet_group": "Grupa portfeli", "wallet_group_description_existing_seed": "Wybrałeś użycie istniejącego seeda dla tego portfela. Możesz ponownie zweryfikować seed, jeśli potrzebujesz go potwierdzić lub zapisać.", "wallet_group_description_four": "aby utworzyć portfel z całkowicie nowym seedem.", @@ -1346,9 +1380,11 @@ "walletConnect": "WalletConnect", "wallets": "Portfele", "warning": "Ostrzeżenie", + "wc_action_approve": "Zatwierdź", "wc_approve_request_title": "Zatwierdź żądanie", "wc_connect_request_title": "Połącz żądanie", "wc_connected_to": "Połączono z ${name}", + "wc_desc": "Łatwo łącz się z aplikacjami dApp", "wc_max_network_fee": "Maksymalna opłata sieciowa", "wc_message_to_sign": "Wiadomość do podpisania", "wc_network_fee": "Opłata sieciowa", @@ -1399,6 +1435,7 @@ "yat_popup_content": "Możesz teraz wysyłać i odbierać kryptowaluty w Cake Wallet za pomocą swojego Yat — krótkiej, opartej na emoji nazwy użytkownika. Zarządzaj Yatami w dowolnym momencie na ekranie ustawień", "yat_popup_title": "Twój adres portfela może zostać zamieniony na emoji.", "yes": "Tak", + "yes_exit": "Tak. Wyjdź", "yesterday": "Wczoraj", "you_now_have_debit_card": "Masz teraz kartę debetową", "you_pay": "Płacisz", @@ -1451,4 +1488,4 @@ "wc_action_approve": "Zatwierdź", "wc_desc": "Łatwo łącz się z aplikacjami dApp", "yes_exit": "Tak. Wyjdź" -} \ No newline at end of file +} diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 5ef972b66..48bd607ec 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -76,6 +76,7 @@ "approve_tokens": "Aprovar tokens", "apps": "Apps", "arbiscan_history": "Histórico do ArbiScan", + "are_you_sure_exit": "Tem a certeza de que pretende sair?", "arrive_in_this_address": "${currency} ${tag}chegará a este endereço", "ascending": "Ascendente", "ask_each_time": "Perguntar sempre", @@ -89,6 +90,7 @@ "auto_generate_addresses": "Gerar endereços automaticamente", "auto_generate_subaddresses": "Gerar subendereços automaticamente", "automatic": "Automático", + "available": "Disponível", "available_balance": "Saldo disponível", "available_balance_description": "O “Saldo Disponível” ou “Saldo Confirmado” são fundos que podem ser gastos imediatamente. Se os fundos aparecerem no saldo inferior, mas não no saldo superior, você deve aguardar alguns minutos para que os fundos recebidos obtenham mais confirmações na rede. Depois que tiverem mais confirmações, eles poderão ser gastos.", "avg_savings": "Poupança média", @@ -125,6 +127,7 @@ "block_remaining": "1 Bloco restante", "Blocks_remaining": "${status} Blocos restantes", "bluetooth": "Bluetooth", + "bridging": "A fazer bridge", "bright_theme": "Claro", "bscscan_history": "Histórico do BSCScan", "bump_fee": "Aumentar taxa", @@ -216,6 +219,8 @@ "color_theme": "Tema de cores", "coming_soon_tag": "Em breve", "commit_transaction_amount_fee": "Confirmar transação\nValor: ${amount}\nTaxa: ${fee}", + "compatible_services": "Serviços compatíveis", + "compatible_services_desc": "Use a ferramenta de digitalização para ler os seguintes tipos de códigos QR suportados na Cake Wallet:", "configure": "Configurar", "confirm": "Confirmar", "confirm_delete_template": "Esta ação excluirá este modelo. Deseja continuar?", @@ -279,6 +284,7 @@ "creating_new_wallet": "Criando nova carteira", "creating_new_wallet_error": "Erro: ${description}", "creation_date": "Data de criação", + "crypto_addresses": "Endereços de cripto", "csv_nothing_to_export": "Nada para exportar", "current_swap": "Troca atual", "current_wallet": "Carteira atual", @@ -301,6 +307,7 @@ "default_sell_provider": "Provedor de venda padrão", "delete": "Excluir", "delete_account": "Excluir conta", + "delete_this_node": "Eliminar este nó", "delete_wallet": "Excluir carteira", "delete_wallet_confirm_message": "Tem certeza de que deseja excluir a carteira ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Tem certeza de que deseja excluir a conexão com", @@ -330,6 +337,7 @@ "deuro_savings_welcome_description": "dEURO é uma stablecoin totalmente descentralizada e colateralizada, atrelada ao euro — segura, versátil e resistente à censura, sem precisar depender de oráculos. 1 dEURO = 1 EURO.", "deuro_tooltip_no_eth": "Para ativar dEURO, você deve ter ETH na sua carteira.", "deuro_tx_commited_content": "Pode levar alguns segundos para a transação ser confirmada e aparecer na tela", + "device_confirmation": "Confirmação no dispositivo", "device_is_signing": "O dispositivo está assinando", "dfx_option_description": "Compre cripto com EUR e CHF. Para clientes de varejo e corporativos na Europa", "did_you_back_up_seeds": "Você fez backup de todas as suas seeds?", @@ -466,6 +474,8 @@ "extracted_address_content": "Você enviará fundos para\n${recipient_name}", "failed_authentication": "Falha na autenticação. ${state_error}", "faq": "FAQ", + "favorite_token": "Token favorito", + "favorite_token_desc": "O saldo do token favorito será apresentado no cartão de saldo.", "features": "Recursos", "fee": "Taxa", "fee_less_than_min": "A taxa selecionada é inferior ao mínimo. Aumente a taxa para poder enviar a transação.", @@ -491,6 +501,7 @@ "frozen": "Congelado", "frozen_balance": "Saldo congelado", "full_balance": "Saldo total", + "gallery": "Galeria", "gas_exceeds_allowance": "O gás exigido pela transação excede o limite permitido.", "gas_price": "Preço do gas", "general": "Geral", @@ -526,10 +537,10 @@ "how_to_connect": "Como Conectar", "how_to_use": "Como usar", "how_to_use_card": "Como usar este cartão", + "hww_exit_desc": "Isto irá terminar o processo de emparelhamento em curso com o seu dispositivo de hardware", "i_understand": "Eu entendo", "icon_style": "Estilo do ícone", "id": "ID: ", - "if_you_dont_see_your_device": "Se você não vir seu dispositivo acima, verifique se o seu Ledger está ligado e desbloqueado!", "ignor": "Ignorar", "import": "Importar", "importNFTs": "Importar NFTs", @@ -541,6 +552,7 @@ "infobox_auto_address": "Seu endereço de recebimento mudará a cada transação recebida", "infobox_multichain": "Use este endereço para receber qualquer token ou colecionável em", "infobox_rotation": "Seu endereço de recebimento será alterado sempre que você fechar e reabrir esta tela", + "input": "Entrada", "inputs": "Entradas", "insufficient_funds_for_tx": "Fundos insuficientes para executar a transação com sucesso.", "insufficient_lamport_for_tx": "Você não tem SOL suficiente para cobrir a transação e a taxa de transação. Por favor, adicione mais SOL à sua carteira ou reduza a quantidade de SOL que você está enviando.", @@ -606,6 +618,7 @@ "manage_pow_nodes": "Gerenciar nós de PoW", "manage_providers": "Gerenciar provedores", "manage_yats": "Gerenciar Yats", + "manual_input": "Introdução manual", "mark_as_redeemed": "Marcar como resgatado", "mark_as_redemed": "Marcar como resgatado", "market_place": "Marketplace", @@ -652,6 +665,7 @@ "nanogpt_subtitle": "Todos os modelos mais recentes (GPT-5.3, Claude 4.6).\nSem assinatura, pague com cripto.", "narrow": "Estreito", "network": "Rede", + "networks": "Redes", "new_first_wallet_text": "Manter suas criptos seguras é moleza", "new_node_testing": "Teste de novo nó", "new_subaddress_create": "Criar", @@ -680,8 +694,10 @@ "node_new": "Novo nó", "node_port": "Porta do nó", "node_reset_settings_title": "Redefinir configurações", + "node_share_desc": "Digitalize este código QR a partir do ecrã de digitalização para importar este nó.", "node_test": "Teste", "nodes": "Nós", + "nodes_desc": "Faça a gestão dos nós existentes ou adicione um personalizado.", "nodes_list_reset_to_default_message": "Tem certeza de que deseja redefinir as configurações para o padrão?", "none_of_selected_providers_can_exchange": "Nenhum dos provedores selecionados pode realizar esta troca", "noNFTYet": "Ainda não há NFTs", @@ -691,6 +707,7 @@ "note_tap_to_change": "Nota (toque para mudar)", "notification_permission_denied": "A permissão de notificações foi negada permanentemente. Ative-a manualmente nas configurações.", "nullURIError": "O URI é nulo", + "ocp_desc": "Pague com cripto em lojas", "offer_expires_in": "A oferta expira em: ", "offline": "Offline", "ok": "OK", @@ -718,8 +735,13 @@ "outputs": "Saídas", "overshot": "Excedido", "overwrite_amount": "Substituir valor", + "pairing_code": "Código de emparelhamento", + "pairing_code_desc_1": "O seu dispositivo irá mostrar um código de emparelhamento.", + "pairing_code_desc_2": "Introduza-o aqui para continuar", + "pairing_error": "Erro de emparelhamento", "pairingInvalidEvent": "Evento de emparelhamento inválido", "passphrase": "Frase-senha (Opcional)", + "passphrase_view_keys": "Frase-senha", "passphrases_doesnt_match": "As frases-passe não correspondem, por favor tente novamente", "password": "Senha", "paste": "Colar", @@ -735,6 +757,7 @@ "payjoin_unavailable_sheet_content": "Receber uma transação Payjoin exige que você tenha Bitcoin pronto para uso.\n\nAssim que sua carteira tiver fundos, o Payjoin será ativado automaticamente.", "payjoin_unavailable_sheet_title": "Por que o Payjoin não está disponível?", "payment_id": "ID do pagamento: ", + "payment_invoices": "Faturas de pagamento", "payment_made_easy": "Pagamentos facilitados", "payment_was_received": "Seu pagamento foi recebido.", "payments": "Pagamentos", @@ -814,6 +837,7 @@ "receive_to": "Receber para", "received": "Recebido", "receiving": "Recebendo", + "recieving": "A receber", "recipient_address": "Endereço do destinatário", "reconnect": "Reconectar", "reconnect_alert_text": "Tem certeza de que deseja reconectar?", @@ -889,6 +913,7 @@ "save": "Salvar", "save_backup_password": "Certifique-se de que salvou a sua senha de backup. Você não poderá importar os seus arquivos de backup sem ela.", "save_backup_password_alert": "Salvar senha do backup", + "save_contact": "Guardar contacto", "save_to_downloads": "Salvar em Downloads", "saved_the_trade_id": "Salvei o ID da troca", "savings": "Poupança", @@ -1038,6 +1063,7 @@ "setup_your_debit_card": "Configure seu cartão de débito", "share": "Compartilhar", "share_address": "Compartilhar endereço", + "share_this_node": "Partilhar este nó", "shared_seed_wallet_groups": "Grupos de carteiras com seed compartilhada", "shared_seed_wallets": "Carteiras com frase-semente compartilhada", "show": "Mostrar", @@ -1045,6 +1071,7 @@ "show_balance": "Pressione e segure para mostrar o saldo", "show_balance_send_page": "Toque para revelar", "show_balance_toast": "Pressione e segure para ocultar ou mostrar o saldo", + "show_combined_balance": "Mostrar saldo combinado", "show_details": "Mostrar detalhes", "show_hidden_addresses": "Mostrar endereços ocultos", "show_keys": "Mostrar seed/chaves", @@ -1110,6 +1137,7 @@ "swap_providers": "Provedores de swap", "sweeping_wallet": "Carteira de varredura", "sweeping_wallet_alert": "Isso não deve demorar muito. NÃO SAIA DESTA TELA OU OS FUNDOS VARRIDOS PODEM SER PERDIDOS.", + "swipe_to_bridge": "Deslize para fazer bridge", "swipe_to_send": "Deslize para enviar", "switch_wallet": "Alternar carteira", "switchToETHWallet": "Por favor, mude para uma carteira Ethereum e tente novamente", @@ -1150,6 +1178,7 @@ "testnet_coins_no_value": "As moedas de testnet não têm valor", "third_intro_content": "Os Yats também existem fora da Cake Wallet. Qualquer endereço de carteira no mundo pode ser substituído por um Yat!", "third_intro_title": "Yat funciona bem com os outros", + "this_can_take_few_seconds": "Isto pode demorar alguns segundos", "this_pair_is_not_supported_warning": "Este par não é suportado com a(s) exchange(s) selecionada(s) atualmente. Selecione outra exchange.", "thorchain_contract_address_not_supported": "A THORChain não suporta envios para um endereço de contrato", "thorchain_taproot_address_not_supported": "O provedor ThorChain não oferece suporte a endereços Taproot. Altere o endereço ou selecione um provedor diferente.", @@ -1163,6 +1192,7 @@ "token_decimal": "Decimal do token (ex: USDT)", "token_name": "Nome do token (ex.: Tether)", "token_symbol": "Símbolo do token (ex.: USDT)", + "token_verified_on_jupiter": "Este token é verificado em Jupiter", "tokenID": "ID", "tokens": "Tokens", "tokens_strong_warning": "Nunca envie tokens para este endereço!", @@ -1291,6 +1321,7 @@ "username": "Nome de usuário", "username_available": "Nome de usuário disponível", "username_character_error": "Os nomes de usuário devem conter apenas", + "username_check_your_network": "Não foi possível verificar o nome de utilizador. Verifique a sua ligação de rede.", "username_length_error": "Os nomes de usuário devem conter de 6 a 32 caracteres alfanuméricos.", "username_length_error_1_64": "Os nomes de usuário devem conter de 1 a 64 caracteres alfanuméricos.", "username_not_available": "Este nome de usuário não está disponível.", @@ -1301,6 +1332,7 @@ "verify_message": "Verificar mensagem", "verify_seed": "Verificar a seed", "verify_with_2fa": "Verificar com o Cake 2FA", + "verifying_code": "A verificar o código", "version": "Versão ${currentVersion}", "view_all": "Ver tudo", "view_in_block_explorer": "Ver no Explorador de Blocos", @@ -1313,6 +1345,7 @@ "waiting_payment_confirmation": "Aguardando confirmação de pagamento", "wallet": "Carteira", "wallet_accounts": "Contas da carteira", + "wallet_balance": "Saldo da carteira", "wallet_group": "Grupo de carteiras", "wallet_group_description_existing_seed": "Você optou por usar uma seed existente para esta carteira. Você pode verificar a seed novamente se precisar confirmar ou anotá-la.", "wallet_group_description_four": "para criar uma carteira com uma semente totalmente nova.", @@ -1350,9 +1383,11 @@ "walletConnect": "WalletConnect", "wallets": "Carteiras", "warning": "Aviso", + "wc_action_approve": "Aprovar", "wc_approve_request_title": "Aprovar solicitação", "wc_connect_request_title": "Solicitação de conexão", "wc_connected_to": "Conectado a ${name}", + "wc_desc": "Ligue-se facilmente a DApps", "wc_max_network_fee": "Taxa máxima de rede", "wc_message_to_sign": "Mensagem para assinar", "wc_network_fee": "Taxa de rede", @@ -1403,6 +1438,7 @@ "yat_popup_content": "Agora você pode enviar e receber cripto na Cake Wallet com seu Yat — um nome de usuário curto baseado em emojis. Gerencie Yats a qualquer momento na tela de configurações", "yat_popup_title": "O endereço da sua carteira pode ser transformado em emojis.", "yes": "Sim", + "yes_exit": "Sim. Sair", "yesterday": "Ontem", "you_now_have_debit_card": "Agora você tem um cartão de débito", "you_pay": "Você paga", @@ -1454,4 +1490,4 @@ "wc_action_approve": "Aprovar", "wc_desc": "Ligue-se facilmente a DApps", "yes_exit": "Sim. Sair" -} \ No newline at end of file +} diff --git a/res/values/strings_pt_BR.arb b/res/values/strings_pt_BR.arb index ea1a3a4ab..1398bd905 100644 --- a/res/values/strings_pt_BR.arb +++ b/res/values/strings_pt_BR.arb @@ -524,7 +524,6 @@ "i_understand": "Eu entendo", "icon_style": "Estilo do ícone", "id": "ID: ", - "if_you_dont_see_your_device": "Se você não vir seu dispositivo acima, certifique-se de que seu Ledger esteja ligado e desbloqueado!", "ignor": "Ignorar", "import": "Importar", "importNFTs": "Importar NFTs", @@ -1141,6 +1140,7 @@ "token_name": "Nome do token (ex: Tether)", "token_symbol": "Símbolo do token (ex: USDT)", "tokenID": "ID", + "token_verified_on_jupiter": "Este token é verificado em Jupiter", "tokens": "Tokens", "tokens_strong_warning": "Nunca envie tokens para este endereço!", "tor_connection": "Conexão Tor", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index aaac422dd..40e3fca3c 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -76,6 +76,7 @@ "approve_tokens": "Одобрить токены", "apps": "Приложения", "arbiscan_history": "История ArbiScan", + "are_you_sure_exit": "Вы уверены, что хотите выйти?", "arrive_in_this_address": "${currency} ${tag}поступит на этот адрес", "ascending": "По возрастанию", "ask_each_time": "Спрашивать каждый раз", @@ -89,6 +90,7 @@ "auto_generate_addresses": "Автоматически генерировать адреса", "auto_generate_subaddresses": "Автоматически генерировать субадреса", "automatic": "Автоматически", + "available": "Доступно", "available_balance": "Доступный баланс", "available_balance_description": "«Доступный баланс» или «Подтверждённый баланс» — это средства, которые можно потратить сразу. Если средства отображаются в нижнем балансе, но не в верхнем, значит, нужно подождать несколько минут, пока входящие средства получат больше подтверждений сети. После получения дополнительных подтверждений их можно будет потратить.", "avg_savings": "Средняя экономия", @@ -125,6 +127,7 @@ "block_remaining": "Остался 1 блок", "Blocks_remaining": "${status} блоков осталось", "bluetooth": "Bluetooth", + "bridging": "Перенос через мост", "bright_theme": "Светлая", "bscscan_history": "История BSCScan", "bump_fee": "Увеличить комиссию", @@ -216,6 +219,8 @@ "color_theme": "Цветовая тема", "coming_soon_tag": "Скоро", "commit_transaction_amount_fee": "Подтвердить транзакцию\nСумма: ${amount}\nКомиссия: ${fee}", + "compatible_services": "Совместимые сервисы", + "compatible_services_desc": "Используйте инструмент сканирования, чтобы считывать следующие типы QR-кодов, поддерживаемые в Cake Wallet:", "configure": "Настроить", "confirm": "Подтвердить", "confirm_delete_template": "Это действие удалит этот шаблон. Вы хотите продолжить?", @@ -279,6 +284,7 @@ "creating_new_wallet": "Создание нового кошелька", "creating_new_wallet_error": "Ошибка: ${description}", "creation_date": "Дата создания", + "crypto_addresses": "Криптоадреса", "csv_nothing_to_export": "Нечего экспортировать", "current_swap": "Текущий обмен", "current_wallet": "Текущий кошелёк", @@ -301,6 +307,7 @@ "default_sell_provider": "Провайдер продажи по умолчанию", "delete": "Удалить", "delete_account": "Удалить аккаунт", + "delete_this_node": "Удалить этот узел", "delete_wallet": "Удалить кошелек", "delete_wallet_confirm_message": "Вы уверены, что хотите удалить кошелёк ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Вы уверены, что хотите удалить подключение к", @@ -330,6 +337,7 @@ "deuro_savings_welcome_description": "dEURO — полностью децентрализованный обеспеченный стейблкоин, привязанный к евро: безопасный, универсальный и устойчивый к цензуре, без необходимости полагаться на оракулы. 1 dEURO = 1 EURO.", "deuro_tooltip_no_eth": "Чтобы включить dEURO, в вашем кошельке должен быть ETH.", "deuro_tx_commited_content": "Подтверждение транзакции и её отображение на экране может занять пару секунд", + "device_confirmation": "Подтверждение на устройстве", "device_is_signing": "Устройство подписывает", "dfx_option_description": "Покупайте криптовалюту за EUR и CHF. Для розничных и корпоративных клиентов в Европе", "did_you_back_up_seeds": "Вы сделали резервную копию всех своих сид-фраз?", @@ -466,6 +474,8 @@ "extracted_address_content": "Вы отправляете средства\n${recipient_name}", "failed_authentication": "Ошибка аутентификации. ${state_error}", "faq": "FAQ", + "favorite_token": "Избранный токен", + "favorite_token_desc": "Баланс избранного токена будет отображаться на карточке баланса.", "features": "Возможности", "fee": "Комиссия", "fee_less_than_min": "Выбранная комиссия ниже минимальной. Пожалуйста, увеличьте комиссию, чтобы отправить транзакцию", @@ -491,6 +501,7 @@ "frozen": "Заморожено", "frozen_balance": "Замороженный баланс", "full_balance": "Полный баланс", + "gallery": "Галерея", "gas_exceeds_allowance": "Требуемый для транзакции газ превышает лимит.", "gas_price": "Цена газа", "general": "Общие", @@ -526,10 +537,10 @@ "how_to_connect": "Как подключиться", "how_to_use": "Как использовать", "how_to_use_card": "Как пользоваться этой картой", + "hww_exit_desc": "Это завершит текущий процесс сопряжения с вашим аппаратным устройством", "i_understand": "Я понимаю", "icon_style": "Стиль иконок", "id": "ID: ", - "if_you_dont_see_your_device": "Если вы не видите свое устройство выше, пожалуйста, убедитесь, что ваш Ledger включен и разблокирован!", "ignor": "Игнорировать", "import": "Импорт", "importNFTs": "Импортировать NFT", @@ -541,6 +552,7 @@ "infobox_auto_address": "Ваш адрес для получения будет меняться каждый раз, когда вы получаете транзакцию", "infobox_multichain": "Используйте этот адрес, чтобы получать любые токены или коллекционные предметы в", "infobox_rotation": "Ваш адрес для получения будет меняться каждый раз, когда вы закроете и снова откроете этот экран.", + "input": "Вход", "inputs": "Входы", "insufficient_funds_for_tx": "Недостаточно средств для успешного выполнения транзакции.", "insufficient_lamport_for_tx": "У вас недостаточно SOL, чтобы покрыть транзакцию и комиссию за неё. Пожалуйста, пополните кошелёк SOL или уменьшите сумму SOL, которую вы отправляете.", @@ -606,6 +618,7 @@ "manage_pow_nodes": "Управление PoW-узлами", "manage_providers": "Управление провайдерами", "manage_yats": "Управление Yat", + "manual_input": "Ручной ввод", "mark_as_redeemed": "Отметить как использовано", "market_place": "Маркетплейс", "mask": "Замаскировать", @@ -651,6 +664,7 @@ "nanogpt_subtitle": "Все новейшие модели (GPT-5.3, Claude 4.6).\nБез подписки, оплата криптовалютой.", "narrow": "Узкий", "network": "Сеть", + "networks": "Сети", "new_first_wallet_text": "Хранить вашу криптовалюту в безопасности — проще простого", "new_node_testing": "Тестирование новой ноды", "new_subaddress_create": "Создать", @@ -679,8 +693,10 @@ "node_new": "Новый узел", "node_port": "Порт узла", "node_reset_settings_title": "Сброс настроек", + "node_share_desc": "Отсканируйте этот QR-код на экране сканирования, чтобы импортировать этот узел.", "node_test": "Тест", "nodes": "Узлы", + "nodes_desc": "Управляйте существующими узлами или добавьте пользовательский.", "nodes_list_reset_to_default_message": "Вы уверены, что хотите сбросить настройки до значений по умолчанию?", "none_of_selected_providers_can_exchange": "Ни один из выбранных провайдеров не может выполнить этот обмен", "noNFTYet": "Пока нет NFT", @@ -690,6 +706,7 @@ "note_tap_to_change": "Примечание (нажмите, чтобы изменить)", "notification_permission_denied": "Разрешение на уведомления было навсегда отклонено, пожалуйста, включите его вручную в настройках", "nullURIError": "URI равен null", + "ocp_desc": "Платите криптовалютой в магазинах", "offer_expires_in": "Предложение истекает через: ", "offline": "Офлайн", "ok": "OK", @@ -717,8 +734,13 @@ "outputs": "Выходы", "overshot": "Превышено", "overwrite_amount": "Перезаписать сумму", + "pairing_code": "Код сопряжения", + "pairing_code_desc_1": "Ваше устройство покажет код сопряжения.", + "pairing_code_desc_2": "Введите его здесь, чтобы продолжить", + "pairing_error": "Ошибка сопряжения", "pairingInvalidEvent": "Недопустимое событие сопряжения", "passphrase": "Пассфраза (необязательно)", + "passphrase_view_keys": "Парольная фраза", "passphrases_doesnt_match": "Пасфразы не совпадают, попробуйте ещё раз", "password": "Пароль", "paste": "Вставить", @@ -734,6 +756,7 @@ "payjoin_unavailable_sheet_content": "Для получения транзакции Payjoin у вас должны быть готовы к использованию BTC.\n\nКак только на вашем кошельке появятся средства, Payjoin будет автоматически включён.", "payjoin_unavailable_sheet_title": "Почему Payjoin недоступен?", "payment_id": "ID платежа: ", + "payment_invoices": "Платёжные счета", "payment_made_easy": "Платежи — это просто", "payment_was_received": "Ваш платеж получен.", "payments": "Платежи", @@ -813,6 +836,7 @@ "receive_to": "Получить на", "received": "Получено", "receiving": "Получение", + "recieving": "Получение", "recipient_address": "Адрес получателя", "reconnect": "Переподключиться", "reconnect_alert_text": "Вы уверены, что хотите переподключиться?", @@ -888,6 +912,7 @@ "save": "Сохранить", "save_backup_password": "Пожалуйста, убедитесь, что вы сохранили пароль для резервной копии. Без него вы не сможете импортировать файлы резервной копии.", "save_backup_password_alert": "Сохранить пароль для резервной копии", + "save_contact": "Сохранить контакт", "save_to_downloads": "Сохранить в папку «Загрузки»", "saved_the_trade_id": "Я сохранил ID обмена", "savings": "Сбережения", @@ -1037,6 +1062,7 @@ "setup_your_debit_card": "Настройте свою дебетовую карту", "share": "Поделиться", "share_address": "Поделиться адресом", + "share_this_node": "Поделиться этим узлом", "shared_seed_wallet_groups": "Группы кошельков с общим сидом", "shared_seed_wallets": "Кошельки с общим сидом", "show": "Показать", @@ -1044,6 +1070,7 @@ "show_balance": "Нажмите и удерживайте, чтобы показать баланс", "show_balance_send_page": "Нажмите, чтобы показать", "show_balance_toast": "Нажмите и удерживайте, чтобы скрыть или показать баланс", + "show_combined_balance": "Показать объединённый баланс", "show_details": "Показать подробности", "show_hidden_addresses": "Показать скрытые адреса", "show_keys": "Показать seed/ключи", @@ -1109,6 +1136,7 @@ "swap_providers": "Провайдеры обмена", "sweeping_wallet": "Сканирование кошелька", "sweeping_wallet_alert": "Это не должно занять много времени. НЕ ПОКИДАЙТЕ ЭТОТ ЭКРАН, ИНАЧЕ ПЕРЕНЕСЁННЫЕ СРЕДСТВА МОГУТ БЫТЬ ПОТЕРЯНЫ.", + "swipe_to_bridge": "Проведите, чтобы выполнить перенос через мост", "swipe_to_send": "Проведите, чтобы отправить", "switch_wallet": "Переключить кошелёк", "switchToETHWallet": "Пожалуйста, переключитесь на кошелёк Ethereum и повторите попытку", @@ -1149,6 +1177,7 @@ "testnet_coins_no_value": "Монеты тестнета не имеют ценности", "third_intro_content": "Yat существуют и за пределами Cake Wallet. Любой адрес кошелька на земле можно заменить на Yat!", "third_intro_title": "Yat хорошо работает с другими", + "this_can_take_few_seconds": "Это может занять несколько секунд", "this_pair_is_not_supported_warning": "Эта пара не поддерживается выбранной(ыми) биржей(ами). Пожалуйста, выберите другую биржу.", "thorchain_contract_address_not_supported": "THORChain не поддерживает отправку на адрес контракта", "thorchain_taproot_address_not_supported": "Провайдер ThorChain не поддерживает Taproot-адреса. Пожалуйста, измените адрес или выберите другого провайдера.", @@ -1162,6 +1191,7 @@ "token_decimal": "Десятичные знаки токена (например: USDT)", "token_name": "Название токена (например: Tether)", "token_symbol": "Символ токена (например: USDT)", + "token_verified_on_jupiter": "Этот токен проверен на Jupiter", "tokenID": "ID", "tokens": "Токены", "tokens_strong_warning": "Никогда не отправляйте токены на этот адрес!", @@ -1290,6 +1320,7 @@ "username": "Имя пользователя", "username_available": "Имя пользователя доступно", "username_character_error": "Имя пользователя должно содержать только", + "username_check_your_network": "Не удалось проверить имя пользователя. Пожалуйста, проверьте подключение к сети.", "username_length_error": "Имя пользователя должно содержать 6–32 буквенно-цифровых символа.", "username_length_error_1_64": "Имя пользователя должно содержать 1–64 буквенно-цифровых символа.", "username_not_available": "Это имя пользователя недоступно.", @@ -1300,6 +1331,7 @@ "verify_message": "Проверить сообщение", "verify_seed": "Проверьте сид-фразу", "verify_with_2fa": "Подтвердить через Cake 2FA", + "verifying_code": "Проверка кода", "version": "Версия ${currentVersion}", "view_all": "Посмотреть все", "view_in_block_explorer": "Просмотреть в Block Explorer", @@ -1311,6 +1343,7 @@ "waitFewSecondForTxUpdate": "Пожалуйста, подождите несколько секунд, пока транзакция отобразится в истории транзакций.", "wallet": "Кошелек", "wallet_accounts": "Аккаунты кошелька", + "wallet_balance": "Баланс кошелька", "wallet_group": "Группа кошельков", "wallet_group_description_existing_seed": "Вы выбрали использование существующей сид-фразы для этого кошелька. При необходимости вы можете снова проверить сид-фразу, чтобы подтвердить её или записать.", "wallet_group_description_four": "создать кошелек с совершенно новой seed-фразой.", @@ -1348,9 +1381,11 @@ "walletConnect": "WalletConnect", "wallets": "Кошельки", "warning": "Предупреждение", + "wc_action_approve": "Одобрить", "wc_approve_request_title": "Утвердить запрос", "wc_connect_request_title": "Запрос на подключение", "wc_connected_to": "Подключено к ${name}", + "wc_desc": "Легко подключайтесь к dApps", "wc_max_network_fee": "Максимальная плата за сеть", "wc_message_to_sign": "Сообщение для подписи", "wc_network_fee": "Сетевая плата", @@ -1401,6 +1436,7 @@ "yat_popup_content": "Теперь вы можете отправлять и получать криптовалюту в Cake Wallet с помощью Yat — короткого имени пользователя на основе эмодзи. Управляйте Yat в любое время на экране настроек", "yat_popup_title": "Адрес вашего кошелька можно преобразовать в эмодзи.", "yes": "Да", + "yes_exit": "Да. Выйти", "yesterday": "Вчера", "you_now_have_debit_card": "Теперь у вас есть дебетовая карта", "you_pay": "Вы платите", @@ -1452,4 +1488,4 @@ "wc_action_approve": "Одобрить", "wc_desc": "Легко подключайтесь к dApps", "yes_exit": "Да. Выйти" -} \ No newline at end of file +} diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 669153910..dd6ee027e 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -76,6 +76,7 @@ "approve_tokens": "อนุมัติโทเค็น", "apps": "แอป", "arbiscan_history": "ประวัติ ArbiScan", + "are_you_sure_exit": "คุณแน่ใจหรือไม่ว่าต้องการออก?", "arrive_in_this_address": "${currency} ${tag}จะถูกส่งมาที่ที่อยู่นี้", "ascending": "จากน้อยไปมาก", "ask_each_time": "ถามทุกครั้ง", @@ -89,6 +90,7 @@ "auto_generate_addresses": "สร้างที่อยู่อัตโนมัติ", "auto_generate_subaddresses": "สร้างที่อยู่ย่อยอัตโนมัติ", "automatic": "อัตโนมัติ", + "available": "พร้อมใช้งาน", "available_balance": "ยอดคงเหลือที่ใช้ได้", "available_balance_description": "“ยอดคงเหลือที่ใช้ได้” หรือ “ยอดคงเหลือที่ยืนยันแล้ว” คือเงินที่สามารถใช้จ่ายได้ทันที หากเงินปรากฏในยอดคงเหลือด้านล่างแต่ไม่ปรากฏในยอดคงเหลือด้านบน คุณต้องรอสักครู่เพื่อให้เงินที่เข้ามาได้รับการยืนยันจากเครือข่ายเพิ่มขึ้น เมื่อได้รับการยืนยันมากขึ้นแล้ว เงินนั้นจะสามารถใช้จ่ายได้", "avg_savings": "การประหยัดเฉลี่ย", @@ -125,6 +127,7 @@ "block_remaining": "เหลืออีก 1 บล็อก", "Blocks_remaining": "${status} บล็อกที่เหลือ", "bluetooth": "บลูทูธ", + "bridging": "กำลังบริดจ์", "bright_theme": "สว่าง", "bscscan_history": "ประวัติ BSCScan", "bump_fee": "เพิ่มค่าธรรมเนียม", @@ -216,6 +219,8 @@ "color_theme": "ธีมสี", "coming_soon_tag": "เร็ว ๆ นี้", "commit_transaction_amount_fee": "ยืนยันธุรกรรม\nจำนวน: ${amount}\nค่าธรรมเนียม: ${fee}", + "compatible_services": "บริการที่รองรับ", + "compatible_services_desc": "ใช้เครื่องมือสแกนเพื่ออ่าน QR Code ประเภทต่อไปนี้ที่ Cake Wallet รองรับ:", "configure": "ตั้งค่า", "confirm": "ยืนยัน", "confirm_delete_template": "การดำเนินการนี้จะลบเทมเพลตนี้ คุณต้องการดำเนินการต่อหรือไม่?", @@ -279,6 +284,7 @@ "creating_new_wallet": "กำลังสร้างกระเป๋าเงินใหม่", "creating_new_wallet_error": "ข้อผิดพลาด: ${description}", "creation_date": "วันที่สร้าง", + "crypto_addresses": "ที่อยู่คริปโต", "csv_nothing_to_export": "ไม่มีข้อมูลให้ส่งออก", "current_swap": "การสวอปปัจจุบัน", "current_wallet": "กระเป๋าเงินปัจจุบัน", @@ -301,6 +307,7 @@ "default_sell_provider": "ผู้ให้บริการขายเริ่มต้น", "delete": "ลบ", "delete_account": "ลบบัญชี", + "delete_this_node": "ลบโหนดนี้", "delete_wallet": "ลบกระเป๋าเงิน", "delete_wallet_confirm_message": "คุณแน่ใจหรือไม่ว่าต้องการลบกระเป๋าเงิน ${wallet_name}?", "deleteConnectionConfirmationPrompt": "คุณแน่ใจหรือไม่ว่าต้องการลบการเชื่อมต่อกับ", @@ -330,6 +337,7 @@ "deuro_savings_welcome_description": "dEURO เป็นสเตเบิลคอยน์แบบกระจายอำนาจอย่างเต็มรูปแบบ มีหลักประกัน และตรึงมูลค่ากับยูโร — ปลอดภัย อเนกประสงค์ และทนต่อการเซ็นเซอร์ โดยไม่ต้องพึ่งพาออราเคิล 1 dEURO = 1 EURO.", "deuro_tooltip_no_eth": "หากต้องการเปิดใช้งาน dEURO คุณต้องมี ETH ในกระเป๋าเงินของคุณ", "deuro_tx_commited_content": "อาจใช้เวลาสองสามวินาทีเพื่อให้ธุรกรรมได้รับการยืนยันและแสดงบนหน้าจอ", + "device_confirmation": "การยืนยันบนอุปกรณ์", "device_is_signing": "อุปกรณ์กำลังลงนาม", "dfx_option_description": "ซื้อคริปโตด้วย EUR และ CHF สำหรับลูกค้ารายย่อยและลูกค้าองค์กรในยุโรป", "did_you_back_up_seeds": "คุณได้สำรอง seed ทั้งหมดของคุณแล้วหรือยัง?", @@ -466,6 +474,8 @@ "extracted_address_content": "คุณกำลังจะส่งเงินทุนไปยัง\n${recipient_name}", "failed_authentication": "การยืนยันตัวตนล้มเหลว ${state_error}", "faq": "คำถามที่พบบ่อย", + "favorite_token": "โทเค็นโปรด", + "favorite_token_desc": "ยอดคงเหลือของโทเค็นโปรดจะแสดงบนการ์ดยอดคงเหลือ", "features": "คุณสมบัติ", "fee": "ค่าธรรมเนียม", "fee_less_than_min": "ค่าธรรมเนียมที่เลือกต่ำกว่าขั้นต่ำ โปรดเพิ่มค่าธรรมเนียมเพื่อให้สามารถส่งธุรกรรมได้", @@ -491,6 +501,7 @@ "frozen": "ถูกระงับ", "frozen_balance": "ยอดคงเหลือที่ถูกระงับ", "full_balance": "ยอดคงเหลือทั้งหมด", + "gallery": "แกลเลอรี", "gas_exceeds_allowance": "Gas ที่ธุรกรรมต้องใช้เกินกว่าค่าเผื่อที่กำหนด", "gas_price": "ราคา Gas", "general": "ทั่วไป", @@ -526,10 +537,10 @@ "how_to_connect": "วิธีเชื่อมต่อ", "how_to_use": "วิธีใช้งาน", "how_to_use_card": "วิธีใช้การ์ดนี้", + "hww_exit_desc": "การดำเนินการนี้จะสิ้นสุดกระบวนการจับคู่กับอุปกรณ์ฮาร์ดแวร์ของคุณที่กำลังดำเนินอยู่", "i_understand": "ฉันเข้าใจ", "icon_style": "สไตล์ไอคอน", "id": "ID: ", - "if_you_dont_see_your_device": "หากคุณไม่เห็นอุปกรณ์ของคุณด้านบน โปรดตรวจสอบให้แน่ใจว่า Ledger ของคุณเปิดอยู่และปลดล็อกแล้ว!", "ignor": "เพิกเฉย", "import": "นำเข้า", "importNFTs": "นำเข้า NFT", @@ -541,6 +552,7 @@ "infobox_auto_address": "ที่อยู่สำหรับรับของคุณจะเปลี่ยนทุกครั้งที่คุณได้รับธุรกรรม", "infobox_multichain": "ใช้ที่อยู่นี้เพื่อรับโทเค็นหรือของสะสมใด ๆ บน", "infobox_rotation": "ที่อยู่สำหรับรับของคุณจะเปลี่ยนทุกครั้งที่คุณปิดและเปิดหน้าจอนี้ใหม่", + "input": "อินพุต", "inputs": "อินพุต", "insufficient_funds_for_tx": "ยอดเงินไม่เพียงพอที่จะดำเนินการธุรกรรมให้สำเร็จ", "insufficient_lamport_for_tx": "คุณมี SOL ไม่เพียงพอที่จะครอบคลุมธุรกรรมและค่าธรรมเนียมธุรกรรม กรุณาเพิ่ม SOL ในกระเป๋าเงินของคุณหรือปรับลดจำนวน SOL ที่คุณกำลังส่ง", @@ -606,6 +618,7 @@ "manage_pow_nodes": "จัดการโหนด PoW", "manage_providers": "จัดการผู้ให้บริการ", "manage_yats": "จัดการ Yats", + "manual_input": "ป้อนข้อมูลด้วยตนเอง", "mark_as_redeemed": "ทำเครื่องหมายว่าแลกใช้แล้ว", "market_place": "มาร์เก็ตเพลส", "mask": "มาสก์", @@ -651,6 +664,7 @@ "nanogpt_subtitle": "โมเดลใหม่ล่าสุดทั้งหมด (GPT-5.3, Claude 4.6)\nไม่ต้องสมัครสมาชิก จ่ายด้วยคริปโต", "narrow": "แคบ", "network": "เครือข่าย", + "networks": "เครือข่าย", "new_first_wallet_text": "การเก็บคริปโตของคุณให้ปลอดภัยเป็นเรื่องง่ายเหมือนปอกกล้วยเข้าปาก", "new_node_testing": "การทดสอบโหนดใหม่", "new_subaddress_create": "สร้าง", @@ -679,8 +693,10 @@ "node_new": "โหนดใหม่", "node_port": "พอร์ตโหนด", "node_reset_settings_title": "รีเซ็ตการตั้งค่า", + "node_share_desc": "สแกน QR Code นี้จากหน้าจอสแกนเพื่อนำเข้าโหนดนี้", "node_test": "ทดสอบ", "nodes": "โหนด", + "nodes_desc": "จัดการโหนดที่มีอยู่หรือเพิ่มโหนดแบบกำหนดเอง", "nodes_list_reset_to_default_message": "คุณแน่ใจหรือไม่ว่าต้องการรีเซ็ตการตั้งค่าเป็นค่าเริ่มต้น?", "none_of_selected_providers_can_exchange": "ไม่มีผู้ให้บริการที่เลือกไว้รายใดสามารถทำการสวอปนี้ได้", "noNFTYet": "ยังไม่มี NFT", @@ -690,6 +706,7 @@ "note_tap_to_change": "หมายเหตุ (แตะเพื่อเปลี่ยน)", "notification_permission_denied": "สิทธิ์การแจ้งเตือนถูกปฏิเสธอย่างถาวร โปรดเปิดใช้งานด้วยตนเองในการตั้งค่า", "nullURIError": "URI เป็นค่าว่าง", + "ocp_desc": "ชำระเงินด้วยคริปโตในร้านค้า", "offer_expires_in": "ข้อเสนอจะหมดอายุใน: ", "offline": "ออฟไลน์", "ok": "ตกลง", @@ -716,8 +733,13 @@ "outputs": "เอาต์พุต", "overshot": "เกิน", "overwrite_amount": "เขียนทับจำนวนเงิน", + "pairing_code": "รหัสจับคู่", + "pairing_code_desc_1": "อุปกรณ์ของคุณจะแสดงรหัสจับคู่", + "pairing_code_desc_2": "ป้อนที่นี่เพื่อดำเนินการต่อ", + "pairing_error": "เกิดข้อผิดพลาดในการจับคู่", "pairingInvalidEvent": "เหตุการณ์การจับคู่ไม่ถูกต้อง", "passphrase": "วลีรหัสผ่าน (ไม่บังคับ)", + "passphrase_view_keys": "วลีรหัสผ่าน", "passphrases_doesnt_match": "Passphrase ไม่ตรงกัน โปรดลองอีกครั้ง", "password": "รหัสผ่าน", "paste": "วาง", @@ -733,6 +755,7 @@ "payjoin_unavailable_sheet_content": "การรับธุรกรรม Payjoin จำเป็นต้องมี Bitcoin พร้อมใช้งาน\n\nเมื่อกระเป๋าเงินของคุณมีเงินแล้ว Payjoin จะถูกเปิดใช้งานโดยอัตโนมัติ", "payjoin_unavailable_sheet_title": "ทำไม Payjoin ถึงใช้งานไม่ได้?", "payment_id": "รหัสการชำระเงิน: ", + "payment_invoices": "ใบแจ้งหนี้การชำระเงิน", "payment_made_easy": "การชำระเงินที่ง่ายขึ้น", "payment_was_received": "ได้รับการชำระเงินของคุณแล้ว", "payments": "การชำระเงิน", @@ -812,6 +835,7 @@ "receive_to": "รับไปยัง", "received": "ได้รับ", "receiving": "กำลังรับ", + "recieving": "กำลังรับ", "recipient_address": "ที่อยู่ผู้รับ", "reconnect": "เชื่อมต่อใหม่", "reconnect_alert_text": "คุณแน่ใจหรือไม่ว่าต้องการเชื่อมต่อใหม่?", @@ -887,6 +911,7 @@ "save": "บันทึก", "save_backup_password": "โปรดตรวจสอบให้แน่ใจว่าคุณได้บันทึกรหัสผ่านสำรองข้อมูลไว้แล้ว คุณจะไม่สามารถนำเข้าไฟล์สำรองข้อมูลของคุณได้หากไม่มีรหัสผ่านดังกล่าว", "save_backup_password_alert": "บันทึกรหัสผ่านสำหรับการสำรองข้อมูล", + "save_contact": "บันทึกผู้ติดต่อ", "save_to_downloads": "บันทึกไปยังโฟลเดอร์ดาวน์โหลด", "saved_the_trade_id": "ฉันได้บันทึก ID การเทรดไว้แล้ว", "savings": "เงินออม", @@ -1036,6 +1061,7 @@ "setup_your_debit_card": "ตั้งค่าบัตรเดบิตของคุณ", "share": "แชร์", "share_address": "แชร์ที่อยู่", + "share_this_node": "แชร์โหนดนี้", "shared_seed_wallet_groups": "กลุ่มกระเป๋าเงินที่ใช้ Seed ร่วมกัน", "shared_seed_wallets": "กระเป๋าเงิน Seed ที่ใช้ร่วมกัน", "show": "แสดง", @@ -1043,6 +1069,7 @@ "show_balance": "กดค้างเพื่อแสดงยอดคงเหลือ", "show_balance_send_page": "แตะเพื่อแสดง", "show_balance_toast": "กดค้างเพื่อซ่อนหรือแสดงยอดคงเหลือ", + "show_combined_balance": "แสดงยอดคงเหลือรวม", "show_details": "แสดงรายละเอียด", "show_hidden_addresses": "แสดงที่อยู่ที่ซ่อนอยู่", "show_keys": "แสดงซีด/คีย์", @@ -1108,6 +1135,7 @@ "swap_providers": "ผู้ให้บริการสวอป", "sweeping_wallet": "กระเป๋าสตางค์สำหรับกวาดเหรียญ", "sweeping_wallet_alert": "การดำเนินการนี้ใช้เวลาไม่นาน ห้ามออกจากหน้าจอนี้ มิฉะนั้นเงินที่กวาดอาจสูญหาย", + "swipe_to_bridge": "ปัดเพื่อบริดจ์", "swipe_to_send": "ปัดเพื่อส่ง", "switch_wallet": "สลับกระเป๋าเงิน", "switchToETHWallet": "โปรดสลับไปยังกระเป๋าเงิน Ethereum แล้วลองอีกครั้ง", @@ -1148,6 +1176,7 @@ "testnet_coins_no_value": "เหรียญ Testnet ไม่มีมูลค่า", "third_intro_content": "Yat ใช้ได้นอก Cake Wallet ด้วย ที่อยู่กระเป๋าเงินใดๆ ทั่วโลกสามารถแทนที่ด้วย Yat ได้!", "third_intro_title": "Yat ทำงานร่วมกับสิ่งอื่นได้อย่างราบรื่น", + "this_can_take_few_seconds": "การดำเนินการนี้อาจใช้เวลาสักครู่", "this_pair_is_not_supported_warning": "คู่นี้ไม่รองรับกับแพลตฟอร์มแลกเปลี่ยนที่เลือกอยู่ในขณะนี้ โปรดเลือกแพลตฟอร์มแลกเปลี่ยนอื่น", "thorchain_contract_address_not_supported": "THORChain ไม่รองรับการส่งไปยังที่อยู่สัญญา", "thorchain_taproot_address_not_supported": "ผู้ให้บริการ ThorChain ไม่รองรับที่อยู่ Taproot โปรดเปลี่ยนที่อยู่หรือเลือกผู้ให้บริการอื่น", @@ -1161,6 +1190,7 @@ "token_decimal": "ทศนิยมของโทเค็น (เช่น: USDT)", "token_name": "ชื่อโทเค็น (เช่น: Tether)", "token_symbol": "สัญลักษณ์โทเค็น (เช่น: USDT)", + "token_verified_on_jupiter": "โทเค็นนี้ได้รับการตรวจสอบบน Jupiter", "tokenID": "ID", "tokens": "โทเค็น", "tokens_strong_warning": "ห้ามส่งโทเค็นไปยังที่อยู่นี้เด็ดขาด!", @@ -1289,6 +1319,7 @@ "username": "ชื่อผู้ใช้", "username_available": "ชื่อผู้ใช้พร้อมใช้งาน", "username_character_error": "ชื่อผู้ใช้ต้องมีเฉพาะ", + "username_check_your_network": "ไม่สามารถตรวจสอบชื่อผู้ใช้ได้ โปรดตรวจสอบการเชื่อมต่อเครือข่ายของคุณ", "username_length_error": "ชื่อผู้ใช้ต้องมีอักขระตัวอักษรและตัวเลข 6-32 ตัว", "username_length_error_1_64": "ชื่อผู้ใช้ต้องมีอักขระตัวอักษรและตัวเลข 1-64 ตัว", "username_not_available": "ชื่อผู้ใช้นี้ไม่พร้อมใช้งาน", @@ -1299,6 +1330,7 @@ "verify_message": "ตรวจสอบข้อความ", "verify_seed": "ยืนยัน Seed", "verify_with_2fa": "ยืนยันด้วย Cake 2FA", + "verifying_code": "กำลังตรวจสอบรหัส", "version": "เวอร์ชัน ${currentVersion}", "view_all": "ดูทั้งหมด", "view_in_block_explorer": "ดูใน Block Explorer", @@ -1310,6 +1342,7 @@ "waitFewSecondForTxUpdate": "กรุณารอสักครู่เพื่อให้ธุรกรรมแสดงในประวัติธุรกรรม", "wallet": "วอลเล็ต", "wallet_accounts": "บัญชีกระเป๋าเงิน", + "wallet_balance": "ยอดคงเหลือในกระเป๋าเงิน", "wallet_group": "กลุ่มวอลเล็ต", "wallet_group_description_existing_seed": "คุณได้เลือกใช้ซีดที่มีอยู่สำหรับกระเป๋าเงินนี้ คุณสามารถตรวจสอบซีดอีกครั้งได้ หากต้องการยืนยันหรือจดบันทึกไว้", "wallet_group_description_four": "เพื่อสร้างกระเป๋าเงินด้วยซีดใหม่ทั้งหมด", @@ -1347,9 +1380,11 @@ "walletConnect": "WalletConnect", "wallets": "กระเป๋าเงิน", "warning": "คำเตือน", + "wc_action_approve": "อนุมัติ", "wc_approve_request_title": "อนุมัติคำขอ", "wc_connect_request_title": "เชื่อมต่อคำขอ", "wc_connected_to": "เชื่อมต่อกับ ${name}", + "wc_desc": "เชื่อมต่อกับ DApps ได้อย่างง่ายดาย", "wc_max_network_fee": "ค่าธรรมเนียมเครือข่ายสูงสุด", "wc_message_to_sign": "ข้อความที่จะลงนาม", "wc_network_fee": "ค่าธรรมเนียมเครือข่าย", @@ -1400,6 +1435,7 @@ "yat_popup_content": "ขณะนี้คุณสามารถส่งและรับคริปโตใน Cake Wallet ด้วย Yat ของคุณ — ชื่อผู้ใช้แบบสั้นที่ใช้ อีโมจิ เป็นหลัก จัดการ Yat ได้ทุกเมื่อบนหน้าจอการตั้งค่า", "yat_popup_title": "ที่อยู่กระเป๋าเงินของคุณสามารถแปลงเป็นอีโมจิได้", "yes": "ใช่", + "yes_exit": "ใช่ ออก", "yesterday": "เมื่อวาน", "you_now_have_debit_card": "ตอนนี้คุณมีบัตรเดบิตแล้ว", "you_pay": "คุณจ่าย", @@ -1451,4 +1487,4 @@ "wc_action_approve": "อนุมัติ", "wc_desc": "เชื่อมต่อกับ DApps ได้อย่างง่ายดาย", "yes_exit": "ใช่ ออก" -} \ No newline at end of file +} diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 8e23566a4..55cbebb32 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -76,6 +76,7 @@ "approve_tokens": "Aprubahan ang mga token", "apps": "Apps", "arbiscan_history": "Kasaysayan ng ArbiScan", + "are_you_sure_exit": "Sigurado ka bang gusto mong lumabas?", "arrive_in_this_address": "Darating ang ${currency} ${tag} sa address na ito", "ascending": "Pataas", "ask_each_time": "Magtanong sa bawat pagkakataon", @@ -89,6 +90,7 @@ "auto_generate_addresses": "Awtomatikong bumuo ng mga address", "auto_generate_subaddresses": "Awtomatikong bumuo ng mga subaddress", "automatic": "Awtomatiko", + "available": "Available", "available_balance": "Magagamit na Balanse", "available_balance_description": "Ang “Magagamit na Balanse” o “Nakumpirmang Balanse” ay mga pondong maaari nang gastusin kaagad. Kung lumalabas ang mga pondo sa mas mababang balanse ngunit wala sa itaas na balanse, kailangan mong maghintay ng ilang minuto para makakuha ng mas maraming kumpirmasyon sa network ang mga papasok na pondo. Kapag nakakuha na sila ng mas maraming kumpirmasyon, maaari na silang gastusin.", "avg_savings": "Avg. Matitipid", @@ -125,6 +127,7 @@ "block_remaining": "1 Block ang Natitira", "Blocks_remaining": "${status} Natitirang Block", "bluetooth": "Bluetooth", + "bridging": "Bina-bridge", "bright_theme": "Maningning", "bscscan_history": "Kasaysayan ng BSCScan", "bump_fee": "Itaas ang bayarin", @@ -216,6 +219,8 @@ "color_theme": "Tema ng kulay", "coming_soon_tag": "Paparating na", "commit_transaction_amount_fee": "I-commit ang transaksyon\nHalaga: ${amount}\nBayad: ${fee}", + "compatible_services": "Mga Katugmang Serbisyo", + "compatible_services_desc": "Gamitin ang Scan tool upang basahin ang mga sumusunod na uri ng QR code na sinusuportahan sa Cake Wallet:", "configure": "I-configure", "confirm": "Kumpirmahin", "confirm_delete_template": "Ide-delete ng aksyong ito ang template na ito. Gusto mo bang magpatuloy?", @@ -279,6 +284,7 @@ "creating_new_wallet": "Gumagawa ng bagong wallet", "creating_new_wallet_error": "Error: ${description}", "creation_date": "Petsa ng Paglikha", + "crypto_addresses": "Mga Crypto Address", "csv_nothing_to_export": "Walang i-e-export", "current_swap": "Kasalukuyang Swap", "current_wallet": "Kasalukuyang wallet", @@ -301,6 +307,7 @@ "default_sell_provider": "Default na Provider ng Pagbebenta", "delete": "Tanggalin", "delete_account": "I-delete ang Account", + "delete_this_node": "Tanggalin ang node na ito", "delete_wallet": "I-delete ang wallet", "delete_wallet_confirm_message": "Sigurado ka bang gusto mong tanggalin ang wallet na ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Sigurado ka bang gusto mong tanggalin ang koneksyon sa", @@ -330,6 +337,7 @@ "deuro_savings_welcome_description": "Ang dEURO ay isang ganap na desentralisado at collateralized na stablecoin na naka-peg sa Euro — secure, maraming gamit, at lumalaban sa censorship nang hindi kailangang umasa sa mga oracle. 1 dEURO = 1 EURO.", "deuro_tooltip_no_eth": "Upang paganahin ang dEURO, dapat ay may ETH ka sa iyong wallet.", "deuro_tx_commited_content": "Maaaring tumagal ng ilang segundo bago makumpirma ang transaksyon at lumabas sa screen", + "device_confirmation": "Kumpirmasyon sa Device", "device_is_signing": "Pumipirma ang device", "dfx_option_description": "Bumili ng crypto gamit ang EUR at CHF. Para sa mga retail at corporate na customer sa Europe", "did_you_back_up_seeds": "Na-back up mo na ba ang lahat ng iyong seed?", @@ -466,6 +474,8 @@ "extracted_address_content": "Magpapadala ka ng mga pondo sa\n${recipient_name}", "failed_authentication": "Nabigo ang pag-authenticate. ${state_error}", "faq": "FAQ", + "favorite_token": "Paboritong token", + "favorite_token_desc": "Ipapakita ang balanse ng paboritong token sa balance card.", "features": "Mga Tampok", "fee": "Bayarin", "fee_less_than_min": "Ang napiling bayad ay mas mababa kaysa sa minimum. Mangyaring taasan ang bayad upang maipadala ang transaksyon.", @@ -491,6 +501,7 @@ "frozen": "Naka-freeze", "frozen_balance": "Naka-freeze na balanse", "full_balance": "Buong Balanse", + "gallery": "Gallery", "gas_exceeds_allowance": "Ang gas na kailangan para sa transaksyon ay lumampas sa itinakdang limitasyon.", "gas_price": "Presyo ng gas", "general": "Pangkalahatan", @@ -526,10 +537,10 @@ "how_to_connect": "Paano Kumonekta", "how_to_use": "Paano gamitin", "how_to_use_card": "Paano gamitin ang card na ito", + "hww_exit_desc": "Tatapusin nito ang kasalukuyang proseso ng pagpa-pair sa iyong hardware device", "i_understand": "Naiintindihan ko", "icon_style": "Estilo ng icon", "id": "ID: ", - "if_you_dont_see_your_device": "Kung hindi mo nakikita ang iyong device sa itaas, pakitiyak na gising at naka-unlock ang iyong Ledger!", "ignor": "Huwag pansinin", "import": "I-import", "importNFTs": "Mag-import ng mga NFT", @@ -541,6 +552,7 @@ "infobox_auto_address": "Iikot ang iyong receive address sa tuwing makakatanggap ka ng transaksyon", "infobox_multichain": "Gamitin ang address na ito para makatanggap ng anumang token o collectible sa", "infobox_rotation": "Ang iyong receive address ay magpapalit sa tuwing isasara mo at muling bubuksan ang screen na ito", + "input": "Input", "inputs": "Mga input", "insufficient_funds_for_tx": "Hindi sapat ang pondo upang matagumpay na maisagawa ang transaksyon.", "insufficient_lamport_for_tx": "Wala kang sapat na SOL upang masakop ang transaksyon at ang bayad sa transaksyon nito. Mangyaring magdagdag pa ng SOL sa iyong wallet o bawasan ang halagang SOL na ipinapadala mo.", @@ -606,6 +618,7 @@ "manage_pow_nodes": "Pamahalaan ang mga PoW node", "manage_providers": "Pamahalaan ang mga provider", "manage_yats": "Pamahalaan ang Yats", + "manual_input": "Manu-manong Paglalagay", "mark_as_redeemed": "Markahan bilang Na-redeem", "market_place": "Marketplace", "mask": "Mask", @@ -651,6 +664,7 @@ "nanogpt_subtitle": "Lahat ng pinakabagong modelo (GPT-5.3, Claude 4.6).\nWalang subscription, magbayad gamit ang crypto.", "narrow": "Makitid", "network": "Network", + "networks": "Mga Network", "new_first_wallet_text": "Ang pagpapanatiling ligtas ng iyong crypto ay kasingdali ng pagkain ng keyk", "new_node_testing": "Pagsusuri ng bagong node", "new_subaddress_create": "Lumikha", @@ -679,8 +693,10 @@ "node_new": "Bagong Node", "node_port": "Port ng node", "node_reset_settings_title": "I-reset ang mga setting", + "node_share_desc": "I-scan ang QR code na ito mula sa Scan screen upang i-import ang node na ito.", "node_test": "Pagsubok", "nodes": "Mga Node", + "nodes_desc": "Pamahalaan ang mga umiiral na node o magdagdag ng custom.", "nodes_list_reset_to_default_message": "Sigurado ka bang gusto mong i-reset ang mga setting sa default?", "none_of_selected_providers_can_exchange": "Wala sa mga napiling provider ang makakagawa ng swap na ito", "noNFTYet": "Wala pang mga NFT", @@ -690,6 +706,7 @@ "note_tap_to_change": "Tala (i-tap para baguhin)", "notification_permission_denied": "Permanenteng tinanggihan ang pahintulot para sa mga abiso. Pakipaganahin ito nang manu-mano sa mga setting.", "nullURIError": "Null ang URI", + "ocp_desc": "Magbayad gamit ang crypto sa mga tindahan", "offer_expires_in": "Mag-e-expire ang alok sa: ", "offline": "Offline", "ok": "OK", @@ -716,8 +733,13 @@ "outputs": "Mga Output", "overshot": "Lumampas", "overwrite_amount": "Patungan ang halaga", + "pairing_code": "Pairing Code", + "pairing_code_desc_1": "Magpapakita ang iyong device ng isang pairing code.", + "pairing_code_desc_2": "Ilagay dito upang magpatuloy", + "pairing_error": "Error sa Pagpa-pair", "pairingInvalidEvent": "Hindi Wastong Kaganapan sa Pag-pair", "passphrase": "Passphrase (Opsyonal)", + "passphrase_view_keys": "Passphrase", "passphrases_doesnt_match": "Hindi magkatugma ang mga passphrase, pakisubukang muli", "password": "Password", "paste": "I-paste", @@ -733,6 +755,7 @@ "payjoin_unavailable_sheet_content": "Ang pagtanggap ng isang Payjoin na transaksyon ay nangangailangan na mayroon kang Bitcoin na handang gamitin.\n\nKapag may pondo na ang iyong wallet, awtomatikong maa-enable ang Payjoin.", "payjoin_unavailable_sheet_title": "Bakit hindi available ang Payjoin?", "payment_id": "Payment ID: ", + "payment_invoices": "Mga Invoice ng Pagbabayad", "payment_made_easy": "Pinadali ang mga pagbabayad", "payment_was_received": "Natanggap ang iyong pagbabayad.", "payments": "Mga Pagbabayad", @@ -812,6 +835,7 @@ "receive_to": "Tumanggap sa", "received": "Natanggap", "receiving": "Pagtanggap", + "recieving": "Tumatanggap", "recipient_address": "Address ng tatanggap", "reconnect": "Muling kumonekta", "reconnect_alert_text": "Sigurado ka bang gusto mong kumonekta muli?", @@ -887,6 +911,7 @@ "save": "I-save", "save_backup_password": "Pakisigurado na na-save mo ang iyong backup password. Hindi mo mai-import ang iyong mga backup file kung wala ito.", "save_backup_password_alert": "I-save ang password ng backup", + "save_contact": "I-save ang Contact", "save_to_downloads": "I-save sa Downloads", "saved_the_trade_id": "Na-save ko na ang trade ID", "savings": "Ipon", @@ -1036,6 +1061,7 @@ "setup_your_debit_card": "I-set up ang iyong debit card", "share": "Ibahagi", "share_address": "Ibahagi ang address", + "share_this_node": "Ibahagi ang node na ito", "shared_seed_wallet_groups": "Mga Grupo ng Wallet na may Ibinahaging Seed", "shared_seed_wallets": "Mga Wallet na may Shared Seed", "show": "Ipakita", @@ -1043,6 +1069,7 @@ "show_balance": "Pindutin nang matagal para ipakita ang balanse", "show_balance_send_page": "I-tap para ipakita", "show_balance_toast": "Pindutin nang matagal upang itago o ipakita ang balanse", + "show_combined_balance": "Ipakita ang pinagsamang balanse", "show_details": "Ipakita ang mga Detalye", "show_hidden_addresses": "Ipakita ang mga nakatagong address", "show_keys": "Ipakita ang seed/keys", @@ -1108,6 +1135,7 @@ "swap_providers": "Mga provider ng swap", "sweeping_wallet": "Sweeping wallet", "sweeping_wallet_alert": "Hindi ito dapat magtagal. HUWAG UMALIS SA SCREEN NA ITO O MAARING MAWALA ANG MGA NA-SWEEP NA PONDO.", + "swipe_to_bridge": "Mag-swipe para mag-bridge", "swipe_to_send": "I-swipe para magpadala", "switch_wallet": "Lumipat ng Wallet", "switchToETHWallet": "Mangyaring lumipat sa isang Ethereum wallet at subukang muli", @@ -1148,6 +1176,7 @@ "testnet_coins_no_value": "Walang halaga ang mga testnet coin", "third_intro_content": "Umiiral din ang Yats sa labas ng Cake Wallet. Anumang wallet address sa mundo ay maaaring palitan ng Yat!", "third_intro_title": "Maganda ang pakikisama ng Yat sa iba", + "this_can_take_few_seconds": "Maaaring tumagal ito ng ilang segundo", "this_pair_is_not_supported_warning": "Hindi sinusuportahan ang pares na ito sa kasalukuyang napiling (mga) exchange. Mangyaring pumili ng ibang exchange.", "thorchain_contract_address_not_supported": "Hindi sinusuportahan ng THORChain ang pagpapadala sa isang contract address", "thorchain_taproot_address_not_supported": "Hindi sinusuportahan ng ThorChain provider ang mga Taproot address. Pakibaguhin ang address o pumili ng ibang provider.", @@ -1161,6 +1190,7 @@ "token_decimal": "Desimal ng token (hal: USDT)", "token_name": "Pangalan ng token (hal: Tether)", "token_symbol": "Simbolo ng token (hal: USDT)", + "token_verified_on_jupiter": "Ang token na ito ay na-verify sa Jupiter", "tokenID": "ID", "tokens": "Mga Token", "tokens_strong_warning": "Huwag kailanman magpadala ng mga token sa address na ito!", @@ -1289,6 +1319,7 @@ "username": "Username", "username_available": "Available ang Username", "username_character_error": "Ang mga username ay dapat maglaman lamang ng", + "username_check_your_network": "Hindi ma-check ang username. Pakisuri ang iyong koneksyon sa network.", "username_length_error": "Ang mga username ay dapat maglaman ng 6-32 alphanumeric na karakter.", "username_length_error_1_64": "Dapat maglaman ang mga username ng 1-64 na alphanumeric na character.", "username_not_available": "Hindi available ang username na ito.", @@ -1299,6 +1330,7 @@ "verify_message": "I-verify ang Mensahe", "verify_seed": "I-verify ang Seed", "verify_with_2fa": "I-verify gamit ang Cake 2FA", + "verifying_code": "Vine-verify ang Code", "version": "Bersyon ${currentVersion}", "view_all": "Tingnan lahat", "view_in_block_explorer": "Tingnan sa Block Explorer", @@ -1310,6 +1342,7 @@ "waitFewSecondForTxUpdate": "Mangyaring maghintay ng ilang segundo para lumabas ang transaksyon sa kasaysayan ng mga transaksyon", "wallet": "Wallet", "wallet_accounts": "Mga Account ng Wallet", + "wallet_balance": "Balanse ng Wallet", "wallet_group": "Grupo ng Wallet", "wallet_group_description_existing_seed": "Pinili mong gumamit ng umiiral na seed para sa wallet na ito. Maaari mong i-verify muli ang seed kung kailangan mong kumpirmahin o isulat ito.", "wallet_group_description_four": "upang lumikha ng wallet na may ganap na bagong seed.", @@ -1347,9 +1380,11 @@ "walletConnect": "WalletConnect", "wallets": "Mga Wallet", "warning": "Babala", + "wc_action_approve": "Aprubahan", "wc_approve_request_title": "Aprubahan ang kahilingan", "wc_connect_request_title": "Kahilingan sa pagkonekta", "wc_connected_to": "Nakakonekta sa ${name}", + "wc_desc": "Madaling kumonekta sa mga DApp", "wc_max_network_fee": "Pinakamataas na bayad sa network", "wc_message_to_sign": "Mensahe para pirmahan", "wc_network_fee": "Bayad sa network", @@ -1400,6 +1435,7 @@ "yat_popup_content": "Maaari ka na ngayong magpadala at tumanggap ng crypto sa Cake Wallet gamit ang iyong Yat — isang maikling username na binubuo ng mga emoji. Pamahalaan ang mga Yat anumang oras sa screen ng mga setting", "yat_popup_title": "Maaaring gawing emoji ang address ng iyong wallet.", "yes": "Oo", + "yes_exit": "Oo. Lumabas", "yesterday": "Kahapon", "you_now_have_debit_card": "Mayroon ka na ngayong debit card", "you_pay": "Ikaw ang Magbabayad", @@ -1451,4 +1487,4 @@ "wc_action_approve": "Aprubahan", "wc_desc": "Madaling kumonekta sa mga DApp", "yes_exit": "Oo. Lumabas" -} \ No newline at end of file +} diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index beb1e6f02..5b6dc47ce 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -76,6 +76,7 @@ "approve_tokens": "Tokenları onaylayın", "apps": "Uygulamalar", "arbiscan_history": "ArbiScan Geçmişi", + "are_you_sure_exit": "Çıkmak istediğinizden emin misiniz?", "arrive_in_this_address": "${currency} ${tag}bu adrese ulaşacaktır", "ascending": "Artan", "ask_each_time": "Her seferinde sor", @@ -89,6 +90,7 @@ "auto_generate_addresses": "Adresleri otomatik oluştur", "auto_generate_subaddresses": "Alt adresleri otomatik oluştur", "automatic": "Otomatik", + "available": "Kullanılabilir", "available_balance": "Kullanılabilir Bakiye", "available_balance_description": "“Kullanılabilir Bakiye” veya “Onaylanmış Bakiye”, hemen harcanabilen fonlardır. Fonlar alt bakiyede görünüp üst bakiyede görünmüyorsa, gelen fonların ağda daha fazla onay alması için birkaç dakika beklemeniz gerekir. Daha fazla onay aldıktan sonra harcanabilir olacaktır.", "avg_savings": "Ort. Tasarruf", @@ -125,6 +127,7 @@ "block_remaining": "Kalan 1 Blok", "Blocks_remaining": "${status} Blok Kaldı", "bluetooth": "Bluetooth", + "bridging": "Köprüleniyor", "bright_theme": "Parlak", "bscscan_history": "BSCScan Geçmişi", "bump_fee": "Ücreti artır", @@ -216,6 +219,8 @@ "color_theme": "Renk teması", "coming_soon_tag": "Yakında", "commit_transaction_amount_fee": "İşlemi onayla\nTutar: ${amount}\nÜcret: ${fee}", + "compatible_services": "Uyumlu Hizmetler", + "compatible_services_desc": "Cake Wallet'ta desteklenen aşağıdaki QR kodu türlerini okumak için Tarama aracını kullanın:", "configure": "Yapılandır", "confirm": "Onayla", "confirm_delete_template": "Bu işlem bu şablonu silecek. Devam etmek istiyor musunuz?", @@ -279,6 +284,7 @@ "creating_new_wallet": "Yeni cüzdan oluşturuluyor", "creating_new_wallet_error": "Hata: ${description}", "creation_date": "Oluşturulma Tarihi", + "crypto_addresses": "Kripto Adresleri", "csv_nothing_to_export": "Dışa aktarılacak bir şey yok", "current_swap": "Mevcut Takas", "current_wallet": "Geçerli cüzdan", @@ -301,6 +307,7 @@ "default_sell_provider": "Varsayılan Satış Sağlayıcısı", "delete": "Sil", "delete_account": "Hesabı Sil", + "delete_this_node": "Bu düğümü sil", "delete_wallet": "Cüzdanı sil", "delete_wallet_confirm_message": "${wallet_name} cüzdanını silmek istediğinden emin misin?", "deleteConnectionConfirmationPrompt": "Şuraya olan bağlantıyı silmek istediğinizden emin misiniz:", @@ -330,6 +337,7 @@ "deuro_savings_welcome_description": "dEURO, Euro’ya sabitlenmiş tamamen merkeziyetsiz, teminatlandırılmış bir stablecoin’dir — güvenli, çok yönlü ve oracle’lara güvenmek zorunda kalmadan sansüre dayanıklıdır. 1 dEURO = 1 EURO.", "deuro_tooltip_no_eth": "dEURO'yu etkinleştirmek için cüzdanınızda ETH bulunmalıdır.", "deuro_tx_commited_content": "İşlemin onaylanması ve ekrana yansıması birkaç saniye sürebilir", + "device_confirmation": "Cihaz Onayı", "device_is_signing": "Cihaz imzalıyor", "dfx_option_description": "EUR ve CHF ile kripto satın alın. Avrupa'daki bireysel ve kurumsal müşteriler için", "did_you_back_up_seeds": "Tüm seed'lerinizi yedeklediniz mi?", @@ -466,6 +474,8 @@ "extracted_address_content": "${recipient_name} adresine\nfon göndereceksiniz", "failed_authentication": "Kimlik doğrulama başarısız oldu. ${state_error}", "faq": "SSS", + "favorite_token": "Favori token", + "favorite_token_desc": "Favori token'ın bakiyesi bakiye kartında gösterilecektir.", "features": "Özellikler", "fee": "Ücret", "fee_less_than_min": "Seçilen ücret minimum değerin altındadır, lütfen işlemi gönderebilmek için ücreti artırın", @@ -491,6 +501,7 @@ "frozen": "Dondurulmuş", "frozen_balance": "Dondurulmuş Bakiye", "full_balance": "Toplam Bakiye", + "gallery": "Galeri", "gas_exceeds_allowance": "İşlemin gerektirdiği gas, izin verilen ödeneği aşıyor.", "gas_price": "Gas ücreti", "general": "Genel", @@ -526,10 +537,10 @@ "how_to_connect": "Nasıl Bağlanılır", "how_to_use": "Nasıl kullanılır", "how_to_use_card": "Bu kart nasıl kullanılır", + "hww_exit_desc": "Bu, donanım cihazınızla devam eden eşleştirme işlemini sonlandıracaktır", "i_understand": "Anladım", "icon_style": "Simge stili", "id": "ID: ", - "if_you_dont_see_your_device": "Cihazınızı yukarıda görmüyorsanız, lütfen Ledger'ınızın açık, uyanık ve kilidinin açılmış olduğundan emin olun!", "ignor": "Yoksay", "import": "İçe aktar", "importNFTs": "NFT'leri İçe Aktar", @@ -541,6 +552,7 @@ "infobox_auto_address": "Her işlem aldığınızda alma adresiniz her seferinde değişecektir", "infobox_multichain": "Herhangi bir token veya koleksiyon parçası almak için bu adresi kullanın", "infobox_rotation": "Bu ekranı her kapatıp yeniden açtığınızda alma adresiniz değişecektir", + "input": "Girdi", "inputs": "Girdiler", "insufficient_funds_for_tx": "İşlemi başarıyla gerçekleştirmek için yeterli bakiye yok.", "insufficient_lamport_for_tx": "İşlemi ve işlem ücretini karşılamak için yeterli SOL'unuz yok. Lütfen cüzdanınıza daha fazla SOL ekleyin veya gönderdiğiniz SOL miktarını azaltın.", @@ -606,6 +618,7 @@ "manage_pow_nodes": "PoW düğümlerini yönet", "manage_providers": "Sağlayıcıları yönet", "manage_yats": "Yat'ları yönet", + "manual_input": "Manuel Giriş", "mark_as_redeemed": "Kullanıldı olarak işaretle", "market_place": "Pazar Yeri", "mask": "Maske", @@ -651,6 +664,7 @@ "nanogpt_subtitle": "En yeni modellerin tümü (GPT-5.3, Claude 4.6).\nAbonelik yok, kripto ile ödeyin.", "narrow": "Dar", "network": "Ağ", + "networks": "Ağlar", "new_first_wallet_text": "Kriptonuzu güvende tutmak çocuk oyuncağı", "new_node_testing": "Yeni düğüm testi", "new_subaddress_create": "Oluştur", @@ -679,8 +693,10 @@ "node_new": "Yeni düğüm", "node_port": "Düğüm bağlantı noktası", "node_reset_settings_title": "Ayarları sıfırla", + "node_share_desc": "Bu düğümü içe aktarmak için bu QR kodunu Tarama ekranından tarayın.", "node_test": "Test", "nodes": "Düğümler", + "nodes_desc": "Mevcut düğümleri yönetin veya özel bir düğüm ekleyin.", "nodes_list_reset_to_default_message": "Ayarları varsayılanlara sıfırlamak istediğinizden emin misiniz?", "none_of_selected_providers_can_exchange": "Seçilen sağlayıcıların hiçbiri bu takası gerçekleştiremez", "noNFTYet": "Henüz NFT yok", @@ -690,6 +706,7 @@ "note_tap_to_change": "Not (değiştirmek için dokunun)", "notification_permission_denied": "Bildirim izni kalıcı olarak reddedildi, lütfen ayarlardan manuel olarak etkinleştirin", "nullURIError": "URI null", + "ocp_desc": "Mağazalarda kripto ile ödeme yapın", "offer_expires_in": "Teklifin sona ermesine kalan: ", "offline": "Çevrimdışı", "ok": "Tamam", @@ -716,8 +733,13 @@ "outputs": "Çıktılar", "overshot": "Aşıldı", "overwrite_amount": "Miktarın üzerine yaz", + "pairing_code": "Eşleştirme Kodu", + "pairing_code_desc_1": "Cihazınız bir eşleştirme kodu gösterecektir.", + "pairing_code_desc_2": "Devam etmek için buraya girin", + "pairing_error": "Eşleştirme Hatası", "pairingInvalidEvent": "Geçersiz Eşleştirme Etkinliği", "passphrase": "Parola (İsteğe bağlı)", + "passphrase_view_keys": "Parola", "passphrases_doesnt_match": "Parolalar eşleşmiyor, lütfen tekrar deneyin", "password": "Şifre", "paste": "Yapıştır", @@ -733,6 +755,7 @@ "payjoin_unavailable_sheet_content": "Bir Payjoin işlemi almak için kullanıma hazır Bitcoin’iniz olması gerekir.\n\nCüzdanınızda fonlar olduğu anda Payjoin otomatik olarak etkinleştirilecektir.", "payjoin_unavailable_sheet_title": "Payjoin neden kullanılamıyor?", "payment_id": "Ödeme Kimliği: ", + "payment_invoices": "Ödeme Faturaları", "payment_made_easy": "Ödemeler artık çok kolay", "payment_was_received": "Ödemeniz alındı.", "payments": "Ödemeler", @@ -812,6 +835,7 @@ "receive_to": "Şuraya al", "received": "Alındı", "receiving": "Alma", + "recieving": "Alınıyor", "recipient_address": "Alıcı adresi", "reconnect": "Yeniden bağlan", "reconnect_alert_text": "Yeniden bağlanmak istediğinizden emin misiniz?", @@ -887,6 +911,7 @@ "save": "Kaydet", "save_backup_password": "Lütfen yedekleme parolanı kaydettiğinden emin ol. Bu parola olmadan yedekleme dosyalarını içe aktaramazsın.", "save_backup_password_alert": "Yedekleme parolasını kaydet", + "save_contact": "Kişiyi Kaydet", "save_to_downloads": "İndirilenler'e Kaydet", "saved_the_trade_id": "Takas kimliğini kaydettim", "savings": "Birikimler", @@ -1036,6 +1061,7 @@ "setup_your_debit_card": "Banka kartını ayarla", "share": "Paylaş", "share_address": "Adresi paylaş", + "share_this_node": "Bu düğümü paylaş", "shared_seed_wallet_groups": "Paylaşılan Seed Cüzdan Grupları", "shared_seed_wallets": "Paylaşılan Seed Cüzdanları", "show": "Göster", @@ -1043,6 +1069,7 @@ "show_balance": "Bakiyeyi Görmek İçin Uzun Basın", "show_balance_send_page": "Göstermek için dokunun", "show_balance_toast": "Bakiyeyi gizlemek veya göstermek için uzun basın", + "show_combined_balance": "Birleşik bakiyeyi göster", "show_details": "Detayları Göster", "show_hidden_addresses": "Gizli adresleri göster", "show_keys": "Seed'i/anahtarları göster", @@ -1108,6 +1135,7 @@ "swap_providers": "Takas sağlayıcıları", "sweeping_wallet": "Cüzdan süpürme", "sweeping_wallet_alert": "Bu uzun sürmemeli. BU EKRANDAN AYRILMAYIN, AKSİ TAKDİRDE SÜPÜRÜLEN FONLAR KAYBOLABİLİR.", + "swipe_to_bridge": "Köprülemek için kaydırın", "swipe_to_send": "Göndermek için kaydırın", "switch_wallet": "Cüzdan Değiştir", "switchToETHWallet": "Lütfen bir Ethereum cüzdanına geçin ve tekrar deneyin", @@ -1148,6 +1176,7 @@ "testnet_coins_no_value": "Testnet coinlerinin değeri yok", "third_intro_content": "Yat'lar Cake Wallet'ın dışında da kullanılabilir. Dünyadaki herhangi bir cüzdan adresi bir Yat ile değiştirilebilir!", "third_intro_title": "Yat başkalarıyla uyum içinde çalışır", + "this_can_take_few_seconds": "Bu birkaç saniye sürebilir", "this_pair_is_not_supported_warning": "Bu parite, şu anda seçili borsa(lar) ile desteklenmiyor. Lütfen başka bir borsa seçin.", "thorchain_contract_address_not_supported": "THORChain, bir sözleşme adresine gönderimi desteklemiyor", "thorchain_taproot_address_not_supported": "ThorChain sağlayıcısı Taproot adreslerini desteklemiyor. Lütfen adresi değiştirin veya farklı bir sağlayıcı seçin.", @@ -1161,6 +1190,7 @@ "token_decimal": "Token ondalığı (örn: USDT)", "token_name": "Token adı (örn: Tether)", "token_symbol": "Token sembolü (örn: USDT)", + "token_verified_on_jupiter": "Bu jeton Jupiter'de doğrulandı", "tokenID": "ID", "tokens": "Tokenlar", "tokens_strong_warning": "Bu adrese asla token göndermeyin!", @@ -1289,6 +1319,7 @@ "username": "Kullanıcı adı", "username_available": "Kullanıcı adı kullanılabilir", "username_character_error": "Kullanıcı adları yalnızca şunları içermelidir", + "username_check_your_network": "Kullanıcı adı kontrol edilemedi. Lütfen ağ bağlantınızı kontrol edin.", "username_length_error": "Kullanıcı adları 6-32 alfanümerik karakterden oluşmalıdır.", "username_length_error_1_64": "Kullanıcı adları 1-64 alfanümerik karakter içermelidir.", "username_not_available": "Bu kullanıcı adı kullanılamıyor.", @@ -1299,6 +1330,7 @@ "verify_message": "Mesajı Doğrula", "verify_seed": "Seed'i Doğrula", "verify_with_2fa": "Cake 2FA ile doğrulayın", + "verifying_code": "Kod doğrulanıyor", "version": "Sürüm ${currentVersion}", "view_all": "Tümünü görüntüle", "view_in_block_explorer": "Blok Gezgini'nde görüntüle", @@ -1310,6 +1342,7 @@ "waitFewSecondForTxUpdate": "İşlemin işlem geçmişine yansıması için lütfen birkaç saniye bekleyin", "wallet": "Cüzdan", "wallet_accounts": "Cüzdan Hesapları", + "wallet_balance": "Cüzdan Bakiyesi", "wallet_group": "Cüzdan Grubu", "wallet_group_description_existing_seed": "Bu cüzdan için mevcut bir seed kullanmayı seçtiniz. Onaylamanız veya not almanız gerekiyorsa seed’i tekrar doğrulayabilirsiniz.", "wallet_group_description_four": "tamamen yeni bir seed ile bir cüzdan oluşturmak için.", @@ -1347,9 +1380,11 @@ "walletConnect": "WalletConnect", "wallets": "Cüzdanlar", "warning": "Uyarı", + "wc_action_approve": "Onayla", "wc_approve_request_title": "İsteği onayla", "wc_connect_request_title": "Bağlantı isteği", "wc_connected_to": "${name} ile bağlantı kuruldu", + "wc_desc": "DApp'lere kolayca bağlanın", "wc_max_network_fee": "Maksimum ağ ücreti", "wc_message_to_sign": "İmzalanacak mesaj", "wc_network_fee": "Ağ ücreti", @@ -1400,6 +1435,7 @@ "yat_popup_content": "Artık Cake Wallet'ta kısa, emoji tabanlı bir kullanıcı adı olan Yat'inizle kripto gönderebilir ve alabilirsiniz. Yat'lerinizi istediğiniz zaman ayarlar ekranından yönetebilirsiniz.", "yat_popup_title": "Cüzdan adresiniz emojileştirilebilir.", "yes": "Evet", + "yes_exit": "Evet. Çık", "yesterday": "Dün", "you_now_have_debit_card": "Artık bir banka kartın var", "you_pay": "Sen Ödersin", @@ -1451,4 +1487,4 @@ "wc_action_approve": "Onayla", "wc_desc": "DApp'lere kolayca bağlanın", "yes_exit": "Evet. Çık" -} \ No newline at end of file +} diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 53fee6efb..ca5dbfcb6 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -76,6 +76,7 @@ "approve_tokens": "Схвалити токени", "apps": "Застосунки", "arbiscan_history": "Історія ArbiScan", + "are_you_sure_exit": "Ви впевнені, що хочете вийти?", "arrive_in_this_address": "${currency} ${tag}надійдуть на цю адресу", "ascending": "За зростанням", "ask_each_time": "Запитувати щоразу", @@ -89,6 +90,7 @@ "auto_generate_addresses": "Автоматично генерувати адреси", "auto_generate_subaddresses": "Автоматично генерувати субадреси", "automatic": "Автоматично", + "available": "Доступно", "available_balance": "Доступний баланс", "available_balance_description": "«Доступний баланс» або «Підтверджений баланс» — це кошти, які можна витратити негайно. Якщо кошти відображаються в нижньому балансі, але не у верхньому, потрібно зачекати кілька хвилин, доки вхідні кошти отримають більше підтверджень мережі. Після отримання додаткових підтверджень їх можна буде витратити.", "avg_savings": "Сер. заощадження", @@ -125,6 +127,7 @@ "block_remaining": "Залишився 1 блок", "Blocks_remaining": "${status} блоків залишилось", "bluetooth": "Bluetooth", + "bridging": "Перенесення між мережами", "bright_theme": "Світла", "bscscan_history": "Історія BSCScan", "bump_fee": "Підвищити комісію", @@ -216,6 +219,8 @@ "color_theme": "Колірна тема", "coming_soon_tag": "Скоро", "commit_transaction_amount_fee": "Підтвердити транзакцію\nСума: ${amount}\nКомісія: ${fee}", + "compatible_services": "Сумісні сервіси", + "compatible_services_desc": "Скористайтеся інструментом сканування, щоб зчитати такі типи QR-кодів, які підтримує Cake Wallet:", "configure": "Налаштувати", "confirm": "Підтвердити", "confirm_delete_template": "Ця дія видалить цей шаблон. Бажаєте продовжити?", @@ -279,6 +284,7 @@ "creating_new_wallet": "Створення нового гаманця", "creating_new_wallet_error": "Помилка: ${description}", "creation_date": "Дата створення", + "crypto_addresses": "Криптоадреси", "csv_nothing_to_export": "Nothing to export", "current_swap": "Поточний своп", "current_wallet": "Поточний гаманець", @@ -301,6 +307,7 @@ "default_sell_provider": "Провайдер продажу за замовчуванням", "delete": "Видалити", "delete_account": "Видалити акаунт", + "delete_this_node": "Видалити цей вузол", "delete_wallet": "Видалити гаманець", "delete_wallet_confirm_message": "Ви впевнені, що хочете видалити гаманець ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Ви впевнені, що хочете видалити підключення до", @@ -330,6 +337,7 @@ "deuro_savings_welcome_description": "dEURO — це повністю децентралізований, колатералізований стейблкоїн, прив’язаний до євро — безпечний, універсальний і стійкий до цензури, без необхідності покладатися на оракули. 1 dEURO = 1 EURO.", "deuro_tooltip_no_eth": "Щоб увімкнути dEURO, у вашому гаманці має бути ETH.", "deuro_tx_commited_content": "Підтвердження транзакції та її відображення на екрані може зайняти кілька секунд", + "device_confirmation": "Підтвердження на пристрої", "device_is_signing": "Пристрій підписує", "dfx_option_description": "Купуйте криптовалюту за EUR і CHF. Для роздрібних та корпоративних клієнтів у Європі", "did_you_back_up_seeds": "Ви створили резервні копії всіх своїх сід-фраз?", @@ -466,6 +474,8 @@ "extracted_address_content": "Ви надсилатимете кошти\n${recipient_name}", "failed_authentication": "Не вдалося пройти автентифікацію. ${state_error}", "faq": "FAQ", + "favorite_token": "Улюблений токен", + "favorite_token_desc": "Баланс улюбленого токена відображатиметься на картці балансу.", "features": "Функції", "fee": "Комісія", "fee_less_than_min": "Вибрана комісія менша за мінімальну, будь ласка, збільште комісію, щоб мати змогу надіслати транзакцію", @@ -491,6 +501,7 @@ "frozen": "Заморожено", "frozen_balance": "Заморожений баланс", "full_balance": "Повний баланс", + "gallery": "Галерея", "gas_exceeds_allowance": "Газ, необхідний для транзакції, перевищує ліміт.", "gas_price": "Ціна газу", "general": "Загальні", @@ -526,10 +537,10 @@ "how_to_connect": "Як підключитися", "how_to_use": "Як користуватися", "how_to_use_card": "Як користуватися цією карткою", + "hww_exit_desc": "Це завершить поточний процес сполучення з вашим апаратним пристроєм", "i_understand": "Я розумію", "icon_style": "Стиль значка", "id": "ID: ", - "if_you_dont_see_your_device": "Якщо ви не бачите свій пристрій вище, будь ласка, переконайтеся, що ваш Ledger увімкнений і розблокований!", "ignor": "Ігнорувати", "import": "Імпорт", "importNFTs": "Імпорт NFT", @@ -541,6 +552,7 @@ "infobox_auto_address": "Ваша адреса для отримання змінюватиметься щоразу, коли ви отримаєте транзакцію", "infobox_multichain": "Використовуйте цю адресу, щоб отримати будь-який токен або колекційний предмет на", "infobox_rotation": "Ваша адреса для отримання змінюватиметься щоразу, коли ви закриватимете та знову відкриватимете цей екран", + "input": "Введення", "inputs": "Входи", "insufficient_funds_for_tx": "Недостатньо коштів для успішного виконання транзакції.", "insufficient_lamport_for_tx": "У вас недостатньо SOL, щоб покрити транзакцію та комісію за неї. Будь ласка, додайте до свого гаманця більше SOL або зменшіть суму SOL, яку ви надсилаєте.", @@ -606,6 +618,7 @@ "manage_pow_nodes": "Керувати PoW-вузлами", "manage_providers": "Керувати провайдерами", "manage_yats": "Керувати Yats", + "manual_input": "Ручне введення", "mark_as_redeemed": "Позначити як використане", "market_place": "Маркетплейс", "mask": "Маска", @@ -651,6 +664,7 @@ "nanogpt_subtitle": "Усі найновіші моделі (GPT-5.3, Claude 4.6).\nБез підписки, оплата криптовалютою.", "narrow": "Вузький", "network": "Мережа", + "networks": "Мережі", "new_first_wallet_text": "Зберігати вашу криптовалюту в безпеці — простіше простого", "new_node_testing": "Тестування нового вузла", "new_subaddress_create": "Створити", @@ -679,8 +693,10 @@ "node_new": "Новий вузол", "node_port": "Порт ноди", "node_reset_settings_title": "Скинути налаштування", + "node_share_desc": "Відскануйте цей QR-код на екрані сканування, щоб імпортувати цей вузол.", "node_test": "Тест", "nodes": "Вузли", + "nodes_desc": "Керуйте наявними вузлами або додайте власний.", "nodes_list_reset_to_default_message": "Ви впевнені, що хочете скинути налаштування до значень за замовчуванням?", "none_of_selected_providers_can_exchange": "Жоден із вибраних провайдерів не може виконати цей своп", "noNFTYet": "NFT ще немає", @@ -690,6 +706,7 @@ "note_tap_to_change": "Примітка (натисніть, щоб змінити)", "notification_permission_denied": "Дозвіл на сповіщення було назавжди відхилено, будь ласка, увімкніть його вручну в налаштуваннях", "nullURIError": "URI має значення null", + "ocp_desc": "Розплачуйтеся криптовалютою в магазинах", "offer_expires_in": "Пропозиція діє ще: ", "offline": "Офлайн", "ok": "OK", @@ -716,8 +733,13 @@ "outputs": "Виходи", "overshot": "Перевищено", "overwrite_amount": "Перезаписати суму", + "pairing_code": "Код сполучення", + "pairing_code_desc_1": "Ваш пристрій покаже код сполучення.", + "pairing_code_desc_2": "Введіть його тут, щоб продовжити", + "pairing_error": "Помилка сполучення", "pairingInvalidEvent": "Недійсна подія спарювання", "passphrase": "Парольна фраза (необов’язково)", + "passphrase_view_keys": "Парольна фраза", "passphrases_doesnt_match": "Парольні фрази не збігаються, спробуйте ще раз", "password": "Пароль", "paste": "Вставити", @@ -733,6 +755,7 @@ "payjoin_unavailable_sheet_content": "Для отримання транзакції Payjoin у вас має бути готовий до використання Bitcoin.\n\nЩойно на вашому гаманці з’являться кошти, Payjoin буде автоматично ввімкнено.", "payjoin_unavailable_sheet_title": "Чому Payjoin недоступний?", "payment_id": "ID платежу: ", + "payment_invoices": "Платіжні інвойси", "payment_made_easy": "Платежі стали простішими", "payment_was_received": "Ваш платіж отримано.", "payments": "Платежі", @@ -812,6 +835,7 @@ "receive_to": "Отримати на", "received": "Отримано", "receiving": "Отримання", + "recieving": "Отримання", "recipient_address": "Адреса отримувача", "reconnect": "Перепідключити", "reconnect_alert_text": "Ви впевнені, що хочете перепідключитися?", @@ -888,6 +912,7 @@ "save": "Зберегти", "save_backup_password": "Будь ласка, переконайтеся, що ви зберегли пароль резервної копії. Без нього ви не зможете імпортувати файли резервної копії.", "save_backup_password_alert": "Зберегти пароль резервної копії", + "save_contact": "Зберегти контакт", "save_to_downloads": "Зберегти в «Завантаження»", "saved_the_trade_id": "Я зберіг ID обміну", "savings": "Заощадження", @@ -1037,6 +1062,7 @@ "setup_your_debit_card": "Налаштуйте свою дебетову картку", "share": "Поділитися", "share_address": "Поділитися адресою", + "share_this_node": "Поділитися цим вузлом", "shared_seed_wallet_groups": "Групи гаманців зі спільною seed-фразою", "shared_seed_wallets": "Гаманці зі спільною сід-фразою", "show": "Показати", @@ -1044,6 +1070,7 @@ "show_balance": "Натисніть і утримуйте, щоб показати баланс", "show_balance_send_page": "Торкніться, щоб показати", "show_balance_toast": "Натисніть і утримуйте, щоб приховати або показати баланс", + "show_combined_balance": "Показати сукупний баланс", "show_details": "Показати деталі", "show_hidden_addresses": "Показати приховані адреси", "show_keys": "Показати сід/ключі", @@ -1109,6 +1136,7 @@ "swap_providers": "Провайдери обміну", "sweeping_wallet": "Змітання гаманця", "sweeping_wallet_alert": "Це не повинно зайняти багато часу. НЕ ЗАЛИШАЙТЕ ЦЕЙ ЕКРАН, ІНАКШЕ ЗМЕТЕНІ КОШТИ МОЖУТЬ БУТИ ВТРАЧЕНІ.", + "swipe_to_bridge": "Проведіть, щоб перенести між мережами", "swipe_to_send": "Проведіть, щоб надіслати", "switch_wallet": "Перемкнути гаманець", "switchToETHWallet": "Будь ласка, перемкніться на гаманець Ethereum і спробуйте ще раз", @@ -1149,6 +1177,7 @@ "testnet_coins_no_value": "Тестнет-монети не мають цінності", "third_intro_content": "Yat існують і поза Cake Wallet. Будь-яку адресу гаманця на землі можна замінити на Yat!", "third_intro_title": "Yat добре працює з іншими", + "this_can_take_few_seconds": "Це може зайняти кілька секунд", "this_pair_is_not_supported_warning": "Ця пара не підтримується для вибраної(их) біржі(ж). Будь ласка, виберіть іншу біржу.", "thorchain_contract_address_not_supported": "THORChain не підтримує надсилання на адресу контракту", "thorchain_taproot_address_not_supported": "Провайдер ThorChain не підтримує адреси Taproot. Будь ласка, змініть адресу або виберіть іншого провайдера.", @@ -1162,6 +1191,7 @@ "token_decimal": "Десятковий розряд токена (напр.: USDT)", "token_name": "Назва токена (напр.: Tether)", "token_symbol": "Символ токена (напр.: USDT)", + "token_verified_on_jupiter": "Цей токен перевірено на Jupiter", "tokenID": "ID", "tokens": "Токени", "tokens_strong_warning": "Ніколи не надсилайте токени на цю адресу!", @@ -1290,6 +1320,7 @@ "username": "Ім’я користувача", "username_available": "Ім'я користувача доступне", "username_character_error": "Імена користувачів повинні містити лише", + "username_check_your_network": "Не вдалося перевірити ім’я користувача. Будь ласка, перевірте підключення до мережі.", "username_length_error": "Ім'я користувача має містити 6–32 буквено-цифрові символи.", "username_length_error_1_64": "Імена користувачів мають містити 1–64 буквено-цифрових символів.", "username_not_available": "Це ім’я користувача недоступне.", @@ -1300,6 +1331,7 @@ "verify_message": "Перевірити повідомлення", "verify_seed": "Перевірте сід-фразу", "verify_with_2fa": "Підтвердьте за допомогою Cake 2FA", + "verifying_code": "Перевірка коду", "version": "Версія ${currentVersion}", "view_all": "Переглянути всі", "view_in_block_explorer": "Переглянути в Block Explorer", @@ -1311,6 +1343,7 @@ "waitFewSecondForTxUpdate": "Будь ласка, зачекайте кілька секунд, поки транзакція з’явиться в історії транзакцій", "wallet": "Гаманець", "wallet_accounts": "Облікові записи гаманця", + "wallet_balance": "Баланс гаманця", "wallet_group": "Група гаманців", "wallet_group_description_existing_seed": "Ви вирішили використати наявну сід-фразу для цього гаманця. Ви можете ще раз перевірити сід-фразу, якщо потрібно підтвердити її або записати.", "wallet_group_description_four": "щоб створити гаманець із повністю новою сід-фразою.", @@ -1348,9 +1381,11 @@ "walletConnect": "WalletConnect", "wallets": "Гаманці", "warning": "Попередження", + "wc_action_approve": "Схвалити", "wc_approve_request_title": "Підтвердити запит", "wc_connect_request_title": "Запит на підключення", "wc_connected_to": "Підключено до ${name}", + "wc_desc": "Легко підключайтеся до dApp", "wc_max_network_fee": "Максимальна мережева плата", "wc_message_to_sign": "Повідомлення для підпису", "wc_network_fee": "Плата за мережу", @@ -1401,6 +1436,7 @@ "yat_popup_content": "Тепер ви можете надсилати й отримувати криптовалюту в Cake Wallet за допомогою Yat — короткого імені користувача на основі емодзі. Керуйте Yat у будь-який час на екрані налаштувань", "yat_popup_title": "Адресу вашого гаманця можна перетворити на емодзі.", "yes": "Так", + "yes_exit": "Так. Вийти", "yesterday": "Вчора", "you_now_have_debit_card": "Тепер у вас є дебетова картка", "you_pay": "Ви платите", @@ -1452,4 +1488,4 @@ "wc_action_approve": "Схвалити", "wc_desc": "Легко підключайтеся до dApp", "yes_exit": "Так. Вийти" -} \ No newline at end of file +} diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index b28375292..2f09dda0a 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -76,6 +76,7 @@ "approve_tokens": "ٹوکنز کی منظوری دیں", "apps": "ایپس", "arbiscan_history": "ArbiScan ہسٹری", + "are_you_sure_exit": "کیا آپ واقعی باہر جانا چاہتے ہیں؟", "arrive_in_this_address": "${currency} ${tag}اس پتے پر پہنچے گا", "ascending": "صعودی", "ask_each_time": "ہر بار پوچھیں", @@ -89,6 +90,7 @@ "auto_generate_addresses": "پتے خودکار طور پر تیار کریں", "auto_generate_subaddresses": "سب ایڈریس خودکار طور پر بنائیں", "automatic": "خودکار", + "available": "دستیاب", "available_balance": "دستیاب بیلنس", "available_balance_description": "”دستیاب بیلنس” یا ”تصدیق شدہ بیلنس” وہ فنڈز ہیں جنہیں فوراً خرچ کیا جا سکتا ہے۔ اگر فنڈز نیچے والے بیلنس میں نظر آئیں لیکن اوپر والے بیلنس میں نہیں، تو آپ کو آنے والے فنڈز کے مزید نیٹ ورک کنفرمیشنز حاصل ہونے کے لیے چند منٹ انتظار کرنا ہوگا۔ جب انہیں مزید کنفرمیشنز مل جائیں گی تو وہ خرچ کے قابل ہو جائیں گے۔", "avg_savings": "اوسط بچت", @@ -125,6 +127,7 @@ "block_remaining": "1 بلاک باقی ہے", "Blocks_remaining": "${status} بلاکس باقی ہیں", "bluetooth": "بلوٹوتھ", + "bridging": "برجنگ", "bright_theme": "روشن", "bscscan_history": "BSCScan ہسٹری", "bump_fee": "فیس بڑھائیں", @@ -216,6 +219,8 @@ "color_theme": "رنگ تھیم", "coming_soon_tag": "جلد آ رہا ہے", "commit_transaction_amount_fee": "لین دین کی تصدیق کریں\nرقم: ${amount}\nفیس: ${fee}", + "compatible_services": "موافق سروسز", + "compatible_services_desc": "Cake Wallet پر معاون درج ذیل اقسام کے QR کوڈز پڑھنے کے لیے Scan ٹول استعمال کریں:", "configure": "ترتیب دیں", "confirm": "تصدیق کریں", "confirm_delete_template": "یہ عمل اس ٹیمپلیٹ کو حذف کر دے گا۔ کیا آپ جاری رکھنا چاہتے ہیں؟", @@ -279,6 +284,7 @@ "creating_new_wallet": "نیا والیٹ بنایا جا رہا ہے", "creating_new_wallet_error": "خرابی: ${description}", "creation_date": "تخلیق کی تاریخ", + "crypto_addresses": "کرپٹو ایڈریسز", "csv_nothing_to_export": "ایکسپورٹ کرنے کے لیے کچھ نہیں ہے", "current_swap": "موجودہ سواپ", "current_wallet": "موجودہ والیٹ", @@ -301,6 +307,7 @@ "default_sell_provider": "ڈیفالٹ سیل فراہم کنندہ", "delete": "حذف کریں", "delete_account": "اکاؤنٹ حذف کریں", + "delete_this_node": "یہ نوڈ حذف کریں", "delete_wallet": "والیٹ حذف کریں", "delete_wallet_confirm_message": "کیا آپ واقعی ${wallet_name} والیٹ کو حذف کرنا چاہتے ہیں؟", "deleteConnectionConfirmationPrompt": "کیا آپ کو یقین ہے کہ آپ کنکشن کو حذف کرنا چاہتے ہیں", @@ -330,6 +337,7 @@ "deuro_savings_welcome_description": "dEURO ایک مکمل طور پر غیر مرکزی، کولیٹرل سے سپورٹڈ اسٹیبل کوائن ہے جو یورو کے ساتھ پیگڈ ہے — محفوظ، ہمہ گیر اور سنسرشپ کے خلاف مزاحم، اوریکلز پر انحصار کیے بغیر۔ 1 dEURO = 1 EURO۔", "deuro_tooltip_no_eth": "dEURO کو فعال کرنے کے لیے، آپ کے والیٹ میں ETH ہونا ضروری ہے۔", "deuro_tx_commited_content": "ٹرانزیکشن کی تصدیق ہونے اور اسکرین پر ظاہر ہونے میں چند سیکنڈ لگ سکتے ہیں", + "device_confirmation": "ڈیوائس کی تصدیق", "device_is_signing": "ڈیوائس دستخط کر رہی ہے", "dfx_option_description": "یورو اور CHF کے ساتھ کرپٹو خریدیں۔ یورپ میں خوردہ اور کارپوریٹ صارفین کے لیے", "did_you_back_up_seeds": "کیا آپ نے اپنے تمام سیڈز کا بیک اپ لے لیا ہے؟", @@ -466,6 +474,8 @@ "extracted_address_content": "آپ فنڈز بھیج رہے ہیں\n${recipient_name} کو", "failed_authentication": "تصدیق ناکام ہوگئی۔ ${state_error}", "faq": "عمومی سوالات", + "favorite_token": "پسندیدہ ٹوکن", + "favorite_token_desc": "پسندیدہ ٹوکن کا بیلنس بیلنس کارڈ پر دکھایا جائے گا۔", "features": "خصوصیات", "fee": "فیس", "fee_less_than_min": "منتخب فیس کم از کم حد سے کم ہے، براہ کرم ٹرانزیکشن بھیجنے کے قابل ہونے کے لیے فیس میں اضافہ کریں", @@ -491,6 +501,7 @@ "frozen": "منجمد", "frozen_balance": "منجمد بیلنس", "full_balance": "مکمل بیلنس", + "gallery": "گیلری", "gas_exceeds_allowance": "ٹرانزیکشن کے لیے درکار گیس مقررہ الاؤنس سے زیادہ ہے۔", "gas_price": "گیس کی قیمت", "general": "عمومی", @@ -526,10 +537,10 @@ "how_to_connect": "کیسے کنیکٹ کریں", "how_to_use": "استعمال کرنے کا طریقہ", "how_to_use_card": "اس کارڈ کو استعمال کرنے کا طریقہ", + "hww_exit_desc": "اس سے آپ کے ہارڈ ویئر ڈیوائس کے ساتھ جاری پیئرنگ کا عمل ختم ہو جائے گا", "i_understand": "میں سمجھ گیا/گئی ہوں", "icon_style": "آئیکن اسٹائل", "id": "ID: ", - "if_you_dont_see_your_device": "اگر آپ کو اوپر اپنا ڈیوائس نظر نہیں آ رہا، تو براہِ کرم یقینی بنائیں کہ آپ کا Ledger آن ہے اور اَن لاک ہے!", "ignor": "نظر انداز کریں", "import": "درآمد کریں", "importNFTs": "NFTs درآمد کریں", @@ -541,6 +552,7 @@ "infobox_auto_address": "جب بھی آپ کو کوئی لین دین موصول ہوگا، آپ کا وصولی ایڈریس تبدیل ہو جائے گا", "infobox_multichain": "کسی بھی ٹوکن یا کلیکٹیبل وصول کرنے کے لیے اس ایڈریس کا استعمال کریں", "infobox_rotation": "جب بھی آپ اس اسکرین کو بند کر کے دوبارہ کھولیں گے، آپ کا وصول کرنے والا پتہ تبدیل ہو جائے گا", + "input": "ان پٹ", "inputs": "ان پٹس", "insufficient_funds_for_tx": "لین دین کو کامیابی سے انجام دینے کے لیے ناکافی فنڈز۔", "insufficient_lamport_for_tx": "آپ کے پاس ٹرانزیکشن اور اس کی فیس ادا کرنے کے لیے کافی SOL نہیں ہے۔ براہِ کرم اپنے والیٹ میں مزید SOL شامل کریں یا بھیجی جانے والی SOL کی مقدار کم کریں۔", @@ -606,6 +618,7 @@ "manage_pow_nodes": "PoW نوڈز کا نظم کریں", "manage_providers": "فراہم کنندگان کا نظم کریں", "manage_yats": "یاٹس کا نظم کریں", + "manual_input": "دستی اندراج", "mark_as_redeemed": "ریڈیم شدہ کے طور پر نشان زد کریں", "market_place": "مارکیٹ پلیس", "mask": "ماسک", @@ -651,6 +664,7 @@ "nanogpt_subtitle": "تمام جدید ترین ماڈلز (GPT-5.3، Claude 4.6)۔\nکوئی سبسکرپشن نہیں، کرپٹو کے ساتھ ادائیگی کریں۔", "narrow": "تنگ", "network": "نیٹ ورک", + "networks": "نیٹ ورکس", "new_first_wallet_text": "اپنے کرپٹو کو محفوظ رکھنا بہت آسان ہے", "new_node_testing": "نئے نوڈ کی جانچ", "new_subaddress_create": "بنائیں", @@ -679,8 +693,10 @@ "node_new": "نیا نوڈ", "node_port": "نوڈ پورٹ", "node_reset_settings_title": "ترتیبات ری سیٹ کریں", + "node_share_desc": "اس نوڈ کو امپورٹ کرنے کے لیے Scan اسکرین سے یہ QR کوڈ اسکین کریں۔", "node_test": "ٹیسٹ", "nodes": "نوڈز", + "nodes_desc": "موجودہ نوڈز کا نظم کریں یا ایک کسٹم نوڈ شامل کریں۔", "nodes_list_reset_to_default_message": "کیا آپ واقعی ترتیبات کو ڈیفالٹ پر ری سیٹ کرنا چاہتے ہیں؟", "none_of_selected_providers_can_exchange": "منتخب کردہ فراہم کنندگان میں سے کوئی بھی یہ سواپ نہیں کر سکتا", "noNFTYet": "ابھی تک کوئی NFTs نہیں", @@ -690,6 +706,7 @@ "note_tap_to_change": "نوٹ (تبدیل کرنے کے لیے ٹیپ کریں)", "notification_permission_denied": "نوٹیفکیشن کی اجازت مستقل طور پر مسترد کر دی گئی ہے، براہِ کرم اسے سیٹنگز میں دستی طور پر فعال کریں", "nullURIError": "URI خالی ہے", + "ocp_desc": "اسٹورز میں کرپٹو سے ادائیگی کریں", "offer_expires_in": "پیشکش کی میعاد ختم ہونے میں: ", "offline": "آف لائن", "ok": "ٹھیک ہے", @@ -718,8 +735,13 @@ "outputs": "آؤٹ پٹس", "overshot": "اوور شوٹ", "overwrite_amount": "رقم کو اوور رائٹ کریں", + "pairing_code": "پیئرنگ کوڈ", + "pairing_code_desc_1": "آپ کا ڈیوائس ایک پیئرنگ کوڈ دکھائے گا۔", + "pairing_code_desc_2": "جاری رکھنے کے لیے یہاں درج کریں", + "pairing_error": "پیئرنگ کی خرابی", "pairingInvalidEvent": "غلط پیئرنگ ایونٹ", "passphrase": "پاس فریز (اختیاری)", + "passphrase_view_keys": "پاس فریز", "passphrases_doesnt_match": "پاس فریزز مماثل نہیں ہیں، براہ کرم دوبارہ کوشش کریں", "password": "پاس ورڈ", "paste": "چسپاں کریں", @@ -735,6 +757,7 @@ "payjoin_unavailable_sheet_content": "Payjoin ٹرانزیکشن وصول کرنے کے لیے آپ کے پاس استعمال کے لیے تیار بٹ کوائن ہونا ضروری ہے۔\n\nجیسے ہی آپ کے والیٹ میں فنڈز ہوں گے، Payjoin خود بخود فعال ہو جائے گا۔", "payjoin_unavailable_sheet_title": "Payjoin دستیاب کیوں نہیں ہے؟", "payment_id": "ادائیگی آئی ڈی: ", + "payment_invoices": "ادائیگی کے انوائسز", "payment_made_easy": "ادائیگیاں آسان بنائیں", "payment_was_received": "آپ کی ادائیگی موصول ہو گئی۔", "payments": "ادائیگیاں", @@ -814,6 +837,7 @@ "receive_to": "کو وصول کریں", "received": "موصول ہوا", "receiving": "وصولی", + "recieving": "وصولی", "recipient_address": "وصول کنندہ کا پتہ", "reconnect": "دوبارہ جڑیں", "reconnect_alert_text": "کیا آپ واقعی دوبارہ کنیکٹ کرنا چاہتے ہیں؟", @@ -889,6 +913,7 @@ "save": "محفوظ کریں", "save_backup_password": "براہ کرم یقینی بنائیں کہ آپ نے اپنا بیک اپ پاس ورڈ محفوظ کر لیا ہے۔ اس کے بغیر آپ اپنی بیک اپ فائلیں امپورٹ نہیں کر سکیں گے۔", "save_backup_password_alert": "بیک اپ پاس ورڈ محفوظ کریں", + "save_contact": "رابطہ محفوظ کریں", "save_to_downloads": "ڈاؤن لوڈز میں محفوظ کریں", "saved_the_trade_id": "میں نے ٹریڈ ID محفوظ کر لی ہے۔", "savings": "بچت", @@ -1038,6 +1063,7 @@ "setup_your_debit_card": "اپنا ڈیبٹ کارڈ سیٹ اپ کریں", "share": "شیئر کریں", "share_address": "ایڈریس شیئر کریں", + "share_this_node": "یہ نوڈ شیئر کریں", "shared_seed_wallet_groups": "مشترکہ سیڈ والیٹ گروپس", "shared_seed_wallets": "مشترکہ سیڈ والیٹس", "show": "دکھائیں", @@ -1045,6 +1071,7 @@ "show_balance": "بیلنس دکھانے کے لیے طویل دبائیں", "show_balance_send_page": "ظاہر کرنے کے لیے ٹیپ کریں", "show_balance_toast": "بیلنس چھپانے یا دکھانے کے لیے دیر تک دبائیں", + "show_combined_balance": "مجموعی بیلنس دکھائیں", "show_details": "تفصیلات دکھائیں", "show_hidden_addresses": "پوشیدہ ایڈریسز دکھائیں", "show_keys": "سیڈ/کیز دکھائیں", @@ -1110,6 +1137,7 @@ "swap_providers": "سویپ فراہم کنندگان", "sweeping_wallet": "والٹ سویپ کرنا", "sweeping_wallet_alert": "اس میں زیادہ وقت نہیں لگنا چاہیے۔ اس اسکرین کو مت چھوڑیں، ورنہ سویپ کیے گئے فنڈز ضائع ہو سکتے ہیں۔", + "swipe_to_bridge": "برج کرنے کے لیے سوائپ کریں", "swipe_to_send": "بھیجنے کے لیے سوائپ کریں", "switch_wallet": "والیٹ تبدیل کریں", "switchToETHWallet": "براہِ کرم Ethereum والیٹ پر سوئچ کریں اور دوبارہ کوشش کریں", @@ -1150,6 +1178,7 @@ "testnet_coins_no_value": "ٹیسٹ نیٹ کے سکوں کی کوئی قدر نہیں ہوتی", "third_intro_content": "Yats Cake Wallet سے باہر بھی موجود ہوتے ہیں۔ دنیا میں کسی بھی والیٹ ایڈریس کو Yat سے تبدیل کیا جا سکتا ہے!", "third_intro_title": "Yat دوسروں کے ساتھ اچھی طرح چلتا ہے", + "this_can_take_few_seconds": "اس میں چند سیکنڈ لگ سکتے ہیں", "this_pair_is_not_supported_warning": "یہ جوڑی فی الحال منتخب کردہ ایکسچینج(ز) کے ساتھ سپورٹ نہیں ہے۔ براہ کرم کوئی دوسرا ایکسچینج منتخب کریں۔", "thorchain_contract_address_not_supported": "THORChain کنٹریکٹ ایڈریس پر بھیجنے کی حمایت نہیں کرتا", "thorchain_taproot_address_not_supported": "ThorChain فراہم کنندہ Taproot ایڈریسز کو سپورٹ نہیں کرتا۔ براہِ کرم ایڈریس تبدیل کریں یا کوئی دوسرا فراہم کنندہ منتخب کریں۔", @@ -1163,6 +1192,7 @@ "token_decimal": "ٹوکن اعشاریہ (مثلاً: USDT)", "token_name": "ٹوکن کا نام (مثلاً: Tether)", "token_symbol": "ٹوکن کی علامت (مثلاً: USDT)", + "token_verified_on_jupiter": "یہ ٹوکن Jupiter پر تصدیق شدہ ہے۔", "tokenID": "ID", "tokens": "ٹوکنز", "tokens_strong_warning": "اس پتے پر کبھی بھی ٹوکن نہ بھیجیں!", @@ -1291,6 +1321,7 @@ "username": "یوزرنیم", "username_available": "صارف نام دستیاب ہے", "username_character_error": "صارف نام میں صرف یہ شامل ہو سکتے ہیں", + "username_check_your_network": "صارف نام چیک کرنے سے قاصر۔ براہِ کرم اپنا نیٹ ورک کنکشن چیک کریں۔", "username_length_error": "صارف ناموں میں 6-32 حروف اور اعداد پر مشتمل حروف ہونے ضروری ہیں۔", "username_length_error_1_64": "صارف ناموں میں 1-64 حروفِ تہجی یا اعداد پر مشتمل حروف ہونا ضروری ہے۔", "username_not_available": "یہ صارف نام دستیاب نہیں ہے۔", @@ -1301,6 +1332,7 @@ "verify_message": "پیغام کی توثیق کریں", "verify_seed": "سیڈ کی تصدیق کریں", "verify_with_2fa": "Cake 2FA کے ساتھ تصدیق کریں", + "verifying_code": "کوڈ کی تصدیق کی جا رہی ہے", "version": "ورژن ${currentVersion}", "view_all": "سب دیکھیں", "view_in_block_explorer": "بلاک ایکسپلورر میں دیکھیں", @@ -1312,6 +1344,7 @@ "waitFewSecondForTxUpdate": "براہِ کرم ٹرانزیکشن کو ٹرانزیکشن ہسٹری میں ظاہر ہونے کے لیے چند سیکنڈ انتظار کریں۔", "wallet": "والیٹ", "wallet_accounts": "والیٹ اکاؤنٹس", + "wallet_balance": "والیٹ بیلنس", "wallet_group": "والیٹ گروپ", "wallet_group_description_existing_seed": "آپ نے اس والٹ کے لیے موجودہ سیڈ استعمال کرنے کا انتخاب کیا ہے۔ اگر آپ کو تصدیق کرنے یا لکھ کر محفوظ کرنے کی ضرورت ہو تو آپ سیڈ کی دوبارہ تصدیق کر سکتے ہیں۔", "wallet_group_description_four": "بالکل نئے سیڈ کے ساتھ ایک نیا والیٹ بنانے کے لیے۔", @@ -1349,9 +1382,11 @@ "walletConnect": "WalletConnect", "wallets": "والٹس", "warning": "انتباہ", + "wc_action_approve": "منظور کریں", "wc_approve_request_title": "درخواست منظور کریں۔", "wc_connect_request_title": "رابطہ کی درخواست", "wc_connected_to": "${name} سے منسلک", + "wc_desc": "DApps سے آسانی سے کنیکٹ کریں", "wc_max_network_fee": "زیادہ سے زیادہ نیٹ ورک فیس", "wc_message_to_sign": "دستخط کرنے کا پیغام", "wc_network_fee": "نیٹ ورک فیس", @@ -1402,6 +1437,7 @@ "yat_popup_content": "اب آپ Cake Wallet میں اپنے Yat کے ساتھ کرپٹو بھیج اور وصول کر سکتے ہیں — ایک مختصر، ایموجی پر مبنی صارف نام۔ ترتیبات کی سکرین پر کسی بھی وقت Yats کا نظم کریں۔", "yat_popup_title": "آپ کے والیٹ کا پتہ ایموجی میں تبدیل کیا جا سکتا ہے۔", "yes": "ہاں", + "yes_exit": "ہاں۔ باہر جائیں", "yesterday": "گزشتہ روز", "you_now_have_debit_card": "اب آپ کے پاس ڈیبٹ کارڈ ہے", "you_pay": "آپ ادا کرتے ہیں", @@ -1453,4 +1489,4 @@ "wc_action_approve": "منظور کریں", "wc_desc": "DApps سے آسانی سے کنیکٹ کریں", "yes_exit": "ہاں۔ باہر جائیں" -} \ No newline at end of file +} diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index c5eb9647b..a6d75d190 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -76,6 +76,7 @@ "approve_tokens": "Phê duyệt token", "apps": "Ứng dụng", "arbiscan_history": "Lịch sử ArbiScan", + "are_you_sure_exit": "Bạn có chắc chắn muốn thoát không?", "arrive_in_this_address": "${currency} ${tag} sẽ được gửi đến địa chỉ này", "ascending": "Tăng dần", "ask_each_time": "Hỏi mỗi lần", @@ -89,6 +90,7 @@ "auto_generate_addresses": "Tự động tạo địa chỉ", "auto_generate_subaddresses": "Tự động tạo địa chỉ phụ", "automatic": "Tự động", + "available": "Khả dụng", "available_balance": "Số dư khả dụng", "available_balance_description": "“Số dư khả dụng” hoặc “Số dư đã xác nhận” là số tiền có thể chi tiêu ngay lập tức. Nếu tiền xuất hiện ở số dư phía dưới nhưng không xuất hiện ở số dư phía trên, thì bạn phải đợi vài phút để khoản tiền đến có thêm xác nhận từ mạng. Sau khi có thêm xác nhận, chúng sẽ có thể chi tiêu.", "avg_savings": "Tiết kiệm TB", @@ -125,6 +127,7 @@ "block_remaining": "Còn lại 1 block", "Blocks_remaining": "${status} khối còn lại", "bluetooth": "Bluetooth", + "bridging": "Đang bắc cầu", "bright_theme": "Sáng", "bscscan_history": "Lịch sử BSCScan", "bump_fee": "Tăng phí giao dịch", @@ -188,6 +191,8 @@ "change_wallet_alert_content": "Bạn có muốn chuyển ví hiện tại sang ${wallet_name} không?", "change_wallet_alert_title": "Thay đổi ví hiện tại", "check_receiver_forms": "Vui lòng kiểm tra thông tin người nhận", + "choose_a_payment_method": "Chọn phương thức thanh toán", + "choose_a_provider": "Chọn nhà cung cấp", "choose_account": "Chọn tài khoản", "choose_address": "\n\nVui lòng chọn địa chỉ:", "choose_card_value": "Chọn mệnh giá thẻ", @@ -214,6 +219,8 @@ "color_theme": "Chủ đề màu", "coming_soon_tag": "Sắp ra mắt", "commit_transaction_amount_fee": "Xác nhận giao dịch\nSố tiền: ${amount}\nPhí: ${fee}", + "compatible_services": "Dịch vụ tương thích", + "compatible_services_desc": "Sử dụng công cụ Quét để đọc các loại mã QR sau được hỗ trợ trên Cake Wallet:", "configure": "Cấu hình", "confirm": "Xác nhận", "confirm_delete_template": "Thao tác này sẽ xóa mẫu này. Bạn có muốn tiếp tục không?", @@ -278,6 +285,7 @@ "creating_new_wallet": "Đang tạo ví mới", "creating_new_wallet_error": "Lỗi: ${description}", "creation_date": "Ngày tạo", + "crypto_addresses": "Địa chỉ tiền điện tử", "csv_nothing_to_export": "Không có gì để xuất", "current_swap": "Hoán đổi hiện tại", "current_wallet": "Ví hiện tại", @@ -300,6 +308,7 @@ "default_sell_provider": "Nhà cung cấp bán mặc định", "delete": "Xóa", "delete_account": "Xóa tài khoản", + "delete_this_node": "Xóa node này", "delete_wallet": "Xóa ví", "delete_wallet_confirm_message": "Bạn có chắc chắn muốn xóa ví ${wallet_name} không?", "deleteConnectionConfirmationPrompt": "Bạn có chắc chắn muốn xóa kết nối tới", @@ -329,6 +338,7 @@ "deuro_savings_welcome_description": "dEURO là một stablecoin phi tập trung hoàn toàn, được thế chấp và neo theo đồng Euro — an toàn, linh hoạt và chống kiểm duyệt mà không cần dựa vào oracle. 1 dEURO = 1 EURO.", "deuro_tooltip_no_eth": "Để bật dEURO, bạn phải có ETH trong ví.", "deuro_tx_commited_content": "Có thể mất vài giây để giao dịch được xác nhận và hiển thị trên màn hình", + "device_confirmation": "Xác nhận trên thiết bị", "device_is_signing": "Thiết bị đang ký", "dfx_option_description": "Mua tiền điện tử bằng EUR & CHF. Dành cho khách hàng cá nhân và doanh nghiệp tại Châu Âu", "did_you_back_up_seeds": "Bạn đã sao lưu tất cả cụm từ khôi phục của mình chưa?", @@ -465,6 +475,8 @@ "extracted_address_content": "Bạn sẽ gửi tiền đến\n${recipient_name}", "failed_authentication": "Xác thực thất bại. ${state_error}", "faq": "FAQ", + "favorite_token": "Token yêu thích", + "favorite_token_desc": "Số dư của token yêu thích sẽ hiển thị trên thẻ số dư.", "features": "Tính năng", "fee": "Phí", "fee_less_than_min": "Phí đã chọn thấp hơn mức tối thiểu, vui lòng tăng phí để có thể gửi giao dịch", @@ -490,6 +502,7 @@ "frozen": "Đóng băng", "frozen_balance": "Số dư bị đóng băng", "full_balance": "Toàn bộ số dư", + "gallery": "Thư viện ảnh", "gas_exceeds_allowance": "Gas cần cho giao dịch vượt quá mức cho phép.", "gas_price": "Giá gas", "general": "Chung", @@ -525,10 +538,10 @@ "how_to_connect": "Cách kết nối", "how_to_use": "Cách sử dụng", "how_to_use_card": "Cách sử dụng thẻ này", + "hww_exit_desc": "Thao tác này sẽ kết thúc quá trình ghép nối đang diễn ra với thiết bị phần cứng của bạn", "i_understand": "Tôi hiểu", "icon_style": "Kiểu biểu tượng", "id": "ID: ", - "if_you_dont_see_your_device": "Nếu bạn không thấy thiết bị của mình ở trên, hãy đảm bảo rằng Ledger của bạn đang bật và đã được mở khóa!", "ignor": "Bỏ qua", "import": "Nhập", "importNFTs": "Nhập NFT", @@ -540,6 +553,7 @@ "infobox_auto_address": "Địa chỉ nhận của bạn sẽ thay đổi mỗi khi bạn nhận được một giao dịch", "infobox_multichain": "Sử dụng địa chỉ này để nhận bất kỳ token hoặc vật phẩm sưu tầm nào trên", "infobox_rotation": "Địa chỉ nhận của bạn sẽ thay đổi mỗi khi bạn đóng và mở lại màn hình này", + "input": "Đầu vào", "inputs": "Đầu vào", "insufficient_funds_for_tx": "Không đủ số dư để thực hiện giao dịch thành công.", "insufficient_lamport_for_tx": "Bạn không có đủ SOL để thực hiện giao dịch và thanh toán phí giao dịch. Vui lòng nạp thêm SOL vào ví hoặc giảm số lượng SOL bạn đang gửi.", @@ -605,6 +619,7 @@ "manage_pow_nodes": "Quản lý nút PoW", "manage_providers": "Quản lý nhà cung cấp", "manage_yats": "Quản lý Yats", + "manual_input": "Nhập thủ công", "mark_as_redeemed": "Đánh dấu là đã đổi", "market_place": "Chợ", "mask": "Ẩn", @@ -650,6 +665,7 @@ "nanogpt_subtitle": "Tất cả các mô hình mới nhất (GPT-5.3, Claude 4.6).\nKhông cần đăng ký, thanh toán bằng crypto.", "narrow": "Hẹp", "network": "Mạng", + "networks": "Mạng", "new_first_wallet_text": "Giữ tiền điện tử của bạn an toàn dễ như ăn bánh", "new_node_testing": "Thử nghiệm nút mới", "new_subaddress_create": "Tạo", @@ -666,6 +682,7 @@ "no_extra_detail": "Không có thông tin chi tiết bổ sung", "no_id_needed": "Không cần ID!", "no_id_required": "Không cần ID. Nạp tiền và chi tiêu ở bất kỳ đâu", + "no_providers_available": "Không có nhà cung cấp nào khả dụng", "no_relay_on_domain": "Không có relay cho miền của người dùng hoặc relay không khả dụng. Vui lòng chọn một relay để sử dụng.", "no_relays": "Không có bộ chuyển tiếp", "no_relays_message": "Chúng tôi đã tìm thấy một bản ghi Nostr NIP-05 cho người dùng này, nhưng bản ghi đó không chứa bất kỳ relay nào. Vui lòng hướng dẫn người nhận thêm relay vào bản ghi Nostr của họ.", @@ -677,8 +694,10 @@ "node_new": "Nút mới", "node_port": "Cổng node", "node_reset_settings_title": "Đặt lại cài đặt", + "node_share_desc": "Quét mã QR này từ màn hình Quét để nhập node này.", "node_test": "Thử nghiệm", "nodes": "Các nút", + "nodes_desc": "Quản lý các node hiện có hoặc thêm một node tùy chỉnh.", "nodes_list_reset_to_default_message": "Bạn có chắc chắn muốn đặt lại cài đặt về mặc định không?", "none_of_selected_providers_can_exchange": "Không có nhà cung cấp nào trong số các nhà cung cấp đã chọn có thể thực hiện hoán đổi này", "noNFTYet": "Chưa có NFT nào", @@ -688,6 +707,7 @@ "note_tap_to_change": "Ghi chú (chạm để thay đổi)", "notification_permission_denied": "Quyền thông báo đã bị từ chối vĩnh viễn, vui lòng bật thủ công trong cài đặt", "nullURIError": "URI là rỗng", + "ocp_desc": "Thanh toán bằng tiền điện tử tại cửa hàng", "offer_expires_in": "Ưu đãi hết hạn sau: ", "offline": "Ngoại tuyến", "ok": "OK", @@ -714,8 +734,13 @@ "outputs": "Đầu ra", "overshot": "Vượt quá", "overwrite_amount": "Ghi đè số lượng", + "pairing_code": "Mã ghép nối", + "pairing_code_desc_1": "Thiết bị của bạn sẽ hiển thị một mã ghép nối.", + "pairing_code_desc_2": "Nhập vào đây để tiếp tục", + "pairing_error": "Lỗi ghép nối", "pairingInvalidEvent": "Sự kiện ghép nối không hợp lệ", "passphrase": "Cụm mật khẩu (Tùy chọn)", + "passphrase_view_keys": "Cụm mật khẩu", "passphrases_doesnt_match": "Cụm mật khẩu không khớp, vui lòng thử lại", "password": "Mật khẩu", "paste": "Dán", @@ -731,6 +756,7 @@ "payjoin_unavailable_sheet_content": "Để nhận một giao dịch Payjoin, bạn cần có sẵn Bitcoin để sử dụng.\n\nNgay khi ví của bạn có số dư, Payjoin sẽ tự động được bật.", "payjoin_unavailable_sheet_title": "Tại sao Payjoin không khả dụng?", "payment_id": "ID thanh toán: ", + "payment_invoices": "Hóa đơn thanh toán", "payment_made_easy": "Thanh toán dễ dàng", "payment_was_received": "Khoản thanh toán của bạn đã được nhận.", "payments": "Thanh toán", @@ -810,6 +836,7 @@ "receive_to": "Nhận vào", "received": "Đã nhận", "receiving": "Đang nhận", + "recieving": "Đang nhận", "recipient_address": "Địa chỉ người nhận", "reconnect": "Kết nối lại", "reconnect_alert_text": "Bạn có chắc chắn muốn kết nối lại không?", @@ -885,6 +912,7 @@ "save": "Lưu", "save_backup_password": "Vui lòng đảm bảo rằng bạn đã lưu mật khẩu sao lưu của mình. Bạn sẽ không thể nhập các tệp sao lưu nếu không có mật khẩu này.", "save_backup_password_alert": "Lưu mật khẩu bản sao lưu", + "save_contact": "Lưu liên hệ", "save_to_downloads": "Lưu vào mục Tải xuống", "saved_the_trade_id": "Tôi đã lưu ID giao dịch", "savings": "Tiết kiệm", @@ -956,6 +984,7 @@ "select_sell_provider_notice": "Chọn nhà cung cấp bán ở trên. Bạn có thể bỏ qua màn hình này bằng cách đặt nhà cung cấp bán mặc định trong cài đặt ứng dụng.", "select_token": "Chọn token", "select_your_country": "Vui lòng chọn quốc gia của bạn", + "selected_trocador_provider": "nhà cung cấp Trocador đã chọn", "sell": "Bán", "sell_alert_content": "Hiện tại chúng tôi chỉ hỗ trợ bán Bitcoin, Ethereum và Litecoin. Vui lòng tạo hoặc chuyển sang ví Bitcoin, Ethereum hoặc Litecoin của bạn.", "sell_monero_com_alert_content": "Chưa hỗ trợ bán Monero", @@ -1033,6 +1062,7 @@ "setup_your_debit_card": "Thiết lập thẻ ghi nợ của bạn", "share": "Chia sẻ", "share_address": "Chia sẻ địa chỉ", + "share_this_node": "Chia sẻ node này", "shared_seed_wallet_groups": "Nhóm ví seed được chia sẻ", "shared_seed_wallets": "Ví seed được chia sẻ", "show": "Hiển thị", @@ -1040,6 +1070,7 @@ "show_balance": "Nhấn và giữ để hiển thị số dư", "show_balance_send_page": "Chạm để hiển thị", "show_balance_toast": "Nhấn và giữ để ẩn hoặc hiển thị số dư", + "show_combined_balance": "Hiển thị số dư tổng hợp", "show_details": "Hiển thị chi tiết", "show_hidden_addresses": "Hiển thị các địa chỉ ẩn", "show_keys": "Hiển thị cụm từ khôi phục/khóa", @@ -1105,6 +1136,7 @@ "swap_providers": "Nhà cung cấp hoán đổi", "sweeping_wallet": "Quét ví", "sweeping_wallet_alert": "Việc này sẽ không mất nhiều thời gian. KHÔNG RỜI KHỎI MÀN HÌNH NÀY, NẾU KHÔNG SỐ TIỀN ĐÃ QUÉT CÓ THỂ BỊ MẤT.", + "swipe_to_bridge": "Vuốt để bắc cầu", "swipe_to_send": "Vuốt để gửi", "switch_wallet": "Chuyển đổi ví", "switchToETHWallet": "Vui lòng chuyển sang ví Ethereum và thử lại", @@ -1145,6 +1177,7 @@ "testnet_coins_no_value": "Coin testnet không có giá trị", "third_intro_content": "Yats cũng tồn tại bên ngoài Cake Wallet. Bất kỳ địa chỉ ví nào trên thế giới cũng có thể được thay thế bằng một Yat!", "third_intro_title": "Yat hoạt động tốt với các nền tảng khác", + "this_can_take_few_seconds": "Việc này có thể mất vài giây", "this_pair_is_not_supported_warning": "Cặp này không được hỗ trợ với (các) sàn giao dịch hiện được chọn. Vui lòng chọn một sàn giao dịch khác.", "thorchain_contract_address_not_supported": "THORChain không hỗ trợ gửi đến địa chỉ hợp đồng", "thorchain_taproot_address_not_supported": "Nhà cung cấp ThorChain không hỗ trợ địa chỉ Taproot. Vui lòng đổi địa chỉ hoặc chọn nhà cung cấp khác.", @@ -1158,6 +1191,7 @@ "token_decimal": "Số thập phân của token (ví dụ: USDT)", "token_name": "Tên token (ví dụ: Tether)", "token_symbol": "Ký hiệu token (ví dụ: USDT)", + "token_verified_on_jupiter": "Mã thông báo này được xác minh trên Jupiter", "tokenID": "ID", "tokens": "Token", "tokens_strong_warning": "Không bao giờ gửi token đến địa chỉ này!", @@ -1286,6 +1320,7 @@ "username": "Tên người dùng", "username_available": "Tên người dùng khả dụng", "username_character_error": "Tên người dùng chỉ được chứa", + "username_check_your_network": "Không thể kiểm tra tên người dùng. Vui lòng kiểm tra kết nối mạng của bạn.", "username_length_error": "Tên người dùng phải chứa 6-32 ký tự chữ và số.", "username_length_error_1_64": "Tên người dùng phải chứa 1-64 ký tự chữ và số.", "username_not_available": "Tên người dùng này không khả dụng.", @@ -1296,6 +1331,7 @@ "verify_message": "Xác minh tin nhắn", "verify_seed": "Xác minh cụm từ khôi phục", "verify_with_2fa": "Xác minh bằng Cake 2FA", + "verifying_code": "Đang xác minh mã", "version": "Phiên bản ${currentVersion}", "view_all": "Xem tất cả", "view_in_block_explorer": "Xem trong trình khám phá khối", @@ -1307,6 +1343,7 @@ "waitFewSecondForTxUpdate": "Vui lòng đợi vài giây để giao dịch hiển thị trong lịch sử giao dịch", "wallet": "Ví", "wallet_accounts": "Tài khoản ví", + "wallet_balance": "Số dư ví", "wallet_group": "Nhóm ví", "wallet_group_description_existing_seed": "Bạn đã chọn sử dụng seed hiện có cho ví này. Bạn có thể xác minh lại seed nếu cần để xác nhận hoặc ghi lại.", "wallet_group_description_four": "để tạo một ví với một seed hoàn toàn mới.", @@ -1344,9 +1381,11 @@ "walletConnect": "WalletConnect", "wallets": "Ví", "warning": "Cảnh báo", + "wc_action_approve": "Phê duyệt", "wc_approve_request_title": "Phê duyệt yêu cầu", "wc_connect_request_title": "Yêu cầu kết nối", "wc_connected_to": "Đã kết nối với ${name}", + "wc_desc": "Dễ dàng kết nối với các dApp", "wc_max_network_fee": "Phí mạng tối đa", "wc_message_to_sign": "Tin nhắn để ký", "wc_network_fee": "Phí mạng", @@ -1397,6 +1436,7 @@ "yat_popup_content": "Giờ đây bạn có thể gửi và nhận tiền mã hóa trong Cake Wallet bằng Yat của bạn – một tên người dùng ngắn dựa trên emoji. Bạn có thể quản lý Yat bất cứ lúc nào trong màn hình cài đặt", "yat_popup_title": "Địa chỉ ví của bạn có thể được chuyển thành emoji.", "yes": "Có", + "yes_exit": "Có. Thoát", "yesterday": "Hôm qua", "you_now_have_debit_card": "Giờ đây bạn đã có thẻ ghi nợ", "you_pay": "Bạn trả", @@ -1452,4 +1492,4 @@ "wc_action_approve": "Phê duyệt", "wc_desc": "Dễ dàng kết nối với các dApp", "yes_exit": "Có. Thoát" -} \ No newline at end of file +} diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 089e19207..385df6301 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -76,6 +76,7 @@ "approve_tokens": "Fọwọsi àwọn àmi", "apps": "Àwọn Ìṣàfilọ́lẹ̀", "arbiscan_history": "Ìtàn ArbiScan", + "are_you_sure_exit": "Ṣé ó dá ọ lójú pé o fẹ́ jáde?", "arrive_in_this_address": "${currency} ${tag} yóò dé sí àdírẹ́sì yìí", "ascending": "Ni aṣẹ̀kẹ́sẹ̀", "ask_each_time": "Beere ní gbogbo ìgbà", @@ -89,6 +90,7 @@ "auto_generate_addresses": "Ṣẹda àwọn adirẹsi láìfẹ́ ọwọ́", "auto_generate_subaddresses": "Ṣẹda àwọn àdírẹ́sì kékeré láìfọwọ́", "automatic": "Aifọwọyi", + "available": "Ó wà", "available_balance": "Ìwọ̀ntúnwọ̀nsì tó wà", "available_balance_description": "“Iwọntunwọnsi Wa” tabi “Iwọntunwọnsi Ti a Jẹ́rìí” jẹ́ àwọn owó tí o lè na lẹ́sẹ̀kẹsẹ. Bí àwọn owó bá hàn ní iwọntunwọnsi ìsàlẹ̀ ṣùgbọ́n kì í hàn ní iwọntunwọnsi òkè, nígbà náà o gbọdọ̀ dúró fún ìṣẹ́jú díẹ̀ kí àwọn owó tí ń wọlé lè gba ìjẹ́rìí nẹ́tíwọ́ọ̀kì púpọ̀ síi. Lẹ́yìn tí wọ́n bá ti gba ìjẹ́rìí púpọ̀ síi, o lè na wọ́n.", "avg_savings": "Ìpamọ́ àpapọ̀", @@ -125,6 +127,7 @@ "block_remaining": "Bulọọki 1 tó kù", "Blocks_remaining": "${status} Àwọn bulọọki tó kù", "bluetooth": "Bluetooth", + "bridging": "Ríré ìjápọ̀", "bright_theme": "Imọlẹ", "bscscan_history": "Ìtàn BSCScan", "bump_fee": "Gbé ọya ga", @@ -216,6 +219,8 @@ "color_theme": "Àkòrí àwọ̀", "coming_soon_tag": "N bọ laipẹ", "commit_transaction_amount_fee": "Fọwọ́sí ìdúnàdúrà\nIye: ${amount}\nOwó ìdúnàdúrà: ${fee}", + "compatible_services": "Àwọn Iṣẹ́ Tó Bámu", + "compatible_services_desc": "Lo irinṣẹ́ Scan láti ka àwọn irú kóòdù QR wọ̀nyí tí a ṣe àtìlẹ́yìn fún lórí Cake Wallet:", "configure": "Ṣètò", "confirm": "Jẹ́risí", "confirm_delete_template": "Ìṣe yìí máa pa àwòṣe yìí rẹ́. Ṣé o fẹ́ tẹ̀síwájú?", @@ -279,6 +284,7 @@ "creating_new_wallet": "Ń ṣẹ̀dá àpamọ́wọ́ tuntun", "creating_new_wallet_error": "Aṣiṣe: ${description}", "creation_date": "Ọjọ́ ìdásílẹ̀", + "crypto_addresses": "Àwọn Àdírẹ́sì Crypto", "csv_nothing_to_export": "Kò sí ohunkóhun láti gbé jáde", "current_swap": "Swap lọwọlọwọ", "current_wallet": "Apamọwọ lọwọlọwọ", @@ -301,6 +307,7 @@ "default_sell_provider": "Olupese Tita Aiyipada", "delete": "Páàré", "delete_account": "Pa àkántì rẹ́", + "delete_this_node": "Pa nódù yìí rẹ́", "delete_wallet": "Pa àpamọ́wọ́ rẹ́", "delete_wallet_confirm_message": "Ṣé ó dá ẹ lójú pé ẹ fẹ́ pa wálẹ́ẹ̀tì ${wallet_name} rẹ́?", "deleteConnectionConfirmationPrompt": "Ṣé ó dá ọ lójú pé o fẹ́ pa asopọ̀ sí", @@ -330,6 +337,7 @@ "deuro_savings_welcome_description": "dEURO jẹ stablecoin tí a ṣe ní ìpínkiri ní kikun, tí a fi ìdógba (collateral) ṣe àtìlẹ́yìn, tí ó sì so mọ́ Euro — ó láàbò, ó rọrùn láti lò ní ọ̀nà púpọ̀, ó sì kọ̀ láti jẹ́ kí a ṣe ìfọwọ́kàn sí i, láì nílò láti gbẹ́kẹ̀lé àwọn orákù. 1 dEURO = 1 EURO.", "deuro_tooltip_no_eth": "Láti lè mu dEURO ṣiṣẹ́, o gbọ́dọ̀ ní ETH nínú apamọwọ rẹ.", "deuro_tx_commited_content": "Ó lè gba ìṣẹ́jú-aaya díẹ̀ kí ìdúnàdúrà náà lè jẹ́risi, kí ó sì hàn lórí iboju", + "device_confirmation": "Ìjẹ́rìísí Ẹrọ", "device_is_signing": "Ẹrọ ń fọwọ́ sí", "dfx_option_description": "Ra crypto pẹlu EUR & CHF. Fun awọn alabara soobu ati awọn alabara ile-iṣẹ ní Yúróòpù", "did_you_back_up_seeds": "Ṣe o ti ṣe afẹyinti gbogbo àwọn gbolohun ìrùgbìn rẹ?", @@ -467,6 +475,8 @@ "extracted_address_content": "O máa rán owó sí\n${recipient_name}", "failed_authentication": "Ìfàṣẹsí kò ṣàṣeyọrí. ${state_error}", "faq": "FAQ", + "favorite_token": "Tóókẹ́nì àyànfẹ́", + "favorite_token_desc": "Ìwọ̀ntúnwọ̀nsì tóókẹ́nì àyànfẹ́ náà yóò hàn lórí kaadi ìwọ̀ntúnwọ̀nsì.", "features": "Awọn ẹya", "fee": "Ọya", "fee_less_than_min": "Owo ìdúná tí a yàn kere ju iye tó kéré jù lọ; jọwọ pọ̀ sí i kí o lè fi ìdúná náà ránṣẹ́.", @@ -492,6 +502,7 @@ "frozen": "Dídì", "frozen_balance": "Iwontunwonsi tí a dì", "full_balance": "Ìwọ̀ntúnwọ̀nsì kíkún", + "gallery": "Àwòrán-ìkójọ", "gas_exceeds_allowance": "Gaasi tí ìdúnàdúrà náà nílò ti kọjá ìwọ̀n tí a fàyè gba.", "gas_price": "Iye gaasi", "general": "Gbogbogbo", @@ -527,10 +538,10 @@ "how_to_connect": "Bí a ṣe lè sopọ", "how_to_use": "Bí a ṣe lè lò", "how_to_use_card": "Báwo ni a ṣe ń lo káàdì yìí", + "hww_exit_desc": "Eyi yóò parí ìlànà ìsopọ̀ tí ń lọ lọ́wọ́ pẹ̀lú ẹrọ hardware rẹ", "i_understand": "Mo ye mi", "icon_style": "Aami ara", "id": "ID: ", - "if_you_dont_see_your_device": "Ti o ko ba ri ẹrọ rẹ loke, jọwọ rii daju pe Ledger rẹ wa ni titan ati pe o ti ṣí i silẹ!", "ignor": "Foju kọ", "import": "Gbe wọle", "importNFTs": "Gbe NFTs wọle", @@ -542,6 +553,7 @@ "infobox_auto_address": "Adirẹsi gbigba rẹ yóò máa yí padà ní gbogbo ìgbà tí o bá gba ìdúnàdúrà kan", "infobox_multichain": "Lo adirẹsi yii lati gba eyikeyi tokini tabi ohun ìkójọpọ̀ lori", "infobox_rotation": "Adirẹsi gbigba rẹ yóò yí padà ní gbogbo ìgbà tí o bá ti iboju yìí, tí o sì tún ṣí i", + "input": "Ìwọlé", "inputs": "Àwọn ìwọlé", "insufficient_funds_for_tx": "Ko si owo to lati ṣe idunadura naa ni aṣeyọri.", "insufficient_lamport_for_tx": "O ko ni SOL to lati bo idunadura naa ati owo-ori idunadura rẹ. Jọwọ fi SOL kun apamọwọ rẹ tabi dinku iye SOL ti o n firanṣẹ.", @@ -607,6 +619,7 @@ "manage_pow_nodes": "Ṣakoso àwọn nódì PoW", "manage_providers": "Ṣàkóso àwọn olùpèsè", "manage_yats": "Ṣàkóso Yats", + "manual_input": "Ìwọlé Pẹ̀lú Ọwọ́", "mark_as_redeemed": "Samisi gẹ́gẹ́ bí a ti rà padà", "market_place": "Ọjà", "mask": "Iboju", @@ -652,6 +665,7 @@ "nanogpt_subtitle": "Gbogbo àwọn àwòṣe tuntun jùlọ (GPT-5.3, Claude 4.6).\nKò sí ìforúkọsílẹ̀, sanwó pẹ̀lú crypto.", "narrow": "Dín", "network": "Nẹ́tíwọ́ọ̀kì", + "networks": "Àwọn Nẹ́tíwọ́ọ̀kì", "new_first_wallet_text": "Mímú kíríptò rẹ láàbò rọrùn bíi akara oyinbo", "new_node_testing": "Ìdánwò node tuntun", "new_subaddress_create": "Ṣẹda", @@ -680,8 +694,10 @@ "node_new": "Nóòdù Tuntun", "node_port": "Pọ́ọ̀tì node", "node_reset_settings_title": "Tun àwọn àtòlẹ́sẹẹsẹ ṣe", + "node_share_desc": "Ṣe àyẹ̀wò kóòdù QR yìí láti ojú Scan láti gbé nódù yìí wọlé.", "node_test": "Ìdánwò", "nodes": "Àwọn nódù", + "nodes_desc": "Ṣàkóso àwọn nódù tó wà tẹ́lẹ̀ tàbí ṣàfikún ọ̀kan tí ara rẹ.", "nodes_list_reset_to_default_message": "Ṣé o dá ọ lójú pé o fẹ́ tún àwọn ààtò ṣe sí àtẹ̀yìnwá?", "none_of_selected_providers_can_exchange": "Ko si ọkan ninu awọn olupese tí a yàn tó lè ṣe paṣipaarọ yìí", "noNFTYet": "Ko si NFT sibẹsibẹ", @@ -691,6 +707,7 @@ "note_tap_to_change": "Akọsilẹ (tẹ lati yí padà)", "notification_permission_denied": "A ti kọ igbanilaaye iwifunni patapata, jọwọ mu un ṣiṣẹ lọ́wọ́ rẹ nínú àwọn eto", "nullURIError": "URI jẹ òfo", + "ocp_desc": "Sanwó pẹ̀lú crypto ní àwọn ilé ìtajà", "offer_expires_in": "Ìpèsè yìí máa parí ní: ", "offline": "Àìlórí ayélujára", "ok": "Ó DÁRA", @@ -717,8 +734,13 @@ "outputs": "Àwọn àbájáde", "overshot": "Ti kọja iye", "overwrite_amount": "Rọ́pò iye", + "pairing_code": "Kóòdù Ìsopọ̀", + "pairing_code_desc_1": "Ẹrọ rẹ yóò ṣàfihàn kóòdù ìsopọ̀ kan.", + "pairing_code_desc_2": "Tẹ̀ ẹ́ sílẹ̀ níbí láti tẹ̀síwájú", + "pairing_error": "Àṣìṣe Ìsopọ̀", "pairingInvalidEvent": "Iṣẹlẹ Pipọpọ Aibojumu", "passphrase": "Gbolohun aṣínà (Aṣayan)", + "passphrase_view_keys": "Gbolóhùn aṣínà", "passphrases_doesnt_match": "Awọn gbolohun aṣínà kò bá ara wọn mu, jọwọ gbìyànjú lẹ́ẹ̀kansi", "password": "Ọ̀rọ̀ ìgbaniwọlé", "paste": "Lẹ́", @@ -734,6 +756,7 @@ "payjoin_unavailable_sheet_content": "Gbigba ìdúnàdúrà Payjoin nìbéèrè pé kí o ní Bitcoin tó ṣetán láti lò.\n\nNí kété tí apamọwọ rẹ bá ní owó, Payjoin yóò ṣiṣẹ́ laifọwọ́yì.", "payjoin_unavailable_sheet_title": "Kí nìdí tí Payjoin kò fi wà?", "payment_id": "ID Ìsanwó: ", + "payment_invoices": "Àwọn Ìwé Ìsanwó", "payment_made_easy": "Ṣíṣe ìsanwó rọrùn", "payment_was_received": "A ti gba ìsanwó rẹ.", "payments": "Awọn ìsanwó", @@ -813,6 +836,7 @@ "receive_to": "Gba sí", "received": "Ti gbà", "receiving": "Gbigba", + "recieving": "Gbigba", "recipient_address": "Àdírẹ́sì olùgbà", "reconnect": "Tun so pọ̀", "reconnect_alert_text": "Ṣé ó dá ẹ lójú pé ẹ fẹ́ tún sopọ̀?", @@ -888,6 +912,7 @@ "save": "Fipamọ́", "save_backup_password": "Jọ̀wọ́ rí i dájú pé ẹ ti fipamọ́ ọ̀rọ̀ aṣínà ìpamọ́ yín. Ẹ kò ní lè wọlé sí àwọn fáìlì ìpamọ́ yín láìsí rẹ̀.", "save_backup_password_alert": "Pamọ́ ọ̀rọ̀ aṣínà afẹ́yinti", + "save_contact": "Fi Olùbáṣepọ̀ Pamọ́", "save_to_downloads": "Fipamọ sí Downloads", "saved_the_trade_id": "Mo ti fipamọ́ ID ìṣòwò náà", "savings": "Ifipamọ", @@ -1037,6 +1062,7 @@ "setup_your_debit_card": "Ṣètò káàdì débítì rẹ", "share": "Pín", "share_address": "Pín àdírẹ́sì", + "share_this_node": "Pín nódù yìí", "shared_seed_wallet_groups": "Awọn ẹgbẹ apamọwọ Irugbin Pín", "shared_seed_wallets": "Àwọn Woleti Irúgbìn Pínpín", "show": "Fi hàn", @@ -1044,6 +1070,7 @@ "show_balance": "Tẹ mọ́lẹ̀ láti ṣàfihàn iwọntúnwọ̀nsì", "show_balance_send_page": "Tẹ lati ṣí hàn", "show_balance_toast": "Tẹ mọ́lẹ̀ láti fi ìwọ̀ntúnwọ̀nsì pamọ́ tàbí ṣàfihàn", + "show_combined_balance": "Ṣàfihàn ìwọ̀ntúnwọ̀nsì àpapọ̀", "show_details": "Fi àwọn àlàyè hàn", "show_hidden_addresses": "Ṣàfihàn àwọn àdírẹ́sì tí a ti fipamọ́", "show_keys": "Ṣàfihàn seed/kọ́kọ́rọ́", @@ -1109,6 +1136,7 @@ "swap_providers": "Awọn olupese swap", "sweeping_wallet": "Wallet ìkó", "sweeping_wallet_alert": "Eyi kò yẹ kí ó pẹ́. MÁ ṢE KÚRÒ LÓRÍ IBÓJÚ YÌÍ TÁBÍ OWÓ TÍ A TI SWEEP LÈ SỌ́NÙ.", + "swipe_to_bridge": "Fà láti so ìjápọ̀", "swipe_to_send": "Ra lati fi ránṣẹ́", "switch_wallet": "Yí apamọwọ padà", "switchToETHWallet": "Jọwọ yipada sí apamọwọ Ethereum kí o sì tún gbìyànjú lẹ́ẹ̀kansi", @@ -1149,6 +1177,7 @@ "testnet_coins_no_value": "Awọn kóìnì testnet kò ní iye", "third_intro_content": "Yats wà níta Cake Wallet pàá. A lè rọ́pò àdírẹ́sì àpamọ́wọ́ kankan níbi gbogbo pẹ̀lú Yat!", "third_intro_title": "Yat bá àwọn míì ṣiṣẹ́ dáadáa", + "this_can_take_few_seconds": "Eyi lè gba ìṣẹ́jú-aaya díẹ̀", "this_pair_is_not_supported_warning": "Akọsopọ yii kò ní ìtìlẹ́yìn pẹ̀lú (àwọn) paṣipaarọ tí a yàn lọ́wọ́lọ́wọ́. Jọwọ yan paṣipaarọ míì.", "thorchain_contract_address_not_supported": "THORChain kò ṣe àtìlẹ́yìn fífi ránṣẹ́ sí àdírẹ́sì contract", "thorchain_taproot_address_not_supported": "Olupese ThorChain ko ṣe atilẹyin fún àwọn àdírẹ́sì Taproot. Jọwọ yí àdírẹ́sì náà padà tàbí yan olupese míì.", @@ -1162,6 +1191,7 @@ "token_decimal": "Token decimal (àpẹẹrẹ: 6 fún USDT)", "token_name": "Orukọ token (eg: Tether)", "token_symbol": "Ami àpẹẹrẹ token (àpẹẹrẹ: USDT)", + "token_verified_on_jupiter": "Aami yii jẹ idaniloju lori Jupiter", "tokenID": "ID", "tokens": "Àwọn tóókẹ́nì", "tokens_strong_warning": "Maṣe fi àwọn tóókẹ́nì ránṣẹ́ sí àdírẹ́sì yìí!", @@ -1290,6 +1320,7 @@ "username": "Orúkọ olumulo", "username_available": "Orúkọ olumulo wà", "username_character_error": "Orúkọ olumulo gbọdọ̀ ní àwọn àmi wọ̀nyí nìkan", + "username_check_your_network": "Kò lè ṣàyẹ̀wò orúkọ olumulo. Jọ̀wọ́ ṣàyẹ̀wò ìsopọ̀ nẹ́tíwọ́ọ̀kì rẹ.", "username_length_error": "Orúkọ olumulo gbọdọ ní àwọn ohun kikọ alphanumeric 6-32.", "username_length_error_1_64": "Orúkọ olumulo gbọ́dọ̀ ní àwọn àmi alphanumeric 1-64.", "username_not_available": "Orukọ olumulo yii ko si.", @@ -1300,6 +1331,7 @@ "verify_message": "Ṣàyẹ̀wò Ifiranṣẹ", "verify_seed": "Jẹ́rìí Seed", "verify_with_2fa": "Jẹ́rìí pẹ̀lú Cake 2FA", + "verifying_code": "Ń ṣàyẹ̀wò Kóòdù", "version": "Ẹ̀dà ${currentVersion}", "view_all": "Wo gbogbo rẹ", "view_in_block_explorer": "Wò ní Block Explorer", @@ -1311,6 +1343,7 @@ "waitFewSecondForTxUpdate": "Jọwọ dúró fún ìṣẹ́jú díẹ̀ kí ìdúnàdúrà náà lè hàn nínú ìtàn ìdúnàdúrà", "wallet": "Apamọwọ", "wallet_accounts": "Àwọn àkántì apamọwọ", + "wallet_balance": "Ìwọ̀ntúnwọ̀nsì Apamọwọ", "wallet_group": "Ẹgbẹ́ Apamọwọ", "wallet_group_description_existing_seed": "O ti yan lati lo irugbin (seed) ti o wa tẹlẹ fun apamọwọ yii. O le tun jẹrisi irugbin naa lẹẹkansi ti o ba nilo lati fidi rẹ múlẹ tàbí kọ ọ sílẹ.", "wallet_group_description_four": "Láti ṣẹ̀dá apamọwọ kan pẹ̀lú seed tuntun pátápátá.", @@ -1348,9 +1381,11 @@ "walletConnect": "WalletConnect", "wallets": "Àwọn wálẹ́ẹ̀tì", "warning": "Ìkìlọ̀", + "wc_action_approve": "Fọwọ́sí", "wc_approve_request_title": "fọwọsi ìbéèrè", "wc_connect_request_title": "Sopọ ìbéèrè", "wc_connected_to": "Sopọ si ${name}", + "wc_desc": "So pọ̀ mọ́ àwọn DApp ní rọrùn", "wc_max_network_fee": "Max owo nẹtiwọki", "wc_message_to_sign": "Ifiranṣẹ lati wole", "wc_network_fee": "Nẹtiwọki ọya", @@ -1401,6 +1436,7 @@ "yat_popup_content": "Ẹ lè ránṣẹ́ àti gba crypto nínú Cake Wallet pẹ̀lú Yat yín — orúkọ olùṣàmúlò kúkurú tí a dá lórí emoji. Ṣàkóso àwọn Yat nígbàkúgbà lórí ojú ìṣètò.", "yat_popup_title": "A lè yí àdírẹ́sì wálẹ́ẹ̀tì rẹ padà sí emoji.", "yes": "Bẹẹni", + "yes_exit": "Bẹ́ẹ̀ni. Jáde", "yesterday": "Àná", "you_now_have_debit_card": "O ti ní káàdì débítì báyìí", "you_pay": "Ìwọ san", @@ -1452,4 +1488,4 @@ "wc_action_approve": "Fọwọ́sí", "wc_desc": "So pọ̀ mọ́ àwọn DApp ní rọrùn", "yes_exit": "Bẹ́ẹ̀ni. Jáde" -} \ No newline at end of file +} diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 19a2e9a70..893988b17 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -76,6 +76,7 @@ "approve_tokens": "授权代币", "apps": "应用", "arbiscan_history": "ArbiScan 历史记录", + "are_you_sure_exit": "您确定要退出吗?", "arrive_in_this_address": "${currency} ${tag}将发送到此地址", "ascending": "升序", "ask_each_time": "每次都询问", @@ -89,6 +90,7 @@ "auto_generate_addresses": "自动生成地址", "auto_generate_subaddresses": "自动生成子地址", "automatic": "自动", + "available": "可用", "available_balance": "可用余额", "available_balance_description": "“可用余额”或“确认余额”是指可立即花费的资金。如果资金显示在下方余额中但未显示在上方余额中,则需要等待几分钟,让转入资金获得更多网络确认。获得更多确认后,这些资金即可花费。", "avg_savings": "平均节省", @@ -125,6 +127,7 @@ "block_remaining": "剩余 1 个区块", "Blocks_remaining": "剩余 ${status} 个区块", "bluetooth": "蓝牙", + "bridging": "跨链桥接中", "bright_theme": "明亮", "bscscan_history": "BSCScan 历史记录", "bump_fee": "提高手续费(RBF)", @@ -216,6 +219,8 @@ "color_theme": "颜色主题", "coming_soon_tag": "即将推出", "commit_transaction_amount_fee": "提交交易\n金额:${amount}\n手续费:${fee}", + "compatible_services": "兼容的服务", + "compatible_services_desc": "使用扫描工具读取 Cake Wallet 支持的以下类型二维码:", "configure": "配置", "confirm": "确认", "confirm_delete_template": "此操作将删除此模板。是否继续?", @@ -279,6 +284,7 @@ "creating_new_wallet": "正在创建新钱包", "creating_new_wallet_error": "错误:${description}", "creation_date": "创建日期", + "crypto_addresses": "加密货币地址", "csv_nothing_to_export": "无可导出的内容", "current_swap": "当前兑换", "current_wallet": "当前钱包", @@ -301,6 +307,7 @@ "default_sell_provider": "默认卖出服务商", "delete": "删除", "delete_account": "删除账户", + "delete_this_node": "删除此节点", "delete_wallet": "删除钱包", "delete_wallet_confirm_message": "确定要删除 ${wallet_name} 钱包吗?", "deleteConnectionConfirmationPrompt": "您确定要删除与…的连接吗?", @@ -330,6 +337,7 @@ "deuro_savings_welcome_description": "dEURO 是一种完全去中心化、由抵押品支持的稳定币,与欧元挂钩——安全、多用途且抗审查,无需依赖预言机。1 dEURO = 1 欧元。", "deuro_tooltip_no_eth": "要启用 dEURO,您的钱包中必须有 ETH。", "deuro_tx_commited_content": "交易可能需要几秒钟才能确认,并在屏幕上显示", + "device_confirmation": "设备确认", "device_is_signing": "设备正在签名", "dfx_option_description": "使用 EUR 和 CHF 购买加密货币。面向欧洲的零售和企业客户", "did_you_back_up_seeds": "您已备份所有助记词了吗?", @@ -466,6 +474,8 @@ "extracted_address_content": "您将向\n${recipient_name}\n发送资金", "failed_authentication": "身份验证失败。${state_error}", "faq": "FAQ", + "favorite_token": "收藏的代币", + "favorite_token_desc": "收藏代币的余额将显示在余额卡片上。", "features": "功能", "fee": "手续费", "fee_less_than_min": "所选手续费低于最低值,请提高手续费以便发送交易", @@ -491,6 +501,7 @@ "frozen": "冻结", "frozen_balance": "冻结余额", "full_balance": "全部余额", + "gallery": "相册", "gas_exceeds_allowance": "交易所需的 Gas 超过了允许额度。", "gas_price": "Gas 费", "general": "通用", @@ -526,6 +537,7 @@ "how_to_connect": "如何连接", "how_to_use": "如何使用", "how_to_use_card": "如何使用此卡", + "hww_exit_desc": "这将结束与您的硬件设备正在进行的配对过程", "i_understand": "我明白了", "icon_style": "图标风格", "id": "ID: ", @@ -541,6 +553,7 @@ "infobox_auto_address": "每次收到交易时,您的收款地址都会自动轮换", "infobox_multichain": "使用此地址接收任何代币或收藏品", "infobox_rotation": "每次您关闭并重新打开此页面时,您的接收地址都会更换", + "input": "输入", "inputs": "输入", "insufficient_funds_for_tx": "余额不足,无法成功执行交易。", "insufficient_lamport_for_tx": "您的 SOL 余额不足以支付本次交易及其手续费。请向钱包中充值更多 SOL,或减少要发送的 SOL 数量。", @@ -606,6 +619,7 @@ "manage_pow_nodes": "管理 PoW 节点", "manage_providers": "管理服务提供商", "manage_yats": "管理 Yats", + "manual_input": "手动输入", "mark_as_redeemed": "标记为已兑换", "market_place": "市场", "mask": "掩码", @@ -651,6 +665,7 @@ "nanogpt_subtitle": "所有最新模型(GPT-5.3、Claude 4.6)。\n无需订阅,用加密货币支付。", "narrow": "窄", "network": "网络", + "networks": "网络", "new_first_wallet_text": "让您的加密货币安全无忧,轻而易举", "new_node_testing": "新节点测试", "new_subaddress_create": "创建", @@ -679,8 +694,10 @@ "node_new": "新节点", "node_port": "节点端口", "node_reset_settings_title": "重置设置", + "node_share_desc": "在扫描页面扫描此二维码即可导入此节点。", "node_test": "测试", "nodes": "节点", + "nodes_desc": "管理现有节点或添加自定义节点。", "nodes_list_reset_to_default_message": "您确定要将设置重置为默认值吗?", "none_of_selected_providers_can_exchange": "所选的提供商均无法完成此兑换", "noNFTYet": "暂无 NFT", @@ -690,6 +707,7 @@ "note_tap_to_change": "备注(轻触以更改)", "notification_permission_denied": "通知权限已被永久拒绝,请在设置中手动启用", "nullURIError": "URI 为空", + "ocp_desc": "在商店中使用加密货币支付", "offer_expires_in": "优惠将在以下时间到期:", "offline": "离线", "ok": "确定", @@ -716,8 +734,13 @@ "outputs": "输出", "overshot": "超出", "overwrite_amount": "覆盖金额", + "pairing_code": "配对码", + "pairing_code_desc_1": "您的设备将显示一个配对码。", + "pairing_code_desc_2": "在此处输入以继续", + "pairing_error": "配对错误", "pairingInvalidEvent": "配对无效事件", "passphrase": "密码短语(可选)", + "passphrase_view_keys": "密码短语", "passphrases_doesnt_match": "密语不一致,请重试", "password": "密码", "paste": "粘贴", @@ -733,6 +756,7 @@ "payjoin_unavailable_sheet_content": "接收 Payjoin 交易需要您的钱包中有可用的比特币。\n\n一旦您的钱包有资金,Payjoin 将自动启用。", "payjoin_unavailable_sheet_title": "为什么 Payjoin 不可用?", "payment_id": "支付 ID:", + "payment_invoices": "付款发票", "payment_made_easy": "轻松支付", "payment_was_received": "已收到您的付款。", "payments": "支付", @@ -812,6 +836,7 @@ "receive_to": "接收至", "received": "已接收", "receiving": "接收", + "recieving": "接收中", "recipient_address": "收款地址", "reconnect": "重新连接", "reconnect_alert_text": "您确定要重新连接吗?", @@ -887,6 +912,7 @@ "save": "保存", "save_backup_password": "请确保您已保存备份密码。没有它,您将无法导入备份文件。", "save_backup_password_alert": "保存备份密码", + "save_contact": "保存联系人", "save_to_downloads": "保存到“下载”", "saved_the_trade_id": "我已保存交易 ID", "savings": "储蓄", @@ -1036,6 +1062,7 @@ "setup_your_debit_card": "设置您的借记卡", "share": "分享", "share_address": "分享地址", + "share_this_node": "分享此节点", "shared_seed_wallet_groups": "共享助记词钱包组", "shared_seed_wallets": "共享助记词钱包", "show": "显示", @@ -1043,6 +1070,7 @@ "show_balance": "长按显示余额", "show_balance_send_page": "点击以显示", "show_balance_toast": "长按以隐藏或显示余额", + "show_combined_balance": "显示合并余额", "show_details": "显示详情", "show_hidden_addresses": "显示隐藏的地址", "show_keys": "显示种子短语/密钥", @@ -1108,6 +1136,7 @@ "swap_providers": "兑换服务提供商", "sweeping_wallet": "正在归集钱包", "sweeping_wallet_alert": "这应该不会花很长时间。请勿离开此屏幕,否则被归集的资金可能会丢失。", + "swipe_to_bridge": "滑动以跨链桥接", "swipe_to_send": "滑动以发送", "switch_wallet": "切换钱包", "switchToETHWallet": "请切换到以太坊钱包后重试", @@ -1148,6 +1177,7 @@ "testnet_coins_no_value": "测试网币没有实际价值", "third_intro_content": "Yats 也存在于 Cake Wallet 之外。地球上任何一个钱包地址都可以用一个 Yat 来替代!", "third_intro_title": "Yat 与其他应用兼容良好", + "this_can_take_few_seconds": "这可能需要几秒钟", "this_pair_is_not_supported_warning": "当前所选的交易所不支持该交易对。请选择其他交易所。", "thorchain_contract_address_not_supported": "THORChain 不支持发送到合约地址", "thorchain_taproot_address_not_supported": "ThorChain 提供商不支持 Taproot 地址。请更换地址或选择其他提供商。", @@ -1161,6 +1191,7 @@ "token_decimal": "代币小数位数(例如:USDT)", "token_name": "代币名称(例如:Tether)", "token_symbol": "代币符号(例如:USDT)", + "token_verified_on_jupiter": "该令牌已在 Jupiter 上验证", "tokenID": "ID", "tokens": "代币", "tokens_strong_warning": "切勿将代币发送到此地址!", @@ -1289,6 +1320,7 @@ "username": "用户名", "username_available": "用户名可用", "username_character_error": "用户名只能包含", + "username_check_your_network": "无法检查用户名。请检查您的网络连接。", "username_length_error": "用户名必须包含 6-32 个字母数字字符。", "username_length_error_1_64": "用户名必须包含 1-64 个字母或数字字符。", "username_not_available": "该用户名不可用。", @@ -1299,6 +1331,7 @@ "verify_message": "验证消息", "verify_seed": "验证助记词", "verify_with_2fa": "使用 Cake 2FA 验证", + "verifying_code": "正在验证验证码", "version": "版本 ${currentVersion}", "view_all": "查看全部", "view_in_block_explorer": "在区块浏览器中查看", @@ -1310,6 +1343,7 @@ "waitFewSecondForTxUpdate": "请稍等几秒钟,交易将会显示在交易记录中", "wallet": "钱包", "wallet_accounts": "钱包账户", + "wallet_balance": "钱包余额", "wallet_group": "钱包分组", "wallet_group_description_existing_seed": "您已选择在此钱包中使用现有助记词。如需确认或抄写,您可以再次验证助记词。", "wallet_group_description_four": "创建一个使用全新助记词的钱包。", @@ -1347,9 +1381,11 @@ "walletConnect": "WalletConnect", "wallets": "钱包", "warning": "警告", + "wc_action_approve": "批准", "wc_approve_request_title": "批准请求", "wc_connect_request_title": "连接请求", "wc_connected_to": "连接到${name}", + "wc_desc": "轻松连接到 DApp", "wc_max_network_fee": "最高网络费用", "wc_message_to_sign": "要签名的消息", "wc_network_fee": "网络费", @@ -1400,6 +1436,7 @@ "yat_popup_content": "您现在可以在 Cake Wallet 中使用 Yat 发送和接收加密货币——一种基于表情符号的简短用户名。您可随时在设置页面管理 Yat", "yat_popup_title": "您的钱包地址可以表情符号化。", "yes": "是", + "yes_exit": "是的,退出", "yesterday": "昨天", "you_now_have_debit_card": "您现在已拥有一张借记卡", "you_pay": "您支付", @@ -1414,41 +1451,5 @@ "zcash_card_enable_later": "您随时可以在设置中启用此卡", "zcash_card_missing_funds": "资金不见了?", "zcash_card_scan": "扫描", - "zcash_card_warning": "在该过程完成之前请勿关闭应用程序,否则该过程将需要从头重新开始。", - "are_you_sure_exit": "您确定要退出吗?", - "available": "可用", - "bridging": "跨链桥接中", - "compatible_services": "兼容的服务", - "compatible_services_desc": "使用扫描工具读取 Cake Wallet 支持的以下类型二维码:", - "crypto_addresses": "加密货币地址", - "delete_this_node": "删除此节点", - "device_confirmation": "设备确认", - "favorite_token": "收藏的代币", - "favorite_token_desc": "收藏代币的余额将显示在余额卡片上。", - "gallery": "相册", - "hww_exit_desc": "这将结束与您的硬件设备正在进行的配对过程", - "input": "输入", - "manual_input": "手动输入", - "networks": "网络", - "node_share_desc": "在扫描页面扫描此二维码即可导入此节点。", - "nodes_desc": "管理现有节点或添加自定义节点。", - "ocp_desc": "在商店中使用加密货币支付", - "pairing_code": "配对码", - "pairing_code_desc_1": "您的设备将显示一个配对码。", - "pairing_code_desc_2": "在此处输入以继续", - "pairing_error": "配对错误", - "passphrase_view_keys": "密码短语", - "payment_invoices": "付款发票", - "recieving": "接收中", - "save_contact": "保存联系人", - "share_this_node": "分享此节点", - "show_combined_balance": "显示合并余额", - "swipe_to_bridge": "滑动以跨链桥接", - "this_can_take_few_seconds": "这可能需要几秒钟", - "username_check_your_network": "无法检查用户名。请检查您的网络连接。", - "verifying_code": "正在验证验证码", - "wallet_balance": "钱包余额", - "wc_action_approve": "批准", - "wc_desc": "轻松连接到 DApp", - "yes_exit": "是的,退出" + "zcash_card_warning": "在该过程完成之前请勿关闭应用程序,否则该过程将需要从头重新开始。" } \ No newline at end of file diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index bac26a104..3e7784b97 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -14,15 +14,15 @@ TYPES=($MONERO_COM $CAKEWALLET) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="6.2.1" -MONERO_COM_BUILD_NUMBER=4161 +MONERO_COM_VERSION="6.4.0" +MONERO_COM_BUILD_NUMBER=4164 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="6.2.1" -CAKEWALLET_BUILD_NUMBER=4413 +CAKEWALLET_VERSION="6.4.0" +CAKEWALLET_BUILD_NUMBER=4419 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index a8d022d9b..1c9dd1464 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -12,13 +12,13 @@ TYPES=($MONERO_COM $CAKEWALLET) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="6.2.1" -MONERO_COM_BUILD_NUMBER=167 +MONERO_COM_VERSION="6.4.0" +MONERO_COM_BUILD_NUMBER=170 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="6.2.1" -CAKEWALLET_BUILD_NUMBER=423 +CAKEWALLET_VERSION="6.4.0" +CAKEWALLET_BUILD_NUMBER=433 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" diff --git a/scripts/linux/app_env.sh b/scripts/linux/app_env.sh index 0a66f626c..b0cbb833e 100755 --- a/scripts/linux/app_env.sh +++ b/scripts/linux/app_env.sh @@ -14,8 +14,8 @@ if [ -n "$1" ]; then fi CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="6.1.0" -CAKEWALLET_BUILD_NUMBER=79 +CAKEWALLET_VERSION="6.3.0" +CAKEWALLET_BUILD_NUMBER=80 if ! [[ " ${TYPES[*]} " =~ " ${APP_LINUX_TYPE} " ]]; then echo "Wrong app type." diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index 4d699307d..63e317798 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -16,13 +16,13 @@ if [ -n "$1" ]; then fi MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="6.2.0" -MONERO_COM_BUILD_NUMBER=80 +MONERO_COM_VERSION="6.3.0" +MONERO_COM_BUILD_NUMBER=81 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="6.2.0" -CAKEWALLET_BUILD_NUMBER=148 +CAKEWALLET_VERSION="6.3.0" +CAKEWALLET_BUILD_NUMBER=149 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then diff --git a/scripts/windows/build_exe_installer.iss b/scripts/windows/build_exe_installer.iss index d8fbc6e7e..9fba68e60 100644 --- a/scripts/windows/build_exe_installer.iss +++ b/scripts/windows/build_exe_installer.iss @@ -1,5 +1,5 @@ #define MyAppName "Cake Wallet" -#define MyAppVersion "6.1.0" +#define MyAppVersion "6.3.0" #define MyAppPublisher "Cake Labs LLC" #define MyAppURL "https://cakewallet.com/" #define MyAppExeName "CakeWallet.exe" diff --git a/test/new-ui/widgets/money/currency_symbol_text_test.dart b/test/new-ui/widgets/money/currency_symbol_text_test.dart new file mode 100644 index 000000000..6292f243d --- /dev/null +++ b/test/new-ui/widgets/money/currency_symbol_text_test.dart @@ -0,0 +1,213 @@ +import "package:cake_wallet/entities/balance_display_mode.dart"; +import "package:cake_wallet/entities/bitcoin_amount_display_mode.dart"; +import "package:cake_wallet/new-ui/widgets/money/currency_symbol_text.dart"; +import "package:cake_wallet/new-ui/widgets/money/money_settings_cubit.dart"; +import "package:cw_core/crypto_currency.dart"; +import "package:flutter/material.dart"; +import "package:flutter_bloc/flutter_bloc.dart"; +import "package:flutter_test/flutter_test.dart"; + +class FakeMoneySettingsCubit extends Cubit implements MoneySettingsCubit { + FakeMoneySettingsCubit(super.initialState); + + void setState(MoneySettingsState state) => emit(state); +} + +const _bitcoinMode = MoneySettingsState( + bitcoinAmountDisplayMode: BitcoinAmountDisplayMode.bitcoin, + displayMode: BalanceDisplayMode.fullBalance, +); + +const _satoshiMode = MoneySettingsState( + bitcoinAmountDisplayMode: BitcoinAmountDisplayMode.satoshi, + displayMode: BalanceDisplayMode.fullBalance, +); + +const _lnSatoshiMode = MoneySettingsState( + bitcoinAmountDisplayMode: BitcoinAmountDisplayMode.satoshiForLightning, + displayMode: BalanceDisplayMode.fullBalance, +); + +/// A non-BTC currency whose symbol exceeds the 8-char cap. `raw: -1` keeps it +/// distinct from every real currency, so it never counts as BTC/BTCLN. +const _longSymbol = CryptoCurrency(title: "SUPERLONGTOKEN", name: "superlongtoken", decimals: 8); + +Future _pump( + WidgetTester tester, + Widget child, + MoneySettingsState state, + ) async { + final cubit = FakeMoneySettingsCubit(state); + addTearDown(cubit.close); + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: cubit, + child: Scaffold(body: Center(child: child)), + ), + ), + ); + return cubit; +} + +void main() { + group("useBaseUnit == null (follows settings)", () { + testWidgets("BTC in satoshi mode -> sats", (tester) async { + await _pump(tester, const CurrencySymbolText(CryptoCurrency.btc), _satoshiMode); + + expect(find.text("sats"), findsOneWidget); + }); + + testWidgets("BTC in bitcoin mode -> BTC", (tester) async { + await _pump(tester, const CurrencySymbolText(CryptoCurrency.btc), _bitcoinMode); + + expect(find.text("BTC"), findsOneWidget); + }); + + testWidgets("non-BTC currency ignores satoshi mode", (tester) async { + await _pump(tester, const CurrencySymbolText(CryptoCurrency.xmr), _satoshiMode); + + expect(find.text("XMR"), findsOneWidget); + }); + + testWidgets("BTCLN in LN-satoshi mode -> sats", (tester) async { + await _pump( + tester, + const CurrencySymbolText(CryptoCurrency.btcln), + _lnSatoshiMode, + ); + + expect(find.text("sats"), findsOneWidget); + }); + + testWidgets("BTCLN in bitcoin mode -> BTC", (tester) async { + await _pump(tester, const CurrencySymbolText(CryptoCurrency.btcln), _bitcoinMode); + + // BTCLN's title/symbol is "BTC". + expect(find.text("BTC"), findsOneWidget); + }); + }); + + group("useBaseUnit == true (force base unit where supported)", () { + testWidgets("BTC -> sats regardless of settings", (tester) async { + await _pump( + tester, + const CurrencySymbolText(CryptoCurrency.btc, useBaseUnit: true), + _bitcoinMode, // settings say bitcoin, but override wins + ); + + expect(find.text("sats"), findsOneWidget); + }); + + testWidgets("BTCLN -> sats", (tester) async { + await _pump( + tester, + const CurrencySymbolText(CryptoCurrency.btcln, useBaseUnit: true), + _bitcoinMode, + ); + + expect(find.text("sats"), findsOneWidget); + }); + + testWidgets("non-BTC currency keeps its symbol even when forced", (tester) async { + await _pump( + tester, + const CurrencySymbolText(CryptoCurrency.xmr, useBaseUnit: true), + _satoshiMode, + ); + + expect(find.text("XMR"), findsOneWidget); + }); + }); + + group("useBaseUnit == false (always the symbol)", () { + testWidgets("BTC -> BTC even in satoshi mode", (tester) async { + await _pump( + tester, + const CurrencySymbolText(CryptoCurrency.btc, useBaseUnit: false), + _satoshiMode, + ); + + expect(find.text("BTC"), findsOneWidget); + expect(find.text("sats"), findsNothing); + }); + + testWidgets("BTCLN -> BTC even in LN-satoshi mode", (tester) async { + await _pump( + tester, + const CurrencySymbolText(CryptoCurrency.btcln, useBaseUnit: false), + _lnSatoshiMode, + ); + + expect(find.text("BTC"), findsOneWidget); + expect(find.text("sats"), findsNothing); + }); + + testWidgets("non-BTC currency -> symbol", (tester) async { + await _pump( + tester, + const CurrencySymbolText(CryptoCurrency.xmr, useBaseUnit: false), + _bitcoinMode, + ); + + expect(find.text("XMR"), findsOneWidget); + }); + }); + + group("symbol formatting", () { + testWidgets("caps symbols longer than 8 characters", (tester) async { + await _pump( + tester, + const CurrencySymbolText(_longSymbol, useBaseUnit: false), + _bitcoinMode, + ); + + // "SUPERLONGTOKEN" -> first 8 chars. + expect(find.text("SUPERLON"), findsOneWidget); + }); + }); + + group("rebuild on cubit state change", () { + testWidgets("re-renders the symbol when settings change", (tester) async { + final cubit = await _pump( + tester, + const CurrencySymbolText(CryptoCurrency.btc), + _bitcoinMode, + ); + expect(find.text("BTC"), findsOneWidget); + + cubit.setState(_satoshiMode); + await tester.pump(); + + expect(find.text("BTC"), findsNothing); + expect(find.text("sats"), findsOneWidget); + }); + }); + + group("forwarding to the inner Text", () { + testWidgets("passes through Text properties incl. semanticsIdentifier", (tester) async { + await _pump( + tester, + const CurrencySymbolText( + CryptoCurrency.btc, + maxLines: 2, + textAlign: TextAlign.center, + overflow: TextOverflow.fade, + style: TextStyle(fontSize: 18), + semanticsIdentifier: "currency-symbol", + ), + _bitcoinMode, + ); + + final text = tester.widget( + find.descendant(of: find.byType(CurrencySymbolText), matching: find.byType(Text)), + ); + + expect(text.maxLines, 2); + expect(text.textAlign, TextAlign.center); + expect(text.overflow, TextOverflow.fade); + expect(text.style?.fontSize, 18); + expect(text.semanticsIdentifier, "currency-symbol"); + }); + }); +} diff --git a/test/new-ui/widgets/money/money_text_test.dart b/test/new-ui/widgets/money/money_text_test.dart new file mode 100644 index 000000000..c4fc26610 --- /dev/null +++ b/test/new-ui/widgets/money/money_text_test.dart @@ -0,0 +1,340 @@ +import "package:cake_wallet/entities/balance_display_mode.dart"; +import "package:cake_wallet/entities/bitcoin_amount_display_mode.dart"; +import "package:cake_wallet/new-ui/widgets/money/money_settings_cubit.dart"; +import "package:cake_wallet/new-ui/widgets/money/money_text.dart"; +import "package:cw_core/amount/money.dart"; +import "package:cw_core/crypto_currency.dart"; +import "package:flutter/material.dart"; +import "package:flutter_bloc/flutter_bloc.dart"; +import "package:flutter_test/flutter_test.dart"; + +class FakeMoneySettingsCubit extends Cubit implements MoneySettingsCubit { + FakeMoneySettingsCubit(super.initialState); + + void setState(MoneySettingsState state) => emit(state); +} + +/// Visible, BTC shown as bitcoin (not base unit). +const _visibleBtc = MoneySettingsState( + bitcoinAmountDisplayMode: BitcoinAmountDisplayMode.bitcoin, + displayMode: BalanceDisplayMode.fullBalance, +); + +/// Hidden balance. +const _hiddenBtc = MoneySettingsState( + bitcoinAmountDisplayMode: BitcoinAmountDisplayMode.bitcoin, + displayMode: BalanceDisplayMode.hiddenBalance, +); + +/// Visible, satoshi mode -> BTC/BTCLN use base unit. +const _sats = MoneySettingsState( + bitcoinAmountDisplayMode: BitcoinAmountDisplayMode.satoshi, + displayMode: BalanceDisplayMode.fullBalance, +); + +/// Visible, LN-only satoshi mode -> only BTCLN uses base unit. +const _lnSats = MoneySettingsState( + bitcoinAmountDisplayMode: BitcoinAmountDisplayMode.satoshiForLightning, + displayMode: BalanceDisplayMode.fullBalance, +); + +// --- Money fixtures -------------------------------------------------------- +final _btc012 = Money.fromInt(12345678, CryptoCurrency.btc); // 0.12345678 BTC +final _btcTrailing = Money.fromInt(12000000, CryptoCurrency.btc); // 0.12 BTC +final _btcGrouping = Money.fromInt(123450000000, CryptoCurrency.btc); // 1234.5 BTC +final _xmrHalf = Money.fromInt(500000000000, CryptoCurrency.xmr); // 0.5 XMR +final _btclnAmount = Money.fromInt(12345678, CryptoCurrency.btcln); // 0.12345678 BTC (LN) + +const _hidden = "●●●●●●"; + +Future _pump( + WidgetTester tester, + Widget child, + MoneySettingsState state, + ) async { + final cubit = FakeMoneySettingsCubit(state); + addTearDown(cubit.close); + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: cubit, + child: Scaffold(body: Center(child: child)), + ), + ), + ); + return cubit; +} + +void main() { + group("hidden amount", () { + testWidgets("isHiddenAmount:true hides even when state is visible", (tester) async { + await _pump(tester, MoneyText(_btc012, isHiddenAmount: true), _visibleBtc); + + expect(find.text(_hidden), findsOneWidget); + expect(find.text("0.12345678 BTC"), findsNothing); + }); + + testWidgets("isHiddenAmount:false overrides a hidden state", (tester) async { + await _pump(tester, MoneyText(_btc012, isHiddenAmount: false), _hiddenBtc); + + expect(find.text("0.12345678 BTC"), findsOneWidget); + expect(find.text(_hidden), findsNothing); + }); + + testWidgets("isHiddenAmount:null falls back to state.isHidden (hidden)", (tester) async { + await _pump(tester, MoneyText(_btc012), _hiddenBtc); + + expect(find.text(_hidden), findsOneWidget); + }); + + testWidgets("isHiddenAmount:null falls back to state.isHidden (visible)", (tester) async { + await _pump(tester, MoneyText(_btc012), _visibleBtc); + + expect(find.text("0.12345678 BTC"), findsOneWidget); + expect(find.text(_hidden), findsNothing); + }); + }); + + group("symbol vs precision", () { + testWidgets("showSymbol:true appends the currency symbol", (tester) async { + await _pump(tester, MoneyText(_btc012), _visibleBtc); + + expect(find.text("0.12345678 BTC"), findsOneWidget); + }); + + testWidgets("showSymbol:false omits the currency symbol", (tester) async { + await _pump(tester, MoneyText(_btc012, showSymbol: false), _visibleBtc); + + expect(find.text("0.12345678"), findsOneWidget); + expect(find.text("0.12345678 BTC"), findsNothing); + }); + }); + + group("withSymbolPrefix", () { + test("defaults to false on the constructor", () { + expect(MoneyText(_btc012).withSymbolPrefix, false); + }); + + test("defaults to false via optional", () { + expect((MoneyText.optional(_btc012) as MoneyText).withSymbolPrefix, false); + }); + + testWidgets("ignored when showSymbol is false", (tester) async { + await _pump( + tester, + MoneyText(_btc012, showSymbol: false, withSymbolPrefix: true), + _visibleBtc, + ); + + expect(find.text("0.12345678"), findsOneWidget); + expect(find.text("BTC 0.12345678"), findsNothing); + expect(find.text("0.12345678 BTC"), findsNothing); + }); + + testWidgets("prefixes the symbol before the amount", (tester) async { + await _pump(tester, MoneyText(_btc012, withSymbolPrefix: true), _visibleBtc); + + expect(find.text("BTC 0.12345678"), findsOneWidget); + }); + + testWidgets("prefixed amount is still localized (en_US grouping)", (tester) async { + await _pump(tester, MoneyText(_btcGrouping, withSymbolPrefix: true), _visibleBtc); + + expect(find.text("BTC 1,234.5"), findsOneWidget); + }); + + testWidgets("prefixed amount respects an explicit de_DE locale", (tester) async { + await _pump( + tester, + MoneyText(_btcGrouping, withSymbolPrefix: true, locale: const Locale("de", "DE")), + _visibleBtc, + ); + + expect(find.text("BTC 1.234,5"), findsOneWidget); + }); + + testWidgets("prefixes the base-unit ticker (sats)", (tester) async { + await _pump( + tester, + MoneyText(_btc012, withSymbolPrefix: true, useBaseUnit: true), + _visibleBtc, + ); + + expect(find.text("sats 12,345,678"), findsOneWidget); + }); + }); + + group("base unit (useBaseUnit)", () { + testWidgets("explicit useBaseUnit:true renders sats", (tester) async { + await _pump(tester, MoneyText(_btc012, useBaseUnit: true), _visibleBtc); + + // 12345678 sats, localized grouping applied by withLocalSeperator. + expect(find.text("12,345,678 sats"), findsOneWidget); + }); + + testWidgets("explicit useBaseUnit:false overrides a satoshi state", (tester) async { + await _pump(tester, MoneyText(_btc012, useBaseUnit: false), _sats); + + expect(find.text("0.12345678 BTC"), findsOneWidget); + }); + + testWidgets("useBaseUnit:null -> state decides (BTC in satoshi mode -> sats)", (tester) async { + await _pump(tester, MoneyText(_btc012), _sats); + + expect(find.text("12,345,678 sats"), findsOneWidget); + }); + + testWidgets("useBaseUnit:null -> state decides (BTC in bitcoin mode -> BTC)", (tester) async { + await _pump(tester, MoneyText(_btc012), _visibleBtc); + + expect(find.text("0.12345678 BTC"), findsOneWidget); + }); + + testWidgets("non-BTC currency ignores satoshi mode", (tester) async { + await _pump(tester, MoneyText(_xmrHalf), _sats); + + expect(find.text("0.5 XMR"), findsOneWidget); + }); + + testWidgets("LN-satoshi mode makes BTCLN use base unit", (tester) async { + await _pump(tester, MoneyText(_btclnAmount), _lnSats); + + expect(find.text("12,345,678 sats"), findsOneWidget); + }); + + testWidgets("LN-satoshi mode leaves plain BTC untouched", (tester) async { + await _pump(tester, MoneyText(_btc012), _lnSats); + + expect(find.text("0.12345678 BTC"), findsOneWidget); + }); + }); + + group("fractionalDigits & trimZeros", () { + testWidgets("fractionalDigits truncates (does not round)", (tester) async { + await _pump(tester, MoneyText(_btc012, fractionalDigits: 2), _visibleBtc); + + // 0.12345678 truncated to 2 digits -> 0.12 (NOT 0.13). + expect(find.text("0.12 BTC"), findsOneWidget); + }); + + testWidgets("trimZeros:true (default) removes trailing zeros", (tester) async { + await _pump(tester, MoneyText(_btcTrailing), _visibleBtc); + + expect(find.text("0.12 BTC"), findsOneWidget); + }); + + testWidgets("trimZeros:false keeps trailing zeros", (tester) async { + await _pump(tester, MoneyText(_btcTrailing, trimZeros: false), _visibleBtc); + + expect(find.text("0.12000000 BTC"), findsOneWidget); + }); + }); + + group("locale & separators", () { + testWidgets('default locale (en_US) uses "," grouping and "." decimal', (tester) async { + await _pump(tester, MoneyText(_btcGrouping), _visibleBtc); + + expect(find.text("1,234.5 BTC"), findsOneWidget); + }); + + testWidgets("explicit de_DE locale swaps grouping/decimal separators", (tester) async { + await _pump( + tester, + MoneyText(_btcGrouping, locale: const Locale("de", "DE")), + _visibleBtc, + ); + + expect(find.text("1.234,5 BTC"), findsOneWidget); + }); + }); + + group("rebuild on cubit state change", () { + testWidgets("re-renders when the settings state changes", (tester) async { + final cubit = await _pump(tester, MoneyText(_btc012), _visibleBtc); + expect(find.text("0.12345678 BTC"), findsOneWidget); + + cubit.setState(_hiddenBtc); + await tester.pump(); + + expect(find.text("0.12345678 BTC"), findsNothing); + expect(find.text(_hidden), findsOneWidget); + }); + }); + + group("forwarding to the inner Text", () { + testWidgets("passes through Text properties incl. semanticsIdentifier", (tester) async { + await _pump( + tester, + MoneyText( + _btc012, + maxLines: 3, + textAlign: TextAlign.right, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 42), + semanticsIdentifier: "via-ctor", + ), + _visibleBtc, + ); + + final text = tester.widget( + find.descendant(of: find.byType(MoneyText), matching: find.byType(Text)), + ); + + expect(text.maxLines, 3); + expect(text.textAlign, TextAlign.right); + expect(text.overflow, TextOverflow.ellipsis); + expect(text.style?.fontSize, 42); + expect(text.semanticsIdentifier, "via-ctor"); + }); + }); + + group("MoneyText.optional", () { + test("returns SizedBox.shrink for a null amount", () { + final widget = MoneyText.optional(null); + + expect(widget, isA()); + final box = widget as SizedBox; + expect(box.width, 0.0); + expect(box.height, 0.0); + }); + + test("returns a MoneyText for a non-null amount", () { + expect(MoneyText.optional(_btc012), isA()); + }); + + test("forwards money-specific parameters to MoneyText", () { + final widget = MoneyText.optional( + _btc012, + showSymbol: false, + fractionalDigits: 3, + trimZeros: false, + isHiddenAmount: true, + useBaseUnit: true, + withSymbolPrefix: true, + ) as MoneyText; + + expect(widget.showSymbol, false); + expect(widget.fractionalDigits, 3); + expect(widget.trimZeros, false); + expect(widget.isHiddenAmount, true); + expect(widget.useBaseUnit, true); + expect(widget.withSymbolPrefix, true); + }); + + testWidgets("renders identically to a direct MoneyText", (tester) async { + await _pump(tester, MoneyText.optional(_btc012), _visibleBtc); + + expect(find.byType(MoneyText), findsOneWidget); + expect(find.text("0.12345678 BTC"), findsOneWidget); + }); + + test( + "forwards semanticsIdentifier to MoneyText", + () { + final widget = MoneyText.optional(_btc012, semanticsIdentifier: "sid") as MoneyText; + + expect(widget.semanticsIdentifier, "sid"); + }, + ); + }); +} diff --git a/test/new-ui/widgets/new_future_primary_button_test.dart b/test/new-ui/widgets/new_future_primary_button_test.dart new file mode 100644 index 000000000..858dc5fda --- /dev/null +++ b/test/new-ui/widgets/new_future_primary_button_test.dart @@ -0,0 +1,265 @@ +import "dart:async"; + +import "package:cake_wallet/new-ui/widgets/new_future_primary_button.dart"; +import "package:cake_wallet/new-ui/widgets/new_primary_button.dart"; +import "package:flutter/material.dart"; +import "package:flutter_svg/flutter_svg.dart"; +import "package:flutter_test/flutter_test.dart"; + +const _svg = ''; + +void main() { + Widget wrap(Widget child) => MaterialApp(home: Scaffold(body: child)); + + NewPrimaryButton inner(WidgetTester tester) => + tester.widget(find.byType(NewPrimaryButton)); + + Future? fire(WidgetTester tester) { + final result = (inner(tester).onPressed as dynamic)(); + return result is Future ? result : null; + } + + Widget build({ + required Future Function() onPressed, + bool disabled = false, + SvgPicture? image, + Color color = Colors.blue, + Color textColor = Colors.white, + Color borderColor = Colors.transparent, + String text = "Send", + }) => + wrap( + NewFuturePrimaryButton( + onPressed: onPressed, + text: text, + color: color, + textColor: textColor, + borderColor: borderColor, + disabled: disabled, + image: image, + ), + ); + + group("NewFuturePrimaryButton", () { + group("prop forwarding", () { + testWidgets("passes every prop through to NewPrimaryButton", (tester) async { + final image = SvgPicture.string(_svg); + + await tester.pumpWidget( + build( + onPressed: () async {}, + text: "Confirm", + color: Colors.green, + textColor: Colors.black, + borderColor: Colors.red, + disabled: true, + image: image, + ), + ); + + final button = inner(tester); + expect(button.text, "Confirm"); + expect(button.color, Colors.green); + expect(button.textColor, Colors.black); + expect(button.borderColor, Colors.red); + expect(button.disabled, isTrue); + expect(button.image, same(image)); + }); + + testWidgets("defaults: transparent border, not disabled, no image", (tester) async { + await tester.pumpWidget( + wrap( + NewFuturePrimaryButton( + onPressed: () async {}, + text: "Send", + color: Colors.blue, + textColor: Colors.white, + ), + ), + ); + + final button = inner(tester); + expect(button.borderColor, Colors.transparent); + expect(button.disabled, isFalse); + expect(button.image, isNull); + }); + + testWidgets("starts out not loading", (tester) async { + await tester.pumpWidget(build(onPressed: () async {})); + + expect(inner(tester).isLoading, isFalse); + }); + }); + + group("loading lifecycle", () { + testWidgets("flips isLoading true while pending, false once resolved", (tester) async { + final completer = Completer(); + await tester.pumpWidget(build(onPressed: () => completer.future)); + + final pressed = fire(tester); + await tester.pump(); + expect( + inner(tester).isLoading, + isTrue, + reason: "should be loading while the future is in flight", + ); + + completer.complete(); + await pressed; + await tester.pump(); + expect(inner(tester).isLoading, isFalse); + }); + + testWidgets("synchronous-ish callback still settles back to not loading", (tester) async { + var calls = 0; + await tester.pumpWidget(build(onPressed: () async => calls++)); + + await fire(tester); + await tester.pump(); + + expect(calls, 1); + expect(inner(tester).isLoading, isFalse); + }); + + testWidgets("invokes the callback exactly once per press", (tester) async { + var calls = 0; + await tester.pumpWidget(build(onPressed: () async => calls++)); + + await fire(tester); + await tester.pump(); + await fire(tester); + await tester.pump(); + + expect(calls, 2); + }); + }); + + testWidgets("ignores presses while a previous future is pending", (tester) async { + final completer = Completer(); + var calls = 0; + await tester.pumpWidget( + build( + onPressed: () { + calls++; + return completer.future; + }, + ), + ); + + final pressed = fire(tester); + await tester.pump(); + + unawaited(fire(tester)); + unawaited(fire(tester)); + unawaited(fire(tester)); + await tester.pump(); + + expect(calls, 1, reason: "extra presses must be swallowed"); + + completer.complete(); + await pressed; + await tester.pump(); + + await fire(tester); + await tester.pump(); + expect(calls, 2); + }); + + group("handle errors", () { + testWidgets("clears the loading flag when the callback throws", (tester) async { + await tester.pumpWidget( + build(onPressed: () async => throw StateError("boom")), + ); + + await expectLater(fire(tester), throwsA(isA())); + await tester.pump(); + + expect(inner(tester).isLoading, isFalse, reason: "the finally block must reset the flag"); + }); + + testWidgets("rethrows instead of swallowing the failure", (tester) async { + await tester.pumpWidget( + build(onPressed: () async => throw StateError("boom")), + ); + + await expectLater(fire(tester), throwsA(isA())); + }); + + testWidgets("stays pressable after the callback throws", (tester) async { + var calls = 0; + await tester.pumpWidget( + build( + onPressed: () { + calls++; + throw StateError("boom"); + }, + ), + ); + + await expectLater(fire(tester), throwsA(isA())); + await tester.pump(); + await expectLater(fire(tester), throwsA(isA())); + await tester.pump(); + + expect(calls, 2, reason: "the re-entrancy guard must not latch on failure"); + expect(inner(tester).isLoading, isFalse); + }); + }); + + group("disposal mid-flight", () { + testWidgets("does not setState after being disposed", (tester) async { + final completer = Completer(); + await tester.pumpWidget(build(onPressed: () => completer.future)); + + final pressed = fire(tester); + await tester.pump(); + + await tester.pumpWidget(wrap(const SizedBox.shrink())); + expect(find.byType(NewPrimaryButton), findsNothing); + + completer.complete(); + await pressed; + await tester.pump(); + + expect( + tester.takeException(), + isNull, + reason: "the mounted check must guard the second setState", + ); + }); + + testWidgets("still propagates a throw when disposed", (tester) async { + final completer = Completer(); + await tester.pumpWidget(build(onPressed: () => completer.future)); + + final pressed = fire(tester); + await tester.pump(); + await tester.pumpWidget(wrap(const SizedBox.shrink())); + + completer.completeError(StateError("boom")); + + await expectLater(pressed, throwsA(isA())); + expect( + tester.takeException(), + isNull, + reason: "no setState-after-dispose FlutterError on the error path", + ); + }); + }); + + testWidgets("disabled is forwarded but not enforced by this wrapper", (tester) async { + // Press-blocking lives in NewPrimaryButton; this widget only relays the + // flag. If that ever moves up here, this expectation should change. + var calls = 0; + await tester.pumpWidget( + build(onPressed: () async => calls++, disabled: true), + ); + + expect(inner(tester).disabled, isTrue); + + await fire(tester); + await tester.pump(); + expect(calls, 1); + }); + }); +} diff --git a/tool/configure.dart b/tool/configure.dart index d6b76d756..c0ce610a4 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -442,6 +442,8 @@ abstract class Monero { Map exportOutputsUR(Object wallet); bool needExportOutputs(Object wallet, Money amount); + + bool hasUnknownKeyImages(Object wallet); bool importKeyImagesUR(Object wallet, String ur); @@ -455,7 +457,7 @@ abstract class Monero { HardwareWalletType? hardwareWalletType, required int height}); WalletCredentials createMoneroRestoreWalletFromSeedCredentials({required String name, required String password, required String passphrase, required int height, required String mnemonic}); - WalletCredentials createMoneroRestoreWalletFromHardwareCredentials({required String name, required String password, required int height, required HardwareWalletService hardwareWalletService}); + WalletCredentials createMoneroRestoreWalletFromHardwareCredentials({required String name, required String password, required int height, required HardwareWalletService hardwareWalletService, required String? passphrase}); WalletCredentials createMoneroNewWalletCredentials({required String name, required String language, required int seedType, required String? passphrase, String? password, String? mnemonic}); Map getKeys(Object wallet); int? getRestoreHeight(Object wallet); @@ -981,6 +983,7 @@ abstract class Solana { List getDefaultTokenContractAddresses(); List getDefaultTokenSymbols(); bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress); + Future isTokenVerifiedOnJupiter(WalletBase wallet, String mintAddress); // Jupiter swap transaction handling // Signs and prepares a base64-encoded unsigned transaction for sending @@ -1704,6 +1707,7 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/receive_page_option.dart'; +import 'package:cw_core/wallet_addresses.dart'; """; const zcashCWHeaders = """ @@ -1721,13 +1725,15 @@ abstract class Zcash { WalletInfo? walletInfo, String? password, String? mnemonic, - required String? passphrase}); + required String? passphrase, + int network = 0}); WalletCredentials createZcashRestoreWalletFromSeedCredentials( {required String name, required String mnemonic, required String password, String? passphrase, - required int? height}); + required int? height, + int network = 0}); WalletCredentials createZcashRestoreWalletFromPrivateKey( {required String name, required String privateKey, required String password, required int height}); String getAddress(WalletBase wallet); @@ -1766,6 +1772,8 @@ abstract class Zcash { Future getHeightByDate(DateTime date); bool showMissingFundsCard(WalletBase wallet); Future rescanInternalChange(WalletBase wallet); + bool ironwoodActive(WalletAddresses walletAddresses); + Future hasOrchardMigratableBalance(WalletBase wallet); } """;