feat: change scanning to subscription model, sync improvements

This commit is contained in:
Rafael Saes 2024-04-17 16:35:11 -03:00
parent a887ea74b5
commit e4156ba282
51 changed files with 696 additions and 406 deletions

View file

@ -90,7 +90,8 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
String? scriptHash;
String updateScriptHash(BasedUtxoNetwork network) {
String getScriptHash(BasedUtxoNetwork network) {
if (scriptHash != null) return scriptHash!;
scriptHash = sh.scriptHash(address, network: network);
return scriptHash!;
}

View file

@ -2,18 +2,16 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_core/unspent_transaction_output.dart';
class BitcoinUnspent extends Unspent {
BitcoinUnspent(BaseBitcoinAddressRecord addressRecord, String hash, int value, int vout,
{this.silentPaymentTweak})
BitcoinUnspent(BaseBitcoinAddressRecord addressRecord, String hash, int value, int vout)
: bitcoinAddressRecord = addressRecord,
super(addressRecord.address, hash, value, vout, null);
factory BitcoinUnspent.fromJSON(BaseBitcoinAddressRecord address, Map<String, dynamic> json) =>
factory BitcoinUnspent.fromJSON(BaseBitcoinAddressRecord? address, Map<String, dynamic> json) =>
BitcoinUnspent(
address,
address ?? BitcoinAddressRecord.fromJSON(json['address_record'].toString()),
json['tx_hash'] as String,
json['value'] as int,
json['tx_pos'] as int,
silentPaymentTweak: json['silent_payment_tweak'] as String?,
);
Map<String, dynamic> toJson() {
@ -22,11 +20,48 @@ class BitcoinUnspent extends Unspent {
'tx_hash': hash,
'value': value,
'tx_pos': vout,
'silent_payment_tweak': silentPaymentTweak,
};
return json;
}
final BaseBitcoinAddressRecord bitcoinAddressRecord;
String? silentPaymentTweak;
}
class BitcoinSilentPaymentsUnspent extends BitcoinUnspent {
BitcoinSilentPaymentsUnspent(
BitcoinSilentPaymentAddressRecord addressRecord,
String hash,
int value,
int vout, {
required this.silentPaymentTweak,
required this.silentPaymentLabel,
}) : super(addressRecord, hash, value, vout);
@override
factory BitcoinSilentPaymentsUnspent.fromJSON(
BitcoinSilentPaymentAddressRecord? address, Map<String, dynamic> json) =>
BitcoinSilentPaymentsUnspent(
address ?? BitcoinSilentPaymentAddressRecord.fromJSON(json['address_record'].toString()),
json['tx_hash'] as String,
json['value'] as int,
json['tx_pos'] as int,
silentPaymentTweak: json['silent_payment_tweak'] as String?,
silentPaymentLabel: json['silent_payment_label'] as String?,
);
@override
Map<String, dynamic> toJson() {
final json = <String, dynamic>{
'address_record': bitcoinAddressRecord.toJSON(),
'tx_hash': hash,
'value': value,
'tx_pos': vout,
'silent_payment_tweak': silentPaymentTweak,
'silent_payment_label': silentPaymentLabel,
};
return json;
}
String? silentPaymentTweak;
String? silentPaymentLabel;
}

View file

@ -64,19 +64,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
),
);
hasSilentPaymentsScanning = true;
autorun((_) {
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
});
reaction((_) => walletAddresses.addressPageType, (BitcoinAddressType addressPageType) {
final prev = hasSilentPaymentsScanning;
hasSilentPaymentsScanning = addressPageType == SilentPaymentsAddresType.p2sp;
if (prev != hasSilentPaymentsScanning) {
startSync();
}
});
}
static Future<BitcoinWallet> create({

View file

@ -36,14 +36,15 @@ class ElectrumClient {
_errors = {},
unterminatedString = '';
static const connectionTimeout = Duration(seconds: 300);
static const aliveTimerDuration = Duration(seconds: 300);
static const connectionTimeout = Duration(seconds: 10);
static const aliveTimerDuration = Duration(seconds: 10);
bool get isConnected => _isConnected;
Socket? socket;
void Function(bool)? onConnectionStatusChange;
int _id;
final Map<String, SocketTask> _tasks;
Map<String, SocketTask> get tasks => _tasks;
final Map<String, String> _errors;
bool _isConnected;
Timer? _aliveTimer;
@ -277,11 +278,18 @@ class ElectrumClient {
Future<Map<String, dynamic>> getHeader({required int height}) async =>
await call(method: 'blockchain.block.get_header', params: [height]) as Map<String, dynamic>;
Future<Map<String, dynamic>> getTweaks({required int height, required int count}) async =>
await callWithTimeout(
method: 'blockchain.block.tweaks',
params: [height, count],
timeout: 10000) as Map<String, dynamic>;
BehaviorSubject<Object>? tweaksSubscribe({required int height}) {
_id += 1;
return subscribe<Object>(
id: 'blockchain.tweaks.subscribe',
method: 'blockchain.tweaks.subscribe',
params: [height],
);
}
Future<Map<String, dynamic>> getTweaks({required int height}) async =>
await callWithTimeout(method: 'blockchain.tweaks.get', params: [height], timeout: 10000)
as Map<String, dynamic>;
Future<double> estimatefee({required int p}) =>
call(method: 'blockchain.estimatefee', params: [p]).then((dynamic result) {
@ -325,9 +333,6 @@ class ElectrumClient {
});
Future<List<int>> feeRates({BasedUtxoNetwork? network}) async {
if (network == BitcoinNetwork.testnet) {
return [1, 1, 1];
}
try {
final topDoubleString = await estimatefee(p: 1);
final middleDoubleString = await estimatefee(p: 5);
@ -349,7 +354,7 @@ class ElectrumClient {
// "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4"
// }
Future<int?> getCurrentBlockChainTip() =>
call(method: 'blockchain.headers.subscribe').then((result) {
callWithTimeout(method: 'blockchain.headers.subscribe').then((result) {
if (result is Map<String, dynamic>) {
return result["height"] as int;
}
@ -357,6 +362,12 @@ class ElectrumClient {
return null;
});
BehaviorSubject<Object>? chainTipSubscribe() {
_id += 1;
return subscribe<Object>(
id: 'blockchain.headers.subscribe', method: 'blockchain.headers.subscribe');
}
BehaviorSubject<Object>? chainTipUpdate() {
_id += 1;
return subscribe<Object>(
@ -456,6 +467,11 @@ class ElectrumClient {
_tasks[id]?.subject?.add(params.last);
break;
case 'blockchain.tweaks.subscribe':
case 'blockchain.headers.subscribe':
final params = request['params'] as List<dynamic>;
_tasks[method]?.subject?.add(params.last);
break;
default:
break;
}

View file

@ -23,7 +23,7 @@ class ElectrumBalance extends Balance {
}
int confirmed;
final int unconfirmed;
int unconfirmed;
final int frozen;
@override

View file

@ -18,7 +18,7 @@ class ElectrumTransactionBundle {
}
class ElectrumTransactionInfo extends TransactionInfo {
List<BitcoinUnspent>? unspents;
List<BitcoinSilentPaymentsUnspent>? unspents;
ElectrumTransactionInfo(this.type,
{required String id,
@ -178,9 +178,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
outputAddresses.isEmpty ? [] : outputAddresses.map((e) => e.toString()).toList(),
to: data['to'] as String?,
unspents: unspents
.map((unspent) => BitcoinUnspent.fromJSON(
BitcoinSilentPaymentAddressRecord.fromJSON(unspent['address_record'].toString()),
unspent as Map<String, dynamic>))
.map((unspent) =>
BitcoinSilentPaymentsUnspent.fromJSON(null, unspent as Map<String, dynamic>))
.toList(),
);
}

View file

@ -91,7 +91,13 @@ abstract class ElectrumWalletBase
transactionHistory = ElectrumTransactionHistory(walletInfo: walletInfo, password: password);
reaction((_) => syncStatus, (SyncStatus syncStatus) {
if (syncStatus is! AttemptingSyncStatus)
silentPaymentsScanningActive = syncStatus is SyncingSyncStatus;
if (syncStatus is NotConnectedSyncStatus) {
// Needs to re-subscribe to all scripthashes when reconnected
_scripthashesUpdateSubject = {};
}
});
}
@ -122,14 +128,7 @@ abstract class ElectrumWalletBase
@observable
SyncStatus syncStatus;
List<String> get scriptHashes => walletAddresses.allAddresses
.map((addr) => scriptHash(addr.address, network: network))
.toList();
List<String> get publicScriptHashes => walletAddresses.allAddresses
.where((addr) => !addr.isHidden)
.map((addr) => scriptHash(addr.address, network: network))
.toList();
Set<String> get addressesSet => walletAddresses.allAddresses.map((addr) => addr.address).toSet();
String get xpub => hd.base58!;
@ -142,8 +141,8 @@ abstract class ElectrumWalletBase
@override
bool? isTestnet;
@observable
bool hasSilentPaymentsScanning = false;
bool get hasSilentPaymentsScanning => type == WalletType.bitcoin;
@observable
bool nodeSupportsSilentPayments = true;
@observable
@ -151,13 +150,14 @@ abstract class ElectrumWalletBase
@action
Future<void> setSilentPaymentsScanning(bool active) async {
syncStatus = AttemptingSyncStatus();
silentPaymentsScanningActive = active;
if (active) {
await _setInitialHeight();
if ((currentChainTip ?? 0) > walletInfo.restoreHeight) {
_setListeners(walletInfo.restoreHeight, chainTip: currentChainTip);
if (await currentChainTip > walletInfo.restoreHeight) {
_setListeners(walletInfo.restoreHeight, chainTipParam: _currentChainTip);
}
} else {
_isolate?.then((runningIsolate) => runningIsolate.kill(priority: Isolate.immediate));
@ -174,7 +174,11 @@ abstract class ElectrumWalletBase
}
@observable
int? currentChainTip;
int? _currentChainTip;
@computed
Future<int> get currentChainTip async =>
_currentChainTip ?? await electrumClient.getCurrentBlockChainTip() ?? 0;
@override
BitcoinWalletKeys get keys =>
@ -183,7 +187,9 @@ abstract class ElectrumWalletBase
String _password;
List<BitcoinUnspent> unspentCoins;
List<int> _feeRates;
// ignore: prefer_final_fields
Map<String, BehaviorSubject<Object>?> _scripthashesUpdateSubject;
// ignore: prefer_final_fields
BehaviorSubject<Object>? _chainTipUpdateSubject;
bool _isTransactionUpdating;
Future<Isolate>? _isolate;
@ -201,8 +207,8 @@ abstract class ElectrumWalletBase
}
@action
Future<void> _setListeners(int height, {int? chainTip, bool? doSingleScan}) async {
final currentChainTip = chainTip ?? await electrumClient.getCurrentBlockChainTip() ?? 0;
Future<void> _setListeners(int height, {int? chainTipParam, bool? doSingleScan}) async {
final chainTip = chainTipParam ?? await currentChainTip;
syncStatus = AttemptingSyncStatus();
if (_isolate != null) {
@ -218,7 +224,7 @@ abstract class ElectrumWalletBase
silentAddress: walletAddresses.silentAddress!,
network: network,
height: height,
chainTip: currentChainTip,
chainTip: chainTip,
electrumClient: ElectrumClient(),
transactionHistoryIds: transactionHistory.transactions.keys.toList(),
node: ScanNode(node!.uri, node!.useSSL),
@ -237,12 +243,33 @@ abstract class ElectrumWalletBase
final tx = map.value;
if (tx.unspents != null) {
tx.unspents!.forEach((unspent) => walletAddresses.addSilentAddresses(
[unspent.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord]));
final existingTxInfo = transactionHistory.transactions[txid];
final txAlreadyExisted = existingTxInfo != null;
void updateSilentAddressRecord(BitcoinSilentPaymentsUnspent unspent) {
final silentAddress = walletAddresses.silentAddress!;
final silentPaymentAddress = SilentPaymentAddress(
version: silentAddress.version,
B_scan: silentAddress.B_scan,
B_spend: unspent.silentPaymentLabel != null
? silentAddress.B_spend.tweakAdd(
BigintUtils.fromBytes(
BytesUtils.fromHexString(unspent.silentPaymentLabel!)),
)
: silentAddress.B_spend,
hrp: silentAddress.hrp,
);
final addressRecord = walletAddresses.silentAddresses.firstWhereOrNull(
(address) => address.address == silentPaymentAddress.toString());
addressRecord?.txCount += 1;
addressRecord?.balance += unspent.value;
walletAddresses.addSilentAddresses(
[unspent.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord],
);
}
// Updating tx after re-scanned
if (txAlreadyExisted) {
existingTxInfo.amount = tx.amount;
@ -258,37 +285,39 @@ abstract class ElectrumWalletBase
.toList();
if (newUnspents.isNotEmpty) {
newUnspents.forEach(updateSilentAddressRecord);
existingTxInfo.unspents ??= [];
existingTxInfo.unspents!.addAll(newUnspents);
final amount = newUnspents.length > 1
final newAmount = newUnspents.length > 1
? newUnspents.map((e) => e.value).reduce((value, unspent) => value + unspent)
: newUnspents[0].value;
if (existingTxInfo.direction == TransactionDirection.incoming) {
existingTxInfo.amount += amount;
} else {
existingTxInfo.amount = amount;
existingTxInfo.direction = TransactionDirection.incoming;
existingTxInfo.amount += newAmount;
}
// Updates existing TX
transactionHistory.addOne(existingTxInfo);
// Update balance record
balance[currency]!.confirmed += newAmount;
}
} else {
final addressRecord =
walletAddresses.silentAddresses.firstWhere((addr) => addr.address == tx.to);
addressRecord.txCount += 1;
// else: First time seeing this TX after scanning
tx.unspents!.forEach(updateSilentAddressRecord);
// Add new TX record
transactionHistory.addMany(message);
// Update balance record
balance[currency]!.confirmed += tx.amount;
}
await transactionHistory.save();
await updateUnspent();
await updateBalance();
await updateAllUnspents();
}
}
}
// check if is a SyncStatus type since "is SyncStatus" doesn't work here
if (message is SyncResponse) {
if (message.syncStatus is UnsupportedSyncStatus) {
nodeSupportsSilentPayments = false;
@ -296,7 +325,6 @@ abstract class ElectrumWalletBase
syncStatus = message.syncStatus;
walletInfo.restoreHeight = message.height;
await walletInfo.save();
}
}
}
@ -305,32 +333,25 @@ abstract class ElectrumWalletBase
@override
Future<void> startSync() async {
try {
syncStatus = AttemptingSyncStatus();
syncStatus = SyncronizingSyncStatus();
if (hasSilentPaymentsScanning) {
try {
await _setInitialHeight();
} catch (_) {}
}
if (silentPaymentsScanningActive) {
if ((currentChainTip ?? 0) > walletInfo.restoreHeight) {
_setListeners(walletInfo.restoreHeight, chainTip: currentChainTip);
}
await _subscribeForUpdates();
int finished = 0;
void checkFinishedAllUpdates(void _) {
finished++;
if (finished == 2) syncStatus = SyncedSyncStatus();
}
// Always await first as it discovers new addresses in the process
await updateTransactions();
_subscribeForUpdates();
await updateUnspent();
await updateBalance();
_feeRates = await electrumClient.feeRates(network: network);
Timer.periodic(
const Duration(minutes: 1), (timer) async => _feeRates = await electrumClient.feeRates());
if (!silentPaymentsScanningActive || walletInfo.restoreHeight == currentChainTip) {
syncStatus = SyncedSyncStatus();
}
updateAllUnspents().then(checkFinishedAllUpdates);
updateBalance().then(checkFinishedAllUpdates);
} catch (e, stacktrace) {
print(stacktrace);
print(e.toString());
@ -338,29 +359,39 @@ abstract class ElectrumWalletBase
}
}
@action
Future<void> updateFeeRates() async {
final feeRates = await electrumClient.feeRates(network: network);
if (feeRates != [0, 0, 0]) {
_feeRates = feeRates;
}
}
Node? node;
@action
@override
Future<void> connectToNode({required Node node}) async {
final differentNode = this.node?.uri != node.uri || this.node?.useSSL != node.useSSL;
this.node = node;
try {
syncStatus = ConnectingSyncStatus();
if (!electrumClient.isConnected) {
electrumClient.onConnectionStatusChange = null;
await electrumClient.close();
}
await Timer(Duration(seconds: differentNode ? 0 : 10), () async {
electrumClient.onConnectionStatusChange = (bool isConnected) async {
if (isConnected) {
if (isConnected && syncStatus is! SyncedSyncStatus) {
syncStatus = ConnectedSyncStatus();
} else if (isConnected == false) {
} else if (!isConnected) {
syncStatus = LostConnectionSyncStatus();
}
};
await electrumClient.connectToUri(node.uri, useSSL: node.useSSL);
});
} catch (e) {
print(e.toString());
syncStatus = FailedSyncStatus();
@ -436,8 +467,15 @@ abstract class ElectrumWalletBase
for (int i = 0; i < unspentCoins.length; i++) {
final utx = unspentCoins[i];
if (utx.isSending) {
spendsCPFP = utx.confirmations == 0;
if (utx.isSending && !utx.isFrozen) {
if (hasSilentPayment) {
// Check inputs for shared secret derivation
if (utx.bitcoinAddressRecord.type == SegwitAddresType.p2wsh) {
throw BitcoinTransactionSilentPaymentsNotSupported();
}
}
if (!spendsCPFP) spendsCPFP = utx.confirmations == 0;
allInputsAmount += utx.value;
@ -447,11 +485,10 @@ abstract class ElectrumWalletBase
bool? isSilentPayment = false;
if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord;
privkey = walletAddresses.silentAddress!.b_spend.tweakAdd(
BigintUtils.fromBytes(
BytesUtils.fromHexString(
(utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord).silentPaymentTweak!,
),
BytesUtils.fromHexString(unspentAddress.silentPaymentTweak!),
),
);
spendsSilentPayment = true;
@ -464,7 +501,11 @@ abstract class ElectrumWalletBase
);
}
inputPrivKeyInfos.add(ECPrivateInfo(privkey, address.type == SegwitAddresType.p2tr));
inputPrivKeyInfos.add(ECPrivateInfo(
privkey,
address.type == SegwitAddresType.p2tr,
tweak: !isSilentPayment,
));
inputPubKeys.add(privkey.getPublic());
vinOutpoints.add(Outpoint(txid: utx.hash, index: utx.vout));
@ -520,6 +561,10 @@ abstract class ElectrumWalletBase
// Here, when sending all, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount left for change
int amount = allInputsAmount - fee;
if (amount <= 0) {
throw BitcoinTransactionWrongBalanceException();
}
// Attempting to send less than the dust limit
if (_isBelowDust(amount)) {
throw BitcoinTransactionNoDustException();
@ -580,11 +625,10 @@ abstract class ElectrumWalletBase
bool? isSilentPayment = false;
if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord;
privkey = walletAddresses.silentAddress!.b_spend.tweakAdd(
BigintUtils.fromBytes(
BytesUtils.fromHexString(
(utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord).silentPaymentTweak!,
),
BytesUtils.fromHexString(unspentAddress.silentPaymentTweak!),
),
);
spendsSilentPayment = true;
@ -597,7 +641,11 @@ abstract class ElectrumWalletBase
);
}
inputPrivKeyInfos.add(ECPrivateInfo(privkey, address.type == SegwitAddresType.p2tr));
inputPrivKeyInfos.add(ECPrivateInfo(
privkey,
address.type == SegwitAddresType.p2tr,
tweak: !isSilentPayment,
));
inputPubKeys.add(privkey.getPublic());
vinOutpoints.add(Outpoint(txid: utx.hash, index: utx.vout));
@ -637,6 +685,16 @@ abstract class ElectrumWalletBase
int amountLeftForChangeAndFee = allInputsAmount - credentialsAmount;
if (amountLeftForChangeAndFee <= 0) {
if (!spendingAllCoins) {
return estimateTxForAmount(
credentialsAmount,
outputs,
feeRate,
inputsCount: utxos.length + 1,
memo: memo,
hasSilentPayment: hasSilentPayment,
);
}
throw BitcoinTransactionWrongBalanceException();
}
@ -684,6 +742,7 @@ abstract class ElectrumWalletBase
// Still has inputs to spend before failing
if (!spendingAllCoins) {
outputs.removeLast();
return estimateTxForAmount(
credentialsAmount,
outputs,
@ -730,6 +789,7 @@ abstract class ElectrumWalletBase
outputs.removeLast();
}
outputs.removeLast();
return estimateTxForAmount(
credentialsAmount,
outputs,
@ -1025,7 +1085,8 @@ abstract class ElectrumWalletBase
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
Future<void> updateUnspent() async {
@action
Future<void> updateAllUnspents() async {
List<BitcoinUnspent> updatedUnspentCoins = [];
if (hasSilentPaymentsScanning) {
@ -1037,20 +1098,9 @@ abstract class ElectrumWalletBase
});
}
final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet();
await Future.wait(walletAddresses.allAddresses.map((address) => electrumClient
.getListUnspentWithAddress(address.address, network)
.then((unspent) => Future.forEach<Map<String, dynamic>>(unspent, (unspent) async {
try {
final coin = BitcoinUnspent.fromJSON(address, unspent);
final tx = await fetchTransactionInfo(
hash: coin.hash, height: 0, myAddresses: addressesSet);
coin.isChange = tx?.direction == TransactionDirection.outgoing;
coin.confirmations = tx?.confirmations;
updatedUnspentCoins.add(coin);
} catch (_) {}
}))));
await Future.wait(walletAddresses.allAddresses.map((address) async {
updatedUnspentCoins.addAll(await fetchUnspent(address));
}));
unspentCoins = updatedUnspentCoins;
@ -1072,6 +1122,7 @@ abstract class ElectrumWalletBase
coin.isFrozen = coinInfo.isFrozen;
coin.isSending = coinInfo.isSending;
coin.note = coinInfo.note;
coin.bitcoinAddressRecord.balance += coinInfo.value;
} else {
_addCoinInfo(coin);
}
@ -1081,6 +1132,55 @@ abstract class ElectrumWalletBase
await _refreshUnspentCoinsInfo();
}
@action
Future<void> updateUnspents(BitcoinAddressRecord address) async {
final newUnspentCoins = await fetchUnspent(address);
if (newUnspentCoins.isNotEmpty) {
unspentCoins.addAll(newUnspentCoins);
newUnspentCoins.forEach((coin) {
final coinInfoList = unspentCoinsInfo.values.where(
(element) =>
element.walletId.contains(id) &&
element.hash.contains(coin.hash) &&
element.vout == coin.vout,
);
if (coinInfoList.isNotEmpty) {
final coinInfo = coinInfoList.first;
coin.isFrozen = coinInfo.isFrozen;
coin.isSending = coinInfo.isSending;
coin.note = coinInfo.note;
coin.bitcoinAddressRecord.balance += coinInfo.value;
} else {
_addCoinInfo(coin);
}
});
}
}
@action
Future<List<BitcoinUnspent>> fetchUnspent(BitcoinAddressRecord address) async {
final unspents = await electrumClient.getListUnspent(address.getScriptHash(network));
List<BitcoinUnspent> updatedUnspentCoins = [];
await Future.wait(unspents.map((unspent) async {
try {
final coin = BitcoinUnspent.fromJSON(address, unspent);
final tx = await fetchTransactionInfo(hash: coin.hash, height: 0);
coin.isChange = address.isHidden;
coin.confirmations = tx?.confirmations;
updatedUnspentCoins.add(coin);
} catch (_) {}
}));
return updatedUnspentCoins;
}
@action
Future<void> _addCoinInfo(BitcoinUnspent coin) async {
final newInfo = UnspentCoinsInfo(
@ -1093,6 +1193,7 @@ abstract class ElectrumWalletBase
value: coin.value,
vout: coin.vout,
isChange: coin.isChange,
isSilentPayment: coin is BitcoinSilentPaymentsUnspent,
);
await unspentCoinsInfo.add(newInfo);
@ -1309,8 +1410,8 @@ abstract class ElectrumWalletBase
time = status["block_time"] as int?;
final height = status["block_height"] as int? ?? 0;
confirmations =
height > 0 ? (await electrumClient.getCurrentBlockChainTip())! - height + 1 : 0;
final tip = await currentChainTip;
if (tip > 0) confirmations = height > 0 ? tip - height + 1 : 0;
} else {
final verboseTransaction = await electrumClient.getTransactionRaw(hash: hash);
@ -1335,18 +1436,15 @@ abstract class ElectrumWalletBase
}
Future<ElectrumTransactionInfo?> fetchTransactionInfo(
{required String hash,
required int height,
required Set<String> myAddresses,
bool? retryOnFailure}) async {
{required String hash, required int height, bool? retryOnFailure}) async {
try {
return ElectrumTransactionInfo.fromElectrumBundle(
await getTransactionExpanded(hash: hash), walletInfo.type, network,
addresses: myAddresses, height: height);
addresses: addressesSet, height: height);
} catch (e) {
if (e is FormatException && retryOnFailure == true) {
await Future.delayed(const Duration(seconds: 2));
return fetchTransactionInfo(hash: hash, height: height, myAddresses: myAddresses);
return fetchTransactionInfo(hash: hash, height: height);
}
return null;
}
@ -1356,11 +1454,15 @@ abstract class ElectrumWalletBase
Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
try {
final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet();
currentChainTip ??= await electrumClient.getCurrentBlockChainTip() ?? 0;
await Future.wait(ADDRESS_TYPES.map(
(type) => fetchTransactionsForAddressType(addressesSet, historiesWithDetails, type)));
if (type == WalletType.bitcoin) {
await Future.wait(ADDRESS_TYPES
.map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
} else if (type == WalletType.bitcoinCash) {
await fetchTransactionsForAddressType(historiesWithDetails, P2pkhAddressType.p2pkh);
} else if (type == WalletType.litecoin) {
await fetchTransactionsForAddressType(historiesWithDetails, SegwitAddresType.p2wpkh);
}
return historiesWithDetails;
} catch (e) {
@ -1370,7 +1472,6 @@ abstract class ElectrumWalletBase
}
Future<void> fetchTransactionsForAddressType(
Set<String> addressesSet,
Map<String, ElectrumTransactionInfo> historiesWithDetails,
BitcoinAddressType type,
) async {
@ -1379,39 +1480,50 @@ abstract class ElectrumWalletBase
final receiveAddresses = addressesByType.where((addr) => addr.isHidden == false);
await Future.wait(addressesByType.map((addressRecord) async {
final history = await _fetchAddressHistory(addressRecord, addressesSet, currentChainTip!);
final history = await _fetchAddressHistory(addressRecord, await currentChainTip);
if (history.isNotEmpty) {
addressRecord.txCount = history.length;
historiesWithDetails.addAll(history);
final matchedAddresses = addressRecord.isHidden ? hiddenAddresses : receiveAddresses;
final isLastUsedAddress = history.isNotEmpty && matchedAddresses.last == addressRecord;
final isUsedAddressUnderGap = matchedAddresses.toList().indexOf(addressRecord) >=
matchedAddresses.length -
(addressRecord.isHidden
? ElectrumWalletAddressesBase.defaultChangeAddressesCount
: ElectrumWalletAddressesBase.defaultReceiveAddressesCount);
if (isLastUsedAddress) {
// The last address by gap limit is used, discover new addresses for the same address type
if (isUsedAddressUnderGap) {
final prevLength = walletAddresses.allAddresses.length;
// Discover new addresses for the same address type until the gap limit is respected
await walletAddresses.discoverAddresses(
matchedAddresses.toList(),
addressRecord.isHidden,
(address, addressesSet) => _fetchAddressHistory(address, addressesSet, currentChainTip!)
.then((history) => history.isNotEmpty ? address.address : null),
(address) async {
await _subscribeForUpdates();
return _fetchAddressHistory(address, await currentChainTip)
.then((history) => history.isNotEmpty ? address.address : null);
},
type: type,
);
// Continue until the last address by this address type is not used yet
await fetchTransactionsForAddressType(addressesSet, historiesWithDetails, type);
final newLength = walletAddresses.allAddresses.length;
if (newLength > prevLength) {
await fetchTransactionsForAddressType(historiesWithDetails, type);
}
}
}
}));
}
Future<Map<String, ElectrumTransactionInfo>> _fetchAddressHistory(
BitcoinAddressRecord addressRecord, Set<String> addressesSet, int currentHeight) async {
BitcoinAddressRecord addressRecord, int? currentHeight) async {
try {
final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
final history = await electrumClient
.getHistory(addressRecord.scriptHash ?? addressRecord.updateScriptHash(network));
final history = await electrumClient.getHistory(addressRecord.getScriptHash(network));
if (history.isNotEmpty) {
addressRecord.setAsUsed();
@ -1425,14 +1537,13 @@ abstract class ElectrumWalletBase
if (height > 0) {
storedTx.height = height;
// the tx's block itself is the first confirmation so add 1
storedTx.confirmations = currentHeight - height + 1;
if (currentHeight != null) storedTx.confirmations = currentHeight - height + 1;
storedTx.isPending = storedTx.confirmations == 0;
}
historiesWithDetails[txid] = storedTx;
} else {
final tx = await fetchTransactionInfo(
hash: txid, height: height, myAddresses: addressesSet, retryOnFailure: true);
final tx = await fetchTransactionInfo(hash: txid, height: height, retryOnFailure: true);
if (tx != null) {
historiesWithDetails[txid] = tx;
@ -1462,9 +1573,9 @@ abstract class ElectrumWalletBase
return;
}
transactionHistory.transactions.values.forEach((tx) {
if (tx.unspents != null && currentChainTip != null) {
tx.confirmations = currentChainTip! - tx.height + 1;
transactionHistory.transactions.values.forEach((tx) async {
if (tx.unspents != null && tx.unspents!.isNotEmpty && tx.height > 0) {
tx.confirmations = await currentChainTip - tx.height + 1;
}
});
@ -1479,33 +1590,24 @@ abstract class ElectrumWalletBase
}
}
void _subscribeForUpdates() async {
scriptHashes.forEach((sh) async {
Future<void> _subscribeForUpdates() async {
final unsubscribedScriptHashes = walletAddresses.allAddresses.where(
(address) => !_scripthashesUpdateSubject.containsKey(address.getScriptHash(network)),
);
await Future.wait(unsubscribedScriptHashes.map((address) async {
final sh = address.getScriptHash(network);
await _scripthashesUpdateSubject[sh]?.close();
_scripthashesUpdateSubject[sh] = electrumClient.scripthashUpdate(sh);
_scripthashesUpdateSubject[sh] = await electrumClient.scripthashUpdate(sh);
_scripthashesUpdateSubject[sh]?.listen((event) async {
try {
await updateUnspent();
await updateBalance();
await updateTransactions();
} catch (e, s) {
print(e.toString());
_onError?.call(FlutterErrorDetails(
exception: e,
stack: s,
library: this.runtimeType.toString(),
));
}
});
});
await updateUnspents(address);
await _chainTipUpdateSubject?.close();
_chainTipUpdateSubject = electrumClient.chainTipUpdate();
_chainTipUpdateSubject?.listen((_) async {
try {
final currentHeight = await electrumClient.getCurrentBlockChainTip();
if (currentHeight != null) walletInfo.restoreHeight = currentHeight;
_setListeners(walletInfo.restoreHeight, chainTip: currentHeight);
final newBalance = await _fetchBalance(sh);
balance[currency]?.confirmed += newBalance.confirmed;
balance[currency]?.unconfirmed += newBalance.unconfirmed;
await _fetchAddressHistory(address, await currentChainTip);
} catch (e, s) {
print(e.toString());
_onError?.call(FlutterErrorDetails(
@ -1515,6 +1617,7 @@ abstract class ElectrumWalletBase
));
}
});
}));
}
Future<ElectrumBalance> _fetchBalances() async {
@ -1534,17 +1637,15 @@ abstract class ElectrumWalletBase
if (hasSilentPaymentsScanning) {
// Add values from unspent coins that are not fetched by the address list
// i.e. scanned silent payments
unspentCoinsInfo.values.forEach((info) {
unspentCoins.forEach((element) {
if (element.hash == info.hash &&
element.bitcoinAddressRecord.address == info.address &&
element.value == info.value) {
if (info.isFrozen) totalFrozen += element.value;
if (element.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
totalConfirmed += element.value;
}
transactionHistory.transactions.values.forEach((tx) {
if (tx.unspents != null) {
tx.unspents!.forEach((unspent) {
if (unspent.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
if (unspent.isFrozen) totalFrozen += unspent.value;
totalConfirmed += unspent.value;
}
});
}
});
}
@ -1567,6 +1668,13 @@ abstract class ElectrumWalletBase
confirmed: totalConfirmed, unconfirmed: totalUnconfirmed, frozen: totalFrozen);
}
Future<ElectrumBalance> _fetchBalance(String sh) async {
final balance = await electrumClient.getBalance(sh);
final confirmed = balance['confirmed'] as int? ?? 0;
final unconfirmed = balance['unconfirmed'] as int? ?? 0;
return ElectrumBalance(confirmed: confirmed, unconfirmed: unconfirmed, frozen: 0);
}
Future<void> updateBalance() async {
balance[currency] = await _fetchBalances();
await save();
@ -1597,10 +1705,19 @@ abstract class ElectrumWalletBase
}
Future<void> _setInitialHeight() async {
currentChainTip = await electrumClient.getCurrentBlockChainTip();
if (currentChainTip != null && walletInfo.restoreHeight == 0) {
walletInfo.restoreHeight = currentChainTip!;
if (_chainTipUpdateSubject != null) return;
_chainTipUpdateSubject = await electrumClient.chainTipSubscribe();
_chainTipUpdateSubject?.listen((e) async {
final event = e as Map<String, dynamic>;
final height = int.parse(event['height'].toString());
_currentChainTip = height;
if (_currentChainTip != null && _currentChainTip! > 0 && walletInfo.restoreHeight == 0) {
walletInfo.restoreHeight = _currentChainTip!;
}
});
}
static BasedUtxoNetwork _getNetwork(bitcoin.NetworkType networkType, CryptoCurrency? currency) {
@ -1679,8 +1796,6 @@ class SyncResponse {
}
Future<void> startRefresh(ScanData scanData) async {
var cachedBlockchainHeight = scanData.chainTip;
Future<ElectrumClient> getElectrumConnection() async {
final electrumClient = scanData.electrumClient;
if (!electrumClient.isConnected) {
@ -1690,11 +1805,6 @@ Future<void> startRefresh(ScanData scanData) async {
return electrumClient;
}
Future<int> getUpdatedNodeHeight() async {
final electrumClient = await getElectrumConnection();
return await electrumClient.getCurrentBlockChainTip() ?? cachedBlockchainHeight;
}
var lastKnownBlockHeight = 0;
var initialSyncHeight = 0;
@ -1714,60 +1824,54 @@ Future<void> startRefresh(ScanData scanData) async {
return;
}
// Run this until no more blocks left to scan txs. At first this was recursive
// i.e. re-calling the startRefresh function but this was easier for the above values to retain
// their initial values
while (true) {
BehaviorSubject<Object>? tweaksSubscription = null;
lastKnownBlockHeight = syncHeight;
SyncingSyncStatus syncingStatus;
if (scanData.isSingleScan) {
syncingStatus = SyncingSyncStatus(1, 0);
} else {
syncingStatus = SyncingSyncStatus.fromHeightValues(
await getUpdatedNodeHeight(), initialSyncHeight, syncHeight);
syncingStatus =
SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight);
}
scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus));
if (syncingStatus.blocksLeft <= 0 || (scanData.isSingleScan && scanData.height != syncHeight)) {
scanData.sendPort.send(SyncResponse(await getUpdatedNodeHeight(), SyncedSyncStatus()));
scanData.sendPort.send(SyncResponse(scanData.chainTip, SyncedSyncStatus()));
return;
}
try {
final electrumClient = await getElectrumConnection();
// TODO: hardcoded values, if timed out decrease amount of blocks per request?
final scanningBlockCount =
scanData.isSingleScan ? 1 : (scanData.network == BitcoinNetwork.testnet ? 25 : 10);
Map<String, dynamic>? tweaks;
if (tweaksSubscription == null) {
try {
tweaks = await electrumClient.getTweaks(height: syncHeight, count: scanningBlockCount);
} catch (e) {
if (e is RequestFailedTimeoutException) {
return scanData.sendPort.send(
SyncResponse(syncHeight, TimedOutSyncStatus()),
);
}
}
tweaksSubscription = await electrumClient.tweaksSubscribe(height: syncHeight);
if (tweaks == null) {
return scanData.sendPort.send(
SyncResponse(syncHeight, UnsupportedSyncStatus()),
);
}
tweaksSubscription?.listen((t) {
final tweaks = t as Map<String, dynamic>;
if (tweaks.isEmpty) {
syncHeight += scanningBlockCount;
continue;
syncHeight += 1;
scanData.sendPort.send(
SyncResponse(
syncHeight,
SyncingSyncStatus.fromHeightValues(
currentChainTip,
initialSyncHeight,
syncHeight,
),
),
);
return;
}
final blockHeights = tweaks.keys;
for (var i = 0; i < blockHeights.length; i++) {
final blockHeight = tweaks.keys.first.toString();
try {
final blockHeight = blockHeights.elementAt(i).toString();
final blockTweaks = tweaks[blockHeight] as Map<String, dynamic>;
for (var j = 0; j < blockTweaks.keys.length; j++) {
@ -1777,6 +1881,7 @@ Future<void> startRefresh(ScanData scanData) async {
final tweak = details["tweak"].toString();
try {
// scanOutputs called from rust here
final addToWallet = scanOutputs(
outputPubkeys.values.map((o) => o[0].toString()).toList(),
tweak,
@ -1794,8 +1899,22 @@ Future<void> startRefresh(ScanData scanData) async {
continue;
}
addToWallet.forEach((label, value) async {
(value as Map<String, dynamic>).forEach((output, tweak) async {
// placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s)
final txInfo = ElectrumTransactionInfo(
WalletType.bitcoin,
id: txid,
height: syncHeight,
amount: 0,
fee: 0,
direction: TransactionDirection.incoming,
isPending: false,
date: DateTime.now(),
confirmations: scanData.chainTip - int.parse(blockHeight) + 1,
unspents: [],
);
addToWallet.forEach((label, value) {
(value as Map<String, dynamic>).forEach((output, tweak) {
final t_k = tweak.toString();
final receivingOutputAddress = ECPublic.fromHex(output)
@ -1816,8 +1935,8 @@ Future<void> startRefresh(ScanData scanData) async {
int? amount;
int? pos;
outputPubkeys.entries.firstWhere((k) {
final matches = k.value[0] == output;
if (matches) {
final isMatchingOutput = k.value[0] == output;
if (isMatchingOutput) {
amount = int.parse(k.value[1].toString());
pos = int.parse(k.key.toString());
return true;
@ -1825,61 +1944,62 @@ Future<void> startRefresh(ScanData scanData) async {
return false;
});
final json = <String, dynamic>{
'address_record': receivedAddressRecord.toJSON(),
'tx_hash': txid,
'value': amount!,
'tx_pos': pos!,
'silent_payment_tweak': t_k,
};
final tx = BitcoinUnspent.fromJSON(receivedAddressRecord, json);
final silentPaymentAddress = SilentPaymentAddress(
version: scanData.silentAddress.version,
B_scan: scanData.silentAddress.B_scan,
B_spend: label == "None"
? scanData.silentAddress.B_spend
: scanData.silentAddress.B_spend
.tweakAdd(BigintUtils.fromBytes(BytesUtils.fromHexString(label))),
hrp: scanData.silentAddress.hrp,
final unspent = BitcoinSilentPaymentsUnspent(
receivedAddressRecord,
txid,
amount!,
pos!,
silentPaymentTweak: t_k,
silentPaymentLabel: label == "None" ? null : label,
);
final txInfo = ElectrumTransactionInfo(
WalletType.bitcoin,
id: tx.hash,
height: syncHeight,
amount: amount!,
fee: 0,
direction: TransactionDirection.incoming,
isPending: false,
date: DateTime.now(),
confirmations: await getUpdatedNodeHeight() - int.parse(blockHeight) + 1,
to: silentPaymentAddress.toString(),
unspents: [tx],
);
txInfo.unspents!.add(unspent);
txInfo.amount += unspent.value;
});
});
scanData.sendPort.send({txInfo.id: txInfo});
});
});
} catch (_) {}
}
} catch (e, s) {
print([e, s]);
} catch (_) {}
syncHeight += 1;
scanData.sendPort.send(
SyncResponse(
syncHeight,
SyncingSyncStatus.fromHeightValues(
currentChainTip,
initialSyncHeight,
syncHeight,
),
),
);
if (int.parse(blockHeight) >= scanData.chainTip) {
scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus()));
}
// Finished scanning block, add 1 to height and continue to next block in loop
syncHeight += 1;
scanData.sendPort.send(SyncResponse(syncHeight,
SyncingSyncStatus.fromHeightValues(currentChainTip, initialSyncHeight, syncHeight)));
return;
});
} catch (e) {
if (e is RequestFailedTimeoutException) {
return scanData.sendPort.send(
SyncResponse(syncHeight, TimedOutSyncStatus()),
);
}
}
}
if (tweaksSubscription == null) {
return scanData.sendPort.send(
SyncResponse(syncHeight, UnsupportedSyncStatus()),
);
}
} catch (e, stacktrace) {
print(stacktrace);
print(e.toString());
scanData.sendPort.send(SyncResponse(syncHeight, NotConnectedSyncStatus()));
break;
}
}
}

View file

@ -57,7 +57,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
b_spend: ECPrivate.fromHex(masterHd.derivePath(SPEND_PATH).privKey!),
hrp: network == BitcoinNetwork.testnet ? 'tsp' : 'sp');
if (silentAddresses.length == 0)
if (silentAddresses.length == 0) {
silentAddresses.add(BitcoinSilentPaymentAddressRecord(
silentAddress.toString(),
index: 0,
@ -67,6 +67,16 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
network: network,
type: SilentPaymentsAddresType.p2sp,
));
silentAddresses.add(BitcoinSilentPaymentAddressRecord(
silentAddress!.toLabeledSilentPaymentAddress(0).toString(),
index: 0,
isHidden: true,
name: "",
silentPaymentTweak: BytesUtils.toHexString(silentAddress!.generateLabel(0)),
network: network,
type: SilentPaymentsAddresType.p2sp,
));
}
}
updateAddressesByMatch();
@ -446,7 +456,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@action
Future<void> discoverAddresses(List<BitcoinAddressRecord> addressList, bool isHidden,
Future<String?> Function(BitcoinAddressRecord, Set<String>) getAddressHistory,
Future<String?> Function(BitcoinAddressRecord) getAddressHistory,
{BitcoinAddressType type = SegwitAddresType.p2wpkh}) async {
if (!isHidden) {
_validateSideHdAddresses(addressList.toList());
@ -456,8 +466,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
startIndex: addressList.length, isHidden: isHidden, type: type);
addAddresses(newAddresses);
final addressesWithHistory = await Future.wait(newAddresses
.map((addr) => getAddressHistory(addr, _addresses.map((e) => e.address).toSet())));
final addressesWithHistory = await Future.wait(newAddresses.map(getAddressHistory));
final isLastAddressUsed = addressesWithHistory.last == addressList.last.address;
if (isLastAddressUsed) {

View file

@ -2,7 +2,7 @@ import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/exceptions.dart';
class BitcoinTransactionWrongBalanceException extends TransactionWrongBalanceException {
BitcoinTransactionWrongBalanceException() : super(CryptoCurrency.btc);
BitcoinTransactionWrongBalanceException({super.amount}) : super(CryptoCurrency.btc);
}
class BitcoinTransactionNoInputsException extends TransactionNoInputsException {}
@ -25,3 +25,7 @@ class BitcoinTransactionCommitFailedDustOutputSendAll
extends TransactionCommitFailedDustOutputSendAll {}
class BitcoinTransactionCommitFailedVoutNegative extends TransactionCommitFailedVoutNegative {}
class BitcoinTransactionCommitFailedBIP68Final extends TransactionCommitFailedBIP68Final {}
class BitcoinTransactionSilentPaymentsNotSupported extends TransactionInputNotSupported {}

View file

@ -73,6 +73,10 @@ class PendingBitcoinTransaction with PendingTransaction {
if (error.contains("bad-txns-vout-negative")) {
throw BitcoinTransactionCommitFailedVoutNegative();
}
if (error.contains("non-BIP68-final")) {
throw BitcoinTransactionCommitFailedBIP68Final();
}
}
throw BitcoinTransactionCommitFailed();
}

View file

@ -1,9 +1,10 @@
import 'package:cw_core/crypto_currency.dart';
class TransactionWrongBalanceException implements Exception {
TransactionWrongBalanceException(this.currency);
TransactionWrongBalanceException(this.currency, {this.amount});
final CryptoCurrency currency;
final int? amount;
}
class TransactionNoInputsException implements Exception {}
@ -28,3 +29,7 @@ class TransactionCommitFailedDustOutput implements Exception {}
class TransactionCommitFailedDustOutputSendAll implements Exception {}
class TransactionCommitFailedVoutNegative implements Exception {}
class TransactionCommitFailedBIP68Final implements Exception {}
class TransactionInputNotSupported implements Exception {}

View file

@ -31,6 +31,11 @@ class SyncedSyncStatus extends SyncStatus {
double progress() => 1.0;
}
class SyncronizingSyncStatus extends SyncStatus {
@override
double progress() => 0.0;
}
class NotConnectedSyncStatus extends SyncStatus {
const NotConnectedSyncStatus();
@ -43,10 +48,7 @@ class AttemptingSyncStatus extends SyncStatus {
double progress() => 0.0;
}
class FailedSyncStatus extends SyncStatus {
@override
double progress() => 1.0;
}
class FailedSyncStatus extends NotConnectedSyncStatus {}
class ConnectingSyncStatus extends SyncStatus {
@override
@ -58,21 +60,14 @@ class ConnectedSyncStatus extends SyncStatus {
double progress() => 0.0;
}
class UnsupportedSyncStatus extends SyncStatus {
@override
double progress() => 1.0;
}
class UnsupportedSyncStatus extends NotConnectedSyncStatus {}
class TimedOutSyncStatus extends SyncStatus {
@override
double progress() => 1.0;
class TimedOutSyncStatus extends NotConnectedSyncStatus {
@override
String toString() => 'Timed out';
}
class LostConnectionSyncStatus extends SyncStatus {
@override
double progress() => 1.0;
class LostConnectionSyncStatus extends NotConnectedSyncStatus {
@override
String toString() => 'Reconnecting';
}

View file

@ -16,7 +16,8 @@ class UnspentCoinsInfo extends HiveObject {
required this.value,
this.keyImage = null,
this.isChange = false,
this.accountIndex = 0
this.accountIndex = 0,
this.isSilentPayment = false,
});
static const typeId = UNSPENT_COINS_INFO_TYPE_ID;
@ -56,6 +57,9 @@ class UnspentCoinsInfo extends HiveObject {
@HiveField(10, defaultValue: 0)
int accountIndex;
@HiveField(11, defaultValue: false)
bool? isSilentPayment;
String get note => noteRaw ?? '';
set note(String value) => noteRaw = value;

View file

@ -187,7 +187,7 @@ class CWBitcoin extends Bitcoin {
Future<void> updateUnspents(Object wallet) async {
final bitcoinWallet = wallet as ElectrumWallet;
await bitcoinWallet.updateUnspent();
await bitcoinWallet.updateAllUnspents();
}
WalletService createBitcoinWalletService(
@ -386,4 +386,10 @@ class CWBitcoin extends Bitcoin {
final bitcoinWallet = wallet as ElectrumWallet;
bitcoinWallet.walletAddresses.deleteSilentPaymentAddress(address);
}
@override
Future<void> updateFeeRates(Object wallet) async {
final bitcoinWallet = wallet as ElectrumWallet;
await bitcoinWallet.updateFeeRates();
}
}

View file

@ -44,5 +44,9 @@ String syncStatusTitle(SyncStatus syncStatus) {
return S.current.sync_status_timed_out;
}
if (syncStatus is SyncronizingSyncStatus) {
return S.current.sync_status_syncronizing;
}
return '';
}

View file

@ -818,14 +818,16 @@ Future<void> checkCurrentNodes(
}
if (currentBitcoinElectrumServer == null) {
final cakeWalletElectrum = Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin);
final cakeWalletElectrum =
Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin, useSSL: true);
await nodeSource.add(cakeWalletElectrum);
await sharedPreferences.setInt(
PreferencesKey.currentBitcoinElectrumSererIdKey, cakeWalletElectrum.key as int);
}
if (currentLitecoinElectrumServer == null) {
final cakeWalletElectrum = Node(uri: cakeWalletLitecoinElectrumUri, type: WalletType.litecoin);
final cakeWalletElectrum =
Node(uri: cakeWalletLitecoinElectrumUri, type: WalletType.litecoin, useSSL: true);
await nodeSource.add(cakeWalletElectrum);
await sharedPreferences.setInt(
PreferencesKey.currentLitecoinElectrumSererIdKey, cakeWalletElectrum.key as int);
@ -860,7 +862,8 @@ Future<void> checkCurrentNodes(
}
if (currentBitcoinCashNodeServer == null) {
final node = Node(uri: cakeWalletBitcoinCashDefaultNodeUri, type: WalletType.bitcoinCash);
final node =
Node(uri: cakeWalletBitcoinCashDefaultNodeUri, type: WalletType.bitcoinCash, useSSL: true);
await nodeSource.add(node);
await sharedPreferences.setInt(PreferencesKey.currentBitcoinCashNodeIdKey, node.key as int);
}
@ -888,7 +891,8 @@ Future<void> resetBitcoinElectrumServer(
.firstWhereOrNull((node) => node.uriRaw.toString() == cakeWalletBitcoinElectrumUri);
if (cakeWalletNode == null) {
cakeWalletNode = Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin);
cakeWalletNode =
Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin, useSSL: true);
await nodeSource.add(cakeWalletNode);
}

View file

@ -146,7 +146,7 @@ class AddressCell extends StatelessWidget {
mainAxisSize: MainAxisSize.max,
children: [
Text(
'${S.of(context).balance}: $txCount',
'${S.of(context).balance}: $balance',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
@ -400,6 +401,10 @@ class SendPage extends BasePage {
return;
}
if (sendViewModel.isElectrumWallet) {
bitcoin!.updateFeeRates(sendViewModel.wallet);
}
reaction((_) => sendViewModel.state, (ExecutionState state) {
if (state is FailureState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
@ -456,10 +461,12 @@ class SendPage extends BasePage {
sendViewModel.selectedCryptoCurrency.toString());
final waitMessage = sendViewModel.walletType == WalletType.solana
? '. ${S.of(_dialogContext).waitFewSecondForTxUpdate}' : '';
? '. ${S.of(_dialogContext).waitFewSecondForTxUpdate}'
: '';
final newContactMessage = newContactAddress != null
? '\n${S.of(_dialogContext).add_contact_to_address_book}' : '';
? '\n${S.of(_dialogContext).add_contact_to_address_book}'
: '';
final alertContent =
"$successMessage$waitMessage$newContactMessage";

View file

@ -57,6 +57,7 @@ class UnspentCoinsListFormState extends State<UnspentCoinsListForm> {
isSending: item.isSending,
isFrozen: item.isFrozen,
isChange: item.isChange,
isSilentPayment: item.isSilentPayment,
onCheckBoxTap: item.isFrozen
? null
: () async {

View file

@ -12,6 +12,7 @@ class UnspentCoinsListItem extends StatelessWidget {
required this.isSending,
required this.isFrozen,
required this.isChange,
required this.isSilentPayment,
this.onCheckBoxTap,
});
@ -21,18 +22,16 @@ class UnspentCoinsListItem extends StatelessWidget {
final bool isSending;
final bool isFrozen;
final bool isChange;
final bool isSilentPayment;
final Function()? onCheckBoxTap;
@override
Widget build(BuildContext context) {
final unselectedItemColor = Theme.of(context).cardColor;
final selectedItemColor = Theme.of(context).primaryColor;
final itemColor = isSending
? selectedItemColor
: unselectedItemColor;
final amountColor = isSending
? Colors.white
: Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor;
final itemColor = isSending ? selectedItemColor : unselectedItemColor;
final amountColor =
isSending ? Colors.white : Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor;
final addressColor = isSending
? Colors.white.withOpacity(0.5)
: Theme.of(context).extension<CakeTextTheme>()!.buttonSecondaryTextColor;
@ -121,6 +120,23 @@ class UnspentCoinsListItem extends StatelessWidget {
),
),
),
if (isSilentPayment)
Container(
height: 17,
padding: EdgeInsets.only(left: 6, right: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(8.5)),
color: Colors.white),
alignment: Alignment.center,
child: Text(
S.of(context).silent_payments,
style: TextStyle(
color: itemColor,
fontSize: 7,
fontWeight: FontWeight.w600,
),
),
),
],
),
),

View file

@ -567,9 +567,15 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
if (error is TransactionCommitFailedVoutNegative) {
return S.current.tx_rejected_vout_negative;
}
if (error is TransactionCommitFailedBIP68Final) {
return S.current.tx_rejected_bip68_final;
}
if (error is TransactionNoDustOnChangeException) {
return S.current.tx_commit_exception_no_dust_on_change(error.min, error.max);
}
if (error is TransactionInputNotSupported) {
return S.current.tx_invalid_input;
}
}
return errorMessage;

View file

@ -15,7 +15,8 @@ abstract class UnspentCoinsItemBase with Store {
required this.isChange,
required this.amountRaw,
required this.vout,
required this.keyImage
required this.keyImage,
required this.isSilentPayment,
});
@observable
@ -47,4 +48,7 @@ abstract class UnspentCoinsItemBase with Store {
@observable
String? keyImage;
@observable
bool isSilentPayment;
}

View file

@ -86,11 +86,14 @@ abstract class UnspentCoinsListViewModelBase with Store {
@action
void _updateUnspentCoinsInfo() {
_items.clear();
_items.addAll(_getUnspents().map((elem) {
List<UnspentCoinsItem> unspents = [];
_getUnspents().forEach((elem) {
try {
final info =
getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout, elem.keyImage);
return UnspentCoinsItem(
unspents.add(UnspentCoinsItem(
address: elem.address,
amount: '${formatAmountToString(elem.value)} ${wallet.currency.title}',
hash: elem.hash,
@ -101,7 +104,14 @@ abstract class UnspentCoinsListViewModelBase with Store {
vout: elem.vout,
keyImage: elem.keyImage,
isChange: elem.isChange,
);
}));
isSilentPayment: info.isSilentPayment ?? false,
));
} catch (e, s) {
print(s);
print(e.toString());
}
});
_items.addAll(unspents);
}
}

View file

@ -316,8 +316,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
if (isElectrumWallet) {
if (bitcoin!.hasSelectedSilentPayments(wallet)) {
final addressItems = bitcoin!.getSilentPaymentAddresses(wallet).map((address) {
// Silent Payments index 0 is change per BIP
final isPrimary = address.index == 1;
final isPrimary = address.index == 0;
return WalletAddressListItem(
id: address.index,
@ -335,11 +334,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
final receivedAddressItems =
bitcoin!.getSilentPaymentReceivedAddresses(wallet).map((address) {
final isPrimary = address.index == 0;
return WalletAddressListItem(
id: address.index,
isPrimary: isPrimary,
isPrimary: false,
name: address.name,
address: address.address,
txCount: address.txCount,

View file

@ -745,8 +745,10 @@
"trusted": "موثوق به",
"tx_commit_exception_no_dust_on_change": "يتم رفض المعاملة مع هذا المبلغ. باستخدام هذه العملات المعدنية ، يمكنك إرسال ${min} دون تغيير أو ${max} الذي يعيد التغيير.",
"tx_commit_failed": "فشل ارتكاب المعاملة. يرجى الاتصال بالدعم.",
"tx_invalid_input": "أنت تستخدم نوع الإدخال الخاطئ لهذا النوع من الدفع",
"tx_no_dust_exception": "يتم رفض المعاملة عن طريق إرسال مبلغ صغير جدًا. يرجى محاولة زيادة المبلغ.",
"tx_not_enough_inputs_exception": "لا يكفي المدخلات المتاحة. الرجاء تحديد المزيد تحت التحكم في العملة",
"tx_rejected_bip68_final": "تحتوي المعاملة على مدخلات غير مؤكدة وفشلت في استبدال الرسوم.",
"tx_rejected_dust_change": "المعاملة التي يتم رفضها بموجب قواعد الشبكة ، ومبلغ التغيير المنخفض (الغبار). حاول إرسال كل أو تقليل المبلغ.",
"tx_rejected_dust_output": "المعاملة التي يتم رفضها بموجب قواعد الشبكة ، وكمية الإخراج المنخفض (الغبار). يرجى زيادة المبلغ.",
"tx_rejected_dust_output_send_all": "المعاملة التي يتم رفضها بموجب قواعد الشبكة ، وكمية الإخراج المنخفض (الغبار). يرجى التحقق من رصيد العملات المعدنية المحددة تحت التحكم في العملة.",

View file

@ -745,8 +745,10 @@
"trusted": "Надежден",
"tx_commit_exception_no_dust_on_change": "Сделката се отхвърля с тази сума. С тези монети можете да изпратите ${min} без промяна или ${max}, която връща промяна.",
"tx_commit_failed": "Компетацията на транзакцията не успя. Моля, свържете се с поддръжката.",
"tx_invalid_input": "Използвате грешен тип вход за този тип плащане",
"tx_no_dust_exception": "Сделката се отхвърля чрез изпращане на сума твърде малка. Моля, опитайте да увеличите сумата.",
"tx_not_enough_inputs_exception": "Няма достатъчно налични входове. Моля, изберете повече под контрол на монети",
"tx_rejected_bip68_final": "Сделката има непотвърдени входове и не успя да се замени с такса.",
"tx_rejected_dust_change": "Транзакция, отхвърлена от мрежови правила, ниска сума на промяна (прах). Опитайте да изпратите всички или да намалите сумата.",
"tx_rejected_dust_output": "Транзакция, отхвърлена от мрежови правила, ниска стойност на изхода (прах). Моля, увеличете сумата.",
"tx_rejected_dust_output_send_all": "Транзакция, отхвърлена от мрежови правила, ниска стойност на изхода (прах). Моля, проверете баланса на монетите, избрани под контрол на монети.",

View file

@ -745,8 +745,10 @@
"trusted": "Důvěřovat",
"tx_commit_exception_no_dust_on_change": "Transakce je zamítnuta s touto částkou. S těmito mincemi můžete odeslat ${min} bez změny nebo ${max}, které se vrátí změna.",
"tx_commit_failed": "Transakce COMPORT selhala. Kontaktujte prosím podporu.",
"tx_invalid_input": "Pro tento typ platby používáte nesprávný typ vstupu",
"tx_no_dust_exception": "Transakce je zamítnuta odesláním příliš malé. Zkuste prosím zvýšit částku.",
"tx_not_enough_inputs_exception": "Není k dispozici dostatek vstupů. Vyberte prosím více pod kontrolou mincí",
"tx_rejected_bip68_final": "Transakce má nepotvrzené vstupy a nepodařilo se nahradit poplatkem.",
"tx_rejected_dust_change": "Transakce zamítnuta podle síťových pravidel, množství nízké změny (prach). Zkuste odeslat vše nebo snížit částku.",
"tx_rejected_dust_output": "Transakce zamítnuta síťovými pravidly, nízkým množstvím výstupu (prach). Zvyšte prosím částku.",
"tx_rejected_dust_output_send_all": "Transakce zamítnuta síťovými pravidly, nízkým množstvím výstupu (prach). Zkontrolujte prosím zůstatek mincí vybraných pod kontrolou mincí.",

View file

@ -746,8 +746,10 @@
"trusted": "Vertrauenswürdige",
"tx_commit_exception_no_dust_on_change": "Die Transaktion wird diesen Betrag abgelehnt. Mit diesen Münzen können Sie ${min} ohne Veränderung oder ${max} senden, die Änderungen zurückgeben.",
"tx_commit_failed": "Transaktionsausschüsse ist fehlgeschlagen. Bitte wenden Sie sich an Support.",
"tx_invalid_input": "Sie verwenden den falschen Eingangstyp für diese Art von Zahlung",
"tx_no_dust_exception": "Die Transaktion wird abgelehnt, indem eine Menge zu klein gesendet wird. Bitte versuchen Sie, die Menge zu erhöhen.",
"tx_not_enough_inputs_exception": "Nicht genügend Eingänge verfügbar. Bitte wählen Sie mehr unter Münzkontrolle aus",
"tx_rejected_bip68_final": "Die Transaktion hat unbestätigte Inputs und konnte nicht durch Gebühr ersetzt werden.",
"tx_rejected_dust_change": "Transaktion abgelehnt durch Netzwerkregeln, niedriger Änderungsbetrag (Staub). Versuchen Sie, alle zu senden oder die Menge zu reduzieren.",
"tx_rejected_dust_output": "Transaktion durch Netzwerkregeln, niedriger Ausgangsmenge (Staub) abgelehnt. Bitte erhöhen Sie den Betrag.",
"tx_rejected_dust_output_send_all": "Transaktion durch Netzwerkregeln, niedriger Ausgangsmenge (Staub) abgelehnt. Bitte überprüfen Sie den Gleichgewicht der unter Münzkontrolle ausgewählten Münzen.",

View file

@ -745,8 +745,10 @@
"trusted": "Trusted",
"tx_commit_exception_no_dust_on_change": "The transaction is rejected with this amount. With these coins you can send ${min} without change or ${max} that returns change.",
"tx_commit_failed": "Transaction commit failed. Please contact support.",
"tx_invalid_input": "You are using the wrong input type for this type of payment",
"tx_no_dust_exception": "The transaction is rejected by sending an amount too small. Please try increasing the amount.",
"tx_not_enough_inputs_exception": "Not enough inputs available. Please select more under Coin Control",
"tx_rejected_bip68_final": "Transaction has unconfirmed inputs and failed to replace by fee.",
"tx_rejected_dust_change": "Transaction rejected by network rules, low change amount (dust). Try sending ALL or reducing the amount.",
"tx_rejected_dust_output": "Transaction rejected by network rules, low output amount (dust). Please increase the amount.",
"tx_rejected_dust_output_send_all": "Transaction rejected by network rules, low output amount (dust). Please check the balance of coins selected under Coin Control.",

View file

@ -746,8 +746,10 @@
"trusted": "de confianza",
"tx_commit_exception_no_dust_on_change": "La transacción se rechaza con esta cantidad. Con estas monedas puede enviar ${min} sin cambios o ${max} que devuelve el cambio.",
"tx_commit_failed": "La confirmación de transacción falló. Póngase en contacto con el soporte.",
"tx_invalid_input": "Está utilizando el tipo de entrada incorrecto para este tipo de pago",
"tx_no_dust_exception": "La transacción se rechaza enviando una cantidad demasiado pequeña. Intente aumentar la cantidad.",
"tx_not_enough_inputs_exception": "No hay suficientes entradas disponibles. Seleccione más bajo control de monedas",
"tx_rejected_bip68_final": "La transacción tiene entradas no confirmadas y no ha podido reemplazar por tarifa.",
"tx_rejected_dust_change": "Transacción rechazada por reglas de red, bajo cambio de cambio (polvo). Intente enviar todo o reducir la cantidad.",
"tx_rejected_dust_output": "Transacción rechazada por reglas de red, baja cantidad de salida (polvo). Aumente la cantidad.",
"tx_rejected_dust_output_send_all": "Transacción rechazada por reglas de red, baja cantidad de salida (polvo). Verifique el saldo de monedas seleccionadas bajo control de monedas.",

View file

@ -745,8 +745,10 @@
"trusted": "de confiance",
"tx_commit_exception_no_dust_on_change": "La transaction est rejetée avec ce montant. Avec ces pièces, vous pouvez envoyer ${min} sans changement ou ${max} qui renvoie le changement.",
"tx_commit_failed": "La validation de la transaction a échoué. Veuillez contacter l'assistance.",
"tx_invalid_input": "Vous utilisez le mauvais type d'entrée pour ce type de paiement",
"tx_no_dust_exception": "La transaction est rejetée en envoyant un montant trop faible. Veuillez essayer d'augmenter le montant.",
"tx_not_enough_inputs_exception": "Pas assez d'entrées disponibles. Veuillez sélectionner plus sous Control Control",
"tx_rejected_bip68_final": "La transaction a des entrées non confirmées et n'a pas réussi à remplacer par les frais.",
"tx_rejected_dust_change": "Transaction rejetée par les règles du réseau, montant de faible variation (poussière). Essayez d'envoyer tout ou de réduire le montant.",
"tx_rejected_dust_output": "Transaction rejetée par les règles du réseau, faible quantité de sortie (poussière). Veuillez augmenter le montant.",
"tx_rejected_dust_output_send_all": "Transaction rejetée par les règles du réseau, faible quantité de sortie (poussière). Veuillez vérifier le solde des pièces sélectionnées sous le contrôle des pièces de monnaie.",

View file

@ -747,8 +747,10 @@
"trusted": "Amintacce",
"tx_commit_exception_no_dust_on_change": "An ƙi ma'amala da wannan adadin. Tare da waɗannan tsabar kudi Zaka iya aika ${min}, ba tare da canji ba ko ${max} wanda ya dawo canzawa.",
"tx_commit_failed": "Ma'amala ya kasa. Da fatan za a tuntuɓi goyan baya.",
"tx_invalid_input": "Kuna amfani da nau'in shigar da ba daidai ba don wannan nau'in biyan kuɗi",
"tx_no_dust_exception": "An ƙi ma'amala ta hanyar aika adadin ƙarami. Da fatan za a gwada ƙara adadin.",
"tx_not_enough_inputs_exception": "Bai isa ba hanyoyin da ake samu. Da fatan za selectiari a karkashin Kwarewar Coin",
"tx_rejected_bip68_final": "Ma'amala tana da abubuwan da basu dace ba kuma sun kasa maye gurbin ta.",
"tx_rejected_dust_change": "Ma'amala ta ƙi ta dokokin cibiyar sadarwa, ƙarancin canji (ƙura). Gwada aikawa da duka ko rage adadin.",
"tx_rejected_dust_output": "Ma'adar da aka ƙi ta dokokin cibiyar sadarwa, ƙananan fitarwa (ƙura). Da fatan za a ƙara adadin.",
"tx_rejected_dust_output_send_all": "Ma'adar da aka ƙi ta dokokin cibiyar sadarwa, ƙananan fitarwa (ƙura). Da fatan za a duba daidaiton tsabar kudi a ƙarƙashin ikon tsabar kudin.",

View file

@ -747,8 +747,10 @@
"trusted": "भरोसा",
"tx_commit_exception_no_dust_on_change": "लेनदेन को इस राशि से खारिज कर दिया जाता है। इन सिक्कों के साथ आप चेंज या ${min} के बिना ${max} को भेज सकते हैं जो परिवर्तन लौटाता है।",
"tx_commit_failed": "लेन -देन प्रतिबद्ध विफल। कृपया संपर्क समर्थन करें।",
"tx_invalid_input": "आप इस प्रकार के भुगतान के लिए गलत इनपुट प्रकार का उपयोग कर रहे हैं",
"tx_no_dust_exception": "लेनदेन को बहुत छोटी राशि भेजकर अस्वीकार कर दिया जाता है। कृपया राशि बढ़ाने का प्रयास करें।",
"tx_not_enough_inputs_exception": "पर्याप्त इनपुट उपलब्ध नहीं है। कृपया सिक्का नियंत्रण के तहत अधिक चुनें",
"tx_rejected_bip68_final": "लेन -देन में अपुष्ट इनपुट हैं और शुल्क द्वारा प्रतिस्थापित करने में विफल रहे हैं।",
"tx_rejected_dust_change": "नेटवर्क नियमों, कम परिवर्तन राशि (धूल) द्वारा खारिज किए गए लेनदेन। सभी भेजने या राशि को कम करने का प्रयास करें।",
"tx_rejected_dust_output": "नेटवर्क नियमों, कम आउटपुट राशि (धूल) द्वारा खारिज किए गए लेनदेन। कृपया राशि बढ़ाएं।",
"tx_rejected_dust_output_send_all": "नेटवर्क नियमों, कम आउटपुट राशि (धूल) द्वारा खारिज किए गए लेनदेन। कृपया सिक्का नियंत्रण के तहत चुने गए सिक्कों के संतुलन की जाँच करें।",

View file

@ -745,8 +745,10 @@
"trusted": "vjerovao",
"tx_commit_exception_no_dust_on_change": "Transakcija se odbija s tim iznosom. Pomoću ovih kovanica možete poslati ${min} bez promjene ili ${max} koja vraća promjenu.",
"tx_commit_failed": "Obveza transakcije nije uspjela. Molimo kontaktirajte podršku.",
"tx_invalid_input": "Koristite pogrešnu vrstu ulaza za ovu vrstu plaćanja",
"tx_no_dust_exception": "Transakcija se odbija slanjem iznosa premalo. Pokušajte povećati iznos.",
"tx_not_enough_inputs_exception": "Nema dovoljno unosa. Molimo odaberite više pod kontrolom novčića",
"tx_rejected_bip68_final": "Transakcija ima nepotvrđene unose i nije zamijenila naknadom.",
"tx_rejected_dust_change": "Transakcija odbijena mrežnim pravilima, niska količina promjene (prašina). Pokušajte poslati sve ili smanjiti iznos.",
"tx_rejected_dust_output": "Transakcija odbijena mrežnim pravilima, niska količina izlaza (prašina). Molimo povećajte iznos.",
"tx_rejected_dust_output_send_all": "Transakcija odbijena mrežnim pravilima, niska količina izlaza (prašina). Molimo provjerite ravnotežu kovanica odabranih pod kontrolom novčića.",

View file

@ -748,8 +748,10 @@
"trusted": "Dipercayai",
"tx_commit_exception_no_dust_on_change": "Transaksi ditolak dengan jumlah ini. Dengan koin ini Anda dapat mengirim ${min} tanpa perubahan atau ${max} yang mengembalikan perubahan.",
"tx_commit_failed": "Transaksi Gagal. Silakan hubungi Dukungan.",
"tx_invalid_input": "Anda menggunakan jenis input yang salah untuk jenis pembayaran ini",
"tx_no_dust_exception": "Transaksi ditolak dengan mengirimkan jumlah yang terlalu kecil. Silakan coba tingkatkan jumlahnya.",
"tx_not_enough_inputs_exception": "Tidak cukup input yang tersedia. Pilih lebih banyak lagi di bawah Kontrol Koin",
"tx_rejected_bip68_final": "Transaksi memiliki input yang belum dikonfirmasi dan gagal mengganti dengan biaya.",
"tx_rejected_dust_change": "Transaksi ditolak oleh aturan jaringan, jumlah perubahan rendah (debu). Coba kirim semua atau mengurangi jumlahnya.",
"tx_rejected_dust_output": "Transaksi ditolak oleh aturan jaringan, jumlah output rendah (debu). Harap tingkatkan jumlahnya.",
"tx_rejected_dust_output_send_all": "Transaksi ditolak oleh aturan jaringan, jumlah output rendah (debu). Silakan periksa saldo koin yang dipilih di bawah kontrol koin.",

View file

@ -747,8 +747,10 @@
"trusted": "di fiducia",
"tx_commit_exception_no_dust_on_change": "La transazione viene respinta con questo importo. Con queste monete è possibile inviare ${min} senza modifiche o ${max} che restituisce il cambiamento.",
"tx_commit_failed": "Commit di transazione non riuscita. Si prega di contattare il supporto.",
"tx_invalid_input": "Stai usando il tipo di input sbagliato per questo tipo di pagamento",
"tx_no_dust_exception": "La transazione viene respinta inviando un importo troppo piccolo. Per favore, prova ad aumentare l'importo.",
"tx_not_enough_inputs_exception": "Input non sufficienti disponibili. Seleziona di più sotto il controllo delle monete",
"tx_rejected_bip68_final": "La transazione ha input non confermati e non è stata sostituita per tassa.",
"tx_rejected_dust_change": "Transazione respinta dalle regole di rete, quantità bassa variazione (polvere). Prova a inviare tutto o ridurre l'importo.",
"tx_rejected_dust_output": "Transazione respinta dalle regole di rete, bassa quantità di output (polvere). Si prega di aumentare l'importo.",
"tx_rejected_dust_output_send_all": "Transazione respinta dalle regole di rete, bassa quantità di output (polvere). Si prega di controllare il saldo delle monete selezionate sotto controllo delle monete.",

View file

@ -746,8 +746,10 @@
"trusted": "信頼できる",
"tx_commit_exception_no_dust_on_change": "この金額ではトランザクションは拒否されます。 これらのコインを使用すると、おつりなしの ${min} またはおつりを返す ${max} を送信できます。",
"tx_commit_failed": "トランザクションコミットは失敗しました。サポートに連絡してください。",
"tx_invalid_input": "このタイプの支払いに間違った入力タイプを使用しています",
"tx_no_dust_exception": "トランザクションは、小さすぎる金額を送信することにより拒否されます。量を増やしてみてください。",
"tx_not_enough_inputs_exception": "利用可能な入力が十分ではありません。コイン制御下でもっと選択してください",
"tx_rejected_bip68_final": "トランザクションには未確認の入力があり、料金で交換できませんでした。",
"tx_rejected_dust_change": "ネットワークルール、低い変更量(ほこり)によって拒否されたトランザクション。すべてを送信するか、金額を減らしてみてください。",
"tx_rejected_dust_output": "ネットワークルール、低出力量(ダスト)によって拒否されたトランザクション。金額を増やしてください。",
"tx_rejected_dust_output_send_all": "ネットワークルール、低出力量(ダスト)によって拒否されたトランザクション。コイン管理下で選択されたコインのバランスを確認してください。",

View file

@ -746,8 +746,10 @@
"trusted": "신뢰할 수 있는",
"tx_commit_exception_no_dust_on_change": "이 금액으로 거래가 거부되었습니다. 이 코인을 사용하면 거스름돈 없이 ${min}를 보내거나 거스름돈을 반환하는 ${max}를 보낼 수 있습니다.",
"tx_commit_failed": "거래 커밋이 실패했습니다. 지원에 연락하십시오.",
"tx_invalid_input": "이 유형의 지불에 잘못 입력 유형을 사용하고 있습니다.",
"tx_no_dust_exception": "너무 작은 금액을 보내면 거래가 거부됩니다. 금액을 늘리십시오.",
"tx_not_enough_inputs_exception": "사용 가능한 입력이 충분하지 않습니다. 코인 컨트롤에서 더 많은 것을 선택하십시오",
"tx_rejected_bip68_final": "거래는 확인되지 않은 입력을 받았으며 수수료로 교체하지 못했습니다.",
"tx_rejected_dust_change": "네트워크 규칙, 낮은 변경 금액 (먼지)에 의해 거부 된 거래. 전부를 보내거나 금액을 줄이십시오.",
"tx_rejected_dust_output": "네트워크 규칙, 낮은 출력 금액 (먼지)에 의해 거부 된 거래. 금액을 늘리십시오.",
"tx_rejected_dust_output_send_all": "네트워크 규칙, 낮은 출력 금액 (먼지)에 의해 거부 된 거래. 동전 제어에서 선택한 동전의 균형을 확인하십시오.",

View file

@ -745,8 +745,10 @@
"trusted": "ယုံတယ်။",
"tx_commit_exception_no_dust_on_change": "အဆိုပါငွေပေးငွေယူကဒီပမာဏနှင့်အတူပယ်ချခံရသည်။ ဤဒင်္ဂါးပြားများနှင့်အတူပြောင်းလဲမှုကိုပြန်လည်ပြောင်းလဲခြင်းသို့မဟုတ် ${min} မပါဘဲ ${max} ပေးပို့နိုင်သည်။",
"tx_commit_failed": "ငွေပေးငွေယူကျူးလွန်မှုပျက်ကွက်။ ကျေးဇူးပြုပြီးပံ့ပိုးမှုဆက်သွယ်ပါ။",
"tx_invalid_input": "သင်သည်ဤငွေပေးချေမှုအမျိုးအစားအတွက်မှားယွင်းသော input type ကိုအသုံးပြုနေသည်",
"tx_no_dust_exception": "ငွေပမာဏကိုသေးငယ်လွန်းသောငွေပမာဏကိုပေးပို့ခြင်းဖြင့်ပယ်ဖျက်ခြင်းကိုငြင်းပယ်သည်။ ကျေးဇူးပြုပြီးငွေပမာဏကိုတိုးမြှင့်ကြိုးစားပါ။",
"tx_not_enough_inputs_exception": "အလုံအလောက်သွင်းအားစုများမလုံလောက်။ ကျေးဇူးပြုပြီးဒင်္ဂါးပြားထိန်းချုပ်မှုအောက်တွင်ပိုမိုရွေးချယ်ပါ",
"tx_rejected_bip68_final": "ငွေပေးငွေယူသည်အတည်မပြုရသေးသောသွင်းအားစုများရှိပြီးအခကြေးငွေဖြင့်အစားထိုးရန်ပျက်ကွက်ခဲ့သည်။",
"tx_rejected_dust_change": "Network စည်းမျဉ်းစည်းကမ်းများဖြင့်ပယ်ဖျက်ခြင်းသည် Network စည်းမျဉ်းစည်းကမ်းများဖြင့်ငြင်းပယ်ခြင်း, အားလုံးပေးပို့ခြင်းသို့မဟုတ်ငွေပမာဏကိုလျှော့ချကြိုးစားပါ။",
"tx_rejected_dust_output": "Network စည်းမျဉ်းစည်းကမ်းများဖြင့် ပယ်ချ. ငွေပေးချေမှုသည် output output (ဖုန်မှုန့်) ဖြင့်ပယ်ချခဲ့သည်။ ကျေးဇူးပြုပြီးငွေပမာဏကိုတိုးမြှင့်ပေးပါ။",
"tx_rejected_dust_output_send_all": "Network စည်းမျဉ်းစည်းကမ်းများဖြင့် ပယ်ချ. ငွေပေးချေမှုသည် output output (ဖုန်မှုန့်) ဖြင့်ပယ်ချခဲ့သည်။ ဒင်္ဂါးပြားထိန်းချုပ်မှုအောက်တွင်ရွေးချယ်ထားသောဒင်္ဂါးများ၏လက်ကျန်ငွေကိုစစ်ဆေးပါ။",

View file

@ -745,8 +745,10 @@
"trusted": "vertrouwd",
"tx_commit_exception_no_dust_on_change": "De transactie wordt afgewezen met dit bedrag. Met deze munten kunt u ${min} verzenden zonder verandering of ${max} die wijziging retourneert.",
"tx_commit_failed": "Transactiebewissing is mislukt. Neem contact op met de ondersteuning.",
"tx_invalid_input": "U gebruikt het verkeerde invoertype voor dit type betaling",
"tx_no_dust_exception": "De transactie wordt afgewezen door een te klein bedrag te verzenden. Probeer het bedrag te verhogen.",
"tx_not_enough_inputs_exception": "Niet genoeg ingangen beschikbaar. Selecteer meer onder muntenbesturing",
"tx_rejected_bip68_final": "Transactie heeft onbevestigde ingangen en niet vervangen door vergoeding.",
"tx_rejected_dust_change": "Transactie afgewezen door netwerkregels, laag wijzigingsbedrag (stof). Probeer alles te verzenden of het bedrag te verminderen.",
"tx_rejected_dust_output": "Transactie afgewezen door netwerkregels, laag outputbedrag (stof). Verhoog het bedrag.",
"tx_rejected_dust_output_send_all": "Transactie afgewezen door netwerkregels, laag outputbedrag (stof). Controleer het saldo van munten die zijn geselecteerd onder muntcontrole.",

View file

@ -745,8 +745,10 @@
"trusted": "Zaufany",
"tx_commit_exception_no_dust_on_change": "Transakcja jest odrzucana z tą kwotą. Za pomocą tych monet możesz wysłać ${min} bez zmiany lub ${max}, które zwraca zmianę.",
"tx_commit_failed": "Zatwierdzenie transakcji nie powiodło się. Skontaktuj się z obsługą.",
"tx_invalid_input": "Używasz niewłaściwego typu wejściowego dla tego rodzaju płatności",
"tx_no_dust_exception": "Transakcja jest odrzucana przez wysyłanie zbyt małej ilości. Spróbuj zwiększyć kwotę.",
"tx_not_enough_inputs_exception": "Za mało dostępnych danych wejściowych. Wybierz więcej pod kontrolą monet",
"tx_rejected_bip68_final": "Transakcja niepotwierdza wejściów i nie zastąpiła opłaty.",
"tx_rejected_dust_change": "Transakcja odrzucona według reguł sieciowych, niska ilość zmiany (kurz). Spróbuj wysłać całość lub zmniejszyć kwotę.",
"tx_rejected_dust_output": "Transakcja odrzucona według reguł sieciowych, niskiej ilości wyjściowej (pyłu). Zwiększ kwotę.",
"tx_rejected_dust_output_send_all": "Transakcja odrzucona według reguł sieciowych, niskiej ilości wyjściowej (pyłu). Sprawdź saldo monet wybranych pod kontrolą monet.",

View file

@ -747,8 +747,10 @@
"trusted": "confiável",
"tx_commit_exception_no_dust_on_change": "A transação é rejeitada com esse valor. Com essas moedas, você pode enviar ${min} sem alteração ou ${max} que retorna alterações.",
"tx_commit_failed": "A confirmação da transação falhou. Entre em contato com o suporte.",
"tx_invalid_input": "Você está usando o tipo de entrada errado para este tipo de pagamento",
"tx_no_dust_exception": "A transação é rejeitada enviando uma quantia pequena demais. Por favor, tente aumentar o valor.",
"tx_not_enough_inputs_exception": "Não há entradas disponíveis. Selecione mais sob controle de moedas",
"tx_rejected_bip68_final": "A transação tem entradas não confirmadas e não substituiu por taxa.",
"tx_rejected_dust_change": "Transação rejeitada pelas regras de rede, baixa quantidade de troco (poeira). Tente enviar tudo ou reduzir o valor.",
"tx_rejected_dust_output": "Transação rejeitada por regras de rede, baixa quantidade de saída (poeira). Por favor, aumente o valor.",
"tx_rejected_dust_output_send_all": "Transação rejeitada por regras de rede, baixa quantidade de saída (poeira). Por favor, verifique o saldo de moedas selecionadas sob controle de moedas.",
@ -760,7 +762,7 @@
"unconfirmed": "Saldo não confirmado",
"understand": "Entendo",
"unmatched_currencies": "A moeda da sua carteira atual não corresponde à do QR digitalizado",
"unspent_change": "Mudar",
"unspent_change": "Troco",
"unspent_coins_details_title": "Detalhes de moedas não gastas",
"unspent_coins_title": "Moedas não gastas",
"unsupported_asset": "Não oferecemos suporte a esta ação para este recurso. Crie ou mude para uma carteira de um tipo de ativo compatível.",

View file

@ -746,8 +746,10 @@
"trusted": "доверенный",
"tx_commit_exception_no_dust_on_change": "Транзакция отклоняется с этой суммой. С этими монетами вы можете отправлять ${min} без изменения или ${max}, которые возвращают изменение.",
"tx_commit_failed": "Комплект транзакции не удался. Пожалуйста, свяжитесь с поддержкой.",
"tx_invalid_input": "Вы используете неправильный тип ввода для этого типа оплаты",
"tx_no_dust_exception": "Транзакция отклоняется путем отправки слишком маленькой суммы. Пожалуйста, попробуйте увеличить сумму.",
"tx_not_enough_inputs_exception": "Недостаточно входов доступны. Пожалуйста, выберите больше под контролем монет",
"tx_rejected_bip68_final": "Транзакция имеет неподтвержденные входные данные и не смогли заменить на плату.",
"tx_rejected_dust_change": "Транзакция отклоняется в соответствии с правилами сети, низкой суммой изменений (пыль). Попробуйте отправить все или уменьшить сумму.",
"tx_rejected_dust_output": "Транзакция отклоняется в соответствии с правилами сети, низкой выходной суммой (пыль). Пожалуйста, увеличьте сумму.",
"tx_rejected_dust_output_send_all": "Транзакция отклоняется в соответствии с правилами сети, низкой выходной суммой (пыль). Пожалуйста, проверьте баланс монет, выбранных под контролем монет.",

View file

@ -745,8 +745,10 @@
"trusted": "มั่นคง",
"tx_commit_exception_no_dust_on_change": "ธุรกรรมถูกปฏิเสธด้วยจำนวนเงินนี้ ด้วยเหรียญเหล่านี้คุณสามารถส่ง ${min} โดยไม่ต้องเปลี่ยนแปลงหรือ ${max} ที่ส่งคืนการเปลี่ยนแปลง",
"tx_commit_failed": "การทำธุรกรรมล้มเหลว กรุณาติดต่อฝ่ายสนับสนุน",
"tx_invalid_input": "คุณกำลังใช้ประเภทอินพุตที่ไม่ถูกต้องสำหรับการชำระเงินประเภทนี้",
"tx_no_dust_exception": "การทำธุรกรรมถูกปฏิเสธโดยการส่งจำนวนน้อยเกินไป โปรดลองเพิ่มจำนวนเงิน",
"tx_not_enough_inputs_exception": "มีอินพุตไม่เพียงพอ โปรดเลือกเพิ่มเติมภายใต้การควบคุมเหรียญ",
"tx_rejected_bip68_final": "การทำธุรกรรมมีอินพุตที่ไม่ได้รับการยืนยันและไม่สามารถแทนที่ด้วยค่าธรรมเนียม",
"tx_rejected_dust_change": "ธุรกรรมถูกปฏิเสธโดยกฎเครือข่ายจำนวนการเปลี่ยนแปลงต่ำ (ฝุ่น) ลองส่งทั้งหมดหรือลดจำนวนเงิน",
"tx_rejected_dust_output": "การทำธุรกรรมถูกปฏิเสธโดยกฎเครือข่ายจำนวนเอาต์พุตต่ำ (ฝุ่น) โปรดเพิ่มจำนวนเงิน",
"tx_rejected_dust_output_send_all": "การทำธุรกรรมถูกปฏิเสธโดยกฎเครือข่ายจำนวนเอาต์พุตต่ำ (ฝุ่น) โปรดตรวจสอบยอดคงเหลือของเหรียญที่เลือกภายใต้การควบคุมเหรียญ",

View file

@ -745,8 +745,10 @@
"trusted": "Pinagkakatiwalaan",
"tx_commit_exception_no_dust_on_change": "Ang transaksyon ay tinanggihan sa halagang ito. Sa mga barya na ito maaari kang magpadala ng ${min} nang walang pagbabago o ${max} na nagbabalik ng pagbabago.",
"tx_commit_failed": "Nabigo ang transaksyon sa transaksyon. Mangyaring makipag -ugnay sa suporta.",
"tx_invalid_input": "Gumagamit ka ng maling uri ng pag -input para sa ganitong uri ng pagbabayad",
"tx_no_dust_exception": "Ang transaksyon ay tinanggihan sa pamamagitan ng pagpapadala ng isang maliit na maliit. Mangyaring subukang dagdagan ang halaga.",
"tx_not_enough_inputs_exception": "Hindi sapat na magagamit ang mga input. Mangyaring pumili ng higit pa sa ilalim ng control ng barya",
"tx_rejected_bip68_final": "Ang transaksyon ay hindi nakumpirma na mga input at nabigo na palitan ng bayad.",
"tx_rejected_dust_change": "Ang transaksyon na tinanggihan ng mga patakaran sa network, mababang halaga ng pagbabago (alikabok). Subukang ipadala ang lahat o bawasan ang halaga.",
"tx_rejected_dust_output": "Ang transaksyon na tinanggihan ng mga patakaran sa network, mababang halaga ng output (alikabok). Mangyaring dagdagan ang halaga.",
"tx_rejected_dust_output_send_all": "Ang transaksyon na tinanggihan ng mga patakaran sa network, mababang halaga ng output (alikabok). Mangyaring suriin ang balanse ng mga barya na napili sa ilalim ng kontrol ng barya.",

View file

@ -745,8 +745,10 @@
"trusted": "Güvenilir",
"tx_commit_exception_no_dust_on_change": "İşlem bu miktarla reddedilir. Bu madeni paralarla değişiklik yapmadan ${min} veya değişikliği döndüren ${max} gönderebilirsiniz.",
"tx_commit_failed": "İşlem taahhüdü başarısız oldu. Lütfen Destek ile iletişime geçin.",
"tx_invalid_input": "Bu tür ödeme için yanlış giriş türünü kullanıyorsunuz",
"tx_no_dust_exception": "İşlem, çok küçük bir miktar gönderilerek reddedilir. Lütfen miktarı artırmayı deneyin.",
"tx_not_enough_inputs_exception": "Yeterli giriş yok. Lütfen madeni para kontrolü altında daha fazlasını seçin",
"tx_rejected_bip68_final": "İşlemin doğrulanmamış girdileri var ve ücrete göre değiştirilemedi.",
"tx_rejected_dust_change": "Ağ kurallarına göre reddedilen işlem, düşük değişim miktarı (toz). Tümünü göndermeyi veya miktarı azaltmayı deneyin.",
"tx_rejected_dust_output": "Ağ kurallarına göre reddedilen işlem, düşük çıktı miktarı (toz). Lütfen miktarı artırın.",
"tx_rejected_dust_output_send_all": "Ağ kurallarına göre reddedilen işlem, düşük çıktı miktarı (toz). Lütfen madeni para kontrolü altında seçilen madeni para dengesini kontrol edin.",

View file

@ -746,8 +746,10 @@
"trusted": "довіряють",
"tx_commit_exception_no_dust_on_change": "Транзакція відхилена цією сумою. За допомогою цих монет ви можете надіслати ${min} без змін або ${max}, що повертає зміни.",
"tx_commit_failed": "Транзакційна комісія не вдалося. Будь ласка, зв'яжіться з підтримкою.",
"tx_invalid_input": "Ви використовуєте неправильний тип введення для цього типу оплати",
"tx_no_dust_exception": "Угода відхиляється, відправивши суму занадто мала. Будь ласка, спробуйте збільшити суму.",
"tx_not_enough_inputs_exception": "Недостатньо доступних входів. Виберіть більше під контролем монети",
"tx_rejected_bip68_final": "Трансакція має непідтверджені входи і не замінила плату.",
"tx_rejected_dust_change": "Транзакція відхилена за допомогою мережевих правил, низька кількість змін (пил). Спробуйте надіслати все або зменшити суму.",
"tx_rejected_dust_output": "Транзакція відхилена за допомогою мережевих правил, низька кількість вихідної кількості (пил). Будь ласка, збільшуйте суму.",
"tx_rejected_dust_output_send_all": "Транзакція відхилена за допомогою мережевих правил, низька кількість вихідної кількості (пил). Будь ласка, перевірте баланс монет, вибраних під контролем монет.",

View file

@ -747,8 +747,10 @@
"trusted": "قابل اعتماد",
"tx_commit_exception_no_dust_on_change": "اس رقم سے لین دین کو مسترد کردیا گیا ہے۔ ان سککوں کے ذریعہ آپ بغیر کسی تبدیلی کے ${min} یا ${max} بھیج سکتے ہیں جو لوٹتے ہیں۔",
"tx_commit_failed": "ٹرانزیکشن کمٹ ناکام ہوگیا۔ براہ کرم سپورٹ سے رابطہ کریں۔",
"tx_invalid_input": "آپ اس قسم کی ادائیگی کے لئے غلط ان پٹ کی قسم استعمال کررہے ہیں",
"tx_no_dust_exception": "لین دین کو بہت چھوٹی رقم بھیج کر مسترد کردیا جاتا ہے۔ براہ کرم رقم میں اضافہ کرنے کی کوشش کریں۔",
"tx_not_enough_inputs_exception": "کافی ان پٹ دستیاب نہیں ہے۔ براہ کرم سکے کے کنٹرول میں مزید منتخب کریں",
"tx_rejected_bip68_final": "لین دین میں غیر مصدقہ آدانوں کی ہے اور وہ فیس کے ذریعہ تبدیل کرنے میں ناکام رہا ہے۔",
"tx_rejected_dust_change": "نیٹ ورک کے قواعد ، کم تبدیلی کی رقم (دھول) کے ذریعہ لین دین کو مسترد کردیا گیا۔ سب کو بھیجنے یا رقم کو کم کرنے کی کوشش کریں۔",
"tx_rejected_dust_output": "لین دین کو نیٹ ورک کے قواعد ، کم آؤٹ پٹ رقم (دھول) کے ذریعہ مسترد کردیا گیا۔ براہ کرم رقم میں اضافہ کریں۔",
"tx_rejected_dust_output_send_all": "لین دین کو نیٹ ورک کے قواعد ، کم آؤٹ پٹ رقم (دھول) کے ذریعہ مسترد کردیا گیا۔ براہ کرم سکے کے کنٹرول میں منتخب کردہ سکے کا توازن چیک کریں۔",

View file

@ -746,8 +746,10 @@
"trusted": "A ti fọkàn ẹ̀ tán",
"tx_commit_exception_no_dust_on_change": "Iṣowo naa ti kọ pẹlu iye yii. Pẹlu awọn owó wọnyi o le firanṣẹ ${min} laisi ayipada tabi ${max} ni iyipada iyipada.",
"tx_commit_failed": "Idunadura iṣowo kuna. Jọwọ kan si atilẹyin.",
"tx_invalid_input": "O nlo iru titẹ nkan ti ko tọ fun iru isanwo yii",
"tx_no_dust_exception": "Iṣowo naa ni kọ nipa fifiranṣẹ iye ti o kere ju. Jọwọ gbiyanju pọ si iye naa.",
"tx_not_enough_inputs_exception": "Ko to awọn titẹsi to. Jọwọ yan diẹ sii labẹ iṣakoso owo",
"tx_rejected_bip68_final": "Iṣowo ni awọn igbewọle gbangba ati kuna lati rọpo nipasẹ owo.",
"tx_rejected_dust_change": "Idunadura kọ nipasẹ awọn ofin nẹtiwọọki, iye iyipada kekere (eruku). Gbiyanju lati firanṣẹ gbogbo rẹ tabi dinku iye.",
"tx_rejected_dust_output": "Idunadura kọ nipasẹ awọn ofin nẹtiwọọki, iye ti o wuwe kekere (eruku). Jọwọ mu iye naa pọ si.",
"tx_rejected_dust_output_send_all": "Idunadura kọ nipasẹ awọn ofin nẹtiwọọki, iye ti o wuwe kekere (eruku). Jọwọ ṣayẹwo dọgbadọgba ti awọn owo ti a yan labẹ iṣakoso owo.",

View file

@ -745,8 +745,10 @@
"trusted": "值得信赖",
"tx_commit_exception_no_dust_on_change": "交易被此金额拒绝。使用这些硬币,您可以发送${min}无需更改或返回${max}的变化。",
"tx_commit_failed": "交易承诺失败。请联系支持。",
"tx_invalid_input": "您正在使用错误的输入类型进行此类付款",
"tx_no_dust_exception": "通过发送太小的金额来拒绝交易。请尝试增加金额。",
"tx_not_enough_inputs_exception": "没有足够的输入。请在硬币控制下选择更多",
"tx_rejected_bip68_final": "交易未确认投入,未能取代费用。",
"tx_rejected_dust_change": "交易被网络规则拒绝,较低的变化数量(灰尘)。尝试发送全部或减少金额。",
"tx_rejected_dust_output": "交易被网络规则,低输出量(灰尘)拒绝。请增加金额。",
"tx_rejected_dust_output_send_all": "交易被网络规则,低输出量(灰尘)拒绝。请检查在硬币控制下选择的硬币的余额。",

View file

@ -173,6 +173,7 @@ abstract class Bitcoin {
Future<void> rescan(Object wallet, {required int height, bool? doSingleScan});
bool getNodeIsCakeElectrs(Object wallet);
void deleteSilentPaymentAddress(Object wallet, String address);
Future<void> updateFeeRates(Object wallet);
}
""";