1137 lines
36 KiB
TypeScript
1137 lines
36 KiB
TypeScript
import * as assert from 'assert';
|
|
import BIP32Factory from 'bip32';
|
|
import * as ecc from 'tiny-secp256k1';
|
|
import * as crypto from 'crypto';
|
|
import ECPairFactory from 'ecpair';
|
|
import { describe, it } from 'mocha';
|
|
|
|
const bip32 = BIP32Factory(ecc);
|
|
const ECPair = ECPairFactory(ecc);
|
|
|
|
import { networks as NETWORKS, payments, Psbt, Signer, SignerAsync } from '..';
|
|
|
|
import * as preFixtures from './fixtures/psbt.json';
|
|
|
|
const validator = (
|
|
pubkey: Buffer,
|
|
msghash: Buffer,
|
|
signature: Buffer,
|
|
): boolean => ECPair.fromPublicKey(pubkey).verify(msghash, signature);
|
|
|
|
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 as BufferEncoding);
|
|
});
|
|
|
|
const fixtures = initBuffers(preFixtures);
|
|
|
|
const upperCaseFirstLetter = (str: string): string =>
|
|
str.replace(/^./, s => s.toUpperCase());
|
|
|
|
const toAsyncSigner = (signer: Signer): SignerAsync => {
|
|
const ret: SignerAsync = {
|
|
publicKey: signer.publicKey,
|
|
sign: (hash: Buffer, lowerR: boolean | undefined): Promise<Buffer> => {
|
|
return new Promise(
|
|
(resolve, rejects): void => {
|
|
setTimeout(() => {
|
|
try {
|
|
const r = signer.sign(hash, lowerR);
|
|
resolve(r);
|
|
} catch (e) {
|
|
rejects(e);
|
|
}
|
|
}, 10);
|
|
},
|
|
);
|
|
},
|
|
};
|
|
return ret;
|
|
};
|
|
const failedAsyncSigner = (publicKey: Buffer): SignerAsync => {
|
|
return {
|
|
publicKey,
|
|
sign: (__: Buffer): Promise<Buffer> => {
|
|
return new Promise(
|
|
(_, reject): void => {
|
|
setTimeout(() => {
|
|
reject(new Error('sign failed'));
|
|
}, 10);
|
|
},
|
|
);
|
|
},
|
|
};
|
|
};
|
|
// 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);
|
|
await assert.doesNotReject(async () => {
|
|
await psbtThatShouldsign.signInputAsync(
|
|
f.shouldSign.inputToCheck,
|
|
ECPair.fromWIF(f.shouldSign.WIF),
|
|
f.shouldSign.sighashTypes || undefined,
|
|
);
|
|
});
|
|
await assert.rejects(async () => {
|
|
await psbtThatShouldsign.signInputAsync(
|
|
f.shouldSign.inputToCheck,
|
|
failedAsyncSigner(ECPair.fromWIF(f.shouldSign.WIF).publicKey),
|
|
f.shouldSign.sighashTypes || undefined,
|
|
);
|
|
}, /sign failed/);
|
|
}
|
|
|
|
if (f.shouldThrow) {
|
|
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt);
|
|
await 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));
|
|
await assert.rejects(async () => {
|
|
await psbtThatShouldThrow.signInputAsync(
|
|
f.shouldThrow.inputToCheck,
|
|
toAsyncSigner(ECPair.fromWIF(f.shouldThrow.WIF)),
|
|
(f.shouldThrow as any).sighashTypes || undefined,
|
|
);
|
|
}, new RegExp(f.shouldThrow.errorMessage));
|
|
await 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);
|
|
await 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);
|
|
await assert.rejects(async () => {
|
|
await psbtThatShouldThrow.signAllInputsAsync(
|
|
ECPair.fromWIF(f.shouldThrow.WIF),
|
|
(f.shouldThrow as any).sighashTypes || undefined,
|
|
);
|
|
}, new RegExp('No inputs were signed'));
|
|
await 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);
|
|
await 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);
|
|
await 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));
|
|
await 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);
|
|
await 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);
|
|
await assert.rejects(async () => {
|
|
await psbtThatShouldThrow.signAllInputsHDAsync(
|
|
bip32.fromBase58(f.shouldThrow.xprv),
|
|
(f.shouldThrow as any).sighashTypes || undefined,
|
|
);
|
|
}, new RegExp('No inputs were signed'));
|
|
await 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.txInputs[0].sequence, 0xffffffff);
|
|
psbt.setInputSequence(0, 0);
|
|
assert.strictEqual(psbt.txInputs[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('getInputType', () => {
|
|
const key = ECPair.makeRandom();
|
|
const { publicKey } = key;
|
|
const p2wpkhPub = (pubkey: Buffer): Buffer =>
|
|
payments.p2wpkh({
|
|
pubkey,
|
|
}).output!;
|
|
const p2pkhPub = (pubkey: Buffer): Buffer =>
|
|
payments.p2pkh({
|
|
pubkey,
|
|
}).output!;
|
|
const p2shOut = (output: Buffer): Buffer =>
|
|
payments.p2sh({
|
|
redeem: { output },
|
|
}).output!;
|
|
const p2wshOut = (output: Buffer): Buffer =>
|
|
payments.p2wsh({
|
|
redeem: { output },
|
|
}).output!;
|
|
const p2shp2wshOut = (output: Buffer): Buffer => p2shOut(p2wshOut(output));
|
|
const noOuter = (output: Buffer): Buffer => output;
|
|
|
|
function getInputTypeTest({
|
|
innerScript,
|
|
outerScript,
|
|
redeemGetter,
|
|
witnessGetter,
|
|
expectedType,
|
|
finalize,
|
|
}: any): void {
|
|
const psbt = new Psbt();
|
|
psbt
|
|
.addInput({
|
|
hash:
|
|
'0000000000000000000000000000000000000000000000000000000000000000',
|
|
index: 0,
|
|
witnessUtxo: {
|
|
script: outerScript(innerScript(publicKey)),
|
|
value: 2e3,
|
|
},
|
|
...(redeemGetter ? { redeemScript: redeemGetter(publicKey) } : {}),
|
|
...(witnessGetter ? { witnessScript: witnessGetter(publicKey) } : {}),
|
|
})
|
|
.addOutput({
|
|
script: Buffer.from('0014d85c2b71d0060b09c9886aeb815e50991dda124d'),
|
|
value: 1800,
|
|
});
|
|
if (finalize) psbt.signInput(0, key).finalizeInput(0);
|
|
const type = psbt.getInputType(0);
|
|
assert.strictEqual(type, expectedType, 'incorrect input type');
|
|
}
|
|
[
|
|
{
|
|
innerScript: p2pkhPub,
|
|
outerScript: noOuter,
|
|
redeemGetter: null,
|
|
witnessGetter: null,
|
|
expectedType: 'pubkeyhash',
|
|
},
|
|
{
|
|
innerScript: p2wpkhPub,
|
|
outerScript: noOuter,
|
|
redeemGetter: null,
|
|
witnessGetter: null,
|
|
expectedType: 'witnesspubkeyhash',
|
|
},
|
|
{
|
|
innerScript: p2pkhPub,
|
|
outerScript: p2shOut,
|
|
redeemGetter: p2pkhPub,
|
|
witnessGetter: null,
|
|
expectedType: 'p2sh-pubkeyhash',
|
|
},
|
|
{
|
|
innerScript: p2wpkhPub,
|
|
outerScript: p2shOut,
|
|
redeemGetter: p2wpkhPub,
|
|
witnessGetter: null,
|
|
expectedType: 'p2sh-witnesspubkeyhash',
|
|
finalize: true,
|
|
},
|
|
{
|
|
innerScript: p2pkhPub,
|
|
outerScript: p2wshOut,
|
|
redeemGetter: null,
|
|
witnessGetter: p2pkhPub,
|
|
expectedType: 'p2wsh-pubkeyhash',
|
|
finalize: true,
|
|
},
|
|
{
|
|
innerScript: p2pkhPub,
|
|
outerScript: p2shp2wshOut,
|
|
redeemGetter: (pk: Buffer): Buffer => p2wshOut(p2pkhPub(pk)),
|
|
witnessGetter: p2pkhPub,
|
|
expectedType: 'p2sh-p2wsh-pubkeyhash',
|
|
},
|
|
].forEach(getInputTypeTest);
|
|
});
|
|
|
|
describe('inputHasHDKey', () => {
|
|
it('should return true if HD key is present', () => {
|
|
const root = bip32.fromSeed(crypto.randomBytes(32));
|
|
const root2 = bip32.fromSeed(crypto.randomBytes(32));
|
|
const path = "m/0'/0";
|
|
const psbt = new Psbt();
|
|
psbt.addInput({
|
|
hash:
|
|
'0000000000000000000000000000000000000000000000000000000000000000',
|
|
index: 0,
|
|
bip32Derivation: [
|
|
{
|
|
masterFingerprint: root.fingerprint,
|
|
path,
|
|
pubkey: root.derivePath(path).publicKey,
|
|
},
|
|
],
|
|
});
|
|
assert.strictEqual(psbt.inputHasHDKey(0, root), true);
|
|
assert.strictEqual(psbt.inputHasHDKey(0, root2), false);
|
|
});
|
|
});
|
|
|
|
describe('inputHasPubkey', () => {
|
|
it('should throw', () => {
|
|
const psbt = new Psbt();
|
|
psbt.addInput({
|
|
hash:
|
|
'0000000000000000000000000000000000000000000000000000000000000000',
|
|
index: 0,
|
|
});
|
|
|
|
assert.throws(() => {
|
|
psbt.inputHasPubkey(0, Buffer.from([]));
|
|
}, new RegExp("Can't find pubkey in input without Utxo data"));
|
|
|
|
psbt.updateInput(0, {
|
|
witnessUtxo: {
|
|
value: 1337,
|
|
script: payments.p2sh({
|
|
redeem: { output: Buffer.from([0x51]) },
|
|
}).output!,
|
|
},
|
|
});
|
|
|
|
assert.throws(() => {
|
|
psbt.inputHasPubkey(0, Buffer.from([]));
|
|
}, new RegExp('scriptPubkey is P2SH but redeemScript missing'));
|
|
|
|
delete psbt.data.inputs[0].witnessUtxo;
|
|
|
|
psbt.updateInput(0, {
|
|
witnessUtxo: {
|
|
value: 1337,
|
|
script: payments.p2wsh({
|
|
redeem: { output: Buffer.from([0x51]) },
|
|
}).output!,
|
|
},
|
|
});
|
|
|
|
assert.throws(() => {
|
|
psbt.inputHasPubkey(0, Buffer.from([]));
|
|
}, new RegExp('scriptPubkey or redeemScript is P2WSH but witnessScript missing'));
|
|
|
|
delete psbt.data.inputs[0].witnessUtxo;
|
|
|
|
psbt.updateInput(0, {
|
|
witnessUtxo: {
|
|
value: 1337,
|
|
script: payments.p2sh({
|
|
redeem: payments.p2wsh({
|
|
redeem: { output: Buffer.from([0x51]) },
|
|
}),
|
|
}).output!,
|
|
},
|
|
redeemScript: payments.p2wsh({
|
|
redeem: { output: Buffer.from([0x51]) },
|
|
}).output!,
|
|
});
|
|
|
|
assert.throws(() => {
|
|
psbt.inputHasPubkey(0, Buffer.from([]));
|
|
}, new RegExp('scriptPubkey or redeemScript is P2WSH but witnessScript missing'));
|
|
|
|
psbt.updateInput(0, {
|
|
witnessScript: Buffer.from([0x51]),
|
|
});
|
|
|
|
assert.doesNotThrow(() => {
|
|
psbt.inputHasPubkey(0, Buffer.from([0x51]));
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('outputHasHDKey', () => {
|
|
it('should return true if HD key is present', () => {
|
|
const root = bip32.fromSeed(crypto.randomBytes(32));
|
|
const root2 = bip32.fromSeed(crypto.randomBytes(32));
|
|
const path = "m/0'/0";
|
|
const psbt = new Psbt();
|
|
psbt
|
|
.addInput({
|
|
hash:
|
|
'0000000000000000000000000000000000000000000000000000000000000000',
|
|
index: 0,
|
|
})
|
|
.addOutput({
|
|
script: Buffer.from(
|
|
'0014000102030405060708090a0b0c0d0e0f00010203',
|
|
'hex',
|
|
),
|
|
value: 2000,
|
|
bip32Derivation: [
|
|
{
|
|
masterFingerprint: root.fingerprint,
|
|
path,
|
|
pubkey: root.derivePath(path).publicKey,
|
|
},
|
|
],
|
|
});
|
|
assert.strictEqual(psbt.outputHasHDKey(0, root), true);
|
|
assert.strictEqual(psbt.outputHasHDKey(0, root2), false);
|
|
});
|
|
});
|
|
|
|
describe('outputHasPubkey', () => {
|
|
it('should throw', () => {
|
|
const psbt = new Psbt();
|
|
psbt
|
|
.addInput({
|
|
hash:
|
|
'0000000000000000000000000000000000000000000000000000000000000000',
|
|
index: 0,
|
|
})
|
|
.addOutput({
|
|
script: payments.p2sh({
|
|
redeem: { output: Buffer.from([0x51]) },
|
|
}).output!,
|
|
value: 1337,
|
|
});
|
|
|
|
assert.throws(() => {
|
|
psbt.outputHasPubkey(0, Buffer.from([]));
|
|
}, new RegExp('scriptPubkey is P2SH but redeemScript missing'));
|
|
|
|
(psbt as any).__CACHE.__TX.outs[0].script = payments.p2wsh({
|
|
redeem: { output: Buffer.from([0x51]) },
|
|
}).output!;
|
|
|
|
assert.throws(() => {
|
|
psbt.outputHasPubkey(0, Buffer.from([]));
|
|
}, new RegExp('scriptPubkey or redeemScript is P2WSH but witnessScript missing'));
|
|
|
|
(psbt as any).__CACHE.__TX.outs[0].script = payments.p2sh({
|
|
redeem: payments.p2wsh({
|
|
redeem: { output: Buffer.from([0x51]) },
|
|
}),
|
|
}).output!;
|
|
|
|
psbt.updateOutput(0, {
|
|
redeemScript: payments.p2wsh({
|
|
redeem: { output: Buffer.from([0x51]) },
|
|
}).output!,
|
|
});
|
|
|
|
assert.throws(() => {
|
|
psbt.outputHasPubkey(0, Buffer.from([]));
|
|
}, new RegExp('scriptPubkey or redeemScript is P2WSH but witnessScript missing'));
|
|
|
|
delete psbt.data.outputs[0].redeemScript;
|
|
|
|
psbt.updateOutput(0, {
|
|
witnessScript: Buffer.from([0x51]),
|
|
});
|
|
|
|
assert.throws(() => {
|
|
psbt.outputHasPubkey(0, Buffer.from([]));
|
|
}, new RegExp('scriptPubkey is P2SH but redeemScript missing'));
|
|
|
|
psbt.updateOutput(0, {
|
|
redeemScript: payments.p2wsh({
|
|
redeem: { output: Buffer.from([0x51]) },
|
|
}).output!,
|
|
});
|
|
|
|
assert.doesNotThrow(() => {
|
|
psbt.outputHasPubkey(0, Buffer.from([0x51]));
|
|
});
|
|
});
|
|
});
|
|
|
|
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(validator), 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, validator),
|
|
true,
|
|
);
|
|
assert.throws(() => {
|
|
psbt.validateSignaturesOfInput(f.nonExistantIndex, validator);
|
|
}, 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, validator, f.pubkey as any),
|
|
true,
|
|
);
|
|
assert.throws(() => {
|
|
psbt.validateSignaturesOfInput(
|
|
f.index,
|
|
validator,
|
|
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, validator);
|
|
psbt.finalizeAllInputs();
|
|
assert.throws(() => {
|
|
psbt.setVersion(3);
|
|
}, new RegExp('Can not modify transaction, signatures exist.'));
|
|
assert.strictEqual(psbt.inputHasPubkey(0, alice.publicKey), true);
|
|
assert.strictEqual(psbt.outputHasPubkey(0, alice.publicKey), false);
|
|
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));
|
|
});
|
|
});
|
|
|
|
describe('Transaction properties', () => {
|
|
it('.version is exposed and is settable', () => {
|
|
const psbt = new Psbt();
|
|
|
|
assert.strictEqual(psbt.version, 2);
|
|
assert.strictEqual(psbt.version, (psbt as any).__CACHE.__TX.version);
|
|
|
|
psbt.version = 1;
|
|
assert.strictEqual(psbt.version, 1);
|
|
assert.strictEqual(psbt.version, (psbt as any).__CACHE.__TX.version);
|
|
});
|
|
|
|
it('.locktime is exposed and is settable', () => {
|
|
const psbt = new Psbt();
|
|
|
|
assert.strictEqual(psbt.locktime, 0);
|
|
assert.strictEqual(psbt.locktime, (psbt as any).__CACHE.__TX.locktime);
|
|
|
|
psbt.locktime = 123;
|
|
assert.strictEqual(psbt.locktime, 123);
|
|
assert.strictEqual(psbt.locktime, (psbt as any).__CACHE.__TX.locktime);
|
|
});
|
|
|
|
it('.txInputs is exposed as a readonly clone', () => {
|
|
const psbt = new Psbt();
|
|
const hash = Buffer.alloc(32);
|
|
const index = 0;
|
|
psbt.addInput({ hash, index });
|
|
|
|
const input = psbt.txInputs[0];
|
|
const internalInput = (psbt as any).__CACHE.__TX.ins[0];
|
|
|
|
assert.ok(input.hash.equals(internalInput.hash));
|
|
assert.strictEqual(input.index, internalInput.index);
|
|
assert.strictEqual(input.sequence, internalInput.sequence);
|
|
|
|
input.hash[0] = 123;
|
|
input.index = 123;
|
|
input.sequence = 123;
|
|
|
|
assert.ok(!input.hash.equals(internalInput.hash));
|
|
assert.notEqual(input.index, internalInput.index);
|
|
assert.notEqual(input.sequence, internalInput.sequence);
|
|
});
|
|
|
|
it('.txOutputs is exposed as a readonly clone', () => {
|
|
const psbt = new Psbt();
|
|
const address = '1LukeQU5jwebXbMLDVydeH4vFSobRV9rkj';
|
|
const value = 100000;
|
|
psbt.addOutput({ address, value });
|
|
|
|
const output = psbt.txOutputs[0];
|
|
const internalInput = (psbt as any).__CACHE.__TX.outs[0];
|
|
|
|
assert.strictEqual(output.address, address);
|
|
|
|
assert.ok(output.script.equals(internalInput.script));
|
|
assert.strictEqual(output.value, internalInput.value);
|
|
|
|
output.script[0] = 123;
|
|
output.value = 123;
|
|
|
|
assert.ok(!output.script.equals(internalInput.script));
|
|
assert.notEqual(output.value, internalInput.value);
|
|
});
|
|
});
|
|
});
|