Move tests to TypeScript (coverage is still JS based)

This commit is contained in:
junderw 2019-09-07 13:42:03 +09:00
parent 11e4a12caf
commit 6c08a0be40
No known key found for this signature in database
GPG key ID: B256185D3A971908
41 changed files with 3920 additions and 2712 deletions

3
.gitignore vendored
View file

@ -2,3 +2,6 @@ coverage
node_modules node_modules
.nyc_output .nyc_output
npm-debug.log npm-debug.log
test/*.js
test/integration/*.js
!test/ts-node-register.js

712
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -16,7 +16,9 @@
], ],
"scripts": { "scripts": {
"build": "npm run clean && tsc -p ./tsconfig.json && npm run formatjs", "build": "npm run clean && tsc -p ./tsconfig.json && npm run formatjs",
"build:tests": "npm run clean:jstests && tsc -p ./test/tsconfig.json",
"clean": "rimraf src types", "clean": "rimraf src types",
"clean:jstests": "rimraf 'test/**/!(ts-node-register)*.js'",
"coverage-report": "npm run build && npm run nobuild:coverage-report", "coverage-report": "npm run build && npm run nobuild:coverage-report",
"coverage-html": "npm run build && npm run nobuild:coverage-html", "coverage-html": "npm run build && npm run nobuild:coverage-html",
"coverage": "npm run build && npm run nobuild:coverage", "coverage": "npm run build && npm run nobuild:coverage",
@ -26,12 +28,13 @@
"gitdiff:ci": "npm run build && git diff --exit-code", "gitdiff:ci": "npm run build && git diff --exit-code",
"integration": "npm run build && npm run nobuild:integration", "integration": "npm run build && npm run nobuild:integration",
"lint": "tslint -p tsconfig.json -c tslint.json", "lint": "tslint -p tsconfig.json -c tslint.json",
"mocha:ts": "mocha --recursive --require test/ts-node-register",
"nobuild:coverage-report": "nyc report --reporter=lcov", "nobuild:coverage-report": "nyc report --reporter=lcov",
"nobuild:coverage-html": "nyc report --reporter=html", "nobuild:coverage-html": "nyc report --reporter=html",
"nobuild:coverage": "nyc --check-coverage --branches 90 --functions 90 --lines 90 mocha", "nobuild:coverage": "npm run build:tests && nyc --check-coverage --branches 90 --functions 90 --lines 90 mocha && npm run clean:jstests",
"nobuild:integration": "mocha --timeout 50000 test/integration/", "nobuild:integration": "npm run mocha:ts -- --timeout 50000 'test/integration/*.ts'",
"nobuild:unit": "mocha", "nobuild:unit": "npm run mocha:ts -- 'test/*.ts'",
"prettier": "prettier 'ts_src/**/*.ts' --ignore-path ./.prettierignore", "prettier": "prettier 'ts_src/**/*.ts' 'test/**/*.ts' --ignore-path ./.prettierignore",
"prettierjs": "prettier 'src/**/*.js' --ignore-path ./.prettierignore", "prettierjs": "prettier 'src/**/*.js' --ignore-path ./.prettierignore",
"test": "npm run build && npm run format:ci && npm run lint && npm run nobuild:coverage", "test": "npm run build && npm run format:ci && npm run lint && npm run nobuild:coverage",
"unit": "npm run build && npm run nobuild:unit" "unit": "npm run build && npm run nobuild:unit"
@ -63,7 +66,10 @@
"wif": "^2.0.1" "wif": "^2.0.1"
}, },
"devDependencies": { "devDependencies": {
"bip39": "^2.3.0", "@types/bs58": "^4.0.0",
"@types/mocha": "^5.2.7",
"@types/proxyquire": "^1.3.28",
"bip39": "^3.0.2",
"bip65": "^1.0.1", "bip65": "^1.0.1",
"bip68": "^1.0.3", "bip68": "^1.0.3",
"bn.js": "^4.11.8", "bn.js": "^4.11.8",
@ -71,12 +77,13 @@
"dhttp": "^3.0.0", "dhttp": "^3.0.0",
"hoodwink": "^2.0.0", "hoodwink": "^2.0.0",
"minimaldata": "^1.0.2", "minimaldata": "^1.0.2",
"mocha": "^5.2.0", "mocha": "^6.2.0",
"nyc": "^14.1.1", "nyc": "^14.1.1",
"prettier": "1.16.4", "prettier": "1.16.4",
"proxyquire": "^2.0.1", "proxyquire": "^2.0.1",
"regtest-client": "0.2.0", "regtest-client": "0.2.0",
"rimraf": "^2.6.3", "rimraf": "^2.6.3",
"ts-node": "^8.3.0",
"tslint": "^5.16.0", "tslint": "^5.16.0",
"typescript": "3.2.2" "typescript": "3.2.2"
}, },

View file

@ -1,134 +1,148 @@
const { describe, it } = require('mocha') import * as assert from 'assert';
const assert = require('assert') import { describe, it } from 'mocha';
const baddress = require('../src/address') import * as baddress from '../src/address';
const bscript = require('../src/script') import * as bscript from '../src/script';
const fixtures = require('./fixtures/address.json') import * as fixtures from './fixtures/address.json';
const NETWORKS = Object.assign({
const NETWORKS = Object.assign(
{
litecoin: { litecoin: {
messagePrefix: '\x19Litecoin Signed Message:\n', messagePrefix: '\x19Litecoin Signed Message:\n',
bip32: { bip32: {
public: 0x019da462, public: 0x019da462,
private: 0x019d9cfe private: 0x019d9cfe,
}, },
pubKeyHash: 0x30, pubKeyHash: 0x30,
scriptHash: 0x32, scriptHash: 0x32,
wif: 0xb0 wif: 0xb0,
} },
}, require('../src/networks')) },
require('../src/networks'),
);
describe('address', () => { describe('address', () => {
describe('fromBase58Check', () => { describe('fromBase58Check', () => {
fixtures.standard.forEach(f => { fixtures.standard.forEach(f => {
if (!f.base58check) return if (!f.base58check) return;
it('decodes ' + f.base58check, () => { it('decodes ' + f.base58check, () => {
const decode = baddress.fromBase58Check(f.base58check) const decode = baddress.fromBase58Check(f.base58check);
assert.strictEqual(decode.version, f.version) assert.strictEqual(decode.version, f.version);
assert.strictEqual(decode.hash.toString('hex'), f.hash) assert.strictEqual(decode.hash.toString('hex'), f.hash);
}) });
}) });
fixtures.invalid.fromBase58Check.forEach(f => { fixtures.invalid.fromBase58Check.forEach(f => {
it('throws on ' + f.exception, () => { it('throws on ' + f.exception, () => {
assert.throws(() => { assert.throws(() => {
baddress.fromBase58Check(f.address) baddress.fromBase58Check(f.address);
}, new RegExp(f.address + ' ' + f.exception)) }, new RegExp(f.address + ' ' + f.exception));
}) });
}) });
}) });
describe('fromBech32', () => { describe('fromBech32', () => {
fixtures.standard.forEach(f => { fixtures.standard.forEach(f => {
if (!f.bech32) return if (!f.bech32) return;
it('decodes ' + f.bech32, () => { it('decodes ' + f.bech32, () => {
const actual = baddress.fromBech32(f.bech32) const actual = baddress.fromBech32(f.bech32);
assert.strictEqual(actual.version, f.version) assert.strictEqual(actual.version, f.version);
assert.strictEqual(actual.prefix, NETWORKS[f.network].bech32) assert.strictEqual(actual.prefix, NETWORKS[f.network].bech32);
assert.strictEqual(actual.data.toString('hex'), f.data) assert.strictEqual(actual.data.toString('hex'), f.data);
}) });
}) });
fixtures.invalid.bech32.forEach((f, i) => { fixtures.invalid.bech32.forEach(f => {
it('decode fails for ' + f.bech32 + '(' + f.exception + ')', () => { it('decode fails for ' + f.address + '(' + f.exception + ')', () => {
assert.throws(() => { assert.throws(() => {
baddress.fromBech32(f.address) baddress.fromBech32(f.address);
}, new RegExp(f.exception)) }, new RegExp(f.exception));
}) });
}) });
}) });
describe('fromOutputScript', () => { describe('fromOutputScript', () => {
fixtures.standard.forEach(f => { fixtures.standard.forEach(f => {
it('encodes ' + f.script.slice(0, 30) + '... (' + f.network + ')', () => { it('encodes ' + f.script.slice(0, 30) + '... (' + f.network + ')', () => {
const script = bscript.fromASM(f.script) const script = bscript.fromASM(f.script);
const address = baddress.fromOutputScript(script, NETWORKS[f.network]) const address = baddress.fromOutputScript(script, NETWORKS[f.network]);
assert.strictEqual(address, f.base58check || f.bech32.toLowerCase()) assert.strictEqual(address, f.base58check || f.bech32!.toLowerCase());
}) });
}) });
fixtures.invalid.fromOutputScript.forEach(f => { fixtures.invalid.fromOutputScript.forEach(f => {
it('throws when ' + f.script.slice(0, 30) + '... ' + f.exception, () => { it('throws when ' + f.script.slice(0, 30) + '... ' + f.exception, () => {
const script = bscript.fromASM(f.script) const script = bscript.fromASM(f.script);
assert.throws(() => { assert.throws(() => {
baddress.fromOutputScript(script) baddress.fromOutputScript(script);
}, new RegExp(f.exception)) }, new RegExp(f.exception));
}) });
}) });
}) });
describe('toBase58Check', () => { describe('toBase58Check', () => {
fixtures.standard.forEach(f => { fixtures.standard.forEach(f => {
if (!f.base58check) return if (!f.base58check) return;
it('encodes ' + f.hash + ' (' + f.network + ')', () => { it('encodes ' + f.hash + ' (' + f.network + ')', () => {
const address = baddress.toBase58Check(Buffer.from(f.hash, 'hex'), f.version) const address = baddress.toBase58Check(
Buffer.from(f.hash, 'hex'),
f.version,
);
assert.strictEqual(address, f.base58check) assert.strictEqual(address, f.base58check);
}) });
}) });
}) });
describe('toBech32', () => { describe('toBech32', () => {
fixtures.bech32.forEach((f, i) => { fixtures.bech32.forEach(f => {
if (!f.bech32) return if (!f.address) return;
const data = Buffer.from(f.data, 'hex') const data = Buffer.from(f.data, 'hex');
it('encode ' + f.address, () => { it('encode ' + f.address, () => {
assert.deepStrictEqual(baddress.toBech32(data, f.version, f.prefix), f.address) assert.deepStrictEqual(
}) baddress.toBech32(data, f.version, f.prefix),
}) f.address.toLowerCase(),
);
});
});
fixtures.invalid.bech32.forEach((f, i) => { // TODO: These fixtures (according to TypeScript) have none of the data used below
if (!f.prefix || f.version === undefined || f.data === undefined) return fixtures.invalid.bech32.forEach((f: any) => {
if (!f.prefix || f.version === undefined || f.data === undefined) return;
it('encode fails (' + f.exception, () => { it('encode fails (' + f.exception, () => {
assert.throws(() => { assert.throws(() => {
baddress.toBech32(Buffer.from(f.data, 'hex'), f.version, f.prefix) baddress.toBech32(Buffer.from(f.data, 'hex'), f.version, f.prefix);
}, new RegExp(f.exception)) }, new RegExp(f.exception));
}) });
}) });
}) });
describe('toOutputScript', () => { describe('toOutputScript', () => {
fixtures.standard.forEach(f => { fixtures.standard.forEach(f => {
it('decodes ' + f.script.slice(0, 30) + '... (' + f.network + ')', () => { it('decodes ' + f.script.slice(0, 30) + '... (' + f.network + ')', () => {
const script = baddress.toOutputScript(f.base58check || f.bech32, NETWORKS[f.network]) const script = baddress.toOutputScript(
(f.base58check || f.bech32)!,
NETWORKS[f.network],
);
assert.strictEqual(bscript.toASM(script), f.script) assert.strictEqual(bscript.toASM(script), f.script);
}) });
}) });
fixtures.invalid.toOutputScript.forEach(f => { fixtures.invalid.toOutputScript.forEach(f => {
it('throws when ' + f.exception, () => { it('throws when ' + f.exception, () => {
assert.throws(() => { assert.throws(() => {
baddress.toOutputScript(f.address, f.network) baddress.toOutputScript(f.address, f.network as any);
}, new RegExp(f.address + ' ' + f.exception)) }, new RegExp(f.address + ' ' + f.exception));
}) });
}) });
}) });
}) });

View file

@ -1,226 +1,251 @@
const { describe, it } = require('mocha') import * as assert from 'assert';
const assert = require('assert') import * as base58 from 'bs58';
const base58 = require('bs58') import { describe, it } from 'mocha';
const bitcoin = require('../') import * as bitcoin from '..';
import * as base58EncodeDecode from './fixtures/core/base58_encode_decode.json';
const base58EncodeDecode = require('./fixtures/core/base58_encode_decode.json') import * as base58KeysInvalid from './fixtures/core/base58_keys_invalid.json';
const base58KeysInvalid = require('./fixtures/core/base58_keys_invalid.json') import * as base58KeysValid from './fixtures/core/base58_keys_valid.json';
const base58KeysValid = require('./fixtures/core/base58_keys_valid.json') import * as blocksValid from './fixtures/core/blocks.json';
const blocksValid = require('./fixtures/core/blocks.json') import * as sigCanonical from './fixtures/core/sig_canonical.json';
const sigCanonical = require('./fixtures/core/sig_canonical.json') import * as sigHash from './fixtures/core/sighash.json';
const sigHash = require('./fixtures/core/sighash.json') import * as sigNoncanonical from './fixtures/core/sig_noncanonical.json';
const sigNoncanonical = require('./fixtures/core/sig_noncanonical.json') import * as txValid from './fixtures/core/tx_valid.json';
const txValid = require('./fixtures/core/tx_valid.json')
describe('Bitcoin-core', () => { describe('Bitcoin-core', () => {
// base58EncodeDecode // base58EncodeDecode
describe('base58', () => { describe('base58', () => {
base58EncodeDecode.forEach(f => { base58EncodeDecode.forEach(f => {
const fhex = f[0] const fhex = f[0];
const fb58 = f[1] const fb58 = f[1];
it('can decode ' + fb58, () => { it('can decode ' + fb58, () => {
const buffer = base58.decode(fb58) const buffer = base58.decode(fb58);
const actual = buffer.toString('hex') const actual = buffer.toString('hex');
assert.strictEqual(actual, fhex) assert.strictEqual(actual, fhex);
}) });
it('can encode ' + fhex, () => { it('can encode ' + fhex, () => {
const buffer = Buffer.from(fhex, 'hex') const buffer = Buffer.from(fhex, 'hex');
const actual = base58.encode(buffer) const actual = base58.encode(buffer);
assert.strictEqual(actual, fb58) assert.strictEqual(actual, fb58);
}) });
}) });
}) });
// base58KeysValid // base58KeysValid
describe('address.toBase58Check', () => { describe('address.toBase58Check', () => {
const typeMap = { const typeMap: any = {
'pubkey': 'pubKeyHash', pubkey: 'pubKeyHash',
'script': 'scriptHash' script: 'scriptHash',
} };
base58KeysValid.forEach(f => { base58KeysValid.forEach(f => {
const expected = f[0] const expected = f[0];
const hash = Buffer.from(f[1], 'hex') const hash = Buffer.from(f[1] as any, 'hex');
const params = f[2] const params = f[2] as any;
if (params.isPrivkey) return if (params.isPrivkey) return;
const network = params.isTestnet ? bitcoin.networks.testnet : bitcoin.networks.bitcoin const network: any = params.isTestnet
const version = network[typeMap[params.addrType]] ? bitcoin.networks.testnet
: bitcoin.networks.bitcoin;
const version = network[typeMap[params.addrType]];
it('can export ' + expected, () => { it('can export ' + expected, () => {
assert.strictEqual(bitcoin.address.toBase58Check(hash, version), expected) assert.strictEqual(
}) bitcoin.address.toBase58Check(hash, version),
}) expected,
}) );
});
});
});
// base58KeysInvalid // base58KeysInvalid
describe('address.fromBase58Check', () => { describe('address.fromBase58Check', () => {
const allowedNetworks = [ const allowedNetworks = [
bitcoin.networks.bitcoin.pubkeyhash, bitcoin.networks.bitcoin.pubKeyHash,
bitcoin.networks.bitcoin.scripthash, bitcoin.networks.bitcoin.scriptHash,
bitcoin.networks.testnet.pubkeyhash, bitcoin.networks.testnet.pubKeyHash,
bitcoin.networks.testnet.scripthash bitcoin.networks.testnet.scriptHash,
] ];
base58KeysInvalid.forEach(f => { base58KeysInvalid.forEach(f => {
const string = f[0] const string = f[0];
it('throws on ' + string, () => { it('throws on ' + string, () => {
assert.throws(() => { assert.throws(() => {
const address = bitcoin.address.fromBase58Check(string) const address = bitcoin.address.fromBase58Check(string);
assert.notStrictEqual(allowedNetworks.indexOf(address.version), -1, 'Invalid network') assert.notStrictEqual(
}, /(Invalid (checksum|network))|(too (short|long))/) allowedNetworks.indexOf(address.version),
}) -1,
}) 'Invalid network',
}) );
}, /(Invalid (checksum|network))|(too (short|long))/);
});
});
});
// base58KeysValid // base58KeysValid
describe('ECPair', () => { describe('ECPair', () => {
base58KeysValid.forEach(f => { base58KeysValid.forEach(f => {
const string = f[0] const strng = f[0] as string;
const hex = f[1] const hex = f[1];
const params = f[2] const params = f[2] as any;
if (!params.isPrivkey) return if (!params.isPrivkey) return;
const network = params.isTestnet ? bitcoin.networks.testnet : bitcoin.networks.bitcoin const network = params.isTestnet
const keyPair = bitcoin.ECPair.fromWIF(string, network) ? bitcoin.networks.testnet
: bitcoin.networks.bitcoin;
const keyPair = bitcoin.ECPair.fromWIF(strng, network);
it('fromWIF imports ' + string, () => { it('fromWIF imports ' + strng, () => {
assert.strictEqual(keyPair.privateKey.toString('hex'), hex) assert.strictEqual(keyPair.privateKey!.toString('hex'), hex);
assert.strictEqual(keyPair.compressed, params.isCompressed) assert.strictEqual(keyPair.compressed, params.isCompressed);
}) });
it('toWIF exports ' + hex + ' to ' + string, () => { it('toWIF exports ' + hex + ' to ' + strng, () => {
assert.strictEqual(keyPair.toWIF(), string) assert.strictEqual(keyPair.toWIF(), strng);
}) });
}) });
}) });
// base58KeysInvalid // base58KeysInvalid
describe('ECPair.fromWIF', () => { describe('ECPair.fromWIF', () => {
const allowedNetworks = [ const allowedNetworks = [
bitcoin.networks.bitcoin, bitcoin.networks.bitcoin,
bitcoin.networks.testnet bitcoin.networks.testnet,
] ];
base58KeysInvalid.forEach(f => { base58KeysInvalid.forEach(f => {
const string = f[0] const string = f[0];
it('throws on ' + string, () => { it('throws on ' + string, () => {
assert.throws(() => { assert.throws(() => {
bitcoin.ECPair.fromWIF(string, allowedNetworks) bitcoin.ECPair.fromWIF(string, allowedNetworks);
}, /(Invalid|Unknown) (checksum|compression flag|network version|WIF length)/) }, /(Invalid|Unknown) (checksum|compression flag|network version|WIF length)/);
}) });
}) });
}) });
describe('Block.fromHex', () => { describe('Block.fromHex', () => {
blocksValid.forEach(f => { blocksValid.forEach(f => {
it('can parse ' + f.id, () => { it('can parse ' + f.id, () => {
const block = bitcoin.Block.fromHex(f.hex) const block = bitcoin.Block.fromHex(f.hex);
assert.strictEqual(block.getId(), f.id) assert.strictEqual(block.getId(), f.id);
assert.strictEqual(block.transactions.length, f.transactions) assert.strictEqual(block.transactions!.length, f.transactions);
}) });
}) });
}) });
// txValid // txValid
describe('Transaction.fromHex', () => { describe('Transaction.fromHex', () => {
txValid.forEach(f => { txValid.forEach(f => {
// Objects that are only a single string are ignored // Objects that are only a single string are ignored
if (f.length === 1) return if (f.length === 1) return;
const inputs = f[0] const inputs = f[0];
const fhex = f[1] const fhex = f[1];
// const verifyFlags = f[2] // TODO: do we need to test this? // const verifyFlags = f[2] // TODO: do we need to test this?
it('can decode ' + fhex, () => { it('can decode ' + fhex, () => {
const transaction = bitcoin.Transaction.fromHex(fhex) const transaction = bitcoin.Transaction.fromHex(fhex as string);
transaction.ins.forEach((txIn, i) => { transaction.ins.forEach((txIn, i) => {
const input = inputs[i] const input = inputs[i];
// reverse because test data is reversed // reverse because test data is reversed
const prevOutHash = Buffer.from(input[0], 'hex').reverse() const prevOutHash = Buffer.from(input[0] as string, 'hex').reverse();
const prevOutIndex = input[1] const prevOutIndex = input[1];
assert.deepStrictEqual(txIn.hash, prevOutHash) assert.deepStrictEqual(txIn.hash, prevOutHash);
// we read UInt32, not Int32 // we read UInt32, not Int32
assert.strictEqual(txIn.index & 0xffffffff, prevOutIndex) assert.strictEqual(txIn.index & 0xffffffff, prevOutIndex);
}) });
}) });
}) });
}) });
// sighash // sighash
describe('Transaction', () => { describe('Transaction', () => {
sigHash.forEach(f => { sigHash.forEach(f => {
// Objects that are only a single string are ignored // Objects that are only a single string are ignored
if (f.length === 1) return if (f.length === 1) return;
const txHex = f[0] const txHex = f[0] as string;
const scriptHex = f[1] const scriptHex = f[1] as string;
const inIndex = f[2] const inIndex = f[2] as number;
const hashType = f[3] const hashType = f[3] as number;
const expectedHash = f[4] const expectedHash = f[4];
const hashTypes = [] const hashTypes = [];
if ((hashType & 0x1f) === bitcoin.Transaction.SIGHASH_NONE) hashTypes.push('SIGHASH_NONE') if ((hashType & 0x1f) === bitcoin.Transaction.SIGHASH_NONE)
else if ((hashType & 0x1f) === bitcoin.Transaction.SIGHASH_SINGLE) hashTypes.push('SIGHASH_SINGLE') hashTypes.push('SIGHASH_NONE');
else hashTypes.push('SIGHASH_ALL') else if ((hashType & 0x1f) === bitcoin.Transaction.SIGHASH_SINGLE)
if (hashType & bitcoin.Transaction.SIGHASH_ANYONECANPAY) hashTypes.push('SIGHASH_ANYONECANPAY') hashTypes.push('SIGHASH_SINGLE');
else hashTypes.push('SIGHASH_ALL');
if (hashType & bitcoin.Transaction.SIGHASH_ANYONECANPAY)
hashTypes.push('SIGHASH_ANYONECANPAY');
const hashTypeName = hashTypes.join(' | ') const hashTypeName = hashTypes.join(' | ');
it('should hash ' + txHex.slice(0, 40) + '... (' + hashTypeName + ')', () => { it(
const transaction = bitcoin.Transaction.fromHex(txHex) 'should hash ' + txHex.slice(0, 40) + '... (' + hashTypeName + ')',
assert.strictEqual(transaction.toHex(), txHex) () => {
const transaction = bitcoin.Transaction.fromHex(txHex);
assert.strictEqual(transaction.toHex(), txHex);
const script = Buffer.from(scriptHex, 'hex') const script = Buffer.from(scriptHex, 'hex');
const scriptChunks = bitcoin.script.decompile(script) const scriptChunks = bitcoin.script.decompile(script);
assert.strictEqual(bitcoin.script.compile(scriptChunks).toString('hex'), scriptHex) assert.strictEqual(
bitcoin.script.compile(scriptChunks!).toString('hex'),
scriptHex,
);
const hash = transaction.hashForSignature(inIndex, script, hashType) const hash = transaction.hashForSignature(inIndex, script, hashType);
// reverse because test data is reversed // reverse because test data is reversed
assert.strictEqual(hash.reverse().toString('hex'), expectedHash) assert.strictEqual(
}) (hash.reverse() as Buffer).toString('hex'),
}) expectedHash,
}) );
},
);
});
});
describe('script.signature.decode', () => { describe('script.signature.decode', () => {
sigCanonical.forEach(hex => { sigCanonical.forEach(hex => {
const buffer = Buffer.from(hex, 'hex') const buffer = Buffer.from(hex, 'hex');
it('can parse ' + hex, () => { it('can parse ' + hex, () => {
const parsed = bitcoin.script.signature.decode(buffer) const parsed = bitcoin.script.signature.decode(buffer);
const actual = bitcoin.script.signature.encode(parsed.signature, parsed.hashType) const actual = bitcoin.script.signature.encode(
parsed.signature,
parsed.hashType,
);
assert.strictEqual(actual.toString('hex'), hex) assert.strictEqual(actual.toString('hex'), hex);
}) });
}) });
sigNoncanonical.forEach((hex, i) => { sigNoncanonical.forEach((hex, i) => {
if (i === 0) return if (i === 0) return;
if (i % 2 !== 0) return if (i % 2 !== 0) return;
const description = sigNoncanonical[i - 1].slice(0, -1) const description = sigNoncanonical[i - 1].slice(0, -1);
const buffer = Buffer.from(hex, 'hex') const buffer = Buffer.from(hex, 'hex');
it('throws on ' + description, () => { it('throws on ' + description, () => {
assert.throws(() => { assert.throws(() => {
bitcoin.script.signature.decode(buffer) bitcoin.script.signature.decode(buffer);
}, /Expected DER (integer|sequence)|(R|S) value (excessively padded|is negative)|(R|S|DER sequence) length is (zero|too short|too long|invalid)|Invalid hashType/) }, /Expected DER (integer|sequence)|(R|S) value (excessively padded|is negative)|(R|S|DER sequence) length is (zero|too short|too long|invalid)|Invalid hashType/);
}) });
}) });
}) });
}) });

View file

