Sp enhancements (#1672)

* fix: enhance regex, fix multiline

* feat: improve scan msg, fix missing txs, use date api

* feat: node fixes, enhance send modal, TX list tag & filter, refactors

* fix: continuous scanning

* fix: missing close

* fix: resubscribe tweaks

* feat: use mempool api setting toggle

* handle any failure of height API and fallback to the old method [skip ci]

---------

Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
This commit is contained in:
Rafael 2024-09-20 14:24:25 +00:00 committed by GitHub
parent b6bc3df622
commit 3a391f10a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
61 changed files with 872 additions and 531 deletions

View file

@ -24,7 +24,6 @@ import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_bitcoin/exceptions.dart';
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
import 'package:cw_bitcoin/script_hash.dart';
import 'package:cw_bitcoin/utils.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/node.dart';
@ -51,8 +50,6 @@ part 'electrum_wallet.g.dart';
class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet;
const int TWEAKS_COUNT = 25;
abstract class ElectrumWalletBase
extends WalletBase<ElectrumBalance, ElectrumTransactionHistory, ElectrumTransactionInfo>
with Store, WalletKeysFile {
@ -166,12 +163,12 @@ abstract class ElectrumWalletBase
Set<String> get addressesSet => walletAddresses.allAddresses.map((addr) => addr.address).toSet();
List<String> get scriptHashes => walletAddresses.addressesByReceiveType
.map((addr) => scriptHash(addr.address, network: network))
.map((addr) => (addr as BitcoinAddressRecord).getScriptHash(network))
.toList();
List<String> get publicScriptHashes => walletAddresses.allAddresses
.where((addr) => !addr.isHidden)
.map((addr) => scriptHash(addr.address, network: network))
.map((addr) => addr.getScriptHash(network))
.toList();
String get xpub => accountHD.publicKey.toExtended;
@ -212,7 +209,7 @@ abstract class ElectrumWalletBase
silentPaymentsScanningActive = active;
if (active) {
syncStatus = StartingScanSyncStatus();
syncStatus = AttemptingScanSyncStatus();
final tip = await getUpdatedChainTip();
@ -290,12 +287,7 @@ abstract class ElectrumWalletBase
}
@action
Future<void> _setListeners(
int height, {
int? chainTipParam,
bool? doSingleScan,
bool? usingSupportedNode,
}) async {
Future<void> _setListeners(int height, {int? chainTipParam, bool? doSingleScan}) async {
final chainTip = chainTipParam ?? await getUpdatedChainTip();
if (chainTip == height) {
@ -303,7 +295,7 @@ abstract class ElectrumWalletBase
return;
}
syncStatus = StartingScanSyncStatus();
syncStatus = AttemptingScanSyncStatus();
if (_isolate != null) {
final runningIsolate = await _isolate!;
@ -550,7 +542,8 @@ abstract class ElectrumWalletBase
electrumClient.onConnectionStatusChange = _onConnectionStatusChange;
await electrumClient.connectToUri(node.uri, useSSL: node.useSSL);
} catch (e) {
} catch (e, stacktrace) {
print(stacktrace);
print(e.toString());
syncStatus = FailedSyncStatus();
}
@ -592,7 +585,7 @@ abstract class ElectrumWalletBase
allInputsAmount += utx.value;
leftAmount = leftAmount - utx.value;
final address = addressTypeFromStr(utx.address, network);
final address = RegexUtils.addressTypeFromStr(utx.address, network);
ECPrivate? privkey;
bool? isSilentPayment = false;
@ -796,10 +789,11 @@ abstract class ElectrumWalletBase
}
final changeAddress = await walletAddresses.getChangeAddress();
final address = addressTypeFromStr(changeAddress, network);
final address = RegexUtils.addressTypeFromStr(changeAddress, network);
outputs.add(BitcoinOutput(
address: address,
value: BigInt.from(amountLeftForChangeAndFee),
isChange: true,
));
int estimatedSize;
@ -833,8 +827,12 @@ abstract class ElectrumWalletBase
if (!_isBelowDust(amountLeftForChange)) {
// Here, lastOutput already is change, return the amount left without the fee to the user's address.
outputs[outputs.length - 1] =
BitcoinOutput(address: lastOutput.address, value: BigInt.from(amountLeftForChange));
outputs[outputs.length - 1] = BitcoinOutput(
address: lastOutput.address,
value: BigInt.from(amountLeftForChange),
isSilentPayment: lastOutput.isSilentPayment,
isChange: true,
);
} else {
// If has change that is lower than dust, will end up with tx rejected by network rules, so estimate again without the added change
outputs.removeLast();
@ -938,18 +936,27 @@ abstract class ElectrumWalletBase
credentialsAmount += outputAmount;
final address =
addressTypeFromStr(out.isParsedAddress ? out.extractedAddress! : out.address, network);
final address = RegexUtils.addressTypeFromStr(
out.isParsedAddress ? out.extractedAddress! : out.address, network);
final isSilentPayment = address is SilentPaymentAddress;
if (address is SilentPaymentAddress) {
if (isSilentPayment) {
hasSilentPayment = true;
}
if (sendAll) {
// The value will be changed after estimating the Tx size and deducting the fee from the total to be sent
outputs.add(BitcoinOutput(address: address, value: BigInt.from(0)));
outputs.add(BitcoinOutput(
address: address,
value: BigInt.from(0),
isSilentPayment: isSilentPayment,
));
} else {
outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount)));
outputs.add(BitcoinOutput(
address: address,
value: BigInt.from(outputAmount),
isSilentPayment: isSilentPayment,
));
}
}
@ -1089,7 +1096,8 @@ abstract class ElectrumWalletBase
});
}
unspentCoins.removeWhere((utxo) => estimatedTx.utxos.any((e) => e.utxo.txHash == utxo.hash));
unspentCoins
.removeWhere((utxo) => estimatedTx.utxos.any((e) => e.utxo.txHash == utxo.hash));
await updateBalance();
});
@ -1237,12 +1245,7 @@ abstract class ElectrumWalletBase
@action
@override
Future<void> rescan({
required int height,
int? chainTip,
ScanData? scanData,
bool? doSingleScan,
}) async {
Future<void> rescan({required int height, bool? doSingleScan}) async {
silentPaymentsScanningActive = true;
_setListeners(height, doSingleScan: doSingleScan);
}
@ -1460,7 +1463,7 @@ abstract class ElectrumWalletBase
final addressRecord =
walletAddresses.allAddresses.firstWhere((element) => element.address == address);
final btcAddress = addressTypeFromStr(addressRecord.address, network);
final btcAddress = RegexUtils.addressTypeFromStr(addressRecord.address, network);
final privkey = generateECPrivate(
hd: addressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
index: addressRecord.index,
@ -1501,7 +1504,7 @@ abstract class ElectrumWalletBase
}
final address = addressFromOutputScript(out.scriptPubKey, network);
final btcAddress = addressTypeFromStr(address, network);
final btcAddress = RegexUtils.addressTypeFromStr(address, network);
outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(out.amount.toInt())));
}
@ -1597,8 +1600,6 @@ abstract class ElectrumWalletBase
Future<ElectrumTransactionBundle> getTransactionExpanded(
{required String hash, int? height}) async {
String transactionHex;
// TODO: time is not always available, and calculating it from height is not always accurate.
// Add settings to choose API provider and use and http server instead of electrum for this.
int? time;
int? confirmations;
@ -1606,6 +1607,29 @@ abstract class ElectrumWalletBase
if (verboseTransaction.isEmpty) {
transactionHex = await electrumClient.getTransactionHex(hash: hash);
if (height != null && await checkIfMempoolAPIIsEnabled()) {
final blockHash = await http.get(
Uri.parse(
"http://mempool.cakewallet.com:8999/api/v1/block-height/$height",
),
);
if (blockHash.statusCode == 200 &&
blockHash.body.isNotEmpty &&
jsonDecode(blockHash.body) != null) {
final blockResponse = await http.get(
Uri.parse(
"http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}",
),
);
if (blockResponse.statusCode == 200 &&
blockResponse.body.isNotEmpty &&
jsonDecode(blockResponse.body)['timestamp'] != null) {
time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString());
}
}
}
} else {
transactionHex = verboseTransaction['hex'] as String;
time = verboseTransaction['time'] as int?;
@ -1860,7 +1884,7 @@ abstract class ElectrumWalletBase
final balanceFutures = <Future<Map<String, dynamic>>>[];
for (var i = 0; i < addresses.length; i++) {
final addressRecord = addresses[i];
final sh = scriptHash(addressRecord.address, network: network);
final sh = addressRecord.getScriptHash(network);
final balanceFuture = electrumClient.getBalance(sh);
balanceFutures.add(balanceFuture);
}
@ -1900,7 +1924,10 @@ abstract class ElectrumWalletBase
}
return ElectrumBalance(
confirmed: totalConfirmed, unconfirmed: totalUnconfirmed, frozen: totalFrozen);
confirmed: totalConfirmed,
unconfirmed: totalUnconfirmed,
frozen: totalFrozen,
);
}
Future<void> updateBalance() async {
@ -1968,7 +1995,7 @@ abstract class ElectrumWalletBase
List<int> possibleRecoverIds = [0, 1];
final baseAddress = addressTypeFromStr(address, network);
final baseAddress = RegexUtils.addressTypeFromStr(address, network);
for (int recoveryId in possibleRecoverIds) {
final pubKey = sig.recoverPublicKey(messageHash, Curves.generatorSecp256k1, recoveryId);
@ -2061,7 +2088,8 @@ abstract class ElectrumWalletBase
_isTryingToConnect = true;
Timer(Duration(seconds: 5), () {
if (this.syncStatus is NotConnectedSyncStatus || this.syncStatus is LostConnectionSyncStatus) {
if (this.syncStatus is NotConnectedSyncStatus ||
this.syncStatus is LostConnectionSyncStatus) {
this.electrumClient.connectToUri(
node!.uri,
useSSL: node!.useSSL ?? false,
@ -2192,21 +2220,22 @@ Future<void> startRefresh(ScanData scanData) async {
BehaviorSubject<Object>? tweaksSubscription = null;
final syncingStatus = scanData.isSingleScan
? SyncingSyncStatus(1, 0)
: SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight);
// Initial status UI update, send how many blocks left to scan
scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus));
final electrumClient = scanData.electrumClient;
await electrumClient.connectToUri(
scanData.node?.uri ?? Uri.parse("tcp://electrs.cakewallet.com:50001"),
useSSL: scanData.node?.useSSL ?? false,
);
int getCountPerRequest(int syncHeight) {
if (scanData.isSingleScan) {
return 1;
}
final amountLeft = scanData.chainTip - syncHeight + 1;
return amountLeft;
}
if (tweaksSubscription == null) {
final count = scanData.isSingleScan ? 1 : TWEAKS_COUNT;
final receiver = Receiver(
scanData.silentAddress.b_scan.toHex(),
scanData.silentAddress.B_spend.toHex(),
@ -2215,16 +2244,45 @@ Future<void> startRefresh(ScanData scanData) async {
scanData.labelIndexes.length,
);
tweaksSubscription = await electrumClient.tweaksSubscribe(height: syncHeight, count: count);
tweaksSubscription?.listen((t) async {
final tweaks = t as Map<String, dynamic>;
// Initial status UI update, send how many blocks in total to scan
final initialCount = getCountPerRequest(syncHeight);
scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight)));
if (tweaks["message"] != null) {
tweaksSubscription = await electrumClient.tweaksSubscribe(
height: syncHeight,
count: initialCount,
);
Future<void> listenFn(t) async {
final tweaks = t as Map<String, dynamic>;
final msg = tweaks["message"];
// success or error msg
final noData = msg != null;
if (noData) {
// re-subscribe to continue receiving messages, starting from the next unscanned height
electrumClient.tweaksSubscribe(height: syncHeight + 1, count: count);
final nextHeight = syncHeight + 1;
final nextCount = getCountPerRequest(nextHeight);
if (nextCount > 0) {
tweaksSubscription?.close();
final nextTweaksSubscription = electrumClient.tweaksSubscribe(
height: nextHeight,
count: nextCount,
);
nextTweaksSubscription?.listen(listenFn);
}
return;
}
// Continuous status UI update, send how many blocks left to scan
final syncingStatus = scanData.isSingleScan
? SyncingSyncStatus(1, 0)
: SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight);
scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus));
final blockHeight = tweaks.keys.first;
final tweakHeight = int.parse(blockHeight);
@ -2264,6 +2322,7 @@ Future<void> startRefresh(ScanData scanData) async {
: DateTime.now(),
confirmations: scanData.chainTip - tweakHeight + 1,
unspents: [],
isReceivedSilentPayment: true,
);
addToWallet.forEach((label, value) {
@ -2318,16 +2377,6 @@ Future<void> startRefresh(ScanData scanData) async {
} catch (_) {}
syncHeight = tweakHeight;
scanData.sendPort.send(
SyncResponse(
syncHeight,
SyncingSyncStatus.fromHeightValues(
scanData.chainTip,
initialSyncHeight,
syncHeight,
),
),
);
if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) {
if (tweakHeight >= scanData.chainTip)
@ -2343,7 +2392,9 @@ Future<void> startRefresh(ScanData scanData) async {
await tweaksSubscription!.close();
await electrumClient.close();
}
});
}
tweaksSubscription?.listen(listenFn);
}
if (tweaksSubscription == null) {
@ -2373,6 +2424,7 @@ class EstimatedTxResult {
final int fee;
final int amount;
final bool spendsSilentPayment;
// final bool sendsToSilentPayment;
final bool hasChange;
final bool isSendAll;
final String? memo;
@ -2386,31 +2438,6 @@ class PublicKeyWithDerivationPath {
final String publicKey;
}
BitcoinBaseAddress addressTypeFromStr(String address, BasedUtxoNetwork network) {
if (network is BitcoinCashNetwork) {
if (!address.startsWith("bitcoincash:") &&
(address.startsWith("q") || address.startsWith("p"))) {
address = "bitcoincash:$address";
}
return BitcoinCashAddress(address).baseAddress;
}
if (P2pkhAddress.regex.hasMatch(address)) {
return P2pkhAddress.fromAddress(address: address, network: network);
} else if (P2shAddress.regex.hasMatch(address)) {
return P2shAddress.fromAddress(address: address, network: network);
} else if (P2wshAddress.regex.hasMatch(address)) {
return P2wshAddress.fromAddress(address: address, network: network);
} else if (P2trAddress.regex.hasMatch(address)) {
return P2trAddress.fromAddress(address: address, network: network);
} else if (SilentPaymentAddress.regex.hasMatch(address)) {
return SilentPaymentAddress.fromAddress(address);
} else {
return P2wpkhAddress.fromAddress(address: address, network: network);
}
}
BitcoinAddressType _getScriptType(BitcoinBaseAddress type) {
if (type is P2pkhAddress) {
return P2pkhAddressType.p2pkh;