diff --git a/lib/bitcoin_flutter.dart b/lib/bitcoin_flutter.dart index a77de46..1b7f5a9 100644 --- a/lib/bitcoin_flutter.dart +++ b/lib/bitcoin_flutter.dart @@ -1,6 +1,3 @@ -/// Support for doing something awesome. -/// -/// More dartdocs go here. library bitcoin_flutter; export 'src/bitcoin_flutter_base.dart'; @@ -11,5 +8,7 @@ export 'src/transaction_builder.dart'; export 'src/ecpair.dart'; export 'src/payments/p2pkh.dart'; export 'src/payments/p2wpkh.dart'; +export 'src/payments/p2sh.dart'; export 'src/payments/index.dart'; -// TODO: Export any libraries intended for clients of this package. +export 'src/utils/magic_hash.dart'; + diff --git a/lib/src/address.dart b/lib/src/address.dart index c310f12..259cbee 100644 --- a/lib/src/address.dart +++ b/lib/src/address.dart @@ -1,13 +1,16 @@ import 'dart:typed_data'; +import 'package:defichain_bech32/defichain_bech32.dart'; + import 'models/networks.dart'; import 'package:bs58check/bs58check.dart' as bs58check; -import 'package:bech32/bech32.dart'; +// import 'package:bech32/bech32.dart'; import 'payments/index.dart' show PaymentData; import 'payments/p2pkh.dart'; +import 'payments/p2sh.dart'; import 'payments/p2wpkh.dart'; class Address { - static bool validateAddress(String address, [NetworkType nw]) { + static bool validateAddress(String address, [NetworkType? nw]) { try { addressToOutputScript(address, nw); return true; @@ -16,33 +19,45 @@ class Address { } } - static Uint8List addressToOutputScript(String address, [NetworkType nw]) { - NetworkType network = nw ?? bitcoin; + static Uint8List? addressToOutputScript(String address, [NetworkType? nw]) { + var network = nw ?? bitcoin; var decodeBase58; var decodeBech32; try { decodeBase58 = bs58check.decode(address); - } catch (err) {} + } catch (err) { + // Base58check decode fail + } if (decodeBase58 != null) { - if (decodeBase58[0] != network.pubKeyHash) - throw new ArgumentError('Invalid version or Network mismatch'); - P2PKH p2pkh = - new P2PKH(data: new PaymentData(address: address), network: network); - return p2pkh.data.output; + if (decodeBase58[0] == network.pubKeyHash) { + return P2PKH(data: PaymentData(address: address), network: network) + .data + .output; + } + if (decodeBase58[0] == network.scriptHash) { + return P2SH(data: PaymentData(address: address), network: network) + .data! + .output; + } + throw ArgumentError('Invalid version or Network mismatch'); } else { try { - decodeBech32 = segwit.decode(address); - } catch (err) {} + decodeBech32 = segwit.decode(SegwitInput(network.bech32!, address)); + } catch (err) { + // Bech32 decode fail + } if (decodeBech32 != null) { - if (network.bech32 != decodeBech32.hrp) - throw new ArgumentError('Invalid prefix or Network mismatch'); - if (decodeBech32.version != 0) - throw new ArgumentError('Invalid address version'); - P2WPKH p2wpkh = new P2WPKH( - data: new PaymentData(address: address), network: network); - return p2wpkh.data.output; + if (network.bech32 != decodeBech32.hrp) { + throw ArgumentError('Invalid prefix or Network mismatch'); + } + if (decodeBech32.version != 0) { + throw ArgumentError('Invalid address version'); + } + var p2wpkh = + P2WPKH(data: PaymentData(address: address), network: network); + return p2wpkh.data!.output; } } - throw new ArgumentError(address + ' has no matching Script'); + throw ArgumentError(address + ' has no matching Script'); } } diff --git a/lib/src/bitcoin_flutter_base.dart b/lib/src/bitcoin_flutter_base.dart index 6de8167..406b7ec 100644 --- a/lib/src/bitcoin_flutter_base.dart +++ b/lib/src/bitcoin_flutter_base.dart @@ -12,65 +12,65 @@ import 'dart:convert'; /// Checks if you are awesome. Spoiler: you are. class HDWallet { - bip32.BIP32 _bip32; - P2PKH _p2pkh; - String seed; + bip32.BIP32? _bip32; + P2PKH? _p2pkh; + String? seed; NetworkType network; - String get privKey { + String? get privKey { if (_bip32 == null) return null; try { - return HEX.encode(_bip32.privateKey); + return HEX.encode(_bip32!.privateKey!); } catch (_) { return null; } } - String get pubKey => _bip32 != null ? HEX.encode(_bip32.publicKey) : null; + String? get pubKey => _bip32 != null ? HEX.encode(_bip32!.publicKey) : null; - String get base58Priv { + String? get base58Priv { if (_bip32 == null) return null; try { - return _bip32.toBase58(); + return _bip32!.toBase58(); } catch (_) { return null; } } - String get base58 => _bip32 != null ? _bip32.neutered().toBase58() : null; + String? get base58 => _bip32 != null ? _bip32!.neutered().toBase58() : null; - String get wif { + String? get wif { if (_bip32 == null) return null; try { - return _bip32.toWIF(); + return _bip32!.toWIF(); } catch (_) { return null; } } - String get address => _p2pkh != null ? _p2pkh.data.address : null; + String? get address => _p2pkh != null ? _p2pkh!.data.address : null; HDWallet( - {@required bip32, @required p2pkh, @required this.network, this.seed}) { + {required bip32, required p2pkh, required this.network, this.seed}) { this._bip32 = bip32; this._p2pkh = p2pkh; } HDWallet derivePath(String path) { - final bip32 = _bip32.derivePath(path); + final bip32 = _bip32!.derivePath(path); final p2pkh = new P2PKH( data: new PaymentData(pubkey: bip32.publicKey), network: network); return HDWallet(bip32: bip32, p2pkh: p2pkh, network: network); } HDWallet derive(int index) { - final bip32 = _bip32.derive(index); + final bip32 = _bip32!.derive(index); final p2pkh = new P2PKH( data: new PaymentData(pubkey: bip32.publicKey), network: network); return HDWallet(bip32: bip32, p2pkh: p2pkh, network: network); } - factory HDWallet.fromSeed(Uint8List seed, {NetworkType network}) { + factory HDWallet.fromSeed(Uint8List seed, {NetworkType? network}) { network = network ?? bitcoin; final seedHex = HEX.encode(seed); final wallet = bip32.BIP32.fromSeed( @@ -85,7 +85,7 @@ class HDWallet { bip32: wallet, p2pkh: p2pkh, network: network, seed: seedHex); } - factory HDWallet.fromBase58(String xpub, {NetworkType network}) { + factory HDWallet.fromBase58(String xpub, {NetworkType? network}) { network = network ?? bitcoin; final wallet = bip32.BIP32.fromBase58( xpub, @@ -98,14 +98,14 @@ class HDWallet { return HDWallet(bip32: wallet, p2pkh: p2pkh, network: network, seed: null); } - Uint8List sign(String message) { + Uint8List? sign(String message) { Uint8List messageHash = magicHash(message, network); - return _bip32.sign(messageHash); + return _bip32!.sign(messageHash); } - bool verify({String message, Uint8List signature}) { + bool? verify({required String message, required Uint8List signature}) { Uint8List messageHash = magicHash(message); - return _bip32.verify(messageHash, signature); + return _bip32!.verify(messageHash, signature); } } @@ -113,27 +113,27 @@ class Wallet { ECPair _keyPair; P2PKH _p2pkh; - String get privKey => - _keyPair != null ? HEX.encode(_keyPair.privateKey) : null; + String? get privKey => + _keyPair != null ? HEX.encode(_keyPair.privateKey!) : null; - String get pubKey => _keyPair != null ? HEX.encode(_keyPair.publicKey) : null; + String? get pubKey => _keyPair != null ? HEX.encode(_keyPair.publicKey!) : null; - String get wif => _keyPair != null ? _keyPair.toWIF() : null; + String? get wif => _keyPair != null ? _keyPair.toWIF() : null; - String get address => _p2pkh != null ? _p2pkh.data.address : null; + String? get address => _p2pkh != null ? _p2pkh.data.address : null; - NetworkType network; + NetworkType? network; Wallet(this._keyPair, this._p2pkh, this.network); - factory Wallet.random([NetworkType network]) { + factory Wallet.random([NetworkType? network]) { final _keyPair = ECPair.makeRandom(network: network); final _p2pkh = new P2PKH( data: new PaymentData(pubkey: _keyPair.publicKey), network: network); return Wallet(_keyPair, _p2pkh, network); } - factory Wallet.fromWIF(String wif, [NetworkType network]) { + factory Wallet.fromWIF(String wif, [NetworkType? network]) { network = network ?? bitcoin; final _keyPair = ECPair.fromWIF(wif, network: network); final _p2pkh = new P2PKH( @@ -146,7 +146,7 @@ class Wallet { return _keyPair.sign(messageHash); } - bool verify({String message, Uint8List signature}) { + bool verify({required String message, required Uint8List signature}) { Uint8List messageHash = magicHash(message, network); return _keyPair.verify(messageHash, signature); } diff --git a/lib/src/classify.dart b/lib/src/classify.dart index 9af1b34..9870b64 100644 --- a/lib/src/classify.dart +++ b/lib/src/classify.dart @@ -2,7 +2,8 @@ import 'dart:typed_data'; import '../src/utils/script.dart' as bscript; import 'templates/pubkeyhash.dart' as pubkeyhash; import 'templates/pubkey.dart' as pubkey; -import 'templates/witnesspubkeyhash.dart' as witnessPubKeyHash; +import 'templates/scriptHash.dart' as scripthash; +import 'templates/witnesspubkeyhash.dart' as witness_pubkey_hash; const SCRIPT_TYPES = { 'P2SM': 'multisig', @@ -16,25 +17,30 @@ const SCRIPT_TYPES = { 'WITNESS_COMMITMENT': 'witnesscommitment' }; -String classifyOutput(Uint8List script) { - if (witnessPubKeyHash.outputCheck(script)) return SCRIPT_TYPES['P2WPKH']; +String? classifyOutput(Uint8List script) { + if (witness_pubkey_hash.outputCheck(script)) return SCRIPT_TYPES['P2WPKH']; if (pubkeyhash.outputCheck(script)) return SCRIPT_TYPES['P2PKH']; + if (scripthash.outputCheck(script)) return SCRIPT_TYPES['P2SH']; final chunks = bscript.decompile(script); - if (chunks == null) throw new ArgumentError('Invalid script'); + if (chunks == null) throw ArgumentError('Invalid script'); return SCRIPT_TYPES['NONSTANDARD']; } -String classifyInput(Uint8List script) { +String? classifyInput(Uint8List? script, bool allowIncomplete) { final chunks = bscript.decompile(script); if (chunks == null) throw new ArgumentError('Invalid script'); if (pubkeyhash.inputCheck(chunks)) return SCRIPT_TYPES['P2PKH']; if (pubkey.inputCheck(chunks)) return SCRIPT_TYPES['P2PK']; + if (scripthash.inputCheck(chunks, allowIncomplete)) { + return SCRIPT_TYPES['P2SH']; + } + if (pubkey.inputCheck(chunks)) return SCRIPT_TYPES['P2PK']; return SCRIPT_TYPES['NONSTANDARD']; } -String classifyWitness(List script) { +String? classifyWitness(List? script) { final chunks = bscript.decompile(script); - if (chunks == null) throw new ArgumentError('Invalid script'); - if (witnessPubKeyHash.inputCheck(chunks)) return SCRIPT_TYPES['P2WPKH']; + if (chunks == null) throw ArgumentError('Invalid script'); + if (witness_pubkey_hash.inputCheck(chunks)) return SCRIPT_TYPES['P2WPKH']; return SCRIPT_TYPES['NONSTANDARD']; } diff --git a/lib/src/ecpair.dart b/lib/src/ecpair.dart index fc8ce89..02b5fdc 100644 --- a/lib/src/ecpair.dart +++ b/lib/src/ecpair.dart @@ -1,88 +1,93 @@ +import 'dart:convert'; import 'dart:typed_data'; import 'dart:math'; -import 'package:bip32/src/utils/ecurve.dart' as ecc; -import 'package:bip32/src/utils/wif.dart' as wif; +import 'package:bip32_defichain/bip32.dart' as seg; +import 'package:bip32_defichain/src/utils/ecurve.dart' as ecc; +import 'package:bip32_defichain/src/utils/wif.dart' as wif; +import 'package:bitcoin_flutter/src/utils/magic_hash.dart'; +import 'package:hex/hex.dart'; +import 'package:pointycastle/ecc/api.dart'; import 'models/networks.dart'; class ECPair { - Uint8List _d; - Uint8List _Q; - NetworkType network; - bool compressed; - ECPair(Uint8List _d, Uint8List _Q, {network, compressed}) { + Uint8List? _d; + Uint8List? _Q; + NetworkType? network; + bool? compressed; + ECPair(Uint8List? _d, Uint8List? _Q, {network, compressed}) { this._d = _d; this._Q = _Q; this.network = network ?? bitcoin; this.compressed = compressed ?? true; } - Uint8List get publicKey { - if (_Q == null) _Q = ecc.pointFromScalar(_d, compressed); + Uint8List? get publicKey { + _Q ??= ecc.pointFromScalar(_d!, compressed!); return _Q; } - Uint8List get privateKey => _d; + Uint8List? get privateKey => _d; String toWIF() { if (privateKey == null) { - throw new ArgumentError('Missing private key'); + throw ArgumentError('Missing private key'); } - return wif.encode(new wif.WIF( - version: network.wif, privateKey: privateKey, compressed: compressed)); + return wif.encode(wif.WIF(version: network!.wif, privateKey: privateKey!, compressed: compressed!)); } Uint8List sign(Uint8List hash) { - return ecc.sign(hash, privateKey); + return ecc.sign(hash, privateKey!); + } + + String signMessage(String message, [NetworkType? network, seg.SegwitType segwitType = seg.SegwitType.None, bool compressed = true]) { + var hash = magicHash(message, network); + + return base64Encode(ecc.signMessage(hash, privateKey!, compressed, segwitType)); } bool verify(Uint8List hash, Uint8List signature) { - return ecc.verify(hash, publicKey, signature); + return ecc.verify(hash, publicKey!, signature); } - factory ECPair.fromWIF(String w, {NetworkType network}) { - wif.WIF decoded = wif.decode(w); + factory ECPair.fromWIF(String w, {NetworkType? network}) { + var decoded = wif.decode(w); final version = decoded.version; // TODO support multi networks NetworkType nw; if (network != null) { nw = network; - if (nw.wif != version) throw new ArgumentError('Invalid network version'); + if (nw.wif != version) throw ArgumentError('Invalid network version'); } else { if (version == bitcoin.wif) { nw = bitcoin; } else if (version == testnet.wif) { nw = testnet; } else { - throw new ArgumentError('Unknown network version'); + throw ArgumentError('Unknown network version'); } } - return ECPair.fromPrivateKey(decoded.privateKey, - compressed: decoded.compressed, network: nw); + return ECPair.fromPrivateKey(decoded.privateKey, compressed: decoded.compressed, network: nw); } - factory ECPair.fromPublicKey(Uint8List publicKey, - {NetworkType network, bool compressed}) { + factory ECPair.fromPublicKey(Uint8List publicKey, {NetworkType? network, bool? compressed}) { if (!ecc.isPoint(publicKey)) { - throw new ArgumentError('Point is not on the curve'); + throw ArgumentError('Point is not on the curve'); } - return new ECPair(null, publicKey, - network: network, compressed: compressed); + return ECPair(null, publicKey, network: network, compressed: compressed); } - factory ECPair.fromPrivateKey(Uint8List privateKey, - {NetworkType network, bool compressed}) { - if (privateKey.length != 32) - throw new ArgumentError( - 'Expected property privateKey of type Buffer(Length: 32)'); - if (!ecc.isPrivate(privateKey)) - throw new ArgumentError('Private key not in range [1, n)'); - return new ECPair(privateKey, null, - network: network, compressed: compressed); + factory ECPair.fromPrivateKey(Uint8List privateKey, {NetworkType? network, bool? compressed}) { + if (privateKey.length != 32) { + throw ArgumentError('Expected property privateKey of type Buffer(Length: 32)'); + } + if (!ecc.isPrivate(privateKey)) { + throw ArgumentError('Private key not in range [1, n)'); + } + return ECPair(privateKey, null, network: network, compressed: compressed); } - factory ECPair.makeRandom( - {NetworkType network, bool compressed, Function rng}) { + factory ECPair.makeRandom({NetworkType? network, bool? compressed, Function? rng}) { final rfunc = rng ?? _randomBytes; - Uint8List d; + Uint8List? d; // int beginTime = DateTime.now().millisecondsSinceEpoch; do { d = rfunc(32); - if (d.length != 32) throw ArgumentError('Expected Buffer(Length: 32)'); + if (d!.length != 32) throw ArgumentError('Expected Buffer(Length: 32)'); // if (DateTime.now().millisecondsSinceEpoch - beginTime > 5000) throw ArgumentError('Timeout'); } while (!ecc.isPrivate(d)); return ECPair.fromPrivateKey(d, network: network, compressed: compressed); diff --git a/lib/src/models/networks.dart b/lib/src/models/networks.dart index dc4771d..a9a633f 100644 --- a/lib/src/models/networks.dart +++ b/lib/src/models/networks.dart @@ -1,20 +1,18 @@ -import 'package:meta/meta.dart'; - class NetworkType { String messagePrefix; - String bech32; + String? bech32; Bip32Type bip32; int pubKeyHash; int scriptHash; int wif; NetworkType( - {@required this.messagePrefix, + {required this.messagePrefix, this.bech32, - @required this.bip32, - @required this.pubKeyHash, - @required this.scriptHash, - @required this.wif}); + required this.bip32, + required this.pubKeyHash, + required this.scriptHash, + required this.wif}); @override String toString() { @@ -26,7 +24,7 @@ class Bip32Type { int public; int private; - Bip32Type({@required this.public, @required this.private}); + Bip32Type({required this.public, required this.private}); @override String toString() { diff --git a/lib/src/payments/index.dart b/lib/src/payments/index.dart index b2b3ed9..a729c50 100644 --- a/lib/src/payments/index.dart +++ b/lib/src/payments/index.dart @@ -1,25 +1,88 @@ import 'dart:typed_data'; class PaymentData { - String address; - Uint8List hash; - Uint8List output; - Uint8List signature; - Uint8List pubkey; - Uint8List input; - List witness; + String? name; + String? address; + Uint8List? hash; + Uint8List? output; + Uint8List? signature; + Uint8List? pubkey; + Uint8List? input; + List? witness; + PaymentData? redeem; PaymentData( - {this.address, + {this.name, + this.address, this.hash, this.output, this.pubkey, this.input, this.signature, - this.witness}); + this.witness, + this.redeem}); + + dynamic operator [](String key) { + switch (key) { + case 'name': + return name; + case 'address': + return address; + case 'hash': + return hash; + case 'output': + return output; + case 'pubkey': + return pubkey; + case 'input': + return input; + case 'signature': + return signature; + case 'witness': + return witness; + case 'redeem': + return redeem; + default: + throw ArgumentError('Invalid PaymentData key'); + } + } + + operator []=(String key, dynamic value) { + switch (key) { + case 'name': + name = value; + break; + case 'address': + address = value; + break; + case 'hash': + hash = value; + break; + case 'output': + output = value; + break; + case 'pubkey': + pubkey = value; + break; + case 'input': + input = value; + break; + case 'signature': + signature = value; + break; + case 'witness': + witness = value; + break; + case 'redeem': + redeem = value; + break; + default: + throw ArgumentError('Invalid PaymentData key'); + } + } @override String toString() { - return 'PaymentData{address: $address, hash: $hash, output: $output, signature: $signature, pubkey: $pubkey, input: $input, witness: $witness}'; + return 'PaymentData{name: $name, address: $address, hash: $hash, output: $output, signature: $signature, pubkey: $pubkey, input: $input, witness: $witness, redeem: ${redeem.toString()}}'; } -} +} \ No newline at end of file diff --git a/lib/src/payments/p2pk.dart b/lib/src/payments/p2pk.dart index 796d6dd..30c1357 100644 --- a/lib/src/payments/p2pk.dart +++ b/lib/src/payments/p2pk.dart @@ -1,18 +1,13 @@ -import 'dart:typed_data'; -import 'package:meta/meta.dart'; -import 'package:bip32/src/utils/ecurve.dart' show isPoint; -import 'package:bs58check/bs58check.dart' as bs58check; +import 'package:bip32_defichain/src/utils/ecurve.dart' show isPoint; -import '../crypto.dart'; import '../models/networks.dart'; import '../payments/index.dart' show PaymentData; -import '../utils/script.dart' as bscript; import '../utils/constants/op.dart'; class P2PK { - PaymentData data; - NetworkType network; - P2PK({@required data, network}) { + late PaymentData data; + NetworkType? network; + P2PK({required data, network}) { this.network = network ?? bitcoin; this.data = data; _init(); @@ -20,9 +15,9 @@ class P2PK { _init() { if (data.output != null) { - if (data.output[data.output.length - 1] != OPS['OP_CHECKSIG']) + if (data.output![data.output!.length - 1] != OPS['OP_CHECKSIG']) throw new ArgumentError('Output is invalid'); - if (!isPoint(data.output.sublist(1, -1))) + if (!isPoint(data.output!.sublist(1, -1))) throw new ArgumentError('Output pubkey is invalid'); } if (data.input != null) { diff --git a/lib/src/payments/p2pkh.dart b/lib/src/payments/p2pkh.dart index 60e5bfd..5fa23a1 100644 --- a/lib/src/payments/p2pkh.dart +++ b/lib/src/payments/p2pkh.dart @@ -1,6 +1,5 @@ import 'dart:typed_data'; -import 'package:meta/meta.dart'; -import 'package:bip32/src/utils/ecurve.dart' show isPoint; +import 'package:bip32_defichain/src/utils/ecurve.dart' show isPoint; import 'package:bs58check/bs58check.dart' as bs58check; import '../crypto.dart'; @@ -10,30 +9,30 @@ import '../utils/script.dart' as bscript; import '../utils/constants/op.dart'; class P2PKH { - PaymentData data; - NetworkType network; - P2PKH({@required data, network}) { + late PaymentData data; + late NetworkType network; + P2PKH({required data, network}) { this.network = network ?? bitcoin; this.data = data; _init(); } _init() { if (data.address != null) { - _getDataFromAddress(data.address); + _getDataFromAddress(data.address!); _getDataFromHash(); } else if (data.hash != null) { _getDataFromHash(); } else if (data.output != null) { - if (!isValidOutput(data.output)) + if (!isValidOutput(data.output!)) throw new ArgumentError('Output is invalid'); - data.hash = data.output.sublist(3, 23); + data.hash = data.output!.sublist(3, 23); _getDataFromHash(); } else if (data.pubkey != null) { - data.hash = hash160(data.pubkey); + data.hash = hash160(data.pubkey!); _getDataFromHash(); _getDataFromChunk(); } else if (data.input != null) { - List _chunks = bscript.decompile(data.input); + List _chunks = bscript.decompile(data.input)!; _getDataFromChunk(_chunks); if (_chunks.length != 2) throw new ArgumentError('Input is invalid'); if (!bscript.isCanonicalScriptSignature(_chunks[0])) @@ -45,12 +44,12 @@ class P2PKH { } } - void _getDataFromChunk([List _chunks]) { + void _getDataFromChunk([List? _chunks]) { if (data.pubkey == null && _chunks != null) { data.pubkey = (_chunks[1] is int) ? new Uint8List.fromList([_chunks[1]]) : _chunks[1]; - data.hash = hash160(data.pubkey); + data.hash = hash160(data.pubkey!); _getDataFromHash(); } if (data.signature == null && _chunks != null) @@ -66,7 +65,7 @@ class P2PKH { if (data.address == null) { final payload = new Uint8List(21); payload.buffer.asByteData().setUint8(0, network.pubKeyHash); - payload.setRange(1, payload.length, data.hash); + payload.setRange(1, payload.length, data.hash!); data.address = bs58check.encode(payload); } if (data.output == null) { @@ -86,7 +85,7 @@ class P2PKH { if (version != network.pubKeyHash) throw new ArgumentError('Invalid version or Network mismatch'); data.hash = payload.sublist(1); - if (data.hash.length != 20) throw new ArgumentError('Invalid address'); + if (data.hash!.length != 20) throw new ArgumentError('Invalid address'); } } diff --git a/lib/src/payments/p2sh.dart b/lib/src/payments/p2sh.dart new file mode 100644 index 0000000..0dae6be --- /dev/null +++ b/lib/src/payments/p2sh.dart @@ -0,0 +1,185 @@ +import 'dart:typed_data'; +// import 'package:bip32_defichain/src/utils/ecurve.dart' show isPoint; +import 'package:bs58check/bs58check.dart' as bs58check; + +import '../crypto.dart'; +import '../models/networks.dart'; +import '../payments/index.dart' show PaymentData; +import '../utils/script.dart' as bscript; +import '../utils/constants/op.dart'; + +class P2SH { + PaymentData? data; + late NetworkType network; + P2SH({required data, network}) { + this.network = network ?? bitcoin; + this.data = data; + _init(); + } + void _init() { + data!.name = 'p2sh'; + + if (data!.address == null && data!.hash == null && data!.output == null && data!.redeem == null && data!.input == null) throw ArgumentError('Not enough data'); + + if (data!.address != null) { + _getDataFromAddress(data!.address!); + _getDataFromHash(); + } + + if (data!.hash != null) { + _getDataFromHash(); + } + + if (data!.output != null) { + if (data!.output!.length != 23 || data!.output![0] != OPS['OP_HASH160'] || data!.output![1] != 0x14 || data!.output![22] != OPS['OP_EQUAL']) { + throw ArgumentError('Output is invalid'); + } + final hash = data!.output!.sublist(2, 22); + + if (data!.hash != null && data!.hash.toString() != hash.toString()) { + throw ArgumentError('Hash mismatch'); + } + data!.hash = hash; + _getDataFromHash(); + } + + if (data!.input != null) { + _getDataFromInput(); + _checkRedeem(data!.redeem!); + _getDataFromRedeem(); + } + + if (data!.redeem != null) { + _checkRedeem(data!.redeem!); + _getDataFromRedeem(); + } + + if (data!.witness != null) { + if (data!.redeem != null && data!.redeem!.witness != null && !_stacksEqual(data!.redeem!.witness!, data!.witness!)) { + throw ArgumentError('Witness and redeem.witness mismatch'); + } + } + } + + void _getDataFromAddress(String address) { + var payload = bs58check.decode(address); + final version = payload.buffer.asByteData().getUint8(0); + if (version != network.scriptHash) { + throw ArgumentError('Invalid version or Network mismatch'); + } + + final hash = payload.sublist(1); + + if (data!.hash != null && data!.hash.toString() != hash.toString()) { + throw ArgumentError('Hash mismatch'); + } + + data!.hash = hash; + + if (data!.hash!.length != 20) throw ArgumentError('Invalid address'); + } + + void _getDataFromHash() { + if (data!.address == null) { + final payload = Uint8List(21); + payload.buffer.asByteData().setUint8(0, network.scriptHash); + payload.setRange(1, payload.length, data!.hash!); + data!.address = bs58check.encode(payload); + } + + data!.output ??= bscript.compile([ + OPS['OP_HASH160'], + data!.hash, + OPS['OP_EQUAL'], + ]); + } + + void _checkRedeem(PaymentData redeem) { + // is the redeem output empty/invalid? + if (redeem.output != null) { + final decompile = bscript.decompile(redeem.output)!; + if (decompile.isEmpty) { + throw ArgumentError('Redeem.output too short'); + } + + // match hash against other sources + final hash2 = hash160(redeem.output!); + if (data!.hash != null && data!.hash!.isNotEmpty && (data!.hash.toString() != hash2.toString())) { + throw ArgumentError('Hash mismatch'); + } + } + + if (redeem.input != null) { + final hasInput = redeem.input!.isNotEmpty; + final hasWitness = redeem.witness != null && redeem.witness!.isNotEmpty; + if (!hasInput && !hasWitness) { + throw ArgumentError('Empty input'); + } + if (hasInput && hasWitness) { + throw ArgumentError('Input and witness provided'); + } + if (hasInput) { + final richunks = bscript.decompile(redeem.input); + if (!bscript.isPushOnly(richunks)) { + throw ArgumentError('Non push-only scriptSig'); + } + } + } + } + + void _getDataFromRedeem() { + if (data!.redeem!.output != null) { + data!.hash = hash160(data!.redeem!.output!); + _getDataFromHash(); + + if (data!.redeem!.input != null) { + var _chunks = bscript.decompile(data!.redeem!.input)!; + _chunks.add(data!.redeem!.output); + _getDataFromChunk(_chunks); + } + } + + data!.witness ??= data!.redeem!.witness ?? []; + } + + void _getDataFromChunk([List? _chunks]) { + if (data!.input == null && _chunks != null) { + data!.input = bscript.compile(_chunks); + } + } + + void _getDataFromInput() { + final chunks = _chunks(); + if (chunks == null || chunks.isEmpty) { + throw ArgumentError('Input too short'); + } + + if (_redeem().output == null) throw ArgumentError('Input is invalid'); + + data!.redeem ??= _redeem(); + } + + List? _chunks() { + return bscript.decompile(data!.input); + } + + PaymentData _redeem() { + final chunks = bscript.decompile(data!.input)!; + final output = chunks[chunks.length - 1] is Uint8List ? chunks[chunks.length - 1] : null; + return PaymentData( + output: output, + input: bscript.compile(chunks.sublist(0, chunks.length - 1)), + witness: data!.witness ?? [], + ); + } + + bool _stacksEqual(List a, List b) { + if (a.length != b.length) return false; + var i = 0; + return a.every((x) { + final res = x.toString() == b[i].toString(); + i += 1; + return res; + }); + } +} \ No newline at end of file diff --git a/lib/src/payments/p2wpkh.dart b/lib/src/payments/p2wpkh.dart index 04cfbe2..775b4a1 100644 --- a/lib/src/payments/p2wpkh.dart +++ b/lib/src/payments/p2wpkh.dart @@ -1,7 +1,6 @@ import 'dart:typed_data'; -import 'package:meta/meta.dart'; -import 'package:bip32/src/utils/ecurve.dart' show isPoint; -import 'package:bech32/bech32.dart'; +import 'package:bip32_defichain/src/utils/ecurve.dart' show isPoint; +import 'package:defichain_bech32/defichain_bech32.dart'; import '../crypto.dart'; import '../models/networks.dart'; @@ -12,96 +11,87 @@ import '../utils/constants/op.dart'; class P2WPKH { final EMPTY_SCRIPT = Uint8List.fromList([]); - PaymentData data; - NetworkType network; - P2WPKH({@required data, network}) { + PaymentData? data; + late NetworkType network; + P2WPKH({required data, network}) { this.network = network ?? bitcoin; this.data = data; _init(); } - _init() { - if (data.address == null && - data.hash == null && - data.output == null && - data.pubkey == null && - data.witness == null) throw new ArgumentError('Not enough data'); + void _init() { + if (data!.address == null && data!.hash == null && data!.output == null && data!.pubkey == null && data!.witness == null) throw ArgumentError('Not enough data'); - if (data.address != null) { - _getDataFromAddress(data.address); + data!.name = 'p2wpkh'; + + if (data!.address != null) { + _getDataFromAddress(data!.address!); } - if (data.hash != null) { + if (data!.hash != null) { _getDataFromHash(); } - if (data.output != null) { - if (data.output.length != 22 || - data.output[0] != OPS['OP_0'] || - data.output[1] != 20) // 0x14 - throw new ArgumentError('Output is invalid'); - if (data.hash == null) { - data.hash = data.output.sublist(2); + if (data!.output != null) { + if (data!.output!.length != 22 || data!.output![0] != OPS['OP_0'] || data!.output![1] != 20) { + throw ArgumentError('Output is invalid'); } + data!.hash ??= data!.output!.sublist(2); _getDataFromHash(); } - if (data.pubkey != null) { - data.hash = hash160(data.pubkey); + if (data!.pubkey != null) { + data!.hash = hash160(data!.pubkey!); _getDataFromHash(); } - if (data.witness != null) { - if (data.witness.length != 2) - throw new ArgumentError('Witness is invalid'); - if (!bscript.isCanonicalScriptSignature(data.witness[0])) - throw new ArgumentError('Witness has invalid signature'); - if (!isPoint(data.witness[1])) - throw new ArgumentError('Witness has invalid pubkey'); - _getDataFromWitness(data.witness); - } else if (data.pubkey != null && data.signature != null) { - data.witness = [data.signature, data.pubkey]; - if (data.input == null) data.input = EMPTY_SCRIPT; + if (data!.witness != null) { + if (data!.witness!.length != 2) throw ArgumentError('Witness is invalid'); + if (!bscript.isCanonicalScriptSignature(data!.witness![0]!)) { + throw ArgumentError('Witness has invalid signature'); + } + if (!isPoint(data!.witness![1]!)) { + throw ArgumentError('Witness has invalid pubkey'); + } + _getDataFromWitness(data!.witness!); + } else if (data!.pubkey != null && data!.signature != null) { + data!.witness = [data!.signature, data!.pubkey]; + data!.input ??= EMPTY_SCRIPT; } } - void _getDataFromWitness([List witness]) { - if (data.input == null) { - data.input = EMPTY_SCRIPT; - } - if (data.pubkey == null) { - data.pubkey = witness[1]; - if (data.hash == null) { - data.hash = hash160(data.pubkey); - } + void _getDataFromWitness([List? witness]) { + data!.input ??= EMPTY_SCRIPT; + if (data!.pubkey == null) { + data!.pubkey = witness![1]; + data!.hash ??= hash160(data!.pubkey!); _getDataFromHash(); } - if (data.signature == null) data.signature = witness[0]; + data!.signature ??= witness![0]; } void _getDataFromHash() { - if (data.address == null) { - data.address = segwit.encode(Segwit(network.bech32, 0, data.hash)); - } - if (data.output == null) { - data.output = bscript.compile([OPS['OP_0'], data.hash]); - } + data!.address ??= segwit.encode(Segwit(network.bech32!, 0, data!.hash!)).address; + data!.output ??= bscript.compile([OPS['OP_0'], data!.hash]); } void _getDataFromAddress(String address) { try { - Segwit _address = segwit.decode(address); - if (network.bech32 != _address.hrp) - throw new ArgumentError('Invalid prefix or Network mismatch'); - if (_address.version != 0) // Only support version 0 now; - throw new ArgumentError('Invalid address version'); - data.hash = Uint8List.fromList(_address.program); + var _address = segwit.decode(SegwitInput(network.bech32!, address)); + if (network.bech32 != _address.hrp) { + throw ArgumentError('Invalid prefix or Network mismatch'); + } + // Only support version 0 now; + if (_address.version != 0) { + throw ArgumentError('Invalid address version'); + } + data!.hash = Uint8List.fromList(_address.program); } on InvalidHrp { - throw new ArgumentError('Invalid prefix or Network mismatch'); + throw ArgumentError('Invalid prefix or Network mismatch'); } on InvalidProgramLength { - throw new ArgumentError('Invalid address data'); + throw ArgumentError('Invalid address data'); } on InvalidWitnessVersion { - throw new ArgumentError('Invalid witness address version'); + throw ArgumentError('Invalid witness address version'); } } -} +} \ No newline at end of file diff --git a/lib/src/templates/pubkey.dart b/lib/src/templates/pubkey.dart index c839e85..d74594f 100644 --- a/lib/src/templates/pubkey.dart +++ b/lib/src/templates/pubkey.dart @@ -6,6 +6,6 @@ bool inputCheck(List chunks) { return chunks.length == 1 && bscript.isCanonicalScriptSignature(chunks[0]); } -bool outputCheck(Uint8List script) { - // TODO -} +// bool outputCheck(Uint8List script) { +// // TODO +// } diff --git a/lib/src/templates/pubkeyhash.dart b/lib/src/templates/pubkeyhash.dart index 88af094..bdcf096 100644 --- a/lib/src/templates/pubkeyhash.dart +++ b/lib/src/templates/pubkeyhash.dart @@ -10,7 +10,7 @@ bool inputCheck(List chunks) { } bool outputCheck(Uint8List script) { - final buffer = bscript.compile(script); + final buffer = bscript.compile(script)!; return buffer.length == 25 && buffer[0] == OPS['OP_DUP'] && buffer[1] == OPS['OP_HASH160'] && diff --git a/lib/src/templates/scriptHash.dart b/lib/src/templates/scriptHash.dart new file mode 100644 index 0000000..c1398b4 --- /dev/null +++ b/lib/src/templates/scriptHash.dart @@ -0,0 +1,43 @@ +import 'dart:typed_data'; +import '../utils/script.dart' as bscript; +import './pubkeyhash.dart' as p2pkh; +import './witnesspubkeyhash.dart' as p2wpkh; +import '../utils/constants/op.dart'; + +bool inputCheck(List chunks, bool allowIncomplete) { + if (chunks.isEmpty) return false; + + final lastChunk = chunks.last; + + if (!(lastChunk is Uint8List)) return false; + + final scriptSigChunks = bscript.decompile( + bscript.compile(chunks.sublist(0, chunks.length - 1)), + ); + + final redeemScriptChunks = bscript.decompile(lastChunk); + // is redeemScript a valid script? + if (redeemScriptChunks == null) return false; + // is redeemScriptSig push only? + if (!bscript.isPushOnly(scriptSigChunks)) return false; + // is witness? + if (chunks.length == 1) { + // TODO p2wsh + return p2wpkh.outputCheck(bscript.compile(redeemScriptChunks)!); + } + + if (p2pkh.inputCheck(scriptSigChunks!) && + p2pkh.outputCheck(bscript.compile(redeemScriptChunks)!)) { + return true; + } + + return false; +} + +bool outputCheck(Uint8List script) { + final buffer = bscript.compile(script)!; + return buffer.length == 23 && + buffer[0] == OPS['OP_HASH160'] && + buffer[1] == 0x14 && + buffer[22] == OPS['OP_EQUAL']; +} diff --git a/lib/src/templates/witnesspubkeyhash.dart b/lib/src/templates/witnesspubkeyhash.dart index ec660d7..200a693 100644 --- a/lib/src/templates/witnesspubkeyhash.dart +++ b/lib/src/templates/witnesspubkeyhash.dart @@ -10,6 +10,6 @@ bool inputCheck(List chunks) { } bool outputCheck(Uint8List script) { - final buffer = bscript.compile(script); + final buffer = bscript.compile(script)!; return buffer.length == 22 && buffer[0] == OPS['OP_0'] && buffer[1] == 0x14; } diff --git a/lib/src/transaction.dart b/lib/src/transaction.dart index 61f8756..0ca3e54 100644 --- a/lib/src/transaction.dart +++ b/lib/src/transaction.dart @@ -1,9 +1,11 @@ import 'dart:typed_data'; +import 'package:collection/collection.dart' show IterableExtension; import 'package:hex/hex.dart'; import 'payments/index.dart' show PaymentData; import 'payments/p2pkh.dart' show P2PKH; import 'payments/p2pk.dart' show P2PK; import 'payments/p2wpkh.dart' show P2WPKH; +import 'payments/p2sh.dart' show P2SH; import 'crypto.dart' as bcrypto; import 'classify.dart'; import 'utils/check_types.dart'; @@ -19,99 +21,110 @@ const SIGHASH_ANYONECANPAY = 0x80; const ADVANCED_TRANSACTION_MARKER = 0x00; const ADVANCED_TRANSACTION_FLAG = 0x01; final EMPTY_SCRIPT = Uint8List.fromList([]); -final EMPTY_WITNESS = new List(); -final ZERO = HEX - .decode('0000000000000000000000000000000000000000000000000000000000000000'); -final ONE = HEX - .decode('0000000000000000000000000000000000000000000000000000000000000001'); +final EMPTY_WITNESS = []; +final ZERO = HEX.decode('0000000000000000000000000000000000000000000000000000000000000000'); +final ONE = HEX.decode('0000000000000000000000000000000000000000000000000000000000000001'); final VALUE_UINT64_MAX = HEX.decode('ffffffffffffffff'); -final BLANK_OUTPUT = - new Output(script: EMPTY_SCRIPT, valueBuffer: VALUE_UINT64_MAX); +final BLANK_OUTPUT = Output(script: EMPTY_SCRIPT, valueBuffer: Uint8List.fromList(VALUE_UINT64_MAX)); + +const int SERIALIZE_TRANSACTION_NO_WITNESS = 0x40000000; +const int SERIALIZE_TRANSACTION_NO_TOKENS = 0x20000000; +const MIN_VERSION_NO_TOKENS = 3; class Transaction { - int version = 1; - int locktime = 0; + int? version = 1; + int? locktime = 0; List ins = []; - List outs = []; + List outs = []; Transaction(); - int addInput(Uint8List hash, int index, [int sequence, Uint8List scriptSig]) { - ins.add(new Input( - hash: hash, - index: index, - sequence: sequence ?? DEFAULT_SEQUENCE, - script: scriptSig ?? EMPTY_SCRIPT, - witness: EMPTY_WITNESS)); + int addInput(Uint8List hash, int? index, [int? sequence, Uint8List? scriptSig]) { + ins.add(Input(hash: hash, index: index, sequence: sequence ?? DEFAULT_SEQUENCE, script: scriptSig ?? EMPTY_SCRIPT, witness: EMPTY_WITNESS)); return ins.length - 1; } - int addOutput(Uint8List scriptPubKey, int value) { - outs.add(new Output(script: scriptPubKey, value: value)); + int addOutput(Uint8List? scriptPubKey, int? value) { + outs.add(Output(script: scriptPubKey, value: value)); + return outs.length - 1; + } + + int addOutputAt(Uint8List? scriptPubKey, int value, int at) { + final output = Output(script: scriptPubKey, value: value); + return addBaseOutputAt(output, at); + } + + int addBaseOutput(OutputBase output) { + outs.add(output); return outs.length - 1; } + int addBaseOutputAt(OutputBase output, int index) { + outs.insert(index, output); + return index; + } + bool hasWitnesses() { - var witness = ins.firstWhere( - (input) => input.witness != null && input.witness.length != 0, - orElse: () => null); + var witness = ins.firstWhereOrNull((input) => input.witness != null && input.witness!.isNotEmpty); return witness != null; } - setInputScript(int index, Uint8List scriptSig) { + void setInputScript(int index, Uint8List? scriptSig) { ins[index].script = scriptSig; } - setWitness(int index, List witness) { + void setWitness(int index, List? witness) { ins[index].witness = witness; } - hashForWitnessV0( - int inIndex, Uint8List prevOutScript, int value, int hashType) { + Uint8List hashForWitnessV0(int inIndex, Uint8List prevOutScript, int value, int hashType) { var tbuffer = Uint8List.fromList([]); var toffset = 0; // Any changes made to the ByteData will also change the buffer, and vice versa. // https://api.dart.dev/stable/2.7.1/dart-typed_data/ByteBuffer/asByteData.html - ByteData bytes = tbuffer.buffer.asByteData(); + var bytes = tbuffer.buffer.asByteData(); var hashOutputs = ZERO; var hashPrevouts = ZERO; var hashSequence = ZERO; - writeSlice(slice) { - tbuffer.setRange(toffset, toffset + slice.length, slice); + void writeSlice(List? slice) { + tbuffer.setRange(toffset, toffset + slice!.length, slice); toffset += slice.length; } - writeUInt8(i) { + // ignore: unused_element + void writeUInt8(i) { bytes.setUint8(toffset, i); toffset++; } - writeUInt32(i) { + void writeUInt32(i) { bytes.setUint32(toffset, i, Endian.little); toffset += 4; } - writeInt32(i) { + // ignore: unused_element + void writeInt32(i) { bytes.setInt32(toffset, i, Endian.little); toffset += 4; } - writeUInt64(i) { + void writeUInt64(i) { bytes.setUint64(toffset, i, Endian.little); toffset += 8; } - writeVarInt(i) { + void writeVarInt(i) { varuint.encode(i, tbuffer, toffset); toffset += varuint.encodingLength(i); } - writeVarSlice(slice) { + void writeVarSlice(slice) { writeVarInt(slice.length); writeSlice(slice); } - writeVector(vector) { + // ignore: unused_element + void writeVector(vector) { writeVarInt(vector.length); vector.forEach((buf) { writeVarSlice(buf); @@ -119,7 +132,7 @@ class Transaction { } if ((hashType & SIGHASH_ANYONECANPAY) == 0) { - tbuffer = new Uint8List(36 * this.ins.length); + tbuffer = Uint8List(36 * ins.length); bytes = tbuffer.buffer.asByteData(); toffset = 0; @@ -130,10 +143,8 @@ class Transaction { hashPrevouts = bcrypto.hash256(tbuffer); } - if ((hashType & SIGHASH_ANYONECANPAY) == 0 && - (hashType & 0x1f) != SIGHASH_SINGLE && - (hashType & 0x1f) != SIGHASH_NONE) { - tbuffer = new Uint8List(4 * this.ins.length); + if ((hashType & SIGHASH_ANYONECANPAY) == 0 && (hashType & 0x1f) != SIGHASH_SINGLE && (hashType & 0x1f) != SIGHASH_NONE) { + tbuffer = Uint8List(4 * ins.length); bytes = tbuffer.buffer.asByteData(); toffset = 0; ins.forEach((txIn) { @@ -142,30 +153,34 @@ class Transaction { hashSequence = bcrypto.hash256(tbuffer); } - if ((hashType & 0x1f) != SIGHASH_SINGLE && - (hashType & 0x1f) != SIGHASH_NONE) { - var txOutsSize = - outs.fold(0, (sum, output) => sum + 8 + varSliceSize(output.script)); - tbuffer = new Uint8List(txOutsSize); + if ((hashType & 0x1f) != SIGHASH_SINGLE && (hashType & 0x1f) != SIGHASH_NONE) { + var txOutsSize = outs.fold(0, (dynamic sum, output) => sum + (this.version! > MIN_VERSION_NO_TOKENS ? 1 : 0) + 8 + varSliceSize(output.script!)); + tbuffer = Uint8List(txOutsSize); bytes = tbuffer.buffer.asByteData(); toffset = 0; outs.forEach((txOut) { writeUInt64(txOut.value); writeVarSlice(txOut.script); + if (version! > MIN_VERSION_NO_TOKENS) { + writeVarInt(txOut.tokenId); + } }); hashOutputs = bcrypto.hash256(tbuffer); } else if ((hashType & 0x1f) == SIGHASH_SINGLE && inIndex < outs.length) { // SIGHASH_SINGLE only hash that according output var output = outs[inIndex]; - tbuffer = new Uint8List(8 + varSliceSize(output.script)); + tbuffer = Uint8List(8 + (this.version! > MIN_VERSION_NO_TOKENS ? 1 : 0) + varSliceSize(output.script!)); bytes = tbuffer.buffer.asByteData(); toffset = 0; writeUInt64(output.value); writeVarSlice(output.script); + if (version! > MIN_VERSION_NO_TOKENS) { + writeVarInt(output.tokenId); + } hashOutputs = bcrypto.hash256(tbuffer); } - tbuffer = new Uint8List(156 + varSliceSize(prevOutScript)); + tbuffer = Uint8List(156 + varSliceSize(prevOutScript)); bytes = tbuffer.buffer.asByteData(); toffset = 0; var input = ins[inIndex]; @@ -178,22 +193,21 @@ class Transaction { writeUInt64(value); writeUInt32(input.sequence); writeSlice(hashOutputs); - writeUInt32(this.locktime); + writeUInt32(locktime); writeUInt32(hashType); return bcrypto.hash256(tbuffer); } - hashForSignature(int inIndex, Uint8List prevOutScript, int hashType) { + List hashForSignature(int inIndex, Uint8List? prevOutScript, int? hashType) { if (inIndex >= ins.length) return ONE; // ignore OP_CODESEPARATOR - final ourScript = - bscript.compile(bscript.decompile(prevOutScript).where((x) { + final ourScript = bscript.compile(bscript.decompile(prevOutScript)!.where((x) { return x != OPS['OP_CODESEPARATOR']; }).toList()); final txTmp = Transaction.clone(this); // SIGHASH_NONE: ignore all outputs? (wildcard payee) - if ((hashType & 0x1f) == SIGHASH_NONE) { + if ((hashType! & 0x1f) == SIGHASH_NONE) { txTmp.outs = []; // ignore sequence numbers (except at inIndex) for (var i = 0; i < txTmp.ins.length; i++) { @@ -236,51 +250,54 @@ class Transaction { } // serialize and hash final buffer = Uint8List(txTmp.virtualSize() + 4); - buffer.buffer - .asByteData() - .setUint32(buffer.length - 4, hashType, Endian.little); + buffer.buffer.asByteData().setUint32(buffer.length - 4, hashType, Endian.little); txTmp._toBuffer(buffer, 0); return bcrypto.hash256(buffer); } - _byteLength(_ALLOW_WITNESS) { + num _byteLength(_ALLOW_WITNESS) { var hasWitness = _ALLOW_WITNESS && hasWitnesses(); return (hasWitness ? 10 : 8) + varuint.encodingLength(ins.length) + varuint.encodingLength(outs.length) + - ins.fold(0, (sum, input) => sum + 40 + varSliceSize(input.script)) + - outs.fold(0, (sum, output) => sum + 8 + varSliceSize(output.script)) + - (hasWitness - ? ins.fold(0, (sum, input) => sum + vectorSize(input.witness)) - : 0); + ins.fold(0, (sum, input) => sum + 40 + varSliceSize(input.script!)) + + outs.fold(0, (sum, output) => sum + (this.version! > MIN_VERSION_NO_TOKENS ? 1 : 0) + 8 + varSliceSize(output.script!)) + + (hasWitness ? ins.fold(0, (sum, input) => sum + vectorSizeNew(input)) : 0); } - int vectorSize(List someVector) { + int vectorSizeNew(Input input) { + if (input.witness != null && input.witness!.isNotEmpty) { + return vectorSize(input.witness!); + } + + return varuint.encodingLength(0); + } + + int vectorSize(List someVector) { var length = someVector.length; - return varuint.encodingLength(length) + - someVector.fold(0, (sum, witness) => sum + varSliceSize(witness)); + return varuint.encodingLength(length) + someVector.fold(0, ((sum, witness) => sum + varSliceSize(witness!) as int) as int Function(int, Uint8List?)); } int weight() { var base = _byteLength(false); var total = _byteLength(true); - return base * 3 + total; + return base * 3 + total as int; } int byteLength() { - return _byteLength(true); + return _byteLength(true) as int; } int virtualSize() { return (weight() / 4).ceil(); } - Uint8List toBuffer([Uint8List buffer, int initialOffset]) { - return this._toBuffer(buffer, initialOffset, true); + Uint8List toBuffer([Uint8List? buffer, int? initialOffset]) { + return _toBuffer(buffer, initialOffset, true); } String toHex() { - return HEX.encode(this.toBuffer()); + return HEX.encode(toBuffer()); } bool isCoinbaseHash(buffer) { @@ -304,51 +321,51 @@ class Transaction { return HEX.encode(getHash().reversed.toList()); } - _toBuffer([Uint8List buffer, initialOffset, bool _ALLOW_WITNESS = false]) { + Uint8List _toBuffer([Uint8List? buffer, initialOffset, bool _ALLOW_WITNESS = false]) { // _ALLOW_WITNESS is used to separate witness part when calculating tx id - if (buffer == null) buffer = new Uint8List(_byteLength(_ALLOW_WITNESS)); + buffer ??= Uint8List(_byteLength(_ALLOW_WITNESS) as int); // Any changes made to the ByteData will also change the buffer, and vice versa. // https://api.dart.dev/stable/2.7.1/dart-typed_data/ByteBuffer/asByteData.html var bytes = buffer.buffer.asByteData(); var offset = initialOffset ?? 0; - writeSlice(slice) { - buffer.setRange(offset, offset + slice.length, slice); + void writeSlice(slice) { + buffer!.setRange(offset, offset + slice.length, slice); offset += slice.length; } - writeUInt8(i) { + void writeUInt8(i) { bytes.setUint8(offset, i); offset++; } - writeUInt32(i) { + void writeUInt32(i) { bytes.setUint32(offset, i, Endian.little); offset += 4; } - writeInt32(i) { + void writeInt32(i) { bytes.setInt32(offset, i, Endian.little); offset += 4; } - writeUInt64(i) { + void writeUInt64(i) { bytes.setUint64(offset, i, Endian.little); offset += 8; } - writeVarInt(i) { + void writeVarInt(i) { varuint.encode(i, buffer, offset); offset += varuint.encodingLength(i); } - writeVarSlice(slice) { + void writeVarSlice(slice) { writeVarInt(slice.length); writeSlice(slice); } - writeVector(vector) { + void writeVector(vector) { writeVarInt(vector.length); vector.forEach((buf) { writeVarSlice(buf); @@ -363,7 +380,7 @@ class Transaction { writeUInt8(ADVANCED_TRANSACTION_FLAG); } - writeVarInt(this.ins.length); + writeVarInt(ins.length); ins.forEach((txIn) { writeSlice(txIn.hash); @@ -372,7 +389,7 @@ class Transaction { writeUInt32(txIn.sequence); }); - writeVarInt(this.outs.length); + writeVarInt(outs.length); outs.forEach((txOut) { if (txOut.valueBuffer == null) { @@ -381,15 +398,22 @@ class Transaction { writeSlice(txOut.valueBuffer); } writeVarSlice(txOut.script); + if (this.version! > MIN_VERSION_NO_TOKENS) { + writeVarInt(txOut.tokenId); + } }); if (_ALLOW_WITNESS && hasWitnesses()) { ins.forEach((txInt) { - writeVector(txInt.witness); + if (txInt.witness == null) { + writeVarInt(0); + } else { + writeVector(txInt.witness); + } }); } - writeUInt32(this.locktime); + writeUInt32(locktime); // End writeBuffer // avoid slicing unless necessary @@ -399,14 +423,14 @@ class Transaction { } factory Transaction.clone(Transaction _tx) { - Transaction tx = new Transaction(); + var tx = Transaction(); tx.version = _tx.version; tx.locktime = _tx.locktime; tx.ins = _tx.ins.map((input) { return Input.clone(input); }).toList(); tx.outs = _tx.outs.map((output) { - return Output.clone(output); + return OutputBase.clone(output); }).toList(); return tx; } @@ -418,7 +442,7 @@ class Transaction { var offset = 0; // Any changes made to the ByteData will also change the buffer, and vice versa. // https://api.dart.dev/stable/2.7.1/dart-typed_data/ByteBuffer/asByteData.html - ByteData bytes = buffer.buffer.asByteData(); + var bytes = buffer.buffer.asByteData(); int readUInt8() { final i = bytes.getUint8(offset); @@ -444,7 +468,7 @@ class Transaction { return i; } - Uint8List readSlice(n) { + Uint8List readSlice(int n) { offset += n; return buffer.sublist(offset - n, offset); } @@ -461,22 +485,21 @@ class Transaction { List readVector() { var count = readVarInt(); - List vector = []; + var vector = []; for (var i = 0; i < count; ++i) { vector.add(readVarSlice()); } return vector; } - final tx = new Transaction(); + final tx = Transaction(); tx.version = readInt32(); final marker = readUInt8(); final flag = readUInt8(); var hasWitnesses = false; - if (marker == ADVANCED_TRANSACTION_MARKER && - flag == ADVANCED_TRANSACTION_FLAG) { + if (marker == ADVANCED_TRANSACTION_MARKER && flag == ADVANCED_TRANSACTION_FLAG) { hasWitnesses = true; } else { offset -= 2; // Reset offset if not segwit tx @@ -484,16 +507,12 @@ class Transaction { final vinLen = readVarInt(); for (var i = 0; i < vinLen; ++i) { - tx.ins.add(new Input( - hash: readSlice(32), - index: readUInt32(), - script: readVarSlice(), - sequence: readUInt32())); + tx.ins.add(Input(hash: readSlice(32), index: readUInt32(), script: readVarSlice(), sequence: readUInt32())); } final voutLen = readVarInt(); for (var i = 0; i < voutLen; ++i) { - tx.outs.add(new Output(value: readUInt64(), script: readVarSlice())); + tx.outs.add(Output(value: readUInt64(), script: readVarSlice())); } if (hasWitnesses) { @@ -506,8 +525,9 @@ class Transaction { if (noStrict) return tx; - if (offset != buffer.length) - throw new ArgumentError('Transaction has unexpected data'); + if (offset != buffer.length) { + throw ArgumentError('Transaction has unexpected data'); + } return tx; } @@ -517,35 +537,45 @@ class Transaction { bool noStrict = false, }) { return Transaction.fromBuffer( - HEX.decode(hex), + Uint8List.fromList(HEX.decode(hex)), noStrict: noStrict, ); } @override String toString() { - this.ins.forEach((txInput) { + final s = []; + ins.forEach((txInput) { + s.add(txInput.toString()); print(txInput.toString()); }); - this.outs.forEach((txOutput) { + outs.forEach((txOutput) { + s.add(txOutput.toString()); print(txOutput.toString()); }); + return s.join('\n'); } } class Input { - Uint8List hash; - int index; - int sequence; - int value; - Uint8List script; - Uint8List signScript; - Uint8List prevOutScript; - String prevOutType; - bool hasWitness; - List pubkeys; - List signatures; - List witness; + Uint8List? hash; + int? index; + int? sequence; + int? value; + Uint8List? script; + Uint8List? signScript; + Uint8List? prevOutScript; + Uint8List? redeemScript; + Uint8List? witnessScript; + String? signType; + String? prevOutType; + String? redeemScriptType; + String? witnessScriptType; + bool? hasWitness; + List? pubkeys; + List? signatures; + List? witness; + int? maxSignatures; Input( {this.hash, @@ -554,165 +584,203 @@ class Input { this.sequence, this.value, this.prevOutScript, + this.redeemScript, + this.witnessScript, this.pubkeys, this.signatures, this.witness, - this.prevOutType}) { - this.hasWitness = false; // Default value - if (this.hash != null && !isHash256bit(this.hash)) - throw new ArgumentError('Invalid input hash'); - if (this.index != null && !isUint(this.index, 32)) - throw new ArgumentError('Invalid input index'); - if (this.sequence != null && !isUint(this.sequence, 32)) - throw new ArgumentError('Invalid input sequence'); - if (this.value != null && !isShatoshi(this.value)) + this.signType, + this.prevOutType, + this.redeemScriptType, + this.witnessScriptType, + this.maxSignatures}) { + hasWitness = false; // Default value + if (hash != null && !isHash256bit(hash!)) { + throw ArgumentError('Invalid input hash'); + } + if (index != null && !isUint(index!, 32)) { + throw ArgumentError('Invalid input index'); + } + if (sequence != null && !isUint(sequence!, 32)) { + throw ArgumentError('Invalid input sequence'); + } + if (value != null && !isShatoshi(value!)) { throw ArgumentError('Invalid ouput value'); + } } - factory Input.expandInput(Uint8List scriptSig, List witness, - [String type, Uint8List scriptPubKey]) { + factory Input.expandInput(Uint8List scriptSig, List? witness, [String? type, Uint8List? scriptPubKey]) { + if (scriptSig.isEmpty && witness!.isEmpty) { + return Input(); + } if (type == null || type == '') { - var ssType = classifyInput(scriptSig); + var ssType = classifyInput(scriptSig, true); var wsType = classifyWitness(witness); if (ssType == SCRIPT_TYPES['NONSTANDARD']) ssType = null; if (wsType == SCRIPT_TYPES['NONSTANDARD']) wsType = null; type = ssType ?? wsType; } if (type == SCRIPT_TYPES['P2WPKH']) { - P2WPKH p2wpkh = new P2WPKH(data: new PaymentData(witness: witness)); - return new Input( - prevOutScript: p2wpkh.data.output, - prevOutType: SCRIPT_TYPES['P2WPKH'], - pubkeys: [p2wpkh.data.pubkey], - signatures: [p2wpkh.data.signature]); - } else if (type == SCRIPT_TYPES['P2PKH']) { - P2PKH p2pkh = new P2PKH(data: new PaymentData(input: scriptSig)); - return new Input( - prevOutScript: p2pkh.data.output, - prevOutType: SCRIPT_TYPES['P2PKH'], - pubkeys: [p2pkh.data.pubkey], - signatures: [p2pkh.data.signature]); - } else if (type == SCRIPT_TYPES['P2PK']) { - P2PK p2pk = new P2PK(data: new PaymentData(input: scriptSig)); - return new Input( - prevOutType: SCRIPT_TYPES['P2PK'], - pubkeys: [], - signatures: [p2pk.data.signature]); - } + var p2wpkh = P2WPKH(data: PaymentData(witness: witness)); + return Input(prevOutScript: p2wpkh.data!.output, prevOutType: SCRIPT_TYPES['P2WPKH'], pubkeys: [p2wpkh.data!.pubkey], signatures: [p2wpkh.data!.signature]); + } + if (type == SCRIPT_TYPES['P2PKH']) { + var p2pkh = P2PKH(data: PaymentData(input: scriptSig)); + return Input(prevOutScript: p2pkh.data!.output, prevOutType: SCRIPT_TYPES['P2PKH'], pubkeys: [p2pkh.data!.pubkey], signatures: [p2pkh.data!.signature]); + } + if (type == SCRIPT_TYPES['P2PK']) { + var p2pk = P2PK(data: PaymentData(input: scriptSig)); + return Input(prevOutType: SCRIPT_TYPES['P2PK'], pubkeys: [], signatures: [p2pk.data.signature]); + } + if (type == SCRIPT_TYPES['P2MS']) { + // TODO + } + if (type == SCRIPT_TYPES['P2SH']) { + var p2sh = P2SH(data: PaymentData(input: scriptSig, witness: witness)); + final output = p2sh.data!.output; + final redeem = p2sh.data!.redeem!; + final outputType = classifyOutput(redeem.output!); + final expanded = Input.expandInput( + redeem.input!, + redeem.witness, + outputType, + redeem.output, + ); + if (expanded.prevOutType == null) return Input(); + return Input( + prevOutScript: output, + prevOutType: SCRIPT_TYPES['P2SH'], + redeemScript: redeem.output, + redeemScriptType: expanded.prevOutType, + witnessScript: expanded.witnessScript, + witnessScriptType: expanded.witnessScriptType, + pubkeys: expanded.pubkeys, + signatures: expanded.signatures); + } + return Input( + prevOutType: SCRIPT_TYPES['NONSTANDARD'], + prevOutScript: scriptSig, + ); } factory Input.clone(Input input) { - return new Input( - hash: input.hash != null ? Uint8List.fromList(input.hash) : null, + return Input( + hash: input.hash != null ? Uint8List.fromList(input.hash!) : null, index: input.index, - script: input.script != null ? Uint8List.fromList(input.script) : null, + script: input.script != null ? Uint8List.fromList(input.script!) : null, sequence: input.sequence, value: input.value, - prevOutScript: input.prevOutScript != null - ? Uint8List.fromList(input.prevOutScript) - : null, - pubkeys: input.pubkeys != null - ? input.pubkeys.map( - (pubkey) => pubkey != null ? Uint8List.fromList(pubkey) : null) - : null, - signatures: input.signatures != null - ? input.signatures.map((signature) => - signature != null ? Uint8List.fromList(signature) : null) - : null, + prevOutScript: input.prevOutScript != null ? Uint8List.fromList(input.prevOutScript!) : null, + pubkeys: input.pubkeys != null ? input.pubkeys!.map((pubkey) => pubkey != null ? Uint8List.fromList(pubkey) : null) as List? : null, + signatures: input.signatures != null ? input.signatures!.map((signature) => signature != null ? Uint8List.fromList(signature) : null) as List? : null, ); } @override String toString() { - return 'Input{hash: $hash, index: $index, sequence: $sequence, value: $value, script: $script, signScript: $signScript, prevOutScript: $prevOutScript, pubkeys: $pubkeys, signatures: $signatures, witness: $witness, prevOutType: $prevOutType}'; + return ''' + Input{ + hash: $hash, + index: $index, + sequence: $sequence, + value: $value, + script: $script, + signScript: $signScript, + prevOutScript: $prevOutScript, + redeemScript: $redeemScript, + witnessScript: $witnessScript, + pubkeys: $pubkeys, + signatures: $signatures, + witness: $witness, + signType: $signType, + prevOutType: $prevOutType, + redeemScriptType: $redeemScriptType, + witnessScriptType: $witnessScriptType, + } + '''; } } -class Output { - Uint8List script; - int value; - Uint8List valueBuffer; - List pubkeys; - List signatures; +class OutputBase { + String? type; + Uint8List? script; + int? value; + Uint8List? valueBuffer; + List? pubkeys; + List? signatures; + int? maxSignatures; - Output( - {this.script, - this.value, - this.pubkeys, - this.signatures, - this.valueBuffer}) { - if (value != null && !isShatoshi(value)) - throw ArgumentError('Invalid ouput value'); - } + final int tokenId; + + OutputBase({this.type, this.script, this.value, this.pubkeys, this.signatures, this.valueBuffer, this.maxSignatures, this.tokenId = 0}) {} - factory Output.expandOutput(Uint8List script, [Uint8List ourPubKey]) { - if (ourPubKey == null) return new Output(); - var type = classifyOutput(script); + factory OutputBase.expandOutput(Uint8List? script, [Uint8List? ourPubKey]) { + if (ourPubKey == null) return OutputBase(); + var type = classifyOutput(script!); if (type == SCRIPT_TYPES['P2WPKH']) { - Uint8List wpkh1 = - new P2WPKH(data: new PaymentData(output: script)).data.hash; - Uint8List wpkh2 = bcrypto.hash160(ourPubKey); - if (wpkh1 != wpkh2) throw ArgumentError('Hash mismatch!'); - return new Output(pubkeys: [ourPubKey], signatures: [null]); - } else if (type == SCRIPT_TYPES['P2PKH']) { - Uint8List pkh1 = - new P2PKH(data: new PaymentData(output: script)).data.hash; - Uint8List pkh2 = bcrypto.hash160(ourPubKey); - if (pkh1 != pkh2) throw ArgumentError('Hash mismatch!'); - return new Output(pubkeys: [ourPubKey], signatures: [null]); - } - } - - factory Output.clone(Output output) { - return new Output( - script: output.script != null ? Uint8List.fromList(output.script) : null, + var wpkh1 = P2WPKH(data: PaymentData(output: script)).data!.hash; + var wpkh2 = bcrypto.hash160(ourPubKey); + if (wpkh1.toString() != wpkh2.toString()) { + throw ArgumentError('Hash mismatch!'); + } + return OutputBase(type: type, pubkeys: [ourPubKey], signatures: [null]); + } + + if (type == SCRIPT_TYPES['P2PKH']) { + var pkh1 = P2PKH(data: PaymentData(output: script)).data!.hash; + var pkh2 = bcrypto.hash160(ourPubKey); + if (pkh1.toString() != pkh2.toString()) { + throw ArgumentError('Hash mismatch!'); + } + return OutputBase(type: type, pubkeys: [ourPubKey], signatures: [null]); + } + + return OutputBase(); + } + + factory OutputBase.clone(OutputBase output) { + return OutputBase( + type: output.type, + script: output.script != null ? Uint8List.fromList(output.script!) : null, value: output.value, - valueBuffer: output.valueBuffer != null - ? Uint8List.fromList(output.valueBuffer) - : null, - pubkeys: output.pubkeys != null - ? output.pubkeys.map( - (pubkey) => pubkey != null ? Uint8List.fromList(pubkey) : null) - : null, - signatures: output.signatures != null - ? output.signatures.map((signature) => - signature != null ? Uint8List.fromList(signature) : null) - : null, + valueBuffer: output.valueBuffer != null ? Uint8List.fromList(output.valueBuffer!) : null, + pubkeys: output.pubkeys != null ? output.pubkeys!.map((pubkey) => pubkey != null ? Uint8List.fromList(pubkey) : null) as List? : null, + signatures: output.signatures != null ? output.signatures!.map((signature) => signature != null ? Uint8List.fromList(signature) : null) as List? : null, ); } @override String toString() { - return 'Output{script: $script, value: $value, valueBuffer: $valueBuffer, pubkeys: $pubkeys, signatures: $signatures}'; + return ''' + Output{ + type: $type, + script: $script, + value: $value, + valueBuffer: $valueBuffer, + pubkeys: $pubkeys, + signatures: $signatures + } + '''; + } +} + +class Output extends OutputBase { + Output({String? type, Uint8List? script, int? value, Uint8List? valueBuffer, List? pubkeys, List? signartures, int? maxSignatures}) + : super(type: type, script: script, value: value, valueBuffer: valueBuffer, pubkeys: pubkeys, signatures: signartures, maxSignatures: maxSignatures) { + if (value != null && !isShatoshi(value)) { + throw ArgumentError('Invalid ouput value'); + } } } bool isCoinbaseHash(Uint8List buffer) { - if (!isHash256bit(buffer)) throw new ArgumentError('Invalid hash'); + if (!isHash256bit(buffer)) throw ArgumentError('Invalid hash'); for (var i = 0; i < 32; ++i) { if (buffer[i] != 0) return false; } return true; } -bool _isP2PKHInput(script) { - final chunks = bscript.decompile(script); - return chunks != null && - chunks.length == 2 && - bscript.isCanonicalScriptSignature(chunks[0]) && - bscript.isCanonicalPubKey(chunks[1]); -} - -bool _isP2PKHOutput(script) { - final buffer = bscript.compile(script); - return buffer.length == 25 && - buffer[0] == OPS['OP_DUP'] && - buffer[1] == OPS['OP_HASH160'] && - buffer[2] == 0x14 && - buffer[23] == OPS['OP_EQUALVERIFY'] && - buffer[24] == OPS['OP_CHECKSIG']; -} - int varSliceSize(Uint8List someScript) { final length = someScript.length; return varuint.encodingLength(length) + length; diff --git a/lib/src/transaction_builder.dart b/lib/src/transaction_builder.dart index 7c76f55..b82adea 100644 --- a/lib/src/transaction_builder.dart +++ b/lib/src/transaction_builder.dart @@ -1,10 +1,9 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:bitcoin_flutter/src/utils/constants/op.dart'; -import 'package:meta/meta.dart'; import 'package:hex/hex.dart'; -import 'package:bs58check/bs58check.dart' as bs58check; -import 'package:bech32/bech32.dart'; +// import 'package:bs58check/bs58check.dart' as bs58check; +// import 'package:bech32/bech32.dart'; import 'utils/script.dart' as bscript; import 'ecpair.dart'; import 'models/networks.dart'; @@ -13,33 +12,34 @@ import 'address.dart'; import 'payments/index.dart' show PaymentData; import 'payments/p2pkh.dart'; import 'payments/p2wpkh.dart'; +import 'payments/p2sh.dart'; import 'classify.dart'; const int MAX_OP_RETURN_SIZE = 100; class TransactionBuilder { - NetworkType network; - int maximumFeeRate; - List _inputs; - Transaction _tx; - Map _prevTxSet = {}; + NetworkType? network; + late int maximumFeeRate; + List? _inputs; + Transaction? _tx; + final Map _prevTxSet = {}; - TransactionBuilder({NetworkType network, int maximumFeeRate}) { + TransactionBuilder({NetworkType? network, int? maximumFeeRate}) { this.network = network ?? bitcoin; this.maximumFeeRate = maximumFeeRate ?? 2500; this._inputs = []; this._tx = new Transaction(); - this._tx.version = 2; + this._tx!.version = 2; } - List get inputs => _inputs; + List? get inputs => _inputs; factory TransactionBuilder.fromTransaction(Transaction transaction, - [NetworkType network]) { + [NetworkType? network]) { final txb = new TransactionBuilder(network: network); // Copy transaction fields - txb.setVersion(transaction.version); - txb.setLockTime(transaction.locktime); + txb.setVersion(transaction.version!); + txb.setLockTime(transaction.locktime!); // Copy outputs (done first to avoid signature invalidation) transaction.outs.forEach((txOut) { @@ -48,7 +48,7 @@ class TransactionBuilder { transaction.ins.forEach((txIn) { txb._addInputUnsafe( - txIn.hash, + txIn.hash!, txIn.index, new Input( sequence: txIn.sequence, @@ -68,25 +68,26 @@ class TransactionBuilder { setVersion(int version) { if (version < 0 || version > 0xFFFFFFFF) throw ArgumentError('Expected Uint32'); - _tx.version = version; + _tx!.version = version; } setLockTime(int locktime) { if (locktime < 0 || locktime > 0xFFFFFFFF) throw ArgumentError('Expected Uint32'); // if any signatures exist, throw - if (this._inputs.map((input) { + if (_inputs!.map((input) { if (input.signatures == null) return false; - return input.signatures.map((s) { + return input.signatures!.map((s) { return s != null; }).contains(true); }).contains(true)) { throw new ArgumentError('No, this would invalidate signatures'); } - _tx.locktime = locktime; + _tx!.locktime = locktime; + return true; } - int addOutput(dynamic data, int value) { + int addOutput(dynamic data, int? value) { var scriptPubKey; if (data is String) { scriptPubKey = Address.addressToOutputScript(data, this.network); @@ -98,7 +99,7 @@ class TransactionBuilder { if (!_canModifyOutputs()) { throw new ArgumentError('No, this would invalidate signatures'); } - return _tx.addOutput(scriptPubKey, value); + return _tx!.addOutput(scriptPubKey, value); } int addOutputData(dynamic data) { @@ -107,7 +108,9 @@ class TransactionBuilder { if (data.length <= MAX_OP_RETURN_SIZE) { scriptPubKey = bscript.compile([OPS['OP_RETURN'], utf8.encode(data)]); } else { - throw new ArgumentError('Too much data embedded, max OP_RETURN size is '+MAX_OP_RETURN_SIZE.toString()); + throw new ArgumentError( + 'Too much data embedded, max OP_RETURN size is ' + + MAX_OP_RETURN_SIZE.toString()); } } else if (data is Uint8List) { scriptPubKey = data; @@ -117,11 +120,11 @@ class TransactionBuilder { if (!_canModifyOutputs()) { throw new ArgumentError('No, this would invalidate signatures'); } - return _tx.addOutput(scriptPubKey, 0); + return _tx!.addOutput(scriptPubKey, 0); } - int addInput(dynamic txHash, int vout, - [int sequence, Uint8List prevOutScript]) { + int addInput(dynamic txHash, int? vout, + [int? sequence, Uint8List? prevOutScript]) { if (!_canModifyInputs()) { throw new ArgumentError('No, this would invalidate signatures'); } @@ -132,7 +135,7 @@ class TransactionBuilder { } else if (txHash is Uint8List) { hash = txHash; } else if (txHash is Transaction) { - final txOut = txHash.outs[vout]; + final txOut = txHash.outs[vout!]; prevOutScript = txOut.script; value = txOut.value; hash = txHash.getHash(); @@ -147,57 +150,106 @@ class TransactionBuilder { } sign( - {@required int vin, - @required ECPair keyPair, - Uint8List redeemScript, - int witnessValue, - Uint8List witnessScript, - int hashType}) { + {required int vin, + required ECPair keyPair, + Uint8List? redeemScript, + int? witnessValue, + Uint8List? witnessScript, + int? hashType}) { if (keyPair.network != null && - keyPair.network.toString().compareTo(network.toString()) != 0) + keyPair.network.toString().compareTo(network.toString()) != 0) { throw new ArgumentError('Inconsistent network'); - if (vin >= _inputs.length) + } + if (vin >= _inputs!.length) { throw new ArgumentError('No input at index: $vin'); + } hashType = hashType ?? SIGHASH_ALL; - if (this._needsOutputs(hashType)) + if (_needsOutputs(hashType)) { throw new ArgumentError('Transaction needs outputs'); - final input = _inputs[vin]; + } + final input = _inputs![vin]; final ourPubKey = keyPair.publicKey; + // if redeemScript was previously provided, enforce consistency + if (input.redeemScript != null && + redeemScript != null && + input.redeemScript.toString() != redeemScript.toString()) { + throw ArgumentError('Inconsistent redeemScript'); + } if (!_canSign(input)) { if (witnessValue != null) { + if (input.value != null && input.value != witnessValue) { + throw ArgumentError('Input did not match witnessValue'); + } input.value = witnessValue; } if (redeemScript != null && witnessScript != null) { // TODO p2wsh } if (redeemScript != null) { - // TODO + final p2sh = P2SH( + data: PaymentData(redeem: PaymentData(output: redeemScript)), + network: network); + if (input.prevOutScript != null) { + // TODO check + } + final expanded = + OutputBase.expandOutput(p2sh.data!.redeem!.output, ourPubKey); + + if (expanded.pubkeys == null) { + throw ArgumentError( + '${expanded.type} not supported as redeemScript (${bscript.toASM(redeemScript)})', + ); + } + + if (input.signatures != null && + input.signatures!.any((x) => x != null)) { + expanded.signatures = input.signatures; + } + + Uint8List? signScript = redeemScript; + if (expanded.type == SCRIPT_TYPES['P2WPKH']) { + signScript = P2PKH( + data: PaymentData(pubkey: expanded.pubkeys![0]), + network: network) + .data! + .output; + } + input.redeemScript = redeemScript; + input.redeemScriptType = expanded.type; + input.prevOutType = SCRIPT_TYPES['P2SH']; + input.prevOutScript = p2sh.data!.output; + input.hasWitness = (expanded.type == SCRIPT_TYPES['P2WPKH']); + input.signScript = signScript; + input.signType = expanded.type; + input.pubkeys = expanded.pubkeys; + input.signatures = expanded.signatures; + input.maxSignatures = expanded.maxSignatures; } if (witnessScript != null) { // TODO } if (input.prevOutScript != null && input.prevOutType != null) { - var type = classifyOutput(input.prevOutScript); + var type = classifyOutput(input.prevOutScript!); if (type == SCRIPT_TYPES['P2WPKH']) { input.prevOutType = SCRIPT_TYPES['P2WPKH']; input.hasWitness = true; input.signatures = [null]; input.pubkeys = [ourPubKey]; - input.signScript = new P2PKH( - data: new PaymentData(pubkey: ourPubKey), - network: this.network) - .data - .output; - } else { - // DRY CODE - Uint8List prevOutScript = pubkeyToOutputScript(ourPubKey); + input.signScript = + P2PKH(data: PaymentData(pubkey: ourPubKey), network: network) + .data! + .output; + } else if (type == SCRIPT_TYPES['P2PKH']) { + var prevOutScript = pubkeyToOutputScript(ourPubKey); input.prevOutType = SCRIPT_TYPES['P2PKH']; input.signatures = [null]; input.pubkeys = [ourPubKey]; input.signScript = prevOutScript; + } else { + // TODO other type } } else { - Uint8List prevOutScript = pubkeyToOutputScript(ourPubKey); + var prevOutScript = pubkeyToOutputScript(ourPubKey); input.prevOutType = SCRIPT_TYPES['P2PKH']; input.signatures = [null]; input.pubkeys = [ourPubKey]; @@ -205,24 +257,23 @@ class TransactionBuilder { } } var signatureHash; - if (input.hasWitness) { - signatureHash = this - ._tx - .hashForWitnessV0(vin, input.signScript, input.value, hashType); - } else { + if (input.hasWitness!) { signatureHash = - this._tx.hashForSignature(vin, input.signScript, hashType); + _tx!.hashForWitnessV0(vin, input.signScript!, input.value!, hashType); + } else { + signatureHash = _tx!.hashForSignature(vin, input.signScript, hashType); } // enforce in order signing of public keys var signed = false; - for (var i = 0; i < input.pubkeys.length; i++) { - if (HEX.encode(ourPubKey).compareTo(HEX.encode(input.pubkeys[i])) != 0) + for (var i = 0; i < input.pubkeys!.length; i++) { + if (HEX.encode(ourPubKey!).compareTo(HEX.encode(input.pubkeys![i]!)) != 0) continue; - if (input.signatures[i] != null) + if (input.signatures![i] != null) { throw new ArgumentError('Signature already exists'); + } final signature = keyPair.sign(signatureHash); - input.signatures[i] = bscript.encodeSignature(signature, hashType); + input.signatures![i] = bscript.encodeSignature(signature, hashType); signed = true; } if (!signed) throw new ArgumentError('Key pair cannot sign for this input'); @@ -238,63 +289,62 @@ class TransactionBuilder { Transaction _build(bool allowIncomplete) { if (!allowIncomplete) { - if (_tx.ins.length == 0) + if (_tx!.ins.isEmpty) throw new ArgumentError('Transaction has no inputs'); - if (_tx.outs.length == 0) + if (_tx!.outs.isEmpty) throw new ArgumentError('Transaction has no outputs'); } - final tx = Transaction.clone(_tx); - - for (var i = 0; i < _inputs.length; i++) { - if (_inputs[i].pubkeys != null && - _inputs[i].signatures != null && - _inputs[i].pubkeys.length != 0 && - _inputs[i].signatures.length != 0) { - if (_inputs[i].prevOutType == SCRIPT_TYPES['P2PKH']) { - P2PKH payment = new P2PKH( - data: new PaymentData( - pubkey: _inputs[i].pubkeys[0], - signature: _inputs[i].signatures[0]), - network: network); - tx.setInputScript(i, payment.data.input); - tx.setWitness(i, payment.data.witness); - } else if (_inputs[i].prevOutType == SCRIPT_TYPES['P2WPKH']) { - P2WPKH payment = new P2WPKH( - data: new PaymentData( - pubkey: _inputs[i].pubkeys[0], - signature: _inputs[i].signatures[0]), - network: network); - tx.setInputScript(i, payment.data.input); - tx.setWitness(i, payment.data.witness); + final tx = Transaction.clone(_tx!); + + for (var i = 0; i < _inputs!.length; i++) { + final input = _inputs![i]; + if (input.pubkeys != null && + input.signatures != null && + input.pubkeys!.isNotEmpty && + input.signatures!.isNotEmpty) { + final result = + buildByType(input.prevOutType, input, allowIncomplete, network); + if (result == null) { + if (!allowIncomplete && + input.prevOutType == SCRIPT_TYPES['NONSTANDARD']) { + throw ArgumentError('Unknown input type'); + } + if (!allowIncomplete) { + throw ArgumentError('Not enough information'); + } + continue; } + + tx.setInputScript(i, result.input); + tx.setWitness(i, result.witness); } else if (!allowIncomplete) { - throw new ArgumentError('Transaction is not complete'); + throw ArgumentError('Transaction is not complete'); } } if (!allowIncomplete) { // do not rely on this, its merely a last resort - if (_overMaximumFees(tx.virtualSize())) { - throw new ArgumentError('Transaction has absurd fees'); - } + // if (_overMaximumFees(tx.virtualSize())) { + // throw new ArgumentError('Transaction has absurd fees'); + // } } return tx; } bool _overMaximumFees(int bytes) { - int incoming = _inputs.fold(0, (cur, acc) => cur + (acc.value ?? 0)); - int outgoing = _tx.outs.fold(0, (cur, acc) => cur + (acc.value ?? 0)); + int incoming = _inputs!.fold(0, (cur, acc) => cur + (acc.value ?? 0)); + int outgoing = _tx!.outs.fold(0, (cur, acc) => cur + (acc.value ?? 0)); int fee = incoming - outgoing; int feeRate = fee ~/ bytes; return feeRate > maximumFeeRate; } bool _canModifyInputs() { - return _inputs.every((input) { + return _inputs!.every((input) { if (input.signatures == null) return true; - return input.signatures.every((signature) { + return input.signatures!.every((signature) { if (signature == null) return true; return _signatureHashType(signature) & SIGHASH_ANYONECANPAY != 0; }); @@ -302,11 +352,11 @@ class TransactionBuilder { } bool _canModifyOutputs() { - final nInputs = _tx.ins.length; - final nOutputs = _tx.outs.length; - return _inputs.every((input) { + final nInputs = _tx!.ins.length; + final nOutputs = _tx!.outs.length; + return _inputs!.every((input) { if (input.signatures == null) return true; - return input.signatures.every((signature) { + return input.signatures!.every((signature) { if (signature == null) return true; final hashType = _signatureHashType(signature); final hashTypeMod = hashType & 0x1f; @@ -324,15 +374,15 @@ class TransactionBuilder { bool _needsOutputs(int signingHashType) { if (signingHashType == SIGHASH_ALL) { - return this._tx.outs.length == 0; + return this._tx!.outs.length == 0; } // if inputs are being signed with SIGHASH_NONE, we don't strictly need outputs // .build() will fail, but .buildIncomplete() is OK - return (this._tx.outs.length == 0) && - _inputs.map((input) { - if (input.signatures == null || input.signatures.length == 0) + return (this._tx!.outs.length == 0) && + _inputs!.map((input) { + if (input.signatures == null || input.signatures!.length == 0) return false; - return input.signatures.map((signature) { + return input.signatures!.map((signature) { if (signature == null) return false; // no signature, no issue final hashType = _signatureHashType(signature); if (hashType & SIGHASH_NONE != 0) @@ -346,11 +396,11 @@ class TransactionBuilder { return input.pubkeys != null && input.signScript != null && input.signatures != null && - input.signatures.length == input.pubkeys.length && - input.pubkeys.length > 0; + input.signatures!.length == input.pubkeys!.length && + input.pubkeys!.length > 0; } - _addInputUnsafe(Uint8List hash, int vout, Input options) { + int _addInputUnsafe(Uint8List hash, int? vout, Input options) { String txHash = HEX.encode(hash); Input input; if (isCoinbaseHash(hash)) { @@ -361,24 +411,24 @@ class TransactionBuilder { throw new ArgumentError('Duplicate TxOut: ' + prevTxOut); if (options.script != null) { input = - Input.expandInput(options.script, options.witness ?? EMPTY_WITNESS); + Input.expandInput(options.script!, options.witness ?? EMPTY_WITNESS!); } else { input = new Input(); } if (options.value != null) input.value = options.value; if (input.prevOutScript == null && options.prevOutScript != null) { if (input.pubkeys == null && input.signatures == null) { - var expanded = Output.expandOutput(options.prevOutScript); - if (expanded.pubkeys != null && !expanded.pubkeys.isEmpty) { + var expanded = OutputBase.expandOutput(options.prevOutScript); + if (expanded.pubkeys != null && !expanded.pubkeys!.isEmpty) { input.pubkeys = expanded.pubkeys; input.signatures = expanded.signatures; } } input.prevOutScript = options.prevOutScript; - input.prevOutType = classifyOutput(options.prevOutScript); + input.prevOutType = classifyOutput(options.prevOutScript!); } - int vin = _tx.addInput(hash, vout, options.sequence, options.script); - _inputs.add(input); + int vin = _tx!.addInput(hash, vout, options.sequence, options.script); + _inputs!.add(input); _prevTxSet[prevTxOut] = true; return vin; } @@ -387,12 +437,46 @@ class TransactionBuilder { return buffer.buffer.asByteData().getUint8(buffer.length - 1); } - Transaction get tx => _tx; + Transaction? get tx => _tx; Map get prevTxSet => _prevTxSet; } -Uint8List pubkeyToOutputScript(Uint8List pubkey, [NetworkType nw]) { +PaymentData? buildByType( + String? type, Input input, bool allowIncomplete, NetworkType? network) { + if (type == SCRIPT_TYPES['P2PKH']) { + return P2PKH( + data: PaymentData( + pubkey: input.pubkeys![0], signature: input.signatures![0]), + network: network) + .data; + } else if (type == SCRIPT_TYPES['P2WPKH']) { + return P2WPKH( + data: PaymentData( + pubkey: input.pubkeys![0], signature: input.signatures![0]), + network: network) + .data; + } else if (type == SCRIPT_TYPES['P2SH']) { + final redeem = + buildByType(input.redeemScriptType, input, allowIncomplete, network); + + if (redeem == null) { + return null; + } + return P2SH( + data: PaymentData( + redeem: PaymentData( + output: redeem.output ?? input.redeemScript, + input: redeem.input, + witness: redeem.witness, + )), + network: network) + .data; + } + return null; +} + +Uint8List? pubkeyToOutputScript(Uint8List? pubkey, [NetworkType? nw]) { NetworkType network = nw ?? bitcoin; P2PKH p2pkh = new P2PKH(data: new PaymentData(pubkey: pubkey), network: network); diff --git a/lib/src/utils/magic_hash.dart b/lib/src/utils/magic_hash.dart index f99dba4..2c3fd56 100644 --- a/lib/src/utils/magic_hash.dart +++ b/lib/src/utils/magic_hash.dart @@ -4,9 +4,9 @@ import '../../src/crypto.dart'; import 'varuint.dart'; import '../../src/models/networks.dart'; -Uint8List magicHash(String message, [NetworkType network]) { +Uint8List magicHash(String message, [NetworkType? network]) { network = network ?? bitcoin; - Uint8List messagePrefix = utf8.encode(network.messagePrefix); + Uint8List messagePrefix = utf8.encode(network.messagePrefix) as Uint8List; int messageVISize = encodingLength(message.length); int length = messagePrefix.length + messageVISize + message.length; Uint8List buffer = new Uint8List(length); diff --git a/lib/src/utils/push_data.dart b/lib/src/utils/push_data.dart index f608a94..a664735 100644 --- a/lib/src/utils/push_data.dart +++ b/lib/src/utils/push_data.dart @@ -2,51 +2,51 @@ import 'dart:typed_data'; import 'constants/op.dart'; class DecodedPushData { - int opcode; - int number; - int size; + int? opcode; + int? number; + int? size; DecodedPushData({this.opcode, this.number, this.size}); } class EncodedPushData { - int size; - Uint8List buffer; + int? size; + Uint8List? buffer; EncodedPushData({this.size, this.buffer}); } -EncodedPushData encode(Uint8List buffer, number, offset) { +EncodedPushData encode(Uint8List? buffer, number, offset) { var size = encodingLength(number); // ~6 bit if (size == 1) { - buffer.buffer.asByteData().setUint8(offset, number); + buffer!.buffer.asByteData().setUint8(offset, number); // 8 bit } else if (size == 2) { - buffer.buffer.asByteData().setUint8(offset, OPS['OP_PUSHDATA1']); + buffer!.buffer.asByteData().setUint8(offset, OPS['OP_PUSHDATA1']!); buffer.buffer.asByteData().setUint8(offset + 1, number); // 16 bit } else if (size == 3) { - buffer.buffer.asByteData().setUint8(offset, OPS['OP_PUSHDATA2']); + buffer!.buffer.asByteData().setUint8(offset, OPS['OP_PUSHDATA2']!); buffer.buffer.asByteData().setUint16(offset + 1, number, Endian.little); // 32 bit } else { - buffer.buffer.asByteData().setUint8(offset, OPS['OP_PUSHDATA4']); + buffer!.buffer.asByteData().setUint8(offset, OPS['OP_PUSHDATA4']!); buffer.buffer.asByteData().setUint32(offset + 1, number, Endian.little); } return new EncodedPushData(size: size, buffer: buffer); } -DecodedPushData decode(Uint8List bf, int offset) { +DecodedPushData? decode(Uint8List bf, int offset) { ByteBuffer buffer = bf.buffer; int opcode = buffer.asByteData().getUint8(offset); int number, size; // ~6 bit - if (opcode < OPS['OP_PUSHDATA1']) { + if (opcode < OPS['OP_PUSHDATA1']!) { number = opcode; size = 1; diff --git a/lib/src/utils/script.dart b/lib/src/utils/script.dart index ac6678b..c5d8365 100644 --- a/lib/src/utils/script.dart +++ b/lib/src/utils/script.dart @@ -10,15 +10,30 @@ Map REVERSE_OPS = final OP_INT_BASE = OPS['OP_RESERVED']; final ZERO = Uint8List.fromList([0]); -Uint8List compile(List chunks) { - final bufferSize = chunks.fold(0, (acc, chunk) { +bool isOPInt(dynamic value) { + return (value is num && + (value == OPS['OP_0'] || + (value >= OPS['OP_1']! && value <= OPS['OP_16']!) || + value == OPS['OP_1NEGATE'])); +} + +bool isPushOnlyChunk(dynamic value) { + return (value is Uint8List) || isOPInt(value); +} + +bool isPushOnly(dynamic value) { + return (value is List) && value.every(isPushOnlyChunk); +} + +Uint8List? compile(List chunks) { + final bufferSize = chunks.fold(0, (dynamic acc, chunk) { if (chunk is int) return acc + 1; if (chunk.length == 1 && asMinimalOP(chunk) != null) { return acc + 1; } return acc + pushData.encodingLength(chunk.length) + chunk.length; }); - var buffer = new Uint8List(bufferSize); + Uint8List? buffer = new Uint8List(bufferSize); var offset = 0; chunks.forEach((chunk) { @@ -27,29 +42,29 @@ Uint8List compile(List chunks) { // adhere to BIP62.3, minimal push policy final opcode = asMinimalOP(chunk); if (opcode != null) { - buffer.buffer.asByteData().setUint8(offset, opcode); + buffer!.buffer.asByteData().setUint8(offset, opcode); offset += 1; return null; } pushData.EncodedPushData epd = pushData.encode(buffer, chunk.length, offset); - offset += epd.size; + offset += epd.size!; buffer = epd.buffer; - buffer.setRange(offset, offset + chunk.length, chunk); + buffer!.setRange(offset, offset + chunk.length, chunk); offset += chunk.length; // opcode } else { - buffer.buffer.asByteData().setUint8(offset, chunk); + buffer!.buffer.asByteData().setUint8(offset, chunk); offset += 1; } }); - if (offset != buffer.length) + if (offset != buffer!.length) throw new ArgumentError("Could not decode chunks"); return buffer; } -List decompile(dynamic buffer) { +List? decompile(dynamic buffer) { List chunks = []; if (buffer == null) return chunks; @@ -65,13 +80,13 @@ List decompile(dynamic buffer) { // did reading a pushDataInt fail? if (d == null) return null; - i += d.size; + i += d.size!; // attempt to read too much data? - if (i + d.number > buffer.length) return null; + if (i + d.number! > buffer.length) return null; - final data = buffer.sublist(i, i + d.number); - i += d.number; + final data = buffer.sublist(i, i + d.number!); + i += d.number!; // decompile minimally final op = asMinimalOP(data); @@ -90,22 +105,22 @@ List decompile(dynamic buffer) { return chunks; } -Uint8List fromASM(String asm) { +Uint8List? fromASM(String? asm) { if (asm == '') return Uint8List.fromList([]); - return compile(asm.split(' ').map((chunkStr) { + return compile(asm!.split(' ').map((chunkStr) { if (OPS[chunkStr] != null) return OPS[chunkStr]; return HEX.decode(chunkStr); }).toList()); } -String toASM(List c) { - List chunks; +String toASM(List? c) { + List? chunks; if (c is Uint8List) { chunks = decompile(c); } else { chunks = c; } - return chunks.map((chunk) { + return chunks!.map((chunk) { // data? if (chunk is Uint8List) { final op = asMinimalOP(chunk); @@ -117,10 +132,10 @@ String toASM(List c) { }).join(' '); } -int asMinimalOP(Uint8List buffer) { +int? asMinimalOP(Uint8List buffer) { if (buffer.length == 0) return OPS['OP_0']; if (buffer.length != 1) return null; - if (buffer[0] >= 1 && buffer[0] <= 16) return OP_INT_BASE + buffer[0]; + if (buffer[0] >= 1 && buffer[0] <= 16) return OP_INT_BASE! + buffer[0]; if (buffer[0] == 0x81) return OPS['OP_1NEGATE']; return null; } @@ -179,17 +194,17 @@ Uint8List bip66encode(r, s) { if (lenS > 1 && (s[0] == 0x00) && s[1] & 0x80 == 0) throw new ArgumentError('S value excessively padded'); - var signature = new Uint8List(6 + lenR + lenS); + var signature = new Uint8List(6 + lenR + lenS as int); // 0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S] signature[0] = 0x30; signature[1] = signature.length - 2; signature[2] = 0x02; signature[3] = r.length; - signature.setRange(4, 4 + lenR, r); - signature[4 + lenR] = 0x02; - signature[5 + lenR] = s.length; - signature.setRange(6 + lenR, 6 + lenR + lenS, s); + signature.setRange(4, 4 + lenR as int, r); + signature[4 + lenR as int] = 0x02; + signature[5 + lenR as int] = s.length; + signature.setRange(6 + lenR as int, 6 + lenR + lenS as int, s); return signature; } diff --git a/lib/src/utils/varuint.dart b/lib/src/utils/varuint.dart index 04aff4c..1694249 100644 --- a/lib/src/utils/varuint.dart +++ b/lib/src/utils/varuint.dart @@ -1,7 +1,7 @@ import 'check_types.dart'; import 'dart:typed_data'; -Uint8List encode(int number, [Uint8List buffer, int offset]) { +Uint8List encode(int number, [Uint8List? buffer, int? offset]) { if (!isUint(number, 53)) ; buffer = buffer ?? new Uint8List(encodingLength(number)); @@ -30,7 +30,7 @@ Uint8List encode(int number, [Uint8List buffer, int offset]) { return buffer; } -int decode(Uint8List buffer, [int offset]) { +int decode(Uint8List buffer, [int? offset]) { offset = offset ?? 0; ByteData bytes = buffer.buffer.asByteData(); final first = bytes.getUint8(offset); diff --git a/pubspec.yaml b/pubspec.yaml index ee9b2ca..b6b5008 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,23 +2,30 @@ name: bitcoin_flutter description: A dart Bitcoin library for Flutter. BIP32, BIP39, P2PKH integration. version: 2.0.2 homepage: https://github.com/anicdh +publish_to: none environment: - sdk: ">=2.3.0 <3.0.0" + sdk: '>=2.12.0 <3.0.0' dependencies: bip39: ^1.0.6 bip32: ^2.0.0 pointycastle: ^3.6.0 hex: ^0.2.0 - bs58check: ^1.0.2 - bech32: 0.2.1 + # bs58check: ^1.0.2 + # bech32: 0.2.1 + collection: ^1.15.0-nullsafety.4 + defichain_bech32: + git: + url: https://github.com/ArunBabu98/defichain_bech32 + ref: dart3 + bip32_defichain: ^3.0.1+1 dev_dependencies: test: ^1.21.1 dependency_overrides: analyzer: 4.1.0 - convert: ^3.0.0 + convert: ^3.1.1 crypto: ^3.0.0 hex: ^0.2.0 diff --git a/test/ecpair_test.dart b/test/ecpair_test.dart index ec70e54..2bfc45c 100644 --- a/test/ecpair_test.dart +++ b/test/ecpair_test.dart @@ -15,30 +15,30 @@ main() { group('ECPair', () { group('fromPrivateKey', () { test('defaults to compressed', () { - final keyPair = ECPair.fromPrivateKey(ONE); + final keyPair = ECPair.fromPrivateKey(ONE as Uint8List); expect(keyPair.compressed, true); }); test('supports the uncompressed option', () { - final keyPair = ECPair.fromPrivateKey(ONE, compressed: false); + final keyPair = ECPair.fromPrivateKey(ONE as Uint8List, compressed: false); expect(keyPair.compressed, false); }); test('supports the network option', () { - final keyPair = ECPair.fromPrivateKey(ONE, + final keyPair = ECPair.fromPrivateKey(ONE as Uint8List, network: NETWORKS.testnet, compressed: false); expect(keyPair.network, NETWORKS.testnet); }); (fixtures['valid'] as List).forEach((f) { test('derives public key for ${f['WIF']}', () { final d = HEX.decode(f['d']); - final keyPair = ECPair.fromPrivateKey(d, compressed: f['compressed']); - expect(HEX.encode(keyPair.publicKey), f['Q']); + final keyPair = ECPair.fromPrivateKey(d as Uint8List, compressed: f['compressed']); + expect(HEX.encode(keyPair.publicKey!), f['Q']); }); }); (fixtures['invalid']['fromPrivateKey'] as List).forEach((f) { test('throws ' + f['exception'], () { final d = HEX.decode(f['d']); try { - expect(ECPair.fromPrivateKey(d), isArgumentError); + expect(ECPair.fromPrivateKey(d as Uint8List), isArgumentError); } catch (err) { expect((err as ArgumentError).message, f['exception']); } @@ -50,7 +50,7 @@ main() { test('throws ' + f['exception'], () { final Q = HEX.decode(f['Q']); try { - expect(ECPair.fromPublicKey(Q), isArgumentError); + expect(ECPair.fromPublicKey(Q as Uint8List), isArgumentError); } catch (err) { expect((err as ArgumentError).message, f['exception']); } @@ -62,7 +62,7 @@ main() { test('imports ${f['WIF']}', () { final keyPair = ECPair.fromWIF(f['WIF']); var network = _getNetwork(f); - expect(HEX.encode(keyPair.privateKey), f['d']); + expect(HEX.encode(keyPair.privateKey!), f['d']); expect(keyPair.compressed, f['compressed']); expect(keyPair.network, network); }); @@ -121,7 +121,7 @@ main() { group('.network', () { (fixtures['valid'] as List).forEach((f) { test('return ${f['network']} for ${f['WIF']}', () { - NETWORKS.NetworkType network = _getNetwork(f); + NETWORKS.NetworkType? network = _getNetwork(f); final keyPair = ECPair.fromWIF(f['WIF']); expect(keyPair.network, network); }); @@ -130,7 +130,7 @@ main() { }); } -NETWORKS.NetworkType _getNetwork(f) { +NETWORKS.NetworkType? _getNetwork(f) { var network; if (f['network'] != null) { if (f['network'] == 'bitcoin') { diff --git a/test/integration/addresses_test.dart b/test/integration/addresses_test.dart index ebe72db..ab6834c 100644 --- a/test/integration/addresses_test.dart +++ b/test/integration/addresses_test.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import '../../lib/src/models/networks.dart' as NETWORKS; import '../../lib/src/ecpair.dart' show ECPair; import '../../lib/src/payments/index.dart' show PaymentData; @@ -30,7 +32,7 @@ main() { }); test('can generate an address from a SHA256 hash', () { final hash = new SHA256Digest() - .process(utf8.encode('correct horse battery staple')); + .process(utf8.encode('correct horse battery staple') as Uint8List); final keyPair = ECPair.fromPrivateKey(hash); final address = new P2PKH(data: new PaymentData(pubkey: keyPair.publicKey)) @@ -76,7 +78,7 @@ main() { final address = new P2WPKH(data: new PaymentData(pubkey: keyPair.publicKey)) .data - .address; + !.address; expect(address, 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'); }); test('can generate a SegWit testnet address', () { @@ -87,7 +89,7 @@ main() { data: new PaymentData(pubkey: keyPair.publicKey), network: testnet) .data - .address; + !.address; expect(address, 'tb1qgmp0h7lvexdxx9y05pmdukx09xcteu9sx2h4ya'); }); }); diff --git a/test/integration/bip32_test.dart b/test/integration/bip32_test.dart index 22119f0..75ecf4e 100644 --- a/test/integration/bip32_test.dart +++ b/test/integration/bip32_test.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:bitcoin_flutter/src/models/networks.dart'; import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; import 'package:bitcoin_flutter/src/payments/p2pkh.dart'; @@ -43,7 +45,7 @@ void main() { test('can create a BIP32, bitcoin, account 0, external address', () { const path = "m/0'/0/0"; final root = bip32.BIP32.fromSeed(HEX.decode( - 'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd')); + 'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd') as Uint8List); final child1 = root.derivePath(path); // option 2, manually final child1b = root.deriveHardened(0).derive(0).derive(0); @@ -52,7 +54,7 @@ void main() { }); test('can create a BIP44, bitcoin, account 0, external address', () { final root = bip32.BIP32.fromSeed(HEX.decode( - 'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd')); + 'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd') as Uint8List); final child1 = root.derivePath("m/44'/0'/0'/0/0"); // option 2, manually final child1b = root @@ -87,7 +89,7 @@ void main() { }); } -String getAddress(node, [network]) { +String? getAddress(node, [network]) { return P2PKH(data: new PaymentData(pubkey: node.publicKey), network: network) .data .address; diff --git a/test/integration/transactions_test.dart b/test/integration/transactions_test.dart index 574b0f8..17edaf3 100644 --- a/test/integration/transactions_test.dart +++ b/test/integration/transactions_test.dart @@ -92,7 +92,7 @@ main() { 0, null, p2wpkh - .output); // Alice's previous transaction output, has 200000 satoshis + !.output); // Alice's previous transaction output, has 200000 satoshis txb.addOutput('tb1qchsmnkk5c8wsjg8vxecmsntynpmkxme0yvh2yt', 1000000); txb.addOutput('tb1qn40fftdp6z2lvzmsz4s0gyks3gq86y2e8svgap', 8995000); diff --git a/test/payments/p2pkh_test.dart b/test/payments/p2pkh_test.dart index 63721e7..1cadcc8 100644 --- a/test/payments/p2pkh_test.dart +++ b/test/payments/p2pkh_test.dart @@ -57,10 +57,10 @@ PaymentData _preformPaymentData(dynamic x) { final output = x['output'] != null ? bscript.fromASM(x['output']) : x['outputHex'] != null ? HEX.decode(x['outputHex']) : null; final pubkey = x['pubkey'] != null ? HEX.decode(x['pubkey']) : null; final signature = x['signature'] != null ? HEX.decode(x['signature']) : null; - return new PaymentData(address: address, hash: hash, input: input, output: output, pubkey: pubkey, signature: signature); + return new PaymentData(address: address, hash: hash as Uint8List?, input: input, output: output as Uint8List?, pubkey: pubkey as Uint8List?, signature: signature as Uint8List?); } -String _toString(dynamic x) { +String? _toString(dynamic x) { if (x == null) { return null; } diff --git a/test/payments/p2wpkh_test.dart b/test/payments/p2wpkh_test.dart index 4adb17a..7279007 100644 --- a/test/payments/p2wpkh_test.dart +++ b/test/payments/p2wpkh_test.dart @@ -17,25 +17,25 @@ main() { final arguments = _preformPaymentData(f['arguments']); final p2wpkh = new P2WPKH(data: arguments); if (arguments.address == null) { - expect(p2wpkh.data.address, f['expected']['address']); + expect(p2wpkh.data!.address, f['expected']['address']); } if (arguments.hash == null) { - expect(_toString(p2wpkh.data.hash), f['expected']['hash']); + expect(_toString(p2wpkh.data!.hash), f['expected']['hash']); } if (arguments.pubkey == null) { - expect(_toString(p2wpkh.data.pubkey), f['expected']['pubkey']); + expect(_toString(p2wpkh.data!.pubkey), f['expected']['pubkey']); } if (arguments.input == null) { - expect(_toString(p2wpkh.data.input), f['expected']['input']); + expect(_toString(p2wpkh.data!.input), f['expected']['input']); } if (arguments.output == null) { - expect(_toString(p2wpkh.data.output), f['expected']['output']); + expect(_toString(p2wpkh.data!.output), f['expected']['output']); } if (arguments.signature == null) { - expect(_toString(p2wpkh.data.signature), f['expected']['signature']); + expect(_toString(p2wpkh.data!.signature), f['expected']['signature']); } if (arguments.witness == null) { - expect(_toString(p2wpkh.data.witness), f['expected']['witness']); + expect(_toString(p2wpkh.data!.witness), f['expected']['witness']); } }); }); @@ -64,10 +64,10 @@ PaymentData _preformPaymentData(dynamic x) { final output = x['output'] != null ? bscript.fromASM(x['output']) : x['outputHex'] != null ? HEX.decode(x['outputHex']) : null; final pubkey = x['pubkey'] != null ? HEX.decode(x['pubkey']) : null; final signature = x['signature'] != null ? HEX.decode(x['signature']) : null; - return new PaymentData(address: address, hash: hash, input: input, output: output, pubkey: pubkey, signature: signature, witness: witness); + return new PaymentData(address: address, hash: hash as Uint8List?, input: input, output: output as Uint8List?, pubkey: pubkey as Uint8List?, signature: signature as Uint8List?, witness: witness); } -String _toString(dynamic x) { +String? _toString(dynamic x) { if (x == null) { return null; } diff --git a/test/transaction_builder_test.dart b/test/transaction_builder_test.dart index 749ac45..f4e9736 100644 --- a/test/transaction_builder_test.dart +++ b/test/transaction_builder_test.dart @@ -31,7 +31,7 @@ constructSign(f, TransactionBuilder txb) { return txb; } -TransactionBuilder construct(f, [bool dontSign]) { +TransactionBuilder construct(f, [bool? dontSign]) { final network = NETWORKS[f['network']]; final txb = new TransactionBuilder(network: network); if (f['version'] != null) txb.setVersion(f['version']); @@ -71,7 +71,7 @@ main() { .readAsStringSync(encoding: utf8)); group('TransactionBuilder', () { final keyPair = ECPair.fromPrivateKey(HEX.decode( - '0000000000000000000000000000000000000000000000000000000000000001')); + '0000000000000000000000000000000000000000000000000000000000000001') as Uint8List); final scripts = [ '1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH', '1cMh228HTCiwS8ZsaakH8A8wze1JR5ZsP' @@ -131,27 +131,27 @@ main() { }); }); group('addInput', () { - TransactionBuilder txb; + late TransactionBuilder txb; setUp(() { txb = new TransactionBuilder(); }); test('accepts a txHash, index [and sequence number]', () { final vin = txb.addInput(txHash, 1, 54); expect(vin, 0); - final txIn = txb.tx.ins[0]; + final txIn = txb.tx!.ins[0]; expect(txIn.hash, txHash); expect(txIn.index, 1); expect(txIn.sequence, 54); - expect(txb.inputs[0].prevOutScript, null); + expect(txb.inputs![0].prevOutScript, null); }); test('accepts a txHash, index [, sequence number and scriptPubKey]', () { final vin = txb.addInput(txHash, 1, 54, scripts.elementAt(1)); expect(vin, 0); - final txIn = txb.tx.ins[0]; + final txIn = txb.tx!.ins[0]; expect(txIn.hash, txHash); expect(txIn.index, 1); expect(txIn.sequence, 54); - expect(txb.inputs[0].prevOutScript, scripts.elementAt(1)); + expect(txb.inputs![0].prevOutScript, scripts.elementAt(1)); }); test('accepts a prevTx, index [and sequence number]', () { final prevTx = new Transaction(); @@ -161,11 +161,11 @@ main() { final vin = txb.addInput(prevTx, 1, 54); expect(vin, 0); - final txIn = txb.tx.ins[0]; + final txIn = txb.tx!.ins[0]; expect(txIn.hash, prevTx.getHash()); expect(txIn.index, 1); expect(txIn.sequence, 54); - expect(txb.inputs[0].prevOutScript, scripts.elementAt(1)); + expect(txb.inputs![0].prevOutScript, scripts.elementAt(1)); }); test('returns the input index', () { expect(txb.addInput(txHash, 0), 0); @@ -186,7 +186,7 @@ main() { }); }); group('addOutput', () { - TransactionBuilder txb; + late TransactionBuilder txb; setUp(() { txb = new TransactionBuilder(); }); @@ -197,14 +197,14 @@ main() { .address; final vout = txb.addOutput(address, 1000); expect(vout, 0); - final txout = txb.tx.outs[0]; + final txout = txb.tx!.outs[0]; expect(txout.script, scripts.elementAt(0)); expect(txout.value, 1000); }); test('accepts a ScriptPubKey and value', () { final vout = txb.addOutput(scripts.elementAt(0), 1000); expect(vout, 0); - final txout = txb.tx.outs[0]; + final txout = txb.tx!.outs[0]; expect(txout.script, scripts.elementAt(0)); expect(txout.value, 1000); }); @@ -260,9 +260,9 @@ main() { }); }); group('addOutputData', () { - TransactionBuilder txb; - String data; - String data2; + late TransactionBuilder txb; + late String data; + late String data2; setUp(() { txb = new TransactionBuilder(); data = 'Hey this is a random string without Bitcoins.'; @@ -271,7 +271,7 @@ main() { test('accepts a ScriptPubKey', () { final vout = txb.addOutputData(scripts.elementAt(0)); expect(vout, 0); - final txout = txb.tx.outs[0]; + final txout = txb.tx!.outs[0]; expect(txout.script, scripts.elementAt(0)); expect(txout.value, 0); }); diff --git a/test/transaction_test.dart b/test/transaction_test.dart index f817c57..b83dd13 100644 --- a/test/transaction_test.dart +++ b/test/transaction_test.dart @@ -9,11 +9,11 @@ import '../lib/src/transaction.dart'; main() { final fixtures = json.decode(new File('test/fixtures/transaction.json') .readAsStringSync(encoding: utf8)); - final valids = (fixtures['valid'] as List); + final valids = (fixtures['valid'] as List?); group('Transaction', () { group('fromBuffer/fromHex', () { - valids.forEach(importExport); + valids!.forEach(importExport); (fixtures['hashForSignature'] as List).forEach(importExport); (fixtures['invalid']['fromBuffer'] as List).forEach((f) { test('throws on ${f['exception']}', () { @@ -33,7 +33,7 @@ main() { }); group('toBuffer/toHex', () { - valids.forEach((f) { + valids!.forEach((f) { test('exports ${f['description']} (${f['id']})', () { Transaction actual = fromRaw(f['raw'], false); expect(actual.toHex(), f['hex']); @@ -49,7 +49,7 @@ main() { group('weight/virtualSize', () { test('computes virtual size', () { - valids.forEach((f) { + valids!.forEach((f) { final txHex = (f['whex'] != null && f['whex'] != '') ? f['whex'] : f['hex']; final transaction = Transaction.fromHex(txHex); @@ -59,7 +59,7 @@ main() { }); group('addInput', () { - var prevTxHash; + late var prevTxHash; setUp(() { prevTxHash = HEX.decode( 'ffffffff00ffff000000000000000000000000000000000000000000101010ff'); @@ -72,7 +72,7 @@ main() { test('defaults to empty script, and 0xffffffff SEQUENCE number', () { final tx = new Transaction(); tx.addInput(prevTxHash, 0); - expect(tx.ins[0].script.length, 0); + expect(tx.ins[0].script!.length, 0); expect(tx.ins[0].sequence, 0xffffffff); }); (fixtures['invalid']['addInput'] as List).forEach((f) { @@ -80,7 +80,7 @@ main() { final tx = new Transaction(); final hash = HEX.decode(f['hash']); try { - expect(tx.addInput(hash, f['index']), isArgumentError); + expect(tx.addInput(hash as Uint8List, f['index']), isArgumentError); } catch (err) { expect((err as ArgumentError).message, f['exception']); } @@ -105,7 +105,7 @@ main() { }); } - valids.forEach(verify); + valids!.forEach(verify); }); group('isCoinbase', () { @@ -118,7 +118,7 @@ main() { }); } - valids.forEach(verify); + valids!.forEach(verify); }); group('hashForSignature', () { @@ -160,7 +160,7 @@ Transaction fromRaw(raw, [isWitness]) { } else if (txIn['script'] != null && txIn['script'] != '') { scriptSig = bscript.fromASM(txIn['script']); } - tx.addInput(txHash, txIn['index'], txIn['sequence'], scriptSig); + tx.addInput(txHash as Uint8List, txIn['index'], txIn['sequence'], scriptSig); if (isWitness) { var witness = (txIn['witness'] as List)