@ -1,157 +1,172 @@
const { describe, it, beforeEach } = require('mocha') import * as assert from 'assert';
const assert = require('assert') import { beforeEach, describe, it } from 'mocha';
const Block = require('..').Block import { Block } from '..';
const fixtures = require('./fixtures/block') import * as fixtures from './fixtures/block.json';
describe('Block', () => { describe('Block', () => {
describe('version', () => { describe('version', () => {
it('should be interpreted as an int32le', () => { it('should be interpreted as an int32le', () => {
const blockHex = 'ffffffff0000000000000000000000000000000000000000000000000000000000000000414141414141414141414141414141414141414141414141414141414141414101000000020000000300000000' const blockHex =
const block = Block.fromHex(blockHex) 'ffffffff0000000000000000000000000000000000000000000000000000000000000000414141414141414141414141414141414141414141414141414141414141414101000000020000000300000000';
assert.strictEqual(-1, block.version) const block = Block.fromHex(blockHex);
assert.strictEqual(1, block.timestamp) assert.strictEqual(-1, block.version);
}) assert.strictEqual(1, block.timestamp);
}) });
});
describe('calculateTarget', () => { describe('calculateTarget', () => {
fixtures.targets.forEach(f => { fixtures.targets.forEach(f => {
it('returns ' + f.expected + ' for 0x' + f.bits, () => { it('returns ' + f.expected + ' for 0x' + f.bits, () => {
const bits = parseInt(f.bits, 16) const bits = parseInt(f.bits, 16);
assert.strictEqual(Block.calculateTarget(bits).toString('hex'), f.expected) assert.strictEqual(
}) Block.calculateTarget(bits).toString('hex'),
}) f.expected,
}) );
});
});
});
describe('fromBuffer/fromHex', () => { describe('fromBuffer/fromHex', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
it('imports ' + f.description, () => { it('imports ' + f.description, () => {
const block = Block.fromHex(f.hex) const block = Block.fromHex(f.hex);
assert.strictEqual(block.version, f.version) assert.strictEqual(block.version, f.version);
assert.strictEqual(block.prevHash.toString('hex'), f.prevHash) assert.strictEqual(block.prevHash!.toString('hex'), f.prevHash);
assert.strictEqual(block.merkleRoot.toString('hex'), f.merkleRoot) assert.strictEqual(block.merkleRoot!.toString('hex'), f.merkleRoot);
if (block.witnessCommit) { if (block.witnessCommit) {
assert.strictEqual(block.witnessCommit.toString('hex'), f.witnessCommit) assert.strictEqual(
block.witnessCommit.toString('hex'),
f.witnessCommit,
);
} }
assert.strictEqual(block.timestamp, f.timestamp) assert.strictEqual(block.timestamp, f.timestamp);
assert.strictEqual(block.bits, f.bits) assert.strictEqual(block.bits, f.bits);
assert.strictEqual(block.nonce, f.nonce) assert.strictEqual(block.nonce, f.nonce);
assert.strictEqual(!block.transactions, f.hex.length === 160) assert.strictEqual(!block.transactions, f.hex.length === 160);
}) });
}) });
fixtures.invalid.forEach(f => { fixtures.invalid.forEach(f => {
it('throws on ' + f.exception, () => { it('throws on ' + f.exception, () => {
assert.throws(() => { assert.throws(() => {
Block.fromHex(f.hex) Block.fromHex(f.hex);
}, new RegExp(f.exception)) }, new RegExp(f.exception));
}) });
}) });
}) });
describe('toBuffer/toHex', () => { describe('toBuffer/toHex', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
let block let block: Block;
beforeEach(() => { beforeEach(() => {
block = Block.fromHex(f.hex) block = Block.fromHex(f.hex);
}) });
it('exports ' + f.description, () => { it('exports ' + f.description, () => {
assert.strictEqual(block.toHex(true), f.hex.slice(0, 160)) assert.strictEqual(block.toHex(true), f.hex.slice(0, 160));
assert.strictEqual(block.toHex(), f.hex) assert.strictEqual(block.toHex(), f.hex);
}) });
}) });
}) });
describe('getHash/getId', () => { describe('getHash/getId', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
let block let block: Block;
beforeEach(() => { beforeEach(() => {
block = Block.fromHex(f.hex) block = Block.fromHex(f.hex);
}) });
it('returns ' + f.id + ' for ' + f.description, () => { it('returns ' + f.id + ' for ' + f.description, () => {
assert.strictEqual(block.getHash().toString('hex'), f.hash) assert.strictEqual(block.getHash().toString('hex'), f.hash);
assert.strictEqual(block.getId(), f.id) assert.strictEqual(block.getId(), f.id);
}) });
}) });
}) });
describe('getUTCDate', () => { describe('getUTCDate', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
let block let block: Block;
beforeEach(() => { beforeEach(() => {
block = Block.fromHex(f.hex) block = Block.fromHex(f.hex);
}) });
it('returns UTC date of ' + f.id, () => { it('returns UTC date of ' + f.id, () => {
const utcDate = block.getUTCDate().getTime() const utcDate = block.getUTCDate().getTime();
assert.strictEqual(utcDate, f.timestamp * 1e3) assert.strictEqual(utcDate, f.timestamp * 1e3);
}) });
}) });
}) });
describe('calculateMerkleRoot', () => { describe('calculateMerkleRoot', () => {
it('should throw on zero-length transaction array', () => { it('should throw on zero-length transaction array', () => {
assert.throws(() => { assert.throws(() => {
Block.calculateMerkleRoot([]) Block.calculateMerkleRoot([]);
}, /Cannot compute merkle root for zero transactions/) }, /Cannot compute merkle root for zero transactions/);
}) });
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
if (f.hex.length === 160) return if (f.hex.length === 160) return;
let block let block: Block;
beforeEach(() => { beforeEach(() => {
block = Block.fromHex(f.hex) block = Block.fromHex(f.hex);
}) });
it('returns ' + f.merkleRoot + ' for ' + f.id, () => { it('returns ' + f.merkleRoot + ' for ' + f.id, () => {
assert.strictEqual(Block.calculateMerkleRoot(block.transactions).toString('hex'), f.merkleRoot) assert.strictEqual(
}) Block.calculateMerkleRoot(block.transactions!).toString('hex'),
f.merkleRoot,
);
});
if (f.witnessCommit) { if (f.witnessCommit) {
it('returns witness commit ' + f.witnessCommit + ' for ' + f.id, () => { it('returns witness commit ' + f.witnessCommit + ' for ' + f.id, () => {
assert.strictEqual(Block.calculateMerkleRoot(block.transactions, true).toString('hex'), f.witnessCommit) assert.strictEqual(
}) Block.calculateMerkleRoot(block.transactions!, true).toString(
'hex',
),
f.witnessCommit,
);
});
} }
}) });
}) });
describe('checkTxRoots', () => { describe('checkTxRoots', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
if (f.hex.length === 160) return if (f.hex.length === 160) return;
let block let block: Block;
beforeEach(() => { beforeEach(() => {
block = Block.fromHex(f.hex) block = Block.fromHex(f.hex);
}) });
it('returns ' + f.valid + ' for ' + f.id, () => { it('returns ' + f.valid + ' for ' + f.id, () => {
assert.strictEqual(block.checkTxRoots(), true) assert.strictEqual(block.checkTxRoots(), true);
}) });
}) });
}) });
describe('checkProofOfWork', () => { describe('checkProofOfWork', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
let block let block: Block;
beforeEach(() => { beforeEach(() => {
block = Block.fromHex(f.hex) block = Block.fromHex(f.hex);
}) });
it('returns ' + f.valid + ' for ' + f.id, () => { it('returns ' + f.valid + ' for ' + f.id, () => {
assert.strictEqual(block.checkProofOfWork(), f.valid) assert.strictEqual(block.checkProofOfWork(), f.valid);
}) });
}) });
}) });
}) });

View file

@ -1,49 +1,49 @@
const { describe, it } = require('mocha') import * as assert from 'assert';
const assert = require('assert') import { describe, it } from 'mocha';
const bufferutils = require('../src/bufferutils') import * as bufferutils from '../src/bufferutils';
const fixtures = require('./fixtures/bufferutils.json') import * as fixtures from './fixtures/bufferutils.json';
describe('bufferutils', () => { describe('bufferutils', () => {
describe('readUInt64LE', () => { describe('readUInt64LE', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
it('decodes ' + f.hex, () => { it('decodes ' + f.hex, () => {
const buffer = Buffer.from(f.hex, 'hex') const buffer = Buffer.from(f.hex, 'hex');
const number = bufferutils.readUInt64LE(buffer, 0) const number = bufferutils.readUInt64LE(buffer, 0);
assert.strictEqual(number, f.dec) assert.strictEqual(number, f.dec);
}) });
}) });
fixtures.invalid.readUInt64LE.forEach(f => { fixtures.invalid.readUInt64LE.forEach(f => {
it('throws on ' + f.description, () => { it('throws on ' + f.description, () => {
const buffer = Buffer.from(f.hex, 'hex') const buffer = Buffer.from(f.hex, 'hex');
assert.throws(() => { assert.throws(() => {
bufferutils.readUInt64LE(buffer, 0) bufferutils.readUInt64LE(buffer, 0);
}, new RegExp(f.exception)) }, new RegExp(f.exception));
}) });
}) });
}) });
describe('writeUInt64LE', () => { describe('writeUInt64LE', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
it('encodes ' + f.dec, () => { it('encodes ' + f.dec, () => {
const buffer = Buffer.alloc(8, 0) const buffer = Buffer.alloc(8, 0);
bufferutils.writeUInt64LE(buffer, f.dec, 0) bufferutils.writeUInt64LE(buffer, f.dec, 0);
assert.strictEqual(buffer.toString('hex'), f.hex) assert.strictEqual(buffer.toString('hex'), f.hex);
}) });
}) });
fixtures.invalid.readUInt64LE.forEach(f => { fixtures.invalid.readUInt64LE.forEach(f => {
it('throws on ' + f.description, () => { it('throws on ' + f.description, () => {
const buffer = Buffer.alloc(8, 0) const buffer = Buffer.alloc(8, 0);
assert.throws(() => { assert.throws(() => {
bufferutils.writeUInt64LE(buffer, f.dec, 0) bufferutils.writeUInt64LE(buffer, f.dec, 0);
}, new RegExp(f.exception)) }, new RegExp(f.exception));
}) });
}) });
}) });
}) });

View file

@ -1,18 +1,18 @@
const { describe, it } = require('mocha') import * as assert from 'assert';
const assert = require('assert') import { describe, it } from 'mocha';
const bscript = require('../src/script') import * as bscript from '../src/script';
const classify = require('../src/classify') import * as classify from '../src/classify';
const fixtures = require('./fixtures/templates.json') import * as fixtures from './fixtures/templates.json';
const multisig = require('../src/templates/multisig') import * as multisig from '../src/templates/multisig';
const nullData = require('../src/templates/nulldata') import * as nullData from '../src/templates/nulldata';
const pubKey = require('../src/templates/pubkey') import * as pubKey from '../src/templates/pubkey';
const pubKeyHash = require('../src/templates/pubkeyhash') import * as pubKeyHash from '../src/templates/pubkeyhash';
const scriptHash = require('../src/templates/scripthash') import * as scriptHash from '../src/templates/scripthash';
const witnessPubKeyHash = require('../src/templates/witnesspubkeyhash') import * as witnessPubKeyHash from '../src/templates/witnesspubkeyhash';
const witnessScriptHash = require('../src/templates/witnessscripthash') import * as witnessScriptHash from '../src/templates/witnessscripthash';
const witnessCommitment = require('../src/templates/witnesscommitment') import * as witnessCommitment from '../src/templates/witnesscommitment';
const tmap = { const tmap = {
pubKey, pubKey,
@ -22,49 +22,48 @@ const tmap = {
witnessScriptHash, witnessScriptHash,
multisig, multisig,
nullData, nullData,
witnessCommitment witnessCommitment,
} };
describe('classify', () => { describe('classify', () => {
describe('input', () => { describe('input', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
if (!f.input) return if (!f.input) return;
it('classifies ' + f.input + ' as ' + f.type, () => { it('classifies ' + f.input + ' as ' + f.type, () => {
const input = bscript.fromASM(f.input) const input = bscript.fromASM(f.input);
const type = classify.input(input) const type = classify.input(input);
assert.strictEqual(type, f.type) assert.strictEqual(type, f.type);
}) });
}) });
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
if (!f.input) return if (!f.input) return;
if (!f.typeIncomplete) return if (!f.typeIncomplete) return;
it('classifies incomplete ' + f.input + ' as ' + f.typeIncomplete, () => { it('classifies incomplete ' + f.input + ' as ' + f.typeIncomplete, () => {
const input = bscript.fromASM(f.input) const input = bscript.fromASM(f.input);
const type = classify.input(input, true) const type = classify.input(input, true);
assert.strictEqual(type, f.typeIncomplete) assert.strictEqual(type, f.typeIncomplete);
}) });
}) });
}) });
describe('classifyOutput', () => { describe('classifyOutput', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
if (!f.output) return if (!f.output) return;
it('classifies ' + f.output + ' as ' + f.type, () => { it('classifies ' + f.output + ' as ' + f.type, () => {
const output = bscript.fromASM(f.output) const output = bscript.fromASM(f.output);
const type = classify.output(output) const type = classify.output(output);
assert.strictEqual(type, f.type) assert.strictEqual(type, f.type);
}) });
}) });
}) });
[
;[
'pubKey', 'pubKey',
'pubKeyHash', 'pubKeyHash',
'scriptHash', 'scriptHash',
@ -72,85 +71,110 @@ describe('classify', () => {
'witnessScriptHash', 'witnessScriptHash',
'multisig', 'multisig',
'nullData', 'nullData',
'witnessCommitment' 'witnessCommitment',
].forEach(name => { ].forEach(name => {
const inputType = tmap[name].input const inputType = (tmap as any)[name].input;
const outputType = tmap[name].output const outputType = (tmap as any)[name].output;
describe(name + '.input.check', () => { describe(name + '.input.check', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
if (name.toLowerCase() === classify.types.P2WPKH) return if (name.toLowerCase() === classify.types.P2WPKH) return;
if (name.toLowerCase() === classify.types.P2WSH) return if (name.toLowerCase() === classify.types.P2WSH) return;
const expected = name.toLowerCase() === f.type.toLowerCase() const expected = name.toLowerCase() === f.type.toLowerCase();
if (inputType && f.input) { if (inputType && f.input) {
const input = bscript.fromASM(f.input) const input = bscript.fromASM(f.input);
it('returns ' + expected + ' for ' + f.input, () => { it('returns ' + expected + ' for ' + f.input, () => {
assert.strictEqual(inputType.check(input), expected) assert.strictEqual(inputType.check(input), expected);
}) });
if (f.typeIncomplete) { if (f.typeIncomplete) {
const expectedIncomplete = name.toLowerCase() === f.typeIncomplete const expectedIncomplete = name.toLowerCase() === f.typeIncomplete;
it('returns ' + expected + ' for ' + f.input, () => { it('returns ' + expected + ' for ' + f.input, () => {
assert.strictEqual(inputType.check(input, true), expectedIncomplete) assert.strictEqual(
}) inputType.check(input, true),
expectedIncomplete,
);
});
} }
} }
}) });
if (!(fixtures.invalid[name])) return if (!(fixtures.invalid as any)[name]) return;
fixtures.invalid[name].inputs.forEach(f => { (fixtures.invalid as any)[name].inputs.forEach((f: any) => {
if (!f.input && !f.inputHex) return if (!f.input && !f.inputHex) return;
it('returns false for ' + f.description + ' (' + (f.input || f.inputHex) + ')', () => { it(
let input 'returns false for ' +
f.description +
' (' +
(f.input || f.inputHex) +
')',
() => {
let input;
if (f.input) { if (f.input) {
input = bscript.fromASM(f.input) input = bscript.fromASM(f.input);
} else { } else {
input = Buffer.from(f.inputHex, 'hex') input = Buffer.from(f.inputHex, 'hex');
} }
assert.strictEqual(inputType.check(input), false) assert.strictEqual(inputType.check(input), false);
}) },
}) );
}) });
});
describe(name + '.output.check', () => { describe(name + '.output.check', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
const expected = name.toLowerCase() === f.type const expected = name.toLowerCase() === f.type;
if (outputType && f.output) { if (outputType && f.output) {
it('returns ' + expected + ' for ' + f.output, () => { it('returns ' + expected + ' for ' + f.output, () => {
const output = bscript.fromASM(f.output) const output = bscript.fromASM(f.output);
if (name.toLowerCase() === 'nulldata' && f.type === classify.types.WITNESS_COMMITMENT) return if (
if (name.toLowerCase() === 'witnesscommitment' && f.type === classify.types.NULLDATA) return name.toLowerCase() === 'nulldata' &&
assert.strictEqual(outputType.check(output), expected) f.type === classify.types.WITNESS_COMMITMENT
}) )
return;
if (
name.toLowerCase() === 'witnesscommitment' &&
f.type === classify.types.NULLDATA
)
return;
assert.strictEqual(outputType.check(output), expected);
});
} }
}) });
if (!(fixtures.invalid[name])) return if (!(fixtures.invalid as any)[name]) return;
fixtures.invalid[name].outputs.forEach(f => { (fixtures.invalid as any)[name].outputs.forEach((f: any) => {
if (!f.output && !f.outputHex) return if (!f.output && !f.outputHex) return;
it('returns false for ' + f.description + ' (' + (f.output || f.outputHex) + ')', () => { it(
let output 'returns false for ' +
f.description +
' (' +
(f.output || f.outputHex) +
')',
() => {
let output;
if (f.output) { if (f.output) {
output = bscript.fromASM(f.output) output = bscript.fromASM(f.output);
} else { } else {
output = Buffer.from(f.outputHex, 'hex') output = Buffer.from(f.outputHex, 'hex');
} }
assert.strictEqual(outputType.check(output), false) assert.strictEqual(outputType.check(output), false);
}) },
}) );
}) });
}) });
}) });
});

View file

@ -1,23 +1,22 @@
const { describe, it } = require('mocha') import * as assert from 'assert';
const assert = require('assert') import { describe, it } from 'mocha';
const bcrypto = require('../src/crypto') import { crypto as bcrypto } from '..';
import * as fixtures from './fixtures/crypto.json';
const fixtures = require('./fixtures/crypto')
describe('crypto', () => { describe('crypto', () => {
['hash160', 'hash256', 'ripemd160', 'sha1', 'sha256'].forEach(algorithm => { ['hash160', 'hash256', 'ripemd160', 'sha1', 'sha256'].forEach(algorithm => {
describe(algorithm, () => { describe(algorithm, () => {
fixtures.forEach(f => { fixtures.forEach(f => {
const fn = bcrypto[algorithm] const fn = (bcrypto as any)[algorithm];
const expected = f[algorithm] const expected = (f as any)[algorithm];
it('returns ' + expected + ' for ' + f.hex, () => { it('returns ' + expected + ' for ' + f.hex, () => {
const data = Buffer.from(f.hex, 'hex') const data = Buffer.from(f.hex, 'hex');
const actual = fn(data).toString('hex') const actual = fn(data).toString('hex');
assert.strictEqual(actual, expected) assert.strictEqual(actual, expected);
}) });
}) });
}) });
}) });
}) });

View file

@ -1,284 +1,334 @@
const { describe, it, beforeEach } = require('mocha') import * as assert from 'assert';
const assert = require('assert') import { beforeEach, describe, it } from 'mocha';
const proxyquire = require('proxyquire') import * as proxyquire from 'proxyquire';
const hoodwink = require('hoodwink') import { ECPair, ECPairInterface, networks as NETWORKS } from '..';
import * as fixtures from './fixtures/ecpair.json';
const hoodwink = require('hoodwink');
const tinysecp = require('tiny-secp256k1');
const ECPair = require('../src/ecpair') const NETWORKS_LIST = Object.values(NETWORKS);
const tinysecp = require('tiny-secp256k1') const ZERO = Buffer.alloc(32, 0);
const ONE = Buffer.from(
const fixtures = require('./fixtures/ecpair.json') '0000000000000000000000000000000000000000000000000000000000000001',
'hex',
const NETWORKS = require('../src/networks') );
const NETWORKS_LIST = [] // Object.values(NETWORKS) const GROUP_ORDER = Buffer.from(
for (let networkName in NETWORKS) { 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141',
NETWORKS_LIST.push(NETWORKS[networkName]) 'hex',
} );
const GROUP_ORDER_LESS_1 = Buffer.from(
const ZERO = Buffer.alloc(32, 0) 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140',
const ONE = Buffer.from('0000000000000000000000000000000000000000000000000000000000000001', 'hex') 'hex',
const GROUP_ORDER = Buffer.from('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', 'hex') );
const GROUP_ORDER_LESS_1 = Buffer.from('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140', 'hex')
describe('ECPair', () => { describe('ECPair', () => {
describe('getPublicKey', () => { describe('getPublicKey', () => {
let keyPair let keyPair: ECPairInterface;
beforeEach(() => { beforeEach(() => {
keyPair = ECPair.fromPrivateKey(ONE) keyPair = ECPair.fromPrivateKey(ONE);
}) });
it('calls pointFromScalar lazily', hoodwink(() => { it(
assert.strictEqual(keyPair.__Q, undefined) 'calls pointFromScalar lazily',
hoodwink(() => {
assert.strictEqual((keyPair as any).__Q, undefined);
// .publicKey forces the memoization // .publicKey forces the memoization
assert.strictEqual(keyPair.publicKey.toString('hex'), '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798') assert.strictEqual(
assert.strictEqual(keyPair.__Q.toString('hex'), '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798') keyPair.publicKey.toString('hex'),
})) '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
}) );
assert.strictEqual(
(keyPair as any).__Q.toString('hex'),
'0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
);
}),
);
});
describe('fromPrivateKey', () => { describe('fromPrivateKey', () => {
it('defaults to compressed', () => { it('defaults to compressed', () => {
const keyPair = ECPair.fromPrivateKey(ONE) const keyPair = ECPair.fromPrivateKey(ONE);
assert.strictEqual(keyPair.compressed, true) assert.strictEqual(keyPair.compressed, true);
}) });
it('supports the uncompressed option', () => { it('supports the uncompressed option', () => {
const keyPair = ECPair.fromPrivateKey(ONE, { const keyPair = ECPair.fromPrivateKey(ONE, {
compressed: false compressed: false,
}) });
assert.strictEqual(keyPair.compressed, false) assert.strictEqual(keyPair.compressed, false);
}) });
it('supports the network option', () => { it('supports the network option', () => {
const keyPair = ECPair.fromPrivateKey(ONE, { const keyPair = ECPair.fromPrivateKey(ONE, {
compressed: false, compressed: false,
network: NETWORKS.testnet network: NETWORKS.testnet,
}) });
assert.strictEqual(keyPair.network, NETWORKS.testnet) assert.strictEqual(keyPair.network, NETWORKS.testnet);
}) });
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
it('derives public key for ' + f.WIF, () => { it('derives public key for ' + f.WIF, () => {
const d = Buffer.from(f.d, 'hex') const d = Buffer.from(f.d, 'hex');
const keyPair = ECPair.fromPrivateKey(d, { const keyPair = ECPair.fromPrivateKey(d, {
compressed: f.compressed compressed: f.compressed,
}) });
assert.strictEqual(keyPair.publicKey.toString('hex'), f.Q) assert.strictEqual(keyPair.publicKey.toString('hex'), f.Q);
}) });
}) });
fixtures.invalid.fromPrivateKey.forEach(f => { fixtures.invalid.fromPrivateKey.forEach(f => {
it('throws ' + f.exception, () => { it('throws ' + f.exception, () => {
const d = Buffer.from(f.d, 'hex') const d = Buffer.from(f.d, 'hex');
assert.throws(() => { assert.throws(() => {
ECPair.fromPrivateKey(d, f.options) ECPair.fromPrivateKey(d, (f as any).options);
}, new RegExp(f.exception)) }, new RegExp(f.exception));
}) });
}) });
}) });
describe('fromPublicKey', () => { describe('fromPublicKey', () => {
fixtures.invalid.fromPublicKey.forEach(f => { fixtures.invalid.fromPublicKey.forEach(f => {
it('throws ' + f.exception, () => { it('throws ' + f.exception, () => {
const Q = Buffer.from(f.Q, 'hex') const Q = Buffer.from(f.Q, 'hex');
assert.throws(() => { assert.throws(() => {
ECPair.fromPublicKey(Q, f.options) ECPair.fromPublicKey(Q, (f as any).options);
}, new RegExp(f.exception)) }, new RegExp(f.exception));
}) });
}) });
}) });
describe('fromWIF', () => { describe('fromWIF', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
it('imports ' + f.WIF + ' (' + f.network + ')', () => { it('imports ' + f.WIF + ' (' + f.network + ')', () => {
const network = NETWORKS[f.network] const network = (NETWORKS as any)[f.network];
const keyPair = ECPair.fromWIF(f.WIF, network) const keyPair = ECPair.fromWIF(f.WIF, network);
assert.strictEqual(keyPair.privateKey.toString('hex'), f.d) assert.strictEqual(keyPair.privateKey!.toString('hex'), f.d);
assert.strictEqual(keyPair.compressed, f.compressed) assert.strictEqual(keyPair.compressed, f.compressed);
assert.strictEqual(keyPair.network, network) assert.strictEqual(keyPair.network, network);
}) });
}) });
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
it('imports ' + f.WIF + ' (via list of networks)', () => { it('imports ' + f.WIF + ' (via list of networks)', () => {
const keyPair = ECPair.fromWIF(f.WIF, NETWORKS_LIST) const keyPair = ECPair.fromWIF(f.WIF, NETWORKS_LIST);
assert.strictEqual(keyPair.privateKey.toString('hex'), f.d) assert.strictEqual(keyPair.privateKey!.toString('hex'), f.d);
assert.strictEqual(keyPair.compressed, f.compressed) assert.strictEqual(keyPair.compressed, f.compressed);
assert.strictEqual(keyPair.network, NETWORKS[f.network]) assert.strictEqual(keyPair.network, (NETWORKS as any)[f.network]);
}) });
}) });
fixtures.invalid.fromWIF.forEach(f => { fixtures.invalid.fromWIF.forEach(f => {
it('throws on ' + f.WIF, () => { it('throws on ' + f.WIF, () => {
assert.throws(() => { assert.throws(() => {
const networks = f.network ? NETWORKS[f.network] : NETWORKS_LIST const networks = f.network
? (NETWORKS as any)[f.network]
: NETWORKS_LIST;
ECPair.fromWIF(f.WIF, networks) ECPair.fromWIF(f.WIF, networks);
}, new RegExp(f.exception)) }, new RegExp(f.exception));
}) });
}) });
}) });
describe('toWIF', () => { describe('toWIF', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
it('exports ' + f.WIF, () => { it('exports ' + f.WIF, () => {
const keyPair = ECPair.fromWIF(f.WIF, NETWORKS_LIST) const keyPair = ECPair.fromWIF(f.WIF, NETWORKS_LIST);
const result = keyPair.toWIF() const result = keyPair.toWIF();
assert.strictEqual(result, f.WIF) assert.strictEqual(result, f.WIF);
}) });
}) });
}) });
describe('makeRandom', () => { describe('makeRandom', () => {
const d = Buffer.alloc(32, 4) const d = Buffer.alloc(32, 4);
const exWIF = 'KwMWvwRJeFqxYyhZgNwYuYjbQENDAPAudQx5VEmKJrUZcq6aL2pv' const exWIF = 'KwMWvwRJeFqxYyhZgNwYuYjbQENDAPAudQx5VEmKJrUZcq6aL2pv';
describe('uses randombytes RNG', () => { describe('uses randombytes RNG', () => {
it('generates a ECPair', () => { it('generates a ECPair', () => {
const stub = { randombytes: () => { return d } } const stub = {
const ProxiedECPair = proxyquire('../src/ecpair', stub) randombytes: (): Buffer => {
return d;
},
};
const ProxiedECPair = proxyquire('../src/ecpair', stub);
const keyPair = ProxiedECPair.makeRandom() const keyPair = ProxiedECPair.makeRandom();
assert.strictEqual(keyPair.toWIF(), exWIF) assert.strictEqual(keyPair.toWIF(), exWIF);
}) });
}) });
it('allows a custom RNG to be used', () => { it('allows a custom RNG to be used', () => {
const keyPair = ECPair.makeRandom({ const keyPair = ECPair.makeRandom({
rng: size => { return d.slice(0, size) } rng: (size): Buffer => {
}) return d.slice(0, size);
},
});
assert.strictEqual(keyPair.toWIF(), exWIF) assert.strictEqual(keyPair.toWIF(), exWIF);
}) });
it('retains the same defaults as ECPair constructor', () => { it('retains the same defaults as ECPair constructor', () => {
const keyPair = ECPair.makeRandom() const keyPair = ECPair.makeRandom();
assert.strictEqual(keyPair.compressed, true) assert.strictEqual(keyPair.compressed, true);
assert.strictEqual(keyPair.network, NETWORKS.bitcoin) assert.strictEqual(keyPair.network, NETWORKS.bitcoin);
}) });
it('supports the options parameter', () => { it('supports the options parameter', () => {
const keyPair = ECPair.makeRandom({ const keyPair = ECPair.makeRandom({
compressed: false, compressed: false,
network: NETWORKS.testnet network: NETWORKS.testnet,
}) });
assert.strictEqual(keyPair.compressed, false) assert.strictEqual(keyPair.compressed, false);
assert.strictEqual(keyPair.network, NETWORKS.testnet) assert.strictEqual(keyPair.network, NETWORKS.testnet);
}) });
it('throws if d is bad length', () => { it('throws if d is bad length', () => {
function rng () { function rng(): Buffer {
return Buffer.alloc(28) return Buffer.alloc(28);
} }
assert.throws(() => { assert.throws(() => {
ECPair.makeRandom({ rng: rng }) ECPair.makeRandom({ rng });
}, /Expected Buffer\(Length: 32\), got Buffer\(Length: 28\)/) }, /Expected Buffer\(Length: 32\), got Buffer\(Length: 28\)/);
}) });
it('loops until d is within interval [1, n) : 1', hoodwink(function () { it(
'loops until d is within interval [1, n) : 1',
hoodwink(function(this: any): void {
const rng = this.stub(() => { const rng = this.stub(() => {
if (rng.calls === 0) return ZERO // 0 if (rng.calls === 0) return ZERO; // 0
return ONE // >0 return ONE; // >0
}, 2) }, 2);
ECPair.makeRandom({ rng: rng }) ECPair.makeRandom({ rng });
})) }),
);
it('loops until d is within interval [1, n) : n - 1', hoodwink(function () { it(
'loops until d is within interval [1, n) : n - 1',
hoodwink(function(this: any): void {
const rng = this.stub(() => { const rng = this.stub(() => {
if (rng.calls === 0) return ZERO // <1 if (rng.calls === 0) return ZERO; // <1
if (rng.calls === 1) return GROUP_ORDER // >n-1 if (rng.calls === 1) return GROUP_ORDER; // >n-1
return GROUP_ORDER_LESS_1 // n-1 return GROUP_ORDER_LESS_1; // n-1
}, 3) }, 3);
ECPair.makeRandom({ rng: rng }) ECPair.makeRandom({ rng });
})) }),
}) );
});
describe('.network', () => { describe('.network', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
it('returns ' + f.network + ' for ' + f.WIF, () => { it('returns ' + f.network + ' for ' + f.WIF, () => {
const network = NETWORKS[f.network] const network = (NETWORKS as any)[f.network];
const keyPair = ECPair.fromWIF(f.WIF, NETWORKS_LIST) const keyPair = ECPair.fromWIF(f.WIF, NETWORKS_LIST);
assert.strictEqual(keyPair.network, network) assert.strictEqual(keyPair.network, network);
}) });
}) });
}) });
describe('tinysecp wrappers', () => { describe('tinysecp wrappers', () => {
let keyPair let keyPair: ECPairInterface;
let hash let hash: Buffer;
let signature let signature: Buffer;
beforeEach(() => { beforeEach(() => {
keyPair = ECPair.makeRandom() keyPair = ECPair.makeRandom();
hash = ZERO hash = ZERO;
signature = Buffer.alloc(64, 1) signature = Buffer.alloc(64, 1);
}) });
describe('signing', () => { describe('signing', () => {
it('wraps tinysecp.sign', hoodwink(function () { it(
this.mock(tinysecp, 'sign', (h, d) => { 'wraps tinysecp.sign',
assert.strictEqual(h, hash) hoodwink(function(this: any): void {
assert.strictEqual(d, keyPair.privateKey) this.mock(
return signature tinysecp,
}, 1) 'sign',
(h: any, d: any) => {
assert.strictEqual(h, hash);
assert.strictEqual(d, keyPair.privateKey);
return signature;
},
1,
);
assert.strictEqual(keyPair.sign(hash), signature) assert.strictEqual(keyPair.sign(hash), signature);
})) }),
);
it('throws if no private key is found', () => { it('throws if no private key is found', () => {
delete keyPair.__D delete (keyPair as any).__D;
assert.throws(() => { assert.throws(() => {
keyPair.sign(hash) keyPair.sign(hash);
}, /Missing private key/) }, /Missing private key/);
}) });
}) });
describe('verify', () => { describe('verify', () => {
it('wraps tinysecp.verify', hoodwink(function () { it(
this.mock(tinysecp, 'verify', (h, q, s) => { 'wraps tinysecp.verify',
assert.strictEqual(h, hash) hoodwink(function(this: any): void {
assert.strictEqual(q, keyPair.publicKey) this.mock(
assert.strictEqual(s, signature) tinysecp,
return true 'verify',
}, 1) (h: any, q: any, s: any) => {
assert.strictEqual(h, hash);
assert.strictEqual(q, keyPair.publicKey);
assert.strictEqual(s, signature);
return true;
},
1,
);
assert.strictEqual(keyPair.verify(hash, signature), true) assert.strictEqual(keyPair.verify(hash, signature), true);
})) }),
}) );
}) });
});
describe('optional low R signing', () => { describe('optional low R signing', () => {
const sig = Buffer.from('95a6619140fca3366f1d3b013b0367c4f86e39508a50fdce' + const sig = Buffer.from(
'95a6619140fca3366f1d3b013b0367c4f86e39508a50fdce' +
'e5245fbb8bd60aa6086449e28cf15387cf9f85100bfd0838624ca96759e59f65c10a00' + 'e5245fbb8bd60aa6086449e28cf15387cf9f85100bfd0838624ca96759e59f65c10a00' +
'16b86f5229', 'hex') '16b86f5229',
const sigLowR = Buffer.from('6a2660c226e8055afad317eeba918a304be79208d505' + 'hex',
);
const sigLowR = Buffer.from(
'6a2660c226e8055afad317eeba918a304be79208d505' +
'3bc5ea4a5e4c5892b4a061c717c5284ae5202d721c0e49b4717b79966280906b1d3b52' + '3bc5ea4a5e4c5892b4a061c717c5284ae5202d721c0e49b4717b79966280906b1d3b52' +
'95d1fdde963c35', 'hex') '95d1fdde963c35',
const lowRKeyPair = ECPair.fromWIF('L3nThUzbAwpUiBAjR5zCu66ybXSPMr2zZ3ikp' + 'hex',
'ScpTPiYTxBynfZu') );
const dataToSign = Buffer.from('b6c5c548a7f6164c8aa7af5350901626ebd69f9ae' + const lowRKeyPair = ECPair.fromWIF(
'2c1ecf8871f5088ec204cfe', 'hex') 'L3nThUzbAwpUiBAjR5zCu66ybXSPMr2zZ3ikp' + 'ScpTPiYTxBynfZu',
);
const dataToSign = Buffer.from(
'b6c5c548a7f6164c8aa7af5350901626ebd69f9ae' + '2c1ecf8871f5088ec204cfe',
'hex',
);
it('signs with normal R by default', () => { it('signs with normal R by default', () => {
const signed = lowRKeyPair.sign(dataToSign) const signed = lowRKeyPair.sign(dataToSign);
assert.deepStrictEqual(sig, signed) assert.deepStrictEqual(sig, signed);
}) });
it('signs with low R when true is passed', () => { it('signs with low R when true is passed', () => {
const signed = lowRKeyPair.sign(dataToSign, true) const signed = lowRKeyPair.sign(dataToSign, true);
assert.deepStrictEqual(sigLowR, signed) assert.deepStrictEqual(sigLowR, signed);
}) });
}) });
}) });

