diff --git a/package-lock.json b/package-lock.json index e9059fc..850e840 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7745,22 +7745,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/semantic-release/node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/semantic-release/node_modules/wrap-ansi": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", diff --git a/src/lib/psbt.js b/src/lib/psbt.js index 5f61863..be68d89 100644 --- a/src/lib/psbt.js +++ b/src/lib/psbt.js @@ -112,7 +112,7 @@ class Psbt { throw new Error('unknownKeyVals must be an Array'); } addKeyVals.forEach(keyVal => - this.addUnknownKeyValToInput(outputIndex, keyVal), + this.addUnknownKeyValToOutput(outputIndex, keyVal), ); utils_1.addOutputAttributes(this.outputs, outputData); return this; diff --git a/src/tests/addInputOutput.js b/src/tests/addInputOutput.js index f1e2f15..9e94b97 100644 --- a/src/tests/addInputOutput.js +++ b/src/tests/addInputOutput.js @@ -3,6 +3,85 @@ Object.defineProperty(exports, '__esModule', { value: true }); const tape = require('tape'); const psbt_1 = require('../lib/psbt'); const txTools_1 = require('./utils/txTools'); +const SCRIPT = Buffer.from( + 'a914e18870f2c297fbfca54c5c6f645c7745a5b66eda87', + 'hex', +); +// Proprietary key: 0xfc prefix + identifier + subtype (type byte 0xfc is +// above both InputTypes (16 entries) and OutputTypes (6 entries), so it is +// always an unknown key for either map). +const PROPRIETARY_KEY = Buffer.from('fc0a626974676f2f6d7573696700', 'hex'); +const PROPRIETARY_VAL = Buffer.from('deadbeef', 'hex'); +tape( + 'Test: addOutput routes unknownKeyVals to output map, not input map', + t => { + const psbt = new psbt_1.Psbt(txTools_1.getDefaultTx()); + psbt.addInput({ + hash: '865dce988413971fd812d0e81a3395ed916a87ea533e1a16c0f4e15df96fa7d4', + index: 0, + }); + psbt.addOutput({ + script: SCRIPT, + value: 1000000, + unknownKeyVals: [{ key: PROPRIETARY_KEY, value: PROPRIETARY_VAL }], + }); + // Key must be on the output, not the input + t.deepEqual( + psbt.outputs[0].unknownKeyVals, + [{ key: PROPRIETARY_KEY, value: PROPRIETARY_VAL }], + 'output[0].unknownKeyVals contains the proprietary key', + ); + t.deepEqual( + psbt.inputs[0].unknownKeyVals, + [], + 'input[0].unknownKeyVals is empty', + ); + // Placement must survive a serialize/parse round-trip + const psbt2 = psbt_1.Psbt.fromHex( + psbt.toHex(), + txTools_1.transactionFromBuffer, + ); + t.deepEqual( + psbt2.outputs[0].unknownKeyVals, + [{ key: PROPRIETARY_KEY, value: PROPRIETARY_VAL }], + 'output[0].unknownKeyVals survives round-trip', + ); + // fromBuffer leaves unknownKeyVals undefined when there are none; + // either undefined or [] is acceptable here. + t.ok( + !psbt2.inputs[0].unknownKeyVals || + psbt2.inputs[0].unknownKeyVals.length === 0, + 'input[0].unknownKeyVals is empty after round-trip', + ); + t.end(); + }, +); +tape( + 'Test: addOutput with unknownKeyVals does not throw when outputs > inputs', + t => { + // Common case: 1 input, 2 outputs — the second addOutput formerly threw + // "No input #1" because it called addUnknownKeyValToInput(1, ...). + const psbt = new psbt_1.Psbt(txTools_1.getDefaultTx()); + psbt.addInput({ + hash: '865dce988413971fd812d0e81a3395ed916a87ea533e1a16c0f4e15df96fa7d4', + index: 0, + }); + psbt.addOutput({ script: SCRIPT, value: 1000000 }); + t.doesNotThrow(() => { + psbt.addOutput({ + script: SCRIPT, + value: 500000, + unknownKeyVals: [{ key: PROPRIETARY_KEY, value: PROPRIETARY_VAL }], + }); + }, 'second addOutput with unknownKeyVals must not throw'); + t.deepEqual( + psbt.outputs[1].unknownKeyVals, + [{ key: PROPRIETARY_KEY, value: PROPRIETARY_VAL }], + 'output[1] holds the proprietary key', + ); + t.end(); + }, +); tape('Test: add Input Output', t => { const psbt = new psbt_1.Psbt(txTools_1.getDefaultTx()); psbt.addInput({ diff --git a/ts_src/lib/psbt.ts b/ts_src/lib/psbt.ts index 68905a3..30565d8 100644 --- a/ts_src/lib/psbt.ts +++ b/ts_src/lib/psbt.ts @@ -156,7 +156,7 @@ export class Psbt { throw new Error('unknownKeyVals must be an Array'); } addKeyVals.forEach((keyVal: KeyValue) => - this.addUnknownKeyValToInput(outputIndex, keyVal), + this.addUnknownKeyValToOutput(outputIndex, keyVal), ); addOutputAttributes(this.outputs, outputData); return this; diff --git a/ts_src/tests/addInputOutput.ts b/ts_src/tests/addInputOutput.ts index f49d5ab..30f204e 100644 --- a/ts_src/tests/addInputOutput.ts +++ b/ts_src/tests/addInputOutput.ts @@ -2,6 +2,92 @@ import * as tape from 'tape'; import { Psbt } from '../lib/psbt'; import { getDefaultTx, transactionFromBuffer } from './utils/txTools'; +const SCRIPT = Buffer.from( + 'a914e18870f2c297fbfca54c5c6f645c7745a5b66eda87', + 'hex', +); + +// Proprietary key: 0xfc prefix + identifier + subtype (type byte 0xfc is +// above both InputTypes (16 entries) and OutputTypes (6 entries), so it is +// always an unknown key for either map). +const PROPRIETARY_KEY = Buffer.from('fc0a626974676f2f6d7573696700', 'hex'); +const PROPRIETARY_VAL = Buffer.from('deadbeef', 'hex'); + +tape( + 'Test: addOutput routes unknownKeyVals to output map, not input map', + t => { + const psbt = new Psbt(getDefaultTx()); + psbt.addInput({ + hash: '865dce988413971fd812d0e81a3395ed916a87ea533e1a16c0f4e15df96fa7d4', + index: 0, + }); + psbt.addOutput({ + script: SCRIPT, + value: 1000000, + unknownKeyVals: [{ key: PROPRIETARY_KEY, value: PROPRIETARY_VAL }], + }); + + // Key must be on the output, not the input + t.deepEqual( + psbt.outputs[0].unknownKeyVals, + [{ key: PROPRIETARY_KEY, value: PROPRIETARY_VAL }], + 'output[0].unknownKeyVals contains the proprietary key', + ); + t.deepEqual( + psbt.inputs[0].unknownKeyVals, + [], + 'input[0].unknownKeyVals is empty', + ); + + // Placement must survive a serialize/parse round-trip + const psbt2 = Psbt.fromHex(psbt.toHex(), transactionFromBuffer); + t.deepEqual( + psbt2.outputs[0].unknownKeyVals, + [{ key: PROPRIETARY_KEY, value: PROPRIETARY_VAL }], + 'output[0].unknownKeyVals survives round-trip', + ); + // fromBuffer leaves unknownKeyVals undefined when there are none; + // either undefined or [] is acceptable here. + t.ok( + !psbt2.inputs[0].unknownKeyVals || + psbt2.inputs[0].unknownKeyVals.length === 0, + 'input[0].unknownKeyVals is empty after round-trip', + ); + + t.end(); + }, +); + +tape( + 'Test: addOutput with unknownKeyVals does not throw when outputs > inputs', + t => { + // Common case: 1 input, 2 outputs — the second addOutput formerly threw + // "No input #1" because it called addUnknownKeyValToInput(1, ...). + const psbt = new Psbt(getDefaultTx()); + psbt.addInput({ + hash: '865dce988413971fd812d0e81a3395ed916a87ea533e1a16c0f4e15df96fa7d4', + index: 0, + }); + psbt.addOutput({ script: SCRIPT, value: 1000000 }); + + t.doesNotThrow(() => { + psbt.addOutput({ + script: SCRIPT, + value: 500000, + unknownKeyVals: [{ key: PROPRIETARY_KEY, value: PROPRIETARY_VAL }], + }); + }, 'second addOutput with unknownKeyVals must not throw'); + + t.deepEqual( + psbt.outputs[1].unknownKeyVals, + [{ key: PROPRIETARY_KEY, value: PROPRIETARY_VAL }], + 'output[1] holds the proprietary key', + ); + + t.end(); + }, +); + tape('Test: add Input Output', t => { const psbt = new Psbt(getDefaultTx()); psbt.addInput({