import { createHash } from "node:crypto"; import { FIXTURE_DESCRIPTOR_SCRIPT_TYPES, FIXTURE_PROFILES, type FixtureDescriptorId, type FixtureScriptType, } from "../core/fixture-profiles.js"; import type { PsbtFixture } from "../core/fixtures.js"; import { parsePsbtDocument } from "../psbt/document.js"; import type { UserFixtureOutputSpec, UserFixtureSpec, UserParserFixtureSpec } from "./manifest.js"; const BUILT_IN_FIXTURE_IDS = new Set([ "happy-path", "bdk-finalize-regression", ...FIXTURE_PROFILES.map(({ id }) => id), ]); export interface CompiledUserFixturePlan { readonly id: string; readonly inputDescriptorIds: readonly FixtureDescriptorId[]; readonly outputDescriptorIds: readonly FixtureDescriptorId[]; readonly scriptTypes: readonly FixtureScriptType[]; readonly sequences: readonly number[]; readonly locktime: number; readonly transactionVersion: 2; readonly feeSats: number; readonly outputAmounts: readonly (number | null)[]; readonly specSha256: `sha256:${string}`; } function canonicalOutput(output: UserFixtureOutputSpec): Record { return "amountSats" in output ? { descriptor: output.descriptor, amountSats: output.amountSats } : { descriptor: output.descriptor, remainder: true }; } function specificationHash(spec: UserFixtureSpec): `sha256:${string}` { const canonical = JSON.stringify({ id: spec.id, inputs: spec.inputs.map((input) => ({ descriptor: input.descriptor, sequence: input.sequence, })), outputs: spec.outputs.map(canonicalOutput), feeSats: spec.feeSats, locktime: spec.locktime, transactionVersion: spec.transactionVersion, }); return `sha256:${createHash("sha256").update(canonical).digest("hex")}`; } function compileUserFixturePlan(spec: UserFixtureSpec): CompiledUserFixturePlan { if (BUILT_IN_FIXTURE_IDS.has(spec.id)) { throw new TypeError(`Custom fixture ${spec.id} collides with a built-in fixture id`); } const remainderCount = spec.outputs.filter((output) => "remainder" in output).length; if (remainderCount !== 1) { throw new TypeError(`Custom fixture ${spec.id} must contain exactly one remainder output`); } const inputDescriptorIds = spec.inputs.map(({ descriptor }) => descriptor); return { id: spec.id, inputDescriptorIds, outputDescriptorIds: spec.outputs.map(({ descriptor }) => descriptor), scriptTypes: inputDescriptorIds.map( (descriptor) => FIXTURE_DESCRIPTOR_SCRIPT_TYPES[descriptor], ), sequences: spec.inputs.map(({ sequence }) => sequence), locktime: spec.locktime, transactionVersion: spec.transactionVersion, feeSats: spec.feeSats, outputAmounts: spec.outputs.map((output) => "amountSats" in output ? output.amountSats : null, ), specSha256: specificationHash(spec), }; } export function compileUserFixturePlans( specs: readonly UserFixtureSpec[], ): readonly CompiledUserFixturePlan[] { return [...specs] .sort((left, right) => left.id.localeCompare(right.id)) .map(compileUserFixturePlan); } export function compileUserParserFixtures( specs: readonly UserParserFixtureSpec[], ): ReadonlyMap { return new Map( specs.map((spec) => { if (BUILT_IN_FIXTURE_IDS.has(spec.id)) { throw new TypeError(`Parser fixture ${spec.id} collides with a built-in fixture id`); } const document = parsePsbtDocument(spec.psbt); return [ spec.id, { id: spec.id, initialPsbt: spec.psbt, outpoints: [], inputCount: document.inputCount, outputCount: document.outputCount, feeSats: 0, scriptTypes: [], inputDescriptors: [], outputDescriptor: "", psbtVersion: document.psbtVersion, transactionId: "0".repeat(64), psbtSha256: document.sha256, unsignedTxSha256: `sha256:${document.sha256}`, } as PsbtFixture, ] as const; }), ); }