View file

@ -1,8 +0,0 @@
const { RegtestUtils } = require('regtest-client')
const APIPASS = process.env.APIPASS || 'satoshi'
const APIURL = process.env.APIURL || 'https://regtest.bitbank.cc/1'
const regtestUtils = new RegtestUtils({ APIPASS, APIURL })
module.exports = regtestUtils;

View file

@ -0,0 +1,6 @@
import { RegtestUtils } from 'regtest-client';
const APIPASS = process.env.APIPASS || 'satoshi';
const APIURL = process.env.APIURL || 'https://regtest.bitbank.cc/1';
export const regtestUtils = new RegtestUtils({ APIPASS, APIURL });

View file

@ -1,117 +1,137 @@
const { describe, it } = require('mocha') import * as assert from 'assert';
const assert = require('assert') import { describe, it } from 'mocha';
const bitcoin = require('../../') import * as bitcoin from '../..';
const dhttp = require('./_regtest').dhttp import { regtestUtils } from './_regtest';
const TESTNET = bitcoin.networks.testnet const dhttp = regtestUtils.dhttp;
const TESTNET = bitcoin.networks.testnet;
describe('bitcoinjs-lib (addresses)', () => { describe('bitcoinjs-lib (addresses)', () => {
it('can generate a random address [and support the retrieval of transactions for that address (via 3PBP)', async () => { it('can generate a random address [and support the retrieval of transactions for that address (via 3PBP)', async () => {
const keyPair = bitcoin.ECPair.makeRandom() const keyPair = bitcoin.ECPair.makeRandom();
const { address } = bitcoin.payments.p2pkh({ pubkey: keyPair.publicKey }) const { address } = bitcoin.payments.p2pkh({ pubkey: keyPair.publicKey });
// bitcoin P2PKH addresses start with a '1' // bitcoin P2PKH addresses start with a '1'
assert.strictEqual(address.startsWith('1'), true) assert.strictEqual(address!.startsWith('1'), true);
const result = await dhttp({ const result = await dhttp({
method: 'GET', method: 'GET',
url: 'https://blockchain.info/rawaddr/' + address url: 'https://blockchain.info/rawaddr/' + address,
}) });
// random private keys [probably!] have no transactions // random private keys [probably!] have no transactions
assert.strictEqual(result.n_tx, 0) assert.strictEqual((result as any).n_tx, 0);
assert.strictEqual(result.total_received, 0) assert.strictEqual((result as any).total_received, 0);
assert.strictEqual(result.total_sent, 0) assert.strictEqual((result as any).total_sent, 0);
}) });
it('can import an address via WIF', () => { it('can import an address via WIF', () => {
const keyPair = bitcoin.ECPair.fromWIF('KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn') const keyPair = bitcoin.ECPair.fromWIF(
const { address } = bitcoin.payments.p2pkh({ pubkey: keyPair.publicKey }) 'KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn',
);
const { address } = bitcoin.payments.p2pkh({ pubkey: keyPair.publicKey });
assert.strictEqual(address, '1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH') assert.strictEqual(address, '1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH');
}) });
it('can generate a P2SH, pay-to-multisig (2-of-3) address', () => { it('can generate a P2SH, pay-to-multisig (2-of-3) address', () => {
const pubkeys = [ const pubkeys = [
'026477115981fe981a6918a6297d9803c4dc04f328f22041bedff886bbc2962e01', '026477115981fe981a6918a6297d9803c4dc04f328f22041bedff886bbc2962e01',
'02c96db2302d19b43d4c69368babace7854cc84eb9e061cde51cfa77ca4a22b8b9', '02c96db2302d19b43d4c69368babace7854cc84eb9e061cde51cfa77ca4a22b8b9',
'03c6103b3b83e4a24a0e33a4df246ef11772f9992663db0c35759a5e2ebf68d8e9' '03c6103b3b83e4a24a0e33a4df246ef11772f9992663db0c35759a5e2ebf68d8e9',
].map((hex) => Buffer.from(hex, 'hex')) ].map(hex => Buffer.from(hex, 'hex'));
const { address } = bitcoin.payments.p2sh({ const { address } = bitcoin.payments.p2sh({
redeem: bitcoin.payments.p2ms({ m: 2, pubkeys }) redeem: bitcoin.payments.p2ms({ m: 2, pubkeys }),
}) });
assert.strictEqual(address, '36NUkt6FWUi3LAWBqWRdDmdTWbt91Yvfu7') assert.strictEqual(address, '36NUkt6FWUi3LAWBqWRdDmdTWbt91Yvfu7');
}) });
it('can generate a SegWit address', () => { it('can generate a SegWit address', () => {
const keyPair = bitcoin.ECPair.fromWIF('KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn') const keyPair = bitcoin.ECPair.fromWIF(
const { address } = bitcoin.payments.p2wpkh({ pubkey: keyPair.publicKey }) 'KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn',
);
const { address } = bitcoin.payments.p2wpkh({ pubkey: keyPair.publicKey });
assert.strictEqual(address, 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4') assert.strictEqual(address, 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4');
}) });
it('can generate a SegWit address (via P2SH)', () => { it('can generate a SegWit address (via P2SH)', () => {
const keyPair = bitcoin.ECPair.fromWIF('KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn') const keyPair = bitcoin.ECPair.fromWIF(
'KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn',
);
const { address } = bitcoin.payments.p2sh({ const { address } = bitcoin.payments.p2sh({
redeem: bitcoin.payments.p2wpkh({ pubkey: keyPair.publicKey }) redeem: bitcoin.payments.p2wpkh({ pubkey: keyPair.publicKey }),
}) });
assert.strictEqual(address, '3JvL6Ymt8MVWiCNHC7oWU6nLeHNJKLZGLN') assert.strictEqual(address, '3JvL6Ymt8MVWiCNHC7oWU6nLeHNJKLZGLN');
}) });
it('can generate a P2WSH (SegWit), pay-to-multisig (3-of-4) address', () => { it('can generate a P2WSH (SegWit), pay-to-multisig (3-of-4) address', () => {
const pubkeys = [ const pubkeys = [
'026477115981fe981a6918a6297d9803c4dc04f328f22041bedff886bbc2962e01', '026477115981fe981a6918a6297d9803c4dc04f328f22041bedff886bbc2962e01',
'02c96db2302d19b43d4c69368babace7854cc84eb9e061cde51cfa77ca4a22b8b9', '02c96db2302d19b43d4c69368babace7854cc84eb9e061cde51cfa77ca4a22b8b9',
'023e4740d0ba639e28963f3476157b7cf2fb7c6fdf4254f97099cf8670b505ea59', '023e4740d0ba639e28963f3476157b7cf2fb7c6fdf4254f97099cf8670b505ea59',
'03c6103b3b83e4a24a0e33a4df246ef11772f9992663db0c35759a5e2ebf68d8e9' '03c6103b3b83e4a24a0e33a4df246ef11772f9992663db0c35759a5e2ebf68d8e9',
].map((hex) => Buffer.from(hex, 'hex')) ].map(hex => Buffer.from(hex, 'hex'));
const { address } = bitcoin.payments.p2wsh({ const { address } = bitcoin.payments.p2wsh({
redeem: bitcoin.payments.p2ms({ m: 3, pubkeys }) redeem: bitcoin.payments.p2ms({ m: 3, pubkeys }),
}) });
assert.strictEqual(address, 'bc1q75f6dv4q8ug7zhujrsp5t0hzf33lllnr3fe7e2pra3v24mzl8rrqtp3qul') assert.strictEqual(
}) address,
'bc1q75f6dv4q8ug7zhujrsp5t0hzf33lllnr3fe7e2pra3v24mzl8rrqtp3qul',
);
});
it('can generate a P2SH(P2WSH(...)), pay-to-multisig (2-of-2) address', () => { it('can generate a P2SH(P2WSH(...)), pay-to-multisig (2-of-2) address', () => {
const pubkeys = [ const pubkeys = [
'026477115981fe981a6918a6297d9803c4dc04f328f22041bedff886bbc2962e01', '026477115981fe981a6918a6297d9803c4dc04f328f22041bedff886bbc2962e01',
'02c96db2302d19b43d4c69368babace7854cc84eb9e061cde51cfa77ca4a22b8b9' '02c96db2302d19b43d4c69368babace7854cc84eb9e061cde51cfa77ca4a22b8b9',
].map((hex) => Buffer.from(hex, 'hex')) ].map(hex => Buffer.from(hex, 'hex'));
const { address } = bitcoin.payments.p2sh({ const { address } = bitcoin.payments.p2sh({
redeem: bitcoin.payments.p2wsh({ redeem: bitcoin.payments.p2wsh({
redeem: bitcoin.payments.p2ms({ m: 2, pubkeys }) redeem: bitcoin.payments.p2ms({ m: 2, pubkeys }),
}) }),
}) });
assert.strictEqual(address, '3P4mrxQfmExfhxqjLnR2Ah4WES5EB1KBrN') assert.strictEqual(address, '3P4mrxQfmExfhxqjLnR2Ah4WES5EB1KBrN');
}) });
// examples using other network information // examples using other network information
it('can generate a Testnet address', () => { it('can generate a Testnet address', () => {
const keyPair = bitcoin.ECPair.makeRandom({ network: TESTNET }) const keyPair = bitcoin.ECPair.makeRandom({ network: TESTNET });
const { address } = bitcoin.payments.p2pkh({ pubkey: keyPair.publicKey, network: TESTNET }) const { address } = bitcoin.payments.p2pkh({
pubkey: keyPair.publicKey,
network: TESTNET,
});
// bitcoin testnet P2PKH addresses start with a 'm' or 'n' // bitcoin testnet P2PKH addresses start with a 'm' or 'n'
assert.strictEqual(address.startsWith('m') || address.startsWith('n'), true) assert.strictEqual(
}) address!.startsWith('m') || address!.startsWith('n'),
true,
);
});
it('can generate a Litecoin address', () => { it('can generate a Litecoin address', () => {
// WARNING: although possible, bitcoinjs is NOT necessarily compatible with Litecoin // WARNING: although possible, bitcoinjs is NOT necessarily compatible with Litecoin
const LITECOIN = { const LITECOIN = {
messagePrefix: '\x19Litecoin Signed Message:\n', messagePrefix: '\x19Litecoin Signed Message:\n',
bech32: 'ltc',
bip32: { bip32: {
public: 0x019da462, public: 0x019da462,
private: 0x019d9cfe private: 0x019d9cfe,
}, },
pubKeyHash: 0x30, pubKeyHash: 0x30,
scriptHash: 0x32, scriptHash: 0x32,
wif: 0xb0 wif: 0xb0,
} };
const keyPair = bitcoin.ECPair.makeRandom({ network: LITECOIN }) const keyPair = bitcoin.ECPair.makeRandom({ network: LITECOIN });
const { address } = bitcoin.payments.p2pkh({ pubkey: keyPair.publicKey, network: LITECOIN }) const { address } = bitcoin.payments.p2pkh({
pubkey: keyPair.publicKey,
network: LITECOIN,
});
assert.strictEqual(address.startsWith('L'), true) assert.strictEqual(address!.startsWith('L'), true);
}) });
}) });

View file

@ -1,101 +1,151 @@
const { describe, it } = require('mocha') import * as assert from 'assert';
const assert = require('assert') import { describe, it } from 'mocha';
const bip32 = require('bip32') import * as bip32 from 'bip32';
const bip39 = require('bip39') import * as bip39 from 'bip39';
const bitcoin = require('../../') import * as bitcoin from '../..';
function getAddress (node, network) { function getAddress(node: any, network?: any): string {
return bitcoin.payments.p2pkh({ pubkey: node.publicKey, network }).address return bitcoin.payments.p2pkh({ pubkey: node.publicKey, network }).address!;
} }
describe('bitcoinjs-lib (BIP32)', () => { describe('bitcoinjs-lib (BIP32)', () => {
it('can import a BIP32 testnet xpriv and export to WIF', () => { it('can import a BIP32 testnet xpriv and export to WIF', () => {
const xpriv = 'tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK' const xpriv =
const node = bip32.fromBase58(xpriv, bitcoin.networks.testnet) 'tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK';
const node = bip32.fromBase58(xpriv, bitcoin.networks.testnet);
assert.strictEqual(node.toWIF(), 'cQfoY67cetFNunmBUX5wJiw3VNoYx3gG9U9CAofKE6BfiV1fSRw7') assert.strictEqual(
}) node.toWIF(),
'cQfoY67cetFNunmBUX5wJiw3VNoYx3gG9U9CAofKE6BfiV1fSRw7',
);
});
it('can export a BIP32 xpriv, then import it', () => { it('can export a BIP32 xpriv, then import it', () => {
const mnemonic = 'praise you muffin lion enable neck grocery crumble super myself license ghost' const mnemonic =
const seed = bip39.mnemonicToSeed(mnemonic) 'praise you muffin lion enable neck grocery crumble super myself license ghost';
const node = bip32.fromSeed(seed) const seed = bip39.mnemonicToSeedSync(mnemonic);
const string = node.toBase58() const node = bip32.fromSeed(seed);
const restored = bip32.fromBase58(string) const string = node.toBase58();
const restored = bip32.fromBase58(string);
assert.strictEqual(getAddress(node), getAddress(restored)) // same public key assert.strictEqual(getAddress(node), getAddress(restored)); // same public key
assert.strictEqual(node.toWIF(), restored.toWIF()) // same private key assert.strictEqual(node.toWIF(), restored.toWIF()); // same private key
}) });
it('can export a BIP32 xpub', () => { it('can export a BIP32 xpub', () => {
const mnemonic = 'praise you muffin lion enable neck grocery crumble super myself license ghost' const mnemonic =
const seed = bip39.mnemonicToSeed(mnemonic) 'praise you muffin lion enable neck grocery crumble super myself license ghost';
const node = bip32.fromSeed(seed) const seed = bip39.mnemonicToSeedSync(mnemonic);
const string = node.neutered().toBase58() const node = bip32.fromSeed(seed);
const string = node.neutered().toBase58();
assert.strictEqual(string, 'xpub661MyMwAqRbcGhVeaVfEBA25e3cP9DsJQZoE8iep5fZSxy3TnPBNBgWnMZx56oreNc48ZoTkQfatNJ9VWnQ7ZcLZcVStpaXLTeG8bGrzX3n') assert.strictEqual(
}) string,
'xpub661MyMwAqRbcGhVeaVfEBA25e3cP9DsJQZoE8iep5fZSxy3TnPBNBgWnMZx56oreNc48ZoTkQfatNJ9VWnQ7ZcLZcVStpaXLTeG8bGrzX3n',
);
});
it('can create a BIP32, bitcoin, account 0, external address', () => { it('can create a BIP32, bitcoin, account 0, external address', () => {
const path = "m/0'/0/0" const path = "m/0'/0/0";
const root = bip32.fromSeed(Buffer.from('dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd', 'hex')) const root = bip32.fromSeed(
Buffer.from(
'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',
'hex',
),
);
const child1 = root.derivePath(path) const child1 = root.derivePath(path);
// option 2, manually // option 2, manually
const child1b = root.deriveHardened(0) const child1b = root
.derive(0) .deriveHardened(0)
.derive(0) .derive(0)
.derive(0);
assert.strictEqual(getAddress(child1), '1JHyB1oPXufr4FXkfitsjgNB5yRY9jAaa7') assert.strictEqual(
assert.strictEqual(getAddress(child1b), '1JHyB1oPXufr4FXkfitsjgNB5yRY9jAaa7') getAddress(child1),
}) '1JHyB1oPXufr4FXkfitsjgNB5yRY9jAaa7',
);
assert.strictEqual(
getAddress(child1b),
'1JHyB1oPXufr4FXkfitsjgNB5yRY9jAaa7',
);
});
it('can create a BIP44, bitcoin, account 0, external address', () => { it('can create a BIP44, bitcoin, account 0, external address', () => {
const root = bip32.fromSeed(Buffer.from('dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd', 'hex')) const root = bip32.fromSeed(
Buffer.from(
'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',
'hex',
),
);
const child1 = root.derivePath("m/44'/0'/0'/0/0") const child1 = root.derivePath("m/44'/0'/0'/0/0");
// option 2, manually // option 2, manually
const child1b = root.deriveHardened(44) const child1b = root
.deriveHardened(44)
.deriveHardened(0) .deriveHardened(0)
.deriveHardened(0) .deriveHardened(0)
.derive(0) .derive(0)
.derive(0) .derive(0);
assert.strictEqual(getAddress(child1), '12Tyvr1U8A3ped6zwMEU5M8cx3G38sP5Au') assert.strictEqual(
assert.strictEqual(getAddress(child1b), '12Tyvr1U8A3ped6zwMEU5M8cx3G38sP5Au') getAddress(child1),
}) '12Tyvr1U8A3ped6zwMEU5M8cx3G38sP5Au',
);
assert.strictEqual(
getAddress(child1b),
'12Tyvr1U8A3ped6zwMEU5M8cx3G38sP5Au',
);
});
it('can create a BIP49, bitcoin testnet, account 0, external address', () => { it('can create a BIP49, bitcoin testnet, account 0, external address', () => {
const mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about' const mnemonic =
const seed = bip39.mnemonicToSeed(mnemonic) 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
const root = bip32.fromSeed(seed) const seed = bip39.mnemonicToSeedSync(mnemonic);
const root = bip32.fromSeed(seed);
const path = "m/49'/1'/0'/0/0" const path = "m/49'/1'/0'/0/0";
const child = root.derivePath(path) const child = root.derivePath(path);
const { address } = bitcoin.payments.p2sh({ const { address } = bitcoin.payments.p2sh({
redeem: bitcoin.payments.p2wpkh({ pubkey: child.publicKey, network: bitcoin.networks.testnet }), redeem: bitcoin.payments.p2wpkh({
network: bitcoin.networks.testnet pubkey: child.publicKey,
}) network: bitcoin.networks.testnet,
assert.strictEqual(address, '2Mww8dCYPUpKHofjgcXcBCEGmniw9CoaiD2') }),
}) network: bitcoin.networks.testnet,
});
assert.strictEqual(address, '2Mww8dCYPUpKHofjgcXcBCEGmniw9CoaiD2');
});
it('can use BIP39 to generate BIP32 addresses', () => { it('can use BIP39 to generate BIP32 addresses', () => {
// var mnemonic = bip39.generateMnemonic() // var mnemonic = bip39.generateMnemonic()
const mnemonic = 'praise you muffin lion enable neck grocery crumble super myself license ghost' const mnemonic =
assert(bip39.validateMnemonic(mnemonic)) 'praise you muffin lion enable neck grocery crumble super myself license ghost';
assert(bip39.validateMnemonic(mnemonic));
const seed = bip39.mnemonicToSeed(mnemonic) const seed = bip39.mnemonicToSeedSync(mnemonic);
const root = bip32.fromSeed(seed) const root = bip32.fromSeed(seed);
// receive addresses // receive addresses
assert.strictEqual(getAddress(root.derivePath("m/0'/0/0")), '1AVQHbGuES57wD68AJi7Gcobc3RZrfYWTC') assert.strictEqual(
assert.strictEqual(getAddress(root.derivePath("m/0'/0/1")), '1Ad6nsmqDzbQo5a822C9bkvAfrYv9mc1JL') getAddress(root.derivePath("m/0'/0/0")),
'1AVQHbGuES57wD68AJi7Gcobc3RZrfYWTC',
);
assert.strictEqual(
getAddress(root.derivePath("m/0'/0/1")),
'1Ad6nsmqDzbQo5a822C9bkvAfrYv9mc1JL',
);
// change addresses // change addresses
assert.strictEqual(getAddress(root.derivePath("m/0'/1/0")), '1349KVc5NgedaK7DvuD4xDFxL86QN1Hvdn') assert.strictEqual(
assert.strictEqual(getAddress(root.derivePath("m/0'/1/1")), '1EAvj4edpsWcSer3duybAd4KiR4bCJW5J6') getAddress(root.derivePath("m/0'/1/0")),
}) '1349KVc5NgedaK7DvuD4xDFxL86QN1Hvdn',
}) );
assert.strictEqual(
getAddress(root.derivePath("m/0'/1/1")),
'1EAvj4edpsWcSer3duybAd4KiR4bCJW5J6',
);
});
});

View file

@ -1,22 +1,21 @@
'use strict' import * as assert from 'assert';
import { describe, it } from 'mocha';
const { describe, it } = require('mocha') import * as bitcoin from '../..';
const assert = require('assert')
const bitcoin = require('../../')
describe('bitcoinjs-lib (blocks)', () => { describe('bitcoinjs-lib (blocks)', () => {
it('can extract a height from a CoinBase transaction', () => { it('can extract a height from a CoinBase transaction', () => {
// from 00000000000000000097669cdca131f24d40c4cc7d80eaa65967a2d09acf6ce6 // from 00000000000000000097669cdca131f24d40c4cc7d80eaa65967a2d09acf6ce6
const txHex = '010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff50037f9a07174d696e656420627920416e74506f6f6c685b205a2b1f7bfabe6d6d36afe1910eca9405b66f97750940a656e38e2c0312958190ff8e98fd16761d220400000000000000aa340000d49f0000ffffffff02b07fc366000000001976a9148349212dc27ce3ab4c5b29b85c4dec643d764b1788ac0000000000000000266a24aa21a9ed72d9432948505e3d3062f1307a3f027a5dea846ff85e47159680919c12bf1e400120000000000000000000000000000000000000000000000000000000000000000000000000' const txHex =
const tx = bitcoin.Transaction.fromHex(txHex) '010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff50037f9a07174d696e656420627920416e74506f6f6c685b205a2b1f7bfabe6d6d36afe1910eca9405b66f97750940a656e38e2c0312958190ff8e98fd16761d220400000000000000aa340000d49f0000ffffffff02b07fc366000000001976a9148349212dc27ce3ab4c5b29b85c4dec643d764b1788ac0000000000000000266a24aa21a9ed72d9432948505e3d3062f1307a3f027a5dea846ff85e47159680919c12bf1e400120000000000000000000000000000000000000000000000000000000000000000000000000';
const tx = bitcoin.Transaction.fromHex(txHex);
assert.strictEqual(tx.ins.length, 1) assert.strictEqual(tx.ins.length, 1);
const script = tx.ins[0].script const script = tx.ins[0].script;
// bitcoin.script.decompile(script) // returns [] :( // bitcoin.script.decompile(script) // returns [] :(
assert.strictEqual(script[0], 0x03) assert.strictEqual(script[0], 0x03);
const heightBuffer = script.slice(1, 4) const heightBuffer = script.slice(1, 4);
const height = bitcoin.script.number.decode(heightBuffer) const height = bitcoin.script.number.decode(heightBuffer);
assert.strictEqual(height, 498303) assert.strictEqual(height, 498303);
}) });
}) });

View file

