import { Tag } from "cbor-rpc/lib/cbor-x"; import { cborBackend } from "cbor-rpc"; import { blake } from "../lib/blake"; import { Buffer28 } from "../types"; import { RawNativeScript as CddlRawNativeScript, RawSlot } from "./cddlTypes"; import { Buffer } from "buffer"; type RawPlutusV1Script = Buffer; type RawPlutusV2Script = Buffer; type RawPlutusV3Script = Buffer; type RawScriptPubKey = [0, Buffer28]; type RawScriptAll = [1, CddlRawNativeScript[]]; type RawScriptAny = [2, CddlRawNativeScript[]]; type RawScriptNOfK = [3, number, CddlRawNativeScript[]]; type RawInvalidBefore = [4, RawSlot]; type RawInvalidHereafter = [5, RawSlot]; export type RawScript = { tag: 24; value: Buffer }; export type NativeScriptType = "sig" | "all" | "any" | "atLeast" | "before" | "after"; export type ScriptType = NativeScriptType | "PlutusScriptV1" | "PlutusScriptV2" | "PlutusScriptV3"; export type PlutusScriptType = Exclude; export type NativeScriptJSON = | { type: "sig"; keyHash: string } | { type: "all"; scripts: NativeScriptJSON[] } | { type: "any"; scripts: NativeScriptJSON[] } | { type: "atLeast"; required: number; scripts: NativeScriptJSON[] } | { type: "before"; slot: number | string } | { type: "after"; slot: number | string }; export type PlutusScriptJSON = { cborHex: string; description: string; type: T; }; export type ScriptJSON = PlutusScriptJSON | NativeScriptJSON; function tryDecode(bytes: Buffer): any | undefined { try { return cborBackend.decode(bytes); } catch { return undefined; } } function isTag24(obj: any): obj is Tag { return !!obj && typeof obj === "object" && obj.tag === 24 && Buffer.isBuffer(obj.value); } function isNativeScriptCborObject(obj: any): obj is CddlRawNativeScript { if (!Array.isArray(obj) || obj.length < 2 || typeof obj[0] !== "number") { return false; } switch (obj[0]) { case 0: return obj.length === 2 && Buffer.isBuffer(obj[1]) && obj[1].length === 28; case 1: case 2: return obj.length === 2 && Array.isArray(obj[1]); case 3: return obj.length === 3 && typeof obj[1] === "number" && Array.isArray(obj[2]); case 4: case 5: return obj.length === 2 && (typeof obj[1] === "bigint" || typeof obj[1] === "number"); default: return false; } } function isVersionedScriptTuple(obj: any): obj is [number, any] { if ( !Array.isArray(obj) || obj.length !== 2 || typeof obj[0] !== "number" || !Number.isInteger(obj[0]) || obj[0] < 0 || obj[0] > 3 ) { return false; } if (obj[0] === 0) { return isNativeScriptCborObject(obj[1]); } return Buffer.isBuffer(obj[1]); } function isNativeScriptType(type: ScriptType): type is NativeScriptType { return ( type === "sig" || type === "all" || type === "any" || type === "atLeast" || type === "before" || type === "after" ); } function slotFromJson(value: number | string): RawSlot { if (typeof value === "number") { if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) { throw new Error("Invalid slot value"); } return value; } if (!/^\d+$/.test(value)) { throw new Error("Invalid slot value"); } return BigInt(value); } function slotToJson(value: RawSlot): number | string { if (typeof value === "bigint") { return value.toString(); } return value; } function scriptTypeToVersionTag(scriptType: ScriptType): number { switch (scriptType) { case "sig": case "all": case "any": case "atLeast": case "before": case "after": return 0; case "PlutusScriptV1": return 1; case "PlutusScriptV2": return 2; case "PlutusScriptV3": return 3; default: throw new Error("Unknown Script type"); } } function computeScriptHash(discriminator: number, innerBytes: Buffer): Buffer { const prefixed = Buffer.concat([Buffer.from([discriminator]), innerBytes]); return blake.hash28(prefixed); } function computeNativeScriptHash(innerBytes: Buffer): Buffer { return computeScriptHash(0, innerBytes); } export class Script { type: ScriptType; constructor(type: ScriptType) { this.type = type; } hash(): Buffer { throw new Error("Method 'hash' must be implemented in derived classes"); } protected currentBytes(): Buffer { throw new Error("Method 'currentBytes' must be implemented in derived classes"); } static fromRawScript(obj: RawScript): Script { const decodedReferenceScript = cborBackend.decode(obj.value); if (!isVersionedScriptTuple(decodedReferenceScript)) { throw new Error("Invalid reference script format"); } return scriptFromVersionedCborTuple(decodedReferenceScript); } static fromCborObject(obj: any): Script { if (isTag24(obj)) { return Script.fromRawScript({ tag: 24, value: obj.value }); } if (isVersionedScriptTuple(obj)) { return scriptFromVersionedCborTuple(obj); } if (isNativeScriptCborObject(obj)) { return NativeScript.fromCborObject(obj); } throw new Error("Invalid Script CBOR object"); } static fromHex(hex: string, typeHint?: ScriptType): Script { return Script.fromBytes(Buffer.from(hex, "hex"), typeHint); } static fromCborBytes(bytes: Buffer, typeHint?: ScriptType): Script { return Script.fromBytes(bytes, typeHint); } static fromBytes(bytes: Buffer, typeHint?: ScriptType): Script { const decoded = tryDecode(bytes); if (isTag24(decoded)) { return Script.fromRawScript({ tag: 24, value: decoded.value }); } if (isVersionedScriptTuple(decoded)) { return scriptFromVersionedCborTuple(decoded); } if (isNativeScriptCborObject(decoded)) { return NativeScript.fromCborObject(decoded); } if (typeHint) { if (isNativeScriptType(typeHint)) { const nativeObj = Buffer.isBuffer(decoded) ? tryDecode(decoded) : decoded; if (!isNativeScriptCborObject(nativeObj)) { throw new Error("Failed to decode NativeScript bytes"); } return NativeScript.fromCborObject(nativeObj); } return PlutusScript.fromBytes(bytes, typeHint); } throw new Error( "Unable to detect script format. Provide typeHint for raw Plutus bytes (PlutusScriptV1|PlutusScriptV2|PlutusScriptV3)." ); } static fromJSON(obj: ScriptJSON): Script { return Script.fromJson(obj); } static fromJson(obj: ScriptJSON): Script { if (!obj || typeof obj !== "object") { throw new Error("Invalid JSON object for Script"); } if (!("type" in obj)) { throw new Error("Missing 'type' field in Script JSON"); } if (!("cborHex" in obj)) { return NativeScript.fromJson(obj as NativeScriptJSON); } return Script.fromHex((obj as PlutusScriptJSON).cborHex, (obj as PlutusScriptJSON).type); } toJSON(): ScriptJSON { throw new Error("Method 'toJSON' must be implemented in derived classes"); } toCborBytes(): Buffer { return this.currentBytes(); } toCborObject(): any { const bytes = this.currentBytes(); const versionTag = scriptTypeToVersionTag(this.type); const payload = cborBackend.decode(bytes); return new Tag(cborBackend.encode([versionTag, payload]), 24); } toBytes(): Buffer { return cborBackend.encode(this.toCborObject()); } toHex(): string { return this.toBytes().toString("hex"); } } function scriptFromVersionedCborTuple(tuple: [number, any]): Script { switch (tuple[0]) { case 0: return NativeScript.fromCborObject(tuple[1] as CddlRawNativeScript); case 1: if (!Buffer.isBuffer(tuple[1])) throw new Error("Invalid CBOR payload for PlutusScriptV1"); return PlutusScript.fromCborObject([1, tuple[1]]); case 2: if (!Buffer.isBuffer(tuple[1])) throw new Error("Invalid CBOR payload for PlutusScriptV2"); return PlutusScript.fromCborObject([2, tuple[1]]); case 3: if (!Buffer.isBuffer(tuple[1])) throw new Error("Invalid CBOR payload for PlutusScriptV3"); return PlutusScript.fromCborObject([3, tuple[1]]); default: throw new Error("Unknown Script type"); } } export class PlutusScript extends Script { declare type: PlutusScriptType; bytes: Buffer; constructor(type: PlutusScriptType, cbor: Buffer) { super(type); this.type = type; this.bytes = cborBackend.encode(cbor); } static override fromCborObject( obj: [1, RawPlutusV1Script] | [2, RawPlutusV2Script] | [3, RawPlutusV3Script] ): PlutusScript { switch (obj[0]) { case 1: return new PlutusScript("PlutusScriptV1", obj[1]); case 2: return new PlutusScript("PlutusScriptV2", obj[1]); case 3: return new PlutusScript("PlutusScriptV3", obj[1]); default: throw new Error("Invalid CBOR type for PlutusScript"); } } static override fromBytes(bytes: Buffer, type: PlutusScriptType): PlutusScript { const decoded = tryDecode(bytes); const rawPlutusBytes = Buffer.isBuffer(decoded) ? decoded : bytes; return new PlutusScript(type, rawPlutusBytes); } static override fromCborBytes(bytes: Buffer, type: PlutusScriptType): PlutusScript { return PlutusScript.fromBytes(bytes, type); } static override fromHex(hex: string, type: PlutusScriptType): PlutusScript { return PlutusScript.fromBytes(Buffer.from(hex, "hex"), type); } static override fromJSON(obj: PlutusScriptJSON): PlutusScript { return PlutusScript.fromJson(obj); } static override fromJson(obj: PlutusScriptJSON): PlutusScript { if (!("cborHex" in obj) || !obj.cborHex) { throw new Error("Missing 'cborHex' field for Plutus script"); } if (!("type" in obj) || !obj.type) { throw new Error("Missing 'type' field for Plutus script"); } return PlutusScript.fromHex(obj.cborHex, obj.type); } override toCborObject(): [number, RawPlutusV1Script | RawPlutusV2Script | RawPlutusV3Script] { const versionTag = this.type === "PlutusScriptV1" ? 1 : this.type === "PlutusScriptV2" ? 2 : 3; return [versionTag, cborBackend.decode(this.currentBytes())]; } protected override currentBytes(): Buffer { return this.bytes; } override toJSON(): PlutusScriptJSON { return { cborHex: this.currentBytes().toString("hex"), type: this.type, description: "", }; } override toBytes(): Buffer { return this.currentBytes(); } override toHex(): string { return this.toBytes().toString("hex"); } override hash(): Buffer { const discriminator = this.type === "PlutusScriptV1" ? 1 : this.type === "PlutusScriptV2" ? 2 : 3; return computeScriptHash(discriminator, cborBackend.decode(this.bytes)); } } export class NativeScript extends Script { nativeScriptType: NativeScriptType; constructor(nativeScriptType: NativeScriptType) { super(nativeScriptType); this.nativeScriptType = nativeScriptType; } get bytes(): Buffer { return cborBackend.encode(this.toCborObject()); } protected override currentBytes(): Buffer { return this.bytes; } static override fromHex(hex: string): NativeScript { return NativeScript.fromBytes(Buffer.from(hex, "hex")); } static override fromBytes(bytes: Buffer): NativeScript { const decoded = tryDecode(bytes); if (isNativeScriptCborObject(decoded)) { return NativeScript.fromCborObject(decoded); } if (Buffer.isBuffer(decoded)) { const nested = tryDecode(decoded); if (isNativeScriptCborObject(nested)) { return NativeScript.fromCborObject(nested); } } if (Array.isArray(decoded) && decoded.length === 2 && decoded[0] === 0 && isNativeScriptCborObject(decoded[1])) { return NativeScript.fromCborObject(decoded[1]); } throw new Error("Unable to detect NativeScript format"); } static override fromCborBytes(bytes: Buffer): NativeScript { return NativeScript.fromBytes(bytes); } static override fromJSON(obj: NativeScriptJSON): NativeScript { return NativeScript.fromJson(obj); } static override fromJson(obj: NativeScriptJSON): NativeScript { switch (obj.type) { case "sig": if (!("keyHash" in obj) || !obj.keyHash) { throw new Error("Missing 'keyHash' field for 'sig' script"); } return new ScriptPubKey(Buffer.from(obj.keyHash, "hex") as Buffer28); case "all": if (!("scripts" in obj) || !Array.isArray(obj.scripts)) { throw new Error("Missing or invalid 'scripts' field for 'all' script"); } return new ScriptAll(obj.scripts.map((script) => NativeScript.fromJson(script))); case "any": if (!("scripts" in obj) || !Array.isArray(obj.scripts)) { throw new Error("Missing or invalid 'scripts' field for 'any' script"); } return new ScriptAny(obj.scripts.map((script) => NativeScript.fromJson(script))); case "atLeast": if (!("required" in obj) || typeof obj.required !== "number") { throw new Error("Missing or invalid 'required' field for 'atLeast' script"); } if (!("scripts" in obj) || !Array.isArray(obj.scripts)) { throw new Error("Missing or invalid 'scripts' field for 'atLeast' script"); } return new ScriptNofK( obj.required, obj.scripts.map((script) => NativeScript.fromJson(script)) ); case "before": if (!("slot" in obj)) { throw new Error("Missing 'slot' field for 'before' script"); } return new ScriptInvalidHereafter(slotFromJson(obj.slot)); case "after": if (!("slot" in obj)) { throw new Error("Missing 'slot' field for 'after' script"); } return new ScriptInvalidBefore(slotFromJson(obj.slot)); default: throw new Error(`Unknown NativeScript JSON type: ${(obj as any).type}`); } } override toCborObject(): CddlRawNativeScript { throw new Error("Method 'toCborObject' must be implemented in derived classes"); } override toJSON(): NativeScriptJSON { throw new Error("Method 'toJSON' must be implemented in derived classes"); } override toBytes(): Buffer { return this.currentBytes(); } override toHex(): string { return this.toBytes().toString("hex"); } override hash(): Buffer { return computeNativeScriptHash(this.bytes); } static override fromCborObject(obj: CddlRawNativeScript): NativeScript { const raw = obj as any; const value = raw[1]; switch (obj[0]) { case 0: return new ScriptPubKey(value); case 1: return new ScriptAll(value.map(NativeScript.fromCborObject)); case 2: return new ScriptAny(value.map(NativeScript.fromCborObject)); case 3: return new ScriptNofK(value, raw[2].map(NativeScript.fromCborObject)); case 4: return new ScriptInvalidBefore(value); case 5: return new ScriptInvalidHereafter(value); default: throw new Error("Unknown NativeScript type"); } } } export class ScriptPubKey extends NativeScript { addrKeyHash: Buffer28; constructor(addrKeyHash: Buffer28) { super("sig"); this.addrKeyHash = addrKeyHash; } override toCborObject(): RawScriptPubKey { return [0, this.addrKeyHash]; } override toJSON(): NativeScriptJSON { return { type: "sig", keyHash: this.addrKeyHash.toString("hex"), }; } } export class ScriptAll extends NativeScript { nativeScripts: NativeScript[]; constructor(nativeScripts: NativeScript[]) { super("all"); this.nativeScripts = nativeScripts; } override toCborObject(): RawScriptAll { return [1, this.nativeScripts.map((ns) => ns.toCborObject())]; } override toJSON(): NativeScriptJSON { return { type: "all", scripts: this.nativeScripts.map((script) => script.toJSON()), }; } } export class ScriptAny extends NativeScript { nativeScripts: NativeScript[]; constructor(nativeScripts: NativeScript[]) { super("any"); this.nativeScripts = nativeScripts; } override toCborObject(): RawScriptAny { return [2, this.nativeScripts.map((ns) => ns.toCborObject())]; } override toJSON(): NativeScriptJSON { return { type: "any", scripts: this.nativeScripts.map((script) => script.toJSON()), }; } } export class ScriptNofK extends NativeScript { n: number; nativeScripts: NativeScript[]; constructor(n: number, nativeScripts: NativeScript[]) { super("atLeast"); this.n = n; this.nativeScripts = nativeScripts; } override toCborObject(): RawScriptNOfK { return [3, this.n, this.nativeScripts.map((ns) => ns.toCborObject())]; } override toJSON(): NativeScriptJSON { return { type: "atLeast", required: this.n, scripts: this.nativeScripts.map((script) => script.toJSON()), }; } } export class ScriptInvalidBefore extends NativeScript { invalidBefore: RawSlot; constructor(invalidBefore: RawSlot) { super("after"); this.invalidBefore = invalidBefore; } override toCborObject(): RawInvalidBefore { return [4, this.invalidBefore]; } override toJSON(): NativeScriptJSON { return { type: "after", slot: slotToJson(this.invalidBefore), }; } } export class ScriptInvalidHereafter extends NativeScript { invalidHereafter: RawSlot; constructor(invalidHereafter: RawSlot) { super("before"); this.invalidHereafter = invalidHereafter; } override toCborObject(): RawInvalidHereafter { return [5, this.invalidHereafter]; } override toJSON(): NativeScriptJSON { return { type: "before", slot: slotToJson(this.invalidHereafter), }; } } export function parseNativeScript( nativeScripts: CddlRawNativeScript[] | Set | CddlRawNativeScript ): NativeScript[] { const list: CddlRawNativeScript[] = nativeScripts instanceof Set ? Array.from(nativeScripts) : Array.isArray(nativeScripts) ? (typeof nativeScripts[0] === "number" ? [nativeScripts as CddlRawNativeScript] : nativeScripts) : []; return list.map((nativeScript) => NativeScript.fromCborObject(nativeScript)); } export const fromJsonToBytes = function (obj: ScriptJSON): Buffer { return Script.fromJson(obj).toBytes(); };