From 234e9ad607f92cf150851dfeaa0ce4d9fba38244 Mon Sep 17 00:00:00 2001
From: JoeGruff <joegruffins@gmail.com>
Date: Thu, 8 Feb 2024 19:58:58 +0900
Subject: [PATCH] decred: Fix fee estimation.

---
 cw_decred/lib/api/libdcrwallet.dart        | 12 ++--
 cw_decred/lib/transaction_priority.dart    | 19 ++++++
 cw_decred/lib/wallet.dart                  | 69 +++++++++++++++++++---
 lib/entities/priority_for_wallet_type.dart |  3 +
 4 files changed, 92 insertions(+), 11 deletions(-)

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 [];
   }