@ -1,198 +1,219 @@
const { describe, it, before } = require('mocha') import * as assert from 'assert';
const assert = require('assert') import { before, describe, it } from 'mocha';
const bitcoin = require('../../') import * as bitcoin from '../..';
const regtestUtils = require('./_regtest') import { regtestUtils } from './_regtest';
const regtest = regtestUtils.network const regtest = regtestUtils.network;
const bip65 = require('bip65') const bip65 = require('bip65');
const alice = bitcoin.ECPair.fromWIF('cScfkGjbzzoeewVWmU2hYPUHeVGJRDdFt7WhmrVVGkxpmPP8BHWe', regtest) const alice = bitcoin.ECPair.fromWIF(
const bob = bitcoin.ECPair.fromWIF('cMkopUXKWsEzAjfa1zApksGRwjVpJRB3831qM9W4gKZsLwjHXA9x', regtest) 'cScfkGjbzzoeewVWmU2hYPUHeVGJRDdFt7WhmrVVGkxpmPP8BHWe',
console.warn = () => {} // Silence the Deprecation Warning regtest,
);
const bob = bitcoin.ECPair.fromWIF(
'cMkopUXKWsEzAjfa1zApksGRwjVpJRB3831qM9W4gKZsLwjHXA9x',
regtest,
);
console.warn = () => {}; // Silence the Deprecation Warning
describe('bitcoinjs-lib (transactions w/ CLTV)', () => { describe('bitcoinjs-lib (transactions w/ CLTV)', () => {
// force update MTP // force update MTP
before(async () => { before(async () => {
await regtestUtils.mine(11) await regtestUtils.mine(11);
}) });
const hashType = bitcoin.Transaction.SIGHASH_ALL const hashType = bitcoin.Transaction.SIGHASH_ALL;
function cltvCheckSigOutput (aQ, bQ, lockTime) { type keyPair = { publicKey: Buffer };
return bitcoin.script.compile([ function cltvCheckSigOutput(aQ: keyPair, bQ: keyPair, lockTime: number) {
bitcoin.opcodes.OP_IF, return bitcoin.script.fromASM(
bitcoin.script.number.encode(lockTime), `
bitcoin.opcodes.OP_CHECKLOCKTIMEVERIFY, OP_IF
bitcoin.opcodes.OP_DROP, ${bitcoin.script.number.encode(lockTime).toString('hex')}
OP_CHECKLOCKTIMEVERIFY
bitcoin.opcodes.OP_ELSE, OP_DROP
bQ.publicKey, OP_ELSE
bitcoin.opcodes.OP_CHECKSIGVERIFY, ${bQ.publicKey.toString('hex')}
bitcoin.opcodes.OP_ENDIF, OP_CHECKSIGVERIFY
OP_ENDIF
aQ.publicKey, ${aQ.publicKey.toString('hex')}
bitcoin.opcodes.OP_CHECKSIG OP_CHECKSIG
]) `
.trim()
.replace(/\s+/g, ' '),
);
} }
function utcNow () { function utcNow() {
return Math.floor(Date.now() / 1000) return Math.floor(Date.now() / 1000);
} }
// expiry past, {Alice's signature} OP_TRUE // expiry past, {Alice's signature} OP_TRUE
it('can create (and broadcast via 3PBP) a Transaction where Alice can redeem the output after the expiry (in the past)', async () => { it('can create (and broadcast via 3PBP) a Transaction where Alice can redeem the output after the expiry (in the past)', async () => {
// 3 hours ago // 3 hours ago
const lockTime = bip65.encode({ utc: utcNow() - (3600 * 3) }) const lockTime = bip65.encode({ utc: utcNow() - 3600 * 3 });
const redeemScript = cltvCheckSigOutput(alice, bob, lockTime) const redeemScript = cltvCheckSigOutput(alice, bob, lockTime);
const { address } = bitcoin.payments.p2sh({ redeem: { output: redeemScript, network: regtest }, network: regtest }) const { address } = bitcoin.payments.p2sh({
redeem: { output: redeemScript, network: regtest },
network: regtest,
});
// fund the P2SH(CLTV) address // fund the P2SH(CLTV) address
const unspent = await regtestUtils.faucet(address, 1e5) const unspent = await regtestUtils.faucet(address!, 1e5);
const txb = new bitcoin.TransactionBuilder(regtest) const txb = new bitcoin.TransactionBuilder(regtest);
txb.setLockTime(lockTime) txb.setLockTime(lockTime);
// Note: nSequence MUST be <= 0xfffffffe otherwise LockTime is ignored, and is immediately spendable. // Note: nSequence MUST be <= 0xfffffffe otherwise LockTime is ignored, and is immediately spendable.
txb.addInput(unspent.txId, unspent.vout, 0xfffffffe) txb.addInput(unspent.txId, unspent.vout, 0xfffffffe);
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4) txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4);
// {Alice's signature} OP_TRUE // {Alice's signature} OP_TRUE
const tx = txb.buildIncomplete() const tx = txb.buildIncomplete();
const signatureHash = tx.hashForSignature(0, redeemScript, hashType) const signatureHash = tx.hashForSignature(0, redeemScript, hashType);
const redeemScriptSig = bitcoin.payments.p2sh({ const redeemScriptSig = bitcoin.payments.p2sh({
redeem: { redeem: {
input: bitcoin.script.compile([ input: bitcoin.script.compile([
bitcoin.script.signature.encode(alice.sign(signatureHash), hashType), bitcoin.script.signature.encode(alice.sign(signatureHash), hashType),
bitcoin.opcodes.OP_TRUE bitcoin.opcodes.OP_TRUE,
]), ]),
output: redeemScript output: redeemScript,
} },
}).input }).input;
tx.setInputScript(0, redeemScriptSig) tx.setInputScript(0, redeemScriptSig!);
await regtestUtils.broadcast(tx.toHex()) await regtestUtils.broadcast(tx.toHex());
await regtestUtils.verify({ await regtestUtils.verify({
txId: tx.getId(), txId: tx.getId(),
address: regtestUtils.RANDOM_ADDRESS, address: regtestUtils.RANDOM_ADDRESS,
vout: 0, vout: 0,
value: 7e4 value: 7e4,
}) });
}) });
// expiry will pass, {Alice's signature} OP_TRUE // expiry will pass, {Alice's signature} OP_TRUE
it('can create (and broadcast via 3PBP) a Transaction where Alice can redeem the output after the expiry (in the future)', async () => { it('can create (and broadcast via 3PBP) a Transaction where Alice can redeem the output after the expiry (in the future)', async () => {
const height = await regtestUtils.height() const height = await regtestUtils.height();
// 5 blocks from now // 5 blocks from now
const lockTime = bip65.encode({ blocks: height + 5 }) const lockTime = bip65.encode({ blocks: height + 5 });
const redeemScript = cltvCheckSigOutput(alice, bob, lockTime) const redeemScript = cltvCheckSigOutput(alice, bob, lockTime);
const { address } = bitcoin.payments.p2sh({ redeem: { output: redeemScript, network: regtest }, network: regtest }) const { address } = bitcoin.payments.p2sh({
redeem: { output: redeemScript, network: regtest },
network: regtest,
});
// fund the P2SH(CLTV) address // fund the P2SH(CLTV) address
const unspent = await regtestUtils.faucet(address, 1e5) const unspent = await regtestUtils.faucet(address!, 1e5);
const txb = new bitcoin.TransactionBuilder(regtest) const txb = new bitcoin.TransactionBuilder(regtest);
txb.setLockTime(lockTime) txb.setLockTime(lockTime);
// Note: nSequence MUST be <= 0xfffffffe otherwise LockTime is ignored, and is immediately spendable. // Note: nSequence MUST be <= 0xfffffffe otherwise LockTime is ignored, and is immediately spendable.
txb.addInput(unspent.txId, unspent.vout, 0xfffffffe) txb.addInput(unspent.txId, unspent.vout, 0xfffffffe);
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4) txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4);
// {Alice's signature} OP_TRUE // {Alice's signature} OP_TRUE
const tx = txb.buildIncomplete() const tx = txb.buildIncomplete();
const signatureHash = tx.hashForSignature(0, redeemScript, hashType) const signatureHash = tx.hashForSignature(0, redeemScript, hashType);
const redeemScriptSig = bitcoin.payments.p2sh({ const redeemScriptSig = bitcoin.payments.p2sh({
redeem: { redeem: {
input: bitcoin.script.compile([ input: bitcoin.script.compile([
bitcoin.script.signature.encode(alice.sign(signatureHash), hashType), bitcoin.script.signature.encode(alice.sign(signatureHash), hashType),
bitcoin.opcodes.OP_TRUE bitcoin.opcodes.OP_TRUE,
]), ]),
output: redeemScript output: redeemScript,
} },
}).input }).input;
tx.setInputScript(0, redeemScriptSig) tx.setInputScript(0, redeemScriptSig!);
// TODO: test that it failures _prior_ to expiry, unfortunately, race conditions when run concurrently // TODO: test that it failures _prior_ to expiry, unfortunately, race conditions when run concurrently
// ... // ...
// into the future! // into the future!
await regtestUtils.mine(5) await regtestUtils.mine(5);
await regtestUtils.broadcast(tx.toHex()) await regtestUtils.broadcast(tx.toHex());
await regtestUtils.verify({ await regtestUtils.verify({
txId: tx.getId(), txId: tx.getId(),
address: regtestUtils.RANDOM_ADDRESS, address: regtestUtils.RANDOM_ADDRESS,
vout: 0, vout: 0,
value: 7e4 value: 7e4,
}) });
}) });
// expiry ignored, {Bob's signature} {Alice's signature} OP_FALSE // expiry ignored, {Bob's signature} {Alice's signature} OP_FALSE
it('can create (and broadcast via 3PBP) a Transaction where Alice and Bob can redeem the output at any time', async () => { it('can create (and broadcast via 3PBP) a Transaction where Alice and Bob can redeem the output at any time', async () => {
// two hours ago // two hours ago
const lockTime = bip65.encode({ utc: utcNow() - (3600 * 2) }) const lockTime = bip65.encode({ utc: utcNow() - 3600 * 2 });
const redeemScript = cltvCheckSigOutput(alice, bob, lockTime) const redeemScript = cltvCheckSigOutput(alice, bob, lockTime);
const { address } = bitcoin.payments.p2sh({ redeem: { output: redeemScript, network: regtest }, network: regtest }) const { address } = bitcoin.payments.p2sh({
redeem: { output: redeemScript, network: regtest },
network: regtest,
});
// fund the P2SH(CLTV) address // fund the P2SH(CLTV) address
const unspent = await regtestUtils.faucet(address, 2e5) const unspent = await regtestUtils.faucet(address!, 2e5);
const txb = new bitcoin.TransactionBuilder(regtest) const txb = new bitcoin.TransactionBuilder(regtest);
txb.setLockTime(lockTime) txb.setLockTime(lockTime);
// Note: nSequence MUST be <= 0xfffffffe otherwise LockTime is ignored, and is immediately spendable. // Note: nSequence MUST be <= 0xfffffffe otherwise LockTime is ignored, and is immediately spendable.
txb.addInput(unspent.txId, unspent.vout, 0xfffffffe) txb.addInput(unspent.txId, unspent.vout, 0xfffffffe);
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 8e4) txb.addOutput(regtestUtils.RANDOM_ADDRESS, 8e4);
// {Alice's signature} {Bob's signature} OP_FALSE // {Alice's signature} {Bob's signature} OP_FALSE
const tx = txb.buildIncomplete() const tx = txb.buildIncomplete();
const signatureHash = tx.hashForSignature(0, redeemScript, hashType) const signatureHash = tx.hashForSignature(0, redeemScript, hashType);
const redeemScriptSig = bitcoin.payments.p2sh({ const redeemScriptSig = bitcoin.payments.p2sh({
redeem: { redeem: {
input: bitcoin.script.compile([ input: bitcoin.script.compile([
bitcoin.script.signature.encode(alice.sign(signatureHash), hashType), bitcoin.script.signature.encode(alice.sign(signatureHash), hashType),
bitcoin.script.signature.encode(bob.sign(signatureHash), hashType), bitcoin.script.signature.encode(bob.sign(signatureHash), hashType),
bitcoin.opcodes.OP_FALSE bitcoin.opcodes.OP_FALSE,
]), ]),
output: redeemScript output: redeemScript,
} },
}).input }).input;
tx.setInputScript(0, redeemScriptSig) tx.setInputScript(0, redeemScriptSig!);
await regtestUtils.broadcast(tx.toHex()) await regtestUtils.broadcast(tx.toHex());
await regtestUtils.verify({ await regtestUtils.verify({
txId: tx.getId(), txId: tx.getId(),
address: regtestUtils.RANDOM_ADDRESS, address: regtestUtils.RANDOM_ADDRESS,
vout: 0, vout: 0,
value: 8e4 value: 8e4,
}) });
}) });
// expiry in the future, {Alice's signature} OP_TRUE // expiry in the future, {Alice's signature} OP_TRUE
it('can create (but fail to broadcast via 3PBP) a Transaction where Alice attempts to redeem before the expiry', async () => { it('can create (but fail to broadcast via 3PBP) a Transaction where Alice attempts to redeem before the expiry', async () => {
// two hours from now // two hours from now
const lockTime = bip65.encode({ utc: utcNow() + (3600 * 2) }) const lockTime = bip65.encode({ utc: utcNow() + 3600 * 2 });
const redeemScript = cltvCheckSigOutput(alice, bob, lockTime) const redeemScript = cltvCheckSigOutput(alice, bob, lockTime);
const { address } = bitcoin.payments.p2sh({ redeem: { output: redeemScript, network: regtest }, network: regtest }) const { address } = bitcoin.payments.p2sh({
redeem: { output: redeemScript, network: regtest },
network: regtest,
});
// fund the P2SH(CLTV) address // fund the P2SH(CLTV) address
const unspent = await regtestUtils.faucet(address, 2e4) const unspent = await regtestUtils.faucet(address!, 2e4);
const txb = new bitcoin.TransactionBuilder(regtest) const txb = new bitcoin.TransactionBuilder(regtest);
txb.setLockTime(lockTime) txb.setLockTime(lockTime);
// Note: nSequence MUST be <= 0xfffffffe otherwise LockTime is ignored, and is immediately spendable. // Note: nSequence MUST be <= 0xfffffffe otherwise LockTime is ignored, and is immediately spendable.
txb.addInput(unspent.txId, unspent.vout, 0xfffffffe) txb.addInput(unspent.txId, unspent.vout, 0xfffffffe);
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 1e4) txb.addOutput(regtestUtils.RANDOM_ADDRESS, 1e4);
// {Alice's signature} OP_TRUE // {Alice's signature} OP_TRUE
const tx = txb.buildIncomplete() const tx = txb.buildIncomplete();
const signatureHash = tx.hashForSignature(0, redeemScript, hashType) const signatureHash = tx.hashForSignature(0, redeemScript, hashType);
const redeemScriptSig = bitcoin.payments.p2sh({ const redeemScriptSig = bitcoin.payments.p2sh({
redeem: { redeem: {
input: bitcoin.script.compile([ input: bitcoin.script.compile([
bitcoin.script.signature.encode(alice.sign(signatureHash), hashType), bitcoin.script.signature.encode(alice.sign(signatureHash), hashType),
bitcoin.script.signature.encode(bob.sign(signatureHash), hashType), bitcoin.script.signature.encode(bob.sign(signatureHash), hashType),
bitcoin.opcodes.OP_TRUE bitcoin.opcodes.OP_TRUE,
]), ]),
output: redeemScript output: redeemScript,
} },
}).input }).input;
tx.setInputScript(0, redeemScriptSig) tx.setInputScript(0, redeemScriptSig!);
await regtestUtils.broadcast(tx.toHex()).catch(err => { await regtestUtils.broadcast(tx.toHex()).catch(err => {
assert.throws(() => { assert.throws(() => {
if (err) throw err if (err) throw err;
}, /Error: non-final \(code 64\)/) }, /Error: non-final \(code 64\)/);
}) });
}) });
}) });

View file

@ -1,27 +1,41 @@
const { describe, it, before } = require('mocha') import * as assert from 'assert';
const assert = require('assert') import { before, describe, it } from 'mocha';
const bitcoin = require('../../') import * as bitcoin from '../..';
const regtestUtils = require('./_regtest') import { regtestUtils } from './_regtest';
const regtest = regtestUtils.network const regtest = regtestUtils.network;
const bip68 = require('bip68') const bip68 = require('bip68');
const alice = bitcoin.ECPair.fromWIF('cScfkGjbzzoeewVWmU2hYPUHeVGJRDdFt7WhmrVVGkxpmPP8BHWe', regtest) const alice = bitcoin.ECPair.fromWIF(
const bob = bitcoin.ECPair.fromWIF('cMkopUXKWsEzAjfa1zApksGRwjVpJRB3831qM9W4gKZsLwjHXA9x', regtest) 'cScfkGjbzzoeewVWmU2hYPUHeVGJRDdFt7WhmrVVGkxpmPP8BHWe',
const charles = bitcoin.ECPair.fromWIF('cMkopUXKWsEzAjfa1zApksGRwjVpJRB3831qM9W4gKZsMSb4Ubnf', regtest) regtest,
const dave = bitcoin.ECPair.fromWIF('cMkopUXKWsEzAjfa1zApksGRwjVpJRB3831qM9W4gKZsMwS4pqnx', regtest) );
console.warn = () => {} // Silence the Deprecation Warning const bob = bitcoin.ECPair.fromWIF(
'cMkopUXKWsEzAjfa1zApksGRwjVpJRB3831qM9W4gKZsLwjHXA9x',
regtest,
);
const charles = bitcoin.ECPair.fromWIF(
'cMkopUXKWsEzAjfa1zApksGRwjVpJRB3831qM9W4gKZsMSb4Ubnf',
regtest,
);
const dave = bitcoin.ECPair.fromWIF(
'cMkopUXKWsEzAjfa1zApksGRwjVpJRB3831qM9W4gKZsMwS4pqnx',
regtest,
);
console.warn = () => {}; // Silence the Deprecation Warning
describe('bitcoinjs-lib (transactions w/ CSV)', () => { describe('bitcoinjs-lib (transactions w/ CSV)', () => {
// force update MTP // force update MTP
before(async () => { before(async () => {
await regtestUtils.mine(11) await regtestUtils.mine(11);
}) });
const hashType = bitcoin.Transaction.SIGHASH_ALL const hashType = bitcoin.Transaction.SIGHASH_ALL;
type keyPair = { publicKey: Buffer };
// IF MTP (from when confirmed) > seconds, _alice can redeem // IF MTP (from when confirmed) > seconds, _alice can redeem
function csvCheckSigOutput (_alice, _bob, sequence) { function csvCheckSigOutput(_alice: keyPair, _bob: keyPair, sequence: number) {
return bitcoin.script.fromASM(` return bitcoin.script.fromASM(
`
OP_IF OP_IF
${bitcoin.script.number.encode(sequence).toString('hex')} ${bitcoin.script.number.encode(sequence).toString('hex')}
OP_CHECKSEQUENCEVERIFY OP_CHECKSEQUENCEVERIFY
@ -32,7 +46,10 @@ describe('bitcoinjs-lib (transactions w/ CSV)', () => {
OP_ENDIF OP_ENDIF
${_alice.publicKey.toString('hex')} ${_alice.publicKey.toString('hex')}
OP_CHECKSIG OP_CHECKSIG
`.trim().replace(/\s+/g, ' ')) `
.trim()
.replace(/\s+/g, ' '),
);
} }
// 2 of 3 multisig of _bob, _charles, _dave, // 2 of 3 multisig of _bob, _charles, _dave,
@ -41,8 +58,16 @@ describe('bitcoinjs-lib (transactions w/ CSV)', () => {
// Ref: https://github.com/bitcoinbook/bitcoinbook/blob/f8b883dcd4e3d1b9adf40fed59b7e898fbd9241f/ch07.asciidoc#complex-script-example // Ref: https://github.com/bitcoinbook/bitcoinbook/blob/f8b883dcd4e3d1b9adf40fed59b7e898fbd9241f/ch07.asciidoc#complex-script-example
// Note: bitcoinjs-lib will not offer specific support for problems with // Note: bitcoinjs-lib will not offer specific support for problems with
// advanced script usages such as below. Use at your own risk. // advanced script usages such as below. Use at your own risk.
function complexCsvOutput (_alice, _bob, _charles, _dave, sequence1, sequence2) { function complexCsvOutput(
return bitcoin.script.fromASM(` _alice: keyPair,
_bob: keyPair,
_charles: keyPair,
_dave: keyPair,
sequence1: number,
sequence2: number,
) {
return bitcoin.script.fromASM(
`
OP_IF OP_IF
OP_IF OP_IF
OP_2 OP_2
@ -66,253 +91,294 @@ describe('bitcoinjs-lib (transactions w/ CSV)', () => {
${_alice.publicKey.toString('hex')} ${_alice.publicKey.toString('hex')}
OP_CHECKSIG OP_CHECKSIG
OP_ENDIF OP_ENDIF
`.trim().replace(/\s+/g, ' ')) `
.trim()
.replace(/\s+/g, ' '),
);
} }
// expiry will pass, {Alice's signature} OP_TRUE // expiry will pass, {Alice's signature} OP_TRUE
it('can create (and broadcast via 3PBP) a Transaction where Alice can redeem the output after the expiry (in the future) (simple CHECKSEQUENCEVERIFY)', async () => { it('can create (and broadcast via 3PBP) a Transaction where Alice can redeem the output after the expiry (in the future) (simple CHECKSEQUENCEVERIFY)', async () => {
// 5 blocks from now // 5 blocks from now
const sequence = bip68.encode({ blocks: 5 }) const sequence = bip68.encode({ blocks: 5 });
const p2sh = bitcoin.payments.p2sh({ const p2sh = bitcoin.payments.p2sh({
redeem: { redeem: {
output: csvCheckSigOutput(alice, bob, sequence) output: csvCheckSigOutput(alice, bob, sequence),
}, },
network: regtest network: regtest,
}) });
// fund the P2SH(CSV) address // fund the P2SH(CSV) address
const unspent = await regtestUtils.faucet(p2sh.address, 1e5) const unspent = await regtestUtils.faucet(p2sh.address!, 1e5);
const txb = new bitcoin.TransactionBuilder(regtest) const txb = new bitcoin.TransactionBuilder(regtest);
txb.addInput(unspent.txId, unspent.vout, sequence) txb.addInput(unspent.txId, unspent.vout, sequence);
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4) txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4);
// {Alice's signature} OP_TRUE // {Alice's signature} OP_TRUE
const tx = txb.buildIncomplete() const tx = txb.buildIncomplete();
const signatureHash = tx.hashForSignature(0, p2sh.redeem.output, hashType) const signatureHash = tx.hashForSignature(
0,
p2sh.redeem!.output!,
hashType,
);
const redeemScriptSig = bitcoin.payments.p2sh({ const redeemScriptSig = bitcoin.payments.p2sh({
network: regtest, network: regtest,
redeem: { redeem: {
network: regtest, network: regtest,
output: p2sh.redeem.output, output: p2sh.redeem!.output,
input: bitcoin.script.compile([ input: bitcoin.script.compile([
bitcoin.script.signature.encode(alice.sign(signatureHash), hashType), bitcoin.script.signature.encode(alice.sign(signatureHash), hashType),
bitcoin.opcodes.OP_TRUE bitcoin.opcodes.OP_TRUE,
]) ]),
} },
}).input }).input;
tx.setInputScript(0, redeemScriptSig) tx.setInputScript(0, redeemScriptSig!);
// TODO: test that it failures _prior_ to expiry, unfortunately, race conditions when run concurrently // TODO: test that it failures _prior_ to expiry, unfortunately, race conditions when run concurrently
// ... // ...
// into the future! // into the future!
await regtestUtils.mine(10) await regtestUtils.mine(10);
await regtestUtils.broadcast(tx.toHex()) await regtestUtils.broadcast(tx.toHex());
await regtestUtils.verify({ await regtestUtils.verify({
txId: tx.getId(), txId: tx.getId(),
address: regtestUtils.RANDOM_ADDRESS, address: regtestUtils.RANDOM_ADDRESS,
vout: 0, vout: 0,
value: 7e4 value: 7e4,
}) });
}) });
// expiry in the future, {Alice's signature} OP_TRUE // expiry in the future, {Alice's signature} OP_TRUE
it('can create (but fail to broadcast via 3PBP) a Transaction where Alice attempts to redeem before the expiry (simple CHECKSEQUENCEVERIFY)', async () => { it('can create (but fail to broadcast via 3PBP) a Transaction where Alice attempts to redeem before the expiry (simple CHECKSEQUENCEVERIFY)', async () => {
// two hours after confirmation // two hours after confirmation
const sequence = bip68.encode({ seconds: 7168 }) const sequence = bip68.encode({ seconds: 7168 });
const p2sh = bitcoin.payments.p2sh({ const p2sh = bitcoin.payments.p2sh({
network: regtest, network: regtest,
redeem: { redeem: {
output: csvCheckSigOutput(alice, bob, sequence) output: csvCheckSigOutput(alice, bob, sequence),
} },
}) });
// fund the P2SH(CSV) address // fund the P2SH(CSV) address
const unspent = await regtestUtils.faucet(p2sh.address, 2e4) const unspent = await regtestUtils.faucet(p2sh.address!, 2e4);
const txb = new bitcoin.TransactionBuilder(regtest) const txb = new bitcoin.TransactionBuilder(regtest);
txb.addInput(unspent.txId, unspent.vout, sequence) txb.addInput(unspent.txId, unspent.vout, sequence);
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 1e4) txb.addOutput(regtestUtils.RANDOM_ADDRESS, 1e4);
// {Alice's signature} OP_TRUE // {Alice's signature} OP_TRUE
const tx = txb.buildIncomplete() const tx = txb.buildIncomplete();
const signatureHash = tx.hashForSignature(0, p2sh.redeem.output, hashType) const signatureHash = tx.hashForSignature(
0,
p2sh.redeem!.output!,
hashType,
);
const redeemScriptSig = bitcoin.payments.p2sh({ const redeemScriptSig = bitcoin.payments.p2sh({
network: regtest, network: regtest,
redeem: { redeem: {
network: regtest, network: regtest,
output: p2sh.redeem.output, output: p2sh.redeem!.output,
input: bitcoin.script.compile([ input: bitcoin.script.compile([
bitcoin.script.signature.encode(alice.sign(signatureHash), hashType), bitcoin.script.signature.encode(alice.sign(signatureHash), hashType),
bitcoin.script.signature.encode(bob.sign(signatureHash), hashType), bitcoin.script.signature.encode(bob.sign(signatureHash), hashType),
bitcoin.opcodes.OP_TRUE bitcoin.opcodes.OP_TRUE,
]) ]),
} },
}).input }).input;
tx.setInputScript(0, redeemScriptSig) tx.setInputScript(0, redeemScriptSig!);
await regtestUtils.broadcast(tx.toHex()).catch(err => { await regtestUtils.broadcast(tx.toHex()).catch(err => {
assert.throws(() => { assert.throws(() => {
if (err) throw err if (err) throw err;
}, /Error: non-BIP68-final \(code 64\)/) }, /Error: non-BIP68-final \(code 64\)/);
}) });
}) });
// Check first combination of complex CSV, 2 of 3 // Check first combination of complex CSV, 2 of 3
it('can create (and broadcast via 3PBP) a Transaction where Bob and Charles can send (complex CHECKSEQUENCEVERIFY)', async () => { it('can create (and broadcast via 3PBP) a Transaction where Bob and Charles can send (complex CHECKSEQUENCEVERIFY)', async () => {
const height = await regtestUtils.height()
// 2 blocks from now // 2 blocks from now
const sequence1 = bip68.encode({ blocks: 2 }) const sequence1 = bip68.encode({ blocks: 2 });
// 5 blocks from now // 5 blocks from now
const sequence2 = bip68.encode({ blocks: 5 }) const sequence2 = bip68.encode({ blocks: 5 });
const p2sh = bitcoin.payments.p2sh({ const p2sh = bitcoin.payments.p2sh({
redeem: { redeem: {
output: complexCsvOutput(alice, bob, charles, dave, sequence1, sequence2) output: complexCsvOutput(
alice,
bob,
charles,
dave,
sequence1,
sequence2,
),
}, },
network: regtest network: regtest,
}) });
// fund the P2SH(CCSV) address // fund the P2SH(CCSV) address
const unspent = await regtestUtils.faucet(p2sh.address, 1e5) const unspent = await regtestUtils.faucet(p2sh.address!, 1e5);
const txb = new bitcoin.TransactionBuilder(regtest) const txb = new bitcoin.TransactionBuilder(regtest);
txb.addInput(unspent.txId, unspent.vout) txb.addInput(unspent.txId, unspent.vout);
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4) txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4);
// OP_0 {Bob sig} {Charles sig} OP_TRUE OP_TRUE // OP_0 {Bob sig} {Charles sig} OP_TRUE OP_TRUE
const tx = txb.buildIncomplete() const tx = txb.buildIncomplete();
const signatureHash = tx.hashForSignature(0, p2sh.redeem.output, hashType) const signatureHash = tx.hashForSignature(
0,
p2sh.redeem!.output!,
hashType,
);
const redeemScriptSig = bitcoin.payments.p2sh({ const redeemScriptSig = bitcoin.payments.p2sh({
network: regtest, network: regtest,
redeem: { redeem: {
network: regtest, network: regtest,
output: p2sh.redeem.output, output: p2sh.redeem!.output,
input: bitcoin.script.compile([ input: bitcoin.script.compile([
bitcoin.opcodes.OP_0, bitcoin.opcodes.OP_0,
bitcoin.script.signature.encode(bob.sign(signatureHash), hashType), bitcoin.script.signature.encode(bob.sign(signatureHash), hashType),
bitcoin.script.signature.encode(charles.sign(signatureHash), hashType), bitcoin.script.signature.encode(
charles.sign(signatureHash),
hashType,
),
bitcoin.opcodes.OP_TRUE, bitcoin.opcodes.OP_TRUE,
bitcoin.opcodes.OP_TRUE bitcoin.opcodes.OP_TRUE,
]) ]),
} },
}).input }).input;
tx.setInputScript(0, redeemScriptSig) tx.setInputScript(0, redeemScriptSig!);
await regtestUtils.broadcast(tx.toHex()) await regtestUtils.broadcast(tx.toHex());
await regtestUtils.verify({ await regtestUtils.verify({
txId: tx.getId(), txId: tx.getId(),
address: regtestUtils.RANDOM_ADDRESS, address: regtestUtils.RANDOM_ADDRESS,
vout: 0, vout: 0,
value: 7e4 value: 7e4,
}) });
}) });
// Check first combination of complex CSV, mediator + 1 of 3 after 2 blocks // Check first combination of complex CSV, mediator + 1 of 3 after 2 blocks
it('can create (and broadcast via 3PBP) a Transaction where Alice (mediator) and Bob can send after 2 blocks (complex CHECKSEQUENCEVERIFY)', async () => { it('can create (and broadcast via 3PBP) a Transaction where Alice (mediator) and Bob can send after 2 blocks (complex CHECKSEQUENCEVERIFY)', async () => {
const height = await regtestUtils.height()
// 2 blocks from now // 2 blocks from now
const sequence1 = bip68.encode({ blocks: 2 }) const sequence1 = bip68.encode({ blocks: 2 });
// 5 blocks from now // 5 blocks from now
const sequence2 = bip68.encode({ blocks: 5 }) const sequence2 = bip68.encode({ blocks: 5 });
const p2sh = bitcoin.payments.p2sh({ const p2sh = bitcoin.payments.p2sh({
redeem: { redeem: {
output: complexCsvOutput(alice, bob, charles, dave, sequence1, sequence2) output: complexCsvOutput(
alice,
bob,
charles,
dave,
sequence1,
sequence2,
),
}, },
network: regtest network: regtest,
}) });
// fund the P2SH(CCSV) address // fund the P2SH(CCSV) address
const unspent = await regtestUtils.faucet(p2sh.address, 1e5) const unspent = await regtestUtils.faucet(p2sh.address!, 1e5);
const txb = new bitcoin.TransactionBuilder(regtest) const txb = new bitcoin.TransactionBuilder(regtest);
txb.addInput(unspent.txId, unspent.vout, sequence1) // Set sequence1 for input txb.addInput(unspent.txId, unspent.vout, sequence1); // Set sequence1 for input
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4) txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4);
// OP_0 {Bob sig} {Alice mediator sig} OP_FALSE OP_TRUE // OP_0 {Bob sig} {Alice mediator sig} OP_FALSE OP_TRUE
const tx = txb.buildIncomplete() const tx = txb.buildIncomplete();
const signatureHash = tx.hashForSignature(0, p2sh.redeem.output, hashType) const signatureHash = tx.hashForSignature(
0,
p2sh.redeem!.output!,
hashType,
);
const redeemScriptSig = bitcoin.payments.p2sh({ const redeemScriptSig = bitcoin.payments.p2sh({
network: regtest, network: regtest,
redeem: { redeem: {
network: regtest, network: regtest,
output: p2sh.redeem.output, output: p2sh.redeem!.output,
input: bitcoin.script.compile([ input: bitcoin.script.compile([
bitcoin.opcodes.OP_0, bitcoin.opcodes.OP_0,
bitcoin.script.signature.encode(bob.sign(signatureHash), hashType), bitcoin.script.signature.encode(bob.sign(signatureHash), hashType),
bitcoin.script.signature.encode(alice.sign(signatureHash), hashType), bitcoin.script.signature.encode(alice.sign(signatureHash), hashType),
bitcoin.opcodes.OP_0, bitcoin.opcodes.OP_0,
bitcoin.opcodes.OP_TRUE bitcoin.opcodes.OP_TRUE,
]) ]),
} },
}).input }).input;
tx.setInputScript(0, redeemScriptSig) tx.setInputScript(0, redeemScriptSig!);
// Wait 2 blocks // Wait 2 blocks
await regtestUtils.mine(2) await regtestUtils.mine(2);
await regtestUtils.broadcast(tx.toHex()) await regtestUtils.broadcast(tx.toHex());
await regtestUtils.verify({ await regtestUtils.verify({
txId: tx.getId(), txId: tx.getId(),
address: regtestUtils.RANDOM_ADDRESS, address: regtestUtils.RANDOM_ADDRESS,
vout: 0, vout: 0,
value: 7e4 value: 7e4,
}) });
}) });
// Check first combination of complex CSV, mediator after 5 blocks // Check first combination of complex CSV, mediator after 5 blocks
it('can create (and broadcast via 3PBP) a Transaction where Alice (mediator) can send after 5 blocks (complex CHECKSEQUENCEVERIFY)', async () => { it('can create (and broadcast via 3PBP) a Transaction where Alice (mediator) can send after 5 blocks (complex CHECKSEQUENCEVERIFY)', async () => {
const height = await regtestUtils.height()
// 2 blocks from now // 2 blocks from now
const sequence1 = bip68.encode({ blocks: 2 }) const sequence1 = bip68.encode({ blocks: 2 });
// 5 blocks from now // 5 blocks from now
const sequence2 = bip68.encode({ blocks: 5 }) const sequence2 = bip68.encode({ blocks: 5 });
const p2sh = bitcoin.payments.p2sh({ const p2sh = bitcoin.payments.p2sh({
redeem: { redeem: {
output: complexCsvOutput(alice, bob, charles, dave, sequence1, sequence2) output: complexCsvOutput(
alice,
bob,
charles,
dave,
sequence1,
sequence2,
),
}, },
network: regtest network: regtest,
}) });
// fund the P2SH(CCSV) address // fund the P2SH(CCSV) address
const unspent = await regtestUtils.faucet(p2sh.address, 1e5) const unspent = await regtestUtils.faucet(p2sh.address!, 1e5);
const txb = new bitcoin.TransactionBuilder(regtest) const txb = new bitcoin.TransactionBuilder(regtest);
txb.addInput(unspent.txId, unspent.vout, sequence2) // Set sequence2 for input txb.addInput(unspent.txId, unspent.vout, sequence2); // Set sequence2 for input
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4) txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4);
// {Alice mediator sig} OP_FALSE // {Alice mediator sig} OP_FALSE
const tx = txb.buildIncomplete() const tx = txb.buildIncomplete();
const signatureHash = tx.hashForSignature(0, p2sh.redeem.output, hashType) const signatureHash = tx.hashForSignature(
0,
p2sh.redeem!.output!,
hashType,
);
const redeemScriptSig = bitcoin.payments.p2sh({ const redeemScriptSig = bitcoin.payments.p2sh({
network: regtest, network: regtest,
redeem: { redeem: {
network: regtest, network: regtest,
output: p2sh.redeem.output, output: p2sh.redeem!.output,
input: bitcoin.script.compile([ input: bitcoin.script.compile([
bitcoin.script.signature.encode(alice.sign(signatureHash), hashType), bitcoin.script.signature.encode(alice.sign(signatureHash), hashType),
bitcoin.opcodes.OP_0 bitcoin.opcodes.OP_0,
]) ]),
} },
}).input }).input;
tx.setInputScript(0, redeemScriptSig) tx.setInputScript(0, redeemScriptSig!);
// Wait 5 blocks // Wait 5 blocks
await regtestUtils.mine(5) await regtestUtils.mine(5);
await regtestUtils.broadcast(tx.toHex()) await regtestUtils.broadcast(tx.toHex());
await regtestUtils.verify({ await regtestUtils.verify({
txId: tx.getId(), txId: tx.getId(),
address: regtestUtils.RANDOM_ADDRESS, address: regtestUtils.RANDOM_ADDRESS,
vout: 0, vout: 0,
value: 7e4 value: 7e4,
}) });
}) });
}) });

