Merge pull request #1465 from bitcoinjs/tsTests

WIP: Move tests to TypeScript
This commit is contained in:
Jonathan Underwood 2019-09-11 10:37:58 +09:00 committed by GitHub
commit b3831c9616
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 5683 additions and 4475 deletions

3
.gitignore vendored
View file

@ -2,3 +2,6 @@ coverage
node_modules
.nyc_output
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": {
"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:jstests": "rimraf 'test/**/!(ts-node-register)*.js'",
"coverage-report": "npm run build && npm run nobuild:coverage-report",
"coverage-html": "npm run build && npm run nobuild:coverage-html",
"coverage": "npm run build && npm run nobuild:coverage",
@ -26,12 +28,13 @@
"gitdiff:ci": "npm run build && git diff --exit-code",
"integration": "npm run build && npm run nobuild:integration",
"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-html": "nyc report --reporter=html",
"nobuild:coverage": "nyc --check-coverage --branches 90 --functions 90 --lines 90 mocha",
"nobuild:integration": "mocha --timeout 50000 test/integration/",
"nobuild:unit": "mocha",
"prettier": "prettier 'ts_src/**/*.ts' --ignore-path ./.prettierignore",
"nobuild:coverage": "npm run build:tests && nyc --check-coverage --branches 90 --functions 90 --lines 90 mocha && npm run clean:jstests",
"nobuild:integration": "npm run mocha:ts -- --timeout 50000 'test/integration/*.ts'",
"nobuild:unit": "npm run mocha:ts -- 'test/*.ts'",
"prettier": "prettier 'ts_src/**/*.ts' 'test/**/*.ts' --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",
"unit": "npm run build && npm run nobuild:unit"
@ -63,7 +66,10 @@
"wif": "^2.0.1"
},
"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",
"bip68": "^1.0.3",
"bn.js": "^4.11.8",
@ -71,12 +77,13 @@
"dhttp": "^3.0.0",
"hoodwink": "^2.0.0",
"minimaldata": "^1.0.2",
"mocha": "^5.2.0",
"mocha": "^6.2.0",
"nyc": "^14.1.1",
"prettier": "1.16.4",
"proxyquire": "^2.0.1",
"regtest-client": "0.2.0",
"rimraf": "^2.6.3",
"ts-node": "^8.3.0",
"tslint": "^5.16.0",
"typescript": "3.2.2"
},

View file

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

148
test/address.spec.ts Normal file
View file

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

View file

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

251
test/bitcoin.core.spec.ts Normal file
View file

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

View file

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

172
test/block.spec.ts Normal file
View file

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

View file

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

49
test/bufferutils.spec.ts Normal file
View file

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

View file

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

180
test/classify.spec.ts Normal file
View file

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

View file

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

22
test/crypto.spec.ts Normal file
View file

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

View file

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

334
test/ecpair.spec.ts Normal file
View file

