feat: spend, scan changes

This commit is contained in:
Rafael Saes 2023-11-17 18:53:07 -03:00
parent 908461d76d
commit 2f6ad5248d
36 changed files with 489 additions and 266 deletions

View file

@ -1,10 +1,33 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
# This file should be version controlled.
version:
revision: b1395592de68cc8ac4522094ae59956dd21a91db
revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
channel: stable
project_type: package
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
- platform: macos
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
- platform: linux
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View file

@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:typed_data';
class BitcoinAddressRecord {
BitcoinAddressRecord(this.address,
@ -24,7 +25,7 @@ class BitcoinAddressRecord {
final String address;
final bool isHidden;
final String? silentAddressLabel;
final String? silentPaymentTweak;
final Uint8List? silentPaymentTweak;
final int index;
bool get isUsed => _isUsed;
@ -35,6 +36,12 @@ class BitcoinAddressRecord {
void setAsUsed() => _isUsed = true;
String toJSON() =>
json.encode({'address': address, 'index': index, 'isHidden': isHidden, 'isUsed': isUsed});
String toJSON() => json.encode({
'address': address,
'index': index,
'isHidden': isHidden,
'isUsed': isUsed,
'silentAddressLabel': silentAddressLabel,
'silentPaymentTweak': silentPaymentTweak
});
}

View file

@ -1,3 +1,5 @@
import 'dart:typed_data';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_core/unspent_transaction_output.dart';
@ -12,5 +14,5 @@ class BitcoinUnspent extends Unspent {
address, json['tx_hash'] as String, json['value'] as int, json['tx_pos'] as int);
final BitcoinAddressRecord bitcoinAddressRecord;
String? silentPaymentTweak;
Uint8List? silentPaymentTweak;
}

View file

@ -232,14 +232,14 @@ class ElectrumClient {
{required String hash, required NetworkType networkType}) async =>
callWithTimeout(
method: 'blockchain.transaction.get',
params: networkType == bitcoin ? [hash, true] : [hash],
params: networkType.bech32 == bitcoin.bech32 ? [hash, true] : [hash],
timeout: 10000)
.then((dynamic result) {
if (result is Map<String, dynamic>) {
return result;
}
if (networkType == testnet && result is String) {
if (networkType.bech32 == testnet.bech32 && result is String) {
return result;
}
@ -262,6 +262,7 @@ class ElectrumClient {
headers: <String, String>{'Content-Type': 'application/json; charset=utf-8'},
body: transactionRaw)
.then((http.Response response) {
print(response.body);
if (response.statusCode == 200) {
return response.body;
}

View file

@ -132,6 +132,8 @@ abstract class ElectrumWalletBase
Map<String, BehaviorSubject<Object>?> _scripthashesUpdateSubject;
BehaviorSubject<Object>? _chainTipUpdateSubject;
bool _isTransactionUpdating;
int _initialSyncHeight = 0;
Future<Isolate>? _isolate;
void Function(FlutterErrorDetails)? _onError;
@ -299,33 +301,56 @@ abstract class ElectrumWalletBase
List<bitcoin.PrivateKeyInfo> inputPrivKeys = [];
List<bitcoin.Outpoint> outpoints = [];
inputs.forEach((input) {
inputPrivKeys.add(bitcoin.PrivateKeyInfo(
bitcoin.PrivateKey.fromHex(
bitcoin.getSecp256k1(),
HEX.encode(generateKeyPair(
hd: input.bitcoinAddressRecord.isHidden
? walletAddresses.sideHd
: walletAddresses.mainHd,
index: input.bitcoinAddressRecord.index,
network: networkType)
.privateKey!)),
false));
final isSilentPayment = input.bitcoinAddressRecord.silentPaymentTweak != null;
outpoints.add(bitcoin.Outpoint(txid: input.hash, index: input.vout));
if (input.isP2wpkh) {
final p2wpkh = bitcoin
.P2WPKH(
data: generatePaymentData(
hd: input.bitcoinAddressRecord.isHidden
? walletAddresses.sideHd
: walletAddresses.mainHd,
index: input.bitcoinAddressRecord.index),
if (isSilentPayment) {
// https://github.com/bitcoin/bips/blob/c55f80c53c98642357712c1839cfdc0551d531c4/bip-0352.mediawiki#user-content-Spending
final d = walletAddresses.silentAddress!.spendPrivkey
.tweakAdd(input.bitcoinAddressRecord.silentPaymentTweak!.bigint)!;
inputPrivKeys.add(bitcoin.PrivateKeyInfo(d, true));
print(["output", d]);
final p2tr = bitcoin
.P2TR(
data: bitcoin.PaymentData(pubkey: d.publicKey.toCompressedHex().fromHex),
network: networkType)
.data;
txb.addInput(input.hash, input.vout, null, p2wpkh.output);
print(["output", p2tr.output]);
txb.addInput(input.hash, input.vout, null, p2tr.output);
} else {
txb.addInput(input.hash, input.vout);
inputPrivKeys.add(bitcoin.PrivateKeyInfo(
bitcoin.PrivateKey.fromHex(
bitcoin.getSecp256k1(),
generateKeyPair(
hd: input.bitcoinAddressRecord.isHidden
? walletAddresses.sideHd
: walletAddresses.mainHd,
index: input.bitcoinAddressRecord.index,
network: networkType)
.privateKey!
.hex),
false));
if (input.isP2wpkh) {
final p2wpkh = bitcoin
.P2WPKH(
data: generatePaymentData(
hd: input.bitcoinAddressRecord.isHidden
? walletAddresses.sideHd
: walletAddresses.mainHd,
index: input.bitcoinAddressRecord.index),
network: networkType)
.data;
txb.addInput(input.hash, input.vout, null, p2wpkh.output);
} else {
txb.addInput(input.hash, input.vout);
}
}
});
@ -503,51 +528,85 @@ abstract class ElectrumWalletBase
bitcoin.ECPair keyPairFor({required int index}) =>
generateKeyPair(hd: hd, index: index, network: networkType);
@action
@override
Future<void> rescan({required int height}) async {
Future<void> rescan({required int height, int? chainTip, ScanData? scanData}) async {
syncStatus = AttemptingSyncStatus();
walletInfo.restoreHeight = height;
ReceivePort receivePort = ReceivePort();
Isolate.spawn(startRefresh, {
"silentAddress": walletAddresses.silentAddress!.toString(),
"scanPrivateKeyCompressed": walletAddresses.silentAddress!.scanPrivkey.toCompressedHex(),
"spendPubkeyCompressed": walletAddresses.silentAddress!.spendPubkey.toCompressedHex(),
"height": walletInfo.restoreHeight.toString(),
"node": electrumClient.uri.toString(),
"receivePort": receivePort.sendPort,
});
await for (var unspentScannedCoins in receivePort) {
if (unspentScannedCoins is List<BitcoinUnspent>) {
if (unspentScannedCoins.isNotEmpty) {
print("UNSPENT COIN FOUND!");
final myNewAddresses = unspentScannedCoins.map((coin) {
return BitcoinAddressRecord(
coin.address,
index: 0,
isHidden: false,
isUsed: true,
silentAddressLabel: null,
silentPaymentTweak: coin.silentPaymentTweak,
);
});
walletAddresses.addAddresses(myNewAddresses);
await updateTransactions();
_subscribeForUpdates();
await updateUnspent();
await updateBalance();
await walletInfo.save();
final currentChainTip = chainTip ?? await electrumClient.getCurrentBlockChainTip() ?? 0;
if (_isolate != null) {
final runningIsolate = await _isolate!;
runningIsolate.kill(priority: Isolate.immediate);
}
if (currentChainTip <= height) {
syncStatus = SyncedSyncStatus();
return;
}
final receivePort = ReceivePort();
_isolate = Isolate.spawn(
startRefresh,
ScanData(
sendPort: receivePort.sendPort,
silentAddress: walletAddresses.silentAddress!.toString(),
scanPrivkeyCompressed:
walletAddresses.silentAddress!.scanPrivkey.toCompressedHex().fromHex,
spendPubkeyCompressed:
walletAddresses.silentAddress!.spendPubkey.toCompressedHex().fromHex,
networkType: networkType,
height: walletInfo.restoreHeight,
chainTip: currentChainTip,
initialSyncHeight: _initialSyncHeight,
electrumClient: ElectrumClient(),
transactionHistoryIds: transactionHistory.transactions.keys.toList(),
node: electrumClient.uri.toString()));
await for (var message in receivePort) {
if (message is BitcoinUnspent) {
final myNewUnspent = message;
final hasUnspent = unspentCoins.any((element) {
if (element.address == message.address) {
unspentCoins.remove(element);
unspentCoins.add(myNewUnspent);
return true;
}
return false;
});
if (!hasUnspent) {
unspentCoins.add(myNewUnspent);
}
final currentHeight = await electrumClient.getCurrentBlockChainTip();
if (currentHeight != null && currentHeight > walletInfo.restoreHeight) {
walletInfo.restoreHeight = walletInfo.restoreHeight + 1;
await save();
// recursive, scan next block again until at the current tip
rescan(height: walletInfo.restoreHeight);
} else {
syncStatus = SyncedSyncStatus();
final myNewAddress = message.bitcoinAddressRecord;
final hasAddress = walletAddresses.addresses.any((element) {
if (element.address == message.address) {
walletAddresses.addresses.remove(element);
walletAddresses.addresses.add(myNewAddress);
return true;
}
return false;
});
if (!hasAddress) {
walletAddresses.addresses.add(myNewAddress);
}
await save();
await updateUnspent();
await updateBalance();
await updateTransactions();
_subscribeForUpdates();
}
// check if is a SyncStatus type since "is SyncStatus" doesn't work here
if (message is SyncResponse) {
syncStatus = message.syncStatus;
walletInfo.restoreHeight = message.height;
await walletInfo.save();
}
}
}
@ -571,7 +630,15 @@ abstract class ElectrumWalletBase
return null;
}
}).whereNotNull())));
unspentCoins.addAll(unspent.expand((e) => e).toList());
unspent.expand((e) => e).forEach((newUnspent) {
try {
if (!unspentCoins.any((currentUnspent) =>
currentUnspent.address.contains(newUnspent.address) &&
currentUnspent.hash.contains(newUnspent.hash))) {
unspentCoins.add(newUnspent);
}
} catch (_) {}
});
if (unspentCoinsInfo.isEmpty) {
unspentCoins.forEach((coin) => _addCoinInfo(coin));
@ -580,8 +647,10 @@ abstract class ElectrumWalletBase
if (unspentCoins.isNotEmpty) {
unspentCoins.forEach((coin) {
final coinInfoList = unspentCoinsInfo.values
.where((element) => element.walletId.contains(id) && element.hash.contains(coin.hash));
final coinInfoList = unspentCoinsInfo.values.where((element) =>
element.walletId.contains(id) &&
element.hash.contains(coin.hash) &&
element.address.contains(coin.address));
if (coinInfoList.isNotEmpty) {
final coinInfo = coinInfoList.first;
@ -642,6 +711,10 @@ abstract class ElectrumWalletBase
final addressHashes = <String, BitcoinAddressRecord>{};
final normalizedHistories = <Map<String, dynamic>>[];
walletAddresses.addresses.forEach((addressRecord) {
if (addressRecord.address ==
"tb1pch9qmsq87wy4my4akd60x2r2yt784zfmfwqeuk7w7g7u45za4ktq9pdnmf") {
print(["during fetch txs", addressRecord.address, addressRecord.silentPaymentTweak]);
}
final sh = scriptHash(addressRecord.address, networkType: networkType);
addressHashes[sh] = addressRecord;
});
@ -708,6 +781,9 @@ abstract class ElectrumWalletBase
await updateUnspent();
await updateBalance();
await updateTransactions();
final currentHeight = await electrumClient.getCurrentBlockChainTip();
if (currentHeight != null) walletInfo.restoreHeight = currentHeight;
rescan(height: walletInfo.restoreHeight);
} catch (e, s) {
print(e.toString());
_onError?.call(FlutterErrorDetails(
@ -718,22 +794,6 @@ abstract class ElectrumWalletBase
}
});
});
await _chainTipUpdateSubject?.close();
_chainTipUpdateSubject = electrumClient.chainTipUpdate();
_chainTipUpdateSubject?.listen((event) async {
try {
final currentHeight = await electrumClient.getCurrentBlockChainTip();
if (currentHeight != null) walletInfo.restoreHeight = currentHeight;
rescan(height: walletInfo.restoreHeight);
} catch (e, s) {
print(e.toString());
_onError?.call(FlutterErrorDetails(
exception: e,
stack: s,
library: this.runtimeType.toString(),
));
}
});
}
Future<ElectrumBalance> _fetchBalances() async {
@ -810,13 +870,16 @@ abstract class ElectrumWalletBase
}
Future<void> _setInitialHeight() async {
if (walletInfo.isRecovery || walletInfo.restoreHeight != 0) {
if (walletInfo.isRecovery) {
return;
}
// final currentHeight = 2538835;
final currentHeight = await electrumClient.getCurrentBlockChainTip();
if (currentHeight != null) walletInfo.restoreHeight = currentHeight;
if (walletInfo.restoreHeight == 0) {
final currentHeight = await electrumClient.getCurrentBlockChainTip();
if (currentHeight != null) walletInfo.restoreHeight = currentHeight;
}
_initialSyncHeight = walletInfo.restoreHeight;
}
}
@ -849,7 +912,7 @@ Future<ElectrumTransactionBundle> getTransactionExpanded(
String transactionHex;
int? time;
int confirmations = 0;
if (networkType == bitcoin.testnet) {
if (networkType.bech32 == bitcoin.testnet.bech32) {
transactionHex = verboseTransaction as String;
confirmations = 1;
} else {
@ -871,132 +934,231 @@ Future<ElectrumTransactionBundle> getTransactionExpanded(
return ElectrumTransactionBundle(original, ins: ins, time: time, confirmations: confirmations);
}
Future<void> startRefresh(Map<String, dynamic> data) async {
final sendPort = data["receivePort"] as SendPort;
String? checkpointTx = data["checkpoint_tx"] as String?;
class ScanData {
final SendPort sendPort;
final Uint8List scanPrivkeyCompressed;
final Uint8List spendPubkeyCompressed;
final String silentAddress;
final int height;
final String node;
final bitcoin.NetworkType networkType;
final int chainTip;
final int initialSyncHeight;
final ElectrumClient electrumClient;
final List<String> transactionHistoryIds;
final int? checkpointTxPos;
ScanData(
{required this.sendPort,
required this.scanPrivkeyCompressed,
required this.spendPubkeyCompressed,
required this.silentAddress,
required this.height,
required this.node,
required this.networkType,
required this.chainTip,
required this.initialSyncHeight,
required this.electrumClient,
required this.transactionHistoryIds,
this.checkpointTxPos});
factory ScanData.withCheckpoint(ScanData scanData, int newHeight, int? checkpointTx) {
return ScanData(
sendPort: scanData.sendPort,
scanPrivkeyCompressed: scanData.scanPrivkeyCompressed,
spendPubkeyCompressed: scanData.spendPubkeyCompressed,
silentAddress: scanData.silentAddress,
height: newHeight,
node: scanData.node,
networkType: scanData.networkType,
chainTip: scanData.chainTip,
initialSyncHeight: scanData.initialSyncHeight,
electrumClient: scanData.electrumClient,
transactionHistoryIds: scanData.transactionHistoryIds,
checkpointTxPos: checkpointTx);
}
}
class SyncResponse {
final int height;
final SyncStatus syncStatus;
SyncResponse(this.height, this.syncStatus);
}
Future<void> startRefresh(ScanData scanData) async {
final currentChainTip = scanData.chainTip;
if (scanData.height >= currentChainTip) {
scanData.sendPort.send(SyncResponse(scanData.height, SyncedSyncStatus()));
return;
}
var checkpointTxPos = scanData.checkpointTxPos;
final height = scanData.height;
print(["HEIGHT:", height]);
try {
final height = int.parse(data["height"] as String);
// final height = 2538835;
final track = currentChainTip - height;
final diff = (currentChainTip - scanData.initialSyncHeight) - track;
final ptc = diff <= 0 ? 0.0 : diff / track;
scanData.sendPort.send(SyncResponse(height, SyncingSyncStatus(track, ptc)));
final node = data["node"] as String;
final electrumClient = ElectrumClient();
await electrumClient.connectToUri(Uri.parse(node));
final electrumClient = scanData.electrumClient;
if (!electrumClient.isConnected) {
final node = scanData.node;
await electrumClient.connectToUri(Uri.parse(node));
}
print(["HEIGHT:", height]);
List<String> txids = [];
int pos = 0;
// tx pos always begin from 1 --> we know pos 0 is coinbase tx
int pos = checkpointTxPos ?? 1;
while (true) {
try {
var txid = await electrumClient.getTxidFromPos(height: height, pos: pos);
txids.add(txid);
final txid = await electrumClient.getTxidFromPos(height: height, pos: pos);
print(["scanning tx:", txid]);
// TODO: if already tx already scanned & stored skip
// if (scanData.transactionHistoryIds.contains(txid)) {
// // already scanned tx, continue to next tx
// checkpointTxPos = pos;
// pos++;
// continue;
// }
List<String> pubkeys = [];
List<bitcoin.Outpoint> outpoints = [];
try {
final txBundle = await getTransactionExpanded(
hash: txid,
height: height,
electrumClient: electrumClient,
networkType: scanData.networkType);
bool skip = false;
txBundle.originalTransaction.ins.forEach((input) {
if (input.witness == null) {
skip = true;
return;
}
if (input.witness!.length != 2) {
skip = true;
return;
}
final pubkey = input.witness![1].hex;
pubkeys.add(pubkey);
outpoints.add(bitcoin.Outpoint(
txid: HEX.encode(input.hash!.reversed.toList()), index: input.index!));
});
if (skip) {
// skipped tx, save checkpoint in case of issues and continue to next tx
checkpointTxPos = pos;
pos++;
continue;
}
Map<String, bitcoin.Outpoint> outpointsByP2TRpubkey = {};
int i = 0;
txBundle.originalTransaction.outs.forEach((output) {
if (bitcoin.classifyOutput(output.script!) != "taproot") {
return;
}
final address = bitcoin.P2trAddress(program: output.script!.sublist(2).hex).toAddress(
scanData.networkType.bech32 == bitcoin.testnet.bech32
? bitcoin.NetworkInfo.TESTNET
: bitcoin.NetworkInfo.BITCOIN);
print(["verifying taproot address:", address]);
outpointsByP2TRpubkey[address] =
bitcoin.Outpoint(txid: txid, index: i, value: output.value!);
i++;
});
if (pubkeys.isEmpty || outpoints.isEmpty || outpointsByP2TRpubkey.isEmpty) {
// skipped tx, save checkpoint in case of issues and continue to next tx
checkpointTxPos = pos;
pos++;
continue;
}
Uint8List sumOfInputPublicKeys =
bitcoin.getSumInputPubKeys(pubkeys).toCompressedHex().fromHex;
final outpointHash = bitcoin.SilentPayment.hashOutpoints(outpoints);
final result = bitcoin.scanOutputs(
scanData.scanPrivkeyCompressed,
scanData.spendPubkeyCompressed,
sumOfInputPublicKeys,
outpointHash,
outpointsByP2TRpubkey.keys.toList());
if (result.isEmpty) {
// no results tx, save checkpoint in case of issues and continue to next tx
checkpointTxPos = pos;
pos++;
continue;
}
print("UNSPENT COIN FOUND!");
print(result);
result.forEach((key, value) {
final outpoint = outpointsByP2TRpubkey[key];
if (outpoint == null) {
return;
}
// found utxo for tx
scanData.sendPort.send(BitcoinUnspent(
BitcoinAddressRecord(
key,
index: 0,
isHidden: false,
isUsed: true,
silentAddressLabel: null,
silentPaymentTweak: value,
),
outpoint.txid,
outpoint.value!,
outpoint.index,
silentPaymentTweak: value,
));
});
} catch (e, stacktrace) {
print(stacktrace);
print(e.toString());
}
pos++;
} catch (_) {
// no more txs
} catch (e, stacktrace) {
print(stacktrace);
print(e.toString());
// last position, no more txs for the given block height
break;
}
}
if (checkpointTx != null && checkpointTx.isNotEmpty) {
txids = txids.sublist(txids.indexOf(checkpointTx) + 1);
final newHeight = height + 1;
if (newHeight < currentChainTip) {
// recursive, scan next block again until at the current tip
return startRefresh(ScanData.withCheckpoint(scanData, newHeight, 1));
}
List<BitcoinUnspent> unspentCoins = [];
for (var txid in txids) {
checkpointTx = txid.toString();
List<String> pubkeys = [];
List<bitcoin.Outpoint> outpoints = [];
Map<String, bitcoin.Outpoint> outpointsByP2TRpubkey = {};
try {
// TODO: networkType
final txBundle = await getTransactionExpanded(
hash: txid,
height: height,
electrumClient: electrumClient,
networkType: bitcoin.testnet);
bool skip = false;
txBundle.originalTransaction.ins.forEach((input) {
if (input.witness == null) {
skip = true;
return;
}
if (input.witness!.length != 2) {
skip = true;
return;
}
final pubkey = input.witness![1].hex;
pubkeys.add(pubkey);
outpoints.add(bitcoin.Outpoint(
txid: HEX.encode(input.hash!.reversed.toList()), index: input.index!));
});
if (skip) continue;
int i = 0;
txBundle.originalTransaction.outs.forEach((output) {
if (bitcoin.classifyOutput(output.script!) != "taproot") {
return;
}
print(bitcoin.P2trAddress(program: output.script!.sublist(2).hex)
.toAddress(bitcoin.NetworkInfo.TESTNET));
outpointsByP2TRpubkey[bitcoin.P2trAddress(program: output.script!.sublist(2).hex)
.toAddress(bitcoin.NetworkInfo.TESTNET)] =
bitcoin.Outpoint(txid: txid, index: i, value: output.value!);
i++;
});
if (pubkeys.isEmpty && outpoints.isEmpty && outpointsByP2TRpubkey.isEmpty) {
continue;
}
Uint8List sumOfInputPublicKeys =
bitcoin.getSumInputPubKeys(pubkeys).toCompressedHex().fromHex;
final outpointHash = bitcoin.SilentPayment.hashOutpoints(outpoints);
final result = bitcoin.scanOutputs(
(data["scanPrivateKeyCompressed"] as String).fromHex,
(data["spendPubkeyCompressed"] as String).fromHex,
sumOfInputPublicKeys,
outpointHash,
outpointsByP2TRpubkey.keys.toList());
if (result.isEmpty) {
continue;
}
result.forEach((key, value) {
final outpoint = outpointsByP2TRpubkey[key];
if (outpoint == null) {
return;
}
unspentCoins.add(BitcoinUnspent(
BitcoinAddressRecord(key, index: 0),
outpoint.txid,
outpoint.value!,
outpoint.index,
silentPaymentTweak: value.hex,
));
});
} catch (_) {}
}
sendPort.send(unspentCoins);
// otherwise, finished scanning
scanData.sendPort.send(SyncResponse(currentChainTip, SyncedSyncStatus()));
} catch (e, stacktrace) {
print(stacktrace);
print(e.toString());
// timeout, wait 30sec
sendPort.send(Future.delayed(const Duration(seconds: 30),
() => startRefresh({...data, "checkpoint_tx": checkpointTx ?? ""})));
startRefresh(ScanData.withCheckpoint(scanData, height, checkpointTxPos));
}
}

View file

@ -51,4 +51,6 @@ class ConnectedSyncStatus extends SyncStatus {
class LostConnectionSyncStatus extends SyncStatus {
@override
double progress() => 1.0;
}
@override
String toString() => 'Reconnecting';
}

View file

@ -3,7 +3,9 @@ import 'package:cw_core/sync_status.dart';
String syncStatusTitle(SyncStatus syncStatus) {
if (syncStatus is SyncingSyncStatus) {
return S.current.Blocks_remaining('${syncStatus.blocksLeft}');
return syncStatus.blocksLeft == 1
? S.current.Block_remaining('${syncStatus.blocksLeft}')
: S.current.Blocks_remaining('${syncStatus.blocksLeft}');
}
if (syncStatus is SyncedSyncStatus) {
@ -35,4 +37,4 @@ String syncStatusTitle(SyncStatus syncStatus) {
}
return '';
}
}

View file

@ -194,7 +194,7 @@ void callbackDispatcher() {
outpoint.txid,
outpoint.value!,
outpoint.index,
silentPaymentTweak: value.hex,
silentPaymentTweak: value,
));
});
}
@ -455,7 +455,7 @@ Future<List<BitcoinUnspent>> startRefresh(Map<dynamic, dynamic> data) async {
outpoint.txid,
outpoint.value!,
outpoint.index,
silentPaymentTweak: value.hex,
silentPaymentTweak: value,
));
});
}

View file

@ -120,4 +120,4 @@ void showUnspentCoinsAlert(BuildContext context) {
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
}
}

View file

@ -9,8 +9,8 @@ enum RescanWalletState { rescaning, none }
abstract class RescanViewModelBase with Store {
RescanViewModelBase(this._wallet)
: state = RescanWalletState.none,
isButtonEnabled = false;
: state = RescanWalletState.none,
isButtonEnabled = false;
final WalletBase _wallet;
@ -23,8 +23,9 @@ abstract class RescanViewModelBase with Store {
@action
Future<void> rescanCurrentWallet({required int restoreHeight}) async {
state = RescanWalletState.rescaning;
await _wallet.rescan(height: restoreHeight);
_wallet.rescan(height: restoreHeight);
_wallet.transactionHistory.clear();
state = RescanWalletState.none;
}
}
}

View file

@ -735,5 +735,6 @@
"switchToETHWallet": "ﻯﺮﺧﺃ ﺓﺮﻣ ﺔﻟﻭﺎﺤﻤﻟﺍﻭ Ethereum ﺔﻈﻔﺤﻣ ﻰﻟﺇ ﻞﻳﺪﺒﺘﻟﺍ ﻰﺟﺮﻳ",
"use_testnet": "استخدم testnet",
"address_and_silent_addresses": "العنوان والعناوين الصامتة",
"silent_addresses": "عناوين صامتة"
}
"silent_addresses": "عناوين صامتة",
"Block_remaining": "كتلة ${status} المتبقية"
}

View file

@ -731,5 +731,6 @@
"switchToETHWallet": "Моля, преминете към портфейл Ethereum и опитайте отново",
"use_testnet": "Използвайте TestNet",
"address_and_silent_addresses": "Адрес и мълчаливи адреси",
"silent_addresses": "Безшумни адреси"
}
"silent_addresses": "Безшумни адреси",
"Block_remaining": "${status} останал блок"
}

View file

@ -731,5 +731,6 @@
"switchToETHWallet": "Přejděte na peněženku Ethereum a zkuste to znovu",
"use_testnet": "Použijte testNet",
"address_and_silent_addresses": "Adresa a tiché adresy",
"silent_addresses": "Tiché adresy"
}
"silent_addresses": "Tiché adresy",
"Block_remaining": "${status} Blok zbývající"
}

