feat: restore & scan imp

This commit is contained in:
Rafael Saes 2024-11-22 21:29:29 -03:00
parent 9bb3d9f35b
commit c35dec0d09
13 changed files with 448 additions and 172 deletions

View file

@ -108,6 +108,53 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
int initialSilentAddressIndex = 0, int initialSilentAddressIndex = 0,
required bool mempoolAPIEnabled, required bool mempoolAPIEnabled,
}) async { }) async {
List<int>? seedBytes = null;
final Map<CWBitcoinDerivationType, Bip32Slip10Secp256k1> hdWallets = {};
if (walletInfo.isRecovery) {
for (final derivation in walletInfo.derivations ?? <DerivationInfo>[]) {
if (derivation.description?.contains("SP") ?? false) {
continue;
}
if (derivation.derivationType == DerivationType.bip39) {
seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase);
hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
break;
} else {
try {
seedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase);
hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
} catch (e) {
print("electrum_v2 seed error: $e");
try {
seedBytes = ElectrumV1SeedGenerator(mnemonic).generate();
hdWallets[CWBitcoinDerivationType.electrum] =
Bip32Slip10Secp256k1.fromSeed(seedBytes);
} catch (e) {
print("electrum_v1 seed error: $e");
}
}
break;
}
}
if (hdWallets[CWBitcoinDerivationType.bip39] != null) {
hdWallets[CWBitcoinDerivationType.old_bip39] = hdWallets[CWBitcoinDerivationType.bip39]!;
}
if (hdWallets[CWBitcoinDerivationType.electrum] != null) {
hdWallets[CWBitcoinDerivationType.old_electrum] =
hdWallets[CWBitcoinDerivationType.electrum]!;
}
} else {
seedBytes = walletInfo.derivationInfo?.derivationType == DerivationType.electrum
? ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase)
: Bip39SeedGenerator.generateFromString(mnemonic, passphrase);
}
return BitcoinWallet( return BitcoinWallet(
mnemonic: mnemonic, mnemonic: mnemonic,
passphrase: passphrase ?? "", passphrase: passphrase ?? "",
@ -119,9 +166,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
initialSilentAddressIndex: initialSilentAddressIndex, initialSilentAddressIndex: initialSilentAddressIndex,
initialBalance: initialBalance, initialBalance: initialBalance,
encryptionFileUtils: encryptionFileUtils, encryptionFileUtils: encryptionFileUtils,
seedBytes: walletInfo.derivationInfo?.derivationType == DerivationType.electrum seedBytes: seedBytes,
? ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase) hdWallets: hdWallets,
: Bip39SeedGenerator.generateFromString(mnemonic, passphrase),
initialRegularAddressIndex: initialRegularAddressIndex, initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex,
addressPageType: addressPageType, addressPageType: addressPageType,
@ -253,9 +299,13 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
} }
Future<bool> getNodeIsElectrs() async { Future<bool> getNodeIsElectrs() async {
final version = await sendWorker(ElectrumWorkerGetVersionRequest()) as List<String>; if (node?.uri.host.contains("electrs") ?? false) {
return true;
}
if (version.isNotEmpty) { final version = await sendWorker(ElectrumWorkerGetVersionRequest());
if (version is List<String> && version.isNotEmpty) {
final server = version[0]; final server = version[0];
if (server.toLowerCase().contains('electrs')) { if (server.toLowerCase().contains('electrs')) {
@ -263,6 +313,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
node!.save(); node!.save();
return node!.isElectrs!; return node!.isElectrs!;
} }
} else if (version is String && version.toLowerCase().contains('electrs')) {
node!.isElectrs = true;
node!.save();
return node!.isElectrs!;
} }
node!.isElectrs = false; node!.isElectrs = false;
@ -271,33 +325,39 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
} }
Future<bool> getNodeSupportsSilentPayments() async { Future<bool> getNodeSupportsSilentPayments() async {
return true; // TODO: handle disconnection on check
// TODO: use cached values
if (node == null) {
return false;
}
final isFulcrum = node!.uri.host.contains("fulcrum");
if (isFulcrum) {
return false;
}
// As of today (august 2024), only ElectrumRS supports silent payments // As of today (august 2024), only ElectrumRS supports silent payments
// if (!(await getNodeIsElectrs())) { if (!(await getNodeIsElectrs())) {
// return false; return false;
// } }
// if (node == null) { try {
// return false; final workerResponse = (await sendWorker(ElectrumWorkerCheckTweaksRequest())) as String;
// } final tweaksResponse = ElectrumWorkerCheckTweaksResponse.fromJson(
json.decode(workerResponse) as Map<String, dynamic>,
);
final supportsScanning = tweaksResponse.result == true;
// try { if (supportsScanning) {
// final tweaksResponse = await electrumClient.getTweaks(height: 0); node!.supportsSilentPayments = true;
node!.save();
return node!.supportsSilentPayments!;
}
} catch (_) {}
// if (tweaksResponse != null) { node!.supportsSilentPayments = false;
// node!.supportsSilentPayments = true; node!.save();
// node!.save(); return node!.supportsSilentPayments!;
// return node!.supportsSilentPayments!;
// }
// } on RequestFailedTimeoutException catch (_) {
// node!.supportsSilentPayments = false;
// node!.save();
// return node!.supportsSilentPayments!;
// } catch (_) {}
// node!.supportsSilentPayments = false;
// node!.save();
// return node!.supportsSilentPayments!;
} }
LedgerConnection? _ledgerConnection; LedgerConnection? _ledgerConnection;
@ -383,16 +443,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
if (tip > walletInfo.restoreHeight) { if (tip > walletInfo.restoreHeight) {
_setListeners(walletInfo.restoreHeight); _setListeners(walletInfo.restoreHeight);
} }
} else { } else if (syncStatus is! SyncedSyncStatus) {
alwaysScan = false; await sendWorker(ElectrumWorkerStopScanningRequest());
await startSync();
// _isolate?.then((value) => value.kill(priority: Isolate.immediate));
// if (rpc!.isConnected) {
// syncStatus = SyncedSyncStatus();
// } else {
// syncStatus = NotConnectedSyncStatus();
// }
} }
} }
@ -565,9 +618,16 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
messageJson = message as Map<String, dynamic>; messageJson = message as Map<String, dynamic>;
} }
final workerMethod = messageJson['method'] as String; final workerMethod = messageJson['method'] as String;
final workerError = messageJson['error'] as String?;
switch (workerMethod) { switch (workerMethod) {
case ElectrumRequestMethods.tweaksSubscribeMethod: case ElectrumRequestMethods.tweaksSubscribeMethod:
if (workerError != null) {
print(messageJson);
// _onConnectionStatusChange(ConnectionStatus.failed);
break;
}
final response = ElectrumWorkerTweaksSubscribeResponse.fromJson(messageJson); final response = ElectrumWorkerTweaksSubscribeResponse.fromJson(messageJson);
onTweaksSyncResponse(response.result); onTweaksSyncResponse(response.result);
break; break;
@ -651,9 +711,16 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
syncStatus = SyncingSyncStatus(newSyncStatus.blocksLeft, newSyncStatus.ptc); syncStatus = SyncingSyncStatus(newSyncStatus.blocksLeft, newSyncStatus.ptc);
} else { } else {
syncStatus = newSyncStatus; syncStatus = newSyncStatus;
if (newSyncStatus is SyncedSyncStatus) {
silentPaymentsScanningActive = false;
}
} }
await walletInfo.updateRestoreHeight(result.height!); final height = result.height;
if (height != null) {
await walletInfo.updateRestoreHeight(height);
}
} }
await save(); await save();
@ -801,6 +868,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
case SyncingSyncStatus: case SyncingSyncStatus:
return; return;
case SyncedTipSyncStatus: case SyncedTipSyncStatus:
silentPaymentsScanningActive = false;
// Message is shown on the UI for 3 seconds, then reverted to synced // Message is shown on the UI for 3 seconds, then reverted to synced
Timer(Duration(seconds: 3), () { Timer(Duration(seconds: 3), () {
if (this.syncStatus is SyncedTipSyncStatus) this.syncStatus = SyncedSyncStatus(); if (this.syncStatus is SyncedTipSyncStatus) this.syncStatus = SyncedSyncStatus();

View file

@ -361,6 +361,15 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
return labels; return labels;
} }
@override
@action
void updateHiddenAddresses() {
super.updateHiddenAddresses();
this.hiddenAddresses.addAll(silentPaymentAddresses
.where((addressRecord) => addressRecord.isHidden)
.map((addressRecord) => addressRecord.address));
}
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = super.toJson(); final json = super.toJson();
json['silentPaymentAddresses'] = json['silentPaymentAddresses'] =

View file

@ -125,7 +125,7 @@ abstract class ElectrumWalletBase
workerSendPort!.send(json); workerSendPort!.send(json);
try { try {
return completer.future.timeout(Duration(seconds: 5)); return completer.future.timeout(Duration(seconds: 30));
} catch (e) { } catch (e) {
_errorCompleters.addAll({messageId: e}); _errorCompleters.addAll({messageId: e});
_responseCompleters.remove(messageId); _responseCompleters.remove(messageId);
@ -146,13 +146,8 @@ abstract class ElectrumWalletBase
final workerMethod = messageJson['method'] as String; final workerMethod = messageJson['method'] as String;
final workerError = messageJson['error'] as String?; final workerError = messageJson['error'] as String?;
if (workerError != null) {
print('Worker error: $workerError');
return;
}
final responseId = messageJson['id'] as int?; final responseId = messageJson['id'] as int?;
if (responseId != null && _responseCompleters.containsKey(responseId)) { if (responseId != null && _responseCompleters.containsKey(responseId)) {
_responseCompleters[responseId]!.complete(message); _responseCompleters[responseId]!.complete(message);
_responseCompleters.remove(responseId); _responseCompleters.remove(responseId);
@ -160,6 +155,11 @@ abstract class ElectrumWalletBase
switch (workerMethod) { switch (workerMethod) {
case ElectrumWorkerMethods.connectionMethod: case ElectrumWorkerMethods.connectionMethod:
if (workerError != null) {
_onConnectionStatusChange(ConnectionStatus.failed);
break;
}
final response = ElectrumWorkerConnectionResponse.fromJson(messageJson); final response = ElectrumWorkerConnectionResponse.fromJson(messageJson);
_onConnectionStatusChange(response.result); _onConnectionStatusChange(response.result);
break; break;
@ -214,6 +214,7 @@ abstract class ElectrumWalletBase
bool? alwaysScan; bool? alwaysScan;
bool mempoolAPIEnabled; bool mempoolAPIEnabled;
bool _updatingHistories = false;
final Map<CWBitcoinDerivationType, Bip32Slip10Secp256k1> hdWallets; final Map<CWBitcoinDerivationType, Bip32Slip10Secp256k1> hdWallets;
Bip32Slip10Secp256k1 get bip32 => walletAddresses.hdWallet; Bip32Slip10Secp256k1 get bip32 => walletAddresses.hdWallet;
@ -323,7 +324,8 @@ abstract class ElectrumWalletBase
List<String> scripthashesListening; List<String> scripthashesListening;
bool _chainTipListenerOn = false; bool _chainTipListenerOn = false;
bool _isInitialSync = true; // TODO: improve this
int _syncedTimes = 0;
void Function(FlutterErrorDetails)? _onError; void Function(FlutterErrorDetails)? _onError;
Timer? _autoSaveTimer; Timer? _autoSaveTimer;
@ -348,9 +350,11 @@ abstract class ElectrumWalletBase
syncStatus = SynchronizingSyncStatus(); syncStatus = SynchronizingSyncStatus();
// INFO: FIRST: Call subscribe for headers, wait for completion to update currentChainTip (needed for other methods) // INFO: FIRST (always): Call subscribe for headers, wait for completion to update currentChainTip (needed for other methods)
await sendWorker(ElectrumWorkerHeadersSubscribeRequest()); await sendWorker(ElectrumWorkerHeadersSubscribeRequest());
_syncedTimes = 0;
// INFO: SECOND: Start loading transaction histories for every address, this will help discover addresses until the unused gap limit has been reached, which will help finding the full balance and unspents next // INFO: SECOND: Start loading transaction histories for every address, this will help discover addresses until the unused gap limit has been reached, which will help finding the full balance and unspents next
await updateTransactions(); await updateTransactions();
@ -365,13 +369,14 @@ abstract class ElectrumWalletBase
_updateFeeRateTimer ??= _updateFeeRateTimer ??=
Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates()); Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates());
_isInitialSync = false; if (_syncedTimes == 3) {
syncStatus = SyncedSyncStatus(); syncStatus = SyncedSyncStatus();
}
await save(); await save();
} catch (e, stacktrace) { } catch (e, stacktrace) {
print(stacktrace);
print("startSync $e"); print("startSync $e");
print(stacktrace);
syncStatus = FailedSyncStatus(); syncStatus = FailedSyncStatus();
} }
} }
@ -389,8 +394,10 @@ abstract class ElectrumWalletBase
} }
@action @action
Future<void> onFeesResponse(TransactionPriorities result) async { Future<void> onFeesResponse(TransactionPriorities? result) async {
feeRates = result; if (result != null) {
feeRates = result;
}
} }
Node? node; Node? node;
@ -400,8 +407,6 @@ abstract class ElectrumWalletBase
Future<void> connectToNode({required Node node}) async { Future<void> connectToNode({required Node node}) async {
this.node = node; this.node = node;
if (syncStatus is ConnectingSyncStatus) return;
try { try {
syncStatus = ConnectingSyncStatus(); syncStatus = ConnectingSyncStatus();
@ -416,6 +421,7 @@ abstract class ElectrumWalletBase
_workerIsolate = await Isolate.spawn<SendPort>(ElectrumWorker.run, receivePort!.sendPort); _workerIsolate = await Isolate.spawn<SendPort>(ElectrumWorker.run, receivePort!.sendPort);
_workerSubscription = receivePort!.listen((message) { _workerSubscription = receivePort!.listen((message) {
print('Main: received message: $message');
if (message is SendPort) { if (message is SendPort) {
workerSendPort = message; workerSendPort = message;
workerSendPort!.send( workerSendPort!.send(
@ -1159,15 +1165,11 @@ abstract class ElectrumWalletBase
@action @action
Future<void> updateAllUnspents() async { Future<void> updateAllUnspents() async {
final req = ElectrumWorkerListUnspentRequest( workerSendPort!.send(
scripthashes: walletAddresses.allScriptHashes.toList(), ElectrumWorkerListUnspentRequest(
scripthashes: walletAddresses.allScriptHashes.toList(),
).toJson(),
); );
if (_isInitialSync) {
await sendWorker(req);
} else {
workerSendPort!.send(req.toJson());
}
} }
@action @action
@ -1222,6 +1224,11 @@ abstract class ElectrumWalletBase
unspentCoins.forEach(updateCoin); unspentCoins.forEach(updateCoin);
await refreshUnspentCoinsInfo(); await refreshUnspentCoinsInfo();
_syncedTimes++;
if (_syncedTimes == 3) {
syncStatus = SyncedSyncStatus();
}
} }
@action @action
@ -1299,10 +1306,13 @@ abstract class ElectrumWalletBase
@action @action
Future<void> onHistoriesResponse(List<AddressHistoriesResponse> histories) async { Future<void> onHistoriesResponse(List<AddressHistoriesResponse> histories) async {
if (histories.isEmpty) { if (histories.isEmpty || _updatingHistories) {
_updatingHistories = false;
return; return;
} }
_updatingHistories = true;
final addressesWithHistory = <BitcoinAddressRecord>[]; final addressesWithHistory = <BitcoinAddressRecord>[];
BitcoinAddressType? lastDiscoveredType; BitcoinAddressType? lastDiscoveredType;
@ -1340,7 +1350,13 @@ abstract class ElectrumWalletBase
isChange: isChange, isChange: isChange,
derivationType: addressRecord.derivationType, derivationType: addressRecord.derivationType,
addressType: addressRecord.addressType, addressType: addressRecord.addressType,
derivationInfo: BitcoinAddressUtils.getDerivationFromType(addressRecord.addressType), derivationInfo: BitcoinAddressUtils.getDerivationFromType(
addressRecord.addressType,
isElectrum: [
CWBitcoinDerivationType.electrum,
CWBitcoinDerivationType.old_electrum,
].contains(addressRecord.derivationType),
),
); );
final newAddressList = final newAddressList =
@ -1364,6 +1380,12 @@ abstract class ElectrumWalletBase
} }
walletAddresses.updateHiddenAddresses(); walletAddresses.updateHiddenAddresses();
_updatingHistories = false;
_syncedTimes++;
if (_syncedTimes == 3) {
syncStatus = SyncedSyncStatus();
}
} }
Future<String?> canReplaceByFee(ElectrumTransactionInfo tx) async { Future<String?> canReplaceByFee(ElectrumTransactionInfo tx) async {
@ -1606,10 +1628,8 @@ abstract class ElectrumWalletBase
@action @action
Future<void> updateTransactions([List<BitcoinAddressRecord>? addresses]) async { Future<void> updateTransactions([List<BitcoinAddressRecord>? addresses]) async {
addresses ??= walletAddresses.allAddresses.toList(); workerSendPort!.send(ElectrumWorkerGetHistoryRequest(
addresses: walletAddresses.allAddresses.toList(),
final req = ElectrumWorkerGetHistoryRequest(
addresses: addresses,
storedTxs: transactionHistory.transactions.values.toList(), storedTxs: transactionHistory.transactions.values.toList(),
walletType: type, walletType: type,
// If we still don't have currentChainTip, txs will still be fetched but shown // If we still don't have currentChainTip, txs will still be fetched but shown
@ -1617,13 +1637,7 @@ abstract class ElectrumWalletBase
chainTip: currentChainTip ?? getBitcoinHeightByDate(date: DateTime.now()), chainTip: currentChainTip ?? getBitcoinHeightByDate(date: DateTime.now()),
network: network, network: network,
mempoolAPIEnabled: mempoolAPIEnabled, mempoolAPIEnabled: mempoolAPIEnabled,
); ).toJson());
if (_isInitialSync) {
await sendWorker(req);
} else {
workerSendPort!.send(req.toJson());
}
} }
@action @action
@ -1663,17 +1677,18 @@ abstract class ElectrumWalletBase
unconfirmed: totalUnconfirmed, unconfirmed: totalUnconfirmed,
frozen: totalFrozen, frozen: totalFrozen,
); );
_syncedTimes++;
if (_syncedTimes == 3) {
syncStatus = SyncedSyncStatus();
}
} }
@action @action
Future<void> updateBalance() async { Future<void> updateBalance() async {
final req = ElectrumWorkerGetBalanceRequest(scripthashes: walletAddresses.allScriptHashes); workerSendPort!.send(ElectrumWorkerGetBalanceRequest(
scripthashes: walletAddresses.allScriptHashes,
if (_isInitialSync) { ).toJson());
await sendWorker(req);
} else {
workerSendPort!.send(req.toJson());
}
} }
@override @override

View file

@ -23,6 +23,8 @@ class ElectrumWorker {
final SendPort sendPort; final SendPort sendPort;
ElectrumApiProvider? _electrumClient; ElectrumApiProvider? _electrumClient;
BasedUtxoNetwork? _network; BasedUtxoNetwork? _network;
bool _isScanning = false;
bool _stopScanRequested = false;
ElectrumWorker._(this.sendPort, {ElectrumApiProvider? electrumClient}) ElectrumWorker._(this.sendPort, {ElectrumApiProvider? electrumClient})
: _electrumClient = electrumClient; : _electrumClient = electrumClient;
@ -45,8 +47,6 @@ class ElectrumWorker {
} }
void handleMessage(dynamic message) async { void handleMessage(dynamic message) async {
print("Worker: received message: $message");
try { try {
Map<String, dynamic> messageJson; Map<String, dynamic> messageJson;
if (message is String) { if (message is String) {
@ -97,10 +97,35 @@ class ElectrumWorker {
ElectrumWorkerBroadcastRequest.fromJson(messageJson), ElectrumWorkerBroadcastRequest.fromJson(messageJson),
); );
break; break;
case ElectrumRequestMethods.tweaksSubscribeMethod: case ElectrumWorkerMethods.checkTweaksMethod:
await _handleScanSilentPayments( await _handleCheckTweaks(
ElectrumWorkerTweaksSubscribeRequest.fromJson(messageJson), ElectrumWorkerCheckTweaksRequest.fromJson(messageJson),
); );
break;
case ElectrumWorkerMethods.stopScanningMethod:
await _handleStopScanning(
ElectrumWorkerStopScanningRequest.fromJson(messageJson),
);
break;
case ElectrumRequestMethods.estimateFeeMethod:
case ElectrumRequestMethods.tweaksSubscribeMethod:
if (_isScanning) {
_stopScanRequested = false;
}
if (!_stopScanRequested) {
await _handleScanSilentPayments(
ElectrumWorkerTweaksSubscribeRequest.fromJson(messageJson),
);
} else {
_stopScanRequested = false;
_sendResponse(
ElectrumWorkerTweaksSubscribeResponse(
result: TweaksSyncResponse(syncStatus: SyncedSyncStatus()),
),
);
}
break; break;
case ElectrumRequestMethods.estimateFeeMethod: case ElectrumRequestMethods.estimateFeeMethod:
await _handleGetFeeRates( await _handleGetFeeRates(
@ -113,8 +138,7 @@ class ElectrumWorker {
); );
break; break;
} }
} catch (e, s) { } catch (e) {
print(s);
_sendError(ElectrumWorkerErrorResponse(error: e.toString())); _sendError(ElectrumWorkerErrorResponse(error: e.toString()));
} }
} }
@ -122,25 +146,29 @@ class ElectrumWorker {
Future<void> _handleConnect(ElectrumWorkerConnectionRequest request) async { Future<void> _handleConnect(ElectrumWorkerConnectionRequest request) async {
_network = request.network; _network = request.network;
_electrumClient = await ElectrumApiProvider.connect( try {
request.useSSL _electrumClient = await ElectrumApiProvider.connect(
? ElectrumSSLService.connect( request.useSSL
request.uri, ? ElectrumSSLService.connect(
onConnectionStatusChange: (status) { request.uri,
_sendResponse(ElectrumWorkerConnectionResponse(status: status, id: request.id)); onConnectionStatusChange: (status) {
}, _sendResponse(
defaultRequestTimeOut: const Duration(seconds: 5), ElectrumWorkerConnectionResponse(status: status, id: request.id),
connectionTimeOut: const Duration(seconds: 5), );
) },
: ElectrumTCPService.connect( )
request.uri, : ElectrumTCPService.connect(
onConnectionStatusChange: (status) { request.uri,
_sendResponse(ElectrumWorkerConnectionResponse(status: status, id: request.id)); onConnectionStatusChange: (status) {
}, _sendResponse(
defaultRequestTimeOut: const Duration(seconds: 5), ElectrumWorkerConnectionResponse(status: status, id: request.id),
connectionTimeOut: const Duration(seconds: 5), );
), },
); ),
);
} catch (e) {
_sendError(ElectrumWorkerConnectionError(error: e.toString()));
}
} }
Future<void> _handleHeadersSubscribe(ElectrumWorkerHeadersSubscribeRequest request) async { Future<void> _handleHeadersSubscribe(ElectrumWorkerHeadersSubscribeRequest request) async {
@ -230,6 +258,7 @@ class ElectrumWorker {
hash: txid, hash: txid,
currentChainTip: result.chainTip, currentChainTip: result.chainTip,
mempoolAPIEnabled: result.mempoolAPIEnabled, mempoolAPIEnabled: result.mempoolAPIEnabled,
getTime: true,
confirmations: tx?.confirmations, confirmations: tx?.confirmations,
date: tx?.date, date: tx?.date,
), ),
@ -367,6 +396,7 @@ class ElectrumWorker {
required String hash, required String hash,
required int currentChainTip, required int currentChainTip,
required bool mempoolAPIEnabled, required bool mempoolAPIEnabled,
bool getTime = false,
int? confirmations, int? confirmations,
DateTime? date, DateTime? date,
}) async { }) async {
@ -378,52 +408,54 @@ class ElectrumWorker {
ElectrumGetTransactionHex(transactionHash: hash), ElectrumGetTransactionHex(transactionHash: hash),
); );
if (mempoolAPIEnabled) { if (getTime) {
try { if (mempoolAPIEnabled) {
final txVerbose = await http.get( try {
Uri.parse( final txVerbose = await http.get(
"http://mempool.cakewallet.com:8999/api/v1/tx/$hash/status",
),
);
if (txVerbose.statusCode == 200 &&
txVerbose.body.isNotEmpty &&
jsonDecode(txVerbose.body) != null) {
height = jsonDecode(txVerbose.body)['block_height'] as int;
final blockHash = await http.get(
Uri.parse( Uri.parse(
"http://mempool.cakewallet.com:8999/api/v1/block-height/$height", "http://mempool.cakewallet.com:8999/api/v1/tx/$hash/status",
), ),
); );
if (blockHash.statusCode == 200 && blockHash.body.isNotEmpty) { if (txVerbose.statusCode == 200 &&
final blockResponse = await http.get( txVerbose.body.isNotEmpty &&
jsonDecode(txVerbose.body) != null) {
height = jsonDecode(txVerbose.body)['block_height'] as int;
final blockHash = await http.get(
Uri.parse( Uri.parse(
"http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}", "http://mempool.cakewallet.com:8999/api/v1/block-height/$height",
), ),
); );
if (blockResponse.statusCode == 200 && if (blockHash.statusCode == 200 && blockHash.body.isNotEmpty) {
blockResponse.body.isNotEmpty && final blockResponse = await http.get(
jsonDecode(blockResponse.body)['timestamp'] != null) { Uri.parse(
time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString()); "http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}",
),
);
if (date != null) { if (blockResponse.statusCode == 200 &&
final newDate = DateTime.fromMillisecondsSinceEpoch(time * 1000); blockResponse.body.isNotEmpty &&
isDateValidated = newDate == date; jsonDecode(blockResponse.body)['timestamp'] != null) {
time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString());
if (date != null) {
final newDate = DateTime.fromMillisecondsSinceEpoch(time * 1000);
isDateValidated = newDate == date;
}
} }
} }
} }
} } catch (_) {}
} catch (_) {} }
}
if (confirmations == null && height != null) { if (confirmations == null && height != null) {
final tip = currentChainTip; final tip = currentChainTip;
if (tip > 0 && height > 0) { if (tip > 0 && height > 0) {
// Add one because the block itself is the first confirmation // Add one because the block itself is the first confirmation
confirmations = tip - height + 1; confirmations = tip - height + 1;
}
} }
} }
@ -498,20 +530,47 @@ class ElectrumWorker {
} }
} }
Future<void> _handleCheckTweaks(ElectrumWorkerCheckTweaksRequest request) async {
final response = await _electrumClient!.request(
ElectrumTweaksSubscribe(
height: 0,
count: 1,
historicalMode: false,
),
);
final supportsScanning = response != null;
_sendResponse(
ElectrumWorkerCheckTweaksResponse(result: supportsScanning, id: request.id),
);
}
Future<void> _handleStopScanning(ElectrumWorkerStopScanningRequest request) async {
_stopScanRequested = true;
_sendResponse(
ElectrumWorkerStopScanningResponse(result: true, id: request.id),
);
}
Future<void> _handleScanSilentPayments(ElectrumWorkerTweaksSubscribeRequest request) async { Future<void> _handleScanSilentPayments(ElectrumWorkerTweaksSubscribeRequest request) async {
_isScanning = true;
final scanData = request.scanData; final scanData = request.scanData;
// TODO: confirmedSwitch use new connection
// final _electrumClient = await ElectrumApiProvider.connect(
// ElectrumTCPService.connect(
// Uri.parse("tcp://electrs.cakewallet.com:50001"),
// onConnectionStatusChange: (status) {
// _sendResponse(
// ElectrumWorkerConnectionResponse(status: status, id: request.id),
// );
// },
// ),
// );
int syncHeight = scanData.height; int syncHeight = scanData.height;
int initialSyncHeight = syncHeight; int initialSyncHeight = syncHeight;
int getCountPerRequest(int syncHeight) {
if (scanData.isSingleScan) {
return 1;
}
final amountLeft = scanData.chainTip - syncHeight + 1;
return amountLeft;
}
final receivers = scanData.silentPaymentsWallets.map( final receivers = scanData.silentPaymentsWallets.map(
(wallet) { (wallet) {
return Receiver( return Receiver(
@ -525,7 +584,6 @@ class ElectrumWorker {
); );
// Initial status UI update, send how many blocks in total to scan // Initial status UI update, send how many blocks in total to scan
final initialCount = getCountPerRequest(syncHeight);
_sendResponse(ElectrumWorkerTweaksSubscribeResponse( _sendResponse(ElectrumWorkerTweaksSubscribeResponse(
result: TweaksSyncResponse( result: TweaksSyncResponse(
height: syncHeight, height: syncHeight,
@ -535,14 +593,19 @@ class ElectrumWorker {
final req = ElectrumTweaksSubscribe( final req = ElectrumTweaksSubscribe(
height: syncHeight, height: syncHeight,
count: initialCount, count: 1,
historicalMode: false, historicalMode: false,
); );
final stream = await _electrumClient!.subscribe(req); final stream = await _electrumClient!.subscribe(req);
Future<void> listenFn(Map<String, dynamic> event, ElectrumTweaksSubscribe req) async { void listenFn(Map<String, dynamic> event, ElectrumTweaksSubscribe req) {
final response = req.onResponse(event); final response = req.onResponse(event);
if (_stopScanRequested || response == null) {
_stopScanRequested = false;
_isScanning = false;
return;
}
// success or error msg // success or error msg
final noData = response.message != null; final noData = response.message != null;
@ -554,13 +617,12 @@ class ElectrumWorker {
// re-subscribe to continue receiving messages, starting from the next unscanned height // re-subscribe to continue receiving messages, starting from the next unscanned height
final nextHeight = syncHeight + 1; final nextHeight = syncHeight + 1;
final nextCount = getCountPerRequest(nextHeight);
if (nextCount > 0) { if (nextHeight <= scanData.chainTip) {
final nextStream = await _electrumClient!.subscribe( final nextStream = _electrumClient!.subscribe(
ElectrumTweaksSubscribe( ElectrumTweaksSubscribe(
height: syncHeight, height: nextHeight,
count: initialCount, count: 1,
historicalMode: false, historicalMode: false,
), ),
); );
@ -693,6 +755,7 @@ class ElectrumWorker {
} }
stream?.listen((event) => listenFn(event, req)); stream?.listen((event) => listenFn(event, req));
_isScanning = false;
} }
Future<void> _handleGetVersion(ElectrumWorkerGetVersionRequest request) async { Future<void> _handleGetVersion(ElectrumWorkerGetVersionRequest request) async {

View file

@ -5,10 +5,14 @@ class ElectrumWorkerMethods {
static const String connectionMethod = "connection"; static const String connectionMethod = "connection";
static const String unknownMethod = "unknown"; static const String unknownMethod = "unknown";
static const String txHashMethod = "txHash"; static const String txHashMethod = "txHash";
static const String checkTweaksMethod = "checkTweaks";
static const String stopScanningMethod = "stopScanning";
static const ElectrumWorkerMethods connect = ElectrumWorkerMethods._(connectionMethod); static const ElectrumWorkerMethods connect = ElectrumWorkerMethods._(connectionMethod);
static const ElectrumWorkerMethods unknown = ElectrumWorkerMethods._(unknownMethod); static const ElectrumWorkerMethods unknown = ElectrumWorkerMethods._(unknownMethod);
static const ElectrumWorkerMethods txHash = ElectrumWorkerMethods._(txHashMethod); static const ElectrumWorkerMethods txHash = ElectrumWorkerMethods._(txHashMethod);
static const ElectrumWorkerMethods checkTweaks = ElectrumWorkerMethods._(checkTweaksMethod);
static const ElectrumWorkerMethods stopScanning = ElectrumWorkerMethods._(stopScanningMethod);
@override @override
String toString() { String toString() {

View file

@ -0,0 +1,49 @@
part of 'methods.dart';
class ElectrumWorkerCheckTweaksRequest implements ElectrumWorkerRequest {
ElectrumWorkerCheckTweaksRequest({this.id});
final int? id;
@override
final String method = ElectrumWorkerMethods.checkTweaks.method;
@override
factory ElectrumWorkerCheckTweaksRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerCheckTweaksRequest(id: json['id'] as int?);
}
@override
Map<String, dynamic> toJson() {
return {'method': method, 'id': id};
}
}
class ElectrumWorkerCheckTweaksError extends ElectrumWorkerErrorResponse {
ElectrumWorkerCheckTweaksError({required super.error, super.id}) : super();
@override
final String method = ElectrumWorkerMethods.checkTweaks.method;
}
class ElectrumWorkerCheckTweaksResponse extends ElectrumWorkerResponse<bool, String> {
ElectrumWorkerCheckTweaksResponse({
required super.result,
super.error,
super.id,
}) : super(method: ElectrumWorkerMethods.checkTweaks.method);
@override
String resultJson(result) {
return result.toString();
}
@override
factory ElectrumWorkerCheckTweaksResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerCheckTweaksResponse(
result: json['result'] == "true",
error: json['error'] as String?,
id: json['id'] as int?,
);
}
}

View file

@ -37,7 +37,7 @@ class ElectrumWorkerGetFeesError extends ElectrumWorkerErrorResponse {
} }
class ElectrumWorkerGetFeesResponse class ElectrumWorkerGetFeesResponse
extends ElectrumWorkerResponse<TransactionPriorities, Map<String, int>> { extends ElectrumWorkerResponse<TransactionPriorities?, Map<String, int>> {
ElectrumWorkerGetFeesResponse({ ElectrumWorkerGetFeesResponse({
required super.result, required super.result,
super.error, super.error,
@ -46,13 +46,15 @@ class ElectrumWorkerGetFeesResponse
@override @override
Map<String, int> resultJson(result) { Map<String, int> resultJson(result) {
return result.toJson(); return result?.toJson() ?? {};
} }
@override @override
factory ElectrumWorkerGetFeesResponse.fromJson(Map<String, dynamic> json) { factory ElectrumWorkerGetFeesResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerGetFeesResponse( return ElectrumWorkerGetFeesResponse(
result: deserializeTransactionPriorities(json['result'] as Map<String, dynamic>), result: json['result'] == null
? null
: deserializeTransactionPriorities(json['result'] as Map<String, dynamic>),
error: json['error'] as String?, error: json['error'] as String?,
id: json['id'] as int?, id: json['id'] as int?,
); );

View file

@ -20,3 +20,5 @@ part 'list_unspent.dart';
part 'tweaks_subscribe.dart'; part 'tweaks_subscribe.dart';
part 'get_fees.dart'; part 'get_fees.dart';
part 'version.dart'; part 'version.dart';
part 'check_tweaks_method.dart';
part 'stop_scanning.dart';

View file

@ -0,0 +1,49 @@
part of 'methods.dart';
class ElectrumWorkerStopScanningRequest implements ElectrumWorkerRequest {
ElectrumWorkerStopScanningRequest({this.id});
final int? id;
@override
final String method = ElectrumWorkerMethods.stopScanning.method;
@override
factory ElectrumWorkerStopScanningRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerStopScanningRequest(id: json['id'] as int?);
}
@override
Map<String, dynamic> toJson() {
return {'method': method, 'id': id};
}
}
class ElectrumWorkerStopScanningError extends ElectrumWorkerErrorResponse {
ElectrumWorkerStopScanningError({required super.error, super.id}) : super();
@override
final String method = ElectrumWorkerMethods.stopScanning.method;
}
class ElectrumWorkerStopScanningResponse extends ElectrumWorkerResponse<bool, String> {
ElectrumWorkerStopScanningResponse({
required super.result,
super.error,
super.id,
}) : super(method: ElectrumWorkerMethods.stopScanning.method);
@override
String resultJson(result) {
return result.toString();
}
@override
factory ElectrumWorkerStopScanningResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerStopScanningResponse(
result: json['result'] as bool,
error: json['error'] as String?,
id: json['id'] as int?,
);
}
}

View file

@ -245,6 +245,9 @@ Future<int> getHavenCurrentHeight() async {
// Data taken from https://timechaincalendar.com/ // Data taken from https://timechaincalendar.com/
const bitcoinDates = { const bitcoinDates = {
"2024-11": 868345,
"2024-10": 863584,
"2024-09": 859317,
"2024-08": 854889, "2024-08": 854889,
"2024-07": 850182, "2024-07": 850182,
"2024-06": 846005, "2024-06": 846005,

View file

@ -422,7 +422,9 @@ class CWBitcoin extends Bitcoin {
var bip39SeedBytes; var bip39SeedBytes;
try { try {
bip39SeedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); bip39SeedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase);
} catch (_) {} } catch (e) {
print("bip39 seed error: $e");
}
if (bip39SeedBytes != null) { if (bip39SeedBytes != null) {
for (final addressType in BITCOIN_ADDRESS_TYPES) { for (final addressType in BITCOIN_ADDRESS_TYPES) {

View file

@ -161,16 +161,21 @@ class AddressCell extends StatelessWidget {
if (derivationPath.isNotEmpty) if (derivationPath.isNotEmpty)
Padding( Padding(
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
child: Flexible( child: Row(
child: AutoSizeText( mainAxisAlignment: MainAxisAlignment.center,
derivationPath, children: [
maxLines: 1, Flexible(
overflow: TextOverflow.ellipsis, child: AutoSizeText(
style: TextStyle( derivationPath,
fontSize: isChange ? 10 : 14, maxLines: 1,
color: textColor, overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: isChange ? 10 : 14,
color: textColor,
),
),
), ),
), ],
), ),
), ),
if (hasBalance || hasReceived) if (hasBalance || hasReceived)

View file

@ -167,6 +167,10 @@ class _AddressListState extends State<AddressList> {
: backgroundColor, : backgroundColor,
textColor: textColor, textColor: textColor,
onTap: (_) { onTap: (_) {
if (item.isChange || item.isHidden) {
return;
}
if (widget.onSelect != null) { if (widget.onSelect != null) {
widget.onSelect!(item.address); widget.onSelect!(item.address);
return; return;