mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-06-28 20:39:51 +00:00
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:
parent
b6bc3df622
commit
3a391f10a3
61 changed files with 872 additions and 531 deletions
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue