From 4abe70062fbfa7f1896c3d63235a49bafabf4240 Mon Sep 17 00:00:00 2001 From: Hector Chu Date: Thu, 25 Apr 2024 15:23:01 +0100 Subject: [PATCH] Fee estimation --- cw_bitcoin/lib/electrum.dart | 25 ++++++--- cw_bitcoin/lib/electrum_wallet.dart | 52 ++++++++++++------- cw_bitcoin/lib/litecoin_wallet.dart | 44 ++++++++++++++++ .../com/cakewallet/mweb/CwMwebPlugin.kt | 4 +- cw_mweb/lib/cw_mweb.dart | 5 +- 5 files changed, 100 insertions(+), 30 deletions(-) diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 0553170cc..2b3f490a3 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -331,14 +331,19 @@ class ElectrumClient { // "height": 520481, // "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4" // } - Future getCurrentBlockChainTip() => - call(method: 'blockchain.headers.subscribe').then((result) { - if (result is Map) { - return result["height"] as int; - } + BehaviorSubject>? tipListener; + int? currentTip; - return null; - }); + Future getCurrentBlockChainTip() async { + final method = 'blockchain.headers.subscribe'; + final cb = (result) => currentTip = result['height'] as int; + if (tipListener == null) { + tipListener = subscribe(id: method, method: method); + tipListener?.listen(cb); + cb(await call(method: method)); + } + return currentTip; + } BehaviorSubject? scripthashUpdate(String scripthash) { _id += 1; @@ -424,6 +429,12 @@ class ElectrumClient { void _methodHandler({required String method, required Map request}) { switch (method) { + case 'blockchain.headers.subscribe': + final params = request['params'] as List; + final id = 'blockchain.headers.subscribe'; + + _tasks[id]?.subject?.add(params.last); + break; case 'blockchain.scripthash.subscribe': final params = request['params'] as List; final scripthash = params.first as String?; diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 37937f45c..1f5c14319 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -392,24 +392,13 @@ abstract class ElectrumWalletBase value: BigInt.from(amountLeftForChangeAndFee), )); - int estimatedSize; - if (network is BitcoinCashNetwork) { - estimatedSize = ForkedTransactionBuilder.estimateTransactionSize( - utxos: utxos, - outputs: outputs, - network: network as BitcoinCashNetwork, - memo: memo, - ); - } else { - estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize( - utxos: utxos, - outputs: outputs, - network: network, - memo: memo, - ); - } - - int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize); + int fee = await calcFee( + utxos: utxos, + outputs: outputs, + network: network, + memo: memo, + feeRate: feeRate, + ); if (fee == 0) { throw BitcoinTransactionNoFeeException(); @@ -499,6 +488,33 @@ abstract class ElectrumWalletBase ); } + Future calcFee({ + required List utxos, + required List outputs, + required BasedUtxoNetwork network, + String? memo, + required int feeRate}) async { + + int estimatedSize; + if (network is BitcoinCashNetwork) { + estimatedSize = ForkedTransactionBuilder.estimateTransactionSize( + utxos: utxos, + outputs: outputs, + network: network, + memo: memo, + ); + } else { + estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize( + utxos: utxos, + outputs: outputs, + network: network, + memo: memo, + ); + } + + return feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize); + } + @override Future createTransaction(Object credentials) async { try { diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index adce6e6b7..b84675669 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1,12 +1,15 @@ import 'dart:async'; +import 'dart:math'; import 'package:convert/convert.dart'; import 'package:crypto/crypto.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/unspent_coins_info.dart'; @@ -279,4 +282,45 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return 0; } + + @override + Future calcFee({ + required List utxos, + required List outputs, + required BasedUtxoNetwork network, + String? memo, + required int feeRate}) async { + + final txb = BitcoinTransactionBuilder(utxos: utxos, + outputs: outputs, fee: BigInt.zero, network: network); + final scanSecret = mwebHd.derive(0x80000000).privKey!; + final spendSecret = mwebHd.derive(0x80000001).privKey!; + final stub = await CwMweb.stub(); + final resp = await stub.create(CreateRequest( + rawTx: txb.buildTransaction((a, b, c, d) => '').toBytes(), + scanSecret: hex.decode(scanSecret), + spendSecret: hex.decode(spendSecret), + feeRatePerKb: Int64(feeRate * 1000), + dryRun: true)); + final tx = BtcTransaction.fromRaw(hex.encode(resp.rawTx)); + final posUtxos = utxos.where((utxo) => tx.inputs.any((input) => + input.txId == utxo.utxo.txHash && input.txIndex == utxo.utxo.vout)).toList(); + final preOutputSum = outputs.fold(0, (acc, output) => acc + output.toOutput.amount.toInt()); + final posOutputSum = tx.outputs.fold(0, (acc, output) => acc + output.amount.toInt()); + final mwebInputSum = utxos.sumOfUtxosValue() - posUtxos.sumOfUtxosValue(); + final expectedPegin = max(0, preOutputSum - mwebInputSum.toInt()); + var fee = posOutputSum - expectedPegin; + if (expectedPegin > 0) { + fee += await super.calcFee(utxos: posUtxos, outputs: tx.outputs.map((output) => + BitcoinScriptOutput(script: output.scriptPubKey, value: output.amount)).toList(), + network: network, memo: memo, feeRate: feeRate) + feeRate * 41; + } + return fee; + } + + @override + Future createTransaction(Object credentials) async { + final tx = await super.createTransaction(credentials); + return tx; + } } diff --git a/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt b/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt index cf194417b..1dfbe12e5 100644 --- a/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt +++ b/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt @@ -17,6 +17,7 @@ class CwMwebPlugin: FlutterPlugin, MethodCallHandler { /// This local reference serves to register the plugin with the Flutter Engine and unregister it /// when the Flutter Engine is detached from the Activity private lateinit var channel : MethodChannel + private var port: Long? = null override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, "cw_mweb") @@ -26,7 +27,8 @@ class CwMwebPlugin: FlutterPlugin, MethodCallHandler { override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { if (call.method == "start") { val dataDir = call.argument("dataDir") ?: "" - result.success(Mwebd.newServer("", dataDir, "").start(0)) + port = port ?: Mwebd.newServer("", dataDir, "").start(0) + result.success(port) } else { result.notImplemented() } diff --git a/cw_mweb/lib/cw_mweb.dart b/cw_mweb/lib/cw_mweb.dart index a54295673..f16d451d9 100644 --- a/cw_mweb/lib/cw_mweb.dart +++ b/cw_mweb/lib/cw_mweb.dart @@ -4,13 +4,10 @@ import 'cw_mweb_platform_interface.dart'; import 'mwebd.pbgrpc.dart'; class CwMweb { - static Future? port; - static Future stub() async { final appDir = await getApplicationSupportDirectory(); - port ??= CwMwebPlatform.instance.start(appDir.path); return RpcClient(ClientChannel('127.0.0.1', - port: await port ?? 0, + port: await CwMwebPlatform.instance.start(appDir.path) ?? 0, options: const ChannelOptions( credentials: ChannelCredentials.insecure()))); }