Fee estimation

This commit is contained in:
Hector Chu 2024-04-25 15:23:01 +01:00
parent 404672f10f
commit 4abe70062f
5 changed files with 100 additions and 30 deletions

View file

@ -331,14 +331,19 @@ class ElectrumClient {
// "height": 520481,
// "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4"
// }
Future<int?> getCurrentBlockChainTip() =>
call(method: 'blockchain.headers.subscribe').then((result) {
if (result is Map<String, dynamic>) {
return result["height"] as int;
}
BehaviorSubject<Map<String, dynamic>>? tipListener;
int? currentTip;
return null;
});
Future<int?> 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<Object>? scripthashUpdate(String scripthash) {
_id += 1;
@ -424,6 +429,12 @@ class ElectrumClient {
void _methodHandler({required String method, required Map<String, dynamic> request}) {
switch (method) {
case 'blockchain.headers.subscribe':
final params = request['params'] as List<dynamic>;
final id = 'blockchain.headers.subscribe';
_tasks[id]?.subject?.add(params.last);
break;
case 'blockchain.scripthash.subscribe':
final params = request['params'] as List<dynamic>;
final scripthash = params.first as String?;

View file

@ -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<int> calcFee({
required List<UtxoWithAddress> utxos,
required List<BitcoinBaseOutput> 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<PendingTransaction> createTransaction(Object credentials) async {
try {

View file

@ -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<int> calcFee({
required List<UtxoWithAddress> utxos,
required List<BitcoinBaseOutput> 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<int>(0, (acc, output) => acc + output.toOutput.amount.toInt());
final posOutputSum = tx.outputs.fold<int>(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<PendingTransaction> createTransaction(Object credentials) async {
final tx = await super.createTransaction(credentials);
return tx;
}
}

View file

@ -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()
}

View file

@ -4,13 +4,10 @@ import 'cw_mweb_platform_interface.dart';
import 'mwebd.pbgrpc.dart';
class CwMweb {
static Future<int?>? port;
static Future<RpcClient> 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())));
}