View file

@ -1,23 +1,27 @@
const bitcoin = require('../../') import { describe, it } from 'mocha';
import * as bitcoin from '../..';
const { describe, it } = require('mocha') import { regtestUtils } from './_regtest';
const regtestUtils = require('./_regtest') const NETWORK = regtestUtils.network;
const NETWORK = regtestUtils.network
const keyPairs = [ const keyPairs = [
bitcoin.ECPair.makeRandom({ network: NETWORK }), bitcoin.ECPair.makeRandom({ network: NETWORK }),
bitcoin.ECPair.makeRandom({ network: NETWORK }) bitcoin.ECPair.makeRandom({ network: NETWORK }),
] ];
console.warn = () => {} // Silence the Deprecation Warning console.warn = () => {}; // Silence the Deprecation Warning
async function buildAndSign (depends, prevOutput, redeemScript, witnessScript) { async function buildAndSign(
const unspent = await regtestUtils.faucetComplex(prevOutput, 5e4) depends: any,
prevOutput: any,
redeemScript: any,
witnessScript: any,
) {
const unspent = await regtestUtils.faucetComplex(prevOutput, 5e4);
const txb = new bitcoin.TransactionBuilder(NETWORK) const txb = new bitcoin.TransactionBuilder(NETWORK);
txb.addInput(unspent.txId, unspent.vout, null, prevOutput) txb.addInput(unspent.txId, unspent.vout, undefined, prevOutput);
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 2e4) txb.addOutput(regtestUtils.RANDOM_ADDRESS, 2e4);
const posType = depends.prevOutScriptType const posType = depends.prevOutScriptType;
const needsValue = !!witnessScript || posType.slice(-6) === 'p2wpkh' const needsValue = !!witnessScript || posType.slice(-6) === 'p2wpkh';
if (depends.signatures) { if (depends.signatures) {
keyPairs.forEach(keyPair => { keyPairs.forEach(keyPair => {
@ -28,8 +32,8 @@ async function buildAndSign (depends, prevOutput, redeemScript, witnessScript) {
redeemScript, redeemScript,
witnessValue: needsValue ? unspent.value : undefined, witnessValue: needsValue ? unspent.value : undefined,
witnessScript, witnessScript,
}) });
}) });
} else if (depends.signature) { } else if (depends.signature) {
txb.sign({ txb.sign({
prevOutScriptType: posType, prevOutScriptType: posType,
@ -38,52 +42,94 @@ async function buildAndSign (depends, prevOutput, redeemScript, witnessScript) {
redeemScript, redeemScript,
witnessValue: needsValue ? unspent.value : undefined, witnessValue: needsValue ? unspent.value : undefined,
witnessScript, witnessScript,
}) });
} }
return regtestUtils.broadcast(txb.build().toHex()) return regtestUtils.broadcast(txb.build().toHex());
} }
;['p2ms', 'p2pk', 'p2pkh', 'p2wpkh'].forEach(k => { ['p2ms', 'p2pk', 'p2pkh', 'p2wpkh'].forEach(k => {
const fixtures = require('../fixtures/' + k) const fixtures = require('../fixtures/' + k);
const { depends } = fixtures.dynamic const { depends } = fixtures.dynamic;
const fn = bitcoin.payments[k] const fn: any = (bitcoin.payments as any)[k];
const base = {} const base: any = {};
if (depends.pubkey) base.pubkey = keyPairs[0].publicKey if (depends.pubkey) base.pubkey = keyPairs[0].publicKey;
if (depends.pubkeys) base.pubkeys = keyPairs.map(x => x.publicKey) if (depends.pubkeys) base.pubkeys = keyPairs.map(x => x.publicKey);
if (depends.m) base.m = base.pubkeys.length if (depends.m) base.m = base.pubkeys.length;
const { output } = fn(base) const { output } = fn(base);
if (!output) throw new TypeError('Missing output') if (!output) throw new TypeError('Missing output');
describe('bitcoinjs-lib (payments - ' + k + ')', () => { describe('bitcoinjs-lib (payments - ' + k + ')', () => {
it('can broadcast as an output, and be spent as an input', async () => { it('can broadcast as an output, and be spent as an input', async () => {
Object.assign(depends, { prevOutScriptType: k }) Object.assign(depends, { prevOutScriptType: k });
await buildAndSign(depends, output, undefined, undefined) await buildAndSign(depends, output, undefined, undefined);
}) });
it('can (as P2SH(' + k + ')) broadcast as an output, and be spent as an input', async () => { it(
const p2sh = bitcoin.payments.p2sh({ redeem: { output }, network: NETWORK }) 'can (as P2SH(' +
Object.assign(depends, { prevOutScriptType: 'p2sh-' + k }) k +
await buildAndSign(depends, p2sh.output, p2sh.redeem.output, undefined) ')) broadcast as an output, and be spent as an input',
}) async () => {
const p2sh = bitcoin.payments.p2sh({
redeem: { output },
network: NETWORK,
});
Object.assign(depends, { prevOutScriptType: 'p2sh-' + k });
await buildAndSign(
depends,
p2sh.output,
p2sh.redeem!.output,
undefined,
);
},
);
// NOTE: P2WPKH cannot be wrapped in P2WSH, consensus fail // NOTE: P2WPKH cannot be wrapped in P2WSH, consensus fail
if (k === 'p2wpkh') return if (k === 'p2wpkh') return;
it('can (as P2WSH(' + k + ')) broadcast as an output, and be spent as an input', async () => { it(
const p2wsh = bitcoin.payments.p2wsh({ redeem: { output }, network: NETWORK }) 'can (as P2WSH(' +
Object.assign(depends, { prevOutScriptType: 'p2wsh-' + k }) k +
await buildAndSign(depends, p2wsh.output, undefined, p2wsh.redeem.output) ')) broadcast as an output, and be spent as an input',
}) async () => {
const p2wsh = bitcoin.payments.p2wsh({
redeem: { output },
network: NETWORK,
});
Object.assign(depends, { prevOutScriptType: 'p2wsh-' + k });
await buildAndSign(
depends,
p2wsh.output,
undefined,
p2wsh.redeem!.output,
);
},
);
it('can (as P2SH(P2WSH(' + k + '))) broadcast as an output, and be spent as an input', async () => { it(
const p2wsh = bitcoin.payments.p2wsh({ redeem: { output }, network: NETWORK }) 'can (as P2SH(P2WSH(' +
const p2sh = bitcoin.payments.p2sh({ redeem: { output: p2wsh.output }, network: NETWORK }) k +
'))) broadcast as an output, and be spent as an input',
async () => {
const p2wsh = bitcoin.payments.p2wsh({
redeem: { output },
network: NETWORK,
});
const p2sh = bitcoin.payments.p2sh({
redeem: { output: p2wsh.output },
network: NETWORK,
});
Object.assign(depends, { prevOutScriptType: 'p2sh-p2wsh-' + k }) Object.assign(depends, { prevOutScriptType: 'p2sh-p2wsh-' + k });
await buildAndSign(depends, p2sh.output, p2sh.redeem.output, p2wsh.redeem.output) await buildAndSign(
}) depends,
}) p2sh.output,
}) p2sh.redeem!.output,
p2wsh.redeem!.output,
);
},
);
});
});

View file

