From 8084f490b593b4dd74956ab00cf516213a69762b Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Thu, 16 Nov 2023 00:12:23 +0100 Subject: [PATCH] Cw 467 mark change outputs in unspent outputs list (#1137) * CW-490 Use native Coin Freeze * CW-467 Code cleanup * CW-467 Fix native Code * CW-467 Extend Unspend * CW-467 Add isChange * CW-467 Minor Fixes * CW-467 Add isChange to Electrum Unspents * CW-467 Localize Change Tag * CW-467 Fix frozen balance showing on other monero wallets * CW-467 Fix frozen balance showing on other monero wallets --- cw_bitcoin/lib/electrum_wallet.dart | 116 +++++++++--------- cw_core/lib/unspent_coins_info.dart | 10 +- cw_core/lib/unspent_transaction_output.dart | 2 + cw_monero/ios/Classes/monero_api.cpp | 15 +++ cw_monero/lib/api/coins_info.dart | 12 ++ cw_monero/lib/api/signatures.dart | 7 ++ cw_monero/lib/api/transaction_history.dart | 22 +++- cw_monero/lib/api/types.dart | 7 ++ cw_monero/lib/monero_unspent.dart | 36 +++--- cw_monero/lib/monero_wallet.dart | 31 +++-- lib/monero/cw_monero.dart | 5 +- .../unspent_coins_list_page.dart | 81 +++--------- .../widgets/unspent_coins_list_item.dart | 46 ++++--- .../dashboard/balance_view_model.dart | 18 +-- .../unspent_coins/unspent_coins_item.dart | 4 + .../unspent_coins_list_view_model.dart | 59 +++------ res/values/strings_ar.arb | 3 +- res/values/strings_bg.arb | 3 +- res/values/strings_cs.arb | 3 +- res/values/strings_de.arb | 3 +- res/values/strings_en.arb | 3 +- res/values/strings_es.arb | 3 +- res/values/strings_fr.arb | 3 +- res/values/strings_ha.arb | 3 +- res/values/strings_hi.arb | 3 +- res/values/strings_hr.arb | 3 +- res/values/strings_id.arb | 3 +- res/values/strings_it.arb | 3 +- res/values/strings_ja.arb | 3 +- res/values/strings_ko.arb | 3 +- res/values/strings_my.arb | 3 +- res/values/strings_nl.arb | 3 +- res/values/strings_pl.arb | 3 +- res/values/strings_pt.arb | 3 +- res/values/strings_ru.arb | 3 +- res/values/strings_th.arb | 3 +- res/values/strings_tl.arb | 3 +- res/values/strings_tr.arb | 3 +- res/values/strings_uk.arb | 3 +- res/values/strings_ur.arb | 3 +- res/values/strings_yo.arb | 3 +- res/values/strings_zh.arb | 3 +- 42 files changed, 292 insertions(+), 257 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 804b5337..05486aa2 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -2,41 +2,41 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; -import 'package:cw_core/pending_transaction.dart'; -import 'package:cw_core/unspent_coins_info.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:hive/hive.dart'; -import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; -import 'package:mobx/mobx.dart'; -import 'package:rxdart/subjects.dart'; -import 'package:flutter/foundation.dart'; + import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; -import 'package:cw_bitcoin/electrum_transaction_info.dart'; -import 'package:cw_core/pathForWallet.dart'; +import 'package:collection/collection.dart'; import 'package:cw_bitcoin/address_to_output_script.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; -import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; -import 'package:cw_bitcoin/electrum_transaction_history.dart'; import 'package:cw_bitcoin/bitcoin_transaction_no_inputs_exception.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_transaction_wrong_balance_exception.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/bitcoin_wallet_keys.dart'; +import 'package:cw_bitcoin/electrum.dart'; +import 'package:cw_bitcoin/electrum_balance.dart'; +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/file.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/script_hash.dart'; import 'package:cw_bitcoin/utils.dart'; -import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/node.dart'; +import 'package:cw_core/pathForWallet.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/transaction_priority.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:cw_bitcoin/electrum.dart'; +import 'package:flutter/foundation.dart'; import 'package:hex/hex.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:collection/collection.dart'; -import 'package:bip32/bip32.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; +import 'package:rxdart/subjects.dart'; part 'electrum_wallet.g.dart'; @@ -47,18 +47,18 @@ abstract class ElectrumWalletBase with Store { ElectrumWalletBase( {required String password, - required WalletInfo walletInfo, - required Box unspentCoinsInfo, - required this.networkType, - required this.mnemonic, - required Uint8List seedBytes, - List? initialAddresses, - ElectrumClient? electrumClient, - ElectrumBalance? initialBalance, - CryptoCurrency? currency}) + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + required this.networkType, + required this.mnemonic, + required Uint8List seedBytes, + List? initialAddresses, + ElectrumClient? electrumClient, + ElectrumBalance? initialBalance, + CryptoCurrency? currency}) : hd = currency == CryptoCurrency.bch - ? bitcoinCashHDWallet(seedBytes) - : bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/0"), + ? bitcoinCashHDWallet(seedBytes) + : bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/0"), syncStatus = NotConnectedSyncStatus(), _password = password, _feeRates = [], @@ -67,9 +67,9 @@ abstract class ElectrumWalletBase _scripthashesUpdateSubject = {}, balance = ObservableMap.of(currency != null ? { - currency: - initialBalance ?? const ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0) - } + currency: + initialBalance ?? const ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0) + } : {}), this.unspentCoinsInfo = unspentCoinsInfo, super(walletInfo) { @@ -79,8 +79,7 @@ abstract class ElectrumWalletBase } static bitcoin.HDWallet bitcoinCashHDWallet(Uint8List seedBytes) => - bitcoin.HDWallet.fromSeed(seedBytes) - .derivePath("m/44'/145'/0'/0"); + bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'/0"); static int estimatedTransactionSize(int inputsCount, int outputsCounts) => inputsCount * 146 + outputsCounts * 33 + 8; @@ -294,10 +293,12 @@ abstract class ElectrumWalletBase if (input.isP2wpkh) { final p2wpkh = bitcoin .P2WPKH( - data: generatePaymentData( - hd: input.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, - index: input.bitcoinAddressRecord.index), - network: networkType) + data: generatePaymentData( + hd: input.bitcoinAddressRecord.isHidden + ? walletAddresses.sideHd + : walletAddresses.mainHd, + index: input.bitcoinAddressRecord.index), + network: networkType) .data; txb.addInput(input.hash, input.vout, null, p2wpkh.output); @@ -347,12 +348,12 @@ abstract class ElectrumWalletBase } String toJSON() => json.encode({ - 'mnemonic': mnemonic, - 'account_index': walletAddresses.currentReceiveAddressIndex.toString(), - 'change_address_index': walletAddresses.currentChangeAddressIndex.toString(), - 'addresses': walletAddresses.addresses.map((addr) => addr.toJSON()).toList(), - 'balance': balance[currency]?.toJSON() - }); + 'mnemonic': mnemonic, + 'account_index': walletAddresses.currentReceiveAddressIndex.toString(), + 'change_address_index': walletAddresses.currentChangeAddressIndex.toString(), + 'addresses': walletAddresses.addresses.map((addr) => addr.toJSON()).toList(), + 'balance': balance[currency]?.toJSON() + }); int feeRate(TransactionPriority priority) { try { @@ -367,7 +368,7 @@ abstract class ElectrumWalletBase } int feeAmountForPriority( - BitcoinTransactionPriority priority, int inputsCount, int outputsCount) => + BitcoinTransactionPriority priority, int inputsCount, int outputsCount) => feeRate(priority) * estimatedTransactionSize(inputsCount, outputsCount); int feeAmountWithFeeRate(int feeRate, int inputsCount, int outputsCount) => @@ -467,18 +468,20 @@ abstract class ElectrumWalletBase Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); Future updateUnspent() async { - final unspent = await Future.wait(walletAddresses - .addresses.map((address) => electrumClient + final unspent = await Future.wait(walletAddresses.addresses.map((address) => electrumClient .getListUnspentWithAddress(address.address, networkType) - .then((unspent) => unspent - .map((unspent) { - try { - return BitcoinUnspent.fromJSON(address, unspent); - } catch(_) { - return null; - } - }).whereNotNull()))); + .then((unspent) => unspent.map((unspent) { + try { + return BitcoinUnspent.fromJSON(address, unspent); + } catch (_) { + return null; + } + }).whereNotNull()))); unspentCoins = unspent.expand((e) => e).toList(); + unspentCoins.forEach((coin) async { + final tx = await fetchTransactionInfo(hash: coin.hash, height: 0); + coin.isChange = tx!.direction == TransactionDirection.outgoing; + }); if (unspentCoinsInfo.isEmpty) { unspentCoins.forEach((coin) => _addCoinInfo(coin)); @@ -515,6 +518,7 @@ abstract class ElectrumWalletBase address: coin.bitcoinAddressRecord.address, value: coin.value, vout: coin.vout, + isChange: coin.isChange, ); await unspentCoinsInfo.add(newInfo); @@ -524,7 +528,7 @@ abstract class ElectrumWalletBase try { final List keys = []; final currentWalletUnspentCoins = - unspentCoinsInfo.values.where((element) => element.walletId.contains(id)); + unspentCoinsInfo.values.where((element) => element.walletId.contains(id)); if (currentWalletUnspentCoins.isNotEmpty) { currentWalletUnspentCoins.forEach((element) { @@ -657,7 +661,7 @@ abstract class ElectrumWalletBase final addresses = walletAddresses.addresses.toList(); final balanceFutures = >>[]; for (var i = 0; i < addresses.length; i++) { - final addressRecord = addresses[i] ; + final addressRecord = addresses[i]; final sh = scriptHash(addressRecord.address, networkType: networkType); final balanceFuture = electrumClient.getBalance(sh); balanceFutures.add(balanceFuture); diff --git a/cw_core/lib/unspent_coins_info.dart b/cw_core/lib/unspent_coins_info.dart index 68bbcbfd..25abd3e4 100644 --- a/cw_core/lib/unspent_coins_info.dart +++ b/cw_core/lib/unspent_coins_info.dart @@ -14,7 +14,9 @@ class UnspentCoinsInfo extends HiveObject { required this.address, required this.vout, required this.value, - this.keyImage = null + this.keyImage = null, + this.isChange = false, + this.accountIndex = 0 }); static const typeId = UNSPENT_COINS_INFO_TYPE_ID; @@ -47,6 +49,12 @@ class UnspentCoinsInfo extends HiveObject { @HiveField(8, defaultValue: null) String? keyImage; + + @HiveField(9, defaultValue: false) + bool isChange; + + @HiveField(10, defaultValue: 0) + int accountIndex; String get note => noteRaw ?? ''; diff --git a/cw_core/lib/unspent_transaction_output.dart b/cw_core/lib/unspent_transaction_output.dart index 6827f4c0..b52daf43 100644 --- a/cw_core/lib/unspent_transaction_output.dart +++ b/cw_core/lib/unspent_transaction_output.dart @@ -2,6 +2,7 @@ class Unspent { Unspent(this.address, this.hash, this.value, this.vout, this.keyImage) : isSending = true, isFrozen = false, + isChange = false, note = ''; final String address; @@ -10,6 +11,7 @@ class Unspent { final int vout; final String? keyImage; + bool isChange; bool isSending; bool isFrozen; String note; diff --git a/cw_monero/ios/Classes/monero_api.cpp b/cw_monero/ios/Classes/monero_api.cpp index e04282fe..a0712255 100644 --- a/cw_monero/ios/Classes/monero_api.cpp +++ b/cw_monero/ios/Classes/monero_api.cpp @@ -841,6 +841,12 @@ extern "C" return m_transaction_history->count(); } + TransactionInfoRow* get_transaction(char * txId) + { + Monero::TransactionInfo *row = m_transaction_history->transaction(std::string(txId)); + return new TransactionInfoRow(row); + } + int LedgerExchange( unsigned char *command, unsigned int cmd_len, @@ -970,6 +976,15 @@ extern "C" return result; } + void freeze_coin(int index) + { + m_coins->setFrozen(index); + } + + void thaw_coin(int index) + { + m_coins->thaw(index); + } #ifdef __cplusplus } diff --git a/cw_monero/lib/api/coins_info.dart b/cw_monero/lib/api/coins_info.dart index 9a5303f9..d7350a6e 100644 --- a/cw_monero/lib/api/coins_info.dart +++ b/cw_monero/lib/api/coins_info.dart @@ -16,8 +16,20 @@ final coinNative = moneroApi .lookup>('coin') .asFunction(); +final freezeCoinNative = moneroApi + .lookup>('freeze_coin') + .asFunction(); + +final thawCoinNative = moneroApi + .lookup>('thaw_coin') + .asFunction(); + void refreshCoins(int accountIndex) => refreshCoinsNative(accountIndex); int countOfCoins() => coinsCountNative(); CoinsInfoRow getCoin(int index) => coinNative(index).ref; + +void freezeCoin(int index) => freezeCoinNative(index); + +void thawCoin(int index) => thawCoinNative(index); diff --git a/cw_monero/lib/api/signatures.dart b/cw_monero/lib/api/signatures.dart index e208414c..9be828df 100644 --- a/cw_monero/lib/api/signatures.dart +++ b/cw_monero/lib/api/signatures.dart @@ -1,6 +1,7 @@ import 'dart:ffi'; import 'package:cw_monero/api/structs/coins_info_row.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; +import 'package:cw_monero/api/structs/transaction_info_row.dart'; import 'package:cw_monero/api/structs/ut8_box.dart'; import 'package:ffi/ffi.dart'; @@ -81,6 +82,8 @@ typedef account_set_label = Void Function(Int32 accountIndex, Pointer labe typedef transactions_refresh = Void Function(); +typedef get_transaction = Pointer Function(Pointer txId); + typedef get_tx_key = Pointer? Function(Pointer txId); typedef transactions_count = Int64 Function(); @@ -139,3 +142,7 @@ typedef coins_count = Int64 Function(); // typedef coins_from_txid = Pointer Function(Pointer txid); typedef coin = Pointer Function(Int32 index); + +typedef freeze_coin = Void Function(Int32 index); + +typedef thaw_coin = Void Function(Int32 index); diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index 1964c406..73c8de80 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -1,15 +1,16 @@ import 'dart:ffi'; + import 'package:cw_monero/api/convert_utf8_to_string.dart'; +import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart'; +import 'package:cw_monero/api/monero_api.dart'; import 'package:cw_monero/api/monero_output.dart'; +import 'package:cw_monero/api/signatures.dart'; +import 'package:cw_monero/api/structs/pending_transaction.dart'; +import 'package:cw_monero/api/structs/transaction_info_row.dart'; import 'package:cw_monero/api/structs/ut8_box.dart'; +import 'package:cw_monero/api/types.dart'; import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; -import 'package:cw_monero/api/signatures.dart'; -import 'package:cw_monero/api/types.dart'; -import 'package:cw_monero/api/monero_api.dart'; -import 'package:cw_monero/api/structs/transaction_info_row.dart'; -import 'package:cw_monero/api/structs/pending_transaction.dart'; -import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart'; final transactionsRefreshNative = moneroApi .lookup>('transactions_refresh') @@ -38,6 +39,10 @@ final transactionCommitNative = moneroApi final getTxKeyNative = moneroApi.lookup>('get_tx_key').asFunction(); +final getTransactionNative = moneroApi + .lookup>('get_transaction') + .asFunction(); + String getTxKey(String txId) { final txIdPointer = txId.toNativeUtf8(); final keyPointer = getTxKeyNative(txIdPointer); @@ -65,6 +70,11 @@ List getAllTransactions() { .toList(); } +TransactionInfoRow getTransaction(String txId) { + final txIdPointer = txId.toNativeUtf8(); + return getTransactionNative(txIdPointer).ref; +} + PendingTransactionDescription createTransactionSync( {required String address, required String paymentId, diff --git a/cw_monero/lib/api/types.dart b/cw_monero/lib/api/types.dart index 2c92f2d8..4c0c980d 100644 --- a/cw_monero/lib/api/types.dart +++ b/cw_monero/lib/api/types.dart @@ -1,6 +1,7 @@ import 'dart:ffi'; import 'package:cw_monero/api/structs/coins_info_row.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; +import 'package:cw_monero/api/structs/transaction_info_row.dart'; import 'package:cw_monero/api/structs/ut8_box.dart'; import 'package:ffi/ffi.dart'; @@ -81,6 +82,8 @@ typedef AccountSetLabel = void Function(int accountIndex, Pointer label); typedef TransactionsRefresh = void Function(); +typedef GetTransaction = Pointer Function(Pointer txId); + typedef GetTxKey = Pointer? Function(Pointer txId); typedef TransactionsCount = int Function(); @@ -139,3 +142,7 @@ typedef RefreshCoins = void Function(int); typedef CoinsCount = int Function(); typedef GetCoin = Pointer Function(int); + +typedef FreezeCoin = void Function(int); + +typedef ThawCoin = void Function(int); diff --git a/cw_monero/lib/monero_unspent.dart b/cw_monero/lib/monero_unspent.dart index c2ff9f9d..65b5c595 100644 --- a/cw_monero/lib/monero_unspent.dart +++ b/cw_monero/lib/monero_unspent.dart @@ -1,28 +1,20 @@ +import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_monero/api/structs/coins_info_row.dart'; -class MoneroUnspent { - MoneroUnspent(this.address, this.hash, this.keyImage, this.value, this.isFrozen, this.isUnlocked) - : isSending = true, - note = ''; +class MoneroUnspent extends Unspent { + MoneroUnspent( + String address, String hash, String keyImage, int value, bool isFrozen, this.isUnlocked) + : super(address, hash, value, 0, keyImage) { + this.isFrozen = isFrozen; + } - MoneroUnspent.fromCoinsInfoRow(CoinsInfoRow coinsInfoRow) - : address = coinsInfoRow.getAddress(), - hash = coinsInfoRow.getHash(), - keyImage = coinsInfoRow.getKeyImage(), - value = coinsInfoRow.amount, - isFrozen = coinsInfoRow.frozen == 1, - isUnlocked = coinsInfoRow.unlocked == 1, - isSending = true, - note = ''; - - final String address; - final String hash; - final String keyImage; - final int value; + factory MoneroUnspent.fromCoinsInfoRow(CoinsInfoRow coinsInfoRow) => MoneroUnspent( + coinsInfoRow.getAddress(), + coinsInfoRow.getHash(), + coinsInfoRow.getKeyImage(), + coinsInfoRow.amount, + coinsInfoRow.frozen == 1, + coinsInfoRow.unlocked == 1); final bool isUnlocked; - - bool isFrozen; - bool isSending; - String note; } diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 83219af5..d7e66ece 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; + import 'package:cw_core/account.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/monero_amount_format.dart'; @@ -22,14 +23,14 @@ import 'package:cw_monero/api/transaction_history.dart' as transaction_history; import 'package:cw_monero/api/wallet.dart' as monero_wallet; import 'package:cw_monero/exceptions/monero_transaction_creation_exception.dart'; import 'package:cw_monero/exceptions/monero_transaction_no_inputs_exception.dart'; -import 'package:cw_monero/pending_monero_transaction.dart'; import 'package:cw_monero/monero_transaction_creation_credentials.dart'; import 'package:cw_monero/monero_transaction_history.dart'; import 'package:cw_monero/monero_transaction_info.dart'; import 'package:cw_monero/monero_unspent.dart'; import 'package:cw_monero/monero_wallet_addresses.dart'; -import 'package:mobx/mobx.dart'; +import 'package:cw_monero/pending_monero_transaction.dart'; import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; part 'monero_wallet.g.dart'; @@ -204,7 +205,7 @@ abstract class MoneroWalletBase for (final utx in unspentCoins) { if (utx.isSending) { allInputsAmount += utx.value; - inputs.add(utx.keyImage); + inputs.add(utx.keyImage!); } } final spendAllCoins = inputs.length == unspentCoins.length; @@ -395,7 +396,9 @@ abstract class MoneroWalletBase for (var i = 0; i < coinCount; i++) { final coin = getCoin(i); if (coin.spent == 0) { - unspentCoins.add(MoneroUnspent.fromCoinsInfoRow(coin)); + final unspent = MoneroUnspent.fromCoinsInfoRow(coin); + unspent.isChange = transaction_history.getTransaction(unspent.hash).direction == 1; + unspentCoins.add(unspent); } } @@ -406,8 +409,10 @@ abstract class MoneroWalletBase if (unspentCoins.isNotEmpty) { unspentCoins.forEach((coin) { - final coinInfoList = unspentCoinsInfo.values - .where((element) => element.walletId.contains(id) && element.hash.contains(coin.hash)); + final coinInfoList = unspentCoinsInfo.values.where((element) => + element.walletId.contains(id) && + element.accountIndex == walletAddresses.account!.id && + element.keyImage!.contains(coin.keyImage!)); if (coinInfoList.isNotEmpty) { final coinInfo = coinInfoList.first; @@ -435,7 +440,9 @@ abstract class MoneroWalletBase address: coin.address, value: coin.value, vout: 0, - keyImage: coin.keyImage); + keyImage: coin.keyImage, + isChange: coin.isChange, + accountIndex: walletAddresses.account!.id); await unspentCoinsInfo.add(newInfo); } @@ -443,12 +450,13 @@ abstract class MoneroWalletBase Future _refreshUnspentCoinsInfo() async { try { final List keys = []; - final currentWalletUnspentCoins = - unspentCoinsInfo.values.where((element) => element.walletId.contains(id)); + final currentWalletUnspentCoins = unspentCoinsInfo.values.where((element) => + element.walletId.contains(id) && element.accountIndex == walletAddresses.account!.id); if (currentWalletUnspentCoins.isNotEmpty) { currentWalletUnspentCoins.forEach((element) { - final existUnspentCoins = unspentCoins.where((coin) => element.hash.contains(coin.hash)); + final existUnspentCoins = + unspentCoins.where((coin) => element.keyImage!.contains(coin.keyImage!)); if (existUnspentCoins.isEmpty) { keys.add(element.key); @@ -566,7 +574,8 @@ abstract class MoneroWalletBase int _getFrozenBalance() { var frozenBalance = 0; - for (var coin in unspentCoinsInfo.values) { + for (var coin in unspentCoinsInfo.values.where((element) => + element.walletId == id && element.accountIndex == walletAddresses.account!.id)) { if (coin.isFrozen) frozenBalance += coin.value; } diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index 1de7b845..9ae248ca 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -321,10 +321,7 @@ class CWMonero extends Monero { @override List getUnspents(Object wallet) { final moneroWallet = wallet as MoneroWallet; - return moneroWallet.unspentCoins - .map((MoneroUnspent moneroUnspent) => Unspent(moneroUnspent.address, moneroUnspent.hash, - moneroUnspent.value, 0, moneroUnspent.keyImage)) - .toList(); + return moneroWallet.unspentCoins; } @override diff --git a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart index 1a173f62..36cbda64 100644 --- a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart +++ b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart @@ -1,15 +1,13 @@ import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart'; -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:cake_wallet/generated/i18n.dart'; class UnspentCoinsListPage extends BasePage { UnspentCoinsListPage({required this.unspentCoinsListViewModel}); @@ -17,31 +15,10 @@ class UnspentCoinsListPage extends BasePage { @override String get title => S.current.unspent_coins_title; - //@override - //Widget trailing(BuildContext context) { - // final questionImage = Image.asset('assets/images/question_mark.png', - // color: Theme.of(context).extension()!.titleColor); - - // return SizedBox( - // height: 20.0, - // width: 20.0, - // child: ButtonTheme( - // minWidth: double.minPositive, - // child: FlatButton( - // highlightColor: Colors.transparent, - // splashColor: Colors.transparent, - // padding: EdgeInsets.all(0), - // onPressed: () => showUnspentCoinsAlert(context), - // child: questionImage), - // ), - // ); - //} - final UnspentCoinsListViewModel unspentCoinsListViewModel; @override - Widget body(BuildContext context) => - UnspentCoinsListForm(unspentCoinsListViewModel); + Widget body(BuildContext context) => UnspentCoinsListForm(unspentCoinsListViewModel); } class UnspentCoinsListForm extends StatefulWidget { @@ -50,8 +27,7 @@ class UnspentCoinsListForm extends StatefulWidget { final UnspentCoinsListViewModel unspentCoinsListViewModel; @override - UnspentCoinsListFormState createState() => - UnspentCoinsListFormState(unspentCoinsListViewModel); + UnspentCoinsListFormState createState() => UnspentCoinsListFormState(unspentCoinsListViewModel); } class UnspentCoinsListFormState extends State { @@ -59,16 +35,6 @@ class UnspentCoinsListFormState extends State { final UnspentCoinsListViewModel unspentCoinsListViewModel; - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback(afterLayout); - } - - void afterLayout(dynamic _) { - //showUnspentCoinsAlert(context); - } - @override Widget build(BuildContext context) { return Container( @@ -76,8 +42,7 @@ class UnspentCoinsListFormState extends State { child: Observer( builder: (_) => ListView.separated( itemCount: unspentCoinsListViewModel.items.length, - separatorBuilder: (_, __) => - SizedBox(height: 15), + separatorBuilder: (_, __) => SizedBox(height: 15), itemBuilder: (_, int index) { return Observer(builder: (_) { final item = unspentCoinsListViewModel.items[index]; @@ -86,38 +51,22 @@ class UnspentCoinsListFormState extends State { : item.address; return GestureDetector( - onTap: () => - Navigator.of(context) - .pushNamed(Routes.unspentCoinsDetails, - arguments: [item, unspentCoinsListViewModel]), + onTap: () => Navigator.of(context).pushNamed(Routes.unspentCoinsDetails, + arguments: [item, unspentCoinsListViewModel]), child: UnspentCoinsListItem( note: item.note, amount: item.amount, address: address, isSending: item.isSending, isFrozen: item.isFrozen, + isChange: item.isChange, onCheckBoxTap: item.isFrozen - ? null - : () async { - item.isSending = !item.isSending; - await unspentCoinsListViewModel - .saveUnspentCoinInfo(item);})); + ? null + : () async { + item.isSending = !item.isSending; + await unspentCoinsListViewModel.saveUnspentCoinInfo(item); + })); }); - } - ) - ) - ); + }))); } } - -void showUnspentCoinsAlert(BuildContext context) { - showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: '', - alertContent: 'Information about unspent coins', - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); -} \ No newline at end of file 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 93cf27af..d629e945 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 @@ -1,8 +1,8 @@ import 'package:auto_size_text/auto_size_text.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/standard_checkbox.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:flutter/material.dart'; -import 'package:cake_wallet/generated/i18n.dart'; class UnspentCoinsListItem extends StatelessWidget { UnspentCoinsListItem({ @@ -11,6 +11,7 @@ class UnspentCoinsListItem extends StatelessWidget { required this.address, required this.isSending, required this.isFrozen, + required this.isChange, this.onCheckBoxTap, }); @@ -19,6 +20,7 @@ class UnspentCoinsListItem extends StatelessWidget { final String address; final bool isSending; final bool isFrozen; + final bool isChange; final Function()? onCheckBoxTap; @override @@ -27,9 +29,8 @@ class UnspentCoinsListItem extends StatelessWidget { final selectedItemColor = Theme.of(context).primaryColor; final itemColor = isSending ? selectedItemColor : unselectedItemColor; - final amountColor = isSending - ? Colors.white - : Theme.of(context).extension()!.buttonTextColor; + final amountColor = + isSending ? Colors.white : Theme.of(context).extension()!.buttonTextColor; final addressColor = isSending ? Colors.white.withOpacity(0.5) : Theme.of(context).extension()!.buttonSecondaryTextColor; @@ -47,7 +48,8 @@ class UnspentCoinsListItem extends StatelessWidget { child: StandardCheckbox( iconColor: amountColor, borderColor: addressColor, - value: isSending, onChanged: (value) => onCheckBoxTap?.call())), + value: isSending, + onChanged: (value) => onCheckBoxTap?.call())), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -57,9 +59,7 @@ class UnspentCoinsListItem extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ if (note.isNotEmpty) AutoSizeText( note, @@ -69,8 +69,8 @@ class UnspentCoinsListItem extends StatelessWidget { ), AutoSizeText( amount, - style: - TextStyle(color: amountColor, fontSize: 15, fontWeight: FontWeight.w600), + style: TextStyle( + color: amountColor, fontSize: 15, fontWeight: FontWeight.w600), maxLines: 1, ) ]), @@ -84,23 +84,41 @@ class UnspentCoinsListItem extends StatelessWidget { alignment: Alignment.center, child: Text( S.of(context).frozen, - style: - TextStyle(color: amountColor, fontSize: 7, fontWeight: FontWeight.w600), - )) + style: TextStyle( + color: amountColor, fontSize: 7, fontWeight: FontWeight.w600), + )), ], ), Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ AutoSizeText( - '${address.substring(0, 5)}...${address.substring(address.length-5)}', // ToDo: Maybe use address label + '${address.substring(0, 5)}...${address.substring(address.length - 5)}', // ToDo: Maybe use address label style: TextStyle( color: addressColor, fontSize: 12, ), 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, + ), + ), + ), ], ), ), diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 06d05b3a..f32b43cd 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -175,10 +175,8 @@ abstract class BalanceViewModelBase with Store { return '---'; } - return _getFiatBalance( - price: price, - cryptoAmount: getFormattedFrozenBalance(walletBalance)) + ' ' + fiatCurrency.toString(); - + return _getFiatBalance(price: price, cryptoAmount: getFormattedFrozenBalance(walletBalance)) + + ' ${fiatCurrency.toString()}'; } @computed @@ -201,10 +199,8 @@ abstract class BalanceViewModelBase with Store { return '---'; } - return _getFiatBalance( - price: price, - cryptoAmount: walletBalance.formattedAvailableBalance) + ' ' + fiatCurrency.toString(); - + return _getFiatBalance(price: price, cryptoAmount: walletBalance.formattedAvailableBalance) + + ' ${fiatCurrency.toString()}'; } @computed @@ -216,10 +212,8 @@ abstract class BalanceViewModelBase with Store { return '---'; } - return _getFiatBalance( - price: price, - cryptoAmount: walletBalance.formattedAdditionalBalance) + ' ' + fiatCurrency.toString(); - + return _getFiatBalance(price: price, cryptoAmount: walletBalance.formattedAdditionalBalance) + + ' ${fiatCurrency.toString()}'; } @computed diff --git a/lib/view_model/unspent_coins/unspent_coins_item.dart b/lib/view_model/unspent_coins/unspent_coins_item.dart index 9d1f6c71..bb5c4dd7 100644 --- a/lib/view_model/unspent_coins/unspent_coins_item.dart +++ b/lib/view_model/unspent_coins/unspent_coins_item.dart @@ -12,6 +12,7 @@ abstract class UnspentCoinsItemBase with Store { required this.isFrozen, required this.note, required this.isSending, + required this.isChange, required this.amountRaw, required this.vout, required this.keyImage @@ -35,6 +36,9 @@ abstract class UnspentCoinsItemBase with Store { @observable bool isSending; + @observable + bool isChange; + @observable int amountRaw; 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 709c5056..1815b168 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 @@ -1,10 +1,8 @@ -import 'package:collection/collection.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart'; import 'package:cw_core/unspent_coins_info.dart'; -import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:hive/hive.dart'; @@ -26,66 +24,49 @@ abstract class UnspentCoinsListViewModelBase with Store { @computed ObservableList get items => ObservableList.of(_getUnspents().map((elem) { - final amount = formatAmountToString(elem.value) + ' ${wallet.currency.title}'; - final info = getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout, elem.keyImage); return UnspentCoinsItem( address: elem.address, - amount: amount, + amount: '${formatAmountToString(elem.value)} ${wallet.currency.title}', hash: elem.hash, - isFrozen: info?.isFrozen ?? false, - note: info?.note ?? '', - isSending: info?.isSending ?? true, + isFrozen: info.isFrozen, + note: info.note, + isSending: info.isSending, amountRaw: elem.value, vout: elem.vout, - keyImage: elem.keyImage); + keyImage: elem.keyImage, + isChange: elem.isChange, + ); })); Future saveUnspentCoinInfo(UnspentCoinsItem item) async { try { final info = getUnspentCoinInfo(item.hash, item.address, item.amountRaw, item.vout, item.keyImage); - if (info == null) { - final newInfo = UnspentCoinsInfo( - walletId: wallet.id, - hash: item.hash, - address: item.address, - value: item.amountRaw, - vout: item.vout, - isFrozen: item.isFrozen, - isSending: item.isSending, - noteRaw: item.note, - keyImage: item.keyImage); - await _unspentCoinsInfo.add(newInfo); - _updateUnspents(); - wallet.updateBalance(); - return; - } info.isFrozen = item.isFrozen; info.isSending = item.isSending; info.note = item.note; await info.save(); - _updateUnspents(); - wallet.updateBalance(); + await _updateUnspents(); + await wallet.updateBalance(); } catch (e) { print(e.toString()); } } - UnspentCoinsInfo? getUnspentCoinInfo( - String hash, String address, int value, int vout, String? keyImage) { - return _unspentCoinsInfo.values.firstWhereOrNull((element) => - element.walletId == wallet.id && - element.hash == hash && - element.address == address && - element.value == value && - element.vout == vout && - element.keyImage == keyImage); - } + UnspentCoinsInfo getUnspentCoinInfo( + String hash, String address, int value, int vout, String? keyImage) => + _unspentCoinsInfo.values.firstWhere((element) => + element.walletId == wallet.id && + element.hash == hash && + element.address == address && + element.value == value && + element.vout == vout && + element.keyImage == keyImage); String formatAmountToString(int fullBalance) { if (wallet.type == WalletType.monero) @@ -95,7 +76,7 @@ abstract class UnspentCoinsListViewModelBase with Store { return ''; } - void _updateUnspents() { + Future _updateUnspents() async { if (wallet.type == WalletType.monero) return monero!.updateUnspents(wallet); if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type)) return bitcoin!.updateUnspents(wallet); diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 4100bdeb..54a9a1d7 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -726,5 +726,6 @@ "domain_looks_up": "ﻝﺎﺠﻤﻟﺍ ﺚﺤﺑ ﺕﺎﻴﻠﻤﻋ", "require_for_exchanges_to_external_wallets": "ﺔﻴﺟﺭﺎﺧ ﻆﻓﺎﺤﻣ ﻰﻟﺇ ﺕﻻﺩﺎﺒﺘﻟﺍ ﺐﻠﻄﺘﺗ", "camera_permission_is_required": ".ﺍﺮﻴﻣﺎﻜﻟﺍ ﻥﺫﺇ ﺏﻮﻠﻄﻣ", - "switchToETHWallet": "ﻯﺮﺧﺃ ﺓﺮﻣ ﺔﻟﻭﺎﺤﻤﻟﺍﻭ Ethereum ﺔﻈﻔﺤﻣ ﻰﻟﺇ ﻞﻳﺪﺒﺘﻟﺍ ﻰﺟﺮﻳ" + "switchToETHWallet": "ﻯﺮﺧﺃ ﺓﺮﻣ ﺔﻟﻭﺎﺤﻤﻟﺍﻭ Ethereum ﺔﻈﻔﺤﻣ ﻰﻟﺇ ﻞﻳﺪﺒﺘﻟﺍ ﻰﺟﺮﻳ", + "unspent_change": "يتغير" } diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index e12a2411..89be7d8a 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -722,5 +722,6 @@ "domain_looks_up": "Търсене на домейни", "require_for_exchanges_to_external_wallets": "Изискване за обмен към външни портфейли", "camera_permission_is_required": "Изисква се разрешение за камерата.\nМоля, активирайте го от настройките на приложението.", - "switchToETHWallet": "Моля, преминете към портфейл Ethereum и опитайте отново" + "switchToETHWallet": "Моля, преминете към портфейл Ethereum и опитайте отново", + "unspent_change": "Промяна" } diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 52087ef6..4b3d2c44 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -722,5 +722,6 @@ "domain_looks_up": "Vyhledávání domén", "require_for_exchanges_to_external_wallets": "Vyžadovat pro výměny do externích peněženek", "camera_permission_is_required": "Vyžaduje se povolení fotoaparátu.\nPovolte jej v nastavení aplikace.", - "switchToETHWallet": "Přejděte na peněženku Ethereum a zkuste to znovu" + "switchToETHWallet": "Přejděte na peněženku Ethereum a zkuste to znovu", + "unspent_change": "Změna" } diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 733b645c..8fa81fdb 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -730,5 +730,6 @@ "domain_looks_up": "Domain-Suchen", "require_for_exchanges_to_external_wallets": "Erforderlich für den Umtausch in externe Wallets", "camera_permission_is_required": "Eine Kameraerlaubnis ist erforderlich.\nBitte aktivieren Sie es in den App-Einstellungen.", - "switchToETHWallet": "Bitte wechseln Sie zu einem Ethereum-Wallet und versuchen Sie es erneut" + "switchToETHWallet": "Bitte wechseln Sie zu einem Ethereum-Wallet und versuchen Sie es erneut", + "unspent_change": "Wechselgeld" } diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 4264d6b3..327bfbdc 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -731,5 +731,6 @@ "domain_looks_up": "Domain lookups", "require_for_exchanges_to_external_wallets": "Require for exchanges to external wallets", "camera_permission_is_required": "Camera permission is required. \nPlease enable it from app settings.", - "switchToETHWallet": "Please switch to an Ethereum wallet and try again" + "switchToETHWallet": "Please switch to an Ethereum wallet and try again", + "unspent_change": "Change" } diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 728d15af..89143e97 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -730,5 +730,6 @@ "domain_looks_up": "Búsquedas de dominio", "require_for_exchanges_to_external_wallets": "Requerido para intercambios a billeteras externas", "camera_permission_is_required": "Se requiere permiso de la cámara.\nHabilítelo desde la configuración de la aplicación.", - "switchToETHWallet": "Cambie a una billetera Ethereum e inténtelo nuevamente." + "switchToETHWallet": "Cambie a una billetera Ethereum e inténtelo nuevamente.", + "unspent_change": "Cambiar" } diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index d4d27ae0..8e43afee 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -730,5 +730,6 @@ "domain_looks_up": "Recherches de domaine", "require_for_exchanges_to_external_wallets": "Exiger pour les échanges vers des portefeuilles externes", "camera_permission_is_required": "L'autorisation de la caméra est requise.\nVeuillez l'activer à partir des paramètres de l'application.", - "switchToETHWallet": "Veuillez passer à un portefeuille (wallet) Ethereum et réessayer" + "switchToETHWallet": "Veuillez passer à un portefeuille (wallet) Ethereum et réessayer", + "unspent_change": "Changement" } diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 57199fa4..506c6850 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -708,5 +708,6 @@ "domain_looks_up": "Binciken yanki", "require_for_exchanges_to_external_wallets": "Bukatar musanya zuwa wallet na waje", "camera_permission_is_required": "Ana buƙatar izinin kyamara.\nDa fatan za a kunna shi daga saitunan app.", - "switchToETHWallet": "Da fatan za a canza zuwa walat ɗin Ethereum kuma a sake gwadawa" + "switchToETHWallet": "Da fatan za a canza zuwa walat ɗin Ethereum kuma a sake gwadawa", + "unspent_change": "Canza" } diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 536731c9..5c77d0f0 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -730,5 +730,6 @@ "domain_looks_up": "डोमेन लुकअप", "require_for_exchanges_to_external_wallets": "बाहरी वॉलेट में एक्सचेंज की आवश्यकता है", "camera_permission_is_required": "कैमरे की अनुमति आवश्यक है.\nकृपया इसे ऐप सेटिंग से सक्षम करें।", - "switchToETHWallet": "कृपया एथेरियम वॉलेट पर स्विच करें और पुनः प्रयास करें" + "switchToETHWallet": "कृपया एथेरियम वॉलेट पर स्विच करें और पुनः प्रयास करें", + "unspent_change": "परिवर्तन" } diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 936e4133..e09df5a9 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -728,5 +728,6 @@ "domain_looks_up": "Pretraga domena", "require_for_exchanges_to_external_wallets": "Zahtijeva razmjene na vanjske novčanike", "camera_permission_is_required": "Potrebno je dopuštenje kamere.\nOmogućite ga u postavkama aplikacije.", - "switchToETHWallet": "Prijeđite na Ethereum novčanik i pokušajte ponovno" + "switchToETHWallet": "Prijeđite na Ethereum novčanik i pokušajte ponovno", + "unspent_change": "Promijeniti" } diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 036e3e96..1f164074 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -718,5 +718,6 @@ "domain_looks_up": "Pencarian domain", "require_for_exchanges_to_external_wallets": "Memerlukan pertukaran ke dompet eksternal", "camera_permission_is_required": "Izin kamera diperlukan.\nSilakan aktifkan dari pengaturan aplikasi.", - "switchToETHWallet": "Silakan beralih ke dompet Ethereum dan coba lagi" + "switchToETHWallet": "Silakan beralih ke dompet Ethereum dan coba lagi", + "unspent_change": "Mengubah" } diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 5138b1a5..0a626932 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -730,5 +730,6 @@ "domain_looks_up": "Ricerche di domini", "require_for_exchanges_to_external_wallets": "Richiede scambi con portafogli esterni", "camera_permission_is_required": "È richiesta l'autorizzazione della fotocamera.\nAbilitalo dalle impostazioni dell'app.", - "switchToETHWallet": "Passa a un portafoglio Ethereum e riprova" + "switchToETHWallet": "Passa a un portafoglio Ethereum e riprova", + "unspent_change": "Modifica" } diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index cc6dad9a..40ab152a 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -730,5 +730,6 @@ "domain_looks_up": "ドメイン検索", "require_for_exchanges_to_external_wallets": "外部ウォレットへの交換に必要", "camera_permission_is_required": "カメラの許可が必要です。\nアプリの設定から有効にしてください。", - "switchToETHWallet": "イーサリアムウォレットに切り替えてもう一度お試しください" + "switchToETHWallet": "イーサリアムウォレットに切り替えてもう一度お試しください", + "unspent_change": "変化" } diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 9ce833b0..ea95072c 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -728,5 +728,6 @@ "domain_looks_up": "도메인 조회", "require_for_exchanges_to_external_wallets": "외부 지갑으로의 교환을 위해 필요", "camera_permission_is_required": "카메라 권한이 필요합니다.\n앱 설정에서 활성화해 주세요.", - "switchToETHWallet": "이더리움 지갑으로 전환한 후 다시 시도해 주세요." + "switchToETHWallet": "이더리움 지갑으로 전환한 후 다시 시도해 주세요.", + "unspent_change": "변화" } diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index c92871c5..f2a4a8fa 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -728,5 +728,6 @@ "domain_looks_up": "ဒိုမိန်းရှာဖွေမှုများ", "require_for_exchanges_to_external_wallets": "ပြင်ပပိုက်ဆံအိတ်များသို့ လဲလှယ်ရန် လိုအပ်သည်။", "camera_permission_is_required": "ကင်မရာခွင့်ပြုချက် လိုအပ်ပါသည်။\nအက်ပ်ဆက်တင်များမှ ၎င်းကိုဖွင့်ပါ။", - "switchToETHWallet": "ကျေးဇူးပြု၍ Ethereum ပိုက်ဆံအိတ်သို့ ပြောင်းပြီး ထပ်စမ်းကြည့်ပါ။" + "switchToETHWallet": "ကျေးဇူးပြု၍ Ethereum ပိုက်ဆံအိတ်သို့ ပြောင်းပြီး ထပ်စမ်းကြည့်ပါ။", + "unspent_change": "ပေြာင်းလဲခြင်း" } diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index c93861b2..a582a943 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -730,5 +730,6 @@ "domain_looks_up": "Domein opzoeken", "require_for_exchanges_to_external_wallets": "Vereist voor uitwisselingen naar externe portemonnees", "camera_permission_is_required": "Cameratoestemming is vereist.\nSchakel dit in via de app-instellingen.", - "switchToETHWallet": "Schakel over naar een Ethereum-portemonnee en probeer het opnieuw" + "switchToETHWallet": "Schakel over naar een Ethereum-portemonnee en probeer het opnieuw", + "unspent_change": "Wijziging" } diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 5d32aaa7..85b8668b 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -730,5 +730,6 @@ "domain_looks_up": "Wyszukiwanie domen", "require_for_exchanges_to_external_wallets": "Wymagaj wymiany na portfele zewnętrzne", "camera_permission_is_required": "Wymagane jest pozwolenie na korzystanie z aparatu.\nWłącz tę funkcję w ustawieniach aplikacji.", - "switchToETHWallet": "Przejdź na portfel Ethereum i spróbuj ponownie" + "switchToETHWallet": "Przejdź na portfel Ethereum i spróbuj ponownie", + "unspent_change": "Zmiana" } diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index aa1f7459..3c725319 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -729,5 +729,6 @@ "domain_looks_up": "Pesquisas de domínio", "require_for_exchanges_to_external_wallets": "Exigir trocas para carteiras externas", "camera_permission_is_required": "É necessária permissão da câmera.\nAtive-o nas configurações do aplicativo.", - "switchToETHWallet": "Mude para uma carteira Ethereum e tente novamente" + "switchToETHWallet": "Mude para uma carteira Ethereum e tente novamente", + "unspent_change": "Mudar" } diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 31fc00bc..a833e2c4 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -730,5 +730,6 @@ "domain_looks_up": "Поиск доменов", "require_for_exchanges_to_external_wallets": "Требовать обмена на внешние кошельки", "camera_permission_is_required": "Требуется разрешение камеры.\nПожалуйста, включите его в настройках приложения.", - "switchToETHWallet": "Пожалуйста, переключитесь на кошелек Ethereum и повторите попытку." + "switchToETHWallet": "Пожалуйста, переключитесь на кошелек Ethereum и повторите попытку.", + "unspent_change": "Изменять" } diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index b9757a08..f20d7e3a 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -728,5 +728,6 @@ "domain_looks_up": "การค้นหาโดเมน", "require_for_exchanges_to_external_wallets": "จำเป็นต้องแลกเปลี่ยนกับกระเป๋าเงินภายนอก", "camera_permission_is_required": "ต้องได้รับอนุญาตจากกล้อง\nโปรดเปิดใช้งานจากการตั้งค่าแอป", - "switchToETHWallet": "โปรดเปลี่ยนไปใช้กระเป๋าเงิน Ethereum แล้วลองอีกครั้ง" + "switchToETHWallet": "โปรดเปลี่ยนไปใช้กระเป๋าเงิน Ethereum แล้วลองอีกครั้ง", + "unspent_change": "เปลี่ยน" } diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 6157d9eb..e323a352 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -725,5 +725,6 @@ "domain_looks_up": "Mga paghahanap ng domain", "require_for_exchanges_to_external_wallets": "Kinakailangan para sa mga palitan sa mga panlabas na wallet", "camera_permission_is_required": "Kinakailangan ang pahintulot sa camera.\nMangyaring paganahin ito mula sa mga setting ng app.", - "switchToETHWallet": "Mangyaring lumipat sa isang Ethereum wallet at subukang muli" + "switchToETHWallet": "Mangyaring lumipat sa isang Ethereum wallet at subukang muli", + "unspent_change": "Baguhin" } diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index ddc66922..b13b85bb 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -728,5 +728,6 @@ "domain_looks_up": "Etki alanı aramaları", "require_for_exchanges_to_external_wallets": "Harici cüzdanlara geçiş yapılmasını zorunlu kılın", "camera_permission_is_required": "Kamera izni gereklidir.\nLütfen uygulama ayarlarından etkinleştirin.", - "switchToETHWallet": "Lütfen bir Ethereum cüzdanına geçin ve tekrar deneyin" + "switchToETHWallet": "Lütfen bir Ethereum cüzdanına geçin ve tekrar deneyin", + "unspent_change": "Değiştirmek" } diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 9fb39b6c..6ee8a491 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -730,5 +730,6 @@ "domain_looks_up": "Пошук доменів", "require_for_exchanges_to_external_wallets": "Потрібен для обміну на зовнішні гаманці", "camera_permission_is_required": "Потрібен дозвіл камери.\nУвімкніть його в налаштуваннях програми.", - "switchToETHWallet": "Перейдіть на гаманець Ethereum і повторіть спробу" + "switchToETHWallet": "Перейдіть на гаманець Ethereum і повторіть спробу", + "unspent_change": "Зміна" } diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index e3f93489..2f44cb51 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -722,5 +722,6 @@ "domain_looks_up": "ڈومین تلاش کرنا", "require_for_exchanges_to_external_wallets": "۔ﮯﮨ ﺕﺭﻭﺮﺿ ﯽﮐ ﮯﻟﺩﺎﺒﺗ ﮟﯿﻣ ﮮﻮﭩﺑ ﯽﻧﻭﺮﯿﺑ", "camera_permission_is_required": "۔ﮯﮨ ﺭﺎﮐﺭﺩ ﺕﺯﺎﺟﺍ ﯽﮐ ﮮﺮﻤﯿﮐ", - "switchToETHWallet": "۔ﮟﯾﺮﮐ ﺶﺷﻮﮐ ﮦﺭﺎﺑﻭﺩ ﺭﻭﺍ ﮟﯾﺮﮐ ﭻﺋﻮﺳ ﺮﭘ ﭧﯿﻟﺍﻭ Ethereum ﻡﺮﮐ ﮦﺍﺮﺑ" + "switchToETHWallet": "۔ﮟﯾﺮﮐ ﺶﺷﻮﮐ ﮦﺭﺎﺑﻭﺩ ﺭﻭﺍ ﮟﯾﺮﮐ ﭻﺋﻮﺳ ﺮﭘ ﭧﯿﻟﺍﻭ Ethereum ﻡﺮﮐ ﮦﺍﺮﺑ", + "unspent_change": "تبدیل کریں" } diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index aafb7080..09f9d1b8 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -724,5 +724,6 @@ "domain_looks_up": "Awọn wiwa agbegbe", "require_for_exchanges_to_external_wallets": "Beere fun awọn paṣipaarọ si awọn apamọwọ ita", "camera_permission_is_required": "A nilo igbanilaaye kamẹra.\nJọwọ jeki o lati app eto.", - "switchToETHWallet": "Jọwọ yipada si apamọwọ Ethereum ki o tun gbiyanju lẹẹkansi" + "switchToETHWallet": "Jọwọ yipada si apamọwọ Ethereum ki o tun gbiyanju lẹẹkansi", + "unspent_change": "Yipada" } diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 336083a7..1eec3f47 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -729,5 +729,6 @@ "domain_looks_up": "域名查找", "require_for_exchanges_to_external_wallets": "需要兑换到外部钱包", "camera_permission_is_required": "需要相机许可。\n请从应用程序设置中启用它。", - "switchToETHWallet": "请切换到以太坊钱包并重试" + "switchToETHWallet": "请切换到以太坊钱包并重试", + "unspent_change": "改变" }