View file

@ -729,18 +729,16 @@
"awaitDAppProcessing": "Bitte warten Sie, bis die dApp die Verarbeitung abgeschlossen hat.",
"copyWalletConnectLink": "Kopieren Sie den WalletConnect-Link von dApp und fügen Sie ihn hier ein",
"enterWalletConnectURI": "Geben Sie den WalletConnect-URI ein",
"seed_key": "Seed-Schlüssel",
"enter_seed_phrase": "Geben Sie Ihre Seed-Phrase ein",
"seed_key": "Samenschlüssel",
"enter_seed_phrase": "Geben Sie Ihre Samenphrase ein",
"add_contact": "Kontakt hinzufügen",
"exchange_provider_unsupported": "${providerName} wird nicht mehr unterstützt!",
"domain_looks_up": "Domain-Suchen",
"require_for_exchanges_to_external_wallets": "Erforderlich für den Umtausch in externe Wallets",
"camera_permission_is_required": "Eine Kameraerlaubnis ist erforderlich.\nBitte aktivieren Sie es in den App-Einstellungen.",
"switchToETHWallet": "Bitte wechseln Sie zu einem Ethereum-Wallet und versuchen Sie es erneut",
"seed_key": "Samenschlüssel",
"enter_seed_phrase": "Geben Sie Ihre Samenphrase ein",
"add_contact": "Kontakt hinzufügen",
"use_testnet": "TESTNET verwenden",
"address_and_silent_addresses": "Adresse und stille Adressen",
"silent_addresses": "Stille Adressen"
}
"silent_addresses": "Stille Adressen",
"Block_remaining": "${status} Block verbleibend"
}