@ -1,9 +1,9 @@
const { describe, it } = require('mocha'); import * as assert from 'assert';
const assert = require('assert'); import { describe, it } from 'mocha';
const bitcoin = require('../../'); import * as bitcoin from '../..';
const bip32 = require('bip32'); import { regtestUtils } from './_regtest';
import * as bip32 from 'bip32';
const rng = require('randombytes'); const rng = require('randombytes');
const regtestUtils = require('./_regtest');
const regtest = regtestUtils.network; const regtest = regtestUtils.network;
// See bottom of file for some helper functions used to make the payment objects needed. // See bottom of file for some helper functions used to make the payment objects needed.
@ -174,7 +174,7 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
const psbt = new bitcoin.Psbt({ network: regtest }) const psbt = new bitcoin.Psbt({ network: regtest })
.addInput(inputData1) .addInput(inputData1)
.addOutput({ .addOutput({
script: embed.output, script: embed.output!,
value: 1000, value: 1000,
}) })
.addOutput({ .addOutput({
@ -350,7 +350,12 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
// For learning purposes, ignore this test. // For learning purposes, ignore this test.
// REPEATING ABOVE BUT WITH nonWitnessUtxo by passing false to getInputData // REPEATING ABOVE BUT WITH nonWitnessUtxo by passing false to getInputData
const p2wpkh = createPayment('p2wpkh'); const p2wpkh = createPayment('p2wpkh');
const inputData = await getInputData(5e4, p2wpkh.payment, false, 'noredeem'); const inputData = await getInputData(
5e4,
p2wpkh.payment,
false,
'noredeem',
);
const psbt = new bitcoin.Psbt({ network: regtest }) const psbt = new bitcoin.Psbt({ network: regtest })
.addInput(inputData) .addInput(inputData)
.addOutput({ .addOutput({
@ -486,7 +491,12 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
// For learning purposes, ignore this test. // For learning purposes, ignore this test.
// REPEATING ABOVE BUT WITH nonWitnessUtxo by passing false to getInputData // REPEATING ABOVE BUT WITH nonWitnessUtxo by passing false to getInputData
const p2sh = createPayment('p2sh-p2wsh-p2ms(3 of 4)'); const p2sh = createPayment('p2sh-p2wsh-p2ms(3 of 4)');
const inputData = await getInputData(5e4, p2sh.payment, false, 'p2sh-p2wsh'); const inputData = await getInputData(
5e4,
p2sh.payment,
false,
'p2sh-p2wsh',
);
const psbt = new bitcoin.Psbt({ network: regtest }) const psbt = new bitcoin.Psbt({ network: regtest })
.addInput(inputData) .addInput(inputData)
.addOutput({ .addOutput({
@ -528,9 +538,9 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
masterFingerprint, masterFingerprint,
path, path,
pubkey, pubkey,
} },
] ],
} };
const p2wpkh = createPayment('p2wpkh', [childNode]); const p2wpkh = createPayment('p2wpkh', [childNode]);
const inputData = await getInputData(5e4, p2wpkh.payment, true, 'noredeem'); const inputData = await getInputData(5e4, p2wpkh.payment, true, 'noredeem');
{ {
@ -539,7 +549,7 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
} }
// You can add extra attributes for updateData into the addInput(s) object(s) // You can add extra attributes for updateData into the addInput(s) object(s)
Object.assign(inputData, updateData) Object.assign(inputData, updateData);
const psbt = new bitcoin.Psbt({ network: regtest }) const psbt = new bitcoin.Psbt({ network: regtest })
.addInput(inputData) .addInput(inputData)
@ -551,7 +561,10 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
.signInputHD(0, hdRoot); // must sign with root!!! .signInputHD(0, hdRoot); // must sign with root!!!
assert.strictEqual(psbt.validateSignaturesOfInput(0), true); assert.strictEqual(psbt.validateSignaturesOfInput(0), true);
assert.strictEqual(psbt.validateSignaturesOfInput(0, childNode.publicKey), true); assert.strictEqual(
psbt.validateSignaturesOfInput(0, childNode.publicKey),
true,
);
psbt.finalizeAllInputs(); psbt.finalizeAllInputs();
const tx = psbt.extractTransaction(); const tx = psbt.extractTransaction();
@ -568,18 +581,18 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
}); });
}); });
function createPayment(_type, myKeys, network) { function createPayment(_type: string, myKeys?: any[], network?: any) {
network = network || regtest; network = network || regtest;
const splitType = _type.split('-').reverse(); const splitType = _type.split('-').reverse();
const isMultisig = splitType[0].slice(0, 4) === 'p2ms'; const isMultisig = splitType[0].slice(0, 4) === 'p2ms';
const keys = myKeys || []; const keys = myKeys || [];
let m; let m: number | undefined;
if (isMultisig) { if (isMultisig) {
const match = splitType[0].match(/^p2ms\((\d+) of (\d+)\)$/); const match = splitType[0].match(/^p2ms\((\d+) of (\d+)\)$/);
m = parseInt(match[1]); m = parseInt(match![1]);
let n = parseInt(match[2]); let n = parseInt(match![2]);
if (keys.length > 0 && keys.length !== n) { if (keys.length > 0 && keys.length !== n) {
throw new Error('Need n keys for multisig') throw new Error('Need n keys for multisig');
} }
while (!myKeys && n > 1) { while (!myKeys && n > 1) {
keys.push(bitcoin.ECPair.makeRandom({ network })); keys.push(bitcoin.ECPair.makeRandom({ network }));
@ -588,7 +601,7 @@ function createPayment(_type, myKeys, network) {
} }
if (!myKeys) keys.push(bitcoin.ECPair.makeRandom({ network })); if (!myKeys) keys.push(bitcoin.ECPair.makeRandom({ network }));
let payment; let payment: any;
splitType.forEach(type => { splitType.forEach(type => {
if (type.slice(0, 4) === 'p2ms') { if (type.slice(0, 4) === 'p2ms') {
payment = bitcoin.payments.p2ms({ payment = bitcoin.payments.p2ms({
@ -597,12 +610,12 @@ function createPayment(_type, myKeys, network) {
network, network,
}); });
} else if (['p2sh', 'p2wsh'].indexOf(type) > -1) { } else if (['p2sh', 'p2wsh'].indexOf(type) > -1) {
payment = bitcoin.payments[type]({ payment = (bitcoin.payments as any)[type]({
redeem: payment, redeem: payment,
network, network,
}); });
} else { } else {
payment = bitcoin.payments[type]({ payment = (bitcoin.payments as any)[type]({
pubkey: keys[0].publicKey, pubkey: keys[0].publicKey,
network, network,
}); });
@ -615,13 +628,18 @@ function createPayment(_type, myKeys, network) {
}; };
} }
function getWitnessUtxo(out) { function getWitnessUtxo(out: any) {
delete out.address; delete out.address;
out.script = Buffer.from(out.script, 'hex'); out.script = Buffer.from(out.script, 'hex');
return out; return out;
} }
async function getInputData(amount, payment, isSegwit, redeemType) { async function getInputData(
amount: number,
payment: any,
isSegwit: boolean,
redeemType: string,
) {
const unspent = await regtestUtils.faucetComplex(payment.output, amount); const unspent = await regtestUtils.faucetComplex(payment.output, amount);
const utx = await regtestUtils.fetch(unspent.txId); const utx = await regtestUtils.fetch(unspent.txId);
// for non segwit inputs, you must pass the full transaction buffer // for non segwit inputs, you must pass the full transaction buffer
@ -629,7 +647,7 @@ async function getInputData(amount, payment, isSegwit, redeemType) {
// for segwit inputs, you only need the output script and value as an object. // for segwit inputs, you only need the output script and value as an object.
const witnessUtxo = getWitnessUtxo(utx.outs[unspent.vout]); const witnessUtxo = getWitnessUtxo(utx.outs[unspent.vout]);
const mixin = isSegwit ? { witnessUtxo } : { nonWitnessUtxo }; const mixin = isSegwit ? { witnessUtxo } : { nonWitnessUtxo };
const mixin2 = {}; const mixin2: any = {};
switch (redeemType) { switch (redeemType) {
case 'p2sh': case 'p2sh':
mixin2.redeemScript = payment.redeem.output; mixin2.redeemScript = payment.redeem.output;

View file

@ -1,361 +1,421 @@
const { describe, it } = require('mocha') import * as assert from 'assert';
const assert = require('assert') import { describe, it } from 'mocha';
const bitcoin = require('../../') import * as bitcoin from '../..';
const regtestUtils = require('./_regtest') import { regtestUtils } from './_regtest';
const regtest = regtestUtils.network const regtest = regtestUtils.network;
console.warn = () => {} // Silence the Deprecation Warning console.warn = () => {}; // Silence the Deprecation Warning
function rng () { function rng() {
return Buffer.from('YT8dAtK4d16A3P1z+TpwB2jJ4aFH3g9M1EioIBkLEV4=', 'base64') return Buffer.from('YT8dAtK4d16A3P1z+TpwB2jJ4aFH3g9M1EioIBkLEV4=', 'base64');
} }
describe('bitcoinjs-lib (transactions)', () => { describe('bitcoinjs-lib (transactions)', () => {
it('can create a 1-to-1 Transaction', () => { it('can create a 1-to-1 Transaction', () => {
const alice = bitcoin.ECPair.fromWIF('L1uyy5qTuGrVXrmrsvHWHgVzW9kKdrp27wBC7Vs6nZDTF2BRUVwy') const alice = bitcoin.ECPair.fromWIF(
const txb = new bitcoin.TransactionBuilder() 'L1uyy5qTuGrVXrmrsvHWHgVzW9kKdrp27wBC7Vs6nZDTF2BRUVwy',
);
const txb = new bitcoin.TransactionBuilder();
txb.setVersion(1) txb.setVersion(1);
txb.addInput('61d520ccb74288c96bc1a2b20ea1c0d5a704776dd0164a396efec3ea7040349d', 0) // Alice's previous transaction output, has 15000 satoshis txb.addInput(
txb.addOutput('1cMh228HTCiwS8ZsaakH8A8wze1JR5ZsP', 12000) '61d520ccb74288c96bc1a2b20ea1c0d5a704776dd0164a396efec3ea7040349d',
0,
); // Alice's previous transaction output, has 15000 satoshis
txb.addOutput('1cMh228HTCiwS8ZsaakH8A8wze1JR5ZsP', 12000);
// (in)15000 - (out)12000 = (fee)3000, this is the miner fee // (in)15000 - (out)12000 = (fee)3000, this is the miner fee
txb.sign({ txb.sign({
prevOutScriptType: 'p2pkh', prevOutScriptType: 'p2pkh',
vin: 0, vin: 0,
keyPair: alice keyPair: alice,
}) });
// prepare for broadcast to the Bitcoin network, see "can broadcast a Transaction" below // prepare for broadcast to the Bitcoin network, see "can broadcast a Transaction" below
assert.strictEqual(txb.build().toHex(), '01000000019d344070eac3fe6e394a16d06d7704a7d5c0a10eb2a2c16bc98842b7cc20d561000000006b48304502210088828c0bdfcdca68d8ae0caeb6ec62cd3fd5f9b2191848edae33feb533df35d302202e0beadd35e17e7f83a733f5277028a9b453d525553e3f5d2d7a7aa8010a81d60121029f50f51d63b345039a290c94bffd3180c99ed659ff6ea6b1242bca47eb93b59fffffffff01e02e0000000000001976a91406afd46bcdfd22ef94ac122aa11f241244a37ecc88ac00000000') assert.strictEqual(
}) txb.build().toHex(),
'01000000019d344070eac3fe6e394a16d06d7704a7d5c0a10eb2a2c16bc98842b7cc20d561000000006b48304502210088828c0bdfcdca68d8ae0caeb6ec62cd3fd5f9b2191848edae33feb533df35d302202e0beadd35e17e7f83a733f5277028a9b453d525553e3f5d2d7a7aa8010a81d60121029f50f51d63b345039a290c94bffd3180c99ed659ff6ea6b1242bca47eb93b59fffffffff01e02e0000000000001976a91406afd46bcdfd22ef94ac122aa11f241244a37ecc88ac00000000',
);
});
it('can create a 2-to-2 Transaction', () => { it('can create a 2-to-2 Transaction', () => {
const alice = bitcoin.ECPair.fromWIF('L1Knwj9W3qK3qMKdTvmg3VfzUs3ij2LETTFhxza9LfD5dngnoLG1') const alice = bitcoin.ECPair.fromWIF(
const bob = bitcoin.ECPair.fromWIF('KwcN2pT3wnRAurhy7qMczzbkpY5nXMW2ubh696UBc1bcwctTx26z') 'L1Knwj9W3qK3qMKdTvmg3VfzUs3ij2LETTFhxza9LfD5dngnoLG1',
);
const bob = bitcoin.ECPair.fromWIF(
'KwcN2pT3wnRAurhy7qMczzbkpY5nXMW2ubh696UBc1bcwctTx26z',
);
const txb = new bitcoin.TransactionBuilder() const txb = new bitcoin.TransactionBuilder();
txb.setVersion(1) txb.setVersion(1);
txb.addInput('b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c', 6) // Alice's previous transaction output, has 200000 satoshis txb.addInput(
txb.addInput('7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730', 0) // Bob's previous transaction output, has 300000 satoshis 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c',
txb.addOutput('1CUNEBjYrCn2y1SdiUMohaKUi4wpP326Lb', 180000) 6,
txb.addOutput('1JtK9CQw1syfWj1WtFMWomrYdV3W2tWBF9', 170000) ); // Alice's previous transaction output, has 200000 satoshis
txb.addInput(
'7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730',
0,
); // Bob's previous transaction output, has 300000 satoshis
txb.addOutput('1CUNEBjYrCn2y1SdiUMohaKUi4wpP326Lb', 180000);
txb.addOutput('1JtK9CQw1syfWj1WtFMWomrYdV3W2tWBF9', 170000);
// (in)(200000 + 300000) - (out)(180000 + 170000) = (fee)150000, this is the miner fee // (in)(200000 + 300000) - (out)(180000 + 170000) = (fee)150000, this is the miner fee
txb.sign({ txb.sign({
prevOutScriptType: 'p2pkh', prevOutScriptType: 'p2pkh',
vin: 1, vin: 1,
keyPair: bob keyPair: bob,
}) // Bob signs his input, which was the second input (1th) }); // Bob signs his input, which was the second input (1th)
txb.sign({ txb.sign({
prevOutScriptType: 'p2pkh', prevOutScriptType: 'p2pkh',
vin: 0, vin: 0,
keyPair: alice keyPair: alice,
}) // Alice signs her input, which was the first input (0th) }); // Alice signs her input, which was the first input (0th)
// prepare for broadcast to the Bitcoin network, see "can broadcast a Transaction" below // prepare for broadcast to the Bitcoin network, see "can broadcast a Transaction" below
assert.strictEqual(txb.build().toHex(), '01000000024c94e48a870b85f41228d33cf25213dfcc8dd796e7211ed6b1f9a014809dbbb5060000006a473044022041450c258ce7cac7da97316bf2ea1ce66d88967c4df94f3e91f4c2a30f5d08cb02203674d516e6bb2b0afd084c3551614bd9cec3c2945231245e891b145f2d6951f0012103e05ce435e462ec503143305feb6c00e06a3ad52fbf939e85c65f3a765bb7baacffffffff3077d9de049574c3af9bc9c09a7c9db80f2d94caaf63988c9166249b955e867d000000006b483045022100aeb5f1332c79c446d3f906e4499b2e678500580a3f90329edf1ba502eec9402e022072c8b863f8c8d6c26f4c691ac9a6610aa4200edc697306648ee844cfbc089d7a012103df7940ee7cddd2f97763f67e1fb13488da3fbdd7f9c68ec5ef0864074745a289ffffffff0220bf0200000000001976a9147dd65592d0ab2fe0d0257d571abf032cd9db93dc88ac10980200000000001976a914c42e7ef92fdb603af844d064faad95db9bcdfd3d88ac00000000') assert.strictEqual(
}) txb.build().toHex(),
'01000000024c94e48a870b85f41228d33cf25213dfcc8dd796e7211ed6b1f9a014809dbbb5060000006a473044022041450c258ce7cac7da97316bf2ea1ce66d88967c4df94f3e91f4c2a30f5d08cb02203674d516e6bb2b0afd084c3551614bd9cec3c2945231245e891b145f2d6951f0012103e05ce435e462ec503143305feb6c00e06a3ad52fbf939e85c65f3a765bb7baacffffffff3077d9de049574c3af9bc9c09a7c9db80f2d94caaf63988c9166249b955e867d000000006b483045022100aeb5f1332c79c446d3f906e4499b2e678500580a3f90329edf1ba502eec9402e022072c8b863f8c8d6c26f4c691ac9a6610aa4200edc697306648ee844cfbc089d7a012103df7940ee7cddd2f97763f67e1fb13488da3fbdd7f9c68ec5ef0864074745a289ffffffff0220bf0200000000001976a9147dd65592d0ab2fe0d0257d571abf032cd9db93dc88ac10980200000000001976a914c42e7ef92fdb603af844d064faad95db9bcdfd3d88ac00000000',
);
});
it('can create (and broadcast via 3PBP) a typical Transaction', async () => { it('can create (and broadcast via 3PBP) a typical Transaction', async () => {
const alice1 = bitcoin.ECPair.makeRandom({ network: regtest }) const alice1 = bitcoin.ECPair.makeRandom({ network: regtest });
const alice2 = bitcoin.ECPair.makeRandom({ network: regtest }) const alice2 = bitcoin.ECPair.makeRandom({ network: regtest });
const aliceChange = bitcoin.ECPair.makeRandom({ network: regtest, rng: rng }) const aliceChange = bitcoin.ECPair.makeRandom({
network: regtest,
rng: rng,
});
const alice1pkh = bitcoin.payments.p2pkh({ pubkey: alice1.publicKey, network: regtest }) const alice1pkh = bitcoin.payments.p2pkh({
const alice2pkh = bitcoin.payments.p2pkh({ pubkey: alice2.publicKey, network: regtest }) pubkey: alice1.publicKey,
const aliceCpkh = bitcoin.payments.p2pkh({ pubkey: aliceChange.publicKey, network: regtest }) network: regtest,
});
const alice2pkh = bitcoin.payments.p2pkh({
pubkey: alice2.publicKey,
network: regtest,
});
const aliceCpkh = bitcoin.payments.p2pkh({
pubkey: aliceChange.publicKey,
network: regtest,
});
// give Alice 2 unspent outputs // give Alice 2 unspent outputs
const unspent0 = await regtestUtils.faucet(alice1pkh.address, 5e4) const unspent0 = await regtestUtils.faucet(alice1pkh.address!, 5e4);
const unspent1 = await regtestUtils.faucet(alice2pkh.address, 7e4) const unspent1 = await regtestUtils.faucet(alice2pkh.address!, 7e4);
const txb = new bitcoin.TransactionBuilder(regtest) const txb = new bitcoin.TransactionBuilder(regtest);
txb.addInput(unspent0.txId, unspent0.vout) // alice1 unspent txb.addInput(unspent0.txId, unspent0.vout); // alice1 unspent
txb.addInput(unspent1.txId, unspent1.vout) // alice2 unspent txb.addInput(unspent1.txId, unspent1.vout); // alice2 unspent
txb.addOutput('mwCwTceJvYV27KXBc3NJZys6CjsgsoeHmf', 8e4) // the actual "spend" txb.addOutput('mwCwTceJvYV27KXBc3NJZys6CjsgsoeHmf', 8e4); // the actual "spend"
txb.addOutput(aliceCpkh.address, 1e4) // Alice's change txb.addOutput(aliceCpkh.address!, 1e4); // Alice's change
// (in)(5e4 + 7e4) - (out)(8e4 + 1e4) = (fee)3e4 = 30000, this is the miner fee // (in)(5e4 + 7e4) - (out)(8e4 + 1e4) = (fee)3e4 = 30000, this is the miner fee
// Alice signs each input with the respective private keys // Alice signs each input with the respective private keys
txb.sign({ txb.sign({
prevOutScriptType: 'p2pkh', prevOutScriptType: 'p2pkh',
vin: 0, vin: 0,
keyPair: alice1 keyPair: alice1,
}) });
txb.sign({ txb.sign({
prevOutScriptType: 'p2pkh', prevOutScriptType: 'p2pkh',
vin: 1, vin: 1,
keyPair: alice2 keyPair: alice2,
}) });
// build and broadcast our RegTest network // build and broadcast our RegTest network
await regtestUtils.broadcast(txb.build().toHex()) await regtestUtils.broadcast(txb.build().toHex());
// to build and broadcast to the actual Bitcoin network, see https://github.com/bitcoinjs/bitcoinjs-lib/issues/839 // to build and broadcast to the actual Bitcoin network, see https://github.com/bitcoinjs/bitcoinjs-lib/issues/839
}) });
it('can create (and broadcast via 3PBP) a Transaction with an OP_RETURN output', async () => { it('can create (and broadcast via 3PBP) a Transaction with an OP_RETURN output', async () => {
const keyPair = bitcoin.ECPair.makeRandom({ network: regtest }) const keyPair = bitcoin.ECPair.makeRandom({ network: regtest });
const p2pkh = bitcoin.payments.p2pkh({ pubkey: keyPair.publicKey, network: regtest }) const p2pkh = bitcoin.payments.p2pkh({
pubkey: keyPair.publicKey,
network: regtest,
});
const unspent = await regtestUtils.faucet(p2pkh.address, 2e5) const unspent = await regtestUtils.faucet(p2pkh.address!, 2e5);
const txb = new bitcoin.TransactionBuilder(regtest) const txb = new bitcoin.TransactionBuilder(regtest);
const data = Buffer.from('bitcoinjs-lib', 'utf8') const data = Buffer.from('bitcoinjs-lib', 'utf8');
const embed = bitcoin.payments.embed({ data: [data] }) const embed = bitcoin.payments.embed({ data: [data] });
txb.addInput(unspent.txId, unspent.vout) txb.addInput(unspent.txId, unspent.vout);
txb.addOutput(embed.output, 1000) txb.addOutput(embed.output!, 1000);
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 1e5) txb.addOutput(regtestUtils.RANDOM_ADDRESS, 1e5);
txb.sign({ txb.sign({
prevOutScriptType: 'p2pkh', prevOutScriptType: 'p2pkh',
vin: 0, vin: 0,
keyPair, keyPair,
}) });
// build and broadcast to the RegTest network // build and broadcast to the RegTest network
await regtestUtils.broadcast(txb.build().toHex()) await regtestUtils.broadcast(txb.build().toHex());
}) });
it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2MS(2 of 4)) (multisig) input', async () => { it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2MS(2 of 4)) (multisig) input', async () => {
const keyPairs = [ const keyPairs = [
bitcoin.ECPair.makeRandom({ network: regtest }), bitcoin.ECPair.makeRandom({ network: regtest }),
bitcoin.ECPair.makeRandom({ network: regtest }), bitcoin.ECPair.makeRandom({ network: regtest }),
bitcoin.ECPair.makeRandom({ network: regtest }), bitcoin.ECPair.makeRandom({ network: regtest }),
bitcoin.ECPair.makeRandom({ network: regtest }) bitcoin.ECPair.makeRandom({ network: regtest }),
] ];
const pubkeys = keyPairs.map(x => x.publicKey) const pubkeys = keyPairs.map(x => x.publicKey);
const p2ms = bitcoin.payments.p2ms({ m: 2, pubkeys: pubkeys, network: regtest }) const p2ms = bitcoin.payments.p2ms({
const p2sh = bitcoin.payments.p2sh({ redeem: p2ms, network: regtest }) m: 2,
pubkeys: pubkeys,
network: regtest,
});
const p2sh = bitcoin.payments.p2sh({ redeem: p2ms, network: regtest });
const unspent = await regtestUtils.faucet(p2sh.address, 2e4) const unspent = await regtestUtils.faucet(p2sh.address!, 2e4);
const txb = new bitcoin.TransactionBuilder(regtest) const txb = new bitcoin.TransactionBuilder(regtest);
txb.addInput(unspent.txId, unspent.vout) txb.addInput(unspent.txId, unspent.vout);
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 1e4) txb.addOutput(regtestUtils.RANDOM_ADDRESS, 1e4);
txb.sign({ txb.sign({
prevOutScriptType: 'p2sh-p2ms', prevOutScriptType: 'p2sh-p2ms',
vin: 0, vin: 0,
keyPair: keyPairs[0], keyPair: keyPairs[0],
redeemScript: p2sh.redeem.output, redeemScript: p2sh.redeem!.output,
}) });
txb.sign({ txb.sign({
prevOutScriptType: 'p2sh-p2ms', prevOutScriptType: 'p2sh-p2ms',
vin: 0, vin: 0,
keyPair: keyPairs[2], keyPair: keyPairs[2],
redeemScript: p2sh.redeem.output, redeemScript: p2sh.redeem!.output,
}) });
const tx = txb.build() const tx = txb.build();
// build and broadcast to the Bitcoin RegTest network // build and broadcast to the Bitcoin RegTest network
await regtestUtils.broadcast(tx.toHex()) await regtestUtils.broadcast(tx.toHex());
await regtestUtils.verify({ await regtestUtils.verify({
txId: tx.getId(), txId: tx.getId(),
address: regtestUtils.RANDOM_ADDRESS, address: regtestUtils.RANDOM_ADDRESS,
vout: 0, vout: 0,
value: 1e4 value: 1e4,
}) });
}) });
it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WPKH) input', async () => { it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WPKH) input', async () => {
const keyPair = bitcoin.ECPair.makeRandom({ network: regtest }) const keyPair = bitcoin.ECPair.makeRandom({ network: regtest });
const p2wpkh = bitcoin.payments.p2wpkh({ pubkey: keyPair.publicKey, network: regtest }) const p2wpkh = bitcoin.payments.p2wpkh({
const p2sh = bitcoin.payments.p2sh({ redeem: p2wpkh, network: regtest }) pubkey: keyPair.publicKey,
network: regtest,
});
const p2sh = bitcoin.payments.p2sh({ redeem: p2wpkh, network: regtest });
const unspent = await regtestUtils.faucet(p2sh.address, 5e4) const unspent = await regtestUtils.faucet(p2sh.address!, 5e4);
const txb = new bitcoin.TransactionBuilder(regtest) const txb = new bitcoin.TransactionBuilder(regtest);
txb.addInput(unspent.txId, unspent.vout) txb.addInput(unspent.txId, unspent.vout);
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 2e4) txb.addOutput(regtestUtils.RANDOM_ADDRESS, 2e4);
txb.sign({ txb.sign({
prevOutScriptType: 'p2sh-p2wpkh', prevOutScriptType: 'p2sh-p2wpkh',
vin: 0, vin: 0,
keyPair: keyPair, keyPair: keyPair,
redeemScript: p2sh.redeem.output, redeemScript: p2sh.redeem!.output,
witnessValue: unspent.value, witnessValue: unspent.value,
}) });
const tx = txb.build() const tx = txb.build();
// build and broadcast to the Bitcoin RegTest network // build and broadcast to the Bitcoin RegTest network
await regtestUtils.broadcast(tx.toHex()) await regtestUtils.broadcast(tx.toHex());
await regtestUtils.verify({ await regtestUtils.verify({
txId: tx.getId(), txId: tx.getId(),
address: regtestUtils.RANDOM_ADDRESS, address: regtestUtils.RANDOM_ADDRESS,
vout: 0, vout: 0,
value: 2e4 value: 2e4,
}) });
}) });
it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WPKH input', async () => { it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WPKH input', async () => {
const keyPair = bitcoin.ECPair.makeRandom({ network: regtest }) const keyPair = bitcoin.ECPair.makeRandom({ network: regtest });
const p2wpkh = bitcoin.payments.p2wpkh({ pubkey: keyPair.publicKey, network: regtest }) const p2wpkh = bitcoin.payments.p2wpkh({
pubkey: keyPair.publicKey,
network: regtest,
});
const unspent = await regtestUtils.faucetComplex(p2wpkh.output, 5e4) const unspent = await regtestUtils.faucetComplex(p2wpkh.output!, 5e4);
// XXX: build the Transaction w/ a P2WPKH input // XXX: build the Transaction w/ a P2WPKH input
const txb = new bitcoin.TransactionBuilder(regtest) const txb = new bitcoin.TransactionBuilder(regtest);
txb.addInput(unspent.txId, unspent.vout, null, p2wpkh.output) // NOTE: provide the prevOutScript! txb.addInput(unspent.txId, unspent.vout, undefined, p2wpkh.output); // NOTE: provide the prevOutScript!
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 2e4) txb.addOutput(regtestUtils.RANDOM_ADDRESS, 2e4);
txb.sign({ txb.sign({
prevOutScriptType: 'p2wpkh', prevOutScriptType: 'p2wpkh',
vin: 0, vin: 0,
keyPair: keyPair, keyPair: keyPair,
witnessValue: unspent.value, witnessValue: unspent.value,
}) // NOTE: no redeem script }); // NOTE: no redeem script
const tx = txb.build() const tx = txb.build();
// build and broadcast (the P2WPKH transaction) to the Bitcoin RegTest network // build and broadcast (the P2WPKH transaction) to the Bitcoin RegTest network
await regtestUtils.broadcast(tx.toHex()) await regtestUtils.broadcast(tx.toHex());
await regtestUtils.verify({ await regtestUtils.verify({
txId: tx.getId(), txId: tx.getId(),
address: regtestUtils.RANDOM_ADDRESS, address: regtestUtils.RANDOM_ADDRESS,
vout: 0, vout: 0,
value: 2e4 value: 2e4,
}) });
}) });
it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WSH(P2PK) input', async () => { it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WSH(P2PK) input', async () => {
const keyPair = bitcoin.ECPair.makeRandom({ network: regtest }) const keyPair = bitcoin.ECPair.makeRandom({ network: regtest });
const p2pk = bitcoin.payments.p2pk({ pubkey: keyPair.publicKey, network: regtest }) const p2pk = bitcoin.payments.p2pk({
const p2wsh = bitcoin.payments.p2wsh({ redeem: p2pk, network: regtest }) pubkey: keyPair.publicKey,
network: regtest,
});
const p2wsh = bitcoin.payments.p2wsh({ redeem: p2pk, network: regtest });
const unspent = await regtestUtils.faucetComplex(p2wsh.output, 5e4) const unspent = await regtestUtils.faucetComplex(p2wsh.output!, 5e4);
// XXX: build the Transaction w/ a P2WSH input // XXX: build the Transaction w/ a P2WSH input
const txb = new bitcoin.TransactionBuilder(regtest) const txb = new bitcoin.TransactionBuilder(regtest);
txb.addInput(unspent.txId, unspent.vout, null, p2wsh.output) // NOTE: provide the prevOutScript! txb.addInput(unspent.txId, unspent.vout, undefined, p2wsh.output); // NOTE: provide the prevOutScript!
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 2e4) txb.addOutput(regtestUtils.RANDOM_ADDRESS, 2e4);
txb.sign({ txb.sign({
prevOutScriptType: 'p2wsh-p2pk', prevOutScriptType: 'p2wsh-p2pk',
vin: 0, vin: 0,
keyPair: keyPair, keyPair: keyPair,
witnessValue: 5e4, witnessValue: 5e4,
witnessScript: p2wsh.redeem.output, witnessScript: p2wsh.redeem!.output,
}) // NOTE: provide a witnessScript! }); // NOTE: provide a witnessScript!
const tx = txb.build() const tx = txb.build();
// build and broadcast (the P2WSH transaction) to the Bitcoin RegTest network // build and broadcast (the P2WSH transaction) to the Bitcoin RegTest network
await regtestUtils.broadcast(tx.toHex()) await regtestUtils.broadcast(tx.toHex());
await regtestUtils.verify({ await regtestUtils.verify({
txId: tx.getId(), txId: tx.getId(),
address: regtestUtils.RANDOM_ADDRESS, address: regtestUtils.RANDOM_ADDRESS,
vout: 0, vout: 0,
value: 2e4 value: 2e4,
}) });
}) });
it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WSH(P2MS(3 of 4))) (SegWit multisig) input', async () => { it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WSH(P2MS(3 of 4))) (SegWit multisig) input', async () => {
const keyPairs = [ const keyPairs = [
bitcoin.ECPair.makeRandom({ network: regtest }), bitcoin.ECPair.makeRandom({ network: regtest }),
bitcoin.ECPair.makeRandom({ network: regtest }), bitcoin.ECPair.makeRandom({ network: regtest }),
bitcoin.ECPair.makeRandom({ network: regtest }), bitcoin.ECPair.makeRandom({ network: regtest }),
bitcoin.ECPair.makeRandom({ network: regtest }) bitcoin.ECPair.makeRandom({ network: regtest }),
] ];
const pubkeys = keyPairs.map(x => x.publicKey) const pubkeys = keyPairs.map(x => x.publicKey);
const p2ms = bitcoin.payments.p2ms({ m: 3, pubkeys, network: regtest }) const p2ms = bitcoin.payments.p2ms({ m: 3, pubkeys, network: regtest });
const p2wsh = bitcoin.payments.p2wsh({ redeem: p2ms, network: regtest }) const p2wsh = bitcoin.payments.p2wsh({ redeem: p2ms, network: regtest });
const p2sh = bitcoin.payments.p2sh({ redeem: p2wsh, network: regtest }) const p2sh = bitcoin.payments.p2sh({ redeem: p2wsh, network: regtest });
const unspent = await regtestUtils.faucet(p2sh.address, 6e4) const unspent = await regtestUtils.faucet(p2sh.address!, 6e4);
const txb = new bitcoin.TransactionBuilder(regtest) const txb = new bitcoin.TransactionBuilder(regtest);
txb.addInput(unspent.txId, unspent.vout, null, p2sh.output) txb.addInput(unspent.txId, unspent.vout, undefined, p2sh.output);
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 3e4) txb.addOutput(regtestUtils.RANDOM_ADDRESS, 3e4);
txb.sign({ txb.sign({
prevOutScriptType: 'p2sh-p2wsh-p2ms', prevOutScriptType: 'p2sh-p2wsh-p2ms',
vin: 0, vin: 0,
keyPair: keyPairs[0], keyPair: keyPairs[0],
redeemScript: p2sh.redeem.output, redeemScript: p2sh.redeem!.output,
witnessValue: unspent.value, witnessValue: unspent.value,
witnessScript: p2wsh.redeem.output, witnessScript: p2wsh.redeem!.output,
}) });
txb.sign({ txb.sign({
prevOutScriptType: 'p2sh-p2wsh-p2ms', prevOutScriptType: 'p2sh-p2wsh-p2ms',
vin: 0, vin: 0,
keyPair: keyPairs[2], keyPair: keyPairs[2],
redeemScript: p2sh.redeem.output, redeemScript: p2sh.redeem!.output,
witnessValue: unspent.value, witnessValue: unspent.value,
witnessScript: p2wsh.redeem.output, witnessScript: p2wsh.redeem!.output,
}) });
txb.sign({ txb.sign({
prevOutScriptType: 'p2sh-p2wsh-p2ms', prevOutScriptType: 'p2sh-p2wsh-p2ms',
vin: 0, vin: 0,
keyPair: keyPairs[3], keyPair: keyPairs[3],
redeemScript: p2sh.redeem.output, redeemScript: p2sh.redeem!.output,
witnessValue: unspent.value, witnessValue: unspent.value,
witnessScript: p2wsh.redeem.output, witnessScript: p2wsh.redeem!.output,
}) });
const tx = txb.build() const tx = txb.build();
// build and broadcast to the Bitcoin RegTest network // build and broadcast to the Bitcoin RegTest network
await regtestUtils.broadcast(tx.toHex()) await regtestUtils.broadcast(tx.toHex());
await regtestUtils.verify({ await regtestUtils.verify({
txId: tx.getId(), txId: tx.getId(),
address: regtestUtils.RANDOM_ADDRESS, address: regtestUtils.RANDOM_ADDRESS,
vout: 0, vout: 0,
value: 3e4 value: 3e4,
}) });
}) });
it('can verify Transaction (P2PKH) signatures', () => { it('can verify Transaction (P2PKH) signatures', () => {
const txHex = '010000000321c5f7e7bc98b3feda84aad36a5c99a02bcb8823a2f3eccbcd5da209698b5c20000000006b48304502210099e021772830207cf7c55b69948d3b16b4dcbf1f55a9cd80ebf8221a169735f9022064d33f11d62cd28240b3862afc0b901adc9f231c7124dd19bdb30367b61964c50121032b4c06c06c3ec0b7fa29519dfa5aae193ee2cc35ca127f29f14ec605d62fb63dffffffff8a75ce85441ddb3f342708ee33cc8ed418b07d9ba9e0e7c4e1cccfe9f52d8a88000000006946304302207916c23dae212c95a920423902fa44e939fb3d542f4478a7b46e9cde53705800021f0d74e9504146e404c1b8f9cba4dff2d4782e3075491c9ed07ce4a7d1c4461a01210216c92abe433106491bdeb4a261226f20f5a4ac86220cc6e37655aac6bf3c1f2affffffffdfef93f69fe32e944fad79fa8f882b3a155d80383252348caba1a77a5abbf7ef000000006b483045022100faa6e9ca289b46c64764a624c59ac30d9abcf1d4a04c4de9089e67cbe0d300a502206930afa683f6807502de5c2431bf9a1fd333c8a2910a76304df0f3d23d83443f0121039e05da8b8ea4f9868ecebb25998c7701542986233f4401799551fbecf316b18fffffffff01ff4b0000000000001976a9146c86476d1d85cd60116cd122a274e6a570a5a35c88acc96d0700' const txHex =
'010000000321c5f7e7bc98b3feda84aad36a5c99a02bcb8823a2f3eccbcd5da209698b5c20000000006b48304502210099e021772830207cf7c55b69948d3b16b4dcbf1f55a9cd80ebf8221a169735f9022064d33f11d62cd28240b3862afc0b901adc9f231c7124dd19bdb30367b61964c50121032b4c06c06c3ec0b7fa29519dfa5aae193ee2cc35ca127f29f14ec605d62fb63dffffffff8a75ce85441ddb3f342708ee33cc8ed418b07d9ba9e0e7c4e1cccfe9f52d8a88000000006946304302207916c23dae212c95a920423902fa44e939fb3d542f4478a7b46e9cde53705800021f0d74e9504146e404c1b8f9cba4dff2d4782e3075491c9ed07ce4a7d1c4461a01210216c92abe433106491bdeb4a261226f20f5a4ac86220cc6e37655aac6bf3c1f2affffffffdfef93f69fe32e944fad79fa8f882b3a155d80383252348caba1a77a5abbf7ef000000006b483045022100faa6e9ca289b46c64764a624c59ac30d9abcf1d4a04c4de9089e67cbe0d300a502206930afa683f6807502de5c2431bf9a1fd333c8a2910a76304df0f3d23d83443f0121039e05da8b8ea4f9868ecebb25998c7701542986233f4401799551fbecf316b18fffffffff01ff4b0000000000001976a9146c86476d1d85cd60116cd122a274e6a570a5a35c88acc96d0700';
const keyPairs = [ const keyPairs = [
'032b4c06c06c3ec0b7fa29519dfa5aae193ee2cc35ca127f29f14ec605d62fb63d', '032b4c06c06c3ec0b7fa29519dfa5aae193ee2cc35ca127f29f14ec605d62fb63d',
'0216c92abe433106491bdeb4a261226f20f5a4ac86220cc6e37655aac6bf3c1f2a', '0216c92abe433106491bdeb4a261226f20f5a4ac86220cc6e37655aac6bf3c1f2a',
'039e05da8b8ea4f9868ecebb25998c7701542986233f4401799551fbecf316b18f' '039e05da8b8ea4f9868ecebb25998c7701542986233f4401799551fbecf316b18f',
].map(q => { return bitcoin.ECPair.fromPublicKey(Buffer.from(q, 'hex')) }) ].map(q => {
return bitcoin.ECPair.fromPublicKey(Buffer.from(q, 'hex'));
});
const tx = bitcoin.Transaction.fromHex(txHex) const tx = bitcoin.Transaction.fromHex(txHex);
tx.ins.forEach((input, i) => { tx.ins.forEach((input, i) => {
const keyPair = keyPairs[i] const keyPair = keyPairs[i];
const p2pkh = bitcoin.payments.p2pkh({ const p2pkh = bitcoin.payments.p2pkh({
pubkey: keyPair.publicKey, pubkey: keyPair.publicKey,
input: input.script input: input.script,
}) });
const ss = bitcoin.script.signature.decode(p2pkh.signature) const ss = bitcoin.script.signature.decode(p2pkh.signature!);
const hash = tx.hashForSignature(i, p2pkh.output, ss.hashType) const hash = tx.hashForSignature(i, p2pkh.output!, ss.hashType);
assert.strictEqual(keyPair.verify(hash, ss.signature), true) assert.strictEqual(keyPair.verify(hash, ss.signature), true);
}) });
}) });
it('can verify Transaction (P2SH(P2WPKH)) signatures', () => { it('can verify Transaction (P2SH(P2WPKH)) signatures', () => {
const utxos = { const utxos = {
'f72d1d83ac40fcedd01415751556a905844ab5f44bbb7728565ebb91b1590109:0': { 'f72d1d83ac40fcedd01415751556a905844ab5f44bbb7728565ebb91b1590109:0': {
value: 50000 value: 50000,
} },
} };
const txHex = '02000000000101090159b191bb5e562877bb4bf4b54a8405a95615751514d0edfc40ac831d2df7000000001716001435a179e5516947a39ae9c8a25e9fe62c0fc598edffffffff01204e0000000000001976a91431d43308d3c886d53e9ae8a45728370571ff456988ac0247304402206ec41f685b997a51f325b07ee852e82a535f6b52ef54485cc133e05168aa052a022070bafa86108acb51c77b2b259ae8fb7fd1efa10fef804fcfe9b13c2db719acf5012103fb03e9d0a9af86cbed94225dbb8bb70f6b82109bce0a61ddcf41dab6cbb4871100000000' const txHex =
const tx = bitcoin.Transaction.fromHex(txHex) '02000000000101090159b191bb5e562877bb4bf4b54a8405a95615751514d0edfc40ac831d2df7000000001716001435a179e5516947a39ae9c8a25e9fe62c0fc598edffffffff01204e0000000000001976a91431d43308d3c886d53e9ae8a45728370571ff456988ac0247304402206ec41f685b997a51f325b07ee852e82a535f6b52ef54485cc133e05168aa052a022070bafa86108acb51c77b2b259ae8fb7fd1efa10fef804fcfe9b13c2db719acf5012103fb03e9d0a9af86cbed94225dbb8bb70f6b82109bce0a61ddcf41dab6cbb4871100000000';
const tx = bitcoin.Transaction.fromHex(txHex);
tx.ins.forEach((input, i) => { tx.ins.forEach((input, i) => {
const txId = Buffer.from(input.hash).reverse().toString('hex') const txId = (Buffer.from(input.hash).reverse() as Buffer).toString(
const utxo = utxos[`${txId}:${i}`] 'hex',
if (!utxo) throw new Error('Missing utxo') );
const utxo = (utxos as any)[`${txId}:${i}`];
if (!utxo) throw new Error('Missing utxo');
const p2sh = bitcoin.payments.p2sh({ const p2sh = bitcoin.payments.p2sh({
input: input.script, input: input.script,
witness: input.witness witness: input.witness,
}) });
const p2wpkh = bitcoin.payments.p2wpkh(p2sh.redeem) const p2wpkh = bitcoin.payments.p2wpkh(p2sh.redeem!);
const p2pkh = bitcoin.payments.p2pkh({ pubkey: p2wpkh.pubkey }) // because P2WPKH is annoying const p2pkh = bitcoin.payments.p2pkh({ pubkey: p2wpkh.pubkey }); // because P2WPKH is annoying
const ss = bitcoin.script.signature.decode(p2wpkh.signature) const ss = bitcoin.script.signature.decode(p2wpkh.signature!);
const hash = tx.hashForWitnessV0(i, p2pkh.output, utxo.value, ss.hashType) const hash = tx.hashForWitnessV0(
const keyPair = bitcoin.ECPair.fromPublicKey(p2wpkh.pubkey) // aka, cQ3EtF4mApRcogNGSeyPTKbmfxxn3Yfb1wecfKSws9a8bnYuxoAk i,
p2pkh.output!,
utxo.value,
ss.hashType,
);
const keyPair = bitcoin.ECPair.fromPublicKey(p2wpkh.pubkey!); // aka, cQ3EtF4mApRcogNGSeyPTKbmfxxn3Yfb1wecfKSws9a8bnYuxoAk
assert.strictEqual(keyPair.verify(hash, ss.signature), true) assert.strictEqual(keyPair.verify(hash, ss.signature), true);
}) });
}) });
}) });