@ -0,0 +1,334 @@
import * as assert from 'assert';
import { beforeEach, describe, it } from 'mocha';
import * as proxyquire from 'proxyquire';
import { ECPair, ECPairInterface, networks as NETWORKS } from '..';
import * as fixtures from './fixtures/ecpair.json';
const hoodwink = require('hoodwink');
const tinysecp = require('tiny-secp256k1');
const NETWORKS_LIST = Object.values(NETWORKS);
const ZERO = Buffer.alloc(32, 0);
const ONE = Buffer.from(
'0000000000000000000000000000000000000000000000000000000000000001',
'hex',
);
const GROUP_ORDER = Buffer.from(
'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141',
'hex',
);
const GROUP_ORDER_LESS_1 = Buffer.from(
'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140',
'hex',
);
describe('ECPair', () => {
describe('getPublicKey', () => {
let keyPair: ECPairInterface;
beforeEach(() => {
keyPair = ECPair.fromPrivateKey(ONE);
});
it(
'calls pointFromScalar lazily',
hoodwink(() => {
assert.strictEqual((keyPair as any).__Q, undefined);
// .publicKey forces the memoization
assert.strictEqual(
keyPair.publicKey.toString('hex'),
'0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
);
assert.strictEqual(
(keyPair as any).__Q.toString('hex'),
'0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
);
}),
);
});
describe('fromPrivateKey', () => {
it('defaults to compressed', () => {
const keyPair = ECPair.fromPrivateKey(ONE);
assert.strictEqual(keyPair.compressed, true);
});
it('supports the uncompressed option', () => {
const keyPair = ECPair.fromPrivateKey(ONE, {
compressed: false,
});
assert.strictEqual(keyPair.compressed, false);
});
it('supports the network option', () => {
const keyPair = ECPair.fromPrivateKey(ONE, {
compressed: false,
network: NETWORKS.testnet,
});
assert.strictEqual(keyPair.network, NETWORKS.testnet);
});
fixtures.valid.forEach(f => {
it('derives public key for ' + f.WIF, () => {
const d = Buffer.from(f.d, 'hex');
const keyPair = ECPair.fromPrivateKey(d, {
compressed: f.compressed,
});
assert.strictEqual(keyPair.publicKey.toString('hex'), f.Q);
});
});
fixtures.invalid.fromPrivateKey.forEach(f => {
it('throws ' + f.exception, () => {
const d = Buffer.from(f.d, 'hex');
assert.throws(() => {
ECPair.fromPrivateKey(d, (f as any).options);
}, new RegExp(f.exception));
});
});
});
describe('fromPublicKey', () => {
fixtures.invalid.fromPublicKey.forEach(f => {
it('throws ' + f.exception, () => {
const Q = Buffer.from(f.Q, 'hex');
assert.throws(() => {
ECPair.fromPublicKey(Q, (f as any).options);
}, new RegExp(f.exception));
});
});
});
describe('fromWIF', () => {
fixtures.valid.forEach(f => {
it('imports ' + f.WIF + ' (' + f.network + ')', () => {
const network = (NETWORKS as any)[f.network];
const keyPair = ECPair.fromWIF(f.WIF, network);
assert.strictEqual(keyPair.privateKey!.toString('hex'), f.d);
assert.strictEqual(keyPair.compressed, f.compressed);
assert.strictEqual(keyPair.network, network);
});
});
fixtures.valid.forEach(f => {
it('imports ' + f.WIF + ' (via list of networks)', () => {
const keyPair = ECPair.fromWIF(f.WIF, NETWORKS_LIST);
assert.strictEqual(keyPair.privateKey!.toString('hex'), f.d);
assert.strictEqual(keyPair.compressed, f.compressed);
assert.strictEqual(keyPair.network, (NETWORKS as any)[f.network]);
});
});
fixtures.invalid.fromWIF.forEach(f => {
it('throws on ' + f.WIF, () => {
assert.throws(() => {
const networks = f.network
? (NETWORKS as any)[f.network]
: NETWORKS_LIST;
ECPair.fromWIF(f.WIF, networks);
}, new RegExp(f.exception));
});
});
});
describe('toWIF', () => {
fixtures.valid.forEach(f => {
it('exports ' + f.WIF, () => {
const keyPair = ECPair.fromWIF(f.WIF, NETWORKS_LIST);
const result = keyPair.toWIF();
assert.strictEqual(result, f.WIF);
});
});
});
describe('makeRandom', () => {
const d = Buffer.alloc(32, 4);
const exWIF = 'KwMWvwRJeFqxYyhZgNwYuYjbQENDAPAudQx5VEmKJrUZcq6aL2pv';
describe('uses randombytes RNG', () => {
it('generates a ECPair', () => {
const stub = {
randombytes: (): Buffer => {
return d;
},
};
const ProxiedECPair = proxyquire('../src/ecpair', stub);
const keyPair = ProxiedECPair.makeRandom();
assert.strictEqual(keyPair.toWIF(), exWIF);
});
});
it('allows a custom RNG to be used', () => {
const keyPair = ECPair.makeRandom({
rng: (size): Buffer => {
return d.slice(0, size);
},
});
assert.strictEqual(keyPair.toWIF(), exWIF);
});
it('retains the same defaults as ECPair constructor', () => {
const keyPair = ECPair.makeRandom();
assert.strictEqual(keyPair.compressed, true);
assert.strictEqual(keyPair.network, NETWORKS.bitcoin);
});
it('supports the options parameter', () => {
const keyPair = ECPair.makeRandom({
compressed: false,
network: NETWORKS.testnet,
});
assert.strictEqual(keyPair.compressed, false);
assert.strictEqual(keyPair.network, NETWORKS.testnet);
});
it('throws if d is bad length', () => {
function rng(): Buffer {
return Buffer.alloc(28);
}
assert.throws(() => {
ECPair.makeRandom({ rng });
}, /Expected Buffer\(Length: 32\), got Buffer\(Length: 28\)/);
});
it(
'loops until d is within interval [1, n) : 1',
hoodwink(function(this: any): void {
const rng = this.stub(() => {
if (rng.calls === 0) return ZERO; // 0
return ONE; // >0
}, 2);
ECPair.makeRandom({ rng });
}),
);
it(
'loops until d is within interval [1, n) : n - 1',
hoodwink(function(this: any): void {
const rng = this.stub(() => {
if (rng.calls === 0) return ZERO; // <1
if (rng.calls === 1) return GROUP_ORDER; // >n-1
return GROUP_ORDER_LESS_1; // n-1
}, 3);
ECPair.makeRandom({ rng });
}),
);
});
describe('.network', () => {
fixtures.valid.forEach(f => {
it('returns ' + f.network + ' for ' + f.WIF, () => {
const network = (NETWORKS as any)[f.network];
const keyPair = ECPair.fromWIF(f.WIF, NETWORKS_LIST);
assert.strictEqual(keyPair.network, network);
});
});
});
describe('tinysecp wrappers', () => {
let keyPair: ECPairInterface;
let hash: Buffer;
let signature: Buffer;
beforeEach(() => {
keyPair = ECPair.makeRandom();
hash = ZERO;
signature = Buffer.alloc(64, 1);
});
describe('signing', () => {
it(
'wraps tinysecp.sign',
hoodwink(function(this: any): void {
this.mock(
tinysecp,
'sign',
(h: any, d: any) => {
assert.strictEqual(h, hash);
assert.strictEqual(d, keyPair.privateKey);
return signature;
},
1,
);
assert.strictEqual(keyPair.sign(hash), signature);
}),
);
it('throws if no private key is found', () => {
delete (keyPair as any).__D;
assert.throws(() => {
keyPair.sign(hash);
}, /Missing private key/);
});
});
describe('verify', () => {
it(
'wraps tinysecp.verify',
hoodwink(function(this: any): void {
this.mock(
tinysecp,
'verify',
(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);
}),
);
});
});
describe('optional low R signing', () => {
const sig = Buffer.from(
'95a6619140fca3366f1d3b013b0367c4f86e39508a50fdce' +
'e5245fbb8bd60aa6086449e28cf15387cf9f85100bfd0838624ca96759e59f65c10a00' +
'16b86f5229',
'hex',
);
const sigLowR = Buffer.from(
'6a2660c226e8055afad317eeba918a304be79208d505' +
'3bc5ea4a5e4c5892b4a061c717c5284ae5202d721c0e49b4717b79966280906b1d3b52' +
'95d1fdde963c35',
'hex',
);
const lowRKeyPair = ECPair.fromWIF(
'L3nThUzbAwpUiBAjR5zCu66ybXSPMr2zZ3ikp' + 'ScpTPiYTxBynfZu',
);
const dataToSign = Buffer.from(
'b6c5c548a7f6164c8aa7af5350901626ebd69f9ae' + '2c1ecf8871f5088ec204cfe',
'hex',
);
it('signs with normal R by default', () => {
const signed = lowRKeyPair.sign(dataToSign);
assert.deepStrictEqual(sig, signed);
});
it('signs with low R when true is passed', () => {
const signed = lowRKeyPair.sign(dataToSign, true);
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')
const assert = require('assert')
const bitcoin = require('../../')
const dhttp = require('./_regtest').dhttp
const TESTNET = bitcoin.networks.testnet
import * as assert from 'assert';
import { describe, it } from 'mocha';
import * as bitcoin from '../..';
import { regtestUtils } from './_regtest';
const dhttp = regtestUtils.dhttp;
const TESTNET = bitcoin.networks.testnet;
describe('bitcoinjs-lib (addresses)', () => {
it('can generate a random address [and support the retrieval of transactions for that address (via 3PBP)', async () => {
const keyPair = bitcoin.ECPair.makeRandom()
const { address } = bitcoin.payments.p2pkh({ pubkey: keyPair.publicKey })
const keyPair = bitcoin.ECPair.makeRandom();
const { address } = bitcoin.payments.p2pkh({ pubkey: keyPair.publicKey });
// bitcoin P2PKH addresses start with a '1'
assert.strictEqual(address.startsWith('1'), true)
assert.strictEqual(address!.startsWith('1'), true);
const result = await dhttp({
method: 'GET',
url: 'https://blockchain.info/rawaddr/' + address
})
url: 'https://blockchain.info/rawaddr/' + address,
});
// random private keys [probably!] have no transactions
assert.strictEqual(result.n_tx, 0)
assert.strictEqual(result.total_received, 0)
assert.strictEqual(result.total_sent, 0)
})
assert.strictEqual((result as any).n_tx, 0);
assert.strictEqual((result as any).total_received, 0);
assert.strictEqual((result as any).total_sent, 0);
});
it('can import an address via WIF', () => {
const keyPair = bitcoin.ECPair.fromWIF('KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn')
const { address } = bitcoin.payments.p2pkh({ pubkey: keyPair.publicKey })
const keyPair = bitcoin.ECPair.fromWIF(
'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', () => {
const pubkeys = [
'026477115981fe981a6918a6297d9803c4dc04f328f22041bedff886bbc2962e01',
'02c96db2302d19b43d4c69368babace7854cc84eb9e061cde51cfa77ca4a22b8b9',
'03c6103b3b83e4a24a0e33a4df246ef11772f9992663db0c35759a5e2ebf68d8e9'
].map((hex) => Buffer.from(hex, 'hex'))
'03c6103b3b83e4a24a0e33a4df246ef11772f9992663db0c35759a5e2ebf68d8e9',
].map(hex => Buffer.from(hex, 'hex'));
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', () => {
const keyPair = bitcoin.ECPair.fromWIF('KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn')
const { address } = bitcoin.payments.p2wpkh({ pubkey: keyPair.publicKey })
const keyPair = bitcoin.ECPair.fromWIF(
'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)', () => {
const keyPair = bitcoin.ECPair.fromWIF('KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn')
const keyPair = bitcoin.ECPair.fromWIF(
'KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn',
);
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', () => {
const pubkeys = [
'026477115981fe981a6918a6297d9803c4dc04f328f22041bedff886bbc2962e01',
'02c96db2302d19b43d4c69368babace7854cc84eb9e061cde51cfa77ca4a22b8b9',
'023e4740d0ba639e28963f3476157b7cf2fb7c6fdf4254f97099cf8670b505ea59',
'03c6103b3b83e4a24a0e33a4df246ef11772f9992663db0c35759a5e2ebf68d8e9'
].map((hex) => Buffer.from(hex, 'hex'))
'03c6103b3b83e4a24a0e33a4df246ef11772f9992663db0c35759a5e2ebf68d8e9',
].map(hex => Buffer.from(hex, 'hex'));
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', () => {
const pubkeys = [
'026477115981fe981a6918a6297d9803c4dc04f328f22041bedff886bbc2962e01',
'02c96db2302d19b43d4c69368babace7854cc84eb9e061cde51cfa77ca4a22b8b9'
].map((hex) => Buffer.from(hex, 'hex'))
'02c96db2302d19b43d4c69368babace7854cc84eb9e061cde51cfa77ca4a22b8b9',
].map(hex => Buffer.from(hex, 'hex'));
const { address } = bitcoin.payments.p2sh({
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
it('can generate a Testnet address', () => {
const keyPair = bitcoin.ECPair.makeRandom({ network: TESTNET })
const { address } = bitcoin.payments.p2pkh({ pubkey: keyPair.publicKey, network: TESTNET })
const keyPair = bitcoin.ECPair.makeRandom({ network: TESTNET });
const { address } = bitcoin.payments.p2pkh({
pubkey: keyPair.publicKey,
network: TESTNET,
});
// 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', () => {
// WARNING: although possible, bitcoinjs is NOT necessarily compatible with Litecoin
const LITECOIN = {
messagePrefix: '\x19Litecoin Signed Message:\n',
bech32: 'ltc',
bip32: {
public: 0x019da462,
private: 0x019d9cfe
private: 0x019d9cfe,
},
pubKeyHash: 0x30,
scriptHash: 0x32,
wif: 0xb0
}
wif: 0xb0,
};
const keyPair = bitcoin.ECPair.makeRandom({ network: LITECOIN })
const { address } = bitcoin.payments.p2pkh({ pubkey: keyPair.publicKey, network: LITECOIN })
const keyPair = bitcoin.ECPair.makeRandom({ 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 +0,0 @@
const { describe, it } = require('mocha')
const assert = require('assert')
const bip32 = require('bip32')
const bip39 = require('bip39')
const bitcoin = require('../../')
function getAddress (node, network) {
return bitcoin.payments.p2pkh({ pubkey: node.publicKey, network }).address
}
describe('bitcoinjs-lib (BIP32)', () => {
it('can import a BIP32 testnet xpriv and export to WIF', () => {
const xpriv = 'tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK'
const node = bip32.fromBase58(xpriv, bitcoin.networks.testnet)
assert.strictEqual(node.toWIF(), 'cQfoY67cetFNunmBUX5wJiw3VNoYx3gG9U9CAofKE6BfiV1fSRw7')
})
it('can export a BIP32 xpriv, then import it', () => {
const mnemonic = 'praise you muffin lion enable neck grocery crumble super myself license ghost'
const seed = bip39.mnemonicToSeed(mnemonic)
const node = bip32.fromSeed(seed)
const string = node.toBase58()
const restored = bip32.fromBase58(string)
assert.strictEqual(getAddress(node), getAddress(restored)) // same public key
assert.strictEqual(node.toWIF(), restored.toWIF()) // same private key
})
it('can export a BIP32 xpub', () => {
const mnemonic = 'praise you muffin lion enable neck grocery crumble super myself license ghost'
const seed = bip39.mnemonicToSeed(mnemonic)
const node = bip32.fromSeed(seed)
const string = node.neutered().toBase58()
assert.strictEqual(string, 'xpub661MyMwAqRbcGhVeaVfEBA25e3cP9DsJQZoE8iep5fZSxy3TnPBNBgWnMZx56oreNc48ZoTkQfatNJ9VWnQ7ZcLZcVStpaXLTeG8bGrzX3n')
})
it('can create a BIP32, bitcoin, account 0, external address', () => {
const path = "m/0'/0/0"
const root = bip32.fromSeed(Buffer.from('dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd', 'hex'))
const child1 = root.derivePath(path)
// option 2, manually
const child1b = root.deriveHardened(0)
.derive(0)
.derive(0)
assert.strictEqual(getAddress(child1), '1JHyB1oPXufr4FXkfitsjgNB5yRY9jAaa7')
assert.strictEqual(getAddress(child1b), '1JHyB1oPXufr4FXkfitsjgNB5yRY9jAaa7')
})
it('can create a BIP44, bitcoin, account 0, external address', () => {
const root = bip32.fromSeed(Buffer.from('dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd', 'hex'))
const child1 = root.derivePath("m/44'/0'/0'/0/0")
// option 2, manually
const child1b = root.deriveHardened(44)
.deriveHardened(0)
.deriveHardened(0)
.derive(0)
.derive(0)
assert.strictEqual(getAddress(child1), '12Tyvr1U8A3ped6zwMEU5M8cx3G38sP5Au')
assert.strictEqual(getAddress(child1b), '12Tyvr1U8A3ped6zwMEU5M8cx3G38sP5Au')
})
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 seed = bip39.mnemonicToSeed(mnemonic)
const root = bip32.fromSeed(seed)
const path = "m/49'/1'/0'/0/0"
const child = root.derivePath(path)
const { address } = bitcoin.payments.p2sh({
redeem: bitcoin.payments.p2wpkh({ pubkey: child.publicKey, network: bitcoin.networks.testnet }),
network: bitcoin.networks.testnet
})
assert.strictEqual(address, '2Mww8dCYPUpKHofjgcXcBCEGmniw9CoaiD2')
})
it('can use BIP39 to generate BIP32 addresses', () => {
// var mnemonic = bip39.generateMnemonic()
const mnemonic = 'praise you muffin lion enable neck grocery crumble super myself license ghost'
assert(bip39.validateMnemonic(mnemonic))
const seed = bip39.mnemonicToSeed(mnemonic)
const root = bip32.fromSeed(seed)
// receive addresses
assert.strictEqual(getAddress(root.derivePath("m/0'/0/0")), '1AVQHbGuES57wD68AJi7Gcobc3RZrfYWTC')
assert.strictEqual(getAddress(root.derivePath("m/0'/0/1")), '1Ad6nsmqDzbQo5a822C9bkvAfrYv9mc1JL')
// change addresses
assert.strictEqual(getAddress(root.derivePath("m/0'/1/0")), '1349KVc5NgedaK7DvuD4xDFxL86QN1Hvdn')
assert.strictEqual(getAddress(root.derivePath("m/0'/1/1")), '1EAvj4edpsWcSer3duybAd4KiR4bCJW5J6')
})
})

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,27 +1,41 @@
const { describe, it, before } = require('mocha')
const assert = require('assert')
const bitcoin = require('../../')
const regtestUtils = require('./_regtest')
const regtest = regtestUtils.network
const bip68 = require('bip68')
import * as assert from 'assert';
import { before, describe, it } from 'mocha';
import * as bitcoin from '../..';
import { regtestUtils } from './_regtest';
const regtest = regtestUtils.network;
const bip68 = require('bip68');
const alice = bitcoin.ECPair.fromWIF('cScfkGjbzzoeewVWmU2hYPUHeVGJRDdFt7WhmrVVGkxpmPP8BHWe', regtest)
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
const alice = bitcoin.ECPair.fromWIF(
'cScfkGjbzzoeewVWmU2hYPUHeVGJRDdFt7WhmrVVGkxpmPP8BHWe',
regtest,
);
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)', () => {
// force update MTP
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
function csvCheckSigOutput (_alice, _bob, sequence) {
return bitcoin.script.fromASM(`
function csvCheckSigOutput(_alice: keyPair, _bob: keyPair, sequence: number) {
return bitcoin.script.fromASM(
`
OP_IF
${bitcoin.script.number.encode(sequence).toString('hex')}
OP_CHECKSEQUENCEVERIFY
@ -32,7 +46,10 @@ describe('bitcoinjs-lib (transactions w/ CSV)', () => {
OP_ENDIF
${_alice.publicKey.toString('hex')}
OP_CHECKSIG
`.trim().replace(/\s+/g, ' '))
`
.trim()
.replace(/\s+/g, ' '),
);
}
// 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
// Note: bitcoinjs-lib will not offer specific support for problems with
// advanced script usages such as below. Use at your own risk.
function complexCsvOutput (_alice, _bob, _charles, _dave, sequence1, sequence2) {
return bitcoin.script.fromASM(`
function complexCsvOutput(
_alice: keyPair,
_bob: keyPair,
_charles: keyPair,
_dave: keyPair,
sequence1: number,
sequence2: number,
) {
return bitcoin.script.fromASM(
`
OP_IF
OP_IF
OP_2
@ -66,253 +91,294 @@ describe('bitcoinjs-lib (transactions w/ CSV)', () => {
${_alice.publicKey.toString('hex')}
OP_CHECKSIG
OP_ENDIF
`.trim().replace(/\s+/g, ' '))
`
.trim()
.replace(/\s+/g, ' '),
);
}
// 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 () => {
// 5 blocks from now
const sequence = bip68.encode({ blocks: 5 })
const sequence = bip68.encode({ blocks: 5 });
const p2sh = bitcoin.payments.p2sh({
redeem: {
output: csvCheckSigOutput(alice, bob, sequence)
output: csvCheckSigOutput(alice, bob, sequence),
},
network: regtest
})
network: regtest,
});
// 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)
txb.addInput(unspent.txId, unspent.vout, sequence)
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4)
const txb = new bitcoin.TransactionBuilder(regtest);
txb.addInput(unspent.txId, unspent.vout, sequence);
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4);
// {Alice's signature} OP_TRUE
const tx = txb.buildIncomplete()
const signatureHash = tx.hashForSignature(0, p2sh.redeem.output, hashType)
const tx = txb.buildIncomplete();
const signatureHash = tx.hashForSignature(
0,
p2sh.redeem!.output!,
hashType,
);
const redeemScriptSig = bitcoin.payments.p2sh({
network: regtest,
redeem: {
network: regtest,
output: p2sh.redeem.output,
output: p2sh.redeem!.output,
input: bitcoin.script.compile([
bitcoin.script.signature.encode(alice.sign(signatureHash), hashType),
bitcoin.opcodes.OP_TRUE
])
}
}).input
tx.setInputScript(0, redeemScriptSig)
bitcoin.opcodes.OP_TRUE,
]),
},
}).input;
tx.setInputScript(0, redeemScriptSig!);
// TODO: test that it failures _prior_ to expiry, unfortunately, race conditions when run concurrently
// ...
// into the future!
await regtestUtils.mine(10)
await regtestUtils.mine(10);
await regtestUtils.broadcast(tx.toHex())
await regtestUtils.broadcast(tx.toHex());
await regtestUtils.verify({
txId: tx.getId(),
address: regtestUtils.RANDOM_ADDRESS,
vout: 0,
value: 7e4
})
})
value: 7e4,
});
});
// 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 () => {
// two hours after confirmation
const sequence = bip68.encode({ seconds: 7168 })
const sequence = bip68.encode({ seconds: 7168 });
const p2sh = bitcoin.payments.p2sh({
network: regtest,
redeem: {
output: csvCheckSigOutput(alice, bob, sequence)
}
})
output: csvCheckSigOutput(alice, bob, sequence),
},
});
// 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)
txb.addInput(unspent.txId, unspent.vout, sequence)
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 1e4)
const txb = new bitcoin.TransactionBuilder(regtest);
txb.addInput(unspent.txId, unspent.vout, sequence);
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 1e4);
// {Alice's signature} OP_TRUE
const tx = txb.buildIncomplete()
const signatureHash = tx.hashForSignature(0, p2sh.redeem.output, hashType)
const tx = txb.buildIncomplete();
const signatureHash = tx.hashForSignature(
0,
p2sh.redeem!.output!,
hashType,
);
const redeemScriptSig = bitcoin.payments.p2sh({
network: regtest,
redeem: {
network: regtest,
output: p2sh.redeem.output,
output: p2sh.redeem!.output,
input: bitcoin.script.compile([
bitcoin.script.signature.encode(alice.sign(signatureHash), hashType),
bitcoin.script.signature.encode(bob.sign(signatureHash), hashType),
bitcoin.opcodes.OP_TRUE
])
}
}).input
tx.setInputScript(0, redeemScriptSig)
bitcoin.opcodes.OP_TRUE,
]),
},
}).input;
tx.setInputScript(0, redeemScriptSig!);
await regtestUtils.broadcast(tx.toHex()).catch(err => {
assert.throws(() => {
if (err) throw err
}, /Error: non-BIP68-final \(code 64\)/)
})
})
if (err) throw err;
}, /Error: non-BIP68-final \(code 64\)/);
});
});
// 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 () => {
const height = await regtestUtils.height()
// 2 blocks from now
const sequence1 = bip68.encode({ blocks: 2 })
const sequence1 = bip68.encode({ blocks: 2 });
// 5 blocks from now
const sequence2 = bip68.encode({ blocks: 5 })
const sequence2 = bip68.encode({ blocks: 5 });
const p2sh = bitcoin.payments.p2sh({
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
const unspent = await regtestUtils.faucet(p2sh.address, 1e5)
const unspent = await regtestUtils.faucet(p2sh.address!, 1e5);
const txb = new bitcoin.TransactionBuilder(regtest)
txb.addInput(unspent.txId, unspent.vout)
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4)
const txb = new bitcoin.TransactionBuilder(regtest);
txb.addInput(unspent.txId, unspent.vout);
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4);
// OP_0 {Bob sig} {Charles sig} OP_TRUE OP_TRUE
const tx = txb.buildIncomplete()
const signatureHash = tx.hashForSignature(0, p2sh.redeem.output, hashType)
const tx = txb.buildIncomplete();
const signatureHash = tx.hashForSignature(
0,
p2sh.redeem!.output!,
hashType,
);
const redeemScriptSig = bitcoin.payments.p2sh({
network: regtest,
redeem: {
network: regtest,
output: p2sh.redeem.output,
output: p2sh.redeem!.output,
input: bitcoin.script.compile([
bitcoin.opcodes.OP_0,
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
])
}
}).input
tx.setInputScript(0, redeemScriptSig)
bitcoin.opcodes.OP_TRUE,
]),
},
}).input;
tx.setInputScript(0, redeemScriptSig!);
await regtestUtils.broadcast(tx.toHex())
await regtestUtils.broadcast(tx.toHex());
await regtestUtils.verify({
txId: tx.getId(),
address: regtestUtils.RANDOM_ADDRESS,
vout: 0,
value: 7e4
})
})
value: 7e4,
});
});
// 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 () => {
const height = await regtestUtils.height()
// 2 blocks from now
const sequence1 = bip68.encode({ blocks: 2 })
const sequence1 = bip68.encode({ blocks: 2 });
// 5 blocks from now
const sequence2 = bip68.encode({ blocks: 5 })
const sequence2 = bip68.encode({ blocks: 5 });
const p2sh = bitcoin.payments.p2sh({
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
const unspent = await regtestUtils.faucet(p2sh.address, 1e5)
const unspent = await regtestUtils.faucet(p2sh.address!, 1e5);
const txb = new bitcoin.TransactionBuilder(regtest)
txb.addInput(unspent.txId, unspent.vout, sequence1) // Set sequence1 for input
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4)
const txb = new bitcoin.TransactionBuilder(regtest);
txb.addInput(unspent.txId, unspent.vout, sequence1); // Set sequence1 for input
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4);
// OP_0 {Bob sig} {Alice mediator sig} OP_FALSE OP_TRUE
const tx = txb.buildIncomplete()
const signatureHash = tx.hashForSignature(0, p2sh.redeem.output, hashType)
const tx = txb.buildIncomplete();
const signatureHash = tx.hashForSignature(
0,
p2sh.redeem!.output!,
hashType,
);
const redeemScriptSig = bitcoin.payments.p2sh({
network: regtest,
redeem: {
network: regtest,
output: p2sh.redeem.output,
output: p2sh.redeem!.output,
input: bitcoin.script.compile([
bitcoin.opcodes.OP_0,
bitcoin.script.signature.encode(bob.sign(signatureHash), hashType),
bitcoin.script.signature.encode(alice.sign(signatureHash), hashType),
bitcoin.opcodes.OP_0,
bitcoin.opcodes.OP_TRUE
])
}
}).input
tx.setInputScript(0, redeemScriptSig)
bitcoin.opcodes.OP_TRUE,
]),
},
}).input;
tx.setInputScript(0, redeemScriptSig!);
// Wait 2 blocks
await regtestUtils.mine(2)
await regtestUtils.mine(2);
await regtestUtils.broadcast(tx.toHex())
await regtestUtils.broadcast(tx.toHex());
await regtestUtils.verify({
txId: tx.getId(),
address: regtestUtils.RANDOM_ADDRESS,
vout: 0,
value: 7e4
})
})
value: 7e4,
});
});
// 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 () => {
const height = await regtestUtils.height()
// 2 blocks from now
const sequence1 = bip68.encode({ blocks: 2 })
const sequence1 = bip68.encode({ blocks: 2 });
// 5 blocks from now
const sequence2 = bip68.encode({ blocks: 5 })
const sequence2 = bip68.encode({ blocks: 5 });
const p2sh = bitcoin.payments.p2sh({
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
const unspent = await regtestUtils.faucet(p2sh.address, 1e5)
const unspent = await regtestUtils.faucet(p2sh.address!, 1e5);
const txb = new bitcoin.TransactionBuilder(regtest)
txb.addInput(unspent.txId, unspent.vout, sequence2) // Set sequence2 for input
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4)
const txb = new bitcoin.TransactionBuilder(regtest);
txb.addInput(unspent.txId, unspent.vout, sequence2); // Set sequence2 for input
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4);
// {Alice mediator sig} OP_FALSE
const tx = txb.buildIncomplete()
const signatureHash = tx.hashForSignature(0, p2sh.redeem.output, hashType)
const tx = txb.buildIncomplete();
const signatureHash = tx.hashForSignature(
0,
p2sh.redeem!.output!,
hashType,
);
const redeemScriptSig = bitcoin.payments.p2sh({
network: regtest,
redeem: {
network: regtest,
output: p2sh.redeem.output,
output: p2sh.redeem!.output,
input: bitcoin.script.compile([
bitcoin.script.signature.encode(alice.sign(signatureHash), hashType),
bitcoin.opcodes.OP_0
])
}
}).input
tx.setInputScript(0, redeemScriptSig)
bitcoin.opcodes.OP_0,
]),
},
}).input;
tx.setInputScript(0, redeemScriptSig!);
// Wait 5 blocks
await regtestUtils.mine(5)
await regtestUtils.mine(5);
await regtestUtils.broadcast(tx.toHex())
await regtestUtils.broadcast(tx.toHex());
await regtestUtils.verify({
txId: tx.getId(),
address: regtestUtils.RANDOM_ADDRESS,
vout: 0,
value: 7e4
})
})
})
value: 7e4,
});
});
});

View file

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

View file

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

View file