View file

@ -28,6 +28,7 @@
"failed_authentication": "Failed authentication. ${state_error}",
"wallet_menu": "Menu",
"Blocks_remaining": "${status} Blocks Remaining",
"Block_remaining": "${status} Block Remaining",
"please_try_to_connect_to_another_node": "Please try to connect to another node",
"xmr_hidden": "Hidden",
"xmr_available_balance": "Available Balance",

View file

@ -739,5 +739,6 @@
"switchToETHWallet": "Cambie a una billetera Ethereum e inténtelo nuevamente.",
"use_testnet": "Use TestNet",
"address_and_silent_addresses": "Dirección y direcciones silenciosas",
"silent_addresses": "Direcciones silenciosas"
}
"silent_addresses": "Direcciones silenciosas",
"Block_remaining": "${status} bloque restante"
}

View file

@ -739,5 +739,6 @@
"switchToETHWallet": "Veuillez passer à un portefeuille Ethereum et réessayer",
"use_testnet": "Utiliser TestNet",
"address_and_silent_addresses": "Adresse et adresses silencieuses",
"silent_addresses": "Adresses silencieuses"
}
"silent_addresses": "Adresses silencieuses",
"Block_remaining": "${status} bloc restant"
}

View file

@ -717,5 +717,6 @@
"switchToETHWallet": "Da fatan za a canza zuwa walat ɗin Ethereum kuma a sake gwadawa",
"use_testnet": "Amfani da gwaji",
"address_and_silent_addresses": "Adireshin da adreshin shiru",
"silent_addresses": "Adireshin Shiru"
}
"silent_addresses": "Adireshin Shiru",
"Block_remaining": "${status} toshe ragowar"
}

