Merge pull request #1465 from bitcoinjs/tsTests
WIP: Move tests to TypeScript
This commit is contained in:
commit
b3831c9616
61 changed files with 5683 additions and 4475 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -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
712
package-lock.json
generated
File diff suppressed because it is too large
Load diff
19
package.json
19
package.json
|
@ -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"
|
||||
},
|
||||
|
|
134
test/address.js
134
test/address.js
|
@ -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
148
test/address.spec.ts
Normal 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));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
251
test/bitcoin.core.spec.ts
Normal 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/);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
157
test/block.js
157
test/block.js
|
@ -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
172
test/block.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
49
test/bufferutils.spec.ts
Normal 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));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
156
test/classify.js
156
test/classify.js
|
@ -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
180
test/classify.spec.ts
Normal 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);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
22
test/crypto.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
284
test/ecpair.js
284
test/ecpair.js
|
@ -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
334
test/ecpair.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
6
test/integration/_regtest.ts
Normal file
6
test/integration/_regtest.ts
Normal 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 });
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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')
|
||||
})
|
||||
})
|
151
test/integration/bip32.spec.ts
Normal file
151
test/integration/bip32.spec.ts
Normal 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',
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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)
|
||||
})
|
||||
})
|
21
test/integration/blocks.spec.ts
Normal file
21
test/integration/blocks.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
|
@ -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\)/)
|
||||
})
|
||||
})
|
||||
})
|
219
test/integration/cltv.spec.ts
Normal file
219
test/integration/cltv.spec.ts
Normal 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\)/);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
135
test/integration/payments.spec.ts
Normal file
135
test/integration/payments.spec.ts
Normal 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,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
421
test/integration/transactions.spec.ts
Normal file
421
test/integration/transactions.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
113
test/payments.spec.ts
Normal 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);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
169
test/payments.utils.ts
Normal 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;
|
||||
}
|
688
test/psbt.js
688
test/psbt.js
|
@ -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
713
test/psbt.spec.ts
Normal 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));
|
||||
});
|
||||
});
|
||||
});
|
157
test/script.js
157
test/script.js
|
@ -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
176
test/script.spec.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
26
test/script_number.spec.ts
Normal file
26
test/script_number.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
63
test/script_signature.spec.ts
Normal file
63
test/script_signature.spec.ts
Normal 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));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
338
test/transaction.spec.ts
Normal 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"/);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
947
test/transaction_builder.spec.ts
Normal file
947
test/transaction_builder.spec.ts
Normal 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
5
test/ts-node-register.js
Normal 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
40
test/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
|
@ -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
57
test/types.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -25,6 +25,8 @@ export interface Payment {
|
|||
witness?: Buffer[];
|
||||
}
|
||||
|
||||
export type PaymentCreator = (a: Payment, opts?: PaymentOpts) => Payment;
|
||||
|
||||
export type PaymentFunction = () => Payment;
|
||||
|
||||
export interface PaymentOpts {
|
||||
|
|
|
@ -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
6
types/block.d.ts
vendored
|
@ -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
4
types/classify.d.ts
vendored
|
@ -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
2
types/index.d.ts
vendored
|
@ -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';
|
||||
|
|
1
types/payments/index.d.ts
vendored
1
types/payments/index.d.ts
vendored
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue