diff --git a/cw_decred/lib/api/libdcrwallet.dart b/cw_decred/lib/api/libdcrwallet.dart index 12308b8ef..156ac3641 100644 --- a/cw_decred/lib/api/libdcrwallet.dart +++ b/cw_decred/lib/api/libdcrwallet.dart @@ -146,10 +146,14 @@ Map balance(String walletName) { return jsonDecode(res.payload); } -int calculateEstimatedFeeWithFeeRate(int feeRate, int amount) { - // Ideally we create a tx with wallet going to this amount and just return - // the fee we get back. TODO. - return 123000; +String estimateFee(String walletName, int numBlocks) { + final cName = walletName.toCString(); + final cNumBlocks = numBlocks.toString().toCString(); + final res = executePayloadFn( + fn: () => dcrwalletApi.estimateFee(cName, cNumBlocks), + ptrsToFree: [cName, cNumBlocks], + ); + return res.payload; } String createSignedTransaction( diff --git a/cw_decred/lib/transaction_priority.dart b/cw_decred/lib/transaction_priority.dart index f9e601539..1e1482f2f 100644 --- a/cw_decred/lib/transaction_priority.dart +++ b/cw_decred/lib/transaction_priority.dart @@ -52,3 +52,22 @@ class DecredTransactionPriority extends TransactionPriority { String labelWithRate(int rate) => '${toString()} ($rate ${units}/byte)'; } + +class FeeCache { + int _feeRate; + DateTime stamp; + FeeCache(this._feeRate) : this.stamp = DateTime(0, 0, 0, 0, 0, 0, 0, 0); + + bool isOld() { + return this.stamp.add(const Duration(minutes: 30)).isBefore(DateTime.now()); + } + + void update(int feeRate) { + this._feeRate = feeRate; + this.stamp = DateTime.now(); + } + + int feeRate() { + return this._feeRate; + } +} diff --git a/cw_decred/lib/wallet.dart b/cw_decred/lib/wallet.dart index 32e7d8a48..7aec45cfb 100644 --- a/cw_decred/lib/wallet.dart +++ b/cw_decred/lib/wallet.dart @@ -44,11 +44,17 @@ abstract class DecredWalletBase extends WalletBase<DecredBalance, transactionHistory = DecredTransactionHistory(); } - final defaultFeeRate = 10000; + // NOTE: Hitting this max fee would be unexpected with current on chain use + // but this may need to be updated in the future. + final maxFeeRate = 100000; + static final defaultFeeRate = 10000; final String _password; final idPrefix = "decred_"; bool connecting = false; String persistantPeer = ""; + FeeCache feeRateFast = FeeCache(defaultFeeRate); + FeeCache feeRateMedium = FeeCache(defaultFeeRate); + FeeCache feeRateSlow = FeeCache(defaultFeeRate); Timer? syncTimer; Box<UnspentCoinsInfo> unspentCoinsInfo; @@ -240,7 +246,6 @@ abstract class DecredWalletBase extends WalletBase<DecredBalance, outputs.add(o); } ; - // TODO: Fix fee rate. final signReq = { "inputs": inputs, "ignoreInputs": ignoreInputs, @@ -264,17 +269,67 @@ abstract class DecredWalletBase extends WalletBase<DecredBalance, } int feeRate(TransactionPriority priority) { - // TODO - return 1000; + if (!(priority is DecredTransactionPriority)) { + return defaultFeeRate; + } + int Function(int nb) feeForNb = (int nb) { + try { + final feeStr = libdcrwallet.estimateFee(walletInfo.name, nb); + var fee = int.parse(feeStr); + if (fee > maxFeeRate) { + throw "dcr fee returned from estimate fee was over max"; + } else if (fee <= 0) { + throw "dcr fee returned from estimate fee was zero"; + } + return fee; + } catch (e) { + print(e); + return defaultFeeRate; + } + }; + final p = priority as DecredTransactionPriority; + switch (p) { + case DecredTransactionPriority.slow: + if (feeRateSlow.isOld()) { + feeRateSlow.update(feeForNb(4)); + } + return feeRateSlow.feeRate(); + case DecredTransactionPriority.medium: + if (feeRateMedium.isOld()) { + feeRateMedium.update(feeForNb(2)); + } + return feeRateMedium.feeRate(); + case DecredTransactionPriority.fast: + if (feeRateFast.isOld()) { + feeRateFast.update(feeForNb(1)); + } + return feeRateFast.feeRate(); + } + return defaultFeeRate; } @override int calculateEstimatedFee(TransactionPriority priority, int? amount) { if (priority is DecredTransactionPriority) { - return libdcrwallet.calculateEstimatedFeeWithFeeRate( - this.feeRate(priority), amount ?? 0); - } + final P2PKHOutputSize = + 36; // 8 bytes value + 2 bytes version + at least 1 byte varint script size + P2PKHPkScriptSize + // MsgTxOverhead is 4 bytes version (lower 2 bytes for the real transaction + // version and upper 2 bytes for the serialization type) + 4 bytes locktime + // + 4 bytes expiry + 3 bytes of varints for the number of transaction + // inputs (x2 for witness and prefix) and outputs + final MsgTxOverhead = 15; + // TxInOverhead is the overhead for a wire.TxIn with a scriptSig length < + // 254. prefix (41 bytes) + ValueIn (8 bytes) + BlockHeight (4 bytes) + + // BlockIndex (4 bytes) + sig script var int (at least 1 byte) + final TxInOverhead = 57; + final P2PKHInputSize = TxInOverhead + + 109; // TxInOverhead (57) + var int (1) + P2PKHSigScriptSize (108) + // Estimate using a transaction consuming three inputs and paying to one + // address with change. + return this.feeRate(priority) * + (MsgTxOverhead + P2PKHInputSize * 3 + P2PKHOutputSize * 2); + } return 0; } diff --git a/lib/entities/priority_for_wallet_type.dart b/lib/entities/priority_for_wallet_type.dart index 534287494..3dd85c92c 100644 --- a/lib/entities/priority_for_wallet_type.dart +++ b/lib/entities/priority_for_wallet_type.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_type.dart'; @@ -32,6 +33,8 @@ List<TransactionPriority> priorityForWalletType(WalletType type) { case WalletType.solana: case WalletType.tron: return []; + case WalletType.decred: + return decred!.getTransactionPriorities(); default: return []; }