View file

@ -739,5 +739,6 @@
"switchToETHWallet": "कृपया एथेरियम वॉलेट पर स्विच करें और पुनः प्रयास करें",
"use_testnet": "टेस्टनेट का उपयोग करें",
"address_and_silent_addresses": "पता और मूक पते",
"silent_addresses": "मूक पते"
}
"silent_addresses": "मूक पते",
"Block_remaining": "${status} शेष ब्लॉक"
}

View file

@ -737,5 +737,6 @@
"switchToETHWallet": "Prijeđite na Ethereum novčanik i pokušajte ponovno",
"use_testnet": "Koristite TestNet",
"address_and_silent_addresses": "Adresa i tihe adrese",
"silent_addresses": "Tihe adrese"
}
"silent_addresses": "Tihe adrese",
"Block_remaining": "${status} blok preostaje"
}

View file

@ -727,5 +727,6 @@
"switchToETHWallet": "Silakan beralih ke dompet Ethereum dan coba lagi",
"use_testnet": "Gunakan TestNet",
"address_and_silent_addresses": "Alamat dan alamat diam",
"silent_addresses": "Alamat diam"
}
"silent_addresses": "Alamat diam",
"Block_remaining": "${status} blok tersisa"
}

View file

@ -739,5 +739,6 @@
"switchToETHWallet": "Passa a un portafoglio Ethereum e riprova",
"use_testnet": "Usa TestNet",
"address_and_silent_addresses": "Indirizzo e indirizzi silenziosi",
"silent_addresses": "Indirizzi silenziosi"
}
"silent_addresses": "Indirizzi silenziosi",
"Block_remaining": "${status} blocco rimanente"
}