View file

@ -1,49 +1,55 @@
const { describe, it } = require('mocha') import * as assert from 'assert';
const assert = require('assert') import { describe, it } from 'mocha';
const u = require('./payments.utils') import * as u from './payments.utils';
import { PaymentCreator } from '../src/payments';
;['embed', 'p2ms', 'p2pk', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh'].forEach(p => { ['embed', 'p2ms', 'p2pk', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh'].forEach(p => {
describe(p, () => { describe(p, () => {
let fn let fn: PaymentCreator;
let payment = require('../src/payments/' + p) let payment = require('../src/payments/' + p);
if (p === 'embed') { if (p === 'embed') {
fn = payment.p2data fn = payment.p2data;
} else { } else {
fn = payment[p] fn = payment[p];
} }
const fixtures = require('./fixtures/' + p) const fixtures = require('./fixtures/' + p);
fixtures.valid.forEach((f, i) => { fixtures.valid.forEach((f: any) => {
it(f.description + ' as expected', () => { it(f.description + ' as expected', () => {
const args = u.preform(f.arguments) const args = u.preform(f.arguments);
const actual = fn(args, f.options) const actual = fn(args, f.options);
u.equate(actual, f.expected, f.arguments) u.equate(actual, f.expected, f.arguments);
}) });
it(f.description + ' as expected (no validation)', () => { it(f.description + ' as expected (no validation)', () => {
const args = u.preform(f.arguments) const args = u.preform(f.arguments);
const actual = fn(args, Object.assign({}, f.options, { const actual = fn(
validate: false args,
})) Object.assign({}, f.options, {
validate: false,
}),
);
u.equate(actual, f.expected, f.arguments) u.equate(actual, f.expected, f.arguments);
}) });
}) });
fixtures.invalid.forEach(f => { fixtures.invalid.forEach((f: any) => {
it('throws ' + f.exception + (f.description ? ('for ' + f.description) : ''), () => { it(
const args = u.preform(f.arguments) 'throws ' + f.exception + (f.description ? 'for ' + f.description : ''),
() => {
const args = u.preform(f.arguments);
assert.throws(() => { assert.throws(() => {
fn(args, f.options) fn(args, f.options);
}, new RegExp(f.exception)) }, new RegExp(f.exception));
}) },
}) );
});
if (p === 'p2sh') { if (p === 'p2sh') {
const p2wsh = require('../src/payments/p2wsh').p2wsh const p2wsh = require('../src/payments/p2wsh').p2wsh;
const p2pk = require('../src/payments/p2pk').p2pk const p2pk = require('../src/payments/p2pk').p2pk;
it('properly assembles nested p2wsh with names', () => { it('properly assembles nested p2wsh with names', () => {
const actual = fn({ const actual = fn({
redeem: p2wsh({ redeem: p2wsh({
@ -51,42 +57,57 @@ const u = require('./payments.utils')
pubkey: Buffer.from( pubkey: Buffer.from(
'03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058', '03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058',
'hex', 'hex',
) ),
}) }),
}) }),
}) });
assert.strictEqual(actual.address, '3MGbrbye4ttNUXM8WAvBFRKry4fkS9fjuw') assert.strictEqual(
assert.strictEqual(actual.name, 'p2sh-p2wsh-p2pk') actual.address,
assert.strictEqual(actual.redeem.name, 'p2wsh-p2pk') '3MGbrbye4ttNUXM8WAvBFRKry4fkS9fjuw',
assert.strictEqual(actual.redeem.redeem.name, 'p2pk') );
}) assert.strictEqual(actual.name, 'p2sh-p2wsh-p2pk');
assert.strictEqual(actual.redeem!.name, 'p2wsh-p2pk');
assert.strictEqual(actual.redeem!.redeem!.name, 'p2pk');
});
} }
// cross-verify dynamically too // cross-verify dynamically too
if (!fixtures.dynamic) return if (!fixtures.dynamic) return;
const { depends, details } = fixtures.dynamic const { depends, details } = fixtures.dynamic;
details.forEach(f => { details.forEach((f: any) => {
const detail = u.preform(f) const detail = u.preform(f);
const disabled = {} const disabled: any = {};
if (f.disabled) f.disabled.forEach(k => { disabled[k] = true }) if (f.disabled)
f.disabled.forEach((k: string) => {
disabled[k] = true;
});
for (let key in depends) { for (let key in depends) {
if (key in disabled) continue if (key in disabled) continue;
const dependencies = depends[key] const dependencies = depends[key];
dependencies.forEach(dependency => { dependencies.forEach((dependency: any) => {
if (!Array.isArray(dependency)) dependency = [dependency] if (!Array.isArray(dependency)) dependency = [dependency];
const args = {} const args = {};
dependency.forEach(d => { u.from(d, detail, args) }) dependency.forEach((d: any) => {
const expected = u.from(key, detail) u.from(d, detail, args);
});
const expected = u.from(key, detail);
it(f.description + ', ' + key + ' derives from ' + JSON.stringify(dependency), () => { it(
u.equate(fn(args), expected) f.description +
}) ', ' +
}) key +
' derives from ' +
JSON.stringify(dependency),
() => {
u.equate(fn(args), expected);
},
);
});
} }
}) });
}) });
}) });

View file

@ -1,134 +0,0 @@
const t = require('assert')
const bscript = require('../src/script')
const BNETWORKS = require('../src/networks')
function tryHex (x) {
if (Buffer.isBuffer(x)) return x.toString('hex')
if (Array.isArray(x)) return x.map(tryHex)
return x
}
function fromHex (x) {
if (typeof x === 'string') return Buffer.from(x, 'hex')
if (Array.isArray(x)) return x.map(fromHex)
return x
}
function tryASM (x) {
if (Buffer.isBuffer(x)) return bscript.toASM(x)
return x
}
function asmToBuffer (x) {
if (x === '') return Buffer.alloc(0)
return bscript.fromASM(x)
}
function carryOver (a, b) {
for (let k in b) {
if (k in a && k === 'redeem') {
carryOver(a[k], b[k])
continue
}
// don't, the value was specified
if (k in a) continue
// otherwise, expect match
a[k] = b[k]
}
}
function equateBase (a, b, context) {
if ('output' in b) t.strictEqual(tryASM(a.output), tryASM(b.output), `Inequal ${context}output`)
if ('input' in b) t.strictEqual(tryASM(a.input), tryASM(b.input), `Inequal ${context}input`)
if ('witness' in b) t.deepStrictEqual(tryHex(a.witness), tryHex(b.witness), `Inequal ${context}witness`)
}
function equate (a, b, args) {
b = Object.assign({}, b)
carryOver(b, args)
// by null, we mean 'undefined', but JSON
if (b.input === null) b.input = undefined
if (b.output === null) b.output = undefined
if (b.witness === null) b.witness = undefined
if (b.redeem) {
if (b.redeem.input === null) b.redeem.input = undefined
if (b.redeem.output === null) b.redeem.output = undefined
if (b.redeem.witness === null) b.redeem.witness = undefined
}
equateBase(a, b, '')
if (b.redeem) equateBase(a.redeem, b.redeem, 'redeem.')
if (b.network) t.deepStrictEqual(a.network, BNETWORKS[b.network], 'Inequal *.network')
// contextual
if (b.signature === null) b.signature = undefined
if (b.signatures === null) b.signatures = undefined
if ('address' in b) t.strictEqual(a.address, b.address, 'Inequal *.address')
if ('hash' in b) t.strictEqual(tryHex(a.hash), tryHex(b.hash), 'Inequal *.hash')
if ('pubkey' in b) t.strictEqual(tryHex(a.pubkey), tryHex(b.pubkey), 'Inequal *.pubkey')
if ('signature' in b) t.strictEqual(tryHex(a.signature), tryHex(b.signature), 'Inequal signature')
if ('m' in b) t.strictEqual(a.m, b.m, 'Inequal *.m')
if ('n' in b) t.strictEqual(a.n, b.n, 'Inequal *.n')
if ('pubkeys' in b) t.deepStrictEqual(tryHex(a.pubkeys), tryHex(b.pubkeys), 'Inequal *.pubkeys')
if ('signatures' in b) t.deepStrictEqual(tryHex(a.signatures), tryHex(b.signatures), 'Inequal *.signatures')
if ('data' in b) t.deepStrictEqual(tryHex(a.data), tryHex(b.data), 'Inequal *.data')
}
function preform (x) {
x = Object.assign({}, x)
if (x.network) x.network = BNETWORKS[x.network]
if (typeof x.inputHex === 'string') {
x.input = Buffer.from(x.inputHex, 'hex')
delete x.inputHex
}
if (typeof x.outputHex === 'string') {
x.output = Buffer.from(x.outputHex, 'hex')
delete x.outputHex
}
if (typeof x.output === 'string') x.output = asmToBuffer(x.output)
if (typeof x.input === 'string') x.input = asmToBuffer(x.input)
if (Array.isArray(x.witness)) x.witness = x.witness.map(fromHex)
if (x.data) x.data = x.data.map(fromHex)
if (x.hash) x.hash = Buffer.from(x.hash, 'hex')
if (x.pubkey) x.pubkey = Buffer.from(x.pubkey, 'hex')
if (x.signature) x.signature = Buffer.from(x.signature, 'hex')
if (x.pubkeys) x.pubkeys = x.pubkeys.map(fromHex)
if (x.signatures) x.signatures = x.signatures.map(y => { return Number.isFinite(y) ? y : Buffer.from(y, 'hex') })
if (x.redeem) {
x.redeem = Object.assign({}, x.redeem)
if (typeof x.redeem.input === 'string') x.redeem.input = asmToBuffer(x.redeem.input)
if (typeof x.redeem.output === 'string') x.redeem.output = asmToBuffer(x.redeem.output)
if (Array.isArray(x.redeem.witness)) x.redeem.witness = x.redeem.witness.map(fromHex)
if (x.redeem.network) x.redeem.network = BNETWORKS[x.redeem.network]
}
return x
}
function from (path, object, result) {
path = path.split('.')
result = result || {}
let r = result
path.forEach((k, i) => {
if (i < path.length - 1) {
r[k] = r[k] || {}
// recurse
r = r[k]
object = object[k]
} else {
r[k] = object[k]
}
})
return result
}
module.exports = {
from,
equate,
preform
}

169
test/payments.utils.ts Normal file
View file

@ -0,0 +1,169 @@
import * as t from 'assert';
import * as bscript from '../src/script';
import * as BNETWORKS from '../src/networks';
function tryHex(x: Buffer | Buffer[]): string | string[] {
if (Buffer.isBuffer(x)) return x.toString('hex');
if (Array.isArray(x)) return x.map(tryHex) as string[];
return x;
}
function fromHex(x: string | string[]): Buffer | Buffer[] {
if (typeof x === 'string') return Buffer.from(x, 'hex');
if (Array.isArray(x)) return x.map(fromHex) as Buffer[];
return x;
}
function tryASM(x: Buffer): string {
if (Buffer.isBuffer(x)) return bscript.toASM(x);
return x;
}
function asmToBuffer(x: string) {
if (x === '') return Buffer.alloc(0);
return bscript.fromASM(x);
}
function carryOver(a: any, b: any) {
for (let k in b) {
if (k in a && k === 'redeem') {
carryOver(a[k], b[k]);
continue;
}
// don't, the value was specified
if (k in a) continue;
// otherwise, expect match
a[k] = b[k];
}
}
function equateBase(a: any, b: any, context: string) {
if ('output' in b)
t.strictEqual(
tryASM(a.output),
tryASM(b.output),
`Inequal ${context}output`,
);
if ('input' in b)
t.strictEqual(tryASM(a.input), tryASM(b.input), `Inequal ${context}input`);
if ('witness' in b)
t.deepStrictEqual(
tryHex(a.witness),
tryHex(b.witness),
`Inequal ${context}witness`,
);
}
export function equate(a: any, b: any, args?: any) {
b = Object.assign({}, b);
carryOver(b, args);
// by null, we mean 'undefined', but JSON
if (b.input === null) b.input = undefined;
if (b.output === null) b.output = undefined;
if (b.witness === null) b.witness = undefined;
if (b.redeem) {
if (b.redeem.input === null) b.redeem.input = undefined;
if (b.redeem.output === null) b.redeem.output = undefined;
if (b.redeem.witness === null) b.redeem.witness = undefined;
}
equateBase(a, b, '');
if (b.redeem) equateBase(a.redeem, b.redeem, 'redeem.');
if (b.network)
t.deepStrictEqual(
a.network,
(BNETWORKS as any)[b.network],
'Inequal *.network',
);
// contextual
if (b.signature === null) b.signature = undefined;
if (b.signatures === null) b.signatures = undefined;
if ('address' in b) t.strictEqual(a.address, b.address, 'Inequal *.address');
if ('hash' in b)
t.strictEqual(tryHex(a.hash), tryHex(b.hash), 'Inequal *.hash');
if ('pubkey' in b)
t.strictEqual(tryHex(a.pubkey), tryHex(b.pubkey), 'Inequal *.pubkey');
if ('signature' in b)
t.strictEqual(
tryHex(a.signature),
tryHex(b.signature),
'Inequal signature',
);
if ('m' in b) t.strictEqual(a.m, b.m, 'Inequal *.m');
if ('n' in b) t.strictEqual(a.n, b.n, 'Inequal *.n');
if ('pubkeys' in b)
t.deepStrictEqual(
tryHex(a.pubkeys),
tryHex(b.pubkeys),
'Inequal *.pubkeys',
);
if ('signatures' in b)
t.deepStrictEqual(
tryHex(a.signatures),
tryHex(b.signatures),
'Inequal *.signatures',
);
if ('data' in b)
t.deepStrictEqual(tryHex(a.data), tryHex(b.data), 'Inequal *.data');
}
export function preform(x: any) {
x = Object.assign({}, x);
if (x.network) x.network = (BNETWORKS as any)[x.network];
if (typeof x.inputHex === 'string') {
x.input = Buffer.from(x.inputHex, 'hex');
delete x.inputHex;
}
if (typeof x.outputHex === 'string') {
x.output = Buffer.from(x.outputHex, 'hex');
delete x.outputHex;
}
if (typeof x.output === 'string') x.output = asmToBuffer(x.output);
if (typeof x.input === 'string') x.input = asmToBuffer(x.input);
if (Array.isArray(x.witness)) x.witness = x.witness.map(fromHex);
if (x.data) x.data = x.data.map(fromHex);
if (x.hash) x.hash = Buffer.from(x.hash, 'hex');
if (x.pubkey) x.pubkey = Buffer.from(x.pubkey, 'hex');
if (x.signature) x.signature = Buffer.from(x.signature, 'hex');
if (x.pubkeys) x.pubkeys = x.pubkeys.map(fromHex);
if (x.signatures)
x.signatures = x.signatures.map((y: any) => {
return Number.isFinite(y) ? y : Buffer.from(y, 'hex');
});
if (x.redeem) {
x.redeem = Object.assign({}, x.redeem);
if (typeof x.redeem.input === 'string')
x.redeem.input = asmToBuffer(x.redeem.input);
if (typeof x.redeem.output === 'string')
x.redeem.output = asmToBuffer(x.redeem.output);
if (Array.isArray(x.redeem.witness))
x.redeem.witness = x.redeem.witness.map(fromHex);
if (x.redeem.network)
x.redeem.network = (BNETWORKS as any)[x.redeem.network];
}
return x;
}
export function from(path: string, object: any, result?: any) {
const paths = path.split('.');
result = result || {};
let r = result;
paths.forEach((k, i) => {
if (i < paths.length - 1) {
r[k] = r[k] || {};
// recurse
r = r[k];
object = object[k];
} else {
r[k] = object[k];
}
});
return result;
}

File diff suppressed because it is too large Load diff

View file

@ -1,157 +1,176 @@
const { describe, it } = require('mocha') import * as assert from 'assert';
const assert = require('assert') import { describe, it } from 'mocha';
const bscript = require('../src/script') import * as bscript from '../src/script';
const minimalData = require('minimaldata') import * as fixtures from './fixtures/script.json';
import * as fixtures2 from './fixtures/templates.json';
const fixtures = require('./fixtures/script.json') const minimalData = require('minimaldata');
const fixtures2 = require('./fixtures/templates.json')
describe('script', () => { describe('script', () => {
// TODO // TODO
describe('isCanonicalPubKey', () => { describe('isCanonicalPubKey', () => {
it('rejects if not provided a Buffer', () => { it('rejects if not provided a Buffer', () => {
assert.strictEqual(false, bscript.isCanonicalPubKey(0)) assert.strictEqual(false, bscript.isCanonicalPubKey(0 as any));
}) });
it('rejects smaller than 33', () => { it('rejects smaller than 33', () => {
for (var i = 0; i < 33; i++) { for (var i = 0; i < 33; i++) {
assert.strictEqual(false, bscript.isCanonicalPubKey(Buffer.from('', i))) assert.strictEqual(
false,
bscript.isCanonicalPubKey(Buffer.allocUnsafe(i)),
);
} }
}) });
}) });
describe.skip('isCanonicalScriptSignature', () => { describe.skip('isCanonicalScriptSignature', () => {});
})
describe('fromASM/toASM', () => { describe('fromASM/toASM', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
it('encodes/decodes ' + f.asm, () => { it('encodes/decodes ' + f.asm, () => {
const script = bscript.fromASM(f.asm) const script = bscript.fromASM(f.asm);
assert.strictEqual(bscript.toASM(script), f.asm) assert.strictEqual(bscript.toASM(script), f.asm);
}) });
}) });
fixtures.invalid.fromASM.forEach(f => { fixtures.invalid.fromASM.forEach(f => {
it('throws ' + f.description, () => { it('throws ' + f.description, () => {
assert.throws(() => { assert.throws(() => {
bscript.fromASM(f.script) bscript.fromASM(f.script);
}, new RegExp(f.description)) }, new RegExp(f.description));
}) });
}) });
}) });
describe('fromASM/toASM (templates)', () => { describe('fromASM/toASM (templates)', () => {
fixtures2.valid.forEach(f => { fixtures2.valid.forEach(f => {
if (f.inputHex) { if (f.inputHex) {
const ih = bscript.toASM(Buffer.from(f.inputHex, 'hex')) const ih = bscript.toASM(Buffer.from(f.inputHex, 'hex'));
it('encodes/decodes ' + ih, () => { it('encodes/decodes ' + ih, () => {
const script = bscript.fromASM(f.input) const script = bscript.fromASM(f.input);
assert.strictEqual(script.toString('hex'), f.inputHex) assert.strictEqual(script.toString('hex'), f.inputHex);
assert.strictEqual(bscript.toASM(script), f.input) assert.strictEqual(bscript.toASM(script), f.input);
}) });
} }
if (f.outputHex) { if (f.outputHex) {
it('encodes/decodes ' + f.output, () => { it('encodes/decodes ' + f.output, () => {
const script = bscript.fromASM(f.output) const script = bscript.fromASM(f.output);
assert.strictEqual(script.toString('hex'), f.outputHex) assert.strictEqual(script.toString('hex'), f.outputHex);
assert.strictEqual(bscript.toASM(script), f.output) assert.strictEqual(bscript.toASM(script), f.output);
}) });
} }
}) });
}) });
describe('isPushOnly', () => { describe('isPushOnly', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
it('returns ' + !!f.stack + ' for ' + f.asm, () => { it('returns ' + !!f.stack + ' for ' + f.asm, () => {
const script = bscript.fromASM(f.asm) const script = bscript.fromASM(f.asm);
const chunks = bscript.decompile(script) const chunks = bscript.decompile(script);
assert.strictEqual(bscript.isPushOnly(chunks), !!f.stack) assert.strictEqual(bscript.isPushOnly(chunks!), !!f.stack);
}) });
}) });
}) });
describe('toStack', () => { describe('toStack', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
it('returns ' + !!f.stack + ' for ' + f.asm, () => { it('returns ' + !!f.stack + ' for ' + f.asm, () => {
if (!f.stack || !f.asm) return if (!f.stack || !f.asm) return;
const script = bscript.fromASM(f.asm) const script = bscript.fromASM(f.asm);
const stack = bscript.toStack(script) const stack = bscript.toStack(script);
assert.deepStrictEqual(stack.map(x => { assert.deepStrictEqual(
return x.toString('hex') stack.map(x => {
}), f.stack) return x.toString('hex');
}),
f.stack,
);
assert.strictEqual(bscript.toASM(bscript.compile(stack)), f.asm, 'should rebuild same script from stack') assert.strictEqual(
}) bscript.toASM(bscript.compile(stack)),
}) f.asm,
}) 'should rebuild same script from stack',
);
});
});
});
describe('compile (via fromASM)', () => { describe('compile (via fromASM)', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
it('(' + f.type + ') compiles ' + f.asm, () => { it('compiles ' + f.asm, () => {
const scriptSig = bscript.fromASM(f.asm) const scriptSig = bscript.fromASM(f.asm);
assert.strictEqual(scriptSig.toString('hex'), f.script) assert.strictEqual(scriptSig.toString('hex'), f.script);
if (f.nonstandard) { if (f.nonstandard) {
const scriptSigNS = bscript.fromASM(f.nonstandard.scriptSig) const scriptSigNS = bscript.fromASM(f.nonstandard.scriptSig);
assert.strictEqual(scriptSigNS.toString('hex'), f.script) assert.strictEqual(scriptSigNS.toString('hex'), f.script);
} }
}) });
}) });
}) });
describe('decompile', () => { describe('decompile', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
it('decompiles ' + f.asm, () => { it('decompiles ' + f.asm, () => {
const chunks = bscript.decompile(Buffer.from(f.script, 'hex')) const chunks = bscript.decompile(Buffer.from(f.script, 'hex'));
assert.strictEqual(bscript.compile(chunks).toString('hex'), f.script) assert.strictEqual(bscript.compile(chunks!).toString('hex'), f.script);
assert.strictEqual(bscript.toASM(chunks), f.asm) assert.strictEqual(bscript.toASM(chunks!), f.asm);
if (f.nonstandard) { if (f.nonstandard) {
const chunksNS = bscript.decompile(Buffer.from(f.nonstandard.scriptSigHex, 'hex')) const chunksNS = bscript.decompile(
Buffer.from(f.nonstandard.scriptSigHex, 'hex'),
);
assert.strictEqual(bscript.compile(chunksNS).toString('hex'), f.script) assert.strictEqual(
bscript.compile(chunksNS!).toString('hex'),
f.script,
);
// toASM converts verbatim, only `compile` transforms the script to a minimalpush compliant script // toASM converts verbatim, only `compile` transforms the script to a minimalpush compliant script
assert.strictEqual(bscript.toASM(chunksNS), f.nonstandard.scriptSig) assert.strictEqual(bscript.toASM(chunksNS!), f.nonstandard.scriptSig);
} }
}) });
}) });
fixtures.invalid.decompile.forEach(f => { fixtures.invalid.decompile.forEach(f => {
it('fails to decompile ' + f.script + ', because "' + f.description + '"', () => { it(
const chunks = bscript.decompile(Buffer.from(f.script, 'hex')) 'fails to decompile ' + f.script + ', because "' + f.description + '"',
() => {
const chunks = bscript.decompile(Buffer.from(f.script, 'hex'));
assert.strictEqual(chunks, null) assert.strictEqual(chunks, null);
}) },
}) );
}) });
});
describe('SCRIPT_VERIFY_MINIMALDATA policy', () => { describe('SCRIPT_VERIFY_MINIMALDATA policy', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
it('compliant for ' + f.type + ' scriptSig ' + f.asm, () => { it('compliant for scriptSig ' + f.asm, () => {
const script = Buffer.from(f.script, 'hex') const script = Buffer.from(f.script, 'hex');
assert(minimalData(script)) assert(minimalData(script));
}) });
}) });
function testEncodingForSize (i) { function testEncodingForSize(i: number) {
it('compliant for data PUSH of length ' + i, () => { it('compliant for data PUSH of length ' + i, () => {
const buffer = Buffer.alloc(i) const buffer = Buffer.alloc(i);
const script = bscript.compile([buffer]) const script = bscript.compile([buffer]);
assert(minimalData(script), 'Failed for ' + i + ' length script: ' + script.toString('hex')) assert(
}) minimalData(script),
'Failed for ' + i + ' length script: ' + script.toString('hex'),
);
});
} }
for (var i = 0; i < 520; ++i) { for (var i = 0; i < 520; ++i) {
testEncodingForSize(i) testEncodingForSize(i);
} }
}) });
}) });

View file

@ -1,26 +1,26 @@
const { describe, it } = require('mocha') import * as assert from 'assert';
const assert = require('assert') import { describe, it } from 'mocha';
const scriptNumber = require('../src/script_number') import * as scriptNumber from '../src/script_number';
const fixtures = require('./fixtures/script_number.json') import * as fixtures from './fixtures/script_number.json';
describe('script-number', () => { describe('script-number', () => {
describe('decode', () => { describe('decode', () => {
fixtures.forEach(f => { fixtures.forEach(f => {
it(f.hex + ' returns ' + f.number, () => { it(f.hex + ' returns ' + f.number, () => {
const actual = scriptNumber.decode(Buffer.from(f.hex, 'hex'), f.bytes) const actual = scriptNumber.decode(Buffer.from(f.hex, 'hex'), f.bytes);
assert.strictEqual(actual, f.number) assert.strictEqual(actual, f.number);
}) });
}) });
}) });
describe('encode', () => { describe('encode', () => {
fixtures.forEach(f => { fixtures.forEach(f => {
it(f.number + ' returns ' + f.hex, () => { it(f.number + ' returns ' + f.hex, () => {
const actual = scriptNumber.encode(f.number) const actual = scriptNumber.encode(f.number);
assert.strictEqual(actual.toString('hex'), f.hex) assert.strictEqual(actual.toString('hex'), f.hex);
}) });
}) });
}) });
}) });

View file

