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

Konstantin Ullrich committed Nov 16, 2023 at 00:12 UTC 8084f490b593b4dd74956ab00cf516213a69762b
42 files changed +292 -257
cw_bitcoin/lib/electrum_wallet.dart
+60 -56
@@ -2,41 +2,41 @@ import 'dart:async';
2 import 'dart:convert';
3 import 'dart:io';
4 import 'dart:math';
5 -import 'package:cw_core/pending_transaction.dart';
6 -import 'package:cw_core/unspent_coins_info.dart';
7 -import 'package:cw_core/wallet_type.dart';
8 -import 'package:hive/hive.dart';
9 -import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
10 -import 'package:mobx/mobx.dart';
11 -import 'package:rxdart/subjects.dart';
12 -import 'package:flutter/foundation.dart';
5 +
6 import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
14 -import 'package:cw_bitcoin/electrum_transaction_info.dart';
15 -import 'package:cw_core/pathForWallet.dart';
7 +import 'package:collection/collection.dart';
8 import 'package:cw_bitcoin/address_to_output_script.dart';
9 import 'package:cw_bitcoin/bitcoin_address_record.dart';
18 -import 'package:cw_bitcoin/electrum_balance.dart';
10 import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
20 -import 'package:cw_bitcoin/electrum_transaction_history.dart';
11 import 'package:cw_bitcoin/bitcoin_transaction_no_inputs_exception.dart';
12 import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
13 import 'package:cw_bitcoin/bitcoin_transaction_wrong_balance_exception.dart';
14 import 'package:cw_bitcoin/bitcoin_unspent.dart';
15 import 'package:cw_bitcoin/bitcoin_wallet_keys.dart';
16 +import 'package:cw_bitcoin/electrum.dart';
17 +import 'package:cw_bitcoin/electrum_balance.dart';
18 +import 'package:cw_bitcoin/electrum_transaction_history.dart';
19 +import 'package:cw_bitcoin/electrum_transaction_info.dart';
20 +import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
21 import 'package:cw_bitcoin/file.dart';
22 import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
23 import 'package:cw_bitcoin/script_hash.dart';
24 import 'package:cw_bitcoin/utils.dart';
30 -import 'package:cw_core/wallet_base.dart';
25 +import 'package:cw_core/crypto_currency.dart';
26 import 'package:cw_core/node.dart';
27 +import 'package:cw_core/pathForWallet.dart';
28 +import 'package:cw_core/pending_transaction.dart';
29 import 'package:cw_core/sync_status.dart';
30 +import 'package:cw_core/transaction_direction.dart';
31 import 'package:cw_core/transaction_priority.dart';
32 +import 'package:cw_core/unspent_coins_info.dart';
33 +import 'package:cw_core/wallet_base.dart';
34 import 'package:cw_core/wallet_info.dart';
35 -import 'package:cw_bitcoin/electrum.dart';
35 +import 'package:flutter/foundation.dart';
36 import 'package:hex/hex.dart';
37 -import 'package:cw_core/crypto_currency.dart';
38 -import 'package:collection/collection.dart';
39 -import 'package:bip32/bip32.dart';
37 +import 'package:hive/hive.dart';
38 +import 'package:mobx/mobx.dart';
39 +import 'package:rxdart/subjects.dart';
40
41 part 'electrum_wallet.g.dart';
42
@@ -47,18 +47,18 @@ abstract class ElectrumWalletBase
47 with Store {
48 ElectrumWalletBase(
49 {required String password,
50 - required WalletInfo walletInfo,
51 - required Box<UnspentCoinsInfo> unspentCoinsInfo,
52 - required this.networkType,
53 - required this.mnemonic,
54 - required Uint8List seedBytes,
55 - List<BitcoinAddressRecord>? initialAddresses,
56 - ElectrumClient? electrumClient,
57 - ElectrumBalance? initialBalance,
58 - CryptoCurrency? currency})
50 + required WalletInfo walletInfo,
51 + required Box<UnspentCoinsInfo> unspentCoinsInfo,
52 + required this.networkType,
53 + required this.mnemonic,
54 + required Uint8List seedBytes,
55 + List<BitcoinAddressRecord>? initialAddresses,
56 + ElectrumClient? electrumClient,
57 + ElectrumBalance? initialBalance,
58 + CryptoCurrency? currency})
59 : hd = currency == CryptoCurrency.bch
60 - ? bitcoinCashHDWallet(seedBytes)
61 - : bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/0"),
60 + ? bitcoinCashHDWallet(seedBytes)
61 + : bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/0"),
62 syncStatus = NotConnectedSyncStatus(),
63 _password = password,
64 _feeRates = <int>[],
@@ -67,9 +67,9 @@ abstract class ElectrumWalletBase
67 _scripthashesUpdateSubject = {},
68 balance = ObservableMap<CryptoCurrency, ElectrumBalance>.of(currency != null
69 ? {
70 - currency:
71 - initialBalance ?? const ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0)
72 - }
70 + currency:
71 + initialBalance ?? const ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0)
72 + }
73 : {}),
74 this.unspentCoinsInfo = unspentCoinsInfo,
75 super(walletInfo) {
@@ -79,8 +79,7 @@ abstract class ElectrumWalletBase
79 }
80
81 static bitcoin.HDWallet bitcoinCashHDWallet(Uint8List seedBytes) =>
82 - bitcoin.HDWallet.fromSeed(seedBytes)
83 - .derivePath("m/44'/145'/0'/0");
82 + bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'/0");
83
84 static int estimatedTransactionSize(int inputsCount, int outputsCounts) =>
85 inputsCount * 146 + outputsCounts * 33 + 8;
@@ -294,10 +293,12 @@ abstract class ElectrumWalletBase
293 if (input.isP2wpkh) {
294 final p2wpkh = bitcoin
295 .P2WPKH(
297 - data: generatePaymentData(
298 - hd: input.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
299 - index: input.bitcoinAddressRecord.index),
300 - network: networkType)
296 + data: generatePaymentData(
297 + hd: input.bitcoinAddressRecord.isHidden
298 + ? walletAddresses.sideHd
299 + : walletAddresses.mainHd,
300 + index: input.bitcoinAddressRecord.index),
301 + network: networkType)
302 .data;
303
304 txb.addInput(input.hash, input.vout, null, p2wpkh.output);
@@ -347,12 +348,12 @@ abstract class ElectrumWalletBase
348 }
349
350 String toJSON() => json.encode({
350 - 'mnemonic': mnemonic,
351 - 'account_index': walletAddresses.currentReceiveAddressIndex.toString(),
352 - 'change_address_index': walletAddresses.currentChangeAddressIndex.toString(),
353 - 'addresses': walletAddresses.addresses.map((addr) => addr.toJSON()).toList(),
354 - 'balance': balance[currency]?.toJSON()
355 - });
351 + 'mnemonic': mnemonic,
352 + 'account_index': walletAddresses.currentReceiveAddressIndex.toString(),
353 + 'change_address_index': walletAddresses.currentChangeAddressIndex.toString(),
354 + 'addresses': walletAddresses.addresses.map((addr) => addr.toJSON()).toList(),
355 + 'balance': balance[currency]?.toJSON()
356 + });
357
358 int feeRate(TransactionPriority priority) {
359 try {
@@ -367,7 +368,7 @@ abstract class ElectrumWalletBase
368 }
369
370 int feeAmountForPriority(
370 - BitcoinTransactionPriority priority, int inputsCount, int outputsCount) =>
371 + BitcoinTransactionPriority priority, int inputsCount, int outputsCount) =>
372 feeRate(priority) * estimatedTransactionSize(inputsCount, outputsCount);
373
374 int feeAmountWithFeeRate(int feeRate, int inputsCount, int outputsCount) =>
@@ -467,18 +468,20 @@ abstract class ElectrumWalletBase
468 Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
469
470 Future<void> updateUnspent() async {
470 - final unspent = await Future.wait(walletAddresses
471 - .addresses.map((address) => electrumClient
471 + final unspent = await Future.wait(walletAddresses.addresses.map((address) => electrumClient
472 .getListUnspentWithAddress(address.address, networkType)
473 - .then((unspent) => unspent
474 - .map((unspent) {
475 - try {
476 - return BitcoinUnspent.fromJSON(address, unspent);
477 - } catch(_) {
478 - return null;
479 - }
480 - }).whereNotNull())));
473 + .then((unspent) => unspent.map((unspent) {
474 + try {
475 + return BitcoinUnspent.fromJSON(address, unspent);
476 + } catch (_) {
477 + return null;
478 + }
479 + }).whereNotNull())));
480 unspentCoins = unspent.expand((e) => e).toList();
481 + unspentCoins.forEach((coin) async {
482 + final tx = await fetchTransactionInfo(hash: coin.hash, height: 0);
483 + coin.isChange = tx!.direction == TransactionDirection.outgoing;
484 + });
485
486 if (unspentCoinsInfo.isEmpty) {
487 unspentCoins.forEach((coin) => _addCoinInfo(coin));
@@ -515,6 +518,7 @@ abstract class ElectrumWalletBase
518 address: coin.bitcoinAddressRecord.address,
519 value: coin.value,
520 vout: coin.vout,
521 + isChange: coin.isChange,
522 );
523
524 await unspentCoinsInfo.add(newInfo);
@@ -524,7 +528,7 @@ abstract class ElectrumWalletBase
528 try {
529 final List<dynamic> keys = <dynamic>[];
530 final currentWalletUnspentCoins =
527 - unspentCoinsInfo.values.where((element) => element.walletId.contains(id));
531 + unspentCoinsInfo.values.where((element) => element.walletId.contains(id));
532
533 if (currentWalletUnspentCoins.isNotEmpty) {
534 currentWalletUnspentCoins.forEach((element) {
@@ -657,7 +661,7 @@ abstract class ElectrumWalletBase
661 final addresses = walletAddresses.addresses.toList();
662 final balanceFutures = <Future<Map<String, dynamic>>>[];
663 for (var i = 0; i < addresses.length; i++) {
660 - final addressRecord = addresses[i] ;
664 + final addressRecord = addresses[i];
665 final sh = scriptHash(addressRecord.address, networkType: networkType);
666 final balanceFuture = electrumClient.getBalance(sh);
667 balanceFutures.add(balanceFuture);
cw_core/lib/unspent_coins_info.dart
+9 -1
@@ -14,7 +14,9 @@ class UnspentCoinsInfo extends HiveObject {
14 required this.address,
15 required this.vout,
16 required this.value,
17 - this.keyImage = null
17 + this.keyImage = null,
18 + this.isChange = false,
19 + this.accountIndex = 0
20 });
21
22 static const typeId = UNSPENT_COINS_INFO_TYPE_ID;
@@ -47,6 +49,12 @@ class UnspentCoinsInfo extends HiveObject {
49
50 @HiveField(8, defaultValue: null)
51 String? keyImage;
52 +
53 + @HiveField(9, defaultValue: false)
54 + bool isChange;
55 +
56 + @HiveField(10, defaultValue: 0)
57 + int accountIndex;
58
59 String get note => noteRaw ?? '';
60
cw_core/lib/unspent_transaction_output.dart
+2
@@ -2,6 +2,7 @@ class Unspent {
2 Unspent(this.address, this.hash, this.value, this.vout, this.keyImage)
3 : isSending = true,
4 isFrozen = false,
5 + isChange = false,
6 note = '';
7
8 final String address;
@@ -10,6 +11,7 @@ class Unspent {
11 final int vout;
12 final String? keyImage;
13
14 + bool isChange;
15 bool isSending;
16 bool isFrozen;
17 String note;
cw_monero/ios/Classes/monero_api.cpp
+15
@@ -841,6 +841,12 @@ extern "C"
841 return m_transaction_history->count();
842 }
843
844 + TransactionInfoRow* get_transaction(char * txId)
845 + {
846 + Monero::TransactionInfo *row = m_transaction_history->transaction(std::string(txId));
847 + return new TransactionInfoRow(row);
848 + }
849 +
850 int LedgerExchange(
851 unsigned char *command,
852 unsigned int cmd_len,
@@ -970,6 +976,15 @@ extern "C"
976 return result;
977 }
978
979 + void freeze_coin(int index)
980 + {
981 + m_coins->setFrozen(index);
982 + }
983 +
984 + void thaw_coin(int index)
985 + {
986 + m_coins->thaw(index);
987 + }
988
989 #ifdef __cplusplus
990 }
cw_monero/lib/api/coins_info.dart
+12
@@ -16,8 +16,20 @@ final coinNative = moneroApi
16 .lookup<NativeFunction<coin>>('coin')
17 .asFunction<GetCoin>();
18
19 +final freezeCoinNative = moneroApi
20 + .lookup<NativeFunction<freeze_coin>>('freeze_coin')
21 + .asFunction<FreezeCoin>();
22 +
23 +final thawCoinNative = moneroApi
24 + .lookup<NativeFunction<thaw_coin>>('thaw_coin')
25 + .asFunction<ThawCoin>();
26 +
27 void refreshCoins(int accountIndex) => refreshCoinsNative(accountIndex);
28
29 int countOfCoins() => coinsCountNative();
30
31 CoinsInfoRow getCoin(int index) => coinNative(index).ref;
32 +
33 +void freezeCoin(int index) => freezeCoinNative(index);
34 +
35 +void thawCoin(int index) => thawCoinNative(index);
cw_monero/lib/api/signatures.dart
+7
@@ -1,6 +1,7 @@
1 import 'dart:ffi';
2 import 'package:cw_monero/api/structs/coins_info_row.dart';
3 import 'package:cw_monero/api/structs/pending_transaction.dart';
4 +import 'package:cw_monero/api/structs/transaction_info_row.dart';
5 import 'package:cw_monero/api/structs/ut8_box.dart';
6 import 'package:ffi/ffi.dart';
7
@@ -81,6 +82,8 @@ typedef account_set_label = Void Function(Int32 accountIndex, Pointer<Utf8> labe
82
83 typedef transactions_refresh = Void Function();
84
85 +typedef get_transaction = Pointer<TransactionInfoRow> Function(Pointer<Utf8> txId);
86 +
87 typedef get_tx_key = Pointer<Utf8>? Function(Pointer<Utf8> txId);
88
89 typedef transactions_count = Int64 Function();
@@ -139,3 +142,7 @@ typedef coins_count = Int64 Function();
142 // typedef coins_from_txid = Pointer<CoinsInfoRow> Function(Pointer<Utf8> txid);
143
144 typedef coin = Pointer<CoinsInfoRow> Function(Int32 index);
145 +
146 +typedef freeze_coin = Void Function(Int32 index);
147 +
148 +typedef thaw_coin = Void Function(Int32 index);
cw_monero/lib/api/transaction_history.dart
+16 -6
@@ -1,15 +1,16 @@
1 import 'dart:ffi';
2 +
3 import 'package:cw_monero/api/convert_utf8_to_string.dart';
4 +import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart';
5 +import 'package:cw_monero/api/monero_api.dart';
6 import 'package:cw_monero/api/monero_output.dart';
7 +import 'package:cw_monero/api/signatures.dart';
8 +import 'package:cw_monero/api/structs/pending_transaction.dart';
9 +import 'package:cw_monero/api/structs/transaction_info_row.dart';
10 import 'package:cw_monero/api/structs/ut8_box.dart';
11 +import 'package:cw_monero/api/types.dart';
12 import 'package:ffi/ffi.dart';
13 import 'package:flutter/foundation.dart';
7 -import 'package:cw_monero/api/signatures.dart';
8 -import 'package:cw_monero/api/types.dart';
9 -import 'package:cw_monero/api/monero_api.dart';
10 -import 'package:cw_monero/api/structs/transaction_info_row.dart';
11 -import 'package:cw_monero/api/structs/pending_transaction.dart';
12 -import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart';
14
15 final transactionsRefreshNative = moneroApi
16 .lookup<NativeFunction<transactions_refresh>>('transactions_refresh')
@@ -38,6 +39,10 @@ final transactionCommitNative = moneroApi
39 final getTxKeyNative =
40 moneroApi.lookup<NativeFunction<get_tx_key>>('get_tx_key').asFunction<GetTxKey>();
41
42 +final getTransactionNative = moneroApi
43 + .lookup<NativeFunction<get_transaction>>('get_transaction')
44 + .asFunction<GetTransaction>();
45 +
46 String getTxKey(String txId) {
47 final txIdPointer = txId.toNativeUtf8();
48 final keyPointer = getTxKeyNative(txIdPointer);
@@ -65,6 +70,11 @@ List<TransactionInfoRow> getAllTransactions() {
70 .toList();
71 }
72
73 +TransactionInfoRow getTransaction(String txId) {
74 + final txIdPointer = txId.toNativeUtf8();
75 + return getTransactionNative(txIdPointer).ref;
76 +}
77 +
78 PendingTransactionDescription createTransactionSync(
79 {required String address,
80 required String paymentId,
cw_monero/lib/api/types.dart
+7
@@ -1,6 +1,7 @@
1 import 'dart:ffi';
2 import 'package:cw_monero/api/structs/coins_info_row.dart';
3 import 'package:cw_monero/api/structs/pending_transaction.dart';
4 +import 'package:cw_monero/api/structs/transaction_info_row.dart';
5 import 'package:cw_monero/api/structs/ut8_box.dart';
6 import 'package:ffi/ffi.dart';
7
@@ -81,6 +82,8 @@ typedef AccountSetLabel = void Function(int accountIndex, Pointer<Utf8> label);
82
83 typedef TransactionsRefresh = void Function();
84
85 +typedef GetTransaction = Pointer<TransactionInfoRow> Function(Pointer<Utf8> txId);
86 +
87 typedef GetTxKey = Pointer<Utf8>? Function(Pointer<Utf8> txId);
88
89 typedef TransactionsCount = int Function();
@@ -139,3 +142,7 @@ typedef RefreshCoins = void Function(int);
142 typedef CoinsCount = int Function();
143
144 typedef GetCoin = Pointer<CoinsInfoRow> Function(int);
145 +
146 +typedef FreezeCoin = void Function(int);
147 +
148 +typedef ThawCoin = void Function(int);
cw_monero/lib/monero_unspent.dart
+14 -22
@@ -1,28 +1,20 @@
1 +import 'package:cw_core/unspent_transaction_output.dart';
2 import 'package:cw_monero/api/structs/coins_info_row.dart';
3
3 -class MoneroUnspent {
4 - MoneroUnspent(this.address, this.hash, this.keyImage, this.value, this.isFrozen, this.isUnlocked)
5 - : isSending = true,
6 - note = '';
4 +class MoneroUnspent extends Unspent {
5 + MoneroUnspent(
6 + String address, String hash, String keyImage, int value, bool isFrozen, this.isUnlocked)
7 + : super(address, hash, value, 0, keyImage) {
8 + this.isFrozen = isFrozen;
9 + }
10
8 - MoneroUnspent.fromCoinsInfoRow(CoinsInfoRow coinsInfoRow)
9 - : address = coinsInfoRow.getAddress(),
10 - hash = coinsInfoRow.getHash(),
11 - keyImage = coinsInfoRow.getKeyImage(),
12 - value = coinsInfoRow.amount,
13 - isFrozen = coinsInfoRow.frozen == 1,
14 - isUnlocked = coinsInfoRow.unlocked == 1,
15 - isSending = true,
16 - note = '';
17 -
18 - final String address;
19 - final String hash;
20 - final String keyImage;
21 - final int value;
11 + factory MoneroUnspent.fromCoinsInfoRow(CoinsInfoRow coinsInfoRow) => MoneroUnspent(
12 + coinsInfoRow.getAddress(),
13 + coinsInfoRow.getHash(),
14 + coinsInfoRow.getKeyImage(),
15 + coinsInfoRow.amount,
16 + coinsInfoRow.frozen == 1,
17 + coinsInfoRow.unlocked == 1);
18
19 final bool isUnlocked;
24 -
25 - bool isFrozen;
26 - bool isSending;
27 - String note;
20 }
cw_monero/lib/monero_wallet.dart
+20 -11
@@ -1,5 +1,6 @@
1 import 'dart:async';
2 import 'dart:io';
3 +
4 import 'package:cw_core/account.dart';
5 import 'package:cw_core/crypto_currency.dart';
6 import 'package:cw_core/monero_amount_format.dart';
@@ -22,14 +23,14 @@ import 'package:cw_monero/api/transaction_history.dart' as transaction_history;
23 import 'package:cw_monero/api/wallet.dart' as monero_wallet;
24 import 'package:cw_monero/exceptions/monero_transaction_creation_exception.dart';
25 import 'package:cw_monero/exceptions/monero_transaction_no_inputs_exception.dart';
25 -import 'package:cw_monero/pending_monero_transaction.dart';
26 import 'package:cw_monero/monero_transaction_creation_credentials.dart';
27 import 'package:cw_monero/monero_transaction_history.dart';
28 import 'package:cw_monero/monero_transaction_info.dart';
29 import 'package:cw_monero/monero_unspent.dart';
30 import 'package:cw_monero/monero_wallet_addresses.dart';
31 -import 'package:mobx/mobx.dart';
31 +import 'package:cw_monero/pending_monero_transaction.dart';
32 import 'package:hive/hive.dart';
33 +import 'package:mobx/mobx.dart';
34
35 part 'monero_wallet.g.dart';
36
@@ -204,7 +205,7 @@ abstract class MoneroWalletBase
205 for (final utx in unspentCoins) {
206 if (utx.isSending) {
207 allInputsAmount += utx.value;
207 - inputs.add(utx.keyImage);
208 + inputs.add(utx.keyImage!);
209 }
210 }
211 final spendAllCoins = inputs.length == unspentCoins.length;
@@ -395,7 +396,9 @@ abstract class MoneroWalletBase
396 for (var i = 0; i < coinCount; i++) {
397 final coin = getCoin(i);
398 if (coin.spent == 0) {
398 - unspentCoins.add(MoneroUnspent.fromCoinsInfoRow(coin));
399 + final unspent = MoneroUnspent.fromCoinsInfoRow(coin);
400 + unspent.isChange = transaction_history.getTransaction(unspent.hash).direction == 1;
401 + unspentCoins.add(unspent);
402 }
403 }
404
@@ -406,8 +409,10 @@ abstract class MoneroWalletBase
409
410 if (unspentCoins.isNotEmpty) {
411 unspentCoins.forEach((coin) {
409 - final coinInfoList = unspentCoinsInfo.values
410 - .where((element) => element.walletId.contains(id) && element.hash.contains(coin.hash));
412 + final coinInfoList = unspentCoinsInfo.values.where((element) =>
413 + element.walletId.contains(id) &&
414 + element.accountIndex == walletAddresses.account!.id &&
415 + element.keyImage!.contains(coin.keyImage!));
416
417 if (coinInfoList.isNotEmpty) {
418 final coinInfo = coinInfoList.first;
@@ -435,7 +440,9 @@ abstract class MoneroWalletBase
440 address: coin.address,
441 value: coin.value,
442 vout: 0,
438 - keyImage: coin.keyImage);
443 + keyImage: coin.keyImage,
444 + isChange: coin.isChange,
445 + accountIndex: walletAddresses.account!.id);
446
447 await unspentCoinsInfo.add(newInfo);
448 }
@@ -443,12 +450,13 @@ abstract class MoneroWalletBase
450 Future<void> _refreshUnspentCoinsInfo() async {
451 try {
452 final List<dynamic> keys = <dynamic>[];
446 - final currentWalletUnspentCoins =
447 - unspentCoinsInfo.values.where((element) => element.walletId.contains(id));
453 + final currentWalletUnspentCoins = unspentCoinsInfo.values.where((element) =>
454 + element.walletId.contains(id) && element.accountIndex == walletAddresses.account!.id);
455
456 if (currentWalletUnspentCoins.isNotEmpty) {
457 currentWalletUnspentCoins.forEach((element) {
451 - final existUnspentCoins = unspentCoins.where((coin) => element.hash.contains(coin.hash));
458 + final existUnspentCoins =
459 + unspentCoins.where((coin) => element.keyImage!.contains(coin.keyImage!));
460
461 if (existUnspentCoins.isEmpty) {
462 keys.add(element.key);
@@ -566,7 +574,8 @@ abstract class MoneroWalletBase
574 int _getFrozenBalance() {
575 var frozenBalance = 0;
576
569 - for (var coin in unspentCoinsInfo.values) {
577 + for (var coin in unspentCoinsInfo.values.where((element) =>
578 + element.walletId == id && element.accountIndex == walletAddresses.account!.id)) {
579 if (coin.isFrozen) frozenBalance += coin.value;
580 }
581
lib/monero/cw_monero.dart
+1 -4
@@ -321,10 +321,7 @@ class CWMonero extends Monero {
321 @override
322 List<Unspent> getUnspents(Object wallet) {
323 final moneroWallet = wallet as MoneroWallet;
324 - return moneroWallet.unspentCoins
325 - .map((MoneroUnspent moneroUnspent) => Unspent(moneroUnspent.address, moneroUnspent.hash,
326 - moneroUnspent.value, 0, moneroUnspent.keyImage))
327 - .toList();
324 + return moneroWallet.unspentCoins;
325 }
326
327 @override
lib/src/screens/unspent_coins/unspent_coins_list_page.dart
+15 -66
@@ -1,15 +1,13 @@
1 import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
2 +import 'package:cake_wallet/generated/i18n.dart';
3 import 'package:cake_wallet/routes.dart';
4 +import 'package:cake_wallet/src/screens/base_page.dart';
5 import 'package:cake_wallet/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart';
4 -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
5 -import 'package:cake_wallet/utils/show_pop_up.dart';
6 import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart';
7 import 'package:cw_core/wallet_type.dart';
8 -import 'package:flutter/material.dart';
8 import 'package:flutter/cupertino.dart';
10 -import 'package:cake_wallet/src/screens/base_page.dart';
9 +import 'package:flutter/material.dart';
10 import 'package:flutter_mobx/flutter_mobx.dart';
12 -import 'package:cake_wallet/generated/i18n.dart';
11
12 class UnspentCoinsListPage extends BasePage {
13 UnspentCoinsListPage({required this.unspentCoinsListViewModel});
@@ -17,31 +15,10 @@ class UnspentCoinsListPage extends BasePage {
15 @override
16 String get title => S.current.unspent_coins_title;
17
20 - //@override
21 - //Widget trailing(BuildContext context) {
22 - // final questionImage = Image.asset('assets/images/question_mark.png',
23 - // color: Theme.of(context).extension<CakeTextTheme>()!.titleColor);
24 -
25 - // return SizedBox(
26 - // height: 20.0,
27 - // width: 20.0,
28 - // child: ButtonTheme(
29 - // minWidth: double.minPositive,
30 - // child: FlatButton(
31 - // highlightColor: Colors.transparent,
32 - // splashColor: Colors.transparent,
33 - // padding: EdgeInsets.all(0),
34 - // onPressed: () => showUnspentCoinsAlert(context),
35 - // child: questionImage),
36 - // ),
37 - // );
38 - //}
39 -
18 final UnspentCoinsListViewModel unspentCoinsListViewModel;
19
20 @override
43 - Widget body(BuildContext context) =>
44 - UnspentCoinsListForm(unspentCoinsListViewModel);
21 + Widget body(BuildContext context) => UnspentCoinsListForm(unspentCoinsListViewModel);
22 }
23
24 class UnspentCoinsListForm extends StatefulWidget {
@@ -50,8 +27,7 @@ class UnspentCoinsListForm extends StatefulWidget {
27 final UnspentCoinsListViewModel unspentCoinsListViewModel;
28
29 @override
53 - UnspentCoinsListFormState createState() =>
54 - UnspentCoinsListFormState(unspentCoinsListViewModel);
30 + UnspentCoinsListFormState createState() => UnspentCoinsListFormState(unspentCoinsListViewModel);
31 }
32
33 class UnspentCoinsListFormState extends State<UnspentCoinsListForm> {
@@ -59,16 +35,6 @@ class UnspentCoinsListFormState extends State<UnspentCoinsListForm> {
35
36 final UnspentCoinsListViewModel unspentCoinsListViewModel;
37
62 - @override
63 - void initState() {
64 - super.initState();
65 - WidgetsBinding.instance.addPostFrameCallback(afterLayout);
66 - }
67 -
68 - void afterLayout(dynamic _) {
69 - //showUnspentCoinsAlert(context);
70 - }
71 -
38 @override
39 Widget build(BuildContext context) {
40 return Container(
@@ -76,8 +42,7 @@ class UnspentCoinsListFormState extends State<UnspentCoinsListForm> {
42 child: Observer(
43 builder: (_) => ListView.separated(
44 itemCount: unspentCoinsListViewModel.items.length,
79 - separatorBuilder: (_, __) =>
80 - SizedBox(height: 15),
45 + separatorBuilder: (_, __) => SizedBox(height: 15),
46 itemBuilder: (_, int index) {
47 return Observer(builder: (_) {
48 final item = unspentCoinsListViewModel.items[index];
@@ -86,38 +51,22 @@ class UnspentCoinsListFormState extends State<UnspentCoinsListForm> {
51 : item.address;
52
53 return GestureDetector(
89 - onTap: () =>
90 - Navigator.of(context)
91 - .pushNamed(Routes.unspentCoinsDetails,
92 - arguments: [item, unspentCoinsListViewModel]),
54 + onTap: () => Navigator.of(context).pushNamed(Routes.unspentCoinsDetails,
55 + arguments: [item, unspentCoinsListViewModel]),
56 child: UnspentCoinsListItem(
57 note: item.note,
58 amount: item.amount,
59 address: address,
60 isSending: item.isSending,
61 isFrozen: item.isFrozen,
62 + isChange: item.isChange,
63 onCheckBoxTap: item.isFrozen
100 - ? null
101 - : () async {
102 - item.isSending = !item.isSending;
103 - await unspentCoinsListViewModel
104 - .saveUnspentCoinInfo(item);}));
64 + ? null
65 + : () async {
66 + item.isSending = !item.isSending;
67 + await unspentCoinsListViewModel.saveUnspentCoinInfo(item);
68 + }));
69 });
106 - }
107 - )
108 - )
109 - );
70 + })));
71 }
72 }
112 -
113 -void showUnspentCoinsAlert(BuildContext context) {
114 - showPopUp<void>(
115 - context: context,
116 - builder: (BuildContext context) {
117 - return AlertWithOneAction(
118 - alertTitle: '',
119 - alertContent: 'Information about unspent coins',
120 - buttonText: S.of(context).ok,
121 - buttonAction: () => Navigator.of(context).pop());
122 - });
123 -}
\ No newline at end of file
lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart
+32 -14
@@ -1,8 +1,8 @@
1 import 'package:auto_size_text/auto_size_text.dart';
2 +import 'package:cake_wallet/generated/i18n.dart';
3 import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
4 import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
5 import 'package:flutter/material.dart';
5 -import 'package:cake_wallet/generated/i18n.dart';
6
7 class UnspentCoinsListItem extends StatelessWidget {
8 UnspentCoinsListItem({
@@ -11,6 +11,7 @@ class UnspentCoinsListItem extends StatelessWidget {
11 required this.address,
12 required this.isSending,
13 required this.isFrozen,
14 + required this.isChange,
15 this.onCheckBoxTap,
16 });
17
@@ -19,6 +20,7 @@ class UnspentCoinsListItem extends StatelessWidget {
20 final String address;
21 final bool isSending;
22 final bool isFrozen;
23 + final bool isChange;
24 final Function()? onCheckBoxTap;
25
26 @override
@@ -27,9 +29,8 @@ class UnspentCoinsListItem extends StatelessWidget {
29 final selectedItemColor = Theme.of(context).primaryColor;
30 final itemColor = isSending ? selectedItemColor : unselectedItemColor;
31
30 - final amountColor = isSending
31 - ? Colors.white
32 - : Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor;
32 + final amountColor =
33 + isSending ? Colors.white : Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor;
34 final addressColor = isSending
35 ? Colors.white.withOpacity(0.5)
36 : Theme.of(context).extension<CakeTextTheme>()!.buttonSecondaryTextColor;
@@ -47,7 +48,8 @@ class UnspentCoinsListItem extends StatelessWidget {
48 child: StandardCheckbox(
49 iconColor: amountColor,
50 borderColor: addressColor,
50 - value: isSending, onChanged: (value) => onCheckBoxTap?.call())),
51 + value: isSending,
52 + onChanged: (value) => onCheckBoxTap?.call())),
53 Expanded(
54 child: Column(
55 mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -57,9 +59,7 @@ class UnspentCoinsListItem extends StatelessWidget {
59 mainAxisAlignment: MainAxisAlignment.spaceBetween,
60 crossAxisAlignment: CrossAxisAlignment.start,
61 children: [
60 - Column(
61 - crossAxisAlignment: CrossAxisAlignment.start,
62 - children: [
62 + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
63 if (note.isNotEmpty)
64 AutoSizeText(
65 note,
@@ -69,8 +69,8 @@ class UnspentCoinsListItem extends StatelessWidget {
69 ),
70 AutoSizeText(
71 amount,
72 - style:
73 - TextStyle(color: amountColor, fontSize: 15, fontWeight: FontWeight.w600),
72 + style: TextStyle(
73 + color: amountColor, fontSize: 15, fontWeight: FontWeight.w600),
74 maxLines: 1,
75 )
76 ]),
@@ -84,23 +84,41 @@ class UnspentCoinsListItem extends StatelessWidget {
84 alignment: Alignment.center,
85 child: Text(
86 S.of(context).frozen,
87 - style:
88 - TextStyle(color: amountColor, fontSize: 7, fontWeight: FontWeight.w600),
89 - ))
87 + style: TextStyle(
88 + color: amountColor, fontSize: 7, fontWeight: FontWeight.w600),
89 + )),
90 ],
91 ),
92 Expanded(
93 child: Row(
94 crossAxisAlignment: CrossAxisAlignment.center,
95 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
96 children: [
97 AutoSizeText(
97 - '${address.substring(0, 5)}...${address.substring(address.length-5)}', // ToDo: Maybe use address label
98 + '${address.substring(0, 5)}...${address.substring(address.length - 5)}', // ToDo: Maybe use address label
99 style: TextStyle(
100 color: addressColor,
101 fontSize: 12,
102 ),
103 maxLines: 1,
104 ),
105 + if (isChange)
106 + Container(
107 + height: 17,
108 + padding: EdgeInsets.only(left: 6, right: 6),
109 + decoration: BoxDecoration(
110 + borderRadius: BorderRadius.all(Radius.circular(8.5)),
111 + color: Colors.white),
112 + alignment: Alignment.center,
113 + child: Text(
114 + S.of(context).unspent_change,
115 + style: TextStyle(
116 + color: itemColor,
117 + fontSize: 7,
118 + fontWeight: FontWeight.w600,
119 + ),
120 + ),
121 + ),
122 ],
123 ),
124 ),
lib/view_model/dashboard/balance_view_model.dart
+6 -12
@@ -175,10 +175,8 @@ abstract class BalanceViewModelBase with Store {
175 return '---';
176 }
177
178 - return _getFiatBalance(
179 - price: price,
180 - cryptoAmount: getFormattedFrozenBalance(walletBalance)) + ' ' + fiatCurrency.toString();
181 -
178 + return _getFiatBalance(price: price, cryptoAmount: getFormattedFrozenBalance(walletBalance)) +
179 + ' ${fiatCurrency.toString()}';
180 }
181
182 @computed
@@ -201,10 +199,8 @@ abstract class BalanceViewModelBase with Store {
199 return '---';
200 }
201
204 - return _getFiatBalance(
205 - price: price,
206 - cryptoAmount: walletBalance.formattedAvailableBalance) + ' ' + fiatCurrency.toString();
207 -
202 + return _getFiatBalance(price: price, cryptoAmount: walletBalance.formattedAvailableBalance) +
203 + ' ${fiatCurrency.toString()}';
204 }
205
206 @computed
@@ -216,10 +212,8 @@ abstract class BalanceViewModelBase with Store {
212 return '---';
213 }
214
219 - return _getFiatBalance(
220 - price: price,
221 - cryptoAmount: walletBalance.formattedAdditionalBalance) + ' ' + fiatCurrency.toString();
222 -
215 + return _getFiatBalance(price: price, cryptoAmount: walletBalance.formattedAdditionalBalance) +
216 + ' ${fiatCurrency.toString()}';
217 }
218
219 @computed
lib/view_model/unspent_coins/unspent_coins_item.dart
+4
@@ -12,6 +12,7 @@ abstract class UnspentCoinsItemBase with Store {
12 required this.isFrozen,
13 required this.note,
14 required this.isSending,
15 + required this.isChange,
16 required this.amountRaw,
17 required this.vout,
18 required this.keyImage
@@ -35,6 +36,9 @@ abstract class UnspentCoinsItemBase with Store {
36 @observable
37 bool isSending;
38
39 + @observable
40 + bool isChange;
41 +
42 @observable
43 int amountRaw;
44
lib/view_model/unspent_coins/unspent_coins_list_view_model.dart
+20 -39
@@ -1,10 +1,8 @@
1 -import 'package:collection/collection.dart';
1 import 'package:cake_wallet/bitcoin/bitcoin.dart';
3 -import 'package:cw_core/unspent_transaction_output.dart';
2 import 'package:cake_wallet/monero/monero.dart';
3 import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart';
4 import 'package:cw_core/unspent_coins_info.dart';
7 -import 'package:cw_core/wallet_addresses.dart';
5 +import 'package:cw_core/unspent_transaction_output.dart';
6 import 'package:cw_core/wallet_base.dart';
7 import 'package:cw_core/wallet_type.dart';
8 import 'package:hive/hive.dart';
@@ -26,66 +24,49 @@ abstract class UnspentCoinsListViewModelBase with Store {
24
25 @computed
26 ObservableList<UnspentCoinsItem> get items => ObservableList.of(_getUnspents().map((elem) {
29 - final amount = formatAmountToString(elem.value) + ' ${wallet.currency.title}';
30 -
27 final info =
28 getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout, elem.keyImage);
29
30 return UnspentCoinsItem(
31 address: elem.address,
36 - amount: amount,
32 + amount: '${formatAmountToString(elem.value)} ${wallet.currency.title}',
33 hash: elem.hash,
38 - isFrozen: info?.isFrozen ?? false,
39 - note: info?.note ?? '',
40 - isSending: info?.isSending ?? true,
34 + isFrozen: info.isFrozen,
35 + note: info.note,
36 + isSending: info.isSending,
37 amountRaw: elem.value,
38 vout: elem.vout,
43 - keyImage: elem.keyImage);
39 + keyImage: elem.keyImage,
40 + isChange: elem.isChange,
41 + );
42 }));
43
44 Future<void> saveUnspentCoinInfo(UnspentCoinsItem item) async {
45 try {
46 final info =
47 getUnspentCoinInfo(item.hash, item.address, item.amountRaw, item.vout, item.keyImage);
50 - if (info == null) {
51 - final newInfo = UnspentCoinsInfo(
52 - walletId: wallet.id,
53 - hash: item.hash,
54 - address: item.address,
55 - value: item.amountRaw,
56 - vout: item.vout,
57 - isFrozen: item.isFrozen,
58 - isSending: item.isSending,
59 - noteRaw: item.note,
60 - keyImage: item.keyImage);
48
62 - await _unspentCoinsInfo.add(newInfo);
63 - _updateUnspents();
64 - wallet.updateBalance();
65 - return;
66 - }
49 info.isFrozen = item.isFrozen;
50 info.isSending = item.isSending;
51 info.note = item.note;
52
53 await info.save();
72 - _updateUnspents();
73 - wallet.updateBalance();
54 + await _updateUnspents();
55 + await wallet.updateBalance();
56 } catch (e) {
57 print(e.toString());
58 }
59 }
60
79 - UnspentCoinsInfo? getUnspentCoinInfo(
80 - String hash, String address, int value, int vout, String? keyImage) {
81 - return _unspentCoinsInfo.values.firstWhereOrNull((element) =>
82 - element.walletId == wallet.id &&
83 - element.hash == hash &&
84 - element.address == address &&
85 - element.value == value &&
86 - element.vout == vout &&
87 - element.keyImage == keyImage);
88 - }
61 + UnspentCoinsInfo getUnspentCoinInfo(
62 + String hash, String address, int value, int vout, String? keyImage) =>
63 + _unspentCoinsInfo.values.firstWhere((element) =>
64 + element.walletId == wallet.id &&
65 + element.hash == hash &&
66 + element.address == address &&
67 + element.value == value &&
68 + element.vout == vout &&
69 + element.keyImage == keyImage);
70
71 String formatAmountToString(int fullBalance) {
72 if (wallet.type == WalletType.monero)
@@ -95,7 +76,7 @@ abstract class UnspentCoinsListViewModelBase with Store {
76 return '';
77 }
78
98 - void _updateUnspents() {
79 + Future<void> _updateUnspents() async {
80 if (wallet.type == WalletType.monero) return monero!.updateUnspents(wallet);
81 if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type))
82 return bitcoin!.updateUnspents(wallet);
res/values/strings_ar.arb
+2 -1
@@ -726,5 +726,6 @@
726 "domain_looks_up": "ﻝﺎﺠﻤﻟﺍ ﺚﺤﺑ ﺕﺎﻴﻠﻤﻋ",
727 "require_for_exchanges_to_external_wallets": "ﺔﻴﺟﺭﺎﺧ ﻆﻓﺎﺤﻣ ﻰﻟﺇ ﺕﻻﺩﺎﺒﺘﻟﺍ ﺐﻠﻄﺘﺗ",
728 "camera_permission_is_required": ".ﺍﺮﻴﻣﺎﻜﻟﺍ ﻥﺫﺇ ﺏﻮﻠﻄﻣ",
729 - "switchToETHWallet": "ﻯﺮﺧﺃ ﺓﺮﻣ ﺔﻟﻭﺎﺤﻤﻟﺍﻭ Ethereum ﺔﻈﻔﺤﻣ ﻰﻟﺇ ﻞﻳﺪﺒﺘﻟﺍ ﻰﺟﺮﻳ"
729 + "switchToETHWallet": "ﻯﺮﺧﺃ ﺓﺮﻣ ﺔﻟﻭﺎﺤﻤﻟﺍﻭ Ethereum ﺔﻈﻔﺤﻣ ﻰﻟﺇ ﻞﻳﺪﺒﺘﻟﺍ ﻰﺟﺮﻳ",
730 + "unspent_change": "يتغير"
731 }
res/values/strings_bg.arb
+2 -1
@@ -722,5 +722,6 @@
722 "domain_looks_up": "Търсене на домейни",
723 "require_for_exchanges_to_external_wallets": "Изискване за обмен към външни портфейли",
724 "camera_permission_is_required": "Изисква се разрешение за камерата.\nМоля, активирайте го от настройките на приложението.",
725 - "switchToETHWallet": "Моля, преминете към портфейл Ethereum и опитайте отново"
725 + "switchToETHWallet": "Моля, преминете към портфейл Ethereum и опитайте отново",
726 + "unspent_change": "Промяна"
727 }
res/values/strings_cs.arb
+2 -1
@@ -722,5 +722,6 @@
722 "domain_looks_up": "Vyhledávání domén",
723 "require_for_exchanges_to_external_wallets": "Vyžadovat pro výměny do externích peněženek",
724 "camera_permission_is_required": "Vyžaduje se povolení fotoaparátu.\nPovolte jej v nastavení aplikace.",
725 - "switchToETHWallet": "Přejděte na peněženku Ethereum a zkuste to znovu"
725 + "switchToETHWallet": "Přejděte na peněženku Ethereum a zkuste to znovu",
726 + "unspent_change": "Změna"
727 }
res/values/strings_de.arb
+2 -1
@@ -730,5 +730,6 @@
730 "domain_looks_up": "Domain-Suchen",
731 "require_for_exchanges_to_external_wallets": "Erforderlich für den Umtausch in externe Wallets",
732 "camera_permission_is_required": "Eine Kameraerlaubnis ist erforderlich.\nBitte aktivieren Sie es in den App-Einstellungen.",
733 - "switchToETHWallet": "Bitte wechseln Sie zu einem Ethereum-Wallet und versuchen Sie es erneut"
733 + "switchToETHWallet": "Bitte wechseln Sie zu einem Ethereum-Wallet und versuchen Sie es erneut",
734 + "unspent_change": "Wechselgeld"
735 }
res/values/strings_en.arb
+2 -1
@@ -731,5 +731,6 @@
731 "domain_looks_up": "Domain lookups",
732 "require_for_exchanges_to_external_wallets": "Require for exchanges to external wallets",
733 "camera_permission_is_required": "Camera permission is required. \nPlease enable it from app settings.",
734 - "switchToETHWallet": "Please switch to an Ethereum wallet and try again"
734 + "switchToETHWallet": "Please switch to an Ethereum wallet and try again",
735 + "unspent_change": "Change"
736 }
res/values/strings_es.arb
+2 -1
@@ -730,5 +730,6 @@
730 "domain_looks_up": "Búsquedas de dominio",
731 "require_for_exchanges_to_external_wallets": "Requerido para intercambios a billeteras externas",
732 "camera_permission_is_required": "Se requiere permiso de la cámara.\nHabilítelo desde la configuración de la aplicación.",
733 - "switchToETHWallet": "Cambie a una billetera Ethereum e inténtelo nuevamente."
733 + "switchToETHWallet": "Cambie a una billetera Ethereum e inténtelo nuevamente.",
734 + "unspent_change": "Cambiar"
735 }
res/values/strings_fr.arb
+2 -1
@@ -730,5 +730,6 @@
730 "domain_looks_up": "Recherches de domaine",
731 "require_for_exchanges_to_external_wallets": "Exiger pour les échanges vers des portefeuilles externes",
732 "camera_permission_is_required": "L'autorisation de la caméra est requise.\nVeuillez l'activer à partir des paramètres de l'application.",
733 - "switchToETHWallet": "Veuillez passer à un portefeuille (wallet) Ethereum et réessayer"
733 + "switchToETHWallet": "Veuillez passer à un portefeuille (wallet) Ethereum et réessayer",
734 + "unspent_change": "Changement"
735 }
res/values/strings_ha.arb
+2 -1
@@ -708,5 +708,6 @@
708 "domain_looks_up": "Binciken yanki",
709 "require_for_exchanges_to_external_wallets": "Bukatar musanya zuwa wallet na waje",
710 "camera_permission_is_required": "Ana buƙatar izinin kyamara.\nDa fatan za a kunna shi daga saitunan app.",
711 - "switchToETHWallet": "Da fatan za a canza zuwa walat ɗin Ethereum kuma a sake gwadawa"
711 + "switchToETHWallet": "Da fatan za a canza zuwa walat ɗin Ethereum kuma a sake gwadawa",
712 + "unspent_change": "Canza"
713 }
res/values/strings_hi.arb
+2 -1
@@ -730,5 +730,6 @@
730 "domain_looks_up": "डोमेन लुकअप",
731 "require_for_exchanges_to_external_wallets": "बाहरी वॉलेट में एक्सचेंज की आवश्यकता है",
732 "camera_permission_is_required": "कैमरे की अनुमति आवश्यक है.\nकृपया इसे ऐप सेटिंग से सक्षम करें।",
733 - "switchToETHWallet": "कृपया एथेरियम वॉलेट पर स्विच करें और पुनः प्रयास करें"
733 + "switchToETHWallet": "कृपया एथेरियम वॉलेट पर स्विच करें और पुनः प्रयास करें",
734 + "unspent_change": "परिवर्तन"
735 }
res/values/strings_hr.arb
+2 -1
@@ -728,5 +728,6 @@
728 "domain_looks_up": "Pretraga domena",
729 "require_for_exchanges_to_external_wallets": "Zahtijeva razmjene na vanjske novčanike",
730 "camera_permission_is_required": "Potrebno je dopuštenje kamere.\nOmogućite ga u postavkama aplikacije.",
731 - "switchToETHWallet": "Prijeđite na Ethereum novčanik i pokušajte ponovno"
731 + "switchToETHWallet": "Prijeđite na Ethereum novčanik i pokušajte ponovno",
732 + "unspent_change": "Promijeniti"
733 }
res/values/strings_id.arb
+2 -1
@@ -718,5 +718,6 @@
718 "domain_looks_up": "Pencarian domain",
719 "require_for_exchanges_to_external_wallets": "Memerlukan pertukaran ke dompet eksternal",
720 "camera_permission_is_required": "Izin kamera diperlukan.\nSilakan aktifkan dari pengaturan aplikasi.",
721 - "switchToETHWallet": "Silakan beralih ke dompet Ethereum dan coba lagi"
721 + "switchToETHWallet": "Silakan beralih ke dompet Ethereum dan coba lagi",
722 + "unspent_change": "Mengubah"
723 }
res/values/strings_it.arb
+2 -1
@@ -730,5 +730,6 @@
730 "domain_looks_up": "Ricerche di domini",
731 "require_for_exchanges_to_external_wallets": "Richiede scambi con portafogli esterni",
732 "camera_permission_is_required": "È richiesta l'autorizzazione della fotocamera.\nAbilitalo dalle impostazioni dell'app.",
733 - "switchToETHWallet": "Passa a un portafoglio Ethereum e riprova"
733 + "switchToETHWallet": "Passa a un portafoglio Ethereum e riprova",
734 + "unspent_change": "Modifica"
735 }
res/values/strings_ja.arb
+2 -1
@@ -730,5 +730,6 @@
730 "domain_looks_up": "ドメイン検索",
731 "require_for_exchanges_to_external_wallets": "外部ウォレットへの交換に必要",
732 "camera_permission_is_required": "カメラの許可が必要です。\nアプリの設定から有効にしてください。",
733 - "switchToETHWallet": "イーサリアムウォレットに切り替えてもう一度お試しください"
733 + "switchToETHWallet": "イーサリアムウォレットに切り替えてもう一度お試しください",
734 + "unspent_change": "変化"
735 }
res/values/strings_ko.arb
+2 -1
@@ -728,5 +728,6 @@
728 "domain_looks_up": "도메인 조회",
729 "require_for_exchanges_to_external_wallets": "외부 지갑으로의 교환을 위해 필요",
730 "camera_permission_is_required": "카메라 권한이 필요합니다.\n앱 설정에서 활성화해 주세요.",
731 - "switchToETHWallet": "이더리움 지갑으로 전환한 후 다시 시도해 주세요."
731 + "switchToETHWallet": "이더리움 지갑으로 전환한 후 다시 시도해 주세요.",
732 + "unspent_change": "변화"
733 }
res/values/strings_my.arb
+2 -1
@@ -728,5 +728,6 @@
728 "domain_looks_up": "ဒိုမိန်းရှာဖွေမှုများ",
729 "require_for_exchanges_to_external_wallets": "ပြင်ပပိုက်ဆံအိတ်များသို့ လဲလှယ်ရန် လိုအပ်သည်။",
730 "camera_permission_is_required": "ကင်မရာခွင့်ပြုချက် လိုအပ်ပါသည်။\nအက်ပ်ဆက်တင်များမှ ၎င်းကိုဖွင့်ပါ။",
731 - "switchToETHWallet": "ကျေးဇူးပြု၍ Ethereum ပိုက်ဆံအိတ်သို့ ပြောင်းပြီး ထပ်စမ်းကြည့်ပါ။"
731 + "switchToETHWallet": "ကျေးဇူးပြု၍ Ethereum ပိုက်ဆံအိတ်သို့ ပြောင်းပြီး ထပ်စမ်းကြည့်ပါ။",
732 + "unspent_change": "ပေြာင်းလဲခြင်း"
733 }
res/values/strings_nl.arb
+2 -1
@@ -730,5 +730,6 @@
730 "domain_looks_up": "Domein opzoeken",
731 "require_for_exchanges_to_external_wallets": "Vereist voor uitwisselingen naar externe portemonnees",
732 "camera_permission_is_required": "Cameratoestemming is vereist.\nSchakel dit in via de app-instellingen.",
733 - "switchToETHWallet": "Schakel over naar een Ethereum-portemonnee en probeer het opnieuw"
733 + "switchToETHWallet": "Schakel over naar een Ethereum-portemonnee en probeer het opnieuw",
734 + "unspent_change": "Wijziging"
735 }
res/values/strings_pl.arb
+2 -1
@@ -730,5 +730,6 @@
730 "domain_looks_up": "Wyszukiwanie domen",
731 "require_for_exchanges_to_external_wallets": "Wymagaj wymiany na portfele zewnętrzne",
732 "camera_permission_is_required": "Wymagane jest pozwolenie na korzystanie z aparatu.\nWłącz tę funkcję w ustawieniach aplikacji.",
733 - "switchToETHWallet": "Przejdź na portfel Ethereum i spróbuj ponownie"
733 + "switchToETHWallet": "Przejdź na portfel Ethereum i spróbuj ponownie",
734 + "unspent_change": "Zmiana"
735 }
res/values/strings_pt.arb
+2 -1
@@ -729,5 +729,6 @@
729 "domain_looks_up": "Pesquisas de domínio",
730 "require_for_exchanges_to_external_wallets": "Exigir trocas para carteiras externas",
731 "camera_permission_is_required": "É necessária permissão da câmera.\nAtive-o nas configurações do aplicativo.",
732 - "switchToETHWallet": "Mude para uma carteira Ethereum e tente novamente"
732 + "switchToETHWallet": "Mude para uma carteira Ethereum e tente novamente",
733 + "unspent_change": "Mudar"
734 }
res/values/strings_ru.arb
+2 -1
@@ -730,5 +730,6 @@
730 "domain_looks_up": "Поиск доменов",
731 "require_for_exchanges_to_external_wallets": "Требовать обмена на внешние кошельки",
732 "camera_permission_is_required": "Требуется разрешение камеры.\nПожалуйста, включите его в настройках приложения.",
733 - "switchToETHWallet": "Пожалуйста, переключитесь на кошелек Ethereum и повторите попытку."
733 + "switchToETHWallet": "Пожалуйста, переключитесь на кошелек Ethereum и повторите попытку.",
734 + "unspent_change": "Изменять"
735 }
res/values/strings_th.arb
+2 -1
@@ -728,5 +728,6 @@
728 "domain_looks_up": "การค้นหาโดเมน",
729 "require_for_exchanges_to_external_wallets": "จำเป็นต้องแลกเปลี่ยนกับกระเป๋าเงินภายนอก",
730 "camera_permission_is_required": "ต้องได้รับอนุญาตจากกล้อง\nโปรดเปิดใช้งานจากการตั้งค่าแอป",
731 - "switchToETHWallet": "โปรดเปลี่ยนไปใช้กระเป๋าเงิน Ethereum แล้วลองอีกครั้ง"
731 + "switchToETHWallet": "โปรดเปลี่ยนไปใช้กระเป๋าเงิน Ethereum แล้วลองอีกครั้ง",
732 + "unspent_change": "เปลี่ยน"
733 }
res/values/strings_tl.arb
+2 -1
@@ -725,5 +725,6 @@
725 "domain_looks_up": "Mga paghahanap ng domain",
726 "require_for_exchanges_to_external_wallets": "Kinakailangan para sa mga palitan sa mga panlabas na wallet",
727 "camera_permission_is_required": "Kinakailangan ang pahintulot sa camera.\nMangyaring paganahin ito mula sa mga setting ng app.",
728 - "switchToETHWallet": "Mangyaring lumipat sa isang Ethereum wallet at subukang muli"
728 + "switchToETHWallet": "Mangyaring lumipat sa isang Ethereum wallet at subukang muli",
729 + "unspent_change": "Baguhin"
730 }
res/values/strings_tr.arb
+2 -1
@@ -728,5 +728,6 @@
728 "domain_looks_up": "Etki alanı aramaları",
729 "require_for_exchanges_to_external_wallets": "Harici cüzdanlara geçiş yapılmasını zorunlu kılın",
730 "camera_permission_is_required": "Kamera izni gereklidir.\nLütfen uygulama ayarlarından etkinleştirin.",
731 - "switchToETHWallet": "Lütfen bir Ethereum cüzdanına geçin ve tekrar deneyin"
731 + "switchToETHWallet": "Lütfen bir Ethereum cüzdanına geçin ve tekrar deneyin",
732 + "unspent_change": "Değiştirmek"
733 }
res/values/strings_uk.arb
+2 -1
@@ -730,5 +730,6 @@
730 "domain_looks_up": "Пошук доменів",
731 "require_for_exchanges_to_external_wallets": "Потрібен для обміну на зовнішні гаманці",
732 "camera_permission_is_required": "Потрібен дозвіл камери.\nУвімкніть його в налаштуваннях програми.",
733 - "switchToETHWallet": "Перейдіть на гаманець Ethereum і повторіть спробу"
733 + "switchToETHWallet": "Перейдіть на гаманець Ethereum і повторіть спробу",
734 + "unspent_change": "Зміна"
735 }
res/values/strings_ur.arb
+2 -1
@@ -722,5 +722,6 @@
722 "domain_looks_up": "ڈومین تلاش کرنا",
723 "require_for_exchanges_to_external_wallets": "۔ﮯﮨ ﺕﺭﻭﺮﺿ ﯽﮐ ﮯﻟﺩﺎﺒﺗ ﮟﯿﻣ ﮮﻮﭩﺑ ﯽﻧﻭﺮﯿﺑ",
724 "camera_permission_is_required": "۔ﮯﮨ ﺭﺎﮐﺭﺩ ﺕﺯﺎﺟﺍ ﯽﮐ ﮮﺮﻤﯿﮐ",
725 - "switchToETHWallet": "۔ﮟﯾﺮﮐ ﺶﺷﻮﮐ ﮦﺭﺎﺑﻭﺩ ﺭﻭﺍ ﮟﯾﺮﮐ ﭻﺋﻮﺳ ﺮﭘ ﭧﯿﻟﺍﻭ Ethereum ﻡﺮﮐ ﮦﺍﺮﺑ"
725 + "switchToETHWallet": "۔ﮟﯾﺮﮐ ﺶﺷﻮﮐ ﮦﺭﺎﺑﻭﺩ ﺭﻭﺍ ﮟﯾﺮﮐ ﭻﺋﻮﺳ ﺮﭘ ﭧﯿﻟﺍﻭ Ethereum ﻡﺮﮐ ﮦﺍﺮﺑ",
726 + "unspent_change": "تبدیل کریں"
727 }
res/values/strings_yo.arb
+2 -1
@@ -724,5 +724,6 @@
724 "domain_looks_up": "Awọn wiwa agbegbe",
725 "require_for_exchanges_to_external_wallets": "Beere fun awọn paṣipaarọ si awọn apamọwọ ita",
726 "camera_permission_is_required": "A nilo igbanilaaye kamẹra.\nJọwọ jeki o lati app eto.",
727 - "switchToETHWallet": "Jọwọ yipada si apamọwọ Ethereum ki o tun gbiyanju lẹẹkansi"
727 + "switchToETHWallet": "Jọwọ yipada si apamọwọ Ethereum ki o tun gbiyanju lẹẹkansi",
728 + "unspent_change": "Yipada"
729 }
res/values/strings_zh.arb
+2 -1
@@ -729,5 +729,6 @@
729 "domain_looks_up": "域名查找",
730 "require_for_exchanges_to_external_wallets": "需要兑换到外部钱包",
731 "camera_permission_is_required": "需要相机许可。\n请从应用程序设置中启用它。",
732 - "switchToETHWallet": "请切换到以太坊钱包并重试"
732 + "switchToETHWallet": "请切换到以太坊钱包并重试",
733 + "unspent_change": "改变"
734 }