View file

@ -739,5 +739,6 @@
"switchToETHWallet": "イーサリアムウォレットに切り替えてもう一度お試しください",
"use_testnet": "TestNetを使用します",
"address_and_silent_addresses": "住所とサイレントアドレス",
"silent_addresses": "サイレントアドレス"
}
"silent_addresses": "サイレントアドレス",
"Block_remaining": "${status}ブロックの残り"
}

View file

@ -737,5 +737,6 @@
"switchToETHWallet": "이더리움 지갑으로 전환한 후 다시 시도해 주세요.",
"use_testnet": "TestNet을 사용하십시오",
"address_and_silent_addresses": "주소 및 조용한 주소",
"silent_addresses": "조용한 주소"
}
"silent_addresses": "조용한 주소",
"Block_remaining": "${status} 나머지 블록"
}

View file

@ -737,5 +737,6 @@
"switchToETHWallet": "ကျေးဇူးပြု၍ Ethereum ပိုက်ဆံအိတ်သို့ ပြောင်းပြီး ထပ်စမ်းကြည့်ပါ။",
"use_testnet": "testnet ကိုသုံးပါ",
"address_and_silent_addresses": "လိပ်စာနှင့်အသံတိတ်လိပ်စာများ",
"silent_addresses": "အသံတိတ်လိပ်စာများ"
}
"silent_addresses": "အသံတိတ်လိပ်စာများ",
"Block_remaining": "ကျန်ရှိသော ${status}"
}