@ -1,64 +1,63 @@
const { describe, it } = require('mocha') import * as assert from 'assert';
const assert = require('assert') import { describe, it } from 'mocha';
const bscriptSig = require('../src/script').signature import { signature as bscriptSig } from '../src/script';
const Buffer = require('safe-buffer').Buffer import * as fixtures from './fixtures/signature.json';
const fixtures = require('./fixtures/signature.json')
describe('Script Signatures', () => { describe('Script Signatures', () => {
function fromRaw (signature) { function fromRaw(signature: { r: string; s: string }) {
return Buffer.concat([ return Buffer.concat(
Buffer.from(signature.r, 'hex'), [Buffer.from(signature.r, 'hex'), Buffer.from(signature.s, 'hex')],
Buffer.from(signature.s, 'hex') 64,
], 64) );
} }
function toRaw (signature) { function toRaw(signature: Buffer) {
return { return {
r: signature.slice(0, 32).toString('hex'), r: signature.slice(0, 32).toString('hex'),
s: signature.slice(32, 64).toString('hex') s: signature.slice(32, 64).toString('hex'),
} };
} }
describe('encode', () => { describe('encode', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
it('encodes ' + f.hex, () => { it('encodes ' + f.hex, () => {
const buffer = bscriptSig.encode(fromRaw(f.raw), f.hashType) const buffer = bscriptSig.encode(fromRaw(f.raw), f.hashType);
assert.strictEqual(buffer.toString('hex'), f.hex) assert.strictEqual(buffer.toString('hex'), f.hex);
}) });
}) });
fixtures.invalid.forEach(f => { fixtures.invalid.forEach(f => {
if (!f.raw) return if (!f.raw) return;
it('throws ' + f.exception, () => { it('throws ' + f.exception, () => {
const signature = fromRaw(f.raw) const signature = fromRaw(f.raw);
assert.throws(() => { assert.throws(() => {
bscriptSig.encode(signature, f.hashType) bscriptSig.encode(signature, f.hashType);
}, new RegExp(f.exception)) }, new RegExp(f.exception));
}) });
}) });
}) });
describe('decode', () => { describe('decode', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
it('decodes ' + f.hex, () => { it('decodes ' + f.hex, () => {
const decode = bscriptSig.decode(Buffer.from(f.hex, 'hex')) const decode = bscriptSig.decode(Buffer.from(f.hex, 'hex'));
assert.deepStrictEqual(toRaw(decode.signature), f.raw) assert.deepStrictEqual(toRaw(decode.signature), f.raw);
assert.strictEqual(decode.hashType, f.hashType) assert.strictEqual(decode.hashType, f.hashType);
}) });
}) });
fixtures.invalid.forEach(f => { fixtures.invalid.forEach(f => {
it('throws on ' + f.hex, () => { it('throws on ' + f.hex, () => {
const buffer = Buffer.from(f.hex, 'hex') const buffer = Buffer.from(f.hex, 'hex');
assert.throws(() => { assert.throws(() => {
bscriptSig.decode(buffer) bscriptSig.decode(buffer);
}, new RegExp(f.exception)) }, new RegExp(f.exception));
}) });
}) });
}) });
}) });

View file

@ -1,288 +1,338 @@
const { describe, it, beforeEach } = require('mocha') import * as assert from 'assert';
const assert = require('assert') import { beforeEach, describe, it } from 'mocha';
const bscript = require('../src/script') import { Transaction } from '..';
const fixtures = require('./fixtures/transaction') import * as bscript from '../src/script';
const Transaction = require('..').Transaction import * as fixtures from './fixtures/transaction.json';
describe('Transaction', () => { describe('Transaction', () => {
function fromRaw (raw, noWitness) { function fromRaw(raw: any, noWitness?: boolean) {
const tx = new Transaction() const tx = new Transaction();
tx.version = raw.version tx.version = raw.version;
tx.locktime = raw.locktime tx.locktime = raw.locktime;
raw.ins.forEach((txIn, i) => { raw.ins.forEach((txIn: any, i: number) => {
const txHash = Buffer.from(txIn.hash, 'hex') const txHash = Buffer.from(txIn.hash, 'hex');
let scriptSig let scriptSig;
if (txIn.data) { if (txIn.data) {
scriptSig = Buffer.from(txIn.data, 'hex') scriptSig = Buffer.from(txIn.data, 'hex');
} else if (txIn.script) { } else if (txIn.script) {
scriptSig = bscript.fromASM(txIn.script) scriptSig = bscript.fromASM(txIn.script);
} }
tx.addInput(txHash, txIn.index, txIn.sequence, scriptSig) tx.addInput(txHash, txIn.index, txIn.sequence, scriptSig);
if (!noWitness && txIn.witness) { if (!noWitness && txIn.witness) {
const witness = txIn.witness.map(x => { const witness = txIn.witness.map((x: string) => {
return Buffer.from(x, 'hex') return Buffer.from(x, 'hex');
}) });
tx.setWitness(i, witness) tx.setWitness(i, witness);
} }
}) });
raw.outs.forEach(txOut => { raw.outs.forEach((txOut: any) => {
let script let script: Buffer;
if (txOut.data) { if (txOut.data) {
script = Buffer.from(txOut.data, 'hex') script = Buffer.from(txOut.data, 'hex');
} else if (txOut.script) { } else if (txOut.script) {
script = bscript.fromASM(txOut.script) script = bscript.fromASM(txOut.script);
} }
tx.addOutput(script, txOut.value) tx.addOutput(script!, txOut.value);
}) });
return tx return tx;
} }
describe('fromBuffer/fromHex', () => { describe('fromBuffer/fromHex', () => {
function importExport (f) { function importExport(f: any) {
const id = f.id || f.hash const id = f.id || f.hash;
const txHex = f.hex || f.txHex const txHex = f.hex || f.txHex;
it('imports ' + f.description + ' (' + id + ')', () => { it('imports ' + f.description + ' (' + id + ')', () => {
const actual = Transaction.fromHex(txHex) const actual = Transaction.fromHex(txHex);
assert.strictEqual(actual.toHex(), txHex) assert.strictEqual(actual.toHex(), txHex);
}) });
if (f.whex) { if (f.whex) {
it('imports ' + f.description + ' (' + id + ') as witness', () => { it('imports ' + f.description + ' (' + id + ') as witness', () => {
const actual = Transaction.fromHex(f.whex) const actual = Transaction.fromHex(f.whex);
assert.strictEqual(actual.toHex(), f.whex) assert.strictEqual(actual.toHex(), f.whex);
}) });
} }
} }
fixtures.valid.forEach(importExport) fixtures.valid.forEach(importExport);
fixtures.hashForSignature.forEach(importExport) fixtures.hashForSignature.forEach(importExport);
fixtures.hashForWitnessV0.forEach(importExport) fixtures.hashForWitnessV0.forEach(importExport);
fixtures.invalid.fromBuffer.forEach(f => { fixtures.invalid.fromBuffer.forEach(f => {
it('throws on ' + f.exception, () => { it('throws on ' + f.exception, () => {
assert.throws(() => { assert.throws(() => {
Transaction.fromHex(f.hex) Transaction.fromHex(f.hex);
}, new RegExp(f.exception)) }, new RegExp(f.exception));
}) });
}) });
it('.version should be interpreted as an int32le', () => { it('.version should be interpreted as an int32le', () => {
const txHex = 'ffffffff0000ffffffff' const txHex = 'ffffffff0000ffffffff';
const tx = Transaction.fromHex(txHex) const tx = Transaction.fromHex(txHex);
assert.strictEqual(-1, tx.version) assert.strictEqual(-1, tx.version);
assert.strictEqual(0xffffffff, tx.locktime) assert.strictEqual(0xffffffff, tx.locktime);
}) });
}) });
describe('toBuffer/toHex', () => { describe('toBuffer/toHex', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
it('exports ' + f.description + ' (' + f.id + ')', () => { it('exports ' + f.description + ' (' + f.id + ')', () => {
const actual = fromRaw(f.raw, true) const actual = fromRaw(f.raw, true);
assert.strictEqual(actual.toHex(), f.hex) assert.strictEqual(actual.toHex(), f.hex);
}) });
if (f.whex) { if (f.whex) {
it('exports ' + f.description + ' (' + f.id + ') as witness', () => { it('exports ' + f.description + ' (' + f.id + ') as witness', () => {
const wactual = fromRaw(f.raw) const wactual = fromRaw(f.raw);
assert.strictEqual(wactual.toHex(), f.whex) assert.strictEqual(wactual.toHex(), f.whex);
}) });
} }
}) });
it('accepts target Buffer and offset parameters', () => { it('accepts target Buffer and offset parameters', () => {
const f = fixtures.valid[0] const f = fixtures.valid[0];
const actual = fromRaw(f.raw) const actual = fromRaw(f.raw);
const byteLength = actual.byteLength() const byteLength = actual.byteLength();
const target = Buffer.alloc(byteLength * 2) const target = Buffer.alloc(byteLength * 2);
const a = actual.toBuffer(target, 0) const a = actual.toBuffer(target, 0);
const b = actual.toBuffer(target, byteLength) const b = actual.toBuffer(target, byteLength);
assert.strictEqual(a.length, byteLength) assert.strictEqual(a.length, byteLength);
assert.strictEqual(b.length, byteLength) assert.strictEqual(b.length, byteLength);
assert.strictEqual(a.toString('hex'), f.hex) assert.strictEqual(a.toString('hex'), f.hex);
assert.strictEqual(b.toString('hex'), f.hex) assert.strictEqual(b.toString('hex'), f.hex);
assert.deepStrictEqual(a, b) assert.deepStrictEqual(a, b);
assert.deepStrictEqual(a, target.slice(0, byteLength)) assert.deepStrictEqual(a, target.slice(0, byteLength));
assert.deepStrictEqual(b, target.slice(byteLength)) assert.deepStrictEqual(b, target.slice(byteLength));
}) });
}) });
describe('hasWitnesses', () => { describe('hasWitnesses', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
it('detects if the transaction has witnesses: ' + (f.whex ? 'true' : 'false'), () => { it(
assert.strictEqual(Transaction.fromHex(f.whex ? f.whex : f.hex).hasWitnesses(), !!f.whex) 'detects if the transaction has witnesses: ' +
}) (f.whex ? 'true' : 'false'),
}) () => {
}) assert.strictEqual(
Transaction.fromHex(f.whex ? f.whex : f.hex).hasWitnesses(),
!!f.whex,
);
},
);
});
});
describe('weight/virtualSize', () => { describe('weight/virtualSize', () => {
it('computes virtual size', () => { it('computes virtual size', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
const transaction = Transaction.fromHex(f.whex ? f.whex : f.hex) const transaction = Transaction.fromHex(f.whex ? f.whex : f.hex);
assert.strictEqual(transaction.virtualSize(), f.virtualSize) assert.strictEqual(transaction.virtualSize(), f.virtualSize);
}) });
}) });
it('computes weight', () => { it('computes weight', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
const transaction = Transaction.fromHex(f.whex ? f.whex : f.hex) const transaction = Transaction.fromHex(f.whex ? f.whex : f.hex);
assert.strictEqual(transaction.weight(), f.weight) assert.strictEqual(transaction.weight(), f.weight);
}) });
}) });
}) });
describe('addInput', () => { describe('addInput', () => {
let prevTxHash let prevTxHash: Buffer;
beforeEach(() => { beforeEach(() => {
prevTxHash = Buffer.from('ffffffff00ffff000000000000000000000000000000000000000000101010ff', 'hex') prevTxHash = Buffer.from(
}) 'ffffffff00ffff000000000000000000000000000000000000000000101010ff',
'hex',
);
});
it('returns an index', () => { it('returns an index', () => {
const tx = new Transaction() const tx = new Transaction();
assert.strictEqual(tx.addInput(prevTxHash, 0), 0) assert.strictEqual(tx.addInput(prevTxHash, 0), 0);
assert.strictEqual(tx.addInput(prevTxHash, 0), 1) assert.strictEqual(tx.addInput(prevTxHash, 0), 1);
}) });
it('defaults to empty script, witness and 0xffffffff SEQUENCE number', () => { it('defaults to empty script, witness and 0xffffffff SEQUENCE number', () => {
const tx = new Transaction() const tx = new Transaction();
tx.addInput(prevTxHash, 0) tx.addInput(prevTxHash, 0);
assert.strictEqual(tx.ins[0].script.length, 0) assert.strictEqual(tx.ins[0].script.length, 0);
assert.strictEqual(tx.ins[0].witness.length, 0) assert.strictEqual(tx.ins[0].witness.length, 0);
assert.strictEqual(tx.ins[0].sequence, 0xffffffff) assert.strictEqual(tx.ins[0].sequence, 0xffffffff);
}) });
fixtures.invalid.addInput.forEach(f => { fixtures.invalid.addInput.forEach(f => {
it('throws on ' + f.exception, () => { it('throws on ' + f.exception, () => {
const tx = new Transaction() const tx = new Transaction();
const hash = Buffer.from(f.hash, 'hex') const hash = Buffer.from(f.hash, 'hex');
assert.throws(() => { assert.throws(() => {
tx.addInput(hash, f.index) tx.addInput(hash, f.index);
}, new RegExp(f.exception)) }, new RegExp(f.exception));
}) });
}) });
}) });
describe('addOutput', () => { describe('addOutput', () => {
it('returns an index', () => { it('returns an index', () => {
const tx = new Transaction() const tx = new Transaction();
assert.strictEqual(tx.addOutput(Buffer.alloc(0), 0), 0) assert.strictEqual(tx.addOutput(Buffer.alloc(0), 0), 0);
assert.strictEqual(tx.addOutput(Buffer.alloc(0), 0), 1) assert.strictEqual(tx.addOutput(Buffer.alloc(0), 0), 1);
}) });
}) });
describe('clone', () => { describe('clone', () => {
fixtures.valid.forEach(f => { fixtures.valid.forEach(f => {
let actual let actual: Transaction;
let expected let expected: Transaction;
beforeEach(() => { beforeEach(() => {
expected = Transaction.fromHex(f.hex) expected = Transaction.fromHex(f.hex);
actual = expected.clone() actual = expected.clone();
}) });
it('should have value equality', () => { it('should have value equality', () => {
assert.deepStrictEqual(actual, expected) assert.deepStrictEqual(actual, expected);
}) });
it('should not have reference equality', () => { it('should not have reference equality', () => {
assert.notStrictEqual(actual, expected) assert.notStrictEqual(actual, expected);
}) });
}) });
}) });
describe('getHash/getId', () => { describe('getHash/getId', () => {
function verify (f) { function verify(f: any) {
it('should return the id for ' + f.id + '(' + f.description + ')', () => { it('should return the id for ' + f.id + '(' + f.description + ')', () => {
const tx = Transaction.fromHex(f.whex || f.hex) const tx = Transaction.fromHex(f.whex || f.hex);
assert.strictEqual(tx.getHash().toString('hex'), f.hash) assert.strictEqual(tx.getHash().toString('hex'), f.hash);
assert.strictEqual(tx.getId(), f.id) assert.strictEqual(tx.getId(), f.id);
}) });
} }
fixtures.valid.forEach(verify) fixtures.valid.forEach(verify);
}) });
describe('isCoinbase', () => { describe('isCoinbase', () => {
function verify (f) { function verify(f: any) {
it('should return ' + f.coinbase + ' for ' + f.id + '(' + f.description + ')', () => { it(
const tx = Transaction.fromHex(f.hex) 'should return ' +
f.coinbase +
' for ' +
f.id +
'(' +
f.description +
')',
() => {
const tx = Transaction.fromHex(f.hex);
assert.strictEqual(tx.isCoinbase(), f.coinbase) assert.strictEqual(tx.isCoinbase(), f.coinbase);
}) },
);
} }
fixtures.valid.forEach(verify) fixtures.valid.forEach(verify);
}) });
describe('hashForSignature', () => { describe('hashForSignature', () => {
it('does not use Witness serialization', () => { it('does not use Witness serialization', () => {
const randScript = Buffer.from('6a', 'hex') const randScript = Buffer.from('6a', 'hex');
const tx = new Transaction() const tx = new Transaction();
tx.addInput(Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex'), 0) tx.addInput(
tx.addOutput(randScript, 5000000000) Buffer.from(
'0000000000000000000000000000000000000000000000000000000000000000',
'hex',
),
0,
);
tx.addOutput(randScript, 5000000000);
const original = tx.__toBuffer const original = (tx as any).__toBuffer;
tx.__toBuffer = (a, b, c) => { (tx as any).__toBuffer = function(
if (c !== false) throw new Error('hashForSignature MUST pass false') this: Transaction,
a: any,
b: any,
c: any,
): any {
if (c !== false) throw new Error('hashForSignature MUST pass false');
return original.call(this, a, b, c) return original.call(this, a, b, c);
} };
assert.throws(() => { assert.throws(() => {
tx.__toBuffer(undefined, undefined, true) (tx as any).__toBuffer(undefined, undefined, true);
}, /hashForSignature MUST pass false/) }, /hashForSignature MUST pass false/);
// assert hashForSignature does not pass false // assert hashForSignature does not pass false
assert.doesNotThrow(() => { assert.doesNotThrow(() => {
tx.hashForSignature(0, randScript, 1) tx.hashForSignature(0, randScript, 1);
}) });
}) });
fixtures.hashForSignature.forEach(f => { fixtures.hashForSignature.forEach(f => {
it('should return ' + f.hash + ' for ' + (f.description ? ('case "' + f.description + '"') : f.script), () => { it(
const tx = Transaction.fromHex(f.txHex) 'should return ' +
const script = bscript.fromASM(f.script) f.hash +
' for ' +
(f.description ? 'case "' + f.description + '"' : f.script),
() => {
const tx = Transaction.fromHex(f.txHex);
const script = bscript.fromASM(f.script);
assert.strictEqual(tx.hashForSignature(f.inIndex, script, f.type).toString('hex'), f.hash) assert.strictEqual(
}) tx.hashForSignature(f.inIndex, script, f.type).toString('hex'),
}) f.hash,
}) );
},
);
});
});
describe('hashForWitnessV0', () => { describe('hashForWitnessV0', () => {
fixtures.hashForWitnessV0.forEach(f => { fixtures.hashForWitnessV0.forEach(f => {
it('should return ' + f.hash + ' for ' + (f.description ? ('case "' + f.description + '"') : ''), () => { it(
const tx = Transaction.fromHex(f.txHex) 'should return ' +
const script = bscript.fromASM(f.script) f.hash +
' for ' +
(f.description ? 'case "' + f.description + '"' : ''),
() => {
const tx = Transaction.fromHex(f.txHex);
const script = bscript.fromASM(f.script);
assert.strictEqual(tx.hashForWitnessV0(f.inIndex, script, f.value, f.type).toString('hex'), f.hash) assert.strictEqual(
}) tx
}) .hashForWitnessV0(f.inIndex, script, f.value, f.type)
}) .toString('hex'),
f.hash,
);
},
);
});
});
describe('setWitness', () => { describe('setWitness', () => {
it('only accepts a a witness stack (Array of Buffers)', () => { it('only accepts a a witness stack (Array of Buffers)', () => {
assert.throws(() => { assert.throws(() => {
(new Transaction()).setWitness(0, 'foobar') (new Transaction().setWitness as any)(0, 'foobar');
}, /Expected property "1" of type \[Buffer], got String "foobar"/) }, /Expected property "1" of type \[Buffer], got String "foobar"/);
}) });
}) });
}) });

File diff suppressed because it is too large Load diff

5
test/ts-node-register.js Normal file
View file

@ -0,0 +1,5 @@
// This file is required to run mocha tests on the TS files directly
require("ts-node").register({
project: "test/tsconfig.json",
});

40
test/tsconfig.json Normal file
View file

@ -0,0 +1,40 @@
{
"compilerOptions": {
"target": "ES2017",
"module": "commonjs",
"outDir": "../",
"declaration": false,
"rootDir": "../",
"rootDirs": [
"../src",
"../types"
],
"types": [
"node",
"mocha"
],
"allowJs": false,
"resolveJsonModule": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"esModuleInterop": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"baseUrl": ".",
"paths": {
"../src/*": ["../ts_src/*"]
}
},
"include": [
"./**/*.ts"
],
"exclude": [
"../ts_src/**/*.ts"
]
}

View file

@ -1,38 +1,44 @@
const { describe, it } = require('mocha') import * as assert from 'assert';
const assert = require('assert') import { describe, it } from 'mocha';
const types = require('../src/types') import * as types from '../src/types';
const typeforce = require('typeforce') const typeforce = require('typeforce');
describe('types', () => { describe('types', () => {
describe('Buffer Hash160/Hash256', () => { describe('Buffer Hash160/Hash256', () => {
const buffer20byte = Buffer.alloc(20) const buffer20byte = Buffer.alloc(20);
const buffer32byte = Buffer.alloc(32) const buffer32byte = Buffer.alloc(32);
it('return true for valid size', () => { it('return true for valid size', () => {
assert(types.Hash160bit(buffer20byte)) assert(types.Hash160bit(buffer20byte));
assert(types.Hash256bit(buffer32byte)) assert(types.Hash256bit(buffer32byte));
}) });
it('return true for oneOf', () => { it('return true for oneOf', () => {
assert.doesNotThrow(() => { assert.doesNotThrow(() => {
typeforce(types.oneOf(types.Hash160bit, types.Hash256bit), buffer32byte) typeforce(
}) types.oneOf(types.Hash160bit, types.Hash256bit),
buffer32byte,
);
});
assert.doesNotThrow(() => { assert.doesNotThrow(() => {
typeforce(types.oneOf(types.Hash256bit, types.Hash160bit), buffer32byte) typeforce(
}) types.oneOf(types.Hash256bit, types.Hash160bit),
}) buffer32byte,
);
});
});
it('throws for invalid size', () => { it('throws for invalid size', () => {
assert.throws(() => { assert.throws(() => {
types.Hash160bit(buffer32byte) types.Hash160bit(buffer32byte);
}, /Expected Buffer\(Length: 20\), got Buffer\(Length: 32\)/) }, /Expected Buffer\(Length: 20\), got Buffer\(Length: 32\)/);
assert.throws(() => { assert.throws(() => {
types.Hash256bit(buffer20byte) types.Hash256bit(buffer20byte);
}, /Expected Buffer\(Length: 32\), got Buffer\(Length: 20\)/) }, /Expected Buffer\(Length: 32\), got Buffer\(Length: 20\)/);
}) });
}) });
describe('Satoshi', () => { describe('Satoshi', () => {
[ [
@ -41,11 +47,11 @@ describe('types', () => {
{ value: 1, result: true }, { value: 1, result: true },
{ value: 20999999 * 1e8, result: true }, { value: 20999999 * 1e8, result: true },
{ value: 21000000 * 1e8, result: true }, { value: 21000000 * 1e8, result: true },
{ value: 21000001 * 1e8, result: false } { value: 21000001 * 1e8, result: false },
].forEach(f => { ].forEach(f => {
it('returns ' + f.result + ' for valid for ' + f.value, () => { it('returns ' + f.result + ' for valid for ' + f.value, () => {
assert.strictEqual(types.Satoshi(f.value), f.result) assert.strictEqual(types.Satoshi(f.value), f.result);
}) });
}) });
}) });
}) });

View file

@ -148,7 +148,7 @@ export class Block {
return anyTxHasWitness(this.transactions!); return anyTxHasWitness(this.transactions!);
} }
byteLength(headersOnly: boolean): number { byteLength(headersOnly?: boolean): number {
if (headersOnly || !this.transactions) return 80; if (headersOnly || !this.transactions) return 80;
return ( return (
@ -174,7 +174,7 @@ export class Block {
} }
// TODO: buffer, offset compatibility // TODO: buffer, offset compatibility
toBuffer(headersOnly: boolean): Buffer { toBuffer(headersOnly?: boolean): Buffer {
const buffer: Buffer = Buffer.allocUnsafe(this.byteLength(headersOnly)); const buffer: Buffer = Buffer.allocUnsafe(this.byteLength(headersOnly));
let offset: number = 0; let offset: number = 0;
@ -213,7 +213,7 @@ export class Block {
return buffer; return buffer;
} }
toHex(headersOnly: boolean): string { toHex(headersOnly?: boolean): string {
return this.toBuffer(headersOnly).toString('hex'); return this.toBuffer(headersOnly).toString('hex');
} }

View file

@ -38,7 +38,7 @@ function classifyOutput(script: Buffer): string {
return types.NONSTANDARD; return types.NONSTANDARD;
} }
function classifyInput(script: Buffer, allowIncomplete: boolean): string { function classifyInput(script: Buffer, allowIncomplete?: boolean): string {
// XXX: optimization, below functions .decompile before use // XXX: optimization, below functions .decompile before use
const chunks = decompile(script); const chunks = decompile(script);
if (!chunks) throw new TypeError('Invalid script'); if (!chunks) throw new TypeError('Invalid script');
@ -51,7 +51,7 @@ function classifyInput(script: Buffer, allowIncomplete: boolean): string {
return types.NONSTANDARD; return types.NONSTANDARD;
} }
function classifyWitness(script: Buffer[], allowIncomplete: boolean): string { function classifyWitness(script: Buffer[], allowIncomplete?: boolean): string {
// XXX: optimization, below functions .decompile before use // XXX: optimization, below functions .decompile before use
const chunks = decompile(script); const chunks = decompile(script);
if (!chunks) throw new TypeError('Invalid script'); if (!chunks) throw new TypeError('Invalid script');

View file

@ -17,6 +17,12 @@ export { TransactionBuilder } from './transaction_builder';
export { BIP32Interface } from 'bip32'; export { BIP32Interface } from 'bip32';
export { ECPairInterface, Signer, SignerAsync } from './ecpair'; export { ECPairInterface, Signer, SignerAsync } from './ecpair';
export { Network } from './networks'; export { Network } from './networks';
export { Payment, PaymentOpts, Stack, StackElement } from './payments'; export {
Payment,
PaymentCreator,
PaymentOpts,
Stack,
StackElement,
} from './payments';
export { OpCode } from './script'; export { OpCode } from './script';
export { Input as TxInput, Output as TxOutput } from './transaction'; export { Input as TxInput, Output as TxOutput } from './transaction';

View file

@ -25,6 +25,8 @@ export interface Payment {
witness?: Buffer[]; witness?: Buffer[];
} }
export type PaymentCreator = (a: Payment, opts?: PaymentOpts) => Payment;
export type PaymentFunction = () => Payment; export type PaymentFunction = () => Payment;
export interface PaymentOpts { export interface PaymentOpts {

View file

@ -16,7 +16,7 @@
"no-bitwise": false, "no-bitwise": false,
"no-console": false, "no-console": false,
"no-empty": [true, "allow-empty-catch"], "no-empty": [true, "allow-empty-catch"],
"no-implicit-dependencies": true, "no-implicit-dependencies": [true, "dev"],
"no-return-await": true, "no-return-await": true,
"no-var-requires": false, "no-var-requires": false,
"no-unused-expression": false, "no-unused-expression": false,

6
types/block.d.ts vendored
View file

@ -16,12 +16,12 @@ export declare class Block {
getWitnessCommit(): Buffer | null; getWitnessCommit(): Buffer | null;
hasWitnessCommit(): boolean; hasWitnessCommit(): boolean;
hasWitness(): boolean; hasWitness(): boolean;
byteLength(headersOnly: boolean): number; byteLength(headersOnly?: boolean): number;
getHash(): Buffer; getHash(): Buffer;
getId(): string; getId(): string;
getUTCDate(): Date; getUTCDate(): Date;
toBuffer(headersOnly: boolean): Buffer; toBuffer(headersOnly?: boolean): Buffer;
toHex(headersOnly: boolean): string; toHex(headersOnly?: boolean): string;
checkTxRoots(): boolean; checkTxRoots(): boolean;
checkProofOfWork(): boolean; checkProofOfWork(): boolean;
private __checkMerkleRoot; private __checkMerkleRoot;

4
types/classify.d.ts vendored
View file

@ -11,6 +11,6 @@ declare const types: {
WITNESS_COMMITMENT: string; WITNESS_COMMITMENT: string;
}; };
declare function classifyOutput(script: Buffer): string; declare function classifyOutput(script: Buffer): string;
declare function classifyInput(script: Buffer, allowIncomplete: boolean): string; declare function classifyInput(script: Buffer, allowIncomplete?: boolean): string;
declare function classifyWitness(script: Buffer[], allowIncomplete: boolean): string; declare function classifyWitness(script: Buffer[], allowIncomplete?: boolean): string;
export { classifyInput as input, classifyOutput as output, classifyWitness as witness, types, }; export { classifyInput as input, classifyOutput as output, classifyWitness as witness, types, };

2
types/index.d.ts vendored
View file

@ -14,6 +14,6 @@ export { TransactionBuilder } from './transaction_builder';
export { BIP32Interface } from 'bip32'; export { BIP32Interface } from 'bip32';
export { ECPairInterface, Signer, SignerAsync } from './ecpair'; export { ECPairInterface, Signer, SignerAsync } from './ecpair';
export { Network } from './networks'; export { Network } from './networks';
export { Payment, PaymentOpts, Stack, StackElement } from './payments'; export { Payment, PaymentCreator, PaymentOpts, Stack, StackElement, } from './payments';
export { OpCode } from './script'; export { OpCode } from './script';
export { Input as TxInput, Output as TxOutput } from './transaction'; export { Input as TxInput, Output as TxOutput } from './transaction';

View file

@ -24,6 +24,7 @@ export interface Payment {
redeem?: Payment; redeem?: Payment;
witness?: Buffer[]; witness?: Buffer[];
} }
export declare type PaymentCreator = (a: Payment, opts?: PaymentOpts) => Payment;
export declare type PaymentFunction = () => Payment; export declare type PaymentFunction = () => Payment;
export interface PaymentOpts { export interface PaymentOpts {
validate?: boolean; validate?: boolean;