Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions modules/unspents/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
},
"dependencies": {
"@bitgo/utxo-lib": "^11.19.0",
"@bitgo/wasm-utxo": "^1.17.0",
"lodash": "~4.17.21",
"tcomb": "~3.2.29",
"varuint-bitcoin": "^1.0.4"
Expand Down
117 changes: 112 additions & 5 deletions modules/unspents/src/dimensions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as utxolib from '@bitgo/utxo-lib';
import { bitgo } from '@bitgo/utxo-lib';
import { fixedScriptWallet } from '@bitgo/wasm-utxo';

const { isChainCode, scriptTypeForChain } = bitgo;
type ChainCode = bitgo.ChainCode;

Expand Down Expand Up @@ -227,20 +229,75 @@ export class Dimensions {
};

/**
* @return
* Wasm-utxo input script type names (aliases for utxolib types)
*/
static readonly WasmInputScriptTypes = ['p2trLegacy', 'p2trMusig2ScriptPath', 'p2trMusig2KeyPath'] as const;

/**
* All supported input script type names (utxolib + wasm-utxo)
*/
static readonly InputScriptTypes = [
// utxolib names
'p2sh',
'p2shP2wsh',
'p2wsh',
'p2shP2pk',
'p2tr',
'p2trMusig2',
'taprootKeyPathSpend',
'taprootScriptPathSpend',
// wasm-utxo names (aliases)
...Dimensions.WasmInputScriptTypes,
] as const;

/**
* Create Dimensions from a script type.
*
* Accepts both utxolib and wasm-utxo naming conventions:
* - utxolib: 'p2sh', 'p2shP2wsh', 'p2wsh', 'p2shP2pk', 'p2tr', 'p2trMusig2', 'taprootKeyPathSpend', 'taprootScriptPathSpend'
* - wasm-utxo: 'p2trLegacy', 'p2trMusig2ScriptPath', 'p2trMusig2KeyPath'
*
* @param scriptType - The script type string (from utxolib or wasm-utxo)
* @param params - Optional parameters
* @param params.scriptPathLevel - For taproot script path spends, specify level 1 or 2.
* - For 'p2tr'/'taprootScriptPathSpend': required (1 or 2)
* - For 'p2trMusig2': undefined = key path, 1 = script path
* - For 'p2trLegacy': defaults to 2 (recovery path) if not specified
* @returns Dimensions for the input
*
* @example
* ```typescript
* // Using utxolib names
* Dimensions.fromScriptType('p2shP2wsh');
* Dimensions.fromScriptType('taprootKeyPathSpend');
*
* // Using wasm-utxo names (from parseTransactionWithWalletKeys)
* Dimensions.fromScriptType('p2trMusig2KeyPath');
* Dimensions.fromScriptType('p2trLegacy', { scriptPathLevel: 1 });
* ```
*/
static fromScriptType(
scriptType: utxolib.bitgo.outputScripts.ScriptType | utxolib.bitgo.ParsedScriptType2Of3 | 'p2pkh',
scriptType:
| utxolib.bitgo.outputScripts.ScriptType
| utxolib.bitgo.ParsedScriptType2Of3
| 'p2pkh'
// wasm-utxo names (aliases)
| 'p2trLegacy'
| 'p2trMusig2ScriptPath'
| 'p2trMusig2KeyPath',
params: {
scriptPathLevel?: number;
} = {}
): Dimensions {
switch (scriptType) {
// Common types (same name in both utxolib and wasm-utxo)
case 'p2sh':
case 'p2shP2wsh':
case 'p2wsh':
case 'p2shP2pk':
return Dimensions.SingleInput[scriptType];

// utxolib taproot names
case 'p2tr':
case 'taprootScriptPathSpend':
switch (params.scriptPathLevel) {
Expand All @@ -262,6 +319,25 @@ export class Dimensions {
}
case 'taprootKeyPathSpend':
return Dimensions.SingleInput.p2trKeypath;

// wasm-utxo taproot names (aliases)
case 'p2trLegacy':
// Legacy taproot (non-musig2) script path; default to level 2 (recovery) if not specified
switch (params.scriptPathLevel ?? 2) {
case 1:
return Dimensions.SingleInput.p2trScriptPathLevel1;
case 2:
return Dimensions.SingleInput.p2trScriptPathLevel2;
default:
throw new Error(`unexpected script path level for p2trLegacy: ${params.scriptPathLevel}`);
}
case 'p2trMusig2ScriptPath':
// MuSig2 script path spend (always level 1)
return Dimensions.SingleInput.p2trScriptPathLevel1;
case 'p2trMusig2KeyPath':
// MuSig2 key path spend
return Dimensions.SingleInput.p2trKeypath;

default:
throw new Error(`unexpected scriptType ${scriptType}`);
}
Expand Down Expand Up @@ -431,11 +507,42 @@ export class Dimensions {
}

/**
* Create Dimensions from psbt inputs and outputs
* @param psbt
* Create Dimensions from a PSBT.
*
* Accepts either:
* - A utxolib UtxoPsbt instance
* - A wasm-utxo BitGoPsbt instance + network
*
* @param psbt - Either a UtxoPsbt instance or a wasm-utxo BitGoPsbt instance
* @param network - Required when passing wasm-utxo BitGoPsbt, optional for utxolib UtxoPsbt
* @return {Dimensions}
*
* @example
* ```typescript
* // With utxolib PSBT (existing usage)
* const dims = Dimensions.fromPsbt(utxolibPsbt);
*
* // With wasm-utxo PSBT (new usage)
* const dims = Dimensions.fromPsbt(wasmPsbt, network);
* ```
*/
static fromPsbt(psbt: bitgo.UtxoPsbt): Dimensions {
static fromPsbt(psbt: bitgo.UtxoPsbt): Dimensions;
static fromPsbt(wasmPsbt: fixedScriptWallet.BitGoPsbt, network: utxolib.Network): Dimensions;
static fromPsbt(psbt: bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt, network?: utxolib.Network): Dimensions {
// Check if it's a wasm-utxo BitGoPsbt
if (psbt instanceof fixedScriptWallet.BitGoPsbt) {
// wasm-utxo BitGoPsbt - serialize and parse with utxolib
if (!network) {
throw new Error('network is required when passing wasm-utxo BitGoPsbt');
}
const buffer = Buffer.from(psbt.serialize());
const utxolibPsbt = bitgo.createPsbtFromBuffer(buffer, network);
return Dimensions.fromPsbtInputs(utxolibPsbt.data.inputs).plus(
Dimensions.fromOutputs(utxolibPsbt.getUnsignedTx().outs)
);
}

// utxolib UtxoPsbt
return Dimensions.fromPsbtInputs(psbt.data.inputs).plus(Dimensions.fromOutputs(psbt.getUnsignedTx().outs));
}

Expand Down
48 changes: 48 additions & 0 deletions modules/unspents/test/dimensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,54 @@ describe('Dimensions from unspent types', function () {
});
});

describe('Dimensions fromScriptType with wasm-utxo names', function () {
it('accepts wasm-utxo script type names', function () {
// wasm-utxo taproot names
Dimensions.fromScriptType('p2trMusig2KeyPath').should.eql(Dimensions.SingleInput.p2trKeypath);
Dimensions.fromScriptType('p2trMusig2ScriptPath').should.eql(Dimensions.SingleInput.p2trScriptPathLevel1);

// p2trLegacy defaults to level 2 (recovery path)
Dimensions.fromScriptType('p2trLegacy').should.eql(Dimensions.SingleInput.p2trScriptPathLevel2);
Dimensions.fromScriptType('p2trLegacy', { scriptPathLevel: 1 }).should.eql(
Dimensions.SingleInput.p2trScriptPathLevel1
);
Dimensions.fromScriptType('p2trLegacy', { scriptPathLevel: 2 }).should.eql(
Dimensions.SingleInput.p2trScriptPathLevel2
);
});

it('accepts utxolib script type names', function () {
// utxolib names should still work
Dimensions.fromScriptType('taprootKeyPathSpend').should.eql(Dimensions.SingleInput.p2trKeypath);
Dimensions.fromScriptType('taprootScriptPathSpend', { scriptPathLevel: 1 }).should.eql(
Dimensions.SingleInput.p2trScriptPathLevel1
);
Dimensions.fromScriptType('p2trMusig2').should.eql(Dimensions.SingleInput.p2trKeypath);
Dimensions.fromScriptType('p2trMusig2', { scriptPathLevel: 1 }).should.eql(
Dimensions.SingleInput.p2trScriptPathLevel1
);
});

it('provides WasmInputScriptTypes constant', function () {
Dimensions.WasmInputScriptTypes.should.containEql('p2trLegacy');
Dimensions.WasmInputScriptTypes.should.containEql('p2trMusig2ScriptPath');
Dimensions.WasmInputScriptTypes.should.containEql('p2trMusig2KeyPath');
});

it('provides InputScriptTypes constant with all types', function () {
// utxolib types
Dimensions.InputScriptTypes.should.containEql('p2sh');
Dimensions.InputScriptTypes.should.containEql('p2shP2wsh');
Dimensions.InputScriptTypes.should.containEql('p2wsh');
Dimensions.InputScriptTypes.should.containEql('taprootKeyPathSpend');
Dimensions.InputScriptTypes.should.containEql('taprootScriptPathSpend');
// wasm-utxo types
Dimensions.InputScriptTypes.should.containEql('p2trLegacy');
Dimensions.InputScriptTypes.should.containEql('p2trMusig2ScriptPath');
Dimensions.InputScriptTypes.should.containEql('p2trMusig2KeyPath');
});
});

describe('Dimensions estimates', function () {
it('calculates vsizes', function () {
function dim(nP2shInputs: number, nP2shP2wshInputs: number, nP2wshInputs: number, nOutputs: number): Dimensions {
Expand Down
28 changes: 28 additions & 0 deletions modules/unspents/test/signedTx/txCombinations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,34 @@ describe(`Dimensions for PSBT combinations`, function () {
should.throws(() => Dimensions.fromPsbt(psbt));
});

it(`accepts wasm-utxo BitGoPsbt`, function () {
const psbt = constructPsbt(rootWalletKeys, ['p2shP2wsh'], ['p2wpkh'], 'unsigned');

// Get dimensions from UtxoPsbt directly
const dimsFromUtxolib = Dimensions.fromPsbt(psbt);

// Create a wasm-utxo BitGoPsbt from the same serialized bytes
const { fixedScriptWallet } = require('@bitgo/wasm-utxo');
const wasmPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(psbt.toBuffer(), 'bitcoin');

// Get dimensions from wasm-utxo BitGoPsbt
const dimsFromWasm = Dimensions.fromPsbt(wasmPsbt, utxolib.networks.bitcoin);

// Should be equal
dimsFromWasm.should.eql(dimsFromUtxolib);
});

it(`requires network when passing wasm-utxo BitGoPsbt`, function () {
const psbt = constructPsbt(rootWalletKeys, ['p2shP2wsh'], ['p2wpkh'], 'unsigned');

// Create a wasm-utxo BitGoPsbt
const { fixedScriptWallet } = require('@bitgo/wasm-utxo');
const wasmPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(psbt.toBuffer(), 'bitcoin');

// Should throw when network is not provided
should.throws(() => (Dimensions.fromPsbt as any)(wasmPsbt), /network is required/);
});

runCombinations(params, (inputTypeCombo: InputScriptType[], outputTypeCombo: TestUnspentType[]) => {
const expectedInputDims = Dimensions.sum(...inputTypeCombo.map(getInputDimensionsForUnspentType));
const expectedOutputDims = Dimensions.sum(...outputTypeCombo.map(getOutputDimensionsForUnspentType));
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,11 @@
resolved "https://registry.npmjs.org/@bitgo/wasm-utxo/-/wasm-utxo-1.17.0.tgz#ea5c6ebd1a9a50965aa987485db2e875433e7275"
integrity sha512-1/ZO6MRRvHzKggwNGc0pCs781Xi8tTwsAbC2M+3qwdDSYXRAdG2IQ+Hmx84MTJ7xKhWX5m8rtezJL47dGwT1YQ==

"@bitgo/wasm-utxo@^1.17.0":
version "1.19.0"
resolved "https://registry.npmjs.org/@bitgo/wasm-utxo/-/wasm-utxo-1.19.0.tgz#c44db54da8bfa748f3a7a24f769519ff56783236"
integrity sha512-M6NtRfJrWoJP68IF1bm2eNMzUdIGnIQjIDwcIMXaqJCuWXPQot8KbKHVJPe3EpdB9g4a/J5hd6JIhZRF8m7Dhw==

"@brandonblack/musig@^0.0.1-alpha.0":
version "0.0.1-alpha.1"
resolved "https://registry.npmjs.org/@brandonblack/musig/-/musig-0.0.1-alpha.1.tgz"
Expand Down
Loading