View file

@ -739,5 +739,6 @@
"switchToETHWallet": "Schakel over naar een Ethereum-portemonnee en probeer het opnieuw",
"use_testnet": "Gebruik testnet",
"address_and_silent_addresses": "Adres en stille adressen",
"silent_addresses": "Stille adressen"
}
"silent_addresses": "Stille adressen",
"Block_remaining": "${status} blok resterend"
}

View file

@ -739,5 +739,6 @@
"switchToETHWallet": "Przejdź na portfel Ethereum i spróbuj ponownie",
"use_testnet": "Użyj testne",
"address_and_silent_addresses": "Adres i ciche adresy",
"silent_addresses": "Ciche adresy"
}
"silent_addresses": "Ciche adresy",
"Block_remaining": "${status} Block pozostały"
}

View file

@ -738,5 +738,6 @@
"switchToETHWallet": "Mude para uma carteira Ethereum e tente novamente",
"use_testnet": "Use testNet",
"address_and_silent_addresses": "Endereço e endereços silenciosos",
"silent_addresses": "Endereços silenciosos"
}
"silent_addresses": "Endereços silenciosos",
"Block_remaining": "${status} bloco restante"
}

View file

@ -739,5 +739,6 @@
"switchToETHWallet": "Пожалуйста, переключитесь на кошелек Ethereum и повторите попытку.",
"use_testnet": "Используйте Testnet",
"address_and_silent_addresses": "Адрес и молчаливые адреса",
"silent_addresses": "Молчаливые адреса"
}
"silent_addresses": "Молчаливые адреса",
"Block_remaining": "${status} оставшееся блок"
}