@ -1,9 +1,9 @@
const { describe, it } = require('mocha');
const assert = require('assert');
const bitcoin = require('../../');
const bip32 = require('bip32');
import * as assert from 'assert';
import { describe, it } from 'mocha';
import * as bitcoin from '../..';
import { regtestUtils } from './_regtest';
import * as bip32 from 'bip32';
const rng = require('randombytes');
const regtestUtils = require('./_regtest');
const regtest = regtestUtils.network;
// 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 })
.addInput(inputData1)
.addOutput({
script: embed.output,
script: embed.output!,
value: 1000,
})
.addOutput({
@ -350,7 +350,12 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
// For learning purposes, ignore this test.
// REPEATING ABOVE BUT WITH nonWitnessUtxo by passing false to getInputData
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 })
.addInput(inputData)
.addOutput({
@ -486,7 +491,12 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
// For learning purposes, ignore this test.
// REPEATING ABOVE BUT WITH nonWitnessUtxo by passing false to getInputData
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 })
.addInput(inputData)
.addOutput({
@ -528,9 +538,9 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
masterFingerprint,
path,
pubkey,
}
]
}
},
],
};
const p2wpkh = createPayment('p2wpkh', [childNode]);
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)
Object.assign(inputData, updateData)
Object.assign(inputData, updateData);
const psbt = new bitcoin.Psbt({ network: regtest })
.addInput(inputData)
@ -551,7 +561,10 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
.signInputHD(0, hdRoot); // must sign with root!!!
assert.strictEqual(psbt.validateSignaturesOfInput(0), true);
assert.strictEqual(psbt.validateSignaturesOfInput(0, childNode.publicKey), true);
assert.strictEqual(
psbt.validateSignaturesOfInput(0, childNode.publicKey),
true,
);
psbt.finalizeAllInputs();
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;
const splitType = _type.split('-').reverse();
const isMultisig = splitType[0].slice(0, 4) === 'p2ms';
const keys = myKeys || [];
let m;
let m: number | undefined;
if (isMultisig) {
const match = splitType[0].match(/^p2ms\((\d+) of (\d+)\)$/);
m = parseInt(match[1]);
let n = parseInt(match[2]);
m = parseInt(match![1]);
let n = parseInt(match![2]);
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) {
keys.push(bitcoin.ECPair.makeRandom({ network }));
@ -588,7 +601,7 @@ function createPayment(_type, myKeys, network) {
}
if (!myKeys) keys.push(bitcoin.ECPair.makeRandom({ network }));
let payment;
let payment: any;
splitType.forEach(type => {
if (type.slice(0, 4) === 'p2ms') {
payment = bitcoin.payments.p2ms({
@ -597,12 +610,12 @@ function createPayment(_type, myKeys, network) {
network,
});
} else if (['p2sh', 'p2wsh'].indexOf(type) > -1) {
payment = bitcoin.payments[type]({
payment = (bitcoin.payments as any)[type]({
redeem: payment,
network,
});
} else {
payment = bitcoin.payments[type]({
payment = (bitcoin.payments as any)[type]({
pubkey: keys[0].publicKey,
network,
});
@ -615,13 +628,18 @@ function createPayment(_type, myKeys, network) {
};
}
function getWitnessUtxo(out) {
function getWitnessUtxo(out: any) {
delete out.address;
out.script = Buffer.from(out.script, 'hex');
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 utx = await regtestUtils.fetch(unspent.txId);
// 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.
const witnessUtxo = getWitnessUtxo(utx.outs[unspent.vout]);
const mixin = isSegwit ? { witnessUtxo } : { nonWitnessUtxo };
const mixin2 = {};
const mixin2: any = {};
switch (redeemType) {
case 'p2sh':
mixin2.redeemScript = payment.redeem.output;

View file

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

View file

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

View file

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

113
test/payments.spec.ts Normal file
View file

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

View file

@ -1,688 +0,0 @@
const { describe, it } = require('mocha')
const assert = require('assert')
const bip32 = require('bip32')
const ECPair = require('../src/ecpair')
const Psbt = require('..').Psbt
const NETWORKS = require('../src/networks')
const initBuffers = object => JSON.parse(JSON.stringify(object), (key, value) => {
const regex = new RegExp(/^Buffer.from\(['"](.*)['"], ['"](.*)['"]\)$/)
const result = regex.exec(value)
if (!result) return value
const data = result[1]
const encoding = result[2]
return Buffer.from(data, encoding)
})
const fixtures = initBuffers(require('./fixtures/psbt'))
const upperCaseFirstLetter = str => str.replace(/^./, s => s.toUpperCase())
const b = hex => Buffer.from(hex, 'hex');
describe(`Psbt`, () => {
describe('BIP174 Test Vectors', () => {
fixtures.bip174.invalid.forEach(f => {
it(`Invalid: ${f.description}`, () => {
assert.throws(() => {
Psbt.fromBase64(f.psbt)
}, new RegExp(f.errorMessage))
})
})
fixtures.bip174.valid.forEach(f => {
it(`Valid: ${f.description}`, () => {
assert.doesNotThrow(() => {
Psbt.fromBase64(f.psbt)
})
})
})
fixtures.bip174.failSignChecks.forEach(f => {
const keyPair = ECPair.makeRandom()
it(`Fails Signer checks: ${f.description}`, () => {
const psbt = Psbt.fromBase64(f.psbt)
assert.throws(() => {
psbt.signInput(f.inputToCheck, keyPair)
}, new RegExp(f.errorMessage))
})
})
fixtures.bip174.creator.forEach(f => {
it('Creates expected PSBT', () => {
const psbt = new Psbt()
for (const input of f.inputs) {
psbt.addInput(input)
}
for (const output of f.outputs) {
const script = Buffer.from(output.script, 'hex');
psbt.addOutput({...output, script})
}
assert.strictEqual(psbt.toBase64(), f.result)
})
})
fixtures.bip174.updater.forEach(f => {
it('Updates PSBT to the expected result', () => {
const psbt = Psbt.fromBase64(f.psbt)
for (const inputOrOutput of ['input', 'output']) {
const fixtureData = f[`${inputOrOutput}Data`]
if (fixtureData) {
for (const [i, data] of fixtureData.entries()) {
const txt = upperCaseFirstLetter(inputOrOutput)
psbt[`update${txt}`](i, data)
}
}
}
assert.strictEqual(psbt.toBase64(), f.result)
})
})
fixtures.bip174.signer.forEach(f => {
it('Signs PSBT to the expected result', () => {
const psbt = Psbt.fromBase64(f.psbt)
f.keys.forEach(({inputToSign, WIF}) => {
const keyPair = ECPair.fromWIF(WIF, NETWORKS.testnet);
psbt.signInput(inputToSign, keyPair);
})
assert.strictEqual(psbt.toBase64(), f.result)
})
})
fixtures.bip174.combiner.forEach(f => {
it('Combines two PSBTs to the expected result', () => {
const psbts = f.psbts.map(psbt => Psbt.fromBase64(psbt))
psbts[0].combine(psbts[1])
// Produces a different Base64 string due to implemetation specific key-value ordering.
// That means this test will fail:
// assert.strictEqual(psbts[0].toBase64(), f.result)
// However, if we compare the actual PSBT properties we can see they are logically identical:
assert.deepStrictEqual(psbts[0], Psbt.fromBase64(f.result))
})
})
fixtures.bip174.finalizer.forEach(f => {
it("Finalizes inputs and gives the expected PSBT", () => {
const psbt = Psbt.fromBase64(f.psbt)
psbt.finalizeAllInputs()
assert.strictEqual(psbt.toBase64(), f.result)
})
})
fixtures.bip174.extractor.forEach(f => {
it('Extracts the expected transaction from a PSBT', () => {
const psbt1 = Psbt.fromBase64(f.psbt)
const transaction1 = psbt1.extractTransaction(true).toHex()
const psbt2 = Psbt.fromBase64(f.psbt)
const transaction2 = psbt2.extractTransaction().toHex()
assert.strictEqual(transaction1, transaction2)
assert.strictEqual(transaction1, f.transaction)
const psbt3 = Psbt.fromBase64(f.psbt)
delete psbt3.data.inputs[0].finalScriptSig
delete psbt3.data.inputs[0].finalScriptWitness
assert.throws(() => {
psbt3.extractTransaction()
}, new RegExp('Not finalized'))
const psbt4 = Psbt.fromBase64(f.psbt)
psbt4.setMaximumFeeRate(1)
assert.throws(() => {
psbt4.extractTransaction()
}, new RegExp('Warning: You are paying around [\\d.]+ in fees'))
const psbt5 = Psbt.fromBase64(f.psbt)
psbt5.extractTransaction(true)
const fr1 = psbt5.getFeeRate()
const fr2 = psbt5.getFeeRate()
assert.strictEqual(fr1, fr2)
const psbt6 = Psbt.fromBase64(f.psbt)
const f1 = psbt6.getFee()
const f2 = psbt6.getFee()
assert.strictEqual(f1, f2)
})
})
})
describe('signInputAsync', () => {
fixtures.signInput.checks.forEach(f => {
it(f.description, async () => {
if (f.shouldSign) {
const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt)
assert.doesNotReject(async () => {
await psbtThatShouldsign.signInputAsync(
f.shouldSign.inputToCheck,
ECPair.fromWIF(f.shouldSign.WIF),
f.shouldSign.sighashTypes || undefined,
)
})
}
if (f.shouldThrow) {
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt)
assert.rejects(async () => {
await psbtThatShouldThrow.signInputAsync(
f.shouldThrow.inputToCheck,
ECPair.fromWIF(f.shouldThrow.WIF),
f.shouldThrow.sighashTypes || undefined,
)
}, new RegExp(f.shouldThrow.errorMessage))
assert.rejects(async () => {
await psbtThatShouldThrow.signInputAsync(
f.shouldThrow.inputToCheck,
)
}, new RegExp('Need Signer to sign input'))
}
})
})
})
describe('signInput', () => {
fixtures.signInput.checks.forEach(f => {
it(f.description, () => {
if (f.shouldSign) {
const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt)
assert.doesNotThrow(() => {
psbtThatShouldsign.signInput(
f.shouldSign.inputToCheck,
ECPair.fromWIF(f.shouldSign.WIF),
f.shouldSign.sighashTypes || undefined,
)
})
}
if (f.shouldThrow) {
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt)
assert.throws(() => {
psbtThatShouldThrow.signInput(
f.shouldThrow.inputToCheck,
ECPair.fromWIF(f.shouldThrow.WIF),
f.shouldThrow.sighashTypes || undefined,
)
}, new RegExp(f.shouldThrow.errorMessage))
assert.throws(() => {
psbtThatShouldThrow.signInput(
f.shouldThrow.inputToCheck,
)
}, new RegExp('Need Signer to sign input'))
}
})
})
})
describe('signAllInputsAsync', () => {
fixtures.signInput.checks.forEach(f => {
if (f.description === 'checks the input exists') return
it(f.description, async () => {
if (f.shouldSign) {
const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt)
assert.doesNotReject(async () => {
await psbtThatShouldsign.signAllInputsAsync(
ECPair.fromWIF(f.shouldSign.WIF),
f.shouldSign.sighashTypes || undefined,
)
})
}
if (f.shouldThrow) {
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt)
assert.rejects(async () => {
await psbtThatShouldThrow.signAllInputsAsync(
ECPair.fromWIF(f.shouldThrow.WIF),
f.shouldThrow.sighashTypes || undefined,
)
}, new RegExp('No inputs were signed'))
assert.rejects(async () => {
await psbtThatShouldThrow.signAllInputsAsync()
}, new RegExp('Need Signer to sign input'))
}
})
})
})
describe('signAllInputs', () => {
fixtures.signInput.checks.forEach(f => {
if (f.description === 'checks the input exists') return
it(f.description, () => {
if (f.shouldSign) {
const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt)
assert.doesNotThrow(() => {
psbtThatShouldsign.signAllInputs(
ECPair.fromWIF(f.shouldSign.WIF),
f.shouldSign.sighashTypes || undefined,
)
})
}
if (f.shouldThrow) {
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt)
assert.throws(() => {
psbtThatShouldThrow.signAllInputs(
ECPair.fromWIF(f.shouldThrow.WIF),
f.shouldThrow.sighashTypes || undefined,
)
}, new RegExp('No inputs were signed'))
assert.throws(() => {
psbtThatShouldThrow.signAllInputs()
}, new RegExp('Need Signer to sign input'))
}
})
})
})
describe('signInputHDAsync', () => {
fixtures.signInputHD.checks.forEach(f => {
it(f.description, async () => {
if (f.shouldSign) {
const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt)
assert.doesNotReject(async () => {
await psbtThatShouldsign.signInputHDAsync(
f.shouldSign.inputToCheck,
bip32.fromBase58(f.shouldSign.xprv),
f.shouldSign.sighashTypes || undefined,
)
})
}
if (f.shouldThrow) {
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt)
assert.rejects(async () => {
await psbtThatShouldThrow.signInputHDAsync(
f.shouldThrow.inputToCheck,
bip32.fromBase58(f.shouldThrow.xprv),
f.shouldThrow.sighashTypes || undefined,
)
}, new RegExp(f.shouldThrow.errorMessage))
assert.rejects(async () => {
await psbtThatShouldThrow.signInputHDAsync(
f.shouldThrow.inputToCheck,
)
}, new RegExp('Need HDSigner to sign input'))
}
})
})
})
describe('signInputHD', () => {
fixtures.signInputHD.checks.forEach(f => {
it(f.description, () => {
if (f.shouldSign) {
const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt)
assert.doesNotThrow(() => {
psbtThatShouldsign.signInputHD(
f.shouldSign.inputToCheck,
bip32.fromBase58(f.shouldSign.xprv),
f.shouldSign.sighashTypes || undefined,
)
})
}
if (f.shouldThrow) {
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt)
assert.throws(() => {
psbtThatShouldThrow.signInputHD(
f.shouldThrow.inputToCheck,
bip32.fromBase58(f.shouldThrow.xprv),
f.shouldThrow.sighashTypes || undefined,
)
}, new RegExp(f.shouldThrow.errorMessage))
assert.throws(() => {
psbtThatShouldThrow.signInputHD(
f.shouldThrow.inputToCheck,
)
}, new RegExp('Need HDSigner to sign input'))
}
})
})
})
describe('signAllInputsHDAsync', () => {
fixtures.signInputHD.checks.forEach(f => {
it(f.description, async () => {
if (f.shouldSign) {
const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt)
assert.doesNotReject(async () => {
await psbtThatShouldsign.signAllInputsHDAsync(
bip32.fromBase58(f.shouldSign.xprv),
f.shouldSign.sighashTypes || undefined,
)
})
}
if (f.shouldThrow) {
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt)
assert.rejects(async () => {
await psbtThatShouldThrow.signAllInputsHDAsync(
bip32.fromBase58(f.shouldThrow.xprv),
f.shouldThrow.sighashTypes || undefined,
)
}, new RegExp('No inputs were signed'))
assert.rejects(async () => {
await psbtThatShouldThrow.signAllInputsHDAsync()
}, new RegExp('Need HDSigner to sign input'))
}
})
})
})
describe('signAllInputsHD', () => {
fixtures.signInputHD.checks.forEach(f => {
it(f.description, () => {
if (f.shouldSign) {
const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt)
assert.doesNotThrow(() => {
psbtThatShouldsign.signAllInputsHD(
bip32.fromBase58(f.shouldSign.xprv),
f.shouldSign.sighashTypes || undefined,
)
})
}
if (f.shouldThrow) {
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt)
assert.throws(() => {
psbtThatShouldThrow.signAllInputsHD(
bip32.fromBase58(f.shouldThrow.xprv),
f.shouldThrow.sighashTypes || undefined,
)
}, new RegExp('No inputs were signed'))
assert.throws(() => {
psbtThatShouldThrow.signAllInputsHD()
}, new RegExp('Need HDSigner to sign input'))
}
})
})
})
describe('finalizeAllInputs', () => {
fixtures.finalizeAllInputs.forEach(f => {
it(`Finalizes inputs of type "${f.type}"`, () => {
const psbt = Psbt.fromBase64(f.psbt)
psbt.finalizeAllInputs()
assert.strictEqual(psbt.toBase64(), f.result)
})
})
it('fails if no script found', () => {
const psbt = new Psbt()
psbt.addInput({
hash: '0000000000000000000000000000000000000000000000000000000000000000',
index: 0
})
assert.throws(() => {
psbt.finalizeAllInputs()
}, new RegExp('No script found for input #0'))
psbt.updateInput(0, {
witnessUtxo: {
script: Buffer.from('0014d85c2b71d0060b09c9886aeb815e50991dda124d', 'hex'),
value: 2e5
}
})
assert.throws(() => {
psbt.finalizeAllInputs()
}, new RegExp('Can not finalize input #0'))
})
})
describe('addInput', () => {
fixtures.addInput.checks.forEach(f => {
it(f.description, () => {
const psbt = new Psbt()
if (f.exception) {
assert.throws(() => {
psbt.addInput(f.inputData)
}, new RegExp(f.exception))
assert.throws(() => {
psbt.addInputs([f.inputData])
}, new RegExp(f.exception))
} else {
assert.doesNotThrow(() => {
psbt.addInputs([f.inputData])
if (f.equals) {
assert.strictEqual(psbt.toBase64(), f.equals)
}
})
assert.throws(() => {
psbt.addInput(f.inputData)
}, new RegExp('Duplicate input detected.'))
}
})
})
})
describe('addOutput', () => {
fixtures.addOutput.checks.forEach(f => {
it(f.description, () => {
const psbt = new Psbt()
if (f.exception) {
assert.throws(() => {
psbt.addOutput(f.outputData)
}, new RegExp(f.exception))
assert.throws(() => {
psbt.addOutputs([f.outputData])
}, new RegExp(f.exception))
} else {
assert.doesNotThrow(() => {
psbt.addOutput(f.outputData)
})
assert.doesNotThrow(() => {
psbt.addOutputs([f.outputData])
})
}
})
})
})
describe('setVersion', () => {
it('Sets the version value of the unsigned transaction', () => {
const psbt = new Psbt()
assert.strictEqual(psbt.extractTransaction().version, 2)
psbt.setVersion(1)
assert.strictEqual(psbt.extractTransaction().version, 1)
})
})
describe('setLocktime', () => {
it('Sets the nLockTime value of the unsigned transaction', () => {
const psbt = new Psbt()
assert.strictEqual(psbt.extractTransaction().locktime, 0)
psbt.setLocktime(1)
assert.strictEqual(psbt.extractTransaction().locktime, 1)
})
})
describe('setInputSequence', () => {
it('Sets the sequence number for a given input', () => {
const psbt = new Psbt()
psbt.addInput({
hash: '0000000000000000000000000000000000000000000000000000000000000000',
index: 0
});
assert.strictEqual(psbt.inputCount, 1)
assert.strictEqual(psbt.__CACHE.__TX.ins[0].sequence, 0xffffffff)
psbt.setInputSequence(0, 0)
assert.strictEqual(psbt.__CACHE.__TX.ins[0].sequence, 0)
})
it('throws if input index is too high', () => {
const psbt = new Psbt()
psbt.addInput({
hash: '0000000000000000000000000000000000000000000000000000000000000000',
index: 0
});
assert.throws(() => {
psbt.setInputSequence(1, 0)
}, new RegExp('Input index too high'))
})
})
describe('clone', () => {
it('Should clone a psbt exactly with no reference', () => {
const f = fixtures.clone
const psbt = Psbt.fromBase64(f.psbt)
const notAClone = Object.assign(new Psbt(), psbt) // references still active
const clone = psbt.clone()
assert.strictEqual(psbt.validateSignaturesOfAllInputs(), true)
assert.strictEqual(clone.toBase64(), psbt.toBase64())
assert.strictEqual(clone.toBase64(), notAClone.toBase64())
assert.strictEqual(psbt.toBase64(), notAClone.toBase64())
psbt.__CACHE.__TX.version |= 0xff0000
assert.notStrictEqual(clone.toBase64(), psbt.toBase64())
assert.notStrictEqual(clone.toBase64(), notAClone.toBase64())
assert.strictEqual(psbt.toBase64(), notAClone.toBase64())
})
})
describe('setMaximumFeeRate', () => {
it('Sets the maximumFeeRate value', () => {
const psbt = new Psbt()
assert.strictEqual(psbt.opts.maximumFeeRate, 5000)
psbt.setMaximumFeeRate(6000)
assert.strictEqual(psbt.opts.maximumFeeRate, 6000)
})
})
describe('validateSignaturesOfInput', () => {
const f = fixtures.validateSignaturesOfInput
it('Correctly validates a signature', () => {
const psbt = Psbt.fromBase64(f.psbt)
assert.strictEqual(psbt.validateSignaturesOfInput(f.index), true)
assert.throws(() => {
psbt.validateSignaturesOfInput(f.nonExistantIndex)
}, new RegExp('No signatures to validate'))
})
it('Correctly validates a signature against a pubkey', () => {
const psbt = Psbt.fromBase64(f.psbt)
assert.strictEqual(psbt.validateSignaturesOfInput(f.index, f.pubkey), true)
assert.throws(() => {
psbt.validateSignaturesOfInput(f.index, f.incorrectPubkey)
}, new RegExp('No signatures for this pubkey'))
})
})
describe('getFeeRate', () => {
it('Throws error if called before inputs are finalized', () => {
const f = fixtures.getFeeRate
const psbt = Psbt.fromBase64(f.psbt)
assert.throws(() => {
psbt.getFeeRate()
}, new RegExp('PSBT must be finalized to calculate fee rate'))
psbt.finalizeAllInputs()
assert.strictEqual(psbt.getFeeRate(), f.fee)
psbt.__CACHE.__FEE_RATE = undefined
assert.strictEqual(psbt.getFeeRate(), f.fee)
})
})
describe('create 1-to-1 transaction', () => {
const alice = ECPair.fromWIF('L2uPYXe17xSTqbCjZvL2DsyXPCbXspvcu5mHLDYUgzdUbZGSKrSr')
const psbt = new Psbt()
psbt.addInput({
hash: '7d067b4a697a09d2c3cff7d4d9506c9955e93bff41bf82d439da7d030382bc3e',
index: 0,
nonWitnessUtxo: Buffer.from(
'0200000001f9f34e95b9d5c8abcd20fc5bd4a825d1517be62f0f775e5f36da944d9' +
'452e550000000006b483045022100c86e9a111afc90f64b4904bd609e9eaed80d48' +
'ca17c162b1aca0a788ac3526f002207bb79b60d4fc6526329bf18a77135dc566020' +
'9e761da46e1c2f1152ec013215801210211755115eabf846720f5cb18f248666fec' +
'631e5e1e66009ce3710ceea5b1ad13ffffffff01905f0100000000001976a9148bb' +
'c95d2709c71607c60ee3f097c1217482f518d88ac00000000',
'hex',
),
sighashType: 1,
})
psbt.addOutput({
address: '1KRMKfeZcmosxALVYESdPNez1AP1mEtywp',
value: 80000
})
psbt.signInput(0, alice)
assert.throws(() => {
psbt.setVersion(3)
}, new RegExp('Can not modify transaction, signatures exist.'))
psbt.validateSignaturesOfInput(0)
psbt.finalizeAllInputs()
assert.throws(() => {
psbt.setVersion(3)
}, new RegExp('Can not modify transaction, signatures exist.'))
assert.strictEqual(
psbt.extractTransaction().toHex(),
'02000000013ebc8203037dda39d482bf41ff3be955996c50d9d4f7cfc3d2097a694a7' +
'b067d000000006b483045022100931b6db94aed25d5486884d83fc37160f37f3368c0' +
'd7f48c757112abefec983802205fda64cff98c849577026eb2ce916a50ea70626a766' +
'9f8596dd89b720a26b4d501210365db9da3f8a260078a7e8f8b708a1161468fb2323f' +
'fda5ec16b261ec1056f455ffffffff0180380100000000001976a914ca0d36044e0dc' +
'08a22724efa6f6a07b0ec4c79aa88ac00000000',
)
})
describe('Method return types', () => {
it('fromBuffer returns Psbt type (not base class)', () => {
const psbt = Psbt.fromBuffer(Buffer.from(
'70736274ff01000a01000000000000000000000000', 'hex' //cHNidP8BAAoBAAAAAAAAAAAAAAAA
));
assert.strictEqual(psbt instanceof Psbt, true);
assert.ok(psbt.__CACHE.__TX);
})
it('fromBase64 returns Psbt type (not base class)', () => {
const psbt = Psbt.fromBase64('cHNidP8BAAoBAAAAAAAAAAAAAAAA');
assert.strictEqual(psbt instanceof Psbt, true);
assert.ok(psbt.__CACHE.__TX);
})
it('fromHex returns Psbt type (not base class)', () => {
const psbt = Psbt.fromHex('70736274ff01000a01000000000000000000000000');
assert.strictEqual(psbt instanceof Psbt, true);
assert.ok(psbt.__CACHE.__TX);
})
})
describe('Cache', () => {
it('non-witness UTXOs are cached', () => {
const f = fixtures.cache.nonWitnessUtxo;
const psbt = Psbt.fromBase64(f.psbt)
const index = f.inputIndex;
// Cache is empty
assert.strictEqual(psbt.__CACHE.__NON_WITNESS_UTXO_BUF_CACHE[index], undefined)
// Cache is populated
psbt.updateInput(index, { nonWitnessUtxo: f.nonWitnessUtxo })
const value = psbt.data.inputs[index].nonWitnessUtxo
assert.ok(psbt.__CACHE.__NON_WITNESS_UTXO_BUF_CACHE[index].equals(value))
assert.ok(psbt.__CACHE.__NON_WITNESS_UTXO_BUF_CACHE[index].equals(f.nonWitnessUtxo))
// Cache is rebuilt from internal transaction object when cleared
psbt.data.inputs[index].nonWitnessUtxo = Buffer.from([1,2,3])
psbt.__CACHE.__NON_WITNESS_UTXO_BUF_CACHE[index] = undefined
assert.ok(psbt.data.inputs[index].nonWitnessUtxo.equals(value))
})
})
})

