From b81f07a0826bd82a9be535ddee8f26321b90c41e Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Thu, 19 Feb 2026 11:40:31 +0100 Subject: [PATCH] test(wasm-utxo): remove utxo-lib dependencies from fixed-script tests Remove utxo-lib dependencies from fixed-script PSBT tests and implement fixture auto-generation. Tests now generate fixtures on-demand when missing, ensuring consistency across all signature states (unsigned, halfsigned, fullsigned). Changes: - Implement fixture generation in generateFixture.ts - Update fixtureUtil to auto-generate missing fixtures - Remove utxo-lib network references, use NetworkName type - Remove txid validation against utxo-lib - Remove OP_RETURN output construction via utxo-lib - Update all test files to use async fixture loading - Switch to format validation for txids instead of cross-library checks Issue: BTC-3047 Co-authored-by: llm-git --- packages/wasm-utxo/js/coinName.ts | 51 ++ packages/wasm-utxo/js/testutils/AcidTest.ts | 162 +++-- packages/wasm-utxo/test/dimensions.ts | 66 +- .../test/fixedScript/finalizeExtract.ts | 14 +- .../wasm-utxo/test/fixedScript/fixtureUtil.ts | 42 +- .../test/fixedScript/generateFixture.ts | 136 ++++ .../test/fixedScript/musig2Nonces.ts | 4 +- .../test/fixedScript/networkSupport.util.ts | 28 +- .../parseTransactionWithWalletKeys.ts | 43 +- .../test/fixedScript/psbtReconstruction.ts | 35 +- .../fixedScript/signAndVerifySignature.ts | 27 +- packages/wasm-utxo/test/fixtures.ts | 13 +- .../psbt-lite.bitcoin.fullsigned.json | 95 +-- .../psbt-lite.bitcoin.halfsigned.json | 95 +-- .../psbt-lite.bitcoin.unsigned.json | 95 +-- .../psbt-lite.bitcoincash.fullsigned.json | 115 +--- .../psbt-lite.bitcoincash.halfsigned.json | 88 +-- .../psbt-lite.bitcoincash.unsigned.json | 76 +-- .../psbt-lite.bitcoingold.fullsigned.json | 220 ++---- .../psbt-lite.bitcoingold.halfsigned.json | 168 ++--- .../psbt-lite.bitcoingold.unsigned.json | 144 ++-- .../psbt-lite.dash.fullsigned.json | 113 +--- .../psbt-lite.dash.halfsigned.json | 86 +-- .../fixed-script/psbt-lite.dash.unsigned.json | 74 +-- .../psbt-lite.dogecoin.fullsigned.json | 113 +--- .../psbt-lite.dogecoin.halfsigned.json | 86 +-- .../psbt-lite.dogecoin.unsigned.json | 74 +-- .../psbt-lite.ecash.fullsigned.json | 115 +--- .../psbt-lite.ecash.halfsigned.json | 88 +-- .../psbt-lite.ecash.unsigned.json | 76 +-- .../psbt-lite.litecoin.fullsigned.json | 218 ++---- .../psbt-lite.litecoin.halfsigned.json | 166 ++--- .../psbt-lite.litecoin.unsigned.json | 142 ++-- .../psbt-lite.zcash.fullsigned.json | 104 +-- .../psbt-lite.zcash.halfsigned.json | 77 +-- .../psbt-lite.zcash.unsigned.json | 65 +- .../fixed-script/psbt.bitcoin.fullsigned.json | 626 ------------------ .../fixed-script/psbt.bitcoin.halfsigned.json | 542 --------------- .../fixed-script/psbt.bitcoin.unsigned.json | 473 ------------- .../psbt.bitcoincash.fullsigned.json | 154 ----- .../psbt.bitcoincash.halfsigned.json | 139 ---- .../psbt.bitcoincash.unsigned.json | 127 ---- .../psbt.bitcoingold.fullsigned.json | 309 --------- .../psbt.bitcoingold.halfsigned.json | 269 -------- .../psbt.bitcoingold.unsigned.json | 245 ------- .../fixed-script/psbt.dash.fullsigned.json | 154 ----- .../fixed-script/psbt.dash.halfsigned.json | 139 ---- .../fixed-script/psbt.dash.unsigned.json | 127 ---- .../psbt.dogecoin.fullsigned.json | 154 ----- .../psbt.dogecoin.halfsigned.json | 139 ---- .../fixed-script/psbt.dogecoin.unsigned.json | 127 ---- .../fixed-script/psbt.ecash.fullsigned.json | 154 ----- .../fixed-script/psbt.ecash.halfsigned.json | 139 ---- .../fixed-script/psbt.ecash.unsigned.json | 127 ---- .../psbt.litecoin.fullsigned.json | 309 --------- .../psbt.litecoin.halfsigned.json | 269 -------- .../fixed-script/psbt.litecoin.unsigned.json | 245 ------- .../fixed-script/psbt.zcash.fullsigned.json | 163 ----- .../fixed-script/psbt.zcash.halfsigned.json | 142 ---- .../fixed-script/psbt.zcash.unsigned.json | 130 ---- 60 files changed, 1196 insertions(+), 7520 deletions(-) create mode 100644 packages/wasm-utxo/test/fixedScript/generateFixture.ts delete mode 100644 packages/wasm-utxo/test/fixtures/fixed-script/psbt.bitcoin.fullsigned.json delete mode 100644 packages/wasm-utxo/test/fixtures/fixed-script/psbt.bitcoin.halfsigned.json delete mode 100644 packages/wasm-utxo/test/fixtures/fixed-script/psbt.bitcoin.unsigned.json delete mode 100644 packages/wasm-utxo/test/fixtures/fixed-script/psbt.bitcoincash.fullsigned.json delete mode 100644 packages/wasm-utxo/test/fixtures/fixed-script/psbt.bitcoincash.halfsigned.json delete mode 100644 packages/wasm-utxo/test/fixtures/fixed-script/psbt.bitcoincash.unsigned.json delete mode 100644 packages/wasm-utxo/test/fixtures/fixed-script/psbt.bitcoingold.fullsigned.json delete mode 100644 packages/wasm-utxo/test/fixtures/fixed-script/psbt.bitcoingold.halfsigned.json delete mode 100644 packages/wasm-utxo/test/fixtures/fixed-script/psbt.bitcoingold.unsigned.json delete mode 100644 packages/wasm-utxo/test/fixtures/fixed-script/psbt.dash.fullsigned.json delete mode 100644 packages/wasm-utxo/test/fixtures/fixed-script/psbt.dash.halfsigned.json delete mode 100644 packages/wasm-utxo/test/fixtures/fixed-script/psbt.dash.unsigned.json delete mode 100644 packages/wasm-utxo/test/fixtures/fixed-script/psbt.dogecoin.fullsigned.json delete mode 100644 packages/wasm-utxo/test/fixtures/fixed-script/psbt.dogecoin.halfsigned.json delete mode 100644 packages/wasm-utxo/test/fixtures/fixed-script/psbt.dogecoin.unsigned.json delete mode 100644 packages/wasm-utxo/test/fixtures/fixed-script/psbt.ecash.fullsigned.json delete mode 100644 packages/wasm-utxo/test/fixtures/fixed-script/psbt.ecash.halfsigned.json delete mode 100644 packages/wasm-utxo/test/fixtures/fixed-script/psbt.ecash.unsigned.json delete mode 100644 packages/wasm-utxo/test/fixtures/fixed-script/psbt.litecoin.fullsigned.json delete mode 100644 packages/wasm-utxo/test/fixtures/fixed-script/psbt.litecoin.halfsigned.json delete mode 100644 packages/wasm-utxo/test/fixtures/fixed-script/psbt.litecoin.unsigned.json delete mode 100644 packages/wasm-utxo/test/fixtures/fixed-script/psbt.zcash.fullsigned.json delete mode 100644 packages/wasm-utxo/test/fixtures/fixed-script/psbt.zcash.halfsigned.json delete mode 100644 packages/wasm-utxo/test/fixtures/fixed-script/psbt.zcash.unsigned.json diff --git a/packages/wasm-utxo/js/coinName.ts b/packages/wasm-utxo/js/coinName.ts index 7dd350a2..aae222bb 100644 --- a/packages/wasm-utxo/js/coinName.ts +++ b/packages/wasm-utxo/js/coinName.ts @@ -64,3 +64,54 @@ export function isTestnet(name: CoinName): boolean { export function isCoinName(v: string): v is CoinName { return (coinNames as readonly string[]).includes(v); } + +import type { UtxolibName } from "./utxolibCompat.js"; + +/** Convert a CoinName or UtxolibName to CoinName */ +export function toCoinName(name: CoinName | UtxolibName): CoinName { + switch (name) { + case "bitcoin": + return "btc"; + case "testnet": + return "tbtc"; + case "bitcoinTestnet4": + return "tbtc4"; + case "bitcoinPublicSignet": + return "tbtcsig"; + case "bitcoinBitGoSignet": + return "tbtcbgsig"; + case "bitcoincash": + return "bch"; + case "bitcoincashTestnet": + return "tbch"; + case "ecash": + return "bcha"; + case "ecashTest": + return "tbcha"; + case "bitcoingold": + return "btg"; + case "bitcoingoldTestnet": + return "tbtg"; + case "bitcoinsv": + return "bsv"; + case "bitcoinsvTestnet": + return "tbsv"; + case "dashTest": + return "tdash"; + case "dogecoin": + return "doge"; + case "dogecoinTest": + return "tdoge"; + case "litecoin": + return "ltc"; + case "litecoinTest": + return "tltc"; + case "zcash": + return "zec"; + case "zcashTest": + return "tzec"; + default: + // CoinName values pass through (including "dash" which is both CoinName and UtxolibName) + return name; + } +} diff --git a/packages/wasm-utxo/js/testutils/AcidTest.ts b/packages/wasm-utxo/js/testutils/AcidTest.ts index 19fb06e2..a388a3d0 100644 --- a/packages/wasm-utxo/js/testutils/AcidTest.ts +++ b/packages/wasm-utxo/js/testutils/AcidTest.ts @@ -1,10 +1,9 @@ -import { BitGoPsbt, type SignerKey } from "../fixedScriptWallet/BitGoPsbt.js"; +import { BitGoPsbt, type NetworkName, type SignerKey } from "../fixedScriptWallet/BitGoPsbt.js"; import { ZcashBitGoPsbt } from "../fixedScriptWallet/ZcashBitGoPsbt.js"; import { RootWalletKeys } from "../fixedScriptWallet/RootWalletKeys.js"; import { BIP32 } from "../bip32.js"; import { ECPair } from "../ecpair.js"; import { - assertChainCode, ChainCode, createOpReturnScript, inputScriptTypes, @@ -16,7 +15,7 @@ import { type ScriptId, } from "../fixedScriptWallet/index.js"; import type { CoinName } from "../coinName.js"; -import { coinNames, isMainnet } from "../coinName.js"; +import { coinNames, isMainnet, toCoinName } from "../coinName.js"; import { getDefaultWalletKeys, getWalletKeysForSeed, getKeyTriple } from "./keys.js"; import type { Triple } from "../triple.js"; @@ -91,6 +90,22 @@ type SuiteConfig = { // Re-export for convenience export { inputScriptTypes, outputScriptTypes }; +/** Map InputScriptType to the OutputScriptType used for chain code derivation */ +function inputScriptTypeToOutputScriptType(scriptType: InputScriptType): OutputScriptType { + switch (scriptType) { + case "p2sh": + case "p2shP2wsh": + case "p2wsh": + case "p2trLegacy": + return scriptType; + case "p2shP2pk": + return "p2sh"; + case "p2trMusig2ScriptPath": + case "p2trMusig2KeyPath": + return "p2trMusig2"; + } +} + /** * Creates a valid PSBT with as many features as possible (kitchen sink). * @@ -113,7 +128,7 @@ export { inputScriptTypes, outputScriptTypes }; * - psbt-lite: Only witness_utxo (no non_witness_utxo) */ export class AcidTest { - public readonly network: CoinName; + public readonly network: CoinName | NetworkName; public readonly signStage: SignStage; public readonly txFormat: TxFormat; public readonly rootWalletKeys: RootWalletKeys; @@ -126,7 +141,7 @@ export class AcidTest { private readonly bitgoXprv: BIP32; constructor( - network: CoinName, + network: CoinName | NetworkName, signStage: SignStage, txFormat: TxFormat, rootWalletKeys: RootWalletKeys, @@ -151,13 +166,14 @@ export class AcidTest { * Create an AcidTest with specific configuration */ static withConfig( - network: CoinName, + network: CoinName | NetworkName, signStage: SignStage, txFormat: TxFormat, suiteConfig: SuiteConfig = {}, ): AcidTest { const rootWalletKeys = getDefaultWalletKeys(); const otherWalletKeys = getWalletKeysForSeed("too many secrets"); + const coin = toCoinName(network); // Filter inputs based on network support const inputs: Input[] = inputScriptTypes @@ -167,9 +183,9 @@ export class AcidTest { // Map input script types to output script types for support check if (scriptType === "p2trMusig2KeyPath" || scriptType === "p2trMusig2ScriptPath") { - return supportsScriptType(network, "p2trMusig2"); + return supportsScriptType(coin, "p2trMusig2"); } - return supportsScriptType(network, scriptType); + return supportsScriptType(coin, scriptType); }) .filter( (scriptType) => @@ -183,7 +199,7 @@ export class AcidTest { // Filter outputs based on network support const outputs: Output[] = outputScriptTypes - .filter((scriptType) => supportsScriptType(network, scriptType)) + .filter((scriptType) => supportsScriptType(coin, scriptType)) .map((scriptType, index) => ({ scriptType, value: BigInt(900 + index * 100), // Deterministic amounts @@ -232,12 +248,16 @@ export class AcidTest { */ createPsbt(): BitGoPsbt { // Use ZcashBitGoPsbt for Zcash networks - const isZcash = this.network === "zec" || this.network === "tzec"; + const isZcash = + this.network === "zec" || + this.network === "tzec" || + this.network === "zcash" || + this.network === "zcashTest"; const psbt = isZcash - ? ZcashBitGoPsbt.createEmptyWithConsensusBranchId(this.network, this.rootWalletKeys, { - version: 2, - lockTime: 0, - consensusBranchId: 0xc2d6d0b4, // NU5 + ? ZcashBitGoPsbt.createEmpty(this.network, this.rootWalletKeys, { + // Sapling activation height: mainnet=419200, testnet=280000 + blockHeight: + this.network === "zec" || this.network === "zcash" ? 419200 : 280000, }) : BitGoPsbt.createEmpty(this.network, this.rootWalletKeys, { version: 2, @@ -246,54 +266,36 @@ export class AcidTest { // Add inputs with deterministic outpoints this.inputs.forEach((input, index) => { - // Resolve scriptId: either from explicit scriptId or from scriptType + index - const scriptId: ScriptId = input.scriptId ?? { - chain: ChainCode.value("p2sh", "external"), - index: input.index ?? index, - }; const walletKeys = input.walletKeys ?? this.rootWalletKeys; + const outpoint = { txid: "0".repeat(64), vout: index, value: input.value }; - // Get scriptType: either explicit or derive from scriptId chain - const scriptType = input.scriptType ?? ChainCode.scriptType(assertChainCode(scriptId.chain)); + // scriptId variant: caller provides explicit chain + index + if (input.scriptId) { + psbt.addWalletInput(outpoint, walletKeys, { + scriptId: input.scriptId, + signPath: { signer: "user", cosigner: "bitgo" }, + }); + return; + } + + const scriptType = input.scriptType ?? "p2sh"; if (scriptType === "p2shP2pk") { - // Add replay protection input - const replayKey = this.getReplayProtectionKey(); - // Convert BIP32 to ECPair using public key - const ecpair = ECPair.fromPublicKey(replayKey.publicKey); - psbt.addReplayProtectionInput( - { - txid: "0".repeat(64), - vout: index, - value: input.value, - }, - ecpair, - ); - } else { - // Determine signing path based on input type - let signPath: { signer: SignerKey; cosigner: SignerKey }; + const ecpair = ECPair.fromPublicKey(this.getReplayProtectionKey().publicKey); + psbt.addReplayProtectionInput(outpoint, ecpair); + return; + } - if (scriptType === "p2trMusig2ScriptPath") { - // Script path uses user + backup - signPath = { signer: "user", cosigner: "backup" }; - } else { - // Default: user + bitgo - signPath = { signer: "user", cosigner: "bitgo" }; - } + const scriptId: ScriptId = { + chain: ChainCode.value(inputScriptTypeToOutputScriptType(scriptType), "external"), + index: input.index ?? index, + }; + const signPath: { signer: SignerKey; cosigner: SignerKey } = + scriptType === "p2trMusig2ScriptPath" + ? { signer: "user", cosigner: "backup" } + : { signer: "user", cosigner: "bitgo" }; - psbt.addWalletInput( - { - txid: "0".repeat(64), - vout: index, - value: input.value, - }, - walletKeys, - { - scriptId, - signPath, - }, - ); - } + psbt.addWalletInput(outpoint, walletKeys, { scriptId, signPath }); }); // Add outputs @@ -366,40 +368,32 @@ export class AcidTest { ); if (hasMusig2Inputs) { - const isZcash = this.network === "zec" || this.network === "tzec"; + const isZcash = + this.network === "zec" || + this.network === "tzec" || + this.network === "zcash" || + this.network === "zcashTest"; if (isZcash) { throw new Error("Zcash does not support MuSig2/Taproot inputs"); } - // Generate nonces with user key + // MuSig2 requires ALL participant nonces before ANY signing. + // Generate nonces directly on the same PSBT for each participant key. psbt.generateMusig2Nonces(userKey); - if (this.signStage === "fullsigned") { - // Create a second PSBT with cosigner nonces for combination - // For p2trMusig2ScriptPath use backup, for p2trMusig2KeyPath use bitgo - // Since we might have both types, we need to generate nonces separately - const bytes = psbt.serialize(); - - const hasKeyPath = this.inputs.some((input) => input.scriptType === "p2trMusig2KeyPath"); - const hasScriptPath = this.inputs.some( - (input) => input.scriptType === "p2trMusig2ScriptPath", - ); - - if (hasKeyPath && !hasScriptPath) { - // Only key path inputs - generate bitgo nonces for all - const psbt2 = BitGoPsbt.fromBytes(bytes, this.network); - psbt2.generateMusig2Nonces(bitgoKey); - psbt.combineMusig2Nonces(psbt2); - } else if (hasScriptPath && !hasKeyPath) { - // Only script path inputs - generate backup nonces for all - const psbt2 = BitGoPsbt.fromBytes(bytes, this.network); - psbt2.generateMusig2Nonces(backupKey); - psbt.combineMusig2Nonces(psbt2); - } else { - const psbt2 = BitGoPsbt.fromBytes(bytes, this.network); - psbt2.generateMusig2Nonces(bitgoKey); - psbt.combineMusig2Nonces(psbt2); - } + const hasKeyPath = this.inputs.some((input) => input.scriptType === "p2trMusig2KeyPath"); + const hasScriptPath = this.inputs.some( + (input) => input.scriptType === "p2trMusig2ScriptPath", + ); + + // Key path uses user+bitgo, script path uses user+backup. + // generateMusig2Nonces fails if the key isn't a participant in any musig2 input, + // so we only call it for keys that match. + if (hasKeyPath) { + psbt.generateMusig2Nonces(bitgoKey); + } + if (hasScriptPath) { + psbt.generateMusig2Nonces(backupKey); } } diff --git a/packages/wasm-utxo/test/dimensions.ts b/packages/wasm-utxo/test/dimensions.ts index cbc1c085..51f0e650 100644 --- a/packages/wasm-utxo/test/dimensions.ts +++ b/packages/wasm-utxo/test/dimensions.ts @@ -1,5 +1,4 @@ import assert from "node:assert"; -import * as utxolib from "@bitgo/utxo-lib"; import { Dimensions, fixedScriptWallet } from "../js/index.js"; import { Transaction } from "../js/transaction.js"; import { @@ -383,16 +382,14 @@ describe("Dimensions", function () { describe("integration tests with fixtures", function () { // Zcash has additional transaction overhead (version group, expiry height, etc.) // that we don't account for in Dimensions - skip it for now - const networksToTest = getFixtureNetworks().filter((n) => n !== utxolib.networks.zcash); - - networksToTest.forEach((network) => { - const networkName = utxolib.getNetworkName(network); + const networksToTest = getFixtureNetworks().filter((n) => n !== "zcash"); + networksToTest.forEach((networkName) => { describe(`${networkName}`, function () { let fixture: Fixture; - before(function () { - fixture = loadPsbtFixture(networkName, "fullsigned"); + before(async function () { + fixture = await loadPsbtFixture(networkName, "fullsigned"); }); it("actual vSize is within estimated min/max bounds", function () { @@ -432,48 +429,25 @@ describe("Dimensions", function () { }); describe("manual construction test", function () { - it("builds correct dimensions for bitcoin fixture", function () { - const fixture = loadPsbtFixture("bitcoin", "fullsigned"); + it("builds correct dimensions for bitcoin fixture", async function () { + const fixture = await loadPsbtFixture("bitcoin", "fullsigned"); if (!fixture.extractedTransaction) { return; } - // Build dimensions based on fixture input types: - // 0: p2sh, 1: p2shP2wsh, 2: p2wsh, 3: p2tr (script), - // 4: p2trMusig2 (script path), 5: p2trMusig2 (keypath), 6: p2shP2pk - let dim = Dimensions.empty() - .plus(Dimensions.fromInput({ chain: 0 })) // p2sh - .plus(Dimensions.fromInput({ chain: 11 })) // p2shP2wsh - .plus(Dimensions.fromInput({ chain: 21 })) // p2wsh - .plus(Dimensions.fromInput({ chain: 31 })) // p2tr script path level 1 - .plus( - Dimensions.fromInput({ - chain: 41, - signPath: { signer: "user", cosigner: "backup" }, - }), - ) // p2trMusig2 script path - .plus(Dimensions.fromInput({ chain: 41 })) // p2trMusig2 keypath - .plus(Dimensions.fromInput({ scriptType: "p2shP2pk" })); // replay protection + // Build dimensions from fixture psbtInputs (adapts to AcidTest structure) + let dim = Dimensions.empty(); + for (const psbtInput of fixture.psbtInputs) { + const scriptType = fixtureTypeToInputScriptType(psbtInput.type); + if (scriptType === null) { + throw new Error(`Unknown input type: ${psbtInput.type}`); + } + dim = dim.plus(Dimensions.fromInput({ scriptType })); + } // Add outputs dim = dim.plus(dimensionsFromOutputs(fixture.outputs)); - // Build dimensions using scriptType - let dimFromTypes = Dimensions.empty() - .plus(Dimensions.fromInput({ scriptType: "p2sh" })) - .plus(Dimensions.fromInput({ scriptType: "p2shP2wsh" })) - .plus(Dimensions.fromInput({ scriptType: "p2wsh" })) - .plus(Dimensions.fromInput({ scriptType: "p2trLegacy" })) - .plus(Dimensions.fromInput({ scriptType: "p2trMusig2ScriptPath" })) - .plus(Dimensions.fromInput({ scriptType: "p2trMusig2KeyPath" })) - .plus(Dimensions.fromInput({ scriptType: "p2shP2pk" })); - - dimFromTypes = dimFromTypes.plus(dimensionsFromOutputs(fixture.outputs)); - - // Both methods should produce same weights - assert.strictEqual(dim.getWeight("min"), dimFromTypes.getWeight("min")); - assert.strictEqual(dim.getWeight("max"), dimFromTypes.getWeight("max")); - // Get actual vSize const txBytes = Buffer.from(fixture.extractedTransaction, "hex"); const actualVSize = Transaction.fromBytes(txBytes).getVSize(); @@ -492,14 +466,12 @@ describe("Dimensions", function () { describe("fromPsbt", function () { // Zcash has additional transaction overhead that we don't account for - const networksToTest = getFixtureNetworks().filter((n) => n !== utxolib.networks.zcash); - - networksToTest.forEach((network) => { - const networkName = utxolib.getNetworkName(network); + const networksToTest = getFixtureNetworks().filter((n) => n !== "zcash"); + networksToTest.forEach((networkName) => { describe(`${networkName}`, function () { - it("actual vSize is within fromPsbt estimated bounds", function () { - const fixture = loadPsbtFixture(networkName, "fullsigned"); + it("actual vSize is within fromPsbt estimated bounds", async function () { + const fixture = await loadPsbtFixture(networkName, "fullsigned"); if (!fixture.extractedTransaction) { this.skip(); return; diff --git a/packages/wasm-utxo/test/fixedScript/finalizeExtract.ts b/packages/wasm-utxo/test/fixedScript/finalizeExtract.ts index 23fd9c06..9f4ca71a 100644 --- a/packages/wasm-utxo/test/fixedScript/finalizeExtract.ts +++ b/packages/wasm-utxo/test/fixedScript/finalizeExtract.ts @@ -1,5 +1,4 @@ import assert from "node:assert"; -import * as utxolib from "@bitgo/utxo-lib"; import { fixedScriptWallet } from "../../js/index.js"; import { loadPsbtFixture, @@ -10,16 +9,14 @@ import { import { getFixtureNetworks } from "./networkSupport.util.js"; describe("finalize and extract transaction", function () { - getFixtureNetworks().forEach((network) => { - const networkName = utxolib.getNetworkName(network); - + getFixtureNetworks().forEach((networkName) => { describe(`network: ${networkName}`, function () { let fullsignedFixture: Fixture; let fullsignedPsbtBuffer: Buffer; let fullsignedBitgoPsbt: fixedScriptWallet.BitGoPsbt; - before(function () { - fullsignedFixture = loadPsbtFixture(networkName, "fullsigned"); + before(async function () { + fullsignedFixture = await loadPsbtFixture(networkName, "fullsigned"); fullsignedPsbtBuffer = getPsbtBuffer(fullsignedFixture); fullsignedBitgoPsbt = fixedScriptWallet.BitGoPsbt.fromBytes( fullsignedPsbtBuffer, @@ -108,11 +105,6 @@ describe("finalize and extract transaction", function () { const txid = extractedTx.getId(); assert.strictEqual(txid.length, 64, "txid should be 64 characters"); assert.match(txid, /^[0-9a-f]{64}$/, "txid should be lowercase hex"); - - // Verify txid matches utxolib calculation - const expectedTxHex = getExtractedTransactionHex(fullsignedFixture); - const utxolibTx = utxolib.bitgo.createTransactionFromHex(expectedTxHex, network); - assert.strictEqual(txid, utxolibTx.getId(), "txid should match utxolib calculation"); }); }); }); diff --git a/packages/wasm-utxo/test/fixedScript/fixtureUtil.ts b/packages/wasm-utxo/test/fixedScript/fixtureUtil.ts index 75013cbc..45c723af 100644 --- a/packages/wasm-utxo/test/fixedScript/fixtureUtil.ts +++ b/packages/wasm-utxo/test/fixedScript/fixtureUtil.ts @@ -9,6 +9,8 @@ import { RootWalletKeys } from "../../js/fixedScriptWallet/RootWalletKeys.js"; import { ECPair } from "../../js/ecpair.js"; import { fixedScriptWallet } from "../../js/index.js"; import type { BitGoPsbt, NetworkName } from "../../js/fixedScriptWallet/index.js"; +import { getFixture } from "../fixtures.js"; +import { generateAllStates } from "./generateFixture.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -18,14 +20,11 @@ export type SignatureState = "unsigned" | "halfsigned" | "fullsigned"; export type Triple = [T, T, T]; export type Bip32Derivation = { - masterFingerprint: string; pubkey: string; path: string; }; -export type TapBip32Derivation = Bip32Derivation & { - leafHashes: string[]; -}; +export type TapBip32Derivation = Bip32Derivation; export type WitnessUtxo = { value: string; @@ -115,19 +114,40 @@ export function getBitGoPsbt(fixture: Fixture, networkName: NetworkName): BitGoP return fixedScriptWallet.BitGoPsbt.fromBytes(getPsbtBuffer(fixture), networkName); } -/** - * Load a PSBT fixture from JSON file - */ -export function loadPsbtFixture(network: string, signatureState: string): Fixture { - const fixturePath = path.join( +function getFixturePath(network: string, signatureState: string): string { + return path.join( __dirname, "..", "fixtures", "fixed-script", `psbt-lite.${network}.${signatureState}.json`, ); - const fixtureContent = fs.readFileSync(fixturePath, "utf-8"); - return JSON.parse(fixtureContent) as Fixture; +} + +const SIGNATURE_STATES: SignatureState[] = ["unsigned", "halfsigned", "fullsigned"]; + +/** + * Load a PSBT fixture from JSON file. + * If the fixture does not exist, generates all three signature states + * (unsigned, halfsigned, fullsigned) and writes them to disk. + */ +export async function loadPsbtFixture( + network: string, + signatureState: SignatureState, +): Promise { + const fixturePath = getFixturePath(network, signatureState); + return getFixture(fixturePath, () => { + const allStates = generateAllStates(network as NetworkName); + // Write sibling states so all three are consistent + for (const state of SIGNATURE_STATES) { + if (state !== signatureState) { + const siblingPath = getFixturePath(network, state); + fs.mkdirSync(path.dirname(siblingPath), { recursive: true }); + fs.writeFileSync(siblingPath, JSON.stringify(allStates[state], null, 2)); + } + } + return allStates[signatureState]; + }) as Promise; } /** diff --git a/packages/wasm-utxo/test/fixedScript/generateFixture.ts b/packages/wasm-utxo/test/fixedScript/generateFixture.ts new file mode 100644 index 00000000..2c154719 --- /dev/null +++ b/packages/wasm-utxo/test/fixedScript/generateFixture.ts @@ -0,0 +1,136 @@ +import { AcidTest } from "../../js/testutils/AcidTest.js"; +import { getKeyTriple, getWalletKeysForSeed } from "../../js/testutils/keys.js"; +import { ECPair } from "../../js/ecpair.js"; +import { BitGoPsbt, type InputScriptType, type NetworkName } from "../../js/fixedScriptWallet/index.js"; +import { RootWalletKeys } from "../../js/fixedScriptWallet/RootWalletKeys.js"; +import type { + Fixture, + PsbtInput, + PsbtOutput, + Output, + SignatureState, + Bip32Derivation, +} from "./fixtureUtil.js"; + +export function createOtherWalletKeys(): RootWalletKeys { + return getWalletKeysForSeed("too many secrets"); +} + +function toFixtureType(scriptType: InputScriptType): string { + switch (scriptType) { + case "p2sh": + case "p2shP2wsh": + case "p2wsh": + case "p2shP2pk": + return scriptType; + case "p2trLegacy": + return "p2tr"; + case "p2trMusig2ScriptPath": + return "p2trMusig2"; + case "p2trMusig2KeyPath": + return "taprootKeyPathSpend"; + default: + throw new Error(`Unknown script type: ${String(scriptType)}`); + } +} + +function reverseHex(hex: string): string { + return Buffer.from(hex, "hex").reverse().toString("hex"); +} + +function toBip32Derivation(d: { pubkey: Uint8Array; path: string }): Bip32Derivation { + return { + pubkey: Buffer.from(d.pubkey).toString("hex"), + path: d.path, + }; +} + +function snapshotFixture(acid: AcidTest, psbt: BitGoPsbt): Fixture { + const xprivs = getKeyTriple("default"); + const rpBip32 = acid.getReplayProtectionKey(); + const rpKey = ECPair.fromPublicKey(rpBip32.publicKey); + + const parsed = psbt.parseTransactionWithWalletKeys(acid.rootWalletKeys, { + replayProtection: { publicKeys: [rpKey] }, + }); + + const psbtInputData = psbt.getInputs(); + const psbtOutputData = psbt.getOutputs(); + + const inputs = parsed.inputs.map((input) => ({ + hash: reverseHex(input.previousOutput.txid), + index: input.previousOutput.vout, + sequence: input.sequence, + })); + + const psbtInputs: PsbtInput[] = parsed.inputs.map((input, i) => { + const data = psbtInputData[i]; + const result: PsbtInput = { + type: toFixtureType(input.scriptType), + sighashType: input.scriptType.startsWith("p2tr") ? 0 : 1, + }; + if (data.witnessUtxo) { + result.witnessUtxo = { + value: data.witnessUtxo.value.toString(), + script: Buffer.from(data.witnessUtxo.script).toString("hex"), + }; + } + if (data.bip32Derivation.length > 0) { + result.bip32Derivation = data.bip32Derivation.map(toBip32Derivation); + } + if (data.tapBip32Derivation.length > 0) { + result.tapBip32Derivation = data.tapBip32Derivation.map(toBip32Derivation); + } + return result; + }); + + const outputs: Output[] = psbtOutputData.map((out) => ({ + script: Buffer.from(out.script).toString("hex"), + value: out.value.toString(), + })); + + const psbtOutputs: PsbtOutput[] = psbtOutputData.map((out) => { + const result: PsbtOutput = {}; + if (out.bip32Derivation.length > 0) { + result.bip32Derivation = out.bip32Derivation.map(toBip32Derivation); + } + if (out.tapBip32Derivation.length > 0) { + result.tapBip32Derivation = out.tapBip32Derivation.map(toBip32Derivation); + } + return result; + }); + + return { + walletKeys: xprivs.map((k) => k.toBase58()) as [string, string, string], + psbtBase64: Buffer.from(psbt.serialize()).toString("base64"), + psbtBase64Finalized: null, + inputs, + psbtInputs, + psbtInputsFinalized: null, + outputs, + psbtOutputs, + extractedTransaction: null, + }; +} + +export function generateAllStates(network: NetworkName): Record { + const unsignedAcid = AcidTest.withConfig(network, "unsigned", "psbt-lite"); + const halfsignedAcid = AcidTest.withConfig(network, "halfsigned", "psbt-lite"); + const fullsignedAcid = AcidTest.withConfig(network, "fullsigned", "psbt-lite"); + + const unsignedPsbt = unsignedAcid.createPsbt(); + const halfsignedPsbt = halfsignedAcid.createPsbt(); + const fullsignedPsbt = fullsignedAcid.createPsbt(); + + const unsigned = snapshotFixture(unsignedAcid, unsignedPsbt); + const halfsigned = snapshotFixture(halfsignedAcid, halfsignedPsbt); + const fullsigned = snapshotFixture(fullsignedAcid, fullsignedPsbt); + + // Finalize the fullsigned PSBT and capture finalized data + fullsignedPsbt.finalizeAllInputs(); + fullsigned.psbtBase64Finalized = Buffer.from(fullsignedPsbt.serialize()).toString("base64"); + const tx = fullsignedPsbt.extractTransaction(); + fullsigned.extractedTransaction = Buffer.from(tx.toBytes()).toString("hex"); + + return { unsigned, halfsigned, fullsigned }; +} diff --git a/packages/wasm-utxo/test/fixedScript/musig2Nonces.ts b/packages/wasm-utxo/test/fixedScript/musig2Nonces.ts index 156bae19..7119c9e1 100644 --- a/packages/wasm-utxo/test/fixedScript/musig2Nonces.ts +++ b/packages/wasm-utxo/test/fixedScript/musig2Nonces.ts @@ -10,8 +10,8 @@ describe("MuSig2 nonce management", function () { let backupKey: BIP32; let bitgoKey: BIP32; - before(function () { - fixture = loadPsbtFixture(networkName, "unsigned"); + before(async function () { + fixture = await loadPsbtFixture(networkName, "unsigned"); userKey = BIP32.fromBase58(fixture.walletKeys[0]); backupKey = BIP32.fromBase58(fixture.walletKeys[1]); bitgoKey = BIP32.fromBase58(fixture.walletKeys[2]); diff --git a/packages/wasm-utxo/test/fixedScript/networkSupport.util.ts b/packages/wasm-utxo/test/fixedScript/networkSupport.util.ts index 030e63c0..123a0017 100644 --- a/packages/wasm-utxo/test/fixedScript/networkSupport.util.ts +++ b/packages/wasm-utxo/test/fixedScript/networkSupport.util.ts @@ -1,15 +1,17 @@ -import * as utxolib from "@bitgo/utxo-lib"; +import type { NetworkName } from "../../js/fixedScriptWallet/index.js"; -/** - * Get networks that have psbt fixtures - */ -export function getFixtureNetworks(): utxolib.Network[] { - return utxolib.getNetworkList().filter((network) => { - return ( - // we only have fixtures for mainnet networks - utxolib.isMainnet(network) && - // we don't have fixtures for bitcoinsv since it is not really supported any longer - network !== utxolib.networks.bitcoinsv - ); - }); +/** Mainnet networks that have fixed-script wallet PSBT fixtures. */ +const FIXTURE_NETWORKS: NetworkName[] = [ + "bitcoin", + "bitcoincash", + "bitcoingold", + "dash", + "dogecoin", + "ecash", + "litecoin", + "zcash", +]; + +export function getFixtureNetworks(): NetworkName[] { + return FIXTURE_NETWORKS; } diff --git a/packages/wasm-utxo/test/fixedScript/parseTransactionWithWalletKeys.ts b/packages/wasm-utxo/test/fixedScript/parseTransactionWithWalletKeys.ts index 306dce73..12d2b652 100644 --- a/packages/wasm-utxo/test/fixedScript/parseTransactionWithWalletKeys.ts +++ b/packages/wasm-utxo/test/fixedScript/parseTransactionWithWalletKeys.ts @@ -1,5 +1,4 @@ import assert from "node:assert"; -import * as utxolib from "@bitgo/utxo-lib"; import { fixedScriptWallet } from "../../js/index.js"; import { BitGoPsbt, InputScriptType } from "../../js/fixedScriptWallet/index.js"; import type { RootWalletKeys } from "../../js/fixedScriptWallet/RootWalletKeys.js"; @@ -12,6 +11,7 @@ import { type Fixture, } from "./fixtureUtil.js"; import { getFixtureNetworks } from "./networkSupport.util.js"; +import { createOtherWalletKeys } from "./generateFixture.js"; function getExpectedInputScriptType(fixtureScriptType: string): InputScriptType { // Map fixture types to InputScriptType values @@ -33,15 +33,8 @@ function getExpectedInputScriptType(fixtureScriptType: string): InputScriptType } } -function getOtherWalletKeys(): utxolib.bitgo.RootWalletKeys { - const otherWalletKeys = utxolib.testutil.getKeyTriple("too many secrets"); - return new utxolib.bitgo.RootWalletKeys(otherWalletKeys); -} - describe("parseTransactionWithWalletKeys", function () { - getFixtureNetworks().forEach((network) => { - const networkName = utxolib.getNetworkName(network); - + getFixtureNetworks().forEach((networkName) => { describe(`network: ${networkName}`, function () { let fullsignedPsbtBytes: Buffer; let bitgoPsbt: BitGoPsbt; @@ -49,21 +42,18 @@ describe("parseTransactionWithWalletKeys", function () { let replayProtectionKey: ECPair; let fixture: Fixture; - before(function () { - fixture = loadPsbtFixture(networkName, "fullsigned"); + before(async function () { + fixture = await loadPsbtFixture(networkName, "fullsigned"); fullsignedPsbtBytes = getPsbtBuffer(fixture); bitgoPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(fullsignedPsbtBytes, networkName); rootWalletKeys = loadWalletKeysFromFixture(fixture); replayProtectionKey = loadReplayProtectionKeyFromFixture(fixture); }); - it("should have matching unsigned transaction ID", function () { + it("should have valid unsigned transaction ID format", function () { const unsignedTxid = bitgoPsbt.unsignedTxid(); - const expectedUnsignedTxid = utxolib.bitgo - .createPsbtFromBuffer(fullsignedPsbtBytes, network) - .getUnsignedTx() - .getId(); - assert.strictEqual(unsignedTxid, expectedUnsignedTxid); + assert.strictEqual(unsignedTxid.length, 64, "unsignedTxid should be 64 chars"); + assert.match(unsignedTxid, /^[0-9a-f]{64}$/, "unsignedTxid should be lowercase hex"); }); it("should parse transaction and identify internal/external outputs", function () { @@ -141,9 +131,11 @@ describe("parseTransactionWithWalletKeys", function () { const nonAddressOutputs = externalOutputs.filter((o) => o.address === null); assert.strictEqual(nonAddressOutputs.length, 1); const [opReturnOutput] = nonAddressOutputs; - const expectedOpReturn = utxolib.payments.embed({ - data: [Buffer.from("setec astronomy")], - }).output; + const opReturnData = Buffer.from("setec astronomy"); + const expectedOpReturn = Buffer.concat([ + Buffer.from([0x6a, opReturnData.length]), + opReturnData, + ]); assert.strictEqual( Buffer.from(opReturnOutput.script).toString("hex"), expectedOpReturn.toString("hex"), @@ -171,7 +163,8 @@ describe("parseTransactionWithWalletKeys", function () { }); // Verify spend amount (should be > 0 since there are external outputs) - assert.strictEqual(parsed.spendAmount, 900n * 2n); + // AcidTest uses other wallet (800) + null wallet (700) = 1500 + assert.strictEqual(parsed.spendAmount, 800n + 700n); // Verify miner fee calculation const totalInputValue = parsed.inputs.reduce((sum, i) => sum + i.value, 0n); @@ -244,20 +237,18 @@ describe("parseTransactionWithWalletKeys", function () { it("should fail to parse with other wallet keys", function () { assert.throws( () => { - bitgoPsbt.parseTransactionWithWalletKeys(getOtherWalletKeys(), { + bitgoPsbt.parseTransactionWithWalletKeys(createOtherWalletKeys(), { replayProtection: { publicKeys: [replayProtectionKey] }, }); }, (error: Error) => { - return error.message.includes( - "Failed to parse transaction: Input 0: wallet validation failed", - ); + return error.message.includes("wallet validation failed"); }, ); }); it("should recognize output for other wallet keys", function () { - const parsedOutputs = bitgoPsbt.parseOutputsWithWalletKeys(getOtherWalletKeys()); + const parsedOutputs = bitgoPsbt.parseOutputsWithWalletKeys(createOtherWalletKeys()); // Should return an array of parsed outputs assert.ok(Array.isArray(parsedOutputs), "Should return an array"); diff --git a/packages/wasm-utxo/test/fixedScript/psbtReconstruction.ts b/packages/wasm-utxo/test/fixedScript/psbtReconstruction.ts index a5b29f2f..88590478 100644 --- a/packages/wasm-utxo/test/fixedScript/psbtReconstruction.ts +++ b/packages/wasm-utxo/test/fixedScript/psbtReconstruction.ts @@ -1,5 +1,4 @@ import assert from "node:assert"; -import * as utxolib from "@bitgo/utxo-lib"; import { fixedScriptWallet } from "../../js/index.js"; import { BitGoPsbt, @@ -16,6 +15,7 @@ import { type Fixture, } from "./fixtureUtil.js"; import { getFixtureNetworks } from "./networkSupport.util.js"; +import { createOtherWalletKeys } from "./generateFixture.js"; // Zcash Sapling consensus branch ID for test fixtures const ZCASH_SAPLING_BRANCH_ID = 0x76b809bb; @@ -36,23 +36,6 @@ function getSignPathFromScriptType(scriptType: InputScriptType): SignPath | unde } } -/** - * Get "other wallet keys" for testing outputs from different wallet - * Uses the same seed as utxo-lib tests: "too many secrets" - */ -function getOtherWalletKeys(): RootWalletKeys { - const otherWalletKeys = utxolib.testutil.getKeyTriple("too many secrets"); - const neuteredKeys = otherWalletKeys.map((key) => key.neutered()) as [ - utxolib.BIP32Interface, - utxolib.BIP32Interface, - utxolib.BIP32Interface, - ]; - return fixedScriptWallet.RootWalletKeys.from({ - triple: neuteredKeys, - derivationPrefixes: ["0/0", "0/0", "0/0"], - }); -} - /** * Reverse a hex string by bytes (for txid conversion) * Bitcoin txids in fixtures are in internal byte order (reversed) @@ -62,20 +45,18 @@ function reverseHex(hex: string): string { } describe("PSBT reconstruction", function () { - getFixtureNetworks().forEach((network) => { - const networkName = utxolib.getNetworkName(network); - + getFixtureNetworks().forEach((networkName) => { describe(`network: ${networkName}`, function () { let fixture: Fixture; let originalPsbt: BitGoPsbt; let rootWalletKeys: RootWalletKeys; let otherWalletKeys: RootWalletKeys; - before(function () { - fixture = loadPsbtFixture(networkName, "unsigned"); + before(async function () { + fixture = await loadPsbtFixture(networkName, "unsigned"); originalPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(getPsbtBuffer(fixture), networkName); rootWalletKeys = loadWalletKeysFromFixture(fixture); - otherWalletKeys = getOtherWalletKeys(); + otherWalletKeys = createOtherWalletKeys(); }); it("should reconstruct PSBT from parsed data with matching unsigned txid", function () { @@ -193,7 +174,7 @@ describe("PSBT reconstruction", function () { assert.strictEqual(typeof originalPsbt.version, "number", "version should be a number"); assert.strictEqual(typeof originalPsbt.lockTime, "number", "lockTime should be a number"); // Version depends on network: Zcash uses version 4 (Sapling) or 5 (NU5), others use 1 or 2 - if (network === utxolib.networks.zcash) { + if (networkName === "zcash") { assert.ok( originalPsbt.version === 4 || originalPsbt.version === 5, `Zcash version should be 4 or 5, got ${originalPsbt.version}`, @@ -346,9 +327,9 @@ describe("PSBT reconstruction", function () { ); }); - it("should extract transaction with valid getId() after finalization", function () { + it("should extract transaction with valid getId() after finalization", async function () { // Load fullsigned fixture for this network - const fullsignedFixture = loadPsbtFixture(networkName, "fullsigned"); + const fullsignedFixture = await loadPsbtFixture(networkName, "fullsigned"); const psbt = fixedScriptWallet.BitGoPsbt.fromBytes( getPsbtBuffer(fullsignedFixture), networkName, diff --git a/packages/wasm-utxo/test/fixedScript/signAndVerifySignature.ts b/packages/wasm-utxo/test/fixedScript/signAndVerifySignature.ts index 1820c8eb..a605bdc0 100644 --- a/packages/wasm-utxo/test/fixedScript/signAndVerifySignature.ts +++ b/packages/wasm-utxo/test/fixedScript/signAndVerifySignature.ts @@ -1,5 +1,4 @@ import assert from "node:assert"; -import * as utxolib from "@bitgo/utxo-lib"; import { BIP32, ECPair } from "../../js/index.js"; import { BitGoPsbt, @@ -243,9 +242,7 @@ function runTestsForFixture( } describe("verifySignature", function () { - getFixtureNetworks().forEach((network) => { - const networkName = utxolib.getNetworkName(network); - + getFixtureNetworks().forEach((networkName) => { describe(`network: ${networkName}`, function () { let rootWalletKeys: RootWalletKeys; let replayProtectionKey: ECPair; @@ -254,10 +251,10 @@ describe("verifySignature", function () { let halfsignedFixture: Fixture; let fullsignedFixture: Fixture; - before(function () { - unsignedFixture = loadPsbtFixture(networkName, "unsigned"); - halfsignedFixture = loadPsbtFixture(networkName, "halfsigned"); - fullsignedFixture = loadPsbtFixture(networkName, "fullsigned"); + before(async function () { + unsignedFixture = await loadPsbtFixture(networkName, "unsigned"); + halfsignedFixture = await loadPsbtFixture(networkName, "halfsigned"); + fullsignedFixture = await loadPsbtFixture(networkName, "fullsigned"); rootWalletKeys = loadWalletKeysFromFixture(fullsignedFixture); replayProtectionKey = loadReplayProtectionKeyFromFixture(fullsignedFixture); xprivs = loadXprivsFromFixture(fullsignedFixture); @@ -347,13 +344,15 @@ describe("verifySignature", function () { it("should verify signature with raw public key (Uint8Array)", function () { const psbt = getBitGoPsbt(fullsignedFixture, networkName); + // Find first wallet input (non-p2shP2pk) for xpub verification + const walletInputIndex = fullsignedFixture.psbtInputs.findIndex( + (input) => input.type !== "p2shP2pk", + ); + assert.ok(walletInputIndex >= 0, "Should have a wallet input"); + // Verify that xpub-based verification works const userKey = rootWalletKeys.userKey(); - const hasXpubSig = psbt.verifySignature(0, userKey); - - // This test specifically checks that raw public key verification works - // We test the underlying WASM API by ensuring both xpub and raw pubkey - // calls reach the correct methods + const hasXpubSig = psbt.verifySignature(walletInputIndex, userKey); // Use a random public key that's not in the PSBT to test the API works const randomSeed = Buffer.alloc(32, 0xcc); @@ -361,7 +360,7 @@ describe("verifySignature", function () { const randomPubkey = randomKey.publicKey; // This should return false (no signature for this key) - const result = psbt.verifySignature(0, randomPubkey); + const result = psbt.verifySignature(walletInputIndex, randomPubkey); assert.strictEqual(result, false, "Should return false for public key not in PSBT"); // Verify the xpub check still works (regression test) diff --git a/packages/wasm-utxo/test/fixtures.ts b/packages/wasm-utxo/test/fixtures.ts index d2cbf7d9..85c7fbe0 100644 --- a/packages/wasm-utxo/test/fixtures.ts +++ b/packages/wasm-utxo/test/fixtures.ts @@ -1,7 +1,13 @@ import * as fs from "fs/promises"; -export async function getFixture(path: string, defaultValue: unknown): Promise { + +type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }; + +export async function getFixture( + path: string, + defaultValue: (() => JsonValue) | JsonValue, +): Promise { try { - return JSON.parse(await fs.readFile(path, "utf8")) as unknown; + return JSON.parse(await fs.readFile(path, "utf8")) as JsonValue; } catch (e) { if ( typeof e === "object" && @@ -9,7 +15,8 @@ export async function getFixture(path: string, defaultValue: unknown): Promise