View file

@ -737,5 +737,6 @@
"switchToETHWallet": "โปรดเปลี่ยนไปใช้กระเป๋าเงิน Ethereum แล้วลองอีกครั้ง",
"use_testnet": "ใช้ testnet",
"address_and_silent_addresses": "ที่อยู่และที่อยู่เงียบ",
"silent_addresses": "ที่อยู่เงียบ"
}
"silent_addresses": "ที่อยู่เงียบ",
"Block_remaining": "${status} เหลือบล็อกที่เหลืออยู่"
}

View file

@ -734,5 +734,6 @@
"switchToETHWallet": "Mangyaring lumipat sa isang Ethereum wallet at subukang muli",
"use_testnet": "Gumamit ng testnet",
"address_and_silent_addresses": "Address at tahimik na mga address",
"silent_addresses": "Tahimik na mga address"
"silent_addresses": "Tahimik na mga address",
"Block_remaining": "${status} I -block ang natitira"
}

View file

@ -737,5 +737,6 @@
"switchToETHWallet": "Lütfen bir Ethereum cüzdanına geçin ve tekrar deneyin",
"use_testnet": "TestNet kullanın",
"address_and_silent_addresses": "Adres ve sessiz adresler",
"silent_addresses": "Sessiz adresler"
}
"silent_addresses": "Sessiz adresler",
"Block_remaining": "${status} blok kalan blok"
}