713
test/psbt.spec.ts Normal file
View file

@ -0,0 +1,713 @@
import * as assert from 'assert';
import { describe, it } from 'mocha';
import { bip32, ECPair, networks as NETWORKS, Psbt } from '..';
import * as preFixtures from './fixtures/psbt.json';
const initBuffers = (object: any): typeof preFixtures =>
JSON.parse(JSON.stringify(object), (_, value) => {
const regex = new RegExp(/^Buffer.from\(['"](.*)['"], ['"](.*)['"]\)$/);
const result = regex.exec(value);
if (!result) return value;
const data = result[1];
const encoding = result[2];
return Buffer.from(data, encoding);
});
const fixtures = initBuffers(preFixtures);
const upperCaseFirstLetter = (str: string) =>
str.replace(/^./, s => s.toUpperCase());
// const b = (hex: string) => Buffer.from(hex, 'hex');
describe(`Psbt`, () => {
describe('BIP174 Test Vectors', () => {
fixtures.bip174.invalid.forEach(f => {
it(`Invalid: ${f.description}`, () => {
assert.throws(() => {
Psbt.fromBase64(f.psbt);
}, new RegExp(f.errorMessage));
});
});
fixtures.bip174.valid.forEach(f => {
it(`Valid: ${f.description}`, () => {
assert.doesNotThrow(() => {
Psbt.fromBase64(f.psbt);
});
});
});
fixtures.bip174.failSignChecks.forEach(f => {
const keyPair = ECPair.makeRandom();
it(`Fails Signer checks: ${f.description}`, () => {
const psbt = Psbt.fromBase64(f.psbt);
assert.throws(() => {
psbt.signInput(f.inputToCheck, keyPair);
}, new RegExp(f.errorMessage));
});
});
fixtures.bip174.creator.forEach(f => {
it('Creates expected PSBT', () => {
const psbt = new Psbt();
for (const input of f.inputs) {
psbt.addInput(input);
}
for (const output of f.outputs) {
const script = Buffer.from(output.script, 'hex');
psbt.addOutput({ ...output, script });
}
assert.strictEqual(psbt.toBase64(), f.result);
});
});
fixtures.bip174.updater.forEach(f => {
it('Updates PSBT to the expected result', () => {
const psbt = Psbt.fromBase64(f.psbt);
for (const inputOrOutput of ['input', 'output']) {
const fixtureData = (f as any)[`${inputOrOutput}Data`];
if (fixtureData) {
for (const [i, data] of fixtureData.entries()) {
const txt = upperCaseFirstLetter(inputOrOutput);
(psbt as any)[`update${txt}`](i, data);
}
}
}
assert.strictEqual(psbt.toBase64(), f.result);
});
});
fixtures.bip174.signer.forEach(f => {
it('Signs PSBT to the expected result', () => {
const psbt = Psbt.fromBase64(f.psbt);
f.keys.forEach(({ inputToSign, WIF }) => {
const keyPair = ECPair.fromWIF(WIF, NETWORKS.testnet);
psbt.signInput(inputToSign, keyPair);
});
assert.strictEqual(psbt.toBase64(), f.result);
});
});
fixtures.bip174.combiner.forEach(f => {
it('Combines two PSBTs to the expected result', () => {
const psbts = f.psbts.map(psbt => Psbt.fromBase64(psbt));
psbts[0].combine(psbts[1]);
// Produces a different Base64 string due to implemetation specific key-value ordering.
// That means this test will fail:
// assert.strictEqual(psbts[0].toBase64(), f.result)
// However, if we compare the actual PSBT properties we can see they are logically identical:
assert.deepStrictEqual(psbts[0], Psbt.fromBase64(f.result));
});
});
fixtures.bip174.finalizer.forEach(f => {
it('Finalizes inputs and gives the expected PSBT', () => {
const psbt = Psbt.fromBase64(f.psbt);
psbt.finalizeAllInputs();
assert.strictEqual(psbt.toBase64(), f.result);
});
});
fixtures.bip174.extractor.forEach(f => {
it('Extracts the expected transaction from a PSBT', () => {
const psbt1 = Psbt.fromBase64(f.psbt);
const transaction1 = psbt1.extractTransaction(true).toHex();
const psbt2 = Psbt.fromBase64(f.psbt);
const transaction2 = psbt2.extractTransaction().toHex();
assert.strictEqual(transaction1, transaction2);
assert.strictEqual(transaction1, f.transaction);
const psbt3 = Psbt.fromBase64(f.psbt);
delete psbt3.data.inputs[0].finalScriptSig;
delete psbt3.data.inputs[0].finalScriptWitness;
assert.throws(() => {
psbt3.extractTransaction();
}, new RegExp('Not finalized'));
const psbt4 = Psbt.fromBase64(f.psbt);
psbt4.setMaximumFeeRate(1);
assert.throws(() => {
psbt4.extractTransaction();
}, new RegExp('Warning: You are paying around [\\d.]+ in fees'));
const psbt5 = Psbt.fromBase64(f.psbt);
psbt5.extractTransaction(true);
const fr1 = psbt5.getFeeRate();
const fr2 = psbt5.getFeeRate();
assert.strictEqual(fr1, fr2);
const psbt6 = Psbt.fromBase64(f.psbt);
const f1 = psbt6.getFee();
const f2 = psbt6.getFee();
assert.strictEqual(f1, f2);
});
});
});
describe('signInputAsync', () => {
fixtures.signInput.checks.forEach(f => {
it(f.description, async () => {
if (f.shouldSign) {
const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt);
assert.doesNotReject(async () => {
await psbtThatShouldsign.signInputAsync(
f.shouldSign.inputToCheck,
ECPair.fromWIF(f.shouldSign.WIF),
f.shouldSign.sighashTypes || undefined,
);
});
}
if (f.shouldThrow) {
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt);
assert.rejects(async () => {
await psbtThatShouldThrow.signInputAsync(
f.shouldThrow.inputToCheck,
ECPair.fromWIF(f.shouldThrow.WIF),
(f.shouldThrow as any).sighashTypes || undefined,
);
}, new RegExp(f.shouldThrow.errorMessage));
assert.rejects(async () => {
await (psbtThatShouldThrow.signInputAsync as any)(
f.shouldThrow.inputToCheck,
);
}, new RegExp('Need Signer to sign input'));
}
});
});
});
describe('signInput', () => {
fixtures.signInput.checks.forEach(f => {
it(f.description, () => {
if (f.shouldSign) {
const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt);
assert.doesNotThrow(() => {
psbtThatShouldsign.signInput(
f.shouldSign.inputToCheck,
ECPair.fromWIF(f.shouldSign.WIF),
f.shouldSign.sighashTypes || undefined,
);
});
}
if (f.shouldThrow) {
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt);
assert.throws(() => {
psbtThatShouldThrow.signInput(
f.shouldThrow.inputToCheck,
ECPair.fromWIF(f.shouldThrow.WIF),
(f.shouldThrow as any).sighashTypes || undefined,
);
}, new RegExp(f.shouldThrow.errorMessage));
assert.throws(() => {
(psbtThatShouldThrow.signInput as any)(f.shouldThrow.inputToCheck);
}, new RegExp('Need Signer to sign input'));
}
});
});
});
describe('signAllInputsAsync', () => {
fixtures.signInput.checks.forEach(f => {
if (f.description === 'checks the input exists') return;
it(f.description, async () => {
if (f.shouldSign) {
const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt);
assert.doesNotReject(async () => {
await psbtThatShouldsign.signAllInputsAsync(
ECPair.fromWIF(f.shouldSign.WIF),
f.shouldSign.sighashTypes || undefined,
);
});
}
if (f.shouldThrow) {
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt);
assert.rejects(async () => {
await psbtThatShouldThrow.signAllInputsAsync(
ECPair.fromWIF(f.shouldThrow.WIF),
(f.shouldThrow as any).sighashTypes || undefined,
);
}, new RegExp('No inputs were signed'));
assert.rejects(async () => {
await (psbtThatShouldThrow.signAllInputsAsync as any)();
}, new RegExp('Need Signer to sign input'));
}
});
});
});
describe('signAllInputs', () => {
fixtures.signInput.checks.forEach(f => {
if (f.description === 'checks the input exists') return;
it(f.description, () => {
if (f.shouldSign) {
const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt);
assert.doesNotThrow(() => {
psbtThatShouldsign.signAllInputs(
ECPair.fromWIF(f.shouldSign.WIF),
f.shouldSign.sighashTypes || undefined,
);
});
}
if (f.shouldThrow) {
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt);
assert.throws(() => {
psbtThatShouldThrow.signAllInputs(
ECPair.fromWIF(f.shouldThrow.WIF),
(f.shouldThrow as any).sighashTypes || undefined,
);
}, new RegExp('No inputs were signed'));
assert.throws(() => {
(psbtThatShouldThrow.signAllInputs as any)();
}, new RegExp('Need Signer to sign input'));
}
});
});
});
describe('signInputHDAsync', () => {
fixtures.signInputHD.checks.forEach(f => {
it(f.description, async () => {
if (f.shouldSign) {
const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt);
assert.doesNotReject(async () => {
await psbtThatShouldsign.signInputHDAsync(
f.shouldSign.inputToCheck,
bip32.fromBase58(f.shouldSign.xprv),
(f.shouldSign as any).sighashTypes || undefined,
);
});
}
if (f.shouldThrow) {
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt);
assert.rejects(async () => {
await psbtThatShouldThrow.signInputHDAsync(
f.shouldThrow.inputToCheck,
bip32.fromBase58(f.shouldThrow.xprv),
(f.shouldThrow as any).sighashTypes || undefined,
);
}, new RegExp(f.shouldThrow.errorMessage));
assert.rejects(async () => {
await (psbtThatShouldThrow.signInputHDAsync as any)(
f.shouldThrow.inputToCheck,
);
}, new RegExp('Need HDSigner to sign input'));
}
});
});
});
describe('signInputHD', () => {
fixtures.signInputHD.checks.forEach(f => {
it(f.description, () => {
if (f.shouldSign) {
const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt);
assert.doesNotThrow(() => {
psbtThatShouldsign.signInputHD(
f.shouldSign.inputToCheck,
bip32.fromBase58(f.shouldSign.xprv),
(f.shouldSign as any).sighashTypes || undefined,
);
});
}
if (f.shouldThrow) {
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt);
assert.throws(() => {
psbtThatShouldThrow.signInputHD(
f.shouldThrow.inputToCheck,
bip32.fromBase58(f.shouldThrow.xprv),
(f.shouldThrow as any).sighashTypes || undefined,
);
}, new RegExp(f.shouldThrow.errorMessage));
assert.throws(() => {
(psbtThatShouldThrow.signInputHD as any)(
f.shouldThrow.inputToCheck,
);
}, new RegExp('Need HDSigner to sign input'));
}
});
});
});
describe('signAllInputsHDAsync', () => {
fixtures.signInputHD.checks.forEach(f => {
it(f.description, async () => {
if (f.shouldSign) {
const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt);
assert.doesNotReject(async () => {
await psbtThatShouldsign.signAllInputsHDAsync(
bip32.fromBase58(f.shouldSign.xprv),
(f.shouldSign as any).sighashTypes || undefined,
);
});
}
if (f.shouldThrow) {
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt);
assert.rejects(async () => {
await psbtThatShouldThrow.signAllInputsHDAsync(
bip32.fromBase58(f.shouldThrow.xprv),
(f.shouldThrow as any).sighashTypes || undefined,
);
}, new RegExp('No inputs were signed'));
assert.rejects(async () => {
await (psbtThatShouldThrow.signAllInputsHDAsync as any)();
}, new RegExp('Need HDSigner to sign input'));
}
});
});
});
describe('signAllInputsHD', () => {
fixtures.signInputHD.checks.forEach(f => {
it(f.description, () => {
if (f.shouldSign) {
const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt);
assert.doesNotThrow(() => {
psbtThatShouldsign.signAllInputsHD(
bip32.fromBase58(f.shouldSign.xprv),
(f.shouldSign as any).sighashTypes || undefined,
);
});
}
if (f.shouldThrow) {
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt);
assert.throws(() => {
psbtThatShouldThrow.signAllInputsHD(
bip32.fromBase58(f.shouldThrow.xprv),
(f.shouldThrow as any).sighashTypes || undefined,
);
}, new RegExp('No inputs were signed'));
assert.throws(() => {
(psbtThatShouldThrow.signAllInputsHD as any)();
}, new RegExp('Need HDSigner to sign input'));
}
});
});
});
describe('finalizeAllInputs', () => {
fixtures.finalizeAllInputs.forEach(f => {
it(`Finalizes inputs of type "${f.type}"`, () => {
const psbt = Psbt.fromBase64(f.psbt);
psbt.finalizeAllInputs();
assert.strictEqual(psbt.toBase64(), f.result);
});
});
it('fails if no script found', () => {
const psbt = new Psbt();
psbt.addInput({
hash:
'0000000000000000000000000000000000000000000000000000000000000000',
index: 0,
});
assert.throws(() => {
psbt.finalizeAllInputs();
}, new RegExp('No script found for input #0'));
psbt.updateInput(0, {
witnessUtxo: {
script: Buffer.from(
'0014d85c2b71d0060b09c9886aeb815e50991dda124d',
'hex',
),
value: 2e5,
},
});
assert.throws(() => {
psbt.finalizeAllInputs();
}, new RegExp('Can not finalize input #0'));
});
});
describe('addInput', () => {
fixtures.addInput.checks.forEach(f => {
it(f.description, () => {
const psbt = new Psbt();
if (f.exception) {
assert.throws(() => {
psbt.addInput(f.inputData as any);
}, new RegExp(f.exception));
assert.throws(() => {
psbt.addInputs([f.inputData as any]);
}, new RegExp(f.exception));
} else {
assert.doesNotThrow(() => {
psbt.addInputs([f.inputData as any]);
if (f.equals) {
assert.strictEqual(psbt.toBase64(), f.equals);
}
});
assert.throws(() => {
psbt.addInput(f.inputData as any);
}, new RegExp('Duplicate input detected.'));
}
});
});
});
describe('addOutput', () => {
fixtures.addOutput.checks.forEach(f => {
it(f.description, () => {
const psbt = new Psbt();
if (f.exception) {
assert.throws(() => {
psbt.addOutput(f.outputData as any);
}, new RegExp(f.exception));
assert.throws(() => {
psbt.addOutputs([f.outputData as any]);
}, new RegExp(f.exception));
} else {
assert.doesNotThrow(() => {
psbt.addOutput(f.outputData as any);
});
assert.doesNotThrow(() => {
psbt.addOutputs([f.outputData as any]);
});
}
});
});
});
describe('setVersion', () => {
it('Sets the version value of the unsigned transaction', () => {
const psbt = new Psbt();
assert.strictEqual(psbt.extractTransaction().version, 2);
psbt.setVersion(1);
assert.strictEqual(psbt.extractTransaction().version, 1);
});
});
describe('setLocktime', () => {
it('Sets the nLockTime value of the unsigned transaction', () => {
const psbt = new Psbt();
assert.strictEqual(psbt.extractTransaction().locktime, 0);
psbt.setLocktime(1);
assert.strictEqual(psbt.extractTransaction().locktime, 1);
});
});
describe('setInputSequence', () => {
it('Sets the sequence number for a given input', () => {
const psbt = new Psbt();
psbt.addInput({
hash:
'0000000000000000000000000000000000000000000000000000000000000000',
index: 0,
});
assert.strictEqual(psbt.inputCount, 1);
assert.strictEqual(
(psbt as any).__CACHE.__TX.ins[0].sequence,
0xffffffff,
);
psbt.setInputSequence(0, 0);
assert.strictEqual((psbt as any).__CACHE.__TX.ins[0].sequence, 0);
});
it('throws if input index is too high', () => {
const psbt = new Psbt();
psbt.addInput({
hash:
'0000000000000000000000000000000000000000000000000000000000000000',
index: 0,
});
assert.throws(() => {
psbt.setInputSequence(1, 0);
}, new RegExp('Input index too high'));
});
});
describe('clone', () => {
it('Should clone a psbt exactly with no reference', () => {
const f = fixtures.clone;
const psbt = Psbt.fromBase64(f.psbt);
const notAClone = Object.assign(new Psbt(), psbt); // references still active
const clone = psbt.clone();
assert.strictEqual(psbt.validateSignaturesOfAllInputs(), true);
assert.strictEqual(clone.toBase64(), psbt.toBase64());
assert.strictEqual(clone.toBase64(), notAClone.toBase64());
assert.strictEqual(psbt.toBase64(), notAClone.toBase64());
(psbt as any).__CACHE.__TX.version |= 0xff0000;
assert.notStrictEqual(clone.toBase64(), psbt.toBase64());
assert.notStrictEqual(clone.toBase64(), notAClone.toBase64());
assert.strictEqual(psbt.toBase64(), notAClone.toBase64());
});
});
describe('setMaximumFeeRate', () => {
it('Sets the maximumFeeRate value', () => {
const psbt = new Psbt();
assert.strictEqual((psbt as any).opts.maximumFeeRate, 5000);
psbt.setMaximumFeeRate(6000);
assert.strictEqual((psbt as any).opts.maximumFeeRate, 6000);
});
});
describe('validateSignaturesOfInput', () => {
const f = fixtures.validateSignaturesOfInput;
it('Correctly validates a signature', () => {
const psbt = Psbt.fromBase64(f.psbt);
assert.strictEqual(psbt.validateSignaturesOfInput(f.index), true);
assert.throws(() => {
psbt.validateSignaturesOfInput(f.nonExistantIndex);
}, new RegExp('No signatures to validate'));
});
it('Correctly validates a signature against a pubkey', () => {
const psbt = Psbt.fromBase64(f.psbt);
assert.strictEqual(
psbt.validateSignaturesOfInput(f.index, f.pubkey as any),
true,
);
assert.throws(() => {
psbt.validateSignaturesOfInput(f.index, f.incorrectPubkey as any);
}, new RegExp('No signatures for this pubkey'));
});
});
describe('getFeeRate', () => {
it('Throws error if called before inputs are finalized', () => {
const f = fixtures.getFeeRate;
const psbt = Psbt.fromBase64(f.psbt);
assert.throws(() => {
psbt.getFeeRate();
}, new RegExp('PSBT must be finalized to calculate fee rate'));
psbt.finalizeAllInputs();
assert.strictEqual(psbt.getFeeRate(), f.fee);
(psbt as any).__CACHE.__FEE_RATE = undefined;
assert.strictEqual(psbt.getFeeRate(), f.fee);
});
});
describe('create 1-to-1 transaction', () => {
const alice = ECPair.fromWIF(
'L2uPYXe17xSTqbCjZvL2DsyXPCbXspvcu5mHLDYUgzdUbZGSKrSr',
);
const psbt = new Psbt();
psbt.addInput({
hash: '7d067b4a697a09d2c3cff7d4d9506c9955e93bff41bf82d439da7d030382bc3e',
index: 0,
nonWitnessUtxo: Buffer.from(
'0200000001f9f34e95b9d5c8abcd20fc5bd4a825d1517be62f0f775e5f36da944d9' +
'452e550000000006b483045022100c86e9a111afc90f64b4904bd609e9eaed80d48' +
'ca17c162b1aca0a788ac3526f002207bb79b60d4fc6526329bf18a77135dc566020' +
'9e761da46e1c2f1152ec013215801210211755115eabf846720f5cb18f248666fec' +
'631e5e1e66009ce3710ceea5b1ad13ffffffff01905f0100000000001976a9148bb' +
'c95d2709c71607c60ee3f097c1217482f518d88ac00000000',
'hex',
),
sighashType: 1,
});
psbt.addOutput({
address: '1KRMKfeZcmosxALVYESdPNez1AP1mEtywp',
value: 80000,
});
psbt.signInput(0, alice);
assert.throws(() => {
psbt.setVersion(3);
}, new RegExp('Can not modify transaction, signatures exist.'));
psbt.validateSignaturesOfInput(0);
psbt.finalizeAllInputs();
assert.throws(() => {
psbt.setVersion(3);
}, new RegExp('Can not modify transaction, signatures exist.'));
assert.strictEqual(
psbt.extractTransaction().toHex(),
'02000000013ebc8203037dda39d482bf41ff3be955996c50d9d4f7cfc3d2097a694a7' +
'b067d000000006b483045022100931b6db94aed25d5486884d83fc37160f37f3368c0' +
'd7f48c757112abefec983802205fda64cff98c849577026eb2ce916a50ea70626a766' +
'9f8596dd89b720a26b4d501210365db9da3f8a260078a7e8f8b708a1161468fb2323f' +
'fda5ec16b261ec1056f455ffffffff0180380100000000001976a914ca0d36044e0dc' +
'08a22724efa6f6a07b0ec4c79aa88ac00000000',
);
});
describe('Method return types', () => {
it('fromBuffer returns Psbt type (not base class)', () => {
const psbt = Psbt.fromBuffer(
Buffer.from(
'70736274ff01000a01000000000000000000000000',
'hex', //cHNidP8BAAoBAAAAAAAAAAAAAAAA
),
);
assert.strictEqual(psbt instanceof Psbt, true);
assert.ok((psbt as any).__CACHE.__TX);
});
it('fromBase64 returns Psbt type (not base class)', () => {
const psbt = Psbt.fromBase64('cHNidP8BAAoBAAAAAAAAAAAAAAAA');
assert.strictEqual(psbt instanceof Psbt, true);
assert.ok((psbt as any).__CACHE.__TX);
});
it('fromHex returns Psbt type (not base class)', () => {
const psbt = Psbt.fromHex('70736274ff01000a01000000000000000000000000');
assert.strictEqual(psbt instanceof Psbt, true);
assert.ok((psbt as any).__CACHE.__TX);
});
});
describe('Cache', () => {
it('non-witness UTXOs are cached', () => {
const f = fixtures.cache.nonWitnessUtxo;
const psbt = Psbt.fromBase64(f.psbt);
const index = f.inputIndex;
// Cache is empty
assert.strictEqual(
(psbt as any).__CACHE.__NON_WITNESS_UTXO_BUF_CACHE[index],
undefined,
);
// Cache is populated
psbt.updateInput(index, { nonWitnessUtxo: f.nonWitnessUtxo as any });
const value = psbt.data.inputs[index].nonWitnessUtxo;
assert.ok(
(psbt as any).__CACHE.__NON_WITNESS_UTXO_BUF_CACHE[index].equals(value),
);
assert.ok(
(psbt as any).__CACHE.__NON_WITNESS_UTXO_BUF_CACHE[index].equals(
f.nonWitnessUtxo,
),
);
// Cache is rebuilt from internal transaction object when cleared
psbt.data.inputs[index].nonWitnessUtxo = Buffer.from([1, 2, 3]);
(psbt as any).__CACHE.__NON_WITNESS_UTXO_BUF_CACHE[index] = undefined;
assert.ok((psbt as any).data.inputs[index].nonWitnessUtxo.equals(value));
});
});
});

