From 07a5a28a0d0801bad289a888209ead4e13b7360d Mon Sep 17 00:00:00 2001 From: w3irdrobot Date: Tue, 14 Jul 2026 14:22:47 -0400 Subject: [PATCH 1/4] fix: estimate taproot witness length in fees --- .../core/__tests__/dlc/CoinSelect.spec.ts | 74 ++++++++++++++- .../core/__tests__/dlc/TxFinalizer.spec.ts | 47 ++++++++++ packages/core/lib/dlc/CoinSelect.ts | 91 +++++++++++++++---- packages/core/lib/dlc/TxFinalizer.ts | 71 ++++++++++++--- 4 files changed, 251 insertions(+), 32 deletions(-) create mode 100644 packages/core/__tests__/dlc/TxFinalizer.spec.ts diff --git a/packages/core/__tests__/dlc/CoinSelect.spec.ts b/packages/core/__tests__/dlc/CoinSelect.spec.ts index 72b51e73..ec2f7b5e 100644 --- a/packages/core/__tests__/dlc/CoinSelect.spec.ts +++ b/packages/core/__tests__/dlc/CoinSelect.spec.ts @@ -1,7 +1,13 @@ import { Value } from '@node-dlc/bitcoin'; import { expect } from 'chai'; -import { dualFees, dualFundingCoinSelect, UTXO } from '../../lib'; +import { + dualFees, + dualFundingCoinSelect, + dustThreshold, + getMaxWitnessLen, + UTXO, +} from '../../lib'; const getUtxos = (totalCollateral: bigint, numUtxos = 1) => { const utxos: UTXO[] = []; @@ -174,5 +180,71 @@ describe('CoinSelect', () => { expect(inputs.length).to.be.lessThan(10); expect(fee).to.equal(BigInt(687)); }); + + it('should calculate lower fees for taproot inputs', () => { + const feeRate = BigInt(450); + const p2wpkhFee = dualFees(feeRate, 1, 1); + const p2trFee = dualFees(feeRate, [getMaxWitnessLen('p2tr')], 1); + + expect(p2trFee).to.be.lessThan(p2wpkhFee); + }); + + it('should select taproot UTXOs using taproot witness costs', () => { + const feeRate = BigInt(10); + const offerCollateral = Value.fromSats(20000); + const p2trAddress = + 'bcrt1pqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpqqenm'; + const p2trFee = dualFees(feeRate, [getMaxWitnessLen('p2tr')], 1); + const p2wpkhFee = dualFees(feeRate, [getMaxWitnessLen('p2wpkh')], 1); + const p2trDust = dustThreshold(feeRate, { + address: p2trAddress, + txid: '', + value: 0, + vout: 0, + scriptType: 'p2tr', + }); + const p2wpkhDust = dustThreshold(feeRate, { + address: 'bcrt1qjzut0906d9sk4hml4k6sz6cssljktf4c7yl80f', + txid: '', + value: 0, + vout: 0, + scriptType: 'p2wpkh', + }); + const p2trRequired = offerCollateral.sats + p2trFee + p2trDust; + const p2wpkhRequired = offerCollateral.sats + p2wpkhFee + p2wpkhDust; + + // Value sits above Taproot requirements but below P2WPKH requirements. + const p2trUtxo: UTXO = { + address: p2trAddress, + txid: 'c7bf12ac16aba1cf6c7769117294853453f7da3006363dfe4e8979847e32f7e1', + value: Number(p2trRequired), + vout: 0, + scriptType: 'p2tr', + }; + + const p2wpkhUtxo: UTXO = { + ...p2trUtxo, + address: 'bcrt1qjzut0906d9sk4hml4k6sz6cssljktf4c7yl80f', + scriptType: 'p2wpkh', + }; + + expect(p2trRequired).to.be.lessThan(p2wpkhRequired); + expect(BigInt(p2trUtxo.value)).to.equal(p2trRequired); + expect(BigInt(p2trUtxo.value)).to.be.lessThan(p2wpkhRequired); + + const taprootSelection = dualFundingCoinSelect( + [p2trUtxo], + [offerCollateral.sats], + feeRate, + ); + const p2wpkhSelection = dualFundingCoinSelect( + [p2wpkhUtxo], + [offerCollateral.sats], + feeRate, + ); + + expect(taprootSelection.inputs.length).to.equal(1); + expect(p2wpkhSelection.inputs.length).to.equal(0); + }); }); }); diff --git a/packages/core/__tests__/dlc/TxFinalizer.spec.ts b/packages/core/__tests__/dlc/TxFinalizer.spec.ts new file mode 100644 index 00000000..8c086095 --- /dev/null +++ b/packages/core/__tests__/dlc/TxFinalizer.spec.ts @@ -0,0 +1,47 @@ +import { expect } from 'chai'; + +import { + DEFAULT_MAX_WITNESS_LEN, + getFinalizerByCount, + getMaxWitnessLen, +} from '../../lib'; + +describe('TxFinalizer', () => { + describe('getFinalizerByCount', () => { + it('should default synthetic inputs to p2wpkh witness length', () => { + const defaultFinalizer = getFinalizerByCount(BigInt(1), 1, 1, 1); + const explicitFinalizer = getFinalizerByCount( + BigInt(1), + 1, + 1, + 1, + DEFAULT_MAX_WITNESS_LEN, + DEFAULT_MAX_WITNESS_LEN, + ); + + expect(defaultFinalizer.offerFundingFee).to.equal( + explicitFinalizer.offerFundingFee, + ); + }); + + it('should calculate lower funding fees for p2tr synthetic inputs', () => { + const feeRate = BigInt(10); + const p2wpkhFinalizer = getFinalizerByCount(feeRate, 1, 1, 1); + const p2trFinalizer = getFinalizerByCount( + feeRate, + 1, + 1, + 1, + getMaxWitnessLen('p2tr'), + getMaxWitnessLen('p2tr'), + ); + + expect(p2trFinalizer.offerFundingFee).to.be.lessThan( + p2wpkhFinalizer.offerFundingFee, + ); + expect(p2trFinalizer.acceptFundingFee).to.be.lessThan( + p2wpkhFinalizer.acceptFundingFee, + ); + }); + }); +}); diff --git a/packages/core/lib/dlc/CoinSelect.ts b/packages/core/lib/dlc/CoinSelect.ts index 76013744..c3a5cfc6 100644 --- a/packages/core/lib/dlc/CoinSelect.ts +++ b/packages/core/lib/dlc/CoinSelect.ts @@ -1,6 +1,12 @@ import { FundingInput } from '@node-dlc/messaging'; -import { DualFundingTxFinalizer } from './TxFinalizer'; +import { + DEFAULT_MAX_WITNESS_LEN, + DualFundingTxFinalizer, + FundingInputScriptType, + fundingInputVBytes, + getMaxWitnessLen, +} from './TxFinalizer'; export interface UTXO { txid: string; @@ -8,43 +14,86 @@ export interface UTXO { value: number; address: string; derivationPath?: string; + scriptPubKey?: Buffer | string; + scriptType?: FundingInputScriptType; } -const TX_INPUT_SIZE = { - LEGACY: 148, - P2SH: 92, - BECH32: 69, +const scriptPubKeyBuffer = (scriptPubKey: Buffer | string): Buffer => { + return Buffer.isBuffer(scriptPubKey) + ? scriptPubKey + : Buffer.from(scriptPubKey, 'hex'); }; -const inputBytes = () => { - return BigInt(TX_INPUT_SIZE.BECH32); +const scriptTypeFromUtxo = (utxo?: UTXO): FundingInputScriptType => { + if (!utxo) return 'p2wpkh'; + + if (utxo.scriptType) return utxo.scriptType; + + if (utxo.scriptPubKey) { + const scriptPubKey = scriptPubKeyBuffer(utxo.scriptPubKey); + if ( + scriptPubKey.length === 34 && + scriptPubKey[0] === 0x51 && + scriptPubKey[1] === 0x20 + ) + return 'p2tr'; + if ( + scriptPubKey.length === 34 && + scriptPubKey[0] === 0x00 && + scriptPubKey[1] === 0x20 + ) + return 'p2wsh'; + } + + if (/^(bc|tb|bcrt)1p/i.test(utxo.address)) return 'p2tr'; + + return 'p2wpkh'; }; -export const dustThreshold = (feeRate: bigint): bigint => { - return BigInt(inputBytes()) * feeRate; +const inputBytes = (utxo?: UTXO): bigint => { + return fundingInputVBytes(getMaxWitnessLen(scriptTypeFromUtxo(utxo))); +}; + +export const dustThreshold = (feeRate: bigint, utxo?: UTXO): bigint => { + return inputBytes(utxo) * feeRate; }; // order by descending value, minus the inputs approximate fee const utxoScore = (x: UTXO, feeRate: bigint): bigint => { - return BigInt(x.value) - feeRate * inputBytes(); + return BigInt(x.value) - feeRate * inputBytes(x); +}; + +const fundingInputWithWitnessLen = (maxWitnessLen: number): FundingInput => { + const input = new FundingInput(); + input.maxWitnessLen = maxWitnessLen; + input.redeemScript = Buffer.from('', 'hex'); + return input; }; +const witnessLensFromInputCount = ( + numInputsOrWitnessLens: number | number[], +): number[] => + Array.isArray(numInputsOrWitnessLens) + ? numInputsOrWitnessLens + : Array.from( + { length: numInputsOrWitnessLens }, + () => DEFAULT_MAX_WITNESS_LEN, + ); + export const dualFees = ( feeRate: bigint, - numInputs: number, + numInputsOrWitnessLens: number | number[], numContracts: number, ): bigint => { - const input = new FundingInput(); - input.maxWitnessLen = 108; - input.redeemScript = Buffer.from('', 'hex'); - const fakeSPK = Buffer.from( '0014663117d27e78eb432505180654e603acb30e8a4a', 'hex', ); - const offerInputs = Array.from({ length: numInputs }, () => input); - const acceptInputs = Array.from({ length: 1 }, () => input); + const offerInputs = witnessLensFromInputCount(numInputsOrWitnessLens).map( + fundingInputWithWitnessLen, + ); + const acceptInputs = [fundingInputWithWitnessLen(DEFAULT_MAX_WITNESS_LEN)]; return new DualFundingTxFinalizer( offerInputs, @@ -85,7 +134,7 @@ export const dualFundingCoinSelect = ( for (let i = 0; i < utxos.length; ++i) { const utxo = utxos[i]; - const utxoBytes = inputBytes(); + const utxoBytes = inputBytes(utxo); const utxoFee = feeRate * utxoBytes; const utxoValue = utxo.value; @@ -102,7 +151,11 @@ export const dualFundingCoinSelect = ( inAccum += utxoValue; inputs.push(utxo); - const fee = dualFees(feeRate, inputs.length, collaterals.length); + const fee = dualFees( + feeRate, + inputs.map((input) => getMaxWitnessLen(scriptTypeFromUtxo(input))), + collaterals.length, + ); // go again? if (inAccum < outAccum + fee) continue; diff --git a/packages/core/lib/dlc/TxFinalizer.ts b/packages/core/lib/dlc/TxFinalizer.ts index b91f08e5..c71c5635 100644 --- a/packages/core/lib/dlc/TxFinalizer.ts +++ b/packages/core/lib/dlc/TxFinalizer.ts @@ -4,6 +4,49 @@ import { Decimal } from 'decimal.js'; const BATCH_FUND_TX_BASE_WEIGHT = 42; const FUNDING_OUTPUT_SIZE = 43; +export type FundingInputScriptType = 'p2wpkh' | 'p2wsh' | 'p2tr'; + +export const FUNDING_INPUT_MAX_WITNESS_LEN: Record< + FundingInputScriptType, + number +> = { + p2wpkh: 108, + p2wsh: 108, + p2tr: 66, +}; + +export const DEFAULT_MAX_WITNESS_LEN = FUNDING_INPUT_MAX_WITNESS_LEN.p2wpkh; + +export const getMaxWitnessLen = ( + scriptType: FundingInputScriptType = 'p2wpkh', +): number => FUNDING_INPUT_MAX_WITNESS_LEN[scriptType]; + +export const fundingInputVBytes = ( + maxWitnessLen = DEFAULT_MAX_WITNESS_LEN, + scriptSigLength = 0, +): bigint => + BigInt( + new Decimal(164 + maxWitnessLen + scriptSigLength).div(4).ceil().toNumber(), + ); + +const fundingInputWithWitnessLen = (maxWitnessLen: number): FundingInput => { + const input = new FundingInput(); + input.maxWitnessLen = maxWitnessLen; + input.redeemScript = Buffer.from('', 'hex'); + return input; +}; + +const fundingInputsWithWitnessLens = ( + count: number, + maxWitnessLenOrLens: number | number[] = DEFAULT_MAX_WITNESS_LEN, +): FundingInput[] => { + const witnessLens = Array.isArray(maxWitnessLenOrLens) + ? maxWitnessLenOrLens + : Array.from({ length: count }, () => maxWitnessLenOrLens); + + return witnessLens.map(fundingInputWithWitnessLen); +}; + export class DualFundingTxFinalizer { constructor( readonly offerInputs: FundingInput[], @@ -175,19 +218,19 @@ export const getFinalizer = ( offerInputs?: FundingInput[], acceptInputs?: FundingInput[], numContracts?: number, + offerMaxWitnessLen: number | number[] = DEFAULT_MAX_WITNESS_LEN, + acceptMaxWitnessLen: number | number[] = DEFAULT_MAX_WITNESS_LEN, ): DualFundingTxFinalizer => { - const input = new FundingInput(); - input.maxWitnessLen = 108; - input.redeemScript = Buffer.from('', 'hex'); - const fakeSPK = Buffer.from( '0014663117d27e78eb432505180654e603acb30e8a4a', 'hex', ); - offerInputs = offerInputs || Array.from({ length: 1 }, () => input); + offerInputs = + offerInputs || fundingInputsWithWitnessLens(1, offerMaxWitnessLen); - acceptInputs = acceptInputs || Array.from({ length: 1 }, () => input); + acceptInputs = + acceptInputs || fundingInputsWithWitnessLens(1, acceptMaxWitnessLen); return new DualFundingTxFinalizer( offerInputs, @@ -206,12 +249,16 @@ export const getFinalizerByCount = ( numOfferInputs: number, numAcceptInputs: number, numContracts: number, + offerMaxWitnessLen: number | number[] = DEFAULT_MAX_WITNESS_LEN, + acceptMaxWitnessLen: number | number[] = DEFAULT_MAX_WITNESS_LEN, ): DualFundingTxFinalizer => { - const input = new FundingInput(); - input.maxWitnessLen = 108; - input.redeemScript = Buffer.from('', 'hex'); - - const offerInputs = Array.from({ length: numOfferInputs }, () => input); - const acceptInputs = Array.from({ length: numAcceptInputs }, () => input); + const offerInputs = fundingInputsWithWitnessLens( + numOfferInputs, + offerMaxWitnessLen, + ); + const acceptInputs = fundingInputsWithWitnessLens( + numAcceptInputs, + acceptMaxWitnessLen, + ); return getFinalizer(feeRate, offerInputs, acceptInputs, numContracts); }; From 980de6683094c1fe5a0eca77cff99c713336a842 Mon Sep 17 00:00:00 2001 From: w3irdrobot Date: Tue, 14 Jul 2026 15:00:11 -0400 Subject: [PATCH 2/4] test: fix bigint fee assertions --- packages/core/__tests__/dlc/CoinSelect.spec.ts | 6 +++--- packages/core/__tests__/dlc/TxFinalizer.spec.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/core/__tests__/dlc/CoinSelect.spec.ts b/packages/core/__tests__/dlc/CoinSelect.spec.ts index ec2f7b5e..ec5e760b 100644 --- a/packages/core/__tests__/dlc/CoinSelect.spec.ts +++ b/packages/core/__tests__/dlc/CoinSelect.spec.ts @@ -186,7 +186,7 @@ describe('CoinSelect', () => { const p2wpkhFee = dualFees(feeRate, 1, 1); const p2trFee = dualFees(feeRate, [getMaxWitnessLen('p2tr')], 1); - expect(p2trFee).to.be.lessThan(p2wpkhFee); + expect(p2trFee < p2wpkhFee).to.equal(true); }); it('should select taproot UTXOs using taproot witness costs', () => { @@ -228,9 +228,9 @@ describe('CoinSelect', () => { scriptType: 'p2wpkh', }; - expect(p2trRequired).to.be.lessThan(p2wpkhRequired); + expect(p2trRequired < p2wpkhRequired).to.equal(true); expect(BigInt(p2trUtxo.value)).to.equal(p2trRequired); - expect(BigInt(p2trUtxo.value)).to.be.lessThan(p2wpkhRequired); + expect(BigInt(p2trUtxo.value) < p2wpkhRequired).to.equal(true); const taprootSelection = dualFundingCoinSelect( [p2trUtxo], diff --git a/packages/core/__tests__/dlc/TxFinalizer.spec.ts b/packages/core/__tests__/dlc/TxFinalizer.spec.ts index 8c086095..e0d1f341 100644 --- a/packages/core/__tests__/dlc/TxFinalizer.spec.ts +++ b/packages/core/__tests__/dlc/TxFinalizer.spec.ts @@ -36,12 +36,12 @@ describe('TxFinalizer', () => { getMaxWitnessLen('p2tr'), ); - expect(p2trFinalizer.offerFundingFee).to.be.lessThan( - p2wpkhFinalizer.offerFundingFee, - ); - expect(p2trFinalizer.acceptFundingFee).to.be.lessThan( - p2wpkhFinalizer.acceptFundingFee, - ); + expect( + p2trFinalizer.offerFundingFee < p2wpkhFinalizer.offerFundingFee, + ).to.equal(true); + expect( + p2trFinalizer.acceptFundingFee < p2wpkhFinalizer.acceptFundingFee, + ).to.equal(true); }); }); }); From 3ca5fa9d9edf2953eeeb81fb2e0a89fa8f4dfee0 Mon Sep 17 00:00:00 2001 From: w3irdrobot Date: Tue, 14 Jul 2026 15:30:13 -0400 Subject: [PATCH 3/4] test: update witness fee expectations --- .../core/__tests__/dlc/CoinSelect.spec.ts | 19 +++---------------- .../__tests__/dlc/finance/CsoInfo.spec.ts | 6 +++--- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/packages/core/__tests__/dlc/CoinSelect.spec.ts b/packages/core/__tests__/dlc/CoinSelect.spec.ts index ec5e760b..d88cd85f 100644 --- a/packages/core/__tests__/dlc/CoinSelect.spec.ts +++ b/packages/core/__tests__/dlc/CoinSelect.spec.ts @@ -196,22 +196,9 @@ describe('CoinSelect', () => { 'bcrt1pqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpqqenm'; const p2trFee = dualFees(feeRate, [getMaxWitnessLen('p2tr')], 1); const p2wpkhFee = dualFees(feeRate, [getMaxWitnessLen('p2wpkh')], 1); - const p2trDust = dustThreshold(feeRate, { - address: p2trAddress, - txid: '', - value: 0, - vout: 0, - scriptType: 'p2tr', - }); - const p2wpkhDust = dustThreshold(feeRate, { - address: 'bcrt1qjzut0906d9sk4hml4k6sz6cssljktf4c7yl80f', - txid: '', - value: 0, - vout: 0, - scriptType: 'p2wpkh', - }); - const p2trRequired = offerCollateral.sats + p2trFee + p2trDust; - const p2wpkhRequired = offerCollateral.sats + p2wpkhFee + p2wpkhDust; + const changeBuffer = dustThreshold(feeRate); + const p2trRequired = offerCollateral.sats + p2trFee + changeBuffer; + const p2wpkhRequired = offerCollateral.sats + p2wpkhFee + changeBuffer; // Value sits above Taproot requirements but below P2WPKH requirements. const p2trUtxo: UTXO = { diff --git a/packages/core/__tests__/dlc/finance/CsoInfo.spec.ts b/packages/core/__tests__/dlc/finance/CsoInfo.spec.ts index 8f3775ab..3103e4ce 100644 --- a/packages/core/__tests__/dlc/finance/CsoInfo.spec.ts +++ b/packages/core/__tests__/dlc/finance/CsoInfo.spec.ts @@ -701,7 +701,7 @@ describe('CsoInfo', () => { expect(actualNormalizedMaxGain.sats).to.equal( expectedNormalizedMaxGain.sats, ); // TODO: Fix issue with this line - expect(minPayout).to.equal(BigInt(121200)); + expect(minPayout).to.equal(BigInt(121700)); expect(maxPayout).to.equal(collateral.sats); expect(actualContractSize.sats).to.equal(contractSize.sats); expect(actualOfferCollateral.sats).to.equal( @@ -785,7 +785,7 @@ describe('CsoInfo', () => { expect(actualMaxLossForContractSize.sats).to.equal( expectedMaxLossForContractSize.sats, ); - expect(minPayout).to.equal(BigInt(121200)); + expect(minPayout).to.equal(BigInt(121700)); expect(maxPayout).to.equal(collateral.sats); expect(actualContractSize.sats).to.equal(contractSize.sats); expect(actualOfferCollateral.sats).to.equal( @@ -869,7 +869,7 @@ describe('CsoInfo', () => { expect(actualMaxLossForContractSize.sats).to.equal( expectedMaxLossForContractSize.sats, ); - expect(minPayout).to.equal(BigInt(121200)); + expect(minPayout).to.equal(BigInt(121700)); expect(maxPayout).to.equal(collateral.sats); expect(actualContractSize.sats).to.equal(contractSize.sats); expect(actualOfferCollateral.sats).to.equal( From 8b20f3f983288da52d5880531f2af0a93578acfd Mon Sep 17 00:00:00 2001 From: w3irdrobot Date: Fri, 17 Jul 2026 12:59:48 -0400 Subject: [PATCH 4/4] fix: validate witness length inputs --- packages/core/__tests__/dlc/TxFinalizer.spec.ts | 6 ++++++ packages/core/lib/dlc/CoinSelect.ts | 10 +--------- packages/core/lib/dlc/TxFinalizer.ts | 13 ++++++++++++- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/core/__tests__/dlc/TxFinalizer.spec.ts b/packages/core/__tests__/dlc/TxFinalizer.spec.ts index e0d1f341..3cb3a9b8 100644 --- a/packages/core/__tests__/dlc/TxFinalizer.spec.ts +++ b/packages/core/__tests__/dlc/TxFinalizer.spec.ts @@ -43,5 +43,11 @@ describe('TxFinalizer', () => { p2trFinalizer.acceptFundingFee < p2wpkhFinalizer.acceptFundingFee, ).to.equal(true); }); + + it('should reject mismatched witness length arrays', () => { + expect(() => + getFinalizerByCount(BigInt(1), 2, 1, 1, [getMaxWitnessLen('p2tr')]), + ).to.throw('Expected 2 witness lengths, got 1'); + }); }); }); diff --git a/packages/core/lib/dlc/CoinSelect.ts b/packages/core/lib/dlc/CoinSelect.ts index c3a5cfc6..a8753255 100644 --- a/packages/core/lib/dlc/CoinSelect.ts +++ b/packages/core/lib/dlc/CoinSelect.ts @@ -1,10 +1,9 @@ -import { FundingInput } from '@node-dlc/messaging'; - import { DEFAULT_MAX_WITNESS_LEN, DualFundingTxFinalizer, FundingInputScriptType, fundingInputVBytes, + fundingInputWithWitnessLen, getMaxWitnessLen, } from './TxFinalizer'; @@ -63,13 +62,6 @@ const utxoScore = (x: UTXO, feeRate: bigint): bigint => { return BigInt(x.value) - feeRate * inputBytes(x); }; -const fundingInputWithWitnessLen = (maxWitnessLen: number): FundingInput => { - const input = new FundingInput(); - input.maxWitnessLen = maxWitnessLen; - input.redeemScript = Buffer.from('', 'hex'); - return input; -}; - const witnessLensFromInputCount = ( numInputsOrWitnessLens: number | number[], ): number[] => diff --git a/packages/core/lib/dlc/TxFinalizer.ts b/packages/core/lib/dlc/TxFinalizer.ts index c71c5635..ba48483a 100644 --- a/packages/core/lib/dlc/TxFinalizer.ts +++ b/packages/core/lib/dlc/TxFinalizer.ts @@ -29,7 +29,9 @@ export const fundingInputVBytes = ( new Decimal(164 + maxWitnessLen + scriptSigLength).div(4).ceil().toNumber(), ); -const fundingInputWithWitnessLen = (maxWitnessLen: number): FundingInput => { +export const fundingInputWithWitnessLen = ( + maxWitnessLen: number, +): FundingInput => { const input = new FundingInput(); input.maxWitnessLen = maxWitnessLen; input.redeemScript = Buffer.from('', 'hex'); @@ -40,6 +42,15 @@ const fundingInputsWithWitnessLens = ( count: number, maxWitnessLenOrLens: number | number[] = DEFAULT_MAX_WITNESS_LEN, ): FundingInput[] => { + if ( + Array.isArray(maxWitnessLenOrLens) && + maxWitnessLenOrLens.length !== count + ) { + throw new Error( + `Expected ${count} witness lengths, got ${maxWitnessLenOrLens.length}`, + ); + } + const witnessLens = Array.isArray(maxWitnessLenOrLens) ? maxWitnessLenOrLens : Array.from({ length: count }, () => maxWitnessLenOrLens);