View file

@ -739,5 +739,6 @@
"switchToETHWallet": "Перейдіть на гаманець Ethereum і повторіть спробу",
"use_testnet": "Використовуйте тестову мережу",
"address_and_silent_addresses": "Адреса та мовчазні адреси",
"silent_addresses": "Мовчазні адреси"
}
"silent_addresses": "Мовчазні адреси",
"Block_remaining": "${status} блок залишився"
}

View file

@ -731,5 +731,6 @@
"switchToETHWallet": "۔ﮟﯾﺮﮐ ﺶﺷﻮﮐ ﮦﺭﺎﺑﻭﺩ ﺭﻭﺍ ﮟﯾﺮﮐ ﭻﺋﻮﺳ ﺮﭘ ﭧﯿﻟﺍﻭ Ethereum ﻡﺮﮐ ﮦﺍﺮﺑ",
"use_testnet": "ٹیسٹ نیٹ استعمال کریں",
"address_and_silent_addresses": "پتہ اور خاموش پتے",
"silent_addresses": "خاموش پتے"
"silent_addresses": "خاموش پتے",
"Block_remaining": "${status} باقی بلاک"
}

View file

@ -733,5 +733,6 @@
"switchToETHWallet": "Jọwọ yipada si apamọwọ Ethereum ki o tun gbiyanju lẹẹkansi",
"use_testnet": "Lo tele",
"address_and_silent_addresses": "Adirẹsi ati awọn adirẹsi ipalọlọ",
"silent_addresses": "Awọn adirẹsi ipalọlọ"
}
"silent_addresses": "Awọn adirẹsi ipalọlọ",
"Block_remaining": "${status} Bdund díẹ"
}

View file

@ -738,5 +738,6 @@
"switchToETHWallet": "请切换到以太坊钱包并重试",
"use_testnet": "使用TestNet",
"address_and_silent_addresses": "地址和无声地址",
"silent_addresses": "无声地址"
}
"silent_addresses": "无声地址",
"Block_remaining": "${status}块剩余"
}