View file

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

176
test/script.spec.ts Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

338
test/transaction.spec.ts Normal file
View file

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

View file

@ -1,789 +0,0 @@
const { describe, it, beforeEach } = require('mocha')
const assert = require('assert')
const baddress = require('../src/address')
const bscript = require('../src/script')
const payments = require('../src/payments')
const ECPair = require('../src/ecpair')
const Transaction = require('..').Transaction
const TransactionBuilder = require('..').TransactionBuilder
const NETWORKS = require('../src/networks')
console.warn = () => {} // Silence the Deprecation Warning
const fixtures = require('./fixtures/transaction_builder')
function constructSign (f, txb, useOldSignArgs) {
const network = NETWORKS[f.network]
const stages = f.stages && f.stages.concat()
f.inputs.forEach((input, index) => {
if (!input.signs) return
input.signs.forEach(sign => {
const keyPair = ECPair.fromWIF(sign.keyPair, network)
let redeemScript
let witnessScript
let witnessValue
if (sign.redeemScript) {
redeemScript = bscript.fromASM(sign.redeemScript)
}
if (sign.value) {
witnessValue = sign.value
}
if (sign.witnessScript) {
witnessScript = bscript.fromASM(sign.witnessScript)
}
if (useOldSignArgs) {
// DEPRECATED: v6 will remove this interface
txb.sign(index, keyPair, redeemScript, sign.hashType, witnessValue, witnessScript)
} else {
// prevOutScriptType is required, see /ts_src/transaction_builder.ts
// The PREVOUT_TYPES constant is a Set with all possible values.
txb.sign({
prevOutScriptType: sign.prevOutScriptType,
vin: index,
keyPair,
redeemScript,
hashType: sign.hashType,
witnessValue,
witnessScript,
})
}
if (sign.stage) {
const tx = txb.buildIncomplete()
assert.strictEqual(tx.toHex(), stages.shift())
txb = TransactionBuilder.fromTransaction(tx, network)
}
})
})
return txb
}
function construct (f, dontSign, useOldSignArgs) {
const network = NETWORKS[f.network]
const txb = new TransactionBuilder(network)
if (Number.isFinite(f.version)) txb.setVersion(f.version)
if (f.locktime !== undefined) txb.setLockTime(f.locktime)
f.inputs.forEach(input => {
let prevTx
if (input.txRaw) {
const constructed = construct(input.txRaw)
if (input.txRaw.incomplete) prevTx = constructed.buildIncomplete()
else prevTx = constructed.build()
} else if (input.txHex) {
prevTx = Transaction.fromHex(input.txHex)
} else {
prevTx = input.txId
}
let prevTxScript
if (input.prevTxScript) {
prevTxScript = bscript.fromASM(input.prevTxScript)
}
txb.addInput(prevTx, input.vout, input.sequence, prevTxScript)
})
f.outputs.forEach(output => {
if (output.address) {
txb.addOutput(output.address, output.value)
} else {
txb.addOutput(bscript.fromASM(output.script), output.value)
}
})
if (dontSign) return txb
return constructSign(f, txb, useOldSignArgs)
}
// TODO: Remove loop in v6
for (const useOldSignArgs of [ false, true ]) {
// Search for "useOldSignArgs"
// to find the second part of this console.warn replace
let consoleWarn;
if (useOldSignArgs) {
consoleWarn = console.warn;
// Silence console.warn during these tests
console.warn = () => undefined;
}
describe(`TransactionBuilder: useOldSignArgs === ${useOldSignArgs}`, () => {
// constants
const keyPair = ECPair.fromPrivateKey(Buffer.from('0000000000000000000000000000000000000000000000000000000000000001', 'hex'))
const scripts = [
'1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH',
'1cMh228HTCiwS8ZsaakH8A8wze1JR5ZsP'
].map(x => {
return baddress.toOutputScript(x)
})
const txHash = Buffer.from('0e7cea811c0be9f73c0aca591034396e7264473fc25c1ca45195d7417b36cbe2', 'hex')
describe('fromTransaction', () => {
fixtures.valid.build.forEach(f => {
it('returns TransactionBuilder, with ' + f.description, () => {
const network = NETWORKS[f.network || 'bitcoin']
const tx = Transaction.fromHex(f.txHex)
const txb = TransactionBuilder.fromTransaction(tx, network)
const txAfter = f.incomplete ? txb.buildIncomplete() : txb.build()
assert.strictEqual(txAfter.toHex(), f.txHex)
assert.strictEqual(txb.network, network)
})
})
fixtures.valid.fromTransaction.forEach(f => {
it('returns TransactionBuilder, with ' + f.description, () => {
const tx = new Transaction()
f.inputs.forEach(input => {
const txHash2 = Buffer.from(input.txId, 'hex').reverse()
tx.addInput(txHash2, input.vout, undefined, bscript.fromASM(input.scriptSig))
})
f.outputs.forEach(output => {
tx.addOutput(bscript.fromASM(output.script), output.value)
})
const txb = TransactionBuilder.fromTransaction(tx)
const txAfter = f.incomplete ? txb.buildIncomplete() : txb.build()
txAfter.ins.forEach((input, i) => {
assert.strictEqual(bscript.toASM(input.script), f.inputs[i].scriptSigAfter)
})
txAfter.outs.forEach((output, i) => {
assert.strictEqual(bscript.toASM(output.script), f.outputs[i].script)
})
})
})
fixtures.valid.fromTransactionSequential.forEach(f => {
it('with ' + f.description, () => {
const network = NETWORKS[f.network]
const tx = Transaction.fromHex(f.txHex)
const txb = TransactionBuilder.fromTransaction(tx, network)
tx.ins.forEach((input, i) => {
assert.strictEqual(bscript.toASM(input.script), f.inputs[i].scriptSig)
})
constructSign(f, txb, useOldSignArgs)
const txAfter = f.incomplete ? txb.buildIncomplete() : txb.build()
txAfter.ins.forEach((input, i) => {
assert.strictEqual(bscript.toASM(input.script), f.inputs[i].scriptSigAfter)
})
assert.strictEqual(txAfter.toHex(), f.txHexAfter)
})
})
it('classifies transaction inputs', () => {
const tx = Transaction.fromHex(fixtures.valid.classification.hex)
const txb = TransactionBuilder.fromTransaction(tx)
txb.__INPUTS.forEach(i => {
assert.strictEqual(i.prevOutType, 'scripthash')
assert.strictEqual(i.redeemScriptType, 'multisig')
})
})
fixtures.invalid.fromTransaction.forEach(f => {
it('throws ' + f.exception, () => {
const tx = Transaction.fromHex(f.txHex)
assert.throws(() => {
TransactionBuilder.fromTransaction(tx)
}, new RegExp(f.exception))
})
})
})
describe('addInput', () => {
let txb
beforeEach(() => {
txb = new TransactionBuilder()
})
it('accepts a txHash, index [and sequence number]', () => {
const vin = txb.addInput(txHash, 1, 54)
assert.strictEqual(vin, 0)
const txIn = txb.__TX.ins[0]
assert.strictEqual(txIn.hash, txHash)
assert.strictEqual(txIn.index, 1)
assert.strictEqual(txIn.sequence, 54)
assert.strictEqual(txb.__INPUTS[0].prevOutScript, undefined)
})
it('accepts a txHash, index [, sequence number and scriptPubKey]', () => {
const vin = txb.addInput(txHash, 1, 54, scripts[1])
assert.strictEqual(vin, 0)
const txIn = txb.__TX.ins[0]
assert.strictEqual(txIn.hash, txHash)
assert.strictEqual(txIn.index, 1)
assert.strictEqual(txIn.sequence, 54)
assert.strictEqual(txb.__INPUTS[0].prevOutScript, scripts[1])
})
it('accepts a prevTx, index [and sequence number]', () => {
const prevTx = new Transaction()
prevTx.addOutput(scripts[0], 0)
prevTx.addOutput(scripts[1], 1)
const vin = txb.addInput(prevTx, 1, 54)
assert.strictEqual(vin, 0)
const txIn = txb.__TX.ins[0]
assert.deepStrictEqual(txIn.hash, prevTx.getHash())
assert.strictEqual(txIn.index, 1)
assert.strictEqual(txIn.sequence, 54)
assert.strictEqual(txb.__INPUTS[0].prevOutScript, scripts[1])
})
it('returns the input index', () => {
assert.strictEqual(txb.addInput(txHash, 0), 0)
assert.strictEqual(txb.addInput(txHash, 1), 1)
})
it('throws if SIGHASH_ALL has been used to sign any existing scriptSigs', () => {
txb.addInput(txHash, 0)
txb.addOutput(scripts[0], 1000)
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
})
assert.throws(() => {
txb.addInput(txHash, 0)
}, /No, this would invalidate signatures/)
})
})
describe('addOutput', () => {
let txb
beforeEach(() => {
txb = new TransactionBuilder()
})
it('accepts an address string and value', () => {
const { address } = payments.p2pkh({ pubkey: keyPair.publicKey })
const vout = txb.addOutput(address, 1000)
assert.strictEqual(vout, 0)
const txout = txb.__TX.outs[0]
assert.deepStrictEqual(txout.script, scripts[0])
assert.strictEqual(txout.value, 1000)
})
it('accepts a ScriptPubKey and value', () => {
const vout = txb.addOutput(scripts[0], 1000)
assert.strictEqual(vout, 0)
const txout = txb.__TX.outs[0]
assert.deepStrictEqual(txout.script, scripts[0])
assert.strictEqual(txout.value, 1000)
})
it('throws if address is of the wrong network', () => {
assert.throws(() => {
txb.addOutput('2NGHjvjw83pcVFgMcA7QvSMh2c246rxLVz9', 1000)
}, /2NGHjvjw83pcVFgMcA7QvSMh2c246rxLVz9 has no matching Script/)
})
it('add second output after signed first input with SIGHASH_NONE', () => {
txb.addInput(txHash, 0)
txb.addOutput(scripts[0], 2000)
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
hashType: Transaction.SIGHASH_NONE,
})
assert.strictEqual(txb.addOutput(scripts[1], 9000), 1)
})
it('add first output after signed first input with SIGHASH_NONE', () => {
txb.addInput(txHash, 0)
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
hashType: Transaction.SIGHASH_NONE,
})
assert.strictEqual(txb.addOutput(scripts[0], 2000), 0)
})
it('add second output after signed first input with SIGHASH_SINGLE', () => {
txb.addInput(txHash, 0)
txb.addOutput(scripts[0], 2000)
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
hashType: Transaction.SIGHASH_SINGLE,
})
assert.strictEqual(txb.addOutput(scripts[1], 9000), 1)
})
it('add first output after signed first input with SIGHASH_SINGLE', () => {
txb.addInput(txHash, 0)
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
hashType: Transaction.SIGHASH_SINGLE,
})
assert.throws(() => {
txb.addOutput(scripts[0], 2000)
}, /No, this would invalidate signatures/)
})
it('throws if SIGHASH_ALL has been used to sign any existing scriptSigs', () => {
txb.addInput(txHash, 0)
txb.addOutput(scripts[0], 2000)
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
})
assert.throws(() => {
txb.addOutput(scripts[1], 9000)
}, /No, this would invalidate signatures/)
})
})
describe('setLockTime', () => {
it('throws if if there exist any scriptSigs', () => {
const txb = new TransactionBuilder()
txb.addInput(txHash, 0)
txb.addOutput(scripts[0], 100)
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
})
assert.throws(() => {
txb.setLockTime(65535)
}, /No, this would invalidate signatures/)
})
})
describe('sign', () => {
it('supports the alternative abstract interface { publicKey, sign }', () => {
const keyPair = {
publicKey: ECPair.makeRandom({ rng: () => { return Buffer.alloc(32, 1) } }).publicKey,
sign: hash => { return Buffer.alloc(64, 0x5f) }
}
const txb = new TransactionBuilder()
txb.setVersion(1)
txb.addInput('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 1)
txb.addOutput('1111111111111111111114oLvT2', 100000)
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
})
assert.strictEqual(txb.build().toHex(), '0100000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff010000006a47304402205f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f02205f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f0121031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078fffffffff01a0860100000000001976a914000000000000000000000000000000000000000088ac00000000')
})
it('supports low R signature signing', () => {
let txb = new TransactionBuilder()
txb.setVersion(1)
txb.addInput('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 1)
txb.addOutput('1111111111111111111114oLvT2', 100000)
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
})
// high R
assert.strictEqual(txb.build().toHex(), '0100000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff010000006b483045022100b872677f35c9c14ad9c41d83649fb049250f32574e0b2547d67e209ed14ff05d022059b36ad058be54e887a1a311d5c393cb4941f6b93a0b090845ec67094de8972b01210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ffffffff01a0860100000000001976a914000000000000000000000000000000000000000088ac00000000')
txb = new TransactionBuilder()
txb.setVersion(1)
txb.addInput('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 1)
txb.addOutput('1111111111111111111114oLvT2', 100000)
txb.setLowR()
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
})
// low R
assert.strictEqual(txb.build().toHex(), '0100000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff010000006a473044022012a601efa8756ebe83e9ac7a7db061c3147e3b49d8be67685799fe51a4c8c62f02204d568d301d5ce14af390d566d4fd50e7b8ee48e71ec67786c029e721194dae3601210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ffffffff01a0860100000000001976a914000000000000000000000000000000000000000088ac00000000')
})
it('fails when missing required arguments', () => {
let txb = new TransactionBuilder()
txb.setVersion(1)
txb.addInput('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 1)
txb.addOutput('1111111111111111111114oLvT2', 100000)
assert.throws(() => {
txb.sign()
}, /TransactionBuilder sign first arg must be TxbSignArg or number/)
assert.throws(() => {
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 1,
keyPair,
})
}, /No input at index: 1/)
assert.throws(() => {
txb.sign({
prevOutScriptType: 'p2pkh',
keyPair,
})
}, /sign must include vin parameter as Number \(input index\)/)
assert.throws(() => {
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair: {},
})
}, /sign must include keyPair parameter as Signer interface/)
assert.throws(() => {
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
hashType: 'string',
})
}, /sign hashType parameter must be a number/)
if (useOldSignArgs) {
assert.throws(() => {
txb.sign(0)
}, /sign requires keypair/)
}
})
fixtures.invalid.sign.forEach(f => {
it('throws ' + f.exception + (f.description ? ' (' + f.description + ')' : ''), () => {
const txb = construct(f, true)
let threw = false
f.inputs.forEach((input, index) => {
input.signs.forEach(sign => {
const keyPairNetwork = NETWORKS[sign.network || f.network]
const keyPair2 = ECPair.fromWIF(sign.keyPair, keyPairNetwork)
let redeemScript
let witnessScript
if (sign.redeemScript) {
redeemScript = bscript.fromASM(sign.redeemScript)
}
if (sign.witnessScript) {
witnessScript = bscript.fromASM(sign.witnessScript)
}
if (sign.throws) {
assert.throws(() => {
txb.sign({
prevOutScriptType: sign.prevOutScriptType,
vin: index,
keyPair: keyPair2,
redeemScript,
hashType: sign.hashType,
witnessValue: sign.value,
witnessScript,
})
}, new RegExp(f.exception))
threw = true
} else {
txb.sign({
prevOutScriptType: sign.prevOutScriptType,
vin: index,
keyPair: keyPair2,
redeemScript,
hashType: sign.hashType,
witnessValue: sign.value,
witnessScript,
})
}
})
})
assert.strictEqual(threw, true)
})
})
})
describe('build', () => {
fixtures.valid.build.forEach(f => {
it('builds "' + f.description + '"', () => {
const txb = construct(f, undefined, useOldSignArgs)
const tx = f.incomplete ? txb.buildIncomplete() : txb.build()
assert.strictEqual(tx.toHex(), f.txHex)
})
})
// TODO: remove duplicate test code
fixtures.invalid.build.forEach(f => {
describe('for ' + (f.description || f.exception), () => {
it('throws ' + f.exception, () => {
assert.throws(() => {
let txb
if (f.txHex) {
txb = TransactionBuilder.fromTransaction(Transaction.fromHex(f.txHex))
} else {
txb = construct(f, undefined, useOldSignArgs)
}
txb.build()
}, new RegExp(f.exception))
})
// if throws on incomplete too, enforce that
if (f.incomplete) {
it('throws ' + f.exception, () => {
assert.throws(() => {
let txb
if (f.txHex) {
txb = TransactionBuilder.fromTransaction(Transaction.fromHex(f.txHex))
} else {
txb = construct(f, undefined, useOldSignArgs)
}
txb.buildIncomplete()
}, new RegExp(f.exception))
})
} else {
it('does not throw if buildIncomplete', () => {
let txb
if (f.txHex) {
txb = TransactionBuilder.fromTransaction(Transaction.fromHex(f.txHex))
} else {
txb = construct(f, undefined, useOldSignArgs)
}
txb.buildIncomplete()
})
}
})
})
it('for incomplete with 0 signatures', () => {
const randomTxData = '0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100aa5d8aa40a90f23ce2c3d11bc845ca4a12acd99cbea37de6b9f6d86edebba8cb022022dedc2aa0a255f74d04c0b76ece2d7c691f9dd11a64a8ac49f62a99c3a05f9d01232103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ac00000000'
const randomAddress = '1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH'
const randomTx = Transaction.fromHex(randomTxData)
let tx = new TransactionBuilder()
tx.addInput(randomTx, 0)
tx.addOutput(randomAddress, 1000)
tx = tx.buildIncomplete()
assert(tx)
})
it('for incomplete P2SH with 0 signatures', () => {
const inp = Buffer.from('010000000173120703f67318aef51f7251272a6816d3f7523bb25e34b136d80be959391c100000000000ffffffff0100c817a80400000017a91471a8ec07ff69c6c4fee489184c462a9b1b9237488700000000', 'hex') // arbitrary P2SH input
const inpTx = Transaction.fromBuffer(inp)
const txb = new TransactionBuilder(NETWORKS.testnet)
txb.addInput(inpTx, 0)
txb.addOutput('2NAkqp5xffoomp5RLBcakuGpZ12GU4twdz4', 1e8) // arbitrary output
txb.buildIncomplete()
})
it('for incomplete P2WPKH with 0 signatures', () => {
const inp = Buffer.from('010000000173120703f67318aef51f7251272a6816d3f7523bb25e34b136d80be959391c100000000000ffffffff0100c817a8040000001600141a15805e1f4040c9f68ccc887fca2e63547d794b00000000', 'hex')
const inpTx = Transaction.fromBuffer(inp)
const txb = new TransactionBuilder(NETWORKS.testnet)
txb.addInput(inpTx, 0)
txb.addOutput('2NAkqp5xffoomp5RLBcakuGpZ12GU4twdz4', 1e8) // arbitrary output
txb.buildIncomplete()
})
it('for incomplete P2WSH with 0 signatures', () => {
const inpTx = Transaction.fromBuffer(Buffer.from('010000000173120703f67318aef51f7251272a6816d3f7523bb25e34b136d80be959391c100000000000ffffffff0100c817a80400000022002072df76fcc0b231b94bdf7d8c25d7eef4716597818d211e19ade7813bff7a250200000000', 'hex'))
const txb = new TransactionBuilder(NETWORKS.testnet)
txb.addInput(inpTx, 0)
txb.addOutput('2NAkqp5xffoomp5RLBcakuGpZ12GU4twdz4', 1e8) // arbitrary output
txb.buildIncomplete()
})
})
describe('multisig', () => {
fixtures.valid.multisig.forEach(f => {
it(f.description, () => {
const network = NETWORKS[f.network]
let txb = construct(f, true)
let tx
f.inputs.forEach((input, i) => {
const redeemScript = bscript.fromASM(input.redeemScript)
input.signs.forEach(sign => {
// rebuild the transaction each-time after the first
if (tx) {
// manually override the scriptSig?
if (sign.scriptSigBefore) {
tx.ins[i].script = bscript.fromASM(sign.scriptSigBefore)
}
// rebuild
txb = TransactionBuilder.fromTransaction(tx, network)
}
const keyPair2 = ECPair.fromWIF(sign.keyPair, network)
txb.sign({
prevOutScriptType: sign.prevOutScriptType,
vin: i,
keyPair: keyPair2,
redeemScript,
hashType: sign.hashType,
})
// update the tx
tx = txb.buildIncomplete()
// now verify the serialized scriptSig is as expected
assert.strictEqual(bscript.toASM(tx.ins[i].script), sign.scriptSig)
})
})
tx = txb.build()
assert.strictEqual(tx.toHex(), f.txHex)
})
})
})
describe('various edge case', () => {
const network = NETWORKS.testnet
it('should warn of high fee for segwit transaction based on VSize, not Size', () => {
const rawtx = '01000000000104fdaac89627208b4733484ca56bc291f4cf4fa8d7c5f29893c52b46788a0a' +
'1df90000000000fffffffffdaac89627208b4733484ca56bc291f4cf4fa8d7c5f29893c52b46788a0a1df9' +
'0100000000ffffffffa2ef7aaab316a3e5b5b0a78d1d35c774b95a079f9f0c762277a49caf1f26bca40000' +
'000000ffffffffa2ef7aaab316a3e5b5b0a78d1d35c774b95a079f9f0c762277a49caf1f26bca401000000' +
'00ffffffff0100040000000000001976a914cf307285359ab7ef6a2daa0522c7908ddf5fe7a988ac024730' +
'440220113324438816338406841775e079b04c50d04f241da652a4035b1017ea1ecf5502205802191eb49c' +
'54bf2a5667aea72e51c3ca92085efc60f12d1ebda3a64aff343201210283409659355b6d1cc3c32decd5d5' +
'61abaac86c37a353b52895a5e6c196d6f44802483045022100dc2892874e6d8708e3f5a058c5c9263cdf03' +
'969492270f89ee4933caf6daf8bb0220391dfe61a002709b63b9d64422d3db09b727839d1287e10a128a5d' +
'b52a82309301210283409659355b6d1cc3c32decd5d561abaac86c37a353b52895a5e6c196d6f448024830' +
'450221009e3ed3a6ae93a018f443257b43e47b55cf7f7f3547d8807178072234686b22160220576121cfe6' +
'77c7eddf5575ea0a7c926247df6eca723c4f85df306e8bc08ea2df01210283409659355b6d1cc3c32decd5' +
'd561abaac86c37a353b52895a5e6c196d6f44802473044022007be81ffd4297441ab10e740fc9bab9545a2' +
'194a565cd6aa4cc38b8eaffa343402201c5b4b61d73fa38e49c1ee68cc0e6dfd2f5dae453dd86eb142e87a' +
'0bafb1bc8401210283409659355b6d1cc3c32decd5d561abaac86c37a353b52895a5e6c196d6f44800000000'
const txb = TransactionBuilder.fromTransaction(Transaction.fromHex(rawtx))
txb.__INPUTS[0].value = 241530
txb.__INPUTS[1].value = 241530
txb.__INPUTS[2].value = 248920
txb.__INPUTS[3].value = 248920
assert.throws(() => {
txb.build()
}, new RegExp('Transaction has absurd fees'))
})
it('should classify witness inputs with witness = true during multisigning', () => {
const keyPair = ECPair.fromWIF('cRAwuVuVSBZMPu7hdrYvMCZ8eevzmkExjFbaBLhqnDdrezxN3nTS', network)
const witnessScript = Buffer.from('522102bbbd6eb01efcbe4bd9664b886f26f69de5afcb2e479d72596c8bf21929e352e22102d9c3f7180ef13ec5267723c9c2ffab56a4215241f837502ea8977c8532b9ea1952ae', 'hex')
const redeemScript = Buffer.from('002024376a0a9abab599d0e028248d48ebe817bc899efcffa1cd2984d67289daf5af', 'hex')
const scriptPubKey = Buffer.from('a914b64f1a3eacc1c8515592a6f10457e8ff90e4db6a87', 'hex')
const txb = new TransactionBuilder(network)
txb.setVersion(1)
txb.addInput('a4696c4b0cd27ec2e173ab1fa7d1cc639a98ee237cec95a77ca7ff4145791529', 1, 0xffffffff, scriptPubKey)
txb.addOutput(scriptPubKey, 99000)
txb.sign({
prevOutScriptType: 'p2sh-p2wsh-p2ms',
vin: 0,
keyPair,
redeemScript,
witnessValue: 100000,
witnessScript,
})
// 2-of-2 signed only once
const tx = txb.buildIncomplete()
// Only input is segwit, so txid should be accurate with the final tx
assert.strictEqual(tx.getId(), 'f15d0a65b21b4471405b21a099f8b18e1ae4d46d55efbd0f4766cf11ad6cb821')
const txHex = tx.toHex()
TransactionBuilder.fromTransaction(Transaction.fromHex(txHex))
})
it('should handle badly pre-filled OP_0s', () => {
// OP_0 is used where a signature is missing
const redeemScripSig = bscript.fromASM('OP_0 OP_0 3045022100daf0f4f3339d9fbab42b098045c1e4958ee3b308f4ae17be80b63808558d0adb02202f07e3d1f79dc8da285ae0d7f68083d769c11f5621ebd9691d6b48c0d4283d7d01 52410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b84104c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a4104f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e67253ae')
const redeemScript = bscript.fromASM('OP_2 0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 04c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a 04f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e672 OP_3 OP_CHECKMULTISIG')
const tx = new Transaction()
tx.addInput(Buffer.from('cff58855426469d0ef16442ee9c644c4fb13832467bcbc3173168a7916f07149', 'hex'), 0, undefined, redeemScripSig)
tx.addOutput(Buffer.from('76a914aa4d7985c57e011a8b3dd8e0e5a73aaef41629c588ac', 'hex'), 1000)
// now import the Transaction
const txb = TransactionBuilder.fromTransaction(tx, NETWORKS.testnet)
const keyPair2 = ECPair.fromWIF('91avARGdfge8E4tZfYLoxeJ5sGBdNJQH4kvjJoQFacbgx3cTMqe', network)
txb.sign({
prevOutScriptType: 'p2sh-p2ms',
vin: 0,
keyPair: keyPair2,
redeemScript,
})
const tx2 = txb.build()
assert.strictEqual(tx2.getId(), 'eab59618a564e361adef6d918bd792903c3d41bcf1220137364fb847880467f9')
assert.strictEqual(bscript.toASM(tx2.ins[0].script), 'OP_0 3045022100daf0f4f3339d9fbab42b098045c1e4958ee3b308f4ae17be80b63808558d0adb02202f07e3d1f79dc8da285ae0d7f68083d769c11f5621ebd9691d6b48c0d4283d7d01 3045022100a346c61738304eac5e7702188764d19cdf68f4466196729db096d6c87ce18cdd022018c0e8ad03054b0e7e235cda6bedecf35881d7aa7d94ff425a8ace7220f38af001 52410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b84104c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a4104f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e67253ae')
})
it('should not classify blank scripts as nonstandard', () => {
let txb = new TransactionBuilder()
txb.setVersion(1)
txb.addInput('aa94ab02c182214f090e99a0d57021caffd0f195a81c24602b1028b130b63e31', 0)
const incomplete = txb.buildIncomplete().toHex()
const keyPair = ECPair.fromWIF('L1uyy5qTuGrVXrmrsvHWHgVzW9kKdrp27wBC7Vs6nZDTF2BRUVwy')
// sign, as expected
txb.addOutput('1Gokm82v6DmtwKEB8AiVhm82hyFSsEvBDK', 15000)
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
})
const txId = txb.build().getId()
assert.strictEqual(txId, '54f097315acbaedb92a95455da3368eb45981cdae5ffbc387a9afc872c0f29b3')
// and, repeat
txb = TransactionBuilder.fromTransaction(Transaction.fromHex(incomplete))
txb.addOutput('1Gokm82v6DmtwKEB8AiVhm82hyFSsEvBDK', 15000)
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
})
const txId2 = txb.build().getId()
assert.strictEqual(txId, txId2)
// TODO: Remove me in v6
if (useOldSignArgs) {
console.warn = consoleWarn;
}
})
})
})
}

