├── .circleci └── config.yml ├── .gitignore ├── lib ├── src │ ├── templates │ │ ├── pubkey.dart │ │ ├── witnesspubkeyhash.dart │ │ └── pubkeyhash.dart │ ├── utils │ │ ├── check_types.dart │ │ ├── magic_hash.dart │ │ ├── push_data.dart │ │ ├── varuint.dart │ │ ├── constants │ │ │ └── op.dart │ │ └── script.dart │ ├── payments │ │ ├── index.dart │ │ ├── p2pk.dart │ │ ├── p2wpkh.dart │ │ └── p2pkh.dart │ ├── crypto.dart │ ├── models │ │ └── networks.dart │ ├── classify.dart │ ├── address.dart │ ├── ecpair.dart │ ├── bitcoin_flutter_base.dart │ ├── transaction_builder.dart │ └── transaction.dart └── bitcoin_flutter.dart ├── analysis_options.yaml ├── pubspec.yaml ├── LICENSE ├── example └── bitcoin_flutter_example.dart ├── test ├── bitcoin_test.dart ├── address_test.dart ├── payments │ ├── p2pkh_test.dart │ └── p2wpkh_test.dart ├── integration │ ├── addresses_test.dart │ ├── bip32_test.dart │ └── transactions_test.dart ├── fixtures │ ├── ecpair.json │ ├── p2pkh.json │ ├── p2wpkh.json │ ├── transaction.json │ └── transaction_builder.json ├── ecpair_test.dart ├── transaction_test.dart └── transaction_builder_test.dart ├── CHANGELOG.md └── README.md /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: google/dart 6 | steps: 7 | - checkout 8 | - run: pub get 9 | - run: pub run test 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | .packages 4 | # Remove the following pattern if you wish to check in your lock file 5 | pubspec.lock 6 | 7 | # Conventional directory for build outputs 8 | build/ 9 | 10 | # Directory created by dartdoc 11 | doc/api/ 12 | 13 | .idea/ -------------------------------------------------------------------------------- /lib/src/templates/pubkey.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import '../utils/script.dart' as bscript; 3 | import '../utils/constants/op.dart'; 4 | 5 | bool inputCheck(List chunks) { 6 | return chunks.length == 1 && bscript.isCanonicalScriptSignature(chunks[0]); 7 | } 8 | 9 | bool outputCheck(Uint8List script) { 10 | // TODO 11 | } 12 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | # exclude: 3 | # - path/to/excluded/files/** 4 | 5 | # Lint rules and documentation, see http://dart-lang.github.io/linter/lints 6 | linter: 7 | rules: 8 | - cancel_subscriptions 9 | - hash_and_equals 10 | - iterable_contains_unrelated_type 11 | - list_remove_unrelated_type 12 | - test_types_in_equals 13 | - unrelated_type_equality_checks 14 | - valid_regexps 15 | -------------------------------------------------------------------------------- /lib/src/utils/check_types.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'dart:math'; 3 | 4 | const SATOSHI_MAX = 21 * 1e14; 5 | 6 | bool isShatoshi(int value) { 7 | return isUint(value, 53) && value <= SATOSHI_MAX; 8 | } 9 | 10 | bool isUint(int value, int bit) { 11 | return (value >= 0 && value <= pow(2, bit) - 1); 12 | } 13 | 14 | bool isHash160bit(Uint8List value) { 15 | return value.length == 20; 16 | } 17 | 18 | bool isHash256bit(Uint8List value) { 19 | return value.length == 32; 20 | } 21 | -------------------------------------------------------------------------------- /lib/bitcoin_flutter.dart: -------------------------------------------------------------------------------- 1 | /// Support for doing something awesome. 2 | /// 3 | /// More dartdocs go here. 4 | library bitcoin_flutter; 5 | 6 | export 'src/bitcoin_flutter_base.dart'; 7 | export 'src/models/networks.dart'; 8 | export 'src/transaction.dart'; 9 | export 'src/address.dart'; 10 | export 'src/transaction_builder.dart'; 11 | export 'src/ecpair.dart'; 12 | export 'src/payments/p2pkh.dart'; 13 | export 'src/payments/p2wpkh.dart'; 14 | export 'src/payments/index.dart'; 15 | // TODO: Export any libraries intended for clients of this package. 16 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: bitcoin_flutter 2 | description: A dart Bitcoin library for Flutter. BIP32, BIP39, P2PKH integration. 3 | version: 2.0.2 4 | homepage: https://github.com/anicdh 5 | 6 | environment: 7 | sdk: ">=2.3.0 <3.0.0" 8 | 9 | dependencies: 10 | bip39: ^1.0.6 11 | bip32: ^2.0.0 12 | pointycastle: ^3.6.0 13 | hex: ^0.2.0 14 | bs58check: ^1.0.2 15 | bech32: 0.2.1 16 | 17 | dev_dependencies: 18 | test: ^1.21.1 19 | 20 | dependency_overrides: 21 | analyzer: 4.1.0 22 | convert: ^3.0.0 23 | crypto: ^3.0.0 24 | hex: ^0.2.0 25 | -------------------------------------------------------------------------------- /lib/src/templates/witnesspubkeyhash.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import '../utils/script.dart' as bscript; 3 | import '../utils/constants/op.dart'; 4 | 5 | bool inputCheck(List chunks) { 6 | return chunks != null && 7 | chunks.length == 2 && 8 | bscript.isCanonicalScriptSignature(chunks[0]) && 9 | bscript.isCanonicalPubKey(chunks[1]); 10 | } 11 | 12 | bool outputCheck(Uint8List script) { 13 | final buffer = bscript.compile(script); 14 | return buffer.length == 22 && buffer[0] == OPS['OP_0'] && buffer[1] == 0x14; 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/payments/index.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | class PaymentData { 4 | String address; 5 | Uint8List hash; 6 | Uint8List output; 7 | Uint8List signature; 8 | Uint8List pubkey; 9 | Uint8List input; 10 | List witness; 11 | 12 | PaymentData( 13 | {this.address, 14 | this.hash, 15 | this.output, 16 | this.pubkey, 17 | this.input, 18 | this.signature, 19 | this.witness}); 20 | 21 | @override 22 | String toString() { 23 | return 'PaymentData{address: $address, hash: $hash, output: $output, signature: $signature, pubkey: $pubkey, input: $input, witness: $witness}'; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/templates/pubkeyhash.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import '../utils/script.dart' as bscript; 3 | import '../utils/constants/op.dart'; 4 | 5 | bool inputCheck(List chunks) { 6 | return chunks != null && 7 | chunks.length == 2 && 8 | bscript.isCanonicalScriptSignature(chunks[0]) && 9 | bscript.isCanonicalPubKey(chunks[1]); 10 | } 11 | 12 | bool outputCheck(Uint8List script) { 13 | final buffer = bscript.compile(script); 14 | return buffer.length == 25 && 15 | buffer[0] == OPS['OP_DUP'] && 16 | buffer[1] == OPS['OP_HASH160'] && 17 | buffer[2] == 0x14 && 18 | buffer[23] == OPS['OP_EQUALVERIFY'] && 19 | buffer[24] == OPS['OP_CHECKSIG']; 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/utils/magic_hash.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'dart:convert'; 3 | import '../../src/crypto.dart'; 4 | import 'varuint.dart'; 5 | import '../../src/models/networks.dart'; 6 | 7 | Uint8List magicHash(String message, [NetworkType network]) { 8 | network = network ?? bitcoin; 9 | Uint8List messagePrefix = utf8.encode(network.messagePrefix); 10 | int messageVISize = encodingLength(message.length); 11 | int length = messagePrefix.length + messageVISize + message.length; 12 | Uint8List buffer = new Uint8List(length); 13 | buffer.setRange(0, messagePrefix.length, messagePrefix); 14 | encode(message.length, buffer, messagePrefix.length); 15 | buffer.setRange( 16 | messagePrefix.length + messageVISize, length, utf8.encode(message)); 17 | return hash256(buffer); 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/crypto.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'package:pointycastle/digests/sha512.dart'; 3 | import 'package:pointycastle/api.dart' show KeyParameter; 4 | import 'package:pointycastle/macs/hmac.dart'; 5 | import 'package:pointycastle/digests/ripemd160.dart'; 6 | import 'package:pointycastle/digests/sha256.dart'; 7 | 8 | Uint8List hash160(Uint8List buffer) { 9 | Uint8List _tmp = new SHA256Digest().process(buffer); 10 | return new RIPEMD160Digest().process(_tmp); 11 | } 12 | 13 | Uint8List hmacSHA512(Uint8List key, Uint8List data) { 14 | final _tmp = new HMac(new SHA512Digest(), 128)..init(new KeyParameter(key)); 15 | return _tmp.process(data); 16 | } 17 | 18 | Uint8List hash256(Uint8List buffer) { 19 | Uint8List _tmp = new SHA256Digest().process(buffer); 20 | return new SHA256Digest().process(_tmp); 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/payments/p2pk.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'package:meta/meta.dart'; 3 | import 'package:bip32/src/utils/ecurve.dart' show isPoint; 4 | import 'package:bs58check/bs58check.dart' as bs58check; 5 | 6 | import '../crypto.dart'; 7 | import '../models/networks.dart'; 8 | import '../payments/index.dart' show PaymentData; 9 | import '../utils/script.dart' as bscript; 10 | import '../utils/constants/op.dart'; 11 | 12 | class P2PK { 13 | PaymentData data; 14 | NetworkType network; 15 | P2PK({@required data, network}) { 16 | this.network = network ?? bitcoin; 17 | this.data = data; 18 | _init(); 19 | } 20 | 21 | _init() { 22 | if (data.output != null) { 23 | if (data.output[data.output.length - 1] != OPS['OP_CHECKSIG']) 24 | throw new ArgumentError('Output is invalid'); 25 | if (!isPoint(data.output.sublist(1, -1))) 26 | throw new ArgumentError('Output pubkey is invalid'); 27 | } 28 | if (data.input != null) { 29 | // TODO 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2018 anicdh 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /example/bitcoin_flutter_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:bitcoin_flutter/bitcoin_flutter.dart'; 2 | import 'package:bip39/bip39.dart' as bip39; 3 | 4 | main() { 5 | var seed = bip39.mnemonicToSeed( 6 | 'praise you muffin lion enable neck grocery crumble super myself license ghost'); 7 | var hdWallet = new HDWallet.fromSeed(seed); 8 | print(hdWallet.address); 9 | // => 12eUJoaWBENQ3tNZE52ZQaHqr3v4tTX4os 10 | print(hdWallet.pubKey); 11 | // => 0360729fb3c4733e43bf91e5208b0d240f8d8de239cff3f2ebd616b94faa0007f4 12 | print(hdWallet.privKey); 13 | // => 01304181d699cd89db7de6337d597adf5f78dc1f0784c400e41a3bd829a5a226 14 | print(hdWallet.wif); 15 | // => KwG2BU1ERd3ndbFUrdpR7ymLZbsd7xZpPKxsgJzUf76A4q9CkBpY 16 | 17 | var wallet = 18 | Wallet.fromWIF('Kxr9tQED9H44gCmp6HAdmemAzU3n84H3dGkuWTKvE23JgHMW8gct'); 19 | print(wallet.address); 20 | // => 19AAjaTUbRjQCMuVczepkoPswiZRhjtg31 21 | print(wallet.pubKey); 22 | // => 03aea0dfd576151cb399347aa6732f8fdf027b9ea3ea2e65fb754803f776e0a509 23 | print(wallet.privKey); 24 | // => 3095cb26affefcaaa835ff968d60437c7c764da40cdd1a1b497406c7902a8ac9 25 | print(wallet.wif); 26 | // => Kxr9tQED9H44gCmp6HAdmemAzU3n84H3dGkuWTKvE23JgHMW8gct 27 | } 28 | -------------------------------------------------------------------------------- /test/bitcoin_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bitcoin_flutter/bitcoin_flutter.dart'; 2 | import 'package:test/test.dart'; 3 | import 'package:bip39/bip39.dart' as bip39; 4 | 5 | void main() { 6 | group('bitcoin-dart (HDWallet)', () { 7 | var seed = bip39.mnemonicToSeed( 8 | 'praise you muffin lion enable neck grocery crumble super myself license ghost'); 9 | HDWallet hdWallet = new HDWallet.fromSeed(seed); 10 | test('valid seed', () { 11 | expect(hdWallet.seed, 12 | 'f4f0cda65a9068e308fad4c96e8fe22213dd535fe7a7e91ca70c162a38a49aaacfe0dde5fafbbdf63cf783c2619db7174bc25cbfff574fb7037b1b9cec3d09b6'); 13 | }); 14 | test('valid address', () { 15 | expect(hdWallet.address, '12eUJoaWBENQ3tNZE52ZQaHqr3v4tTX4os'); 16 | }); 17 | test('valid public key', () { 18 | expect(hdWallet.base58, 19 | 'xpub661MyMwAqRbcGhVeaVfEBA25e3cP9DsJQZoE8iep5fZSxy3TnPBNBgWnMZx56oreNc48ZoTkQfatNJ9VWnQ7ZcLZcVStpaXLTeG8bGrzX3n'); 20 | }); 21 | test('valid private key', () { 22 | expect(hdWallet.base58Priv, 23 | 'xprv9s21ZrQH143K4DRBUU8Dp25M61mtjm9T3LsdLLFCXL2U6AiKEqs7dtCJWGFcDJ9DtHpdwwmoqLgzPrW7unpwUyL49FZvut9xUzpNB6wbEnz'); 24 | }); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.2 2 | - Add support for optional 'noStrict' parameter in Transaction.fromBuffer 3 | 4 | ## 2.0.1 5 | - Add payments/index.dart to lib exports 6 | 7 | ## 2.0.0 **Backwards Incompatibility** 8 | - Please update your sign function if you use this version. sign now [required parameter name](https://github.com/anicdh/bitcoin_flutter/blob/master/lib/src/transaction_builder.dart#L121) 9 | - Support building a Transaction with a SegWit P2WPKH input 10 | - Add Address.validateAddress to validate address 11 | 12 | ## 1.1.0 13 | 14 | - Add PaymentData, P2PKHData to be deprecated, will remove next version 15 | - Support p2wpkh 16 | 17 | ## 1.0.7 18 | 19 | - Try catch getter privKey, base58Priv, wif 20 | - Possible to create a neutered HD Wallet 21 | 22 | ## 1.0.6 23 | 24 | - Accept non-standard payment 25 | 26 | ## 1.0.5 27 | 28 | - Add ECPair to index 29 | 30 | ## 1.0.4 31 | 32 | - Add transaction to index 33 | 34 | ## 1.0.3 35 | 36 | - Fix bug testnet BIP32 37 | 38 | ## 1.0.2 39 | 40 | - Add sign and verify for HD Wallet and Wallet 41 | 42 | ## 1.0.1 43 | 44 | - Add derive and derive path for HD Wallet 45 | 46 | ## 1.0.0 47 | 48 | - Transaction implementation 49 | 50 | ## 0.1.1 51 | 52 | - HDWallet from Seed implementation 53 | - Wallet from WIF implementation 54 | -------------------------------------------------------------------------------- /lib/src/models/networks.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | class NetworkType { 4 | String messagePrefix; 5 | String bech32; 6 | Bip32Type bip32; 7 | int pubKeyHash; 8 | int scriptHash; 9 | int wif; 10 | 11 | NetworkType( 12 | {@required this.messagePrefix, 13 | this.bech32, 14 | @required this.bip32, 15 | @required this.pubKeyHash, 16 | @required this.scriptHash, 17 | @required this.wif}); 18 | 19 | @override 20 | String toString() { 21 | return 'NetworkType{messagePrefix: $messagePrefix, bech32: $bech32, bip32: ${bip32.toString()}, pubKeyHash: $pubKeyHash, scriptHash: $scriptHash, wif: $wif}'; 22 | } 23 | } 24 | 25 | class Bip32Type { 26 | int public; 27 | int private; 28 | 29 | Bip32Type({@required this.public, @required this.private}); 30 | 31 | @override 32 | String toString() { 33 | return 'Bip32Type{public: $public, private: $private}'; 34 | } 35 | } 36 | 37 | final bitcoin = new NetworkType( 38 | messagePrefix: '\x18Bitcoin Signed Message:\n', 39 | bech32: 'bc', 40 | bip32: new Bip32Type(public: 0x0488b21e, private: 0x0488ade4), 41 | pubKeyHash: 0x00, 42 | scriptHash: 0x05, 43 | wif: 0x80); 44 | 45 | final testnet = new NetworkType( 46 | messagePrefix: '\x18Bitcoin Signed Message:\n', 47 | bech32: 'tb', 48 | bip32: new Bip32Type(public: 0x043587cf, private: 0x04358394), 49 | pubKeyHash: 0x6f, 50 | scriptHash: 0xc4, 51 | wif: 0xef); 52 | -------------------------------------------------------------------------------- /lib/src/classify.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import '../src/utils/script.dart' as bscript; 3 | import 'templates/pubkeyhash.dart' as pubkeyhash; 4 | import 'templates/pubkey.dart' as pubkey; 5 | import 'templates/witnesspubkeyhash.dart' as witnessPubKeyHash; 6 | 7 | const SCRIPT_TYPES = { 8 | 'P2SM': 'multisig', 9 | 'NONSTANDARD': 'nonstandard', 10 | 'NULLDATA': 'nulldata', 11 | 'P2PK': 'pubkey', 12 | 'P2PKH': 'pubkeyhash', 13 | 'P2SH': 'scripthash', 14 | 'P2WPKH': 'witnesspubkeyhash', 15 | 'P2WSH': 'witnessscripthash', 16 | 'WITNESS_COMMITMENT': 'witnesscommitment' 17 | }; 18 | 19 | String classifyOutput(Uint8List script) { 20 | if (witnessPubKeyHash.outputCheck(script)) return SCRIPT_TYPES['P2WPKH']; 21 | if (pubkeyhash.outputCheck(script)) return SCRIPT_TYPES['P2PKH']; 22 | final chunks = bscript.decompile(script); 23 | if (chunks == null) throw new ArgumentError('Invalid script'); 24 | return SCRIPT_TYPES['NONSTANDARD']; 25 | } 26 | 27 | String classifyInput(Uint8List script) { 28 | final chunks = bscript.decompile(script); 29 | if (chunks == null) throw new ArgumentError('Invalid script'); 30 | if (pubkeyhash.inputCheck(chunks)) return SCRIPT_TYPES['P2PKH']; 31 | if (pubkey.inputCheck(chunks)) return SCRIPT_TYPES['P2PK']; 32 | return SCRIPT_TYPES['NONSTANDARD']; 33 | } 34 | 35 | String classifyWitness(List script) { 36 | final chunks = bscript.decompile(script); 37 | if (chunks == null) throw new ArgumentError('Invalid script'); 38 | if (witnessPubKeyHash.inputCheck(chunks)) return SCRIPT_TYPES['P2WPKH']; 39 | return SCRIPT_TYPES['NONSTANDARD']; 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/address.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'models/networks.dart'; 3 | import 'package:bs58check/bs58check.dart' as bs58check; 4 | import 'package:bech32/bech32.dart'; 5 | import 'payments/index.dart' show PaymentData; 6 | import 'payments/p2pkh.dart'; 7 | import 'payments/p2wpkh.dart'; 8 | 9 | class Address { 10 | static bool validateAddress(String address, [NetworkType nw]) { 11 | try { 12 | addressToOutputScript(address, nw); 13 | return true; 14 | } catch (err) { 15 | return false; 16 | } 17 | } 18 | 19 | static Uint8List addressToOutputScript(String address, [NetworkType nw]) { 20 | NetworkType network = nw ?? bitcoin; 21 | var decodeBase58; 22 | var decodeBech32; 23 | try { 24 | decodeBase58 = bs58check.decode(address); 25 | } catch (err) {} 26 | if (decodeBase58 != null) { 27 | if (decodeBase58[0] != network.pubKeyHash) 28 | throw new ArgumentError('Invalid version or Network mismatch'); 29 | P2PKH p2pkh = 30 | new P2PKH(data: new PaymentData(address: address), network: network); 31 | return p2pkh.data.output; 32 | } else { 33 | try { 34 | decodeBech32 = segwit.decode(address); 35 | } catch (err) {} 36 | if (decodeBech32 != null) { 37 | if (network.bech32 != decodeBech32.hrp) 38 | throw new ArgumentError('Invalid prefix or Network mismatch'); 39 | if (decodeBech32.version != 0) 40 | throw new ArgumentError('Invalid address version'); 41 | P2WPKH p2wpkh = new P2WPKH( 42 | data: new PaymentData(address: address), network: network); 43 | return p2wpkh.data.output; 44 | } 45 | } 46 | throw new ArgumentError(address + ' has no matching Script'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/address_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import '../lib/src/address.dart' show Address; 3 | import '../lib/src/models/networks.dart' as NETWORKS; 4 | 5 | main() { 6 | group('Address', () { 7 | group('validateAddress', () { 8 | test('base58 addresses and valid network', () { 9 | expect( 10 | Address.validateAddress( 11 | 'mhv6wtF2xzEqMNd3TbXx9TjLLo6mp2MUuT', NETWORKS.testnet), 12 | true); 13 | expect(Address.validateAddress('1K6kARGhcX9nJpJeirgcYdGAgUsXD59nHZ'), 14 | true); 15 | }); 16 | test('base58 addresses and invalid network', () { 17 | expect( 18 | Address.validateAddress( 19 | 'mhv6wtF2xzEqMNd3TbXx9TjLLo6mp2MUuT', NETWORKS.bitcoin), 20 | false); 21 | expect( 22 | Address.validateAddress( 23 | '1K6kARGhcX9nJpJeirgcYdGAgUsXD59nHZ', NETWORKS.testnet), 24 | false); 25 | }); 26 | test('bech32 addresses and valid network', () { 27 | expect( 28 | Address.validateAddress( 29 | 'tb1qgmp0h7lvexdxx9y05pmdukx09xcteu9sx2h4ya', NETWORKS.testnet), 30 | true); 31 | expect( 32 | Address.validateAddress( 33 | 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'), 34 | true); 35 | // expect(Address.validateAddress('tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy'), true); TODO 36 | }); 37 | test('bech32 addresses and invalid network', () { 38 | expect( 39 | Address.validateAddress( 40 | 'tb1qgmp0h7lvexdxx9y05pmdukx09xcteu9sx2h4ya'), 41 | false); 42 | expect( 43 | Address.validateAddress( 44 | 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4', NETWORKS.testnet), 45 | false); 46 | }); 47 | test('invalid addresses', () { 48 | expect(Address.validateAddress('3333333casca'), false); 49 | }); 50 | }); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/utils/push_data.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'constants/op.dart'; 3 | 4 | class DecodedPushData { 5 | int opcode; 6 | int number; 7 | int size; 8 | DecodedPushData({this.opcode, this.number, this.size}); 9 | } 10 | 11 | class EncodedPushData { 12 | int size; 13 | Uint8List buffer; 14 | 15 | EncodedPushData({this.size, this.buffer}); 16 | } 17 | 18 | EncodedPushData encode(Uint8List buffer, number, offset) { 19 | var size = encodingLength(number); 20 | // ~6 bit 21 | if (size == 1) { 22 | buffer.buffer.asByteData().setUint8(offset, number); 23 | 24 | // 8 bit 25 | } else if (size == 2) { 26 | buffer.buffer.asByteData().setUint8(offset, OPS['OP_PUSHDATA1']); 27 | buffer.buffer.asByteData().setUint8(offset + 1, number); 28 | 29 | // 16 bit 30 | } else if (size == 3) { 31 | buffer.buffer.asByteData().setUint8(offset, OPS['OP_PUSHDATA2']); 32 | buffer.buffer.asByteData().setUint16(offset + 1, number, Endian.little); 33 | 34 | // 32 bit 35 | } else { 36 | buffer.buffer.asByteData().setUint8(offset, OPS['OP_PUSHDATA4']); 37 | buffer.buffer.asByteData().setUint32(offset + 1, number, Endian.little); 38 | } 39 | 40 | return new EncodedPushData(size: size, buffer: buffer); 41 | } 42 | 43 | DecodedPushData decode(Uint8List bf, int offset) { 44 | ByteBuffer buffer = bf.buffer; 45 | int opcode = buffer.asByteData().getUint8(offset); 46 | int number, size; 47 | 48 | // ~6 bit 49 | if (opcode < OPS['OP_PUSHDATA1']) { 50 | number = opcode; 51 | size = 1; 52 | 53 | // 8 bit 54 | } else if (opcode == OPS['OP_PUSHDATA1']) { 55 | if (offset + 2 > buffer.lengthInBytes) return null; 56 | number = buffer.asByteData().getUint8(offset + 1); 57 | size = 2; 58 | 59 | // 16 bit 60 | } else if (opcode == OPS['OP_PUSHDATA2']) { 61 | if (offset + 3 > buffer.lengthInBytes) return null; 62 | number = buffer.asByteData().getUint16(offset + 1); 63 | size = 3; 64 | 65 | // 32 bit 66 | } else { 67 | if (offset + 5 > buffer.lengthInBytes) return null; 68 | if (opcode != OPS['OP_PUSHDATA4']) 69 | throw new ArgumentError('Unexpected opcode'); 70 | 71 | number = buffer.asByteData().getUint32(offset + 1); 72 | size = 5; 73 | } 74 | 75 | return DecodedPushData(opcode: opcode, number: number, size: size); 76 | } 77 | 78 | int encodingLength(i) { 79 | return i < OPS['OP_PUSHDATA1'] ? 1 : i <= 0xff ? 2 : i <= 0xffff ? 3 : 5; 80 | } 81 | -------------------------------------------------------------------------------- /lib/src/utils/varuint.dart: -------------------------------------------------------------------------------- 1 | import 'check_types.dart'; 2 | import 'dart:typed_data'; 3 | 4 | Uint8List encode(int number, [Uint8List buffer, int offset]) { 5 | if (!isUint(number, 53)) ; 6 | 7 | buffer = buffer ?? new Uint8List(encodingLength(number)); 8 | offset = offset ?? 0; 9 | ByteData bytes = buffer.buffer.asByteData(); 10 | // 8 bit 11 | if (number < 0xfd) { 12 | bytes.setUint8(offset, number); 13 | // 16 bit 14 | } else if (number <= 0xffff) { 15 | bytes.setUint8(offset, 0xfd); 16 | bytes.setUint16(offset + 1, number, Endian.little); 17 | 18 | // 32 bit 19 | } else if (number <= 0xffffffff) { 20 | bytes.setUint8(offset, 0xfe); 21 | bytes.setUint32(offset + 1, number, Endian.little); 22 | 23 | // 64 bit 24 | } else { 25 | bytes.setUint8(offset, 0xff); 26 | bytes.setUint32(offset + 1, number, Endian.little); 27 | bytes.setUint32(offset + 5, (number ~/ 0x100000000) | 0, Endian.little); 28 | } 29 | 30 | return buffer; 31 | } 32 | 33 | int decode(Uint8List buffer, [int offset]) { 34 | offset = offset ?? 0; 35 | ByteData bytes = buffer.buffer.asByteData(); 36 | final first = bytes.getUint8(offset); 37 | 38 | // 8 bit 39 | if (first < 0xfd) { 40 | return first; 41 | // 16 bit 42 | } else if (first == 0xfd) { 43 | return bytes.getUint16(offset + 1, Endian.little); 44 | 45 | // 32 bit 46 | } else if (first == 0xfe) { 47 | return bytes.getUint32(offset + 1, Endian.little); 48 | // 64 bit 49 | } else { 50 | var lo = bytes.getUint32(offset + 1, Endian.little); 51 | var hi = bytes.getUint32(offset + 5, Endian.little); 52 | var number = hi * 0x0100000000 + lo; 53 | if (!isUint(number, 53)) throw ArgumentError("Expected UInt53"); 54 | return number; 55 | } 56 | } 57 | 58 | int encodingLength(int number) { 59 | if (!isUint(number, 53)) throw ArgumentError("Expected UInt53"); 60 | return (number < 0xfd 61 | ? 1 62 | : number <= 0xffff ? 3 : number <= 0xffffffff ? 5 : 9); 63 | } 64 | 65 | int readUInt64LE(ByteData bytes, int offset) { 66 | final a = bytes.getUint32(offset, Endian.little); 67 | var b = bytes.getUint32(offset + 4, Endian.little); 68 | b *= 0x100000000; 69 | isUint(b + a, 64); 70 | return b + a; 71 | } 72 | 73 | int writeUInt64LE(ByteData bytes, int offset, int value) { 74 | isUint(value, 64); 75 | bytes.setInt32(offset, value & -1, Endian.little); 76 | bytes.setUint32(offset + 4, value ~/ 0x100000000, Endian.little); 77 | return offset + 8; 78 | } 79 | -------------------------------------------------------------------------------- /test/payments/p2pkh_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; 2 | import 'package:bitcoin_flutter/src/payments/p2pkh.dart'; 3 | import 'package:test/test.dart'; 4 | import 'package:bitcoin_flutter/src/utils/script.dart' as bscript; 5 | import 'dart:io'; 6 | import 'dart:convert'; 7 | import 'package:hex/hex.dart'; 8 | import 'dart:typed_data'; 9 | 10 | main() { 11 | final fixtures = json.decode(new File("./test/fixtures/p2pkh.json").readAsStringSync(encoding: utf8)); 12 | group('(valid case)', () { 13 | (fixtures["valid"] as List).forEach((f) { 14 | test(f['description'] + ' as expected', () { 15 | final arguments = _preformPaymentData(f['arguments']); 16 | final p2pkh = new P2PKH(data: arguments); 17 | if (arguments.address == null) { 18 | expect(p2pkh.data.address, f['expected']['address']); 19 | } 20 | if (arguments.hash == null) { 21 | expect(_toString(p2pkh.data.hash), f['expected']['hash']); 22 | } 23 | if (arguments.pubkey == null) { 24 | expect(_toString(p2pkh.data.pubkey), f['expected']['pubkey']); 25 | } 26 | if (arguments.input == null) { 27 | expect(_toString(p2pkh.data.input), f['expected']['input']); 28 | } 29 | if (arguments.output == null) { 30 | expect(_toString(p2pkh.data.output), f['expected']['output']); 31 | } 32 | if (arguments.signature == null) { 33 | expect(_toString(p2pkh.data.signature), f['expected']['signature']); 34 | } 35 | }); 36 | }); 37 | }); 38 | group('(invalid case)', () { 39 | (fixtures["invalid"] as List).forEach((f) { 40 | test('throws ' + f['exception'] + (f['description'] != null ? ('for ' + f['description']) : ''), () { 41 | final arguments = _preformPaymentData(f['arguments']); 42 | try { 43 | expect(new P2PKH(data: arguments), isArgumentError); 44 | } catch(err) { 45 | expect((err as ArgumentError).message, f['exception']); 46 | } 47 | 48 | }); 49 | }); 50 | }); 51 | } 52 | 53 | PaymentData _preformPaymentData(dynamic x) { 54 | final address = x['address']; 55 | final hash = x['hash'] != null ? HEX.decode(x['hash']) : null; 56 | final input = x['input'] != null ? bscript.fromASM(x['input']) : null; 57 | final output = x['output'] != null ? bscript.fromASM(x['output']) : x['outputHex'] != null ? HEX.decode(x['outputHex']) : null; 58 | final pubkey = x['pubkey'] != null ? HEX.decode(x['pubkey']) : null; 59 | final signature = x['signature'] != null ? HEX.decode(x['signature']) : null; 60 | return new PaymentData(address: address, hash: hash, input: input, output: output, pubkey: pubkey, signature: signature); 61 | } 62 | 63 | String _toString(dynamic x) { 64 | if (x == null) { 65 | return null; 66 | } 67 | if (x is Uint8List) { 68 | return HEX.encode(x); 69 | } 70 | if (x is List) { 71 | return bscript.toASM(x); 72 | } 73 | return ''; 74 | } 75 | -------------------------------------------------------------------------------- /lib/src/utils/constants/op.dart: -------------------------------------------------------------------------------- 1 | /// Script opcodes. 2 | const OPS = { 3 | // Push value ops. 4 | 'OP_0': 0x00, 5 | 'OP_FALSE': 0x00, 6 | 'OP_PUSHDATA1': 0x4c, 7 | 'OP_PUSHDATA2': 0x4d, 8 | 'OP_PUSHDATA4': 0x4e, 9 | 'OP_1NEGATE': 0x4f, 10 | 'OP_RESERVED': 0x50, 11 | 'OP_1': 0x51, 12 | 'OP_TRUE': 0x51, 13 | 'OP_2': 0x52, 14 | 'OP_3': 0x53, 15 | 'OP_4': 0x54, 16 | 'OP_5': 0x55, 17 | 'OP_6': 0x56, 18 | 'OP_7': 0x57, 19 | 'OP_8': 0x58, 20 | 'OP_9': 0x59, 21 | 'OP_10': 0x5a, 22 | 'OP_11': 0x5b, 23 | 'OP_12': 0x5c, 24 | 'OP_13': 0x5d, 25 | 'OP_14': 0x5e, 26 | 'OP_15': 0x5f, 27 | 'OP_16': 0x60, 28 | 29 | // Control ops. 30 | 'OP_NOP': 0x61, 31 | 'OP_VER': 0x62, 32 | 'OP_IF': 0x63, 33 | 'OP_NOTIF': 0x64, 34 | 'OP_VERIF': 0x65, 35 | 'OP_VERNOTIF': 0x66, 36 | 'OP_ELSE': 0x67, 37 | 'OP_ENDIF': 0x68, 38 | 'OP_VERIFY': 0x69, 39 | 'OP_RETURN': 0x6a, 40 | 41 | // Stack ops. 42 | 'OP_TOALTSTACK': 0x6b, 43 | 'OP_FROMALTSTACK': 0x6c, 44 | 'OP_2DROP': 0x6d, 45 | 'OP_2DUP': 0x6e, 46 | 'OP_3DUP': 0x6f, 47 | 'OP_2OVER': 0x70, 48 | 'OP_2ROT': 0x71, 49 | 'OP_2SWAP': 0x72, 50 | 'OP_IFDUP': 0x73, 51 | 'OP_DEPTH': 0x74, 52 | 'OP_DROP': 0x75, 53 | 'OP_DUP': 0x76, 54 | 'OP_NIP': 0x77, 55 | 'OP_OVER': 0x78, 56 | 'OP_PICK': 0x79, 57 | 'OP_ROLL': 0x7a, 58 | 'OP_ROT': 0x7b, 59 | 'OP_SWAP': 0x7c, 60 | 'OP_TUCK': 0x7d, 61 | 62 | // Splice ops. 63 | 'OP_CAT': 0x7e, 64 | 'OP_SUBSTR': 0x7f, 65 | 'OP_LEFT': 0x80, 66 | 'OP_RIGHT': 0x81, 67 | 'OP_SIZE': 0x82, 68 | 69 | // Bit logic ops. 70 | 'OP_INVERT': 0x83, 71 | 'OP_AND': 0x84, 72 | 'OP_OR': 0x85, 73 | 'OP_XOR': 0x86, 74 | 'OP_EQUAL': 0x87, 75 | 'OP_EQUALVERIFY': 0x88, 76 | 'OP_RESERVED1': 0x89, 77 | 'OP_RESERVED2': 0x8a, 78 | 79 | // Numeric ops. 80 | 'OP_1ADD': 0x8b, 81 | 'OP_1SUB': 0x8c, 82 | 'OP_2MUL': 0x8d, 83 | 'OP_2DIV': 0x8e, 84 | 'OP_NEGATE': 0x8f, 85 | 'OP_ABS': 0x90, 86 | 'OP_NOT': 0x91, 87 | 'OP_0NOTEQUAL': 0x92, 88 | 89 | 'OP_ADD': 0x93, 90 | 'OP_SUB': 0x94, 91 | 'OP_MUL': 0x95, 92 | 'OP_DIV': 0x96, 93 | 'OP_MOD': 0x97, 94 | 'OP_LSHIFT': 0x98, 95 | 'OP_RSHIFT': 0x99, 96 | 97 | 'OP_BOOLAND': 0x9a, 98 | 'OP_BOOLOR': 0x9b, 99 | 'OP_NUMEQUAL': 0x9c, 100 | 'OP_NUMEQUALVERIFY': 0x9d, 101 | 'OP_NUMNOTEQUAL': 0x9e, 102 | 'OP_LESSTHAN': 0x9f, 103 | 'OP_GREATERTHAN': 0xa0, 104 | 'OP_LESSTHANOREQUAL': 0xa1, 105 | 'OP_GREATERTHANOREQUAL': 0xa2, 106 | 'OP_MIN': 0xa3, 107 | 'OP_MAX': 0xa4, 108 | 109 | 'OP_WITHIN': 0xa5, 110 | 111 | // Crypto ops. 112 | 'OP_RIPEMD160': 0xa6, 113 | 'OP_SHA1': 0xa7, 114 | 'OP_SHA256': 0xa8, 115 | 'OP_HASH160': 0xa9, 116 | 'OP_HASH256': 0xaa, 117 | 'OP_CODESEPARATOR': 0xab, 118 | 'OP_CHECKSIG': 0xac, 119 | 'OP_CHECKSIGVERIFY': 0xad, 120 | 'OP_CHECKMULTISIG': 0xae, 121 | 'OP_CHECKMULTISIGVERIFY': 0xaf, 122 | 123 | // Expansion ops. 124 | 'OP_NOP1': 0xb0, 125 | 'OP_CHECKLOCKTIMEVERIFY': 0xb1, 126 | 'OP_NOP2': 0xb1, 127 | 'OP_CHECKSEQUENCEVERIFY': 0xb2, 128 | 'OP_NOP3': 0xb2, 129 | 'OP_NOP4': 0xb3, 130 | 'OP_NOP5': 0xb4, 131 | 'OP_NOP6': 0xb5, 132 | 'OP_NOP7': 0xb6, 133 | 'OP_NOP8': 0xb7, 134 | 'OP_NOP9': 0xb8, 135 | 'OP_NOP10': 0xb9, 136 | 137 | 'OP_INVALIDOPCODE': 0xff, 138 | }; 139 | -------------------------------------------------------------------------------- /test/payments/p2wpkh_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; 2 | import 'package:bitcoin_flutter/src/payments/p2wpkh.dart'; 3 | import 'package:test/test.dart'; 4 | import 'package:bitcoin_flutter/src/utils/script.dart' as bscript; 5 | import 'dart:io'; 6 | import 'dart:convert'; 7 | import 'package:hex/hex.dart'; 8 | import 'dart:typed_data'; 9 | 10 | main() { 11 | 12 | final fixtures = json.decode(new File("./test/fixtures/p2wpkh.json").readAsStringSync(encoding: utf8)); 13 | 14 | group('(valid case)', () { 15 | (fixtures["valid"] as List).forEach((f) { 16 | test(f['description'] + ' as expected', () { 17 | final arguments = _preformPaymentData(f['arguments']); 18 | final p2wpkh = new P2WPKH(data: arguments); 19 | if (arguments.address == null) { 20 | expect(p2wpkh.data.address, f['expected']['address']); 21 | } 22 | if (arguments.hash == null) { 23 | expect(_toString(p2wpkh.data.hash), f['expected']['hash']); 24 | } 25 | if (arguments.pubkey == null) { 26 | expect(_toString(p2wpkh.data.pubkey), f['expected']['pubkey']); 27 | } 28 | if (arguments.input == null) { 29 | expect(_toString(p2wpkh.data.input), f['expected']['input']); 30 | } 31 | if (arguments.output == null) { 32 | expect(_toString(p2wpkh.data.output), f['expected']['output']); 33 | } 34 | if (arguments.signature == null) { 35 | expect(_toString(p2wpkh.data.signature), f['expected']['signature']); 36 | } 37 | if (arguments.witness == null) { 38 | expect(_toString(p2wpkh.data.witness), f['expected']['witness']); 39 | } 40 | }); 41 | }); 42 | }); 43 | 44 | group('(invalid case)', () { 45 | (fixtures["invalid"] as List).forEach((f) { 46 | test('throws ' + f['exception'] + (f['description'] != null ? ('for ' + f['description']) : ''), () { 47 | final arguments = _preformPaymentData(f['arguments']); 48 | try { 49 | expect(new P2WPKH(data: arguments), isArgumentError); 50 | } catch(err) { 51 | expect((err as ArgumentError).message, f['exception']); 52 | } 53 | 54 | }); 55 | }); 56 | }); 57 | } 58 | 59 | PaymentData _preformPaymentData(dynamic x) { 60 | final address = x['address']; 61 | final hash = x['hash'] != null ? HEX.decode(x['hash']) : null; 62 | final input = x['input'] != null ? bscript.fromASM(x['input']) : null; 63 | final witness = x['witness'] != null ? (x['witness'] as List).map((e) => HEX.decode(e.toString()) as Uint8List).toList() : null; 64 | final output = x['output'] != null ? bscript.fromASM(x['output']) : x['outputHex'] != null ? HEX.decode(x['outputHex']) : null; 65 | final pubkey = x['pubkey'] != null ? HEX.decode(x['pubkey']) : null; 66 | final signature = x['signature'] != null ? HEX.decode(x['signature']) : null; 67 | return new PaymentData(address: address, hash: hash, input: input, output: output, pubkey: pubkey, signature: signature, witness: witness); 68 | } 69 | 70 | String _toString(dynamic x) { 71 | if (x == null) { 72 | return null; 73 | } 74 | if (x is Uint8List) { 75 | return HEX.encode(x); 76 | } 77 | if (x is List) { 78 | return bscript.toASM(x); 79 | } 80 | return ''; 81 | } 82 | -------------------------------------------------------------------------------- /lib/src/ecpair.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'dart:math'; 3 | import 'package:bip32/src/utils/ecurve.dart' as ecc; 4 | import 'package:bip32/src/utils/wif.dart' as wif; 5 | import 'models/networks.dart'; 6 | 7 | class ECPair { 8 | Uint8List _d; 9 | Uint8List _Q; 10 | NetworkType network; 11 | bool compressed; 12 | ECPair(Uint8List _d, Uint8List _Q, {network, compressed}) { 13 | this._d = _d; 14 | this._Q = _Q; 15 | this.network = network ?? bitcoin; 16 | this.compressed = compressed ?? true; 17 | } 18 | Uint8List get publicKey { 19 | if (_Q == null) _Q = ecc.pointFromScalar(_d, compressed); 20 | return _Q; 21 | } 22 | 23 | Uint8List get privateKey => _d; 24 | String toWIF() { 25 | if (privateKey == null) { 26 | throw new ArgumentError('Missing private key'); 27 | } 28 | return wif.encode(new wif.WIF( 29 | version: network.wif, privateKey: privateKey, compressed: compressed)); 30 | } 31 | 32 | Uint8List sign(Uint8List hash) { 33 | return ecc.sign(hash, privateKey); 34 | } 35 | 36 | bool verify(Uint8List hash, Uint8List signature) { 37 | return ecc.verify(hash, publicKey, signature); 38 | } 39 | 40 | factory ECPair.fromWIF(String w, {NetworkType network}) { 41 | wif.WIF decoded = wif.decode(w); 42 | final version = decoded.version; 43 | // TODO support multi networks 44 | NetworkType nw; 45 | if (network != null) { 46 | nw = network; 47 | if (nw.wif != version) throw new ArgumentError('Invalid network version'); 48 | } else { 49 | if (version == bitcoin.wif) { 50 | nw = bitcoin; 51 | } else if (version == testnet.wif) { 52 | nw = testnet; 53 | } else { 54 | throw new ArgumentError('Unknown network version'); 55 | } 56 | } 57 | return ECPair.fromPrivateKey(decoded.privateKey, 58 | compressed: decoded.compressed, network: nw); 59 | } 60 | factory ECPair.fromPublicKey(Uint8List publicKey, 61 | {NetworkType network, bool compressed}) { 62 | if (!ecc.isPoint(publicKey)) { 63 | throw new ArgumentError('Point is not on the curve'); 64 | } 65 | return new ECPair(null, publicKey, 66 | network: network, compressed: compressed); 67 | } 68 | factory ECPair.fromPrivateKey(Uint8List privateKey, 69 | {NetworkType network, bool compressed}) { 70 | if (privateKey.length != 32) 71 | throw new ArgumentError( 72 | 'Expected property privateKey of type Buffer(Length: 32)'); 73 | if (!ecc.isPrivate(privateKey)) 74 | throw new ArgumentError('Private key not in range [1, n)'); 75 | return new ECPair(privateKey, null, 76 | network: network, compressed: compressed); 77 | } 78 | factory ECPair.makeRandom( 79 | {NetworkType network, bool compressed, Function rng}) { 80 | final rfunc = rng ?? _randomBytes; 81 | Uint8List d; 82 | // int beginTime = DateTime.now().millisecondsSinceEpoch; 83 | do { 84 | d = rfunc(32); 85 | if (d.length != 32) throw ArgumentError('Expected Buffer(Length: 32)'); 86 | // if (DateTime.now().millisecondsSinceEpoch - beginTime > 5000) throw ArgumentError('Timeout'); 87 | } while (!ecc.isPrivate(d)); 88 | return ECPair.fromPrivateKey(d, network: network, compressed: compressed); 89 | } 90 | } 91 | 92 | const int _SIZE_BYTE = 255; 93 | Uint8List _randomBytes(int size) { 94 | final rng = Random.secure(); 95 | final bytes = Uint8List(size); 96 | for (var i = 0; i < size; i++) { 97 | bytes[i] = rng.nextInt(_SIZE_BYTE); 98 | } 99 | return bytes; 100 | } 101 | -------------------------------------------------------------------------------- /lib/src/payments/p2wpkh.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'package:meta/meta.dart'; 3 | import 'package:bip32/src/utils/ecurve.dart' show isPoint; 4 | import 'package:bech32/bech32.dart'; 5 | 6 | import '../crypto.dart'; 7 | import '../models/networks.dart'; 8 | import '../payments/index.dart' show PaymentData; 9 | import '../utils/script.dart' as bscript; 10 | import '../utils/constants/op.dart'; 11 | 12 | class P2WPKH { 13 | final EMPTY_SCRIPT = Uint8List.fromList([]); 14 | 15 | PaymentData data; 16 | NetworkType network; 17 | P2WPKH({@required data, network}) { 18 | this.network = network ?? bitcoin; 19 | this.data = data; 20 | _init(); 21 | } 22 | 23 | _init() { 24 | if (data.address == null && 25 | data.hash == null && 26 | data.output == null && 27 | data.pubkey == null && 28 | data.witness == null) throw new ArgumentError('Not enough data'); 29 | 30 | if (data.address != null) { 31 | _getDataFromAddress(data.address); 32 | } 33 | 34 | if (data.hash != null) { 35 | _getDataFromHash(); 36 | } 37 | 38 | if (data.output != null) { 39 | if (data.output.length != 22 || 40 | data.output[0] != OPS['OP_0'] || 41 | data.output[1] != 20) // 0x14 42 | throw new ArgumentError('Output is invalid'); 43 | if (data.hash == null) { 44 | data.hash = data.output.sublist(2); 45 | } 46 | _getDataFromHash(); 47 | } 48 | 49 | if (data.pubkey != null) { 50 | data.hash = hash160(data.pubkey); 51 | _getDataFromHash(); 52 | } 53 | 54 | if (data.witness != null) { 55 | if (data.witness.length != 2) 56 | throw new ArgumentError('Witness is invalid'); 57 | if (!bscript.isCanonicalScriptSignature(data.witness[0])) 58 | throw new ArgumentError('Witness has invalid signature'); 59 | if (!isPoint(data.witness[1])) 60 | throw new ArgumentError('Witness has invalid pubkey'); 61 | _getDataFromWitness(data.witness); 62 | } else if (data.pubkey != null && data.signature != null) { 63 | data.witness = [data.signature, data.pubkey]; 64 | if (data.input == null) data.input = EMPTY_SCRIPT; 65 | } 66 | } 67 | 68 | void _getDataFromWitness([List witness]) { 69 | if (data.input == null) { 70 | data.input = EMPTY_SCRIPT; 71 | } 72 | if (data.pubkey == null) { 73 | data.pubkey = witness[1]; 74 | if (data.hash == null) { 75 | data.hash = hash160(data.pubkey); 76 | } 77 | _getDataFromHash(); 78 | } 79 | if (data.signature == null) data.signature = witness[0]; 80 | } 81 | 82 | void _getDataFromHash() { 83 | if (data.address == null) { 84 | data.address = segwit.encode(Segwit(network.bech32, 0, data.hash)); 85 | } 86 | if (data.output == null) { 87 | data.output = bscript.compile([OPS['OP_0'], data.hash]); 88 | } 89 | } 90 | 91 | void _getDataFromAddress(String address) { 92 | try { 93 | Segwit _address = segwit.decode(address); 94 | if (network.bech32 != _address.hrp) 95 | throw new ArgumentError('Invalid prefix or Network mismatch'); 96 | if (_address.version != 0) // Only support version 0 now; 97 | throw new ArgumentError('Invalid address version'); 98 | data.hash = Uint8List.fromList(_address.program); 99 | } on InvalidHrp { 100 | throw new ArgumentError('Invalid prefix or Network mismatch'); 101 | } on InvalidProgramLength { 102 | throw new ArgumentError('Invalid address data'); 103 | } on InvalidWitnessVersion { 104 | throw new ArgumentError('Invalid witness address version'); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /test/integration/addresses_test.dart: -------------------------------------------------------------------------------- 1 | import '../../lib/src/models/networks.dart' as NETWORKS; 2 | import '../../lib/src/ecpair.dart' show ECPair; 3 | import '../../lib/src/payments/index.dart' show PaymentData; 4 | import '../../lib/src/payments/p2pkh.dart' show P2PKH, P2PKHData; 5 | import '../../lib/src/payments/p2wpkh.dart' show P2WPKH; 6 | import 'package:pointycastle/digests/sha256.dart'; 7 | import 'dart:convert'; 8 | import 'package:test/test.dart'; 9 | 10 | NETWORKS.NetworkType litecoin = new NETWORKS.NetworkType( 11 | messagePrefix: '\x19Litecoin Signed Message:\n', 12 | bip32: new NETWORKS.Bip32Type(public: 0x019da462, private: 0x019d9cfe), 13 | pubKeyHash: 0x30, 14 | scriptHash: 0x32, 15 | wif: 0xb0); 16 | // deterministic RNG for testing only 17 | rng(int number) { 18 | return utf8.encode('zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'); 19 | } 20 | 21 | main() { 22 | group('bitcoinjs-lib (addresses)', () { 23 | test('can generate a random address', () { 24 | final keyPair = ECPair.makeRandom(rng: rng); 25 | final address = 26 | new P2PKH(data: new PaymentData(pubkey: keyPair.publicKey)) 27 | .data 28 | .address; 29 | expect(address, '1F5VhMHukdnUES9kfXqzPzMeF1GPHKiF64'); 30 | }); 31 | test('can generate an address from a SHA256 hash', () { 32 | final hash = new SHA256Digest() 33 | .process(utf8.encode('correct horse battery staple')); 34 | final keyPair = ECPair.fromPrivateKey(hash); 35 | final address = 36 | new P2PKH(data: new PaymentData(pubkey: keyPair.publicKey)) 37 | .data 38 | .address; 39 | expect(address, '1C7zdTfnkzmr13HfA2vNm5SJYRK6nEKyq8'); 40 | }); 41 | test('can import an address via WIF', () { 42 | final keyPair = ECPair.fromWIF( 43 | 'Kxr9tQED9H44gCmp6HAdmemAzU3n84H3dGkuWTKvE23JgHMW8gct'); 44 | final address = 45 | new P2PKH(data: new PaymentData(pubkey: keyPair.publicKey)) 46 | .data 47 | .address; 48 | expect(address, '19AAjaTUbRjQCMuVczepkoPswiZRhjtg31'); 49 | }); 50 | test('can generate a Testnet address', () { 51 | final testnet = NETWORKS.testnet; 52 | final keyPair = ECPair.makeRandom(network: testnet, rng: rng); 53 | final wif = keyPair.toWIF(); 54 | final address = new P2PKH( 55 | data: new PaymentData(pubkey: keyPair.publicKey), 56 | network: testnet) 57 | .data 58 | .address; 59 | expect(address, 'mubSzQNtZfDj1YdNP6pNDuZy6zs6GDn61L'); 60 | expect(wif, 'cRgnQe9MUu1JznntrLaoQpB476M8PURvXVQB5R2eqms5tXnzNsrr'); 61 | }); 62 | test('can generate a Litecoin address', () { 63 | final keyPair = ECPair.makeRandom(network: litecoin, rng: rng); 64 | final wif = keyPair.toWIF(); 65 | final address = new P2PKH( 66 | data: new PaymentData(pubkey: keyPair.publicKey), 67 | network: litecoin) 68 | .data 69 | .address; 70 | expect(address, 'LZJSxZbjqJ2XVEquqfqHg1RQTDdfST5PTn'); 71 | expect(wif, 'T7A4PUSgTDHecBxW1ZiYFrDNRih2o7M8Gf9xpoCgudPF9gDiNvuS'); 72 | }); 73 | test('can generate a SegWit address', () { 74 | final keyPair = ECPair.fromWIF( 75 | 'KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn'); 76 | final address = 77 | new P2WPKH(data: new PaymentData(pubkey: keyPair.publicKey)) 78 | .data 79 | .address; 80 | expect(address, 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'); 81 | }); 82 | test('can generate a SegWit testnet address', () { 83 | final testnet = NETWORKS.testnet; 84 | final keyPair = ECPair.fromWIF( 85 | 'cPaJYBMDLjQp5gSUHnBfhX4Rgj95ekBS6oBttwQLw3qfsKKcDfuB'); 86 | final address = new P2WPKH( 87 | data: new PaymentData(pubkey: keyPair.publicKey), 88 | network: testnet) 89 | .data 90 | .address; 91 | expect(address, 'tb1qgmp0h7lvexdxx9y05pmdukx09xcteu9sx2h4ya'); 92 | }); 93 | }); 94 | } 95 | -------------------------------------------------------------------------------- /lib/src/payments/p2pkh.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'package:meta/meta.dart'; 3 | import 'package:bip32/src/utils/ecurve.dart' show isPoint; 4 | import 'package:bs58check/bs58check.dart' as bs58check; 5 | 6 | import '../crypto.dart'; 7 | import '../models/networks.dart'; 8 | import '../payments/index.dart' show PaymentData; 9 | import '../utils/script.dart' as bscript; 10 | import '../utils/constants/op.dart'; 11 | 12 | class P2PKH { 13 | PaymentData data; 14 | NetworkType network; 15 | P2PKH({@required data, network}) { 16 | this.network = network ?? bitcoin; 17 | this.data = data; 18 | _init(); 19 | } 20 | _init() { 21 | if (data.address != null) { 22 | _getDataFromAddress(data.address); 23 | _getDataFromHash(); 24 | } else if (data.hash != null) { 25 | _getDataFromHash(); 26 | } else if (data.output != null) { 27 | if (!isValidOutput(data.output)) 28 | throw new ArgumentError('Output is invalid'); 29 | data.hash = data.output.sublist(3, 23); 30 | _getDataFromHash(); 31 | } else if (data.pubkey != null) { 32 | data.hash = hash160(data.pubkey); 33 | _getDataFromHash(); 34 | _getDataFromChunk(); 35 | } else if (data.input != null) { 36 | List _chunks = bscript.decompile(data.input); 37 | _getDataFromChunk(_chunks); 38 | if (_chunks.length != 2) throw new ArgumentError('Input is invalid'); 39 | if (!bscript.isCanonicalScriptSignature(_chunks[0])) 40 | throw new ArgumentError('Input has invalid signature'); 41 | if (!isPoint(_chunks[1])) 42 | throw new ArgumentError('Input has invalid pubkey'); 43 | } else { 44 | throw new ArgumentError('Not enough data'); 45 | } 46 | } 47 | 48 | void _getDataFromChunk([List _chunks]) { 49 | if (data.pubkey == null && _chunks != null) { 50 | data.pubkey = (_chunks[1] is int) 51 | ? new Uint8List.fromList([_chunks[1]]) 52 | : _chunks[1]; 53 | data.hash = hash160(data.pubkey); 54 | _getDataFromHash(); 55 | } 56 | if (data.signature == null && _chunks != null) 57 | data.signature = (_chunks[0] is int) 58 | ? new Uint8List.fromList([_chunks[0]]) 59 | : _chunks[0]; 60 | if (data.input == null && data.pubkey != null && data.signature != null) { 61 | data.input = bscript.compile([data.signature, data.pubkey]); 62 | } 63 | } 64 | 65 | void _getDataFromHash() { 66 | if (data.address == null) { 67 | final payload = new Uint8List(21); 68 | payload.buffer.asByteData().setUint8(0, network.pubKeyHash); 69 | payload.setRange(1, payload.length, data.hash); 70 | data.address = bs58check.encode(payload); 71 | } 72 | if (data.output == null) { 73 | data.output = bscript.compile([ 74 | OPS['OP_DUP'], 75 | OPS['OP_HASH160'], 76 | data.hash, 77 | OPS['OP_EQUALVERIFY'], 78 | OPS['OP_CHECKSIG'] 79 | ]); 80 | } 81 | } 82 | 83 | void _getDataFromAddress(String address) { 84 | Uint8List payload = bs58check.decode(address); 85 | final version = payload.buffer.asByteData().getUint8(0); 86 | if (version != network.pubKeyHash) 87 | throw new ArgumentError('Invalid version or Network mismatch'); 88 | data.hash = payload.sublist(1); 89 | if (data.hash.length != 20) throw new ArgumentError('Invalid address'); 90 | } 91 | } 92 | 93 | isValidOutput(Uint8List data) { 94 | return data.length == 25 && 95 | data[0] == OPS['OP_DUP'] && 96 | data[1] == OPS['OP_HASH160'] && 97 | data[2] == 0x14 && 98 | data[23] == OPS['OP_EQUALVERIFY'] && 99 | data[24] == OPS['OP_CHECKSIG']; 100 | } 101 | 102 | // Backward compatibility 103 | @Deprecated( 104 | "The 'P2PKHData' class is deprecated. Use the 'PaymentData' package instead.") 105 | class P2PKHData extends PaymentData { 106 | P2PKHData({address, hash, output, pubkey, input, signature, witness}) 107 | : super( 108 | address: address, 109 | hash: hash, 110 | output: output, 111 | pubkey: pubkey, 112 | input: input, 113 | signature: signature, 114 | witness: witness); 115 | } 116 | -------------------------------------------------------------------------------- /test/integration/bip32_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bitcoin_flutter/src/models/networks.dart'; 2 | import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; 3 | import 'package:bitcoin_flutter/src/payments/p2pkh.dart'; 4 | import 'package:test/test.dart'; 5 | import 'package:hex/hex.dart'; 6 | import 'package:bip39/bip39.dart' as bip39; 7 | import 'package:bip32/bip32.dart' as bip32; 8 | 9 | void main() { 10 | group('bitcoin-dart (BIP32)', () { 11 | test('can import a BIP32 testnet xpriv and export to WIF', () { 12 | const xpriv = 13 | 'tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK'; 14 | final node = bip32.BIP32.fromBase58( 15 | xpriv, 16 | bip32.NetworkType( 17 | wif: testnet.wif, 18 | bip32: new bip32.Bip32Type( 19 | public: testnet.bip32.public, 20 | private: testnet.bip32.private))); 21 | expect( 22 | node.toWIF(), 'cQfoY67cetFNunmBUX5wJiw3VNoYx3gG9U9CAofKE6BfiV1fSRw7'); 23 | }); 24 | test('can export a BIP32 xpriv, then import it', () { 25 | const mnemonic = 26 | 'praise you muffin lion enable neck grocery crumble super myself license ghost'; 27 | final seed = bip39.mnemonicToSeed(mnemonic); 28 | final node = bip32.BIP32.fromSeed(seed); 29 | final string = node.toBase58(); 30 | final restored = bip32.BIP32.fromBase58(string); 31 | expect(getAddress(node), getAddress(restored)); // same public key 32 | expect(node.toWIF(), restored.toWIF()); // same private key 33 | }); 34 | test('can export a BIP32 xpub', () { 35 | const mnemonic = 36 | 'praise you muffin lion enable neck grocery crumble super myself license ghost'; 37 | final seed = bip39.mnemonicToSeed(mnemonic); 38 | final node = bip32.BIP32.fromSeed(seed); 39 | final string = node.neutered().toBase58(); 40 | expect(string, 41 | 'xpub661MyMwAqRbcGhVeaVfEBA25e3cP9DsJQZoE8iep5fZSxy3TnPBNBgWnMZx56oreNc48ZoTkQfatNJ9VWnQ7ZcLZcVStpaXLTeG8bGrzX3n'); 42 | }); 43 | test('can create a BIP32, bitcoin, account 0, external address', () { 44 | const path = "m/0'/0/0"; 45 | final root = bip32.BIP32.fromSeed(HEX.decode( 46 | 'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd')); 47 | final child1 = root.derivePath(path); 48 | // option 2, manually 49 | final child1b = root.deriveHardened(0).derive(0).derive(0); 50 | expect(getAddress(child1), '1JHyB1oPXufr4FXkfitsjgNB5yRY9jAaa7'); 51 | expect(getAddress(child1b), '1JHyB1oPXufr4FXkfitsjgNB5yRY9jAaa7'); 52 | }); 53 | test('can create a BIP44, bitcoin, account 0, external address', () { 54 | final root = bip32.BIP32.fromSeed(HEX.decode( 55 | 'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd')); 56 | final child1 = root.derivePath("m/44'/0'/0'/0/0"); 57 | // option 2, manually 58 | final child1b = root 59 | .deriveHardened(44) 60 | .deriveHardened(0) 61 | .deriveHardened(0) 62 | .derive(0) 63 | .derive(0); 64 | expect(getAddress(child1), '12Tyvr1U8A3ped6zwMEU5M8cx3G38sP5Au'); 65 | expect(getAddress(child1b), '12Tyvr1U8A3ped6zwMEU5M8cx3G38sP5Au'); 66 | }); 67 | /* TODO Support BIP49 68 | test('can create a BIP49, bitcoin testnet, account 0, external address', () { 69 | }); */ 70 | test('can use BIP39 to generate BIP32 addresses', () { 71 | final mnemonic = 72 | 'praise you muffin lion enable neck grocery crumble super myself license ghost'; 73 | assert(bip39.validateMnemonic(mnemonic)); 74 | final seed = bip39.mnemonicToSeed(mnemonic); 75 | final root = bip32.BIP32.fromSeed(seed); 76 | // receive addresses 77 | expect(getAddress(root.derivePath("m/0'/0/0")), 78 | '1AVQHbGuES57wD68AJi7Gcobc3RZrfYWTC'); 79 | expect(getAddress(root.derivePath("m/0'/0/1")), 80 | '1Ad6nsmqDzbQo5a822C9bkvAfrYv9mc1JL'); 81 | // change addresses 82 | expect(getAddress(root.derivePath("m/0'/1/0")), 83 | '1349KVc5NgedaK7DvuD4xDFxL86QN1Hvdn'); 84 | expect(getAddress(root.derivePath("m/0'/1/1")), 85 | '1EAvj4edpsWcSer3duybAd4KiR4bCJW5J6'); 86 | }); 87 | }); 88 | } 89 | 90 | String getAddress(node, [network]) { 91 | return P2PKH(data: new PaymentData(pubkey: node.publicKey), network: network) 92 | .data 93 | .address; 94 | } 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pub version 2 | 3 | # bitcoin_flutter 4 | 5 | A dart Bitcoin library for Flutter. 6 | 7 | Released under the terms of the [MIT LICENSE](LICENSE). 8 | 9 | Inspired by [bitcoinjs](https://github.com/bitcoinjs/bitcoinjs-lib) 10 | 11 | Otherwise, pull requests are appreciated. 12 | 13 | ## Installing 14 | 15 | [Flutter Packages](https://pub.dartlang.org/packages/bitcoin_flutter#-installing-tab-) 16 | 17 | ## Examples 18 | 19 | ```dart 20 | import 'package:bitcoin_flutter/bitcoin_flutter.dart'; 21 | import 'package:bip39/bip39.dart' as bip39; 22 | 23 | main() { 24 | var seed = bip39.mnemonicToSeed("praise you muffin lion enable neck grocery crumble super myself license ghost"); 25 | var hdWallet = new HDWallet.fromSeed(seed); 26 | print(hdWallet.address); 27 | // => 12eUJoaWBENQ3tNZE52ZQaHqr3v4tTX4os 28 | print(hdWallet.pubKey); 29 | // => 0360729fb3c4733e43bf91e5208b0d240f8d8de239cff3f2ebd616b94faa0007f4 30 | print(hdWallet.privKey); 31 | // => 01304181d699cd89db7de6337d597adf5f78dc1f0784c400e41a3bd829a5a226 32 | print(hdWallet.wif); 33 | // => KwG2BU1ERd3ndbFUrdpR7ymLZbsd7xZpPKxsgJzUf76A4q9CkBpY 34 | 35 | var wallet = Wallet.fromWIF("Kxr9tQED9H44gCmp6HAdmemAzU3n84H3dGkuWTKvE23JgHMW8gct"); 36 | print(wallet.address); 37 | // => 19AAjaTUbRjQCMuVczepkoPswiZRhjtg31 38 | print(wallet.pubKey); 39 | // => 03aea0dfd576151cb399347aa6732f8fdf027b9ea3ea2e65fb754803f776e0a509 40 | print(wallet.privKey); 41 | // => 3095cb26affefcaaa835ff968d60437c7c764da40cdd1a1b497406c7902a8ac9 42 | print(wallet.wif); 43 | // => Kxr9tQED9H44gCmp6HAdmemAzU3n84H3dGkuWTKvE23JgHMW8gct 44 | } 45 | ``` 46 | 47 | The below examples are implemented as integration tests: 48 | - [Generate a random address](https://github.com/anicdh/bitcoin-dart/blob/master/test/integration/addresses_test.dart#L21) 49 | - [Validating address](https://github.com/anicdh/bitcoin-dart/blob/master/test/address_test.dart) 50 | - [Generate an address from a SHA256 hash](https://github.com/anicdh/bitcoin-dart/blob/master/test/integration/addresses_test.dart#L26) 51 | - [Import an address via WIF](https://github.com/anicdh/bitcoin-dart/blob/master/test/integration/addresses_test.dart#L32) 52 | - [Generate a Testnet address](https://github.com/anicdh/bitcoin-dart/blob/master/test/integration/addresses_test.dart#L37) 53 | - [Generate a Litecoin address](https://github.com/anicdh/bitcoin-dart/blob/master/test/integration/addresses_test.dart#L45) 54 | - [Generate a native Segwit address](https://github.com/anicdh/bitcoin-dart/blob/master/test/integration/addresses_test.dart#L53) 55 | - [Create a 1-to-1 Transaction](https://github.com/anicdh/bitcoin-dart/blob/master/test/integration/transactions_test.dart#L10) 56 | - [Create a 2-to-2 Transaction](https://github.com/anicdh/bitcoin-dart/blob/master/test/integration/transactions_test.dart#L29) 57 | - [Create an OP_RETURN Transaction](https://github.com/anicdh/bitcoin-dart/blob/master/test/integration/transactions_test.dart#L61) 58 | - [Create a Transaction with a SegWit P2WPKH input](https://github.com/anicdh/bitcoin-dart/blob/master/test/integration/transactions_test.dart#L45) 59 | - [Import a BIP32 testnet xpriv and export to WIF](https://github.com/anicdh/bitcoin-dart/blob/master/test/integration/bip32_test.dart#L9) 60 | - [Export a BIP32 xpriv, then import it](https://github.com/anicdh/bitcoin-dart/blob/master/test/integration/bip32_test.dart#L14) 61 | - [Export a BIP32 xpub](https://github.com/anicdh/bitcoin-dart/blob/master/test/integration/bip32_test.dart#L23) 62 | - [Create a BIP32, bitcoin, account 0, external address](https://github.com/anicdh/bitcoin-dart/blob/master/test/integration/bip32_test.dart#L30) 63 | - [Create a BIP44, bitcoin, account 0, external address](https://github.com/anicdh/bitcoin-dart/blob/master/test/integration/bip32_test.dart#L41) 64 | - [Use BIP39 to generate BIP32 addresses](https://github.com/anicdh/bitcoin-dart/blob/master/test/integration/bip32_test.dart#L56) 65 | 66 | 67 | ### TODO 68 | - Generate a SegWit P2SH address 69 | - Generate a SegWit multisig address 70 | - Create a Transaction with a P2SH(multisig) input 71 | - Build a Transaction w/ psbt format 72 | - Add Tapscript / Taproot feature 73 | 74 | ### Running the test suite 75 | 76 | ``` bash 77 | pub run test 78 | ``` 79 | 80 | ## Complementing Libraries 81 | - [BIP39](https://github.com/anicdh/bip39) - Mnemonic generation for deterministic keys 82 | - [BIP32](https://github.com/anicdh/bip32) - BIP32 83 | - [Base58 Check](https://github.com/anicdh/bs58check-dart) - Base58 check encoding/decoding 84 | 85 | ## LICENSE [MIT](LICENSE) 86 | -------------------------------------------------------------------------------- /test/fixtures/ecpair.json: -------------------------------------------------------------------------------- 1 | { 2 | "valid": [ 3 | { 4 | "d": "0000000000000000000000000000000000000000000000000000000000000001", 5 | "Q": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", 6 | "compressed": true, 7 | "network": "bitcoin", 8 | "address": "1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH", 9 | "WIF": "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn" 10 | }, 11 | { 12 | "d": "0000000000000000000000000000000000000000000000000000000000000001", 13 | "Q": "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8", 14 | "compressed": false, 15 | "network": "bitcoin", 16 | "address": "1EHNa6Q4Jz2uvNExL497mE43ikXhwF6kZm", 17 | "WIF": "5HpHagT65TZzG1PH3CSu63k8DbpvD8s5ip4nEB3kEsreAnchuDf" 18 | }, 19 | { 20 | "d": "2bfe58ab6d9fd575bdc3a624e4825dd2b375d64ac033fbc46ea79dbab4f69a3e", 21 | "Q": "02b80011a883a0fd621ad46dfc405df1e74bf075cbaf700fd4aebef6e96f848340", 22 | "compressed": true, 23 | "network": "bitcoin", 24 | "address": "1MasfEKgSiaSeri2C6kgznaqBNtyrZPhNq", 25 | "WIF": "KxhEDBQyyEFymvfJD96q8stMbJMbZUb6D1PmXqBWZDU2WvbvVs9o" 26 | }, 27 | { 28 | "d": "6c4313b03f2e7324d75e642f0ab81b734b724e13fec930f309e222470236d66b", 29 | "Q": "024289801366bcee6172b771cf5a7f13aaecd237a0b9a1ff9d769cabc2e6b70a34", 30 | "compressed": true, 31 | "network": "bitcoin", 32 | "address": "1LwwMWdSEMHJ2dMhSvAHZ3g95tG2UBv9jg", 33 | "WIF": "KzrA86mCVMGWnLGBQu9yzQa32qbxb5dvSK4XhyjjGAWSBKYX4rHx" 34 | }, 35 | { 36 | "d": "6c4313b03f2e7324d75e642f0ab81b734b724e13fec930f309e222470236d66b", 37 | "Q": "044289801366bcee6172b771cf5a7f13aaecd237a0b9a1ff9d769cabc2e6b70a34cec320a0565fb7caf11b1ca2f445f9b7b012dda5718b3cface369ee3a034ded6", 38 | "compressed": false, 39 | "network": "bitcoin", 40 | "address": "1zXcfvKCLgsFdJDYPuqpu1sF3q92tnnUM", 41 | "WIF": "5JdxzLtFPHNe7CAL8EBC6krdFv9pwPoRo4e3syMZEQT9srmK8hh" 42 | }, 43 | { 44 | "d": "6c4313b03f2e7324d75e642f0ab81b734b724e13fec930f309e222470236d66b", 45 | "Q": "024289801366bcee6172b771cf5a7f13aaecd237a0b9a1ff9d769cabc2e6b70a34", 46 | "compressed": true, 47 | "network": "testnet", 48 | "address": "n1TteZiR3NiYojqKAV8fNxtTwsrjM7kVdj", 49 | "WIF": "cRD9b1m3vQxmwmjSoJy7Mj56f4uNFXjcWMCzpQCEmHASS4edEwXv" 50 | }, 51 | { 52 | "d": "6c4313b03f2e7324d75e642f0ab81b734b724e13fec930f309e222470236d66b", 53 | "Q": "044289801366bcee6172b771cf5a7f13aaecd237a0b9a1ff9d769cabc2e6b70a34cec320a0565fb7caf11b1ca2f445f9b7b012dda5718b3cface369ee3a034ded6", 54 | "compressed": false, 55 | "network": "testnet", 56 | "address": "mgWUuj1J1N882jmqFxtDepEC73Rr22E9GU", 57 | "WIF": "92Qba5hnyWSn5Ffcka56yMQauaWY6ZLd91Vzxbi4a9CCetaHtYj" 58 | }, 59 | { 60 | "d": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140", 61 | "Q": "0379be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", 62 | "compressed": true, 63 | "network": "bitcoin", 64 | "address": "1GrLCmVQXoyJXaPJQdqssNqwxvha1eUo2E", 65 | "WIF": "L5oLkpV3aqBjhki6LmvChTCV6odsp4SXM6FfU2Gppt5kFLaHLuZ9" 66 | } 67 | ], 68 | "invalid": { 69 | "fromPrivateKey": [ 70 | { 71 | "exception": "Private key not in range [1, n)", 72 | "d": "0000000000000000000000000000000000000000000000000000000000000000" 73 | }, 74 | { 75 | "exception": "Private key not in range [1, n)", 76 | "d": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141" 77 | }, 78 | { 79 | "exception": "Private key not in range [1, n)", 80 | "d": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364142" 81 | } 82 | ], 83 | "fromPublicKey": [ 84 | { 85 | "exception": "Point is not on the curve", 86 | "Q": "" 87 | }, 88 | { 89 | "description": "Bad X coordinate (== P)", 90 | "exception": "Point is not on the curve", 91 | "Q": "040000000000000000000000000000000000000000000000000000000000000001fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f" 92 | } 93 | ], 94 | "fromWIF": [ 95 | { 96 | "exception": "Invalid network version", 97 | "network": "bitcoin", 98 | "WIF": "92Qba5hnyWSn5Ffcka56yMQauaWY6ZLd91Vzxbi4a9CCetaHtYj" 99 | }, 100 | { 101 | "exception": "Unknown network version", 102 | "WIF": "brQnSed3Fia1w9VcbbS6ZGDgJ6ENkgwuQY2LS7pEC5bKHD1fMF" 103 | }, 104 | { 105 | "exception": "Invalid compression flag", 106 | "WIF": "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sfZr2ym" 107 | }, 108 | { 109 | "exception": "Invalid WIF length", 110 | "WIF": "3tq8Vmhh9SN5XhjTGSWgx8iKk59XbKG6UH4oqpejRuJhfYD" 111 | }, 112 | { 113 | "exception": "Invalid WIF length", 114 | "WIF": "38uMpGARR2BJy5p4dNFKYg9UsWNoBtkpbdrXDjmfvz8krCtw3T1W92ZDSR" 115 | } 116 | ] 117 | } 118 | } -------------------------------------------------------------------------------- /lib/src/bitcoin_flutter_base.dart: -------------------------------------------------------------------------------- 1 | // TODO: Put public facing types in this file. 2 | import 'dart:typed_data'; 3 | import 'package:bitcoin_flutter/src/utils/magic_hash.dart'; 4 | import 'package:hex/hex.dart'; 5 | import 'package:bip32/bip32.dart' as bip32; 6 | import 'models/networks.dart'; 7 | import 'payments/index.dart' show PaymentData; 8 | import 'payments/p2pkh.dart'; 9 | import 'ecpair.dart'; 10 | import 'package:meta/meta.dart'; 11 | import 'dart:convert'; 12 | 13 | /// Checks if you are awesome. Spoiler: you are. 14 | class HDWallet { 15 | bip32.BIP32 _bip32; 16 | P2PKH _p2pkh; 17 | String seed; 18 | NetworkType network; 19 | 20 | String get privKey { 21 | if (_bip32 == null) return null; 22 | try { 23 | return HEX.encode(_bip32.privateKey); 24 | } catch (_) { 25 | return null; 26 | } 27 | } 28 | 29 | String get pubKey => _bip32 != null ? HEX.encode(_bip32.publicKey) : null; 30 | 31 | String get base58Priv { 32 | if (_bip32 == null) return null; 33 | try { 34 | return _bip32.toBase58(); 35 | } catch (_) { 36 | return null; 37 | } 38 | } 39 | 40 | String get base58 => _bip32 != null ? _bip32.neutered().toBase58() : null; 41 | 42 | String get wif { 43 | if (_bip32 == null) return null; 44 | try { 45 | return _bip32.toWIF(); 46 | } catch (_) { 47 | return null; 48 | } 49 | } 50 | 51 | String get address => _p2pkh != null ? _p2pkh.data.address : null; 52 | 53 | HDWallet( 54 | {@required bip32, @required p2pkh, @required this.network, this.seed}) { 55 | this._bip32 = bip32; 56 | this._p2pkh = p2pkh; 57 | } 58 | 59 | HDWallet derivePath(String path) { 60 | final bip32 = _bip32.derivePath(path); 61 | final p2pkh = new P2PKH( 62 | data: new PaymentData(pubkey: bip32.publicKey), network: network); 63 | return HDWallet(bip32: bip32, p2pkh: p2pkh, network: network); 64 | } 65 | 66 | HDWallet derive(int index) { 67 | final bip32 = _bip32.derive(index); 68 | final p2pkh = new P2PKH( 69 | data: new PaymentData(pubkey: bip32.publicKey), network: network); 70 | return HDWallet(bip32: bip32, p2pkh: p2pkh, network: network); 71 | } 72 | 73 | factory HDWallet.fromSeed(Uint8List seed, {NetworkType network}) { 74 | network = network ?? bitcoin; 75 | final seedHex = HEX.encode(seed); 76 | final wallet = bip32.BIP32.fromSeed( 77 | seed, 78 | bip32.NetworkType( 79 | bip32: bip32.Bip32Type( 80 | public: network.bip32.public, private: network.bip32.private), 81 | wif: network.wif)); 82 | final p2pkh = new P2PKH( 83 | data: new PaymentData(pubkey: wallet.publicKey), network: network); 84 | return HDWallet( 85 | bip32: wallet, p2pkh: p2pkh, network: network, seed: seedHex); 86 | } 87 | 88 | factory HDWallet.fromBase58(String xpub, {NetworkType network}) { 89 | network = network ?? bitcoin; 90 | final wallet = bip32.BIP32.fromBase58( 91 | xpub, 92 | bip32.NetworkType( 93 | bip32: bip32.Bip32Type( 94 | public: network.bip32.public, private: network.bip32.private), 95 | wif: network.wif)); 96 | final p2pkh = new P2PKH( 97 | data: new PaymentData(pubkey: wallet.publicKey), network: network); 98 | return HDWallet(bip32: wallet, p2pkh: p2pkh, network: network, seed: null); 99 | } 100 | 101 | Uint8List sign(String message) { 102 | Uint8List messageHash = magicHash(message, network); 103 | return _bip32.sign(messageHash); 104 | } 105 | 106 | bool verify({String message, Uint8List signature}) { 107 | Uint8List messageHash = magicHash(message); 108 | return _bip32.verify(messageHash, signature); 109 | } 110 | } 111 | 112 | class Wallet { 113 | ECPair _keyPair; 114 | P2PKH _p2pkh; 115 | 116 | String get privKey => 117 | _keyPair != null ? HEX.encode(_keyPair.privateKey) : null; 118 | 119 | String get pubKey => _keyPair != null ? HEX.encode(_keyPair.publicKey) : null; 120 | 121 | String get wif => _keyPair != null ? _keyPair.toWIF() : null; 122 | 123 | String get address => _p2pkh != null ? _p2pkh.data.address : null; 124 | 125 | NetworkType network; 126 | 127 | Wallet(this._keyPair, this._p2pkh, this.network); 128 | 129 | factory Wallet.random([NetworkType network]) { 130 | final _keyPair = ECPair.makeRandom(network: network); 131 | final _p2pkh = new P2PKH( 132 | data: new PaymentData(pubkey: _keyPair.publicKey), network: network); 133 | return Wallet(_keyPair, _p2pkh, network); 134 | } 135 | 136 | factory Wallet.fromWIF(String wif, [NetworkType network]) { 137 | network = network ?? bitcoin; 138 | final _keyPair = ECPair.fromWIF(wif, network: network); 139 | final _p2pkh = new P2PKH( 140 | data: new PaymentData(pubkey: _keyPair.publicKey), network: network); 141 | return Wallet(_keyPair, _p2pkh, network); 142 | } 143 | 144 | Uint8List sign(String message) { 145 | Uint8List messageHash = magicHash(message, network); 146 | return _keyPair.sign(messageHash); 147 | } 148 | 149 | bool verify({String message, Uint8List signature}) { 150 | Uint8List messageHash = magicHash(message, network); 151 | return _keyPair.verify(messageHash, signature); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /test/ecpair_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:hex/hex.dart'; 3 | import 'dart:typed_data'; 4 | import 'dart:io'; 5 | import 'dart:convert'; 6 | import '../lib/src/ecpair.dart' show ECPair; 7 | import '../lib/src/models/networks.dart' as NETWORKS; 8 | 9 | final ONE = HEX 10 | .decode('0000000000000000000000000000000000000000000000000000000000000001'); 11 | 12 | main() { 13 | final fixtures = json.decode( 14 | new File('test/fixtures/ecpair.json').readAsStringSync(encoding: utf8)); 15 | group('ECPair', () { 16 | group('fromPrivateKey', () { 17 | test('defaults to compressed', () { 18 | final keyPair = ECPair.fromPrivateKey(ONE); 19 | expect(keyPair.compressed, true); 20 | }); 21 | test('supports the uncompressed option', () { 22 | final keyPair = ECPair.fromPrivateKey(ONE, compressed: false); 23 | expect(keyPair.compressed, false); 24 | }); 25 | test('supports the network option', () { 26 | final keyPair = ECPair.fromPrivateKey(ONE, 27 | network: NETWORKS.testnet, compressed: false); 28 | expect(keyPair.network, NETWORKS.testnet); 29 | }); 30 | (fixtures['valid'] as List).forEach((f) { 31 | test('derives public key for ${f['WIF']}', () { 32 | final d = HEX.decode(f['d']); 33 | final keyPair = ECPair.fromPrivateKey(d, compressed: f['compressed']); 34 | expect(HEX.encode(keyPair.publicKey), f['Q']); 35 | }); 36 | }); 37 | (fixtures['invalid']['fromPrivateKey'] as List).forEach((f) { 38 | test('throws ' + f['exception'], () { 39 | final d = HEX.decode(f['d']); 40 | try { 41 | expect(ECPair.fromPrivateKey(d), isArgumentError); 42 | } catch (err) { 43 | expect((err as ArgumentError).message, f['exception']); 44 | } 45 | }); 46 | }); 47 | }); 48 | group('fromPublicKey', () { 49 | (fixtures['invalid']['fromPublicKey'] as List).forEach((f) { 50 | test('throws ' + f['exception'], () { 51 | final Q = HEX.decode(f['Q']); 52 | try { 53 | expect(ECPair.fromPublicKey(Q), isArgumentError); 54 | } catch (err) { 55 | expect((err as ArgumentError).message, f['exception']); 56 | } 57 | }); 58 | }); 59 | }); 60 | group('fromWIF', () { 61 | (fixtures['valid'] as List).forEach((f) { 62 | test('imports ${f['WIF']}', () { 63 | final keyPair = ECPair.fromWIF(f['WIF']); 64 | var network = _getNetwork(f); 65 | expect(HEX.encode(keyPair.privateKey), f['d']); 66 | expect(keyPair.compressed, f['compressed']); 67 | expect(keyPair.network, network); 68 | }); 69 | }); 70 | (fixtures['invalid']['fromWIF'] as List).forEach((f) { 71 | test('throws ' + f['exception'], () { 72 | var network = _getNetwork(f); 73 | try { 74 | expect(ECPair.fromWIF(f['WIF'], network: network), isArgumentError); 75 | } catch (err) { 76 | expect((err as ArgumentError).message, f['exception']); 77 | } 78 | }); 79 | }); 80 | }); 81 | group('toWIF', () { 82 | (fixtures['valid'] as List).forEach((f) { 83 | test('export ${f['WIF']}', () { 84 | final keyPair = ECPair.fromWIF(f['WIF']); 85 | expect(keyPair.toWIF(), f['WIF']); 86 | }); 87 | }); 88 | }); 89 | group('makeRandom', () { 90 | final d = Uint8List.fromList(List.generate(32, (i) => 4)); 91 | final exWIF = 'KwMWvwRJeFqxYyhZgNwYuYjbQENDAPAudQx5VEmKJrUZcq6aL2pv'; 92 | test('allows a custom RNG to be used', () { 93 | final keyPair = ECPair.makeRandom(rng: (size) { 94 | return d.sublist(0, size); 95 | }); 96 | expect(keyPair.toWIF(), exWIF); 97 | }); 98 | test('retains the same defaults as ECPair constructor', () { 99 | final keyPair = ECPair.makeRandom(); 100 | expect(keyPair.compressed, true); 101 | expect(keyPair.network, NETWORKS.bitcoin); 102 | }); 103 | test('supports the options parameter', () { 104 | final keyPair = 105 | ECPair.makeRandom(compressed: false, network: NETWORKS.testnet); 106 | expect(keyPair.compressed, false); 107 | expect(keyPair.network, NETWORKS.testnet); 108 | }); 109 | test('throws if d is bad length', () { 110 | rng(int number) { 111 | return new Uint8List(28); 112 | } 113 | 114 | try { 115 | ECPair.makeRandom(rng: rng); 116 | } catch (err) { 117 | expect((err as ArgumentError).message, 'Expected Buffer(Length: 32)'); 118 | } 119 | }); 120 | }); 121 | group('.network', () { 122 | (fixtures['valid'] as List).forEach((f) { 123 | test('return ${f['network']} for ${f['WIF']}', () { 124 | NETWORKS.NetworkType network = _getNetwork(f); 125 | final keyPair = ECPair.fromWIF(f['WIF']); 126 | expect(keyPair.network, network); 127 | }); 128 | }); 129 | }); 130 | }); 131 | } 132 | 133 | NETWORKS.NetworkType _getNetwork(f) { 134 | var network; 135 | if (f['network'] != null) { 136 | if (f['network'] == 'bitcoin') { 137 | network = NETWORKS.bitcoin; 138 | } else if (f['network'] == 'testnet') { 139 | network = NETWORKS.testnet; 140 | } 141 | } 142 | return network; 143 | } 144 | -------------------------------------------------------------------------------- /test/fixtures/p2pkh.json: -------------------------------------------------------------------------------- 1 | { 2 | "valid": [ 3 | { 4 | "description": "output from address", 5 | "arguments": { 6 | "address": "134D6gYy8DsR5m4416BnmgASuMBqKvogQh" 7 | }, 8 | "expected": { 9 | "hash": "168b992bcfc44050310b3a94bd0771136d0b28d1", 10 | "output": "OP_DUP OP_HASH160 168b992bcfc44050310b3a94bd0771136d0b28d1 OP_EQUALVERIFY OP_CHECKSIG", 11 | "signature": null, 12 | "input": null 13 | } 14 | }, 15 | { 16 | "description": "output from hash", 17 | "arguments": { 18 | "hash": "168b992bcfc44050310b3a94bd0771136d0b28d1" 19 | }, 20 | "expected": { 21 | "address": "134D6gYy8DsR5m4416BnmgASuMBqKvogQh", 22 | "output": "OP_DUP OP_HASH160 168b992bcfc44050310b3a94bd0771136d0b28d1 OP_EQUALVERIFY OP_CHECKSIG", 23 | "signature": null, 24 | "input": null 25 | } 26 | }, 27 | { 28 | "description": "output from output", 29 | "arguments": { 30 | "output": "OP_DUP OP_HASH160 168b992bcfc44050310b3a94bd0771136d0b28d1 OP_EQUALVERIFY OP_CHECKSIG" 31 | }, 32 | "expected": { 33 | "address": "134D6gYy8DsR5m4416BnmgASuMBqKvogQh", 34 | "hash": "168b992bcfc44050310b3a94bd0771136d0b28d1", 35 | "signature": null, 36 | "input": null 37 | } 38 | }, 39 | { 40 | "description": "output from pubkey", 41 | "arguments": { 42 | "pubkey": "030000000000000000000000000000000000000000000000000000000000000001" 43 | }, 44 | "expected": { 45 | "address": "134D6gYy8DsR5m4416BnmgASuMBqKvogQh", 46 | "hash": "168b992bcfc44050310b3a94bd0771136d0b28d1", 47 | "output": "OP_DUP OP_HASH160 168b992bcfc44050310b3a94bd0771136d0b28d1 OP_EQUALVERIFY OP_CHECKSIG", 48 | "signature": null, 49 | "input": null 50 | } 51 | }, 52 | { 53 | "description": "input/output from pubkey/signature", 54 | "arguments": { 55 | "pubkey": "030000000000000000000000000000000000000000000000000000000000000001", 56 | "signature": "300602010002010001" 57 | }, 58 | "expected": { 59 | "address": "134D6gYy8DsR5m4416BnmgASuMBqKvogQh", 60 | "hash": "168b992bcfc44050310b3a94bd0771136d0b28d1", 61 | "output": "OP_DUP OP_HASH160 168b992bcfc44050310b3a94bd0771136d0b28d1 OP_EQUALVERIFY OP_CHECKSIG", 62 | "input": "300602010002010001 030000000000000000000000000000000000000000000000000000000000000001" 63 | } 64 | }, 65 | { 66 | "description": "input/output from input", 67 | "arguments": { 68 | "input": "300602010002010001 030000000000000000000000000000000000000000000000000000000000000001" 69 | }, 70 | "expected": { 71 | "address": "134D6gYy8DsR5m4416BnmgASuMBqKvogQh", 72 | "hash": "168b992bcfc44050310b3a94bd0771136d0b28d1", 73 | "output": "OP_DUP OP_HASH160 168b992bcfc44050310b3a94bd0771136d0b28d1 OP_EQUALVERIFY OP_CHECKSIG", 74 | "pubkey": "030000000000000000000000000000000000000000000000000000000000000001", 75 | "signature": "300602010002010001" 76 | } 77 | } 78 | ], 79 | "invalid": [ 80 | { 81 | "exception": "Not enough data", 82 | "arguments": {} 83 | }, 84 | { 85 | "exception": "Not enough data", 86 | "arguments": { 87 | "signature": "300602010002010001" 88 | } 89 | }, 90 | { 91 | "description": "Unexpected OP_RESERVED", 92 | "exception": "Output is invalid", 93 | "arguments": { 94 | "output": "OP_DUP OP_HASH160 168b992bcfc44050310b3a94bd0771136d0b28d1 OP_EQUALVERIFY OP_RESERVED" 95 | } 96 | }, 97 | { 98 | "description": "Unexpected OP_DUP", 99 | "exception": "Output is invalid", 100 | "options": {}, 101 | "arguments": { 102 | "output": "OP_DUP OP_DUP 168b992bcfc44050310b3a94bd0771136d0b28d137 OP_EQUALVERIFY" 103 | } 104 | }, 105 | { 106 | "description": "Hash too short (too many chunks)", 107 | "exception": "Output is invalid", 108 | "arguments": { 109 | "output": "OP_DUP OP_DUP 168b992bcfc44050310b3a94bd0771136d0b28d1 OP_TRUE OP_EQUALVERIFY" 110 | } 111 | }, 112 | { 113 | "description": "Non-minimally encoded (non BIP62 compliant)", 114 | "exception": "Output is invalid", 115 | "arguments": { 116 | "outputHex": "76a94c14aa4d7985c57e011a8b3dd8e0e5a73aaef41629c588ac" 117 | } 118 | }, 119 | { 120 | "exception": "Invalid version or Network mismatch", 121 | "arguments": { 122 | "address": "3LRW7jeCvQCRdPF8S3yUCfRAx4eqXFmdcr" 123 | } 124 | }, 125 | { 126 | "exception": "Invalid address", 127 | "arguments": { 128 | "address": "111111111111111111117K4nzc" 129 | } 130 | }, 131 | { 132 | "exception": "Invalid address", 133 | "arguments": { 134 | "address": "111111111111111111111111133izVn" 135 | } 136 | }, 137 | { 138 | "exception": "Input has invalid signature", 139 | "arguments": { 140 | "input": "ffffffffffffffffff 030000000000000000000000000000000000000000000000000000000000000001" 141 | } 142 | }, 143 | { 144 | "exception": "Input has invalid pubkey", 145 | "arguments": { 146 | "input": "300602010002010001 ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" 147 | } 148 | }, 149 | { 150 | "description": "Input has unexpected data", 151 | "exception": "Input is invalid", 152 | "arguments": { 153 | "input": "300602010002010001 030000000000000000000000000000000000000000000000000000000000000001 ffff" 154 | } 155 | } 156 | ] 157 | } -------------------------------------------------------------------------------- /test/fixtures/p2wpkh.json: -------------------------------------------------------------------------------- 1 | { 2 | "valid": [ 3 | { 4 | "description": "output from address", 5 | "arguments": { 6 | "address": "bc1qafk4yhqvj4wep57m62dgrmutldusqde8adh20d" 7 | }, 8 | "options": {}, 9 | "expected": { 10 | "name": "p2wpkh", 11 | "hash": "ea6d525c0c955d90d3dbd29a81ef8bfb79003727", 12 | "output": "OP_0 ea6d525c0c955d90d3dbd29a81ef8bfb79003727", 13 | "signature": null, 14 | "input": null, 15 | "witness": null 16 | } 17 | }, 18 | { 19 | "description": "output from hash", 20 | "arguments": { 21 | "hash": "ea6d525c0c955d90d3dbd29a81ef8bfb79003727" 22 | }, 23 | "expected": { 24 | "name": "p2wpkh", 25 | "address": "bc1qafk4yhqvj4wep57m62dgrmutldusqde8adh20d", 26 | "output": "OP_0 ea6d525c0c955d90d3dbd29a81ef8bfb79003727", 27 | "signature": null, 28 | "input": null, 29 | "witness": null 30 | } 31 | }, 32 | { 33 | "description": "output from output", 34 | "arguments": { 35 | "output": "OP_0 ea6d525c0c955d90d3dbd29a81ef8bfb79003727" 36 | }, 37 | "expected": { 38 | "name": "p2wpkh", 39 | "address": "bc1qafk4yhqvj4wep57m62dgrmutldusqde8adh20d", 40 | "hash": "ea6d525c0c955d90d3dbd29a81ef8bfb79003727", 41 | "signature": null, 42 | "input": null, 43 | "witness": null 44 | } 45 | }, 46 | { 47 | "description": "output from pubkey", 48 | "arguments": { 49 | "pubkey": "030000000000000000000000000000000000000000000000000000000000000001" 50 | }, 51 | "expected": { 52 | "name": "p2wpkh", 53 | "address": "bc1qz69ej270c3q9qvgt822t6pm3zdksk2x35j2jlm", 54 | "hash": "168b992bcfc44050310b3a94bd0771136d0b28d1", 55 | "output": "OP_0 168b992bcfc44050310b3a94bd0771136d0b28d1", 56 | "signature": null, 57 | "input": null, 58 | "witness": null 59 | } 60 | }, 61 | { 62 | "description": "witness/output from pubkey/signature", 63 | "arguments": { 64 | "pubkey": "030000000000000000000000000000000000000000000000000000000000000001", 65 | "signature": "300602010002010001" 66 | }, 67 | "expected": { 68 | "name": "p2wpkh", 69 | "address": "bc1qz69ej270c3q9qvgt822t6pm3zdksk2x35j2jlm", 70 | "hash": "168b992bcfc44050310b3a94bd0771136d0b28d1", 71 | "output": "OP_0 168b992bcfc44050310b3a94bd0771136d0b28d1", 72 | "input": "", 73 | "witness": ["300602010002010001", "030000000000000000000000000000000000000000000000000000000000000001"] 74 | } 75 | }, 76 | { 77 | "description": "witness/output from witness", 78 | "arguments": { 79 | "witness": ["300602010002010001", "030000000000000000000000000000000000000000000000000000000000000001"] 80 | }, 81 | "expected": { 82 | "name": "p2wpkh", 83 | "address": "bc1qz69ej270c3q9qvgt822t6pm3zdksk2x35j2jlm", 84 | "hash": "168b992bcfc44050310b3a94bd0771136d0b28d1", 85 | "output": "OP_0 168b992bcfc44050310b3a94bd0771136d0b28d1", 86 | "pubkey": "030000000000000000000000000000000000000000000000000000000000000001", 87 | "signature": "300602010002010001", 88 | "input": "" 89 | } 90 | } 91 | ], 92 | "invalid": [ 93 | { 94 | "exception": "Not enough data", 95 | "arguments": {} 96 | }, 97 | { 98 | "exception": "Not enough data", 99 | "arguments": { 100 | "signature": "300602010002010001" 101 | } 102 | }, 103 | { 104 | "exception": "Output is invalid", 105 | "description": "Unexpected OP", 106 | "arguments": { 107 | "output": "OP_RESERVED ea6d525c0c955d90d3dbd29a81ef8bfb79003727" 108 | } 109 | }, 110 | { 111 | "exception": "Invalid prefix or Network mismatch", 112 | "arguments": { 113 | "address": "foo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs30dvv" 114 | } 115 | }, 116 | { 117 | "exception": "Invalid address version", 118 | "arguments": { 119 | "address": "bc1pqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq5us4ke" 120 | } 121 | }, 122 | { 123 | "exception": "Invalid address data", 124 | "arguments": { 125 | "address": "bc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqmql8k8" 126 | } 127 | }, 128 | { 129 | "exception": "Witness is invalid", 130 | "arguments": { 131 | "witness": [] 132 | } 133 | }, 134 | { 135 | "exception": "Witness has invalid signature", 136 | "arguments": { 137 | "witness": ["ffffffffffffffffff", "030000000000000000000000000000000000000000000000000000000000000001"] 138 | } 139 | }, 140 | { 141 | "exception": "Witness has invalid pubkey", 142 | "arguments": { 143 | "witness": ["300602010002010001", "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"] 144 | } 145 | } 146 | ], 147 | "dynamic": { 148 | "depends": { 149 | "address": [ "address", "output", "hash", "pubkey", "witness" ], 150 | "hash": [ "address", "output", "hash", "pubkey", "witness" ], 151 | "output": [ "address", "output", "hash", "pubkey", "witness" ], 152 | "pubkey": [ "witness" ], 153 | "signature": [ "witness" ], 154 | "input": [ "witness" ], 155 | "witness": [ [ "pubkey", "signature" ] ] 156 | }, 157 | "details": [ 158 | { 159 | "description": "p2wpkh", 160 | "address": "bc1qz69ej270c3q9qvgt822t6pm3zdksk2x35j2jlm", 161 | "hash": "168b992bcfc44050310b3a94bd0771136d0b28d1", 162 | "output": "OP_0 168b992bcfc44050310b3a94bd0771136d0b28d1", 163 | "pubkey": "030000000000000000000000000000000000000000000000000000000000000001", 164 | "signature": "300602010002010001", 165 | "input": "", 166 | "witness": [ 167 | "300602010002010001", 168 | "030000000000000000000000000000000000000000000000000000000000000001" 169 | ] 170 | } 171 | ] 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /test/integration/transactions_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import '../../lib/src/ecpair.dart'; 3 | import '../../lib/src/transaction_builder.dart'; 4 | import '../../lib/src/models/networks.dart' as NETWORKS; 5 | import '../../lib/src/payments/p2wpkh.dart' show P2WPKH; 6 | import '../../lib/src/payments/index.dart' show PaymentData; 7 | 8 | main() { 9 | group('bitcoinjs-lib (transactions)', () { 10 | test('can create a 1-to-1 Transaction', () { 11 | final alice = ECPair.fromWIF( 12 | 'L1uyy5qTuGrVXrmrsvHWHgVzW9kKdrp27wBC7Vs6nZDTF2BRUVwy'); 13 | final txb = new TransactionBuilder(); 14 | 15 | txb.setVersion(1); 16 | txb.addInput( 17 | '61d520ccb74288c96bc1a2b20ea1c0d5a704776dd0164a396efec3ea7040349d', 18 | 0); // Alice's previous transaction output, has 15000 satoshis 19 | txb.addOutput('1cMh228HTCiwS8ZsaakH8A8wze1JR5ZsP', 12000); 20 | // (in)15000 - (out)12000 = (fee)3000, this is the miner fee 21 | 22 | txb.sign(vin: 0, keyPair: alice); 23 | 24 | // prepare for broadcast to the Bitcoin network, see 'can broadcast a Transaction' below 25 | expect(txb.build().toHex(), 26 | '01000000019d344070eac3fe6e394a16d06d7704a7d5c0a10eb2a2c16bc98842b7cc20d561000000006b48304502210088828c0bdfcdca68d8ae0caeb6ec62cd3fd5f9b2191848edae33feb533df35d302202e0beadd35e17e7f83a733f5277028a9b453d525553e3f5d2d7a7aa8010a81d60121029f50f51d63b345039a290c94bffd3180c99ed659ff6ea6b1242bca47eb93b59fffffffff01e02e0000000000001976a91406afd46bcdfd22ef94ac122aa11f241244a37ecc88ac00000000'); 27 | }); 28 | 29 | test('can create a 2-to-2 Transaction', () { 30 | final alice = ECPair.fromWIF( 31 | 'L1Knwj9W3qK3qMKdTvmg3VfzUs3ij2LETTFhxza9LfD5dngnoLG1'); 32 | final bob = ECPair.fromWIF( 33 | 'KwcN2pT3wnRAurhy7qMczzbkpY5nXMW2ubh696UBc1bcwctTx26z'); 34 | 35 | final txb = new TransactionBuilder(); 36 | txb.setVersion(1); 37 | txb.addInput( 38 | 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c', 39 | 6); // Alice's previous transaction output, has 200000 satoshis 40 | txb.addInput( 41 | '7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730', 42 | 0); // Bob's previous transaction output, has 300000 satoshis 43 | txb.addOutput('1CUNEBjYrCn2y1SdiUMohaKUi4wpP326Lb', 180000); 44 | txb.addOutput('1JtK9CQw1syfWj1WtFMWomrYdV3W2tWBF9', 170000); 45 | // (in)(200000 + 300000) - (out)(180000 + 170000) = (fee)150000, this is the miner fee 46 | 47 | txb.sign( 48 | vin: 1, 49 | keyPair: 50 | bob); // Bob signs his input, which was the second input (1th) 51 | txb.sign( 52 | vin: 0, 53 | keyPair: 54 | alice); // Alice signs her input, which was the first input (0th) 55 | 56 | // prepare for broadcast to the Bitcoin network, see 'can broadcast a Transaction' below 57 | expect(txb.build().toHex(), 58 | '01000000024c94e48a870b85f41228d33cf25213dfcc8dd796e7211ed6b1f9a014809dbbb5060000006a473044022041450c258ce7cac7da97316bf2ea1ce66d88967c4df94f3e91f4c2a30f5d08cb02203674d516e6bb2b0afd084c3551614bd9cec3c2945231245e891b145f2d6951f0012103e05ce435e462ec503143305feb6c00e06a3ad52fbf939e85c65f3a765bb7baacffffffff3077d9de049574c3af9bc9c09a7c9db80f2d94caaf63988c9166249b955e867d000000006b483045022100aeb5f1332c79c446d3f906e4499b2e678500580a3f90329edf1ba502eec9402e022072c8b863f8c8d6c26f4c691ac9a6610aa4200edc697306648ee844cfbc089d7a012103df7940ee7cddd2f97763f67e1fb13488da3fbdd7f9c68ec5ef0864074745a289ffffffff0220bf0200000000001976a9147dd65592d0ab2fe0d0257d571abf032cd9db93dc88ac10980200000000001976a914c42e7ef92fdb603af844d064faad95db9bcdfd3d88ac00000000'); 59 | }); 60 | 61 | test('can create an OP_RETURN Transaction', () { 62 | final alice = ECPair.fromWIF( 63 | 'L1uyy5qTuGrVXrmrsvHWHgVzW9kKdrp27wBC7Vs6nZDTF2BRUVwy'); 64 | final txb = new TransactionBuilder(); 65 | 66 | txb.setVersion(1); 67 | txb.addInput( 68 | '61d520ccb74288c96bc1a2b20ea1c0d5a704776dd0164a396efec3ea7040349d', 69 | 0); // Alice's previous transaction output, has 15000 satoshis 70 | txb.addOutputData('Hey this is a random string without Bitcoins'); 71 | 72 | txb.sign(vin: 0, keyPair: alice); 73 | 74 | // prepare for broadcast to the Bitcoin network, see 'can broadcast a Transaction' below 75 | expect(txb.build().toHex(), 76 | '01000000019d344070eac3fe6e394a16d06d7704a7d5c0a10eb2a2c16bc98842b7cc20d561000000006a47304402200852194e22d2b5faf9db66407d769b13278708b77e55df5d9c8638367af4c0870220638083bdaf06d8147ad4bfaddb975a2a4a056cca806a717e956d334c01482b3a0121029f50f51d63b345039a290c94bffd3180c99ed659ff6ea6b1242bca47eb93b59fffffffff0100000000000000002e6a2c486579207468697320697320612072616e646f6d20737472696e6720776974686f757420426974636f696e7300000000'); 77 | }); 78 | 79 | test('can create (and broadcast via 3PBP) a Transaction, w/ a P2WPKH input', 80 | () { 81 | final alice = ECPair.fromWIF( 82 | 'cUNfunNKXNNJDvUvsjxz5tznMR6ob1g5K6oa4WGbegoQD3eqf4am', 83 | network: NETWORKS.testnet); 84 | final p2wpkh = new P2WPKH( 85 | data: new PaymentData(pubkey: alice.publicKey), 86 | network: NETWORKS.testnet) 87 | .data; 88 | final txb = new TransactionBuilder(network: NETWORKS.testnet); 89 | txb.setVersion(1); 90 | txb.addInput( 91 | '53676626f5042d42e15313492ab7e708b87559dc0a8c74b7140057af51a2ed5b', 92 | 0, 93 | null, 94 | p2wpkh 95 | .output); // Alice's previous transaction output, has 200000 satoshis 96 | txb.addOutput('tb1qchsmnkk5c8wsjg8vxecmsntynpmkxme0yvh2yt', 1000000); 97 | txb.addOutput('tb1qn40fftdp6z2lvzmsz4s0gyks3gq86y2e8svgap', 8995000); 98 | 99 | txb.sign(vin: 0, keyPair: alice, witnessValue: 10000000); 100 | // // prepare for broadcast to the Bitcoin network, see 'can broadcast a Transaction' below 101 | expect(txb.build().toHex(), 102 | '010000000001015beda251af570014b7748c0adc5975b808e7b72a491353e1422d04f5266667530000000000ffffffff0240420f0000000000160014c5e1b9dad4c1dd0920ec3671b84d649877636f2fb8408900000000001600149d5e94ada1d095f60b701560f412d08a007d11590247304402203c4670ff81d352924af311552e0379861268bebb2222eeb0e66b3cdd1d4345b60220585b57982d958208cdd52f4ead4ecb86cfa9ff7740c2f6933e77135f1cc4c58f012102f9f43a191c6031a5ffae27c5f9911218e78857923284ac1154abc2cc008544b200000000'); 103 | }); 104 | }); 105 | } 106 | -------------------------------------------------------------------------------- /test/transaction_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'dart:io'; 3 | import 'dart:convert'; 4 | import 'package:hex/hex.dart'; 5 | import 'dart:typed_data'; 6 | import '../lib/src/utils/script.dart' as bscript; 7 | import '../lib/src/transaction.dart'; 8 | 9 | main() { 10 | final fixtures = json.decode(new File('test/fixtures/transaction.json') 11 | .readAsStringSync(encoding: utf8)); 12 | final valids = (fixtures['valid'] as List); 13 | 14 | group('Transaction', () { 15 | group('fromBuffer/fromHex', () { 16 | valids.forEach(importExport); 17 | (fixtures['hashForSignature'] as List).forEach(importExport); 18 | (fixtures['invalid']['fromBuffer'] as List).forEach((f) { 19 | test('throws on ${f['exception']}', () { 20 | try { 21 | expect(Transaction.fromHex(f['hex']), isArgumentError); 22 | } catch (err) { 23 | expect((err as ArgumentError).message, f['exception']); 24 | } 25 | }); 26 | }); 27 | 28 | test('.version should be interpreted as an int32le', () { 29 | final txHex = 'ffffffff0000ffffffff'; 30 | final tx = Transaction.fromHex(txHex); 31 | expect(-1, tx.version); 32 | }); 33 | }); 34 | 35 | group('toBuffer/toHex', () { 36 | valids.forEach((f) { 37 | test('exports ${f['description']} (${f['id']})', () { 38 | Transaction actual = fromRaw(f['raw'], false); 39 | expect(actual.toHex(), f['hex']); 40 | }); 41 | if (f['whex'] != null && f['whex'] != '') { 42 | test('exports ${f['description']} (${f['id']}) as witness', () { 43 | Transaction actual = fromRaw(f['raw'], true); 44 | expect(actual.toHex(), f['whex']); 45 | }); 46 | } 47 | }); 48 | }); 49 | 50 | group('weight/virtualSize', () { 51 | test('computes virtual size', () { 52 | valids.forEach((f) { 53 | final txHex = 54 | (f['whex'] != null && f['whex'] != '') ? f['whex'] : f['hex']; 55 | final transaction = Transaction.fromHex(txHex); 56 | expect(transaction.virtualSize(), f['virtualSize']); 57 | }); 58 | }); 59 | }); 60 | 61 | group('addInput', () { 62 | var prevTxHash; 63 | setUp(() { 64 | prevTxHash = HEX.decode( 65 | 'ffffffff00ffff000000000000000000000000000000000000000000101010ff'); 66 | }); 67 | test('returns an index', () { 68 | final tx = new Transaction(); 69 | expect(tx.addInput(prevTxHash, 0), 0); 70 | expect(tx.addInput(prevTxHash, 0), 1); 71 | }); 72 | test('defaults to empty script, and 0xffffffff SEQUENCE number', () { 73 | final tx = new Transaction(); 74 | tx.addInput(prevTxHash, 0); 75 | expect(tx.ins[0].script.length, 0); 76 | expect(tx.ins[0].sequence, 0xffffffff); 77 | }); 78 | (fixtures['invalid']['addInput'] as List).forEach((f) { 79 | test('throws on ' + f['exception'], () { 80 | final tx = new Transaction(); 81 | final hash = HEX.decode(f['hash']); 82 | try { 83 | expect(tx.addInput(hash, f['index']), isArgumentError); 84 | } catch (err) { 85 | expect((err as ArgumentError).message, f['exception']); 86 | } 87 | }); 88 | }); 89 | }); 90 | 91 | test('addOutput returns an index', () { 92 | final tx = new Transaction(); 93 | expect(tx.addOutput(new Uint8List(0), 0), 0); 94 | expect(tx.addOutput(new Uint8List(0), 0), 1); 95 | }); 96 | 97 | group('getHash/getId', () { 98 | verify(f) { 99 | test('should return the id for ${f['id']} (${f['description']})', () { 100 | final txHex = 101 | (f['whex'] != null && f['whex'] != '') ? f['whex'] : f['hex']; 102 | final tx = Transaction.fromHex(txHex); 103 | expect(HEX.encode(tx.getHash()), f['hash']); 104 | expect(tx.getId(), f['id']); 105 | }); 106 | } 107 | 108 | valids.forEach(verify); 109 | }); 110 | 111 | group('isCoinbase', () { 112 | verify(f) { 113 | test( 114 | 'should return ${f['coinbase']} for ${f['id']} (${f['description']})', 115 | () { 116 | final tx = Transaction.fromHex(f['hex']); 117 | expect(tx.isCoinbase(), f['coinbase']); 118 | }); 119 | } 120 | 121 | valids.forEach(verify); 122 | }); 123 | 124 | group('hashForSignature', () { 125 | (fixtures['hashForSignature'] as List).forEach((f) { 126 | test( 127 | 'should return ${f['hash']} for ${f['description'] != null ? 'case "' + f['description'] + '"' : f['script']}', 128 | () { 129 | final tx = Transaction.fromHex(f['txHex']); 130 | final script = bscript.fromASM(f['script']); 131 | expect( 132 | HEX.encode(tx.hashForSignature(f['inIndex'], script, f['type'])), 133 | f['hash']); 134 | }); 135 | }); 136 | }); 137 | }); 138 | } 139 | 140 | importExport(dynamic f) { 141 | final id = f['id'] ?? f['hash']; 142 | final txHex = f['hex'] ?? f['txHex']; 143 | test('imports ${f['description']} ($id)', () { 144 | final actual = Transaction.fromHex(txHex); 145 | expect(actual.toHex(), txHex); 146 | }); 147 | } 148 | 149 | Transaction fromRaw(raw, [isWitness]) { 150 | final tx = new Transaction(); 151 | tx.version = raw['version']; 152 | tx.locktime = raw['locktime']; 153 | 154 | (raw['ins'] as List).asMap().forEach((indx, txIn) { 155 | final txHash = HEX.decode(txIn['hash']); 156 | var scriptSig; 157 | 158 | if (txIn['data'] != null) { 159 | scriptSig = HEX.decode(txIn['data']); 160 | } else if (txIn['script'] != null && txIn['script'] != '') { 161 | scriptSig = bscript.fromASM(txIn['script']); 162 | } 163 | tx.addInput(txHash, txIn['index'], txIn['sequence'], scriptSig); 164 | 165 | if (isWitness) { 166 | var witness = (txIn['witness'] as List) 167 | .map((e) => HEX.decode(e.toString()) as Uint8List) 168 | .toList(); 169 | tx.setWitness(indx, witness); 170 | } 171 | }); 172 | 173 | (raw['outs'] as List).forEach((txOut) { 174 | var script; 175 | if (txOut['data'] != null) { 176 | script = HEX.decode(txOut['data']); 177 | } else if (txOut['script'] != null) { 178 | script = bscript.fromASM(txOut['script']); 179 | } 180 | tx.addOutput(script, txOut['value']); 181 | }); 182 | 183 | return tx; 184 | } 185 | -------------------------------------------------------------------------------- /lib/src/utils/script.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'package:hex/hex.dart'; 3 | import 'package:bip32/src/utils/ecurve.dart' as ecc; 4 | import 'constants/op.dart'; 5 | import 'push_data.dart' as pushData; 6 | import 'check_types.dart'; 7 | 8 | Map REVERSE_OPS = 9 | OPS.map((String string, int number) => new MapEntry(number, string)); 10 | final OP_INT_BASE = OPS['OP_RESERVED']; 11 | final ZERO = Uint8List.fromList([0]); 12 | 13 | Uint8List compile(List chunks) { 14 | final bufferSize = chunks.fold(0, (acc, chunk) { 15 | if (chunk is int) return acc + 1; 16 | if (chunk.length == 1 && asMinimalOP(chunk) != null) { 17 | return acc + 1; 18 | } 19 | return acc + pushData.encodingLength(chunk.length) + chunk.length; 20 | }); 21 | var buffer = new Uint8List(bufferSize); 22 | 23 | var offset = 0; 24 | chunks.forEach((chunk) { 25 | // data chunk 26 | if (chunk is Uint8List) { 27 | // adhere to BIP62.3, minimal push policy 28 | final opcode = asMinimalOP(chunk); 29 | if (opcode != null) { 30 | buffer.buffer.asByteData().setUint8(offset, opcode); 31 | offset += 1; 32 | return null; 33 | } 34 | pushData.EncodedPushData epd = 35 | pushData.encode(buffer, chunk.length, offset); 36 | offset += epd.size; 37 | buffer = epd.buffer; 38 | buffer.setRange(offset, offset + chunk.length, chunk); 39 | offset += chunk.length; 40 | // opcode 41 | } else { 42 | buffer.buffer.asByteData().setUint8(offset, chunk); 43 | offset += 1; 44 | } 45 | }); 46 | 47 | if (offset != buffer.length) 48 | throw new ArgumentError("Could not decode chunks"); 49 | return buffer; 50 | } 51 | 52 | List decompile(dynamic buffer) { 53 | List chunks = []; 54 | 55 | if (buffer == null) return chunks; 56 | if (buffer is List && buffer.length == 2) return buffer; 57 | 58 | var i = 0; 59 | while (i < buffer.length) { 60 | final opcode = buffer[i]; 61 | 62 | // data chunk 63 | if ((opcode > OPS['OP_0']) && (opcode <= OPS['OP_PUSHDATA4'])) { 64 | final d = pushData.decode(buffer, i); 65 | 66 | // did reading a pushDataInt fail? 67 | if (d == null) return null; 68 | i += d.size; 69 | 70 | // attempt to read too much data? 71 | if (i + d.number > buffer.length) return null; 72 | 73 | final data = buffer.sublist(i, i + d.number); 74 | i += d.number; 75 | 76 | // decompile minimally 77 | final op = asMinimalOP(data); 78 | if (op != null) { 79 | chunks.add(op); 80 | } else { 81 | chunks.add(data); 82 | } 83 | 84 | // opcode 85 | } else { 86 | chunks.add(opcode); 87 | i += 1; 88 | } 89 | } 90 | return chunks; 91 | } 92 | 93 | Uint8List fromASM(String asm) { 94 | if (asm == '') return Uint8List.fromList([]); 95 | return compile(asm.split(' ').map((chunkStr) { 96 | if (OPS[chunkStr] != null) return OPS[chunkStr]; 97 | return HEX.decode(chunkStr); 98 | }).toList()); 99 | } 100 | 101 | String toASM(List c) { 102 | List chunks; 103 | if (c is Uint8List) { 104 | chunks = decompile(c); 105 | } else { 106 | chunks = c; 107 | } 108 | return chunks.map((chunk) { 109 | // data? 110 | if (chunk is Uint8List) { 111 | final op = asMinimalOP(chunk); 112 | if (op == null) return HEX.encode(chunk); 113 | chunk = op; 114 | } 115 | // opcode! 116 | return REVERSE_OPS[chunk]; 117 | }).join(' '); 118 | } 119 | 120 | int asMinimalOP(Uint8List buffer) { 121 | if (buffer.length == 0) return OPS['OP_0']; 122 | if (buffer.length != 1) return null; 123 | if (buffer[0] >= 1 && buffer[0] <= 16) return OP_INT_BASE + buffer[0]; 124 | if (buffer[0] == 0x81) return OPS['OP_1NEGATE']; 125 | return null; 126 | } 127 | 128 | bool isDefinedHashType(hashType) { 129 | final hashTypeMod = hashType & ~0x80; 130 | // return hashTypeMod > SIGHASH_ALL && hashTypeMod < SIGHASH_SINGLE 131 | return hashTypeMod > 0x00 && hashTypeMod < 0x04; 132 | } 133 | 134 | bool isCanonicalPubKey(Uint8List buffer) { 135 | return ecc.isPoint(buffer); 136 | } 137 | 138 | bool isCanonicalScriptSignature(Uint8List buffer) { 139 | if (!isDefinedHashType(buffer[buffer.length - 1])) return false; 140 | return bip66check(buffer.sublist(0, buffer.length - 1)); 141 | } 142 | 143 | bool bip66check(buffer) { 144 | if (buffer.length < 8) return false; 145 | if (buffer.length > 72) return false; 146 | if (buffer[0] != 0x30) return false; 147 | if (buffer[1] != buffer.length - 2) return false; 148 | if (buffer[2] != 0x02) return false; 149 | 150 | var lenR = buffer[3]; 151 | if (lenR == 0) return false; 152 | if (5 + lenR >= buffer.length) return false; 153 | if (buffer[4 + lenR] != 0x02) return false; 154 | 155 | var lenS = buffer[5 + lenR]; 156 | if (lenS == 0) return false; 157 | if ((6 + lenR + lenS) != buffer.length) return false; 158 | 159 | if (buffer[4] & 0x80 != 0) return false; 160 | if (lenR > 1 && (buffer[4] == 0x00) && buffer[5] & 0x80 == 0) return false; 161 | 162 | if (buffer[lenR + 6] & 0x80 != 0) return false; 163 | if (lenS > 1 && (buffer[lenR + 6] == 0x00) && buffer[lenR + 7] & 0x80 == 0) 164 | return false; 165 | return true; 166 | } 167 | 168 | Uint8List bip66encode(r, s) { 169 | var lenR = r.length; 170 | var lenS = s.length; 171 | if (lenR == 0) throw new ArgumentError('R length is zero'); 172 | if (lenS == 0) throw new ArgumentError('S length is zero'); 173 | if (lenR > 33) throw new ArgumentError('R length is too long'); 174 | if (lenS > 33) throw new ArgumentError('S length is too long'); 175 | if (r[0] & 0x80 != 0) throw new ArgumentError('R value is negative'); 176 | if (s[0] & 0x80 != 0) throw new ArgumentError('S value is negative'); 177 | if (lenR > 1 && (r[0] == 0x00) && r[1] & 0x80 == 0) 178 | throw new ArgumentError('R value excessively padded'); 179 | if (lenS > 1 && (s[0] == 0x00) && s[1] & 0x80 == 0) 180 | throw new ArgumentError('S value excessively padded'); 181 | 182 | var signature = new Uint8List(6 + lenR + lenS); 183 | 184 | // 0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S] 185 | signature[0] = 0x30; 186 | signature[1] = signature.length - 2; 187 | signature[2] = 0x02; 188 | signature[3] = r.length; 189 | signature.setRange(4, 4 + lenR, r); 190 | signature[4 + lenR] = 0x02; 191 | signature[5 + lenR] = s.length; 192 | signature.setRange(6 + lenR, 6 + lenR + lenS, s); 193 | return signature; 194 | } 195 | 196 | Uint8List encodeSignature(Uint8List signature, int hashType) { 197 | if (!isUint(hashType, 8)) throw ArgumentError("Invalid hasType $hashType"); 198 | if (signature.length != 64) throw ArgumentError("Invalid signature"); 199 | final hashTypeMod = hashType & ~0x80; 200 | if (hashTypeMod <= 0 || hashTypeMod >= 4) 201 | throw new ArgumentError('Invalid hashType $hashType'); 202 | 203 | final hashTypeBuffer = new Uint8List(1); 204 | hashTypeBuffer.buffer.asByteData().setUint8(0, hashType); 205 | final r = toDER(signature.sublist(0, 32)); 206 | final s = toDER(signature.sublist(32, 64)); 207 | List combine = List.from(bip66encode(r, s)); 208 | combine.addAll(List.from(hashTypeBuffer)); 209 | return Uint8List.fromList(combine); 210 | } 211 | 212 | Uint8List toDER(Uint8List x) { 213 | var i = 0; 214 | while (x[i] == 0) ++i; 215 | if (i == x.length) return ZERO; 216 | x = x.sublist(i); 217 | List combine = List.from(ZERO); 218 | combine.addAll(x); 219 | if (x[0] & 0x80 != 0) return Uint8List.fromList(combine); 220 | return x; 221 | } 222 | -------------------------------------------------------------------------------- /lib/src/transaction_builder.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | import 'package:bitcoin_flutter/src/utils/constants/op.dart'; 4 | import 'package:meta/meta.dart'; 5 | import 'package:hex/hex.dart'; 6 | import 'package:bs58check/bs58check.dart' as bs58check; 7 | import 'package:bech32/bech32.dart'; 8 | import 'utils/script.dart' as bscript; 9 | import 'ecpair.dart'; 10 | import 'models/networks.dart'; 11 | import 'transaction.dart'; 12 | import 'address.dart'; 13 | import 'payments/index.dart' show PaymentData; 14 | import 'payments/p2pkh.dart'; 15 | import 'payments/p2wpkh.dart'; 16 | import 'classify.dart'; 17 | 18 | const int MAX_OP_RETURN_SIZE = 100; 19 | 20 | class TransactionBuilder { 21 | NetworkType network; 22 | int maximumFeeRate; 23 | List _inputs; 24 | Transaction _tx; 25 | Map _prevTxSet = {}; 26 | 27 | TransactionBuilder({NetworkType network, int maximumFeeRate}) { 28 | this.network = network ?? bitcoin; 29 | this.maximumFeeRate = maximumFeeRate ?? 2500; 30 | this._inputs = []; 31 | this._tx = new Transaction(); 32 | this._tx.version = 2; 33 | } 34 | 35 | List get inputs => _inputs; 36 | 37 | factory TransactionBuilder.fromTransaction(Transaction transaction, 38 | [NetworkType network]) { 39 | final txb = new TransactionBuilder(network: network); 40 | // Copy transaction fields 41 | txb.setVersion(transaction.version); 42 | txb.setLockTime(transaction.locktime); 43 | 44 | // Copy outputs (done first to avoid signature invalidation) 45 | transaction.outs.forEach((txOut) { 46 | txb.addOutput(txOut.script, txOut.value); 47 | }); 48 | 49 | transaction.ins.forEach((txIn) { 50 | txb._addInputUnsafe( 51 | txIn.hash, 52 | txIn.index, 53 | new Input( 54 | sequence: txIn.sequence, 55 | script: txIn.script, 56 | witness: txIn.witness)); 57 | }); 58 | 59 | // fix some things not possible through the public API 60 | // print(txb.toString()); 61 | // txb.__INPUTS.forEach((input, i) => { 62 | // fixMultisigOrder(input, transaction, i); 63 | // }); 64 | 65 | return txb; 66 | } 67 | 68 | setVersion(int version) { 69 | if (version < 0 || version > 0xFFFFFFFF) 70 | throw ArgumentError('Expected Uint32'); 71 | _tx.version = version; 72 | } 73 | 74 | setLockTime(int locktime) { 75 | if (locktime < 0 || locktime > 0xFFFFFFFF) 76 | throw ArgumentError('Expected Uint32'); 77 | // if any signatures exist, throw 78 | if (this._inputs.map((input) { 79 | if (input.signatures == null) return false; 80 | return input.signatures.map((s) { 81 | return s != null; 82 | }).contains(true); 83 | }).contains(true)) { 84 | throw new ArgumentError('No, this would invalidate signatures'); 85 | } 86 | _tx.locktime = locktime; 87 | } 88 | 89 | int addOutput(dynamic data, int value) { 90 | var scriptPubKey; 91 | if (data is String) { 92 | scriptPubKey = Address.addressToOutputScript(data, this.network); 93 | } else if (data is Uint8List) { 94 | scriptPubKey = data; 95 | } else { 96 | throw new ArgumentError('Address invalid'); 97 | } 98 | if (!_canModifyOutputs()) { 99 | throw new ArgumentError('No, this would invalidate signatures'); 100 | } 101 | return _tx.addOutput(scriptPubKey, value); 102 | } 103 | 104 | int addOutputData(dynamic data) { 105 | var scriptPubKey; 106 | if (data is String) { 107 | if (data.length <= MAX_OP_RETURN_SIZE) { 108 | scriptPubKey = bscript.compile([OPS['OP_RETURN'], utf8.encode(data)]); 109 | } else { 110 | throw new ArgumentError('Too much data embedded, max OP_RETURN size is '+MAX_OP_RETURN_SIZE.toString()); 111 | } 112 | } else if (data is Uint8List) { 113 | scriptPubKey = data; 114 | } else { 115 | throw new ArgumentError('Invalid data'); 116 | } 117 | if (!_canModifyOutputs()) { 118 | throw new ArgumentError('No, this would invalidate signatures'); 119 | } 120 | return _tx.addOutput(scriptPubKey, 0); 121 | } 122 | 123 | int addInput(dynamic txHash, int vout, 124 | [int sequence, Uint8List prevOutScript]) { 125 | if (!_canModifyInputs()) { 126 | throw new ArgumentError('No, this would invalidate signatures'); 127 | } 128 | Uint8List hash; 129 | var value; 130 | if (txHash is String) { 131 | hash = Uint8List.fromList(HEX.decode(txHash).reversed.toList()); 132 | } else if (txHash is Uint8List) { 133 | hash = txHash; 134 | } else if (txHash is Transaction) { 135 | final txOut = txHash.outs[vout]; 136 | prevOutScript = txOut.script; 137 | value = txOut.value; 138 | hash = txHash.getHash(); 139 | } else { 140 | throw new ArgumentError('txHash invalid'); 141 | } 142 | return _addInputUnsafe( 143 | hash, 144 | vout, 145 | new Input( 146 | sequence: sequence, prevOutScript: prevOutScript, value: value)); 147 | } 148 | 149 | sign( 150 | {@required int vin, 151 | @required ECPair keyPair, 152 | Uint8List redeemScript, 153 | int witnessValue, 154 | Uint8List witnessScript, 155 | int hashType}) { 156 | if (keyPair.network != null && 157 | keyPair.network.toString().compareTo(network.toString()) != 0) 158 | throw new ArgumentError('Inconsistent network'); 159 | if (vin >= _inputs.length) 160 | throw new ArgumentError('No input at index: $vin'); 161 | hashType = hashType ?? SIGHASH_ALL; 162 | if (this._needsOutputs(hashType)) 163 | throw new ArgumentError('Transaction needs outputs'); 164 | final input = _inputs[vin]; 165 | final ourPubKey = keyPair.publicKey; 166 | if (!_canSign(input)) { 167 | if (witnessValue != null) { 168 | input.value = witnessValue; 169 | } 170 | if (redeemScript != null && witnessScript != null) { 171 | // TODO p2wsh 172 | } 173 | if (redeemScript != null) { 174 | // TODO 175 | } 176 | if (witnessScript != null) { 177 | // TODO 178 | } 179 | if (input.prevOutScript != null && input.prevOutType != null) { 180 | var type = classifyOutput(input.prevOutScript); 181 | if (type == SCRIPT_TYPES['P2WPKH']) { 182 | input.prevOutType = SCRIPT_TYPES['P2WPKH']; 183 | input.hasWitness = true; 184 | input.signatures = [null]; 185 | input.pubkeys = [ourPubKey]; 186 | input.signScript = new P2PKH( 187 | data: new PaymentData(pubkey: ourPubKey), 188 | network: this.network) 189 | .data 190 | .output; 191 | } else { 192 | // DRY CODE 193 | Uint8List prevOutScript = pubkeyToOutputScript(ourPubKey); 194 | input.prevOutType = SCRIPT_TYPES['P2PKH']; 195 | input.signatures = [null]; 196 | input.pubkeys = [ourPubKey]; 197 | input.signScript = prevOutScript; 198 | } 199 | } else { 200 | Uint8List prevOutScript = pubkeyToOutputScript(ourPubKey); 201 | input.prevOutType = SCRIPT_TYPES['P2PKH']; 202 | input.signatures = [null]; 203 | input.pubkeys = [ourPubKey]; 204 | input.signScript = prevOutScript; 205 | } 206 | } 207 | var signatureHash; 208 | if (input.hasWitness) { 209 | signatureHash = this 210 | ._tx 211 | .hashForWitnessV0(vin, input.signScript, input.value, hashType); 212 | } else { 213 | signatureHash = 214 | this._tx.hashForSignature(vin, input.signScript, hashType); 215 | } 216 | 217 | // enforce in order signing of public keys 218 | var signed = false; 219 | for (var i = 0; i < input.pubkeys.length; i++) { 220 | if (HEX.encode(ourPubKey).compareTo(HEX.encode(input.pubkeys[i])) != 0) 221 | continue; 222 | if (input.signatures[i] != null) 223 | throw new ArgumentError('Signature already exists'); 224 | final signature = keyPair.sign(signatureHash); 225 | input.signatures[i] = bscript.encodeSignature(signature, hashType); 226 | signed = true; 227 | } 228 | if (!signed) throw new ArgumentError('Key pair cannot sign for this input'); 229 | } 230 | 231 | Transaction build() { 232 | return _build(false); 233 | } 234 | 235 | Transaction buildIncomplete() { 236 | return _build(true); 237 | } 238 | 239 | Transaction _build(bool allowIncomplete) { 240 | if (!allowIncomplete) { 241 | if (_tx.ins.length == 0) 242 | throw new ArgumentError('Transaction has no inputs'); 243 | if (_tx.outs.length == 0) 244 | throw new ArgumentError('Transaction has no outputs'); 245 | } 246 | 247 | final tx = Transaction.clone(_tx); 248 | 249 | for (var i = 0; i < _inputs.length; i++) { 250 | if (_inputs[i].pubkeys != null && 251 | _inputs[i].signatures != null && 252 | _inputs[i].pubkeys.length != 0 && 253 | _inputs[i].signatures.length != 0) { 254 | if (_inputs[i].prevOutType == SCRIPT_TYPES['P2PKH']) { 255 | P2PKH payment = new P2PKH( 256 | data: new PaymentData( 257 | pubkey: _inputs[i].pubkeys[0], 258 | signature: _inputs[i].signatures[0]), 259 | network: network); 260 | tx.setInputScript(i, payment.data.input); 261 | tx.setWitness(i, payment.data.witness); 262 | } else if (_inputs[i].prevOutType == SCRIPT_TYPES['P2WPKH']) { 263 | P2WPKH payment = new P2WPKH( 264 | data: new PaymentData( 265 | pubkey: _inputs[i].pubkeys[0], 266 | signature: _inputs[i].signatures[0]), 267 | network: network); 268 | tx.setInputScript(i, payment.data.input); 269 | tx.setWitness(i, payment.data.witness); 270 | } 271 | } else if (!allowIncomplete) { 272 | throw new ArgumentError('Transaction is not complete'); 273 | } 274 | } 275 | 276 | if (!allowIncomplete) { 277 | // do not rely on this, its merely a last resort 278 | if (_overMaximumFees(tx.virtualSize())) { 279 | throw new ArgumentError('Transaction has absurd fees'); 280 | } 281 | } 282 | 283 | return tx; 284 | } 285 | 286 | bool _overMaximumFees(int bytes) { 287 | int incoming = _inputs.fold(0, (cur, acc) => cur + (acc.value ?? 0)); 288 | int outgoing = _tx.outs.fold(0, (cur, acc) => cur + (acc.value ?? 0)); 289 | int fee = incoming - outgoing; 290 | int feeRate = fee ~/ bytes; 291 | return feeRate > maximumFeeRate; 292 | } 293 | 294 | bool _canModifyInputs() { 295 | return _inputs.every((input) { 296 | if (input.signatures == null) return true; 297 | return input.signatures.every((signature) { 298 | if (signature == null) return true; 299 | return _signatureHashType(signature) & SIGHASH_ANYONECANPAY != 0; 300 | }); 301 | }); 302 | } 303 | 304 | bool _canModifyOutputs() { 305 | final nInputs = _tx.ins.length; 306 | final nOutputs = _tx.outs.length; 307 | return _inputs.every((input) { 308 | if (input.signatures == null) return true; 309 | return input.signatures.every((signature) { 310 | if (signature == null) return true; 311 | final hashType = _signatureHashType(signature); 312 | final hashTypeMod = hashType & 0x1f; 313 | if (hashTypeMod == SIGHASH_NONE) return true; 314 | if (hashTypeMod == SIGHASH_SINGLE) { 315 | // if SIGHASH_SINGLE is set, and nInputs > nOutputs 316 | // some signatures would be invalidated by the addition 317 | // of more outputs 318 | return nInputs <= nOutputs; 319 | } 320 | return false; 321 | }); 322 | }); 323 | } 324 | 325 | bool _needsOutputs(int signingHashType) { 326 | if (signingHashType == SIGHASH_ALL) { 327 | return this._tx.outs.length == 0; 328 | } 329 | // if inputs are being signed with SIGHASH_NONE, we don't strictly need outputs 330 | // .build() will fail, but .buildIncomplete() is OK 331 | return (this._tx.outs.length == 0) && 332 | _inputs.map((input) { 333 | if (input.signatures == null || input.signatures.length == 0) 334 | return false; 335 | return input.signatures.map((signature) { 336 | if (signature == null) return false; // no signature, no issue 337 | final hashType = _signatureHashType(signature); 338 | if (hashType & SIGHASH_NONE != 0) 339 | return false; // SIGHASH_NONE doesn't care about outputs 340 | return true; // SIGHASH_* does care 341 | }).contains(true); 342 | }).contains(true); 343 | } 344 | 345 | bool _canSign(Input input) { 346 | return input.pubkeys != null && 347 | input.signScript != null && 348 | input.signatures != null && 349 | input.signatures.length == input.pubkeys.length && 350 | input.pubkeys.length > 0; 351 | } 352 | 353 | _addInputUnsafe(Uint8List hash, int vout, Input options) { 354 | String txHash = HEX.encode(hash); 355 | Input input; 356 | if (isCoinbaseHash(hash)) { 357 | throw new ArgumentError('coinbase inputs not supported'); 358 | } 359 | final prevTxOut = '$txHash:$vout'; 360 | if (_prevTxSet[prevTxOut] != null) 361 | throw new ArgumentError('Duplicate TxOut: ' + prevTxOut); 362 | if (options.script != null) { 363 | input = 364 | Input.expandInput(options.script, options.witness ?? EMPTY_WITNESS); 365 | } else { 366 | input = new Input(); 367 | } 368 | if (options.value != null) input.value = options.value; 369 | if (input.prevOutScript == null && options.prevOutScript != null) { 370 | if (input.pubkeys == null && input.signatures == null) { 371 | var expanded = Output.expandOutput(options.prevOutScript); 372 | if (expanded.pubkeys != null && !expanded.pubkeys.isEmpty) { 373 | input.pubkeys = expanded.pubkeys; 374 | input.signatures = expanded.signatures; 375 | } 376 | } 377 | input.prevOutScript = options.prevOutScript; 378 | input.prevOutType = classifyOutput(options.prevOutScript); 379 | } 380 | int vin = _tx.addInput(hash, vout, options.sequence, options.script); 381 | _inputs.add(input); 382 | _prevTxSet[prevTxOut] = true; 383 | return vin; 384 | } 385 | 386 | int _signatureHashType(Uint8List buffer) { 387 | return buffer.buffer.asByteData().getUint8(buffer.length - 1); 388 | } 389 | 390 | Transaction get tx => _tx; 391 | 392 | Map get prevTxSet => _prevTxSet; 393 | } 394 | 395 | Uint8List pubkeyToOutputScript(Uint8List pubkey, [NetworkType nw]) { 396 | NetworkType network = nw ?? bitcoin; 397 | P2PKH p2pkh = 398 | new P2PKH(data: new PaymentData(pubkey: pubkey), network: network); 399 | return p2pkh.data.output; 400 | } 401 | -------------------------------------------------------------------------------- /test/transaction_builder_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'dart:io'; 3 | import 'dart:convert'; 4 | import 'dart:typed_data'; 5 | import 'package:hex/hex.dart'; 6 | import '../lib/src/models/networks.dart'; 7 | import '../lib/src/ecpair.dart'; 8 | import '../lib/src/transaction.dart'; 9 | import '../lib/src/address.dart'; 10 | import '../lib/src/transaction_builder.dart'; 11 | import '../lib/src/utils/script.dart' as bscript; 12 | import '../lib/src/payments/index.dart' show PaymentData; 13 | import '../lib/src/payments/p2pkh.dart'; 14 | 15 | final NETWORKS = {'bitcoin': bitcoin, 'testnet': testnet}; 16 | 17 | constructSign(f, TransactionBuilder txb) { 18 | final network = NETWORKS[f['network']]; 19 | final inputs = f['inputs'] as List; 20 | for (var i = 0; i < inputs.length; i++) { 21 | if (inputs[i]['signs'] == null) continue; 22 | (inputs[i]['signs'] as List).forEach((sign) { 23 | ECPair keyPair = ECPair.fromWIF(sign['keyPair'], network: network); 24 | txb.sign( 25 | vin: i, 26 | keyPair: keyPair, 27 | witnessValue: sign['value'], 28 | hashType: sign['hashType']); 29 | }); 30 | } 31 | return txb; 32 | } 33 | 34 | TransactionBuilder construct(f, [bool dontSign]) { 35 | final network = NETWORKS[f['network']]; 36 | final txb = new TransactionBuilder(network: network); 37 | if (f['version'] != null) txb.setVersion(f['version']); 38 | (f['inputs'] as List).forEach((input) { 39 | var prevTx; 40 | if (input['txRaw'] != null) { 41 | final constructed = construct(input['txRaw']); 42 | if (input['txRaw']['incomplete']) 43 | prevTx = constructed.buildIncomplete(); 44 | else 45 | prevTx = constructed.build(); 46 | } else if (input['txHex'] != null) { 47 | prevTx = Transaction.fromHex(input['txHex']); 48 | } else { 49 | prevTx = input['txId']; 50 | } 51 | var prevTxScript; 52 | if (input['prevTxScript'] != null) { 53 | prevTxScript = bscript.fromASM(input['prevTxScript']); 54 | } 55 | txb.addInput(prevTx, input['vout'], input['sequence'], prevTxScript); 56 | }); 57 | (f['outputs'] as List).forEach((output) { 58 | if (output['address'] != null) { 59 | txb.addOutput(output['address'], output['value']); 60 | } else { 61 | txb.addOutput(bscript.fromASM(output['script']), output['value']); 62 | } 63 | }); 64 | if (dontSign != null && dontSign) return txb; 65 | return constructSign(f, txb); 66 | } 67 | 68 | main() { 69 | final fixtures = json.decode( 70 | new File('test/fixtures/transaction_builder.json') 71 | .readAsStringSync(encoding: utf8)); 72 | group('TransactionBuilder', () { 73 | final keyPair = ECPair.fromPrivateKey(HEX.decode( 74 | '0000000000000000000000000000000000000000000000000000000000000001')); 75 | final scripts = [ 76 | '1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH', 77 | '1cMh228HTCiwS8ZsaakH8A8wze1JR5ZsP' 78 | ].map((x) => Address.addressToOutputScript(x)); 79 | final txHash = HEX.decode( 80 | '0e7cea811c0be9f73c0aca591034396e7264473fc25c1ca45195d7417b36cbe2'); 81 | group('fromTransaction', () { 82 | (fixtures['valid']['build'] as List).forEach((f) { 83 | test('returns TransactionBuilder, with ${f['description']}', () { 84 | final network = NETWORKS[f['network'] ?? 'bitcoin']; 85 | final tx = Transaction.fromHex(f['txHex']); 86 | final txb = TransactionBuilder.fromTransaction(tx, network); 87 | final txAfter = 88 | f['incomplete'] != null ? txb.buildIncomplete() : txb.build(); 89 | expect(txAfter.toHex(), f['txHex']); 90 | expect(txb.network, network); 91 | }); 92 | }); 93 | (fixtures['valid']['fromTransaction'] as List).forEach((f) { 94 | test('returns TransactionBuilder, with ${f['description']}', () { 95 | final tx = new Transaction(); 96 | f['inputs'] as List 97 | ..forEach((input) { 98 | final txHash2 = Uint8List.fromList( 99 | HEX.decode(input['txId']).reversed.toList()); 100 | tx.addInput(txHash2, input['vout'], null, 101 | bscript.fromASM(input['scriptSig'])); 102 | }); 103 | f['outputs'] as List 104 | ..forEach((output) { 105 | tx.addOutput(bscript.fromASM(output['script']), output['value']); 106 | }); 107 | 108 | final txb = TransactionBuilder.fromTransaction(tx); 109 | final txAfter = f['incomplete'] ? txb.buildIncomplete() : txb.build(); 110 | 111 | for (var i = 0; i < txAfter.ins.length; i++) { 112 | test(bscript.toASM(txAfter.ins[i].script), 113 | f['inputs'][i]['scriptSigAfter']); 114 | } 115 | for (var i = 0; i < txAfter.outs.length; i++) { 116 | test(bscript.toASM(txAfter.outs[i].script), 117 | f['outputs'][i]['script']); 118 | } 119 | }); 120 | }); 121 | fixtures['invalid']['fromTransaction'] as List 122 | ..forEach((f) { 123 | test('throws ${f['exception']}', () { 124 | final tx = Transaction.fromHex(f['txHex']); 125 | try { 126 | expect(TransactionBuilder.fromTransaction(tx), isArgumentError); 127 | } catch (err) { 128 | expect((err as ArgumentError).message, f['exception']); 129 | } 130 | }); 131 | }); 132 | }); 133 | group('addInput', () { 134 | TransactionBuilder txb; 135 | setUp(() { 136 | txb = new TransactionBuilder(); 137 | }); 138 | test('accepts a txHash, index [and sequence number]', () { 139 | final vin = txb.addInput(txHash, 1, 54); 140 | expect(vin, 0); 141 | final txIn = txb.tx.ins[0]; 142 | expect(txIn.hash, txHash); 143 | expect(txIn.index, 1); 144 | expect(txIn.sequence, 54); 145 | expect(txb.inputs[0].prevOutScript, null); 146 | }); 147 | test('accepts a txHash, index [, sequence number and scriptPubKey]', () { 148 | final vin = txb.addInput(txHash, 1, 54, scripts.elementAt(1)); 149 | expect(vin, 0); 150 | final txIn = txb.tx.ins[0]; 151 | expect(txIn.hash, txHash); 152 | expect(txIn.index, 1); 153 | expect(txIn.sequence, 54); 154 | expect(txb.inputs[0].prevOutScript, scripts.elementAt(1)); 155 | }); 156 | test('accepts a prevTx, index [and sequence number]', () { 157 | final prevTx = new Transaction(); 158 | prevTx.addOutput(scripts.elementAt(0), 0); 159 | prevTx.addOutput(scripts.elementAt(1), 1); 160 | 161 | final vin = txb.addInput(prevTx, 1, 54); 162 | expect(vin, 0); 163 | 164 | final txIn = txb.tx.ins[0]; 165 | expect(txIn.hash, prevTx.getHash()); 166 | expect(txIn.index, 1); 167 | expect(txIn.sequence, 54); 168 | expect(txb.inputs[0].prevOutScript, scripts.elementAt(1)); 169 | }); 170 | test('returns the input index', () { 171 | expect(txb.addInput(txHash, 0), 0); 172 | expect(txb.addInput(txHash, 1), 1); 173 | }); 174 | test( 175 | 'throws if SIGHASH_ALL has been used to sign any existing scriptSigs', 176 | () { 177 | txb.addInput(txHash, 0); 178 | txb.addOutput(scripts.elementAt(0), 1000); 179 | txb.sign(vin: 0, keyPair: keyPair); 180 | try { 181 | expect(txb.addInput(txHash, 0), isArgumentError); 182 | } catch (err) { 183 | expect((err as ArgumentError).message, 184 | 'No, this would invalidate signatures'); 185 | } 186 | }); 187 | }); 188 | group('addOutput', () { 189 | TransactionBuilder txb; 190 | setUp(() { 191 | txb = new TransactionBuilder(); 192 | }); 193 | test('accepts an address string and value', () { 194 | final address = 195 | new P2PKH(data: new PaymentData(pubkey: keyPair.publicKey)) 196 | .data 197 | .address; 198 | final vout = txb.addOutput(address, 1000); 199 | expect(vout, 0); 200 | final txout = txb.tx.outs[0]; 201 | expect(txout.script, scripts.elementAt(0)); 202 | expect(txout.value, 1000); 203 | }); 204 | test('accepts a ScriptPubKey and value', () { 205 | final vout = txb.addOutput(scripts.elementAt(0), 1000); 206 | expect(vout, 0); 207 | final txout = txb.tx.outs[0]; 208 | expect(txout.script, scripts.elementAt(0)); 209 | expect(txout.value, 1000); 210 | }); 211 | test('throws if address is of the wrong network', () { 212 | try { 213 | expect(txb.addOutput('2NGHjvjw83pcVFgMcA7QvSMh2c246rxLVz9', 1000), 214 | isArgumentError); 215 | } catch (err) { 216 | expect((err as ArgumentError).message, 217 | 'Invalid version or Network mismatch'); 218 | } 219 | }); 220 | test('add second output after signed first input with SIGHASH_NONE', () { 221 | txb.addInput(txHash, 0); 222 | txb.addOutput(scripts.elementAt(0), 2000); 223 | txb.sign(vin: 0, keyPair: keyPair, hashType: SIGHASH_NONE); 224 | expect(txb.addOutput(scripts.elementAt(1), 9000), 1); 225 | }); 226 | test('add first output after signed first input with SIGHASH_NONE', () { 227 | txb.addInput(txHash, 0); 228 | txb.sign(vin: 0, keyPair: keyPair, hashType: SIGHASH_NONE); 229 | expect(txb.addOutput(scripts.elementAt(0), 2000), 0); 230 | }); 231 | test('add second output after signed first input with SIGHASH_SINGLE', 232 | () { 233 | txb.addInput(txHash, 0); 234 | txb.addOutput(scripts.elementAt(0), 2000); 235 | txb.sign(vin: 0, keyPair: keyPair, hashType: SIGHASH_SINGLE); 236 | expect(txb.addOutput(scripts.elementAt(1), 9000), 1); 237 | }); 238 | test('add first output after signed first input with SIGHASH_SINGLE', () { 239 | txb.addInput(txHash, 0); 240 | txb.sign(vin: 0, keyPair: keyPair, hashType: SIGHASH_SINGLE); 241 | try { 242 | expect(txb.addOutput(scripts.elementAt(0), 2000), isArgumentError); 243 | } catch (err) { 244 | expect((err as ArgumentError).message, 245 | 'No, this would invalidate signatures'); 246 | } 247 | }); 248 | test( 249 | 'throws if SIGHASH_ALL has been used to sign any existing scriptSigs', 250 | () { 251 | txb.addInput(txHash, 0); 252 | txb.addOutput(scripts.elementAt(0), 2000); 253 | txb.sign(vin: 0, keyPair: keyPair); 254 | try { 255 | expect(txb.addOutput(scripts.elementAt(1), 9000), isArgumentError); 256 | } catch (err) { 257 | expect((err as ArgumentError).message, 258 | 'No, this would invalidate signatures'); 259 | } 260 | }); 261 | }); 262 | group('addOutputData', () { 263 | TransactionBuilder txb; 264 | String data; 265 | String data2; 266 | setUp(() { 267 | txb = new TransactionBuilder(); 268 | data = 'Hey this is a random string without Bitcoins.'; 269 | data2 = 'And this is another string.'; 270 | }); 271 | test('accepts a ScriptPubKey', () { 272 | final vout = txb.addOutputData(scripts.elementAt(0)); 273 | expect(vout, 0); 274 | final txout = txb.tx.outs[0]; 275 | expect(txout.script, scripts.elementAt(0)); 276 | expect(txout.value, 0); 277 | }); 278 | test('throws if too much data is provided', () { 279 | try { 280 | expect(txb.addOutputData('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'), 281 | isArgumentError); 282 | } catch (err) { 283 | expect((err as ArgumentError).message, 284 | 'Too much data embedded, max OP_RETURN size is 100'); 285 | } 286 | }); 287 | test('add second output after signed first input with SIGHASH_NONE', () { 288 | txb.addInput(txHash, 0); 289 | txb.addOutputData(data); 290 | txb.sign(vin: 0, keyPair: keyPair, hashType: SIGHASH_NONE); 291 | expect(txb.addOutputData(data2), 1); 292 | }); 293 | test('add first output after signed first input with SIGHASH_NONE', () { 294 | txb.addInput(txHash, 0); 295 | txb.sign(vin: 0, keyPair: keyPair, hashType: SIGHASH_NONE); 296 | expect(txb.addOutputData(data), 0); 297 | }); 298 | test('add second output after signed first input with SIGHASH_SINGLE', 299 | () { 300 | txb.addInput(txHash, 0); 301 | txb.addOutputData(data); 302 | txb.sign(vin: 0, keyPair: keyPair, hashType: SIGHASH_SINGLE); 303 | expect(txb.addOutputData(data2), 1); 304 | }); 305 | test('add first output after signed first input with SIGHASH_SINGLE', () { 306 | txb.addInput(txHash, 0); 307 | txb.sign(vin: 0, keyPair: keyPair, hashType: SIGHASH_SINGLE); 308 | try { 309 | expect(txb.addOutputData(data), isArgumentError); 310 | } catch (err) { 311 | expect((err as ArgumentError).message, 312 | 'No, this would invalidate signatures'); 313 | } 314 | }); 315 | test( 316 | 'throws if SIGHASH_ALL has been used to sign any existing scriptSigs', 317 | () { 318 | txb.addInput(txHash, 0); 319 | txb.addOutputData(data); 320 | txb.sign(vin: 0, keyPair: keyPair); 321 | try { 322 | expect(txb.addOutputData(data2), isArgumentError); 323 | } catch (err) { 324 | expect((err as ArgumentError).message, 325 | 'No, this would invalidate signatures'); 326 | } 327 | }); 328 | }); 329 | group('setLockTime', () { 330 | test('throws if if there exist any scriptSigs', () { 331 | final txb = new TransactionBuilder(); 332 | txb.addInput(txHash, 0); 333 | txb.addOutput(scripts.elementAt(0), 100); 334 | txb.sign(vin: 0, keyPair: keyPair); 335 | try { 336 | expect(txb.setLockTime(65535), isArgumentError); 337 | } catch (err) { 338 | expect((err as ArgumentError).message, 339 | 'No, this would invalidate signatures'); 340 | } 341 | }); 342 | }); 343 | group('sign', () { 344 | fixtures['invalid']['sign'] as List 345 | ..forEach((f) { 346 | test( 347 | 'throws ${f['exception']} ${f['description'] != null ? f['description'] : ''}', 348 | () { 349 | final txb = construct(f, true); 350 | var threw = false; 351 | final inputs = f['inputs'] as List; 352 | for (var i = 0; i < inputs.length; i++) { 353 | inputs[i]['signs'] as List 354 | ..forEach((sign) { 355 | final keyPairNetwork = 356 | NETWORKS[sign['network'] ?? f['network']]; 357 | final keyPair2 = 358 | ECPair.fromWIF(sign['keyPair'], network: keyPairNetwork); 359 | if (sign['throws'] != null && sign['throws']) { 360 | try { 361 | expect( 362 | txb.sign( 363 | vin: i, 364 | keyPair: keyPair2, 365 | hashType: sign['hashType']), 366 | isArgumentError); 367 | } catch (err) { 368 | expect((err as ArgumentError).message, f['exception']); 369 | } 370 | threw = true; 371 | } else { 372 | txb.sign( 373 | vin: i, keyPair: keyPair2, hashType: sign['hashType']); 374 | } 375 | }); 376 | } 377 | expect(threw, true); 378 | }); 379 | }); 380 | }); 381 | group('build', () { 382 | fixtures['valid']['build'] as List 383 | ..forEach((f) { 384 | test('builds ${f['description']}', () { 385 | final txb = construct(f); 386 | final tx = 387 | f['incomplete'] != null ? txb.buildIncomplete() : txb.build(); 388 | 389 | expect(tx.toHex(), f['txHex']); 390 | }); 391 | }); 392 | fixtures['invalid']['build'] as List 393 | ..forEach((f) { 394 | group('for ${f['description'] ?? f['exception']}', () { 395 | test('throws ${f['exception']}', () { 396 | try { 397 | TransactionBuilder txb; 398 | if (f['txHex'] != null) { 399 | txb = TransactionBuilder.fromTransaction( 400 | Transaction.fromHex(f['txHex'])); 401 | } else { 402 | txb = construct(f); 403 | } 404 | expect(txb.build(), isArgumentError); 405 | } catch (err) { 406 | expect((err as ArgumentError).message, f['exception']); 407 | } 408 | }); 409 | // if throws on incomplete too, enforce that 410 | if (f['incomplete'] != null && f['incomplete']) { 411 | test('throws ${f['exception']}', () { 412 | try { 413 | TransactionBuilder txb; 414 | if (f['txHex'] != null) { 415 | txb = TransactionBuilder.fromTransaction( 416 | Transaction.fromHex(f['txHex'])); 417 | } else { 418 | txb = construct(f); 419 | } 420 | expect(txb.buildIncomplete(), isArgumentError); 421 | } catch (err) { 422 | expect((err as ArgumentError).message, f['exception']); 423 | } 424 | }); 425 | } else { 426 | test('does not throw if buildIncomplete', () { 427 | TransactionBuilder txb; 428 | if (f['txHex'] != null) { 429 | txb = TransactionBuilder.fromTransaction( 430 | Transaction.fromHex(f['txHex'])); 431 | } else { 432 | txb = construct(f); 433 | } 434 | txb.buildIncomplete(); 435 | }); 436 | } 437 | }); 438 | }); 439 | }); 440 | }); 441 | } 442 | -------------------------------------------------------------------------------- /lib/src/transaction.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'package:hex/hex.dart'; 3 | import 'payments/index.dart' show PaymentData; 4 | import 'payments/p2pkh.dart' show P2PKH; 5 | import 'payments/p2pk.dart' show P2PK; 6 | import 'payments/p2wpkh.dart' show P2WPKH; 7 | import 'crypto.dart' as bcrypto; 8 | import 'classify.dart'; 9 | import 'utils/check_types.dart'; 10 | import 'utils/script.dart' as bscript; 11 | import 'utils/constants/op.dart'; 12 | import 'utils/varuint.dart' as varuint; 13 | 14 | const DEFAULT_SEQUENCE = 0xffffffff; 15 | const SIGHASH_ALL = 0x01; 16 | const SIGHASH_NONE = 0x02; 17 | const SIGHASH_SINGLE = 0x03; 18 | const SIGHASH_ANYONECANPAY = 0x80; 19 | const ADVANCED_TRANSACTION_MARKER = 0x00; 20 | const ADVANCED_TRANSACTION_FLAG = 0x01; 21 | final EMPTY_SCRIPT = Uint8List.fromList([]); 22 | final EMPTY_WITNESS = new List(); 23 | final ZERO = HEX 24 | .decode('0000000000000000000000000000000000000000000000000000000000000000'); 25 | final ONE = HEX 26 | .decode('0000000000000000000000000000000000000000000000000000000000000001'); 27 | final VALUE_UINT64_MAX = HEX.decode('ffffffffffffffff'); 28 | final BLANK_OUTPUT = 29 | new Output(script: EMPTY_SCRIPT, valueBuffer: VALUE_UINT64_MAX); 30 | 31 | class Transaction { 32 | int version = 1; 33 | int locktime = 0; 34 | List ins = []; 35 | List outs = []; 36 | Transaction(); 37 | 38 | int addInput(Uint8List hash, int index, [int sequence, Uint8List scriptSig]) { 39 | ins.add(new Input( 40 | hash: hash, 41 | index: index, 42 | sequence: sequence ?? DEFAULT_SEQUENCE, 43 | script: scriptSig ?? EMPTY_SCRIPT, 44 | witness: EMPTY_WITNESS)); 45 | return ins.length - 1; 46 | } 47 | 48 | int addOutput(Uint8List scriptPubKey, int value) { 49 | outs.add(new Output(script: scriptPubKey, value: value)); 50 | return outs.length - 1; 51 | } 52 | 53 | bool hasWitnesses() { 54 | var witness = ins.firstWhere( 55 | (input) => input.witness != null && input.witness.length != 0, 56 | orElse: () => null); 57 | return witness != null; 58 | } 59 | 60 | setInputScript(int index, Uint8List scriptSig) { 61 | ins[index].script = scriptSig; 62 | } 63 | 64 | setWitness(int index, List witness) { 65 | ins[index].witness = witness; 66 | } 67 | 68 | hashForWitnessV0( 69 | int inIndex, Uint8List prevOutScript, int value, int hashType) { 70 | var tbuffer = Uint8List.fromList([]); 71 | var toffset = 0; 72 | // Any changes made to the ByteData will also change the buffer, and vice versa. 73 | // https://api.dart.dev/stable/2.7.1/dart-typed_data/ByteBuffer/asByteData.html 74 | ByteData bytes = tbuffer.buffer.asByteData(); 75 | var hashOutputs = ZERO; 76 | var hashPrevouts = ZERO; 77 | var hashSequence = ZERO; 78 | 79 | writeSlice(slice) { 80 | tbuffer.setRange(toffset, toffset + slice.length, slice); 81 | toffset += slice.length; 82 | } 83 | 84 | writeUInt8(i) { 85 | bytes.setUint8(toffset, i); 86 | toffset++; 87 | } 88 | 89 | writeUInt32(i) { 90 | bytes.setUint32(toffset, i, Endian.little); 91 | toffset += 4; 92 | } 93 | 94 | writeInt32(i) { 95 | bytes.setInt32(toffset, i, Endian.little); 96 | toffset += 4; 97 | } 98 | 99 | writeUInt64(i) { 100 | bytes.setUint64(toffset, i, Endian.little); 101 | toffset += 8; 102 | } 103 | 104 | writeVarInt(i) { 105 | varuint.encode(i, tbuffer, toffset); 106 | toffset += varuint.encodingLength(i); 107 | } 108 | 109 | writeVarSlice(slice) { 110 | writeVarInt(slice.length); 111 | writeSlice(slice); 112 | } 113 | 114 | writeVector(vector) { 115 | writeVarInt(vector.length); 116 | vector.forEach((buf) { 117 | writeVarSlice(buf); 118 | }); 119 | } 120 | 121 | if ((hashType & SIGHASH_ANYONECANPAY) == 0) { 122 | tbuffer = new Uint8List(36 * this.ins.length); 123 | bytes = tbuffer.buffer.asByteData(); 124 | toffset = 0; 125 | 126 | ins.forEach((txIn) { 127 | writeSlice(txIn.hash); 128 | writeUInt32(txIn.index); 129 | }); 130 | hashPrevouts = bcrypto.hash256(tbuffer); 131 | } 132 | 133 | if ((hashType & SIGHASH_ANYONECANPAY) == 0 && 134 | (hashType & 0x1f) != SIGHASH_SINGLE && 135 | (hashType & 0x1f) != SIGHASH_NONE) { 136 | tbuffer = new Uint8List(4 * this.ins.length); 137 | bytes = tbuffer.buffer.asByteData(); 138 | toffset = 0; 139 | ins.forEach((txIn) { 140 | writeUInt32(txIn.sequence); 141 | }); 142 | hashSequence = bcrypto.hash256(tbuffer); 143 | } 144 | 145 | if ((hashType & 0x1f) != SIGHASH_SINGLE && 146 | (hashType & 0x1f) != SIGHASH_NONE) { 147 | var txOutsSize = 148 | outs.fold(0, (sum, output) => sum + 8 + varSliceSize(output.script)); 149 | tbuffer = new Uint8List(txOutsSize); 150 | bytes = tbuffer.buffer.asByteData(); 151 | toffset = 0; 152 | outs.forEach((txOut) { 153 | writeUInt64(txOut.value); 154 | writeVarSlice(txOut.script); 155 | }); 156 | hashOutputs = bcrypto.hash256(tbuffer); 157 | } else if ((hashType & 0x1f) == SIGHASH_SINGLE && inIndex < outs.length) { 158 | // SIGHASH_SINGLE only hash that according output 159 | var output = outs[inIndex]; 160 | tbuffer = new Uint8List(8 + varSliceSize(output.script)); 161 | bytes = tbuffer.buffer.asByteData(); 162 | toffset = 0; 163 | writeUInt64(output.value); 164 | writeVarSlice(output.script); 165 | hashOutputs = bcrypto.hash256(tbuffer); 166 | } 167 | 168 | tbuffer = new Uint8List(156 + varSliceSize(prevOutScript)); 169 | bytes = tbuffer.buffer.asByteData(); 170 | toffset = 0; 171 | var input = ins[inIndex]; 172 | writeUInt32(version); 173 | writeSlice(hashPrevouts); 174 | writeSlice(hashSequence); 175 | writeSlice(input.hash); 176 | writeUInt32(input.index); 177 | writeVarSlice(prevOutScript); 178 | writeUInt64(value); 179 | writeUInt32(input.sequence); 180 | writeSlice(hashOutputs); 181 | writeUInt32(this.locktime); 182 | writeUInt32(hashType); 183 | 184 | return bcrypto.hash256(tbuffer); 185 | } 186 | 187 | hashForSignature(int inIndex, Uint8List prevOutScript, int hashType) { 188 | if (inIndex >= ins.length) return ONE; 189 | // ignore OP_CODESEPARATOR 190 | final ourScript = 191 | bscript.compile(bscript.decompile(prevOutScript).where((x) { 192 | return x != OPS['OP_CODESEPARATOR']; 193 | }).toList()); 194 | final txTmp = Transaction.clone(this); 195 | // SIGHASH_NONE: ignore all outputs? (wildcard payee) 196 | if ((hashType & 0x1f) == SIGHASH_NONE) { 197 | txTmp.outs = []; 198 | // ignore sequence numbers (except at inIndex) 199 | for (var i = 0; i < txTmp.ins.length; i++) { 200 | if (i != inIndex) { 201 | txTmp.ins[i].sequence = 0; 202 | } 203 | } 204 | 205 | // SIGHASH_SINGLE: ignore all outputs, except at the same index? 206 | } else if ((hashType & 0x1f) == SIGHASH_SINGLE) { 207 | // https://github.com/bitcoin/bitcoin/blob/master/src/test/sighash_tests.cpp#L60 208 | if (inIndex >= outs.length) return ONE; 209 | 210 | // truncate outputs after 211 | txTmp.outs.length = inIndex + 1; 212 | 213 | // 'blank' outputs before 214 | for (var i = 0; i < inIndex; i++) { 215 | txTmp.outs[i] = BLANK_OUTPUT; 216 | } 217 | // ignore sequence numbers (except at inIndex) 218 | for (var i = 0; i < txTmp.ins.length; i++) { 219 | if (i != inIndex) { 220 | txTmp.ins[i].sequence = 0; 221 | } 222 | } 223 | } 224 | 225 | // SIGHASH_ANYONECANPAY: ignore inputs entirely? 226 | if (hashType & SIGHASH_ANYONECANPAY != 0) { 227 | txTmp.ins = [txTmp.ins[inIndex]]; 228 | txTmp.ins[0].script = ourScript; 229 | // SIGHASH_ALL: only ignore input scripts 230 | } else { 231 | // 'blank' others input scripts 232 | txTmp.ins.forEach((input) { 233 | input.script = EMPTY_SCRIPT; 234 | }); 235 | txTmp.ins[inIndex].script = ourScript; 236 | } 237 | // serialize and hash 238 | final buffer = Uint8List(txTmp.virtualSize() + 4); 239 | buffer.buffer 240 | .asByteData() 241 | .setUint32(buffer.length - 4, hashType, Endian.little); 242 | txTmp._toBuffer(buffer, 0); 243 | return bcrypto.hash256(buffer); 244 | } 245 | 246 | _byteLength(_ALLOW_WITNESS) { 247 | var hasWitness = _ALLOW_WITNESS && hasWitnesses(); 248 | return (hasWitness ? 10 : 8) + 249 | varuint.encodingLength(ins.length) + 250 | varuint.encodingLength(outs.length) + 251 | ins.fold(0, (sum, input) => sum + 40 + varSliceSize(input.script)) + 252 | outs.fold(0, (sum, output) => sum + 8 + varSliceSize(output.script)) + 253 | (hasWitness 254 | ? ins.fold(0, (sum, input) => sum + vectorSize(input.witness)) 255 | : 0); 256 | } 257 | 258 | int vectorSize(List someVector) { 259 | var length = someVector.length; 260 | return varuint.encodingLength(length) + 261 | someVector.fold(0, (sum, witness) => sum + varSliceSize(witness)); 262 | } 263 | 264 | int weight() { 265 | var base = _byteLength(false); 266 | var total = _byteLength(true); 267 | return base * 3 + total; 268 | } 269 | 270 | int byteLength() { 271 | return _byteLength(true); 272 | } 273 | 274 | int virtualSize() { 275 | return (weight() / 4).ceil(); 276 | } 277 | 278 | Uint8List toBuffer([Uint8List buffer, int initialOffset]) { 279 | return this._toBuffer(buffer, initialOffset, true); 280 | } 281 | 282 | String toHex() { 283 | return HEX.encode(this.toBuffer()); 284 | } 285 | 286 | bool isCoinbaseHash(buffer) { 287 | isHash256bit(buffer); 288 | for (var i = 0; i < 32; ++i) { 289 | if (buffer[i] != 0) return false; 290 | } 291 | return true; 292 | } 293 | 294 | bool isCoinbase() { 295 | return ins.length == 1 && isCoinbaseHash(ins[0].hash); 296 | } 297 | 298 | Uint8List getHash() { 299 | // if (isCoinbase()) return Uint8List.fromList(List.generate(32, (i) => 0)); 300 | return bcrypto.hash256(_toBuffer(null, null, false)); 301 | } 302 | 303 | String getId() { 304 | return HEX.encode(getHash().reversed.toList()); 305 | } 306 | 307 | _toBuffer([Uint8List buffer, initialOffset, bool _ALLOW_WITNESS = false]) { 308 | // _ALLOW_WITNESS is used to separate witness part when calculating tx id 309 | if (buffer == null) buffer = new Uint8List(_byteLength(_ALLOW_WITNESS)); 310 | 311 | // Any changes made to the ByteData will also change the buffer, and vice versa. 312 | // https://api.dart.dev/stable/2.7.1/dart-typed_data/ByteBuffer/asByteData.html 313 | var bytes = buffer.buffer.asByteData(); 314 | var offset = initialOffset ?? 0; 315 | 316 | writeSlice(slice) { 317 | buffer.setRange(offset, offset + slice.length, slice); 318 | offset += slice.length; 319 | } 320 | 321 | writeUInt8(i) { 322 | bytes.setUint8(offset, i); 323 | offset++; 324 | } 325 | 326 | writeUInt32(i) { 327 | bytes.setUint32(offset, i, Endian.little); 328 | offset += 4; 329 | } 330 | 331 | writeInt32(i) { 332 | bytes.setInt32(offset, i, Endian.little); 333 | offset += 4; 334 | } 335 | 336 | writeUInt64(i) { 337 | bytes.setUint64(offset, i, Endian.little); 338 | offset += 8; 339 | } 340 | 341 | writeVarInt(i) { 342 | varuint.encode(i, buffer, offset); 343 | offset += varuint.encodingLength(i); 344 | } 345 | 346 | writeVarSlice(slice) { 347 | writeVarInt(slice.length); 348 | writeSlice(slice); 349 | } 350 | 351 | writeVector(vector) { 352 | writeVarInt(vector.length); 353 | vector.forEach((buf) { 354 | writeVarSlice(buf); 355 | }); 356 | } 357 | 358 | // Start writeBuffer 359 | writeInt32(version); 360 | 361 | if (_ALLOW_WITNESS && hasWitnesses()) { 362 | writeUInt8(ADVANCED_TRANSACTION_MARKER); 363 | writeUInt8(ADVANCED_TRANSACTION_FLAG); 364 | } 365 | 366 | writeVarInt(this.ins.length); 367 | 368 | ins.forEach((txIn) { 369 | writeSlice(txIn.hash); 370 | writeUInt32(txIn.index); 371 | writeVarSlice(txIn.script); 372 | writeUInt32(txIn.sequence); 373 | }); 374 | 375 | writeVarInt(this.outs.length); 376 | 377 | outs.forEach((txOut) { 378 | if (txOut.valueBuffer == null) { 379 | writeUInt64(txOut.value); 380 | } else { 381 | writeSlice(txOut.valueBuffer); 382 | } 383 | writeVarSlice(txOut.script); 384 | }); 385 | 386 | if (_ALLOW_WITNESS && hasWitnesses()) { 387 | ins.forEach((txInt) { 388 | writeVector(txInt.witness); 389 | }); 390 | } 391 | 392 | writeUInt32(this.locktime); 393 | // End writeBuffer 394 | 395 | // avoid slicing unless necessary 396 | if (initialOffset != null) return buffer.sublist(initialOffset, offset); 397 | 398 | return buffer; 399 | } 400 | 401 | factory Transaction.clone(Transaction _tx) { 402 | Transaction tx = new Transaction(); 403 | tx.version = _tx.version; 404 | tx.locktime = _tx.locktime; 405 | tx.ins = _tx.ins.map((input) { 406 | return Input.clone(input); 407 | }).toList(); 408 | tx.outs = _tx.outs.map((output) { 409 | return Output.clone(output); 410 | }).toList(); 411 | return tx; 412 | } 413 | 414 | factory Transaction.fromBuffer( 415 | Uint8List buffer, { 416 | bool noStrict = false, 417 | }) { 418 | var offset = 0; 419 | // Any changes made to the ByteData will also change the buffer, and vice versa. 420 | // https://api.dart.dev/stable/2.7.1/dart-typed_data/ByteBuffer/asByteData.html 421 | ByteData bytes = buffer.buffer.asByteData(); 422 | 423 | int readUInt8() { 424 | final i = bytes.getUint8(offset); 425 | offset++; 426 | return i; 427 | } 428 | 429 | int readUInt32() { 430 | final i = bytes.getUint32(offset, Endian.little); 431 | offset += 4; 432 | return i; 433 | } 434 | 435 | int readInt32() { 436 | final i = bytes.getInt32(offset, Endian.little); 437 | offset += 4; 438 | return i; 439 | } 440 | 441 | int readUInt64() { 442 | final i = bytes.getUint64(offset, Endian.little); 443 | offset += 8; 444 | return i; 445 | } 446 | 447 | Uint8List readSlice(n) { 448 | offset += n; 449 | return buffer.sublist(offset - n, offset); 450 | } 451 | 452 | int readVarInt() { 453 | final vi = varuint.decode(buffer, offset); 454 | offset += varuint.encodingLength(vi); 455 | return vi; 456 | } 457 | 458 | Uint8List readVarSlice() { 459 | return readSlice(readVarInt()); 460 | } 461 | 462 | List readVector() { 463 | var count = readVarInt(); 464 | List vector = []; 465 | for (var i = 0; i < count; ++i) { 466 | vector.add(readVarSlice()); 467 | } 468 | return vector; 469 | } 470 | 471 | final tx = new Transaction(); 472 | tx.version = readInt32(); 473 | 474 | final marker = readUInt8(); 475 | final flag = readUInt8(); 476 | 477 | var hasWitnesses = false; 478 | if (marker == ADVANCED_TRANSACTION_MARKER && 479 | flag == ADVANCED_TRANSACTION_FLAG) { 480 | hasWitnesses = true; 481 | } else { 482 | offset -= 2; // Reset offset if not segwit tx 483 | } 484 | 485 | final vinLen = readVarInt(); 486 | for (var i = 0; i < vinLen; ++i) { 487 | tx.ins.add(new Input( 488 | hash: readSlice(32), 489 | index: readUInt32(), 490 | script: readVarSlice(), 491 | sequence: readUInt32())); 492 | } 493 | 494 | final voutLen = readVarInt(); 495 | for (var i = 0; i < voutLen; ++i) { 496 | tx.outs.add(new Output(value: readUInt64(), script: readVarSlice())); 497 | } 498 | 499 | if (hasWitnesses) { 500 | for (var i = 0; i < vinLen; ++i) { 501 | tx.ins[i].witness = readVector(); 502 | } 503 | } 504 | 505 | tx.locktime = readUInt32(); 506 | 507 | if (noStrict) return tx; 508 | 509 | if (offset != buffer.length) 510 | throw new ArgumentError('Transaction has unexpected data'); 511 | 512 | return tx; 513 | } 514 | 515 | factory Transaction.fromHex( 516 | String hex, { 517 | bool noStrict = false, 518 | }) { 519 | return Transaction.fromBuffer( 520 | HEX.decode(hex), 521 | noStrict: noStrict, 522 | ); 523 | } 524 | 525 | @override 526 | String toString() { 527 | this.ins.forEach((txInput) { 528 | print(txInput.toString()); 529 | }); 530 | this.outs.forEach((txOutput) { 531 | print(txOutput.toString()); 532 | }); 533 | } 534 | } 535 | 536 | class Input { 537 | Uint8List hash; 538 | int index; 539 | int sequence; 540 | int value; 541 | Uint8List script; 542 | Uint8List signScript; 543 | Uint8List prevOutScript; 544 | String prevOutType; 545 | bool hasWitness; 546 | List pubkeys; 547 | List signatures; 548 | List witness; 549 | 550 | Input( 551 | {this.hash, 552 | this.index, 553 | this.script, 554 | this.sequence, 555 | this.value, 556 | this.prevOutScript, 557 | this.pubkeys, 558 | this.signatures, 559 | this.witness, 560 | this.prevOutType}) { 561 | this.hasWitness = false; // Default value 562 | if (this.hash != null && !isHash256bit(this.hash)) 563 | throw new ArgumentError('Invalid input hash'); 564 | if (this.index != null && !isUint(this.index, 32)) 565 | throw new ArgumentError('Invalid input index'); 566 | if (this.sequence != null && !isUint(this.sequence, 32)) 567 | throw new ArgumentError('Invalid input sequence'); 568 | if (this.value != null && !isShatoshi(this.value)) 569 | throw ArgumentError('Invalid ouput value'); 570 | } 571 | 572 | factory Input.expandInput(Uint8List scriptSig, List witness, 573 | [String type, Uint8List scriptPubKey]) { 574 | if (type == null || type == '') { 575 | var ssType = classifyInput(scriptSig); 576 | var wsType = classifyWitness(witness); 577 | if (ssType == SCRIPT_TYPES['NONSTANDARD']) ssType = null; 578 | if (wsType == SCRIPT_TYPES['NONSTANDARD']) wsType = null; 579 | type = ssType ?? wsType; 580 | } 581 | if (type == SCRIPT_TYPES['P2WPKH']) { 582 | P2WPKH p2wpkh = new P2WPKH(data: new PaymentData(witness: witness)); 583 | return new Input( 584 | prevOutScript: p2wpkh.data.output, 585 | prevOutType: SCRIPT_TYPES['P2WPKH'], 586 | pubkeys: [p2wpkh.data.pubkey], 587 | signatures: [p2wpkh.data.signature]); 588 | } else if (type == SCRIPT_TYPES['P2PKH']) { 589 | P2PKH p2pkh = new P2PKH(data: new PaymentData(input: scriptSig)); 590 | return new Input( 591 | prevOutScript: p2pkh.data.output, 592 | prevOutType: SCRIPT_TYPES['P2PKH'], 593 | pubkeys: [p2pkh.data.pubkey], 594 | signatures: [p2pkh.data.signature]); 595 | } else if (type == SCRIPT_TYPES['P2PK']) { 596 | P2PK p2pk = new P2PK(data: new PaymentData(input: scriptSig)); 597 | return new Input( 598 | prevOutType: SCRIPT_TYPES['P2PK'], 599 | pubkeys: [], 600 | signatures: [p2pk.data.signature]); 601 | } 602 | } 603 | 604 | factory Input.clone(Input input) { 605 | return new Input( 606 | hash: input.hash != null ? Uint8List.fromList(input.hash) : null, 607 | index: input.index, 608 | script: input.script != null ? Uint8List.fromList(input.script) : null, 609 | sequence: input.sequence, 610 | value: input.value, 611 | prevOutScript: input.prevOutScript != null 612 | ? Uint8List.fromList(input.prevOutScript) 613 | : null, 614 | pubkeys: input.pubkeys != null 615 | ? input.pubkeys.map( 616 | (pubkey) => pubkey != null ? Uint8List.fromList(pubkey) : null) 617 | : null, 618 | signatures: input.signatures != null 619 | ? input.signatures.map((signature) => 620 | signature != null ? Uint8List.fromList(signature) : null) 621 | : null, 622 | ); 623 | } 624 | 625 | @override 626 | String toString() { 627 | return 'Input{hash: $hash, index: $index, sequence: $sequence, value: $value, script: $script, signScript: $signScript, prevOutScript: $prevOutScript, pubkeys: $pubkeys, signatures: $signatures, witness: $witness, prevOutType: $prevOutType}'; 628 | } 629 | } 630 | 631 | class Output { 632 | Uint8List script; 633 | int value; 634 | Uint8List valueBuffer; 635 | List pubkeys; 636 | List signatures; 637 | 638 | Output( 639 | {this.script, 640 | this.value, 641 | this.pubkeys, 642 | this.signatures, 643 | this.valueBuffer}) { 644 | if (value != null && !isShatoshi(value)) 645 | throw ArgumentError('Invalid ouput value'); 646 | } 647 | 648 | factory Output.expandOutput(Uint8List script, [Uint8List ourPubKey]) { 649 | if (ourPubKey == null) return new Output(); 650 | var type = classifyOutput(script); 651 | if (type == SCRIPT_TYPES['P2WPKH']) { 652 | Uint8List wpkh1 = 653 | new P2WPKH(data: new PaymentData(output: script)).data.hash; 654 | Uint8List wpkh2 = bcrypto.hash160(ourPubKey); 655 | if (wpkh1 != wpkh2) throw ArgumentError('Hash mismatch!'); 656 | return new Output(pubkeys: [ourPubKey], signatures: [null]); 657 | } else if (type == SCRIPT_TYPES['P2PKH']) { 658 | Uint8List pkh1 = 659 | new P2PKH(data: new PaymentData(output: script)).data.hash; 660 | Uint8List pkh2 = bcrypto.hash160(ourPubKey); 661 | if (pkh1 != pkh2) throw ArgumentError('Hash mismatch!'); 662 | return new Output(pubkeys: [ourPubKey], signatures: [null]); 663 | } 664 | } 665 | 666 | factory Output.clone(Output output) { 667 | return new Output( 668 | script: output.script != null ? Uint8List.fromList(output.script) : null, 669 | value: output.value, 670 | valueBuffer: output.valueBuffer != null 671 | ? Uint8List.fromList(output.valueBuffer) 672 | : null, 673 | pubkeys: output.pubkeys != null 674 | ? output.pubkeys.map( 675 | (pubkey) => pubkey != null ? Uint8List.fromList(pubkey) : null) 676 | : null, 677 | signatures: output.signatures != null 678 | ? output.signatures.map((signature) => 679 | signature != null ? Uint8List.fromList(signature) : null) 680 | : null, 681 | ); 682 | } 683 | 684 | @override 685 | String toString() { 686 | return 'Output{script: $script, value: $value, valueBuffer: $valueBuffer, pubkeys: $pubkeys, signatures: $signatures}'; 687 | } 688 | } 689 | 690 | bool isCoinbaseHash(Uint8List buffer) { 691 | if (!isHash256bit(buffer)) throw new ArgumentError('Invalid hash'); 692 | for (var i = 0; i < 32; ++i) { 693 | if (buffer[i] != 0) return false; 694 | } 695 | return true; 696 | } 697 | 698 | bool _isP2PKHInput(script) { 699 | final chunks = bscript.decompile(script); 700 | return chunks != null && 701 | chunks.length == 2 && 702 | bscript.isCanonicalScriptSignature(chunks[0]) && 703 | bscript.isCanonicalPubKey(chunks[1]); 704 | } 705 | 706 | bool _isP2PKHOutput(script) { 707 | final buffer = bscript.compile(script); 708 | return buffer.length == 25 && 709 | buffer[0] == OPS['OP_DUP'] && 710 | buffer[1] == OPS['OP_HASH160'] && 711 | buffer[2] == 0x14 && 712 | buffer[23] == OPS['OP_EQUALVERIFY'] && 713 | buffer[24] == OPS['OP_CHECKSIG']; 714 | } 715 | 716 | int varSliceSize(Uint8List someScript) { 717 | final length = someScript.length; 718 | return varuint.encodingLength(length) + length; 719 | } 720 | -------------------------------------------------------------------------------- /test/fixtures/transaction.json: -------------------------------------------------------------------------------- 1 | { 2 | "valid": [ 3 | { 4 | "description": "Standard transaction (1:1)", 5 | "id": "a0ff943d3f644d8832b1fa74be4d0ad2577615dc28a7ef74ff8c271b603a082a", 6 | "hash": "2a083a601b278cff74efa728dc157657d20a4dbe74fab132884d643f3d94ffa0", 7 | "hex": "0100000001f1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe000000006b4830450221008732a460737d956fd94d49a31890b2908f7ed7025a9c1d0f25e43290f1841716022004fa7d608a291d44ebbbebbadaac18f943031e7de39ef3bf9920998c43e60c0401210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ffffffff01a0860100000000001976a914c42e7ef92fdb603af844d064faad95db9bcdfd3d88ac00000000", 8 | "raw": { 9 | "version": 1, 10 | "ins": [ 11 | { 12 | "hash": "f1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe", 13 | "index": 0, 14 | "script": "30450221008732a460737d956fd94d49a31890b2908f7ed7025a9c1d0f25e43290f1841716022004fa7d608a291d44ebbbebbadaac18f943031e7de39ef3bf9920998c43e60c0401 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" 15 | } 16 | ], 17 | "outs": [ 18 | { 19 | "script": "OP_DUP OP_HASH160 c42e7ef92fdb603af844d064faad95db9bcdfd3d OP_EQUALVERIFY OP_CHECKSIG", 20 | "value": 100000 21 | } 22 | ], 23 | "locktime": 0 24 | }, 25 | "coinbase": false, 26 | "virtualSize": 192, 27 | "weight": 768 28 | }, 29 | { 30 | "description": "Standard transaction (2:2)", 31 | "id": "fcdd6d89c43e76dcff94285d9b6e31d5c60cb5e397a76ebc4920befad30907bc", 32 | "hash": "bc0709d3fabe2049bc6ea797e3b50cc6d5316e9b5d2894ffdc763ec4896dddfc", 33 | "hex": "0100000002f1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe000000006b483045022100e661badd8d2cf1af27eb3b82e61b5d3f5d5512084591796ae31487f5b82df948022006df3c2a2cac79f68e4b179f4bbb8185a0bb3c4a2486d4405c59b2ba07a74c2101210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798fffffffff2fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe0100000083483045022100be54a46a44fb7e6bf4ebf348061d0dace7ddcbb92d4147ce181cf4789c7061f0022068ccab2a89a47fc29bb5074bca99ae846ab446eecf3c3aaeb238a13838783c78012102c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee517a9147ccb85f0ab2d599bc17246c98babd5a20b1cdc7687000000800250c30000000000001976a914c42e7ef92fdb603af844d064faad95db9bcdfd3d88acf04902000000000017a9147ccb85f0ab2d599bc17246c98babd5a20b1cdc768700000000", 34 | "raw": { 35 | "version": 1, 36 | "ins": [ 37 | { 38 | "hash": "f1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe", 39 | "index": 0, 40 | "script": "3045022100e661badd8d2cf1af27eb3b82e61b5d3f5d5512084591796ae31487f5b82df948022006df3c2a2cac79f68e4b179f4bbb8185a0bb3c4a2486d4405c59b2ba07a74c2101 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", 41 | "sequence": 4294967295 42 | }, 43 | { 44 | "hash": "f2fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe", 45 | "index": 1, 46 | "script": "3045022100be54a46a44fb7e6bf4ebf348061d0dace7ddcbb92d4147ce181cf4789c7061f0022068ccab2a89a47fc29bb5074bca99ae846ab446eecf3c3aaeb238a13838783c7801 02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 a9147ccb85f0ab2d599bc17246c98babd5a20b1cdc7687", 47 | "sequence": 2147483648 48 | } 49 | ], 50 | "outs": [ 51 | { 52 | "script": "OP_DUP OP_HASH160 c42e7ef92fdb603af844d064faad95db9bcdfd3d OP_EQUALVERIFY OP_CHECKSIG", 53 | "value": 50000 54 | }, 55 | { 56 | "script": "OP_HASH160 7ccb85f0ab2d599bc17246c98babd5a20b1cdc76 OP_EQUAL", 57 | "value": 150000 58 | } 59 | ], 60 | "locktime": 0 61 | }, 62 | "coinbase": false, 63 | "virtualSize": 396, 64 | "weight": 1584 65 | }, 66 | { 67 | "description": "Standard transaction (14:2)", 68 | "id": "39d57bc27f72e904d81f6b5ef7b4e6e17fa33a06b11e5114a43435830d7b5563", 69 | "hash": "63557b0d833534a414511eb1063aa37fe1e6b4f75e6b1fd804e9727fc27bd539", 70 | "hex": "010000000ee7b73e229790c1e79a02f0c871813b3cf26a4156c5b8d942e88b38fe8d3f43a0000000008c493046022100fd3d8fef44fb0962ba3f07bee1d4cafb84e60e38e6c7d9274504b3638a8d2f520221009fce009044e615b6883d4bf62e04c48f9fe236e19d644b082b2f0ae5c98e045c014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffff7bfc005f3880a606027c7cd7dd02a0f6a6572eeb84a91aa158311be13695a7ea010000008b483045022100e2e61c40f26e2510b76dc72ea2f568ec514fce185c719e18bca9caaef2b20e9e02207f1100fc79eb0584e970c7f18fb226f178951d481767b4092d50d13c50ccba8b014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffff0e0f8e6bf951fbb84d7d8ef833a1cbf5bb046ea7251973ac6e7661c755386ee3010000008a473044022048f1611e403710f248f7caf479965a6a5f63cdfbd9a714fef4ec1b68331ade1d022074919e79376c363d4575b2fc21513d5949471703efebd4c5ca2885e810eb1fa4014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffffe6f17f35bf9f0aa7a4242ab3e29edbdb74c5274bf263e53043dddb8045cb585b000000008b483045022100886c07cad489dfcf4b364af561835d5cf985f07adf8bd1d5bd6ddea82b0ce6b2022045bdcbcc2b5fc55191bb997039cf59ff70e8515c56b62f293a9add770ba26738014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffffe6f17f35bf9f0aa7a4242ab3e29edbdb74c5274bf263e53043dddb8045cb585b010000008a4730440220535d49b819fdf294d27d82aff2865ed4e18580f0ca9796d793f611cb43a44f47022019584d5e300c415f642e37ba2a814a1e1106b4a9b91dc2a30fb57ceafe041181014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffffd3051677216ea53baa2e6d7f6a75434ac338438c59f314801c8496d1e6d1bf6d010000008b483045022100bf612b0fa46f49e70ab318ca3458d1ed5f59727aa782f7fac5503f54d9b43a590220358d7ed0e3cee63a5a7e972d9fad41f825d95de2fd0c5560382468610848d489014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffff1e751ccc4e7d973201e9174ec78ece050ef2fadd6a108f40f76a9fa314979c31010000008b483045022006e263d5f73e05c48a603e3bd236e8314e5420721d5e9020114b93e8c9220e1102210099d3dead22f4a792123347a238c87e67b55b28a94a0bb7793144cc7ad94a0168014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffff25c4cf2c61743b3f4252d921d937cca942cf32e4f3fa4a544d0b26f014337084010000008a47304402207d6e87588be47bf2d97eaf427bdd992e9d6b306255711328aee38533366a88b50220623099595ae442cb77eaddb3f91753a4fc9df56fde69cfec584c7f97e05533c8014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffffecd93c87eb43c48481e6694904305349bdea94b01104579fa9f02bff66c89663010000008a473044022020f59498aee0cf82cb113768ef3cb721000346d381ff439adb4d405f791252510220448de723aa59412266fabbc689ec25dc94b1688c27a614982047513a80173514014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffffa1fdc0a79ff98d5b6154176e321c22f4f8450dbd950bd013ad31135f5604411e010000008b48304502210088167867f87327f9c0db0444267ff0b6a026eedd629d8f16fe44a34c18e706bf0220675c8baebf89930e2d6e4463adefc50922653af99375242e38f5ee677418738a014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffffb89e8249c3573b58bf1ec7433185452dd57ab8e1daab01c3cc6ddc8b66ad3de8000000008b4830450220073d50ac5ec8388d5b3906921f9368c31ad078c8e1fb72f26d36b533f35ee327022100c398b23e6692e11dca8a1b64aae2ff70c6a781ed5ee99181b56a2f583a967cd4014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffff45ee07e182084454dacfad1e61b04ffdf9c7b01003060a6c841a01f4fff8a5a0010000008b483045022100991d1bf60c41358f08b20e53718a24e05ac0608915df4f6305a5b47cb61e5da7022003f14fc1cc5b737e2c3279a4f9be1852b49dbb3d9d6cc4c8af6a666f600dced8014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffff4cba12549f1d70f8e60aea8b546c8357f7c099e7c7d9d8691d6ee16e7dfa3170010000008c493046022100f14e2b0ef8a8e206db350413d204bc0a5cd779e556b1191c2d30b5ec023cde6f022100b90b2d2bf256c98a88f7c3a653b93cec7d25bb6a517db9087d11dbd189e8851c014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffffa4b3aed39eb2a1dc6eae4609d9909724e211c153927c230d02bd33add3026959010000008b483045022100a8cebb4f1c58f5ba1af91cb8bd4a2ed4e684e9605f5a9dc8b432ed00922d289d0220251145d2d56f06d936fd0c51fa884b4a6a5fafd0c3318f72fb05a5c9aa372195014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffff0240d52303000000001976a914167c3e1f10cc3b691c73afbdb211e156e3e3f25c88ac15462e00000000001976a914290f7d617b75993e770e5606335fa0999a28d71388ac00000000", 71 | "raw": { 72 | "version": 1, 73 | "locktime": 0, 74 | "ins": [ 75 | { 76 | "hash": "e7b73e229790c1e79a02f0c871813b3cf26a4156c5b8d942e88b38fe8d3f43a0", 77 | "index": 0, 78 | "script": "3046022100fd3d8fef44fb0962ba3f07bee1d4cafb84e60e38e6c7d9274504b3638a8d2f520221009fce009044e615b6883d4bf62e04c48f9fe236e19d644b082b2f0ae5c98e045c01 04aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19f", 79 | "sequence": null 80 | }, 81 | { 82 | "hash": "7bfc005f3880a606027c7cd7dd02a0f6a6572eeb84a91aa158311be13695a7ea", 83 | "index": 1, 84 | "script": "3045022100e2e61c40f26e2510b76dc72ea2f568ec514fce185c719e18bca9caaef2b20e9e02207f1100fc79eb0584e970c7f18fb226f178951d481767b4092d50d13c50ccba8b01 04aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19f" 85 | }, 86 | { 87 | "hash": "0e0f8e6bf951fbb84d7d8ef833a1cbf5bb046ea7251973ac6e7661c755386ee3", 88 | "index": 1, 89 | "script": "3044022048f1611e403710f248f7caf479965a6a5f63cdfbd9a714fef4ec1b68331ade1d022074919e79376c363d4575b2fc21513d5949471703efebd4c5ca2885e810eb1fa401 04aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19f" 90 | }, 91 | { 92 | "hash": "e6f17f35bf9f0aa7a4242ab3e29edbdb74c5274bf263e53043dddb8045cb585b", 93 | "index": 0, 94 | "script": "3045022100886c07cad489dfcf4b364af561835d5cf985f07adf8bd1d5bd6ddea82b0ce6b2022045bdcbcc2b5fc55191bb997039cf59ff70e8515c56b62f293a9add770ba2673801 04aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19f" 95 | }, 96 | { 97 | "hash": "e6f17f35bf9f0aa7a4242ab3e29edbdb74c5274bf263e53043dddb8045cb585b", 98 | "index": 1, 99 | "script": "30440220535d49b819fdf294d27d82aff2865ed4e18580f0ca9796d793f611cb43a44f47022019584d5e300c415f642e37ba2a814a1e1106b4a9b91dc2a30fb57ceafe04118101 04aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19f" 100 | }, 101 | { 102 | "hash": "d3051677216ea53baa2e6d7f6a75434ac338438c59f314801c8496d1e6d1bf6d", 103 | "index": 1, 104 | "script": "3045022100bf612b0fa46f49e70ab318ca3458d1ed5f59727aa782f7fac5503f54d9b43a590220358d7ed0e3cee63a5a7e972d9fad41f825d95de2fd0c5560382468610848d48901 04aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19f" 105 | }, 106 | { 107 | "hash": "1e751ccc4e7d973201e9174ec78ece050ef2fadd6a108f40f76a9fa314979c31", 108 | "index": 1, 109 | "script": "3045022006e263d5f73e05c48a603e3bd236e8314e5420721d5e9020114b93e8c9220e1102210099d3dead22f4a792123347a238c87e67b55b28a94a0bb7793144cc7ad94a016801 04aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19f" 110 | }, 111 | { 112 | "hash": "25c4cf2c61743b3f4252d921d937cca942cf32e4f3fa4a544d0b26f014337084", 113 | "index": 1, 114 | "script": "304402207d6e87588be47bf2d97eaf427bdd992e9d6b306255711328aee38533366a88b50220623099595ae442cb77eaddb3f91753a4fc9df56fde69cfec584c7f97e05533c801 04aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19f" 115 | }, 116 | { 117 | "hash": "ecd93c87eb43c48481e6694904305349bdea94b01104579fa9f02bff66c89663", 118 | "index": 1, 119 | "script": "3044022020f59498aee0cf82cb113768ef3cb721000346d381ff439adb4d405f791252510220448de723aa59412266fabbc689ec25dc94b1688c27a614982047513a8017351401 04aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19f" 120 | }, 121 | { 122 | "hash": "a1fdc0a79ff98d5b6154176e321c22f4f8450dbd950bd013ad31135f5604411e", 123 | "index": 1, 124 | "script": "304502210088167867f87327f9c0db0444267ff0b6a026eedd629d8f16fe44a34c18e706bf0220675c8baebf89930e2d6e4463adefc50922653af99375242e38f5ee677418738a01 04aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19f" 125 | }, 126 | { 127 | "hash": "b89e8249c3573b58bf1ec7433185452dd57ab8e1daab01c3cc6ddc8b66ad3de8", 128 | "index": 0, 129 | "script": "30450220073d50ac5ec8388d5b3906921f9368c31ad078c8e1fb72f26d36b533f35ee327022100c398b23e6692e11dca8a1b64aae2ff70c6a781ed5ee99181b56a2f583a967cd401 04aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19f" 130 | }, 131 | { 132 | "hash": "45ee07e182084454dacfad1e61b04ffdf9c7b01003060a6c841a01f4fff8a5a0", 133 | "index": 1, 134 | "script": "3045022100991d1bf60c41358f08b20e53718a24e05ac0608915df4f6305a5b47cb61e5da7022003f14fc1cc5b737e2c3279a4f9be1852b49dbb3d9d6cc4c8af6a666f600dced801 04aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19f" 135 | }, 136 | { 137 | "hash": "4cba12549f1d70f8e60aea8b546c8357f7c099e7c7d9d8691d6ee16e7dfa3170", 138 | "index": 1, 139 | "script": "3046022100f14e2b0ef8a8e206db350413d204bc0a5cd779e556b1191c2d30b5ec023cde6f022100b90b2d2bf256c98a88f7c3a653b93cec7d25bb6a517db9087d11dbd189e8851c01 04aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19f" 140 | }, 141 | { 142 | "hash": "a4b3aed39eb2a1dc6eae4609d9909724e211c153927c230d02bd33add3026959", 143 | "index": 1, 144 | "script": "3045022100a8cebb4f1c58f5ba1af91cb8bd4a2ed4e684e9605f5a9dc8b432ed00922d289d0220251145d2d56f06d936fd0c51fa884b4a6a5fafd0c3318f72fb05a5c9aa37219501 04aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19f" 145 | } 146 | ], 147 | "outs": [ 148 | { 149 | "value": 52680000, 150 | "script": "OP_DUP OP_HASH160 167c3e1f10cc3b691c73afbdb211e156e3e3f25c OP_EQUALVERIFY OP_CHECKSIG" 151 | }, 152 | { 153 | "value": 3032597, 154 | "script": "OP_DUP OP_HASH160 290f7d617b75993e770e5606335fa0999a28d713 OP_EQUALVERIFY OP_CHECKSIG" 155 | } 156 | ] 157 | }, 158 | "coinbase": false, 159 | "virtualSize": 2596, 160 | "weight": 10384 161 | }, 162 | { 163 | "description": "Coinbase transaction", 164 | "id": "8e070d4eb85eb02e02dd938d6552316b9d723330707870c518064b7a0d232da3", 165 | "hash": "a32d230d7a4b0618c57078703033729d6b3152658d93dd022eb05eb84e0d078e", 166 | "hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff29032832051c4d696e656420627920416e74506f6f6c20626a343a45ef0454c5de8d5e5300004e2c0000ffffffff01414f1995000000001976a914b05793fe86a9f51a5f5ae3a6f07fd31932128a3f88ac00000000", 167 | "raw": { 168 | "version": 1, 169 | "ins": [ 170 | { 171 | "hash": "0000000000000000000000000000000000000000000000000000000000000000", 172 | "index": 4294967295, 173 | "data": "032832051c4d696e656420627920416e74506f6f6c20626a343a45ef0454c5de8d5e5300004e2c0000" 174 | } 175 | ], 176 | "outs": [ 177 | { 178 | "script": "OP_DUP OP_HASH160 b05793fe86a9f51a5f5ae3a6f07fd31932128a3f OP_EQUALVERIFY OP_CHECKSIG", 179 | "value": 2501463873 180 | } 181 | ], 182 | "locktime": 0 183 | }, 184 | "coinbase": true, 185 | "virtualSize": 126, 186 | "weight": 504 187 | }, 188 | { 189 | "description": "Transaction that ignores script chunking rules - Bug #367", 190 | "id": "ebc9fa1196a59e192352d76c0f6e73167046b9d37b8302b6bb6968dfd279b767", 191 | "hash": "67b779d2df6869bbb602837bd3b9467016736e0f6cd75223199ea59611fac9eb", 192 | "hex": "01000000019ac03d5ae6a875d970128ef9086cef276a1919684a6988023cc7254691d97e6d010000006b4830450221009d41dc793ba24e65f571473d40b299b6459087cea1509f0d381740b1ac863cb6022039c425906fcaf51b2b84d8092569fb3213de43abaff2180e2a799d4fcb4dd0aa012102d5ede09a8ae667d0f855ef90325e27f6ce35bbe60a1e6e87af7f5b3c652140fdffffffff080100000000000000010101000000000000000202010100000000000000014c0100000000000000034c02010100000000000000014d0100000000000000044dffff010100000000000000014e0100000000000000064effffffff0100000000", 193 | "raw": { 194 | "version": 1, 195 | "locktime": 0, 196 | "ins": [ 197 | { 198 | "hash": "9ac03d5ae6a875d970128ef9086cef276a1919684a6988023cc7254691d97e6d", 199 | "index": 1, 200 | "script": "30450221009d41dc793ba24e65f571473d40b299b6459087cea1509f0d381740b1ac863cb6022039c425906fcaf51b2b84d8092569fb3213de43abaff2180e2a799d4fcb4dd0aa01 02d5ede09a8ae667d0f855ef90325e27f6ce35bbe60a1e6e87af7f5b3c652140fd" 201 | } 202 | ], 203 | "outs": [ 204 | { 205 | "data": "01", 206 | "value": 1 207 | }, 208 | { 209 | "data": "0201", 210 | "value": 1 211 | }, 212 | { 213 | "data": "4c", 214 | "value": 1 215 | }, 216 | { 217 | "data": "4c0201", 218 | "value": 1 219 | }, 220 | { 221 | "data": "4d", 222 | "value": 1 223 | }, 224 | { 225 | "data": "4dffff01", 226 | "value": 1 227 | }, 228 | { 229 | "data": "4e", 230 | "value": 1 231 | }, 232 | { 233 | "data": "4effffffff01", 234 | "value": 1 235 | } 236 | ] 237 | }, 238 | "coinbase": false, 239 | "virtualSize": 249, 240 | "weight": 996 241 | }, 242 | { 243 | "description": "P2PKH", 244 | "id": "c7a77aeadf529759c8db9b3f205d690cdaed3df0eaf441ead648086e85a6a280", 245 | "hash": "80a2a6856e0848d6ea41f4eaf03dedda0c695d203f9bdbc8599752dfea7aa7c7", 246 | "hex": "010000000176d7b05b96e69d9760bacf14e496ea01085eff32be8f4e08b299eb92057826e5000000006b4830450221009bd6ff2561437155913c289923175d3f114cca1c0e2bc5989315d5261502c2c902201b71ad90dce076a5eb872330ed729e7c2c4bc2d0513efff099dbefb3b62eab4f0121038de63cf582d058a399a176825c045672d5ff8ea25b64d28d4375dcdb14c02b2bffffffff0160ea0000000000001976a914851a33a5ef0d4279bd5854949174e2c65b1d450088ac00000000", 247 | "whex": "", 248 | "raw": { 249 | "version": 1, 250 | "ins": [ 251 | { 252 | "hash": "76d7b05b96e69d9760bacf14e496ea01085eff32be8f4e08b299eb92057826e5", 253 | "index": 0, 254 | "script": "30450221009bd6ff2561437155913c289923175d3f114cca1c0e2bc5989315d5261502c2c902201b71ad90dce076a5eb872330ed729e7c2c4bc2d0513efff099dbefb3b62eab4f01 038de63cf582d058a399a176825c045672d5ff8ea25b64d28d4375dcdb14c02b2b" 255 | } 256 | ], 257 | "outs": [ 258 | { 259 | "value": 60000, 260 | "script": "OP_DUP OP_HASH160 851a33a5ef0d4279bd5854949174e2c65b1d4500 OP_EQUALVERIFY OP_CHECKSIG" 261 | } 262 | ], 263 | "locktime": 0 264 | }, 265 | "coinbase": false, 266 | "virtualSize": 192, 267 | "weight": 768 268 | }, 269 | { 270 | "description": "P2WPKH", 271 | "id": "10bbdf7e9301bc21f1da8684f25fd66bc1314904c30da25a8ebf967c58c89269", 272 | "hash": "6992c8587c96bf8e5aa20dc3044931c16bd65ff28486daf121bc01937edfbb10", 273 | "hex": "010000000133defbe3e28860007ff3e21222774c220cb35d554fa3e3796d25bf8ee983e1080000000000ffffffff0160ea0000000000001976a914851a33a5ef0d4279bd5854949174e2c65b1d450088ac00000000", 274 | "whex": "0100000000010133defbe3e28860007ff3e21222774c220cb35d554fa3e3796d25bf8ee983e1080000000000ffffffff0160ea0000000000001976a914851a33a5ef0d4279bd5854949174e2c65b1d450088ac0248304502210097c3006f0b390982eb47f762b2853773c6cedf83668a22d710f4c13c4fd6b15502205e26ef16a81fc818a37f3a34fc6d0700e61100ea6c6773907c9c046042c440340121038de63cf582d058a399a176825c045672d5ff8ea25b64d28d4375dcdb14c02b2b00000000", 275 | "raw": { 276 | "version": 1, 277 | "ins": [ 278 | { 279 | "hash": "33defbe3e28860007ff3e21222774c220cb35d554fa3e3796d25bf8ee983e108", 280 | "index": 0, 281 | "script": "", 282 | "witness": [ 283 | "304502210097c3006f0b390982eb47f762b2853773c6cedf83668a22d710f4c13c4fd6b15502205e26ef16a81fc818a37f3a34fc6d0700e61100ea6c6773907c9c046042c4403401", 284 | "038de63cf582d058a399a176825c045672d5ff8ea25b64d28d4375dcdb14c02b2b" 285 | ] 286 | } 287 | ], 288 | "outs": [ 289 | { 290 | "value": 60000, 291 | "script": "OP_DUP OP_HASH160 851a33a5ef0d4279bd5854949174e2c65b1d4500 OP_EQUALVERIFY OP_CHECKSIG" 292 | } 293 | ], 294 | "locktime": 0 295 | }, 296 | "coinbase": false, 297 | "virtualSize": 113, 298 | "weight": 450 299 | }, 300 | { 301 | "description": "Coinbase transaction w/ witness", 302 | "id": "c881f7b084a367b0603abbcb9c5c639318e6166770e3f9b27a1ee3f8b6a16517", 303 | "hash": "1765a1b6f8e31e7ab2f9e3706716e61893635c9ccbbb3a60b067a384b0f781c8", 304 | "hex": "02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff05022b020101ffffffff02c0cf402500000000232103c6c5964853fd00fb3271ac002831c66825102d223c706ce0ee99e73db3be4aa1ac0000000000000000266a24aa21a9edff828eb21f40ab251d9f107792670aba9299028b894a364fda570f6a089dcfe900000000", 305 | "whex": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff05022b020101ffffffff02c0cf402500000000232103c6c5964853fd00fb3271ac002831c66825102d223c706ce0ee99e73db3be4aa1ac0000000000000000266a24aa21a9edff828eb21f40ab251d9f107792670aba9299028b894a364fda570f6a089dcfe90120000000000000000000000000000000000000000000000000000000000000000000000000", 306 | "raw": { 307 | "version": 2, 308 | "ins": [ 309 | { 310 | "hash": "0000000000000000000000000000000000000000000000000000000000000000", 311 | "index": 4294967295, 312 | "data": "022b020101", 313 | "sequence": 4294967295, 314 | "witness": [ 315 | "0000000000000000000000000000000000000000000000000000000000000000" 316 | ] 317 | } 318 | ], 319 | "outs": [ 320 | { 321 | "script": "03c6c5964853fd00fb3271ac002831c66825102d223c706ce0ee99e73db3be4aa1 OP_CHECKSIG", 322 | "value": 625004480 323 | }, 324 | { 325 | "script": "OP_RETURN aa21a9edff828eb21f40ab251d9f107792670aba9299028b894a364fda570f6a089dcfe9", 326 | "value": 0 327 | } 328 | ], 329 | "locktime": 0 330 | }, 331 | "coinbase": true, 332 | "virtualSize": 156, 333 | "weight": 624 334 | } 335 | ], 336 | "hashForSignature": [ 337 | { 338 | "description": "Out of range inIndex", 339 | "txHex": "010000000200000000000000000000000000000000000000000000000000000000000000000000000000ffffffff00000000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000000000000000", 340 | "inIndex": 2, 341 | "script": "OP_0", 342 | "type": 0, 343 | "hash": "0000000000000000000000000000000000000000000000000000000000000001" 344 | }, 345 | { 346 | "description": "inIndex > nOutputs (SIGHASH_SINGLE)", 347 | "txHex": "010000000200000000000000000000000000000000000000000000000000000000000000000000000000ffffffff00000000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000000000000000", 348 | "inIndex": 2, 349 | "script": "OP_0", 350 | "type": 3, 351 | "hash": "0000000000000000000000000000000000000000000000000000000000000001" 352 | }, 353 | { 354 | "txHex": "010000000200000000000000000000000000000000000000000000000000000000000000000000000000ffffffff00000000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000000000000000", 355 | "inIndex": 0, 356 | "script": "OP_0 OP_3", 357 | "type": 0, 358 | "hash": "3d56a632462b9fc9b89eeddcad7dbe476297f34aff7e5f9320e2a99fb5e97136" 359 | }, 360 | { 361 | "txHex": "010000000200000000000000000000000000000000000000000000000000000000000000000000000000ffffffff00000000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000000000000000", 362 | "inIndex": 0, 363 | "script": "OP_0 OP_CODESEPARATOR OP_3", 364 | "type": 0, 365 | "hash": "3d56a632462b9fc9b89eeddcad7dbe476297f34aff7e5f9320e2a99fb5e97136" 366 | }, 367 | { 368 | "txHex": "010000000200000000000000000000000000000000000000000000000000000000000000000000000000ffffffff00000000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000000000000000", 369 | "inIndex": 0, 370 | "script": "OP_0 OP_CODESEPARATOR OP_4", 371 | "type": 0, 372 | "hash": "fa075877cb54916236806a6562e4a8cdad48adf1268e73d72d1f9fdd867df463" 373 | } 374 | ], 375 | "invalid": { 376 | "addInput": [ 377 | { 378 | "exception": "Invalid input hash", 379 | "hash": "0aed1366a73b6057ee7800d737bff1bdf8c448e98d86bc0998f2b009816d", 380 | "index": 0 381 | }, 382 | { 383 | "exception": "Invalid input hash", 384 | "hash": "0aed1366a73b6057ee7800d737bff1bdf8c448e98d86bc0998f2b009816da9b0ffff", 385 | "index": 0 386 | } 387 | ], 388 | "fromBuffer": [ 389 | { 390 | "exception": "Transaction has unexpected data", 391 | "hex": "0100000002f1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe000000006b483045022100e661badd8d2cf1af27eb3b82e61b5d3f5d5512084591796ae31487f5b82df948022006df3c2a2cac79f68e4b179f4bbb8185a0bb3c4a2486d4405c59b2ba07a74c2101210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798fffffffff2fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe0100000083483045022100be54a46a44fb7e6bf4ebf348061d0dace7ddcbb92d4147ce181cf4789c7061f0022068ccab2a89a47fc29bb5074bca99ae846ab446eecf3c3aaeb238a13838783c78012102c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee517a9147ccb85f0ab2d599bc17246c98babd5a20b1cdc7687ffffffff0250c30000000000001976a914c42e7ef92fdb603af844d064faad95db9bcdfd3d88acf04902000000000017a9147ccb85f0ab2d599bc17246c98babd5a20b1cdc768700000000ffffffff" 392 | } 393 | ] 394 | } 395 | } 396 | -------------------------------------------------------------------------------- /test/fixtures/transaction_builder.json: -------------------------------------------------------------------------------- 1 | { 2 | "valid": { 3 | "build": [ 4 | { 5 | "description": "Transaction w/ P2PKH -> P2PKH", 6 | "txHex": "0100000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000006b483045022100a3b254e1c10b5d039f36c05f323995d6e5a367d98dd78a13d5bbc3991b35720e022022fccea3897d594de0689601fbd486588d5bfa6915be2386db0397ee9a6e80b601210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ffffffff0110270000000000001976a914aa4d7985c57e011a8b3dd8e0e5a73aaef41629c588ac00000000", 7 | "version": 1, 8 | "inputs": [ 9 | { 10 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 11 | "vout": 0, 12 | "signs": [ 13 | { 14 | "keyPair": "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn" 15 | } 16 | ] 17 | } 18 | ], 19 | "outputs": [ 20 | { 21 | "script": "OP_DUP OP_HASH160 aa4d7985c57e011a8b3dd8e0e5a73aaef41629c5 OP_EQUALVERIFY OP_CHECKSIG", 22 | "value": 10000 23 | } 24 | ] 25 | }, 26 | { 27 | "description": "Transaction w/ P2PKH -> P2WSH", 28 | "txHex": "0100000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000006a473044022056c99ba23eb15b3e666b188f87b04d5ef23eeda5298939cdaec35a3bddf3835602205887a5a460f299819b0c93948fafab8b2d64d8c051934431e3bb9acebef5d1b001210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ffffffff01102700000000000022002032447752937d355ca2defddcd1f6b4fc53d182f8901cebbcff42f5e381bf0b8000000000", 29 | "version": 1, 30 | "inputs": [ 31 | { 32 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 33 | "vout": 0, 34 | "signs": [ 35 | { 36 | "prevOutScriptType": "p2pkh", 37 | "keyPair": "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn" 38 | } 39 | ] 40 | } 41 | ], 42 | "outputs": [ 43 | { 44 | "script": "OP_0 32447752937d355ca2defddcd1f6b4fc53d182f8901cebbcff42f5e381bf0b80", 45 | "value": 10000 46 | } 47 | ] 48 | }, 49 | { 50 | "description": "P2WPKH -> P2WPKH", 51 | "txHex": "01000000000101ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff011027000000000000160014aa4d7985c57e011a8b3dd8e0e5a73aaef41629c502483045022100a8fc5e4c6d7073474eff2af5d756966e75be0cdfbba299518526080ce8b584be02200f26d41082764df89e3c815b8eaf51034a3b68a25f1be51208f54222c1bb6c1601210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179800000000", 52 | "version": 1, 53 | "inputs": [ 54 | { 55 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 56 | "vout": 0, 57 | "prevTxScript": "OP_0 751e76e8199196d454941c45d1b3a323f1433bd6", 58 | "signs": [ 59 | { 60 | "prevOutScriptType": "p2wpkh", 61 | "keyPair": "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn", 62 | "value": 10000 63 | } 64 | ] 65 | } 66 | ], 67 | "outputs": [ 68 | { 69 | "script": "OP_0 aa4d7985c57e011a8b3dd8e0e5a73aaef41629c5", 70 | "value": 10000 71 | } 72 | ] 73 | }, 74 | { 75 | "description": "SIGHASH SINGLE (random)", 76 | "txHex": "01000000012ffb29d53528ad30c37c267fbbeda3c6fce08f5f6f5d3b1eab22193599a3612a010000006b483045022100f963f1d9564075a934d7c3cfa333bd1378859b84cba947e149926fc9ec89b5ae02202b5b912e507bae65002aff972f9752e2aeb2e22c5fdbaaad672090378184df37032102f1c7eac9200f8dee7e34e59318ff2076c8b3e3ac7f43121e57569a1aec1803d4ffffffff0260a62f01000000001976a9140de1f9b92d2ab6d8ead83f9a0ff5cf518dcb03b888ac80969800000000001976a91454d0e925d5ee0ee26768a237067dee793d01a70688ac00000000", 77 | "version": 1, 78 | "inputs": [ 79 | { 80 | "txId": "2a61a399351922ab1e3b5d6f5f8fe0fcc6a3edbb7f267cc330ad2835d529fb2f", 81 | "vout": 1, 82 | "signs": [ 83 | { 84 | "keyPair": "KzRGFiqhXB7SyX6idHQkt77B8mX7adnujdg3VG47jdVK2x4wbUYg", 85 | "hashType": 3, 86 | "value": 30000000 87 | } 88 | ], 89 | "sequence": 4294967295, 90 | "prevTxScript": "OP_DUP OP_HASH160 0de1f9b92d2ab6d8ead83f9a0ff5cf518dcb03b8 OP_EQUALVERIFY OP_CHECKSIG" 91 | } 92 | ], 93 | "outputs": [ 94 | { 95 | "value": 19900000, 96 | "script": "OP_DUP OP_HASH160 0de1f9b92d2ab6d8ead83f9a0ff5cf518dcb03b8 OP_EQUALVERIFY OP_CHECKSIG" 97 | }, 98 | { 99 | "value": 10000000, 100 | "script": "OP_DUP OP_HASH160 54d0e925d5ee0ee26768a237067dee793d01a706 OP_EQUALVERIFY OP_CHECKSIG" 101 | } 102 | ] 103 | }, 104 | { 105 | "description": "SIGHASH ALL", 106 | "txHex": "01000000037db7f0b2a345ded6ddf28da3211a7d7a95a2943e9a879493d6481b7d69613f04010000006a47304402206abb0622b8b6ca83f1f4de84830cf38bf4615dc9e47a7dcdcc489905f26aa9cb02201d2d8a7815242b88e4cd66390ca46da802238f9b1395e0d118213d30dad38184012102f1c7eac9200f8dee7e34e59318ff2076c8b3e3ac7f43121e57569a1aec1803d4ffffffff652c491e5a781a6a3c547fa8d980741acbe4623ae52907278f10e1f064f67e05000000006b483045022100de13b42804f87a09bb46def12ab4608108d8c2db41db4bc09064f9c46fcf493102205e5c759ab7b2895c9b0447e56029f6895ff7bb20e0847c564a88a3cfcf080c4f012102f1c7eac9200f8dee7e34e59318ff2076c8b3e3ac7f43121e57569a1aec1803d4ffffffffb9fa270fa3e4dd8c79f9cbfe5f1953cba071ed081f7c277a49c33466c695db35000000006b4830450221009100a3f5b30182d1cb0172792af6947b6d8d42badb0539f2c209aece5a0628f002200ae91702ca63347e344c85fcb536f30ee97b75cdf4900de534ed5e040e71a548012102f1c7eac9200f8dee7e34e59318ff2076c8b3e3ac7f43121e57569a1aec1803d4ffffffff03204e0000000000001976a9149ed1f577c60e4be1dbf35318ec12f51d25e8577388ac30750000000000001976a914fb407e88c48921d5547d899e18a7c0a36919f54d88ac50c30000000000001976a91404ccb4eed8cfa9f6e394e945178960f5ccddb38788ac00000000", 107 | "version": 1, 108 | "inputs": [ 109 | { 110 | "txId": "043f61697d1b48d69394879a3e94a2957a7d1a21a38df2ddd6de45a3b2f0b77d", 111 | "vout": 1, 112 | "signs": [ 113 | { 114 | "keyPair": "KzRGFiqhXB7SyX6idHQkt77B8mX7adnujdg3VG47jdVK2x4wbUYg", 115 | "hashType": 1, 116 | "value": 40000 117 | } 118 | ], 119 | "sequence": 4294967295, 120 | "prevTxScript": "OP_DUP OP_HASH160 0de1f9b92d2ab6d8ead83f9a0ff5cf518dcb03b8 OP_EQUALVERIFY OP_CHECKSIG" 121 | }, 122 | { 123 | "txId": "057ef664f0e1108f270729e53a62e4cb1a7480d9a87f543c6a1a785a1e492c65", 124 | "vout": 0, 125 | "signs": [ 126 | { 127 | "keyPair": "KzRGFiqhXB7SyX6idHQkt77B8mX7adnujdg3VG47jdVK2x4wbUYg", 128 | "hashType": 1, 129 | "value": 40000 130 | } 131 | ], 132 | "sequence": 4294967295, 133 | "prevTxScript": "OP_DUP OP_HASH160 0de1f9b92d2ab6d8ead83f9a0ff5cf518dcb03b8 OP_EQUALVERIFY OP_CHECKSIG" 134 | }, 135 | { 136 | "txId": "35db95c66634c3497a277c1f08ed71a0cb53195ffecbf9798cdde4a30f27fab9", 137 | "vout": 0, 138 | "signs": [ 139 | { 140 | "keyPair": "KzRGFiqhXB7SyX6idHQkt77B8mX7adnujdg3VG47jdVK2x4wbUYg", 141 | "hashType": 1, 142 | "value": 40000 143 | } 144 | ], 145 | "sequence": 4294967295, 146 | "prevTxScript": "OP_DUP OP_HASH160 0de1f9b92d2ab6d8ead83f9a0ff5cf518dcb03b8 OP_EQUALVERIFY OP_CHECKSIG" 147 | } 148 | ], 149 | "outputs": [ 150 | { 151 | "value": 20000, 152 | "script": "OP_DUP OP_HASH160 9ed1f577c60e4be1dbf35318ec12f51d25e85773 OP_EQUALVERIFY OP_CHECKSIG" 153 | }, 154 | { 155 | "value": 30000, 156 | "script": "OP_DUP OP_HASH160 fb407e88c48921d5547d899e18a7c0a36919f54d OP_EQUALVERIFY OP_CHECKSIG" 157 | }, 158 | { 159 | "value": 50000, 160 | "script": "OP_DUP OP_HASH160 04ccb4eed8cfa9f6e394e945178960f5ccddb387 OP_EQUALVERIFY OP_CHECKSIG" 161 | } 162 | ] 163 | }, 164 | { 165 | "description": "SIGHASH ALL | ANYONECANPAY", 166 | "txHex": "01000000037db7f0b2a345ded6ddf28da3211a7d7a95a2943e9a879493d6481b7d69613f04010000006b483045022100bd2829550e9b3a081747281029b5f5a96bbd83bb6a92fa2f8310f1bd0d53abc90220071b469417c55cdb3b04171fd7900d2768981b7ab011553d84d24ea85d277079812102f1c7eac9200f8dee7e34e59318ff2076c8b3e3ac7f43121e57569a1aec1803d4ffffffff652c491e5a781a6a3c547fa8d980741acbe4623ae52907278f10e1f064f67e05000000006a47304402206295e17c45c6356ffb20365b696bcbb869db7e8697f4b8a684098ee2bff85feb02202905c441abe39ec9c480749236b84fdd3ebd91ecd25b559136370aacfcf2815c812102f1c7eac9200f8dee7e34e59318ff2076c8b3e3ac7f43121e57569a1aec1803d4ffffffffb9fa270fa3e4dd8c79f9cbfe5f1953cba071ed081f7c277a49c33466c695db35000000006b483045022100f58e7c98ac8412944d575bcdece0e5966d4018f05988b5b60b6f46b8cb7a543102201c5854d3361e29b58123f34218cec2c722f5ec7a08235ebd007ec637b07c193a812102f1c7eac9200f8dee7e34e59318ff2076c8b3e3ac7f43121e57569a1aec1803d4ffffffff03204e0000000000001976a9149ed1f577c60e4be1dbf35318ec12f51d25e8577388ac30750000000000001976a914fb407e88c48921d5547d899e18a7c0a36919f54d88ac50c30000000000001976a91404ccb4eed8cfa9f6e394e945178960f5ccddb38788ac00000000", 167 | "version": 1, 168 | "inputs": [ 169 | { 170 | "txId": "043f61697d1b48d69394879a3e94a2957a7d1a21a38df2ddd6de45a3b2f0b77d", 171 | "vout": 1, 172 | "signs": [ 173 | { 174 | "keyPair": "KzRGFiqhXB7SyX6idHQkt77B8mX7adnujdg3VG47jdVK2x4wbUYg", 175 | "hashType": 129, 176 | "value": 40000 177 | } 178 | ], 179 | "sequence": 4294967295, 180 | "prevTxScript": "OP_DUP OP_HASH160 0de1f9b92d2ab6d8ead83f9a0ff5cf518dcb03b8 OP_EQUALVERIFY OP_CHECKSIG" 181 | }, 182 | { 183 | "txId": "057ef664f0e1108f270729e53a62e4cb1a7480d9a87f543c6a1a785a1e492c65", 184 | "vout": 0, 185 | "signs": [ 186 | { 187 | "keyPair": "KzRGFiqhXB7SyX6idHQkt77B8mX7adnujdg3VG47jdVK2x4wbUYg", 188 | "hashType": 129, 189 | "value": 40000 190 | } 191 | ], 192 | "sequence": 4294967295, 193 | "prevTxScript": "OP_DUP OP_HASH160 0de1f9b92d2ab6d8ead83f9a0ff5cf518dcb03b8 OP_EQUALVERIFY OP_CHECKSIG" 194 | }, 195 | { 196 | "txId": "35db95c66634c3497a277c1f08ed71a0cb53195ffecbf9798cdde4a30f27fab9", 197 | "vout": 0, 198 | "signs": [ 199 | { 200 | "keyPair": "KzRGFiqhXB7SyX6idHQkt77B8mX7adnujdg3VG47jdVK2x4wbUYg", 201 | "hashType": 129, 202 | "value": 40000 203 | } 204 | ], 205 | "sequence": 4294967295, 206 | "prevTxScript": "OP_DUP OP_HASH160 0de1f9b92d2ab6d8ead83f9a0ff5cf518dcb03b8 OP_EQUALVERIFY OP_CHECKSIG" 207 | } 208 | ], 209 | "outputs": [ 210 | { 211 | "value": 20000, 212 | "script": "OP_DUP OP_HASH160 9ed1f577c60e4be1dbf35318ec12f51d25e85773 OP_EQUALVERIFY OP_CHECKSIG" 213 | }, 214 | { 215 | "value": 30000, 216 | "script": "OP_DUP OP_HASH160 fb407e88c48921d5547d899e18a7c0a36919f54d OP_EQUALVERIFY OP_CHECKSIG" 217 | }, 218 | { 219 | "value": 50000, 220 | "script": "OP_DUP OP_HASH160 04ccb4eed8cfa9f6e394e945178960f5ccddb387 OP_EQUALVERIFY OP_CHECKSIG" 221 | } 222 | ] 223 | }, 224 | { 225 | "description": "SIGHASH SINGLE", 226 | "txHex": "01000000037db7f0b2a345ded6ddf28da3211a7d7a95a2943e9a879493d6481b7d69613f04010000006b483045022100e822f152bb15a1d623b91913cd0fb915e9f85a8dc6c26d51948208bbc0218e800220255f78549d9614c88eac9551429bc00224f22cdcb41a3af70d52138f7e98d333032102f1c7eac9200f8dee7e34e59318ff2076c8b3e3ac7f43121e57569a1aec1803d4ffffffff652c491e5a781a6a3c547fa8d980741acbe4623ae52907278f10e1f064f67e05000000006a47304402206f37f79adeb86e0e2da679f79ff5c3ba206c6d35cd9a21433f0de34ee83ddbc00220118cabbac5d83b3aa4c2dc01b061e4b2fe83750d85a72ae6a1752300ee5d9aff032102f1c7eac9200f8dee7e34e59318ff2076c8b3e3ac7f43121e57569a1aec1803d4ffffffffb9fa270fa3e4dd8c79f9cbfe5f1953cba071ed081f7c277a49c33466c695db35000000006a473044022042ac843d220a56b3de05f24c85a63e71efa7e5fc7c2ec766a2ffae82a88572b0022051a816b317313ea8d90010a77c3e02d41da4a500e67e6a5347674f836f528d82032102f1c7eac9200f8dee7e34e59318ff2076c8b3e3ac7f43121e57569a1aec1803d4ffffffff03204e0000000000001976a9149ed1f577c60e4be1dbf35318ec12f51d25e8577388ac30750000000000001976a914fb407e88c48921d5547d899e18a7c0a36919f54d88ac50c30000000000001976a91404ccb4eed8cfa9f6e394e945178960f5ccddb38788ac00000000", 227 | "version": 1, 228 | "inputs": [ 229 | { 230 | "txId": "043f61697d1b48d69394879a3e94a2957a7d1a21a38df2ddd6de45a3b2f0b77d", 231 | "vout": 1, 232 | "signs": [ 233 | { 234 | "keyPair": "KzRGFiqhXB7SyX6idHQkt77B8mX7adnujdg3VG47jdVK2x4wbUYg", 235 | "hashType": 3, 236 | "value": 40000 237 | } 238 | ], 239 | "sequence": 4294967295, 240 | "prevTxScript": "OP_DUP OP_HASH160 0de1f9b92d2ab6d8ead83f9a0ff5cf518dcb03b8 OP_EQUALVERIFY OP_CHECKSIG" 241 | }, 242 | { 243 | "txId": "057ef664f0e1108f270729e53a62e4cb1a7480d9a87f543c6a1a785a1e492c65", 244 | "vout": 0, 245 | "signs": [ 246 | { 247 | "keyPair": "KzRGFiqhXB7SyX6idHQkt77B8mX7adnujdg3VG47jdVK2x4wbUYg", 248 | "hashType": 3, 249 | "value": 40000 250 | } 251 | ], 252 | "sequence": 4294967295, 253 | "prevTxScript": "OP_DUP OP_HASH160 0de1f9b92d2ab6d8ead83f9a0ff5cf518dcb03b8 OP_EQUALVERIFY OP_CHECKSIG" 254 | }, 255 | { 256 | "txId": "35db95c66634c3497a277c1f08ed71a0cb53195ffecbf9798cdde4a30f27fab9", 257 | "vout": 0, 258 | "signs": [ 259 | { 260 | "keyPair": "KzRGFiqhXB7SyX6idHQkt77B8mX7adnujdg3VG47jdVK2x4wbUYg", 261 | "hashType": 3, 262 | "value": 40000 263 | } 264 | ], 265 | "sequence": 4294967295, 266 | "prevTxScript": "OP_DUP OP_HASH160 0de1f9b92d2ab6d8ead83f9a0ff5cf518dcb03b8 OP_EQUALVERIFY OP_CHECKSIG" 267 | } 268 | ], 269 | "outputs": [ 270 | { 271 | "value": 20000, 272 | "script": "OP_DUP OP_HASH160 9ed1f577c60e4be1dbf35318ec12f51d25e85773 OP_EQUALVERIFY OP_CHECKSIG" 273 | }, 274 | { 275 | "value": 30000, 276 | "script": "OP_DUP OP_HASH160 fb407e88c48921d5547d899e18a7c0a36919f54d OP_EQUALVERIFY OP_CHECKSIG" 277 | }, 278 | { 279 | "value": 50000, 280 | "script": "OP_DUP OP_HASH160 04ccb4eed8cfa9f6e394e945178960f5ccddb387 OP_EQUALVERIFY OP_CHECKSIG" 281 | } 282 | ] 283 | }, 284 | { 285 | "description": "SIGHASH SINGLE|ANYONECANPAY", 286 | "txHex": "01000000037db7f0b2a345ded6ddf28da3211a7d7a95a2943e9a879493d6481b7d69613f04010000006b483045022100d05a3b6cf2f0301000b0e45c09054f2c61570ce8798ebf571eef72da3b1c94a1022016d7ef3c133fa703bae2c75158ea08d335ac698506f99b3c369c37a9e8fc4beb832102f1c7eac9200f8dee7e34e59318ff2076c8b3e3ac7f43121e57569a1aec1803d4ffffffff652c491e5a781a6a3c547fa8d980741acbe4623ae52907278f10e1f064f67e05000000006b483045022100ee6bf07b051001dcbfa062692a40adddd070303286b714825b3fb4693dd8fcdb022056610885e5053e5d47f2be3433051305abe7978ead8f7cf2d0368947aff6b307832102f1c7eac9200f8dee7e34e59318ff2076c8b3e3ac7f43121e57569a1aec1803d4ffffffffb9fa270fa3e4dd8c79f9cbfe5f1953cba071ed081f7c277a49c33466c695db35000000006b483045022100cfc930d5b5272d0220d9da98fabec97b9e66306f735efa837f43f6adc675cad902202f9dff76b8b9ec8f613d46094f17f64d875804292d8804aa59fd295b6fc1416b832102f1c7eac9200f8dee7e34e59318ff2076c8b3e3ac7f43121e57569a1aec1803d4ffffffff03204e0000000000001976a9149ed1f577c60e4be1dbf35318ec12f51d25e8577388ac30750000000000001976a914fb407e88c48921d5547d899e18a7c0a36919f54d88ac50c30000000000001976a91404ccb4eed8cfa9f6e394e945178960f5ccddb38788ac00000000", 287 | "version": 1, 288 | "inputs": [ 289 | { 290 | "txId": "043f61697d1b48d69394879a3e94a2957a7d1a21a38df2ddd6de45a3b2f0b77d", 291 | "vout": 1, 292 | "signs": [ 293 | { 294 | "keyPair": "KzRGFiqhXB7SyX6idHQkt77B8mX7adnujdg3VG47jdVK2x4wbUYg", 295 | "hashType": 131, 296 | "value": 40000 297 | } 298 | ], 299 | "sequence": 4294967295, 300 | "prevTxScript": "OP_DUP OP_HASH160 0de1f9b92d2ab6d8ead83f9a0ff5cf518dcb03b8 OP_EQUALVERIFY OP_CHECKSIG" 301 | }, 302 | { 303 | "txId": "057ef664f0e1108f270729e53a62e4cb1a7480d9a87f543c6a1a785a1e492c65", 304 | "vout": 0, 305 | "signs": [ 306 | { 307 | "keyPair": "KzRGFiqhXB7SyX6idHQkt77B8mX7adnujdg3VG47jdVK2x4wbUYg", 308 | "hashType": 131, 309 | "value": 40000 310 | } 311 | ], 312 | "sequence": 4294967295, 313 | "prevTxScript": "OP_DUP OP_HASH160 0de1f9b92d2ab6d8ead83f9a0ff5cf518dcb03b8 OP_EQUALVERIFY OP_CHECKSIG" 314 | }, 315 | { 316 | "txId": "35db95c66634c3497a277c1f08ed71a0cb53195ffecbf9798cdde4a30f27fab9", 317 | "vout": 0, 318 | "signs": [ 319 | { 320 | "keyPair": "KzRGFiqhXB7SyX6idHQkt77B8mX7adnujdg3VG47jdVK2x4wbUYg", 321 | "hashType": 131, 322 | "value": 40000 323 | } 324 | ], 325 | "sequence": 4294967295, 326 | "prevTxScript": "OP_DUP OP_HASH160 0de1f9b92d2ab6d8ead83f9a0ff5cf518dcb03b8 OP_EQUALVERIFY OP_CHECKSIG" 327 | } 328 | ], 329 | "outputs": [ 330 | { 331 | "value": 20000, 332 | "script": "OP_DUP OP_HASH160 9ed1f577c60e4be1dbf35318ec12f51d25e85773 OP_EQUALVERIFY OP_CHECKSIG" 333 | }, 334 | { 335 | "value": 30000, 336 | "script": "OP_DUP OP_HASH160 fb407e88c48921d5547d899e18a7c0a36919f54d OP_EQUALVERIFY OP_CHECKSIG" 337 | }, 338 | { 339 | "value": 50000, 340 | "script": "OP_DUP OP_HASH160 04ccb4eed8cfa9f6e394e945178960f5ccddb387 OP_EQUALVERIFY OP_CHECKSIG" 341 | } 342 | ] 343 | }, 344 | { 345 | "description": "SIGHASH NONE", 346 | "txHex": "01000000037db7f0b2a345ded6ddf28da3211a7d7a95a2943e9a879493d6481b7d69613f04010000006b483045022100e7f0a1ddd2c0b81e093e029b8a503afa27fe43549b0668d2141abf35eb3a63be022037f12d12cd50fc94a135f933406a8937557de9b9566a8841ff1548c1b6984531022102f1c7eac9200f8dee7e34e59318ff2076c8b3e3ac7f43121e57569a1aec1803d4ffffffff652c491e5a781a6a3c547fa8d980741acbe4623ae52907278f10e1f064f67e05000000006a473044022008451123ec2535dab545ade9d697519e63b28df5e311ea05e0ce28d39877a7c8022061ce5dbfb7ab478dd9e05b0acfd959ac3eb2641f61958f5d352f37621073d7c0022102f1c7eac9200f8dee7e34e59318ff2076c8b3e3ac7f43121e57569a1aec1803d4ffffffffb9fa270fa3e4dd8c79f9cbfe5f1953cba071ed081f7c277a49c33466c695db35000000006a47304402205c001bcdfb35c70d8aa3bdbc75399afb72eb7cf1926ca7c1dfcddcb4d4d3e0f8022028992fffdcd4e9f34ab726f97c24157917641c2ef99361f588e3d4147d46eea5022102f1c7eac9200f8dee7e34e59318ff2076c8b3e3ac7f43121e57569a1aec1803d4ffffffff03204e0000000000001976a9149ed1f577c60e4be1dbf35318ec12f51d25e8577388ac30750000000000001976a914fb407e88c48921d5547d899e18a7c0a36919f54d88ac50c30000000000001976a91404ccb4eed8cfa9f6e394e945178960f5ccddb38788ac00000000", 347 | "version": 1, 348 | "inputs": [ 349 | { 350 | "txId": "043f61697d1b48d69394879a3e94a2957a7d1a21a38df2ddd6de45a3b2f0b77d", 351 | "vout": 1, 352 | "signs": [ 353 | { 354 | "keyPair": "KzRGFiqhXB7SyX6idHQkt77B8mX7adnujdg3VG47jdVK2x4wbUYg", 355 | "hashType": 2, 356 | "value": 40000 357 | } 358 | ], 359 | "sequence": 4294967295, 360 | "prevTxScript": "OP_DUP OP_HASH160 0de1f9b92d2ab6d8ead83f9a0ff5cf518dcb03b8 OP_EQUALVERIFY OP_CHECKSIG" 361 | }, 362 | { 363 | "txId": "057ef664f0e1108f270729e53a62e4cb1a7480d9a87f543c6a1a785a1e492c65", 364 | "vout": 0, 365 | "signs": [ 366 | { 367 | "keyPair": "KzRGFiqhXB7SyX6idHQkt77B8mX7adnujdg3VG47jdVK2x4wbUYg", 368 | "hashType": 2, 369 | "value": 40000 370 | } 371 | ], 372 | "sequence": 4294967295, 373 | "prevTxScript": "OP_DUP OP_HASH160 0de1f9b92d2ab6d8ead83f9a0ff5cf518dcb03b8 OP_EQUALVERIFY OP_CHECKSIG" 374 | }, 375 | { 376 | "txId": "35db95c66634c3497a277c1f08ed71a0cb53195ffecbf9798cdde4a30f27fab9", 377 | "vout": 0, 378 | "signs": [ 379 | { 380 | "keyPair": "KzRGFiqhXB7SyX6idHQkt77B8mX7adnujdg3VG47jdVK2x4wbUYg", 381 | "hashType": 2, 382 | "value": 40000 383 | } 384 | ], 385 | "sequence": 4294967295, 386 | "prevTxScript": "OP_DUP OP_HASH160 0de1f9b92d2ab6d8ead83f9a0ff5cf518dcb03b8 OP_EQUALVERIFY OP_CHECKSIG" 387 | } 388 | ], 389 | "outputs": [ 390 | { 391 | "value": 20000, 392 | "script": "OP_DUP OP_HASH160 9ed1f577c60e4be1dbf35318ec12f51d25e85773 OP_EQUALVERIFY OP_CHECKSIG" 393 | }, 394 | { 395 | "value": 30000, 396 | "script": "OP_DUP OP_HASH160 fb407e88c48921d5547d899e18a7c0a36919f54d OP_EQUALVERIFY OP_CHECKSIG" 397 | }, 398 | { 399 | "value": 50000, 400 | "script": "OP_DUP OP_HASH160 04ccb4eed8cfa9f6e394e945178960f5ccddb387 OP_EQUALVERIFY OP_CHECKSIG" 401 | } 402 | ] 403 | }, 404 | { 405 | "description": "SIGHASH NONE | ANYONECANPAY", 406 | "txHex": "01000000037db7f0b2a345ded6ddf28da3211a7d7a95a2943e9a879493d6481b7d69613f04010000006a47304402204ed272952177aaa5a1b171c2ca5a7a3d300ffcd7e04b040c0baaa4e3561862a502207e65a5b8f99c8a632b186c8a60496a12bf3116f51909b7497413aefdc3be7bf6822102f1c7eac9200f8dee7e34e59318ff2076c8b3e3ac7f43121e57569a1aec1803d4ffffffff652c491e5a781a6a3c547fa8d980741acbe4623ae52907278f10e1f064f67e05000000006a47304402203ec365300cc67602f4cc5be027959d3667b48db34c6c87d267c94a7e210d5c1f02204843350311c0a9711cad1960b17ce9e323a1ce6f37deefc3ffe63082d480be92822102f1c7eac9200f8dee7e34e59318ff2076c8b3e3ac7f43121e57569a1aec1803d4ffffffffb9fa270fa3e4dd8c79f9cbfe5f1953cba071ed081f7c277a49c33466c695db35000000006b48304502210084f86f905c36372eff9c54ccd509a519a3325bcace8abfeed7ed3f0d579979e902201ff330dd2402e5ca9989a8a294fa36d6cf3a093edb18d29c9d9644186a3efeb4822102f1c7eac9200f8dee7e34e59318ff2076c8b3e3ac7f43121e57569a1aec1803d4ffffffff03204e0000000000001976a9149ed1f577c60e4be1dbf35318ec12f51d25e8577388ac30750000000000001976a914fb407e88c48921d5547d899e18a7c0a36919f54d88ac50c30000000000001976a91404ccb4eed8cfa9f6e394e945178960f5ccddb38788ac00000000", 407 | "version": 1, 408 | "inputs": [ 409 | { 410 | "txId": "043f61697d1b48d69394879a3e94a2957a7d1a21a38df2ddd6de45a3b2f0b77d", 411 | "vout": 1, 412 | "signs": [ 413 | { 414 | "keyPair": "KzRGFiqhXB7SyX6idHQkt77B8mX7adnujdg3VG47jdVK2x4wbUYg", 415 | "hashType": 130, 416 | "value": 40000 417 | } 418 | ], 419 | "sequence": 4294967295, 420 | "prevTxScript": "OP_DUP OP_HASH160 0de1f9b92d2ab6d8ead83f9a0ff5cf518dcb03b8 OP_EQUALVERIFY OP_CHECKSIG" 421 | }, 422 | { 423 | "txId": "057ef664f0e1108f270729e53a62e4cb1a7480d9a87f543c6a1a785a1e492c65", 424 | "vout": 0, 425 | "signs": [ 426 | { 427 | "keyPair": "KzRGFiqhXB7SyX6idHQkt77B8mX7adnujdg3VG47jdVK2x4wbUYg", 428 | "hashType": 130, 429 | "value": 40000 430 | } 431 | ], 432 | "sequence": 4294967295, 433 | "prevTxScript": "OP_DUP OP_HASH160 0de1f9b92d2ab6d8ead83f9a0ff5cf518dcb03b8 OP_EQUALVERIFY OP_CHECKSIG" 434 | }, 435 | { 436 | "txId": "35db95c66634c3497a277c1f08ed71a0cb53195ffecbf9798cdde4a30f27fab9", 437 | "vout": 0, 438 | "signs": [ 439 | { 440 | "keyPair": "KzRGFiqhXB7SyX6idHQkt77B8mX7adnujdg3VG47jdVK2x4wbUYg", 441 | "hashType": 130, 442 | "value": 40000 443 | } 444 | ], 445 | "sequence": 4294967295, 446 | "prevTxScript": "OP_DUP OP_HASH160 0de1f9b92d2ab6d8ead83f9a0ff5cf518dcb03b8 OP_EQUALVERIFY OP_CHECKSIG" 447 | } 448 | ], 449 | "outputs": [ 450 | { 451 | "value": 20000, 452 | "script": "OP_DUP OP_HASH160 9ed1f577c60e4be1dbf35318ec12f51d25e85773 OP_EQUALVERIFY OP_CHECKSIG" 453 | }, 454 | { 455 | "value": 30000, 456 | "script": "OP_DUP OP_HASH160 fb407e88c48921d5547d899e18a7c0a36919f54d OP_EQUALVERIFY OP_CHECKSIG" 457 | }, 458 | { 459 | "value": 50000, 460 | "script": "OP_DUP OP_HASH160 04ccb4eed8cfa9f6e394e945178960f5ccddb387 OP_EQUALVERIFY OP_CHECKSIG" 461 | } 462 | ] 463 | }, 464 | { 465 | "description": "P2PKH", 466 | "txHex": "010000000176d7b05b96e69d9760bacf14e496ea01085eff32be8f4e08b299eb92057826e5000000006b4830450221009bd6ff2561437155913c289923175d3f114cca1c0e2bc5989315d5261502c2c902201b71ad90dce076a5eb872330ed729e7c2c4bc2d0513efff099dbefb3b62eab4f0121038de63cf582d058a399a176825c045672d5ff8ea25b64d28d4375dcdb14c02b2bffffffff0160ea0000000000001976a914851a33a5ef0d4279bd5854949174e2c65b1d450088ac00000000", 467 | "version": 1, 468 | "inputs": [ 469 | { 470 | "txId": "e526780592eb99b2084e8fbe32ff5e0801ea96e414cfba60979de6965bb0d776", 471 | "vout": 0, 472 | "signs": [ 473 | { 474 | "keyPair": "L2FroWqrUgsPpTMhpXcAFnVDLPTToDbveh3bhDaU4jhe7Cw6YujN", 475 | "hashType": 1, 476 | "value": 80000 477 | } 478 | ], 479 | "sequence": 4294967295, 480 | "prevTxScript": "OP_DUP OP_HASH160 851a33a5ef0d4279bd5854949174e2c65b1d4500 OP_EQUALVERIFY OP_CHECKSIG" 481 | } 482 | ], 483 | "outputs": [ 484 | { 485 | "value": 60000, 486 | "script": "OP_DUP OP_HASH160 851a33a5ef0d4279bd5854949174e2c65b1d4500 OP_EQUALVERIFY OP_CHECKSIG" 487 | } 488 | ], 489 | "locktime": 0 490 | } 491 | ], 492 | "fromTransaction": [] 493 | }, 494 | "invalid": { 495 | "build": [ 496 | { 497 | "exception": "Transaction has no inputs", 498 | "inputs": [], 499 | "outputs": [ 500 | { 501 | "script": "OP_DUP OP_HASH160 aa4d7985c57e011a8b3dd8e0e5a73aaef41629c5 OP_EQUALVERIFY OP_CHECKSIG", 502 | "value": 1000 503 | } 504 | ] 505 | }, 506 | { 507 | "exception": "Transaction has no outputs", 508 | "inputs": [ 509 | { 510 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 511 | "vout": 0 512 | } 513 | ], 514 | "outputs": [] 515 | }, 516 | { 517 | "exception": "Transaction has absurd fees", 518 | "inputs": [ 519 | { 520 | "txRaw": { 521 | "inputs": [], 522 | "outputs": [ 523 | { 524 | "address": "1C5XhB1UkFuyCV1CG9dmXaXGu3xDL4nAjv", 525 | "value": 1000000000 526 | } 527 | ], 528 | "incomplete": true 529 | }, 530 | "vout": 0, 531 | "signs": [ 532 | { 533 | "keyPair": "KzBQVXYUGDAvqG7VeU3C7ZMRYiwtsxSVVFcYGzKU9E4aUVDUquZU" 534 | } 535 | ] 536 | } 537 | ], 538 | "outputs": [ 539 | { 540 | "script": "OP_DUP OP_HASH160 ff99e06c1a4ac394b4e1cb3d3a4b2b47749e339a OP_EQUALVERIFY OP_CHECKSIG", 541 | "value": 200000 542 | } 543 | ] 544 | }, 545 | { 546 | "description": "Incomplete transaction, nothing assumed", 547 | "exception": "Transaction is not complete", 548 | "inputs": [ 549 | { 550 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 551 | "vout": 0 552 | } 553 | ], 554 | "outputs": [ 555 | { 556 | "script": "OP_DUP OP_HASH160 aa4d7985c57e011a8b3dd8e0e5a73aaef41629c5 OP_EQUALVERIFY OP_CHECKSIG", 557 | "value": 1000 558 | } 559 | ] 560 | }, 561 | { 562 | "description": "Incomplete transaction, known prevTxScript, thereby throws for missing signatures", 563 | "exception": "Transaction is not complete", 564 | "inputs": [ 565 | { 566 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 567 | "vout": 0, 568 | "signs": [ 569 | { 570 | "keyPair": "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn" 571 | } 572 | ] 573 | }, 574 | { 575 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 576 | "vout": 1, 577 | "prevTxScript": "OP_DUP OP_HASH160 aa4d7985c57e011a8b3dd8e0e5a73aaef41629c5 OP_EQUALVERIFY OP_CHECKSIG" 578 | } 579 | ], 580 | "outputs": [ 581 | { 582 | "script": "OP_DUP OP_HASH160 aa4d7985c57e011a8b3dd8e0e5a73aaef41629c5 OP_EQUALVERIFY OP_CHECKSIG", 583 | "value": 1000 584 | } 585 | ] 586 | }, 587 | { 588 | "description": "Duplicate transaction outs", 589 | "exception": "Duplicate TxOut: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff:0", 590 | "incomplete": true, 591 | "inputs": [ 592 | { 593 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 594 | "vout": 0 595 | }, 596 | { 597 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 598 | "vout": 0 599 | } 600 | ], 601 | "outputs": [ 602 | { 603 | "script": "OP_DUP OP_HASH160 aa4d7985c57e011a8b3dd8e0e5a73aaef41629c5 OP_EQUALVERIFY OP_CHECKSIG", 604 | "value": 1000 605 | } 606 | ] 607 | } 608 | ], 609 | "sign": [ 610 | { 611 | "description": "Too many signatures - P2PKH", 612 | "exception": "Signature already exists", 613 | "inputs": [ 614 | { 615 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 616 | "vout": 1, 617 | "signs": [ 618 | { 619 | "keyPair": "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn" 620 | }, 621 | { 622 | "keyPair": "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn", 623 | "throws": true 624 | } 625 | ] 626 | } 627 | ], 628 | "outputs": [ 629 | { 630 | "script": "OP_DUP OP_HASH160 aa4d7985c57e011a8b3dd8e0e5a73aaef41629c5 OP_EQUALVERIFY OP_CHECKSIG", 631 | "value": 1000 632 | } 633 | ] 634 | }, 635 | { 636 | "description": "Wrong network for keyPair", 637 | "exception": "Inconsistent network", 638 | "network": "bitcoin", 639 | "inputs": [ 640 | { 641 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 642 | "vout": 0, 643 | "signs": [ 644 | { 645 | "keyPair": "91avARGdfge8E4tZfYLoxeJ5sGBdNJQH4kvjJoQFacbgwmaKkrx", 646 | "network": "testnet", 647 | "throws": true 648 | } 649 | ] 650 | } 651 | ], 652 | "outputs": [ 653 | { 654 | "script": "OP_DUP OP_HASH160 aa4d7985c57e011a8b3dd8e0e5a73aaef41629c5 OP_EQUALVERIFY OP_CHECKSIG", 655 | "value": 1000 656 | } 657 | ] 658 | }, 659 | { 660 | "description": "Transaction w/ no outputs (but 1 SIGHASH_NONE)", 661 | "exception": "Transaction needs outputs", 662 | "inputs": [ 663 | { 664 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 665 | "vout": 0, 666 | "signs": [ 667 | { 668 | "keyPair": "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn", 669 | "hashType": 2 670 | } 671 | ] 672 | }, 673 | { 674 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 675 | "vout": 1, 676 | "signs": [ 677 | { 678 | "keyPair": "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn", 679 | "throws": true 680 | } 681 | ] 682 | } 683 | ], 684 | "outputs": [] 685 | }, 686 | { 687 | "description": "Transaction w/ no outputs", 688 | "exception": "Transaction needs outputs", 689 | "inputs": [ 690 | { 691 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 692 | "vout": 0, 693 | "signs": [ 694 | { 695 | "keyPair": "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn", 696 | "throws": true 697 | } 698 | ] 699 | } 700 | ], 701 | "outputs": [] 702 | } 703 | ], 704 | "fromTransaction": [ 705 | { 706 | "exception": "coinbase inputs not supported", 707 | "txHex": "01000000010000000000000000000000000000000000000000000000000000000000000000000000006b483045022100a3b254e1c10b5d039f36c05f323995d6e5a367d98dd78a13d5bbc3991b35720e022022fccea3897d594de0689601fbd486588d5bfa6915be2386db0397ee9a6e80b601210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ffffffff0110270000000000001976a914aa4d7985c57e011a8b3dd8e0e5a73aaef41629c588ac00000000" 708 | } 709 | ] 710 | } 711 | } 712 | --------------------------------------------------------------------------------