diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index e096545c2..925f4e00d 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -96,6 +96,25 @@ jobs: cd /opt/android/cake_wallet flutter pub get + + - name: Install go and gomobile + run: | + # install go > 1.23: + wget https://go.dev/dl/go1.23.1.linux-amd64.tar.gz + sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.1.linux-amd64.tar.gz + export PATH=$PATH:/usr/local/go/bin + export PATH=$PATH:~/go/bin + go install golang.org/x/mobile/cmd/gomobile@latest + gomobile init + + - name: Build mwebd + run: | + # paths are reset after each step, so we need to set them again: + export PATH=$PATH:/usr/local/go/bin + export PATH=$PATH:~/go/bin + cd /opt/android/cake_wallet/scripts/android/ + ./build_mwebd.sh --dont-install + - name: Generate KeyStore run: | cd /opt/android/cake_wallet/android/app diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 7713cc95d..5ea0cb377 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -89,6 +89,25 @@ jobs: cd /opt/android/cake_wallet flutter pub get + - name: Install go and gomobile + run: | + # install go > 1.23: + wget https://go.dev/dl/go1.23.1.linux-amd64.tar.gz + sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.1.linux-amd64.tar.gz + export PATH=$PATH:/usr/local/go/bin + export PATH=$PATH:~/go/bin + go install golang.org/x/mobile/cmd/gomobile@latest + gomobile init + + - name: Build mwebd + run: | + # paths are reset after each step, so we need to set them again: + export PATH=$PATH:/usr/local/go/bin + export PATH=$PATH:~/go/bin + # build mwebd: + cd /opt/android/cake_wallet/scripts/android/ + ./build_mwebd.sh --dont-install + - name: Generate localization run: | cd /opt/android/cake_wallet diff --git a/assets/images/mweb_logo.png b/assets/images/mweb_logo.png new file mode 100644 index 000000000..92317203e Binary files /dev/null and b/assets/images/mweb_logo.png differ diff --git a/assets/images/wallet_group.png b/assets/images/wallet_group.png deleted file mode 100644 index f4a577b22..000000000 Binary files a/assets/images/wallet_group.png and /dev/null differ diff --git a/assets/images/wallet_group_bright.png b/assets/images/wallet_group_bright.png new file mode 100644 index 000000000..263361db6 Binary files /dev/null and b/assets/images/wallet_group_bright.png differ diff --git a/assets/images/wallet_group_dark.png b/assets/images/wallet_group_dark.png new file mode 100644 index 000000000..7cd08d2cd Binary files /dev/null and b/assets/images/wallet_group_dark.png differ diff --git a/assets/images/wallet_group_light.png b/assets/images/wallet_group_light.png new file mode 100644 index 000000000..7827971e7 Binary files /dev/null and b/assets/images/wallet_group_light.png differ diff --git a/assets/solana_node_list.yml b/assets/solana_node_list.yml index e3ff9138e..e5641d3f8 100644 --- a/assets/solana_node_list.yml +++ b/assets/solana_node_list.yml @@ -4,4 +4,7 @@ useSSL: true - uri: api.mainnet-beta.solana.com:443 + useSSL: true +- + uri: solana-rpc.publicnode.com:443 useSSL: true \ No newline at end of file diff --git a/cw_bitcoin/lib/bitcoin_mnemonic.dart b/cw_bitcoin/lib/bitcoin_mnemonic.dart index 0749627e9..21ff3891e 100644 --- a/cw_bitcoin/lib/bitcoin_mnemonic.dart +++ b/cw_bitcoin/lib/bitcoin_mnemonic.dart @@ -8,6 +8,7 @@ import 'package:cw_core/sec_random_native.dart'; import 'package:cw_core/utils/text_normalizer.dart'; const segwit = '100'; +const mweb = 'eb'; final wordlist = englishWordlist; double logBase(num x, num base) => log(x) / log(base); @@ -125,7 +126,7 @@ Future mnemonicToSeedBytes(String mnemonic, return Uint8List.fromList(bytes); } -bool matchesAnyPrefix(String mnemonic) => prefixMatches(mnemonic, [segwit]).any((el) => el); +bool matchesAnyPrefix(String mnemonic) => prefixMatches(mnemonic, [segwit, mweb]).any((el) => el); bool validateMnemonic(String mnemonic, {String prefix = segwit}) { try { diff --git a/cw_bitcoin/lib/bitcoin_receive_page_option.dart b/cw_bitcoin/lib/bitcoin_receive_page_option.dart index aa3d4a4cd..8e72bc7b0 100644 --- a/cw_bitcoin/lib/bitcoin_receive_page_option.dart +++ b/cw_bitcoin/lib/bitcoin_receive_page_option.dart @@ -7,6 +7,7 @@ class BitcoinReceivePageOption implements ReceivePageOption { static const p2tr = BitcoinReceivePageOption._('Taproot (P2TR)'); static const p2wsh = BitcoinReceivePageOption._('Segwit (P2WSH)'); static const p2pkh = BitcoinReceivePageOption._('Legacy (P2PKH)'); + static const mweb = BitcoinReceivePageOption._('MWEB'); static const silent_payments = BitcoinReceivePageOption._('Silent Payments'); @@ -27,6 +28,11 @@ class BitcoinReceivePageOption implements ReceivePageOption { BitcoinReceivePageOption.p2pkh ]; + static const allLitecoin = [ + BitcoinReceivePageOption.p2wpkh, + BitcoinReceivePageOption.mweb + ]; + BitcoinAddressType toType() { switch (this) { case BitcoinReceivePageOption.p2tr: @@ -39,6 +45,8 @@ class BitcoinReceivePageOption implements ReceivePageOption { return P2shAddressType.p2wpkhInP2sh; case BitcoinReceivePageOption.silent_payments: return SilentPaymentsAddresType.p2sp; + case BitcoinReceivePageOption.mweb: + return SegwitAddresType.mweb; case BitcoinReceivePageOption.p2wpkh: default: return SegwitAddresType.p2wpkh; @@ -51,6 +59,8 @@ class BitcoinReceivePageOption implements ReceivePageOption { return BitcoinReceivePageOption.p2tr; case SegwitAddresType.p2wsh: return BitcoinReceivePageOption.p2wsh; + case SegwitAddresType.mweb: + return BitcoinReceivePageOption.mweb; case P2pkhAddressType.p2pkh: return BitcoinReceivePageOption.p2pkh; case P2shAddressType.p2wpkhInP2sh: diff --git a/cw_bitcoin/lib/bitcoin_transaction_priority.dart b/cw_bitcoin/lib/bitcoin_transaction_priority.dart index 7c4dcfd5f..d1f45a545 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_priority.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_priority.dart @@ -87,7 +87,7 @@ class LitecoinTransactionPriority extends BitcoinTransactionPriority { } @override - String get units => 'Latoshi'; + String get units => 'Litoshi'; @override String toString() { diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index d8cca5b16..cfcc71d09 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -506,6 +506,12 @@ class ElectrumClient { void _methodHandler({required String method, required Map request}) { switch (method) { + case 'blockchain.headers.subscribe': + final params = request['params'] as List; + final id = 'blockchain.headers.subscribe'; + + _tasks[id]?.subject?.add(params.last); + break; case 'blockchain.scripthash.subscribe': final params = request['params'] as List; final scripthash = params.first as String?; diff --git a/cw_bitcoin/lib/electrum_balance.dart b/cw_bitcoin/lib/electrum_balance.dart index 15d6843d8..4e37f40b1 100644 --- a/cw_bitcoin/lib/electrum_balance.dart +++ b/cw_bitcoin/lib/electrum_balance.dart @@ -7,7 +7,14 @@ class ElectrumBalance extends Balance { required this.confirmed, required this.unconfirmed, required this.frozen, - }) : super(confirmed, unconfirmed); + this.secondConfirmed = 0, + this.secondUnconfirmed = 0, + }) : super( + confirmed, + unconfirmed, + secondAvailable: secondConfirmed, + secondAdditional: secondUnconfirmed, + ); static ElectrumBalance? fromJSON(String? jsonSource) { if (jsonSource == null) { @@ -25,9 +32,12 @@ class ElectrumBalance extends Balance { int confirmed; int unconfirmed; final int frozen; + int secondConfirmed = 0; + int secondUnconfirmed = 0; @override - String get formattedAvailableBalance => bitcoinAmountToString(amount: confirmed - frozen); + String get formattedAvailableBalance => + bitcoinAmountToString(amount: confirmed - frozen); @override String get formattedAdditionalBalance => bitcoinAmountToString(amount: unconfirmed); @@ -38,6 +48,21 @@ class ElectrumBalance extends Balance { return frozenFormatted == '0.0' ? '' : frozenFormatted; } - String toJSON() => - json.encode({'confirmed': confirmed, 'unconfirmed': unconfirmed, 'frozen': frozen}); + @override + String get formattedSecondAvailableBalance => bitcoinAmountToString(amount: secondConfirmed); + + @override + String get formattedSecondAdditionalBalance => bitcoinAmountToString(amount: secondUnconfirmed); + + @override + String get formattedFullAvailableBalance => + bitcoinAmountToString(amount: confirmed + secondConfirmed - frozen); + + String toJSON() => json.encode({ + 'confirmed': confirmed, + 'unconfirmed': unconfirmed, + 'frozen': frozen, + 'secondConfirmed': secondConfirmed, + 'secondUnconfirmed': secondUnconfirmed + }); } diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index f2ef251c7..1ab7799e3 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -35,7 +35,7 @@ class ElectrumTransactionInfo extends TransactionInfo { List? outputAddresses, required TransactionDirection direction, required bool isPending, - required bool isReplaced, + bool isReplaced = false, required DateTime date, required int confirmations, String? to, diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index fee115650..4f8f3eab5 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -2,9 +2,9 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; -import 'dart:math'; import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/litecoin_wallet_addresses.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; @@ -23,6 +23,7 @@ import 'package:cw_bitcoin/electrum_transaction_history.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/exceptions.dart'; +import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -112,11 +113,18 @@ abstract class ElectrumWalletBase } if (seedBytes != null) { - return currency == CryptoCurrency.bch - ? bitcoinCashHDWallet(seedBytes) - : Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath( + switch (currency) { + case CryptoCurrency.btc: + case CryptoCurrency.ltc: + case CryptoCurrency.tbtc: + return Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath( _hardenedDerivationPath(derivationInfo?.derivationPath ?? electrum_path)) as Bip32Slip10Secp256k1; + case CryptoCurrency.bch: + return bitcoinCashHDWallet(seedBytes); + default: + throw Exception("Unsupported currency"); + } } return Bip32Slip10Secp256k1.fromExtendedKey(xpub!); @@ -163,11 +171,13 @@ abstract class ElectrumWalletBase Set get addressesSet => walletAddresses.allAddresses.map((addr) => addr.address).toSet(); List get scriptHashes => walletAddresses.addressesByReceiveType + .where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress) .map((addr) => (addr as BitcoinAddressRecord).getScriptHash(network)) .toList(); List get publicScriptHashes => walletAddresses.allAddresses .where((addr) => !addr.isHidden) + .where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress) .map((addr) => addr.getScriptHash(network)) .toList(); @@ -274,6 +284,7 @@ abstract class ElectrumWalletBase void Function(FlutterErrorDetails)? _onError; Timer? _autoSaveTimer; + StreamSubscription? _receiveStream; Timer? _updateFeeRateTimer; static const int _autoSaveInterval = 1; @@ -324,7 +335,8 @@ abstract class ElectrumWalletBase isSingleScan: doSingleScan ?? false, )); - await for (var message in receivePort) { + _receiveStream?.cancel(); + _receiveStream = receivePort.listen((var message) async { if (message is Map) { for (final map in message.entries) { final txid = map.key; @@ -387,10 +399,16 @@ abstract class ElectrumWalletBase nodeSupportsSilentPayments = false; } - syncStatus = message.syncStatus; + if (message.syncStatus is SyncingSyncStatus) { + var status = message.syncStatus as SyncingSyncStatus; + syncStatus = SyncingSyncStatus(status.blocksLeft, status.ptc); + } else { + syncStatus = message.syncStatus; + } + await walletInfo.updateRestoreHeight(message.height); } - } + }); } void _updateSilentAddressRecord(BitcoinSilentPaymentsUnspent unspent) { @@ -430,9 +448,9 @@ abstract class ElectrumWalletBase await _setInitialHeight(); } - await _subscribeForUpdates(); - + await subscribeForUpdates(); await updateTransactions(); + await updateAllUnspents(); await updateBalance(); await updateFeeRates(); @@ -537,6 +555,7 @@ abstract class ElectrumWalletBase try { syncStatus = ConnectingSyncStatus(); + await _receiveStream?.cancel(); await electrumClient.close(); electrumClient.onConnectionStatusChange = _onConnectionStatusChange; @@ -682,26 +701,15 @@ abstract class ElectrumWalletBase paysToSilentPayment: hasSilentPayment, ); - int estimatedSize; - if (network is BitcoinCashNetwork) { - estimatedSize = ForkedTransactionBuilder.estimateTransactionSize( - utxos: utxoDetails.utxos, - outputs: outputs, - network: network as BitcoinCashNetwork, - memo: memo, - ); - } else { - estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize( - utxos: utxoDetails.utxos, - outputs: outputs, - network: network, - memo: memo, - inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, - vinOutpoints: utxoDetails.vinOutpoints, - ); - } - - int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize); + int fee = await calcFee( + utxos: utxoDetails.utxos, + outputs: outputs, + network: network, + memo: memo, + feeRate: feeRate, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + vinOutpoints: utxoDetails.vinOutpoints, + ); if (fee == 0) { throw BitcoinTransactionNoFeeException(); @@ -788,7 +796,10 @@ abstract class ElectrumWalletBase throw BitcoinTransactionWrongBalanceException(); } - final changeAddress = await walletAddresses.getChangeAddress(); + final changeAddress = await walletAddresses.getChangeAddress( + outputs: outputs, + utxoDetails: utxoDetails, + ); final address = RegexUtils.addressTypeFromStr(changeAddress, network); outputs.add(BitcoinOutput( address: address, @@ -796,26 +807,13 @@ abstract class ElectrumWalletBase isChange: true, )); - int estimatedSize; - if (network is BitcoinCashNetwork) { - estimatedSize = ForkedTransactionBuilder.estimateTransactionSize( - utxos: utxoDetails.utxos, - outputs: outputs, - network: network as BitcoinCashNetwork, - memo: memo, - ); - } else { - estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize( - utxos: utxoDetails.utxos, - outputs: outputs, - network: network, - memo: memo, - inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, - vinOutpoints: utxoDetails.vinOutpoints, - ); - } - - int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize); + int fee = await calcFee( + utxos: utxoDetails.utxos, + outputs: outputs, + network: network, + memo: memo, + feeRate: feeRate, + ); if (fee == 0) { throw BitcoinTransactionNoFeeException(); @@ -825,6 +823,8 @@ abstract class ElectrumWalletBase final lastOutput = outputs.last; final amountLeftForChange = amountLeftForChangeAndFee - fee; + print(amountLeftForChangeAndFee); + if (!_isBelowDust(amountLeftForChange)) { // Here, lastOutput already is change, return the amount left without the fee to the user's address. outputs[outputs.length - 1] = BitcoinOutput( @@ -874,7 +874,7 @@ abstract class ElectrumWalletBase final totalAmount = amount + fee; - if (totalAmount > balance[currency]!.confirmed) { + if (totalAmount > (balance[currency]!.confirmed + balance[currency]!.secondConfirmed)) { throw BitcoinTransactionWrongBalanceException(); } @@ -909,6 +909,37 @@ abstract class ElectrumWalletBase ); } + Future calcFee({ + required List utxos, + required List outputs, + required BasedUtxoNetwork network, + String? memo, + required int feeRate, + List? inputPrivKeyInfos, + List? vinOutpoints, + }) async { + int estimatedSize; + if (network is BitcoinCashNetwork) { + estimatedSize = ForkedTransactionBuilder.estimateTransactionSize( + utxos: utxos, + outputs: outputs, + network: network, + memo: memo, + ); + } else { + estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize( + utxos: utxos, + outputs: outputs, + network: network, + memo: memo, + inputPrivKeyInfos: inputPrivKeyInfos, + vinOutpoints: vinOutpoints, + ); + } + + return feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize); + } + @override Future createTransaction(Object credentials) async { try { @@ -1134,6 +1165,7 @@ abstract class ElectrumWalletBase 'derivationPath': walletInfo.derivationInfo?.derivationPath, 'silent_addresses': walletAddresses.silentAddresses.map((addr) => addr.toJSON()).toList(), 'silent_address_index': walletAddresses.currentSilentAddressIndex.toString(), + 'mweb_addresses': walletAddresses.mwebAddresses.map((addr) => addr.toJSON()).toList(), }); int feeRate(TransactionPriority priority) { @@ -1253,6 +1285,7 @@ abstract class ElectrumWalletBase @override Future close() async { try { + await _receiveStream?.cancel(); await electrumClient.close(); } catch (_) {} _autoSaveTimer?.cancel(); @@ -1272,77 +1305,66 @@ abstract class ElectrumWalletBase }); } + // Set the balance of all non-silent payment addresses to 0 before updating + walletAddresses.allAddresses.forEach((addr) { + if(addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0; + }); + await Future.wait(walletAddresses.allAddresses.map((address) async { updatedUnspentCoins.addAll(await fetchUnspent(address)); })); unspentCoins = updatedUnspentCoins; - if (unspentCoinsInfo.isEmpty) { - unspentCoins.forEach((coin) => _addCoinInfo(coin)); + if (unspentCoinsInfo.length != updatedUnspentCoins.length) { + unspentCoins.forEach((coin) => addCoinInfo(coin)); return; } - if (unspentCoins.isNotEmpty) { - unspentCoins.forEach((coin) { - final coinInfoList = unspentCoinsInfo.values.where((element) => - element.walletId.contains(id) && - element.hash.contains(coin.hash) && - element.vout == coin.vout); - - if (coinInfoList.isNotEmpty) { - final coinInfo = coinInfoList.first; - - coin.isFrozen = coinInfo.isFrozen; - coin.isSending = coinInfo.isSending; - coin.note = coinInfo.note; - if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) - coin.bitcoinAddressRecord.balance += coinInfo.value; - } else { - _addCoinInfo(coin); - } - }); - } - + await updateCoins(unspentCoins); await _refreshUnspentCoinsInfo(); } - @action - Future updateUnspents(BitcoinAddressRecord address) async { - final newUnspentCoins = await fetchUnspent(address); - - if (newUnspentCoins.isNotEmpty) { - unspentCoins.addAll(newUnspentCoins); - - newUnspentCoins.forEach((coin) { - final coinInfoList = unspentCoinsInfo.values.where( - (element) => - element.walletId.contains(id) && - element.hash.contains(coin.hash) && - element.vout == coin.vout, - ); - - if (coinInfoList.isNotEmpty) { - final coinInfo = coinInfoList.first; - - coin.isFrozen = coinInfo.isFrozen; - coin.isSending = coinInfo.isSending; - coin.note = coinInfo.note; - if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) - coin.bitcoinAddressRecord.balance += coinInfo.value; - } else { - _addCoinInfo(coin); - } - }); + Future updateCoins(List newUnspentCoins) async { + if (newUnspentCoins.isEmpty) { + return; } + + newUnspentCoins.forEach((coin) { + final coinInfoList = unspentCoinsInfo.values.where( + (element) => + element.walletId.contains(id) && + element.hash.contains(coin.hash) && + element.vout == coin.vout, + ); + + if (coinInfoList.isNotEmpty) { + final coinInfo = coinInfoList.first; + + coin.isFrozen = coinInfo.isFrozen; + coin.isSending = coinInfo.isSending; + coin.note = coinInfo.note; + if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) + coin.bitcoinAddressRecord.balance += coinInfo.value; + } else { + addCoinInfo(coin); + } + }); + } + + @action + Future updateUnspentsForAddress(BitcoinAddressRecord address) async { + final newUnspentCoins = await fetchUnspent(address); + await updateCoins(newUnspentCoins); } @action Future> fetchUnspent(BitcoinAddressRecord address) async { - final unspents = await electrumClient.getListUnspent(address.getScriptHash(network)); - + List> unspents = []; List updatedUnspentCoins = []; + unspents = await electrumClient.getListUnspent(address.getScriptHash(network)); + await Future.wait(unspents.map((unspent) async { try { final coin = BitcoinUnspent.fromJSON(address, unspent); @@ -1358,7 +1380,7 @@ abstract class ElectrumWalletBase } @action - Future _addCoinInfo(BitcoinUnspent coin) async { + Future addCoinInfo(BitcoinUnspent coin) async { final newInfo = UnspentCoinsInfo( walletId: id, hash: coin.hash, @@ -1709,12 +1731,14 @@ abstract class ElectrumWalletBase final Map historiesWithDetails = {}; if (type == WalletType.bitcoin) { - await Future.wait(ADDRESS_TYPES + await Future.wait(BITCOIN_ADDRESS_TYPES .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); } else if (type == WalletType.bitcoinCash) { - await fetchTransactionsForAddressType(historiesWithDetails, P2pkhAddressType.p2pkh); + await Future.wait(BITCOIN_CASH_ADDRESS_TYPES + .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); } else if (type == WalletType.litecoin) { - await fetchTransactionsForAddressType(historiesWithDetails, SegwitAddresType.p2wpkh); + await Future.wait(LITECOIN_ADDRESS_TYPES + .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); } transactionHistory.transactions.values.forEach((tx) async { @@ -1748,7 +1772,8 @@ abstract class ElectrumWalletBase 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)); + await walletAddresses.saveAddressesInBox(); await Future.wait(addressesByType.map((addressRecord) async { final history = await _fetchAddressHistory(addressRecord, await getCurrentChainTip()); @@ -1771,7 +1796,7 @@ abstract class ElectrumWalletBase matchedAddresses.toList(), addressRecord.isHidden, (address) async { - await _subscribeForUpdates(); + await subscribeForUpdates(); return _fetchAddressHistory(address, await getCurrentChainTip()) .then((history) => history.isNotEmpty ? address.address : null); }, @@ -1860,7 +1885,7 @@ abstract class ElectrumWalletBase } } - Future _subscribeForUpdates() async { + Future subscribeForUpdates() async { final unsubscribedScriptHashes = walletAddresses.allAddresses.where( (address) => !_scripthashesUpdateSubject.containsKey(address.getScriptHash(network)), ); @@ -1871,7 +1896,7 @@ abstract class ElectrumWalletBase _scripthashesUpdateSubject[sh] = await electrumClient.scripthashUpdate(sh); _scripthashesUpdateSubject[sh]?.listen((event) async { try { - await updateUnspents(address); + await updateUnspentsForAddress(address); await updateBalance(); @@ -1888,8 +1913,10 @@ abstract class ElectrumWalletBase })); } - Future _fetchBalances() async { - final addresses = walletAddresses.allAddresses.toList(); + Future fetchBalances() async { + final addresses = walletAddresses.allAddresses + .where((address) => RegexUtils.addressTypeFromStr(address.address, network) is! MwebAddress) + .toList(); final balanceFutures = >>[]; for (var i = 0; i < addresses.length; i++) { final addressRecord = addresses[i]; @@ -1902,6 +1929,18 @@ abstract class ElectrumWalletBase var totalConfirmed = 0; var totalUnconfirmed = 0; + unspentCoinsInfo.values.forEach((info) { + unspentCoins.forEach((element) { + if (element.hash == info.hash && + element.vout == info.vout && + info.isFrozen && + element.bitcoinAddressRecord.address == info.address && + element.value == info.value) { + totalFrozen += element.value; + } + }); + }); + if (hasSilentPaymentsScanning) { // Add values from unspent coins that are not fetched by the address list // i.e. scanned silent payments @@ -1927,6 +1966,7 @@ abstract class ElectrumWalletBase totalConfirmed += confirmed; totalUnconfirmed += unconfirmed; + addressRecord.balance = confirmed + unconfirmed; if (confirmed > 0 || unconfirmed > 0) { addressRecord.setAsUsed(); } @@ -1940,22 +1980,10 @@ abstract class ElectrumWalletBase } Future updateBalance() async { - balance[currency] = await _fetchBalances(); + balance[currency] = await fetchBalances(); await save(); } - String getChangeAddress() { - const minCountOfHiddenAddresses = 5; - final random = Random(); - var addresses = walletAddresses.allAddresses.where((addr) => addr.isHidden).toList(); - - if (addresses.length < minCountOfHiddenAddresses) { - addresses = walletAddresses.allAddresses.toList(); - } - - return addresses[random.nextInt(addresses.length)].address; - } - @override void setExceptionHandler(void Function(FlutterErrorDetails) onError) => _onError = onError; @@ -2458,6 +2486,8 @@ BitcoinAddressType _getScriptType(BitcoinBaseAddress type) { return SegwitAddresType.p2wsh; } else if (type is P2trAddress) { return SegwitAddresType.p2tr; + } else if (type is MwebAddress) { + return SegwitAddresType.mweb; } else if (type is SilentPaymentsAddresType) { return SilentPaymentsAddresType.p2sp; } else { diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index a0424c934..61d4fdbc3 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -1,6 +1,7 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -10,7 +11,7 @@ part 'electrum_wallet_addresses.g.dart'; class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses; -const List ADDRESS_TYPES = [ +const List BITCOIN_ADDRESS_TYPES = [ SegwitAddresType.p2wpkh, P2pkhAddressType.p2pkh, SegwitAddresType.p2tr, @@ -18,6 +19,15 @@ const List ADDRESS_TYPES = [ P2shAddressType.p2wpkhInP2sh, ]; +const List LITECOIN_ADDRESS_TYPES = [ + SegwitAddresType.p2wpkh, + SegwitAddresType.mweb, +]; + +const List BITCOIN_CASH_ADDRESS_TYPES = [ + P2pkhAddressType.p2pkh, +]; + abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { ElectrumWalletAddressesBase( WalletInfo walletInfo, { @@ -29,6 +39,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { Map? initialChangeAddressIndex, List? initialSilentAddresses, int initialSilentAddressIndex = 0, + List? initialMwebAddresses, Bip32Slip10Secp256k1? masterHd, BitcoinAddressType? initialAddressPageType, }) : _addresses = ObservableList.of((initialAddresses ?? []).toSet()), @@ -49,6 +60,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { silentAddresses = ObservableList.of( (initialSilentAddresses ?? []).toSet()), currentSilentAddressIndex = initialSilentAddressIndex, + mwebAddresses = + ObservableList.of((initialMwebAddresses ?? []).toSet()), super(walletInfo) { if (masterHd != null) { silentAddress = SilentPaymentOwner.fromPrivateKeys( @@ -91,6 +104,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final ObservableList receiveAddresses; final ObservableList changeAddresses; final ObservableList silentAddresses; + final ObservableList mwebAddresses; final BasedUtxoNetwork network; final Bip32Slip10Secp256k1 mainHd; final Bip32Slip10Secp256k1 sideHd; @@ -149,6 +163,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @override set address(String addr) { + if (addr == "Silent Payments" && SilentPaymentsAddresType.p2sp != addressPageType) { + return; + } if (addressPageType == SilentPaymentsAddresType.p2sp) { final selected = silentAddresses.firstWhere((addressRecord) => addressRecord.address == addr); @@ -160,12 +177,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } return; } - - final addressRecord = _addresses.firstWhere((addressRecord) => addressRecord.address == addr); + try { + final addressRecord = _addresses.firstWhere( + (addressRecord) => addressRecord.address == addr, + ); previousAddressRecord = addressRecord; receiveAddresses.remove(addressRecord); receiveAddresses.insert(0, addressRecord); + } catch (e) { + print("ElectrumWalletAddressBase: set address ($addr): $e"); + } } @override @@ -213,7 +235,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { if (walletInfo.type == WalletType.bitcoinCash) { await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); } else if (walletInfo.type == WalletType.litecoin) { - await _generateInitialAddresses(); + await _generateInitialAddresses(type: SegwitAddresType.p2wpkh); + await _generateInitialAddresses(type: SegwitAddresType.mweb); } else if (walletInfo.type == WalletType.bitcoin) { await _generateInitialAddresses(); await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); @@ -221,6 +244,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { await _generateInitialAddresses(type: SegwitAddresType.p2tr); await _generateInitialAddresses(type: SegwitAddresType.p2wsh); } + updateAddressesByMatch(); updateReceiveAddresses(); updateChangeAddresses(); @@ -237,7 +261,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @action - Future getChangeAddress() async { + Future getChangeAddress({List? outputs, UtxoDetails? utxoDetails}) async { updateChangeAddresses(); if (changeAddresses.isEmpty) { @@ -317,12 +341,110 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return address; } - String getAddress( - {required int index, - required Bip32Slip10Secp256k1 hd, - BitcoinAddressType? addressType}) => + String getAddress({ + required int index, + required Bip32Slip10Secp256k1 hd, + BitcoinAddressType? addressType, + }) => ''; + Future getAddressAsync({ + required int index, + required Bip32Slip10Secp256k1 hd, + BitcoinAddressType? addressType, + }) async => + getAddress(index: index, hd: hd, addressType: addressType); + + void addBitcoinAddressTypes() { + final lastP2wpkh = _addresses + .where((addressRecord) => + _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh)) + .toList() + .last; + if (lastP2wpkh.address != address) { + addressesMap[lastP2wpkh.address] = 'P2WPKH'; + } else { + addressesMap[address] = 'Active - P2WPKH'; + } + + final lastP2pkh = _addresses.firstWhere( + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh)); + if (lastP2pkh.address != address) { + addressesMap[lastP2pkh.address] = 'P2PKH'; + } else { + addressesMap[address] = 'Active - P2PKH'; + } + + final lastP2sh = _addresses.firstWhere((addressRecord) => + _isUnusedReceiveAddressByType(addressRecord, P2shAddressType.p2wpkhInP2sh)); + if (lastP2sh.address != address) { + addressesMap[lastP2sh.address] = 'P2SH'; + } else { + addressesMap[address] = 'Active - P2SH'; + } + + final lastP2tr = _addresses.firstWhere( + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr)); + if (lastP2tr.address != address) { + addressesMap[lastP2tr.address] = 'P2TR'; + } else { + addressesMap[address] = 'Active - P2TR'; + } + + final lastP2wsh = _addresses.firstWhere( + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh)); + if (lastP2wsh.address != address) { + addressesMap[lastP2wsh.address] = 'P2WSH'; + } else { + addressesMap[address] = 'Active - P2WSH'; + } + + silentAddresses.forEach((addressRecord) { + if (addressRecord.type != SilentPaymentsAddresType.p2sp || addressRecord.isHidden) { + return; + } + + if (addressRecord.address != address) { + addressesMap[addressRecord.address] = addressRecord.name.isEmpty + ? "Silent Payments" + : "Silent Payments - " + addressRecord.name; + } else { + addressesMap[address] = 'Active - Silent Payments'; + } + }); + } + + void addLitecoinAddressTypes() { + final lastP2wpkh = _addresses + .where((addressRecord) => + _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh)) + .toList() + .last; + if (lastP2wpkh.address != address) { + addressesMap[lastP2wpkh.address] = 'P2WPKH'; + } else { + addressesMap[address] = 'Active - P2WPKH'; + } + + final lastMweb = _addresses.firstWhere( + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.mweb)); + if (lastMweb.address != address) { + addressesMap[lastMweb.address] = 'MWEB'; + } else { + addressesMap[address] = 'Active - MWEB'; + } + } + + void addBitcoinCashAddressTypes() { + final lastP2pkh = _addresses.firstWhere( + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh)); + if (lastP2pkh.address != address) { + addressesMap[lastP2pkh.address] = 'P2PKH'; + } else { + addressesMap[address] = 'Active - P2PKH'; + } + } + @override Future updateAddressesInBox() async { try { @@ -334,63 +456,20 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { allAddressesMap[addressRecord.address] = addressRecord.name; }); - final lastP2wpkh = _addresses - .where((addressRecord) => - _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh)) - .toList() - .last; - if (lastP2wpkh.address != address) { - addressesMap[lastP2wpkh.address] = 'P2WPKH'; - } else { - addressesMap[address] = 'Active - P2WPKH'; + switch (walletInfo.type) { + case WalletType.bitcoin: + addBitcoinAddressTypes(); + break; + case WalletType.litecoin: + addLitecoinAddressTypes(); + break; + case WalletType.bitcoinCash: + addBitcoinCashAddressTypes(); + break; + default: + break; } - final lastP2pkh = _addresses.firstWhere( - (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh)); - if (lastP2pkh.address != address) { - addressesMap[lastP2pkh.address] = 'P2PKH'; - } else { - addressesMap[address] = 'Active - P2PKH'; - } - - final lastP2sh = _addresses.firstWhere((addressRecord) => - _isUnusedReceiveAddressByType(addressRecord, P2shAddressType.p2wpkhInP2sh)); - if (lastP2sh.address != address) { - addressesMap[lastP2sh.address] = 'P2SH'; - } else { - addressesMap[address] = 'Active - P2SH'; - } - - final lastP2tr = _addresses.firstWhere( - (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr)); - if (lastP2tr.address != address) { - addressesMap[lastP2tr.address] = 'P2TR'; - } else { - addressesMap[address] = 'Active - P2TR'; - } - - final lastP2wsh = _addresses.firstWhere( - (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh)); - if (lastP2wsh.address != address) { - addressesMap[lastP2wsh.address] = 'P2WSH'; - } else { - addressesMap[address] = 'Active - P2WSH'; - } - - silentAddresses.forEach((addressRecord) { - if (addressRecord.type != SilentPaymentsAddresType.p2sp || addressRecord.isHidden) { - return; - } - - if (addressRecord.address != address) { - addressesMap[addressRecord.address] = addressRecord.name.isEmpty - ? "Silent Payments" - : "Silent Payments - " + addressRecord.name; - } else { - addressesMap[address] = 'Active - Silent Payments'; - } - }); - await saveAddressesInBox(); } catch (e) { print(e.toString()); @@ -410,6 +489,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { foundAddress = addressRecord; } }); + mwebAddresses.forEach((addressRecord) { + if (addressRecord.address == address) { + foundAddress = addressRecord; + } + }); if (foundAddress != null) { foundAddress!.setNewName(label); @@ -510,7 +594,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { for (var i = startIndex; i < count + startIndex; i++) { final address = BitcoinAddressRecord( - getAddress(index: i, hd: _getHd(isHidden), addressType: type ?? addressPageType), + await getAddressAsync(index: i, hd: _getHd(isHidden), addressType: type ?? addressPageType), index: i, isHidden: isHidden, type: type ?? addressPageType, @@ -540,15 +624,28 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { updateAddressesByMatch(); } + @action + void addMwebAddresses(Iterable addresses) { + final addressesSet = this.mwebAddresses.toSet(); + addressesSet.addAll(addresses); + this.mwebAddresses.clear(); + this.mwebAddresses.addAll(addressesSet); + updateAddressesByMatch(); + } + void _validateAddresses() { - _addresses.forEach((element) { + _addresses.forEach((element) async { + if (element.type == SegwitAddresType.mweb) { + // this would add a ton of startup lag for mweb addresses since we have 1000 of them + return; + } if (!element.isHidden && element.address != - getAddress(index: element.index, hd: mainHd, addressType: element.type)) { + await getAddressAsync(index: element.index, hd: mainHd, addressType: element.type)) { element.isHidden = true; } else if (element.isHidden && element.address != - getAddress(index: element.index, hd: sideHd, addressType: element.type)) { + await getAddressAsync(index: element.index, hd: sideHd, addressType: element.type)) { element.isHidden = false; } }); diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index fa58be238..25cc5637e 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -23,6 +23,7 @@ class ElectrumWalletSnapshot { required this.addressPageType, required this.silentAddresses, required this.silentAddressIndex, + required this.mwebAddresses, this.passphrase, this.derivationType, this.derivationPath, @@ -44,6 +45,8 @@ class ElectrumWalletSnapshot { List addresses; List silentAddresses; + List mwebAddresses; + ElectrumBalance balance; Map regularAddressIndex; Map changeAddressIndex; @@ -56,10 +59,11 @@ class ElectrumWalletSnapshot { final path = await pathForWallet(name: name, type: type); final jsonSource = await encryptionFileUtils.read(path: path, password: password); final data = json.decode(jsonSource) as Map; - final addressesTmp = data['addresses'] as List? ?? []; final mnemonic = data['mnemonic'] as String?; final xpub = data['xpub'] as String?; final passphrase = data['passphrase'] as String? ?? ''; + + final addressesTmp = data['addresses'] as List? ?? []; final addresses = addressesTmp .whereType() .map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network)) @@ -71,6 +75,12 @@ class ElectrumWalletSnapshot { .map((addr) => BitcoinSilentPaymentAddressRecord.fromJSON(addr, network: network)) .toList(); + final mwebAddressTmp = data['mweb_addresses'] as List? ?? []; + final mwebAddresses = mwebAddressTmp + .whereType() + .map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network)) + .toList(); + final balance = ElectrumBalance.fromJSON(data['balance'] as String?) ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; @@ -113,6 +123,7 @@ class ElectrumWalletSnapshot { derivationPath: derivationPath, silentAddresses: silentAddresses, silentAddressIndex: silentAddressIndex, + mwebAddresses: mwebAddresses, ); } } diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 2e4683ae6..d7e6fef61 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1,15 +1,31 @@ +import 'dart:async'; import 'dart:convert'; - +import 'package:convert/convert.dart' as convert; +import 'dart:math'; +import 'package:collection/collection.dart'; +import 'package:crypto/crypto.dart'; +import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; +import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/mweb_utxo.dart'; +import 'package:cw_mweb/mwebd.pbgrpc.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:bip39/bip39.dart' as bip39; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:blockchain_utils/signer/ecdsa_signing_key.dart'; -import 'package:bip39/bip39.dart' as bip39; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; +import 'package:cw_bitcoin/bitcoin_unspent.dart'; +import 'package:cw_bitcoin/electrum_transaction_info.dart'; +import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; +import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; @@ -19,8 +35,11 @@ import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; import 'package:flutter/foundation.dart'; +import 'package:grpc/grpc.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_mweb/cw_mweb.dart'; import 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart'; import 'package:pointycastle/ecc/api.dart'; import 'package:pointycastle/ecc/curves/secp256k1.dart'; @@ -40,34 +59,54 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { String? passphrase, String? addressPageType, List? initialAddresses, + List? initialMwebAddresses, ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, + int? initialMwebHeight, + bool? alwaysScan, }) : super( - mnemonic: mnemonic, - password: password, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfo, - network: LitecoinNetwork.mainnet, - initialAddresses: initialAddresses, - initialBalance: initialBalance, - seedBytes: seedBytes, - encryptionFileUtils: encryptionFileUtils, - passphrase: passphrase, - currency: CryptoCurrency.ltc) { + mnemonic: mnemonic, + password: password, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + network: LitecoinNetwork.mainnet, + initialAddresses: initialAddresses, + initialBalance: initialBalance, + seedBytes: seedBytes, + encryptionFileUtils: encryptionFileUtils, + currency: CryptoCurrency.ltc, + ) { + mwebHd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/1000'") as Bip32Slip10Secp256k1; + mwebEnabled = alwaysScan ?? false; walletAddresses = LitecoinWalletAddresses( walletInfo, initialAddresses: initialAddresses, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, + initialMwebAddresses: initialMwebAddresses, mainHd: hd, sideHd: accountHD.childKey(Bip32KeyIndex(1)), network: network, + mwebHd: mwebHd, + mwebEnabled: mwebEnabled, ); autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; }); } + late final Bip32Slip10Secp256k1 mwebHd; + late final Box mwebUtxosBox; + Timer? _syncTimer; + Timer? _feeRatesTimer; + Timer? _processingTimer; + StreamSubscription? _utxoStream; + late RpcClient _stub; + late bool mwebEnabled; + bool processingUtxos = false; + + List get scanSecret => mwebHd.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; + List get spendSecret => mwebHd.childKey(Bip32KeyIndex(0x80000001)).privateKey.privKey.raw; static Future create( {required String mnemonic, @@ -78,6 +117,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { String? passphrase, String? addressPageType, List? initialAddresses, + List? initialMwebAddresses, ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex}) async { @@ -101,6 +141,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, initialAddresses: initialAddresses, + initialMwebAddresses: initialMwebAddresses, initialBalance: initialBalance, encryptionFileUtils: encryptionFileUtils, passphrase: passphrase, @@ -111,12 +152,14 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { ); } - static Future open( - {required String name, - required WalletInfo walletInfo, - required Box unspentCoinsInfo, - required String password, - required EncryptionFileUtils encryptionFileUtils}) async { + static Future open({ + required String name, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + required String password, + required bool alwaysScan, + required EncryptionFileUtils encryptionFileUtils, + }) async { final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); ElectrumWalletSnapshot? snp = null; @@ -178,6 +221,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, initialAddresses: snp?.addresses, + initialMwebAddresses: snp?.mwebAddresses, initialBalance: snp?.balance, seedBytes: seedBytes!, passphrase: passphrase, @@ -185,6 +229,551 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialRegularAddressIndex: snp?.regularAddressIndex, initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: snp?.addressPageType, + alwaysScan: alwaysScan, + ); + } + + Future waitForMwebAddresses() async { + // ensure that we have the full 1000 mweb addresses generated before continuing: + // should no longer be needed, but leaving here just in case + final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; + while (mwebAddrs.length < 1000) { + print("waiting for mweb addresses to finish generating..."); + await Future.delayed(const Duration(milliseconds: 1000)); + } + } + + @action + @override + Future startSync() async { + if (syncStatus is SyncronizingSyncStatus) { + return; + } + print("STARTING SYNC - MWEB ENABLED: $mwebEnabled"); + try { + syncStatus = SyncronizingSyncStatus(); + await subscribeForUpdates(); + updateFeeRates(); + + _feeRatesTimer?.cancel(); + _feeRatesTimer = + Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates()); + + if (!mwebEnabled) { + try { + await updateAllUnspents(); + await updateTransactions(); + await updateBalance(); + syncStatus = SyncedSyncStatus(); + } catch (e, s) { + print(e); + print(s); + syncStatus = FailedSyncStatus(); + } + return; + } + + await waitForMwebAddresses(); + await getStub(); + await processMwebUtxos(); + await updateTransactions(); + await updateUnspent(); + await updateBalance(); + } catch (e) { + print("failed to start mweb sync: $e"); + syncStatus = FailedSyncStatus(); + return; + } + + _syncTimer?.cancel(); + _syncTimer = Timer.periodic(const Duration(milliseconds: 1500), (timer) async { + if (syncStatus is FailedSyncStatus) return; + + final nodeHeight = + await electrumClient.getCurrentBlockChainTip() ?? 0; // current block height of our node + final resp = await CwMweb.status(StatusRequest()); + print("resp.mwebUtxosHeight: ${resp.mwebUtxosHeight}"); + print("resp.mwebHeaderHeight: ${resp.mwebHeaderHeight}"); + print("resp.blockHeaderHeight: ${resp.blockHeaderHeight}"); + + if (resp.blockHeaderHeight < nodeHeight) { + int h = resp.blockHeaderHeight; + syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); + } else if (resp.mwebHeaderHeight < nodeHeight) { + int h = resp.mwebHeaderHeight; + syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); + } else if (resp.mwebUtxosHeight < nodeHeight) { + syncStatus = SyncingSyncStatus(1, 0.999); + } else { + if (resp.mwebUtxosHeight > walletInfo.restoreHeight) { + await walletInfo.updateRestoreHeight(resp.mwebUtxosHeight); + await checkMwebUtxosSpent(); + // update the confirmations for each transaction: + for (final transaction in transactionHistory.transactions.values) { + if (transaction.isPending) continue; + int txHeight = transaction.height ?? resp.mwebUtxosHeight; + final confirmations = (resp.mwebUtxosHeight - txHeight) + 1; + if (transaction.confirmations == confirmations) continue; + transaction.confirmations = confirmations; + transactionHistory.addOne(transaction); + } + await transactionHistory.save(); + } + + // prevent unnecessary reaction triggers: + if (syncStatus is! SyncedSyncStatus) { + // mwebd is synced, but we could still be processing incoming utxos: + if (!processingUtxos) { + syncStatus = SyncedSyncStatus(); + } + } + return; + } + }); + } + + @action + @override + Future stopSync() async { + _syncTimer?.cancel(); + _utxoStream?.cancel(); + _feeRatesTimer?.cancel(); + await CwMweb.stop(); + } + + Future initMwebUtxosBox() async { + final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${MwebUtxo.boxName}"; + + mwebUtxosBox = await CakeHive.openBox(boxName); + } + + @override + Future renameWalletFiles(String newWalletName) async { + // rename the hive box: + final oldBoxName = "${walletInfo.name.replaceAll(" ", "_")}_${MwebUtxo.boxName}"; + final newBoxName = "${newWalletName.replaceAll(" ", "_")}_${MwebUtxo.boxName}"; + + final oldBox = await CakeHive.openBox(oldBoxName); + mwebUtxosBox = await CakeHive.openBox(newBoxName); + for (final key in oldBox.keys) { + await mwebUtxosBox.put(key, oldBox.get(key)!); + } + oldBox.deleteFromDisk(); + + await super.renameWalletFiles(newWalletName); + } + + @action + @override + Future rescan({ + required int height, + int? chainTip, + ScanData? scanData, + bool? doSingleScan, + bool? usingElectrs, + }) async { + _syncTimer?.cancel(); + int oldHeight = walletInfo.restoreHeight; + await walletInfo.updateRestoreHeight(height); + + // go through mwebUtxos and clear any that are above the new restore height: + if (height == 0) { + await mwebUtxosBox.clear(); + transactionHistory.clear(); + } else { + for (final utxo in mwebUtxosBox.values) { + if (utxo.height > height) { + await mwebUtxosBox.delete(utxo.outputId); + } + } + // TODO: remove transactions that are above the new restore height! + } + + // reset coin balances and txCount to 0: + unspentCoins.forEach((coin) { + if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) + coin.bitcoinAddressRecord.balance = 0; + coin.bitcoinAddressRecord.txCount = 0; + }); + + for (var addressRecord in walletAddresses.allAddresses) { + addressRecord.balance = 0; + addressRecord.txCount = 0; + } + + await startSync(); + } + + @override + Future init() async { + await super.init(); + await initMwebUtxosBox(); + } + + Future handleIncoming(MwebUtxo utxo, RpcClient stub) async { + final status = await stub.status(StatusRequest()); + var date = DateTime.now(); + var confirmations = 0; + if (utxo.height > 0) { + date = DateTime.fromMillisecondsSinceEpoch(utxo.blockTime * 1000); + confirmations = status.blockHeaderHeight - utxo.height + 1; + } + var tx = transactionHistory.transactions.values + .firstWhereOrNull((tx) => tx.outputAddresses?.contains(utxo.outputId) ?? false); + + if (tx == null) { + tx = ElectrumTransactionInfo( + WalletType.litecoin, + id: utxo.outputId, + height: utxo.height, + amount: utxo.value.toInt(), + fee: 0, + direction: TransactionDirection.incoming, + isPending: utxo.height == 0, + date: date, + confirmations: confirmations, + inputAddresses: [], + outputAddresses: [utxo.outputId], + isReplaced: false, + ); + } + + // don't update the confirmations if the tx is updated by electrum: + if (tx.confirmations == 0 || utxo.height != 0) { + tx.height = utxo.height; + tx.isPending = utxo.height == 0; + tx.confirmations = confirmations; + } + + bool isNew = transactionHistory.transactions[tx.id] == null; + + if (!(tx.outputAddresses?.contains(utxo.address) ?? false)) { + tx.outputAddresses?.add(utxo.address); + isNew = true; + } + + if (isNew) { + final addressRecord = walletAddresses.allAddresses + .firstWhereOrNull((addressRecord) => addressRecord.address == utxo.address); + if (addressRecord == null) { + print("we don't have this address in the wallet! ${utxo.address}"); + return; + } + + // update the txCount: + addressRecord.txCount++; + addressRecord.balance += utxo.value.toInt(); + addressRecord.setAsUsed(); + } + + transactionHistory.addOne(tx); + + if (isNew) { + // update the unconfirmed balance when a new tx is added: + // we do this after adding the tx to the history so that sub address balances are updated correctly + // (since that calculation is based on the tx history) + await updateBalance(); + } + } + + Future processMwebUtxos() async { + if (!mwebEnabled) { + return; + } + + int restoreHeight = walletInfo.restoreHeight; + print("SCANNING FROM HEIGHT: $restoreHeight"); + final req = UtxosRequest(scanSecret: scanSecret, fromHeight: restoreHeight); + + // process new utxos as they come in: + _utxoStream?.cancel(); + ResponseStream? responseStream = await CwMweb.utxos(req); + if (responseStream == null) { + throw Exception("failed to get utxos stream!"); + } + _utxoStream = responseStream.listen((Utxo sUtxo) async { + // we're processing utxos, so our balance could still be innacurate: + if (syncStatus is! SyncronizingSyncStatus && syncStatus is! SyncingSyncStatus) { + syncStatus = SyncronizingSyncStatus(); + processingUtxos = true; + _processingTimer?.cancel(); + _processingTimer = Timer.periodic(const Duration(seconds: 2), (timer) async { + processingUtxos = false; + timer.cancel(); + }); + } + + final utxo = MwebUtxo( + address: sUtxo.address, + blockTime: sUtxo.blockTime, + height: sUtxo.height, + outputId: sUtxo.outputId, + value: sUtxo.value.toInt(), + ); + + // if (mwebUtxosBox.containsKey(utxo.outputId)) { + // // we've already stored this utxo, skip it: + // return; + // } + + await updateUnspent(); + await updateBalance(); + + final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; + + // don't process utxos with addresses that are not in the mwebAddrs list: + if (utxo.address.isNotEmpty && !mwebAddrs.contains(utxo.address)) { + return; + } + + await mwebUtxosBox.put(utxo.outputId, utxo); + + await handleIncoming(utxo, _stub); + }); + } + + Future checkMwebUtxosSpent() async { + if (!mwebEnabled) { + return; + } + + final pendingOutgoingTransactions = transactionHistory.transactions.values + .where((tx) => tx.direction == TransactionDirection.outgoing && tx.isPending); + + // check if any of the pending outgoing transactions are now confirmed: + bool updatedAny = false; + for (final tx in pendingOutgoingTransactions) { + updatedAny = await isConfirmed(tx) || updatedAny; + } + + // get output ids of all the mweb utxos that have > 0 height: + final outputIds = + mwebUtxosBox.values.where((utxo) => utxo.height > 0).map((utxo) => utxo.outputId).toList(); + + final resp = await CwMweb.spent(SpentRequest(outputId: outputIds)); + final spent = resp.outputId; + if (spent.isEmpty) { + return; + } + + final status = await CwMweb.status(StatusRequest()); + final height = await electrumClient.getCurrentBlockChainTip(); + if (height == null || status.blockHeaderHeight != height) return; + if (status.mwebUtxosHeight != height) return; // we aren't synced + + int amount = 0; + Set inputAddresses = {}; + var output = convert.AccumulatorSink(); + var input = sha256.startChunkedConversion(output); + + for (final outputId in spent) { + final utxo = mwebUtxosBox.get(outputId); + await mwebUtxosBox.delete(outputId); + if (utxo == null) continue; + final addressRecord = walletAddresses.allAddresses + .firstWhere((addressRecord) => addressRecord.address == utxo.address); + if (!inputAddresses.contains(utxo.address)) { + addressRecord.txCount++; + } + addressRecord.balance -= utxo.value.toInt(); + amount += utxo.value.toInt(); + inputAddresses.add(utxo.address); + input.add(hex.decode(outputId)); + } + + if (inputAddresses.isEmpty) return; + input.close(); + var digest = output.events.single; + final tx = ElectrumTransactionInfo( + WalletType.litecoin, + id: digest.toString(), + height: height, + amount: amount, + fee: 0, + direction: TransactionDirection.outgoing, + isPending: false, + date: DateTime.fromMillisecondsSinceEpoch(status.blockTime * 1000), + confirmations: 1, + inputAddresses: inputAddresses.toList(), + outputAddresses: [], + isReplaced: false, + ); + + transactionHistory.addOne(tx); + await transactionHistory.save(); + + if (updatedAny) { + await updateBalance(); + } + } + + // checks if a pending transaction is now confirmed, and updates the tx info accordingly: + Future isConfirmed(ElectrumTransactionInfo tx) async { + if (!mwebEnabled) return false; + if (!tx.isPending) return false; + + final outputId = [], target = {}; + final isHash = RegExp(r'^[a-f0-9]{64}$').hasMatch; + final spendingOutputIds = tx.inputAddresses?.where(isHash) ?? []; + final payingToOutputIds = tx.outputAddresses?.where(isHash) ?? []; + outputId.addAll(spendingOutputIds); + outputId.addAll(payingToOutputIds); + target.addAll(spendingOutputIds); + + for (final outputId in payingToOutputIds) { + final spendingTx = transactionHistory.transactions.values + .firstWhereOrNull((tx) => tx.inputAddresses?.contains(outputId) ?? false); + if (spendingTx != null && !spendingTx.isPending) { + target.add(outputId); + } + } + + if (outputId.isEmpty) { + return false; + } + + final resp = await CwMweb.spent(SpentRequest(outputId: outputId)); + if (!setEquals(resp.outputId.toSet(), target)) { + return false; + } + + final status = await CwMweb.status(StatusRequest()); + tx.height = status.mwebUtxosHeight; + tx.confirmations = 1; + tx.isPending = false; + await transactionHistory.save(); + return true; + } + + Future updateUnspent() async { + await checkMwebUtxosSpent(); + await updateAllUnspents(); + } + + @override + @action + Future updateAllUnspents() async { + // get ltc unspents: + await super.updateAllUnspents(); + + if (!mwebEnabled) { + return; + } + await getStub(); + + // add the mweb unspents to the list: + List mwebUnspentCoins = []; + // update mweb unspents: + final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; + mwebUtxosBox.keys.forEach((dynamic oId) { + final String outputId = oId as String; + final utxo = mwebUtxosBox.get(outputId); + if (utxo == null) { + return; + } + if (utxo.address.isEmpty) { + // not sure if a bug or a special case but we definitely ignore these + return; + } + final addressRecord = walletAddresses.allAddresses + .firstWhereOrNull((addressRecord) => addressRecord.address == utxo.address); + + if (addressRecord == null) { + print("utxo contains an address that is not in the wallet: ${utxo.address}"); + return; + } + final unspent = BitcoinUnspent( + addressRecord, + outputId, + utxo.value.toInt(), + mwebAddrs.indexOf(utxo.address), + ); + if (unspent.vout == 0) { + unspent.isChange = true; + } + mwebUnspentCoins.add(unspent); + }); + unspentCoins.addAll(mwebUnspentCoins); + } + + @override + Future fetchBalances() async { + final balance = await super.fetchBalances(); + if (!mwebEnabled) { + return balance; + } + await getStub(); + + // update unspent balances: + await updateUnspent(); + + int confirmed = balance.confirmed; + int unconfirmed = balance.unconfirmed; + int confirmedMweb = 0; + int unconfirmedMweb = 0; + try { + mwebUtxosBox.values.forEach((utxo) { + if (utxo.height > 0) { + confirmedMweb += utxo.value.toInt(); + } else { + unconfirmedMweb += utxo.value.toInt(); + } + }); + if (unconfirmedMweb > 0) { + unconfirmedMweb = -1 * (confirmedMweb - unconfirmedMweb); + } + } catch (_) {} + + for (var addressRecord in walletAddresses.allAddresses) { + addressRecord.balance = 0; + addressRecord.txCount = 0; + } + + unspentCoins.forEach((coin) { + final coinInfoList = unspentCoinsInfo.values.where( + (element) => + element.walletId.contains(id) && + element.hash.contains(coin.hash) && + element.vout == coin.vout, + ); + + if (coinInfoList.isNotEmpty) { + final coinInfo = coinInfoList.first; + + coin.isFrozen = coinInfo.isFrozen; + coin.isSending = coinInfo.isSending; + coin.note = coinInfo.note; + if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) + coin.bitcoinAddressRecord.balance += coinInfo.value; + } else { + super.addCoinInfo(coin); + } + }); + + // update the txCount for each address using the tx history, since we can't rely on mwebd + // to have an accurate count, we should just keep it in sync with what we know from the tx history: + for (final tx in transactionHistory.transactions.values) { + // if (tx.isPending) continue; + if (tx.inputAddresses == null || tx.outputAddresses == null) { + continue; + } + final txAddresses = tx.inputAddresses! + tx.outputAddresses!; + for (final address in txAddresses) { + final addressRecord = walletAddresses.allAddresses + .firstWhereOrNull((addressRecord) => addressRecord.address == address); + if (addressRecord == null) { + continue; + } + addressRecord.txCount++; + } + } + + return ElectrumBalance( + confirmed: confirmed, + unconfirmed: unconfirmed, + frozen: balance.frozen, + secondConfirmed: confirmedMweb, + secondUnconfirmed: unconfirmedMweb, ); } @@ -204,6 +793,227 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return 0; } + @override + Future calcFee({ + required List utxos, + required List outputs, + required BasedUtxoNetwork network, + String? memo, + required int feeRate, + List? inputPrivKeyInfos, + List? vinOutpoints, + }) async { + final spendsMweb = utxos.any((utxo) => utxo.utxo.scriptType == SegwitAddresType.mweb); + final paysToMweb = outputs + .any((output) => output.toOutput.scriptPubKey.getAddressType() == SegwitAddresType.mweb); + if (!spendsMweb && !paysToMweb) { + return await super.calcFee( + utxos: utxos, + outputs: outputs, + network: network, + memo: memo, + feeRate: feeRate, + inputPrivKeyInfos: inputPrivKeyInfos, + vinOutpoints: vinOutpoints, + ); + } + + if (!mwebEnabled) { + throw Exception("MWEB is not enabled! can't calculate fee without starting the mweb server!"); + } + + if (outputs.length == 1 && outputs[0].toOutput.amount == BigInt.zero) { + outputs = [ + BitcoinScriptOutput( + script: outputs[0].toOutput.scriptPubKey, value: utxos.sumOfUtxosValue()) + ]; + } + + // https://github.com/ltcmweb/mwebd?tab=readme-ov-file#fee-estimation + final preOutputSum = + outputs.fold(BigInt.zero, (acc, output) => acc + output.toOutput.amount); + final fee = utxos.sumOfUtxosValue() - preOutputSum; + final txb = + BitcoinTransactionBuilder(utxos: utxos, outputs: outputs, fee: fee, network: network); + final resp = await CwMweb.create(CreateRequest( + rawTx: txb.buildTransaction((a, b, c, d) => '').toBytes(), + scanSecret: scanSecret, + spendSecret: spendSecret, + feeRatePerKb: Int64(feeRate * 1000), + dryRun: true)); + final tx = BtcTransaction.fromRaw(hex.encode(resp.rawTx)); + final posUtxos = utxos + .where((utxo) => tx.inputs + .any((input) => input.txId == utxo.utxo.txHash && input.txIndex == utxo.utxo.vout)) + .toList(); + final posOutputSum = tx.outputs.fold(0, (acc, output) => acc + output.amount.toInt()); + final mwebInputSum = utxos.sumOfUtxosValue() - posUtxos.sumOfUtxosValue(); + final expectedPegin = max(0, (preOutputSum - mwebInputSum).toInt()); + var feeIncrease = posOutputSum - expectedPegin; + if (expectedPegin > 0 && fee == BigInt.zero) { + feeIncrease += await super.calcFee( + utxos: posUtxos, + outputs: tx.outputs + .map((output) => + BitcoinScriptOutput(script: output.scriptPubKey, value: output.amount)) + .toList(), + network: network, + memo: memo, + feeRate: feeRate) + + feeRate * 41; + } + return fee.toInt() + feeIncrease; + } + + @override + Future createTransaction(Object credentials) async { + try { + var tx = await super.createTransaction(credentials) as PendingBitcoinTransaction; + tx.isMweb = mwebEnabled; + + if (!mwebEnabled) { + return tx; + } + await waitForMwebAddresses(); + await getStub(); + + final resp = await CwMweb.create(CreateRequest( + rawTx: hex.decode(tx.hex), + scanSecret: scanSecret, + spendSecret: spendSecret, + feeRatePerKb: Int64.parseInt(tx.feeRate) * 1000, + )); + final tx2 = BtcTransaction.fromRaw(hex.encode(resp.rawTx)); + + // check if the transaction doesn't contain any mweb inputs or outputs: + final transactionCredentials = credentials as BitcoinTransactionCredentials; + + bool hasMwebInput = false; + bool hasMwebOutput = false; + + for (final output in transactionCredentials.outputs) { + if (output.extractedAddress?.toLowerCase().contains("mweb") ?? false) { + hasMwebOutput = true; + break; + } + } + + if (tx2.mwebBytes != null && tx2.mwebBytes!.isNotEmpty) { + hasMwebInput = true; + } + + if (!hasMwebInput && !hasMwebOutput) { + return tx; + } + + // check if any of the inputs of this transaction are hog-ex: + // this list is only non-mweb inputs: + tx2.inputs.forEach((txInput) { + bool isHogEx = true; + + final utxo = unspentCoins + .firstWhere((utxo) => utxo.hash == txInput.txId && utxo.vout == txInput.txIndex); + + // TODO: detect actual hog-ex inputs + + if (!isHogEx) { + return; + } + + int confirmations = utxo.confirmations ?? 0; + if (confirmations < 6) { + throw Exception( + "A transaction input has less than 6 confirmations, please try again later."); + } + }); + + tx.hexOverride = tx2 + .copyWith( + witnesses: tx2.inputs.asMap().entries.map((e) { + final utxo = unspentCoins + .firstWhere((utxo) => utxo.hash == e.value.txId && utxo.vout == e.value.txIndex); + final key = generateECPrivate( + hd: utxo.bitcoinAddressRecord.isHidden + ? walletAddresses.sideHd + : walletAddresses.mainHd, + index: utxo.bitcoinAddressRecord.index, + network: network); + final digest = tx2.getTransactionSegwitDigit( + txInIndex: e.key, + script: key.getPublic().toP2pkhAddress().toScriptPubKey(), + amount: BigInt.from(utxo.value), + ); + return TxWitnessInput(stack: [key.signInput(digest), key.getPublic().toHex()]); + }).toList()) + .toHex(); + tx.outputAddresses = resp.outputId; + + return tx + ..addListener((transaction) async { + final addresses = {}; + transaction.inputAddresses?.forEach((id) async { + final utxo = mwebUtxosBox.get(id); + // await mwebUtxosBox.delete(id);// gets deleted in checkMwebUtxosSpent + if (utxo == null) return; + final addressRecord = walletAddresses.allAddresses + .firstWhere((addressRecord) => addressRecord.address == utxo.address); + if (!addresses.contains(utxo.address)) { + addresses.add(utxo.address); + } + addressRecord.balance -= utxo.value.toInt(); + }); + transaction.inputAddresses?.addAll(addresses); + + transactionHistory.addOne(transaction); + await updateUnspent(); + await updateBalance(); + }); + } catch (e, s) { + print(e); + print(s); + if (e.toString().contains("commit failed")) { + throw Exception("Transaction commit failed (no peers responded), please try again."); + } + rethrow; + } + } + + @override + Future save() async { + await super.save(); + } + + @override + Future close() async { + _utxoStream?.cancel(); + _feeRatesTimer?.cancel(); + _syncTimer?.cancel(); + _processingTimer?.cancel(); + await stopSync(); + await super.close(); + } + + Future setMwebEnabled(bool enabled) async { + if (mwebEnabled == enabled) { + return; + } + + mwebEnabled = enabled; + (walletAddresses as LitecoinWalletAddresses).mwebEnabled = enabled; + await stopSync(); + await startSync(); + } + + Future getStub() async { + _stub = await CwMweb.stub(); + return _stub; + } + + Future getStatusRequest() async { + final resp = await CwMweb.status(StatusRequest()); + return resp; + } + @override Future signMessage(String message, {String? address = null}) async { final index = address != null diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 6945db445..3a7856516 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -1,8 +1,15 @@ +import 'dart:async'; +import 'dart:typed_data'; + import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_mweb/cw_mweb.dart'; +import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; part 'litecoin_wallet_addresses.g.dart'; @@ -15,15 +22,145 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with required super.mainHd, required super.sideHd, required super.network, + required this.mwebHd, + required this.mwebEnabled, super.initialAddresses, + super.initialMwebAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, - }) : super(walletInfo); + }) : super(walletInfo) { + for (int i = 0; i < mwebAddresses.length; i++) { + mwebAddrs.add(mwebAddresses[i].address); + } + print("initialized with ${mwebAddrs.length} mweb addresses"); + } + + final Bip32Slip10Secp256k1 mwebHd; + bool mwebEnabled; + int mwebTopUpIndex = 1000; + List mwebAddrs = []; + + List get scanSecret => mwebHd.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; + List get spendPubkey => + mwebHd.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed; @override - String getAddress( - {required int index, - required Bip32Slip10Secp256k1 hd, - BitcoinAddressType? addressType}) => - generateP2WPKHAddress(hd: hd, index: index, network: network); + Future init() async { + await initMwebAddresses(); + await super.init(); + } + + @computed + @override + List get allAddresses { + return List.from(super.allAddresses)..addAll(mwebAddresses); + } + + Future ensureMwebAddressUpToIndexExists(int index) async { + Uint8List scan = Uint8List.fromList(scanSecret); + Uint8List spend = Uint8List.fromList(spendPubkey); + int count = 0; + while (mwebAddrs.length <= (index + 1)) { + final address = await CwMweb.address(scan, spend, mwebAddrs.length); + mwebAddrs.add(address!); + count++; + // sleep for a bit to avoid making the main thread unresponsive: + if (count > 50) { + count = 0; + await Future.delayed(Duration(milliseconds: 100)); + } + } + } + + Future initMwebAddresses() async { + if (mwebAddrs.length < 1000) { + print("Generating MWEB addresses..."); + await ensureMwebAddressUpToIndexExists(1020); + print("done generating MWEB addresses"); + List addressRecords = mwebAddrs + .asMap() + .entries + .map((e) => BitcoinAddressRecord( + e.value, + index: e.key, + type: SegwitAddresType.mweb, + network: network, + )) + .toList(); + addMwebAddresses(addressRecords); + print("added ${addressRecords.length} mweb addresses"); + return; + } + } + + @override + String getAddress({ + required int index, + required Bip32Slip10Secp256k1 hd, + BitcoinAddressType? addressType, + }) { + if (addressType == SegwitAddresType.mweb) { + return hd == sideHd ? mwebAddrs[0] : mwebAddrs[index + 1]; + } + return generateP2WPKHAddress(hd: hd, index: index, network: network); + } + + @override + Future getAddressAsync({ + required int index, + required Bip32Slip10Secp256k1 hd, + BitcoinAddressType? addressType, + }) async { + if (addressType == SegwitAddresType.mweb) { + await ensureMwebAddressUpToIndexExists(index); + } + return getAddress(index: index, hd: hd, addressType: addressType); + } + + @action + @override + Future getChangeAddress({List? outputs, UtxoDetails? utxoDetails}) async { + // use regular change address on peg in, otherwise use mweb for change address: + + if (!mwebEnabled) { + return super.getChangeAddress(); + } + + if (outputs != null && utxoDetails != null) { + // check if this is a PEGIN: + bool outputsToMweb = false; + bool comesFromMweb = false; + + for (var i = 0; i < outputs.length; i++) { + // TODO: probably not the best way to tell if this is an mweb address + // (but it doesn't contain the "mweb" text at this stage) + if (outputs[i].address.toAddress(network).length > 110) { + outputsToMweb = true; + } + } + // TODO: this doesn't respect coin control because it doesn't know which available inputs are selected + utxoDetails.availableInputs.forEach((element) { + if (element.address.contains("mweb")) { + comesFromMweb = true; + } + }); + + bool isPegIn = !comesFromMweb && outputsToMweb; + if (isPegIn && mwebEnabled) { + return super.getChangeAddress(); + } + + // use regular change address if it's not an mweb tx: + if (!comesFromMweb && !outputsToMweb) { + return super.getChangeAddress(); + } + } + + if (mwebEnabled) { + await ensureMwebAddressUpToIndexExists(1); + return mwebAddrs[0]; + } + + return super.getChangeAddress(); + } } diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index e42669402..c659dd658 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -1,10 +1,10 @@ import 'dart:io'; import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart'; +import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:hive/hive.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; -import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart'; import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart'; import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:cw_core/wallet_service.dart'; @@ -14,16 +14,19 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:collection/collection.dart'; import 'package:bip39/bip39.dart' as bip39; +import 'package:path_provider/path_provider.dart'; class LitecoinWalletService extends WalletService< BitcoinNewWalletCredentials, BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromWIFCredentials, BitcoinNewWalletCredentials> { - LitecoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.isDirect); + LitecoinWalletService( + this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect); final Box walletInfoSource; final Box unspentCoinsInfoSource; + final bool alwaysScan; final bool isDirect; @override @@ -64,6 +67,7 @@ class LitecoinWalletService extends WalletService< @override Future openWallet(String name, String password) async { + final walletInfo = walletInfoSource.values .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!; @@ -73,6 +77,7 @@ class LitecoinWalletService extends WalletService< name: name, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, + alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -85,6 +90,7 @@ class LitecoinWalletService extends WalletService< name: name, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, + alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -98,6 +104,23 @@ class LitecoinWalletService extends WalletService< final walletInfo = walletInfoSource.values .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; await walletInfoSource.delete(walletInfo.key); + + // if there are no more litecoin wallets left, cleanup the neutrino db and other files created by mwebd: + if (walletInfoSource.values.where((info) => info.type == WalletType.litecoin).isEmpty) { + final appDirPath = (await getApplicationSupportDirectory()).path; + File neturinoDb = File('$appDirPath/neutrino.db'); + File blockHeaders = File('$appDirPath/block_headers.bin'); + File regFilterHeaders = File('$appDirPath/reg_filter_headers.bin'); + if (neturinoDb.existsSync()) { + neturinoDb.deleteSync(); + } + if (blockHeaders.existsSync()) { + blockHeaders.deleteSync(); + } + if (regFilterHeaders.existsSync()) { + regFilterHeaders.deleteSync(); + } + } } @override @@ -109,6 +132,7 @@ class LitecoinWalletService extends WalletService< name: currentName, walletInfo: currentWalletInfo, unspentCoinsInfo: unspentCoinsInfoSource, + alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index aeba9394a..30cc29ec9 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -1,11 +1,15 @@ +import 'package:grpc/grpc.dart'; import 'package:cw_bitcoin/exceptions.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:cw_mweb/cw_mweb.dart'; +import 'package:cw_mweb/mwebd.pb.dart'; class PendingBitcoinTransaction with PendingTransaction { PendingBitcoinTransaction( @@ -19,6 +23,7 @@ class PendingBitcoinTransaction with PendingTransaction { required this.hasChange, this.isSendAll = false, this.hasTaprootInputs = false, + this.isMweb = false, }) : _listeners = []; final WalletType type; @@ -28,15 +33,19 @@ class PendingBitcoinTransaction with PendingTransaction { final int fee; final String feeRate; final BasedUtxoNetwork? network; - final bool hasChange; final bool isSendAll; + final bool hasChange; final bool hasTaprootInputs; + bool isMweb; + String? idOverride; + String? hexOverride; + List? outputAddresses; @override - String get id => _tx.txId(); + String get id => idOverride ?? _tx.txId(); @override - String get hex => _tx.serialize(); + String get hex => hexOverride ?? _tx.serialize(); @override String get amountFormatted => bitcoinAmountToString(amount: amount); @@ -62,8 +71,7 @@ class PendingBitcoinTransaction with PendingTransaction { final List _listeners; - @override - Future commit() async { + Future _commit() async { int? callId; final result = await electrumClient.broadcastTransaction( @@ -100,6 +108,25 @@ class PendingBitcoinTransaction with PendingTransaction { throw BitcoinTransactionCommitFailed(); } + } + + Future _ltcCommit() async { + try { + final stub = await CwMweb.stub(); + final resp = await stub.broadcast(BroadcastRequest(rawTx: BytesUtils.fromHexString(hex))); + idOverride = resp.txid; + } on GrpcError catch (e) { + throw BitcoinTransactionCommitFailed(errorMessage: e.message); + } + } + + @override + Future commit() async { + if (isMweb) { + await _ltcCommit(); + } else { + await _commit(); + } _listeners.forEach((listener) => listener(transactionInfo())); } @@ -116,5 +143,7 @@ class PendingBitcoinTransaction with PendingTransaction { isPending: true, isReplaced: false, confirmations: 0, + inputAddresses: _tx.inputs.map((input) => input.txId).toList(), + outputAddresses: outputAddresses, fee: fee); } diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index a2478af3c..f741230a5 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.7.0" + archive: + dependency: transitive + description: + name: archive + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + url: "https://pub.dev" + source: hosted + version: "3.4.10" args: dependency: transitive description: @@ -29,10 +37,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: "58082b3f0dca697204dbab0ef9ff208bfaea7767ea771076af9a343488428dda" + sha256: "6b151826fcc95ff246cd219a0bf4c753ea14f4081ad71c61939becf3aba27f70" url: "https://pub.dev" source: hosted - version: "1.5.3" + version: "1.5.5" async: dependency: transitive description: @@ -71,7 +79,7 @@ packages: description: path: "." ref: cake-update-v7 - resolved-ref: bc49e3b1cba601828f8ddc3d016188d8c2499088 + resolved-ref: f577e83fe78766b2655ea0602baa9299b953a31b url: "https://github.com/cake-tech/bitcoin_base" source: git version: "4.7.0" @@ -260,6 +268,13 @@ packages: relative: true source: path version: "0.0.1" + cw_mweb: + dependency: "direct main" + description: + path: "../cw_mweb" + relative: true + source: path + version: "0.0.1" dart_style: dependency: transitive description: @@ -296,10 +311,10 @@ packages: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" ffigen: dependency: transitive description: @@ -379,6 +394,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + googleapis_auth: + dependency: transitive + description: + name: googleapis_auth + sha256: af7c3a3edf9d0de2e1e0a77e994fae0a581c525fa7012af4fa0d4a52ed9484da + url: "https://pub.dev" + source: hosted + version: "1.4.1" graphs: dependency: transitive description: @@ -387,6 +410,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + grpc: + dependency: "direct main" + description: + name: grpc + sha256: e93ee3bce45c134bf44e9728119102358c7cd69de7832d9a874e2e74eb8cab40 + url: "https://pub.dev" + source: hosted + version: "3.2.4" hex: dependency: transitive description: @@ -553,10 +584,10 @@ packages: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.6" mobx: dependency: "direct main" description: @@ -713,10 +744,10 @@ packages: dependency: transitive description: name: quiver - sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" reactive_ble_mobile: dependency: transitive description: @@ -855,7 +886,7 @@ packages: description: path: "." ref: "sp_v4.0.0" - resolved-ref: "9b04f4b0af80dd7dae9274b496a53c23dcc80ea5" + resolved-ref: ca1add293bd1e06920aa049b655832da50d0dab2 url: "https://github.com/cake-tech/sp_scanner" source: git version: "0.0.1" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index d3fe2ee4e..7e33d8260 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -34,11 +34,16 @@ dependencies: ledger_bitcoin: git: url: https://github.com/cake-tech/ledger-bitcoin + cw_mweb: + path: ../cw_mweb + grpc: ^3.2.4 sp_scanner: git: url: https://github.com/cake-tech/sp_scanner ref: sp_v4.0.0 - + bech32: + git: + url: https://github.com/cake-tech/bech32.git dev_dependencies: flutter_test: @@ -54,10 +59,13 @@ dependency_overrides: url: https://github.com/cake-tech/ledger-flutter.git ref: cake-v3 watcher: ^1.1.0 + protobuf: ^3.1.0 bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v7 + ref: cake-update-v8 + pointycastle: 3.7.4 + ffi: 2.1.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index a3b113d9f..cd1e52f51 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -42,7 +42,7 @@ dependency_overrides: bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v7 + ref: cake-update-v8 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_core/lib/balance.dart b/cw_core/lib/balance.dart index 431aff515..7350c80f1 100644 --- a/cw_core/lib/balance.dart +++ b/cw_core/lib/balance.dart @@ -1,13 +1,17 @@ abstract class Balance { - const Balance(this.available, this.additional); + const Balance(this.available, this.additional, {this.secondAvailable, this.secondAdditional}); final int available; final int additional; + final int? secondAvailable; + final int? secondAdditional; + String get formattedAvailableBalance; - String get formattedAdditionalBalance; - String get formattedUnAvailableBalance => ''; + String get formattedSecondAvailableBalance => ''; + String get formattedSecondAdditionalBalance => ''; + String get formattedFullAvailableBalance => formattedAvailableBalance; } diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index d3ef6526a..4f9b5d835 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -174,11 +174,11 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const zen = CryptoCurrency(title: 'ZEN', fullName: 'Horizen', raw: 44, name: 'zen', iconPath: 'assets/images/zen_icon.png', decimals: 8); static const xvg = CryptoCurrency(title: 'XVG', fullName: 'Verge', raw: 45, name: 'xvg', iconPath: 'assets/images/xvg_icon.png', decimals: 8); - static const usdcpoly = CryptoCurrency(title: 'USDC', tag: 'POLY', fullName: 'USD Coin', raw: 46, name: 'usdcpoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6); + static const usdcpoly = CryptoCurrency(title: 'USDC', tag: 'POL', fullName: 'USD Coin', raw: 46, name: 'usdcpoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6); static const dcr = CryptoCurrency(title: 'DCR', fullName: 'Decred', raw: 47, name: 'dcr', iconPath: 'assets/images/dcr_icon.png', decimals: 8); static const kmd = CryptoCurrency(title: 'KMD', fullName: 'Komodo', raw: 48, name: 'kmd', iconPath: 'assets/images/kmd_icon.png', decimals: 8); static const mana = CryptoCurrency(title: 'MANA', tag: 'ETH', fullName: 'Decentraland', raw: 49, name: 'mana', iconPath: 'assets/images/mana_icon.png', decimals: 18); - static const maticpoly = CryptoCurrency(title: 'POL', tag: 'POLY', fullName: 'Polygon', raw: 50, name: 'maticpoly', iconPath: 'assets/images/matic_icon.png', decimals: 18); + static const maticpoly = CryptoCurrency(title: 'POL', tag: 'POL', fullName: 'Polygon', raw: 50, name: 'maticpoly', iconPath: 'assets/images/matic_icon.png', decimals: 18); static const matic = CryptoCurrency(title: 'MATIC', tag: 'ETH', fullName: 'Polygon', raw: 51, name: 'matic', iconPath: 'assets/images/matic_icon.png', decimals: 18); static const mkr = CryptoCurrency(title: 'MKR', tag: 'ETH', fullName: 'Maker', raw: 52, name: 'mkr', iconPath: 'assets/images/mkr_icon.png', decimals: 18); static const near = CryptoCurrency(title: 'NEAR', fullName: 'NEAR Protocol', raw: 53, name: 'near', iconPath: 'assets/images/near_icon.png', decimals: 24); @@ -215,8 +215,8 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const dydx = CryptoCurrency(title: 'DYDX', tag: 'ETH', fullName: 'dYdX', raw: 84, name: 'dydx', iconPath: 'assets/images/dydx_icon.png', decimals: 18); static const steth = CryptoCurrency(title: 'STETH', tag: 'ETH', fullName: 'Lido Staked Ethereum', raw: 85, name: 'steth', iconPath: 'assets/images/steth_icon.png', decimals: 18); static const banano = CryptoCurrency(title: 'BAN', fullName: 'Banano', raw: 86, name: 'banano', iconPath: 'assets/images/nano_icon.png', decimals: 29); - static const usdtPoly = CryptoCurrency(title: 'USDT', tag: 'POLY', fullName: 'Tether USD (PoS)', raw: 87, name: 'usdtpoly', iconPath: 'assets/images/usdt_icon.png', decimals: 6); - static const usdcEPoly = CryptoCurrency(title: 'USDC.E', tag: 'POLY', fullName: 'USD Coin (PoS)', raw: 88, name: 'usdcepoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6); + static const usdtPoly = CryptoCurrency(title: 'USDT', tag: 'POL', fullName: 'Tether USD (PoS)', raw: 87, name: 'usdtpoly', iconPath: 'assets/images/usdt_icon.png', decimals: 6); + static const usdcEPoly = CryptoCurrency(title: 'USDC.E', tag: 'POL', fullName: 'USD Coin (PoS)', raw: 88, name: 'usdcepoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6); static const kaspa = CryptoCurrency(title: 'KAS', fullName: 'Kaspa', raw: 89, name: 'kas', iconPath: 'assets/images/kaspa_icon.png', decimals: 8); static const digibyte = CryptoCurrency(title: 'DGB', fullName: 'DigiByte', raw: 90, name: 'dgb', iconPath: 'assets/images/digibyte.png', decimals: 8); static const usdtSol = CryptoCurrency(title: 'USDT', tag: 'SOL', fullName: 'USDT Tether', raw: 91, name: 'usdtsol', iconPath: 'assets/images/usdt_icon.png', decimals: 6); diff --git a/cw_core/lib/get_height_by_date.dart b/cw_core/lib/get_height_by_date.dart index 10a2aeab5..2b0b77a89 100644 --- a/cw_core/lib/get_height_by_date.dart +++ b/cw_core/lib/get_height_by_date.dart @@ -310,6 +310,11 @@ DateTime getDateByBitcoinHeight(int height) { return estimatedDate; } +int getLtcHeightByDate({required DateTime date}) { + // TODO: use the proxy layer to get the height with a binary search of blocked header heights + return 0; +} + // TODO: enhance all of this global const lists const wowDates = { "2023-12": 583048, diff --git a/cw_core/lib/hive_type_ids.dart b/cw_core/lib/hive_type_ids.dart index 4da616a79..6432c484b 100644 --- a/cw_core/lib/hive_type_ids.dart +++ b/cw_core/lib/hive_type_ids.dart @@ -18,3 +18,4 @@ const SPL_TOKEN_TYPE_ID = 16; const DERIVATION_INFO_TYPE_ID = 17; const TRON_TOKEN_TYPE_ID = 18; const HARDWARE_WALLET_TYPE_TYPE_ID = 19; +const MWEB_UTXO_TYPE_ID = 20; \ No newline at end of file diff --git a/cw_core/lib/mweb_utxo.dart b/cw_core/lib/mweb_utxo.dart new file mode 100644 index 000000000..f8dfab395 --- /dev/null +++ b/cw_core/lib/mweb_utxo.dart @@ -0,0 +1,33 @@ +import 'package:cw_core/hive_type_ids.dart'; +import 'package:hive/hive.dart'; + +part 'mweb_utxo.g.dart'; + +@HiveType(typeId: MWEB_UTXO_TYPE_ID) +class MwebUtxo extends HiveObject { + MwebUtxo({ + required this.height, + required this.value, + required this.address, + required this.outputId, + required this.blockTime, + }); + + static const typeId = MWEB_UTXO_TYPE_ID; + static const boxName = 'MwebUtxo'; + + @HiveField(0) + int height; + + @HiveField(1) + int value; + + @HiveField(2) + String address; + + @HiveField(3) + String outputId; + + @HiveField(4) + int blockTime; +} diff --git a/cw_core/lib/subaddress.dart b/cw_core/lib/subaddress.dart index 8571544a9..4dba7c836 100644 --- a/cw_core/lib/subaddress.dart +++ b/cw_core/lib/subaddress.dart @@ -1,12 +1,22 @@ class Subaddress { - Subaddress({required this.id, required this.address, required this.label}); + Subaddress({ + required this.id, + required this.address, + required this.label, + this.balance = null, + this.txCount = null, + }); Subaddress.fromMap(Map map) : this.id = map['id'] == null ? 0 : int.parse(map['id'] as String), this.address = (map['address'] ?? '') as String, - this.label = (map['label'] ?? '') as String; + this.label = (map['label'] ?? '') as String, + this.balance = (map['balance'] ?? '') as String?, + this.txCount = (map['txCount'] ?? '') as int?; final int id; final String address; final String label; + final String? balance; + final int? txCount; } diff --git a/cw_core/lib/sync_status.dart b/cw_core/lib/sync_status.dart index 788309d8c..627b513b2 100644 --- a/cw_core/lib/sync_status.dart +++ b/cw_core/lib/sync_status.dart @@ -89,4 +89,4 @@ class TimedOutSyncStatus extends NotConnectedSyncStatus { class LostConnectionSyncStatus extends NotConnectedSyncStatus { @override String toString() => 'Reconnecting'; -} +} \ No newline at end of file diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index e987b5d0e..ca488cfed 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -1,26 +1,58 @@ import 'package:cw_core/address_info.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; abstract class WalletAddresses { WalletAddresses(this.walletInfo) : addressesMap = {}, allAddressesMap = {}, - addressInfos = {}; + addressInfos = {}, + usedAddresses = {}, + hiddenAddresses = walletInfo.hiddenAddresses?.toSet() ?? {}, + manualAddresses = walletInfo.manualAddresses?.toSet() ?? {}; final WalletInfo walletInfo; String get address; + String get latestAddress { + if (walletInfo.type == WalletType.monero || walletInfo.type == WalletType.wownero) { + if (addressesMap.keys.length == 0) return address; + return addressesMap[addressesMap.keys.last] ?? address; + } + return _localAddress ?? address; + } + String? get primaryAddress => null; - set address(String address); + String? _localAddress; + + set address(String address) => _localAddress = address; + + String get addressForExchange => address; Map addressesMap; Map allAddressesMap; + Map get usableAddressesMap { + final tmp = addressesMap.map((key, value) => MapEntry(key, value)); // copy address map + tmp.removeWhere((key, value) => hiddenAddresses.contains(key) || manualAddresses.contains(key)); + return tmp; + } + + Map get usableAllAddressesMap { + final tmp = allAddressesMap.map((key, value) => MapEntry(key, value)); // copy address map + tmp.removeWhere((key, value) => hiddenAddresses.contains(key) || manualAddresses.contains(key)); + return tmp; + } + Map> addressInfos; - Set usedAddresses = {}; + Set usedAddresses; + + Set hiddenAddresses; + + Set manualAddresses; Future init(); @@ -32,6 +64,8 @@ abstract class WalletAddresses { walletInfo.addresses = addressesMap; walletInfo.addressInfos = addressInfos; walletInfo.usedAddresses = usedAddresses.toList(); + walletInfo.hiddenAddresses = hiddenAddresses.toList(); + walletInfo.manualAddresses = manualAddresses.toList(); if (walletInfo.isInBox) { await walletInfo.save(); diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 48ea90f7c..4df1aa1f9 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -67,6 +67,8 @@ abstract class WalletBase startSync(); + Future stopSync() async {} + Future createTransaction(Object credentials); int calculateEstimatedFee(TransactionPriority priority, int? amount); diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index 0b0e8e4c5..bd035e30a 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -189,6 +189,15 @@ class WalletInfo extends HiveObject { @HiveField(22) String? parentAddress; + + @HiveField(23) + List? hiddenAddresses; + + @HiveField(24) + List? manualAddresses; + + + String get yatLastUsedAddress => yatLastUsedAddressRaw ?? ''; diff --git a/cw_haven/android/build.gradle b/cw_haven/android/build.gradle index 87e8df641..d29c31d4e 100644 --- a/cw_haven/android/build.gradle +++ b/cw_haven/android/build.gradle @@ -35,7 +35,6 @@ android { } externalNativeBuild { cmake { - path "CMakeLists.txt" } } } diff --git a/cw_haven/lib/haven_wallet_addresses.dart b/cw_haven/lib/haven_wallet_addresses.dart index eeeb763cf..06de44dff 100644 --- a/cw_haven/lib/haven_wallet_addresses.dart +++ b/cw_haven/lib/haven_wallet_addresses.dart @@ -1,6 +1,7 @@ import 'package:cw_core/wallet_addresses_with_account.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/account.dart'; +import 'package:cw_haven/api/wallet.dart'; import 'package:cw_haven/haven_account_list.dart'; import 'package:cw_haven/haven_subaddress_list.dart'; import 'package:cw_core/subaddress.dart'; @@ -36,7 +37,7 @@ abstract class HavenWalletAddressesBase extends WalletAddressesWithAccount init() async { accountList.update(); - account = accountList.accounts.first; + account = accountList.accounts.isEmpty ? Account(id: 0, label: "Primary address") : accountList.accounts.first; updateSubaddressList(accountIndex: account?.id ?? 0); await updateAddressesInBox(); } @@ -81,8 +82,9 @@ abstract class HavenWalletAddressesBase extends WalletAddressesWithAccount monero.Wallet_address( - wptr!, - accountIndex: accountIndex, - addressIndex: addressIndex, - ); + late String address = getAddress( + accountIndex: accountIndex, + addressIndex: addressIndex, + ); final int addressIndex; final int accountIndex; - String get label => monero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + final int received; + final int txCount; + String get label { + final localLabel = monero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + if (localLabel.startsWith("#$addressIndex")) return localLabel; // don't duplicate the ID if it was user-providen + return "#$addressIndex ${localLabel}".trim(); + } } +class TinyTransactionDetails { + TinyTransactionDetails({ + required this.address, + required this.amount, + }); + final List address; + final int amount; +} + +int lastWptr = 0; +int lastTxCount = 0; +List ttDetails = []; + List getAllSubaddresses() { + txhistory = monero.Wallet_history(wptr!); + final txCount = monero.TransactionHistory_count(txhistory!); + if (lastTxCount != txCount && lastWptr != wptr!.address) { + final List newttDetails = []; + lastTxCount = txCount; + lastWptr = wptr!.address; + for (var i = 0; i < txCount; i++) { + final tx = monero.TransactionHistory_transaction(txhistory!, index: i); + if (monero.TransactionInfo_direction(tx) == monero.TransactionInfo_Direction.Out) continue; + final subaddrs = monero.TransactionInfo_subaddrIndex(tx).split(","); + final account = monero.TransactionInfo_subaddrAccount(tx); + newttDetails.add(TinyTransactionDetails( + address: List.generate(subaddrs.length, (index) => getAddress(accountIndex: account, addressIndex: int.tryParse(subaddrs[index])??0)), + amount: monero.TransactionInfo_amount(tx), + )); + } + ttDetails.clear(); + ttDetails.addAll(newttDetails); + } final size = monero.Wallet_numSubaddresses(wptr!, accountIndex: subaddress!.accountIndex); final list = List.generate(size, (index) { + final ttDetailsLocal = ttDetails.where((element) { + final address = getAddress( + accountIndex: subaddress!.accountIndex, + addressIndex: index, + ); + if (element.address.contains(address)) return true; + return false; + }).toList(); + int received = 0; + for (var i = 0; i < ttDetailsLocal.length; i++) { + received += ttDetailsLocal[i].amount; + } return Subaddress( accountIndex: subaddress!.accountIndex, addressIndex: index, + received: received, + txCount: ttDetailsLocal.length, ); }).reversed.toList(); if (list.length == 0) { - list.add(Subaddress(addressIndex: subaddress!.accountIndex, accountIndex: 0)); + list.add( + Subaddress( + addressIndex: subaddress!.accountIndex, + accountIndex: 0, + received: 0, + txCount: 0, + )); } return list; } +int numSubaddresses(int subaccountIndex) { + return monero.Wallet_numSubaddresses(wptr!, accountIndex: subaccountIndex); +} + void addSubaddressSync({required int accountIndex, required String label}) { monero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex, label: label); refreshSubaddresses(accountIndex: accountIndex); diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index bd8b46356..a308b682e 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -5,32 +5,42 @@ import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart'; import 'package:cw_monero/api/monero_output.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; +import 'package:cw_monero/api/wallet.dart'; import 'package:ffi/ffi.dart'; import 'package:monero/monero.dart' as monero; import 'package:monero/src/generated_bindings_monero.g.dart' as monero_gen; +import 'package:mutex/mutex.dart'; String getTxKey(String txId) { return monero.Wallet_getTxKey(wptr!, txid: txId); } - +final txHistoryMutex = Mutex(); monero.TransactionHistory? txhistory; - -void refreshTransactions() { +bool isRefreshingTx = false; +Future refreshTransactions() async { + if (isRefreshingTx == true) return; + isRefreshingTx = true; txhistory ??= monero.Wallet_history(wptr!); - monero.TransactionHistory_refresh(txhistory!); + final ptr = txhistory!.address; + await txHistoryMutex.acquire(); + await Isolate.run(() { + monero.TransactionHistory_refresh(Pointer.fromAddress(ptr)); + }); + txHistoryMutex.release(); + isRefreshingTx = false; } int countOfTransactions() => monero.TransactionHistory_count(txhistory!); -List getAllTransactions() { +Future> getAllTransactions() async { List dummyTxs = []; - + + await txHistoryMutex.acquire(); txhistory ??= monero.Wallet_history(wptr!); - monero.TransactionHistory_refresh(txhistory!); int size = countOfTransactions(); final list = List.generate(size, (index) => Transaction(txInfo: monero.TransactionHistory_transaction(txhistory!, index: index))); - + txHistoryMutex.release(); final accts = monero.Wallet_numSubaddressAccounts(wptr!); for (var i = 0; i < accts; i++) { final fullBalance = monero.Wallet_balance(wptr!, accountIndex: i); @@ -45,6 +55,8 @@ List getAllTransactions() { confirmations: 0, blockheight: 0, accountIndex: i, + addressIndex: 0, + addressIndexList: [0], paymentId: "", amount: fullBalance - availBalance, isSpend: false, @@ -251,19 +263,28 @@ Future createTransactionMultDest( class Transaction { final String displayLabel; - String subaddressLabel = monero.Wallet_getSubaddressLabel(wptr!, accountIndex: 0, addressIndex: 0); - late final String address = monero.Wallet_address( + late final String subaddressLabel = monero.Wallet_getSubaddressLabel( wptr!, - accountIndex: 0, - addressIndex: 0, + accountIndex: accountIndex, + addressIndex: addressIndex, ); + late final String address = getAddress( + accountIndex: accountIndex, + addressIndex: addressIndex, + ); + late final List addressList = List.generate(addressIndexList.length, (index) => + getAddress( + accountIndex: accountIndex, + addressIndex: addressIndexList[index], + )); final String description; final int fee; final int confirmations; late final bool isPending = confirmations < 10; final int blockheight; - final int addressIndex = 0; + final int addressIndex; final int accountIndex; + final List addressIndexList; final String paymentId; final int amount; final bool isSpend; @@ -309,6 +330,8 @@ class Transaction { amount = monero.TransactionInfo_amount(txInfo), paymentId = monero.TransactionInfo_paymentId(txInfo), accountIndex = monero.TransactionInfo_subaddrAccount(txInfo), + addressIndex = int.tryParse(monero.TransactionInfo_subaddrIndex(txInfo).split(", ")[0]) ?? 0, + addressIndexList = monero.TransactionInfo_subaddrIndex(txInfo).split(", ").map((e) => int.tryParse(e) ?? 0).toList(), blockheight = monero.TransactionInfo_blockHeight(txInfo), confirmations = monero.TransactionInfo_confirmations(txInfo), fee = monero.TransactionInfo_fee(txInfo), @@ -331,6 +354,8 @@ class Transaction { required this.confirmations, required this.blockheight, required this.accountIndex, + required this.addressIndexList, + required this.addressIndex, required this.paymentId, required this.amount, required this.isSpend, diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index b493e536e..8e03cff3e 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -66,9 +66,20 @@ String getSeedLegacy(String? language) { return legacy; } -String getAddress({int accountIndex = 0, int addressIndex = 0}) => - monero.Wallet_address(wptr!, +Map>> addressCache = {}; + +String getAddress({int accountIndex = 0, int addressIndex = 0}) { + // print("getaddress: ${accountIndex}/${addressIndex}: ${monero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)}: ${monero.Wallet_address(wptr!, accountIndex: accountIndex, addressIndex: addressIndex)}"); + while (monero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)-1 < addressIndex) { + print("adding subaddress"); + monero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex); + } + addressCache[wptr!.address] ??= {}; + addressCache[wptr!.address]![accountIndex] ??= {}; + addressCache[wptr!.address]![accountIndex]![addressIndex] ??= monero.Wallet_address(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + return addressCache[wptr!.address]![accountIndex]![addressIndex]!; +} int getFullBalance({int accountIndex = 0}) => monero.Wallet_balance(wptr!, accountIndex: accountIndex); diff --git a/cw_monero/lib/monero_subaddress_list.dart b/cw_monero/lib/monero_subaddress_list.dart index c35afb282..c20b23b5e 100644 --- a/cw_monero/lib/monero_subaddress_list.dart +++ b/cw_monero/lib/monero_subaddress_list.dart @@ -1,6 +1,7 @@ import 'package:cw_core/subaddress.dart'; import 'package:cw_monero/api/coins_info.dart'; import 'package:cw_monero/api/subaddress_list.dart' as subaddress_list; +import 'package:cw_monero/api/wallet.dart'; import 'package:flutter/services.dart'; import 'package:mobx/mobx.dart'; @@ -54,18 +55,12 @@ abstract class MoneroSubaddressListBase with Store { final address = s.address; final label = s.label; final id = s.addressIndex; - final hasDefaultAddressName = - label.toLowerCase() == 'Primary account'.toLowerCase() || - label.toLowerCase() == 'Untitled account'.toLowerCase(); - final isPrimaryAddress = id == 0 && hasDefaultAddressName; return Subaddress( id: id, address: address, - label: isPrimaryAddress - ? 'Primary address' - : hasDefaultAddressName - ? '' - : label); + balance: (s.received/1e12).toStringAsFixed(6), + txCount: s.txCount, + label: label); }).toList(); } @@ -103,6 +98,9 @@ abstract class MoneroSubaddressListBase with Store { required List usedAddresses, }) async { _usedAddresses.addAll(usedAddresses); + final _all = _usedAddresses.toSet().toList(); + _usedAddresses.clear(); + _usedAddresses.addAll(_all); if (_isUpdating) { return; } @@ -124,7 +122,8 @@ abstract class MoneroSubaddressListBase with Store { Future> _getAllUnusedAddresses( {required int accountIndex, required String label}) async { final allAddresses = subaddress_list.getAllSubaddresses(); - if (allAddresses.isEmpty || _usedAddresses.contains(allAddresses.last)) { + // first because addresses come in reversed order. + if (allAddresses.isEmpty || _usedAddresses.contains(allAddresses.first.address)) { final isAddressUnused = await _newSubaddress(accountIndex: accountIndex, label: label); if (!isAddressUnused) { return await _getAllUnusedAddresses(accountIndex: accountIndex, label: label); @@ -139,12 +138,13 @@ abstract class MoneroSubaddressListBase with Store { return Subaddress( id: id, address: address, + balance: (s.received/1e12).toStringAsFixed(6), + txCount: s.txCount, label: id == 0 && label.toLowerCase() == 'Primary account'.toLowerCase() ? 'Primary address' : label); - }) - .toList(); + }).toList().reversed.toList(); } Future _newSubaddress({required int accountIndex, required String label}) async { diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 8773d694d..c6d5d2e5f 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -59,7 +59,7 @@ abstract class MoneroWalletBase extends WalletBase isEnabledAutoGenerateSubaddress, (bool enabled) { _updateSubAddress(enabled, account: walletAddresses.account); }); + _onTxHistoryChangeReaction = reaction((_) => transactionHistory, (__) { + _updateSubAddress(isEnabledAutoGenerateSubaddress, account: walletAddresses.account); + }); } static const int _autoSaveInterval = 30; @@ -128,6 +131,7 @@ abstract class MoneroWalletBase extends WalletBase await save()); + // update transaction details after restore + walletAddresses.subaddressList.update(accountIndex: walletAddresses.account?.id??0); } @override @@ -167,6 +173,7 @@ abstract class MoneroWalletBase extends WalletBase> fetchTransactions() async { transaction_history.refreshTransactions(); - return _getAllTransactionsOfAccount(walletAddresses.account?.id) + return (await _getAllTransactionsOfAccount(walletAddresses.account?.id)) .fold>( {}, (Map acc, MoneroTransactionInfo tx) { @@ -594,8 +601,8 @@ abstract class MoneroWalletBase extends WalletBase monero_wallet.getSubaddressLabel(accountIndex, addressIndex); - List _getAllTransactionsOfAccount(int? accountIndex) => - transaction_history - .getAllTransactions() + Future> _getAllTransactionsOfAccount(int? accountIndex) async => + (await transaction_history + .getAllTransactions()) .map( (row) => MoneroTransactionInfo( row.hash, diff --git a/cw_monero/lib/monero_wallet_addresses.dart b/cw_monero/lib/monero_wallet_addresses.dart index d4f22e46f..c8a4448a4 100644 --- a/cw_monero/lib/monero_wallet_addresses.dart +++ b/cw_monero/lib/monero_wallet_addresses.dart @@ -3,6 +3,8 @@ import 'package:cw_core/address_info.dart'; import 'package:cw_core/subaddress.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_monero/api/subaddress_list.dart' as subaddress_list; +import 'package:cw_monero/api/transaction_history.dart'; import 'package:cw_monero/api/wallet.dart'; import 'package:cw_monero/monero_account_list.dart'; import 'package:cw_monero/monero_subaddress_list.dart'; @@ -27,6 +29,30 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { @observable String address; + @override + String get latestAddress { + var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1; + var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + while (hiddenAddresses.contains(address)) { + addressIndex++; + address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + subaddressList.update(accountIndex: account?.id??0); + } + return address; + } + + @override + String get addressForExchange { + var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1; + var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + while (hiddenAddresses.contains(address) || manualAddresses.contains(address) || subaddress_list.getRawLabel(accountIndex: account?.id??0, addressIndex: addressIndex).isNotEmpty) { + addressIndex++; + address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + subaddressList.update(accountIndex: account?.id??0); + } + return address; + } + @observable Account? account; @@ -37,10 +63,12 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { MoneroAccountList accountList; + Set usedAddresses = Set(); + @override Future init() async { accountList.update(); - account = accountList.accounts.first; + account = accountList.accounts.isEmpty ? Account(id: 0, label: "Primary address") : accountList.accounts.first; updateSubaddressList(accountIndex: account?.id ?? 0); await updateAddressesInBox(); } @@ -89,8 +117,9 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { void updateSubaddressList({required int accountIndex}) { subaddressList.update(accountIndex: accountIndex); - subaddress = subaddressList.subaddresses.first; - address = subaddress!.address; + address = subaddressList.subaddresses.isNotEmpty + ? subaddressList.subaddresses.first.address + : getAddress(); } Future updateUsedSubaddress() async { @@ -109,7 +138,10 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { accountIndex: accountIndex, defaultLabel: defaultLabel, usedAddresses: usedAddresses.toList()); - subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel) : subaddressList.subaddresses.last; + subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel, balance: '0', txCount: 0) : subaddressList.subaddresses.last; + if (num.tryParse(subaddress!.balance??'0') != 0) { + getAddress(accountIndex: accountIndex, addressIndex: (subaddress?.id??0)+1); + } address = subaddress!.address; } diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 360d33ae3..ee1d48df1 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -463,8 +463,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: "3cb38bee9385faf46b03fd73aab85f3ac4115bf7" - resolved-ref: "3cb38bee9385faf46b03fd73aab85f3ac4115bf7" + ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b" + resolved-ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b" url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index 75d8b1ccc..cb1f5519f 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -25,7 +25,7 @@ dependencies: monero: git: url: https://github.com/mrcyjanek/monero_c - ref: 3cb38bee9385faf46b03fd73aab85f3ac4115bf7 # monero_c hash + ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash path: impls/monero.dart mutex: ^3.1.0 diff --git a/cw_mweb/.gitignore b/cw_mweb/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/cw_mweb/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/cw_mweb/.metadata b/cw_mweb/.metadata new file mode 100644 index 000000000..606303914 --- /dev/null +++ b/cw_mweb/.metadata @@ -0,0 +1,36 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + channel: stable + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: android + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: ios + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: macos + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/cw_mweb/CHANGELOG.md b/cw_mweb/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_mweb/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_mweb/LICENSE b/cw_mweb/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_mweb/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_mweb/README.md b/cw_mweb/README.md new file mode 100644 index 000000000..8a839b1ec --- /dev/null +++ b/cw_mweb/README.md @@ -0,0 +1,15 @@ +# cw_mweb + +A new Flutter plugin project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/developing-packages/), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter development, view the +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/cw_mweb/analysis_options.yaml b/cw_mweb/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_mweb/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/cw_mweb/android/.gitignore b/cw_mweb/android/.gitignore new file mode 100644 index 000000000..881f3d95c --- /dev/null +++ b/cw_mweb/android/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +/libs +.cxx diff --git a/cw_mweb/android/build.gradle b/cw_mweb/android/build.gradle new file mode 100644 index 000000000..7e67b98ad --- /dev/null +++ b/cw_mweb/android/build.gradle @@ -0,0 +1,76 @@ +group 'com.cakewallet.mweb' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.allprojects { + repositories { + flatDir { + dirs project(':cw_mweb').file('libs') + } + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 31 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + test.java.srcDirs += 'src/test/kotlin' + } + + defaultConfig { + minSdkVersion 16 + } + + dependencies { + testImplementation 'org.jetbrains.kotlin:kotlin-test' + testImplementation 'org.mockito:mockito-core:5.0.0' + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} + +dependencies { + implementation (name: 'mwebd', ext: 'aar') +} diff --git a/cw_mweb/android/settings.gradle b/cw_mweb/android/settings.gradle new file mode 100644 index 000000000..88fbd66fb --- /dev/null +++ b/cw_mweb/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'cw_mweb' diff --git a/cw_mweb/android/src/main/AndroidManifest.xml b/cw_mweb/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..fd3746a8c --- /dev/null +++ b/cw_mweb/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt b/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt new file mode 100644 index 000000000..57ae3d4c3 --- /dev/null +++ b/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt @@ -0,0 +1,58 @@ +package com.cakewallet.mweb + +import androidx.annotation.NonNull + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +import mwebd.Mwebd +import mwebd.Server + +/** CwMwebPlugin */ +class CwMwebPlugin: FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel : MethodChannel + private var server: Server? = null + private var port: Long? = null + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "cw_mweb") + channel.setMethodCallHandler(this) + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + if (call.method == "start") { + server?.stop() + val dataDir = call.argument("dataDir") ?: "" + server = server ?: Mwebd.newServer("", dataDir, "") + port = server?.start(0) + result.success(port) + } else if (call.method == "stop") { + server?.stop() + server = null + port = null + result.success(null) + } else if (call.method == "address") { + val scanSecret: ByteArray = call.argument("scanSecret") ?: ByteArray(0) + val spendPub: ByteArray = call.argument("spendPub") ?: ByteArray(0) + val index: Int = call.argument("index") ?: 0 + val res = Mwebd.address(scanSecret, spendPub, index) + result.success(res) + } else { + result.notImplemented() + } + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + server?.stop() + server = null + port = null + } +} diff --git a/cw_mweb/ios/.gitignore b/cw_mweb/ios/.gitignore new file mode 100644 index 000000000..0c885071e --- /dev/null +++ b/cw_mweb/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/cw_mweb/ios/Assets/.gitkeep b/cw_mweb/ios/Assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/cw_mweb/ios/Classes/CwMwebPlugin.swift b/cw_mweb/ios/Classes/CwMwebPlugin.swift new file mode 100644 index 000000000..f1fd78cd8 --- /dev/null +++ b/cw_mweb/ios/Classes/CwMwebPlugin.swift @@ -0,0 +1,86 @@ +import Flutter +import UIKit +import Mwebd + +public class CwMwebPlugin: NSObject, FlutterPlugin { +public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "cw_mweb", binaryMessenger: registrar.messenger()) + let instance = CwMwebPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + private static var server: MwebdServer? + private static var port: Int = 0 + private static var dataDir: String? + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getPlatformVersion": + result("iOS " + UIDevice.current.systemVersion) + break + case "start": + stopServer() + let args = call.arguments as? [String: String] + let dataDir = args?["dataDir"] + CwMwebPlugin.dataDir = dataDir + startServer(result: result) + break + case "stop": + stopServer() + result(nil) + break + case "address": + let args = call.arguments as! [String: Any] + let scanSecret = args["scanSecret"] as! FlutterStandardTypedData + let spendPub = args["spendPub"] as! FlutterStandardTypedData + let index = args["index"] as! Int32 + + let scanSecretData = scanSecret.data + let spendPubData = spendPub.data + result(MwebdAddress(scanSecretData, spendPubData, index)) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + private func startServer(result: @escaping FlutterResult) { + if CwMwebPlugin.server == nil { + var error: NSError? + CwMwebPlugin.server = MwebdNewServer("", CwMwebPlugin.dataDir, "", &error) + + if let server = CwMwebPlugin.server { + do { + print("Starting server...") + try server.start(0, ret0_: &CwMwebPlugin.port) + print("Server started successfully on port: \(CwMwebPlugin.port)") + result(CwMwebPlugin.port) + } catch let startError as NSError { + print("Server Start Error: \(startError.localizedDescription)") + result(FlutterError(code: "Server Start Error", message: startError.localizedDescription, details: nil)) + } + } else if let error = error { + print("Server Creation Error: \(error.localizedDescription)") + result(FlutterError(code: "Server Creation Error", message: error.localizedDescription, details: nil)) + } else { + print("Unknown Error: Failed to create server") + result(FlutterError(code: "Unknown Error", message: "Failed to create server", details: nil)) + } + } else { + print("Server already running on port: \(CwMwebPlugin.port)") + result(CwMwebPlugin.port) + } + } + + private func stopServer() { + print("Stopping server") + CwMwebPlugin.server?.stop() + CwMwebPlugin.server = nil + CwMwebPlugin.port = 0 + } + + deinit { + stopServer() + } +} diff --git a/cw_mweb/ios/cw_mweb.podspec b/cw_mweb/ios/cw_mweb.podspec new file mode 100644 index 000000000..4a1903bae --- /dev/null +++ b/cw_mweb/ios/cw_mweb.podspec @@ -0,0 +1,26 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint cw_mweb.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'cw_mweb' + s.version = '0.0.1' + s.summary = 'A new Flutter plugin project.' + s.description = <<-DESC +A new Flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '11.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' + s.ios.vendored_frameworks = 'Mwebd.xcframework' + s.preserve_paths = 'Mwebd.xcframework/**/*' + +end diff --git a/cw_mweb/lib/cw_mweb.dart b/cw_mweb/lib/cw_mweb.dart new file mode 100644 index 000000000..63ff1bf97 --- /dev/null +++ b/cw_mweb/lib/cw_mweb.dart @@ -0,0 +1,133 @@ +import 'dart:typed_data'; + +import 'package:grpc/grpc.dart'; +import 'package:path_provider/path_provider.dart'; +import 'cw_mweb_platform_interface.dart'; +import 'mwebd.pbgrpc.dart'; + +class CwMweb { + static RpcClient? _rpcClient; + static ClientChannel? _clientChannel; + static int? _port; + static const TIMEOUT_DURATION = Duration(seconds: 5); + + static Future _initializeClient() async { + await stop(); + // wait a few seconds to make sure the server is stopped + await Future.delayed(const Duration(seconds: 5)); + + final appDir = await getApplicationSupportDirectory(); + _port = await CwMwebPlatform.instance.start(appDir.path); + if (_port == null || _port == 0) { + throw Exception("Failed to start server"); + } + print("Attempting to connect to server on port: $_port"); + + // wait for the server to finish starting up before we try to connect to it: + await Future.delayed(const Duration(seconds: 5)); + + _clientChannel = ClientChannel('127.0.0.1', port: _port!, channelShutdownHandler: () { + print("Channel is shutting down!"); + }, + options: const ChannelOptions( + credentials: ChannelCredentials.insecure(), + keepAlive: ClientKeepAliveOptions(permitWithoutCalls: true), + )); + _rpcClient = RpcClient(_clientChannel!); + } + + static Future stub({int maxRetries = 3}) async { + for (int i = 0; i < maxRetries; i++) { + try { + if (_rpcClient == null) { + await _initializeClient(); + } + final status = await _rpcClient! + .status(StatusRequest(), options: CallOptions(timeout: TIMEOUT_DURATION)); + if (status.blockTime == 0) { + throw Exception("blockTime shouldn't be 0! (this connection is likely broken)"); + } + return _rpcClient!; + } catch (e) { + print("Attempt $i failed: $e"); + _rpcClient = null; + } + } + throw Exception("Failed to connect after $maxRetries attempts"); + } + + static Future stop() async { + try { + await CwMwebPlatform.instance.stop(); + await cleanup(); + } catch (e) { + print("Error stopping server: $e"); + } + } + + static Future address(Uint8List scanSecret, Uint8List spendPub, int index) async { + try { + return CwMwebPlatform.instance.address(scanSecret, spendPub, index); + } catch (e) { + print("Error getting address: $e"); + return null; + } + } + + static Future cleanup() async { + await _clientChannel?.terminate(); + _rpcClient = null; + _clientChannel = null; + _port = null; + } + + // wrappers that handle the connection issues: + static Future spent(SpentRequest request) async { + try { + if (_rpcClient == null) { + await _initializeClient(); + } + return await _rpcClient!.spent(request, options: CallOptions(timeout: TIMEOUT_DURATION)); + } catch (e) { + print("Error getting spent: $e"); + return SpentResponse(); + } + } + + static Future status(StatusRequest request) async { + try { + if (_rpcClient == null) { + await _initializeClient(); + } + return await _rpcClient!.status(request, options: CallOptions(timeout: TIMEOUT_DURATION)); + } catch (e) { + print("Error getting status: $e"); + return StatusResponse(); + } + } + + static Future create(CreateRequest request) async { + try { + if (_rpcClient == null) { + await _initializeClient(); + } + return await _rpcClient!.create(request, options: CallOptions(timeout: TIMEOUT_DURATION)); + } catch (e) { + print("Error getting create: $e"); + return CreateResponse(); + } + } + + static Future?> utxos(UtxosRequest request) async { + try { + if (_rpcClient == null) { + await _initializeClient(); + } + // this is a stream, so we should have an effectively infinite timeout: + return _rpcClient!.utxos(request, options: CallOptions(timeout: const Duration(days: 1000 * 365))); + } catch (e) { + print("Error getting utxos: $e"); + return null; + } + } +} diff --git a/cw_mweb/lib/cw_mweb_method_channel.dart b/cw_mweb/lib/cw_mweb_method_channel.dart new file mode 100644 index 000000000..70e4a1789 --- /dev/null +++ b/cw_mweb/lib/cw_mweb_method_channel.dart @@ -0,0 +1,32 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'cw_mweb_platform_interface.dart'; + +/// An implementation of [CwMwebPlatform] that uses method channels. +class MethodChannelCwMweb extends CwMwebPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('cw_mweb'); + + @override + Future start(String dataDir) async { + final result = await methodChannel.invokeMethod('start', {'dataDir': dataDir}); + return result; + } + + @override + Future stop() async { + await methodChannel.invokeMethod('stop'); + } + + @override + Future address(Uint8List scanSecret, Uint8List spendPub, int index) async { + final result = await methodChannel.invokeMethod('address', { + 'scanSecret': scanSecret, + 'spendPub': spendPub, + 'index': index, + }); + return result; + } +} diff --git a/cw_mweb/lib/cw_mweb_platform_interface.dart b/cw_mweb/lib/cw_mweb_platform_interface.dart new file mode 100644 index 000000000..8cc80f3e9 --- /dev/null +++ b/cw_mweb/lib/cw_mweb_platform_interface.dart @@ -0,0 +1,39 @@ +import 'dart:typed_data'; + +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'cw_mweb_method_channel.dart'; + +abstract class CwMwebPlatform extends PlatformInterface { + /// Constructs a CwMwebPlatform. + CwMwebPlatform() : super(token: _token); + + static final Object _token = Object(); + + static CwMwebPlatform _instance = MethodChannelCwMweb(); + + /// The default instance of [CwMwebPlatform] to use. + /// + /// Defaults to [MethodChannelCwMweb]. + static CwMwebPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [CwMwebPlatform] when + /// they register themselves. + static set instance(CwMwebPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future start(String dataDir) { + throw UnimplementedError('start() has not been implemented.'); + } + + Future stop() { + throw UnimplementedError('stop() has not been implemented.'); + } + + Future address(Uint8List scanSecret, Uint8List spendPub, int index) { + throw UnimplementedError('address(int) has not been implemented.'); + } +} diff --git a/cw_mweb/lib/mwebd.pb.dart b/cw_mweb/lib/mwebd.pb.dart new file mode 100644 index 000000000..d0dd486c0 --- /dev/null +++ b/cw_mweb/lib/mwebd.pb.dart @@ -0,0 +1,801 @@ +// +// Generated code. Do not modify. +// source: mwebd.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +class StatusRequest extends $pb.GeneratedMessage { + factory StatusRequest() => create(); + StatusRequest._() : super(); + factory StatusRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory StatusRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'StatusRequest', createEmptyInstance: create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + StatusRequest clone() => StatusRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + StatusRequest copyWith(void Function(StatusRequest) updates) => super.copyWith((message) => updates(message as StatusRequest)) as StatusRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static StatusRequest create() => StatusRequest._(); + StatusRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static StatusRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static StatusRequest? _defaultInstance; +} + +class StatusResponse extends $pb.GeneratedMessage { + factory StatusResponse({ + $core.int? blockHeaderHeight, + $core.int? mwebHeaderHeight, + $core.int? mwebUtxosHeight, + $core.int? blockTime, + }) { + final $result = create(); + if (blockHeaderHeight != null) { + $result.blockHeaderHeight = blockHeaderHeight; + } + if (mwebHeaderHeight != null) { + $result.mwebHeaderHeight = mwebHeaderHeight; + } + if (mwebUtxosHeight != null) { + $result.mwebUtxosHeight = mwebUtxosHeight; + } + if (blockTime != null) { + $result.blockTime = blockTime; + } + return $result; + } + StatusResponse._() : super(); + factory StatusResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory StatusResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'StatusResponse', createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'blockHeaderHeight', $pb.PbFieldType.O3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'mwebHeaderHeight', $pb.PbFieldType.O3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'mwebUtxosHeight', $pb.PbFieldType.O3) + ..a<$core.int>(4, _omitFieldNames ? '' : 'blockTime', $pb.PbFieldType.OU3) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + StatusResponse clone() => StatusResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + StatusResponse copyWith(void Function(StatusResponse) updates) => super.copyWith((message) => updates(message as StatusResponse)) as StatusResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static StatusResponse create() => StatusResponse._(); + StatusResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static StatusResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static StatusResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get blockHeaderHeight => $_getIZ(0); + @$pb.TagNumber(1) + set blockHeaderHeight($core.int v) { $_setSignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasBlockHeaderHeight() => $_has(0); + @$pb.TagNumber(1) + void clearBlockHeaderHeight() => clearField(1); + + @$pb.TagNumber(2) + $core.int get mwebHeaderHeight => $_getIZ(1); + @$pb.TagNumber(2) + set mwebHeaderHeight($core.int v) { $_setSignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasMwebHeaderHeight() => $_has(1); + @$pb.TagNumber(2) + void clearMwebHeaderHeight() => clearField(2); + + @$pb.TagNumber(3) + $core.int get mwebUtxosHeight => $_getIZ(2); + @$pb.TagNumber(3) + set mwebUtxosHeight($core.int v) { $_setSignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasMwebUtxosHeight() => $_has(2); + @$pb.TagNumber(3) + void clearMwebUtxosHeight() => clearField(3); + + @$pb.TagNumber(4) + $core.int get blockTime => $_getIZ(3); + @$pb.TagNumber(4) + set blockTime($core.int v) { $_setUnsignedInt32(3, v); } + @$pb.TagNumber(4) + $core.bool hasBlockTime() => $_has(3); + @$pb.TagNumber(4) + void clearBlockTime() => clearField(4); +} + +class UtxosRequest extends $pb.GeneratedMessage { + factory UtxosRequest({ + $core.int? fromHeight, + $core.List<$core.int>? scanSecret, + }) { + final $result = create(); + if (fromHeight != null) { + $result.fromHeight = fromHeight; + } + if (scanSecret != null) { + $result.scanSecret = scanSecret; + } + return $result; + } + UtxosRequest._() : super(); + factory UtxosRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory UtxosRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'UtxosRequest', createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'fromHeight', $pb.PbFieldType.O3) + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'scanSecret', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + UtxosRequest clone() => UtxosRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + UtxosRequest copyWith(void Function(UtxosRequest) updates) => super.copyWith((message) => updates(message as UtxosRequest)) as UtxosRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static UtxosRequest create() => UtxosRequest._(); + UtxosRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static UtxosRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static UtxosRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get fromHeight => $_getIZ(0); + @$pb.TagNumber(1) + set fromHeight($core.int v) { $_setSignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasFromHeight() => $_has(0); + @$pb.TagNumber(1) + void clearFromHeight() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get scanSecret => $_getN(1); + @$pb.TagNumber(2) + set scanSecret($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasScanSecret() => $_has(1); + @$pb.TagNumber(2) + void clearScanSecret() => clearField(2); +} + +class Utxo extends $pb.GeneratedMessage { + factory Utxo({ + $core.int? height, + $fixnum.Int64? value, + $core.String? address, + $core.String? outputId, + $core.int? blockTime, + }) { + final $result = create(); + if (height != null) { + $result.height = height; + } + if (value != null) { + $result.value = value; + } + if (address != null) { + $result.address = address; + } + if (outputId != null) { + $result.outputId = outputId; + } + if (blockTime != null) { + $result.blockTime = blockTime; + } + return $result; + } + Utxo._() : super(); + factory Utxo.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Utxo.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Utxo', createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'height', $pb.PbFieldType.O3) + ..a<$fixnum.Int64>(2, _omitFieldNames ? '' : 'value', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..aOS(3, _omitFieldNames ? '' : 'address') + ..aOS(4, _omitFieldNames ? '' : 'outputId') + ..a<$core.int>(5, _omitFieldNames ? '' : 'blockTime', $pb.PbFieldType.OU3) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Utxo clone() => Utxo()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Utxo copyWith(void Function(Utxo) updates) => super.copyWith((message) => updates(message as Utxo)) as Utxo; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Utxo create() => Utxo._(); + Utxo createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Utxo getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Utxo? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get height => $_getIZ(0); + @$pb.TagNumber(1) + set height($core.int v) { $_setSignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasHeight() => $_has(0); + @$pb.TagNumber(1) + void clearHeight() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get value => $_getI64(1); + @$pb.TagNumber(2) + set value($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasValue() => $_has(1); + @$pb.TagNumber(2) + void clearValue() => clearField(2); + + @$pb.TagNumber(3) + $core.String get address => $_getSZ(2); + @$pb.TagNumber(3) + set address($core.String v) { $_setString(2, v); } + @$pb.TagNumber(3) + $core.bool hasAddress() => $_has(2); + @$pb.TagNumber(3) + void clearAddress() => clearField(3); + + @$pb.TagNumber(4) + $core.String get outputId => $_getSZ(3); + @$pb.TagNumber(4) + set outputId($core.String v) { $_setString(3, v); } + @$pb.TagNumber(4) + $core.bool hasOutputId() => $_has(3); + @$pb.TagNumber(4) + void clearOutputId() => clearField(4); + + @$pb.TagNumber(5) + $core.int get blockTime => $_getIZ(4); + @$pb.TagNumber(5) + set blockTime($core.int v) { $_setUnsignedInt32(4, v); } + @$pb.TagNumber(5) + $core.bool hasBlockTime() => $_has(4); + @$pb.TagNumber(5) + void clearBlockTime() => clearField(5); +} + +class AddressRequest extends $pb.GeneratedMessage { + factory AddressRequest({ + $core.int? fromIndex, + $core.int? toIndex, + $core.List<$core.int>? scanSecret, + $core.List<$core.int>? spendPubkey, + }) { + final $result = create(); + if (fromIndex != null) { + $result.fromIndex = fromIndex; + } + if (toIndex != null) { + $result.toIndex = toIndex; + } + if (scanSecret != null) { + $result.scanSecret = scanSecret; + } + if (spendPubkey != null) { + $result.spendPubkey = spendPubkey; + } + return $result; + } + AddressRequest._() : super(); + factory AddressRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory AddressRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AddressRequest', createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'fromIndex', $pb.PbFieldType.OU3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'toIndex', $pb.PbFieldType.OU3) + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'scanSecret', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(4, _omitFieldNames ? '' : 'spendPubkey', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + AddressRequest clone() => AddressRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + AddressRequest copyWith(void Function(AddressRequest) updates) => super.copyWith((message) => updates(message as AddressRequest)) as AddressRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static AddressRequest create() => AddressRequest._(); + AddressRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static AddressRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static AddressRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get fromIndex => $_getIZ(0); + @$pb.TagNumber(1) + set fromIndex($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasFromIndex() => $_has(0); + @$pb.TagNumber(1) + void clearFromIndex() => clearField(1); + + @$pb.TagNumber(2) + $core.int get toIndex => $_getIZ(1); + @$pb.TagNumber(2) + set toIndex($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasToIndex() => $_has(1); + @$pb.TagNumber(2) + void clearToIndex() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get scanSecret => $_getN(2); + @$pb.TagNumber(3) + set scanSecret($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(3) + $core.bool hasScanSecret() => $_has(2); + @$pb.TagNumber(3) + void clearScanSecret() => clearField(3); + + @$pb.TagNumber(4) + $core.List<$core.int> get spendPubkey => $_getN(3); + @$pb.TagNumber(4) + set spendPubkey($core.List<$core.int> v) { $_setBytes(3, v); } + @$pb.TagNumber(4) + $core.bool hasSpendPubkey() => $_has(3); + @$pb.TagNumber(4) + void clearSpendPubkey() => clearField(4); +} + +class AddressResponse extends $pb.GeneratedMessage { + factory AddressResponse({ + $core.Iterable<$core.String>? address, + }) { + final $result = create(); + if (address != null) { + $result.address.addAll(address); + } + return $result; + } + AddressResponse._() : super(); + factory AddressResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory AddressResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AddressResponse', createEmptyInstance: create) + ..pPS(1, _omitFieldNames ? '' : 'address') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + AddressResponse clone() => AddressResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + AddressResponse copyWith(void Function(AddressResponse) updates) => super.copyWith((message) => updates(message as AddressResponse)) as AddressResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static AddressResponse create() => AddressResponse._(); + AddressResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static AddressResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static AddressResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.String> get address => $_getList(0); +} + +class SpentRequest extends $pb.GeneratedMessage { + factory SpentRequest({ + $core.Iterable<$core.String>? outputId, + }) { + final $result = create(); + if (outputId != null) { + $result.outputId.addAll(outputId); + } + return $result; + } + SpentRequest._() : super(); + factory SpentRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SpentRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SpentRequest', createEmptyInstance: create) + ..pPS(1, _omitFieldNames ? '' : 'outputId') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SpentRequest clone() => SpentRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SpentRequest copyWith(void Function(SpentRequest) updates) => super.copyWith((message) => updates(message as SpentRequest)) as SpentRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SpentRequest create() => SpentRequest._(); + SpentRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SpentRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SpentRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.String> get outputId => $_getList(0); +} + +class SpentResponse extends $pb.GeneratedMessage { + factory SpentResponse({ + $core.Iterable<$core.String>? outputId, + }) { + final $result = create(); + if (outputId != null) { + $result.outputId.addAll(outputId); + } + return $result; + } + SpentResponse._() : super(); + factory SpentResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SpentResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SpentResponse', createEmptyInstance: create) + ..pPS(1, _omitFieldNames ? '' : 'outputId') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SpentResponse clone() => SpentResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SpentResponse copyWith(void Function(SpentResponse) updates) => super.copyWith((message) => updates(message as SpentResponse)) as SpentResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SpentResponse create() => SpentResponse._(); + SpentResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SpentResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SpentResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.String> get outputId => $_getList(0); +} + +class CreateRequest extends $pb.GeneratedMessage { + factory CreateRequest({ + $core.List<$core.int>? rawTx, + $core.List<$core.int>? scanSecret, + $core.List<$core.int>? spendSecret, + $fixnum.Int64? feeRatePerKb, + $core.bool? dryRun, + }) { + final $result = create(); + if (rawTx != null) { + $result.rawTx = rawTx; + } + if (scanSecret != null) { + $result.scanSecret = scanSecret; + } + if (spendSecret != null) { + $result.spendSecret = spendSecret; + } + if (feeRatePerKb != null) { + $result.feeRatePerKb = feeRatePerKb; + } + if (dryRun != null) { + $result.dryRun = dryRun; + } + return $result; + } + CreateRequest._() : super(); + factory CreateRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory CreateRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'CreateRequest', createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'rawTx', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'scanSecret', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'spendSecret', $pb.PbFieldType.OY) + ..a<$fixnum.Int64>(4, _omitFieldNames ? '' : 'feeRatePerKb', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..aOB(5, _omitFieldNames ? '' : 'dryRun') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + CreateRequest clone() => CreateRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + CreateRequest copyWith(void Function(CreateRequest) updates) => super.copyWith((message) => updates(message as CreateRequest)) as CreateRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static CreateRequest create() => CreateRequest._(); + CreateRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static CreateRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static CreateRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get rawTx => $_getN(0); + @$pb.TagNumber(1) + set rawTx($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasRawTx() => $_has(0); + @$pb.TagNumber(1) + void clearRawTx() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get scanSecret => $_getN(1); + @$pb.TagNumber(2) + set scanSecret($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasScanSecret() => $_has(1); + @$pb.TagNumber(2) + void clearScanSecret() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get spendSecret => $_getN(2); + @$pb.TagNumber(3) + set spendSecret($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(3) + $core.bool hasSpendSecret() => $_has(2); + @$pb.TagNumber(3) + void clearSpendSecret() => clearField(3); + + @$pb.TagNumber(4) + $fixnum.Int64 get feeRatePerKb => $_getI64(3); + @$pb.TagNumber(4) + set feeRatePerKb($fixnum.Int64 v) { $_setInt64(3, v); } + @$pb.TagNumber(4) + $core.bool hasFeeRatePerKb() => $_has(3); + @$pb.TagNumber(4) + void clearFeeRatePerKb() => clearField(4); + + @$pb.TagNumber(5) + $core.bool get dryRun => $_getBF(4); + @$pb.TagNumber(5) + set dryRun($core.bool v) { $_setBool(4, v); } + @$pb.TagNumber(5) + $core.bool hasDryRun() => $_has(4); + @$pb.TagNumber(5) + void clearDryRun() => clearField(5); +} + +class CreateResponse extends $pb.GeneratedMessage { + factory CreateResponse({ + $core.List<$core.int>? rawTx, + $core.Iterable<$core.String>? outputId, + }) { + final $result = create(); + if (rawTx != null) { + $result.rawTx = rawTx; + } + if (outputId != null) { + $result.outputId.addAll(outputId); + } + return $result; + } + CreateResponse._() : super(); + factory CreateResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory CreateResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'CreateResponse', createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'rawTx', $pb.PbFieldType.OY) + ..pPS(2, _omitFieldNames ? '' : 'outputId') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + CreateResponse clone() => CreateResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + CreateResponse copyWith(void Function(CreateResponse) updates) => super.copyWith((message) => updates(message as CreateResponse)) as CreateResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static CreateResponse create() => CreateResponse._(); + CreateResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static CreateResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static CreateResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get rawTx => $_getN(0); + @$pb.TagNumber(1) + set rawTx($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasRawTx() => $_has(0); + @$pb.TagNumber(1) + void clearRawTx() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.String> get outputId => $_getList(1); +} + +class BroadcastRequest extends $pb.GeneratedMessage { + factory BroadcastRequest({ + $core.List<$core.int>? rawTx, + }) { + final $result = create(); + if (rawTx != null) { + $result.rawTx = rawTx; + } + return $result; + } + BroadcastRequest._() : super(); + factory BroadcastRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory BroadcastRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BroadcastRequest', createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'rawTx', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + BroadcastRequest clone() => BroadcastRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + BroadcastRequest copyWith(void Function(BroadcastRequest) updates) => super.copyWith((message) => updates(message as BroadcastRequest)) as BroadcastRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static BroadcastRequest create() => BroadcastRequest._(); + BroadcastRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static BroadcastRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static BroadcastRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get rawTx => $_getN(0); + @$pb.TagNumber(1) + set rawTx($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasRawTx() => $_has(0); + @$pb.TagNumber(1) + void clearRawTx() => clearField(1); +} + +class BroadcastResponse extends $pb.GeneratedMessage { + factory BroadcastResponse({ + $core.String? txid, + }) { + final $result = create(); + if (txid != null) { + $result.txid = txid; + } + return $result; + } + BroadcastResponse._() : super(); + factory BroadcastResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory BroadcastResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BroadcastResponse', createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'txid') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + BroadcastResponse clone() => BroadcastResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + BroadcastResponse copyWith(void Function(BroadcastResponse) updates) => super.copyWith((message) => updates(message as BroadcastResponse)) as BroadcastResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static BroadcastResponse create() => BroadcastResponse._(); + BroadcastResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static BroadcastResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static BroadcastResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get txid => $_getSZ(0); + @$pb.TagNumber(1) + set txid($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasTxid() => $_has(0); + @$pb.TagNumber(1) + void clearTxid() => clearField(1); +} + + +const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); +const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/cw_mweb/lib/mwebd.pbgrpc.dart b/cw_mweb/lib/mwebd.pbgrpc.dart new file mode 100644 index 000000000..6bc48cfdf --- /dev/null +++ b/cw_mweb/lib/mwebd.pbgrpc.dart @@ -0,0 +1,159 @@ +// +// Generated code. Do not modify. +// source: mwebd.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:async' as $async; +import 'dart:core' as $core; + +import 'package:grpc/service_api.dart' as $grpc; +import 'package:protobuf/protobuf.dart' as $pb; + +import 'mwebd.pb.dart' as $0; + +export 'mwebd.pb.dart'; + +@$pb.GrpcServiceName('Rpc') +class RpcClient extends $grpc.Client { + static final _$status = $grpc.ClientMethod<$0.StatusRequest, $0.StatusResponse>( + '/Rpc/Status', + ($0.StatusRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.StatusResponse.fromBuffer(value)); + static final _$utxos = $grpc.ClientMethod<$0.UtxosRequest, $0.Utxo>( + '/Rpc/Utxos', + ($0.UtxosRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.Utxo.fromBuffer(value)); + static final _$addresses = $grpc.ClientMethod<$0.AddressRequest, $0.AddressResponse>( + '/Rpc/Addresses', + ($0.AddressRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.AddressResponse.fromBuffer(value)); + static final _$spent = $grpc.ClientMethod<$0.SpentRequest, $0.SpentResponse>( + '/Rpc/Spent', + ($0.SpentRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.SpentResponse.fromBuffer(value)); + static final _$create = $grpc.ClientMethod<$0.CreateRequest, $0.CreateResponse>( + '/Rpc/Create', + ($0.CreateRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.CreateResponse.fromBuffer(value)); + static final _$broadcast = $grpc.ClientMethod<$0.BroadcastRequest, $0.BroadcastResponse>( + '/Rpc/Broadcast', + ($0.BroadcastRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.BroadcastResponse.fromBuffer(value)); + + RpcClient($grpc.ClientChannel channel, + {$grpc.CallOptions? options, + $core.Iterable<$grpc.ClientInterceptor>? interceptors}) + : super(channel, options: options, + interceptors: interceptors); + + $grpc.ResponseFuture<$0.StatusResponse> status($0.StatusRequest request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$status, request, options: options); + } + + $grpc.ResponseStream<$0.Utxo> utxos($0.UtxosRequest request, {$grpc.CallOptions? options}) { + return $createStreamingCall(_$utxos, $async.Stream.fromIterable([request]), options: options); + } + + $grpc.ResponseFuture<$0.AddressResponse> addresses($0.AddressRequest request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$addresses, request, options: options); + } + + $grpc.ResponseFuture<$0.SpentResponse> spent($0.SpentRequest request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$spent, request, options: options); + } + + $grpc.ResponseFuture<$0.CreateResponse> create($0.CreateRequest request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$create, request, options: options); + } + + $grpc.ResponseFuture<$0.BroadcastResponse> broadcast($0.BroadcastRequest request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$broadcast, request, options: options); + } +} + +@$pb.GrpcServiceName('Rpc') +abstract class RpcServiceBase extends $grpc.Service { + $core.String get $name => 'Rpc'; + + RpcServiceBase() { + $addMethod($grpc.ServiceMethod<$0.StatusRequest, $0.StatusResponse>( + 'Status', + status_Pre, + false, + false, + ($core.List<$core.int> value) => $0.StatusRequest.fromBuffer(value), + ($0.StatusResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.UtxosRequest, $0.Utxo>( + 'Utxos', + utxos_Pre, + false, + true, + ($core.List<$core.int> value) => $0.UtxosRequest.fromBuffer(value), + ($0.Utxo value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.AddressRequest, $0.AddressResponse>( + 'Addresses', + addresses_Pre, + false, + false, + ($core.List<$core.int> value) => $0.AddressRequest.fromBuffer(value), + ($0.AddressResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.SpentRequest, $0.SpentResponse>( + 'Spent', + spent_Pre, + false, + false, + ($core.List<$core.int> value) => $0.SpentRequest.fromBuffer(value), + ($0.SpentResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.CreateRequest, $0.CreateResponse>( + 'Create', + create_Pre, + false, + false, + ($core.List<$core.int> value) => $0.CreateRequest.fromBuffer(value), + ($0.CreateResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.BroadcastRequest, $0.BroadcastResponse>( + 'Broadcast', + broadcast_Pre, + false, + false, + ($core.List<$core.int> value) => $0.BroadcastRequest.fromBuffer(value), + ($0.BroadcastResponse value) => value.writeToBuffer())); + } + + $async.Future<$0.StatusResponse> status_Pre($grpc.ServiceCall call, $async.Future<$0.StatusRequest> request) async { + return status(call, await request); + } + + $async.Stream<$0.Utxo> utxos_Pre($grpc.ServiceCall call, $async.Future<$0.UtxosRequest> request) async* { + yield* utxos(call, await request); + } + + $async.Future<$0.AddressResponse> addresses_Pre($grpc.ServiceCall call, $async.Future<$0.AddressRequest> request) async { + return addresses(call, await request); + } + + $async.Future<$0.SpentResponse> spent_Pre($grpc.ServiceCall call, $async.Future<$0.SpentRequest> request) async { + return spent(call, await request); + } + + $async.Future<$0.CreateResponse> create_Pre($grpc.ServiceCall call, $async.Future<$0.CreateRequest> request) async { + return create(call, await request); + } + + $async.Future<$0.BroadcastResponse> broadcast_Pre($grpc.ServiceCall call, $async.Future<$0.BroadcastRequest> request) async { + return broadcast(call, await request); + } + + $async.Future<$0.StatusResponse> status($grpc.ServiceCall call, $0.StatusRequest request); + $async.Stream<$0.Utxo> utxos($grpc.ServiceCall call, $0.UtxosRequest request); + $async.Future<$0.AddressResponse> addresses($grpc.ServiceCall call, $0.AddressRequest request); + $async.Future<$0.SpentResponse> spent($grpc.ServiceCall call, $0.SpentRequest request); + $async.Future<$0.CreateResponse> create($grpc.ServiceCall call, $0.CreateRequest request); + $async.Future<$0.BroadcastResponse> broadcast($grpc.ServiceCall call, $0.BroadcastRequest request); +} diff --git a/cw_mweb/macos/Classes/CwMwebPlugin.swift b/cw_mweb/macos/Classes/CwMwebPlugin.swift new file mode 100644 index 000000000..9c0dabd40 --- /dev/null +++ b/cw_mweb/macos/Classes/CwMwebPlugin.swift @@ -0,0 +1,19 @@ +import Cocoa +import FlutterMacOS + +public class CwMwebPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "cw_mweb", binaryMessenger: registrar.messenger) + let instance = CwMwebPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getPlatformVersion": + result("macOS " + ProcessInfo.processInfo.operatingSystemVersionString) + default: + result(FlutterMethodNotImplemented) + } + } +} diff --git a/cw_mweb/macos/cw_mweb.podspec b/cw_mweb/macos/cw_mweb.podspec new file mode 100644 index 000000000..8fadcced9 --- /dev/null +++ b/cw_mweb/macos/cw_mweb.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint cw_mweb.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'cw_mweb' + s.version = '0.0.1' + s.summary = 'A new Flutter plugin project.' + s.description = <<-DESC +A new Flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'FlutterMacOS' + + s.platform = :osx, '10.11' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.swift_version = '5.0' +end diff --git a/cw_mweb/pubspec.yaml b/cw_mweb/pubspec.yaml new file mode 100644 index 000000000..cfe43c70b --- /dev/null +++ b/cw_mweb/pubspec.yaml @@ -0,0 +1,76 @@ +name: cw_mweb +description: A new Flutter plugin project. +version: 0.0.1 +homepage: + +environment: + sdk: '>=3.0.6 <4.0.0' + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + grpc: ^3.2.4 + path_provider: ^2.1.2 + plugin_platform_interface: ^2.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: com.cakewallet.mweb + pluginClass: CwMwebPlugin + ios: + pluginClass: CwMwebPlugin + macos: + pluginClass: CwMwebPlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/cw_wownero/lib/api/subaddress_list.dart b/cw_wownero/lib/api/subaddress_list.dart index d8c91a584..b07f24c4a 100644 --- a/cw_wownero/lib/api/subaddress_list.dart +++ b/cw_wownero/lib/api/subaddress_list.dart @@ -1,4 +1,5 @@ import 'package:cw_wownero/api/account_list.dart'; +import 'package:cw_wownero/api/transaction_history.dart'; import 'package:cw_wownero/api/wallet.dart'; import 'package:monero/wownero.dart' as wownero; @@ -28,27 +29,75 @@ class Subaddress { Subaddress({ required this.addressIndex, required this.accountIndex, + required this.txCount, + required this.received, }); - String get address => wownero.Wallet_address( - wptr!, - accountIndex: accountIndex, - addressIndex: addressIndex, - ); + late String address = getAddress( + accountIndex: accountIndex, + addressIndex: addressIndex, + ); final int addressIndex; final int accountIndex; String get label => wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + final int txCount; + final int received; } +class TinyTransactionDetails { + TinyTransactionDetails({ + required this.address, + required this.amount, + }); + final List address; + final int amount; +} + +int lastWptr = 0; +int lastTxCount = 0; +List ttDetails = []; + List getAllSubaddresses() { + txhistory = wownero.Wallet_history(wptr!); + final txCount = wownero.TransactionHistory_count(txhistory!); + if (lastTxCount != txCount && lastWptr != wptr!.address) { + final List newttDetails = []; + lastTxCount = txCount; + lastWptr = wptr!.address; + for (var i = 0; i < txCount; i++) { + final tx = wownero.TransactionHistory_transaction(txhistory!, index: i); + final subaddrs = wownero.TransactionInfo_subaddrIndex(tx).split(","); + final account = wownero.TransactionInfo_subaddrAccount(tx); + newttDetails.add(TinyTransactionDetails( + address: List.generate(subaddrs.length, (index) => getAddress(accountIndex: account, addressIndex: int.tryParse(subaddrs[index])??0)), + amount: wownero.TransactionInfo_amount(tx), + )); + } + ttDetails.clear(); + ttDetails.addAll(newttDetails); + } final size = wownero.Wallet_numSubaddresses(wptr!, accountIndex: subaddress!.accountIndex); final list = List.generate(size, (index) { + final ttDetailsLocal = ttDetails.where((element) { + final address = getAddress( + accountIndex: subaddress!.accountIndex, + addressIndex: index, + ); + if (address == element.address) return true; + return false; + }).toList(); + int received = 0; + for (var i = 0; i < ttDetailsLocal.length; i++) { + received += ttDetailsLocal[i].amount; + } return Subaddress( accountIndex: subaddress!.accountIndex, addressIndex: index, + received: received, + txCount: ttDetailsLocal.length, ); }).reversed.toList(); if (list.isEmpty) { - list.add(Subaddress(addressIndex: 0, accountIndex: subaddress!.accountIndex)); + list.add(Subaddress(addressIndex: 0, accountIndex: subaddress!.accountIndex, txCount: 0, received: 0)); } return list; } @@ -58,6 +107,10 @@ void addSubaddressSync({required int accountIndex, required String label}) { refreshSubaddresses(accountIndex: accountIndex); } +int numSubaddresses(int subaccountIndex) { + return wownero.Wallet_numSubaddresses(wptr!, accountIndex: subaccountIndex); +} + void setLabelForSubaddressSync( {required int accountIndex, required int addressIndex, required String label}) { wownero.Wallet_setSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex, label: label); diff --git a/cw_wownero/lib/api/transaction_history.dart b/cw_wownero/lib/api/transaction_history.dart index a1e1e3c9b..6b0923e83 100644 --- a/cw_wownero/lib/api/transaction_history.dart +++ b/cw_wownero/lib/api/transaction_history.dart @@ -3,6 +3,7 @@ import 'dart:isolate'; import 'package:cw_wownero/api/account_list.dart'; import 'package:cw_wownero/api/exceptions/creation_transaction_exception.dart'; +import 'package:cw_wownero/api/wallet.dart'; import 'package:cw_wownero/api/wownero_output.dart'; import 'package:cw_wownero/api/structs/pending_transaction.dart'; import 'package:ffi/ffi.dart'; @@ -16,9 +17,16 @@ String getTxKey(String txId) { wownero.TransactionHistory? txhistory; -void refreshTransactions() { +bool isRefreshingTx = false; +Future refreshTransactions() async { + if (isRefreshingTx == true) return; + isRefreshingTx = true; txhistory ??= wownero.Wallet_history(wptr!); - wownero.TransactionHistory_refresh(txhistory!); + final ptr = txhistory!.address; + await Isolate.run(() { + wownero.TransactionHistory_refresh(Pointer.fromAddress(ptr)); + }); + isRefreshingTx = false; } int countOfTransactions() => wownero.TransactionHistory_count(txhistory!); @@ -45,6 +53,8 @@ List getAllTransactions() { confirmations: 0, blockheight: 0, accountIndex: i, + addressIndex: 0, + addressIndexList: [0], paymentId: "", amount: fullBalance - availBalance, isSpend: false, @@ -243,23 +253,28 @@ Future createTransactionMultDest( class Transaction { final String displayLabel; - String subaddressLabel = wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: 0, addressIndex: 0); - late final String address = wownero.Wallet_address( - wptr!, - accountIndex: 0, - addressIndex: 0, + late final String subaddressLabel = wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + late final String address = getAddress( + accountIndex: accountIndex, + addressIndex: addressIndex, ); + late final List addressList = List.generate(addressIndexList.length, (index) => + getAddress( + accountIndex: accountIndex, + addressIndex: addressIndexList[index], + )); final String description; final int fee; final int confirmations; late final bool isPending = confirmations < 3; final int blockheight; - final int addressIndex = 0; + final int addressIndex; final int accountIndex; + final List addressIndexList; final String paymentId; final int amount; final bool isSpend; - late DateTime timeStamp; + late final DateTime timeStamp; late final bool isConfirmed = !isPending; final String hash; final String key; @@ -301,6 +316,8 @@ class Transaction { amount = wownero.TransactionInfo_amount(txInfo), paymentId = wownero.TransactionInfo_paymentId(txInfo), accountIndex = wownero.TransactionInfo_subaddrAccount(txInfo), + addressIndex = int.tryParse(wownero.TransactionInfo_subaddrIndex(txInfo).split(", ")[0]) ?? 0, + addressIndexList = wownero.TransactionInfo_subaddrIndex(txInfo).split(", ").map((e) => int.tryParse(e) ?? 0).toList(), blockheight = wownero.TransactionInfo_blockHeight(txInfo), confirmations = wownero.TransactionInfo_confirmations(txInfo), fee = wownero.TransactionInfo_fee(txInfo), @@ -314,6 +331,8 @@ class Transaction { required this.confirmations, required this.blockheight, required this.accountIndex, + required this.addressIndex, + required this.addressIndexList, required this.paymentId, required this.amount, required this.isSpend, diff --git a/cw_wownero/lib/api/wallet.dart b/cw_wownero/lib/api/wallet.dart index 56f54dfac..baf9c8960 100644 --- a/cw_wownero/lib/api/wallet.dart +++ b/cw_wownero/lib/api/wallet.dart @@ -67,10 +67,19 @@ String getSeedLegacy(String? language) { } return legacy; } +Map>> addressCache = {}; -String getAddress({int accountIndex = 0, int addressIndex = 1}) => - wownero.Wallet_address(wptr!, +String getAddress({int accountIndex = 0, int addressIndex = 1}) { + while (wownero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)-1 < addressIndex) { + print("adding subaddress"); + wownero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex); + } + addressCache[wptr!.address] ??= {}; + addressCache[wptr!.address]![accountIndex] ??= {}; + addressCache[wptr!.address]![accountIndex]![addressIndex] ??= wownero.Wallet_address(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + return addressCache[wptr!.address]![accountIndex]![addressIndex]!; +} int getFullBalance({int accountIndex = 0}) => wownero.Wallet_balance(wptr!, accountIndex: accountIndex); diff --git a/cw_wownero/lib/wownero_subaddress_list.dart b/cw_wownero/lib/wownero_subaddress_list.dart index 61fd09ef9..2ef4f2045 100644 --- a/cw_wownero/lib/wownero_subaddress_list.dart +++ b/cw_wownero/lib/wownero_subaddress_list.dart @@ -1,6 +1,7 @@ import 'package:cw_core/subaddress.dart'; import 'package:cw_wownero/api/coins_info.dart'; import 'package:cw_wownero/api/subaddress_list.dart' as subaddress_list; +import 'package:cw_wownero/api/wallet.dart'; import 'package:flutter/services.dart'; import 'package:mobx/mobx.dart'; @@ -61,6 +62,8 @@ abstract class WowneroSubaddressListBase with Store { return Subaddress( id: id, address: address, + balance: (s.received/1e12).toStringAsFixed(6), + txCount: s.txCount, label: isPrimaryAddress ? 'Primary address' : hasDefaultAddressName @@ -103,6 +106,9 @@ abstract class WowneroSubaddressListBase with Store { required List usedAddresses, }) async { _usedAddresses.addAll(usedAddresses); + final _all = _usedAddresses.toSet().toList(); + _usedAddresses.clear(); + _usedAddresses.addAll(_all); if (_isUpdating) { return; } @@ -140,6 +146,8 @@ abstract class WowneroSubaddressListBase with Store { return Subaddress( id: id, address: address, + balance: (s.received/1e12).toStringAsFixed(6), + txCount: s.txCount, label: id == 0 && label.toLowerCase() == 'Primary account'.toLowerCase() ? 'Primary address' diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index c3f4bcb69..02d968eb3 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -59,7 +59,7 @@ abstract class WowneroWalletBase _isTransactionUpdating = false, _hasSyncAfterStartup = false, _password = password, - isEnabledAutoGenerateSubaddress = false, + isEnabledAutoGenerateSubaddress = true, syncStatus = NotConnectedSyncStatus(), unspentCoins = [], this.unspentCoinsInfo = unspentCoinsInfo, @@ -82,6 +82,10 @@ abstract class WowneroWalletBase reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) { _updateSubAddress(enabled, account: walletAddresses.account); }); + + _onTxHistoryChangeReaction = reaction((_) => transactionHistory, (__) { + _updateSubAddress(isEnabledAutoGenerateSubaddress, account: walletAddresses.account); + }); } static const int _autoSaveInterval = 30; @@ -123,6 +127,7 @@ abstract class WowneroWalletBase wownero_wallet.SyncListener? _listener; ReactionDisposer? _onAccountChangeReaction; + ReactionDisposer? _onTxHistoryChangeReaction; bool _isTransactionUpdating; bool _hasSyncAfterStartup; Timer? _autoSaveTimer; @@ -158,6 +163,7 @@ abstract class WowneroWalletBase void close() async { _listener?.stop(); _onAccountChangeReaction?.reaction.dispose(); + _onTxHistoryChangeReaction?.reaction.dispose(); _autoSaveTimer?.cancel(); } @@ -564,8 +570,8 @@ abstract class WowneroWalletBase } _isTransactionUpdating = true; - transactionHistory.clear(); final transactions = await fetchTransactions(); + transactionHistory.clear(); transactionHistory.addMany(transactions); await transactionHistory.save(); _isTransactionUpdating = false; diff --git a/cw_wownero/lib/wownero_wallet_addresses.dart b/cw_wownero/lib/wownero_wallet_addresses.dart index 9eeb182eb..b2f9ec67a 100644 --- a/cw_wownero/lib/wownero_wallet_addresses.dart +++ b/cw_wownero/lib/wownero_wallet_addresses.dart @@ -3,6 +3,8 @@ import 'package:cw_core/address_info.dart'; import 'package:cw_core/subaddress.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_wownero/api/transaction_history.dart'; +import 'package:cw_wownero/api/subaddress_list.dart' as subaddress_list; import 'package:cw_wownero/api/wallet.dart'; import 'package:cw_wownero/wownero_account_list.dart'; import 'package:cw_wownero/wownero_subaddress_list.dart'; @@ -27,6 +29,27 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { @observable String address; + @override + String get latestAddress { + var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1; + var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + while (hiddenAddresses.contains(address)) { + addressIndex++; + address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + } + return address; + } + + @override + String get addressForExchange { + var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1; + var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + while (hiddenAddresses.contains(address) || manualAddresses.contains(address)) { + addressIndex++; + address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + } + return address; + } @observable Account? account; @@ -36,11 +59,14 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { WowneroSubaddressList subaddressList; WowneroAccountList accountList; + + @override + Set usedAddresses = Set(); @override Future init() async { accountList.update(); - account = accountList.accounts.first; + account = accountList.accounts.isEmpty ? Account(id: 0, label: "Primary address") : accountList.accounts.first; updateSubaddressList(accountIndex: account?.id ?? 0); await updateAddressesInBox(); } @@ -89,8 +115,9 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { void updateSubaddressList({required int accountIndex}) { subaddressList.update(accountIndex: accountIndex); - subaddress = subaddressList.subaddresses.first; - address = subaddress!.address; + address = subaddressList.subaddresses.isNotEmpty + ? subaddressList.subaddresses.first.address + : getAddress(); } Future updateUsedSubaddress() async { @@ -109,7 +136,7 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { accountIndex: accountIndex, defaultLabel: defaultLabel, usedAddresses: usedAddresses.toList()); - subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel) : subaddressList.subaddresses.last; + subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel, balance: '0', txCount: 0) : subaddressList.subaddresses.last; address = subaddress!.address; } diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index 0e97b3214..c90340800 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -463,8 +463,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: "3cb38bee9385faf46b03fd73aab85f3ac4115bf7" - resolved-ref: "3cb38bee9385faf46b03fd73aab85f3ac4115bf7" + ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b" + resolved-ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b" url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" diff --git a/cw_wownero/pubspec.yaml b/cw_wownero/pubspec.yaml index 39a2cc9c0..6943e60c3 100644 --- a/cw_wownero/pubspec.yaml +++ b/cw_wownero/pubspec.yaml @@ -25,7 +25,7 @@ dependencies: monero: git: url: https://github.com/mrcyjanek/monero_c - ref: 3cb38bee9385faf46b03fd73aab85f3ac4115bf7 # monero_c hash + ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash path: impls/monero.dart mutex: ^3.1.0 diff --git a/ios/.gitignore b/ios/.gitignore index e96ef602b..8ded86f14 100644 --- a/ios/.gitignore +++ b/ios/.gitignore @@ -30,3 +30,5 @@ Runner/GeneratedPluginRegistrant.* !default.mode2v3 !default.pbxuser !default.perspectivev3 + +Mwebd.xcframework \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 847769cba..e574aafc1 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -7,6 +7,7 @@ PODS: - Flutter - ReachabilitySwift - CryptoSwift (1.8.2) + - cw_mweb (0.0.1): - device_display_brightness (0.0.1): - Flutter - device_info_plus (0.0.1): @@ -111,6 +112,7 @@ DEPENDENCIES: - barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - CryptoSwift + - cw_mweb (from `.symlinks/plugins/cw_mweb/ios`) - device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - devicelocale (from `.symlinks/plugins/devicelocale/ios`) @@ -156,6 +158,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/barcode_scan2/ios" connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" + cw_mweb: + :path: ".symlinks/plugins/cw_mweb/ios" device_display_brightness: :path: ".symlinks/plugins/device_display_brightness/ios" device_info_plus: @@ -211,6 +215,7 @@ SPEC CHECKSUMS: barcode_scan2: 0af2bb63c81b4565aab6cd78278e4c0fa136dbb0 connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d CryptoSwift: c63a805d8bb5e5538e88af4e44bb537776af11ea + cw_mweb: 87af74f9659fed0c1a2cbfb44413f1070e79e3ae device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 devicelocale: b22617f40038496deffba44747101255cee005b0 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 688fa2c39..10cc6434d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ 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, ); }; }; CE291CFE2C15DB9A00B9F709 /* WowneroWallet.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE291CFD2C15DB9A00B9F709 /* WowneroWallet.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + CEAFE4A02C53926F009FF3AD /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = C58D93382C00FAC6004BCF69 /* libresolv.tbd */; }; CFEFC24F82F78FE747DF1D22 /* LnurlPayInfo.swift in Resources */ = {isa = PBXBuildFile; fileRef = 58C22CBD8C22B9D6023D59F8 /* LnurlPayInfo.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; D0D7A0D4E13F31C4E02E235B /* ReceivePayment.swift in Resources */ = {isa = PBXBuildFile; fileRef = 91C524F800843E0A3F17E004 /* ReceivePayment.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; D3AD73A327249AFE8F016A51 /* BreezSDK.swift in Resources */ = {isa = PBXBuildFile; fileRef = ABD6FCBB0F4244B090459128 /* BreezSDK.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; @@ -84,7 +85,9 @@ ABD6FCBB0F4244B090459128 /* BreezSDK.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BreezSDK.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/BreezSDK.swift"; sourceTree = ""; }; AD0937B0140D5A4C24E73BEA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; B3D5E78267F5F18D882FDC3B /* ServiceConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ServiceConfig.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/ServiceConfig.swift"; sourceTree = ""; }; + C58D93382C00FAC6004BCF69 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; }; CE291CFD2C15DB9A00B9F709 /* WowneroWallet.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = WowneroWallet.framework; sourceTree = ""; }; + CEAFE49D2C539250009FF3AD /* Mwebd.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Mwebd.xcframework; sourceTree = ""; }; DCEA540E3586164FB47AD13E /* LnurlPayInvoice.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LnurlPayInvoice.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/Task/LnurlPayInvoice.swift"; sourceTree = ""; }; F42258C3697CFE3C8C8D1933 /* ServiceLogger.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ServiceLogger.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/ServiceLogger.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -95,6 +98,7 @@ buildActionMask = 2147483647; files = ( 4DFD1BB54A3A50573E19A583 /* Pods_Runner.framework in Frameworks */, + CEAFE4A02C53926F009FF3AD /* libresolv.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -104,6 +108,8 @@ 06957875428D0F5AAE053765 /* Frameworks */ = { isa = PBXGroup; children = ( + CEAFE49D2C539250009FF3AD /* Mwebd.xcframework */, + C58D93382C00FAC6004BCF69 /* libresolv.tbd */, 0C9986A3251A932F00D566FD /* CryptoSwift.framework */, 3C663361C56EBB242598F609 /* Pods_Runner.framework */, ); diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index d1869a05d..50c22d113 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -220,9 +220,9 @@ class CWBitcoin extends Bitcoin { return BitcoinWalletService(walletInfoSource, unspentCoinSource, alwaysScan, isDirect); } - WalletService createLitecoinWalletService( - Box walletInfoSource, Box unspentCoinSource, bool isDirect) { - return LitecoinWalletService(walletInfoSource, unspentCoinSource, isDirect); + WalletService createLitecoinWalletService(Box walletInfoSource, + Box unspentCoinSource, bool alwaysScan, bool isDirect) { + return LitecoinWalletService(walletInfoSource, unspentCoinSource, alwaysScan, isDirect); } @override @@ -261,6 +261,9 @@ class CWBitcoin extends Bitcoin { @override List getBitcoinReceivePageOptions() => BitcoinReceivePageOption.all; + @override + List getLitecoinReceivePageOptions() => BitcoinReceivePageOption.allLitecoin; + @override BitcoinAddressType getBitcoinAddressType(ReceivePageOption option) { switch (option) { @@ -272,6 +275,8 @@ class CWBitcoin extends Bitcoin { return SegwitAddresType.p2tr; case BitcoinReceivePageOption.p2wsh: return SegwitAddresType.p2wsh; + case BitcoinReceivePageOption.mweb: + return SegwitAddresType.mweb; case BitcoinReceivePageOption.p2wpkh: default: return SegwitAddresType.p2wpkh; @@ -555,6 +560,9 @@ class CWBitcoin extends Bitcoin { return await getBitcoinHeightByDate(date: date); } + @override + int getLitecoinHeightByDate({required DateTime date}) => getLtcHeightByDate(date: date); + @override Future rescan(Object wallet, {required int height, bool? doSingleScan}) async { final bitcoinWallet = wallet as ElectrumWallet; @@ -580,6 +588,17 @@ class CWBitcoin extends Bitcoin { } @override + Future setMwebEnabled(Object wallet, bool enabled) async { + final litecoinWallet = wallet as LitecoinWallet; + litecoinWallet.setMwebEnabled(enabled); + } + + @override + bool getMwebEnabled(Object wallet) { + final litecoinWallet = wallet as LitecoinWallet; + return litecoinWallet.mwebEnabled; + } + List updateOutputs(PendingTransaction pendingTransaction, List outputs) { final pendingTx = pendingTransaction as PendingBitcoinTransaction; @@ -588,7 +607,6 @@ class CWBitcoin extends Bitcoin { } final updatedOutputs = outputs.map((output) { - try { final pendingOut = pendingTx!.outputs[outputs.indexOf(output)]; final updatedOutput = output; @@ -609,4 +627,31 @@ class CWBitcoin extends Bitcoin { final tx = txInfo as ElectrumTransactionInfo; return tx.isReceivedSilentPayment; } + + @override + bool txIsMweb(TransactionInfo txInfo) { + final tx = txInfo as ElectrumTransactionInfo; + + List inputAddresses = tx.inputAddresses ?? []; + List outputAddresses = tx.outputAddresses ?? []; + bool inputAddressesContainMweb = false; + bool outputAddressesContainMweb = false; + + for (var address in inputAddresses) { + if (address.toLowerCase().contains('mweb')) { + inputAddressesContainMweb = true; + break; + } + } + + for (var address in outputAddresses) { + if (address.toLowerCase().contains('mweb')) { + outputAddressesContainMweb = true; + break; + } + } + + // TODO: this could be improved: + return inputAddressesContainMweb || outputAddressesContainMweb; + } } diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index e76cb99d1..0aabfa032 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -5,35 +5,44 @@ import 'package:cake_wallet/solana/solana.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; +const BEFORE_REGEX = '(^|\\s)'; +const AFTER_REGEX = '(\$|\\s)'; + class AddressValidator extends TextValidator { AddressValidator({required CryptoCurrency type}) : super( errorMessage: S.current.error_text_address, - useAdditionalValidation: type == CryptoCurrency.btc + useAdditionalValidation: type == CryptoCurrency.btc || type == CryptoCurrency.ltc ? (String txt) => BitcoinAddressUtils.validateAddress( address: txt, - network: BitcoinNetwork.mainnet, + network: type == CryptoCurrency.btc + ? BitcoinNetwork.mainnet + : LitecoinNetwork.mainnet, ) : null, pattern: getPattern(type), length: getLength(type)); static String getPattern(CryptoCurrency type) { + var pattern = ""; if (type is Erc20Token) { - return '0x[0-9a-zA-Z]'; + pattern = '0x[0-9a-zA-Z]+'; } switch (type) { case CryptoCurrency.xmr: - return '^4[0-9a-zA-Z]{94}\$|^8[0-9a-zA-Z]{94}\$|^[0-9a-zA-Z]{106}\$'; + pattern = '4[0-9a-zA-Z]{94}|8[0-9a-zA-Z]{94}|[0-9a-zA-Z]{106}'; case CryptoCurrency.ada: - return '^[0-9a-zA-Z]{59}\$|^[0-9a-zA-Z]{92}\$|^[0-9a-zA-Z]{104}\$' - '|^[0-9a-zA-Z]{105}\$|^addr1[0-9a-zA-Z]{98}\$'; + pattern = '[0-9a-zA-Z]{59}|[0-9a-zA-Z]{92}|[0-9a-zA-Z]{104}' + '|[0-9a-zA-Z]{105}|addr1[0-9a-zA-Z]{98}'; case CryptoCurrency.btc: - return '^${P2pkhAddress.regex.pattern}\$|^${P2shAddress.regex.pattern}\$|^${P2wpkhAddress.regex.pattern}\$|${P2trAddress.regex.pattern}\$|^${P2wshAddress.regex.pattern}\$|^${SilentPaymentAddress.regex.pattern}\$'; + pattern = + '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}'; + case CryptoCurrency.ltc: + pattern = '^${P2wpkhAddress.regex.pattern}\$|^${MwebAddress.regex.pattern}\$'; case CryptoCurrency.nano: - return '[0-9a-zA-Z_]'; + pattern = '[0-9a-zA-Z_]+'; case CryptoCurrency.banano: - return '[0-9a-zA-Z_]'; + pattern = '[0-9a-zA-Z_]+'; case CryptoCurrency.usdc: case CryptoCurrency.usdcpoly: case CryptoCurrency.usdtPoly: @@ -69,11 +78,11 @@ class AddressValidator extends TextValidator { case CryptoCurrency.dydx: case CryptoCurrency.steth: case CryptoCurrency.shib: - return '0x[0-9a-zA-Z]'; + pattern = '0x[0-9a-zA-Z]+'; case CryptoCurrency.xrp: - return '^[0-9a-zA-Z]{34}\$|^X[0-9a-zA-Z]{46}\$'; + pattern = '[0-9a-zA-Z]{34}|X[0-9a-zA-Z]{46}'; case CryptoCurrency.xhv: - return '^hvx|hvi|hvs[0-9a-zA-Z]'; + pattern = 'hvx|hvi|hvs[0-9a-zA-Z]+'; case CryptoCurrency.xag: case CryptoCurrency.xau: case CryptoCurrency.xaud: @@ -95,40 +104,41 @@ class AddressValidator extends TextValidator { case CryptoCurrency.dash: case CryptoCurrency.eos: case CryptoCurrency.wow: - return '[0-9a-zA-Z]'; + pattern = '[0-9a-zA-Z]+'; case CryptoCurrency.bch: - return '^(?!bitcoincash:)[0-9a-zA-Z]*\$|^(?!bitcoincash:)q|p[0-9a-zA-Z]{41}\$|^(?!bitcoincash:)q|p[0-9a-zA-Z]{42}\$|^bitcoincash:q|p[0-9a-zA-Z]{41}\$|^bitcoincash:q|p[0-9a-zA-Z]{42}\$'; + pattern = + '(?!bitcoincash:)[0-9a-zA-Z]*|(?!bitcoincash:)q|p[0-9a-zA-Z]{41}|(?!bitcoincash:)q|p[0-9a-zA-Z]{42}|bitcoincash:q|p[0-9a-zA-Z]{41}|bitcoincash:q|p[0-9a-zA-Z]{42}'; case CryptoCurrency.bnb: - return '[0-9a-zA-Z]'; - case CryptoCurrency.ltc: - return '^(?!(ltc|LTC)1)[0-9a-zA-Z]*\$|(^LTC1[A-Z0-9]*\$)|(^ltc1[a-z0-9]*\$)'; + pattern = '[0-9a-zA-Z]+'; case CryptoCurrency.hbar: - return '[0-9a-zA-Z.]'; + pattern = '[0-9a-zA-Z.]+'; case CryptoCurrency.zaddr: - return '^zs[0-9a-zA-Z]{75}'; + pattern = 'zs[0-9a-zA-Z]{75}'; case CryptoCurrency.zec: - return '^t1[0-9a-zA-Z]{33}\$|^t3[0-9a-zA-Z]{33}\$'; + pattern = 't1[0-9a-zA-Z]{33}|t3[0-9a-zA-Z]{33}'; case CryptoCurrency.dcr: - return 'D[ksecS]([0-9a-zA-Z])+'; + pattern = 'D[ksecS]([0-9a-zA-Z])+'; case CryptoCurrency.rvn: - return '[Rr]([1-9a-km-zA-HJ-NP-Z]){33}'; + pattern = '[Rr]([1-9a-km-zA-HJ-NP-Z]){33}'; case CryptoCurrency.near: - return '[0-9a-f]{64}'; + pattern = '[0-9a-f]{64}'; case CryptoCurrency.rune: - return 'thor1[0-9a-z]{38}'; + pattern = 'thor1[0-9a-z]{38}'; case CryptoCurrency.scrt: - return 'secret1[0-9a-z]{38}'; + pattern = 'secret1[0-9a-z]{38}'; case CryptoCurrency.stx: - return 'S[MP][0-9a-zA-Z]+'; + pattern = 'S[MP][0-9a-zA-Z]+'; case CryptoCurrency.kmd: - return 'R[0-9a-zA-Z]{33}'; + pattern = 'R[0-9a-zA-Z]{33}'; case CryptoCurrency.pivx: - return 'D([1-9a-km-zA-HJ-NP-Z]){33}'; + pattern = 'D([1-9a-km-zA-HJ-NP-Z]){33}'; case CryptoCurrency.btcln: - return '^(lnbc|LNBC)([0-9]{1,}[a-zA-Z0-9]+)'; + pattern = '(lnbc|LNBC)([0-9]{1,}[a-zA-Z0-9]+)'; default: - return '[0-9a-zA-Z]'; + pattern = '[0-9a-zA-Z]+'; } + + return '$BEFORE_REGEX($pattern)$AFTER_REGEX'; } static List? getLength(CryptoCurrency type) { @@ -149,6 +159,8 @@ class AddressValidator extends TextValidator { return null; case CryptoCurrency.btc: return null; + case CryptoCurrency.ltc: + return null; case CryptoCurrency.dash: return [34]; case CryptoCurrency.eos: @@ -195,8 +207,6 @@ class AddressValidator extends TextValidator { return [42, 43, 44, 54, 55]; case CryptoCurrency.bnb: return [42]; - case CryptoCurrency.ltc: - return [34, 43, 63]; case CryptoCurrency.nano: return [64, 65]; case CryptoCurrency.banano: @@ -269,56 +279,54 @@ class AddressValidator extends TextValidator { } static String? getAddressFromStringPattern(CryptoCurrency type) { + String? pattern = null; + switch (type) { case CryptoCurrency.xmr: case CryptoCurrency.wow: - return '([^0-9a-zA-Z]|^)4[0-9a-zA-Z]{94}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)8[0-9a-zA-Z]{94}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)[0-9a-zA-Z]{106}([^0-9a-zA-Z]|\$)'; + pattern = '(4[0-9a-zA-Z]{94})' + '|(8[0-9a-zA-Z]{94})' + '|([0-9a-zA-Z]{106})'; case CryptoCurrency.btc: - return '([^0-9a-zA-Z]|^)([1mn][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2pkhAddress type - '|([^0-9a-zA-Z]|^)([23][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2shAddress type - '|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{25,39})([^0-9a-zA-Z]|\$)' //P2wpkhAddress type - '|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{40,80})([^0-9a-zA-Z]|\$)' //P2wshAddress type - '|([^0-9a-zA-Z]|^)((bc|tb)1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))([^0-9a-zA-Z]|\$)' //P2trAddress type - '|${SilentPaymentAddress.regex.pattern}\$'; - + pattern = + '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}'; case CryptoCurrency.ltc: - return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)' + pattern = '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)' '|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)ltc[a-zA-Z0-9]{26,45}([^0-9a-zA-Z]|\$)'; + '|([^0-9a-zA-Z]|^)ltc[a-zA-Z0-9]{26,45}([^0-9a-zA-Z]|\$)' + '|([^0-9a-zA-Z]|^)((ltc|t)mweb1q[ac-hj-np-z02-9]{90,120})([^0-9a-zA-Z]|\$)'; case CryptoCurrency.eth: - return '0x[0-9a-zA-Z]{42}'; case CryptoCurrency.maticpoly: - return '0x[0-9a-zA-Z]{42}'; + pattern = '0x[0-9a-zA-Z]+'; case CryptoCurrency.nano: - return 'nano_[0-9a-zA-Z]{60}'; + pattern = 'nano_[0-9a-zA-Z]{60}'; case CryptoCurrency.banano: - return 'ban_[0-9a-zA-Z]{60}'; + pattern = 'ban_[0-9a-zA-Z]{60}'; case CryptoCurrency.bch: - return 'bitcoincash:q[0-9a-zA-Z]{41}([^0-9a-zA-Z]|\$)' - '|bitcoincash:q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{41}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)'; + pattern = '(bitcoincash:)?q[0-9a-zA-Z]{41,42}'; case CryptoCurrency.sol: - return '([^0-9a-zA-Z]|^)[1-9A-HJ-NP-Za-km-z]{43,44}([^0-9a-zA-Z]|\$)'; + pattern = '[1-9A-HJ-NP-Za-km-z]+'; case CryptoCurrency.trx: - return '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; + pattern = '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; default: if (type.tag == CryptoCurrency.eth.title) { - return '0x[0-9a-zA-Z]{42}'; + pattern = '0x[0-9a-zA-Z]{42}'; } if (type.tag == CryptoCurrency.maticpoly.tag) { - return '0x[0-9a-zA-Z]{42}'; + pattern = '0x[0-9a-zA-Z]{42}'; } if (type.tag == CryptoCurrency.sol.title) { - return '([^0-9a-zA-Z]|^)[1-9A-HJ-NP-Za-km-z]{43,44}([^0-9a-zA-Z]|\$)'; + pattern = '[1-9A-HJ-NP-Za-km-z]{43,44}'; } if (type.tag == CryptoCurrency.trx.title) { - return '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; + pattern = '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; } - - return null; } + + if (pattern != null) { + return "$BEFORE_REGEX($pattern)$AFTER_REGEX"; + } + + return null; } } diff --git a/lib/core/wallet_loading_service.dart b/lib/core/wallet_loading_service.dart index 0087b1332..e58e14652 100644 --- a/lib/core/wallet_loading_service.dart +++ b/lib/core/wallet_loading_service.dart @@ -85,7 +85,8 @@ class WalletLoadingService { authenticatedErrorStreamController.add(corruptedWalletsSeeds); return wallet; - } catch (_) { + } catch (e) { + print(e); // save seeds and show corrupted wallets' seeds to the user try { final seeds = await _getCorruptedWalletSeeds(walletInfo.name, walletInfo.type); diff --git a/lib/di.dart b/lib/di.dart index 12bdce2a0..7470770c8 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -111,6 +111,7 @@ import 'package:cake_wallet/src/screens/settings/desktop_settings/desktop_settin import 'package:cake_wallet/src/screens/settings/display_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/domain_lookups_page.dart'; import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart'; +import 'package:cake_wallet/src/screens/settings/mweb_settings.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; @@ -161,6 +162,7 @@ import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart'; import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart'; import 'package:cake_wallet/view_model/settings/display_settings_view_model.dart'; +import 'package:cake_wallet/view_model/settings/mweb_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/security_settings_view_model.dart'; @@ -676,7 +678,8 @@ Future setup({ getIt.registerFactory( () => Modify2FAPage(setup2FAViewModel: getIt.get())); - getIt.registerFactory(() => DesktopSettingsPage(getIt.get())); + getIt.registerFactory( + () => DesktopSettingsPage(getIt.get())); getIt.registerFactoryParam( (pageOption, _) => ReceiveOptionViewModel(getIt.get().wallet!, pageOption)); @@ -808,7 +811,9 @@ Future setup({ getIt.registerFactory(() { final wallet = getIt.get().wallet!; - if (wallet.type == WalletType.monero || wallet.type == WalletType.wownero || wallet.type == WalletType.haven) { + if (wallet.type == WalletType.monero || + wallet.type == WalletType.wownero || + wallet.type == WalletType.haven) { return MoneroAccountListViewModel(wallet); } throw Exception( @@ -868,6 +873,9 @@ Future setup({ getIt.registerFactory(() => SilentPaymentsSettingsViewModel(getIt.get(), getIt.get().wallet!)); + getIt.registerFactory( + () => MwebSettingsViewModel(getIt.get(), getIt.get().wallet!)); + getIt.registerFactory(() { return PrivacySettingsViewModel(getIt.get(), getIt.get().wallet!); }); @@ -937,6 +945,8 @@ Future setup({ getIt.registerFactory( () => SilentPaymentsSettingsPage(getIt.get())); + getIt.registerFactory(() => MwebSettingsPage(getIt.get())); + getIt.registerFactory(() => OtherSettingsPage(getIt.get())); getIt.registerFactory(() => NanoChangeRepPage( @@ -1031,8 +1041,12 @@ Future setup({ SettingsStoreBase.walletPasswordDirectInput, ); case WalletType.litecoin: - return bitcoin!.createLitecoinWalletService(_walletInfoSource, _unspentCoinsInfoSource, - SettingsStoreBase.walletPasswordDirectInput); + return bitcoin!.createLitecoinWalletService( + _walletInfoSource, + _unspentCoinsInfoSource, + getIt.get().mwebAlwaysScan, + SettingsStoreBase.walletPasswordDirectInput, + ); case WalletType.ethereum: return ethereum!.createEthereumWalletService( _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); @@ -1244,7 +1258,8 @@ Future setup({ getIt.registerFactory( () => CakePayService(getIt.get(), getIt.get())); - getIt.registerFactory(() => CakePayCardsListViewModel(cakePayService: getIt.get())); + getIt.registerFactory( + () => CakePayCardsListViewModel(cakePayService: getIt.get())); getIt.registerFactory(() => CakePayAuthViewModel(cakePayService: getIt.get())); @@ -1276,12 +1291,12 @@ Future setup({ getIt.registerFactoryParam, void>((List args, _) { final vendor = args.first as CakePayVendor; - return CakePayBuyCardPage(getIt.get(param1: vendor), - getIt.get()); + return CakePayBuyCardPage( + getIt.get(param1: vendor), getIt.get()); }); - getIt.registerFactoryParam, void>( - (List args, _) { + getIt + .registerFactoryParam, void>((List args, _) { final paymentCredential = args.first as PaymentCredential; final card = args[1] as CakePayCard; return CakePayBuyCardDetailPage( diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 212ac82b5..9b873eb77 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -48,6 +48,10 @@ class PreferencesKey { static const customBitcoinFeeRate = 'custom_electrum_fee_rate'; static const silentPaymentsCardDisplay = 'silentPaymentsCardDisplay'; static const silentPaymentsAlwaysScan = 'silentPaymentsAlwaysScan'; + static const mwebCardDisplay = 'mwebCardDisplay'; + static const mwebEnabled = 'mwebEnabled'; + static const hasEnabledMwebBefore = 'hasEnabledMwebBefore'; + static const mwebAlwaysScan = 'mwebAlwaysScan'; static const shouldShowReceiveWarning = 'should_show_receive_warning'; static const shouldShowYatPopup = 'should_show_yat_popup'; static const shouldShowRepWarning = 'should_show_rep_warning'; diff --git a/lib/exchange/provider/letsexchange_exchange_provider.dart b/lib/exchange/provider/letsexchange_exchange_provider.dart index a11e69796..1d4da55cb 100644 --- a/lib/exchange/provider/letsexchange_exchange_provider.dart +++ b/lib/exchange/provider/letsexchange_exchange_provider.dart @@ -278,8 +278,6 @@ class LetsExchangeExchangeProvider extends ExchangeProvider { return 'ERC20'; case 'BSC': return 'BEP20'; - case 'POLY': - return 'MATIC'; default: return currency.tag!; } diff --git a/lib/main.dart b/lib/main.dart index 32a6397c2..dcfd8d0da 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -30,6 +30,7 @@ import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cw_core/address_info.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/hive_type_ids.dart'; +import 'package:cw_core/mweb_utxo.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_info.dart'; @@ -152,6 +153,10 @@ Future initializeAppConfigs() async { CakeHive.registerAdapter(AnonpayInvoiceInfoAdapter()); } + if (!CakeHive.isAdapterRegistered(MwebUtxo.typeId)) { + CakeHive.registerAdapter(MwebUtxoAdapter()); + } + final secureStorage = secureStorageShared; final transactionDescriptionsBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: TransactionDescription.boxKey); @@ -233,7 +238,6 @@ Future initialSetup( secureStorage: secureStorage, ); await bootstrap(navigatorKey); - monero?.onStartup(); } class App extends StatefulWidget { diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index 1f1888b44..b1cf49482 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -61,7 +61,13 @@ class CWMoneroSubaddressList extends MoneroSubaddressList { ObservableList get subaddresses { final moneroWallet = _wallet as MoneroWallet; final subAddresses = moneroWallet.walletAddresses.subaddressList.subaddresses - .map((sub) => Subaddress(id: sub.id, address: sub.address, label: sub.label)) + .map((sub) => Subaddress( + id: sub.id, + address: sub.address, + label: sub.label, + received: sub.balance??"unknown", + txCount: sub.txCount??0, + )) .toList(); return ObservableList.of(subAddresses); } @@ -83,7 +89,12 @@ class CWMoneroSubaddressList extends MoneroSubaddressList { final moneroWallet = wallet as MoneroWallet; return moneroWallet.walletAddresses.subaddressList .getAll() - .map((sub) => Subaddress(id: sub.id, label: sub.label, address: sub.address)) + .map((sub) => Subaddress( + id: sub.id, + label: sub.label, + address: sub.address, + txCount: sub.txCount??0, + received: sub.balance??'unknown')) .toList(); } @@ -91,7 +102,7 @@ class CWMoneroSubaddressList extends MoneroSubaddressList { Future addSubaddress(Object wallet, {required int accountIndex, required String label}) async { final moneroWallet = wallet as MoneroWallet; - await moneroWallet.walletAddresses.subaddressList + return await moneroWallet.walletAddresses.subaddressList .addSubaddress(accountIndex: accountIndex, label: label); } diff --git a/lib/router.dart b/lib/router.dart index 16eeefeb1..7beace174 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -72,6 +72,7 @@ import 'package:cake_wallet/src/screens/settings/desktop_settings/desktop_settin import 'package:cake_wallet/src/screens/settings/display_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/domain_lookups_page.dart'; import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart'; +import 'package:cake_wallet/src/screens/settings/mweb_settings.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; @@ -450,6 +451,10 @@ Route createRoute(RouteSettings settings) { return CupertinoPageRoute( fullscreenDialog: true, builder: (_) => getIt.get()); + case Routes.mwebSettings: + return CupertinoPageRoute( + fullscreenDialog: true, builder: (_) => getIt.get()); + case Routes.connectionSync: return CupertinoPageRoute( fullscreenDialog: true, builder: (_) => getIt.get()); diff --git a/lib/routes.dart b/lib/routes.dart index 83d90248f..0529d7c6f 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -73,6 +73,7 @@ class Routes { static const cakePayAccountPage = '/cake_pay_account_page'; static const webViewPage = '/web_view_page'; static const silentPaymentsSettings = '/silent_payments_settings'; + static const mwebSettings = '/mweb_settings'; static const connectionSync = '/connection_sync_page'; static const securityBackupPage = '/security_and_backup_page'; static const privacyPage = '/privacy_page'; diff --git a/lib/src/screens/dashboard/pages/address_page.dart b/lib/src/screens/dashboard/pages/address_page.dart index 3ac97740d..10f9aef43 100644 --- a/lib/src/screens/dashboard/pages/address_page.dart +++ b/lib/src/screens/dashboard/pages/address_page.dart @@ -155,13 +155,14 @@ class AddressPage extends BasePage { amountTextFieldFocusNode: _cryptoAmountFocus, amountController: _amountController, isLight: dashboardViewModel.settingsStore.currentTheme.type == - ThemeType.light))), + ThemeType.light, + ))), SizedBox(height: 16), Observer(builder: (_) { if (addressListViewModel.hasAddressList) { return SelectButton( text: addressListViewModel.buttonTitle, - onTap: () async => Navigator.of(context).pushNamed(Routes.receive), + onTap: () => Navigator.of(context).pushNamed(Routes.receive), textColor: Theme.of(context).extension()!.textColor, color: Theme.of(context).extension()!.syncedBackgroundColor, borderColor: Theme.of(context).extension()!.cardBorderColor, @@ -225,7 +226,8 @@ class AddressPage extends BasePage { } break; default: - if (addressListViewModel.type == WalletType.bitcoin) { + if (addressListViewModel.type == WalletType.bitcoin || + addressListViewModel.type == WalletType.litecoin) { addressListViewModel.setAddressType(bitcoin!.getBitcoinAddressType(option)); } } diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index 57f908986..a7b2a99fa 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/nft_listing_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/home_screen_account_widget.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart'; @@ -25,6 +26,7 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; import 'package:url_launcher/url_launcher.dart'; class BalancePage extends StatelessWidget { @@ -129,7 +131,7 @@ class CryptoBalanceWidget extends StatelessWidget { builder: (_) { if (dashboardViewModel.getMoneroError != null) { return Padding( - padding: const EdgeInsets.fromLTRB(16,0,16,16), + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: DashBoardRoundedCardWidget( title: "Invalid monero bindings", subTitle: dashboardViewModel.getMoneroError.toString(), @@ -144,13 +146,12 @@ class CryptoBalanceWidget extends StatelessWidget { builder: (_) { if (dashboardViewModel.getWowneroError != null) { return Padding( - padding: const EdgeInsets.fromLTRB(16,0,16,16), - child: DashBoardRoundedCardWidget( - title: "Invalid wownero bindings", - subTitle: dashboardViewModel.getWowneroError.toString(), - onTap: () {}, - ) - ); + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: DashBoardRoundedCardWidget( + title: "Invalid wownero bindings", + subTitle: dashboardViewModel.getWowneroError.toString(), + onTap: () {}, + )); } return Container(); }, @@ -271,6 +272,18 @@ class CryptoBalanceWidget extends StatelessWidget { currency: balance.asset, hasAdditionalBalance: dashboardViewModel.balanceViewModel.hasAdditionalBalance, + hasSecondAdditionalBalance: + dashboardViewModel.balanceViewModel.hasSecondAdditionalBalance, + hasSecondAvailableBalance: + dashboardViewModel.balanceViewModel.hasSecondAvailableBalance, + secondAdditionalBalance: balance.secondAdditionalBalance, + secondAdditionalFiatBalance: balance.fiatSecondAdditionalBalance, + secondAvailableBalance: balance.secondAvailableBalance, + secondAvailableFiatBalance: balance.fiatSecondAvailableBalance, + secondAdditionalBalanceLabel: + '${dashboardViewModel.balanceViewModel.secondAdditionalBalanceLabel}', + secondAvailableBalanceLabel: + '${dashboardViewModel.balanceViewModel.secondAvailableBalanceLabel}', isTestnet: dashboardViewModel.isTestnet, ); }); @@ -284,16 +297,15 @@ class CryptoBalanceWidget extends StatelessWidget { if (dashboardViewModel.isMoneroWalletBrokenReasons.isNotEmpty) ...[ SizedBox(height: 10), Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), - child: DashBoardRoundedCardWidget( - customBorder: 30, - title: "This wallet has encountered an issue", - subTitle: "Here are the things that you should note:\n - " - +dashboardViewModel.isMoneroWalletBrokenReasons.join("\n - ") - +"\n\nPlease restart your wallet and if it doesn't help contact our support.", - onTap: () {}, - ) - ) + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: DashBoardRoundedCardWidget( + customBorder: 30, + title: "This wallet has encountered an issue", + subTitle: "Here are the things that you should note:\n - " + + dashboardViewModel.isMoneroWalletBrokenReasons.join("\n - ") + + "\n\nPlease restart your wallet and if it doesn't help contact our support.", + onTap: () {}, + )) ], if (dashboardViewModel.showSilentPaymentsCard) ...[ SizedBox(height: 10), @@ -360,7 +372,73 @@ class CryptoBalanceWidget extends StatelessWidget { ), ), ), - ] + ], + if (dashboardViewModel.showMwebCard) ...[ + SizedBox(height: 10), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: DashBoardRoundedCardWidget( + customBorder: 30, + title: S.current.litecoin_mweb, + subTitle: S.current.litecoin_enable_mweb_sync, + hint: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => launchUrl( + Uri.parse( + "https://guides.cakewallet.com/docs/cryptos/litecoin/#mweb"), + mode: LaunchMode.externalApplication, + ), + child: Row( + children: [ + Text( + S.current.litecoin_what_is_mweb, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .labelTextColor, + height: 1, + ), + softWrap: true, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Icon(Icons.help_outline, + size: 16, + color: Theme.of(context) + .extension()! + .labelTextColor), + ) + ], + ), + ), + Observer( + builder: (_) => StandardSwitch( + value: dashboardViewModel.mwebScanningActive, + onTaped: () => _toggleMweb(context), + ), + ) + ], + ), + ], + ), + onTap: () => _toggleMweb(context), + icon: ImageIcon( + AssetImage('assets/images/mweb_logo.png'), + color: + Theme.of(context).extension()!.pageTitleTextColor, + size: 50, + ), + ), + ), + ], ], ); }), @@ -400,6 +478,22 @@ class CryptoBalanceWidget extends StatelessWidget { return dashboardViewModel.setSilentPaymentsScanning(newValue); } + + Future _toggleMweb(BuildContext context) async { + if (!dashboardViewModel.hasEnabledMwebBefore) { + await showPopUp( + context: context, + builder: (BuildContext context) => AlertWithOneAction( + alertTitle: S.of(context).warning, + alertContent: S.current.litecoin_mweb_warning, + buttonText: S.of(context).ok, + buttonAction: () { + Navigator.of(context).pop(); + }, + )); + } + dashboardViewModel.setMwebScanningActive(!dashboardViewModel.mwebScanningActive); + } } class BalanceRowWidget extends StatelessWidget { @@ -410,10 +504,18 @@ class BalanceRowWidget extends StatelessWidget { required this.additionalBalanceLabel, required this.additionalBalance, required this.additionalFiatBalance, + required this.secondAvailableBalanceLabel, + required this.secondAvailableBalance, + required this.secondAvailableFiatBalance, + required this.secondAdditionalBalanceLabel, + required this.secondAdditionalBalance, + required this.secondAdditionalFiatBalance, required this.frozenBalance, required this.frozenFiatBalance, required this.currency, required this.hasAdditionalBalance, + required this.hasSecondAvailableBalance, + required this.hasSecondAdditionalBalance, required this.isTestnet, super.key, }); @@ -424,10 +526,18 @@ class BalanceRowWidget extends StatelessWidget { final String additionalBalanceLabel; final String additionalBalance; final String additionalFiatBalance; + final String secondAvailableBalanceLabel; + final String secondAvailableBalance; + final String secondAvailableFiatBalance; + final String secondAdditionalBalanceLabel; + final String secondAdditionalBalance; + final String secondAdditionalFiatBalance; final String frozenBalance; final String frozenFiatBalance; final CryptoCurrency currency; final bool hasAdditionalBalance; + final bool hasSecondAvailableBalance; + final bool hasSecondAdditionalBalance; final bool isTestnet; // void _showBalanceDescription(BuildContext context) { @@ -675,6 +785,94 @@ class BalanceRowWidget extends StatelessWidget { ), ], ), + if (hasSecondAvailableBalance) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24), + Text( + '${secondAvailableBalanceLabel}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.labelTextColor, + height: 1, + ), + ), + SizedBox(height: 8), + AutoSizeText( + secondAvailableBalance, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.assetTitleColor, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, + ), + SizedBox(height: 4), + if (!isTestnet) + Text( + '${secondAvailableFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.textColor, + height: 1, + ), + ), + ], + ), + if (hasSecondAdditionalBalance) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24), + Text( + '${secondAdditionalBalanceLabel}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.labelTextColor, + height: 1, + ), + ), + SizedBox(height: 8), + AutoSizeText( + secondAdditionalBalance, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.assetTitleColor, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, + ), + SizedBox(height: 4), + if (!isTestnet) + Text( + '${secondAdditionalFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.textColor, + height: 1, + ), + ), + ], + ), ], ), ), diff --git a/lib/src/screens/dashboard/pages/transactions_page.dart b/lib/src/screens/dashboard/pages/transactions_page.dart index 2538824ed..b6d1c286b 100644 --- a/lib/src/screens/dashboard/pages/transactions_page.dart +++ b/lib/src/screens/dashboard/pages/transactions_page.dart @@ -86,6 +86,18 @@ class TransactionsPage extends StatelessWidget { final transaction = item.transaction; final transactionType = dashboardViewModel.getTransactionType(transaction); + List tags = []; + if (dashboardViewModel.type == WalletType.bitcoin) { + if (bitcoin!.txIsReceivedSilentPayment(transaction)) { + tags.add(S.of(context).silent_payment); + } + } + if (dashboardViewModel.type == WalletType.litecoin) { + if (bitcoin!.txIsMweb(transaction)) { + tags.add("MWEB"); + } + } + return Observer( builder: (_) => TransactionRow( onTap: () => Navigator.of(context) @@ -100,9 +112,7 @@ class TransactionsPage extends StatelessWidget { isPending: transaction.isPending, title: item.formattedTitle + item.formattedStatus + transactionType, - isReceivedSilentPayment: - dashboardViewModel.type == WalletType.bitcoin && - bitcoin!.txIsReceivedSilentPayment(transaction), + tags: tags, ), ); } diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index 1caeb4c51..30c7b7f78 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -191,6 +191,11 @@ class MenuWidgetState extends State { final item = items[index]; + if (!widget.dashboardViewModel.hasMweb && + item.name(context) == S.current.litecoin_mweb_settings) { + return const SizedBox(); + } + final isLastTile = index == itemCount - 1; return SettingActionButton( diff --git a/lib/src/screens/dashboard/widgets/sign_form.dart b/lib/src/screens/dashboard/widgets/sign_form.dart index c0f8ba328..deead87a4 100644 --- a/lib/src/screens/dashboard/widgets/sign_form.dart +++ b/lib/src/screens/dashboard/widgets/sign_form.dart @@ -20,7 +20,7 @@ class SignForm extends StatefulWidget { SignFormState createState() => SignFormState(); } -class SignFormState extends State { +class SignFormState extends State with AutomaticKeepAliveClientMixin { SignFormState() : formKey = GlobalKey(), messageController = TextEditingController(), @@ -42,8 +42,12 @@ class SignFormState extends State { super.dispose(); } + @override + bool get wantKeepAlive => true; + @override Widget build(BuildContext context) { + super.build(context); return Container( padding: EdgeInsets.only(left: 24, right: 24), child: Column( diff --git a/lib/src/screens/dashboard/widgets/transaction_raw.dart b/lib/src/screens/dashboard/widgets/transaction_raw.dart index 88866fa25..b18131f3d 100644 --- a/lib/src/screens/dashboard/widgets/transaction_raw.dart +++ b/lib/src/screens/dashboard/widgets/transaction_raw.dart @@ -1,4 +1,3 @@ -import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:flutter/material.dart'; import 'package:cw_core/transaction_direction.dart'; @@ -12,7 +11,7 @@ class TransactionRow extends StatelessWidget { required this.formattedAmount, required this.formattedFiatAmount, required this.isPending, - required this.isReceivedSilentPayment, + required this.tags, required this.title, required this.onTap, }); @@ -23,8 +22,8 @@ class TransactionRow extends StatelessWidget { final String formattedAmount; final String formattedFiatAmount; final bool isPending; - final bool isReceivedSilentPayment; final String title; + final List tags; @override Widget build(BuildContext context) { @@ -61,7 +60,7 @@ class TransactionRow extends StatelessWidget { fontWeight: FontWeight.w500, color: Theme.of(context).extension()!.textColor, )), - if (isReceivedSilentPayment) TxTag(tag: S.of(context).silent_payment), + ...tags.map((tag) => Row(children: [SizedBox(width: 8), TxTag(tag: tag)])), ], ), Text(formattedAmount, diff --git a/lib/src/screens/dashboard/widgets/verify_form.dart b/lib/src/screens/dashboard/widgets/verify_form.dart index bf6809586..282ffa9da 100644 --- a/lib/src/screens/dashboard/widgets/verify_form.dart +++ b/lib/src/screens/dashboard/widgets/verify_form.dart @@ -15,7 +15,7 @@ class VerifyForm extends StatefulWidget { VerifyFormState createState() => VerifyFormState(); } -class VerifyFormState extends State { +class VerifyFormState extends State with AutomaticKeepAliveClientMixin { VerifyFormState() : formKey = GlobalKey(), messageController = TextEditingController(), @@ -36,9 +36,13 @@ class VerifyFormState extends State { void dispose() { super.dispose(); } + + @override + bool get wantKeepAlive => true; @override Widget build(BuildContext context) { + super.build(context); return Container( padding: EdgeInsets.only(left: 24, right: 24), child: Form( diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index 78b4d0db8..2f8e3eb5c 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -509,7 +509,7 @@ class ExchangePage extends BasePage { } }); - reaction((_) => exchangeViewModel.wallet.walletAddresses.address, (String address) { + reaction((_) => exchangeViewModel.wallet.walletAddresses.addressForExchange, (String address) { if (exchangeViewModel.depositCurrency == CryptoCurrency.xmr) { depositKey.currentState!.changeAddress(address: address); } @@ -565,7 +565,7 @@ class ExchangePage extends BasePage { key.currentState!.changeWalletName(isCurrentTypeWallet ? exchangeViewModel.wallet.name : ''); key.currentState!.changeAddress( - address: isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.address : ''); + address: isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.addressForExchange : ''); key.currentState!.changeAmount(amount: ''); } @@ -576,9 +576,9 @@ class ExchangePage extends BasePage { if (isCurrentTypeWallet) { key.currentState!.changeWalletName(exchangeViewModel.wallet.name); - key.currentState!.addressController.text = exchangeViewModel.wallet.walletAddresses.address; + key.currentState!.addressController.text = exchangeViewModel.wallet.walletAddresses.addressForExchange; } else if (key.currentState!.addressController.text == - exchangeViewModel.wallet.walletAddresses.address) { + exchangeViewModel.wallet.walletAddresses.addressForExchange) { key.currentState!.changeWalletName(''); key.currentState!.addressController.text = ''; } @@ -629,7 +629,7 @@ class ExchangePage extends BasePage { initialCurrency: exchangeViewModel.depositCurrency, initialWalletName: depositWalletName ?? '', initialAddress: exchangeViewModel.depositCurrency == exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.walletAddresses.address + ? exchangeViewModel.wallet.walletAddresses.addressForExchange : exchangeViewModel.depositAddress, initialIsAmountEditable: true, initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled, @@ -694,7 +694,7 @@ class ExchangePage extends BasePage { initialCurrency: exchangeViewModel.receiveCurrency, initialWalletName: receiveWalletName ?? '', initialAddress: exchangeViewModel.receiveCurrency == exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.walletAddresses.address + ? exchangeViewModel.wallet.walletAddresses.addressForExchange : exchangeViewModel.receiveAddress, initialIsAmountEditable: exchangeViewModel.isReceiveAmountEditable, isAmountEstimated: true, diff --git a/lib/src/screens/exchange/exchange_template_page.dart b/lib/src/screens/exchange/exchange_template_page.dart index 4edc9095a..f82318ca2 100644 --- a/lib/src/screens/exchange/exchange_template_page.dart +++ b/lib/src/screens/exchange/exchange_template_page.dart @@ -129,7 +129,7 @@ class ExchangeTemplatePage extends BasePage { initialWalletName: depositWalletName ?? '', initialAddress: exchangeViewModel.depositCurrency == exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.walletAddresses.address + ? exchangeViewModel.wallet.walletAddresses.addressForExchange : exchangeViewModel.depositAddress, initialIsAmountEditable: true, initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled, @@ -166,7 +166,7 @@ class ExchangeTemplatePage extends BasePage { initialWalletName: receiveWalletName ?? '', initialAddress: exchangeViewModel.receiveCurrency == exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.walletAddresses.address + ? exchangeViewModel.wallet.walletAddresses.addressForExchange : exchangeViewModel.receiveAddress, initialIsAmountEditable: false, isAmountEstimated: true, 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 078768a7a..8b7be18a1 100644 --- a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart +++ b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart @@ -85,7 +85,9 @@ class _AdvancedPrivacySettingsBody extends StatefulWidget { class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBody> { final TextEditingController passphraseController = TextEditingController(); + final TextEditingController confirmPassphraseController = TextEditingController(); final _formKey = GlobalKey(); + final _passphraseFormKey = GlobalKey(); bool? testnetValue; bool obscurePassphrase = true; @@ -93,9 +95,7 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo @override void initState() { passphraseController.text = widget.seedTypeViewModel.passphrase ?? ''; - - passphraseController - .addListener(() => widget.seedTypeViewModel.setPassphrase(passphraseController.text)); + confirmPassphraseController.text = widget.seedTypeViewModel.passphrase ?? ''; if (widget.isChildWallet) { if (widget.privacySettingsViewModel.type == WalletType.bitcoin) { @@ -205,18 +205,47 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo if (widget.privacySettingsViewModel.hasPassphraseOption) Padding( padding: EdgeInsets.all(24), - child: BaseTextFormField( - hintText: S.current.passphrase, - controller: passphraseController, - obscureText: obscurePassphrase, - suffixIcon: GestureDetector( - onTap: () => setState(() { - obscurePassphrase = !obscurePassphrase; - }), - child: Icon( - Icons.remove_red_eye, - color: obscurePassphrase ? Colors.black54 : Colors.black26, - ), + child: Form( + key: _passphraseFormKey, + child: Column( + children: [ + BaseTextFormField( + hintText: S.of(context).passphrase, + controller: passphraseController, + obscureText: obscurePassphrase, + suffixIcon: GestureDetector( + onTap: () => setState(() { + obscurePassphrase = !obscurePassphrase; + }), + child: Icon( + Icons.remove_red_eye, + color: obscurePassphrase ? Colors.black54 : Colors.black26, + ), + ), + ), + const SizedBox(height: 10), + BaseTextFormField( + hintText: S.of(context).confirm_passphrase, + controller: confirmPassphraseController, + obscureText: obscurePassphrase, + validator: (text) { + if (text == passphraseController.text) { + return null; + } + + return S.of(context).passphrases_doesnt_match; + }, + suffixIcon: GestureDetector( + onTap: () => setState(() { + obscurePassphrase = !obscurePassphrase; + }), + child: Icon( + Icons.remove_red_eye, + color: obscurePassphrase ? Colors.black54 : Colors.black26, + ), + ), + ), + ], ), ), ), @@ -272,7 +301,8 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo } widget.nodeViewModel.save(); - } else if (testnetValue == true) { + } + if (testnetValue == true) { // TODO: add type (mainnet/testnet) to Node class so when switching wallets the node can be switched to a matching type // Currently this is so you can create a working testnet wallet but you need to keep switching back the node if you use multiple wallets at once widget.nodeViewModel.address = publicBitcoinTestnetElectrumAddress; @@ -280,6 +310,13 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo widget.nodeViewModel.save(); } + if (passphraseController.text.isNotEmpty) { + if (_passphraseFormKey.currentState != null && !_passphraseFormKey.currentState!.validate()) { + return; + } + + widget.seedTypeViewModel.setPassphrase(passphraseController.text); + } Navigator.pop(context); }, @@ -318,11 +355,4 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo ); }); } - - @override - void dispose() { - passphraseController - .removeListener(() => widget.seedTypeViewModel.setPassphrase(passphraseController.text)); - super.dispose(); - } } diff --git a/lib/src/screens/new_wallet/wallet_group_description_page.dart b/lib/src/screens/new_wallet/wallet_group_description_page.dart index eb3501bed..5becea3a5 100644 --- a/lib/src/screens/new_wallet/wallet_group_description_page.dart +++ b/lib/src/screens/new_wallet/wallet_group_description_page.dart @@ -3,10 +3,11 @@ import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; - +import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:flutter_svg/svg.dart'; class WalletGroupDescriptionPage extends BasePage { WalletGroupDescriptionPage({required this.selectedWalletType}); @@ -16,16 +17,24 @@ class WalletGroupDescriptionPage extends BasePage { @override String get title => S.current.wallet_group; + @override Widget body(BuildContext context) { + + final lightImage = 'assets/images/wallet_group_light.png'; + final darkImage = 'assets/images/wallet_group_dark.png'; + final brightImage = 'assets/images/wallet_group_bright.png'; + + final image = currentTheme.type == ThemeType.light ? lightImage : darkImage; + return Container( alignment: Alignment.center, padding: EdgeInsets.all(24), child: Column( children: [ Image.asset( - 'assets/images/wallet_group.png', - scale: 0.8, + _getThemedWalletGroupImage(currentTheme.type), + height: 200, ), SizedBox(height: 32), Expanded( @@ -87,4 +96,19 @@ class WalletGroupDescriptionPage extends BasePage { ), ); } + + String _getThemedWalletGroupImage(ThemeType theme) { + final lightImage = 'assets/images/wallet_group_light.png'; + final darkImage = 'assets/images/wallet_group_dark.png'; + final brightImage = 'assets/images/wallet_group_bright.png'; + + switch (theme) { + case ThemeType.bright: + return brightImage; + case ThemeType.light: + return lightImage; + default: + return darkImage; + } + } } diff --git a/lib/src/screens/new_wallet/wallet_group_display_page.dart b/lib/src/screens/new_wallet/wallet_group_display_page.dart index 67dde506e..d3ce27446 100644 --- a/lib/src/screens/new_wallet/wallet_group_display_page.dart +++ b/lib/src/screens/new_wallet/wallet_group_display_page.dart @@ -58,6 +58,7 @@ class WalletGroupsDisplayBody extends StatelessWidget { final groupName = group.groupName ?? '${S.of(context).wallet_group} ${index + 1}'; return GroupedWalletExpansionTile( + shouldShowCurrentWalletPointer: false, leadingWidget: Icon(Icons.account_balance_wallet_outlined, size: 28), borderRadius: BorderRadius.all(Radius.circular(16)), diff --git a/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart b/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart index 52e2e74b0..74057cc87 100644 --- a/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart +++ b/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart @@ -12,6 +12,7 @@ class GroupedWalletExpansionTile extends StatelessWidget { this.childWallets = const [], this.onTitleTapped, this.onChildItemTapped = _defaultVoidCallback, + this.onExpansionChanged, this.leadingWidget, this.trailingWidget, this.childTrailingWidget, @@ -22,13 +23,18 @@ class GroupedWalletExpansionTile extends StatelessWidget { this.borderRadius, this.margin, this.tileKey, + this.isCurrentlySelectedWallet = false, + this.shouldShowCurrentWalletPointer = false, }) : super(key: tileKey); final Key? tileKey; final bool isSelected; + final bool isCurrentlySelectedWallet; + final bool shouldShowCurrentWalletPointer; final VoidCallback? onTitleTapped; final void Function(WalletListItem item) onChildItemTapped; + final void Function(bool)? onExpansionChanged; final String title; final Widget? leadingWidget; @@ -70,8 +76,13 @@ class GroupedWalletExpansionTile extends StatelessWidget { splashFactory: NoSplash.splashFactory, ), child: ExpansionTile( + onExpansionChanged: onExpansionChanged, + initiallyExpanded: shouldShowCurrentWalletPointer + ? childWallets.any((element) => element.isCurrent) + : false, key: tileKey, - tilePadding: EdgeInsets.symmetric(vertical: 1, horizontal: 16), + tilePadding: + EdgeInsets.symmetric(vertical: 1, horizontal: !isCurrentlySelectedWallet ? 16 : 0), iconColor: effectiveArrowColor, collapsedIconColor: effectiveArrowColor, leading: leadingWidget, @@ -90,19 +101,46 @@ class GroupedWalletExpansionTile extends StatelessWidget { ), children: childWallets.map( (item) { + final currentColor = item.isCurrent + ? Theme.of(context) + .extension()! + .createNewWalletButtonBackgroundColor + : Theme.of(context).colorScheme.background; final walletTypeToCrypto = walletTypeToCryptoCurrency(item.type); return ListTile( + contentPadding: EdgeInsets.zero, key: ValueKey(item.name), trailing: childTrailingWidget?.call(item), onTap: () => onChildItemTapped(item), - leading: Image.asset( - walletTypeToCrypto.iconPath!, - width: 32, - height: 32, + leading: SizedBox( + width: 60, + child: Row( + children: [ + item.isCurrent && shouldShowCurrentWalletPointer + ? Container( + height: 35, + width: 6, + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topRight: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + color: currentColor, + ), + ) + : SizedBox(width: 6), + SizedBox(width: 16), + Image.asset( + walletTypeToCrypto.iconPath!, + width: 32, + height: 32, + ), + ], + ), ), title: Text( item.name, - maxLines: 1, + maxLines: 2, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w500, diff --git a/lib/src/screens/receive/receive_page.dart b/lib/src/screens/receive/receive_page.dart index 724e5c3bd..7e3c2b555 100644 --- a/lib/src/screens/receive/receive_page.dart +++ b/lib/src/screens/receive/receive_page.dart @@ -121,7 +121,8 @@ class ReceivePage extends BasePage { heroTag: _heroTag, amountTextFieldFocusNode: _cryptoAmountFocus, amountController: _amountController, - isLight: currentTheme.type == ThemeType.light), + isLight: currentTheme.type == ThemeType.light, + ), ), AddressList(addressListViewModel: addressListViewModel), Padding( diff --git a/lib/src/screens/receive/widgets/address_cell.dart b/lib/src/screens/receive/widgets/address_cell.dart index 850c08209..beef7c762 100644 --- a/lib/src/screens/receive/widgets/address_cell.dart +++ b/lib/src/screens/receive/widgets/address_cell.dart @@ -2,6 +2,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; @@ -15,11 +16,14 @@ class AddressCell extends StatelessWidget { required this.textColor, this.onTap, this.onEdit, + this.onHide, + this.isHidden = false, this.onDelete, this.txCount, this.balance, this.isChange = false, - this.hasBalance = false}); + this.hasBalance = false, + this.hasReceived = false}); factory AddressCell.fromItem( WalletAddressListItem item, { @@ -28,7 +32,10 @@ class AddressCell extends StatelessWidget { required Color textColor, Function(String)? onTap, bool hasBalance = false, + bool hasReceived = false, Function()? onEdit, + Function()? onHide, + bool isHidden = false, Function()? onDelete, }) => AddressCell( @@ -40,11 +47,14 @@ class AddressCell extends StatelessWidget { textColor: textColor, onTap: onTap, onEdit: onEdit, + onHide: onHide, + isHidden: isHidden, onDelete: onDelete, txCount: item.txCount, balance: item.balance, isChange: item.isChange, - hasBalance: hasBalance); + hasBalance: hasBalance, + hasReceived: hasReceived,); final String address; final String name; @@ -54,11 +64,14 @@ class AddressCell extends StatelessWidget { final Color textColor; final Function(String)? onTap; final Function()? onEdit; + final Function()? onHide; + final bool isHidden; final Function()? onDelete; final int? txCount; final String? balance; final bool isChange; final bool hasBalance; + final bool hasReceived; static const int addressPreviewLength = 8; @@ -138,7 +151,7 @@ class AddressCell extends StatelessWidget { ), ], ), - if (hasBalance) + if (hasBalance || hasReceived) Padding( padding: const EdgeInsets.only(top: 8.0), child: Row( @@ -146,7 +159,7 @@ class AddressCell extends StatelessWidget { mainAxisSize: MainAxisSize.max, children: [ Text( - '${S.of(context).balance}: $balance', + '${hasReceived ? S.of(context).received : S.of(context).balance}: $balance', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -178,14 +191,28 @@ class AddressCell extends StatelessWidget { enabled: !isCurrent, child: Slidable( key: Key(address), - startActionPane: _actionPane(context), - endActionPane: _actionPane(context), + startActionPane: _actionPaneStart(context), + endActionPane: _actionPaneEnd(context), child: cell, ), ); } - ActionPane _actionPane(BuildContext context) => ActionPane( + ActionPane _actionPaneEnd(BuildContext context) => ActionPane( + motion: const ScrollMotion(), + extentRatio: onDelete != null ? 0.4 : 0.3, + children: [ + SlidableAction( + onPressed: (_) => onHide?.call(), + backgroundColor: isHidden ? Colors.green : Colors.red, + foregroundColor: Colors.white, + icon: isHidden ? CupertinoIcons.arrow_left : CupertinoIcons.arrow_right, + label: isHidden ? S.of(context).show : S.of(context).hide, + ), + ], + ); + + ActionPane _actionPaneStart(BuildContext context) => ActionPane( motion: const ScrollMotion(), extentRatio: onDelete != null ? 0.4 : 0.3, children: [ diff --git a/lib/src/screens/receive/widgets/address_list.dart b/lib/src/screens/receive/widgets/address_list.dart index 8dfbedec1..9f15018d0 100644 --- a/lib/src/screens/receive/widgets/address_list.dart +++ b/lib/src/screens/receive/widgets/address_list.dart @@ -1,3 +1,6 @@ + +import 'dart:math'; + import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; @@ -7,16 +10,19 @@ import 'package:cake_wallet/src/screens/receive/widgets/address_cell.dart'; import 'package:cake_wallet/src/screens/receive/widgets/header_tile.dart'; import 'package:cake_wallet/src/widgets/section_divider.dart'; import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; +import 'package:cake_wallet/utils/list_item.dart'; import 'package:cake_wallet/utils/show_pop_up.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_list_view_model.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -class AddressList extends StatelessWidget { +class AddressList extends StatefulWidget { const AddressList({ super.key, required this.addressListViewModel, @@ -26,59 +32,123 @@ class AddressList extends StatelessWidget { final WalletAddressListViewModel addressListViewModel; final Function(String)? onSelect; + @override + State createState() => _AddressListState(); +} + +class _AddressListState extends State { + + bool showHiddenAddresses = false; + + void _toggleHiddenAddresses() { + setState(() { + showHiddenAddresses = !showHiddenAddresses; + }); + updateItems(); + } + + List getItems(List list, bool showHidden) { + return list.where((element) { + if (element is WalletAddressListItem) { + if (showHidden && element.isHidden) return true; + if (!showHidden && !element.isHidden) return true; + return false; + } + return true; + }).toList(); + } + + List items = []; + + void updateItems() { + setState(() { + items = getItems(widget.addressListViewModel.items, showHiddenAddresses); + }); + } + + @override + void initState() { + super.initState(); + + items = getItems(widget.addressListViewModel.items, showHiddenAddresses); + } + @override Widget build(BuildContext context) { - bool editable = onSelect == null; - return Observer( - builder: (_) => ListView.separated( - padding: EdgeInsets.all(0), - separatorBuilder: (context, _) => const HorizontalSectionDivider(), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: addressListViewModel.items.length, - itemBuilder: (context, index) { - final item = addressListViewModel.items[index]; - Widget cell = Container(); + bool editable = widget.onSelect == null; + return ListView.separated( + padding: EdgeInsets.all(0), + separatorBuilder: (context, _) => const HorizontalSectionDivider(), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + Widget cell = Container(); - if (item is WalletAccountListHeader) { - cell = HeaderTile( - showTrailingButton: true, - walletAddressListViewModel: addressListViewModel, - trailingButtonTap: () async { - if (addressListViewModel.type == WalletType.monero || - addressListViewModel.type == WalletType.haven) { - await showPopUp( - context: context, builder: (_) => getIt.get()); - } else { - await showPopUp( - context: context, builder: (_) => getIt.get()); - } - }, - title: S.of(context).accounts, - trailingIcon: Icon( - Icons.arrow_forward_ios, - size: 14, - color: Theme.of(context).extension()!.iconsColor, - )); - } + if (item is WalletAccountListHeader) { + cell = HeaderTile( + showTrailingButton: true, + walletAddressListViewModel: widget.addressListViewModel, + trailingButtonTap: () async { + if (widget.addressListViewModel.type == WalletType.monero || + widget.addressListViewModel.type == WalletType.haven) { + await showPopUp( + context: context, builder: (_) => getIt.get()); + updateItems(); + } else { + await showPopUp( + context: context, builder: (_) => getIt.get()); + updateItems(); + } + }, + title: S.of(context).accounts, + trailingIcon: Icon( + Icons.arrow_forward_ios, + size: 14, + color: Theme.of(context).extension()!.iconsColor, + )); + } - if (item is WalletAddressListHeader) { - cell = HeaderTile( - title: S.of(context).addresses, - walletAddressListViewModel: addressListViewModel, - showTrailingButton: !addressListViewModel.isAutoGenerateSubaddressEnabled, - showSearchButton: true, - trailingButtonTap: () => Navigator.of(context).pushNamed(Routes.newSubaddress), - trailingIcon: Icon( - Icons.add, - size: 20, - color: Theme.of(context).extension()!.iconsColor, - )); - } + if (item is WalletAddressHiddenListHeader) { + cell = HeaderTile( + title: S.of(context).hidden_addresses, + walletAddressListViewModel: widget.addressListViewModel, + showTrailingButton: true, + showSearchButton: false, + trailingButtonTap: _toggleHiddenAddresses, + trailingIcon: Icon( + showHiddenAddresses ? Icons.toggle_on : Icons.toggle_off, + size: 20, + color: Theme.of(context).extension()!.iconsColor, + )); + } - if (item is WalletAddressListItem) { + if (item is WalletAddressListHeader) { + cell = HeaderTile( + title: S.of(context).addresses, + walletAddressListViewModel: widget.addressListViewModel, + showTrailingButton: widget.addressListViewModel.showAddManualAddresses, + showSearchButton: true, + onSearchCallback: updateItems, + trailingButtonTap: () => Navigator.of(context).pushNamed(Routes.newSubaddress).then((value) { + updateItems(); // refresh the new address + }), + trailingIcon: Icon( + Icons.add, + size: 20, + color: Theme.of(context).extension()!.iconsColor, + )); + } + + if (item is WalletAddressListItem) { + if (item.isHidden && !showHiddenAddresses) { + cell = Container(); + } else if (!item.isHidden && showHiddenAddresses) { + cell = Container(); + } else { cell = Observer(builder: (_) { - final isCurrent = item.address == addressListViewModel.address.address && editable; + final isCurrent = item.address == widget.addressListViewModel.address.address && editable; final backgroundColor = isCurrent ? Theme.of(context).extension()!.currentTileBackgroundColor : Theme.of(context).extension()!.tilesBackgroundColor; @@ -86,35 +156,51 @@ class AddressList extends StatelessWidget { ? Theme.of(context).extension()!.currentTileTextColor : Theme.of(context).extension()!.tilesTextColor; + return AddressCell.fromItem( item, isCurrent: isCurrent, - hasBalance: addressListViewModel.isElectrumWallet, - backgroundColor: backgroundColor, + hasBalance: widget.addressListViewModel.isBalanceAvailable, + hasReceived: widget.addressListViewModel.isReceivedAvailable, + // hasReceived: + backgroundColor: (kDebugMode && item.isHidden) ? + Theme.of(context).colorScheme.error : + (kDebugMode && item.isManual) ? Theme.of(context).colorScheme.error.withBlue(255) : + backgroundColor, textColor: textColor, onTap: (_) { - if (onSelect != null) { - onSelect!(item.address); + if (widget.onSelect != null) { + widget.onSelect!(item.address); return; } - addressListViewModel.setAddress(item); + widget.addressListViewModel.setAddress(item); }, onEdit: editable - ? () => Navigator.of(context).pushNamed(Routes.newSubaddress, arguments: item) + ? () => Navigator.of(context).pushNamed(Routes.newSubaddress, arguments: item).then((value) { + updateItems(); // refresh the new address + }) : null, + isHidden: item.isHidden, + onHide: () => _hideAddress(item), ); }); } + } - return index != 0 - ? cell - : ClipRRect( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(30), topRight: Radius.circular(30)), - child: cell, - ); - }, - ), + return index != 0 + ? cell + : ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), topRight: Radius.circular(30)), + child: cell, + ); + }, ); } + + void _hideAddress(WalletAddressListItem item) async { + await widget.addressListViewModel.toggleHideAddress(item); + updateItems(); + } + } diff --git a/lib/src/screens/receive/widgets/header_tile.dart b/lib/src/screens/receive/widgets/header_tile.dart index faaa9ed07..dc48db89d 100644 --- a/lib/src/screens/receive/widgets/header_tile.dart +++ b/lib/src/screens/receive/widgets/header_tile.dart @@ -10,6 +10,7 @@ class HeaderTile extends StatefulWidget { this.showSearchButton = false, this.showTrailingButton = false, this.trailingButtonTap, + this.onSearchCallback, this.trailingIcon, }); @@ -18,6 +19,7 @@ class HeaderTile extends StatefulWidget { final bool showSearchButton; final bool showTrailingButton; final VoidCallback? trailingButtonTap; + final VoidCallback? onSearchCallback; final Icon? trailingIcon; @override @@ -41,7 +43,10 @@ class _HeaderTileState extends State { _isSearchActive ? Expanded( child: TextField( - onChanged: (value) => widget.walletAddressListViewModel.updateSearchText(value), + onChanged: (value) { + widget.walletAddressListViewModel.updateSearchText(value); + widget.onSearchCallback?.call(); + }, cursorColor: Theme.of(context).extension()!.tilesTextColor, cursorWidth: 0.5, decoration: InputDecoration( diff --git a/lib/src/screens/receive/widgets/qr_widget.dart b/lib/src/screens/receive/widgets/qr_widget.dart index 9f0db059a..9d09e57a1 100644 --- a/lib/src/screens/receive/widgets/qr_widget.dart +++ b/lib/src/screens/receive/widgets/qr_widget.dart @@ -37,6 +37,10 @@ class QRWidget extends StatelessWidget { final int? qrVersion; final String? heroTag; + PaymentURI get addressUri { + return addressListViewModel.uri; + } + @override Widget build(BuildContext context) { final copyImage = Image.asset('assets/images/copy_address.png', @@ -77,14 +81,14 @@ class QRWidget extends StatelessWidget { () async { await Navigator.pushNamed(context, Routes.fullscreenQR, arguments: QrViewData( - data: addressListViewModel.uri.toString(), + data: addressUri.toString(), heroTag: heroTag, )); }, ); }, child: Hero( - tag: Key(heroTag ?? addressListViewModel.uri.toString()), + tag: Key(heroTag ?? addressUri.toString()), child: Center( child: AspectRatio( aspectRatio: 1.0, @@ -105,7 +109,7 @@ class QRWidget extends StatelessWidget { color: Colors.white, ), ), - child: QrImage(data: addressListViewModel.uri.toString())), + child: QrImage(data: addressUri.toString())), ), ), ), @@ -148,7 +152,7 @@ class QRWidget extends StatelessWidget { builder: (context) => Observer( builder: (context) => GestureDetector( onTap: () { - Clipboard.setData(ClipboardData(text: addressListViewModel.address.address)); + Clipboard.setData(ClipboardData(text: addressUri.address)); showBar(context, S.of(context).copied_to_clipboard); }, child: Row( @@ -157,7 +161,7 @@ class QRWidget extends StatelessWidget { children: [ Expanded( child: Text( - addressListViewModel.address.address, + addressUri.address, textAlign: TextAlign.center, style: TextStyle( fontSize: 15, diff --git a/lib/src/screens/rescan/rescan_page.dart b/lib/src/screens/rescan/rescan_page.dart index 58e7a44f6..2c1c213c1 100644 --- a/lib/src/screens/rescan/rescan_page.dart +++ b/lib/src/screens/rescan/rescan_page.dart @@ -33,7 +33,9 @@ class RescanPage extends BasePage { key: _blockchainHeightWidgetKey, onHeightOrDateEntered: (value) => _rescanViewModel.isButtonEnabled = value, isSilentPaymentsScan: _rescanViewModel.isSilentPaymentsScan, + isMwebScan: _rescanViewModel.isMwebScan, doSingleScan: _rescanViewModel.doSingleScan, + hasDatePicker: !_rescanViewModel.isMwebScan,// disable date picker for mweb for now toggleSingleScan: () => _rescanViewModel.doSingleScan = !_rescanViewModel.doSingleScan, walletType: _rescanViewModel.wallet.type, diff --git a/lib/src/screens/restore/wallet_restore_from_seed_form.dart b/lib/src/screens/restore/wallet_restore_from_seed_form.dart index 67576144c..897a6bed0 100644 --- a/lib/src/screens/restore/wallet_restore_from_seed_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_seed_form.dart @@ -78,6 +78,8 @@ class WalletRestoreFromSeedFormState extends State { void Function()? repeatedPasswordListener; void Function()? passphraseListener; + bool obscurePassphrase = true; + @override void initState() { _setSeedType(widget.seedSettingsViewModel.moneroSeedType); @@ -283,7 +285,16 @@ class WalletRestoreFromSeedFormState extends State { BaseTextFormField( hintText: S.current.passphrase, controller: passphraseController, - obscureText: true, + obscureText: obscurePassphrase, + suffixIcon: GestureDetector( + onTap: () => setState(() { + obscurePassphrase = !obscurePassphrase; + }), + child: Icon( + Icons.remove_red_eye, + color: obscurePassphrase ? Colors.black54 : Colors.black26, + ), + ), ), ] ])); diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index c8e109860..4a4e27b3d 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -284,8 +284,11 @@ class WalletRestorePage extends BasePage { } // bip39: - const validSeedLengths = [12, 18, 24]; - if (!(validSeedLengths.contains(seedWords.length))) { + final validBip39SeedLengths = [12, 18, 24]; + final nonBip39WalletTypes = [WalletType.monero, WalletType.wownero, WalletType.haven]; + // if it's a bip39 wallet and the length is not valid return false + if (!nonBip39WalletTypes.contains(walletRestoreViewModel.type) && + !(validBip39SeedLengths.contains(seedWords.length))) { return false; } diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index 76c6c5cfd..2acf6138e 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/core/totp_request_details.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart'; @@ -134,6 +135,10 @@ class RootState extends State with WidgetsBindingObserver { setState(() => _setInactive(true)); } + if (widget.appStore.wallet?.type == WalletType.litecoin) { + widget.appStore.wallet?.stopSync(); + } + break; case AppLifecycleState.resumed: widget.authService.requireAuth().then((value) { @@ -143,6 +148,9 @@ class RootState extends State with WidgetsBindingObserver { }); } }); + if (widget.appStore.wallet?.type == WalletType.litecoin) { + widget.appStore.wallet?.startSync(); + } break; default: break; diff --git a/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart b/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart index 611b2acb7..79f74065a 100644 --- a/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart +++ b/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart @@ -60,6 +60,11 @@ class _DesktopSettingsPageState extends State { return Container(); } + if (!widget.dashboardViewModel.hasMweb && + item.name(context) == S.of(context).litecoin_mweb_settings) { + return Container(); + } + final isLastTile = index == itemCount - 1; return SettingActionButton( isLastTile: isLastTile, diff --git a/lib/src/screens/settings/mweb_settings.dart b/lib/src/screens/settings/mweb_settings.dart new file mode 100644 index 000000000..88dc00f7c --- /dev/null +++ b/lib/src/screens/settings/mweb_settings.dart @@ -0,0 +1,51 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; +import 'package:cake_wallet/view_model/settings/mweb_settings_view_model.dart'; +import 'package:cake_wallet/view_model/settings/silent_payments_settings_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class MwebSettingsPage extends BasePage { + MwebSettingsPage(this._mwebSettingsViewModel); + + @override + String get title => S.current.litecoin_mweb_settings; + + final MwebSettingsViewModel _mwebSettingsViewModel; + + @override + Widget body(BuildContext context) { + return SingleChildScrollView( + child: Observer(builder: (_) { + return Container( + padding: EdgeInsets.only(top: 10), + child: Column( + children: [ + SettingsSwitcherCell( + title: S.current.litecoin_mweb_display_card, + value: _mwebSettingsViewModel.mwebCardDisplay, + onValueChange: (_, bool value) { + _mwebSettingsViewModel.setMwebCardDisplay(value); + }, + ), + SettingsSwitcherCell( + title: S.current.litecoin_mweb_always_scan, + value: _mwebSettingsViewModel.mwebAlwaysScan, + onValueChange: (_, bool value) { + _mwebSettingsViewModel.setMwebAlwaysScan(value); + }, + ), + SettingsCellWithArrow( + title: S.current.litecoin_mweb_scanning, + handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.rescan), + ), + ], + ), + ); + }), + ); + } +} diff --git a/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart b/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart index 60a23c99b..236d06f4e 100644 --- a/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart +++ b/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart @@ -103,40 +103,63 @@ class UnspentCoinsListItem extends StatelessWidget { ), maxLines: 1, ), - if (isChange) - Container( - height: 17, - padding: EdgeInsets.only(left: 6, right: 6), - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(8.5)), - color: Colors.white), - alignment: Alignment.center, - child: Text( - S.of(context).unspent_change, - style: TextStyle( - color: itemColor, - fontSize: 7, - fontWeight: FontWeight.w600, + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (isChange) + Container( + height: 17, + padding: EdgeInsets.only(left: 6, right: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8.5)), + color: Colors.white), + alignment: Alignment.center, + child: Text( + S.of(context).unspent_change, + style: TextStyle( + color: itemColor, + fontSize: 7, + fontWeight: FontWeight.w600, + ), + ), ), - ), - ), - if (isSilentPayment) - Container( - height: 17, - padding: EdgeInsets.only(left: 6, right: 6), - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(8.5)), - color: Colors.white), - alignment: Alignment.center, - child: Text( - S.of(context).silent_payments, - style: TextStyle( - color: itemColor, - fontSize: 7, - fontWeight: FontWeight.w600, + if (address.toLowerCase().contains("mweb")) + Container( + height: 17, + padding: EdgeInsets.only(left: 6, right: 6), + margin: EdgeInsets.only(left: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8.5)), + color: Colors.white), + alignment: Alignment.center, + child: Text( + "MWEB", + style: TextStyle( + color: itemColor, + fontSize: 7, + fontWeight: FontWeight.w600, + ), + ), ), - ), - ), + if (isSilentPayment) + Container( + height: 17, + padding: EdgeInsets.only(left: 6, right: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8.5)), + color: Colors.white), + alignment: Alignment.center, + child: Text( + S.of(context).silent_payments, + style: TextStyle( + color: itemColor, + fontSize: 7, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), ], ), ), diff --git a/lib/src/screens/wallet_list/edit_wallet_button_widget.dart b/lib/src/screens/wallet_list/edit_wallet_button_widget.dart index 8f858348a..fd6f1a0ee 100644 --- a/lib/src/screens/wallet_list/edit_wallet_button_widget.dart +++ b/lib/src/screens/wallet_list/edit_wallet_button_widget.dart @@ -7,11 +7,13 @@ class EditWalletButtonWidget extends StatelessWidget { required this.width, required this.onTap, this.isGroup = false, + this.isExpanded = false, super.key, }); final bool isGroup; final double width; + final bool isExpanded; final VoidCallback onTap; @override @@ -42,7 +44,7 @@ class EditWalletButtonWidget extends StatelessWidget { if (isGroup) ...{ SizedBox(width: 6), Icon( - Icons.keyboard_arrow_down, + isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, size: 24, color: Theme.of(context).extension()!.titlesColor, ), diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index 9c210d8eb..d17534f6b 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -11,6 +11,7 @@ import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/themes/extensions/filter_theme.dart'; +import 'package:cake_wallet/themes/extensions/wallet_list_theme.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; @@ -156,7 +157,18 @@ class WalletListBodyState extends State { final group = widget.walletListViewModel.multiWalletGroups[index]; final groupName = group.groupName ?? '${S.current.wallet_group} ${index + 1}'; + + widget.walletListViewModel.updateTileState( + index, + widget.walletListViewModel.expansionTileStateTrack[index] ?? false, + ); + return GroupedWalletExpansionTile( + onExpansionChanged: (value) { + widget.walletListViewModel.updateTileState(index, value); + setState(() {}); + }, + shouldShowCurrentWalletPointer: true, borderRadius: BorderRadius.all(Radius.circular(16)), margin: EdgeInsets.only(left: 20, right: 20, bottom: 12), title: groupName, @@ -168,6 +180,8 @@ class WalletListBodyState extends State { trailingWidget: EditWalletButtonWidget( width: 74, isGroup: true, + isExpanded: + widget.walletListViewModel.expansionTileStateTrack[index]!, onTap: () { final wallet = widget.walletListViewModel .convertWalletInfoToWalletListItem(group.wallets.first); @@ -193,13 +207,16 @@ class WalletListBodyState extends State { childTrailingWidget: (item) { return item.isCurrent ? SizedBox.shrink() - : EditWalletButtonWidget( - width: 44, - onTap: () => Navigator.of(context).pushNamed( - Routes.walletEdit, - arguments: WalletEditPageArguments( - walletListViewModel: widget.walletListViewModel, - editingWallet: item, + : Padding( + padding: const EdgeInsets.only(right: 16), + child: EditWalletButtonWidget( + width: 44, + onTap: () => Navigator.of(context).pushNamed( + Routes.walletEdit, + arguments: WalletEditPageArguments( + walletListViewModel: widget.walletListViewModel, + editingWallet: item, + ), ), ), ); @@ -232,13 +249,40 @@ class WalletListBodyState extends State { updateFunction: widget.walletListViewModel.reorderAccordingToWalletList, itemBuilder: (context, index) { final wallet = widget.walletListViewModel.singleWalletsList[index]; + final currentColor = wallet.isCurrent + ? Theme.of(context) + .extension()! + .createNewWalletButtonBackgroundColor + : Theme.of(context).colorScheme.background; return GroupedWalletExpansionTile( tileKey: ValueKey('single_wallets_expansion_tile_widget_$index'), - leadingWidget: Image.asset( - walletTypeToCryptoCurrency(wallet.type).iconPath!, - width: 32, - height: 32, + isCurrentlySelectedWallet: wallet.isCurrent, + leadingWidget: SizedBox( + width: wallet.isCurrent ? 56 : 40, + child: Row( + children: [ + wallet.isCurrent + ? Container( + height: 35, + width: 6, + margin: EdgeInsets.only(right: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topRight: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + color: currentColor, + ), + ) + : SizedBox(width: 6), + Image.asset( + walletTypeToCryptoCurrency(wallet.type).iconPath!, + width: 32, + height: 32, + ), + ], + ), ), title: wallet.name, isSelected: false, diff --git a/lib/src/widgets/blockchain_height_widget.dart b/lib/src/widgets/blockchain_height_widget.dart index 40e191438..9d66c1789 100644 --- a/lib/src/widgets/blockchain_height_widget.dart +++ b/lib/src/widgets/blockchain_height_widget.dart @@ -18,6 +18,7 @@ class BlockchainHeightWidget extends StatefulWidget { this.onHeightOrDateEntered, this.hasDatePicker = true, this.isSilentPaymentsScan = false, + this.isMwebScan = false, this.toggleSingleScan, this.doSingleScan = false, this.bitcoinMempoolAPIEnabled, @@ -29,6 +30,7 @@ class BlockchainHeightWidget extends StatefulWidget { final FocusNode? focusNode; final bool hasDatePicker; final bool isSilentPaymentsScan; + final bool isMwebScan; final bool doSingleScan; final Future? bitcoinMempoolAPIEnabled; final Function()? toggleSingleScan; @@ -170,7 +172,9 @@ class BlockchainHeightState extends State { if (date != null) { int height; - if (widget.isSilentPaymentsScan) { + if (widget.isMwebScan) { + height = bitcoin!.getLitecoinHeightByDate(date: date); + } else if (widget.isSilentPaymentsScan) { height = await bitcoin!.getHeightByDate( date: date, bitcoinMempoolAPIEnabled: await widget.bitcoinMempoolAPIEnabled, diff --git a/lib/src/widgets/dashboard_card_widget.dart b/lib/src/widgets/dashboard_card_widget.dart index 4013dd79e..d9b545040 100644 --- a/lib/src/widgets/dashboard_card_widget.dart +++ b/lib/src/widgets/dashboard_card_widget.dart @@ -23,8 +23,8 @@ class DashBoardRoundedCardWidget extends StatelessWidget { final String subTitle; final Widget? hint; final SvgPicture? svgPicture; + final Widget? icon; final Image? image; - final Icon? icon; final double? customBorder; @override diff --git a/lib/src/widgets/setting_actions.dart b/lib/src/widgets/setting_actions.dart index 272ed57c2..a8a9558d5 100644 --- a/lib/src/widgets/setting_actions.dart +++ b/lib/src/widgets/setting_actions.dart @@ -18,6 +18,7 @@ class SettingActions { walletSettingAction, addressBookSettingAction, silentPaymentsSettingAction, + litecoinMwebSettingAction, securityBackupSettingAction, privacySettingAction, displaySettingAction, @@ -30,6 +31,7 @@ class SettingActions { walletSettingAction, addressBookSettingAction, silentPaymentsSettingAction, + litecoinMwebSettingAction, securityBackupSettingAction, privacySettingAction, displaySettingAction, @@ -46,6 +48,15 @@ class SettingActions { }, ); + static SettingActions litecoinMwebSettingAction = SettingActions._( + name: (context) => S.current.litecoin_mweb_settings, + image: 'assets/images/bitcoin_menu.png', + onTap: (BuildContext context) { + Navigator.pop(context); + Navigator.of(context).pushNamed(Routes.mwebSettings); + }, + ); + static SettingActions connectionSettingAction = SettingActions._( name: (context) => S.of(context).connection_sync, image: 'assets/images/nodes_menu.png', diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 9a6631522..0fd3f6b1e 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -115,6 +115,10 @@ abstract class SettingsStoreBase with Store { required this.customBitcoinFeeRate, required this.silentPaymentsCardDisplay, required this.silentPaymentsAlwaysScan, + required this.mwebAlwaysScan, + required this.mwebCardDisplay, + required this.mwebEnabled, + required this.hasEnabledMwebBefore, TransactionPriority? initialBitcoinTransactionPriority, TransactionPriority? initialMoneroTransactionPriority, TransactionPriority? initialWowneroTransactionPriority, @@ -554,6 +558,24 @@ abstract class SettingsStoreBase with Store { (bool silentPaymentsAlwaysScan) => _sharedPreferences.setBool( PreferencesKey.silentPaymentsAlwaysScan, silentPaymentsAlwaysScan)); + reaction( + (_) => mwebAlwaysScan, + (bool mwebAlwaysScan) => + _sharedPreferences.setBool(PreferencesKey.mwebAlwaysScan, mwebAlwaysScan)); + + reaction( + (_) => mwebCardDisplay, + (bool mwebCardDisplay) => + _sharedPreferences.setBool(PreferencesKey.mwebCardDisplay, mwebCardDisplay)); + + reaction((_) => mwebEnabled, + (bool mwebEnabled) => _sharedPreferences.setBool(PreferencesKey.mwebEnabled, mwebEnabled)); + + reaction( + (_) => hasEnabledMwebBefore, + (bool hasEnabledMwebBefore) => + _sharedPreferences.setBool(PreferencesKey.hasEnabledMwebBefore, hasEnabledMwebBefore)); + this.nodes.observe((change) { if (change.newValue != null && change.key != null) { _saveCurrentNode(change.newValue!, change.key!); @@ -767,6 +789,18 @@ abstract class SettingsStoreBase with Store { @observable bool silentPaymentsAlwaysScan; + @observable + bool mwebAlwaysScan; + + @observable + bool mwebCardDisplay; + + @observable + bool mwebEnabled; + + @observable + bool hasEnabledMwebBefore; + final SecureStorage _secureStorage; final SharedPreferences _sharedPreferences; final BackgroundTasks _backgroundTasks; @@ -924,6 +958,11 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true; final silentPaymentsAlwaysScan = sharedPreferences.getBool(PreferencesKey.silentPaymentsAlwaysScan) ?? false; + final mwebAlwaysScan = sharedPreferences.getBool(PreferencesKey.mwebAlwaysScan) ?? false; + final mwebCardDisplay = sharedPreferences.getBool(PreferencesKey.mwebCardDisplay) ?? true; + final mwebEnabled = sharedPreferences.getBool(PreferencesKey.mwebEnabled) ?? false; + final hasEnabledMwebBefore = + sharedPreferences.getBool(PreferencesKey.hasEnabledMwebBefore) ?? false; // If no value if (pinLength == null || pinLength == 0) { @@ -1190,6 +1229,10 @@ abstract class SettingsStoreBase with Store { customBitcoinFeeRate: customBitcoinFeeRate, silentPaymentsCardDisplay: silentPaymentsCardDisplay, silentPaymentsAlwaysScan: silentPaymentsAlwaysScan, + mwebAlwaysScan: mwebAlwaysScan, + mwebCardDisplay: mwebCardDisplay, + mwebEnabled: mwebEnabled, + hasEnabledMwebBefore: hasEnabledMwebBefore, initialMoneroTransactionPriority: moneroTransactionPriority, initialWowneroTransactionPriority: wowneroTransactionPriority, initialBitcoinTransactionPriority: bitcoinTransactionPriority, @@ -1352,6 +1395,10 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true; silentPaymentsAlwaysScan = sharedPreferences.getBool(PreferencesKey.silentPaymentsAlwaysScan) ?? false; + mwebAlwaysScan = sharedPreferences.getBool(PreferencesKey.mwebAlwaysScan) ?? false; + mwebCardDisplay = sharedPreferences.getBool(PreferencesKey.mwebCardDisplay) ?? true; + mwebEnabled = sharedPreferences.getBool(PreferencesKey.mwebEnabled) ?? false; + hasEnabledMwebBefore = sharedPreferences.getBool(PreferencesKey.hasEnabledMwebBefore) ?? false; final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); final bitcoinElectrumServerId = sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index e3a571eea..91797c45e 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -83,7 +83,7 @@ class ExceptionHandler { } static void onError(FlutterErrorDetails errorDetails) async { - if (kDebugMode) { + if (kDebugMode || kProfileMode) { FlutterError.presentError(errorDetails); debugPrint(errorDetails.toString()); return; diff --git a/lib/view_model/contact_list/contact_list_view_model.dart b/lib/view_model/contact_list/contact_list_view_model.dart index d63f78224..df6cbdb9f 100644 --- a/lib/view_model/contact_list/contact_list_view_model.dart +++ b/lib/view_model/contact_list/contact_list_view_model.dart @@ -27,32 +27,41 @@ abstract class ContactListViewModelBase with Store { settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.enabled { walletInfoSource.values.forEach((info) { if (isAutoGenerateEnabled && info.type == WalletType.monero && info.addressInfos != null) { - info.addressInfos!.forEach((key, value) { - final nextUnusedAddress = value.firstWhereOrNull( - (addressInfo) => !(info.usedAddresses?.contains(addressInfo.address) ?? false)); - if (nextUnusedAddress != null) { - final name = _createName(info.name, nextUnusedAddress.label); - walletContacts.add(WalletContact( - nextUnusedAddress.address, - name, - walletTypeToCryptoCurrency(info.type), - )); - } - }); + final key = info.addressInfos!.keys.first; + final value = info.addressInfos![key]; + final address = value?.first; + if (address != null) { + final name = _createName(info.name, address.label); + walletContacts.add(WalletContact( + address.address, + name, + walletTypeToCryptoCurrency(info.type), + )); + } } else if (info.addresses?.isNotEmpty == true && info.addresses!.length > 1) { - info.addresses!.forEach((address, label) { - if (label.isEmpty) { - return; - } - final name = _createName(info.name, label); + if ([WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type)) { + final address = info.address; + final name = _createName(info.name, ""); walletContacts.add(WalletContact( address, name, - walletTypeToCryptoCurrency(info.type, - isTestnet: - info.network == null ? false : info.network!.toLowerCase().contains("testnet")), + walletTypeToCryptoCurrency(info.type), )); - }); + } else { + info.addresses!.forEach((address, label) { + if (label.isEmpty) { + return; + } + final name = _createName(info.name, label); + walletContacts.add(WalletContact( + address, + name, + walletTypeToCryptoCurrency(info.type, + isTestnet: + info.network == null ? false : info.network!.toLowerCase().contains("testnet")), + )); + }); + } } else { walletContacts.add(WalletContact( info.address, diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 045b55261..c3fb5718a 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -21,10 +21,14 @@ class BalanceRecord { const BalanceRecord( {required this.availableBalance, required this.additionalBalance, + required this.secondAvailableBalance, + required this.secondAdditionalBalance, required this.frozenBalance, required this.fiatAvailableBalance, required this.fiatAdditionalBalance, required this.fiatFrozenBalance, + required this.fiatSecondAvailableBalance, + required this.fiatSecondAdditionalBalance, required this.asset, required this.formattedAssetTitle}); final String fiatAdditionalBalance; @@ -33,6 +37,10 @@ class BalanceRecord { final String additionalBalance; final String availableBalance; final String frozenBalance; + final String secondAvailableBalance; + final String secondAdditionalBalance; + final String fiatSecondAdditionalBalance; + final String fiatSecondAvailableBalance; final CryptoCurrency asset; final String formattedAssetTitle; } @@ -158,6 +166,26 @@ abstract class BalanceViewModelBase with Store { } } + @computed + String get secondAvailableBalanceLabel { + switch (wallet.type) { + case WalletType.litecoin: + return S.current.mweb_confirmed; + default: + return S.current.confirmed; + } + } + + @computed + String get secondAdditionalBalanceLabel { + switch (wallet.type) { + case WalletType.litecoin: + return S.current.mweb_unconfirmed; + default: + return S.current.unconfirmed; + } + } + @computed bool get hasMultiBalance => appStore.wallet!.type == WalletType.haven; @@ -243,9 +271,13 @@ abstract class BalanceViewModelBase with Store { availableBalance: '---', additionalBalance: '---', frozenBalance: '---', + secondAvailableBalance: '---', + secondAdditionalBalance: '---', fiatAdditionalBalance: isFiatDisabled ? '' : '---', fiatAvailableBalance: isFiatDisabled ? '' : '---', fiatFrozenBalance: isFiatDisabled ? '' : '---', + fiatSecondAvailableBalance: isFiatDisabled ? '' : '---', + fiatSecondAdditionalBalance: isFiatDisabled ? '' : '---', asset: key, formattedAssetTitle: _formatterAsset(key))); } @@ -274,24 +306,46 @@ abstract class BalanceViewModelBase with Store { ' ' + _getFiatBalance(price: price, cryptoAmount: getFormattedFrozenBalance(value))); + final secondAdditionalFiatBalance = isFiatDisabled + ? '' + : (fiatCurrency.toString() + + ' ' + + _getFiatBalance(price: price, cryptoAmount: value.formattedSecondAdditionalBalance)); + + final secondAvailableFiatBalance = isFiatDisabled + ? '' + : (fiatCurrency.toString() + + ' ' + + _getFiatBalance(price: price, cryptoAmount: value.formattedSecondAvailableBalance)); + return MapEntry( key, BalanceRecord( availableBalance: value.formattedAvailableBalance, additionalBalance: value.formattedAdditionalBalance, frozenBalance: getFormattedFrozenBalance(value), + secondAvailableBalance: value.formattedSecondAvailableBalance, + secondAdditionalBalance: value.formattedSecondAdditionalBalance, fiatAdditionalBalance: additionalFiatBalance, fiatAvailableBalance: availableFiatBalance, fiatFrozenBalance: frozenFiatBalance, + fiatSecondAvailableBalance: secondAvailableFiatBalance, + fiatSecondAdditionalBalance: secondAdditionalFiatBalance, asset: key, formattedAssetTitle: _formatterAsset(key))); }); } @computed - bool get hasAdditionalBalance => _hasAdditionBalanceForWalletType(wallet.type); + bool get hasAdditionalBalance => _hasAdditionalBalanceForWalletType(wallet.type); - bool _hasAdditionBalanceForWalletType(WalletType type) { + @computed + bool get hasSecondAdditionalBalance => _hasSecondAdditionalBalanceForWalletType(wallet.type); + + @computed + bool get hasSecondAvailableBalance => _hasSecondAvailableBalanceForWalletType(wallet.type); + + bool _hasAdditionalBalanceForWalletType(WalletType type) { switch (type) { case WalletType.ethereum: case WalletType.polygon: @@ -303,6 +357,20 @@ abstract class BalanceViewModelBase with Store { } } + bool _hasSecondAdditionalBalanceForWalletType(WalletType type) { + if (wallet.type == WalletType.litecoin && settingsStore.mwebEnabled) { + return true; + } + return false; + } + + bool _hasSecondAvailableBalanceForWalletType(WalletType type) { + if (wallet.type == WalletType.litecoin && settingsStore.mwebEnabled) { + return true; + } + return false; + } + @computed List get formattedBalances { final balance = balances.values.toList(); diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 4c3a9e1ea..21a167e2a 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -137,8 +137,8 @@ abstract class DashboardViewModelBase with Store { FilterItem( value: () => tradeFilterStore.displayLetsExchange, caption: ExchangeProviderDescription.letsExchange.title, - onChanged: () => - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.letsExchange)), + onChanged: () => tradeFilterStore + .toggleDisplayExchange(ExchangeProviderDescription.letsExchange)), FilterItem( value: () => tradeFilterStore.displayStealthEx, caption: ExchangeProviderDescription.stealthEx.title, @@ -255,6 +255,16 @@ abstract class DashboardViewModelBase with Store { silentPaymentsScanningActive = bitcoin!.getScanningActive(wallet); }); } + + if (hasMweb) { + mwebScanningActive = bitcoin!.getMwebEnabled(wallet); + settingsStore.mwebEnabled = mwebScanningActive; + reaction((_) => settingsStore.mwebAlwaysScan, (bool alwaysScan) { + if (alwaysScan) { + mwebScanningActive = true; + } + }); + } } @observable @@ -348,6 +358,7 @@ abstract class DashboardViewModelBase with Store { bool get hasRescan => wallet.type == WalletType.bitcoin || wallet.type == WalletType.monero || + wallet.type == WalletType.litecoin || wallet.type == WalletType.wownero || wallet.type == WalletType.haven; @@ -416,6 +427,33 @@ abstract class DashboardViewModelBase with Store { } } + @computed + bool get hasMweb => wallet.type == WalletType.litecoin; + + @computed + bool get showMwebCard => hasMweb && settingsStore.mwebCardDisplay; + + @observable + bool mwebScanningActive = false; + + @computed + bool get hasEnabledMwebBefore => settingsStore.hasEnabledMwebBefore; + + @action + void setMwebScanningActive(bool active) { + if (!hasMweb) { + return; + } + + if (active) { + settingsStore.hasEnabledMwebBefore = true; + } + + settingsStore.mwebEnabled = active; + mwebScanningActive = active; + bitcoin!.setMwebEnabled(wallet, active); + } + BalanceViewModel balanceViewModel; AppStore appStore; diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index 4269078a5..1aeacd28f 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -151,34 +151,24 @@ abstract class HomeSettingsViewModelBase with Store { bool isEthereum = _balanceViewModel.wallet.type == WalletType.ethereum; - print('An extra log for now'); - bool isPotentialScamViaMoralis = await _isPotentialScamTokenViaMoralis( contractAddress, isEthereum ? 'eth' : 'polygon', ); - print('Is Potential Scam from Moralis: $isPotentialScamViaMoralis'); - bool isPotentialScamViaExplorers = await _isPotentialScamTokenViaExplorers( contractAddress, isEthereum: isEthereum, ); - print('Is Potential Scam from Explorers: $isPotentialScamViaExplorers'); - bool isUnverifiedContract = await _isContractUnverified( contractAddress, isEthereum: isEthereum, ); - print('Is Unverified Contract: $isUnverifiedContract'); - final showWarningForContractAddress = isPotentialScamViaMoralis || isUnverifiedContract || isPotentialScamViaExplorers; - print('Show Warning: $showWarningForContractAddress'); - return showWarningForContractAddress; } finally { isValidatingContractAddress = false; @@ -272,8 +262,8 @@ abstract class HomeSettingsViewModelBase with Store { final decodedResponse = jsonDecode(response.body) as Map; if (decodedResponse['status'] != '1') { - print('${response.body}\n'); - print('${decodedResponse['result']}\n'); + log('${response.body}\n'); + log('${decodedResponse['result']}\n'); return true; } diff --git a/lib/view_model/dashboard/receive_option_view_model.dart b/lib/view_model/dashboard/receive_option_view_model.dart index 1e4726eee..744e4c58d 100644 --- a/lib/view_model/dashboard/receive_option_view_model.dart +++ b/lib/view_model/dashboard/receive_option_view_model.dart @@ -11,19 +11,31 @@ class ReceiveOptionViewModel = ReceiveOptionViewModelBase with _$ReceiveOptionVi abstract class ReceiveOptionViewModelBase with Store { ReceiveOptionViewModelBase(this._wallet, this.initialPageOption) : selectedReceiveOption = initialPageOption ?? - (_wallet.type == WalletType.bitcoin + (_wallet.type == WalletType.bitcoin || + _wallet.type == WalletType.litecoin ? bitcoin!.getSelectedAddressType(_wallet) : ReceivePageOption.mainnet), _options = [] { final walletType = _wallet.type; - _options = walletType == WalletType.haven - ? [ReceivePageOption.mainnet] - : walletType == WalletType.bitcoin - ? [ - ...bitcoin!.getBitcoinReceivePageOptions(), - ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) - ] - : ReceivePageOptions; + switch (walletType) { + case WalletType.bitcoin: + _options = [ + ...bitcoin!.getBitcoinReceivePageOptions(), + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ]; + break; + case WalletType.litecoin: + _options = [ + ...bitcoin!.getLitecoinReceivePageOptions(), + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ]; + break; + case WalletType.haven: + _options = [ReceivePageOption.mainnet]; + break; + default: + _options = ReceivePageOptions; + } } final WalletBase _wallet; diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 3101807b7..47fc32ab6 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -56,7 +56,8 @@ class TransactionListItem extends ActionListItem with Keyable { } String get formattedPendingStatus { - if (balanceViewModel.wallet.type == WalletType.monero || balanceViewModel.wallet.type == WalletType.haven) { + if (balanceViewModel.wallet.type == WalletType.monero || + balanceViewModel.wallet.type == WalletType.haven) { if (transaction.confirmations >= 0 && transaction.confirmations < 10) { return ' (${transaction.confirmations}/10)'; } @@ -77,6 +78,13 @@ class TransactionListItem extends ActionListItem with Keyable { return transaction.isPending ? S.current.pending : ''; } + String get formattedType { + if (transaction.evmSignatureName == 'approval') { + return ' (${transaction.evmSignatureName})'; + } + return ''; + } + CryptoCurrency? get assetOfTransaction { try { if (balanceViewModel.wallet.type == WalletType.ethereum) { diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index b69862379..3f6afaaf5 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -132,7 +132,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with depositAmount = ''; receiveAmount = ''; receiveAddress = ''; - depositAddress = depositCurrency == wallet.currency ? wallet.walletAddresses.address : ''; + depositAddress = depositCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : ''; provider = providersForCurrentPair().first; final initialProvider = provider; provider!.checkIsAvailable().then((bool isAvailable) { @@ -166,6 +166,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with wallet.type == WalletType.litecoin || wallet.type == WalletType.bitcoinCash; + bool get hideAddressAfterExchange => + wallet.type == WalletType.monero || + wallet.type == WalletType.wownero; + bool _useTorOnly; final Box trades; final ExchangeTemplateStore _exchangeTemplateStore; @@ -552,6 +556,11 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with isFixedRate: isFixedRateMode, ); + if (hideAddressAfterExchange) { + wallet.walletAddresses.hiddenAddresses.add(depositAddress); + await wallet.walletAddresses.saveAddressesInBox(); + } + var amount = isFixedRateMode ? receiveAmount : depositAmount; amount = amount.replaceAll(',', '.'); @@ -615,8 +624,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with isReceiveAmountEntered = false; depositAmount = ''; receiveAmount = ''; - depositAddress = depositCurrency == wallet.currency ? wallet.walletAddresses.address : ''; - receiveAddress = receiveCurrency == wallet.currency ? wallet.walletAddresses.address : ''; + depositAddress = depositCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : ''; + receiveAddress = receiveCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : ''; isDepositAddressEnabled = !(depositCurrency == wallet.currency); isFixedRateMode = false; _onPairChange(); diff --git a/lib/view_model/rescan_view_model.dart b/lib/view_model/rescan_view_model.dart index 00eed5633..7ef1f6ba5 100644 --- a/lib/view_model/rescan_view_model.dart +++ b/lib/view_model/rescan_view_model.dart @@ -30,6 +30,8 @@ abstract class RescanViewModelBase with Store { bool get isSilentPaymentsScan => wallet.type == WalletType.bitcoin; @computed + bool get isMwebScan => wallet.type == WalletType.litecoin; + Future get isBitcoinMempoolAPIEnabled async => wallet.type == WalletType.bitcoin && await bitcoin!.checkIfMempoolAPIIsEnabled(wallet); diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index c1ab9d822..79f473eb3 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -217,7 +217,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor PendingTransaction? pendingTransaction; @computed - String get balance => wallet.balance[selectedCryptoCurrency]!.formattedAvailableBalance; + String get balance => wallet.balance[selectedCryptoCurrency]!.formattedFullAvailableBalance; @computed bool get isFiatDisabled => balanceViewModel.isFiatDisabled; @@ -675,6 +675,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor return S.current.tx_no_dust_exception; } if (error is TransactionCommitFailed) { + if (error.errorMessage != null && error.errorMessage!.contains("no peers replied")) { + return S.current.tx_commit_failed_no_peers; + } return "${S.current.tx_commit_failed}${error.errorMessage != null ? "\n\n${error.errorMessage}" : ""}"; } if (error is TransactionCommitFailedDustChange) { diff --git a/lib/view_model/settings/mweb_settings_view_model.dart b/lib/view_model/settings/mweb_settings_view_model.dart new file mode 100644 index 000000000..343947d00 --- /dev/null +++ b/lib/view_model/settings/mweb_settings_view_model.dart @@ -0,0 +1,32 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:mobx/mobx.dart'; + +part 'mweb_settings_view_model.g.dart'; + +class MwebSettingsViewModel = MwebSettingsViewModelBase with _$MwebSettingsViewModel; + +abstract class MwebSettingsViewModelBase with Store { + MwebSettingsViewModelBase(this._settingsStore, this._wallet); + + final SettingsStore _settingsStore; + final WalletBase _wallet; + + @computed + bool get mwebCardDisplay => _settingsStore.mwebCardDisplay; + + @computed + bool get mwebAlwaysScan => _settingsStore.mwebAlwaysScan; + + @action + void setMwebCardDisplay(bool value) { + _settingsStore.mwebCardDisplay = value; + } + + @action + void setMwebAlwaysScan(bool value) { + _settingsStore.mwebAlwaysScan = value; + bitcoin!.setMwebEnabled(_wallet, value); + } +} diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index d3aa2a841..a96d70a90 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -160,9 +160,9 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.ethereum: return 'https://etherscan.io/tx/${txId}'; case WalletType.nano: - return 'https://nanolooker.com/block/${txId}'; + return 'https://nanexplorer.com/nano/block/${txId}'; case WalletType.banano: - return 'https://bananolooker.com/block/${txId}'; + return 'https://nanexplorer.com/banano/block/${txId}'; case WalletType.polygon: return 'https://polygonscan.com/tx/${txId}'; case WalletType.solana: @@ -190,9 +190,9 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.ethereum: return S.current.view_transaction_on + 'etherscan.io'; case WalletType.nano: - return S.current.view_transaction_on + 'nanolooker.com'; + return S.current.view_transaction_on + 'nanexplorer.com'; case WalletType.banano: - return S.current.view_transaction_on + 'bananolooker.com'; + return S.current.view_transaction_on + 'nanexplorer.com'; case WalletType.polygon: return S.current.view_transaction_on + 'polygonscan.com'; case WalletType.solana: diff --git a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart index e2d8469f1..72dcdb27b 100644 --- a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart +++ b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart @@ -38,6 +38,10 @@ abstract class UnspentCoinsListViewModelBase with Store { final info = getUnspentCoinInfo(item.hash, item.address, item.amountRaw, item.vout, item.keyImage); + if (info == null) { + return; + } + info.isFrozen = item.isFrozen; info.isSending = item.isSending; info.note = item.note; @@ -50,15 +54,21 @@ abstract class UnspentCoinsListViewModelBase with Store { } } - UnspentCoinsInfo getUnspentCoinInfo( - String hash, String address, int value, int vout, String? keyImage) => - _unspentCoinsInfo.values.firstWhere((element) => + UnspentCoinsInfo? getUnspentCoinInfo( + String hash, String address, int value, int vout, String? keyImage) { + try { + return _unspentCoinsInfo.values.firstWhere((element) => element.walletId == wallet.id && element.hash == hash && element.address == address && element.value == value && element.vout == vout && element.keyImage == keyImage); + } catch (e) { + print("UnspentCoinsInfo not found for coin: $e"); + return null; + } + } String formatAmountToString(int fullBalance) { if (wallet.type == WalletType.monero) @@ -85,11 +95,18 @@ abstract class UnspentCoinsListViewModelBase with Store { } List _getUnspents() { - if (wallet.type == WalletType.monero) return monero!.getUnspents(wallet); - if (wallet.type == WalletType.wownero) return wownero!.getUnspents(wallet); - if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type)) - return bitcoin!.getUnspents(wallet); - return List.empty(); + switch (wallet.type) { + case WalletType.monero: + return monero!.getUnspents(wallet); + case WalletType.wownero: + return wownero!.getUnspents(wallet); + case WalletType.bitcoin: + case WalletType.litecoin: + case WalletType.bitcoinCash: + return bitcoin!.getUnspents(wallet); + default: + return List.empty(); + } } @action @@ -97,10 +114,13 @@ abstract class UnspentCoinsListViewModelBase with Store { _items.clear(); List unspents = []; - _getUnspents().forEach((elem) { + _getUnspents().forEach((Unspent elem) { try { final info = getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout, elem.keyImage); + if (info == null) { + return; + } unspents.add(UnspentCoinsItem( address: elem.address, diff --git a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart index 2edda3d29..d365c8e00 100644 --- a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart @@ -78,6 +78,8 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { wallet, accountIndex: monero!.getCurrentAccount(wallet).id, label: label); + final addr = await monero!.getSubaddressList(wallet).subaddresses.first.address; // first because the order is reversed + wallet.walletAddresses.manualAddresses.add(addr); await wallet.save(); } @@ -88,6 +90,8 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { wallet, accountIndex: wownero!.getCurrentAccount(wallet).id, label: label); + final addr = await wownero!.getSubaddressList(wallet).subaddresses.first.address; // first because the order is reversed + wallet.walletAddresses.manualAddresses.add(addr); await wallet.save(); } diff --git a/lib/view_model/wallet_address_list/wallet_address_hidden_list_header.dart b/lib/view_model/wallet_address_list/wallet_address_hidden_list_header.dart new file mode 100644 index 000000000..74c7605e9 --- /dev/null +++ b/lib/view_model/wallet_address_list/wallet_address_hidden_list_header.dart @@ -0,0 +1,3 @@ +import 'package:cake_wallet/utils/list_item.dart'; + +class WalletAddressHiddenListHeader extends ListItem {} \ No newline at end of file diff --git a/lib/view_model/wallet_address_list/wallet_address_list_item.dart b/lib/view_model/wallet_address_list/wallet_address_list_item.dart index 6a6e34113..725b1ddbf 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_item.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_item.dart @@ -1,7 +1,7 @@ import 'package:cake_wallet/utils/list_item.dart'; class WalletAddressListItem extends ListItem { - const WalletAddressListItem({ + WalletAddressListItem({ required this.address, required this.isPrimary, this.id, @@ -11,6 +11,8 @@ class WalletAddressListItem extends ListItem { this.isChange = false, // Address that is only ever used once, shouldn't be used to receive funds, copy and paste, share etc this.isOneTimeReceiveAddress = false, + this.isHidden = false, + this.isManual = false, }) : super(); final int? id; @@ -20,6 +22,8 @@ class WalletAddressListItem extends ListItem { final int? txCount; final String? balance; final bool isChange; + bool isHidden; + bool isManual; final bool? isOneTimeReceiveAddress; @override 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 0bd936720..9fb7509eb 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,3 +1,5 @@ +import 'dart:math'; + import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; @@ -16,12 +18,16 @@ import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/utils/list_item.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_hidden_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/amount_converter.dart'; import 'package:cw_core/currency.dart'; +import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:cw_monero/api/wallet.dart'; +import 'package:flutter/foundation.dart'; import 'package:intl/intl.dart'; import 'package:mobx/mobx.dart'; @@ -217,8 +223,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo }) : _baseItems = [], selectedCurrency = walletTypeToCryptoCurrency(appStore.wallet!.type), _cryptoNumberFormat = NumberFormat(_cryptoNumberPattern), - hasAccounts = - appStore.wallet!.type == WalletType.monero || appStore.wallet!.type == WalletType.wownero || appStore.wallet!.type == WalletType.haven, + hasAccounts = appStore.wallet!.type == WalletType.monero || + appStore.wallet!.type == WalletType.wownero || + appStore.wallet!.type == WalletType.haven, amount = '', _settingsStore = appStore.settingsStore, super(appStore: appStore) { @@ -230,7 +237,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo _init(); selectedCurrency = walletTypeToCryptoCurrency(wallet.type); - hasAccounts = wallet.type == WalletType.monero || wallet.type == WalletType.wownero || wallet.type == WalletType.haven; + hasAccounts = wallet.type == WalletType.monero || + wallet.type == WalletType.wownero || + wallet.type == WalletType.haven; } static const String _cryptoNumberPattern = '0.00000000'; @@ -266,56 +275,40 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo WalletType get type => wallet.type; @computed - WalletAddressListItem get address => - WalletAddressListItem(address: wallet.walletAddresses.address, isPrimary: false); + WalletAddressListItem get address { + return WalletAddressListItem(address: wallet.walletAddresses.address, isPrimary: false); + } @computed PaymentURI get uri { - if (wallet.type == WalletType.monero) { - return MoneroURI(amount: amount, address: address.address); + switch (wallet.type) { + case WalletType.monero: + return MoneroURI(amount: amount, address: address.address); + case WalletType.haven: + return HavenURI(amount: amount, address: address.address); + case WalletType.bitcoin: + return BitcoinURI(amount: amount, address: address.address); + case WalletType.litecoin: + return LitecoinURI(amount: amount, address: address.address); + case WalletType.ethereum: + return EthereumURI(amount: amount, address: address.address); + case WalletType.bitcoinCash: + return BitcoinCashURI(amount: amount, address: address.address); + case WalletType.banano: + return NanoURI(amount: amount, address: address.address); + case WalletType.nano: + return NanoURI(amount: amount, address: address.address); + case WalletType.polygon: + return PolygonURI(amount: amount, address: address.address); + case WalletType.solana: + return SolanaURI(amount: amount, address: address.address); + case WalletType.tron: + return TronURI(amount: amount, address: address.address); + case WalletType.wownero: + return WowneroURI(amount: amount, address: address.address); + case WalletType.none: + throw Exception('Unexpected type: ${type.toString()}'); } - - if (wallet.type == WalletType.haven) { - return HavenURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.bitcoin) { - return BitcoinURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.litecoin) { - return LitecoinURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.ethereum) { - return EthereumURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.bitcoinCash) { - return BitcoinCashURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.nano) { - return NanoURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.polygon) { - return PolygonURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.solana) { - return SolanaURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.tron) { - return TronURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.wownero) { - return WowneroURI(amount: amount, address: address.address); - } - - throw Exception('Unexpected type: ${type.toString()}'); } @computed @@ -336,7 +329,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo id: subaddress.id, isPrimary: isPrimary, name: subaddress.label, - address: subaddress.address); + address: subaddress.address, + balance: subaddress.received, + txCount: subaddress.txCount, + ); }); addressList.addAll(addressItems); } @@ -404,7 +400,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo }); addressList.addAll(receivedAddressItems); } else { - final addressItems = bitcoin!.getSubAddresses(wallet).map((subaddress) { + var addressItems = bitcoin!.getSubAddresses(wallet).map((subaddress) { final isPrimary = subaddress.id == 0; return WalletAddressListItem( @@ -417,6 +413,16 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo walletTypeToCryptoCurrency(type), subaddress.balance), isChange: subaddress.isChange); }); + + // don't show all 1000+ mweb addresses: + if (wallet.type == WalletType.litecoin && addressItems.length >= 1000) { + // find the index of the last item with a txCount > 0 + final addressItemsList = addressItems.toList(); + final lastItemWithTxCount = addressItemsList.lastWhere((item) => (item.txCount ?? 0) > 0); + final index = addressItemsList.indexOf(lastItemWithTxCount); + // show only up to that index + 20: + addressItems = addressItemsList.sublist(0, index + 20); + } addressList.addAll(addressItems); } } @@ -446,13 +452,23 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo address: wallet.walletAddresses.address, )); } - + if (wallet.type == WalletType.tron) { final primaryAddress = tron!.getAddress(wallet); addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } + for (var i = 0; i < addressList.length; i++) { + if (!(addressList[i] is WalletAddressListItem)) continue; + (addressList[i] as WalletAddressListItem).isHidden = wallet.walletAddresses.hiddenAddresses.contains((addressList[i] as WalletAddressListItem).address); + } + + for (var i = 0; i < addressList.length; i++) { + if (!(addressList[i] is WalletAddressListItem)) continue; + (addressList[i] as WalletAddressListItem).isManual = wallet.walletAddresses.manualAddresses.contains((addressList[i] as WalletAddressListItem).address); + } + if (searchText.isNotEmpty) { return ObservableList.of(addressList.where((item) { if (item is WalletAddressListItem) { @@ -464,7 +480,21 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo return addressList; } - + Future toggleHideAddress(WalletAddressListItem item) async { + if (item.isHidden) { + wallet.walletAddresses.hiddenAddresses.removeWhere((element) => element == item.address); + } else { + wallet.walletAddresses.hiddenAddresses.add(item.address); + } + await wallet.walletAddresses.saveAddressesInBox(); + if (wallet.type == WalletType.monero) { + monero!.getSubaddressList(wallet).update(wallet, accountIndex: monero!.getCurrentAccount(wallet).id); + } else if (wallet.type == WalletType.wownero) { + wownero!.getSubaddressList(wallet).update(wallet, accountIndex: wownero!.getCurrentAccount(wallet).id); + } else if (wallet.type == WalletType.haven) { + haven!.getSubaddressList(wallet).update(wallet, accountIndex: haven!.getCurrentAccount(wallet).id); + } + } @observable bool hasAccounts; @@ -500,6 +530,14 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo wallet.type == WalletType.litecoin || wallet.type == WalletType.bitcoinCash; + @computed + bool get isBalanceAvailable => isElectrumWallet; + + @computed + bool get isReceivedAvailable => + wallet.type == WalletType.monero || + wallet.type == WalletType.wownero; + @computed bool get isSilentPayments => wallet.type == WalletType.bitcoin && bitcoin!.hasSelectedSilentPayments(wallet); @@ -509,6 +547,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo _settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled && !isSilentPayments; + @computed + bool get showAddManualAddresses => + !isAutoGenerateSubaddressEnabled || + wallet.type == WalletType.monero || + wallet.type == WalletType.wownero; + List _baseItems; final YatStore yatStore; @@ -519,7 +563,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @action Future setAddressType(dynamic option) async { - if (wallet.type == WalletType.bitcoin) { + if (wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin) { await bitcoin!.setAddressType(wallet, option); } } @@ -527,13 +571,22 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo void _init() { _baseItems = []; - if (wallet.type == WalletType.monero || wallet.type == WalletType.wownero || wallet.type == WalletType.haven) { + if (wallet.walletAddresses.hiddenAddresses.isNotEmpty) { + _baseItems.add(WalletAddressHiddenListHeader()); + } + + if (wallet.type == WalletType.monero || + wallet.type == WalletType.wownero || + wallet.type == WalletType.haven) { _baseItems.add(WalletAccountListHeader()); } if (wallet.type != WalletType.nano && wallet.type != WalletType.banano) { _baseItems.add(WalletAddressListHeader()); } + if (wallet.isEnabledAutoGenerateSubaddress) { + wallet.walletAddresses.address = wallet.walletAddresses.latestAddress; + } } @action diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 7041fa382..9fce86712 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -5,8 +5,10 @@ import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/entities/generate_name.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/view_model/restore/restore_mode.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cw_core/pathForWallet.dart'; @@ -82,9 +84,22 @@ abstract class WalletCreationVMBase with Store { walletCreationService.checkIfExists(name); final dirPath = await pathForWalletDir(name: name, type: type); final path = await pathForWallet(name: name, type: type); - final credentials = restoreWallet != null - ? getCredentialsFromRestoredWallet(options, restoreWallet) - : getCredentials(options); + + WalletCredentials credentials; + if (restoreWallet != null) { + if (restoreWallet.restoreMode == WalletRestoreMode.seed && + options == null && + (type == WalletType.nano || + type == WalletType.bitcoin || + type == WalletType.litecoin)) { + final derivationInfo = await getDerivationInfo(restoreWallet); + options ??= {}; + options["derivationInfo"] = derivationInfo.first; + } + credentials = getCredentialsFromRestoredWallet(options, restoreWallet); + } else { + credentials = getCredentials(options); + } final walletInfo = WalletInfo.external( id: WalletBase.idFor(name, type), @@ -185,6 +200,30 @@ abstract class WalletCreationVMBase with Store { } } + Future> getDerivationInfo(RestoredWallet restoreWallet) async { + var list = []; + final walletType = restoreWallet.type; + var appStore = getIt.get(); + var node = appStore.settingsStore.getCurrentNode(walletType); + + switch (walletType) { + case WalletType.bitcoin: + case WalletType.litecoin: + return bitcoin!.getDerivationsFromMnemonic( + mnemonic: restoreWallet.mnemonicSeed!, + node: node, + ); + case WalletType.nano: + return nanoUtil!.getDerivationsFromMnemonic( + mnemonic: restoreWallet.mnemonicSeed!, + node: node, + ); + default: + break; + } + return list; + } + WalletCredentials getCredentials(dynamic options) => throw UnimplementedError(); Future process(WalletCredentials credentials) => throw UnimplementedError(); diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index 539f5c640..4a4fdcb19 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -22,7 +22,8 @@ abstract class WalletListViewModelBase with Store { this._walletManager, ) : wallets = ObservableList(), multiWalletGroups = ObservableList(), - singleWalletsList = ObservableList() { + singleWalletsList = ObservableList(), + expansionTileStateTrack = ObservableMap() { setOrderType(_appStore.settingsStore.walletListOrder); reaction((_) => _appStore.wallet, (_) => updateList()); updateList(); @@ -40,6 +41,18 @@ abstract class WalletListViewModelBase with Store { @observable ObservableList singleWalletsList; + @observable + ObservableMap expansionTileStateTrack; + + @action + void updateTileState(int index, bool isExpanded) { + if (expansionTileStateTrack.containsKey(index)) { + expansionTileStateTrack.update(index, (value) => isExpanded); + } else { + expansionTileStateTrack.addEntries({index: isExpanded}.entries); + } + } + @computed bool get shouldRequireTOTP2FAForAccessingWallet => _appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet; @@ -100,8 +113,8 @@ abstract class WalletListViewModelBase with Store { // delete all wallets from walletInfoSource: await _walletInfoSource.clear(); - // add wallets from wallets list in order of wallets list, by name: - for (WalletListItem wallet in wallets) { + // Reorder single wallets using the singleWalletsList + for (WalletListItem wallet in singleWalletsList) { for (int i = 0; i < walletInfoSourceCopy.length; i++) { if (walletInfoSourceCopy[i].name == wallet.name) { await _walletInfoSource.add(walletInfoSourceCopy[i]); @@ -111,6 +124,20 @@ abstract class WalletListViewModelBase with Store { } } + // Reorder wallets within multi-wallet groups + for (WalletGroup group in multiWalletGroups) { + for (WalletInfo walletInfo in group.wallets) { + for (int i = 0; i < walletInfoSourceCopy.length; i++) { + if (walletInfoSourceCopy[i].name == walletInfo.name) { + await _walletInfoSource.add(walletInfoSourceCopy[i]); + walletInfoSourceCopy.removeAt(i); + break; + } + } + } + } + + // Rebuild the list of wallets and groups updateList(); } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 0b4ee9415..86b3462ac 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import connectivity_plus +import cw_mweb import device_info_plus import devicelocale import flutter_inappwebview_macos @@ -21,6 +22,7 @@ import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) + CwMwebPlugin.register(with: registry.registrar(forPlugin: "CwMwebPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DevicelocalePlugin.register(with: registry.registrar(forPlugin: "DevicelocalePlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) diff --git a/model_generator.sh b/model_generator.sh index 8a1173f7d..293923d1e 100755 --- a/model_generator.sh +++ b/model_generator.sh @@ -11,4 +11,5 @@ cd cw_tron; flutter pub get; flutter packages pub run build_runner build --delet cd cw_wownero; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. cd cw_polygon; flutter pub get; cd .. cd cw_ethereum; flutter pub get; cd .. +cd cw_mweb && flutter pub get && cd .. flutter packages pub run build_runner build --delete-conflicting-outputs diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 98661f7c1..a2f19e596 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -97,10 +97,6 @@ dependencies: polyseed: ^0.0.6 nostr_tools: ^1.0.9 solana: ^0.30.1 - bitcoin_base: - git: - url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v7 ledger_flutter: ^1.0.1 hashlib: ^1.19.2 @@ -138,10 +134,12 @@ dependency_overrides: url: https://github.com/cake-tech/web3dart.git ref: cake flutter_secure_storage_platform_interface: 1.0.2 + protobuf: ^3.1.0 bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v7 + ref: cake-update-v8 + ffi: 2.1.0 flutter_icons: image_path: "assets/images/app_logo.png" diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 23a1304b2..28d0cfad7 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "سيؤدي هذا الإجراء إلى حذف هذه المحفظة. هل ترغب في الاستمرار؟", "confirm_fee_deduction": "تأكيد خصم الرسوم", "confirm_fee_deduction_content": "هل توافق على خصم الرسوم من الإخراج؟", + "confirm_passphrase": "تأكيد عبارة المرور", "confirm_sending": "تأكيد الإرسال", "confirm_silent_payments_switch_node": "العقدة الحالية لا تدعم المدفوعات الصامتة \\ ncake wallet سوف تتحول إلى عقدة متوافقة ، فقط للمسح الضوئي", "confirmations": "التأكيدات", @@ -362,6 +363,14 @@ "ledger_error_wrong_app": "يرجى التأكد", "ledger_please_enable_bluetooth": "يرجى تمكين البلوتوث للكشف عن دفتر الأستاذ الخاص بك", "light_theme": "فاتح", + "litecoin_enable_mweb_sync": "تمكين MWEB المسح الضوئي", + "litecoin_mweb": "mweb", + "litecoin_mweb_always_scan": "اضبط MWEB دائمًا على المسح الضوئي", + "litecoin_mweb_display_card": "عرض بطاقة mweb", + "litecoin_mweb_scanning": "MWEB المسح الضوئي", + "litecoin_mweb_settings": "إعدادات MWEB", + "litecoin_mweb_warning": "سيقوم استخدام MWEB في البداية بتنزيل ~ 600 ميجابايت من البيانات ، وقد يستغرق ما يصل إلى 30 دقيقة حسب سرعة الشبكة. سيتم تنزيل هذه البيانات الأولية مرة واحدة فقط وستكون متاحة لجميع محافظ Litecoin", + "litecoin_what_is_mweb": "ما هو MWEB؟", "live_fee_rates": "أسعار الرسوم المباشرة عبر API", "load_more": "تحميل المزيد", "loading_your_wallet": "يتم تحميل محفظتك", @@ -392,6 +401,8 @@ "monero_light_theme": " ضوء مونيرو", "moonpay_alert_text": "يجب أن تكون قيمة المبلغ أكبر من أو تساوي ${minAmount} ${fiatCurrency}", "more_options": "المزيد من الخيارات", + "mweb_confirmed": "أكد MWEB", + "mweb_unconfirmed": "غير مؤكد MWEB", "name": "ﻢﺳﺍ", "nano_current_rep": "الممثل الحالي", "nano_gpt_thanks_message": "شكرا لاستخدام nanogpt! تذكر أن تعود إلى المتصفح بعد اكتمال معاملتك!", @@ -452,6 +463,7 @@ "overwrite_amount": "تغير المبلغ", "pairingInvalidEvent": "ﺢﻟﺎﺻ ﺮﻴﻏ ﺙﺪﺣ ﻥﺍﺮﻗﺇ", "passphrase": "عبارة الممر (اختياري)", + "passphrases_doesnt_match": "لا تتطابق عبارات المرور ، يرجى المحاولة مرة أخرى", "password": "كلمة المرور", "paste": "لصق", "pause_wallet_creation": ".ﺎﻴًﻟﺎﺣ ﺎﺘًﻗﺆﻣ ﺔﻔﻗﻮﺘﻣ Haven Wallet ءﺎﺸﻧﺇ ﻰﻠﻋ ﺓﺭﺪﻘﻟﺍ", @@ -814,6 +826,7 @@ "trusted": "موثوق به", "tx_commit_exception_no_dust_on_change": "يتم رفض المعاملة مع هذا المبلغ. باستخدام هذه العملات المعدنية ، يمكنك إرسال ${min} دون تغيير أو ${max} الذي يعيد التغيير.", "tx_commit_failed": "فشل ارتكاب المعاملة. يرجى الاتصال بالدعم.", + "tx_commit_failed_no_peers": "فشل المعاملة في البث ، يرجى المحاولة مرة أخرى في ثانية أو نحو ذلك", "tx_invalid_input": "أنت تستخدم نوع الإدخال الخاطئ لهذا النوع من الدفع", "tx_no_dust_exception": "يتم رفض المعاملة عن طريق إرسال مبلغ صغير جدًا. يرجى محاولة زيادة المبلغ.", "tx_not_enough_inputs_exception": "لا يكفي المدخلات المتاحة. الرجاء تحديد المزيد تحت التحكم في العملة", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 860e9c3a7..b6a147cee 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Този портфейл ще бъде изтрит. Искате ли да продължите?", "confirm_fee_deduction": "Потвърдете приспадането на таксите", "confirm_fee_deduction_content": "Съгласни ли сте да приспадате таксата от продукцията?", + "confirm_passphrase": "Потвърдете парола", "confirm_sending": "Потвърждаване на изпращането", "confirm_silent_payments_switch_node": "Текущият ви възел не поддържа Silent Payments \\ Ncake Wallet ще премине към съвместим възел, само за сканиране", "confirmations": "потвърждения", @@ -362,6 +363,14 @@ "ledger_error_wrong_app": "Моля, уверете се, че сте отворили правилното приложение на вашата книга", "ledger_please_enable_bluetooth": "Моля, активирайте Bluetooth да открие вашата книга", "light_theme": "Светло", + "litecoin_enable_mweb_sync": "Активирайте сканирането на MWeb", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Задайте MWeb винаги сканиране", + "litecoin_mweb_display_card": "Показване на MWEB карта", + "litecoin_mweb_scanning": "Сканиране на MWEB", + "litecoin_mweb_settings": "Настройки на MWEB", + "litecoin_mweb_warning": "Използването на MWEB първоначално ще изтегли ~ 600MB данни и може да отнеме до 30 минути в зависимост от скоростта на мрежата. Тези първоначални данни ще изтеглят само веднъж и ще бъдат достъпни за всички портфейли Litecoin", + "litecoin_what_is_mweb": "Какво е MWEB?", "live_fee_rates": "Цени на таксите на живо чрез API", "load_more": "Зареди още", "loading_your_wallet": "Зареждане на портфейл", @@ -392,6 +401,8 @@ "monero_light_theme": "Лека тема Monero", "moonpay_alert_text": "Сумата трябва да бъде най-малко ${minAmount} ${fiatCurrency}", "more_options": "Още настройки", + "mweb_confirmed": "Потвърден MWeb", + "mweb_unconfirmed": "Непотвърден mweb", "name": "Име", "nano_current_rep": "Настоящ представител", "nano_gpt_thanks_message": "Благодаря, че използвахте Nanogpt! Не забравяйте да се върнете обратно към браузъра, след като транзакцията ви приключи!", @@ -452,6 +463,7 @@ "overwrite_amount": "Промени сума", "pairingInvalidEvent": "Невалидно събитие при сдвояване", "passphrase": "Passphrase (по избор)", + "passphrases_doesnt_match": "Пасифрази не съвпадат, моля, опитайте отново", "password": "Парола", "paste": "Поставяне", "pause_wallet_creation": "Възможността за създаване на Haven Wallet в момента е на пауза.", @@ -814,6 +826,7 @@ "trusted": "Надежден", "tx_commit_exception_no_dust_on_change": "Сделката се отхвърля с тази сума. С тези монети можете да изпратите ${min} без промяна или ${max}, която връща промяна.", "tx_commit_failed": "Компетацията на транзакцията не успя. Моля, свържете се с поддръжката.", + "tx_commit_failed_no_peers": "Сделката не успя да излъчи, моля, опитайте отново след секунда или така", "tx_invalid_input": "Използвате грешен тип вход за този тип плащане", "tx_no_dust_exception": "Сделката се отхвърля чрез изпращане на сума твърде малка. Моля, опитайте да увеличите сумата.", "tx_not_enough_inputs_exception": "Няма достатъчно налични входове. Моля, изберете повече под контрол на монети", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index f3e9cab4f..aa995b281 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Tato akce smaže tuto peněženku. Přejete si pokračovat?", "confirm_fee_deduction": "Potvrďte odpočet poplatků", "confirm_fee_deduction_content": "Souhlasíte s odečtením poplatku z výstupu?", + "confirm_passphrase": "Potvrďte přístupovou frázi", "confirm_sending": "Potvrdit odeslání", "confirm_silent_payments_switch_node": "Váš aktuální uzel nepodporuje tiché platby \\ Ncake peněženka se přepne na kompatibilní uzel, pouze pro skenování", "confirmations": "Potvrzení", @@ -362,6 +363,14 @@ "ledger_error_wrong_app": "Ujistěte se, že se na své knize otevřete správnou aplikaci", "ledger_please_enable_bluetooth": "Umožněte prosím Bluetooth detekovat vaši knihu", "light_theme": "Světlý", + "litecoin_enable_mweb_sync": "Povolit skenování MWeb", + "litecoin_mweb": "MWeb", + "litecoin_mweb_always_scan": "Nastavit MWeb vždy skenování", + "litecoin_mweb_display_card": "Zobrazit kartu MWeb", + "litecoin_mweb_scanning": "Skenování mWeb", + "litecoin_mweb_settings": "Nastavení mWeb", + "litecoin_mweb_warning": "Pomocí MWeb zpočátku stahuje ~ 600 MB dat a může trvat až 30 minut v závislosti na rychlosti sítě. Tato počáteční data si stáhnou pouze jednou a budou k dispozici pro všechny litecoinové peněženky", + "litecoin_what_is_mweb": "Co je Mweb?", "live_fee_rates": "Živé sazby poplatků prostřednictvím API", "load_more": "Načíst další", "loading_your_wallet": "Načítám peněženku", @@ -392,6 +401,8 @@ "monero_light_theme": "Světlé téma Monero", "moonpay_alert_text": "Částka musí být větší nebo rovna ${minAmount} ${fiatCurrency}", "more_options": "Více možností", + "mweb_confirmed": "Potvrzený mweb", + "mweb_unconfirmed": "Nepotvrzené mWeb", "name": "název", "nano_current_rep": "Současný zástupce", "nano_gpt_thanks_message": "Děkujeme za používání Nanogpt! Nezapomeňte se po dokončení transakce vydat zpět do prohlížeče!", @@ -452,6 +463,7 @@ "overwrite_amount": "Přepsat částku", "pairingInvalidEvent": "Neplatná událost párování", "passphrase": "Passphrase (volitelné)", + "passphrases_doesnt_match": "Passfrázy se neshodují, zkuste to znovu", "password": "Heslo", "paste": "Vložit", "pause_wallet_creation": "Možnost vytvářet Haven Wallet je momentálně pozastavena.", @@ -814,6 +826,7 @@ "trusted": "Důvěřovat", "tx_commit_exception_no_dust_on_change": "Transakce je zamítnuta s touto částkou. S těmito mincemi můžete odeslat ${min} bez změny nebo ${max}, které se vrátí změna.", "tx_commit_failed": "Transakce COMPORT selhala. Kontaktujte prosím podporu.", + "tx_commit_failed_no_peers": "Transakce se nepodařilo vysílat, zkuste to prosím znovu za vteřinu", "tx_invalid_input": "Pro tento typ platby používáte nesprávný typ vstupu", "tx_no_dust_exception": "Transakce je zamítnuta odesláním příliš malé. Zkuste prosím zvýšit částku.", "tx_not_enough_inputs_exception": "Není k dispozici dostatek vstupů. Vyberte prosím více pod kontrolou mincí", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 80cc11ff0..4f9f8e895 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Diese Aktion löscht diese Wallet. Möchten Sie fortfahren?", "confirm_fee_deduction": "Gebührenabzug bestätigen", "confirm_fee_deduction_content": "Stimmen Sie zu, die Gebühr von der Ausgabe abzuziehen?", + "confirm_passphrase": "Passphrase bestätigen", "confirm_sending": "Senden bestätigen", "confirm_silent_payments_switch_node": "Ihr aktueller Knoten unterstützt keine stillen Zahlungen \\ NCAKE Wallet wechselt zu einem kompatiblen Knoten, nur zum Scannen", "confirmations": "Bestätigungen", @@ -362,6 +363,14 @@ "ledger_error_wrong_app": "Bitte stellen Sie sicher, dass Sie die richtige App auf Ihrem Ledger geöffnet haben", "ledger_please_enable_bluetooth": "Bitte aktivieren Sie Bluetooth um sich mit Ihren Ledger zu verbinden.", "light_theme": "Hell", + "litecoin_enable_mweb_sync": "Aktivieren Sie das MWEB -Scannen", + "litecoin_mweb": "MWeb", + "litecoin_mweb_always_scan": "Setzen Sie MWeb immer scannen", + "litecoin_mweb_display_card": "MWEB -Karte anzeigen", + "litecoin_mweb_scanning": "MWEB Scanning", + "litecoin_mweb_settings": "MWEB -Einstellungen", + "litecoin_mweb_warning": "Durch die Verwendung von MWEB wird zunächst ~ 600 MB Daten heruntergeladen und kann je nach Netzwerkgeschwindigkeit bis zu 30 Minuten dauern. Diese ersten Daten werden nur einmal heruntergeladen und für alle Litecoin -Brieftaschen verfügbar", + "litecoin_what_is_mweb": "Was ist MWeb?", "live_fee_rates": "Live -Gebührenpreise über API", "load_more": "Mehr laden", "loading_your_wallet": "Wallet wird geladen", @@ -392,6 +401,8 @@ "monero_light_theme": "Monero Light-Thema", "moonpay_alert_text": "Der Wert des Betrags muss größer oder gleich ${minAmount} ${fiatCurrency} sein", "more_options": "Weitere Optionen", + "mweb_confirmed": "Bestätigt MWeb", + "mweb_unconfirmed": "Unbestätigter MWeb", "name": "Name", "nano_current_rep": "Aktueller Vertreter", "nano_gpt_thanks_message": "Danke, dass du Nanogpt benutzt hast! Denken Sie daran, nach Abschluss Ihrer Transaktion zurück zum Browser zu gehen!", @@ -452,6 +463,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Paarung ungültiges Ereignis", "passphrase": "Passphrase (optional)", + "passphrases_doesnt_match": "Passphrasen stimmen nicht überein, bitte versuchen Sie es erneut", "password": "Passwort", "paste": "Einfügen", "pause_wallet_creation": "Die Möglichkeit, Haven Wallet zu erstellen, ist derzeit pausiert.", @@ -466,8 +478,8 @@ "placeholder_transactions": "Ihre Transaktionen werden hier angezeigt", "please_fill_totp": "Bitte geben Sie den 8-stelligen Code ein, der auf Ihrem anderen Gerät vorhanden ist", "please_make_selection": "Bitte treffen Sie unten eine Auswahl zum Erstellen oder Wiederherstellen Ihrer Wallet.", - "Please_reference_document": "Weitere Informationen finden Sie in den Dokumenten unten.", "please_reference_document": "Bitte verweisen Sie auf die folgenden Dokumente, um weitere Informationen zu erhalten.", + "Please_reference_document": "Weitere Informationen finden Sie in den Dokumenten unten.", "please_select": "Bitte auswählen:", "please_select_backup_file": "Bitte wählen Sie die Sicherungsdatei und geben Sie das Sicherungskennwort ein.", "please_try_to_connect_to_another_node": "Bitte versuchen Sie, sich mit einem anderen Knoten zu verbinden", @@ -815,6 +827,7 @@ "trusted": "Vertrauenswürdige", "tx_commit_exception_no_dust_on_change": "Die Transaktion wird diesen Betrag abgelehnt. Mit diesen Münzen können Sie ${min} ohne Veränderung oder ${max} senden, die Änderungen zurückgeben.", "tx_commit_failed": "Transaktionsausschüsse ist fehlgeschlagen. Bitte wenden Sie sich an Support.", + "tx_commit_failed_no_peers": "Transaktion konnte nicht übertragen werden. Bitte versuchen Sie es in einer Sekunde oder so erneut", "tx_invalid_input": "Sie verwenden den falschen Eingangstyp für diese Art von Zahlung", "tx_no_dust_exception": "Die Transaktion wird abgelehnt, indem eine Menge zu klein gesendet wird. Bitte versuchen Sie, die Menge zu erhöhen.", "tx_not_enough_inputs_exception": "Nicht genügend Eingänge verfügbar. Bitte wählen Sie mehr unter Münzkontrolle aus", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 636ca0f92..fadf1527b 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -143,8 +143,9 @@ "confirm_delete_wallet": "This action will delete this wallet. Do you wish to continue?", "confirm_fee_deduction": "Confirm Fee Deduction", "confirm_fee_deduction_content": "Do you agree to deduct the fee from the output?", + "confirm_passphrase": "Confirm passphrase", "confirm_sending": "Confirm sending", - "confirm_silent_payments_switch_node": "Your current node does not support silent payments\\nCake Wallet will switch to a compatible node, just for scanning", + "confirm_silent_payments_switch_node": "Your current node does not support Silent Payments.\\n\\nCake Wallet will switch to a compatible node while scanning.", "confirmations": "Confirmations", "confirmed": "Confirmed Balance", "confirmed_tx": "Confirmed", @@ -235,7 +236,7 @@ "email_address": "Email Address", "enable_mempool_api": "Mempool API for accurate fees and dates", "enable_replace_by_fee": "Enable Replace-By-Fee", - "enable_silent_payments_scanning": "Start scanning silent payments, until the tip is reached", + "enable_silent_payments_scanning": "Start scanning for transactions sent to your Silent Payment address.", "enabled": "Enabled", "enter_amount": "Enter Amount", "enter_backup_password": "Enter backup password here", @@ -297,8 +298,8 @@ "failed_authentication": "Failed authentication. ${state_error}", "faq": "FAQ", "features": "Features", - "fee_rate": "Fee rate", "fee_less_than_min": "Selected Fee is less than the minimum, please increase the fees to be able to send the transaction", + "fee_rate": "Fee rate", "fetching": "Fetching", "fiat_api": "Fiat API", "fiat_balance": "Fiat Balance", @@ -332,6 +333,8 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "help", + "hide": "Hide", + "hidden_addresses": "Hidden Addresses", "hidden_balance": "Hidden Balance", "hide_details": "Hide Details", "high_contrast_theme": "High Contrast Theme", @@ -362,6 +365,14 @@ "ledger_error_wrong_app": "Please make sure you opend the right app on your ledger", "ledger_please_enable_bluetooth": "Please enable Bluetooth to detect your Ledger", "light_theme": "Light", + "litecoin_enable_mweb_sync": "Enable MWEB scanning", + "litecoin_mweb": "MWEB", + "litecoin_mweb_always_scan": "Set MWEB always scanning", + "litecoin_mweb_display_card": "Show MWEB card", + "litecoin_mweb_scanning": "MWEB Scanning", + "litecoin_mweb_settings": "MWEB settings", + "litecoin_mweb_warning": "Using MWEB will initially download ~600MB of data, and may take up to 30 minutes depending on network speed. This initial data will only download once and be available for all Litecoin wallets", + "litecoin_what_is_mweb": "What is MWEB?", "live_fee_rates": "Live fee rates via API", "load_more": "Load more", "loading_your_wallet": "Loading your wallet", @@ -392,6 +403,8 @@ "monero_light_theme": "Monero Light Theme", "moonpay_alert_text": "Value of the amount must be more or equal to ${minAmount} ${fiatCurrency}", "more_options": "More Options", + "mweb_confirmed": "Confirmed MWEB", + "mweb_unconfirmed": "Unconfirmed MWEB", "name": "Name", "nano_current_rep": "Current Representative", "nano_gpt_thanks_message": "Thanks for using NanoGPT! Remember to head back to the browser after your transaction completes!", @@ -452,6 +465,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Pairing Invalid Event", "passphrase": "Passphrase (Optional)", + "passphrases_doesnt_match": "Passphrases do not match, please try again", "password": "Password", "paste": "Paste", "pause_wallet_creation": "Ability to create Haven Wallet is currently paused.", @@ -673,6 +687,7 @@ "setup_your_debit_card": "Set up your debit card", "share": "Share", "share_address": "Share address", + "show": "Show", "shared_seed_wallet_groups": "Shared Seed Wallet Groups", "show_details": "Show Details", "show_keys": "Show seed/keys", @@ -693,7 +708,7 @@ "silent_payments_disclaimer": "New addresses are not new identities. It is a re-use of an existing identity with a different label.", "silent_payments_display_card": "Show Silent Payments card", "silent_payments_scan_from_date": "Scan from date", - "silent_payments_scan_from_date_or_blockheight": "Please enter the block height you want to start scanning for incoming silent payments, or, use the date instead. You can choose if the wallet continues scanning every block, or checks only the specified height.", + "silent_payments_scan_from_date_or_blockheight": "Please enter the block height you want to start scanning for incoming Silent Payments or use the date instead. You can choose if the wallet continues scanning every block, or checks only the specified height.", "silent_payments_scan_from_height": "Scan from block height", "silent_payments_scanned_tip": "SCANNED TO TIP! (${tip})", "silent_payments_scanning": "Silent Payments Scanning", @@ -814,6 +829,7 @@ "trusted": "Trusted", "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 or so", "tx_invalid_input": "You are using the wrong input type for this type of payment", "tx_no_dust_exception": "The transaction is rejected by sending an amount too small. Please try increasing the amount.", "tx_not_enough_inputs_exception": "Not enough inputs available. Please select more under Coin Control", @@ -891,7 +907,7 @@ "warning": "Warning", "welcome": "Welcome to", "welcome_to_cakepay": "Welcome to Cake Pay!", - "what_is_silent_payments": "What is silent payments?", + "what_is_silent_payments": "What are Silent Payments?", "widgets_address": "Address", "widgets_or": "or", "widgets_restore_from_blockheight": "Restore from blockheight", @@ -920,4 +936,4 @@ "you_will_get": "Convert to", "you_will_send": "Convert from", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 2f54cdc32..8ef17f8b7 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Esta acción eliminará esta billetera. ¿Desea continuar?", "confirm_fee_deduction": "Confirmar la deducción de la tarifa", "confirm_fee_deduction_content": "¿Acepta deducir la tarifa de la producción?", + "confirm_passphrase": "Confirmar la frase de pases", "confirm_sending": "Confirmar envío", "confirm_silent_payments_switch_node": "Su nodo actual no admite pagos silenciosos \\ ncake billet cambiará a un nodo compatible, solo para escanear", "confirmations": "Confirmaciones", @@ -362,6 +363,14 @@ "ledger_error_wrong_app": "Por favor, asegúrese de abrir la aplicación correcta en su libro mayor.", "ledger_please_enable_bluetooth": "Habilite Bluetooth para detectar su libro mayor", "light_theme": "Ligera", + "litecoin_enable_mweb_sync": "Habilitar el escaneo mweb", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Establecer mweb siempre escaneo", + "litecoin_mweb_display_card": "Mostrar tarjeta MWEB", + "litecoin_mweb_scanning": "Escaneo mweb", + "litecoin_mweb_settings": "Configuración de MWEB", + "litecoin_mweb_warning": "El uso de MWEB inicialmente descargará ~ 600 MB de datos, y puede tomar hasta 30 minutos según la velocidad de la red. Estos datos iniciales solo se descargarán una vez y estarán disponibles para todas las billeteras de Litecoin", + "litecoin_what_is_mweb": "¿Qué es mweb?", "live_fee_rates": "Tasas de tarifas en vivo a través de API", "load_more": "Carga más", "loading_your_wallet": "Cargando tu billetera", @@ -392,6 +401,8 @@ "monero_light_theme": "Tema ligero de Monero", "moonpay_alert_text": "El valor de la cantidad debe ser mayor o igual a ${minAmount} ${fiatCurrency}", "more_options": "Más Opciones", + "mweb_confirmed": "Confirmado mweb", + "mweb_unconfirmed": "Mweb no confirmado", "name": "Nombre", "nano_current_rep": "Representante actual", "nano_gpt_thanks_message": "¡Gracias por usar nanogpt! ¡Recuerde regresar al navegador después de que se complete su transacción!", @@ -452,6 +463,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Evento de emparejamiento no válido", "passphrase": "Passfrase (opcional)", + "passphrases_doesnt_match": "Las frases de contrato no coinciden, intente nuevamente", "password": "Contraseña", "paste": "Pegar", "pause_wallet_creation": "La capacidad para crear Haven Wallet está actualmente pausada.", @@ -815,6 +827,7 @@ "trusted": "de confianza", "tx_commit_exception_no_dust_on_change": "La transacción se rechaza con esta cantidad. Con estas monedas puede enviar ${min} sin cambios o ${max} que devuelve el cambio.", "tx_commit_failed": "La confirmación de transacción falló. Póngase en contacto con el soporte.", + "tx_commit_failed_no_peers": "La transacción no se transmitió, intente nuevamente en un segundo más o menos", "tx_invalid_input": "Está utilizando el tipo de entrada incorrecto para este tipo de pago", "tx_no_dust_exception": "La transacción se rechaza enviando una cantidad demasiado pequeña. Intente aumentar la cantidad.", "tx_not_enough_inputs_exception": "No hay suficientes entradas disponibles. Seleccione más bajo control de monedas", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 301abf940..b73779f94 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Cette action va supprimer ce portefeuille (wallet). Souhaitez-vous contnuer ?", "confirm_fee_deduction": "Confirmer la déduction des frais", "confirm_fee_deduction_content": "Acceptez-vous de déduire les frais de la production?", + "confirm_passphrase": "Confirmer la phrase passante", "confirm_sending": "Confirmer l'envoi", "confirm_silent_payments_switch_node": "Votre nœud actuel ne prend pas en charge les paiements silencieux \\ ncake qui passera à un nœud compatible, juste pour la numérisation", "confirmations": "Confirmations", @@ -362,6 +363,14 @@ "ledger_error_wrong_app": "Veuillez vous assurer d'ouvrir la bonne application sur votre grand livre", "ledger_please_enable_bluetooth": "Veuillez activer Bluetooth pour détecter votre grand livre", "light_theme": "Clair", + "litecoin_enable_mweb_sync": "Activer la numérisation MWEB", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Définir MWEB Score Scanning", + "litecoin_mweb_display_card": "Afficher la carte MWeb", + "litecoin_mweb_scanning": "Scann mweb", + "litecoin_mweb_settings": "Paramètres MWEB", + "litecoin_mweb_warning": "L'utilisation de MWEB téléchargera initialement ~ 600 Mo de données et peut prendre jusqu'à 30 minutes en fonction de la vitesse du réseau. Ces données initiales ne téléchargeront qu'une seule fois et seront disponibles pour tous les portefeuilles litecoin", + "litecoin_what_is_mweb": "Qu'est-ce que MWEB?", "live_fee_rates": "Taux de frais en direct via l'API", "load_more": "Charger plus", "loading_your_wallet": "Chargement de votre portefeuille (wallet)", @@ -392,6 +401,8 @@ "monero_light_theme": "Thème de lumière Monero", "moonpay_alert_text": "Le montant doit être au moins égal à ${minAmount} ${fiatCurrency}", "more_options": "Plus d'options", + "mweb_confirmed": "Confirmé MWEB", + "mweb_unconfirmed": "Mweb non confirmé", "name": "Nom", "nano_current_rep": "Représentant actuel", "nano_gpt_thanks_message": "Merci d'avoir utilisé Nanogpt! N'oubliez pas de retourner au navigateur une fois votre transaction terminée!", @@ -452,6 +463,7 @@ "overwrite_amount": "Remplacer le montant", "pairingInvalidEvent": "Événement de couplage non valide", "passphrase": "Phrase de passe (facultative)", + "passphrases_doesnt_match": "Les phrases de passe ne correspondent pas, veuillez réessayer", "password": "Mot de passe", "paste": "Coller", "pause_wallet_creation": "La possibilité de créer Haven Wallet est actuellement suspendue.", @@ -814,6 +826,7 @@ "trusted": "de confiance", "tx_commit_exception_no_dust_on_change": "La transaction est rejetée avec ce montant. Avec ces pièces, vous pouvez envoyer ${min} sans changement ou ${max} qui renvoie le changement.", "tx_commit_failed": "La validation de la transaction a échoué. Veuillez contacter l'assistance.", + "tx_commit_failed_no_peers": "La transaction n'a pas été diffusée, veuillez réessayer dans une seconde environ", "tx_invalid_input": "Vous utilisez le mauvais type d'entrée pour ce type de paiement", "tx_no_dust_exception": "La transaction est rejetée en envoyant un montant trop faible. Veuillez essayer d'augmenter le montant.", "tx_not_enough_inputs_exception": "Pas assez d'entrées disponibles. Veuillez sélectionner plus sous Control Control", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 027379683..fc88b6036 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Wannan aikin zai share wannan walat. Kuna so ku ci gaba?", "confirm_fee_deduction": "Tabbatar da cire kudade", "confirm_fee_deduction_content": "Shin kun yarda ku cire kuɗin daga fitarwa?", + "confirm_passphrase": "Tabbatar da kalmar wucewa", "confirm_sending": "Tabbatar da aikawa", "confirm_silent_payments_switch_node": "Kumburinku na yanzu ba ya goyan bayan biyan shiru da shiru \\ NCADA Wallet zai canza zuwa kumburi mai dacewa, don bincika", "confirmations": "Tabbatar", @@ -362,6 +363,14 @@ "ledger_error_wrong_app": "Da fatan za a tabbata kun yi amfani da app ɗin dama akan dillalarku", "ledger_please_enable_bluetooth": "Da fatan za a kunna Bluetooth don gano Ledger ɗinku", "light_theme": "Haske", + "litecoin_enable_mweb_sync": "Kunna binciken Mweb", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Saita Mweb koyaushe", + "litecoin_mweb_display_card": "Nuna katin Mweb", + "litecoin_mweb_scanning": "Mweb scanning", + "litecoin_mweb_settings": "Saitunan Mweb", + "litecoin_mweb_warning": "Amfani da Mweb zai fara saukewa ~ 600MB na bayanai, kuma yana iya ɗaukar minti 30 dangane da saurin cibiyar sadarwa. Wannan bayanan farko zai saika saukarwa sau ɗaya kawai kuma a samu don duk wuraren shakatawa", + "litecoin_what_is_mweb": "Menene Mweb?", "live_fee_rates": "Kudin Kiɗa ta API", "load_more": "Like more", "loading_your_wallet": "Ana loda walat ɗin ku", @@ -392,6 +401,8 @@ "monero_light_theme": "Jigon Hasken Monero", "moonpay_alert_text": "Darajar adadin dole ne ya zama fiye ko daidai da ${minAmount} ${fiatCurrency}", "more_options": "Ƙarin Zaɓuɓɓuka", + "mweb_confirmed": "Tabbatar da Mweb", + "mweb_unconfirmed": "Myconfired", "name": "Suna", "nano_current_rep": "Wakilin Yanzu", "nano_gpt_thanks_message": "Na gode da amfani da Nanogpt! Ka tuna da komawa zuwa mai bincike bayan ma'amalar ka ta cika!", @@ -454,6 +465,7 @@ "overwrite_amount": "Rubuta adadin", "pairingInvalidEvent": "Haɗa Lamarin mara inganci", "passphrase": "Passphrase (Zabi)", + "passphrases_doesnt_match": "Passphrases bai dace ba, don Allah sake gwadawa", "password": "Kalmar wucewa", "paste": "Manna", "pause_wallet_creation": "A halin yanzu an dakatar da ikon ƙirƙirar Haven Wallet.", @@ -816,6 +828,7 @@ "trusted": "Amintacce", "tx_commit_exception_no_dust_on_change": "An ƙi ma'amala da wannan adadin. Tare da waɗannan tsabar kudi Zaka iya aika ${min}, ba tare da canji ba ko ${max} wanda ya dawo canzawa.", "tx_commit_failed": "Ma'amala ya kasa. Da fatan za a tuntuɓi goyan baya.", + "tx_commit_failed_no_peers": "Kasuwanci ya kasa watsa, don Allah sake gwadawa a cikin na biyu ko", "tx_invalid_input": "Kuna amfani da nau'in shigar da ba daidai ba don wannan nau'in biyan kuɗi", "tx_no_dust_exception": "An ƙi ma'amala ta hanyar aika adadin ƙarami. Da fatan za a gwada ƙara adadin.", "tx_not_enough_inputs_exception": "Bai isa ba hanyoyin da ake samu. Da fatan za selectiari a karkashin Kwarewar Coin", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 34bed6092..6392f65cf 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "यह क्रिया इस वॉलेट को हटा देगी। क्या आप जारी रखना चाहते हैं?", "confirm_fee_deduction": "शुल्क कटौती की पुष्टि करें", "confirm_fee_deduction_content": "क्या आप आउटपुट से शुल्क में कटौती करने के लिए सहमत हैं?", + "confirm_passphrase": "पासफ़्रेज़ की पुष्टि करें", "confirm_sending": "भेजने की पुष्टि करें", "confirm_silent_payments_switch_node": "आपका वर्तमान नोड मूक भुगतान का समर्थन नहीं करता है \\ ncake वॉलेट एक संगत नोड पर स्विच करेगा, बस स्कैनिंग के लिए", "confirmations": "पुष्टिकरण", @@ -362,6 +363,14 @@ "ledger_error_wrong_app": "कृपया सुनिश्चित करें कि आप अपने लेजर पर सही ऐप को खोलते हैं", "ledger_please_enable_bluetooth": "कृपया अपने बहीखाने का पता लगाने के लिए ब्लूटूथ को सक्षम करें", "light_theme": "रोशनी", + "litecoin_enable_mweb_sync": "MWEB स्कैनिंग सक्षम करें", + "litecoin_mweb": "मावली", + "litecoin_mweb_always_scan": "MWEB हमेशा स्कैनिंग सेट करें", + "litecoin_mweb_display_card": "MWEB कार्ड दिखाएं", + "litecoin_mweb_scanning": "MWEB स्कैनिंग", + "litecoin_mweb_settings": "MWEB सेटिंग्स", + "litecoin_mweb_warning": "MWEB का उपयोग शुरू में ~ 600MB डेटा डाउनलोड करेगा, और नेटवर्क की गति के आधार पर 30 मिनट तक का समय लग सकता है। यह प्रारंभिक डेटा केवल एक बार डाउनलोड करेगा और सभी लिटकोइन वॉलेट के लिए उपलब्ध होगा", + "litecoin_what_is_mweb": "MWEB क्या है?", "live_fee_rates": "एपीआई के माध्यम से लाइव शुल्क दरें", "load_more": "और लोड करें", "loading_your_wallet": "अपना बटुआ लोड कर रहा है", @@ -392,6 +401,8 @@ "monero_light_theme": "मोनेरो लाइट थीम", "moonpay_alert_text": "राशि का मूल्य अधिक है या करने के लिए बराबर होना चाहिए ${minAmount} ${fiatCurrency}", "more_options": "और विकल्प", + "mweb_confirmed": "MWEB की पुष्टि की", + "mweb_unconfirmed": "अपुष्ट MWEB", "name": "नाम", "nano_current_rep": "वर्तमान प्रतिनिधि", "nano_gpt_thanks_message": "Nanogpt का उपयोग करने के लिए धन्यवाद! अपने लेन -देन के पूरा होने के बाद ब्राउज़र पर वापस जाना याद रखें!", @@ -452,6 +463,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "अमान्य ईवेंट युग्मित करना", "passphrase": "पासफ्रेज़ (वैकल्पिक)", + "passphrases_doesnt_match": "PassPhrases मेल नहीं खाता, कृपया पुनः प्रयास करें", "password": "पारण शब्द", "paste": "पेस्ट करें", "pause_wallet_creation": "हेवन वॉलेट बनाने की क्षमता फिलहाल रुकी हुई है।", @@ -816,6 +828,7 @@ "trusted": "भरोसा", "tx_commit_exception_no_dust_on_change": "लेनदेन को इस राशि से खारिज कर दिया जाता है। इन सिक्कों के साथ आप चेंज या ${min} के बिना ${max} को भेज सकते हैं जो परिवर्तन लौटाता है।", "tx_commit_failed": "लेन -देन प्रतिबद्ध विफल। कृपया संपर्क समर्थन करें।", + "tx_commit_failed_no_peers": "लेन -देन प्रसारित करने में विफल रहा, कृपया एक या दो सेकंड में पुनः प्रयास करें", "tx_invalid_input": "आप इस प्रकार के भुगतान के लिए गलत इनपुट प्रकार का उपयोग कर रहे हैं", "tx_no_dust_exception": "लेनदेन को बहुत छोटी राशि भेजकर अस्वीकार कर दिया जाता है। कृपया राशि बढ़ाने का प्रयास करें।", "tx_not_enough_inputs_exception": "पर्याप्त इनपुट उपलब्ध नहीं है। कृपया सिक्का नियंत्रण के तहत अधिक चुनें", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 0632c586b..99fa0534c 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Ovom ćete radnjom izbrisati ovaj novčanik. Želite li nastaviti?", "confirm_fee_deduction": "Potvrdite odbitak naknade", "confirm_fee_deduction_content": "Slažete li se da ćete odbiti naknadu od izlaza?", + "confirm_passphrase": "Potvrdite prolaznu frazu", "confirm_sending": "Potvrdi slanje", "confirm_silent_payments_switch_node": "Vaš trenutni čvor ne podržava tiha plaćanja \\ ncake novčanik prebacit će se na kompatibilni čvor, samo za skeniranje", "confirmations": "Potvrde", @@ -362,6 +363,14 @@ "ledger_error_wrong_app": "Obavezno obavezno otvorite pravu aplikaciju na knjizi", "ledger_please_enable_bluetooth": "Omogućite Bluetooth da otkrije svoju knjigu", "light_theme": "Svijetla", + "litecoin_enable_mweb_sync": "Omogućite MWEB skeniranje", + "litecoin_mweb": "MWeb", + "litecoin_mweb_always_scan": "Postavite MWeb uvijek skeniranje", + "litecoin_mweb_display_card": "Prikaži MWeb karticu", + "litecoin_mweb_scanning": "MWEB skeniranje", + "litecoin_mweb_settings": "Postavke MWEB -a", + "litecoin_mweb_warning": "Korištenje MWEB -a u početku će preuzeti ~ 600MB podataka, a može potrajati do 30 minuta, ovisno o brzini mreže. Ovi početni podaci preuzet će samo jednom i biti dostupni za sve Litecoin novčanike", + "litecoin_what_is_mweb": "Što je MWEB?", "live_fee_rates": "Stope naknada uživo putem API -ja", "load_more": "Učitaj više", "loading_your_wallet": "Novčanik se učitava", @@ -392,6 +401,8 @@ "monero_light_theme": "Monero lagana tema", "moonpay_alert_text": "Vrijednost iznosa mora biti veća ili jednaka ${minAmount} ${fiatCurrency}", "more_options": "Više opcija", + "mweb_confirmed": "Potvrđen MWeb", + "mweb_unconfirmed": "Nepotvrđeni mWeb", "name": "Ime", "nano_current_rep": "Trenutni predstavnik", "nano_gpt_thanks_message": "Hvala što ste koristili nanogpt! Ne zaboravite da se vratite u preglednik nakon što vam se transakcija završi!", @@ -452,6 +463,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Nevažeći događaj uparivanja", "passphrase": "Prolaznica (neobavezno)", + "passphrases_doesnt_match": "Prolazne fraze se ne podudaraju, pokušajte ponovo", "password": "Lozinka", "paste": "Zalijepi", "pause_wallet_creation": "Mogućnost stvaranja novčanika Haven trenutno je pauzirana.", @@ -814,6 +826,7 @@ "trusted": "vjerovao", "tx_commit_exception_no_dust_on_change": "Transakcija se odbija s tim iznosom. Pomoću ovih kovanica možete poslati ${min} bez promjene ili ${max} koja vraća promjenu.", "tx_commit_failed": "Obveza transakcije nije uspjela. Molimo kontaktirajte podršku.", + "tx_commit_failed_no_peers": "Transakcija nije uspjela emitirati, pokušajte ponovo u sekundi ili tako", "tx_invalid_input": "Koristite pogrešnu vrstu ulaza za ovu vrstu plaćanja", "tx_no_dust_exception": "Transakcija se odbija slanjem iznosa premalo. Pokušajte povećati iznos.", "tx_not_enough_inputs_exception": "Nema dovoljno unosa. Molimo odaberite više pod kontrolom novčića", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 8c65817c2..3d37a0a85 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Այս գործողությունը կջնջի այս դրամապանակը: Ցանկանու՞մ եք շարունակել։", "confirm_fee_deduction": "Հաստատեք միջնորդավճար հանումը", "confirm_fee_deduction_content": "Դուք համաձայն եք միջնորդավճար հանել արդյունքից?", + "confirm_passphrase": "Հաստատեք գաղտնաբառը", "confirm_sending": "Հաստատեք ուղարկումը", "confirm_silent_payments_switch_node": "Ձեր ընթացիկ հանգույցը չի աջակցում Լուռ վճարումներին\nCake Wallet-ը կանցնի համատեղելի հանգույց, միայն սկանավորման համար", "confirmations": "Հաստատումներ", @@ -392,6 +393,8 @@ "monero_light_theme": "Monero պայծառ տեսք", "moonpay_alert_text": "Գումարի արժեքը պետք է լինի հավասար կամ ավելի քան ${minAmount} ${fiatCurrency}", "more_options": "Այլ տարբերակներ", + "mweb_confirmed": "Հաստատված MWEB", + "mweb_unconfirmed": "Չկարգավորված Mweb", "name": "Անուն", "nano_current_rep": "Ընթացիկ ներկայացուցիչ", "nano_gpt_thanks_message": "Շնորհակալություն NanoGPT-ն օգտագործելու համար: Հիշեք վերադառնալ դիտարկիչ ձեր փոխանցումն ավարտելուց հետո", @@ -451,6 +454,7 @@ "overwrite_amount": "Գրեք գումարը", "pairingInvalidEvent": "Սխալ միացում", "passphrase": "Պարող արտահայտություն (Ոչ պարտադիր)", + "passphrases_doesnt_match": "Անհատները չեն համընկնում, խնդրում ենք կրկին փորձել", "password": "Գաղտնաբառ", "paste": "Տեղադրել", "pause_wallet_creation": "Հնարավորություն ստեղծել Haven Դրամապանակ ընթացիկ դադարեցված է", @@ -812,6 +816,7 @@ "trusted": "Վստահելի", "tx_commit_exception_no_dust_on_change": "Փոխանցումը մերժվել է այս գումարով: Այս արժույթներով կարող եք ուղարկել ${min} առանց փոփոխության կամ ${max} որը վերադարձնում է փոփոխությունը", "tx_commit_failed": "Փոխանցումը ձախողվել է: Խնդրում ենք դիմել աջակցությանը", + "tx_commit_failed_no_peers": "Գործարքը չի հաջողվել հեռարձակել, խնդրում ենք կրկին փորձել մեկ վայրկյանում", "tx_invalid_input": "Դուք օգտագործում եք սխալ մուտքային տիպ այս տեսակի վճարման համար", "tx_no_dust_exception": "Փոխանցումը մերժվել է շատ փոքր գումարով: Խնդրում ենք փորձել ավելացնել գումարը", "tx_not_enough_inputs_exception": "Չկան բավարար մուտքեր: Խնդրում ենք ընտրել ավելին Coin Control֊ում", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 23402ea5f..0bf910ff8 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Tindakan ini akan menghapus dompet ini. Apakah Anda ingin melanjutkan?", "confirm_fee_deduction": "Konfirmasi pengurangan biaya", "confirm_fee_deduction_content": "Apakah Anda setuju untuk mengurangi biaya dari output?", + "confirm_passphrase": "Konfirmasi frasa sandi", "confirm_sending": "Konfirmasi pengiriman", "confirm_silent_payments_switch_node": "Node Anda saat ini tidak mendukung pembayaran diam \\ ncake Wallet akan beralih ke simpul yang kompatibel, hanya untuk pemindaian", "confirmations": "Konfirmasi", @@ -362,6 +363,14 @@ "ledger_error_wrong_app": "Pastikan Anda membuka aplikasi yang tepat di buku besar Anda", "ledger_please_enable_bluetooth": "Harap aktifkan Bluetooth untuk mendeteksi buku besar Anda", "light_theme": "Terang", + "litecoin_enable_mweb_sync": "Aktifkan pemindaian MWEB", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Atur mWeb selalu memindai", + "litecoin_mweb_display_card": "Tunjukkan kartu mWeb", + "litecoin_mweb_scanning": "Pemindaian MWEB", + "litecoin_mweb_settings": "Pengaturan MWEB", + "litecoin_mweb_warning": "Menggunakan MWEB pada awalnya akan mengunduh ~ 600MB data, dan dapat memakan waktu hingga 30 menit tergantung pada kecepatan jaringan. Data awal ini hanya akan mengunduh sekali dan tersedia untuk semua dompet litecoin", + "litecoin_what_is_mweb": "Apa itu MWEB?", "live_fee_rates": "Tarif biaya langsung melalui API", "load_more": "Muat lebih banyak", "loading_your_wallet": "Memuat dompet Anda", @@ -392,6 +401,8 @@ "monero_light_theme": "Tema Cahaya Monero", "moonpay_alert_text": "Nilai jumlah harus lebih atau sama dengan ${minAmount} ${fiatCurrency}", "more_options": "Opsi Lainnya", + "mweb_confirmed": "Mengkonfirmasi mWeb", + "mweb_unconfirmed": "MWEB yang belum dikonfirmasi", "name": "Nama", "nano_current_rep": "Perwakilan saat ini", "nano_gpt_thanks_message": "Terima kasih telah menggunakan Nanogpt! Ingatlah untuk kembali ke browser setelah transaksi Anda selesai!", @@ -454,6 +465,7 @@ "overwrite_amount": "Timpa jumlah", "pairingInvalidEvent": "Menyandingkan Acara Tidak Valid", "passphrase": "Frasa sandi (opsional)", + "passphrases_doesnt_match": "Sandi tidak cocok, coba lagi", "password": "Kata Sandi", "paste": "Tempel", "pause_wallet_creation": "Kemampuan untuk membuat Haven Wallet saat ini dijeda.", @@ -817,6 +829,7 @@ "trusted": "Dipercayai", "tx_commit_exception_no_dust_on_change": "Transaksi ditolak dengan jumlah ini. Dengan koin ini Anda dapat mengirim ${min} tanpa perubahan atau ${max} yang mengembalikan perubahan.", "tx_commit_failed": "Transaksi Gagal. Silakan hubungi Dukungan.", + "tx_commit_failed_no_peers": "Transaksi gagal untuk disiarkan, silakan coba lagi sebentar lagi", "tx_invalid_input": "Anda menggunakan jenis input yang salah untuk jenis pembayaran ini", "tx_no_dust_exception": "Transaksi ditolak dengan mengirimkan jumlah yang terlalu kecil. Silakan coba tingkatkan jumlahnya.", "tx_not_enough_inputs_exception": "Tidak cukup input yang tersedia. Pilih lebih banyak lagi di bawah Kontrol Koin", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 496d61178..12f9bb2cd 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Questa azione cancellerà questo portafoglio. Desideri continuare?", "confirm_fee_deduction": "Conferma la detrazione delle commissioni", "confirm_fee_deduction_content": "Accetti di detrarre la commissione dall'output?", + "confirm_passphrase": "Conferma passphrase", "confirm_sending": "Conferma l'invio", "confirm_silent_payments_switch_node": "Il tuo nodo corrente non supporta i pagamenti silenziosi \\ ncake Wallet passerà a un nodo compatibile, solo per la scansione", "confirmations": "Conferme", @@ -363,6 +364,14 @@ "ledger_error_wrong_app": "Assicurati di aprire l'app giusta sul libro mastro", "ledger_please_enable_bluetooth": "Si prega di consentire al Bluetooth di rilevare il libro mastro", "light_theme": "Bianco", + "litecoin_enable_mweb_sync": "Abilita la scansione MWeb", + "litecoin_mweb": "MWeb", + "litecoin_mweb_always_scan": "Imposta MWeb per scansionare sempre", + "litecoin_mweb_display_card": "Mostra la scheda MWeb", + "litecoin_mweb_scanning": "Scansione MWeb", + "litecoin_mweb_settings": "Impostazioni MWeb", + "litecoin_mweb_warning": "L'uso di MWeb inizialmente scaricherà ~ 600 MB di dati e potrebbe richiedere fino a 30 minuti a seconda della velocità di rete. Questi dati iniziali scaricheranno solo una volta e saranno disponibili per tutti i portafogli Litecoin", + "litecoin_what_is_mweb": "Cos'è MWeb?", "live_fee_rates": "Tariffe delle commissioni dal vivo tramite API", "load_more": "Carica di più", "loading_your_wallet": "Caricamento portafoglio", @@ -393,6 +402,8 @@ "monero_light_theme": "Tema leggero Monero", "moonpay_alert_text": "Il valore dell'importo deve essere maggiore o uguale a ${minAmount} ${fiatCurrency}", "more_options": "Altre opzioni", + "mweb_confirmed": "MWeb confermato", + "mweb_unconfirmed": "MWeb non confermato", "name": "Nome", "nano_current_rep": "Rappresentante attuale", "nano_gpt_thanks_message": "Grazie per aver usato il nanogpt! Ricorda di tornare al browser dopo il completamento della transazione!", @@ -454,6 +465,7 @@ "overwrite_amount": "Sovrascrivi quantità", "pairingInvalidEvent": "Associazione evento non valido", "passphrase": "Passphrase (opzionale)", + "passphrases_doesnt_match": "Le passphrasi non corrispondono, riprova", "password": "Password", "paste": "Incolla", "pause_wallet_creation": "La possibilità di creare Haven Wallet è attualmente sospesa.", @@ -816,6 +828,7 @@ "trusted": "di fiducia", "tx_commit_exception_no_dust_on_change": "La transazione viene respinta con questo importo. Con queste monete è possibile inviare ${min} senza modifiche o ${max} che restituisce il cambiamento.", "tx_commit_failed": "Commit di transazione non riuscita. Si prega di contattare il supporto.", + "tx_commit_failed_no_peers": "La transazione non è riuscita a trasmettere, riprovare in un secondo o giù di lì", "tx_invalid_input": "Stai usando il tipo di input sbagliato per questo tipo di pagamento", "tx_no_dust_exception": "La transazione viene respinta inviando un importo troppo piccolo. Per favore, prova ad aumentare l'importo.", "tx_not_enough_inputs_exception": "Input non sufficienti disponibili. Seleziona di più sotto il controllo delle monete", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 938465926..5a64563b6 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "このアクションにより、このウォレットが削除されます。 続行しますか?", "confirm_fee_deduction": "料金控除を確認します", "confirm_fee_deduction_content": "出力から料金を差し引くことに同意しますか?", + "confirm_passphrase": "パスフレーズを確認します", "confirm_sending": "送信を確認", "confirm_silent_payments_switch_node": "現在のノードはサイレントペイメントをサポートしていません\\ ncakeウォレットは、スキャン用に互換性のあるノードに切り替えます", "confirmations": "確認", @@ -363,6 +364,14 @@ "ledger_error_wrong_app": "元帳に適切なアプリを開始するようにしてください", "ledger_please_enable_bluetooth": "Bluetoothが元帳を検出できるようにしてください", "light_theme": "光", + "litecoin_enable_mweb_sync": "MWEBスキャンを有効にします", + "litecoin_mweb": "mweb", + "litecoin_mweb_always_scan": "MWEBを常にスキャンします", + "litecoin_mweb_display_card": "MWEBカードを表示します", + "litecoin_mweb_scanning": "MWEBスキャン", + "litecoin_mweb_settings": "MWEB設定", + "litecoin_mweb_warning": "MWEBを使用すると、最初は〜600MBのデータをダウンロードし、ネットワーク速度に応じて最大30分かかる場合があります。この最初のデータは一度だけダウンロードされ、すべてのLitecoinウォレットで利用可能になります", + "litecoin_what_is_mweb": "MWEBとは何ですか?", "live_fee_rates": "API経由のライブ料金", "load_more": "もっと読み込む", "loading_your_wallet": "ウォレットをロードしています", @@ -393,6 +402,8 @@ "monero_light_theme": "モネロ ライト テーマ", "moonpay_alert_text": "金額の値は以上でなければなりません ${minAmount} ${fiatCurrency}", "more_options": "その他のオプション", + "mweb_confirmed": "確認されたMWEB", + "mweb_unconfirmed": "未確認のMWEB", "name": "名前", "nano_current_rep": "現在の代表", "nano_gpt_thanks_message": "NanoGptを使用してくれてありがとう!トランザクションが完了したら、ブラウザに戻ることを忘れないでください!", @@ -453,6 +464,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "ペアリング無効イベント", "passphrase": "パスフレーズ(オプション)", + "passphrases_doesnt_match": "パスフレーズは一致しません。もう一度やり直してください", "password": "パスワード", "paste": "ペースト", "pause_wallet_creation": "Haven Wallet を作成する機能は現在一時停止されています。", @@ -815,6 +827,7 @@ "trusted": "信頼できる", "tx_commit_exception_no_dust_on_change": "この金額ではトランザクションは拒否されます。 これらのコインを使用すると、おつりなしの ${min} またはおつりを返す ${max} を送信できます。", "tx_commit_failed": "トランザクションコミットは失敗しました。サポートに連絡してください。", + "tx_commit_failed_no_peers": "トランザクションはブロードキャストに失敗しました。一瞬かそこらで再試行してください", "tx_invalid_input": "このタイプの支払いに間違った入力タイプを使用しています", "tx_no_dust_exception": "トランザクションは、小さすぎる金額を送信することにより拒否されます。量を増やしてみてください。", "tx_not_enough_inputs_exception": "利用可能な入力が十分ではありません。コイン制御下でもっと選択してください", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index dd9fa7a3c..2f6144d4f 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "이 작업은이 지갑을 삭제합니다. 계속 하시겠습니까?", "confirm_fee_deduction": "수수료 공제를 확인하십시오", "confirm_fee_deduction_content": "출력에서 수수료를 공제하는 데 동의하십니까?", + "confirm_passphrase": "암호를 확인하십시오", "confirm_sending": "전송 확인", "confirm_silent_payments_switch_node": "현재 노드는 무음 지불을 지원하지 않습니다 \\ ncake 지갑은 스캔을 위해 호환 가능한 노드로 전환됩니다.", "confirmations": "확인", @@ -362,6 +363,14 @@ "ledger_error_wrong_app": "원장에서 올바른 앱을 반대하는지 확인하십시오.", "ledger_please_enable_bluetooth": "Bluetooth가 원장을 감지 할 수 있도록하십시오", "light_theme": "빛", + "litecoin_enable_mweb_sync": "mweb 스캔을 활성화합니다", + "litecoin_mweb": "mweb", + "litecoin_mweb_always_scan": "mweb는 항상 스캔을 설정합니다", + "litecoin_mweb_display_card": "mweb 카드를 보여주십시오", + "litecoin_mweb_scanning": "mweb 스캔", + "litecoin_mweb_settings": "mweb 설정", + "litecoin_mweb_warning": "MWEB를 사용하면 처음에는 ~ 600MB의 데이터를 다운로드하며 네트워크 속도에 따라 최대 30 분이 소요될 수 있습니다. 이 초기 데이터는 한 번만 다운로드하여 모든 조명 지갑에 사용할 수 있습니다.", + "litecoin_what_is_mweb": "MWEB 란 무엇입니까?", "live_fee_rates": "API를 통한 라이브 요금 요금", "load_more": "더로드하십시오", "loading_your_wallet": "지갑 넣기", @@ -392,6 +401,8 @@ "monero_light_theme": "모네로 라이트 테마", "moonpay_alert_text": "금액은 다음보다 크거나 같아야합니다 ${minAmount} ${fiatCurrency}", "more_options": "추가 옵션", + "mweb_confirmed": "확인 mweb", + "mweb_unconfirmed": "확인되지 않은 mweb", "name": "이름", "nano_current_rep": "현재 대표", "nano_gpt_thanks_message": "Nanogpt를 사용해 주셔서 감사합니다! 거래가 완료된 후 브라우저로 돌아가는 것을 잊지 마십시오!", @@ -452,6 +463,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "잘못된 이벤트 페어링", "passphrase": "암호화 (선택 사항)", + "passphrases_doesnt_match": "패스 프레이즈가 일치하지 않습니다. 다시 시도하십시오", "password": "암호", "paste": "풀", "pause_wallet_creation": "Haven Wallet 생성 기능이 현재 일시 중지되었습니다.", @@ -815,6 +827,7 @@ "trusted": "신뢰할 수 있는", "tx_commit_exception_no_dust_on_change": "이 금액으로 거래가 거부되었습니다. 이 코인을 사용하면 거스름돈 없이 ${min}를 보내거나 거스름돈을 반환하는 ${max}를 보낼 수 있습니다.", "tx_commit_failed": "거래 커밋이 실패했습니다. 지원에 연락하십시오.", + "tx_commit_failed_no_peers": "트랜잭션이 방송에 실패했는데 1 초 정도 후에 다시 시도하십시오.", "tx_invalid_input": "이 유형의 지불에 잘못 입력 유형을 사용하고 있습니다.", "tx_no_dust_exception": "너무 작은 금액을 보내면 거래가 거부됩니다. 금액을 늘리십시오.", "tx_not_enough_inputs_exception": "사용 가능한 입력이 충분하지 않습니다. 코인 컨트롤에서 더 많은 것을 선택하십시오", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 7c13773af..d6f4f96fb 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "ဤလုပ်ဆောင်ချက်သည် ဤပိုက်ဆံအိတ်ကို ဖျက်လိုက်ပါမည်။ ဆက်လုပ်လိုပါသလား။", "confirm_fee_deduction": "အခကြေးငွေကိုနှုတ်ယူခြင်း", "confirm_fee_deduction_content": "output မှအခကြေးငွေကိုယူရန်သဘောတူပါသလား။", + "confirm_passphrase": "passphrase အတည်ပြုပါ", "confirm_sending": "ပေးပို့အတည်ပြုပါ။", "confirm_silent_payments_switch_node": "သင်၏လက်ရှိ node သည်အသံတိတ်ငွေပေးချေမှုကိုမပံ့ပိုးပါဟု \\ t", "confirmations": "အတည်ပြုချက်များ", @@ -362,6 +363,14 @@ "ledger_error_wrong_app": "ကျေးဇူးပြု. သင့်လက်ျာအက်ပ်ကိုသင်၏ Ledger တွင်ဖွင့်ရန်သေချာစေပါ", "ledger_please_enable_bluetooth": "သင်၏ Ledger ကိုရှာဖွေရန် Bluetooth ကိုဖွင့်ပါ", "light_theme": "အလင်း", + "litecoin_enable_mweb_sync": "mweb scanning ဖွင့်ပါ", + "litecoin_mweb": "မင်္ဂလာပါ", + "litecoin_mweb_always_scan": "Mweb အမြဲစကင်ဖတ်စစ်ဆေးပါ", + "litecoin_mweb_display_card": "MweB ကဒ်ကိုပြပါ", + "litecoin_mweb_scanning": "mweb scanning", + "litecoin_mweb_settings": "Mweb ဆက်တင်များ", + "litecoin_mweb_warning": "MweB ကိုအသုံးပြုခြင်းသည်အစပိုင်းတွင် ~ 600MB ဒေတာများကို download လုပ်ပြီးကွန်ယက်အမြန်နှုန်းပေါ် မူတည်. မိနစ် 30 အထိကြာနိုင်သည်။ ဤကန ဦး ဒေတာကိုတစ်ကြိမ်သာ download လုပ်ပြီး litecoin Walkets အားလုံးအတွက်ရနိုင်သည်", + "litecoin_what_is_mweb": "MweB ဆိုတာဘာလဲ။", "live_fee_rates": "API မှတစ်ဆင့် Live အခကြေးငွေနှုန်းထားများ", "load_more": "ပိုပြီး load", "loading_your_wallet": "သင့်ပိုက်ဆံအိတ်ကို ဖွင့်နေသည်။", @@ -392,6 +401,8 @@ "monero_light_theme": "Monero Light အပြင်အဆင်", "moonpay_alert_text": "ပမာဏ၏တန်ဖိုးသည် ${minAmount} ${fiatCurrency} နှင့် ပိုနေရမည်", "more_options": "နောက်ထပ် ရွေးချယ်စရာများ", + "mweb_confirmed": "အတည်ပြုလိုက် mweb", + "mweb_unconfirmed": "အတည်မပြုနိုင်သော mweb", "name": "နာမည်", "nano_current_rep": "လက်ရှိကိုယ်စားလှယ်", "nano_gpt_thanks_message": "nanogpt ကိုသုံးပြီးကျေးဇူးတင်ပါတယ် သင်၏ငွေပေးငွေယူပြီးနောက် browser သို့ပြန်သွားရန်သတိရပါ။", @@ -452,6 +463,7 @@ "overwrite_amount": "ပမာဏကို ထပ်ရေးပါ။", "pairingInvalidEvent": "မမှန်ကန်သောဖြစ်ရပ်ကို တွဲချိတ်ခြင်း။", "passphrase": "passphrase (optional)", + "passphrases_doesnt_match": "passphrases မကိုက်ညီဘူး, ကျေးဇူးပြုပြီးထပ်ကြိုးစားပါ", "password": "စကားဝှက်", "paste": "ငါးပိ", "pause_wallet_creation": "Haven Wallet ဖန်တီးနိုင်မှုကို လောလောဆယ် ခေတ္တရပ်ထားသည်။", @@ -814,6 +826,7 @@ "trusted": "ယုံတယ်။", "tx_commit_exception_no_dust_on_change": "အဆိုပါငွေပေးငွေယူကဒီပမာဏနှင့်အတူပယ်ချခံရသည်။ ဤဒင်္ဂါးပြားများနှင့်အတူပြောင်းလဲမှုကိုပြန်လည်ပြောင်းလဲခြင်းသို့မဟုတ် ${min} မပါဘဲ ${max} ပေးပို့နိုင်သည်။", "tx_commit_failed": "ငွေပေးငွေယူကျူးလွန်မှုပျက်ကွက်။ ကျေးဇူးပြုပြီးပံ့ပိုးမှုဆက်သွယ်ပါ။", + "tx_commit_failed_no_peers": "ငွေပေးငွေယူထုတ်လွှင့်ရန်ပျက်ကွက်ပါက ကျေးဇူးပြု. ဒုတိယသို့မဟုတ်ထိုအတိုင်းထပ်မံကြိုးစားပါ", "tx_invalid_input": "သင်သည်ဤငွေပေးချေမှုအမျိုးအစားအတွက်မှားယွင်းသော input type ကိုအသုံးပြုနေသည်", "tx_no_dust_exception": "ငွေပမာဏကိုသေးငယ်လွန်းသောငွေပမာဏကိုပေးပို့ခြင်းဖြင့်ပယ်ဖျက်ခြင်းကိုငြင်းပယ်သည်။ ကျေးဇူးပြုပြီးငွေပမာဏကိုတိုးမြှင့်ကြိုးစားပါ။", "tx_not_enough_inputs_exception": "အလုံအလောက်သွင်းအားစုများမလုံလောက်။ ကျေးဇူးပြုပြီးဒင်္ဂါးပြားထိန်းချုပ်မှုအောက်တွင်ပိုမိုရွေးချယ်ပါ", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 75f436518..d9aa0b1fa 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Met deze actie wordt deze portemonnee verwijderd. Wilt u doorgaan?", "confirm_fee_deduction": "Bevestig de aftrek van de kosten", "confirm_fee_deduction_content": "Stemt u ermee in om de vergoeding af te trekken van de output?", + "confirm_passphrase": "Bevestig Passaspherase", "confirm_sending": "Bevestig verzending", "confirm_silent_payments_switch_node": "Uw huidige knooppunt ondersteunt geen stille betalingen \\ ncake -portemonnee schakelt over naar een compatibele knoop", "confirmations": "Bevestigingen", @@ -362,6 +363,14 @@ "ledger_error_wrong_app": "Zorg ervoor dat u de juiste app op uw grootboek opent", "ledger_please_enable_bluetooth": "Schakel Bluetooth in staat om uw grootboek te detecteren", "light_theme": "Licht", + "litecoin_enable_mweb_sync": "MWEB -scanning inschakelen", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Stel mweb altijd op scannen", + "litecoin_mweb_display_card": "Toon MWEB -kaart", + "litecoin_mweb_scanning": "MWEB -scanning", + "litecoin_mweb_settings": "MWEB -instellingen", + "litecoin_mweb_warning": "Het gebruik van MWeb downloadt in eerste instantie ~ 600 MB aan gegevens en kan tot 30 minuten duren, afhankelijk van de netwerksnelheid. Deze eerste gegevens worden slechts eenmaal gedownload en zijn beschikbaar voor alle Litecoin -portefeuilles", + "litecoin_what_is_mweb": "Wat is Mweb?", "live_fee_rates": "Live -tarieven via API", "load_more": "Meer laden", "loading_your_wallet": "Uw portemonnee laden", @@ -392,6 +401,8 @@ "monero_light_theme": "Monero Light-thema", "moonpay_alert_text": "Waarde van het bedrag moet meer of gelijk zijn aan ${minAmount} ${fiatCurrency}", "more_options": "Meer opties", + "mweb_confirmed": "Bevestigde MWEB", + "mweb_unconfirmed": "Onbevestigde MWEB", "name": "Naam", "nano_current_rep": "Huidige vertegenwoordiger", "nano_gpt_thanks_message": "Bedankt voor het gebruik van Nanogpt! Vergeet niet om terug te gaan naar de browser nadat uw transactie is voltooid!", @@ -452,6 +463,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Koppelen Ongeldige gebeurtenis", "passphrase": "PassaspHRASE (optioneel)", + "passphrases_doesnt_match": "Passaspelfiaal komt niet overeen, probeer het opnieuw", "password": "Wachtwoord", "paste": "Plakken", "pause_wallet_creation": "De mogelijkheid om Haven Wallet te maken is momenteel onderbroken.", @@ -814,6 +826,7 @@ "trusted": "vertrouwd", "tx_commit_exception_no_dust_on_change": "De transactie wordt afgewezen met dit bedrag. Met deze munten kunt u ${min} verzenden zonder verandering of ${max} die wijziging retourneert.", "tx_commit_failed": "Transactiebewissing is mislukt. Neem contact op met de ondersteuning.", + "tx_commit_failed_no_peers": "De transactie is niet uitgezonden, probeer het opnieuw binnen een seconde of zo", "tx_invalid_input": "U gebruikt het verkeerde invoertype voor dit type betaling", "tx_no_dust_exception": "De transactie wordt afgewezen door een te klein bedrag te verzenden. Probeer het bedrag te verhogen.", "tx_not_enough_inputs_exception": "Niet genoeg ingangen beschikbaar. Selecteer meer onder muntenbesturing", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index dafe67999..4dba38a16 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Ta czynność usunie ten portfel. Czy chcesz kontynuować?", "confirm_fee_deduction": "Potwierdź odliczenie opłaty", "confirm_fee_deduction_content": "Czy zgadzasz się odliczyć opłatę od wyników?", + "confirm_passphrase": "Potwierdź hasło", "confirm_sending": "Potwierdź wysłanie", "confirm_silent_payments_switch_node": "Twój obecny węzeł nie obsługuje cichych płatności \\ NCAKE Portfel przełączy się na kompatybilny węzeł, tylko do skanowania", "confirmations": "Potwierdzenia", @@ -362,6 +363,14 @@ "ledger_error_wrong_app": "Upewnij się, że opisz odpowiednią aplikację na swojej księdze", "ledger_please_enable_bluetooth": "Włącz Bluetooth wykrywanie księgi", "light_theme": "Jasny", + "litecoin_enable_mweb_sync": "Włącz skanowanie MWEB", + "litecoin_mweb": "MWEB", + "litecoin_mweb_always_scan": "Ustaw MWEB zawsze skanowanie", + "litecoin_mweb_display_card": "Pokaż kartę MWEB", + "litecoin_mweb_scanning": "Skanowanie MWEB", + "litecoin_mweb_settings": "Ustawienia MWEB", + "litecoin_mweb_warning": "Korzystanie z MWEB początkowo pobiera ~ 600 MB danych i może potrwać do 30 minut w zależności od prędkości sieci. Te początkowe dane pobierają tylko raz i będą dostępne dla wszystkich portfeli Litecoin", + "litecoin_what_is_mweb": "Co to jest MWEB?", "live_fee_rates": "Stawki opłaty na żywo za pośrednictwem API", "load_more": "Załaduj więcej", "loading_your_wallet": "Ładowanie portfela", @@ -392,6 +401,8 @@ "monero_light_theme": "Lekki motyw Monero", "moonpay_alert_text": "Wartość kwoty musi być większa lub równa ${minAmount} ${fiatCurrency}", "more_options": "Więcej opcji", + "mweb_confirmed": "Potwierdził MWEB", + "mweb_unconfirmed": "Niepotwierdzone MWEB", "name": "Nazwa", "nano_current_rep": "Obecny przedstawiciel", "nano_gpt_thanks_message": "Dzięki za użycie Nanogpt! Pamiętaj, aby wrócić do przeglądarki po zakończeniu transakcji!", @@ -452,6 +463,7 @@ "overwrite_amount": "Nadpisz ilość", "pairingInvalidEvent": "Nieprawidłowe zdarzenie parowania", "passphrase": "PassPhraza (opcjonalnie)", + "passphrases_doesnt_match": "Passfrazy nie pasują, spróbuj ponownie", "password": "Hasło", "paste": "Wklej", "pause_wallet_creation": "Możliwość utworzenia Portfela Haven jest obecnie wstrzymana.", @@ -814,6 +826,7 @@ "trusted": "Zaufany", "tx_commit_exception_no_dust_on_change": "Transakcja jest odrzucana z tą kwotą. Za pomocą tych monet możesz wysłać ${min} bez zmiany lub ${max}, które zwraca zmianę.", "tx_commit_failed": "Zatwierdzenie transakcji nie powiodło się. Skontaktuj się z obsługą.", + "tx_commit_failed_no_peers": "Transakcja nie była transmitowana, spróbuj ponownie za około sekundę", "tx_invalid_input": "Używasz niewłaściwego typu wejściowego dla tego rodzaju płatności", "tx_no_dust_exception": "Transakcja jest odrzucana przez wysyłanie zbyt małej ilości. Spróbuj zwiększyć kwotę.", "tx_not_enough_inputs_exception": "Za mało dostępnych danych wejściowych. Wybierz więcej pod kontrolą monet", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index eb706dd61..f0defbb94 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Esta ação excluirá esta carteira. Você deseja continuar?", "confirm_fee_deduction": "Confirme dedução da taxa", "confirm_fee_deduction_content": "Você concorda em deduzir a taxa da saída?", + "confirm_passphrase": "Confirme a senha", "confirm_sending": "Confirmar o envio", "confirm_silent_payments_switch_node": "Seu nó atual não suporta pagamentos silenciosos \n A Cake Wallet mudará para um nó compatível, apenas para escanear", "confirmations": "Confirmações", @@ -362,6 +363,14 @@ "ledger_error_wrong_app": "Por favor, certifique -se de optar pelo aplicativo certo no seu livro", "ledger_please_enable_bluetooth": "Ative o Bluetooth para detectar seu livro", "light_theme": "Luz", + "litecoin_enable_mweb_sync": "Ativar digitalização do MWEB", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Definir mweb sempre digitalizando", + "litecoin_mweb_display_card": "Mostre o cartão MWEB", + "litecoin_mweb_scanning": "MWEB Scanning", + "litecoin_mweb_settings": "Configurações do MWEB", + "litecoin_mweb_warning": "O uso do MWEB baixará inicialmente ~ 600 MB de dados e pode levar até 30 minutos, dependendo da velocidade da rede. Esses dados iniciais serão baixados apenas uma vez e estarão disponíveis para todas as carteiras Litecoin", + "litecoin_what_is_mweb": "O que é MWeb?", "live_fee_rates": "Taxas de taxas ao vivo via API", "load_more": "Carregue mais", "loading_your_wallet": "Abrindo sua carteira", @@ -393,6 +402,8 @@ "monero_light_theme": "Monero Light Theme", "moonpay_alert_text": "O valor do montante deve ser maior ou igual a ${minAmount} ${fiatCurrency}", "more_options": "Mais opções", + "mweb_confirmed": "MWEB confirmado", + "mweb_unconfirmed": "MWEB não confirmado", "name": "Nome", "nano_current_rep": "Representante atual", "nano_gpt_thanks_message": "Obrigado por usar o Nanogpt! Lembre -se de voltar para o navegador após a conclusão da transação!", @@ -454,6 +465,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Emparelhamento de evento inválido", "passphrase": "Senha (opcional)", + "passphrases_doesnt_match": "Passagases não correspondem, por favor tente novamente", "password": "Senha", "paste": "Colar", "pause_wallet_creation": "A capacidade de criar a Haven Wallet está atualmente pausada.", @@ -816,6 +828,7 @@ "trusted": "confiável", "tx_commit_exception_no_dust_on_change": "A transação é rejeitada com esse valor. Com essas moedas, você pode enviar ${min} sem alteração ou ${max} que retorna alterações.", "tx_commit_failed": "A confirmação da transação falhou. Entre em contato com o suporte.", + "tx_commit_failed_no_peers": "A transação não foi transmitida, tente novamente em um segundo", "tx_invalid_input": "Você está usando o tipo de entrada errado para este tipo de pagamento", "tx_no_dust_exception": "A transação é rejeitada enviando uma quantia pequena demais. Por favor, tente aumentar o valor.", "tx_not_enough_inputs_exception": "Não há entradas disponíveis. Selecione mais sob controle de moedas", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 50447d721..b6f7fd47c 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Это действие удалит кошелек. Вы хотите продолжить?", "confirm_fee_deduction": "Подтвердите вычет платы", "confirm_fee_deduction_content": "Согласны ли вы вычесть плату из вывода?", + "confirm_passphrase": "Подтвердите Passfrase", "confirm_sending": "Подтвердить отправку", "confirm_silent_payments_switch_node": "Ваш текущий узел не поддерживает Silent Payments \\ ncake Wallet переключится на совместимый узел, только для сканирования", "confirmations": "Подтверждения", @@ -362,6 +363,14 @@ "ledger_error_wrong_app": "Пожалуйста, убедитесь, что вы предлагаете правильное приложение в своей бухгалтерской книге", "ledger_please_enable_bluetooth": "Пожалуйста, включите Bluetooth обнаружить вашу бухгалтерскую книгу", "light_theme": "Светлая", + "litecoin_enable_mweb_sync": "Включить MWEB сканирование", + "litecoin_mweb": "Мвеб", + "litecoin_mweb_always_scan": "Установить MWEB всегда сканирование", + "litecoin_mweb_display_card": "Показать карту MWEB", + "litecoin_mweb_scanning": "MWEB сканирование", + "litecoin_mweb_settings": "Настройки MWEB", + "litecoin_mweb_warning": "Использование MWEB изначально загрузит ~ 600 МБ данных и может занять до 30 минут в зависимости от скорости сети. Эти начальные данные будут загружаться только один раз и будут доступны для всех кошельков Litecoin", + "litecoin_what_is_mweb": "Что такое MWEB?", "live_fee_rates": "Ставки по сбору вживую через API", "load_more": "Загрузи больше", "loading_your_wallet": "Загрузка кошелька", @@ -392,6 +401,8 @@ "monero_light_theme": "Светлая тема Monero", "moonpay_alert_text": "Сумма должна быть больше или равна ${minAmount} ${fiatCurrency}", "more_options": "Дополнительные параметры", + "mweb_confirmed": "Подтверждено MWEB", + "mweb_unconfirmed": "Неподтвержденная MWEB", "name": "Имя", "nano_current_rep": "Нынешний представитель", "nano_gpt_thanks_message": "Спасибо за использование Nanogpt! Не забудьте вернуться в браузер после завершения транзакции!", @@ -453,6 +464,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Недействительное событие сопряжения", "passphrase": "Passfrase (необязательно)", + "passphrases_doesnt_match": "Пасфразы не совпадают, попробуйте еще раз", "password": "Пароль", "paste": "Вставить", "pause_wallet_creation": "Возможность создания Haven Wallet в настоящее время приостановлена.", @@ -815,6 +827,7 @@ "trusted": "доверенный", "tx_commit_exception_no_dust_on_change": "Транзакция отклоняется с этой суммой. С этими монетами вы можете отправлять ${min} без изменения или ${max}, которые возвращают изменение.", "tx_commit_failed": "Комплект транзакции не удался. Пожалуйста, свяжитесь с поддержкой.", + "tx_commit_failed_no_peers": "Транзакция не смогла передать, попробуйте еще раз через секунду или около того", "tx_invalid_input": "Вы используете неправильный тип ввода для этого типа оплаты", "tx_no_dust_exception": "Транзакция отклоняется путем отправки слишком маленькой суммы. Пожалуйста, попробуйте увеличить сумму.", "tx_not_enough_inputs_exception": "Недостаточно входов доступны. Пожалуйста, выберите больше под контролем монет", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index d77fbafbf..d694181bf 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "การดำเนินการนี้จะลบกระเป๋านี้ คุณต้องการดำเนินการต่อหรือไม่?", "confirm_fee_deduction": "ยืนยันการหักค่าธรรมเนียม", "confirm_fee_deduction_content": "คุณตกลงที่จะหักค่าธรรมเนียมจากผลลัพธ์หรือไม่?", + "confirm_passphrase": "ยืนยันวลีรหัสผ่าน", "confirm_sending": "ยืนยันการส่ง", "confirm_silent_payments_switch_node": "โหนดปัจจุบันของคุณไม่รองรับการชำระเงินแบบเงียบ \\ ncake กระเป๋าเงินจะเปลี่ยนเป็นโหนดที่เข้ากันได้เพียงเพื่อการสแกน", "confirmations": "การยืนยัน", @@ -362,6 +363,14 @@ "ledger_error_wrong_app": "โปรดตรวจสอบให้แน่ใจว่าคุณเปิดแอพที่เหมาะสมในบัญชีแยกประเภทของคุณ", "ledger_please_enable_bluetooth": "โปรดเปิดใช้งานบลูทู ธ ในการตรวจจับบัญชีแยกประเภทของคุณ", "light_theme": "สว่าง", + "litecoin_enable_mweb_sync": "เปิดใช้งานการสแกน MWEB", + "litecoin_mweb": "mweb", + "litecoin_mweb_always_scan": "ตั้งค่าการสแกน MWEB เสมอ", + "litecoin_mweb_display_card": "แสดงการ์ด mweb", + "litecoin_mweb_scanning": "การสแกน MWEB", + "litecoin_mweb_settings": "การตั้งค่า MWEB", + "litecoin_mweb_warning": "การใช้ MWEB จะดาวน์โหลดข้อมูล ~ 600MB ในขั้นต้นและอาจใช้เวลาสูงสุด 30 นาทีขึ้นอยู่กับความเร็วเครือข่าย ข้อมูลเริ่มต้นนี้จะดาวน์โหลดได้เพียงครั้งเดียวและพร้อมใช้งานสำหรับกระเป๋าเงินทั้งหมดของ Litecoin", + "litecoin_what_is_mweb": "MWEB คืออะไร?", "live_fee_rates": "อัตราค่าธรรมเนียมสดผ่าน API", "load_more": "โหลดมากขึ้น", "loading_your_wallet": "กำลังโหลดกระเป๋าของคุณ", @@ -392,6 +401,8 @@ "monero_light_theme": "ธีมแสง Monero", "moonpay_alert_text": "มูลค่าของจำนวนต้องมากกว่าหรือเท่ากับ ${minAmount} ${fiatCurrency}", "more_options": "ตัวเลือกเพิ่มเติม", + "mweb_confirmed": "MWEB ยืนยันแล้ว", + "mweb_unconfirmed": "mweb ที่ไม่ได้รับการยืนยัน", "name": "ชื่อ", "nano_current_rep": "ตัวแทนปัจจุบัน", "nano_gpt_thanks_message": "ขอบคุณที่ใช้ Nanogpt! อย่าลืมกลับไปที่เบราว์เซอร์หลังจากการทำธุรกรรมของคุณเสร็จสิ้น!", @@ -452,6 +463,7 @@ "overwrite_amount": "เขียนทับจำนวน", "pairingInvalidEvent": "การจับคู่เหตุการณ์ที่ไม่ถูกต้อง", "passphrase": "ข้อความรหัสผ่าน (ไม่บังคับ)", + "passphrases_doesnt_match": "Passphrases ไม่ตรงกันโปรดลองอีกครั้ง", "password": "รหัสผ่าน", "paste": "วาง", "pause_wallet_creation": "ขณะนี้ความสามารถในการสร้าง Haven Wallet ถูกหยุดชั่วคราว", @@ -814,6 +826,7 @@ "trusted": "มั่นคง", "tx_commit_exception_no_dust_on_change": "ธุรกรรมถูกปฏิเสธด้วยจำนวนเงินนี้ ด้วยเหรียญเหล่านี้คุณสามารถส่ง ${min} โดยไม่ต้องเปลี่ยนแปลงหรือ ${max} ที่ส่งคืนการเปลี่ยนแปลง", "tx_commit_failed": "การทำธุรกรรมล้มเหลว กรุณาติดต่อฝ่ายสนับสนุน", + "tx_commit_failed_no_peers": "การทำธุรกรรมล้มเหลวในการออกอากาศโปรดลองอีกครั้งในวินาทีหรือมากกว่านั้น", "tx_invalid_input": "คุณกำลังใช้ประเภทอินพุตที่ไม่ถูกต้องสำหรับการชำระเงินประเภทนี้", "tx_no_dust_exception": "การทำธุรกรรมถูกปฏิเสธโดยการส่งจำนวนน้อยเกินไป โปรดลองเพิ่มจำนวนเงิน", "tx_not_enough_inputs_exception": "มีอินพุตไม่เพียงพอ โปรดเลือกเพิ่มเติมภายใต้การควบคุมเหรียญ", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 510546e41..2957c800d 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Tatanggalin ng pagkilos na ito ang wallet na ito. Gusto mo bang magpatuloy?", "confirm_fee_deduction": "Kumpirmahin ang pagbabawas ng fee", "confirm_fee_deduction_content": "Sumasang-ayon ka bang bawasan ang fee mula sa output?", + "confirm_passphrase": "Kumpirma ang passphrase", "confirm_sending": "Kumpirmahin ang pagpapadala", "confirm_silent_payments_switch_node": "Ang iyong kasalukuyang node ay hindi sumusuporta sa tahimik na pagbabayad \\ nCake Wallet ay lilipat sa isang katugmang node, para lamang sa pag-scan", "confirmations": "Mga kumpirmasyon", @@ -362,6 +363,14 @@ "ledger_error_wrong_app": "Mangyaring tiyaking pinipili mo ang tamang app sa iyong Ledger", "ledger_please_enable_bluetooth": "Mangyaring paganahin ang Bluetooth upang makita ang iyong Ledger", "light_theme": "Light", + "litecoin_enable_mweb_sync": "Paganahin ang pag -scan ng MWeb", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Itakda ang MWeb na laging nag -scan", + "litecoin_mweb_display_card": "Ipakita ang MWEB Card", + "litecoin_mweb_scanning": "Pag -scan ng Mweb", + "litecoin_mweb_settings": "Mga Setting ng Mweb", + "litecoin_mweb_warning": "Ang paggamit ng MWEB ay unang i -download ang ~ 600MB ng data, at maaaring tumagal ng hanggang sa 30 minuto depende sa bilis ng network. Ang paunang data na ito ay mag -download lamang ng isang beses at magagamit para sa lahat ng mga wallets ng Litecoin", + "litecoin_what_is_mweb": "Ano ang MWEB?", "live_fee_rates": "Mga rate ng live na bayad sa pamamagitan ng API", "load_more": "Mag-load pa", "loading_your_wallet": "Naglo-load ng iyong wallet", @@ -392,6 +401,8 @@ "monero_light_theme": "Monero Light Theme", "moonpay_alert_text": "Ang halaga ay dapat na higit pa o katumbas ng ${minAmount} ${fiatCurrency}", "more_options": "Higit pang mga Pagpipilian", + "mweb_confirmed": "Nakumpirma na MWeb", + "mweb_unconfirmed": "Hindi nakumpirma si Mweb", "name": "Pangalan", "nano_current_rep": "Kasalukuyang Representative", "nano_gpt_thanks_message": "Salamat sa paggamit ng NanoGPT! Tandaan na bumalik sa browser matapos makumpleto ang iyong transaksyon!", @@ -452,6 +463,7 @@ "overwrite_amount": "I-overwrite ang halaga", "pairingInvalidEvent": "Pairing Invalid Event", "passphrase": "Passphrase (opsyonal)", + "passphrases_doesnt_match": "Ang mga passphrases ay hindi tumutugma, mangyaring subukang muli", "password": "Password", "paste": "I-paste", "pause_wallet_creation": "Kasalukuyang naka-pause ang kakayahang gumawa ng Haven Wallet.", @@ -814,6 +826,7 @@ "trusted": "Pinagkakatiwalaan", "tx_commit_exception_no_dust_on_change": "Ang transaksyon ay tinanggihan sa halagang ito. Sa mga barya na ito maaari kang magpadala ng ${min} nang walang sukli o ${max} na nagbabalik ng sukli.", "tx_commit_failed": "Nabigo ang transaksyon. Mangyaring makipag-ugnay sa suporta.", + "tx_commit_failed_no_peers": "Nabigo ang transaksyon na mag -broadcast, mangyaring subukang muli sa isang segundo o higit pa", "tx_invalid_input": "Gumagamit ka ng maling uri ng pag-input para sa ganitong uri ng pagbabayad", "tx_no_dust_exception": "Ang transaksyon ay tinanggihan sa pamamagitan ng pagpapadala ng isang maliit na halaga. Mangyaring subukang dagdagan ang halaga.", "tx_not_enough_inputs_exception": "Hindi sapat na magagamit ang mga input. Mangyaring pumili ng higit pa sa ilalim ng Coin Control", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index ce3782814..a5513e0d2 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Bu eylem, bu cüzdanı silecek. Devam etmek istiyor musun?", "confirm_fee_deduction": "Ücret kesintisini onaylayın", "confirm_fee_deduction_content": "Ücreti çıktıdan düşürmeyi kabul ediyor musunuz?", + "confirm_passphrase": "Parola onaylayın", "confirm_sending": "Göndermeyi onayla", "confirm_silent_payments_switch_node": "Mevcut düğümünüz sessiz ödemeleri desteklemiyor \\ nCake cüzdanı, sadece tarama için uyumlu bir düğüme geçecektir", "confirmations": "Onay", @@ -362,6 +363,14 @@ "ledger_error_wrong_app": "Lütfen defterinizde doğru uygulamayı açtığınızdan emin olun", "ledger_please_enable_bluetooth": "Defterinizi algılamak için lütfen Bluetooth'u etkinleştirin", "light_theme": "Aydınlık", + "litecoin_enable_mweb_sync": "MWEB taramasını etkinleştir", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "MWEB'i her zaman taramayı ayarlayın", + "litecoin_mweb_display_card": "MWEB kartını göster", + "litecoin_mweb_scanning": "MWEB taraması", + "litecoin_mweb_settings": "MWEB ayarları", + "litecoin_mweb_warning": "MWEB kullanmak başlangıçta ~ 600MB veri indirir ve ağ hızına bağlı olarak 30 dakikaya kadar sürebilir. Bu ilk veriler yalnızca bir kez indirilecek ve tüm Litecoin cüzdanları için kullanılabilir olacak", + "litecoin_what_is_mweb": "MWEB nedir?", "live_fee_rates": "API üzerinden canlı ücret oranları", "load_more": "Daha fazla yükle", "loading_your_wallet": "Cüzdanın yükleniyor", @@ -392,6 +401,8 @@ "monero_light_theme": "Monero Hafif Tema", "moonpay_alert_text": "Tutar ${minAmount} ${fiatCurrency} miktarına eşit veya daha fazla olmalıdır", "more_options": "Daha Fazla Seçenek", + "mweb_confirmed": "Onaylanmış mweb", + "mweb_unconfirmed": "Doğrulanmamış mweb", "name": "İsim", "nano_current_rep": "Mevcut temsilci", "nano_gpt_thanks_message": "Nanogpt kullandığınız için teşekkürler! İşleminiz tamamlandıktan sonra tarayıcıya geri dönmeyi unutmayın!", @@ -452,6 +463,7 @@ "overwrite_amount": "Miktarın üzerine yaz", "pairingInvalidEvent": "Geçersiz Etkinliği Eşleştirme", "passphrase": "Passfrase (isteğe bağlı)", + "passphrases_doesnt_match": "Passfrases eşleşmiyor, lütfen tekrar deneyin", "password": "Parola", "paste": "Yapıştır", "pause_wallet_creation": "Haven Cüzdanı oluşturma yeteneği şu anda duraklatıldı.", @@ -814,6 +826,7 @@ "trusted": "Güvenilir", "tx_commit_exception_no_dust_on_change": "İşlem bu miktarla reddedilir. Bu madeni paralarla değişiklik yapmadan ${min} veya değişikliği döndüren ${max} gönderebilirsiniz.", "tx_commit_failed": "İşlem taahhüdü başarısız oldu. Lütfen Destek ile iletişime geçin.", + "tx_commit_failed_no_peers": "İşlem yayın yapamadı, lütfen bir saniye içinde tekrar deneyin", "tx_invalid_input": "Bu tür ödeme için yanlış giriş türünü kullanıyorsunuz", "tx_no_dust_exception": "İşlem, çok küçük bir miktar gönderilerek reddedilir. Lütfen miktarı artırmayı deneyin.", "tx_not_enough_inputs_exception": "Yeterli giriş yok. Lütfen madeni para kontrolü altında daha fazlasını seçin", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 05fc114a7..ccbc995a1 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Ця дія видалить гаманець. Ви хочете продовжити?", "confirm_fee_deduction": "Підтвердьте відрахування комісії", "confirm_fee_deduction_content": "Чи погоджуєтесь ви вирахувати комісію з сумми одержувача?", + "confirm_passphrase": "Підтвердьте пасфрази", "confirm_sending": "Підтвердити відправлення", "confirm_silent_payments_switch_node": "Ваш поточний вузол не підтримує мовчазні платежі \\ ncake Wallet перейде на сумісний вузол, лише для сканування", "confirmations": "Підтвердження", @@ -362,6 +363,14 @@ "ledger_error_wrong_app": "Будь ласка, переконайтеся, що ви відкриваєте потрібну програму на своїй книзі", "ledger_please_enable_bluetooth": "Будь ласка, ввімкніть Bluetooth виявити свою книгу", "light_theme": "Світла", + "litecoin_enable_mweb_sync": "Увімкнути сканування MWEB", + "litecoin_mweb": "Мвеб", + "litecoin_mweb_always_scan": "Встановити mweb завжди сканувати", + "litecoin_mweb_display_card": "Показати карту MWeb", + "litecoin_mweb_scanning": "Сканування Mweb", + "litecoin_mweb_settings": "Налаштування MWEB", + "litecoin_mweb_warning": "Використання MWEB спочатку завантажить ~ 600 Мб даних і може зайняти до 30 хвилин залежно від швидкості мережі. Ці початкові дані завантажуються лише один раз і будуть доступні для всіх гаманців Litecoin", + "litecoin_what_is_mweb": "Що таке mweb?", "live_fee_rates": "Ставки плати за живий через API", "load_more": "Завантажити ще", "loading_your_wallet": "Завантаження гаманця", @@ -392,6 +401,8 @@ "monero_light_theme": "Легка тема Monero", "moonpay_alert_text": "Значення суми має бути більшим або дорівнювати ${minAmount} ${fiatCurrency}", "more_options": "Більше параметрів", + "mweb_confirmed": "Підтвердив Mweb", + "mweb_unconfirmed": "Неперевірений MWEB", "name": "Ім'я", "nano_current_rep": "Поточний представник", "nano_gpt_thanks_message": "Дякуємо за використання наногпта! Не забудьте повернутися до браузера після завершення транзакції!", @@ -452,6 +463,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Недійсна подія сполучення", "passphrase": "Пасофрази (необов’язково)", + "passphrases_doesnt_match": "Пасофрази не відповідають, спробуйте ще раз", "password": "Пароль", "paste": "Вставити", "pause_wallet_creation": "Можливість створення гаманця Haven зараз призупинено.", @@ -815,6 +827,7 @@ "trusted": "довіряють", "tx_commit_exception_no_dust_on_change": "Транзакція відхилена цією сумою. За допомогою цих монет ви можете надіслати ${min} без змін або ${max}, що повертає зміни.", "tx_commit_failed": "Транзакційна комісія не вдалося. Будь ласка, зв'яжіться з підтримкою.", + "tx_commit_failed_no_peers": "Транзакція не вдалося транслювати, спробуйте ще раз за секунду або близько того", "tx_invalid_input": "Ви використовуєте неправильний тип введення для цього типу оплати", "tx_no_dust_exception": "Угода відхиляється, відправивши суму занадто мала. Будь ласка, спробуйте збільшити суму.", "tx_not_enough_inputs_exception": "Недостатньо доступних входів. Виберіть більше під контролем монети", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index f6deb1c9f..4781c1e00 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "اس کارروائی سے یہ پرس حذف ہو جائے گا۔ کیا آپ جاری رکھنا چاہتے ہیں؟", "confirm_fee_deduction": "فیس میں کٹوتی کی تصدیق کریں", "confirm_fee_deduction_content": "کیا آپ آؤٹ پٹ سے فیس کم کرنے پر راضی ہیں؟", + "confirm_passphrase": "پاسفریز کی تصدیق کریں", "confirm_sending": "بھیجنے کی تصدیق کریں۔", "confirm_silent_payments_switch_node": "آپ کا موجودہ نوڈ خاموش ادائیگیوں کی حمایت نہیں کرتا ہے۔", "confirmations": "تصدیقات", @@ -362,6 +363,14 @@ "ledger_error_wrong_app": "براہ کرم یقینی بنائیں کہ آپ اپنے لیجر پر صحیح ایپ کو کھولتے ہیں", "ledger_please_enable_bluetooth": "براہ کرم بلوٹوتھ کو اپنے لیجر کا پتہ لگانے کے لئے اہل بنائیں", "light_theme": "روشنی", + "litecoin_enable_mweb_sync": "MWEB اسکیننگ کو فعال کریں", + "litecoin_mweb": "MWEB", + "litecoin_mweb_always_scan": "MWEB ہمیشہ اسکیننگ سیٹ کریں", + "litecoin_mweb_display_card": "MWEB کارڈ دکھائیں", + "litecoin_mweb_scanning": "MWEB اسکیننگ", + "litecoin_mweb_settings": "MWEB کی ترتیبات", + "litecoin_mweb_warning": "MWEB کا استعمال ابتدائی طور پر m 600mb ڈیٹا ڈاؤن لوڈ کرے گا ، اور نیٹ ورک کی رفتار کے لحاظ سے 30 منٹ تک کا وقت لگ سکتا ہے۔ یہ ابتدائی اعداد و شمار صرف ایک بار ڈاؤن لوڈ کریں گے اور تمام لیٹیکوئن بٹوے کے لئے دستیاب ہوں گے", + "litecoin_what_is_mweb": "MWEB کیا ہے؟", "live_fee_rates": "API کے ذریعے براہ راست فیس کی شرح", "load_more": "مزید لوڈ کریں", "loading_your_wallet": "آپ کا بٹوہ لوڈ ہو رہا ہے۔", @@ -392,6 +401,8 @@ "monero_light_theme": "مونیرو لائٹ تھیم", "moonpay_alert_text": "رقم کی قدر ${minAmount} ${fiatCurrency} کے برابر یا زیادہ ہونی چاہیے۔", "more_options": "مزید زرائے", + "mweb_confirmed": "تصدیق شدہ MWEB", + "mweb_unconfirmed": "غیر مصدقہ MWEB", "name": "ﻡﺎﻧ", "nano_current_rep": "موجودہ نمائندہ", "nano_gpt_thanks_message": "نانوگپٹ استعمال کرنے کا شکریہ! اپنے لین دین کی تکمیل کے بعد براؤزر کی طرف واپس جانا یاد رکھیں!", @@ -454,6 +465,7 @@ "overwrite_amount": "رقم کو اوور رائٹ کریں۔", "pairingInvalidEvent": "ﭧﻧﻮﯾﺍ ﻂﻠﻏ ﺎﻧﺎﻨﺑ ﺍﮌﻮﺟ", "passphrase": "پاسفریز (اختیاری)", + "passphrases_doesnt_match": "پاسفریز مماثل نہیں ہیں ، براہ کرم دوبارہ کوشش کریں", "password": "پاس ورڈ", "paste": "چسپاں کریں۔", "pause_wallet_creation": "Haven Wallet ۔ﮯﮨ ﻑﻮﻗﻮﻣ ﻝﺎﺤﻟﺍ ﯽﻓ ﺖﯿﻠﮨﺍ ﯽﮐ ﮯﻧﺎﻨﺑ", @@ -816,6 +828,7 @@ "trusted": "قابل اعتماد", "tx_commit_exception_no_dust_on_change": "اس رقم سے لین دین کو مسترد کردیا گیا ہے۔ ان سککوں کے ذریعہ آپ بغیر کسی تبدیلی کے ${min} یا ${max} بھیج سکتے ہیں جو لوٹتے ہیں۔", "tx_commit_failed": "ٹرانزیکشن کمٹ ناکام ہوگیا۔ براہ کرم سپورٹ سے رابطہ کریں۔", + "tx_commit_failed_no_peers": "ٹرانزیکشن نشر کرنے میں ناکام ، براہ کرم ایک سیکنڈ یا اس میں دوبارہ کوشش کریں", "tx_invalid_input": "آپ اس قسم کی ادائیگی کے لئے غلط ان پٹ کی قسم استعمال کررہے ہیں", "tx_no_dust_exception": "لین دین کو بہت چھوٹی رقم بھیج کر مسترد کردیا جاتا ہے۔ براہ کرم رقم میں اضافہ کرنے کی کوشش کریں۔", "tx_not_enough_inputs_exception": "کافی ان پٹ دستیاب نہیں ہے۔ براہ کرم سکے کے کنٹرول میں مزید منتخب کریں", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index a21da9e68..f4ed7aebb 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -144,6 +144,7 @@ "confirm_fee_dedction_content": "Bạn có đồng ý trừ phí từ đầu ra không?", "confirm_fee_deduction": "Xác nhận Khấu trừ Phí", "confirm_fee_deduction_content": "Bạn có đồng ý khấu trừ phí từ đầu ra không?", + "confirm_passphrase": "Xác nhận cụm mật khẩu", "confirm_sending": "Xác nhận gửi", "confirm_silent_payments_switch_node": "Nút hiện tại của bạn không hỗ trợ thanh toán im lặng\\nCake Wallet sẽ chuyển sang một nút tương thích chỉ để quét", "confirmations": "Xác nhận", @@ -393,6 +394,8 @@ "monero_light_theme": "Chủ đề sáng Monero", "moonpay_alert_text": "Giá trị số tiền phải lớn hơn hoặc bằng ${minAmount} ${fiatCurrency}", "more_options": "Thêm tùy chọn", + "mweb_confirmed": "Xác nhận MWEB", + "mweb_unconfirmed": "MWEB chưa được xác nhận", "name": "Tên", "nano_current_rep": "Đại diện hiện tại", "nano_gpt_thanks_message": "Cảm ơn bạn đã sử dụng NanoGPT! Hãy nhớ quay lại trình duyệt sau khi giao dịch của bạn hoàn tất!", @@ -452,6 +455,7 @@ "overwrite_amount": "Ghi đè số tiền", "pairingInvalidEvent": "Sự kiện ghép nối không hợp lệ", "passphrase": "Cụm từ bảo mật (Tùy chọn)", + "passphrases_doesnt_match": "Vòng thông không khớp, vui lòng thử lại", "password": "Mật khẩu", "paste": "Dán", "pause_wallet_creation": "Khả năng tạo ví Haven hiện đang bị tạm dừng.", @@ -813,6 +817,7 @@ "trusted": "Đã tin cậy", "tx_commit_exception_no_dust_on_change": "Giao dịch bị từ chối với số tiền này. Với số tiền này bạn có thể gửi ${min} mà không cần đổi tiền lẻ hoặc ${max} trả lại tiền lẻ.", "tx_commit_failed": "Giao dịch không thành công. Vui lòng liên hệ với hỗ trợ.", + "tx_commit_failed_no_peers": "Giao dịch không phát sóng, vui lòng thử lại trong một giây hoặc lâu hơn", "tx_invalid_input": "Bạn đang sử dụng loại đầu vào sai cho loại thanh toán này", "tx_no_dust_exception": "Giao dịch bị từ chối vì gửi một số tiền quá nhỏ. Vui lòng thử tăng số tiền.", "tx_not_enough_inputs_exception": "Không đủ đầu vào có sẵn. Vui lòng chọn thêm dưới Coin Control", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 28e18cbb9..ea0b64e69 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "Ìṣe yìí máa yọ àpamọ́wọ́ yìí kúrò. Ṣé ẹ fẹ́ tẹ̀síwájú?", "confirm_fee_deduction": "Jẹrisi iyọkuro owo", "confirm_fee_deduction_content": "Ṣe o gba lati yọkuro idiyele naa kuro ni iṣejade?", + "confirm_passphrase": "Jẹrisi kọwe", "confirm_sending": "Jẹ́rìí sí ránṣẹ́", "confirm_silent_payments_switch_node": "Ilode rẹ ti lọwọlọwọ ko ṣe atilẹyin awọn sisanwo ti o dakẹ \\ owet apamọwọ yoo yipada si oju-ọrọ ibaramu, o kan fun Scning", "confirmations": "Àwọn ẹ̀rí", @@ -363,6 +364,14 @@ "ledger_error_wrong_app": "Jọwọ rii daju pe iwọ yoo sọ app ti o tọ loju omi rẹ", "ledger_please_enable_bluetooth": "Jọwọ jẹ ki Bluetooth lati rii iṣupọ rẹ", "light_theme": "Funfun bí eérú", + "litecoin_enable_mweb_sync": "Mu mweb ọlọjẹ", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Ṣeto mweb nigbagbogbo n ṣayẹwo", + "litecoin_mweb_display_card": "Fihan kaadi Mweb", + "litecoin_mweb_scanning": "Mweb scanning", + "litecoin_mweb_settings": "Awọn eto Mweb", + "litecoin_mweb_warning": "Lilo Mweb yoo wa lakoko igbasilẹ ~ 600MB ti data, o le gba to iṣẹju 30 da lori iyara nẹtiwọọki. Awọn data akọkọ yii yoo ṣe igbasilẹ lẹẹkan si ki o wa fun gbogbo awọn Wolinkun LiveCooin", + "litecoin_what_is_mweb": "Kini mweb?", "live_fee_rates": "Awọn oṣuwọn Owo laaye laaye nipasẹ API", "load_more": "Ẹru diẹ sii", "loading_your_wallet": "A ń ṣí àpamọ́wọ́ yín", @@ -393,6 +402,8 @@ "monero_light_theme": "Monero Light Akori", "moonpay_alert_text": "Iye owó kò gbọ́dọ̀ kéré ju ${minAmount} ${fiatCurrency}", "more_options": "Ìyàn àfikún", + "mweb_confirmed": "Jẹrisi Mweb", + "mweb_unconfirmed": "Ajopo Mweb", "name": "Oruko", "nano_current_rep": "Aṣoju lọwọlọwọ", "nano_gpt_thanks_message": "O ṣeun fun lilo Nonnogt! Ranti lati tẹle pada si ẹrọ lilọ kiri ayelujara lẹhin iṣowo rẹ pari!", @@ -453,6 +464,7 @@ "overwrite_amount": "Pààrọ̀ iye owó", "pairingInvalidEvent": "Pipọpọ Iṣẹlẹ Ti ko tọ", "passphrase": "Ọrọ kukuru (iyan)", + "passphrases_doesnt_match": "Awọn ọrọ kukuru ko baamu, jọwọ gbiyanju lẹẹkansi", "password": "Ọ̀rọ̀ aṣínà", "paste": "Fikún ẹ̀dà yín", "pause_wallet_creation": "Agbara lati ṣẹda Haven Wallet ti wa ni idaduro lọwọlọwọ.", @@ -815,6 +827,7 @@ "trusted": "A ti fọkàn ẹ̀ tán", "tx_commit_exception_no_dust_on_change": "Iṣowo naa ti kọ pẹlu iye yii. Pẹlu awọn owó wọnyi o le firanṣẹ ${min} laisi ayipada tabi ${max} ni iyipada iyipada.", "tx_commit_failed": "Idunadura iṣowo kuna. Jọwọ kan si atilẹyin.", + "tx_commit_failed_no_peers": "Idunadura kuna lati wa igbohungbe, jọwọ gbiyanju lẹẹkansi ni iṣẹju keji tabi bẹẹ", "tx_invalid_input": "O nlo iru titẹ nkan ti ko tọ fun iru isanwo yii", "tx_no_dust_exception": "Iṣowo naa ni kọ nipa fifiranṣẹ iye ti o kere ju. Jọwọ gbiyanju pọ si iye naa.", "tx_not_enough_inputs_exception": "Ko to awọn titẹsi to. Jọwọ yan diẹ sii labẹ iṣakoso owo", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index e340709d6..ce97c091e 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -143,6 +143,7 @@ "confirm_delete_wallet": "此操作将刪除此钱包。确定吗?", "confirm_fee_deduction": "确认费用扣除", "confirm_fee_deduction_content": "您是否同意从产出中扣除费用?", + "confirm_passphrase": "确认密码", "confirm_sending": "确认发送", "confirm_silent_payments_switch_node": "您当前的节点不支持无声付款\\ ncake钱包将切换到兼容节点,仅用于扫描", "confirmations": "确认", @@ -362,6 +363,14 @@ "ledger_error_wrong_app": "请确保您在分类帐中操作正确的应用程序", "ledger_please_enable_bluetooth": "请启用蓝牙来检测您的分类帐", "light_theme": "艳丽", + "litecoin_enable_mweb_sync": "启用MWEB扫描", + "litecoin_mweb": "MWEB", + "litecoin_mweb_always_scan": "设置MWEB总是扫描", + "litecoin_mweb_display_card": "显示MWEB卡", + "litecoin_mweb_scanning": "MWEB扫描", + "litecoin_mweb_settings": "MWEB设置", + "litecoin_mweb_warning": "使用MWEB最初将下载约600MB的数据,并且最多可能需要30分钟的时间,具体取决于网络速度。此初始数据只能下载一次,并适用于所有莱特币钱包", + "litecoin_what_is_mweb": "什么是MWEB?", "live_fee_rates": "通过API的实时费率", "load_more": "装载更多", "loading_your_wallet": "加载您的钱包", @@ -392,6 +401,8 @@ "monero_light_theme": "门罗币浅色主题", "moonpay_alert_text": "金额的价值必须大于或等于 ${minAmount} ${fiatCurrency}", "more_options": "更多选项", + "mweb_confirmed": "确认的MWEB", + "mweb_unconfirmed": "未经证实的MWEB", "name": "姓名", "nano_current_rep": "当前代表", "nano_gpt_thanks_message": "感谢您使用Nanogpt!事务完成后,请记住回到浏览器!", @@ -452,6 +463,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "配对无效事件", "passphrase": "密码(可选)", + "passphrases_doesnt_match": "密码不匹配,请重试", "password": "密码", "paste": "粘贴", "pause_wallet_creation": "创建 Haven 钱包的功能当前已暂停。", @@ -814,6 +826,7 @@ "trusted": "值得信赖", "tx_commit_exception_no_dust_on_change": "交易被此金额拒绝。使用这些硬币,您可以发送${min}无需更改或返回${max}的变化。", "tx_commit_failed": "交易承诺失败。请联系支持。", + "tx_commit_failed_no_peers": "交易无法广播,请在一秒钟左右的时间内重试", "tx_invalid_input": "您正在使用错误的输入类型进行此类付款", "tx_no_dust_exception": "通过发送太小的金额来拒绝交易。请尝试增加金额。", "tx_not_enough_inputs_exception": "没有足够的输入。请在硬币控制下选择更多", diff --git a/scripts/android/build_all.sh b/scripts/android/build_all.sh index ec70f02a6..ad4ec984b 100755 --- a/scripts/android/build_all.sh +++ b/scripts/android/build_all.sh @@ -10,6 +10,7 @@ DIR=$(dirname "$0") case $APP_ANDROID_TYPE in "monero.com") $DIR/build_monero_all.sh ;; "cakewallet") $DIR/build_monero_all.sh - $DIR/build_haven_all.sh ;; + $DIR/build_haven_all.sh + $DIR/build_mwebd.sh ;; "haven") $DIR/build_haven_all.sh ;; esac diff --git a/scripts/android/build_mwebd.sh b/scripts/android/build_mwebd.sh new file mode 100755 index 000000000..90dbc4c20 --- /dev/null +++ b/scripts/android/build_mwebd.sh @@ -0,0 +1,22 @@ +if [[ "$1" == "--dont-install" ]]; then + echo "Skipping Go installation as per --dont-install flag" +else + # install go > 1.23: + wget https://go.dev/dl/go1.23.1.linux-amd64.tar.gz + sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.1.linux-amd64.tar.gz + export PATH=$PATH:/usr/local/go/bin + export PATH=$PATH:~/go/bin + go install golang.org/x/mobile/cmd/gomobile@latest + gomobile init +fi + +# build mwebd: +git clone https://github.com/ltcmweb/mwebd +cd mwebd +git reset --hard f6ea8a9e3d348b01bb44f03a1cc4ad65b0abe935 +gomobile bind -target=android -androidapi 21 . +mkdir -p ../../../cw_mweb/android/libs/ +mv ./mwebd.aar $_ +# cleanup: +cd .. +rm -rf mwebd \ No newline at end of file diff --git a/scripts/ios/build_all.sh b/scripts/ios/build_all.sh index 565679e2d..ba5c55a1f 100755 --- a/scripts/ios/build_all.sh +++ b/scripts/ios/build_all.sh @@ -9,6 +9,6 @@ DIR=$(dirname "$0") case $APP_IOS_TYPE in "monero.com") $DIR/build_monero_all.sh ;; - "cakewallet") $DIR/build_monero_all.sh && $DIR/build_haven.sh ;; + "cakewallet") $DIR/build_monero_all.sh && $DIR/build_haven.sh && $DIR/build_mwebd.sh ;; "haven") $DIR/build_haven_all.sh ;; esac diff --git a/scripts/ios/build_mwebd.sh b/scripts/ios/build_mwebd.sh new file mode 100755 index 000000000..f0fa64605 --- /dev/null +++ b/scripts/ios/build_mwebd.sh @@ -0,0 +1,20 @@ +#!/bin/bash +if [[ "$1" == "--dont-install" ]]; then + echo "Skipping Go installation as per --dont-install flag" +else + # install go > 1.23: + brew install go + export PATH=$PATH:~/go/bin + go install golang.org/x/mobile/cmd/gomobile@latest + gomobile init +fi + +# build mwebd: +git clone https://github.com/ltcmweb/mwebd +cd mwebd +git reset --hard f6ea8a9e3d348b01bb44f03a1cc4ad65b0abe935 +gomobile bind -target=ios . +mv -fn ./Mwebd.xcframework ../../../ios/ +# cleanup: +cd .. +rm -rf mwebd \ No newline at end of file diff --git a/scripts/prepare_moneroc.sh b/scripts/prepare_moneroc.sh index 1315a5825..24f4d201c 100755 --- a/scripts/prepare_moneroc.sh +++ b/scripts/prepare_moneroc.sh @@ -8,7 +8,7 @@ if [[ ! -d "monero_c" ]]; then git clone https://github.com/mrcyjanek/monero_c --branch rewrite-wip cd monero_c - git checkout 3cb38bee9385faf46b03fd73aab85f3ac4115bf7 + git checkout 6eb571ea498ed7b854934785f00fabfd0dadf75b git reset --hard git submodule update --init --force --recursive ./apply_patches.sh monero diff --git a/tool/append_translation.dart b/tool/append_translation.dart index 24778746c..d196421e9 100644 --- a/tool/append_translation.dart +++ b/tool/append_translation.dart @@ -5,13 +5,14 @@ import 'utils/translation/translation_utils.dart'; /// flutter packages pub run tool/append_translation.dart "hello_world" "Hello World!" void main(List args) async { - if (args.length != 2) { + if (args.length < 2) { throw Exception( - 'Insufficient arguments!\n\nTry to run `./append_translation.dart greetings "Hello World!"`'); + 'Insufficient arguments!\n\nTry to run `./append_translation.dart "greetings" "Hello World!"`'); } final name = args.first; - final text = args.last; + final text = args[1]; + final force = args.last == "--force"; print('Appending "$name": "$text"'); @@ -20,7 +21,7 @@ void main(List args) async { final fileName = getArbFileName(lang); final translation = await getTranslation(text, lang); - appendStringToArbFile(fileName, name, translation); + appendStringToArbFile(fileName, name, translation, force: force); } print('Alphabetizing all files...'); diff --git a/tool/configure.dart b/tool/configure.dart index 815c9e399..d54ec153d 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -115,6 +115,7 @@ import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/litecoin_wallet_service.dart'; +import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_bitcoin/bitcoin_hardware_wallet_service.dart'; @@ -180,8 +181,8 @@ abstract class Bitcoin { List getUnspents(Object wallet); Future updateUnspents(Object wallet); WalletService createBitcoinWalletService( - Box walletInfoSource, Box unspentCoinSource, bool alwaysScan, bool isDirect); - WalletService createLitecoinWalletService(Box walletInfoSource, Box unspentCoinSource, bool isDirect); + Box walletInfoSource, Box unspentCoinSource, bool alwaysScan, bool isDirect); + WalletService createLitecoinWalletService(Box walletInfoSource, Box unspentCoinSource, bool alwaysScan, bool isDirect); TransactionPriority getBitcoinTransactionPriorityMedium(); TransactionPriority getBitcoinTransactionPriorityCustom(); TransactionPriority getLitecoinTransactionPriorityMedium(); @@ -195,6 +196,7 @@ abstract class Bitcoin { Future setAddressType(Object wallet, dynamic option); ReceivePageOption getSelectedAddressType(Object wallet); List getBitcoinReceivePageOptions(); + List getLitecoinReceivePageOptions(); BitcoinAddressType getBitcoinAddressType(ReceivePageOption option); bool hasSelectedSilentPayments(Object wallet); bool isBitcoinReceivePageOption(ReceivePageOption option); @@ -214,16 +216,19 @@ abstract class Bitcoin { int feeAmountWithFeeRate(Object wallet, int feeRate, int inputsCount, int outputsCount, {int? size}); Future checkIfMempoolAPIIsEnabled(Object wallet); Future getHeightByDate({required DateTime date, bool? bitcoinMempoolAPIEnabled}); + int getLitecoinHeightByDate({required DateTime date}); Future rescan(Object wallet, {required int height, bool? doSingleScan}); Future getNodeIsElectrsSPEnabled(Object wallet); void deleteSilentPaymentAddress(Object wallet, String address); Future updateFeeRates(Object wallet); int getMaxCustomFeeRate(Object wallet); - void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device); Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); List updateOutputs(PendingTransaction pendingTransaction, List outputs); bool txIsReceivedSilentPayment(TransactionInfo txInfo); + bool txIsMweb(TransactionInfo txInfo); + Future setMwebEnabled(Object wallet, bool enabled); + bool getMwebEnabled(Object wallet); } """; @@ -298,10 +303,14 @@ class Subaddress { Subaddress({ required this.id, required this.label, - required this.address}); + required this.address, + required this.received, + required this.txCount}); final int id; final String label; final String address; + final String? received; + final int txCount; } class MoneroBalance extends Balance { diff --git a/tool/utils/translation/arb_file_utils.dart b/tool/utils/translation/arb_file_utils.dart index b54dab423..414d318dd 100644 --- a/tool/utils/translation/arb_file_utils.dart +++ b/tool/utils/translation/arb_file_utils.dart @@ -1,11 +1,11 @@ import 'dart:convert'; import 'dart:io'; -void appendStringToArbFile(String fileName, String name, String text) { +void appendStringToArbFile(String fileName, String name, String text, {bool force = false}) { final file = File(fileName); final arbObj = readArbFile(file); - if (arbObj.containsKey(name)) { + if (arbObj.containsKey(name) && !force) { print("String $name already exists in $fileName!"); return; }