diff --git a/lib/src/crypto/utxo/entities/op_codes.dart b/lib/src/crypto/utxo/entities/op_codes.dart index f04a9d8bd..dbe564c4d 100644 --- a/lib/src/crypto/utxo/entities/op_codes.dart +++ b/lib/src/crypto/utxo/entities/op_codes.dart @@ -2,6 +2,8 @@ // TODO: Refactor this class to be more readable +import 'package:collection/collection.dart'; + enum OPCODE { /// Constants OP_0(0x00), @@ -147,6 +149,10 @@ enum OPCODE { return hex.toRadixString(16); } + + static OPCODE? fromHex(int hex) { + return OPCODE.values.singleWhereOrNull((element) => element.hex == hex); + } } /// Constants diff --git a/lib/src/crypto/utxo/entities/payments/p2h.dart b/lib/src/crypto/utxo/entities/payments/p2h.dart index f2c90b348..b8d0ae3ca 100644 --- a/lib/src/crypto/utxo/entities/payments/p2h.dart +++ b/lib/src/crypto/utxo/entities/payments/p2h.dart @@ -124,22 +124,6 @@ class P2Hash { OPCODE.OP_EQUAL.hex, ].toUint8List; } - - /// - /// Utility functions - /// - static Uint8List toP2PKHScript(Uint8List segWitScript) { - final pubkeyhash = segWitScript.sublist(2); - - return [ - OPCODE.OP_DUP.hex, - OPCODE.OP_HASH160.hex, - pubkeyhash.length, - ...pubkeyhash, - OPCODE.OP_EQUALVERIFY.hex, - OPCODE.OP_CHECKSIG.hex, - ].toUint8List; - } } /// diff --git a/lib/src/crypto/utxo/entities/payments/pk_script_converter.dart b/lib/src/crypto/utxo/entities/payments/pk_script_converter.dart deleted file mode 100644 index 8f69cf99e..000000000 --- a/lib/src/crypto/utxo/entities/payments/pk_script_converter.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'dart:typed_data'; - -import 'package:walletkit_dart/src/crypto/utxo/entities/op_codes.dart'; -import 'package:walletkit_dart/src/domain/extensions.dart'; - -class PublicKeyScriptConverter { - final Uint8List script; - - late final Uint8List publicKeyHash; - - PublicKeyScriptConverter(this.script) { - if (script.length == 0) { - throw Exception("Script length is 0"); - } - - // P2PKH - if (script.length == 25) { - if (script[0] != 25 && - script[1] != OPCODE.OP_DUP.hex && - script[2] != OPCODE.OP_HASH160.hex && - script[23] != OPCODE.OP_EQUALVERIFY.hex && - script.last != OPCODE.OP_CHECKSIG.hex) { - throw Exception("Script is not P2PKH"); - } - publicKeyHash = script.sublist(3, 23); - return; - } - - // P2WPKH - if (script.length == 22) { - if (script[0] != OPCODE.OP_0.hex && script[1] != 20) { - throw Exception("Script is not P2WPKH"); - } - publicKeyHash = script.sublist(2, 22); - return; - } - - // P2SH - if (script.length == 23) { - if (script.first != 23 && - script[1] != OPCODE.OP_HASH160.hex && - script[2] != 20 && - script.last != OPCODE.OP_EQUAL.hex) { - throw Exception("Script is not P2SH"); - } - publicKeyHash = script.sublist(3, 23); - return; - } - - // P2WPKH - if (script.length == 34) { - if (script.first != 34 && script[1] != OPCODE.OP_0.hex) { - throw Exception("Script is not P2WPKH"); - } - publicKeyHash = script.sublist(2, 34); - return; - } - - throw Exception("Unknown script type"); - } - - Uint8List get p2wpkhScript { - assert(publicKeyHash.length == 20, "publicKeyHash.length != 20"); - return [ - OPCODE.OP_0.hex, - publicKeyHash.length, - ...publicKeyHash, - ].toUint8List; - } - - Uint8List get p2shScript { - assert(publicKeyHash.length == 20, "publicKeyHash.length != 20"); - return [ - OPCODE.OP_HASH160.hex, - publicKeyHash.length, - ...publicKeyHash, - OPCODE.OP_EQUAL.hex, - ].toUint8List; - } - - Uint8List get p2pkhScript { - assert(publicKeyHash.length == 20, "publicKeyHash.length != 20"); - return [ - OPCODE.OP_DUP.hex, - OPCODE.OP_HASH160.hex, - publicKeyHash.length, - ...publicKeyHash, - OPCODE.OP_EQUALVERIFY.hex, - OPCODE.OP_CHECKSIG.hex, - ].toUint8List; - } - - Uint8List get p2pkScript { - assert(publicKeyHash.length == 33, "publicKeyHash.length != 33"); - return [ - OPCODE.OP_DUP.hex, - OPCODE.OP_HASH160.hex, - publicKeyHash.length, - ...publicKeyHash, - OPCODE.OP_EQUALVERIFY.hex, - OPCODE.OP_CHECKSIG.hex, - ].toUint8List; - } - - Uint8List get p2wshScript { - assert(publicKeyHash.length == 32, "publicKeyHash.length != 32"); - return [ - OPCODE.OP_0.hex, - publicKeyHash.length, - ...publicKeyHash, - ].toUint8List; - } -} diff --git a/lib/src/crypto/utxo/entities/raw_transaction/input.dart b/lib/src/crypto/utxo/entities/raw_transaction/input.dart index 031a9a0bc..1511f36eb 100644 --- a/lib/src/crypto/utxo/entities/raw_transaction/input.dart +++ b/lib/src/crypto/utxo/entities/raw_transaction/input.dart @@ -1,6 +1,8 @@ import 'dart:typed_data'; import 'package:convert/convert.dart'; +import 'package:walletkit_dart/src/crypto/utxo/entities/raw_transaction/output.dart'; +import 'package:walletkit_dart/src/crypto/utxo/entities/raw_transaction/tx_structure.dart'; import 'package:walletkit_dart/src/crypto/utxo/entities/script.dart'; import 'package:walletkit_dart/src/crypto/utxo/entities/op_codes.dart'; import 'package:walletkit_dart/src/utils/int.dart'; @@ -13,149 +15,81 @@ const sequence_length = 4; sealed class Input { final Uint8List txid; final int vout; - final Uint8List? _scriptSig; - final Uint8List? _wittnessScript; - final BigInt? value; - final Uint8List? _prevScriptPubKey; + + final Output? prevOutput; + + final BTCScript? script; + + BigInt? get value => prevOutput?.value; const Input({ required this.txid, required this.vout, - this.value, - Uint8List? prevScriptPubKey, - Uint8List? scriptSig, - Uint8List? wittnessScript, - }) : _scriptSig = scriptSig, - _prevScriptPubKey = prevScriptPubKey, - _wittnessScript = wittnessScript; + required this.prevOutput, + required this.script, + }); - BigInt get weight { - if (_scriptSig == null || _prevScriptPubKey == null) return -1.toBI; - return calculateWeight(_prevScriptPubKey!, _scriptSig!); - } + // BigInt get witnessSize { + // if (_wittnessScript == null || _wittnessScript!.isEmpty) return 0.toBI; - int get intValue => value != null ? value!.toInt() : 0; + // BigInt size = getVarIntSize(_wittnessScript!.length) + // .toBI; // Count of witness elements - String? get scriptSigHex => _scriptSig != null ? _scriptSig!.toHex : null; + // final wittnessChunks = + // decodeScriptWittness(wittnessScript: _wittnessScript!); - String get txIdString => hex.encode(txid); + // for (final chunk in wittnessChunks) { + // size += getVarIntSize(chunk.length).toBI; // Size of this element + // size += chunk.length.toBI; // The element itself + // } - Uint8List get scriptSig => _scriptSig ?? Uint8List(0); + // return size; + // } - Uint8List get wittnessScript => _wittnessScript ?? Uint8List(0); + // BigInt get scriptSize { + // if (_scriptSig == null || _scriptSig!.isEmpty) return 0.toBI; - Uint8List get bytes; + // return getVarIntSize(_scriptSig!.length).toBI + _scriptSig!.length.toBI; + // } - int get size => bytes.length; - - String get toHex => hex.encode(bytes); + BigInt get weight; - Uint8List get previousScriptPubKey => _prevScriptPubKey ?? Uint8List(0); + int get intValue => value != null ? value!.toInt() : 0; - bool get isP2SH => - previousScriptPubKey.length == 23 && - previousScriptPubKey[0] == OP_HASH160 && - previousScriptPubKey[1] == 0x14 && - previousScriptPubKey[22] == OP_EQUAL; + String get txIdString => hex.encode(txid); - bool get isP2PKH => - previousScriptPubKey.length == 25 && - previousScriptPubKey[0] == OP_DUP && - previousScriptPubKey[1] == OP_HASH160 && - previousScriptPubKey[2] == 0x14 && - previousScriptPubKey[23] == OP_EQUALVERIFY && - previousScriptPubKey[24] == OP_CHECKSIG; + Uint8List get bytes; - bool get isP2WPKH => - previousScriptPubKey.length == 22 && - previousScriptPubKey[0] == 0x00 && - previousScriptPubKey[1] == 0x14; + int get size => bytes.length; - bool get isP2WSH => - previousScriptPubKey.length == 34 && - previousScriptPubKey[0] == 0x00 && - previousScriptPubKey[1] == 0x20; + String get toHex => hex.encode(bytes); - bool get isP2PK => - previousScriptPubKey.length == 35 && previousScriptPubKey[0] == 0x21; + BTCLockingScript? get previousScript => prevOutput?.script; - bool get isSegwit => isP2WPKH || isP2WSH || hasWitness; + bool get isSegwit => + hasWitness || + previousScript is PayToWitnessScriptHashScript || + previousScript is PayToWitnessPublicKeyHashScript; - bool get hasWitness => _wittnessScript != null; + bool get hasWitness => script is ScriptWitness; Uint8List get publicKeyFromSig { - /// From ScriptSig (P2PKH, P2PK) - if (_scriptSig != null && _scriptSig!.isNotEmpty) { - final script = Script(_scriptSig!); - - final publicKey = script.chunks[1].data; - if (publicKey == null) { - throw Exception("Invalid Public Key"); - } - if (publicKey.length != 33) { - throw Exception("Invalid Public Key"); - } - return publicKey; - } - - /// From Witness - if (_wittnessScript != null && _wittnessScript!.isNotEmpty) { - final chunks = decodeScriptWittness(wittnessScript: _wittnessScript!); - if (chunks.length != 2) { - throw Exception("Invalid Witness"); - } - final publicKey = chunks[1]; - if (publicKey.length != 33) { - throw Exception("Invalid Public Key"); - } - return publicKey; - } - - throw Exception("No ScriptSig or Witness found"); + return switch (script) { + ScriptSignature scripSig => scripSig.publicKey, + ScriptWitness scriptWitness => scriptWitness.publicKey, + RedeemScript redeemScript => throw Exception("Redeem Script"), + _ => throw Exception("Unknown Script"), + }; } - Input addScript({Uint8List? scriptSig, Uint8List? wittnessScript}); - - BigInt calculateWeight( - Uint8List prevScriptPubKey, - Uint8List? scriptSig, - ) { - if (scriptSig == null || prevScriptPubKey.isEmpty) { - return 0.toBI; - } - - BigInt w = 1.toBI + getScriptWeight(prevScriptPubKey); - - if (!isP2SH) return w; - - final script = Script(scriptSig); - - Uint8List? buffer = Uint8List(0); - - for (final chunk in script.chunks) { - if (buffer != null) { - buffer = chunk.data; - } - if (chunk.opcode > OP_16) { - return weight; - } - } - - if (buffer != null && buffer.isNotEmpty) { - w += getScriptWeight(buffer); - } - - return w; - } + Input addScript(BTCScript script); BTCInput changeSequence(int sequence) { return BTCInput( txid: txid, vout: vout, - value: value, - scriptSig: _scriptSig, - prevScriptPubKey: _prevScriptPubKey, - wittnessScript: _wittnessScript, + script: script, + prevOutput: prevOutput, sequence: sequence, ); } @@ -167,10 +101,8 @@ class BTCInput extends Input { const BTCInput({ required super.txid, required super.vout, - required super.value, - super.scriptSig, - super.prevScriptPubKey, - super.wittnessScript, + required super.prevOutput, + required super.script, this.sequence = 0xffffffff, }); @@ -197,35 +129,31 @@ class BTCInput extends Input { txid: txid, vout: vout, sequence: sequence, - scriptSig: script, - value: null, + script: BTCUnlockingScript.fromBuffer(script), + prevOutput: null, ); } - BTCInput addScript({ - Uint8List? scriptSig, - Uint8List? wittnessScript, - }) { - final _scriptSig = scriptSig ?? this._scriptSig; - final _witnessScript = wittnessScript ?? _wittnessScript; - + BTCInput addScript(BTCScript script) { return BTCInput( txid: txid, vout: vout, - scriptSig: _scriptSig, - prevScriptPubKey: previousScriptPubKey, - wittnessScript: _witnessScript, - value: value, + script: script, + prevOutput: prevOutput, sequence: sequence, ); } Uint8List get bytes { + assert(script != null, "Script is required"); + + final _script = script!; + final buffer = Uint8List( txid.length + output_index_length + - scriptSig.length + - 1 + + _script.size + + getVarIntSize(_script.size) + sequence_length, ); @@ -236,14 +164,28 @@ class BTCInput extends Input { // Write Vout offset += buffer.bytes.writeUint32(offset, vout); - // Write ScriptSig - offset += buffer.writeVarSlice(offset, scriptSig); + // Write Unlocking Script + offset += buffer.writeVarSlice(offset, _script.bytes); // Write Sequence offset += buffer.bytes.writeUint32(offset, sequence); return buffer; } + + @override + BigInt get weight { + final weight = (txid.length + output_index_length + sequence_length).toBI * + 4.toBI; // (32 + 4 + 4) * 4 + + return switch (script!) { + ScriptSignature sig => weight + (sig.weight * 4.toBI), + ScriptWitness witness => weight + witness.weight, + RedeemScript redeem => + weight + redeem.weight, // TODO: I think this doesnt make sense + _ => throw Exception("Unknown Script"), + }; + } } const value_length = 8; @@ -253,37 +195,70 @@ class EC8Input extends Input { const EC8Input({ required super.txid, required super.vout, - required super.value, - super.prevScriptPubKey, - super.scriptSig, - super.wittnessScript, + required super.script, + required super.prevOutput, }); - EC8Input addScript({ + @override + BigInt get weight { + throw UnimplementedError(); + // if (_scriptSig == null || _prevScriptPubKey == null) return -1.toBI; + // return calculateWeight(_prevScriptPubKey!, _scriptSig!); + } + + BigInt calculateWeight( + Uint8List prevScriptPubKey, Uint8List? scriptSig, - Uint8List? wittnessScript, - }) { - final _scriptSig = scriptSig ?? this._scriptSig; - final _witnessScript = wittnessScript ?? _wittnessScript; + ) { + throw UnimplementedError(); + // if (scriptSig == null || prevScriptPubKey.isEmpty) { + // return 0.toBI; + // } + + // BigInt w = 1.toBI + getScriptWeight(prevScriptPubKey); + + // if (!isP2SH) return w; + + // final script = Script(scriptSig); + // Uint8List? buffer = Uint8List(0); + + // for (final chunk in script.chunks) { + // if (buffer != null) { + // buffer = chunk.data; + // } + // if (chunk.opcode > OP_16) { + // return weight; + // } + // } + + // if (buffer != null && buffer.isNotEmpty) { + // w += getScriptWeight(buffer); + // } + + // return w; + } + + EC8Input addScript(BTCScript script) { return EC8Input( txid: txid, vout: vout, - scriptSig: _scriptSig, - value: value, - prevScriptPubKey: previousScriptPubKey, - wittnessScript: _witnessScript, + script: script, + prevOutput: prevOutput, ); } Uint8List get bytes { + assert(script != null, "Script is required"); + final _script = script!; + final buffer = Uint8List( txid.length + output_index_length + value_length + weight_length + - scriptSig.length + - 1, + _script.size + + getVarIntSize(_script.size), ); var offset = 0; @@ -300,7 +275,7 @@ class EC8Input extends Input { offset += buffer.bytes.writeUint32(offset, weight.toInt()); // Write ScriptSig - offset += buffer.writeVarSlice(offset, scriptSig); + offset += buffer.writeVarSlice(offset, _script.bytes); return buffer; } @@ -333,12 +308,16 @@ class EC8Input extends Input { required bool withWeight, required bool withScript, }) { + assert(withWeight || withScript, "At least one of the two is required"); + assert(script != null, "Script is required"); + final _script = script!; + final buffer = Uint8List( txid.length + output_index_length + value_length + (withWeight ? weight_length : 0) + - (withScript ? scriptSig.length + 1 : 0), + (withScript ? _script.size + getVarIntSize(_script.size) : 0), ); var offset = 0; @@ -357,7 +336,7 @@ class EC8Input extends Input { if (withScript) { // Write ScriptSig - offset += buffer.writeVarSlice(offset, scriptSig); + offset += buffer.writeVarSlice(offset, _script.bytes); } return buffer; } @@ -382,44 +361,18 @@ class EC8Input extends Input { offset += off4; /// ScriptSig - final (scriptSig, off5) = buffer.readVarSlice(offset); + final (script, off5) = buffer.readVarSlice(offset); offset += off5; return EC8Input( txid: txid, vout: vout, - scriptSig: scriptSig, - value: BigInt.from(value), + script: BTCUnlockingScript.fromBuffer(script), + prevOutput: null, ); } } -(Uint8List, int) readScriptWittness({ - required Uint8List buffer, - required int offset, -}) { - final (count, off1) = buffer.bytes.readVarInt(offset); - offset += off1; - - final scripts = []; - - for (var i = 0; i < count; i++) { - final (script, off2) = buffer.readVarSlice(offset); - offset += off2; - scripts.add(script); - } - - final wittnessScript = [ - count, - for (final script in scripts) ...[ - script.length, - ...script, - ], - ].toUint8List; - - return (wittnessScript, wittnessScript.length); -} - List decodeScriptWittness({ required Uint8List wittnessScript, }) { diff --git a/lib/src/crypto/utxo/entities/raw_transaction/output.dart b/lib/src/crypto/utxo/entities/raw_transaction/output.dart index e44d58eef..4387f6e17 100644 --- a/lib/src/crypto/utxo/entities/raw_transaction/output.dart +++ b/lib/src/crypto/utxo/entities/raw_transaction/output.dart @@ -2,6 +2,7 @@ import 'dart:typed_data'; import 'package:convert/convert.dart'; import 'package:walletkit_dart/src/crypto/utxo/entities/raw_transaction/input.dart'; +import 'package:walletkit_dart/src/crypto/utxo/entities/raw_transaction/tx_structure.dart'; import 'package:walletkit_dart/src/crypto/utxo/entities/script.dart'; import 'package:walletkit_dart/src/utils/int.dart'; import 'package:walletkit_dart/src/utils/var_uint.dart'; @@ -10,13 +11,11 @@ const value_length = 8; abstract class Output { final BigInt value; - final Uint8List scriptPubKey; - - BigInt get weight => 1.toBI + getScriptWeight(scriptPubKey); + final BTCLockingScript script; int get intValue => value.toInt(); - String get scriptPubKeyHex => hex.encode(scriptPubKey); + String get scriptHex => script.hex; int get size => bytes.length; @@ -24,16 +23,18 @@ abstract class Output { String get toHex => hex.encode(bytes); + BigInt get weight => script.weight; + const Output({ required this.value, - required this.scriptPubKey, + required this.script, }); } class BTCOutput extends Output { const BTCOutput({ required super.value, - required super.scriptPubKey, + required super.script, }); factory BTCOutput.fromBuffer(Uint8List buffer) { @@ -44,17 +45,17 @@ class BTCOutput extends Output { offset += off1; /// ScriptPubKey - final (scriptPubKey, off2) = buffer.readVarSlice(offset); + final (script, off2) = buffer.readVarSlice(offset); offset += off2; return BTCOutput( value: BigInt.from(value), - scriptPubKey: scriptPubKey, + script: BTCLockingScript.fromBuffer(script), ); } Uint8List get bytes { - final buffer = Uint8List(value_length + scriptPubKey.length + 1); + final buffer = Uint8List(value_length + script.size + 1); var offset = 0; @@ -62,7 +63,7 @@ class BTCOutput extends Output { offset += buffer.bytes.writeUint64(offset, intValue); // Write ScriptPubKey - offset += buffer.writeVarSlice(offset, scriptPubKey); + offset += buffer.writeVarSlice(offset, script.bytes); return buffer; } @@ -71,12 +72,12 @@ class BTCOutput extends Output { class EC8Output extends Output { const EC8Output({ required super.value, - required super.scriptPubKey, + required super.script, }); Uint8List get bytes { final buffer = Uint8List( - value_length + weight_length + scriptPubKey.length + 1, + value_length + weight_length + script.size + 1, ); var offset = 0; @@ -88,14 +89,14 @@ class EC8Output extends Output { offset += buffer.bytes.writeUint32(offset, weight.toInt()); // Should be 146 // Write ScriptPubKey - offset += buffer.writeVarSlice(offset, scriptPubKey); + offset += buffer.writeVarSlice(offset, script.bytes); return buffer; } Uint8List get bytesForTxId { final buffer = Uint8List( - value_length + weight_length + scriptPubKey.length + 1, + value_length + weight_length + script.size + 1, ); var offset = 0; @@ -107,7 +108,7 @@ class EC8Output extends Output { offset += buffer.bytes.writeUint32(offset, 0); // Write ScriptPubKey - offset += buffer.writeVarSlice(offset, scriptPubKey); + offset += buffer.writeVarSlice(offset, script.bytes); return buffer; } @@ -123,13 +124,13 @@ class EC8Output extends Output { final (_, off2) = buffer.bytes.readUint32(offset); offset += off2; - /// ScriptPubKey - final (scriptPubKey, off3) = buffer.readVarSlice(offset); + /// Script + final (script, off3) = buffer.readVarSlice(offset); offset += off3; return EC8Output( value: value.toBI, - scriptPubKey: scriptPubKey, + script: BTCLockingScript.fromBuffer(script), ); } } diff --git a/lib/src/crypto/utxo/entities/raw_transaction/raw_transaction.dart b/lib/src/crypto/utxo/entities/raw_transaction/raw_transaction.dart index afe57872b..a150671c7 100644 --- a/lib/src/crypto/utxo/entities/raw_transaction/raw_transaction.dart +++ b/lib/src/crypto/utxo/entities/raw_transaction/raw_transaction.dart @@ -1,7 +1,7 @@ import 'dart:typed_data'; import 'package:collection/collection.dart'; -import 'package:walletkit_dart/src/crypto/utxo/entities/payments/pk_script_converter.dart'; +import 'package:walletkit_dart/src/crypto/utxo/entities/raw_transaction/tx_structure.dart'; import 'package:walletkit_dart/src/crypto/utxo/utils/pubkey_to_address.dart'; import 'package:walletkit_dart/src/crypto/utxo/entities/raw_transaction/input.dart'; import 'package:walletkit_dart/src/crypto/utxo/entities/raw_transaction/output.dart'; @@ -21,15 +21,12 @@ sealed class RawTransaction { /// Non Null if returned from [buildUnsignedTransaction] final Map? inputMap; - BigInt get weight { - return inputs.fold( - 0.toBI, - (prev, input) => prev + input.weight, - ) + - outputs.fold( - 0.toBI, - (prev, output) => prev + output.weight, - ); + /// Weight of the transaction + BigInt get weight; + + /// Virtual Size + BigInt get vSize { + return weight ~/ 4.toBI; } Uint8List get bytes; @@ -40,6 +37,8 @@ sealed class RawTransaction { BigInt get fee => totalInputValue - totalOutputValue; + double get feePerByte => fee.toInt() / size; + // Value of the first output BigInt get targetAmount => outputs.first.value; @@ -65,22 +64,22 @@ sealed class RawTransaction { Uint8List legacySigHash({ required int index, - required Uint8List prevScriptPubKey, + required BTCLockingScript prevScript, required int hashType, + required UTXONetworkType networkType, }) { + assert( + hashType == networkType.sighash.all, "Only SIGHASH_ALL is supported"); + final copy = createCopy(); // clear all scriptSigs for (int i = 0; i < copy.inputs.length; i++) { - copy.inputs[i] = copy.inputs[i].addScript( - scriptSig: Uint8List.fromList([]), - ); + copy.inputs[i] = copy.inputs[i].addScript(EmptyUnlockingScript()); } // set scriptSig for inputIndex to prevScriptPubKeyHex - copy.inputs[index] = copy.inputs[index].addScript( - scriptSig: prevScriptPubKey, - ); + copy.inputs[index] = copy.inputs[index].addScript(prevScript); final bytes = copy is EC8RawTransaction ? copy.bytesForSigning : copy.bytes; @@ -192,6 +191,33 @@ class BTCRawTransaction extends RawTransaction { outputs: outputs, ); + @override + BigInt get weight { + // Base size * 4 + BigInt weight = 8.toBI * 4.toBI; // version + locktime + + if (isSegwit) { + weight += 2.toBI * 4.toBI; // Segwit Marker + Segwit Flag + } + + final inputWeight = inputs.fold( + 0.toBI, + (prev, input) => prev + input.weight, + ); + final outputWeight = outputs.fold( + 0.toBI, + (prev, output) => prev + output.weight, + ); + + weight += inputWeight + outputWeight; + + return weight; + } + + bool get isSegwit { + return inputs.any((input) => input.isSegwit); + } + bool get hasWitness { return inputs.any((input) => input.hasWitness); } @@ -270,7 +296,7 @@ class BTCRawTransaction extends RawTransaction { /// Witness if (isSegwit) { - List<(Uint8List, BTCInput)> wittnessScripts = []; + List<(BTCUnlockingScript, BTCInput)> wittnessScripts = []; for (final input in inputs) { final (emptyScript, emptyScriptLength) = buffer.bytes.readUint8(offset); @@ -279,15 +305,17 @@ class BTCRawTransaction extends RawTransaction { continue; } - final (wittnessScript, length) = - readScriptWittness(buffer: buffer, offset: offset); - wittnessScripts.add((wittnessScript, input)); - offset += length; + final witness = ScriptWitness.fromScript( + buffer.sublist(offset), + ); + + wittnessScripts.add((witness, input)); + offset += witness.size; } for (final (wittnessScript, input) in wittnessScripts) { final index = inputs.indexOf(input); - inputs[index] = input.addScript(wittnessScript: wittnessScript); + inputs[index] = input.addScript(wittnessScript); } } @@ -332,7 +360,7 @@ class BTCRawTransaction extends RawTransaction { 0, (prev, input) { assert(input.isSegwit); - return prev + input.wittnessScript.length; + return prev + input.script!.size; }, ); txByteLength += nonSegwitInputs.length; // Empty Script @@ -369,7 +397,7 @@ class BTCRawTransaction extends RawTransaction { if (hasWitness) for (final input in inputs) { if (input.isSegwit) { - offset += buffer.writeSlice(offset, input.wittnessScript); + offset += buffer.writeSlice(offset, input.script!.bytes); // TODO: Fix continue; } @@ -384,18 +412,22 @@ class BTCRawTransaction extends RawTransaction { /// /// BIP143 SigHash: https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki + /// Currently only support SIG_HASH_ALL /// Uint8List bip143sigHash({ required int index, - required Uint8List prevScriptPubKey, + required BTCLockingScript prevScript, required ElectrumOutput output, required int hashType, + required UTXONetworkType networkType, }) { + assert( + hashType == networkType.sighash.all, "Only SIGHASH_ALL is supported"); + /// /// Always use P2PKH in bip143 sigHash /// - final converter = PublicKeyScriptConverter(prevScriptPubKey); - final p2pkhScript = converter.p2pkhScript; + final p2pkhScript = PayToPublicKeyHashScript(prevScript.data); final outputBuffers = outputs.map((output) => output.bytes); final txOutSize = outputBuffers.fold( @@ -429,13 +461,13 @@ class BTCRawTransaction extends RawTransaction { for (final output in outputs) { tOffset += tBuffer.bytes.writeUint64(tOffset, output.value.toInt()); - tOffset += tBuffer.writeVarSlice(tOffset, output.scriptPubKey); + tOffset += tBuffer.writeVarSlice(tOffset, output.script.bytes); } final hashOutputs = sha256Sha256Hash(tBuffer); /// Final Buffer final inputToSign = inputs[index]; - final prevScriptPubKeyLength = varSliceSize(p2pkhScript); + final prevScriptPubKeyLength = varSliceSize(p2pkhScript.bytes); tBuffer = Uint8List(156 + prevScriptPubKeyLength); tOffset = 0; @@ -446,7 +478,7 @@ class BTCRawTransaction extends RawTransaction { tOffset += tBuffer.writeSlice(tOffset, inputToSign.txid); tOffset += tBuffer.bytes.writeUint32(tOffset, inputToSign.vout); - tOffset += tBuffer.writeVarSlice(tOffset, p2pkhScript); + tOffset += tBuffer.writeVarSlice(tOffset, p2pkhScript.bytes); tOffset += tBuffer.bytes.writeUint64(tOffset, output.value.toInt()); tOffset += tBuffer.bytes.writeUint32(tOffset, inputToSign.sequence); @@ -484,6 +516,18 @@ class EC8RawTransaction extends RawTransaction { outputs: outputs, ); + /// EC8 Transaction weight is calculated differently + BigInt get weight { + return inputs.fold( + 0.toBI, + (prev, input) => prev + input.weight, + ) + + outputs.fold( + 0.toBI, + (prev, output) => prev + output.weight, + ); + } + String get txid { final buffer = bytesForTxId; final hash = sha256Sha256Hash(buffer); @@ -644,7 +688,7 @@ class EC8RawTransaction extends RawTransaction { /// Input to be signed has a scriptSig all other inputs have empty scriptSigs final input = inputs.singleWhereOrNull( - (input) => input.scriptSig.length != 0, + (input) => input.script!.size != 0, ); if (input == null) { throw Exception('No input to be signed'); diff --git a/lib/src/crypto/utxo/entities/raw_transaction/tx_structure.dart b/lib/src/crypto/utxo/entities/raw_transaction/tx_structure.dart new file mode 100644 index 000000000..b90200e2e --- /dev/null +++ b/lib/src/crypto/utxo/entities/raw_transaction/tx_structure.dart @@ -0,0 +1,541 @@ +import 'dart:typed_data'; + +import 'package:bs58check/bs58check.dart' as bs58check; +import 'package:dart_bech32/dart_bech32.dart'; +import 'package:walletkit_dart/src/crypto/utxo/entities/op_codes.dart'; +import 'package:walletkit_dart/src/crypto/utxo/entities/raw_transaction/input.dart'; +import 'package:walletkit_dart/src/crypto/utxo/entities/raw_transaction/output.dart'; +import 'package:walletkit_dart/src/crypto/utxo/entities/script.dart'; +import 'package:walletkit_dart/src/crypto/utxo/utils/pubkey_to_address.dart'; +import 'package:walletkit_dart/src/utils/base32.dart'; +import 'package:walletkit_dart/src/utils/int.dart'; +import 'package:walletkit_dart/walletkit_dart.dart'; + +enum InputType { + P2PK, + P2PKH, + P2SH, + P2WPKH, + P2WSH, + P2TR, +} + +sealed class BTCScript { + final Uint8List bytes; + + const BTCScript(this.bytes); + + BigInt get weight => 1.toBI + getScriptWeight(bytes); + + String get hex => bytes.toHex; + + int get size => bytes.length; +} + +sealed class BTCLockingScript extends BTCScript { + const BTCLockingScript(super.bytes); + + Uint8List get data { + return switch (this) { + PayToPublicKeyScript script => script.publicKey, + PayToPublicKeyHashScript script => script.publicKeyHash, + PayToScriptHashScript script => script.scriptHash, + PayToWitnessPublicKeyHashScript script => script.publicKeyHash, + PayToWitnessScriptHashScript script => script.witnessScriptHash, + OPReturnScript script => script.data, + TimeLockedScript script => script.input.data, + NestedSegwitScript script => script.nestedScriptHash, + PayToTaprootScript script => script.pubkey, + AnyoneCanSpendScript _ => Uint8List(0), + EmptyLockingScript _ => Uint8List(0), + }; + } + + factory BTCLockingScript.fromAddress(String address) { + if (address.startsWith(P2PKH_PREFIX) || + address.startsWith(P2PKH_PREFIX_LTC) || + address.startsWith(P2PKH_PREFIX_ZENIQ) || + address.startsWith(P2PKH_PREFIX_EC8)) { + final decodedHex = bs58check.decode(address); + final pubKeyHash = decodedHex.sublist(1); + if (pubKeyHash.length != 40) { + throw WKFailure("wrong pubKeyHash length"); + } + + return PayToPublicKeyHashScript(pubKeyHash); + } + + if (address.startsWith(P2SH_PREFIX) || + address.startsWith(P2SH_PREFIX_LTC)) { + final scriptHash = bs58check.decode(address).sublist(1); + + return PayToScriptHashScript(scriptHash); + } + + if (address.startsWith(P2WPKH_PREFIX_BTC) || + address.startsWith(P2WPKH_PREFIX_LTC)) { + final decoded = bech32.decode(address); + var words = decoded.words; + // Remove the witness version + words = words.sublist(1); + // Convert 5-bit words to 8-bit + Uint8List publicyKeyHash; + try { + publicyKeyHash = bech32.fromWords(words); + } catch (e) { + publicyKeyHash = words; + } + + return PayToWitnessPublicKeyHashScript(publicyKeyHash); + } + + /// Remove the prefix + if (address.startsWith("bitcoincash:")) { + final _address = address.substring(12); // remove "bitcoincash:" + + final payload = Base32().decode(_address); + + final payloadData = + bech32.fromWords(payload.sublist(0, payload.length - 8)); + + final version = payloadData[0]; + final pubKeyHash = payloadData.sublist(1); + + assert(version == 0); + assert(pubKeyHash.length == 20); + + return PayToWitnessPublicKeyHashScript(pubKeyHash); + } + + throw UnimplementedError("Address type not supported: $address"); + } + + factory BTCLockingScript.fromPublicKey(Uint8List publicKey) { + return PayToPublicKeyScript(publicKey); + } + + factory BTCLockingScript.fromBuffer(Uint8List buffer) { + if (buffer.isEmpty) { + return AnyoneCanSpendScript(); + } + + if (buffer.length < 2) { + throw Exception("Invalid Script"); + } + final secondOpCode = OPCODE.fromHex(buffer[1]); + + if (secondOpCode != null && + switch (secondOpCode) { + OPCODE.OP_CHECKSEQUENCEVERIFY => true, + OPCODE.OP_CHECKLOCKTIMEVERIFY => true, + _ => false, + }) { + return TimeLockedScript.fromScript(buffer); + } + + final firstOpCode = OPCODE.fromHex(buffer[0]); + + if (firstOpCode != null && firstOpCode == OPCODE.OP_RETURN) { + return OPReturnScript.fromScript(buffer); + } + + if (firstOpCode == OPCODE.OP_1) { + return PayToTaprootScript.fromScript(buffer); + } + + // Check for P2PK (public key + OP_CHECKSIG) + if (buffer.length >= 2 && + OPCODE.fromHex(buffer[buffer.length - 1]) == OPCODE.OP_CHECKSIG) { + // Verify this isn't P2PKH by checking for absence of OP_DUP at start + if (firstOpCode != OPCODE.OP_DUP) { + return PayToPublicKeyScript.fromScript(buffer); + } + } + + return switch (buffer.length) { + 22 => PayToWitnessPublicKeyHashScript.fromScript(buffer), + 23 => PayToScriptHashScript.fromScript(buffer), + 25 => PayToPublicKeyHashScript.fromScript(buffer), + 34 => PayToWitnessScriptHashScript.fromScript(buffer), + _ => throw Exception("Unknown script type"), + }; + } +} + +sealed class BTCUnlockingScript extends BTCScript { + const BTCUnlockingScript(super.bytes); + + factory BTCUnlockingScript.fromBuffer(Uint8List buffer) { + final first = buffer[0]; + + return switch (first) { + 0x00 => RedeemScript.fromScript(buffer), + 0x02 => ScriptWitness.fromScript(buffer), + _ => ScriptSignature.fromScript(buffer), + }; + } +} + +/// +/// RedeemScript is used to unlock a P2SH output +/// +final class RedeemScript extends BTCUnlockingScript { + late final Uint8List signature; + late final Uint8List redeemScript; + + RedeemScript.fromScript(super.bytes) { + final signatureLength = bytes[1]; + signature = bytes.sublist(2, 2 + signatureLength).toUint8List; + redeemScript = bytes.sublist(3 + signatureLength).toUint8List; + } + + RedeemScript(this.signature, this.redeemScript) + : super( + [ + 0x00, + signature.length, + ...signature, + redeemScript.length, + ...redeemScript, + ].toUint8List, + ); +} + +final class ScriptSignature extends BTCUnlockingScript { + late final Uint8List signature; + late final Uint8List publicKey; + + ScriptSignature.fromScript(super.bytes) { + final signatureLength = bytes[1]; + signature = bytes.sublist(2, 2 + signatureLength).toUint8List; + publicKey = bytes.sublist(3 + signatureLength).toUint8List; + } + + ScriptSignature(this.signature, this.publicKey) + : super( + [ + signature.length, + ...signature, + publicKey.length, + ...publicKey, + ].toUint8List, + ); +} + +final class ScriptWitness extends BTCUnlockingScript { + late final Uint8List scriptSig; + late final Uint8List publicKey; + + ScriptWitness.fromScript(super.bytes) { + final scriptSigLength = bytes[1]; + scriptSig = bytes.sublist(2, 2 + scriptSigLength).toUint8List; + publicKey = bytes.sublist(3 + scriptSigLength).toUint8List; + } + + ScriptWitness(this.scriptSig, this.publicKey) + : super( + [ + 0x02, + scriptSig.length, + ...scriptSig, + publicKey.length, + ...publicKey, + ].toUint8List, + ); +} + +final class TimeLockedScript extends BTCLockingScript { + final BTCLockingScript input; + final int lockTime; + final bool isRelative; + + TimeLockedScript.fromScript(super.bytes) + : input = BTCLockingScript.fromBuffer(bytes.sublist(3, bytes.length - 1)), + lockTime = bytes[0], + isRelative = bytes[1] == OPCODE.OP_CHECKSEQUENCEVERIFY.hex; + + TimeLockedScript(this.input, this.lockTime, this.isRelative) + : super(isRelative + ? [ + lockTime, + OPCODE.OP_CHECKSEQUENCEVERIFY.hex, + OPCODE.OP_DROP.hex, + ...input.bytes, + ].toUint8List + : [ + lockTime, + OPCODE.OP_CHECKLOCKTIMEVERIFY.hex, + OPCODE.OP_DROP.hex, + ...input.bytes, + ].toUint8List); +} + +final class PayToPublicKeyScript extends BTCLockingScript { + final Uint8List publicKey; + + PayToPublicKeyScript.fromScript(super.bytes) + : publicKey = bytes.sublist(0, bytes.length - 1).toUint8List; + + PayToPublicKeyScript(this.publicKey) + : super( + [ + publicKey.length, + ...publicKey, + OPCODE.OP_CHECKSIG.hex, + ].toUint8List, + ); +} + +final class PayToPublicKeyHashScript extends BTCLockingScript { + final Uint8List publicKeyHash; + + PayToPublicKeyHashScript.fromScript(super.bytes) + : publicKeyHash = bytes.sublist(3, bytes.length - 2).toUint8List; + + PayToPublicKeyHashScript(this.publicKeyHash) + : super( + [ + OPCODE.OP_DUP.hex, + OPCODE.OP_HASH160.hex, + publicKeyHash.length, + ...publicKeyHash, + OPCODE.OP_EQUALVERIFY.hex, + OPCODE.OP_CHECKSIG.hex, + ].toUint8List, + ); +} + +final class PayToScriptHashScript extends BTCLockingScript { + final Uint8List scriptHash; + + PayToScriptHashScript.fromScript(super.bytes) + : scriptHash = bytes.sublist(2, bytes.length - 1).toUint8List; + + PayToScriptHashScript(this.scriptHash) + : super( + [ + OPCODE.OP_HASH160.hex, + scriptHash.length, + ...scriptHash, + OPCODE.OP_EQUAL.hex, + ].toUint8List, + ); +} + +final class PayToWitnessPublicKeyHashScript extends BTCLockingScript { + final Uint8List publicKeyHash; + + PayToWitnessPublicKeyHashScript.fromScript(super.bytes) + : publicKeyHash = bytes.sublist(2, bytes.length).toUint8List; + + PayToWitnessPublicKeyHashScript(this.publicKeyHash) + : super( + [ + OPCODE.OP_0.hex, + publicKeyHash.length, + ...publicKeyHash, + ].toUint8List, + ); +} + +final class PayToWitnessScriptHashScript extends BTCLockingScript { + final Uint8List witnessScriptHash; + + PayToWitnessScriptHashScript.fromScript(super.bytes) + : witnessScriptHash = bytes.sublist(2, bytes.length).toUint8List; + + PayToWitnessScriptHashScript(this.witnessScriptHash) + : super( + [ + OPCODE.OP_0.hex, + witnessScriptHash.length, + ...witnessScriptHash, + ].toUint8List, + ); +} + +final class NestedSegwitScript extends BTCLockingScript { + final Uint8List nestedScriptHash; + + NestedSegwitScript(super.bytes) + : nestedScriptHash = bytes.sublist(2, bytes.length - 1).toUint8List; +} + +final class PayToWitnessPublicKeyHashNestedScript extends NestedSegwitScript { + final Uint8List? pubKeyHash; + + PayToWitnessPublicKeyHashNestedScript.fromScript(super.script) + : pubKeyHash = null; + + PayToWitnessPublicKeyHashNestedScript(Uint8List pubKeyHash) + : pubKeyHash = pubKeyHash, + super([ + OPCODE.OP_HASH160.hex, + 20, + ...ripmed160Hash([00, 14, ...pubKeyHash].toUint8List), + OPCODE.OP_EQUAL.hex, + ].toUint8List); +} + +final class PayToWitnessScriptHashNestedScript extends NestedSegwitScript { + final Uint8List? witnessSript; + + PayToWitnessScriptHashNestedScript.fromScript(super.script) + : witnessSript = null; + + PayToWitnessScriptHashNestedScript(Uint8List witnessSript) + : witnessSript = witnessSript, + super( + [ + OPCODE.OP_HASH160.hex, + ...ripmed160Hash([00, 20, ...sha256Hash(witnessSript)].toUint8List), + OPCODE.OP_EQUAL.hex, + ].toUint8List, + ); +} + +final class PayToTaprootScript extends BTCLockingScript { + final Uint8List pubkey; + + PayToTaprootScript.fromScript(super.bytes) + : pubkey = bytes.sublist(2, bytes.length).toUint8List; + + PayToTaprootScript(this.pubkey) + : super([ + OPCODE.OP_1.hex, + pubkey.length, + ...pubkey, + ].toUint8List); +} + +// final class BareMultiSigScript extends UTXOScript { +// final List pubKeys; +// final int m; + +// BareMultiSigScript.fromScript(super.script) +// : m = script[0] - 0x50, +// pubKeys = script.sublist(1, script.length - 2).toUint8List.split(33); + +// BareMultiSigScript(this.pubKeys, this.m) +// : super( +// [ +// m + 0x50, +// ...pubKeys.fold( +// Uint8List(0), +// (previousValue, element) => +// [...previousValue, ...element].toUint8List, +// ), +// pubKeys.length + 0x50, +// OPCODE.OP_CHECKMULTISIG.hex, +// ].toUint8List, +// ); +// } + +final class OPReturnScript extends BTCLockingScript { + final Uint8List data; + + OPReturnScript.fromScript(super.bytes) + : data = bytes.sublist(2, bytes.length).toUint8List; + + OPReturnScript(this.data) + : assert(data.length <= 80, "Data length must be less than 80 bytes"), + super([ + OPCODE.OP_RETURN.hex, + ...data, + ].toUint8List); +} + +final class AnyoneCanSpendScript extends BTCLockingScript { + AnyoneCanSpendScript() : super([OPCODE.OP_TRUE.hex].toUint8List); +} + +final class EmptyScript extends BTCScript { + EmptyScript() : super(Uint8List(0)); +} + +final class EmptyUnlockingScript extends BTCUnlockingScript { + EmptyUnlockingScript() : super(Uint8List(0)); +} + +final class EmptyLockingScript extends BTCLockingScript { + EmptyLockingScript() : super(Uint8List(0)); +} + +sealed class BTCTransactionStructure { + Set get acceptedInputTypes; + + const BTCTransactionStructure(); + + factory BTCTransactionStructure.create({ + required int version, + required int lockTime, + required List inputs, + required List outputs, + }) { + throw UnimplementedError(); + } + + Uint8List buildBuffer({ + required int version, + required int lockTime, + required List inputs, + required List outputs, + }); +} + +final class LegacyFormat extends BTCTransactionStructure { + @override + Set get acceptedInputTypes => { + InputType.P2PKH, + InputType.P2SH, + InputType.P2PK, + }; + + Uint8List buildBuffer({ + required int version, + required int lockTime, + required List inputs, + required List outputs, + }) { + return Uint8List(0); + } +} + +/// SegWit v0 +final class SegwitFormat extends BTCTransactionStructure { + final int version = 1; + final int flag = 1; + + @override + Set get acceptedInputTypes => { + InputType.P2PKH, + InputType.P2SH, + InputType.P2PK, + InputType.P2WPKH, + InputType.P2WSH, + }; + + @override + Uint8List buildBuffer( + {required int version, + required int lockTime, + required List inputs, + required List outputs}) { + throw UnimplementedError(); + } +} + +/// SegWit v1 +final class TaprootFormat extends SegwitFormat { + @override + final int version = 2; + + @override + final int flag = 0; + + @override + Set get acceptedInputTypes => { + ...super.acceptedInputTypes, + InputType.P2TR, + }; +} diff --git a/lib/src/crypto/utxo/entities/transactions/utxo_transaction.dart b/lib/src/crypto/utxo/entities/transactions/utxo_transaction.dart index da7294357..3e346fc26 100644 --- a/lib/src/crypto/utxo/entities/transactions/utxo_transaction.dart +++ b/lib/src/crypto/utxo/entities/transactions/utxo_transaction.dart @@ -506,8 +506,8 @@ class ElectrumScriptPubKey { ); } - Uint8List get lockingScript { - return Uint8List.fromList(hex.decode(hexString)); + BTCLockingScript get lockingScript { + return BTCLockingScript.fromBuffer(hex.decode(hexString).toUint8List); } Json toJson() { @@ -538,3 +538,12 @@ final class NotAvaialableUTXOTransaction extends UTXOTransaction { transferMethod: TransactionTransferMethod.unknown, ); } + +extension OutputConverter on ElectrumOutput { + Output get toOutput { + return BTCOutput( + value: value, + script: scriptPubKey.lockingScript, + ); + } +} diff --git a/lib/src/crypto/utxo/repositories/electrum_json_rpc_client.dart b/lib/src/crypto/utxo/repositories/electrum_json_rpc_client.dart index 20ddbc4bb..b27c8e00b 100644 --- a/lib/src/crypto/utxo/repositories/electrum_json_rpc_client.dart +++ b/lib/src/crypto/utxo/repositories/electrum_json_rpc_client.dart @@ -215,6 +215,18 @@ class ElectrumXClient { return fee; } + Future estimateSmartFee({required int blocks}) async { + final response = await _client.sendRequest( + { + "method": "blockchain.estimatesmartfee", + "params": [blocks] + }, + ); + final fee = response as double?; + if (fee == null || fee == 0) throw Exception("Fee estimation failed"); + return fee; + } + Future disconnect() async { await _client.disconnect(); return true; diff --git a/lib/src/crypto/utxo/utils/proof_of_payment.dart b/lib/src/crypto/utxo/utils/proof_of_payment.dart index f9f7cae75..1eae7b405 100644 --- a/lib/src/crypto/utxo/utils/proof_of_payment.dart +++ b/lib/src/crypto/utxo/utils/proof_of_payment.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import 'package:collection/collection.dart'; import 'package:walletkit_dart/src/crypto/utxo/entities/raw_transaction/output.dart'; import 'package:walletkit_dart/src/crypto/utxo/entities/op_codes.dart'; +import 'package:walletkit_dart/src/crypto/utxo/entities/raw_transaction/tx_structure.dart'; import 'package:walletkit_dart/src/crypto/utxo/utils/pubkey_to_address.dart'; import 'package:walletkit_dart/src/crypto/utxo/repositories/electrum_json_rpc_client.dart'; import 'package:walletkit_dart/src/utils/int.dart'; @@ -90,16 +91,16 @@ Future proofOfPayment({ /// /// Create Pop Output /// - final pop_output_script = Uint8List(1 + 2 + 32 + nonceBytes.length + 1); + final pop_output_script_data = Uint8List(2 + 32 + nonceBytes.length + 1); var offset = 0; - offset += pop_output_script.bytes.writeUint8(offset, OP_RETURN); - offset += pop_output_script.bytes.writeUint16(offset, 0x01); // POP Version - offset += pop_output_script.writeSlice(offset, txid.hexToBytes); - offset += pop_output_script.writeVarSlice(offset, nonceBytes); + offset += + pop_output_script_data.bytes.writeUint16(offset, 0x01); // POP Version + offset += pop_output_script_data.writeSlice(offset, txid.hexToBytes); + offset += pop_output_script_data.writeVarSlice(offset, nonceBytes); final pop_output = BTCOutput( value: 0.toBI, - scriptPubKey: pop_output_script, + script: OPReturnScript(pop_output_script_data), ); /// diff --git a/lib/src/crypto/utxo/utils/pubkey_to_address.dart b/lib/src/crypto/utxo/utils/pubkey_to_address.dart index d96ca86b6..b3ae31517 100644 --- a/lib/src/crypto/utxo/utils/pubkey_to_address.dart +++ b/lib/src/crypto/utxo/utils/pubkey_to_address.dart @@ -127,6 +127,11 @@ Uint8List ripmed160Sha256Hash(Uint8List buffer) { return ripmed160.process(sha256.process(buffer)); } +Uint8List ripmed160Hash(Uint8List buffer) { + final ripmed160 = RIPEMD160Digest(); + return ripmed160.process(buffer); +} + /// /// Sha256 Hash of Sha256 Hash /// diff --git a/lib/src/crypto/utxo/utils/send.dart b/lib/src/crypto/utxo/utils/send.dart index 971034087..a741a21cb 100644 --- a/lib/src/crypto/utxo/utils/send.dart +++ b/lib/src/crypto/utxo/utils/send.dart @@ -8,6 +8,7 @@ import 'package:walletkit_dart/src/crypto/utxo/entities/payments/input_selection import 'package:walletkit_dart/src/crypto/utxo/entities/payments/p2h.dart'; import 'package:walletkit_dart/src/crypto/utxo/entities/raw_transaction/input.dart'; import 'package:walletkit_dart/src/crypto/utxo/entities/raw_transaction/output.dart'; +import 'package:walletkit_dart/src/crypto/utxo/entities/raw_transaction/tx_structure.dart'; import 'package:walletkit_dart/src/domain/exceptions.dart'; import 'package:walletkit_dart/src/crypto/utxo/repositories/electrum_json_rpc_client.dart'; import 'package:walletkit_dart/src/crypto/utxo/utils/endpoint_utils.dart'; @@ -158,8 +159,6 @@ RawTransaction buildUnsignedTransaction({ "Total Input Value does not match Total Output Value", ); - Logger.log("Estimated Fee: $estimatedFee"); - final outputs = buildOutputs( recipient: targetAddress, value: targetValue, @@ -186,6 +185,10 @@ RawTransaction buildUnsignedTransaction({ "Total Output Value does not match Total Input Value", ); } + Logger.log("Input Fee per Byte: ${feePerByte.displayDouble}"); + Logger.log("Estimated Fee: $estimatedFee"); + Logger.log("Actual Fee: ${tx.fee}"); + Logger.log("Fee per Byte: ${tx.feePerByte}"); return tx; } @@ -303,7 +306,7 @@ List signInputs({ node: bip32Node, ); - signedInputs.add(input.addScript(wittnessScript: witnessSript)); + signedInputs.add(input.addScript(witnessSript)); continue; } @@ -316,13 +319,13 @@ List signInputs({ node: bip32Node, ); - signedInputs.add(input.addScript(scriptSig: scriptSig)); + signedInputs.add(input.addScript(scriptSig)); } return signedInputs; } -Uint8List createScriptSignature({ +BTCUnlockingScript createScriptSignature({ required RawTransaction tx, required int i, required ElectrumOutput output, @@ -338,7 +341,8 @@ Uint8List createScriptSignature({ ZENIQ_NETWORK() when tx is BTCRawTransaction => tx.bip143sigHash( index: i, - prevScriptPubKey: prevScriptPubKey, + prevScript: prevScriptPubKey, + networkType: networkType, output: output, hashType: hashType, ), @@ -347,7 +351,8 @@ Uint8List createScriptSignature({ EUROCOIN_NETWORK() => tx.legacySigHash( index: i, - prevScriptPubKey: prevScriptPubKey, + prevScript: prevScriptPubKey, + networkType: networkType, hashType: hashType, ), _ => @@ -356,18 +361,15 @@ Uint8List createScriptSignature({ final sig = signInput(bip32: node, sigHash: sigHash); - final scriptSig = encodeSignature(sig, hashType); - - final unlockingScript = constructScriptSig( - walletPurpose: walletPurpose, - signature: scriptSig, - publicKey: node.publicKey, - ); + final encodedSig = encodeSignature(sig, hashType); - return unlockingScript; + return switch (walletPurpose) { + HDWalletPurpose.BIP49 => throw UnimplementedError(), + _ => ScriptSignature(encodedSig, node.publicKey), + }; } -Uint8List createScriptWitness({ +ScriptWitness createScriptWitness({ required BTCRawTransaction tx, required int i, required ElectrumOutput output, @@ -381,9 +383,10 @@ Uint8List createScriptWitness({ final sigHash = tx.bip143sigHash( index: i, - prevScriptPubKey: prevScriptPubKey, + prevScript: prevScriptPubKey, output: output, hashType: hashType, + networkType: networkType, ); final sig = signInput(bip32: node, sigHash: sigHash); @@ -392,13 +395,7 @@ Uint8List createScriptWitness({ final pubkey = node.publicKey; - return [ - 0x02, - scriptSig.length, - ...scriptSig, - pubkey.length, - ...pubkey, - ].toUint8List; + return ScriptWitness(scriptSig, pubkey); } (BigInt, Map) buildInputs( @@ -471,20 +468,20 @@ Input buildInput({ BTCInput( txid: txid, vout: vout, - value: utxo.value, - prevScriptPubKey: utxo.scriptPubKey.lockingScript, + script: null, + prevOutput: utxo.toOutput, ), EUROCOIN_NETWORK() => EC8Input( txid: txid, vout: vout, - value: utxo.value, - prevScriptPubKey: utxo.scriptPubKey.lockingScript, + script: null, + prevOutput: utxo.toOutput, ), }; } Output buildOutput(String address, BigInt value, UTXONetworkType networkType) { - final lockingScript = P2Hash(address).publicKeyScript; + final lockingScript = BTCLockingScript.fromAddress(address); return switch (networkType) { BITCOIN_NETWORK() || @@ -493,11 +490,11 @@ Output buildOutput(String address, BigInt value, UTXONetworkType networkType) { LITECOIN_NETWORK() => BTCOutput( value: value, - scriptPubKey: lockingScript, + script: lockingScript, ), EUROCOIN_NETWORK() => EC8Output( value: value, - scriptPubKey: lockingScript, + script: lockingScript, ), }; } @@ -598,6 +595,10 @@ Future rebroadcastTransaction({ ], ); + if (clientsForRebroadcast.isEmpty) { + break; + } + if (rebroadcastCount > type.endpoints.length / 2) { break; } @@ -645,45 +646,13 @@ Uint8List signInput({ } } -Uint8List constructScriptSig({ - required HDWalletPurpose walletPurpose, - required Uint8List signature, - required Uint8List publicKey, - Uint8List? redeemScript, // Required for BIP49 (P2SH-P2WPKH) -}) => - switch (walletPurpose) { - HDWalletPurpose.NO_STRUCTURE || - HDWalletPurpose.BIP44 => - Uint8List.fromList([ - signature.length, - ...signature, - publicKey.length, - ...publicKey, - ]), - HDWalletPurpose.BIP49 => Uint8List.fromList([ - 0x00, - signature.length, - ...signature, - redeemScript!.length, - ...redeemScript, - ]), - - /// Should never be called as it is handled in constructWitnessScript - HDWalletPurpose.BIP84 => Uint8List.fromList([ - 0x00, - signature.length, - ...signature, - publicKey.length, - ...publicKey, - ]), - }; - BigInt calculateFee({ required RawTransaction tx, required Amount feePerByte, }) { return switch (tx) { EC8RawTransaction _ => calculateFeeEC8(tx: tx), + BTCRawTransaction tx when tx.isSegwit => tx.weight * feePerByte.value, _ => tx.size.toBI * feePerByte.value, }; } diff --git a/lib/src/crypto/utxo/utxo_analyzer.dart b/lib/src/crypto/utxo/utxo_analyzer.dart index 998d5cf1d..0b5351c60 100644 --- a/lib/src/crypto/utxo/utxo_analyzer.dart +++ b/lib/src/crypto/utxo/utxo_analyzer.dart @@ -494,9 +494,12 @@ Future estimateFeeForPriority({ required int blocks, required UTXONetworkType network, required ElectrumXClient? initalClient, + bool useSmartFee = false, }) async { final (fee, _, _) = await fetchFromRandomElectrumXNode( - (client) => client.estimateFee(blocks: blocks), + (client) => useSmartFee + ? client.estimateSmartFee(blocks: blocks) + : client.estimateFee(blocks: blocks), client: initalClient, endpoints: network.endpoints, token: network.coin, @@ -506,7 +509,7 @@ Future estimateFeeForPriority({ final feePerKb = Amount.convert(value: fee, decimals: 8); - final feePerB = feePerKb / Amount.from(value: 1000, decimals: 0); + final feePerB = feePerKb.multiplyAndCeil(0.001); return feePerB; } @@ -514,6 +517,7 @@ Future estimateFeeForPriority({ Future getNetworkFees({ required UTXONetworkType network, double multiplier = 1.0, + bool useSmartFee = false, }) async { final blockInOneHour = 3600 ~/ network.blockTime; final blocksTillTomorrow = 24 * 3600 ~/ network.blockTime; diff --git a/lib/src/domain/entities/generic_transaction.dart b/lib/src/domain/entities/generic_transaction.dart index 818eeb6f1..33f6f6b93 100644 --- a/lib/src/domain/entities/generic_transaction.dart +++ b/lib/src/domain/entities/generic_transaction.dart @@ -5,6 +5,8 @@ import 'dart:typed_data'; import 'package:convert/convert.dart'; import 'package:walletkit_dart/src/common/http_repository.dart'; import 'package:walletkit_dart/src/crypto/utxo/entities/payments/p2h.dart'; +import 'package:walletkit_dart/src/crypto/utxo/entities/raw_transaction/output.dart'; +import 'package:walletkit_dart/src/crypto/utxo/entities/raw_transaction/tx_structure.dart'; import 'package:walletkit_dart/src/crypto/utxo/utils/pubkey_to_address.dart'; import 'package:walletkit_dart/walletkit_dart.dart'; diff --git a/lib/src/utils/var_uint.dart b/lib/src/utils/var_uint.dart index e6f61bd31..19434dc99 100644 --- a/lib/src/utils/var_uint.dart +++ b/lib/src/utils/var_uint.dart @@ -138,3 +138,16 @@ int encodingLength(int number) { ? 5 : 9); } + +int getVarIntSize(int value) { + if (value < 0xfd) { + return 1; + } + if (value <= 0xffff) { + return 3; + } + if (value <= 0xffffffff) { + return 5; + } + return 9; +} diff --git a/test/ci/fetching/gasfees_test.dart b/test/ci/fetching/gasfees_test.dart index 5622e9337..14aa6d859 100644 --- a/test/ci/fetching/gasfees_test.dart +++ b/test/ci/fetching/gasfees_test.dart @@ -8,6 +8,15 @@ void main() { expect(gasEntity, isNotNull); print(gasEntity); + + final smartGasEntity = await getNetworkFees( + network: BitcoinNetwork, + useSmartFee: true, + ); + + expect(smartGasEntity, isNotNull); + + print(smartGasEntity); }); test('Estimate Fees LTC', () async { @@ -16,6 +25,15 @@ void main() { expect(gasEntity, isNotNull); print(gasEntity); + + final smartGasEntity = await getNetworkFees( + network: LitecoinNetwork, + useSmartFee: true, + ); + + expect(smartGasEntity, isNotNull); + + print(smartGasEntity); }); test('Estimate Fees BCH', () async { @@ -24,6 +42,15 @@ void main() { expect(gasEntity, isNotNull); print(gasEntity); + + final smartGasEntity = await getNetworkFees( + network: BitcoincashNetwork, + useSmartFee: true, + ); + + expect(smartGasEntity, isNotNull); + + print(smartGasEntity); }); test('Estimate Fees Zeniq', () async { @@ -32,5 +59,14 @@ void main() { expect(gasEntity, isNotNull); print(gasEntity); + + final smartGasEntity = await getNetworkFees( + network: ZeniqNetwork, + useSmartFee: true, + ); + + expect(smartGasEntity, isNotNull); + + print(smartGasEntity); }); } diff --git a/test/ci/proof_of_payment/pop_test.dart b/test/ci/proof_of_payment/pop_test.dart index f8151a9ce..0d286e927 100644 --- a/test/ci/proof_of_payment/pop_test.dart +++ b/test/ci/proof_of_payment/pop_test.dart @@ -49,7 +49,7 @@ void main() { expect(output.value, BigInt.zero); - final outputScript = output.scriptPubKey; + final outputScript = output.script.bytes; var offset = 0; final (op, off1) = outputScript.bytes.readUint8(offset); offset += off1; @@ -71,7 +71,7 @@ void main() { final compareInput = selectedTx.inputs[i]; expect(input.txid.rev.toHex, compareInput.txid); - expect(input.scriptSig, compareInput.scriptSig?.hexToBytes); + expect(input.script?.bytes, compareInput.scriptSig?.hexToBytes); expect(input.vout, compareInput.vout); } @@ -130,7 +130,7 @@ void main() { expect(output.value, BigInt.zero); - final outputScript = output.scriptPubKey; + final outputScript = output.script.bytes; var offset = 0; final (op, off1) = outputScript.bytes.readUint8(offset); offset += off1; @@ -152,7 +152,7 @@ void main() { final compareInput = selectedTx.inputs[i]; expect(input.txid.rev.toHex, compareInput.txid); - expect(input.scriptSig, compareInput.scriptSig?.hexToBytes); + expect(input.script?.bytes, compareInput.scriptSig?.hexToBytes); expect(input.vout, compareInput.vout); } @@ -194,7 +194,7 @@ void main() { expect(output.value, BigInt.zero); - final outputScript = output.scriptPubKey; + final outputScript = output.script.bytes; var offset = 0; final (op, off1) = outputScript.bytes.readUint8(offset); offset += off1; @@ -216,7 +216,7 @@ void main() { final compareInput = selectedTx.inputs[i]; expect(input.txid.rev.toHex, compareInput.txid); - expect(input.scriptSig, compareInput.scriptSig?.hexToBytes); + expect(input.script?.bytes, compareInput.scriptSig?.hexToBytes); expect(input.vout, compareInput.vout); } @@ -262,7 +262,7 @@ void main() { expect(output.value, BigInt.zero); - final outputScript = output.scriptPubKey; + final outputScript = output.script.bytes; var offset = 0; final (op, off1) = outputScript.bytes.readUint8(offset); offset += off1; @@ -284,7 +284,7 @@ void main() { final compareInput = selectedTx.inputs[i]; expect(input.txid.rev.toHex, compareInput.txid); - expect(input.scriptSig, compareInput.scriptSig?.hexToBytes); + expect(input.script?.bytes, compareInput.scriptSig?.hexToBytes); expect(input.vout, compareInput.vout); } @@ -345,7 +345,7 @@ void main() { expect(output.value, BigInt.zero); - final outputScript = output.scriptPubKey; + final outputScript = output.script.bytes; var offset = 0; final (op, off1) = outputScript.bytes.readUint8(offset); offset += off1; @@ -367,7 +367,7 @@ void main() { final compareInput = selectedTx.inputs[i]; expect(input.txid.rev.toHex, compareInput.txid); - expect(input.scriptSig, compareInput.scriptSig?.hexToBytes); + expect(input.script?.bytes, compareInput.scriptSig?.hexToBytes); expect(input.vout, compareInput.vout); } @@ -423,7 +423,7 @@ void main() { expect(output.value, BigInt.zero); - final outputScript = output.scriptPubKey; + final outputScript = output.script.bytes; var offset = 0; final (op, off1) = outputScript.bytes.readUint8(offset); offset += off1; @@ -445,7 +445,7 @@ void main() { final compareInput = selectedTx.inputs[i]; expect(input.txid.rev.toHex, compareInput.txid); - expect(input.scriptSig, compareInput.scriptSig?.hexToBytes); + expect(input.script?.bytes, compareInput.scriptSig?.hexToBytes); expect(input.vout, compareInput.vout); } diff --git a/test/ci/raw_transaction/btc_raw_transaction_test.dart b/test/ci/raw_transaction/btc_raw_transaction_test.dart new file mode 100644 index 000000000..3620a63ff --- /dev/null +++ b/test/ci/raw_transaction/btc_raw_transaction_test.dart @@ -0,0 +1,17 @@ +import 'package:test/test.dart'; +import 'package:walletkit_dart/src/crypto/utxo/entities/raw_transaction/input.dart'; +import 'package:walletkit_dart/walletkit_dart.dart'; + +void main() { + test( + "Legacy BTC Raw Transaction Test", + () async { + final tx = BTCRawTransaction( + version: BitcoinNetwork.txVersion, + lockTime: 0, + inputs: [], + outputs: [], + ); + }, + ); +} diff --git a/test/no_ci/input_simulation_test.dart b/test/no_ci/input_simulation_test.dart index 8bab8227e..4e2774067 100644 --- a/test/no_ci/input_simulation_test.dart +++ b/test/no_ci/input_simulation_test.dart @@ -305,11 +305,11 @@ Future<(UTXOTransaction, bool, String?)> simulateTx({ ZENIQ_NETWORK() => BTCOutput( value: out.value, - scriptPubKey: out.scriptPubKey.lockingScript, + script: out.scriptPubKey.lockingScript, ), EUROCOIN_NETWORK() => EC8Output( value: out.value, - scriptPubKey: out.scriptPubKey.lockingScript, + script: out.scriptPubKey.lockingScript, ), }) .toList();