From 884a822cea7f88a1a025c117fd69a652e756bc5a Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Wed, 6 Nov 2024 12:06:52 -0300 Subject: [PATCH] fix: fee and addresses --- .../lib/bitcoin_transaction_priority.dart | 297 +++++++++--------- cw_bitcoin/lib/bitcoin_wallet.dart | 63 ++-- cw_bitcoin/lib/electrum_wallet.dart | 145 ++++----- cw_bitcoin/lib/electrum_wallet_addresses.dart | 4 + .../lib/electrum_worker/electrum_worker.dart | 80 ++++- .../lib/electrum_worker/methods/get_fees.dart | 60 ++++ .../lib/electrum_worker/methods/methods.dart | 4 + .../lib/electrum_worker/methods/version.dart | 52 +++ cw_bitcoin/lib/litecoin_wallet.dart | 19 +- .../lib/src/bitcoin_cash_wallet.dart | 8 +- cw_core/lib/transaction_priority.dart | 4 + lib/bitcoin/cw_bitcoin.dart | 47 ++- lib/bitcoin_cash/cw_bitcoin_cash.dart | 9 +- lib/store/settings_store.dart | 3 +- 14 files changed, 460 insertions(+), 335 deletions(-) create mode 100644 cw_bitcoin/lib/electrum_worker/methods/get_fees.dart create mode 100644 cw_bitcoin/lib/electrum_worker/methods/version.dart diff --git a/cw_bitcoin/lib/bitcoin_transaction_priority.dart b/cw_bitcoin/lib/bitcoin_transaction_priority.dart index 1e9c0c273..26a4c2f62 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_priority.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_priority.dart @@ -1,25 +1,25 @@ import 'package:cw_core/transaction_priority.dart'; +class BitcoinTransactionPriority extends TransactionPriority { + const BitcoinTransactionPriority({required super.title, required super.raw}); + // Unimportant: the lowest possible, confirms when it confirms no matter how long it takes + static const BitcoinTransactionPriority unimportant = + BitcoinTransactionPriority(title: 'Unimportant', raw: 0); // Normal: low fee, confirms in a reasonable time, normal because in most cases more than this is not needed, gets you in the next 2-3 blocks (about 1 hour) + static const BitcoinTransactionPriority normal = + BitcoinTransactionPriority(title: 'Normal', raw: 1); // Elevated: medium fee, confirms soon, elevated because it's higher than normal, gets you in the next 1-2 blocks (about 30 mins) + static const BitcoinTransactionPriority elevated = + BitcoinTransactionPriority(title: 'Elevated', raw: 2); // Priority: high fee, expected in the next block (about 10 mins). + static const BitcoinTransactionPriority priority = + BitcoinTransactionPriority(title: 'Priority', raw: 3); +// Custom: any fee, user defined + static const BitcoinTransactionPriority custom = + BitcoinTransactionPriority(title: 'Custom', raw: 4); -class BitcoinMempoolAPITransactionPriority extends TransactionPriority { - const BitcoinMempoolAPITransactionPriority({required super.title, required super.raw}); - - static const BitcoinMempoolAPITransactionPriority unimportant = - BitcoinMempoolAPITransactionPriority(title: 'Unimportant', raw: 0); - static const BitcoinMempoolAPITransactionPriority normal = - BitcoinMempoolAPITransactionPriority(title: 'Normal', raw: 1); - static const BitcoinMempoolAPITransactionPriority elevated = - BitcoinMempoolAPITransactionPriority(title: 'Elevated', raw: 2); - static const BitcoinMempoolAPITransactionPriority priority = - BitcoinMempoolAPITransactionPriority(title: 'Priority', raw: 3); - static const BitcoinMempoolAPITransactionPriority custom = - BitcoinMempoolAPITransactionPriority(title: 'Custom', raw: 4); - - static BitcoinMempoolAPITransactionPriority deserialize({required int raw}) { + static BitcoinTransactionPriority deserialize({required int raw}) { switch (raw) { case 0: return unimportant; @@ -41,19 +41,19 @@ class BitcoinMempoolAPITransactionPriority extends TransactionPriority { var label = ''; switch (this) { - case BitcoinMempoolAPITransactionPriority.unimportant: + case BitcoinTransactionPriority.unimportant: label = 'Unimportant ~24hrs+'; // '${S.current.transaction_priority_slow} ~24hrs'; break; - case BitcoinMempoolAPITransactionPriority.normal: + case BitcoinTransactionPriority.normal: label = 'Normal ~1hr+'; // S.current.transaction_priority_medium; break; - case BitcoinMempoolAPITransactionPriority.elevated: + case BitcoinTransactionPriority.elevated: label = 'Elevated'; break; // S.current.transaction_priority_fast; - case BitcoinMempoolAPITransactionPriority.priority: + case BitcoinTransactionPriority.priority: label = 'Priority'; break; // S.current.transaction_priority_fast; - case BitcoinMempoolAPITransactionPriority.custom: + case BitcoinTransactionPriority.custom: label = 'Custom'; break; default: @@ -69,92 +69,22 @@ class BitcoinMempoolAPITransactionPriority extends TransactionPriority { } } -class BitcoinElectrumTransactionPriority extends TransactionPriority { - const BitcoinElectrumTransactionPriority({required String title, required int raw}) +class ElectrumTransactionPriority extends TransactionPriority { + const ElectrumTransactionPriority({required String title, required int raw}) : super(title: title, raw: raw); - static const List all = [ - unimportant, - normal, - elevated, - priority, - custom, - ]; + static const List all = [fast, medium, slow, custom]; - static const BitcoinElectrumTransactionPriority unimportant = - BitcoinElectrumTransactionPriority(title: 'Unimportant', raw: 0); - static const BitcoinElectrumTransactionPriority normal = - BitcoinElectrumTransactionPriority(title: 'Normal', raw: 1); - static const BitcoinElectrumTransactionPriority elevated = - BitcoinElectrumTransactionPriority(title: 'Elevated', raw: 2); - static const BitcoinElectrumTransactionPriority priority = - BitcoinElectrumTransactionPriority(title: 'Priority', raw: 3); - static const BitcoinElectrumTransactionPriority custom = - BitcoinElectrumTransactionPriority(title: 'Custom', raw: 4); + static const ElectrumTransactionPriority slow = + ElectrumTransactionPriority(title: 'Slow', raw: 0); + static const ElectrumTransactionPriority medium = + ElectrumTransactionPriority(title: 'Medium', raw: 1); + static const ElectrumTransactionPriority fast = + ElectrumTransactionPriority(title: 'Fast', raw: 2); + static const ElectrumTransactionPriority custom = + ElectrumTransactionPriority(title: 'Custom', raw: 3); - static BitcoinElectrumTransactionPriority deserialize({required int raw}) { - switch (raw) { - case 0: - return unimportant; - case 1: - return normal; - case 2: - return elevated; - case 3: - return priority; - case 4: - return custom; - default: - throw Exception('Unexpected token: $raw for TransactionPriority deserialize'); - } - } - - @override - String toString() { - var label = ''; - - switch (this) { - case BitcoinElectrumTransactionPriority.unimportant: - label = 'Unimportant'; // '${S.current.transaction_priority_slow} ~24hrs'; - break; - case BitcoinElectrumTransactionPriority.normal: - label = 'Slow ~24hrs+'; // '${S.current.transaction_priority_slow} ~24hrs'; - break; - case BitcoinElectrumTransactionPriority.elevated: - label = 'Medium'; // S.current.transaction_priority_medium; - break; // S.current.transaction_priority_fast; - case BitcoinElectrumTransactionPriority.priority: - label = 'Fast'; - break; // S.current.transaction_priority_fast; - case BitcoinElectrumTransactionPriority.custom: - label = 'Custom'; - break; - default: - break; - } - - return label; - } - - String labelWithRate(int rate, int? customRate) { - final rateValue = this == custom ? customRate ??= 0 : rate; - return '${toString()} ($rateValue ${units}/byte)'; - } -} - -class LitecoinTransactionPriority extends BitcoinElectrumTransactionPriority { - const LitecoinTransactionPriority({required super.title, required super.raw}); - - static const all = [slow, medium, fast]; - - static const LitecoinTransactionPriority slow = - LitecoinTransactionPriority(title: 'Slow', raw: 0); - static const LitecoinTransactionPriority medium = - LitecoinTransactionPriority(title: 'Medium', raw: 1); - static const LitecoinTransactionPriority fast = - LitecoinTransactionPriority(title: 'Fast', raw: 2); - - static LitecoinTransactionPriority deserialize({required int raw}) { + static ElectrumTransactionPriority deserialize({required int raw}) { switch (raw) { case 0: return slow; @@ -162,68 +92,87 @@ class LitecoinTransactionPriority extends BitcoinElectrumTransactionPriority { return medium; case 2: return fast; + case 3: + return custom; default: - throw Exception('Unexpected token: $raw for LitecoinTransactionPriority deserialize'); + throw Exception('Unexpected token: $raw for ElectrumTransactionPriority deserialize'); } } + String get units => 'sat'; + + @override + String toString() { + var label = ''; + + switch (this) { + case ElectrumTransactionPriority.slow: + label = 'Slow ~24hrs+'; // '${S.current.transaction_priority_slow} ~24hrs'; + break; + case ElectrumTransactionPriority.medium: + label = 'Medium'; // S.current.transaction_priority_medium; + break; + case ElectrumTransactionPriority.fast: + label = 'Fast'; + break; // S.current.transaction_priority_fast; + case ElectrumTransactionPriority.custom: + label = 'Custom'; + break; + default: + break; + } + + return label; + } + + String labelWithRate(int rate, int? customRate) { + final rateValue = this == custom ? customRate ??= 0 : rate; + return '${toString()} ($rateValue ${units}/byte)'; + } +} + +class LitecoinTransactionPriority extends ElectrumTransactionPriority { + const LitecoinTransactionPriority({required super.title, required super.raw}); + @override String get units => 'lit'; } -class BitcoinCashTransactionPriority extends BitcoinElectrumTransactionPriority { +class BitcoinCashTransactionPriority extends ElectrumTransactionPriority { const BitcoinCashTransactionPriority({required super.title, required super.raw}); - static const all = [slow, medium, fast]; - - static const BitcoinCashTransactionPriority slow = - BitcoinCashTransactionPriority(title: 'Slow', raw: 0); - static const BitcoinCashTransactionPriority medium = - BitcoinCashTransactionPriority(title: 'Medium', raw: 1); - static const BitcoinCashTransactionPriority fast = - BitcoinCashTransactionPriority(title: 'Fast', raw: 2); - - static BitcoinCashTransactionPriority deserialize({required int raw}) { - switch (raw) { - case 0: - return slow; - case 1: - return medium; - case 2: - return fast; - default: - throw Exception('Unexpected token: $raw for LitecoinTransactionPriority deserialize'); - } - } - @override String get units => 'satoshi'; } -class BitcoinMempoolAPITransactionPriorities implements TransactionPriorities { - const BitcoinMempoolAPITransactionPriorities({ +class BitcoinTransactionPriorities implements TransactionPriorities { + const BitcoinTransactionPriorities({ required this.unimportant, required this.normal, required this.elevated, required this.priority, + required this.custom, }); final int unimportant; final int normal; final int elevated; final int priority; + final int custom; @override int operator [](TransactionPriority type) { switch (type) { - case BitcoinMempoolAPITransactionPriority.unimportant: + case BitcoinTransactionPriority.unimportant: return unimportant; - case BitcoinMempoolAPITransactionPriority.normal: + case BitcoinTransactionPriority.normal: return normal; - case BitcoinMempoolAPITransactionPriority.elevated: + case BitcoinTransactionPriority.elevated: return elevated; - case BitcoinMempoolAPITransactionPriority.priority: + case BitcoinTransactionPriority.priority: return priority; + case BitcoinTransactionPriority.custom: + return custom; default: throw Exception('Unexpected token: $type for TransactionPriorities operator[]'); } @@ -233,7 +182,7 @@ class BitcoinMempoolAPITransactionPriorities implements TransactionPriorities { String labelWithRate(TransactionPriority priorityType, [int? rate]) { late int rateValue; - if (priorityType == BitcoinMempoolAPITransactionPriority.custom) { + if (priorityType == BitcoinTransactionPriority.custom) { if (rate == null) { throw Exception('Rate must be provided for custom transaction priority'); } @@ -244,32 +193,53 @@ class BitcoinMempoolAPITransactionPriorities implements TransactionPriorities { return '${priorityType.toString()} (${rateValue} ${priorityType.units}/byte)'; } + + @override + Map toJson() { + return { + 'unimportant': unimportant, + 'normal': normal, + 'elevated': elevated, + 'priority': priority, + 'custom': custom, + }; + } + + static BitcoinTransactionPriorities fromJson(Map json) { + return BitcoinTransactionPriorities( + unimportant: json['unimportant'] as int, + normal: json['normal'] as int, + elevated: json['elevated'] as int, + priority: json['priority'] as int, + custom: json['custom'] as int, + ); + } } -class BitcoinElectrumTransactionPriorities implements TransactionPriorities { - const BitcoinElectrumTransactionPriorities({ - required this.unimportant, +class ElectrumTransactionPriorities implements TransactionPriorities { + const ElectrumTransactionPriorities({ required this.slow, required this.medium, required this.fast, + required this.custom, }); - final int unimportant; final int slow; final int medium; final int fast; + final int custom; @override int operator [](TransactionPriority type) { switch (type) { - case BitcoinElectrumTransactionPriority.unimportant: - return unimportant; - case BitcoinElectrumTransactionPriority.normal: + case ElectrumTransactionPriority.slow: return slow; - case BitcoinElectrumTransactionPriority.elevated: + case ElectrumTransactionPriority.medium: return medium; - case BitcoinElectrumTransactionPriority.priority: + case ElectrumTransactionPriority.fast: return fast; + case ElectrumTransactionPriority.custom: + return custom; default: throw Exception('Unexpected token: $type for TransactionPriorities operator[]'); } @@ -280,25 +250,46 @@ class BitcoinElectrumTransactionPriorities implements TransactionPriorities { return '${priorityType.toString()} (${this[priorityType]} ${priorityType.units}/byte)'; } - factory BitcoinElectrumTransactionPriorities.fromList(List list) { + factory ElectrumTransactionPriorities.fromList(List list) { if (list.length != 3) { throw Exception( 'Unexpected list length: ${list.length} for BitcoinElectrumTransactionPriorities.fromList'); } - int unimportantFee = list[0]; - - // Electrum servers only provides 3 levels: slow, medium, fast - // so make "unimportant" always lower than slow (but not 0) - if (unimportantFee > 1) { - unimportantFee--; - } - - return BitcoinElectrumTransactionPriorities( - unimportant: unimportantFee, + return ElectrumTransactionPriorities( slow: list[0], medium: list[1], fast: list[2], + custom: 0, + ); + } + + @override + Map toJson() { + return { + 'slow': slow, + 'medium': medium, + 'fast': fast, + 'custom': custom, + }; + } + + static ElectrumTransactionPriorities fromJson(Map json) { + return ElectrumTransactionPriorities( + slow: json['slow'] as int, + medium: json['medium'] as int, + fast: json['fast'] as int, + custom: json['custom'] as int, ); } } + +TransactionPriorities deserializeTransactionPriorities(Map json) { + if (json.containsKey('unimportant')) { + return BitcoinTransactionPriorities.fromJson(json); + } else if (json.containsKey('slow')) { + return ElectrumTransactionPriorities.fromJson(json); + } else { + throw Exception('Unexpected token: $json for deserializeTransactionPriorities'); + } +} diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index e695ce67f..748acdbbe 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -276,6 +276,24 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { ); } + Future getNodeIsElectrs() async { + final version = await sendWorker(ElectrumWorkerGetVersionRequest()) as List; + + if (version.isNotEmpty) { + final server = version[0]; + + if (server.toLowerCase().contains('electrs')) { + node!.isElectrs = true; + node!.save(); + return node!.isElectrs!; + } + } + + node!.isElectrs = false; + node!.save(); + return node!.isElectrs!; + } + Future getNodeSupportsSilentPayments() async { return true; // As of today (august 2024), only ElectrumRS supports silent payments @@ -757,51 +775,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { // ); // } - @override - @action - Future updateFeeRates() async { - // Bitcoin only: use the mempool.space backend API for accurate fee rates - if (mempoolAPIEnabled) { - try { - final recommendedFees = await apiProvider!.getRecommendedFeeRate(); - - final unimportantFee = recommendedFees.economyFee!.satoshis; - final normalFee = recommendedFees.low.satoshis; - int elevatedFee = recommendedFees.medium.satoshis; - int priorityFee = recommendedFees.high.satoshis; - - // Bitcoin only: adjust fee rates to avoid equal fee values - // elevated should be higher than normal - if (normalFee == elevatedFee) { - elevatedFee++; - } - // priority should be higher than elevated - while (priorityFee <= elevatedFee) { - priorityFee++; - } - // this guarantees that, even if all fees are low and equal, - // higher priority fees can be taken when fees start surging - - feeRates = BitcoinMempoolAPITransactionPriorities( - unimportant: unimportantFee, - normal: normalFee, - elevated: elevatedFee, - priority: priorityFee, - ); - return; - } catch (e, stacktrace) { - callError(FlutterErrorDetails( - exception: e, - stack: stacktrace, - library: this.runtimeType.toString(), - )); - } - } else { - // Bitcoin only: Ideally this should be avoided, electrum is terrible at fee rates - await super.updateFeeRates(); - } - } - @override @action Future onHeadersResponse(ElectrumHeaderResponse response) async { diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 9964751ee..47d69d670 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -16,7 +16,6 @@ import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/bitcoin_wallet_keys.dart'; -import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_transaction_history.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; @@ -84,7 +83,6 @@ abstract class ElectrumWalletBase }, syncStatus = NotConnectedSyncStatus(), _password = password, - _isTransactionUpdating = false, isEnabledAutoGenerateSubaddress = true, // TODO: inital unspent coins unspentCoins = BitcoinUnspentCoins(), @@ -115,6 +113,7 @@ abstract class ElectrumWalletBase sharedPrefs.complete(SharedPreferences.getInstance()); } + // Sends a request to the worker and returns a future that completes when the worker responds Future sendWorker(ElectrumWorkerRequest request) { final messageId = ++_messageId; @@ -144,28 +143,14 @@ abstract class ElectrumWalletBase } else { messageJson = message as Map; } + final workerMethod = messageJson['method'] as String; + final workerError = messageJson['error'] as String?; - // if (workerResponse.error != null) { - // print('Worker error: ${workerResponse.error}'); - - // switch (workerResponse.method) { - // // case 'connectionStatus': - // // final status = ConnectionStatus.values.firstWhere( - // // (e) => e.toString() == workerResponse.error, - // // ); - // // _onConnectionStatusChange(status); - // // break; - // // case 'fetchBalances': - // // // Update the balance state - // // // this.balance[currency] = balance!; - // // break; - // case 'blockchain.headers.subscribe': - // _chainTipListenerOn = false; - // break; - // } - // return; - // } + if (workerError != null) { + print('Worker error: $workerError'); + return; + } final responseId = messageJson['id'] as int?; if (responseId != null && _responseCompleters.containsKey(responseId)) { @@ -194,16 +179,13 @@ abstract class ElectrumWalletBase final response = ElectrumWorkerListUnspentResponse.fromJson(messageJson); onUnspentResponse(response.result); break; + case ElectrumRequestMethods.estimateFeeMethod: + final response = ElectrumWorkerGetFeesResponse.fromJson(messageJson); + onFeesResponse(response.result); + break; } } - // Don't forget to clean up in the close method - // @override - // Future close({required bool shouldCleanup}) async { - // await _workerSubscription?.cancel(); - // await super.close(shouldCleanup: shouldCleanup); - // } - static Bip32Slip10Secp256k1 getAccountHDWallet(CryptoCurrency? currency, BasedUtxoNetwork network, List? seedBytes, String? xpub, DerivationInfo? derivationInfo) { if (seedBytes == null && xpub == null) { @@ -317,13 +299,30 @@ abstract class ElectrumWalletBase @observable TransactionPriorities? feeRates; - int feeRate(TransactionPriority priority) => feeRates![priority]; + + int feeRate(TransactionPriority priority) { + if (priority is ElectrumTransactionPriority && feeRates is BitcoinTransactionPriorities) { + final rates = feeRates as BitcoinTransactionPriorities; + + switch (priority) { + case ElectrumTransactionPriority.slow: + return rates.normal; + case ElectrumTransactionPriority.medium: + return rates.elevated; + case ElectrumTransactionPriority.fast: + return rates.priority; + case ElectrumTransactionPriority.custom: + return rates.custom; + } + } + + return feeRates![priority]; + } @observable List scripthashesListening; bool _chainTipListenerOn = false; - bool _isTransactionUpdating; bool _isInitialSync = true; void Function(FlutterErrorDetails)? _onError; @@ -361,13 +360,12 @@ abstract class ElectrumWalletBase // INFO: FOURTH: Finish with unspents await updateAllUnspents(); + await updateFeeRates(); + + _updateFeeRateTimer ??= + Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates()); + _isInitialSync = false; - - // await updateFeeRates(); - - // _updateFeeRateTimer ??= - // Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates()); - syncStatus = SyncedSyncStatus(); await save(); @@ -385,44 +383,18 @@ abstract class ElectrumWalletBase @action Future updateFeeRates() async { - try { - // feeRates = BitcoinElectrumTransactionPriorities.fromList( - // await electrumClient2!.getFeeRates(), - // ); - } catch (e, stacktrace) { - // _onError?.call(FlutterErrorDetails( - // exception: e, - // stack: stacktrace, - // library: this.runtimeType.toString(), - // )); - } + workerSendPort!.send( + ElectrumWorkerGetFeesRequest(mempoolAPIEnabled: mempoolAPIEnabled).toJson(), + ); + } + + @action + Future onFeesResponse(TransactionPriorities result) async { + feeRates = result; } Node? node; - Future getNodeIsElectrs() async { - return true; - if (node == null) { - return false; - } - - // final version = await electrumClient.version(); - - if (version.isNotEmpty) { - final server = version[0]; - - if (server.toLowerCase().contains('electrs')) { - node!.isElectrs = true; - node!.save(); - return node!.isElectrs!; - } - } - - node!.isElectrs = false; - node!.save(); - return node!.isElectrs!; - } - @action @override Future connectToNode({required Node node}) async { @@ -520,11 +492,14 @@ abstract class ElectrumWalletBase spendsSilentPayment = true; isSilentPayment = true; } else if (!isHardwareWallet) { - privkey = ECPrivate.fromBip32( - bip32: walletAddresses.bip32, - account: BitcoinAddressUtils.getAccountFromChange(utx.bitcoinAddressRecord.isChange), - index: utx.bitcoinAddressRecord.index, - ); + final addressRecord = (utx.bitcoinAddressRecord as BitcoinAddressRecord); + final path = addressRecord.derivationInfo.derivationPath + .addElem(Bip32KeyIndex( + BitcoinAddressUtils.getAccountFromChange(addressRecord.isChange), + )) + .addElem(Bip32KeyIndex(addressRecord.index)); + + privkey = ECPrivate.fromBip32(bip32: bip32.derive(path)); } vinOutpoints.add(Outpoint(txid: utx.hash, index: utx.vout)); @@ -1110,7 +1085,7 @@ abstract class ElectrumWalletBase @override int calculateEstimatedFee(TransactionPriority? priority, int? amount, {int? outputsCount, int? size}) { - if (priority is BitcoinMempoolAPITransactionPriority) { + if (priority is BitcoinTransactionPriority) { return calculateEstimatedFeeWithFeeRate( feeRate(priority), amount, @@ -1478,11 +1453,13 @@ abstract class ElectrumWalletBase walletAddresses.allAddresses.firstWhere((element) => element.address == address); final btcAddress = RegexUtils.addressTypeFromStr(addressRecord.address, network); - final privkey = ECPrivate.fromBip32( - bip32: walletAddresses.bip32, - account: addressRecord.isChange ? 1 : 0, - index: addressRecord.index, - ); + final path = addressRecord.derivationInfo.derivationPath + .addElem(Bip32KeyIndex( + BitcoinAddressUtils.getAccountFromChange(addressRecord.isChange), + )) + .addElem(Bip32KeyIndex(addressRecord.index)); + + final privkey = ECPrivate.fromBip32(bip32: bip32.derive(path)); privateKeys.add(privkey); @@ -1694,10 +1671,7 @@ abstract class ElectrumWalletBase unspentCoins.forInfo(unspentCoinsInfo.values).forEach((unspentCoinInfo) { if (unspentCoinInfo.isFrozen) { - // TODO: verify this works well totalFrozen += unspentCoinInfo.value; - totalConfirmed -= unspentCoinInfo.value; - totalUnconfirmed -= unspentCoinInfo.value; } }); @@ -1835,7 +1809,6 @@ abstract class ElectrumWalletBase if (syncStatus is ConnectingSyncStatus || isDisconnectedStatus) { // Needs to re-subscribe to all scripthashes when reconnected scripthashesListening = []; - _isTransactionUpdating = false; _chainTipListenerOn = false; } diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 44e3be7f9..789a0e491 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -605,6 +605,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @action Future generateInitialAddresses({required BitcoinAddressType type}) async { for (final derivationType in hdWallets.keys) { + if (derivationType == CWBitcoinDerivationType.old && type == SegwitAddresType.p2wpkh) { + continue; + } + final derivationInfo = BitcoinAddressUtils.getDerivationFromType( type, isElectrum: derivationType == CWBitcoinDerivationType.electrum, diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index 102d6c313..67ded289d 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -5,6 +5,7 @@ import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; @@ -21,6 +22,7 @@ import 'package:sp_scanner/sp_scanner.dart'; class ElectrumWorker { final SendPort sendPort; ElectrumApiProvider? _electrumClient; + BasedUtxoNetwork? _network; ElectrumWorker._(this.sendPort, {ElectrumApiProvider? electrumClient}) : _electrumClient = electrumClient; @@ -100,6 +102,16 @@ class ElectrumWorker { ElectrumWorkerTweaksSubscribeRequest.fromJson(messageJson), ); break; + case ElectrumRequestMethods.estimateFeeMethod: + await _handleGetFeeRates( + ElectrumWorkerGetFeesRequest.fromJson(messageJson), + ); + break; + case ElectrumRequestMethods.versionMethod: + await _handleGetVersion( + ElectrumWorkerGetVersionRequest.fromJson(messageJson), + ); + break; } } catch (e, s) { print(s); @@ -108,6 +120,8 @@ class ElectrumWorker { } Future _handleConnect(ElectrumWorkerConnectionRequest request) async { + _network = request.network; + _electrumClient = await ElectrumApiProvider.connect( ElectrumTCPService.connect( request.uri, @@ -415,6 +429,56 @@ class ElectrumWorker { ); } + Future _handleGetFeeRates(ElectrumWorkerGetFeesRequest request) async { + if (request.mempoolAPIEnabled) { + try { + final recommendedFees = await ApiProvider.fromMempool( + _network!, + baseUrl: "http://mempool.cakewallet.com:8999/api", + ).getRecommendedFeeRate(); + + final unimportantFee = recommendedFees.economyFee!.satoshis; + final normalFee = recommendedFees.low.satoshis; + int elevatedFee = recommendedFees.medium.satoshis; + int priorityFee = recommendedFees.high.satoshis; + + // Bitcoin only: adjust fee rates to avoid equal fee values + // elevated fee should be higher than normal fee + if (normalFee == elevatedFee) { + elevatedFee++; + } + // priority fee should be higher than elevated fee + while (priorityFee <= elevatedFee) { + priorityFee++; + } + // this guarantees that, even if all fees are low and equal, + // higher priority fee txs can be consumed when chain fees start surging + + _sendResponse( + ElectrumWorkerGetFeesResponse( + result: BitcoinTransactionPriorities( + unimportant: unimportantFee, + normal: normalFee, + elevated: elevatedFee, + priority: priorityFee, + custom: unimportantFee, + ), + ), + ); + } catch (e) { + _sendError(ElectrumWorkerGetFeesError(error: e.toString())); + } + } else { + _sendResponse( + ElectrumWorkerGetFeesResponse( + result: ElectrumTransactionPriorities.fromList( + await _electrumClient!.getFeeRates(), + ), + ), + ); + } + } + Future _handleScanSilentPayments(ElectrumWorkerTweaksSubscribeRequest request) async { final scanData = request.scanData; int syncHeight = scanData.height; @@ -446,7 +510,6 @@ class ElectrumWorker { ), )); - print([syncHeight, initialCount]); final listener = await _electrumClient!.subscribe( ElectrumTweaksSubscribe(height: syncHeight, count: initialCount), ); @@ -578,12 +641,17 @@ class ElectrumWorker { } listener?.call(listenFn); + } - // if (tweaksSubscription == null) { - // return scanData.sendPort.send( - // SyncResponse(syncHeight, UnsupportedSyncStatus()), - // ); - // } + Future _handleGetVersion(ElectrumWorkerGetVersionRequest request) async { + _sendResponse(ElectrumWorkerGetVersionResponse( + result: (await _electrumClient!.request( + ElectrumVersion( + clientName: "", + protocolVersion: ["1.4"], + ), + )), + id: request.id)); } } diff --git a/cw_bitcoin/lib/electrum_worker/methods/get_fees.dart b/cw_bitcoin/lib/electrum_worker/methods/get_fees.dart new file mode 100644 index 000000000..be81e5346 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/get_fees.dart @@ -0,0 +1,60 @@ +part of 'methods.dart'; + +class ElectrumWorkerGetFeesRequest implements ElectrumWorkerRequest { + ElectrumWorkerGetFeesRequest({ + required this.mempoolAPIEnabled, + this.id, + }); + + final bool mempoolAPIEnabled; + final int? id; + + @override + final String method = ElectrumRequestMethods.estimateFee.method; + + @override + factory ElectrumWorkerGetFeesRequest.fromJson(Map json) { + return ElectrumWorkerGetFeesRequest( + mempoolAPIEnabled: json['mempoolAPIEnabled'] as bool, + id: json['id'] as int?, + ); + } + + @override + Map toJson() { + return {'method': method, 'mempoolAPIEnabled': mempoolAPIEnabled}; + } +} + +class ElectrumWorkerGetFeesError extends ElectrumWorkerErrorResponse { + ElectrumWorkerGetFeesError({ + required super.error, + super.id, + }) : super(); + + @override + String get method => ElectrumRequestMethods.estimateFee.method; +} + +class ElectrumWorkerGetFeesResponse + extends ElectrumWorkerResponse> { + ElectrumWorkerGetFeesResponse({ + required super.result, + super.error, + super.id, + }) : super(method: ElectrumRequestMethods.estimateFee.method); + + @override + Map resultJson(result) { + return result.toJson(); + } + + @override + factory ElectrumWorkerGetFeesResponse.fromJson(Map json) { + return ElectrumWorkerGetFeesResponse( + result: deserializeTransactionPriorities(json['result'] as Map), + error: json['error'] as String?, + id: json['id'] as int?, + ); + } +} diff --git a/cw_bitcoin/lib/electrum_worker/methods/methods.dart b/cw_bitcoin/lib/electrum_worker/methods/methods.dart index 6ace715d0..295522d39 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/methods.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/methods.dart @@ -6,6 +6,8 @@ import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; part 'connection.dart'; part 'headers_subscribe.dart'; @@ -16,3 +18,5 @@ part 'get_tx_expanded.dart'; part 'broadcast.dart'; part 'list_unspent.dart'; part 'tweaks_subscribe.dart'; +part 'get_fees.dart'; +part 'version.dart'; diff --git a/cw_bitcoin/lib/electrum_worker/methods/version.dart b/cw_bitcoin/lib/electrum_worker/methods/version.dart new file mode 100644 index 000000000..0f3f814d3 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/version.dart @@ -0,0 +1,52 @@ +part of 'methods.dart'; + +class ElectrumWorkerGetVersionRequest implements ElectrumWorkerRequest { + ElectrumWorkerGetVersionRequest({this.id}); + + final int? id; + + @override + final String method = ElectrumRequestMethods.version.method; + + @override + factory ElectrumWorkerGetVersionRequest.fromJson(Map json) { + return ElectrumWorkerGetVersionRequest(id: json['id'] as int?); + } + + @override + Map toJson() { + return {'method': method}; + } +} + +class ElectrumWorkerGetVersionError extends ElectrumWorkerErrorResponse { + ElectrumWorkerGetVersionError({ + required super.error, + super.id, + }) : super(); + + @override + String get method => ElectrumRequestMethods.version.method; +} + +class ElectrumWorkerGetVersionResponse extends ElectrumWorkerResponse, List> { + ElectrumWorkerGetVersionResponse({ + required super.result, + super.error, + super.id, + }) : super(method: ElectrumRequestMethods.version.method); + + @override + List resultJson(result) { + return result; + } + + @override + factory ElectrumWorkerGetVersionResponse.fromJson(Map json) { + return ElectrumWorkerGetVersionResponse( + result: json['result'] as List, + error: json['error'] as String?, + id: json['id'] as int?, + ); + } +} diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 277864af7..815890757 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -877,13 +877,13 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @override int feeRate(TransactionPriority priority) { - if (priority is LitecoinTransactionPriority) { + if (priority is ElectrumTransactionPriority) { switch (priority) { - case LitecoinTransactionPriority.slow: + case ElectrumTransactionPriority.slow: return 1; - case LitecoinTransactionPriority.medium: + case ElectrumTransactionPriority.medium: return 2; - case LitecoinTransactionPriority.fast: + case ElectrumTransactionPriority.fast: return 3; } } @@ -1036,11 +1036,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { witnesses: tx2.inputs.asMap().entries.map((e) { final utxo = unspentCoins .firstWhere((utxo) => utxo.hash == e.value.txId && utxo.vout == e.value.txIndex); - final key = ECPrivate.fromBip32( - bip32: walletAddresses.bip32, - account: BitcoinAddressUtils.getAccountFromChange(utxo.bitcoinAddressRecord.isChange), - index: utxo.bitcoinAddressRecord.index, - ); + final addressRecord = (utxo.bitcoinAddressRecord as BitcoinAddressRecord); + final path = addressRecord.derivationInfo.derivationPath + .addElem( + Bip32KeyIndex(BitcoinAddressUtils.getAccountFromChange(addressRecord.isChange))) + .addElem(Bip32KeyIndex(addressRecord.index)); + final key = ECPrivate.fromBip32(bip32: bip32.derive(path)); final digest = tx2.getTransactionSegwitDigit( txInIndex: e.key, script: key.getPublic().toP2pkhAddress().toScriptPubKey(), diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index 0045801a7..0019d32c6 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -209,13 +209,13 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { @override int feeRate(TransactionPriority priority) { - if (priority is BitcoinCashTransactionPriority) { + if (priority is ElectrumTransactionPriority) { switch (priority) { - case BitcoinCashTransactionPriority.slow: + case ElectrumTransactionPriority.slow: return 1; - case BitcoinCashTransactionPriority.medium: + case ElectrumTransactionPriority.medium: return 5; - case BitcoinCashTransactionPriority.fast: + case ElectrumTransactionPriority.fast: return 10; } } diff --git a/cw_core/lib/transaction_priority.dart b/cw_core/lib/transaction_priority.dart index 5eb5576f3..35282f49e 100644 --- a/cw_core/lib/transaction_priority.dart +++ b/cw_core/lib/transaction_priority.dart @@ -13,4 +13,8 @@ abstract class TransactionPriorities { const TransactionPriorities(); int operator [](TransactionPriority type); String labelWithRate(TransactionPriority type); + Map toJson(); + factory TransactionPriorities.fromJson(Map json) { + throw UnimplementedError(); + } } diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 1461c1843..d4bc6799b 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -52,7 +52,7 @@ class CWBitcoin extends Bitcoin { name: name, hwAccountData: accountData, walletInfo: walletInfo); @override - TransactionPriority getMediumTransactionPriority() => BitcoinElectrumTransactionPriority.elevated; + TransactionPriority getMediumTransactionPriority() => ElectrumTransactionPriority.medium; @override List getWordList() => wordlist; @@ -70,18 +70,18 @@ class CWBitcoin extends Bitcoin { } @override - List getTransactionPriorities() => BitcoinElectrumTransactionPriority.all; + List getTransactionPriorities() => ElectrumTransactionPriority.all; @override - List getLitecoinTransactionPriorities() => LitecoinTransactionPriority.all; + List getLitecoinTransactionPriorities() => ElectrumTransactionPriority.all; @override TransactionPriority deserializeBitcoinTransactionPriority(int raw) => - BitcoinElectrumTransactionPriority.deserialize(raw: raw); + ElectrumTransactionPriority.deserialize(raw: raw); @override TransactionPriority deserializeLitecoinTransactionPriority(int raw) => - LitecoinTransactionPriority.deserialize(raw: raw); + ElectrumTransactionPriority.deserialize(raw: raw); @override int getFeeRate(Object wallet, TransactionPriority priority) { @@ -111,7 +111,7 @@ class CWBitcoin extends Bitcoin { UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) { final bitcoinFeeRate = - priority == BitcoinElectrumTransactionPriority.custom && feeRate != null ? feeRate : null; + priority == ElectrumTransactionPriority.custom && feeRate != null ? feeRate : null; return BitcoinTransactionCredentials( outputs .map((out) => OutputInfo( @@ -125,7 +125,7 @@ class CWBitcoin extends Bitcoin { formattedCryptoAmount: out.formattedCryptoAmount, memo: out.memo)) .toList(), - priority: priority as BitcoinElectrumTransactionPriority, + priority: priority as ElectrumTransactionPriority, feeRate: bitcoinFeeRate, coinTypeToSpendFrom: coinTypeToSpendFrom, ); @@ -165,12 +165,7 @@ class CWBitcoin extends Bitcoin { final p2shAddr = sk.getPublic().toP2pkhInP2sh(); final estimatedTx = await electrumWallet.estimateSendAllTx( [BitcoinOutput(address: p2shAddr, value: BigInt.zero)], - getFeeRate( - wallet, - wallet.type == WalletType.litecoin - ? priority as LitecoinTransactionPriority - : priority as BitcoinElectrumTransactionPriority, - ), + getFeeRate(wallet, priority), ); return estimatedTx.amount; @@ -200,7 +195,7 @@ class CWBitcoin extends Bitcoin { @override String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate, {int? customRate}) => - (priority as BitcoinElectrumTransactionPriority).labelWithRate(rate, customRate); + (priority as ElectrumTransactionPriority).labelWithRate(rate, customRate); @override List getUnspents(Object wallet, @@ -256,22 +251,19 @@ class CWBitcoin extends Bitcoin { } @override - TransactionPriority getBitcoinTransactionPriorityMedium() => - BitcoinElectrumTransactionPriority.elevated; + TransactionPriority getBitcoinTransactionPriorityMedium() => ElectrumTransactionPriority.fast; @override - TransactionPriority getBitcoinTransactionPriorityCustom() => - BitcoinElectrumTransactionPriority.custom; + TransactionPriority getBitcoinTransactionPriorityCustom() => ElectrumTransactionPriority.custom; @override - TransactionPriority getLitecoinTransactionPriorityMedium() => LitecoinTransactionPriority.medium; + TransactionPriority getLitecoinTransactionPriorityMedium() => ElectrumTransactionPriority.medium; @override - TransactionPriority getBitcoinTransactionPrioritySlow() => - BitcoinElectrumTransactionPriority.normal; + TransactionPriority getBitcoinTransactionPrioritySlow() => ElectrumTransactionPriority.medium; @override - TransactionPriority getLitecoinTransactionPrioritySlow() => LitecoinTransactionPriority.slow; + TransactionPriority getLitecoinTransactionPrioritySlow() => ElectrumTransactionPriority.slow; @override Future setAddressType(Object wallet, dynamic option) async { @@ -436,7 +428,7 @@ class CWBitcoin extends Bitcoin { {int? size}) { final bitcoinWallet = wallet as ElectrumWallet; return bitcoinWallet.feeAmountForPriority( - priority as BitcoinElectrumTransactionPriority, inputsCount, outputsCount); + priority as ElectrumTransactionPriority, inputsCount, outputsCount); } @override @@ -460,8 +452,13 @@ class CWBitcoin extends Bitcoin { @override int getMaxCustomFeeRate(Object wallet) { - final bitcoinWallet = wallet as ElectrumWallet; - return (bitcoinWallet.feeRate(BitcoinElectrumTransactionPriority.priority) * 10).round(); + final electrumWallet = wallet as ElectrumWallet; + final feeRates = electrumWallet.feeRates; + final maxFee = electrumWallet.feeRates is ElectrumTransactionPriorities + ? ElectrumTransactionPriority.fast + : BitcoinTransactionPriority.priority; + + return (electrumWallet.feeRate(maxFee) * 10).round(); } @override diff --git a/lib/bitcoin_cash/cw_bitcoin_cash.dart b/lib/bitcoin_cash/cw_bitcoin_cash.dart index a0cb406c2..0a9131349 100644 --- a/lib/bitcoin_cash/cw_bitcoin_cash.dart +++ b/lib/bitcoin_cash/cw_bitcoin_cash.dart @@ -48,15 +48,14 @@ class CWBitcoinCash extends BitcoinCash { @override TransactionPriority deserializeBitcoinCashTransactionPriority(int raw) => - BitcoinCashTransactionPriority.deserialize(raw: raw); + ElectrumTransactionPriority.deserialize(raw: raw); @override - TransactionPriority getDefaultTransactionPriority() => BitcoinCashTransactionPriority.medium; + TransactionPriority getDefaultTransactionPriority() => ElectrumTransactionPriority.medium; @override - List getTransactionPriorities() => BitcoinCashTransactionPriority.all; + List getTransactionPriorities() => ElectrumTransactionPriority.all; @override - TransactionPriority getBitcoinCashTransactionPrioritySlow() => - BitcoinCashTransactionPriority.slow; + TransactionPriority getBitcoinCashTransactionPrioritySlow() => ElectrumTransactionPriority.slow; } diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index bcea80a54..cd39318f4 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -602,8 +602,7 @@ abstract class SettingsStoreBase with Store { static const defaultActionsMode = 11; static const defaultPinCodeTimeOutDuration = PinCodeRequiredDuration.tenMinutes; static const defaultAutoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.initialized; - // static final walletPasswordDirectInput = Platform.isLinux; - static final walletPasswordDirectInput = false; + static final walletPasswordDirectInput = Platform.isLinux; static const defaultSeedPhraseLength = SeedPhraseLength.twelveWords; static const defaultMoneroSeedType = MoneroSeedType.defaultSeedType; static const defaultBitcoinSeedType = BitcoinSeedType.defaultDerivationType;