View file

@ -0,0 +1,947 @@
import * as assert from 'assert';
import { beforeEach, describe, it } from 'mocha';
import * as baddress from '../src/address';
import * as bscript from '../src/script';
import * as payments from '../src/payments';
import {
ECPair,
networks as NETWORKS,
Transaction,
TransactionBuilder,
} from '..';
console.warn = () => {}; // Silence the Deprecation Warning
import * as fixtures from './fixtures/transaction_builder.json';
function constructSign(
f: any,
txb: any,
useOldSignArgs: any,
): TransactionBuilder {
const network = (NETWORKS as any)[f.network];
const stages = f.stages && f.stages.concat();
f.inputs.forEach((input: any, index: number) => {
if (!input.signs) return;
input.signs.forEach((sign: any) => {
const keyPair = ECPair.fromWIF(sign.keyPair, network);
let redeemScript;
let witnessScript;
let witnessValue;
if (sign.redeemScript) {
redeemScript = bscript.fromASM(sign.redeemScript);
}
if (sign.value) {
witnessValue = sign.value;
}
if (sign.witnessScript) {
witnessScript = bscript.fromASM(sign.witnessScript);
}
if (useOldSignArgs) {
// DEPRECATED: v6 will remove this interface
txb.sign(
index,
keyPair,
redeemScript,
sign.hashType,
witnessValue,
witnessScript,
);
} else {
// prevOutScriptType is required, see /ts_src/transaction_builder.ts
// The PREVOUT_TYPES constant is a Set with all possible values.
txb.sign({
prevOutScriptType: sign.prevOutScriptType,
vin: index,
keyPair,
redeemScript,
hashType: sign.hashType,
witnessValue,
witnessScript,
});
}
if (sign.stage) {
const tx = txb.buildIncomplete();
assert.strictEqual(tx.toHex(), stages.shift());
txb = TransactionBuilder.fromTransaction(tx, network);
}
});
});
return txb;
}
function construct(
f: any,
dontSign?: any,
useOldSignArgs?: any,
): TransactionBuilder {
const network = (NETWORKS as any)[f.network];
const txb = new TransactionBuilder(network);
if (Number.isFinite(f.version)) txb.setVersion(f.version);
if (f.locktime !== undefined) txb.setLockTime(f.locktime);
f.inputs.forEach((input: any) => {
let prevTx;
if (input.txRaw) {
const constructed = construct(input.txRaw);
if (input.txRaw.incomplete) prevTx = constructed.buildIncomplete();
else prevTx = constructed.build();
} else if (input.txHex) {
prevTx = Transaction.fromHex(input.txHex);
} else {
prevTx = input.txId;
}
let prevTxScript;
if (input.prevTxScript) {
prevTxScript = bscript.fromASM(input.prevTxScript);
}
txb.addInput(prevTx, input.vout, input.sequence, prevTxScript);
});
f.outputs.forEach((output: any) => {
if (output.address) {
txb.addOutput(output.address, output.value);
} else {
txb.addOutput(bscript.fromASM(output.script), output.value);
}
});
if (dontSign) return txb;
return constructSign(f, txb, useOldSignArgs);
}
// TODO: Remove loop in v6
for (const useOldSignArgs of [false, true]) {
// Search for "useOldSignArgs"
// to find the second part of this console.warn replace
let consoleWarn: any;
if (useOldSignArgs) {
consoleWarn = console.warn;
// Silence console.warn during these tests
console.warn = () => undefined;
}
describe(`TransactionBuilder: useOldSignArgs === ${useOldSignArgs}`, () => {
// constants
const keyPair = ECPair.fromPrivateKey(
Buffer.from(
'0000000000000000000000000000000000000000000000000000000000000001',
'hex',
),
);
const scripts = [
'1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH',
'1cMh228HTCiwS8ZsaakH8A8wze1JR5ZsP',
].map(x => {
return baddress.toOutputScript(x);
});
const txHash = Buffer.from(
'0e7cea811c0be9f73c0aca591034396e7264473fc25c1ca45195d7417b36cbe2',
'hex',
);
describe('fromTransaction', () => {
fixtures.valid.build.forEach(f => {
it('returns TransactionBuilder, with ' + f.description, () => {
const network = (NETWORKS as any)[f.network || 'bitcoin'];
const tx = Transaction.fromHex(f.txHex);
const txb = TransactionBuilder.fromTransaction(tx, network);
const txAfter = f.incomplete ? txb.buildIncomplete() : txb.build();
assert.strictEqual(txAfter.toHex(), f.txHex);
assert.strictEqual(txb.network, network);
});
});
fixtures.valid.fromTransaction.forEach(f => {
it('returns TransactionBuilder, with ' + f.description, () => {
const tx = new Transaction();
f.inputs.forEach(input => {
const txHash2 = Buffer.from(input.txId, 'hex').reverse() as Buffer;
tx.addInput(
txHash2,
input.vout,
undefined,
bscript.fromASM(input.scriptSig),
);
});
f.outputs.forEach(output => {
tx.addOutput(bscript.fromASM(output.script), output.value);
});
const txb = TransactionBuilder.fromTransaction(tx);
const txAfter = f.incomplete ? txb.buildIncomplete() : txb.build();
txAfter.ins.forEach((input, i) => {
assert.strictEqual(
bscript.toASM(input.script),
f.inputs[i].scriptSigAfter,
);
});
txAfter.outs.forEach((output, i) => {
assert.strictEqual(
bscript.toASM(output.script),
f.outputs[i].script,
);
});
});
});
fixtures.valid.fromTransactionSequential.forEach(f => {
it('with ' + f.description, () => {
const network = (NETWORKS as any)[f.network];
const tx = Transaction.fromHex(f.txHex);
const txb = TransactionBuilder.fromTransaction(tx, network);
tx.ins.forEach((input, i) => {
assert.strictEqual(
bscript.toASM(input.script),
f.inputs[i].scriptSig,
);
});
constructSign(f, txb, useOldSignArgs);
const txAfter = f.incomplete ? txb.buildIncomplete() : txb.build();
txAfter.ins.forEach((input, i) => {
assert.strictEqual(
bscript.toASM(input.script),
f.inputs[i].scriptSigAfter,
);
});
assert.strictEqual(txAfter.toHex(), f.txHexAfter);
});
});
it('classifies transaction inputs', () => {
const tx = Transaction.fromHex(fixtures.valid.classification.hex);
const txb = TransactionBuilder.fromTransaction(tx);
(txb as any).__INPUTS.forEach((i: any) => {
assert.strictEqual(i.prevOutType, 'scripthash');
assert.strictEqual(i.redeemScriptType, 'multisig');
});
});
fixtures.invalid.fromTransaction.forEach(f => {
it('throws ' + f.exception, () => {
const tx = Transaction.fromHex(f.txHex);
assert.throws(() => {
TransactionBuilder.fromTransaction(tx);
}, new RegExp(f.exception));
});
});
});
describe('addInput', () => {
let txb: TransactionBuilder;
beforeEach(() => {
txb = new TransactionBuilder();
});
it('accepts a txHash, index [and sequence number]', () => {
const vin = txb.addInput(txHash, 1, 54);
assert.strictEqual(vin, 0);
const txIn = (txb as any).__TX.ins[0];
assert.strictEqual(txIn.hash, txHash);
assert.strictEqual(txIn.index, 1);
assert.strictEqual(txIn.sequence, 54);
assert.strictEqual((txb as any).__INPUTS[0].prevOutScript, undefined);
});
it('accepts a txHash, index [, sequence number and scriptPubKey]', () => {
const vin = txb.addInput(txHash, 1, 54, scripts[1]);
assert.strictEqual(vin, 0);
const txIn = (txb as any).__TX.ins[0];
assert.strictEqual(txIn.hash, txHash);
assert.strictEqual(txIn.index, 1);
assert.strictEqual(txIn.sequence, 54);
assert.strictEqual((txb as any).__INPUTS[0].prevOutScript, scripts[1]);
});
it('accepts a prevTx, index [and sequence number]', () => {
const prevTx = new Transaction();
prevTx.addOutput(scripts[0], 0);
prevTx.addOutput(scripts[1], 1);
const vin = txb.addInput(prevTx, 1, 54);
assert.strictEqual(vin, 0);
const txIn = (txb as any).__TX.ins[0];
assert.deepStrictEqual(txIn.hash, prevTx.getHash());
assert.strictEqual(txIn.index, 1);
assert.strictEqual(txIn.sequence, 54);
assert.strictEqual((txb as any).__INPUTS[0].prevOutScript, scripts[1]);
});
it('returns the input index', () => {
assert.strictEqual(txb.addInput(txHash, 0), 0);
assert.strictEqual(txb.addInput(txHash, 1), 1);
});
it('throws if SIGHASH_ALL has been used to sign any existing scriptSigs', () => {
txb.addInput(txHash, 0);
txb.addOutput(scripts[0], 1000);
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
});
assert.throws(() => {
txb.addInput(txHash, 0);
}, /No, this would invalidate signatures/);
});
});
describe('addOutput', () => {
let txb: TransactionBuilder;
beforeEach(() => {
txb = new TransactionBuilder();
});
it('accepts an address string and value', () => {
const { address } = payments.p2pkh({ pubkey: keyPair.publicKey });
const vout = txb.addOutput(address!, 1000);
assert.strictEqual(vout, 0);
const txout = (txb as any).__TX.outs[0];
assert.deepStrictEqual(txout.script, scripts[0]);
assert.strictEqual(txout.value, 1000);
});
it('accepts a ScriptPubKey and value', () => {
const vout = txb.addOutput(scripts[0], 1000);
assert.strictEqual(vout, 0);
const txout = (txb as any).__TX.outs[0];
assert.deepStrictEqual(txout.script, scripts[0]);
assert.strictEqual(txout.value, 1000);
});
it('throws if address is of the wrong network', () => {
assert.throws(() => {
txb.addOutput('2NGHjvjw83pcVFgMcA7QvSMh2c246rxLVz9', 1000);
}, /2NGHjvjw83pcVFgMcA7QvSMh2c246rxLVz9 has no matching Script/);
});
it('add second output after signed first input with SIGHASH_NONE', () => {
txb.addInput(txHash, 0);
txb.addOutput(scripts[0], 2000);
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
hashType: Transaction.SIGHASH_NONE,
});
assert.strictEqual(txb.addOutput(scripts[1], 9000), 1);
});
it('add first output after signed first input with SIGHASH_NONE', () => {
txb.addInput(txHash, 0);
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
hashType: Transaction.SIGHASH_NONE,
});
assert.strictEqual(txb.addOutput(scripts[0], 2000), 0);
});
it('add second output after signed first input with SIGHASH_SINGLE', () => {
txb.addInput(txHash, 0);
txb.addOutput(scripts[0], 2000);
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
hashType: Transaction.SIGHASH_SINGLE,
});
assert.strictEqual(txb.addOutput(scripts[1], 9000), 1);
});
it('add first output after signed first input with SIGHASH_SINGLE', () => {
txb.addInput(txHash, 0);
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
hashType: Transaction.SIGHASH_SINGLE,
});
assert.throws(() => {
txb.addOutput(scripts[0], 2000);
}, /No, this would invalidate signatures/);
});
it('throws if SIGHASH_ALL has been used to sign any existing scriptSigs', () => {
txb.addInput(txHash, 0);
txb.addOutput(scripts[0], 2000);
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
});
assert.throws(() => {
txb.addOutput(scripts[1], 9000);
}, /No, this would invalidate signatures/);
});
});
describe('setLockTime', () => {
it('throws if if there exist any scriptSigs', () => {
const txb = new TransactionBuilder();
txb.addInput(txHash, 0);
txb.addOutput(scripts[0], 100);
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
});
assert.throws(() => {
txb.setLockTime(65535);
}, /No, this would invalidate signatures/);
});
});
describe('sign', () => {
it('supports the alternative abstract interface { publicKey, sign }', () => {
const keyPair = {
publicKey: ECPair.makeRandom({
rng: () => {
return Buffer.alloc(32, 1);
},
}).publicKey,
sign: () => {
return Buffer.alloc(64, 0x5f);
},
};
const txb = new TransactionBuilder();
txb.setVersion(1);
txb.addInput(
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
1,
);
txb.addOutput('1111111111111111111114oLvT2', 100000);
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
});
assert.strictEqual(
txb.build().toHex(),
'0100000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff010000006a47304402205f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f02205f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f0121031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078fffffffff01a0860100000000001976a914000000000000000000000000000000000000000088ac00000000',
);
});
it('supports low R signature signing', () => {
let txb = new TransactionBuilder();
txb.setVersion(1);
txb.addInput(
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
1,
);
txb.addOutput('1111111111111111111114oLvT2', 100000);
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
});
// high R
assert.strictEqual(
txb.build().toHex(),
'0100000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff010000006b483045022100b872677f35c9c14ad9c41d83649fb049250f32574e0b2547d67e209ed14ff05d022059b36ad058be54e887a1a311d5c393cb4941f6b93a0b090845ec67094de8972b01210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ffffffff01a0860100000000001976a914000000000000000000000000000000000000000088ac00000000',
);
txb = new TransactionBuilder();
txb.setVersion(1);
txb.addInput(
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
1,
);
txb.addOutput('1111111111111111111114oLvT2', 100000);
txb.setLowR();
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
});
// low R
assert.strictEqual(
txb.build().toHex(),
'0100000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff010000006a473044022012a601efa8756ebe83e9ac7a7db061c3147e3b49d8be67685799fe51a4c8c62f02204d568d301d5ce14af390d566d4fd50e7b8ee48e71ec67786c029e721194dae3601210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ffffffff01a0860100000000001976a914000000000000000000000000000000000000000088ac00000000',
);
});
it('fails when missing required arguments', () => {
let txb = new TransactionBuilder();
txb.setVersion(1);
txb.addInput(
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
1,
);
txb.addOutput('1111111111111111111114oLvT2', 100000);
assert.throws(() => {
(txb as any).sign();
}, /TransactionBuilder sign first arg must be TxbSignArg or number/);
assert.throws(() => {
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 1,
keyPair,
});
}, /No input at index: 1/);
assert.throws(() => {
(txb as any).sign({
prevOutScriptType: 'p2pkh',
keyPair,
});
}, /sign must include vin parameter as Number \(input index\)/);
assert.throws(() => {
(txb as any).sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair: {},
});
}, /sign must include keyPair parameter as Signer interface/);
assert.throws(() => {
(txb as any).sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
hashType: 'string',
});
}, /sign hashType parameter must be a number/);
if (useOldSignArgs) {
assert.throws(() => {
txb.sign(0);
}, /sign requires keypair/);
}
});
fixtures.invalid.sign.forEach(f => {
it(
'throws ' +
f.exception +
(f.description ? ' (' + f.description + ')' : ''),
() => {
const txb = construct(f, true);
let threw = false;
(f.inputs as any).forEach(
(input: any, index: number): void => {
input.signs.forEach((sign: any) => {
const keyPairNetwork = (NETWORKS as any)[
sign.network || f.network
];
const keyPair2 = ECPair.fromWIF(sign.keyPair, keyPairNetwork);
let redeemScript: Buffer | undefined;
let witnessScript: Buffer | undefined;
if (sign.redeemScript) {
redeemScript = bscript.fromASM(sign.redeemScript);
}
if (sign.witnessScript) {
witnessScript = bscript.fromASM(sign.witnessScript);
}
if (sign.throws) {
assert.throws(() => {
txb.sign({
prevOutScriptType: sign.prevOutScriptType,
vin: index,
keyPair: keyPair2,
redeemScript,
hashType: sign.hashType,
witnessValue: sign.value,
witnessScript,
});
}, new RegExp(f.exception));
threw = true;
} else {
txb.sign({
prevOutScriptType: sign.prevOutScriptType,
vin: index,
keyPair: keyPair2,
redeemScript,
hashType: sign.hashType,
witnessValue: sign.value,
witnessScript,
});
}
});
},
);
assert.strictEqual(threw, true);
},
);
});
});
describe('build', () => {
fixtures.valid.build.forEach(f => {
it('builds "' + f.description + '"', () => {
const txb = construct(f, undefined, useOldSignArgs);
const tx = f.incomplete ? txb.buildIncomplete() : txb.build();
assert.strictEqual(tx.toHex(), f.txHex);
});
});
// TODO: remove duplicate test code
fixtures.invalid.build.forEach(f => {
describe('for ' + (f.description || f.exception), () => {
it('throws ' + f.exception, () => {
assert.throws(() => {
let txb;
if (f.txHex) {
txb = TransactionBuilder.fromTransaction(
Transaction.fromHex(f.txHex),
);
} else {
txb = construct(f, undefined, useOldSignArgs);
}
txb.build();
}, new RegExp(f.exception));
});
// if throws on incomplete too, enforce that
if (f.incomplete) {
it('throws ' + f.exception, () => {
assert.throws(() => {
let txb;
if (f.txHex) {
txb = TransactionBuilder.fromTransaction(
Transaction.fromHex(f.txHex),
);
} else {
txb = construct(f, undefined, useOldSignArgs);
}
txb.buildIncomplete();
}, new RegExp(f.exception));
});
} else {
it('does not throw if buildIncomplete', () => {
let txb;
if (f.txHex) {
txb = TransactionBuilder.fromTransaction(
Transaction.fromHex(f.txHex),
);
} else {
txb = construct(f, undefined, useOldSignArgs);
}
txb.buildIncomplete();
});
}
});
});
it('for incomplete with 0 signatures', () => {
const randomTxData =
'0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100aa5d8aa40a90f23ce2c3d11bc845ca4a12acd99cbea37de6b9f6d86edebba8cb022022dedc2aa0a255f74d04c0b76ece2d7c691f9dd11a64a8ac49f62a99c3a05f9d01232103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ac00000000';
const randomAddress = '1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH';
const randomTx = Transaction.fromHex(randomTxData);
const txb = new TransactionBuilder();
txb.addInput(randomTx, 0);
txb.addOutput(randomAddress, 1000);
const tx = txb.buildIncomplete();
assert(tx);
});
it('for incomplete P2SH with 0 signatures', () => {
const inp = Buffer.from(
'010000000173120703f67318aef51f7251272a6816d3f7523bb25e34b136d80be959391c100000000000ffffffff0100c817a80400000017a91471a8ec07ff69c6c4fee489184c462a9b1b9237488700000000',
'hex',
); // arbitrary P2SH input
const inpTx = Transaction.fromBuffer(inp);
const txb = new TransactionBuilder(NETWORKS.testnet);
txb.addInput(inpTx, 0);
txb.addOutput('2NAkqp5xffoomp5RLBcakuGpZ12GU4twdz4', 1e8); // arbitrary output
txb.buildIncomplete();
});
it('for incomplete P2WPKH with 0 signatures', () => {
const inp = Buffer.from(
'010000000173120703f67318aef51f7251272a6816d3f7523bb25e34b136d80be959391c100000000000ffffffff0100c817a8040000001600141a15805e1f4040c9f68ccc887fca2e63547d794b00000000',
'hex',
);
const inpTx = Transaction.fromBuffer(inp);
const txb = new TransactionBuilder(NETWORKS.testnet);
txb.addInput(inpTx, 0);
txb.addOutput('2NAkqp5xffoomp5RLBcakuGpZ12GU4twdz4', 1e8); // arbitrary output
txb.buildIncomplete();
});
it('for incomplete P2WSH with 0 signatures', () => {
const inpTx = Transaction.fromBuffer(
Buffer.from(
'010000000173120703f67318aef51f7251272a6816d3f7523bb25e34b136d80be959391c100000000000ffffffff0100c817a80400000022002072df76fcc0b231b94bdf7d8c25d7eef4716597818d211e19ade7813bff7a250200000000',
'hex',
),
);
const txb = new TransactionBuilder(NETWORKS.testnet);
txb.addInput(inpTx, 0);
txb.addOutput('2NAkqp5xffoomp5RLBcakuGpZ12GU4twdz4', 1e8); // arbitrary output
txb.buildIncomplete();
});
});
describe('multisig', () => {
fixtures.valid.multisig.forEach(f => {
it(f.description, () => {
const network = (NETWORKS as any)[f.network];
let txb = construct(f, true);
let tx: Transaction;
f.inputs.forEach((input, i) => {
const redeemScript = bscript.fromASM(input.redeemScript);
input.signs.forEach(sign => {
// rebuild the transaction each-time after the first
if (tx) {
// manually override the scriptSig?
if (sign.scriptSigBefore) {
tx.ins[i].script = bscript.fromASM(sign.scriptSigBefore);
}
// rebuild
txb = TransactionBuilder.fromTransaction(tx, network);
}
const keyPair2 = ECPair.fromWIF(sign.keyPair, network);
txb.sign({
prevOutScriptType: sign.prevOutScriptType,
vin: i,
keyPair: keyPair2,
redeemScript,
hashType: (sign as any).hashType,
});
// update the tx
tx = txb.buildIncomplete();
// now verify the serialized scriptSig is as expected
assert.strictEqual(
bscript.toASM(tx.ins[i].script),
sign.scriptSig,
);
});
});
tx = txb.build();
assert.strictEqual(tx.toHex(), f.txHex);
});
});
});
describe('various edge case', () => {
const network = NETWORKS.testnet;
it('should warn of high fee for segwit transaction based on VSize, not Size', () => {
const rawtx =
'01000000000104fdaac89627208b4733484ca56bc291f4cf4fa8d7c5f29893c52b46788a0a' +
'1df90000000000fffffffffdaac89627208b4733484ca56bc291f4cf4fa8d7c5f29893c52b46788a0a1df9' +
'0100000000ffffffffa2ef7aaab316a3e5b5b0a78d1d35c774b95a079f9f0c762277a49caf1f26bca40000' +
'000000ffffffffa2ef7aaab316a3e5b5b0a78d1d35c774b95a079f9f0c762277a49caf1f26bca401000000' +
'00ffffffff0100040000000000001976a914cf307285359ab7ef6a2daa0522c7908ddf5fe7a988ac024730' +
'440220113324438816338406841775e079b04c50d04f241da652a4035b1017ea1ecf5502205802191eb49c' +
'54bf2a5667aea72e51c3ca92085efc60f12d1ebda3a64aff343201210283409659355b6d1cc3c32decd5d5' +
'61abaac86c37a353b52895a5e6c196d6f44802483045022100dc2892874e6d8708e3f5a058c5c9263cdf03' +
'969492270f89ee4933caf6daf8bb0220391dfe61a002709b63b9d64422d3db09b727839d1287e10a128a5d' +
'b52a82309301210283409659355b6d1cc3c32decd5d561abaac86c37a353b52895a5e6c196d6f448024830' +
'450221009e3ed3a6ae93a018f443257b43e47b55cf7f7f3547d8807178072234686b22160220576121cfe6' +
'77c7eddf5575ea0a7c926247df6eca723c4f85df306e8bc08ea2df01210283409659355b6d1cc3c32decd5' +
'd561abaac86c37a353b52895a5e6c196d6f44802473044022007be81ffd4297441ab10e740fc9bab9545a2' +
'194a565cd6aa4cc38b8eaffa343402201c5b4b61d73fa38e49c1ee68cc0e6dfd2f5dae453dd86eb142e87a' +
'0bafb1bc8401210283409659355b6d1cc3c32decd5d561abaac86c37a353b52895a5e6c196d6f44800000000';
const txb = TransactionBuilder.fromTransaction(
Transaction.fromHex(rawtx),
);
(txb as any).__INPUTS[0].value = 241530;
(txb as any).__INPUTS[1].value = 241530;
(txb as any).__INPUTS[2].value = 248920;
(txb as any).__INPUTS[3].value = 248920;
assert.throws(() => {
txb.build();
}, new RegExp('Transaction has absurd fees'));
});
it('should classify witness inputs with witness = true during multisigning', () => {
const keyPair = ECPair.fromWIF(
'cRAwuVuVSBZMPu7hdrYvMCZ8eevzmkExjFbaBLhqnDdrezxN3nTS',
network,
);
const witnessScript = Buffer.from(
'522102bbbd6eb01efcbe4bd9664b886f26f69de5afcb2e479d72596c8bf21929e352e22102d9c3f7180ef13ec5267723c9c2ffab56a4215241f837502ea8977c8532b9ea1952ae',
'hex',
);
const redeemScript = Buffer.from(
'002024376a0a9abab599d0e028248d48ebe817bc899efcffa1cd2984d67289daf5af',
'hex',
);
const scriptPubKey = Buffer.from(
'a914b64f1a3eacc1c8515592a6f10457e8ff90e4db6a87',
'hex',
);
const txb = new TransactionBuilder(network);
txb.setVersion(1);
txb.addInput(
'a4696c4b0cd27ec2e173ab1fa7d1cc639a98ee237cec95a77ca7ff4145791529',
1,
0xffffffff,
scriptPubKey,
);
txb.addOutput(scriptPubKey, 99000);
txb.sign({
prevOutScriptType: 'p2sh-p2wsh-p2ms',
vin: 0,
keyPair,
redeemScript,
witnessValue: 100000,
witnessScript,
});
// 2-of-2 signed only once
const tx = txb.buildIncomplete();
// Only input is segwit, so txid should be accurate with the final tx
assert.strictEqual(
tx.getId(),
'f15d0a65b21b4471405b21a099f8b18e1ae4d46d55efbd0f4766cf11ad6cb821',
);
const txHex = tx.toHex();
TransactionBuilder.fromTransaction(Transaction.fromHex(txHex));
});
it('should handle badly pre-filled OP_0s', () => {
// OP_0 is used where a signature is missing
const redeemScripSig = bscript.fromASM(
'OP_0 OP_0 3045022100daf0f4f3339d9fbab42b098045c1e4958ee3b308f4ae17be80b63808558d0adb02202f07e3d1f79dc8da285ae0d7f68083d769c11f5621ebd9691d6b48c0d4283d7d01 52410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b84104c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a4104f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e67253ae',
);
const redeemScript = bscript.fromASM(
'OP_2 0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 04c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a 04f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e672 OP_3 OP_CHECKMULTISIG',
);
const tx = new Transaction();
tx.addInput(
Buffer.from(
'cff58855426469d0ef16442ee9c644c4fb13832467bcbc3173168a7916f07149',
'hex',
),
0,
undefined,
redeemScripSig,
);
tx.addOutput(
Buffer.from(
'76a914aa4d7985c57e011a8b3dd8e0e5a73aaef41629c588ac',
'hex',
),
1000,
);
// now import the Transaction
const txb = TransactionBuilder.fromTransaction(tx, NETWORKS.testnet);
const keyPair2 = ECPair.fromWIF(
'91avARGdfge8E4tZfYLoxeJ5sGBdNJQH4kvjJoQFacbgx3cTMqe',
network,
);
txb.sign({
prevOutScriptType: 'p2sh-p2ms',
vin: 0,
keyPair: keyPair2,
redeemScript,
});
const tx2 = txb.build();
assert.strictEqual(
tx2.getId(),
'eab59618a564e361adef6d918bd792903c3d41bcf1220137364fb847880467f9',
);
assert.strictEqual(
bscript.toASM(tx2.ins[0].script),
'OP_0 3045022100daf0f4f3339d9fbab42b098045c1e4958ee3b308f4ae17be80b63808558d0adb02202f07e3d1f79dc8da285ae0d7f68083d769c11f5621ebd9691d6b48c0d4283d7d01 3045022100a346c61738304eac5e7702188764d19cdf68f4466196729db096d6c87ce18cdd022018c0e8ad03054b0e7e235cda6bedecf35881d7aa7d94ff425a8ace7220f38af001 52410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b84104c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a4104f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e67253ae',
);
});
it('should not classify blank scripts as nonstandard', () => {
let txb = new TransactionBuilder();
txb.setVersion(1);
txb.addInput(
'aa94ab02c182214f090e99a0d57021caffd0f195a81c24602b1028b130b63e31',
0,
);
const incomplete = txb.buildIncomplete().toHex();
const keyPair = ECPair.fromWIF(
'L1uyy5qTuGrVXrmrsvHWHgVzW9kKdrp27wBC7Vs6nZDTF2BRUVwy',
);
// sign, as expected
txb.addOutput('1Gokm82v6DmtwKEB8AiVhm82hyFSsEvBDK', 15000);
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
});
const txId = txb.build().getId();
assert.strictEqual(
txId,
'54f097315acbaedb92a95455da3368eb45981cdae5ffbc387a9afc872c0f29b3',
);
// and, repeat
txb = TransactionBuilder.fromTransaction(
Transaction.fromHex(incomplete),
);
txb.addOutput('1Gokm82v6DmtwKEB8AiVhm82hyFSsEvBDK', 15000);
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
});
const txId2 = txb.build().getId();
assert.strictEqual(txId, txId2);
// TODO: Remove me in v6
if (useOldSignArgs) {
console.warn = consoleWarn;
}
});
});
});
}

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,51 +0,0 @@
const { describe, it } = require('mocha')
const assert = require('assert')
const types = require('../src/types')
const typeforce = require('typeforce')
describe('types', () => {
describe('Buffer Hash160/Hash256', () => {
const buffer20byte = Buffer.alloc(20)
const buffer32byte = Buffer.alloc(32)
it('return true for valid size', () => {
assert(types.Hash160bit(buffer20byte))
assert(types.Hash256bit(buffer32byte))
})
it('return true for oneOf', () => {
assert.doesNotThrow(() => {
typeforce(types.oneOf(types.Hash160bit, types.Hash256bit), buffer32byte)
})
assert.doesNotThrow(() => {
typeforce(types.oneOf(types.Hash256bit, types.Hash160bit), buffer32byte)
})
})
it('throws for invalid size', () => {
assert.throws(() => {
types.Hash160bit(buffer32byte)
}, /Expected Buffer\(Length: 20\), got Buffer\(Length: 32\)/)
assert.throws(() => {
types.Hash256bit(buffer20byte)
}, /Expected Buffer\(Length: 32\), got Buffer\(Length: 20\)/)
})
})
describe('Satoshi', () => {
[
{ value: -1, result: false },
{ value: 0, result: true },
{ value: 1, result: true },
{ value: 20999999 * 1e8, result: true },
{ value: 21000000 * 1e8, result: true },
{ value: 21000001 * 1e8, result: false }
].forEach(f => {
it('returns ' + f.result + ' for valid for ' + f.value, () => {
assert.strictEqual(types.Satoshi(f.value), f.result)
})
})
})
})

57
test/types.spec.ts Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

6
types/block.d.ts vendored
View file

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

4
types/classify.d.ts vendored
View file

@ -11,6 +11,6 @@ declare const types: {
WITNESS_COMMITMENT: string;
};
declare function classifyOutput(script: Buffer): string;
declare function classifyInput(script: Buffer, allowIncomplete: boolean): string;
declare function classifyWitness(script: Buffer[], allowIncomplete: boolean): string;
declare function classifyInput(script: Buffer, allowIncomplete?: boolean): string;
declare function classifyWitness(script: Buffer[], allowIncomplete?: boolean): string;
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 { ECPairInterface, Signer, SignerAsync } from './ecpair';
export { Network } from './networks';
export { Payment, PaymentOpts, Stack, StackElement } from './payments';
export { Payment, PaymentCreator, PaymentOpts, Stack, StackElement, } from './payments';
export { OpCode } from './script';
export { Input as TxInput, Output as TxOutput } from './transaction';

View file

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