555 lines
18 KiB
JavaScript
555 lines
18 KiB
JavaScript
const { describe, it } = require('mocha')
|
|
const assert = require('assert')
|
|
|
|
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 attrs = Object.keys(data)
|
|
for (const attr of attrs) {
|
|
const upperAttr = upperCaseFirstLetter(attr)
|
|
let adder = psbt[`add${upperAttr}To${upperCaseFirstLetter(inputOrOutput)}`]
|
|
if (adder !== undefined) {
|
|
adder = adder.bind(psbt)
|
|
const arg = data[attr]
|
|
if (Array.isArray(arg)) {
|
|
arg.forEach(a => adder(i, a))
|
|
} else {
|
|
adder(i, arg)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('signInputAsync', () => {
|
|
fixtures.signInput.checks.forEach(f => {
|
|
it(f.description, async () => {
|
|
const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt)
|
|
assert.doesNotReject(async () => {
|
|
await psbtThatShouldsign.signInputAsync(
|
|
f.shouldSign.inputToCheck,
|
|
ECPair.fromWIF(f.shouldSign.WIF),
|
|
)
|
|
})
|
|
|
|
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt)
|
|
assert.rejects(async () => {
|
|
await psbtThatShouldThrow.signInputAsync(
|
|
f.shouldThrow.inputToCheck,
|
|
ECPair.fromWIF(f.shouldThrow.WIF),
|
|
)
|
|
}, 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, () => {
|
|
const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt)
|
|
assert.doesNotThrow(() => {
|
|
psbtThatShouldsign.signInput(
|
|
f.shouldSign.inputToCheck,
|
|
ECPair.fromWIF(f.shouldSign.WIF),
|
|
)
|
|
})
|
|
|
|
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt)
|
|
assert.throws(() => {
|
|
psbtThatShouldThrow.signInput(
|
|
f.shouldThrow.inputToCheck,
|
|
ECPair.fromWIF(f.shouldThrow.WIF),
|
|
)
|
|
}, new RegExp(f.shouldThrow.errorMessage))
|
|
assert.throws(() => {
|
|
psbtThatShouldThrow.signInput(
|
|
f.shouldThrow.inputToCheck,
|
|
)
|
|
}, new RegExp('Need Signer to sign input'))
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('signAsync', () => {
|
|
fixtures.signInput.checks.forEach(f => {
|
|
if (f.description === 'checks the input exists') return
|
|
it(f.description, async () => {
|
|
const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt)
|
|
assert.doesNotReject(async () => {
|
|
await psbtThatShouldsign.signAsync(
|
|
ECPair.fromWIF(f.shouldSign.WIF),
|
|
)
|
|
})
|
|
|
|
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt)
|
|
assert.rejects(async () => {
|
|
await psbtThatShouldThrow.signAsync(
|
|
ECPair.fromWIF(f.shouldThrow.WIF),
|
|
)
|
|
}, new RegExp('No inputs were signed'))
|
|
assert.rejects(async () => {
|
|
await psbtThatShouldThrow.signAsync()
|
|
}, new RegExp('Need Signer to sign input'))
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('sign', () => {
|
|
fixtures.signInput.checks.forEach(f => {
|
|
if (f.description === 'checks the input exists') return
|
|
it(f.description, () => {
|
|
const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt)
|
|
assert.doesNotThrow(() => {
|
|
psbtThatShouldsign.sign(
|
|
ECPair.fromWIF(f.shouldSign.WIF),
|
|
)
|
|
})
|
|
|
|
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt)
|
|
assert.throws(() => {
|
|
psbtThatShouldThrow.sign(
|
|
ECPair.fromWIF(f.shouldThrow.WIF),
|
|
)
|
|
}, new RegExp('No inputs were signed'))
|
|
assert.throws(() => {
|
|
psbtThatShouldThrow.sign()
|
|
}, new RegExp('Need Signer 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.addWitnessUtxoToInput(0, {
|
|
script: Buffer.from('0014d85c2b71d0060b09c9886aeb815e50991dda124d', 'hex'),
|
|
value: 2e5
|
|
})
|
|
assert.throws(() => {
|
|
psbt.finalizeAllInputs()
|
|
}, new RegExp('Can not finalize input #0'))
|
|
})
|
|
})
|
|
|
|
describe('fromTransaction', () => {
|
|
fixtures.fromTransaction.forEach(f => {
|
|
it('Creates the expected PSBT from a transaction buffer', () => {
|
|
const psbt = Psbt.fromTransaction(Buffer.from(f.transaction, 'hex'))
|
|
assert.strictEqual(psbt.toBase64(), f.result)
|
|
})
|
|
})
|
|
})
|
|
|
|
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('setSequence', () => {
|
|
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.setSequence(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.setSequence(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.validateAllSignatures(), true)
|
|
|
|
assert.strictEqual(clone.toBase64(), psbt.toBase64())
|
|
assert.strictEqual(clone.toBase64(), notAClone.toBase64())
|
|
assert.strictEqual(psbt.toBase64(), notAClone.toBase64())
|
|
psbt.data.globalMap.unsignedTx[3] = 0xff
|
|
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('validateSignatures', () => {
|
|
const f = fixtures.validateSignatures
|
|
it('Correctly validates a signature', () => {
|
|
const psbt = Psbt.fromBase64(f.psbt)
|
|
|
|
assert.strictEqual(psbt.validateSignatures(f.index), true)
|
|
assert.throws(() => {
|
|
psbt.validateSignatures(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.validateSignatures(f.index, f.pubkey), true)
|
|
assert.throws(() => {
|
|
psbt.validateSignatures(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.validateSignatures(0)
|
|
psbt.finalizeAllInputs()
|
|
assert.strictEqual(
|
|
psbt.extractTransaction().toHex(),
|
|
'02000000013ebc8203037dda39d482bf41ff3be955996c50d9d4f7cfc3d2097a694a7' +
|
|
'b067d000000006b483045022100931b6db94aed25d5486884d83fc37160f37f3368c0' +
|
|
'd7f48c757112abefec983802205fda64cff98c849577026eb2ce916a50ea70626a766' +
|
|
'9f8596dd89b720a26b4d501210365db9da3f8a260078a7e8f8b708a1161468fb2323f' +
|
|
'fda5ec16b261ec1056f455ffffffff0180380100000000001976a914ca0d36044e0dc' +
|
|
'08a22724efa6f6a07b0ec4c79aa88ac00000000',
|
|
)
|
|
})
|
|
|
|
describe('Method return types', () => {
|
|
it('fromTransaction returns Psbt type (not base class)', () => {
|
|
const psbt = Psbt.fromTransaction(Buffer.from([2,0,0,0,0,0,0,0,0,0]));
|
|
assert.strictEqual(psbt instanceof Psbt, true);
|
|
assert.ok(psbt.__CACHE.__TX);
|
|
})
|
|
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.addNonWitnessUtxoToInput(index, 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))
|
|
})
|
|
})
|
|
})
|