diff --git a/coinlib/lib/src/address.dart b/coinlib/lib/src/address.dart index ab9d7e9e..d78a7505 100644 --- a/coinlib/lib/src/address.dart +++ b/coinlib/lib/src/address.dart @@ -1,12 +1,14 @@ import 'dart:typed_data'; + import 'package:coinlib/src/common/bytes.dart'; import 'package:coinlib/src/common/hex.dart'; import 'package:coinlib/src/crypto/ec_public_key.dart'; import 'package:coinlib/src/crypto/hash.dart'; -import 'package:coinlib/src/network.dart'; import 'package:coinlib/src/encode/base58.dart'; import 'package:coinlib/src/encode/bech32.dart'; +import 'package:coinlib/src/network.dart'; import 'package:coinlib/src/scripts/program.dart'; +import 'package:coinlib/src/scripts/programs/mweb.dart'; import 'package:coinlib/src/scripts/programs/p2pkh.dart'; import 'package:coinlib/src/scripts/programs/p2sh.dart'; import 'package:coinlib/src/scripts/programs/p2tr.dart'; @@ -17,12 +19,12 @@ import 'package:coinlib/src/scripts/script.dart'; import 'package:coinlib/src/taproot/taproot.dart'; class InvalidAddress implements Exception {} + class InvalidAddressNetwork implements Exception {} /// Base class for all addresses. Encoded addresses for sub-classes are provided /// via the [toString()] method. abstract class Address { - /// Decodes an address string ([encoded]) and returns the sub-class object for /// the type of address. Throws [InvalidAddress], [InvalidAddressNetwork], /// [InvalidBech32Checksum] or [InvalidBase58Checksum] if there is an error @@ -31,7 +33,7 @@ abstract class Address { // Try base58 try { return Base58Address.fromString(encoded, network); - } on Exception catch(e) { + } on Exception catch (e) { // If not base58, then try bech32 try { return Bech32Address.fromString(encoded, network); @@ -49,27 +51,28 @@ abstract class Address { } Program get program; - } /// Base class for all addresses that use base58: [P2PKHAddress] and /// [P2SHAddress]. abstract class Base58Address implements Address { - /// The 160bit public key or redeemScript hash for the base58 address final Uint8List _hash; + /// The network and address type version of the address final int version; String? _encodedCache; Base58Address._(Uint8List hash, this.version) : _hash = hash { if (version < 0 || version > 255) { - throw ArgumentError("base58 version must be within 0-255", "this.version"); + throw ArgumentError( + "base58 version must be within 0-255", + "this.version", + ); } } factory Base58Address.fromString(String encoded, Network network) { - final data = base58Decode(encoded); if (data.length != 21) throw InvalidAddress(); @@ -87,105 +90,112 @@ abstract class Base58Address implements Address { addr._encodedCache = encoded; return addr; - } @override toString() => _encodedCache ??= base58Encode( - Uint8List.fromList([version, ..._hash]), - ); + Uint8List.fromList([version, ..._hash]), + ); Uint8List get hash => Uint8List.fromList(_hash); - } class P2PKHAddress extends Base58Address { - /// Takes a [hash] directly for a P2PKH address - P2PKHAddress.fromHash(Uint8List hash, { required int version }) - : super._(copyCheckBytes(hash, 20), version); + P2PKHAddress.fromHash(Uint8List hash, {required int version}) + : super._(copyCheckBytes(hash, 20), version); /// Constructs a P2PKH address from a given [pubkey]. - P2PKHAddress.fromPublicKey(ECPublicKey pubkey, { required int version }) - : this.fromHash(hash160(pubkey.data), version: version); + P2PKHAddress.fromPublicKey(ECPublicKey pubkey, {required int version}) + : this.fromHash(hash160(pubkey.data), version: version); @override P2PKH get program => P2PKH.fromHash(hash); - } class P2SHAddress extends Base58Address { - /// Constructs a P2SH address from the redeemScript [hash]. - P2SHAddress.fromHash(Uint8List hash, { required int version }) - : super._(copyCheckBytes(hash, 20), version); + P2SHAddress.fromHash(Uint8List hash, {required int version}) + : super._(copyCheckBytes(hash, 20), version); /// Constructs a P2SH address for a redeemScript - P2SHAddress.fromRedeemScript(Script script, { required int version }) - : super._(hash160(script.compiled), version); + P2SHAddress.fromRedeemScript(Script script, {required int version}) + : super._(hash160(script.compiled), version); @override P2SH get program => P2SH.fromHash(hash); - } /// Base class for addresses that use bech32: [P2WPKHAddress] and /// [P2WSHAddress]. Unknown witness programs are encoded via /// [UnknownWitnessAddress]. abstract class Bech32Address implements Address { - - static const maxWitnessProgramLength = 40; + static const nonMwebMaxWitnessProgramLength = 40; /// The human readable part of the address used to specify the network final String hrp; + /// The program version of the address final int version; final Uint8List _data; String? _encodedCache; - Bech32Address._(this.version, this._data, this.hrp) { - + Bech32Address._( + this.version, + this._data, + this.hrp, { + bool ignoreMaxLengthCheckForMweb = false, + }) { if (version < 0 || version > 16) { throw ArgumentError("bech32 version must be 0-16", "this.version"); } - if (_data.length < 2 || _data.length > maxWitnessProgramLength) { + + if (!ignoreMaxLengthCheckForMweb && + (_data.length < 2 || _data.length > nonMwebMaxWitnessProgramLength)) { throw ArgumentError( - "witness programs must be 2-$maxWitnessProgramLength in length", + "witness programs must be 2-$nonMwebMaxWitnessProgramLength in length", "this.program", ); } + if (!hrpValid(hrp)) { throw ArgumentError("Invalid hrp: $hrp", "this.hrp"); } final encodedLength - // Encoded program length - = (_data.length*8+4)/5 - // Seperator and version - + 2 - + hrp.length - + Bech32.checksumLength; - - if (encodedLength > Bech32.maxLength) { - throw ArgumentError("Bech32Address arguments exceed allowable size"); + // Encoded program length + = (_data.length * 8 + 4) / 5 + // Seperator and version + + + 2 + + hrp.length + + Bech32.checksumLength; + + if (!ignoreMaxLengthCheckForMweb && encodedLength > Bech32.maxLength) { + throw ArgumentError( + "Bech32Address arguments exceed allowable size: $encodedLength", + ); } - } + // Refactored factory: hrpOverride parameter removed factory Bech32Address.fromString(String encoded, Network network) { - - final bech32 = Bech32.decode(encoded); + final bech32 = Bech32.decode( + encoded, + ignoreMaxLengthCheckForMweb: network.mwebBech32Hrp.isNotEmpty, + ); if (bech32.words.isEmpty) throw InvalidAddress(); - if (bech32.hrp != network.bech32Hrp) throw InvalidAddressNetwork(); + if (bech32.hrp != network.bech32Hrp && + bech32.hrp != network.mwebBech32Hrp) { + throw InvalidAddressNetwork(); + } final version = bech32.words[0]; // Must use bech32m starting with version 1 - if ( - (version == 0 && bech32.type != Bech32Type.bech32) - || (version != 0 && bech32.type != Bech32Type.bech32m) - ) { + if ((version == 0 && bech32.type != Bech32Type.bech32) || + (version != 0 && bech32.type != Bech32Type.bech32m)) { throw InvalidAddress(); } @@ -196,25 +206,30 @@ abstract class Bech32Address implements Address { late Bech32Address addr; - if (version == 0) { + if (network.mwebBech32Hrp.isNotEmpty && + version == 0 && + bytes.length == MwebAddress._expectedProgramLength) { + addr = MwebAddress.fromHash(hash: bytes, hrp: bech32.hrp); + } else if (version == 0) { // Version 0 signals P2WPKH or P2WSH - if (bytes.length == 20) { + if (bytes.length == P2WPKHAddress._expectedProgramLength) { addr = P2WPKHAddress.fromHash(bytes, hrp: bech32.hrp); - } else if (bytes.length == 32) { + } else if (bytes.length == P2WSHAddress._expectedProgramLength) { addr = P2WSHAddress.fromHash(bytes, hrp: bech32.hrp); } else { - throw InvalidAddress(); + throw InvalidAddress(); // Unexpected length for P2WPKH/P2WSH v0 } } else if (version == 1) { - // Version 1 is Taproot - if (bytes.length == 32) { + // Version 1 is Taproot (P2TR) + if (bytes.length == P2TRAddress._expectedProgramLength) { addr = P2TRAddress.fromTweakedKeyX(bytes, hrp: bech32.hrp); } else { - throw InvalidAddress(); + throw InvalidAddress(); // Unexpected length for P2TR v1 } } else if (version <= 16) { - // Treat other versions as unknown. - if (bytes.length < 2 || bytes.length > maxWitnessProgramLength) { + // Treat other versions as unknown, but still require valid length range (e.g., 2-40) + if (bytes.length < 2 || bytes.length > nonMwebMaxWitnessProgramLength) { + // Using 40 as a general max for unknown throw InvalidAddress(); } addr = UnknownWitnessAddress(bytes, hrp: bech32.hrp, version: version); @@ -224,86 +239,134 @@ abstract class Bech32Address implements Address { addr._encodedCache = encoded; return addr; - } @override toString() => _encodedCache ??= Bech32( - hrp: hrp, - words: List.from([ - version, ...convertBits(_data, 8, 5, true)!, - ]), - type: version == 0 ? Bech32Type.bech32 : Bech32Type.bech32m, - ).encode(); + hrp: hrp, + words: List.from([ + version, + ...convertBits(_data, 8, 5, true)!, + ]), + type: version == 0 ? Bech32Type.bech32 : Bech32Type.bech32m, + ).encode(); /// The "witness program" data encoded in the address Uint8List get data => Uint8List.fromList(_data); - } class P2WPKHAddress extends Bech32Address { + static const int _expectedProgramLength = 20; /// Constructs a P2WPKH address directly from the [hash] - P2WPKHAddress.fromHash(Uint8List hash, { required String hrp }) - : super._(0, copyCheckBytes(hash, 20), hrp); + P2WPKHAddress.fromHash(Uint8List hash, {required String hrp}) + : super._(0, copyCheckBytes(hash, _expectedProgramLength), hrp); /// Constructs a P2WPKH address from a [pubkey] - P2WPKHAddress.fromPublicKey(ECPublicKey pubkey, { required String hrp }) - : this.fromHash(hash160(pubkey.data), hrp: hrp); + P2WPKHAddress.fromPublicKey(ECPublicKey pubkey, {required String hrp}) + : this.fromHash(hash160(pubkey.data), hrp: hrp); @override P2WPKH get program => P2WPKH.fromHash(_data); - } class P2WSHAddress extends Bech32Address { + static const int _expectedProgramLength = 32; /// Constructs a P2WSH address from the script [hash] - P2WSHAddress.fromHash(Uint8List hash, { required String hrp }) - : super._(0, copyCheckBytes(hash, 32), hrp); + P2WSHAddress.fromHash(Uint8List hash, {required String hrp}) + : super._(0, copyCheckBytes(hash, _expectedProgramLength), hrp); /// Constructs a P2WSH address for a witnessScript - P2WSHAddress.fromWitnessScript(Script script, { required String hrp }) - : super._(0, sha256Hash(script.compiled), hrp); + P2WSHAddress.fromWitnessScript(Script script, {required String hrp}) + : super._(0, sha256Hash(script.compiled), hrp); @override P2WSH get program => P2WSH.fromHash(_data); - } class P2TRAddress extends Bech32Address { + static const int _expectedProgramLength = 32; - P2TRAddress.fromTweakedKeyX(Uint8List tweakedKeyX, { required String hrp }) - : super._(1, copyCheckBytes(tweakedKeyX, 32), hrp); + P2TRAddress.fromTweakedKeyX(Uint8List tweakedKeyX, {required String hrp}) + : super._(1, copyCheckBytes(tweakedKeyX, _expectedProgramLength), hrp); - P2TRAddress.fromTweakedKey(ECPublicKey tweakedKey, { required String hrp }) - : super._(1, tweakedKey.x, hrp); + P2TRAddress.fromTweakedKey(ECPublicKey tweakedKey, {required String hrp}) + : super._(1, tweakedKey.x, hrp); - P2TRAddress.fromTaproot(Taproot taproot, { required String hrp }) - : super._(1, taproot.tweakedKey.x, hrp); + P2TRAddress.fromTaproot(Taproot taproot, {required String hrp}) + : super._(1, taproot.tweakedKey.x, hrp); @override P2TR get program => P2TR.fromTweakedKeyX(_data); +} + +class MwebAddress extends Bech32Address { + static const int _expectedProgramLength = 66; + + factory MwebAddress.fromString(String encoded, {required Network network}) { + // Delegate to the base Bech32Address.fromString, passing the customized network. + final Bech32Address decodedAddr = Bech32Address.fromString( + encoded, + network, + ); + + if (decodedAddr is! MwebAddress) { + throw Exception( + "Invalid MWEB address format or type: $encoded. Decoded as ${decodedAddr.runtimeType}", + ); + } + return decodedAddr; + } + + MwebAddress.fromHash({required Uint8List hash, required String hrp}) + : super._( + 0, + copyCheckBytes(hash, _expectedProgramLength), + hrp, + ignoreMaxLengthCheckForMweb: true, + ) { + if (data.length != _expectedProgramLength) { + throw Exception( + "Invalid MWEB witness program length: ${data.length}, expected $_expectedProgramLength", + ); + } + } + @override + Program get program => RawProgram(Script([MwebScriptOp(data)])); + + @override + toString() => _encodedCache ??= Bech32( + hrp: hrp, + words: List.from([ + version, + ...convertBits(_data, 8, 5, true)!, + ]), + type: version == 0 ? Bech32Type.bech32 : Bech32Type.bech32m, + ignoreMaxLengthCheckForMweb: true, + ).encode(); } /// This address type is for all bech32 addresses that do not match known /// witness versions. class UnknownWitnessAddress extends Bech32Address { - /// Constructs a bech32 witness address from the "witness program" [data], /// witness [version] and [hrp] UnknownWitnessAddress( - Uint8List data, { required int version, required String hrp, } - ) : super._(version, data, hrp); + Uint8List data, { + required int version, + required String hrp, + }) : super._(version, data, hrp); /// Constructs a bech32 witness address with the "witness program" [data] /// provided as a [hex] string. UnknownWitnessAddress.fromHex( - String hex, { required int version, required String hrp, } - ) : super._(version, hexToBytes(hex), hrp); + String hex, { + required int version, + required String hrp, + }) : super._(version, hexToBytes(hex), hrp); @override P2Witness get program => P2Witness.fromData(version, _data); - } diff --git a/coinlib/lib/src/encode/bech32.dart b/coinlib/lib/src/encode/bech32.dart index a525277c..cc62c006 100644 --- a/coinlib/lib/src/encode/bech32.dart +++ b/coinlib/lib/src/encode/bech32.dart @@ -1,8 +1,7 @@ final alphabet = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; /// Returns true if a bech32 hrp uses valid characters. -bool hrpValid(String hrp) - => hrp.codeUnits.every((c) => c > 32 && c < 127); +bool hrpValid(String hrp) => hrp.codeUnits.every((c) => c > 32 && c < 127); void _throwOnInvalidHrp(String hrp) { if (!hrpValid(hrp)) { @@ -10,14 +9,14 @@ void _throwOnInvalidHrp(String hrp) { } } -List _charsToWords(String encoded) - => encoded.codeUnits.map((c) => alphabet.codeUnits.indexOf(c)).toList(); +List _charsToWords(String encoded) => + encoded.codeUnits.map((c) => alphabet.codeUnits.indexOf(c)).toList(); List _hrpExpand(String hrp) => [ - ...hrp.codeUnits.map((c) => c >> 5), - 0, - ...hrp.codeUnits.map((c) => c & 31), -]; + ...hrp.codeUnits.map((c) => c >> 5), + 0, + ...hrp.codeUnits.map((c) => c & 31), + ]; int _polymodStep(int pre) { final b = pre >> 25; @@ -29,11 +28,11 @@ int _polymodStep(int pre) { (-((b >> 4) & 1) & 0x2a1462b3)); } -int _polymod(String hrp, List words) - => [1, ..._hrpExpand(hrp), ...words].reduce((a, b) => _polymodStep(a) ^ b); +int _polymod(String hrp, List words) => + [1, ..._hrpExpand(hrp), ...words].reduce((a, b) => _polymodStep(a) ^ b); List _checksum(String hrp, List words, int checkConst) { - final polymod = _polymod(hrp, [...words,0,0,0,0,0,0]) ^ checkConst; + final polymod = _polymod(hrp, [...words, 0, 0, 0, 0, 0, 0]) ^ checkConst; return List.generate(6, (i) => (polymod >> 5 * (5 - i)) & 31); } @@ -41,7 +40,6 @@ List _checksum(String hrp, List words, int checkConst) { /// ([pad]). Returns the new data or null if conversion was not possible. /// Used to convert to and from the 5-bit words for bech32. List? convertBits(List data, int from, int to, bool pad) { - var acc = 0; var bits = 0; List result = []; @@ -66,13 +64,13 @@ List? convertBits(List data, int from, int to, bool pad) { } return result; - } class InvalidBech32 implements Exception { final String message; InvalidBech32([this.message = ""]); } + class InvalidBech32Checksum implements Exception {} enum Bech32Type { bech32, bech32m } @@ -80,7 +78,6 @@ enum Bech32Type { bech32, bech32m } /// Encapsules 5-bit words that can be bech32 encoded and decoded. The 5-bit /// words may need to be further converted into the required data. class Bech32 { - static const maxLength = 90; static const checksumLength = 6; static const bech32CheckConst = 1; @@ -91,15 +88,20 @@ class Bech32 { final Bech32Type type; Bech32._skipValidation({ - required this.hrp, required List words, required this.type, - }): words = List.unmodifiable(words); + required this.hrp, + required List words, + required this.type, + }) : words = List.unmodifiable(words); /// Creates an encodable object with the human-readable-part ([hrp]), [words] /// for the given bech32 [type] (bech32 or bech32m). Bech32({ - required String hrp, required List words, required this.type, - }) : hrp = hrp.toLowerCase(), words = List.unmodifiable(words) { - + required String hrp, + required List words, + required this.type, + bool ignoreMaxLengthCheckForMweb = false, + }) : hrp = hrp.toLowerCase(), + words = List.unmodifiable(words) { if (hrp.isEmpty) throw InvalidBech32("Missing HRP"); _throwOnInvalidHrp(hrp); @@ -107,18 +109,20 @@ class Bech32 { throw InvalidBech32("Words outside of 5-bit range"); } - if (hrp.length + 1 + words.length + checksumLength > maxLength) { + if (!ignoreMaxLengthCheckForMweb && + hrp.length + 1 + words.length + checksumLength > maxLength) { throw InvalidBech32("Bech32 too long"); } - } /// Decodes a bech32 string into the hrp, 5-bit words and type. May throw an /// [InvalidBech32]. It will throw [InvalidBech32Checksum] if the bech32 is /// valid but doesn't have a valid checksum for either bech32 or bech32m. - factory Bech32.decode(String encoded) { - - if (encoded.length > maxLength) { + factory Bech32.decode( + String encoded, { + bool ignoreMaxLengthCheckForMweb = false, + }) { + if (!ignoreMaxLengthCheckForMweb && encoded.length > maxLength) { throw InvalidBech32("Bech32 too long"); } @@ -153,28 +157,27 @@ class Bech32 { throw InvalidBech32Checksum(); } - final bech32 = Bech32._skipValidation(hrp: hrp, words: dataWords, type: type); + final bech32 = + Bech32._skipValidation(hrp: hrp, words: dataWords, type: type); bech32._encodedCache = encoded; return bech32; - } String? _encodedCache; + /// Encodes a bech32 string String encode() { - if (_encodedCache != null) return _encodedCache!; final wordsWithChecksum = [ - ...words, ..._checksum( - hrp, words, + ...words, + ..._checksum( + hrp, + words, type == Bech32Type.bech32 ? bech32CheckConst : bech32mCheckConst, ), ]; final encodedWords = wordsWithChecksum.map((w) => alphabet[w]).join(); return _encodedCache = "${hrp}1$encodedWords"; - } - } - diff --git a/coinlib/lib/src/network.dart b/coinlib/lib/src/network.dart index 12b88b0c..47d23425 100644 --- a/coinlib/lib/src/network.dart +++ b/coinlib/lib/src/network.dart @@ -1,7 +1,7 @@ class Network { final int wifPrefix, p2pkhPrefix, p2shPrefix, privHDPrefix, pubHDPrefix; - final String bech32Hrp, messagePrefix; + final String bech32Hrp, messagePrefix, mwebBech32Hrp; final BigInt minFee, minOutput, feePerKb; Network({ @@ -11,6 +11,7 @@ class Network { required this.privHDPrefix, required this.pubHDPrefix, required this.bech32Hrp, + this.mwebBech32Hrp = "", required this.messagePrefix, required this.minFee, required this.minOutput, @@ -24,6 +25,7 @@ class Network { privHDPrefix: 0x0488ade4, pubHDPrefix: 0x0488b21e, bech32Hrp: "pc", + mwebBech32Hrp: "", messagePrefix: "Peercoin Signed Message:\n", minFee: BigInt.from(1000), minOutput: BigInt.from(10000), @@ -37,6 +39,7 @@ class Network { privHDPrefix: 0x043587CF, pubHDPrefix: 0x04358394, bech32Hrp: "tpc", + mwebBech32Hrp: "", messagePrefix: "Peercoin Signed Message:\n", minFee: BigInt.from(1000), minOutput: BigInt.from(10000), diff --git a/coinlib/lib/src/scripts/programs/mweb.dart b/coinlib/lib/src/scripts/programs/mweb.dart new file mode 100644 index 00000000..d282a65e --- /dev/null +++ b/coinlib/lib/src/scripts/programs/mweb.dart @@ -0,0 +1,32 @@ +import 'dart:typed_data'; + +import '../../../coinlib.dart'; + +class MwebScriptOp implements ScriptOp { + final Uint8List data; + + MwebScriptOp(this.data); + + @override + String get asm => throw UnimplementedError(); + + @override + Uint8List get compiled => data; + + @override + ECDSAInputSignature? get ecdsaSig => null; + + @override + bool match(ScriptOp other) { + throw UnimplementedError(); + } + + @override + int? get number => null; + + @override + ECPublicKey? get publicKey => null; + + @override + SchnorrInputSignature? get schnorrSig => null; +} diff --git a/coinlib/lib/src/tx/sighash/legacy_signature_hasher.dart b/coinlib/lib/src/tx/sighash/legacy_signature_hasher.dart index daf0396f..98eb61fe 100644 --- a/coinlib/lib/src/tx/sighash/legacy_signature_hasher.dart +++ b/coinlib/lib/src/tx/sighash/legacy_signature_hasher.dart @@ -71,6 +71,7 @@ final class LegacySignatureHasher extends SignatureHasher { inputs: modifiedInputs, outputs: modifiedOutputs, locktime: tx.locktime, + mwebBytes: tx.mwebBytes, ); // Add sighash type onto the end diff --git a/coinlib/lib/src/tx/transaction.dart b/coinlib/lib/src/tx/transaction.dart index aefd7bb2..6254e6bd 100644 --- a/coinlib/lib/src/tx/transaction.dart +++ b/coinlib/lib/src/tx/transaction.dart @@ -47,6 +47,9 @@ class Transaction with Writable { final List inputs; final List outputs; final Locktime locktime; + /// Raw MWEB extension data carried between the witness data and locktime, + /// as used by Litecoin. This is kept as opaque bytes. + final Uint8List? mwebBytes; /// Constructs a transaction with the given [inputs] and [outputs]. /// [TransactionTooLarge] will be thrown if the resulting transction exceeds @@ -59,6 +62,7 @@ class Transaction with Writable { required Iterable inputs, required Iterable outputs, this.locktime = Locktime.zero, + this.mwebBytes, }) : inputs = List.unmodifiable(inputs), outputs = List.unmodifiable(outputs) @@ -77,11 +81,17 @@ class Transaction with Writable { final version = reader.readInt32(); + bool hasWitness = false; + bool hasMweb = false; if (witness) { - // Check for witness data + // Check for witness data. The 0x01 flag bit signals witness data and + // the 0x08 flag bit signals MWEB data as used by Litecoin. final marker = reader.readUInt8(); final flag = reader.readUInt8(); - if (marker != 0 || flag != 1) return null; + if (marker != 0) return null; + hasWitness = (flag & 1) != 0; + hasMweb = (flag & 8) != 0; + if (!hasWitness && !hasMweb) return null; } final rawInputs = List.generate( @@ -96,10 +106,19 @@ class Transaction with Writable { // Match the raw inputs with witness data if this is a witness transaction final inputs = rawInputs.map( - (raw) => Input.match(raw, witness ? reader.readVector() : []), + (raw) => Input.match(raw, hasWitness ? reader.readVector() : []), // Create list now to ensure we read the witness data before the locktime ).toList(); + // Any MWEB data is carried between the witness data and the locktime + Uint8List? mwebBytes; + if (hasMweb) { + final remaining = reader.bytes.lengthInBytes - reader.offset; + if (remaining > 4) { + mwebBytes = reader.readSlice(remaining - 4); + } + } + final locktime = reader.readUInt32(); return Transaction( @@ -107,6 +126,7 @@ class Transaction with Writable { inputs: inputs, outputs: outputs, locktime: Locktime(locktime), + mwebBytes: mwebBytes, ); } @@ -166,7 +186,7 @@ class Transaction with Writable { if (isWitness) { writer.writeUInt8(0); // Marker - writer.writeUInt8(1); // Flag + writer.writeUInt8(mwebBytes != null ? 9 : 1); // Flag } writer.writeVarInt(BigInt.from(inputs.length)); @@ -185,6 +205,10 @@ class Transaction with Writable { } } + if (mwebBytes != null) { + writer.writeSlice(mwebBytes!); + } + writer.writeUInt32(locktime.value); } @@ -194,6 +218,7 @@ class Transaction with Writable { inputs: newInputs, outputs: outputs, locktime: locktime, + mwebBytes: mwebBytes, ); T _requireInputOfType(int inputN) { @@ -346,6 +371,7 @@ class Transaction with Writable { ], outputs: outputs, locktime: locktime, + mwebBytes: mwebBytes, ); /// Returns a new [Transaction] with the [output] added to the end of the @@ -370,6 +396,7 @@ class Transaction with Writable { inputs: modifiedInputs, outputs: [...outputs, output], locktime: locktime, + mwebBytes: mwebBytes, ); } @@ -394,6 +421,7 @@ class Transaction with Writable { ), outputs: outputs, locktime: locktime, + mwebBytes: mwebBytes, ) : this; @@ -414,8 +442,9 @@ class Transaction with Writable { String get txid => bytesToHex(Uint8List.fromList(legacyHash.reversed.toList())); - /// If the transaction has any witness inputs. - bool get isWitness => inputs.any((input) => input is WitnessInput); + /// If the transaction has any witness inputs or carries MWEB data. + bool get isWitness + => inputs.any((input) => input is WitnessInput) || mwebBytes != null; bool get isCoinBase => inputs.length == 1 diff --git a/coinlib/test/address_test.dart b/coinlib/test/address_test.dart index 95ad1376..e1b6d0e4 100644 --- a/coinlib/test/address_test.dart +++ b/coinlib/test/address_test.dart @@ -1,6 +1,8 @@ import 'dart:typed_data'; + import 'package:coinlib/coinlib.dart'; import 'package:test/test.dart'; + import 'vectors/taproot.dart'; final wrongNetwork = Network( @@ -10,6 +12,7 @@ final wrongNetwork = Network( privHDPrefix: 0, pubHDPrefix: 0, bech32Hrp: "wrong", + mwebBech32Hrp: "", messagePrefix: "", feePerKb: BigInt.from(10000), minFee: BigInt.from(1000), @@ -43,9 +46,10 @@ void expectBech32Equal(Bech32Address addr, Bech32Address expected) { } void expectValidAddress( - String encoded, Network network, T expected, + String encoded, + Network network, + T expected, ) { - final baseDecoded = Address.fromString(encoded, network); expect(baseDecoded, isA()); @@ -78,13 +82,10 @@ void expectValidAddress( expectBech32Equal(b32Base, expected); expectBech32Equal(b32Sub, expected); } - } void main() { - group("Address", () { - late ECPublicKey pubkey; setUpAll(() async { await loadCoinlib(); @@ -94,7 +95,6 @@ void main() { }); test("valid P2PKH addresses", () { - expectValidAddress( "P8bB9yPr3vVByqfmM5KXftyGckAtAdu6f8", Network.mainnet, @@ -108,14 +108,13 @@ void main() { "PGkLtYrKeMDbBCaFy4yMRhN9ZTjJp2y8Pb", Network.mainnet, P2PKHAddress.fromPublicKey( - pubkey, version: Network.mainnet.p2pkhPrefix, + pubkey, + version: Network.mainnet.p2pkhPrefix, ), ); - }); test("valid P2SH addresses", () { - expectValidAddress( "pUtBBpAznHgPW9TDtWJcDo7qGXQJqnf1W9", Network.mainnet, @@ -133,11 +132,9 @@ void main() { version: Network.mainnet.p2shPrefix, ), ); - }); test("valid P2WPKH addresses", () { - expectValidAddress( "pc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqmtd2rq", Network.mainnet, @@ -151,14 +148,13 @@ void main() { "pc1qt97wqg464zrhnx23upykca5annqvwkwuvqmpk5", Network.mainnet, P2WPKHAddress.fromPublicKey( - pubkey, hrp: Network.mainnet.bech32Hrp, + pubkey, + hrp: Network.mainnet.bech32Hrp, ), ); - }); test("valid P2WSH addresses", () { - expectValidAddress( "pc1qlllllllllllllllllllllllllllllllllllllllllllllllllllsm5knxw", Network.mainnet, @@ -178,11 +174,9 @@ void main() { hrp: Network.mainnet.bech32Hrp, ), ); - }); test("valid P2TR addresses", () { - expectValidAddress( "pc1punvppl2stp38f7kwv2u2spltjuvuaayuqsthe34hd2dyy5w4g58qj5f0v2", Network.mainnet, @@ -213,11 +207,9 @@ void main() { hrp: Network.mainnet.bech32Hrp, ), ); - }); test("valid unknown witness addresses", () { - // 40 program bytes expectValidAddress( "pc1sqqqsyqcyq5rqwzqfpg9scrgwpugpzysnzs23v9ccrydpk8qarc0jqgfzyvjz2f38pj2w3g", @@ -239,11 +231,9 @@ void main() { hrp: Network.mainnet.bech32Hrp, ), ); - }); test("invalid addresses", () { - for (final invalid in [ // Neither valid bech32 or base58 "", @@ -276,13 +266,13 @@ void main() { reason: invalid, ); } - }); test("invalid checksums", () { expect( () => Address.fromString( - "P8bB9yPr3vVByqfmM5KXftyGckAtAdu6f9", Network.mainnet, + "P8bB9yPr3vVByqfmM5KXftyGckAtAdu6f9", + Network.mainnet, ), throwsA(isA()), ); @@ -305,7 +295,6 @@ void main() { }); test("invalid version, program and hrp arguments", () { - // Too small program expect( () => UnknownWitnessAddress.fromHex( @@ -331,7 +320,7 @@ void main() { () => UnknownWitnessAddress.fromHex( "0001", version: 16, - hrp: "\x7f""1axkwrx", + hrp: "\x7f" "1axkwrx", ), throwsArgumentError, ); @@ -353,38 +342,41 @@ void main() { ), throwsArgumentError, ); - }); final longHrp = - "thishrpis78byteslongleadingthetotalsizetobe90characterswitheverythingincluded1"; + "thishrpis78byteslongleadingthetotalsizetobe90characterswitheverythingincluded1"; test("arguments correct size", () { - - final addr = UnknownWitnessAddress.fromHex("0001", version: 16, hrp: longHrp); + final addr = + UnknownWitnessAddress.fromHex("0001", version: 16, hrp: longHrp); expectValidAddress( "${longHrp}1sqqqs3t97ut", Network( - wifPrefix: 0, p2shPrefix: 0, p2pkhPrefix: 0, - privHDPrefix: 0, pubHDPrefix: 0, - bech32Hrp: longHrp, messagePrefix: "", + wifPrefix: 0, + p2shPrefix: 0, + p2pkhPrefix: 0, + privHDPrefix: 0, + pubHDPrefix: 0, + bech32Hrp: longHrp, + mwebBech32Hrp: "", + messagePrefix: "", feePerKb: BigInt.from(10000), minFee: BigInt.from(1000), minOutput: BigInt.from(10000), ), addr, ); - }); test("arguments too long", () { expect( - () => UnknownWitnessAddress.fromHex("0001", version: 16, hrp: "${longHrp}1"), + () => UnknownWitnessAddress.fromHex("0001", + version: 16, hrp: "${longHrp}1"), throwsArgumentError, ); }); - }); group("Base58Address", () { @@ -406,5 +398,4 @@ void main() { expect(addr.data, Uint8List(20)); }); }); - }